Sfoglia il codice sorgente

Merge pull request #1968 from nextcloud/feature/files_lock

Files lock 🔒
Marino Faggiana 2 anni fa
parent
commit
22b7d702f7

+ 6 - 4
.github/workflows/lint.yml

@@ -9,16 +9,18 @@ on:
       - master
       - develop
   pull_request:
+    types: [synchronize, opened, reopened, ready_for_review]
     branches:
       - master
-      - develop  
+      - develop
 
 jobs:
   Lint:
     runs-on: ubuntu-latest
-    
+    if: github.event.pull_request.draft == false
+
     steps:
      - uses: actions/checkout@v2
-       
+
      - name: GitHub Action for SwiftLint
-       uses: norio-nomura/action-swiftlint@3.1.0
+       uses: norio-nomura/action-swiftlint@3.1.0

+ 4 - 2
.github/workflows/xcode.yml

@@ -1,11 +1,12 @@
 name: Build
 
-on: 
+on:
   push:
-    branches: 
+    branches:
       - master
       - develop
   pull_request:
+    types: [synchronize, opened, reopened, ready_for_review]
     branches: 
       - master
       - develop
@@ -13,6 +14,7 @@ on:
 jobs:
   XCBuild:
     runs-on: macOS-11
+    if: github.event.pull_request.draft == false
     env:
       PROJECT: Nextcloud.xcodeproj
       DESTINATION: platform=iOS Simulator,name=iPhone 11

+ 5 - 3
File Provider Extension/FileProviderItem.swift

@@ -60,11 +60,13 @@ class FileProviderItem: NSObject, NSFileProviderItem {
     }
 
     var capabilities: NSFileProviderItemCapabilities {
-        if metadata.directory {
+        guard !metadata.directory else {
             return [ .allowsAddingSubItems, .allowsContentEnumerating, .allowsReading, .allowsDeleting, .allowsRenaming ]
-        } else {
-            return [ .allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting ]
         }
+        guard !metadata.lock else {
+            return [ .allowsReading ]
+        }
+        return [ .allowsWriting, .allowsReading, .allowsDeleting, .allowsRenaming, .allowsReparenting ]
     }
 
     var isTrashed: Bool {

+ 2 - 0
iOSClient/Brand/NCBrand.swift

@@ -137,6 +137,7 @@ class NCBrandColor: NSObject {
 
         static var buttonMore = UIImage()
         static var buttonStop = UIImage()
+        static var buttonMoreLock = UIImage()
         static var buttonRestore = UIImage()
     }
 
@@ -355,6 +356,7 @@ class NCBrandColor: NSObject {
 
         cacheImages.buttonMore = UIImage(named: "more")!.image(color: gray, size: 50)
         cacheImages.buttonStop = UIImage(named: "stop")!.image(color: gray, size: 50)
+        cacheImages.buttonMoreLock = UIImage(named: "moreLock")!.image(color: gray, size: 50)
         cacheImages.buttonRestore = UIImage(named: "restore")!.image(color: gray, size: 50)
     }
 

+ 12 - 0
iOSClient/Data/NCDatabase.swift

@@ -384,6 +384,13 @@ class tableMetadata: Object, NCUserBaseUrl {
     @objc dynamic var ocId = ""
     @objc dynamic var ownerId = ""
     @objc dynamic var ownerDisplayName = ""
+    @objc public var lock = false
+    @objc public var lockOwner = ""
+    @objc public var lockOwnerEditor = ""
+    @objc public var lockOwnerType = 0
+    @objc public var lockOwnerDisplayName = ""
+    @objc public var lockTime: Date?
+    @objc public var lockTimeOut: Date?
     @objc dynamic var path = ""
     @objc dynamic var permissions = ""
     @objc dynamic var quotaUsedBytes: Int64 = 0
@@ -420,6 +427,11 @@ extension tableMetadata {
     var isPrintable: Bool {
         classFile == NCCommunicationCommon.typeClassFile.image.rawValue || ["application/pdf", "com.adobe.pdf"].contains(contentType) || contentType.hasPrefix("text/")
     }
+
+    /// Returns false if the user is lokced out of the file. I.e. The file is locked but by somone else
+    func canUnlock(as user: String) -> Bool {
+        return !lock || (lockOwner == user && lockOwnerType == 0)
+    }
 }
 
 class tablePhotoLibrary: Object {

+ 1 - 0
iOSClient/Data/NCElementsJSON.swift

@@ -58,6 +58,7 @@ import UIKit
     @objc public let capabilitiesNotification: Array = ["ocs", "data", "capabilities", "notifications", "ocs-endpoints"]
 
     @objc public let capabilitiesFilesUndelete: Array = ["ocs", "data", "capabilities", "files", "undelete"]
+    @objc public let capabilitiesFilesLockVersion: Array = ["ocs", "data", "capabilities", "files", "locking"]
     @objc public let capabilitiesFilesComments: Array = ["ocs", "data", "capabilities", "files", "comments"]                                            // NC 20
 
     @objc public let capabilitiesHWCEnabled: Array = ["ocs", "data", "capabilities", "handwerkcloud", "enabled"]

+ 10 - 1
iOSClient/Data/NCManageDatabase+Metadata.swift

@@ -64,6 +64,13 @@ extension NCManageDatabase {
         metadata.ocId = file.ocId
         metadata.ownerId = file.ownerId
         metadata.ownerDisplayName = file.ownerDisplayName
+        metadata.lock = file.lock
+        metadata.lockOwner = file.lockOwner
+        metadata.lockOwnerEditor = file.lockOwnerEditor
+        metadata.lockOwnerType = file.lockOwnerType
+        metadata.lockOwnerDisplayName = file.lockOwnerDisplayName
+        metadata.lockTime = file.lockTime
+        metadata.lockTimeOut = file.lockTimeOut
         metadata.path = file.path
         metadata.permissions = file.permissions
         metadata.quotaUsedBytes = file.quotaUsedBytes
@@ -319,7 +326,9 @@ extension NCManageDatabase {
 
                     if let result = metadatasResult.first(where: { $0.ocId == metadata.ocId }) {
                         // update
-                        if result.status == NCGlobal.shared.metadataStatusNormal && (result.etag != metadata.etag || result.fileNameView != metadata.fileNameView || result.date != metadata.date || result.permissions != metadata.permissions || result.hasPreview != metadata.hasPreview || result.note != metadata.note) {
+                        // Workaround: check lock bc no etag changes if lock runs out in directory
+                        // https://github.com/nextcloud/server/issues/8477
+                        if result.status == NCGlobal.shared.metadataStatusNormal && (result.etag != metadata.etag || result.fileNameView != metadata.fileNameView || result.date != metadata.date || result.permissions != metadata.permissions || result.hasPreview != metadata.hasPreview || result.note != metadata.note || result.lock != metadata.lock) {
                             ocIdsUdate.append(metadata.ocId)
                             realm.add(tableMetadata.init(value: metadata), update: .all)
                         } else if result.status == NCGlobal.shared.metadataStatusNormal && addCompareLivePhoto && result.livePhoto != metadata.livePhoto {

+ 12 - 0
iOSClient/Images.xcassets/moreLock.imageset/Contents.json

@@ -0,0 +1,12 @@
+{
+  "images" : [
+    {
+      "filename" : "menu-locked-filled.svg",
+      "idiom" : "universal"
+    }
+  ],
+  "info" : {
+    "author" : "xcode",
+    "version" : 1
+  }
+}

File diff suppressed because it is too large
+ 65 - 0
iOSClient/Images.xcassets/moreLock.imageset/menu-locked-filled.svg


+ 7 - 1
iOSClient/Main/Collection Common/NCCollectionViewCommon.swift

@@ -791,7 +791,7 @@ class NCCollectionViewCommon: UIViewController, UIGestureRecognizerDelegate, UIS
 
         guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(objectId) else { return }
 
-        if namedButtonMore == NCGlobal.shared.buttonMoreMore {
+        if namedButtonMore == NCGlobal.shared.buttonMoreMore || namedButtonMore == NCGlobal.shared.buttonMoreLock {
             toggleMenu(metadata: metadata, imageIcon: image)
         } else if namedButtonMore == NCGlobal.shared.buttonMoreStop {
             NCNetworking.shared.cancelTransferMetadata(metadata) { }
@@ -1467,6 +1467,9 @@ extension NCCollectionViewCommon: UICollectionViewDataSource {
 
             if metadata.status == NCGlobal.shared.metadataStatusInDownload || metadata.status == NCGlobal.shared.metadataStatusDownloading || metadata.status == NCGlobal.shared.metadataStatusInUpload || metadata.status == NCGlobal.shared.metadataStatusUploading {
                 cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCBrandColor.cacheImages.buttonStop)
+            } else if metadata.lock == true {
+                cell.setButtonMore(named: NCGlobal.shared.buttonMoreLock, image: NCBrandColor.cacheImages.buttonMoreLock)
+                a11yValues.append(String(format: NSLocalizedString("_locked_by_", comment: ""), metadata.lockOwnerDisplayName))
             } else {
                 cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCBrandColor.cacheImages.buttonMore)
             }
@@ -1636,6 +1639,9 @@ extension NCCollectionViewCommon: UICollectionViewDataSource {
             // Transfer
             if metadata.status == NCGlobal.shared.metadataStatusInDownload || metadata.status == NCGlobal.shared.metadataStatusDownloading || metadata.status == NCGlobal.shared.metadataStatusInUpload || metadata.status == NCGlobal.shared.metadataStatusUploading {
                 cell.setButtonMore(named: NCGlobal.shared.buttonMoreStop, image: NCBrandColor.cacheImages.buttonStop)
+            } else if metadata.lock == true {
+                cell.setButtonMore(named: NCGlobal.shared.buttonMoreLock, image: NCBrandColor.cacheImages.buttonMoreLock)
+                a11yValues.append(String(format: NSLocalizedString("_locked_by_", comment: ""), metadata.lockOwnerDisplayName))
             } else {
                 cell.setButtonMore(named: NCGlobal.shared.buttonMoreMore, image: NCBrandColor.cacheImages.buttonMore)
             }

+ 14 - 0
iOSClient/Main/Collection Common/NCSelectableNavigationView.swift

@@ -95,6 +95,9 @@ extension NCSelectableNavigationView where Self: UIViewController {
         var selectedMetadatas: [tableMetadata] = []
         var selectedMediaMetadatas: [tableMetadata] = []
         var isAnyOffline = false
+        var isAnyFolder = false
+        var isAnyLocked = false
+        var canUnlock = true
 
         for ocId in selectOcId {
             guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { continue }
@@ -102,6 +105,13 @@ extension NCSelectableNavigationView where Self: UIViewController {
             if [NCCommunicationCommon.typeClassFile.image.rawValue, NCCommunicationCommon.typeClassFile.video.rawValue].contains(metadata.classFile) {
                 selectedMediaMetadatas.append(metadata)
             }
+            if metadata.directory { isAnyFolder = true }
+            if metadata.lock {
+                isAnyLocked = true
+                if metadata.lockOwner != appDelegate.userId {
+                    canUnlock = false
+                }
+            }
 
             guard !isAnyOffline else { continue }
             if metadata.directory,
@@ -114,6 +124,10 @@ extension NCSelectableNavigationView where Self: UIViewController {
 
         actions.append(.openInAction(selectedMetadatas: selectedMetadatas, viewController: self, completion: tapSelect))
 
+        if !isAnyFolder, canUnlock, NCManageDatabase.shared.getCapabilitiesServerInt(account: appDelegate.account, elements: NCElementsJSON.shared.capabilitiesFilesLockVersion) >= 1 {
+            actions.append(.lockUnlockFiles(shouldLock: !isAnyLocked, metadatas: selectedMetadatas, completion: tapSelect))
+        }
+
         if !selectedMediaMetadatas.isEmpty {
             actions.append(.saveMediaAction(selectedMediaMetadatas: selectedMediaMetadatas, completion: tapSelect))
         }

+ 27 - 9
iOSClient/Main/NCFunctionCenter.swift

@@ -535,19 +535,19 @@ import SVGKit
         }
     }
 
-    func openSelectView(items: [Any]) {
+    func openSelectView(items: [tableMetadata]) {
 
         let navigationController = UIStoryboard(name: "NCSelect", bundle: nil).instantiateInitialViewController() as! UINavigationController
         let topViewController = navigationController.topViewController as! NCSelect
         var listViewController = [NCSelect]()
 
-        var copyItems: [Any] = []
+        var copyItems: [tableMetadata] = []
         for item in items {
             copyItems.append(item)
         }
 
         let homeUrl = NCUtilityFileSystem.shared.getHomeServer(account: appDelegate.account)
-        var serverUrl = (copyItems[0] as! Nextcloud.tableMetadata).serverUrl
+        var serverUrl = copyItems[0].serverUrl
 
         // Setup view controllers such that the current view is of the same directory the items to be copied are in
         while true {
@@ -616,7 +616,8 @@ import SVGKit
             }
         }
         let titleOffline = isOffline ? NSLocalizedString("_remove_available_offline_", comment: "") :  NSLocalizedString("_set_available_offline_", comment: "")
-
+        let titleLock = metadata.lock ? NSLocalizedString("_unlock_file_", comment: "") :  NSLocalizedString("_lock_file_", comment: "")
+        let iconLock = metadata.lock ? "lock.open" : "lock"
         let copy = UIAction(title: NSLocalizedString("_copy_file_", comment: ""), image: UIImage(systemName: "doc.on.doc")) { _ in
             self.copyPasteboard(pasteboardOcIds: [metadata.ocId], hudView: viewController.view)
         }
@@ -637,7 +638,10 @@ import SVGKit
                 viewController.reloadDataSource()
             }
         }
-
+        
+        let lockUnlock = UIAction(title: titleLock, image: UIImage(systemName: iconLock)) { _ in
+            NCNetworking.shared.lockUnlockFile(metadata, shoulLock: !metadata.lock)
+        }
         let save = UIAction(title: titleSave, image: UIImage(systemName: "square.and.arrow.down")) { _ in
             if metadataMOV != nil {
                 self.saveLivePhoto(metadata: metadata, metadataMOV: metadataMOV!)
@@ -741,7 +745,21 @@ import SVGKit
 
         // FILE
 
-        var children: [UIMenuElement] = [favorite, offline, openIn, rename, moveCopy, copy, copyPath, delete]
+        var children: [UIMenuElement] = [offline, openIn, moveCopy, copy, copyPath]
+        
+        if !metadata.lock {
+            // Workaround: PROPPATCH doesn't work (favorite)
+            // https://github.com/nextcloud/files_lock/issues/68
+            children.insert(favorite, at: 0)
+            children.append(delete)
+            children.insert(rename, at: 3)
+        } else if enableDeleteLocal {
+            children.append(deleteConfirmLocal)
+        }
+
+        if NCManageDatabase.shared.getCapabilitiesServerInt(account: appDelegate.account, elements: NCElementsJSON.shared.capabilitiesFilesLockVersion) >= 1, metadata.canUnlock(as: appDelegate.userId) {
+            children.insert(lockUnlock, at: metadata.lock ? 0 : 1)
+        }
 
         if (metadata.contentType != "image/svg+xml") && (metadata.classFile == NCCommunicationCommon.typeClassFile.image.rawValue || metadata.classFile == NCCommunicationCommon.typeClassFile.video.rawValue) {
             children.insert(save, at: 2)
@@ -756,18 +774,18 @@ import SVGKit
         }
 
         if enableViewInFolder {
-            children.insert(viewInFolder, at: children.count-1)
+            children.insert(viewInFolder, at: children.count - 1)
         }
 
         if (!isFolderEncrypted && metadata.contentType != "image/gif" && metadata.contentType != "image/svg+xml") && (metadata.contentType == "com.adobe.pdf" || metadata.contentType == "application/pdf" || metadata.classFile == NCCommunicationCommon.typeClassFile.image.rawValue) {
-            children.insert(modify, at: children.count-1)
+            children.insert(modify, at: children.count - 1)
         }
 
         if metadata.classFile == NCCommunicationCommon.typeClassFile.image.rawValue && viewController is NCCollectionViewCommon && !NCBrandOptions.shared.disable_background_image {
             let viewController: NCCollectionViewCommon = viewController as! NCCollectionViewCommon
             let layoutKey = viewController.layoutKey
             if layoutKey == NCGlobal.shared.layoutViewFiles {
-                children.insert(saveBackground, at: children.count-1)
+                children.insert(saveBackground, at: children.count - 1)
             }
         }
 

+ 74 - 12
iOSClient/Menu/NCCollectionViewCommon+Menu.swift

@@ -71,22 +71,59 @@ extension NCCollectionViewCommon {
             )
         )
 
+        if metadata.lock {
+            var lockOwnerName = metadata.lockOwnerDisplayName.isEmpty ? metadata.lockOwner : metadata.lockOwnerDisplayName
+            var lockIcon = NCUtility.shared.loadUserImage(for: metadata.lockOwner, displayName: lockOwnerName, userBaseUrl: metadata)
+            if metadata.lockOwnerType != 0 {
+                lockOwnerName += " app"
+                if !metadata.lockOwnerEditor.isEmpty, let appIcon = UIImage(named: metadata.lockOwnerEditor) {
+                    lockIcon = appIcon
+                }
+            }
+
+            var lockTimeString: String?
+            if let lockTime = metadata.lockTimeOut {
+                if lockTime >= Date().addingTimeInterval(60),
+                   let timeInterval = (lockTime.timeIntervalSince1970 - Date().timeIntervalSince1970).format() {
+                    lockTimeString = String(format: NSLocalizedString("_time_remaining_", comment: ""), timeInterval)
+                } else if lockTime > Date() {
+                    lockTimeString = NSLocalizedString("_less_a_minute_", comment: "")
+                } // else: don't show negative time
+            }
+            if let lockTime = metadata.lockTime, lockTimeString == nil {
+                lockTimeString = DateFormatter.localizedString(from: lockTime, dateStyle: .short, timeStyle: .short)
+            }
+
+            actions.append(
+                NCMenuAction(
+                    title: String(format: NSLocalizedString("_locked_by_", comment: ""), lockOwnerName),
+                    details: lockTimeString,
+                    icon: lockIcon,
+                    action: nil)
+            )
+        }
+
+        actions.append(.seperator)
+
         //
         // FAVORITE
-        //
-        actions.append(
-            NCMenuAction(
-                title: metadata.favorite ? NSLocalizedString("_remove_favorites_", comment: "") : NSLocalizedString("_add_favorites_", comment: ""),
-                icon: NCUtility.shared.loadImage(named: "star.fill", color: NCBrandColor.shared.yellowFavorite),
-                action: { _ in
-                    NCNetworking.shared.favoriteMetadata(metadata) { errorCode, errorDescription in
-                        if errorCode != 0 {
-                            NCContentPresenter.shared.messageNotification("_error_", description: errorDescription, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error, errorCode: errorCode)
+        // FIXME: PROPPATCH doesn't work
+        // https://github.com/nextcloud/files_lock/issues/68
+        if !metadata.lock {
+            actions.append(
+                NCMenuAction(
+                    title: metadata.favorite ? NSLocalizedString("_remove_favorites_", comment: "") : NSLocalizedString("_add_favorites_", comment: ""),
+                    icon: NCUtility.shared.loadImage(named: "star.fill", color: NCBrandColor.shared.yellowFavorite),
+                    action: { _ in
+                        NCNetworking.shared.favoriteMetadata(metadata) { errorCode, errorDescription in
+                            if errorCode != 0 {
+                                NCContentPresenter.shared.messageNotification("_error_", description: errorDescription, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error, errorCode: errorCode)
+                            }
                         }
                     }
-                }
+                )
             )
-        )
+        }
 
         //
         // DETAIL
@@ -102,6 +139,20 @@ extension NCCollectionViewCommon {
                 )
             )
         }
+        
+        //
+        // LOCK / UNLOCK
+        //
+        let hasLockCapability = NCManageDatabase.shared.getCapabilitiesServerInt(account: appDelegate.account, elements: NCElementsJSON.shared.capabilitiesFilesLockVersion) >= 1
+        if !metadata.directory, metadata.canUnlock(as: appDelegate.userId), hasLockCapability {
+            let lockAction = NCMenuAction.lockUnlockFiles(shouldLock: !metadata.lock, metadatas: [metadata])
+            if metadata.lock {
+                // make unlock first action, after info rows & seperator
+                actions.insert(lockAction, at: 3)
+            } else {
+                actions.append(lockAction)
+            }
+        }
 
         //
         // OFFLINE
@@ -183,7 +234,7 @@ extension NCCollectionViewCommon {
         //
         // RENAME
         //
-        if !(isFolderEncrypted && metadata.serverUrl == serverUrlHome) {
+        if !(isFolderEncrypted && metadata.serverUrl == serverUrlHome), !metadata.lock {
             actions.append(
                 NCMenuAction(
                     title: NSLocalizedString("_rename_", comment: ""),
@@ -322,3 +373,14 @@ extension NCCollectionViewCommon {
         presentMenu(with: actions)
     }
 }
+
+
+extension TimeInterval {
+    func format() -> String? {
+        let formatter = DateComponentsFormatter()
+        formatter.allowedUnits = [.day, .hour, .minute]
+        formatter.unitsStyle = .full
+        formatter.maximumUnitCount = 1
+        return formatter.string(from: self)
+    }
+}

+ 4 - 2
iOSClient/Menu/NCMedia+Menu.swift

@@ -173,8 +173,10 @@ extension NCMedia {
 
             //
             // DELETE
-            //
-            actions.append(.deleteAction(selectedMetadatas: selectedMetadatas, metadataFolder: nil, viewController: self, completion: tapSelect))
+            // can't delete from cache because is needed for NCMedia view, and if locked can't delete from server either.
+            if !selectedMetadatas.contains(where: { $0.lock && $0.lockOwner != appDelegate.userId }) {
+                actions.append(.deleteAction(selectedMetadatas: selectedMetadatas, metadataFolder: nil, viewController: self, completion: tapSelect))
+            }
         }
     }
 }

+ 2 - 2
iOSClient/Menu/NCMenu+FloatingPanel.swift

@@ -38,7 +38,7 @@ class NCMenuFloatingPanelLayout: FloatingPanelLayout {
 
     let topInset: CGFloat
 
-    init(numberOfActions: Int) {
+    init(actionsHeight: CGFloat) {
         // sometimes UIScreen.main.bounds.size.height is not updated correctly
         // this ensures we use the correct height value
         // can't use `layoutFor size` since menu is dieplayed on top of the whole screen not just the VC
@@ -46,7 +46,7 @@ class NCMenuFloatingPanelLayout: FloatingPanelLayout {
         ? min(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height)
         : max(UIScreen.main.bounds.size.width, UIScreen.main.bounds.size.height)
         let bottomInset = UIApplication.shared.keyWindow?.rootViewController?.view.safeAreaInsets.bottom ?? 0
-        let panelHeight = CGFloat(numberOfActions * 60) + bottomInset
+        let panelHeight = CGFloat(actionsHeight) + bottomInset
 
         topInset = max(48, screenHeight - panelHeight)
     }

+ 27 - 12
iOSClient/Menu/NCMenu.storyboard

@@ -1,9 +1,9 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dbT-V0-aXb">
+<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="dbT-V0-aXb">
     <device id="retina6_1" orientation="portrait" appearance="light"/>
     <dependencies>
         <deployment identifier="iOS"/>
-        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
+        <plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
         <capability name="System colors in document resources" minToolsVersion="11.0"/>
         <capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
     </dependencies>
@@ -18,7 +18,7 @@
                         <color key="backgroundColor" systemColor="systemBackgroundColor"/>
                         <prototypes>
                             <tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="default" indentationWidth="10" reuseIdentifier="menuActionCell" rowHeight="60" id="MT1-Lu-9SA">
-                                <rect key="frame" x="0.0" y="28" width="414" height="60"/>
+                                <rect key="frame" x="0.0" y="44.5" width="414" height="60"/>
                                 <autoresizingMask key="autoresizingMask"/>
                                 <tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="MT1-Lu-9SA" id="tmT-MO-Dwy">
                                     <rect key="frame" x="0.0" y="0.0" width="414" height="60"/>
@@ -31,19 +31,31 @@
                                                 <constraint firstAttribute="width" constant="28" id="gxY-bI-V0v"/>
                                             </constraints>
                                         </imageView>
-                                        <label opaque="NO" userInteractionEnabled="NO" tag="2" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="A8f-xF-j3i">
-                                            <rect key="frame" x="60" y="19.5" width="326" height="21"/>
-                                            <fontDescription key="fontDescription" type="system" pointSize="17"/>
-                                            <nil key="textColor"/>
-                                            <nil key="highlightedColor"/>
-                                        </label>
+                                        <stackView opaque="NO" contentMode="scaleToFill" axis="vertical" spacing="2" translatesAutoresizingMaskIntoConstraints="NO" id="6vv-IO-HJM">
+                                            <rect key="frame" x="60" y="19" width="326" height="22"/>
+                                            <subviews>
+                                                <label opaque="NO" userInteractionEnabled="NO" tag="2" contentMode="left" horizontalHuggingPriority="251" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="A8f-xF-j3i">
+                                                    <rect key="frame" x="0.0" y="0.0" width="326" height="20"/>
+                                                    <fontDescription key="fontDescription" type="system" pointSize="17"/>
+                                                    <nil key="textColor"/>
+                                                    <nil key="highlightedColor"/>
+                                                </label>
+                                                <label opaque="NO" userInteractionEnabled="NO" tag="3" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" verticalCompressionResistancePriority="749" text="Label" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="SjQ-O4-Clh">
+                                                    <rect key="frame" x="0.0" y="22" width="326" height="0.0"/>
+                                                    <fontDescription key="fontDescription" type="system" pointSize="15"/>
+                                                    <color key="textColor" systemColor="secondaryLabelColor"/>
+                                                    <nil key="highlightedColor"/>
+                                                </label>
+                                            </subviews>
+                                        </stackView>
                                     </subviews>
                                     <constraints>
-                                        <constraint firstItem="A8f-xF-j3i" firstAttribute="leading" secondItem="RV0-3K-eSN" secondAttribute="trailing" constant="16" id="ADH-SJ-JNh"/>
+                                        <constraint firstAttribute="trailingMargin" secondItem="6vv-IO-HJM" secondAttribute="trailing" constant="8" id="4pu-DJ-9cK"/>
+                                        <constraint firstItem="6vv-IO-HJM" firstAttribute="leading" secondItem="RV0-3K-eSN" secondAttribute="trailing" constant="16" id="8tD-ZW-Y88"/>
                                         <constraint firstItem="RV0-3K-eSN" firstAttribute="leading" secondItem="tmT-MO-Dwy" secondAttribute="leading" constant="16" id="QQt-st-4hA"/>
                                         <constraint firstItem="RV0-3K-eSN" firstAttribute="centerY" secondItem="tmT-MO-Dwy" secondAttribute="centerY" id="R6O-om-tEz"/>
-                                        <constraint firstAttribute="trailingMargin" secondItem="A8f-xF-j3i" secondAttribute="trailing" constant="8" id="fia-KH-ier"/>
-                                        <constraint firstItem="A8f-xF-j3i" firstAttribute="centerY" secondItem="tmT-MO-Dwy" secondAttribute="centerY" id="kPV-bd-AAL"/>
+                                        <constraint firstItem="6vv-IO-HJM" firstAttribute="top" secondItem="tmT-MO-Dwy" secondAttribute="topMargin" constant="8" id="d50-c3-Ofv"/>
+                                        <constraint firstAttribute="bottomMargin" secondItem="6vv-IO-HJM" secondAttribute="bottom" constant="8" id="zte-5x-B8K"/>
                                     </constraints>
                                 </tableViewCellContentView>
                             </tableViewCell>
@@ -60,6 +72,9 @@
         </scene>
     </scenes>
     <resources>
+        <systemColor name="secondaryLabelColor">
+            <color red="0.23529411764705882" green="0.23529411764705882" blue="0.2627450980392157" alpha="0.59999999999999998" colorSpace="custom" customColorSpace="sRGB"/>
+        </systemColor>
         <systemColor name="systemBackgroundColor">
             <color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
         </systemColor>

+ 20 - 4
iOSClient/Menu/NCMenu.swift

@@ -28,6 +28,10 @@
 import UIKit
 import FloatingPanel
 
+extension Array where Element == NCMenuAction {
+    var listHeight: CGFloat { reduce(0, { $0 + $1.rowHeight }) }
+}
+
 class NCMenu: UITableViewController {
 
     var actions = [NCMenuAction]()
@@ -42,6 +46,8 @@ class NCMenu: UITableViewController {
 
     override func viewDidLoad() {
         super.viewDidLoad()
+        tableView.estimatedRowHeight = 60
+        tableView.rowHeight = UITableView.automaticDimension
     }
 
     override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
@@ -63,15 +69,25 @@ class NCMenu: UITableViewController {
     }
 
     override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
+        let action = actions[indexPath.row]
+        guard action.title != NCMenuAction.seperatorIdentifier else {
+            let cell = UITableViewCell()
+            cell.backgroundColor = NCBrandColor.shared.separator
+            return cell
+        }
         let cell = tableView.dequeueReusableCell(withIdentifier: "menuActionCell", for: indexPath)
         cell.tintColor = NCBrandColor.shared.customer
-        let action = actions[indexPath.row]
         let actionIconView = cell.viewWithTag(1) as? UIImageView
         let actionNameLabel = cell.viewWithTag(2) as? UILabel
+        let actionDetailLabel = cell.viewWithTag(3) as? UILabel
 
         if action.action == nil {
             cell.selectionStyle = .none
         }
+        if let details = action.details {
+            actionDetailLabel?.text = details
+            actionNameLabel?.isHidden = false
+        } else { actionDetailLabel?.isHidden = true }
 
         if action.isOn {
             actionIconView?.image = action.onIcon
@@ -89,17 +105,17 @@ class NCMenu: UITableViewController {
     // MARK: - Tabel View Layout
 
     override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
-        return 60
+        actions[indexPath.row].title == NCMenuAction.seperatorIdentifier ? 3 : UITableView.automaticDimension
     }
 }
 extension NCMenu: FloatingPanelControllerDelegate {
 
     func floatingPanel(_ fpc: FloatingPanelController, layoutFor size: CGSize) -> FloatingPanelLayout {
-        return NCMenuFloatingPanelLayout(numberOfActions: self.actions.count)
+        return NCMenuFloatingPanelLayout(actionsHeight: self.actions.listHeight)
     }
 
     func floatingPanel(_ fpc: FloatingPanelController, layoutFor newCollection: UITraitCollection) -> FloatingPanelLayout {
-        return NCMenuFloatingPanelLayout(numberOfActions: self.actions.count)
+        return NCMenuFloatingPanelLayout(actionsHeight: self.actions.listHeight)
     }
 
     func floatingPanel(_ fpc: FloatingPanelController, animatorForDismissingWith velocity: CGVector) -> UIViewPropertyAnimator {

+ 40 - 6
iOSClient/Menu/NCMenuAction.swift

@@ -11,6 +11,7 @@ import UIKit
 
 class NCMenuAction {
     let title: String
+    let details: String?
     let icon: UIImage
     let selectable: Bool
     var onTitle: String?
@@ -18,16 +19,19 @@ class NCMenuAction {
     var selected: Bool = false
     var isOn: Bool = false
     var action: ((_ menuAction: NCMenuAction) -> Void)?
+    var rowHeight: CGFloat { self.title == NCMenuAction.seperatorIdentifier ? 3 : self.details != nil ? 80 : 60 }
 
-    init(title: String, icon: UIImage, action: ((_ menuAction: NCMenuAction) -> Void)?) {
+    init(title: String, details: String? = nil, icon: UIImage, action: ((_ menuAction: NCMenuAction) -> Void)?) {
         self.title = title
+        self.details = details
         self.icon = icon
         self.action = action
         self.selectable = false
     }
 
-    init(title: String, icon: UIImage, onTitle: String? = nil, onIcon: UIImage? = nil, selected: Bool, on: Bool, action: ((_ menuAction: NCMenuAction) -> Void)?) {
+    init(title: String, details: String? = nil, icon: UIImage, onTitle: String? = nil, onIcon: UIImage? = nil, selected: Bool, on: Bool, action: ((_ menuAction: NCMenuAction) -> Void)?) {
         self.title = title
+        self.details = details
         self.icon = icon
         self.onTitle = onTitle ?? title
         self.onIcon = onIcon ?? icon
@@ -41,6 +45,12 @@ class NCMenuAction {
 // MARK: - Actions
 
 extension NCMenuAction {
+    static let seperatorIdentifier = "NCMenuAction.SEPERATOR"
+
+    /// A static seperator, with no actions, text, or icons
+    static var seperator: NCMenuAction {
+        return NCMenuAction(title: seperatorIdentifier, icon: UIImage(), action: nil)
+    }
 
     /// Select all items
     static func selectAllAction(action: @escaping () -> Void) -> NCMenuAction {
@@ -86,6 +96,7 @@ extension NCMenuAction {
             }
         } // else: no metadata selected
 
+        let canDeleteServer = selectedMetadatas.allSatisfy { !$0.lock }
         var fileList = ""
         for (ix, metadata) in selectedMetadatas.enumerated() {
             guard ix < 3 else { fileList += "\n - ..."; break }
@@ -100,10 +111,12 @@ extension NCMenuAction {
                     title: titleDelete,
                     message: NSLocalizedString("_want_delete_", comment: "") + fileList,
                     preferredStyle: .alert)
-                alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_delete_", comment: ""), style: .default) { (_: UIAlertAction) in
-                    selectedMetadatas.forEach({ NCOperationQueue.shared.delete(metadata: $0, onlyLocalCache: false) })
-                    completion?()
-                })
+                if canDeleteServer {
+                    alertController.addAction(UIAlertAction(title: NSLocalizedString("_yes_delete_", comment: ""), style: .default) { (_: UIAlertAction) in
+                        selectedMetadatas.forEach({ NCOperationQueue.shared.delete(metadata: $0, onlyLocalCache: false) })
+                        completion?()
+                    })
+                }
 
                 // NCMedia removes image from collection view if removed from cache
                 if !(viewController is NCMedia) {
@@ -212,4 +225,25 @@ extension NCMenuAction {
             }
         )
     }
+
+    /// Lock or unlock a file using *files_lock*
+    static func lockUnlockFiles(shouldLock: Bool, metadatas: [tableMetadata], completion: (() -> Void)? = nil) -> NCMenuAction {
+        let titleKey: String
+        if metadatas.count == 1 {
+            titleKey = shouldLock ? "_lock_file_" : "_unlock_file_"
+        } else {
+            titleKey = shouldLock ? "_lock_selected_files_" : "_unlock_selected_files_"
+        }
+        let imageName = !shouldLock ? "lock_open" : "lock"
+        return NCMenuAction(
+            title: NSLocalizedString(titleKey, comment: ""),
+            icon: NCUtility.shared.loadImage(named: imageName),
+            action: { _ in
+                for metadata in metadatas where metadata.lock != shouldLock {
+                    NCNetworking.shared.lockUnlockFile(metadata, shoulLock: shouldLock)
+                }
+                completion?()
+            }
+        )
+    }
 }

+ 15 - 12
iOSClient/Menu/NCViewer+Menu.swift

@@ -40,20 +40,23 @@ extension NCViewer {
 
         //
         // FAVORITE
-        //
-        actions.append(
-            NCMenuAction(
-                title: titleFavorite,
-                icon: NCUtility.shared.loadImage(named: "star.fill", color: NCBrandColor.shared.yellowFavorite),
-                action: { _ in
-                    NCNetworking.shared.favoriteMetadata(metadata) { errorCode, errorDescription in
-                        if errorCode != 0 {
-                            NCContentPresenter.shared.messageNotification("_error_", description: errorDescription, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error, errorCode: errorCode)
+        // Workaround: PROPPATCH doesn't work
+        // https://github.com/nextcloud/files_lock/issues/68
+        if !metadata.lock {
+            actions.append(
+                NCMenuAction(
+                    title: titleFavorite,
+                    icon: NCUtility.shared.loadImage(named: "star.fill", color: NCBrandColor.shared.yellowFavorite),
+                    action: { _ in
+                        NCNetworking.shared.favoriteMetadata(metadata) { errorCode, errorDescription in
+                            if errorCode != 0 {
+                                NCContentPresenter.shared.messageNotification("_error_", description: errorDescription, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error, errorCode: errorCode)
+                            }
                         }
                     }
-                }
+                )
             )
-        )
+        }
 
         //
         // DETAIL
@@ -146,7 +149,7 @@ extension NCViewer {
         //
         // RENAME
         //
-        if !webView {
+        if !webView, !metadata.lock {
             actions.append(
                 NCMenuAction(
                     title: NSLocalizedString("_rename_", comment: ""),

+ 2 - 1
iOSClient/NCGlobal.swift

@@ -112,7 +112,7 @@ class NCGlobal: NSObject {
     // Database Realm
     //
     let databaseDefault                             = "nextcloud.realm"
-    let databaseSchemaVersion: UInt64               = 218
+    let databaseSchemaVersion: UInt64               = 219
 
     // Intro selector
     //
@@ -176,6 +176,7 @@ class NCGlobal: NSObject {
     //
     let buttonMoreMore                              = "more"
     let buttonMoreStop                              = "stop"
+    let buttonMoreLock                              = "moreLock"
 
     // Text -  OnlyOffice - Collabora - QuickLook
     //

+ 17 - 0
iOSClient/Networking/NCNetworking.swift

@@ -1169,6 +1169,23 @@ import Queuer
         }
     }
 
+    // MARK: - Lock Files
+
+    @objc func lockUnlockFile(_ metadata: tableMetadata, shoulLock: Bool) {
+        NCCommunication.shared.lockUnlockFile(serverUrlFileName: metadata.serverUrl + "/" + metadata.fileName, shouldLock: shoulLock) { errorCode, errorDescription in
+            // 0: lock was successful; 412: lock did not change, no error, refresh
+            guard errorCode == 0 || errorCode == 412 else {
+                NCContentPresenter.shared.messageNotification(metadata.fileName, description: "_files_lock_error_", delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error, errorCode: errorCode, priority: .max)
+                return
+            }
+            NCNetworking.shared.readFile(serverUrlFileName: metadata.serverUrl + "/" + metadata.fileName, account: metadata.account) { account, metadata, errorCode, errorDescription in
+                guard errorCode == 0, let metadata = metadata else { return }
+                NCManageDatabase.shared.addMetadata(metadata)
+                NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterReloadDataSource)
+            }
+        }
+    }
+
     // MARK: - WebDav Rename
 
     @objc func renameMetadata(_ metadata: tableMetadata, fileNameNew: String, viewController: UIViewController?, completion: @escaping (_ errorCode: Int, _ errorDescription: String?) -> Void) {

+ 5 - 2
iOSClient/Select/NCSelect.swift

@@ -49,7 +49,7 @@ class NCSelect: UIViewController, UIGestureRecognizerDelegate, UIAdaptivePresent
     @objc var includeImages = false
     @objc var enableSelectFile = false
     @objc var type = ""
-    @objc var items: [Any] = []
+    @objc var items: [tableMetadata] = []
 
     var titleCurrentFolder = NCBrandOptions.shared.brand
     var serverUrl = ""
@@ -141,7 +141,10 @@ class NCSelect: UIViewController, UIGestureRecognizerDelegate, UIAdaptivePresent
             self.view.addSubview(selectCommandViewSelect!)
             selectCommandViewSelect?.selectView = self
             selectCommandViewSelect?.translatesAutoresizingMaskIntoConstraints = false
-
+            if items.contains(where: { $0.lock }) {
+                selectCommandViewSelect?.moveButton?.isEnabled = false
+                selectCommandViewSelect?.moveButton?.titleLabel?.isEnabled = false
+            }
             selectCommandViewSelect?.bottomAnchor.constraint(equalTo: view.bottomAnchor, constant: 0).isActive = true
             selectCommandViewSelect?.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 0).isActive = true
             selectCommandViewSelect?.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: 0).isActive = true

+ 2 - 2
iOSClient/Settings/NCSettings.m

@@ -74,7 +74,7 @@
     // Lock active YES/NO
     row = [XLFormRowDescriptor formRowDescriptorWithTag:@"bloccopasscode" rowType:XLFormRowDescriptorTypeButton title:NSLocalizedString(@"_lock_not_active_", nil)];
     row.cellConfigAtConfigure[@"backgroundColor"] = NCBrandColor.shared.secondarySystemGroupedBackground;
-    [row.cellConfig setObject:[[UIImage imageNamed:@"lock.open"] imageWithColor:NCBrandColor.shared.gray size:25] forKey:@"imageView.image"];
+    [row.cellConfig setObject:[[UIImage imageNamed:@"lock_open"] imageWithColor:NCBrandColor.shared.gray size:25] forKey:@"imageView.image"];
     [row.cellConfig setObject:[UIFont systemFontOfSize:15.0] forKey:@"textLabel.font"];
     [row.cellConfig setObject:NCBrandColor.shared.label forKey:@"textLabel.textColor"];
     [row.cellConfig setObject:@(NSTextAlignmentLeft) forKey:@"textLabel.textAlignment"];
@@ -239,7 +239,7 @@
         [rowBloccoPasscode.cellConfig setObject:[[UIImage imageNamed:@"lock"] imageWithColor:NCBrandColor.shared.gray size:25] forKey:@"imageView.image"];
     } else {
         rowBloccoPasscode.title = NSLocalizedString(@"_lock_not_active_", nil);
-        [rowBloccoPasscode.cellConfig setObject:[[UIImage imageNamed:@"lock.open"] imageWithColor:NCBrandColor.shared.gray size:25] forKey:@"imageView.image"];
+        [rowBloccoPasscode.cellConfig setObject:[[UIImage imageNamed:@"lock_open"] imageWithColor:NCBrandColor.shared.gray size:25] forKey:@"imageView.image"];
     }
     
     if ([CCUtility getEnableTouchFaceID]) [rowEnableTouchDaceID setValue:@1]; else [rowEnableTouchDaceID setValue:@0];

+ 11 - 0
iOSClient/Supporting Files/en.lproj/Localizable.strings

@@ -147,6 +147,15 @@
 "_view_in_folder_"          = "View in folder";
 "_leave_share_"             = "Leave this share";
 
+/* MARK: Files lock */
+
+"_lock_file_"                       = "Lock file";
+"_unlock_file_"                     = "Unlock file";
+"_lock_selected_files_"             = "Lock files";
+"_unlock_selected_files_"           = "Unlock files";
+"_locked_by_"                       = "Locked by %@";
+"_file_locked_no_override_"         = "This file is locked. It cannot be overridden.";
+
 /* Remove a file from a list, don't delete it entirely */
 "_remove_file_"             = "Remove file";
 
@@ -258,6 +267,7 @@
 "_file_already_exists_"     = "Unable to complete the operation, a file with the same name exists";
 "_read_file_error_"         = "Could not read the file";
 "_write_file_error_"        = "Could not write the file";
+"_files_lock_error_"        = "There was an error changing the lock of this file";
 "_more_"                        = "More";
 "_notifications_"               = "Notifications";
 "_logout_"                      = "Log out";
@@ -345,6 +355,7 @@
 "_disable_files_app_footer_"    = "Do not permit the access of files via the iOS Files application";
 "_trial_"                       = "Trial";
 "_trial_expired_day_"           = "Days remaining";
+"_time_remaining_"              = "%@ remaining";
 "_disableLocalCacheAfterUpload_footer_" = "After uploading the file, do not keep it in the local cache";
 "_disableLocalCacheAfterUpload_"        = "Disable local cache";
 "_autoupload_"                      = "Auto upload photos/videos";

+ 9 - 7
iOSClient/Utility/NCUtility.swift

@@ -487,25 +487,27 @@ class NCUtility: NSObject {
         return ""
     }
 
-    func loadImage(named: String, color: UIColor = NCBrandColor.shared.gray, size: CGFloat = 50, symbolConfiguration: Any? = nil) -> UIImage {
+    func loadImage(named imageName: String, color: UIColor = NCBrandColor.shared.gray, size: CGFloat = 50, symbolConfiguration: Any? = nil) -> UIImage {
 
         var image: UIImage?
 
         if #available(iOS 13.0, *) {
+            // see https://stackoverflow.com/questions/71764255
+            let sfSymbolName = imageName.replacingOccurrences(of: "_", with: ".")
             if let symbolConfiguration = symbolConfiguration {
-                image = UIImage(systemName: named, withConfiguration: symbolConfiguration as? UIImage.Configuration)?.imageColor(color)
+                image = UIImage(systemName: sfSymbolName, withConfiguration: symbolConfiguration as? UIImage.Configuration)?.imageColor(color)
             } else {
-                image = UIImage(systemName: named)?.imageColor(color)
+                image = UIImage(systemName: sfSymbolName)?.imageColor(color)
             }
             if image == nil {
-                image = UIImage(named: named)?.image(color: color, size: size)
+                image = UIImage(named: imageName)?.image(color: color, size: size)
             }
         } else {
-            image = UIImage(named: named)?.image(color: color, size: size)
+            image = UIImage(named: imageName)?.image(color: color, size: size)
         }
 
-        if image != nil {
-            return image!
+        if let image = image {
+            return image
         }
 
         return  UIImage(named: "file")!.image(color: color, size: size)

+ 11 - 5
iOSClient/Viewer/NCViewerQuickLook/NCViewerQuickLook.swift

@@ -85,15 +85,21 @@ import NCCommunication
         // called after `previewController(:didSaveEditedCopyOf:)`
         super.viewDidDisappear(animated)
 
-        guard isEditingEnabled, hasChanges else { return }
-
-        let alertController = UIAlertController(title: NSLocalizedString("_save_", comment: ""), message: "", preferredStyle: .alert)
-
-        if metadata?.livePhoto == false {
+        guard isEditingEnabled, hasChanges, let metadata = metadata else { return }
+
+        let alertController = UIAlertController(title: NSLocalizedString("_save_", comment: ""), message: nil, preferredStyle: .alert)
+        var message: String?
+        let userId = (UIApplication.shared.delegate as? AppDelegate)?.userId ?? ""
+        if metadata.livePhoto {
+            message = NSLocalizedString("_message_disable_overwrite_livephoto_", comment: "")
+        } else if metadata.lock {
+            message = NSLocalizedString("_file_locked_no_override_", comment: "")
+        } else {
             alertController.addAction(UIAlertAction(title: NSLocalizedString("_overwrite_original_", comment: ""), style: .default) { _ in
                 self.saveModifiedFile(override: true)
             })
         }
+        alertController.message = message
 
         alertController.addAction(UIAlertAction(title: NSLocalizedString("_save_as_copy_", comment: ""), style: .default) { _ in
             self.saveModifiedFile(override: false)

Some files were not shown because too many files changed in this diff