diff --git a/Nextcloud.xcodeproj/project.pbxproj b/Nextcloud.xcodeproj/project.pbxproj index b24d42ac75..9f2a8b7824 100644 --- a/Nextcloud.xcodeproj/project.pbxproj +++ b/Nextcloud.xcodeproj/project.pbxproj @@ -88,7 +88,32 @@ AFCE353327E4ED1900FEA6C2 /* UIToolbar+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353227E4ED1900FEA6C2 /* UIToolbar+Extension.swift */; }; AFCE353527E4ED5900FEA6C2 /* DateFormatter+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353427E4ED5900FEA6C2 /* DateFormatter+Extension.swift */; }; AFCE353727E4ED7B00FEA6C2 /* NCShareCells.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353627E4ED7B00FEA6C2 /* NCShareCells.swift */; }; - AFCE353927E5DE0500FEA6C2 /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* Shareable.swift */; }; + AFCE353927E5DE0500FEA6C2 /* NCShare+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = AFCE353827E5DE0400FEA6C2 /* NCShare+Helper.swift */; }; + B55AFB5B2FE2936100A1A1A4 /* NCShare+Menu.swift in Sources */ = {isa = PBXBuildFile; fileRef = B55AFB5A2FE2936100A1A1A4 /* NCShare+Menu.swift */; }; + B5BEBD192E93A5260002C9E5 /* Shareable.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BEBD182E93A5260002C9E5 /* Shareable.swift */; }; + B5BEBD1E2E93A74C0002C9E5 /* CreateLinkFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BEBD1A2E93A74C0002C9E5 /* CreateLinkFooterView.swift */; }; + B5BEBD1F2E93A74C0002C9E5 /* NCPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BEBD1B2E93A74C0002C9E5 /* NCPermissions.swift */; }; + B5BEBD202E93A74C0002C9E5 /* NCShareEmailLinkHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BEBD1C2E93A74C0002C9E5 /* NCShareEmailLinkHeaderView.swift */; }; + B5BEBD212E93A74C0002C9E5 /* NoSharesFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BEBD1D2E93A74C0002C9E5 /* NoSharesFooterView.swift */; }; + B5BEBD222E93A74C0002C9E5 /* CreateLinkFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BEBD1A2E93A74C0002C9E5 /* CreateLinkFooterView.swift */; }; + B5BEBD232E93A74C0002C9E5 /* NCPermissions.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BEBD1B2E93A74C0002C9E5 /* NCPermissions.swift */; }; + B5BEBD242E93A74C0002C9E5 /* NCShareEmailLinkHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BEBD1C2E93A74C0002C9E5 /* NCShareEmailLinkHeaderView.swift */; }; + B5BEBD252E93A74C0002C9E5 /* NoSharesFooterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5BEBD1D2E93A74C0002C9E5 /* NoSharesFooterView.swift */; }; + B5E2E6D42DAE52B500AB2EDD /* SharingTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E2E6D32DAE52B500AB2EDD /* SharingTest.swift */; }; + B5E2E6D72DAE571200AB2EDD /* PasswordInputField.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5E2E6D62DAE571200AB2EDD /* PasswordInputField.xib */; }; + B5E2E6D82DAE571200AB2EDD /* PasswordInputField.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E2E6D52DAE571200AB2EDD /* PasswordInputField.swift */; }; + B5E2E6DD2DAE573B00AB2EDD /* NCFilePermissionEditCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5E2E6DC2DAE573B00AB2EDD /* NCFilePermissionEditCell.xib */; }; + B5E2E6DE2DAE573B00AB2EDD /* NCFilePermissionCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5E2E6DA2DAE573B00AB2EDD /* NCFilePermissionCell.xib */; }; + B5E2E6DF2DAE573B00AB2EDD /* NCFilePermissionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E2E6D92DAE573B00AB2EDD /* NCFilePermissionCell.swift */; }; + B5E2E6E02DAE573B00AB2EDD /* NCFilePermissionEditCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E2E6DB2DAE573B00AB2EDD /* NCFilePermissionEditCell.swift */; }; + B5E2E6E22DAE59CD00AB2EDD /* NCShareAdvancePermissionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5E2E6E12DAE59CD00AB2EDD /* NCShareAdvancePermissionHeader.xib */; }; + B5E2E6E72DAE59F000AB2EDD /* NCShareHeaderCustomCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5E2E6E42DAE59F000AB2EDD /* NCShareHeaderCustomCell.xib */; }; + B5E2E6E82DAE59F000AB2EDD /* NCShareTextInputCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5E2E6E62DAE59F000AB2EDD /* NCShareTextInputCell.xib */; }; + B5E2E6E92DAE59F000AB2EDD /* NCShareTextInputCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E2E6E52DAE59F000AB2EDD /* NCShareTextInputCell.swift */; }; + B5E2E6EA2DAE59F000AB2EDD /* NCShareHeaderCustomCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E2E6E32DAE59F000AB2EDD /* NCShareHeaderCustomCell.swift */; }; + B5E2E6ED2DAE66AF00AB2EDD /* NCShareEmailFieldCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B5E2E6EC2DAE66AF00AB2EDD /* NCShareEmailFieldCell.xib */; }; + B5E2E6EE2DAE66AF00AB2EDD /* NCShareEmailFieldCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E2E6EB2DAE66AF00AB2EDD /* NCShareEmailFieldCell.swift */; }; + B5E2E6F02DAE6E3F00AB2EDD /* ShareDownloadLimitNetwork.swift in Sources */ = {isa = PBXBuildFile; fileRef = B5E2E6EF2DAE6E3F00AB2EDD /* ShareDownloadLimitNetwork.swift */; }; CB3666201AF7550816B5CD6A /* NCContextMenuComment.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */; }; D5B6AA7827200C7200D49C24 /* NCActivityTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5B6AA7727200C7200D49C24 /* NCActivityTableViewCell.swift */; }; F310B1EF2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = F310B1EE2BA862F1001C42F5 /* NCViewerMedia+VisionKit.swift */; }; @@ -309,7 +334,7 @@ F72CA05C2F5051DB002E2F06 /* AlertActionBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72CA05B2F5051DB002E2F06 /* AlertActionBannerView.swift */; }; F72CD63A25C19EBF00F46F9A /* NCAutoUpload.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72CD63925C19EBF00F46F9A /* NCAutoUpload.swift */; }; F72D1007210B6882009C96B7 /* NCPushNotificationEncryption.m in Sources */ = {isa = PBXBuildFile; fileRef = F72D1005210B6882009C96B7 /* NCPushNotificationEncryption.m */; }; - F72D404923D2082500A97FD0 /* NCViewerDirectEditing.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72D404823D2082500A97FD0 /* NCViewerDirectEditing.swift */; }; + F72D404923D2082500A97FD0 /* NCViewerNextcloudText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72D404823D2082500A97FD0 /* NCViewerNextcloudText.swift */; }; F72D7EB7263B1207000B3DFC /* MarkdownKit in Frameworks */ = {isa = PBXBuildFile; productRef = F72D7EB6263B1207000B3DFC /* MarkdownKit */; }; F72DA9B425F53E4E00B87DB1 /* SwiftRichString in Frameworks */ = {isa = PBXBuildFile; productRef = F72DA9B325F53E4E00B87DB1 /* SwiftRichString */; }; F72EA95228B7BA2A00C88F0C /* DashboardWidgetProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = F72EA95128B7BA2A00C88F0C /* DashboardWidgetProvider.swift */; }; @@ -344,7 +369,7 @@ F7386E482DA90E0F009A00F6 /* NCAppVersionManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7386E452DA90E02009A00F6 /* NCAppVersionManager.swift */; }; F73BC74F2F23811E003170C2 /* WarningBannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7DF7B3E2F1A2EE400514020 /* WarningBannerView.swift */; }; F73CB3B222E072A000AD728E /* NCShareHeaderView.xib in Resources */ = {isa = PBXBuildFile; fileRef = F73CB3B122E072A000AD728E /* NCShareHeaderView.xib */; }; - F73D11FA253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F73D11F9253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard */; }; + F73D11FA253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F73D11F9253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard */; }; F73EF7A72B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7A62B0223900087E6E9 /* NCManageDatabase+Comments.swift */; }; F73EF7A82B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7A62B0223900087E6E9 /* NCManageDatabase+Comments.swift */; }; F73EF7A92B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F73EF7A62B0223900087E6E9 /* NCManageDatabase+Comments.swift */; }; @@ -435,7 +460,6 @@ F758B45A212C564000515F55 /* NCScan.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F758B457212C564000515F55 /* NCScan.storyboard */; }; F758B45E212C569D00515F55 /* NCScanCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = F758B45D212C569C00515F55 /* NCScanCell.swift */; }; F758B460212C56A400515F55 /* NCScan.swift in Sources */ = {isa = PBXBuildFile; fileRef = F758B45F212C56A400515F55 /* NCScan.swift */; }; - F75A60552FB4493A00F8247E /* NCDirectEditorAdapter.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A60542FB4493A00F8247E /* NCDirectEditorAdapter.swift */; }; F75A9EE623796C6F0044CFCE /* NCNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A9EE523796C6F0044CFCE /* NCNetworking.swift */; }; F75A9EE723796C6F0044CFCE /* NCNetworking.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75A9EE523796C6F0044CFCE /* NCNetworking.swift */; }; F75C0C4823D1FAE300163CC8 /* NCRichWorkspaceCommon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F75C0C4723D1FAE300163CC8 /* NCRichWorkspaceCommon.swift */; }; @@ -449,7 +473,6 @@ F75F4BC02FD008D7009E55ED /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; F75F4BC12FD008D7009E55ED /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; F75F4BC22FD008D7009E55ED /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; - F75F4BC32FD008D7009E55ED /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; F75F4BC42FD008D7009E55ED /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; F75F4BC52FD008D7009E55ED /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; F75F4BC62FD008D7009E55ED /* Optional+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F343A4BA2A1E734600DDA874 /* Optional+Extension.swift */; }; @@ -753,18 +776,6 @@ F7C30DFE291BD0B80017149B /* NCNetworkingE2EEDelete.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFC291BD0B80017149B /* NCNetworkingE2EEDelete.swift */; }; F7C30E00291BD2610017149B /* NCNetworkingE2EERename.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFF291BD2610017149B /* NCNetworkingE2EERename.swift */; }; F7C30E01291BD2610017149B /* NCNetworkingE2EERename.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C30DFF291BD2610017149B /* NCNetworkingE2EERename.swift */; }; - F7C55C512FB4A658004A974F /* NCAssistantInputModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C55C502FB4A651004A974F /* NCAssistantInputModel.swift */; }; - F7C55C7C2FB5AEF7004A974F /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = F7C55C7B2FB5AEF7004A974F /* UniformTypeIdentifiers.framework */; }; - F7C55C882FB5AEF7004A974F /* Action Assistant.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - F7C55C8D2FB5B02C004A974F /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; - F7C55C8E2FB5B03D004A974F /* NCGlobal.swift in Sources */ = {isa = PBXBuildFile; fileRef = F702F2CE25EE5B5C008F8E80 /* NCGlobal.swift */; }; - F7C55C8F2FB5B045004A974F /* NCBrand.swift in Sources */ = {isa = PBXBuildFile; fileRef = F76B3CCD1EAE01BD00921AC9 /* NCBrand.swift */; }; - F7C55C9A2FB5B127004A974F /* ThreadSafeDictionary.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7245923289BB50B00474787 /* ThreadSafeDictionary.swift */; }; - F7C55C9B2FB5B1A7004A974F /* UIColor+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = F70CEF5523E9C7E50007035B /* UIColor+Extension.swift */; }; - F7C55C9F2FB5B83A004A974F /* NextcloudKit in Frameworks */ = {isa = PBXBuildFile; productRef = F7C55C9E2FB5B83A004A974F /* NextcloudKit */; }; - F7C55CC92FB5CE74004A974F /* ActionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C55CC32FB5CE74004A974F /* ActionViewController.swift */; }; - F7C55CCA2FB5CE74004A974F /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F7C55CC42FB5CE74004A974F /* Images.xcassets */; }; - F7C55CCC2FB5CE74004A974F /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7C55CC72FB5CE74004A974F /* MainInterface.storyboard */; }; F7C687E92D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */; }; F7C687EA2D22BDE5004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */; }; F7C687EB2D22BDE5004757BC /* NCManageDatabase+RecommendedFiles.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */; }; @@ -951,9 +962,6 @@ F7FDFF702E437E55000D7688 /* NCAccountRequest.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = F7FDFF512E437E55000D7688 /* NCAccountRequest.storyboard */; }; F7FDFF722E437E55000D7688 /* NCAccountRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FDFF522E437E55000D7688 /* NCAccountRequest.swift */; }; F7FF2CB12842159500EBB7A1 /* NCSectionHeader.xib in Resources */ = {isa = PBXBuildFile; fileRef = F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */; }; - F7FFFCA02FB300440015441E /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; - F7FFFCA22FB300600015441E /* NCAssistantSharedTextStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */; }; - F7FFFCA42FB3088E0015441E /* NCShareExtension+Assistant.swift in Sources */ = {isa = PBXBuildFile; fileRef = F7FFFCA32FB3088D0015441E /* NCShareExtension+Assistant.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -1013,13 +1021,6 @@ remoteGlobalIDString = F771E3CF20E2392D00AFB62D; remoteInfo = "File Provider Extension"; }; - F7C55C862FB5AEF7004A974F /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = F7F67BA01A24D27800EE80DA /* Project object */; - proxyType = 1; - remoteGlobalIDString = F7C55C792FB5AEF7004A974F; - remoteInfo = "Action Assistant"; - }; F7C9739728F17131002C43E2 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = F7F67BA01A24D27800EE80DA /* Project object */; @@ -1143,7 +1144,6 @@ 2C33C48623E2C475005F963B /* Notification Service Extension.appex in Embed Foundation Extensions */, F7C9739928F17131002C43E2 /* WidgetDashboardIntentHandler.appex in Embed Foundation Extensions */, F7346E1C28B0EF5E006CE2D2 /* Widget.appex in Embed Foundation Extensions */, - F7C55C882FB5AEF7004A974F /* Action Assistant.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -1277,8 +1277,29 @@ AFCE353227E4ED1900FEA6C2 /* UIToolbar+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIToolbar+Extension.swift"; sourceTree = ""; }; AFCE353427E4ED5900FEA6C2 /* DateFormatter+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "DateFormatter+Extension.swift"; sourceTree = ""; }; AFCE353627E4ED7B00FEA6C2 /* NCShareCells.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareCells.swift; sourceTree = ""; }; - AFCE353827E5DE0400FEA6C2 /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; + AFCE353827E5DE0400FEA6C2 /* NCShare+Helper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShare+Helper.swift"; sourceTree = ""; }; B4C7A5B36D1ED178FB6B76CB /* NCContextMenuPlayerTracks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuPlayerTracks.swift; sourceTree = ""; }; + B55AFB5A2FE2936100A1A1A4 /* NCShare+Menu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShare+Menu.swift"; sourceTree = ""; }; + B5BEBD182E93A5260002C9E5 /* Shareable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Shareable.swift; sourceTree = ""; }; + B5BEBD1A2E93A74C0002C9E5 /* CreateLinkFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateLinkFooterView.swift; sourceTree = ""; }; + B5BEBD1B2E93A74C0002C9E5 /* NCPermissions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCPermissions.swift; sourceTree = ""; }; + B5BEBD1C2E93A74C0002C9E5 /* NCShareEmailLinkHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareEmailLinkHeaderView.swift; sourceTree = ""; }; + B5BEBD1D2E93A74C0002C9E5 /* NoSharesFooterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoSharesFooterView.swift; sourceTree = ""; }; + B5E2E6D32DAE52B500AB2EDD /* SharingTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingTest.swift; sourceTree = ""; }; + B5E2E6D52DAE571200AB2EDD /* PasswordInputField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasswordInputField.swift; sourceTree = ""; }; + B5E2E6D62DAE571200AB2EDD /* PasswordInputField.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = PasswordInputField.xib; sourceTree = ""; }; + B5E2E6D92DAE573B00AB2EDD /* NCFilePermissionCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFilePermissionCell.swift; sourceTree = ""; }; + B5E2E6DA2DAE573B00AB2EDD /* NCFilePermissionCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCFilePermissionCell.xib; sourceTree = ""; }; + B5E2E6DB2DAE573B00AB2EDD /* NCFilePermissionEditCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCFilePermissionEditCell.swift; sourceTree = ""; }; + B5E2E6DC2DAE573B00AB2EDD /* NCFilePermissionEditCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCFilePermissionEditCell.xib; sourceTree = ""; }; + B5E2E6E12DAE59CD00AB2EDD /* NCShareAdvancePermissionHeader.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCShareAdvancePermissionHeader.xib; sourceTree = ""; }; + B5E2E6E32DAE59F000AB2EDD /* NCShareHeaderCustomCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareHeaderCustomCell.swift; sourceTree = ""; }; + B5E2E6E42DAE59F000AB2EDD /* NCShareHeaderCustomCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCShareHeaderCustomCell.xib; sourceTree = ""; }; + B5E2E6E52DAE59F000AB2EDD /* NCShareTextInputCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareTextInputCell.swift; sourceTree = ""; }; + B5E2E6E62DAE59F000AB2EDD /* NCShareTextInputCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCShareTextInputCell.xib; sourceTree = ""; }; + B5E2E6EB2DAE66AF00AB2EDD /* NCShareEmailFieldCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCShareEmailFieldCell.swift; sourceTree = ""; }; + B5E2E6EC2DAE66AF00AB2EDD /* NCShareEmailFieldCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCShareEmailFieldCell.xib; sourceTree = ""; }; + B5E2E6EF2DAE6E3F00AB2EDD /* ShareDownloadLimitNetwork.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareDownloadLimitNetwork.swift; sourceTree = ""; }; BB7697C94BA14450A0867940 /* NCContextMenuProfile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCContextMenuProfile.swift; sourceTree = ""; }; C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; C04E2F202A17BB4D001BAD85 /* NextcloudIntegrationTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = NextcloudIntegrationTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -1411,7 +1432,7 @@ F72CD63925C19EBF00F46F9A /* NCAutoUpload.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAutoUpload.swift; sourceTree = ""; }; F72D1005210B6882009C96B7 /* NCPushNotificationEncryption.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NCPushNotificationEncryption.m; sourceTree = ""; }; F72D1006210B6882009C96B7 /* NCPushNotificationEncryption.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCPushNotificationEncryption.h; sourceTree = ""; }; - F72D404823D2082500A97FD0 /* NCViewerDirectEditing.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCViewerDirectEditing.swift; sourceTree = ""; }; + F72D404823D2082500A97FD0 /* NCViewerNextcloudText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCViewerNextcloudText.swift; sourceTree = ""; }; F72EA95128B7BA2A00C88F0C /* DashboardWidgetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DashboardWidgetProvider.swift; sourceTree = ""; }; F72EA95328B7BABA00C88F0C /* FilesWidgetProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesWidgetProvider.swift; sourceTree = ""; }; F72EA95728B7BC4F00C88F0C /* FilesData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesData.swift; sourceTree = ""; }; @@ -1438,7 +1459,7 @@ F7386E452DA90E02009A00F6 /* NCAppVersionManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAppVersionManager.swift; sourceTree = ""; }; F73CB3B122E072A000AD728E /* NCShareHeaderView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = NCShareHeaderView.xib; sourceTree = ""; }; F73CB5771ED46807005F2A5A /* NCBridgeSwift.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = NCBridgeSwift.h; sourceTree = ""; }; - F73D11F9253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCViewerDirectEditing.storyboard; sourceTree = ""; }; + F73D11F9253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCViewerNextcloudText.storyboard; sourceTree = ""; }; F73EF7A62B0223900087E6E9 /* NCManageDatabase+Comments.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+Comments.swift"; sourceTree = ""; }; F73EF7B62B0224AB0087E6E9 /* NCManageDatabase+ExternalSites.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+ExternalSites.swift"; sourceTree = ""; }; F73EF7BE2B02250B0087E6E9 /* NCManageDatabase+GPS.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+GPS.swift"; sourceTree = ""; }; @@ -1476,7 +1497,6 @@ F758B457212C564000515F55 /* NCScan.storyboard */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.storyboard; path = NCScan.storyboard; sourceTree = ""; }; F758B45D212C569C00515F55 /* NCScanCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCScanCell.swift; sourceTree = ""; }; F758B45F212C56A400515F55 /* NCScan.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCScan.swift; sourceTree = ""; }; - F75A60542FB4493A00F8247E /* NCDirectEditorAdapter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCDirectEditorAdapter.swift; sourceTree = ""; }; F75A9EE523796C6F0044CFCE /* NCNetworking.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NCNetworking.swift; sourceTree = ""; }; F75B91E21ECAE17800199C96 /* fr */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = fr; path = fr.lproj/Localizable.strings; sourceTree = ""; }; F75B91F71ECAE26300199C96 /* pt-BR */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "pt-BR"; path = "pt-BR.lproj/Localizable.strings"; sourceTree = ""; }; @@ -1753,12 +1773,6 @@ F7C30DF9291BCF790017149B /* NCNetworkingE2EECreateFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EECreateFolder.swift; sourceTree = ""; }; F7C30DFC291BD0B80017149B /* NCNetworkingE2EEDelete.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EEDelete.swift; sourceTree = ""; }; F7C30DFF291BD2610017149B /* NCNetworkingE2EERename.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCNetworkingE2EERename.swift; sourceTree = ""; }; - F7C55C502FB4A651004A974F /* NCAssistantInputModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantInputModel.swift; sourceTree = ""; }; - F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Action Assistant.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; - F7C55C7B2FB5AEF7004A974F /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; - F7C55CC32FB5CE74004A974F /* ActionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActionViewController.swift; sourceTree = ""; }; - F7C55CC42FB5CE74004A974F /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Images.xcassets; sourceTree = ""; }; - F7C55CC62FB5CE74004A974F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; F7C687E82D22BD46004757BC /* NCManageDatabase+RecommendedFiles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCManageDatabase+RecommendedFiles.swift"; sourceTree = ""; }; F7C7B488245EBA4100D93E60 /* NCViewerQuickLook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCViewerQuickLook.swift; sourceTree = ""; }; F7C9555221F0C4CA0024296E /* NCActivity.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = NCActivity.storyboard; sourceTree = ""; }; @@ -1877,8 +1891,6 @@ F7FDFF572E437E55000D7688 /* NCAccountSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccountSettingsView.swift; sourceTree = ""; }; F7FDFF592E437E55000D7688 /* NCAccount.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAccount.swift; sourceTree = ""; }; F7FF2CB02842159500EBB7A1 /* NCSectionHeader.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = NCSectionHeader.xib; sourceTree = ""; }; - F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NCAssistantSharedTextStore.swift; sourceTree = ""; }; - F7FFFCA32FB3088D0015441E /* NCShareExtension+Assistant.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NCShareExtension+Assistant.swift"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -2027,15 +2039,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F7C55C772FB5AEF7004A974F /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - F7C55C9F2FB5B83A004A974F /* NextcloudKit in Frameworks */, - F7C55C7C2FB5AEF7004A974F /* UniformTypeIdentifiers.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; F7C9738D28F17131002C43E2 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2063,6 +2066,7 @@ 371B5A2F23D0B04B00FAFAE9 /* Menu */ = { isa = PBXGroup; children = ( + B55AFB5A2FE2936100A1A1A4 /* NCShare+Menu.swift */, F376A3732E5CC5FF0067EE25 /* ContextMenuActions.swift */, 8932E90EC4278026D86CCCC9 /* NCContextMenuComment.swift */, F78C6FDD296D677300C952C3 /* NCContextMenuMain.swift */, @@ -2104,6 +2108,7 @@ children = ( F34BDB3B2F574A58007A222C /* BidiSafeFilenameTests.swift */, AA52EB452D42AC5A0089C348 /* Placeholder.swift */, + B5E2E6D32DAE52B500AB2EDD /* SharingTest.swift */, ); path = NextcloudUnitTests; sourceTree = ""; @@ -2131,6 +2136,15 @@ isa = PBXGroup; children = ( AA8D316D2D4123B200FE2775 /* DownloadLimit */, + B5E2E6E32DAE59F000AB2EDD /* NCShareHeaderCustomCell.swift */, + B5E2E6E42DAE59F000AB2EDD /* NCShareHeaderCustomCell.xib */, + B5E2E6E52DAE59F000AB2EDD /* NCShareTextInputCell.swift */, + B5E2E6E62DAE59F000AB2EDD /* NCShareTextInputCell.xib */, + B5E2E6E12DAE59CD00AB2EDD /* NCShareAdvancePermissionHeader.xib */, + B5E2E6D92DAE573B00AB2EDD /* NCFilePermissionCell.swift */, + B5E2E6DA2DAE573B00AB2EDD /* NCFilePermissionCell.xib */, + B5E2E6DB2DAE573B00AB2EDD /* NCFilePermissionEditCell.swift */, + B5E2E6DC2DAE573B00AB2EDD /* NCFilePermissionEditCell.xib */, AF93471627E2361E002537EE /* NCShareAdvancePermission.swift */, AF93471827E2361E002537EE /* NCShareAdvancePermissionFooter.swift */, AF93471427E2361E002537EE /* NCShareAdvancePermissionFooter.xib */, @@ -2227,10 +2241,8 @@ F3A0478E2BD2668800658E7B /* Assistant */ = { isa = PBXGroup; children = ( - F7C55C502FB4A651004A974F /* NCAssistantInputModel.swift */, F3A047962BD2668800658E7B /* NCAssistant.swift */, F3A047932BD2668800658E7B /* NCAssistantModel.swift */, - F7FFFC9D2FB300440015441E /* NCAssistantSharedTextStore.swift */, F3DDFE0D2F15452F00A784C8 /* Chat */, F3DDFE1F2F1F951000A784C8 /* Chat Sessions */, F3A047902BD2668800658E7B /* Create Task */, @@ -2408,6 +2420,14 @@ F728CE741BF6322C00E69702 /* Share */ = { isa = PBXGroup; children = ( + B5BEBD1A2E93A74C0002C9E5 /* CreateLinkFooterView.swift */, + B5BEBD1B2E93A74C0002C9E5 /* NCPermissions.swift */, + B5BEBD1C2E93A74C0002C9E5 /* NCShareEmailLinkHeaderView.swift */, + B5BEBD1D2E93A74C0002C9E5 /* NoSharesFooterView.swift */, + B5BEBD182E93A5260002C9E5 /* Shareable.swift */, + B5E2E6EF2DAE6E3F00AB2EDD /* ShareDownloadLimitNetwork.swift */, + B5E2E6EB2DAE66AF00AB2EDD /* NCShareEmailFieldCell.swift */, + B5E2E6EC2DAE66AF00AB2EDD /* NCShareEmailFieldCell.xib */, AF93471327E235EB002537EE /* Advanced */, F724377A2C10B83E00C7C68D /* NCSharePermissions.swift */, F3F442ED2DDE292600FD701F /* NCMetadataPermissions.swift */, @@ -2431,7 +2451,7 @@ AF2D7C7D2742559100ADF566 /* NCShareUserCell.swift */, AF93471727E2361E002537EE /* NCShareHeader.xib */, AF93471527E2361E002537EE /* NCShareHeader.swift */, - AFCE353827E5DE0400FEA6C2 /* Shareable.swift */, + AFCE353827E5DE0400FEA6C2 /* NCShare+Helper.swift */, AA8E03D92D2ED83300E7E89C /* TransientShare.swift */, ); path = Share; @@ -2491,14 +2511,13 @@ path = Offline; sourceTree = ""; }; - F73D11FF253C5F5400DF9BEC /* NCViewerDirectEditing */ = { + F73D11FF253C5F5400DF9BEC /* NCViewerNextcloudText */ = { isa = PBXGroup; children = ( - F75A60542FB4493A00F8247E /* NCDirectEditorAdapter.swift */, - F72D404823D2082500A97FD0 /* NCViewerDirectEditing.swift */, - F73D11F9253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard */, + F72D404823D2082500A97FD0 /* NCViewerNextcloudText.swift */, + F73D11F9253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard */, ); - path = NCViewerDirectEditing; + path = NCViewerNextcloudText; sourceTree = ""; }; F74D3DB81BAC1941000BAE4B /* Networking */ = { @@ -2537,6 +2556,8 @@ F758B41E212C516300515F55 /* Scan document */ = { isa = PBXGroup; children = ( + B5E2E6D52DAE571200AB2EDD /* PasswordInputField.swift */, + B5E2E6D62DAE571200AB2EDD /* PasswordInputField.xib */, F758B457212C564000515F55 /* NCScan.storyboard */, F758B45D212C569C00515F55 /* NCScanCell.swift */, F758B45F212C56A400515F55 /* NCScan.swift */, @@ -2857,7 +2878,7 @@ F79018B1240962C7007C9B6D /* NCViewerMedia */, F723986A253C9C0E00257F49 /* NCViewerQuickLook */, F76D3CEF2428B3DD005DFA87 /* NCViewerPDF */, - F73D11FF253C5F5400DF9BEC /* NCViewerDirectEditing */, + F73D11FF253C5F5400DF9BEC /* NCViewerNextcloudText */, F7239861253C95D500257F49 /* NCViewerRichdocument */, ); path = Viewer; @@ -3055,7 +3076,6 @@ AF730AF927843E4C00B7520E /* NCShareExtension+NCAccountRequestDelegate.swift */, AF22B215277D196700DAB0CC /* NCShareExtension+DataSource.swift */, AF22B216277D196700DAB0CC /* NCShareExtension+Files.swift */, - F7FFFCA32FB3088D0015441E /* NCShareExtension+Assistant.swift */, AF22B20B277C6F4D00DAB0CC /* NCShareCell.swift */, F7148046262EBE4B00693E51 /* Share-Bridging-Header.h */, ); @@ -3092,16 +3112,6 @@ path = E2EE; sourceTree = ""; }; - F7C55CC82FB5CE74004A974F /* Action Assistant */ = { - isa = PBXGroup; - children = ( - F7C55CC32FB5CE74004A974F /* ActionViewController.swift */, - F7C55CC42FB5CE74004A974F /* Images.xcassets */, - F7C55CC72FB5CE74004A974F /* MainInterface.storyboard */, - ); - path = "Action Assistant"; - sourceTree = ""; - }; F7C9739328F17131002C43E2 /* WidgetDashboardIntentHandler */ = { isa = PBXGroup; children = ( @@ -3288,7 +3298,6 @@ F7F1FBA62E27D13700C79E20 /* Frameworks */ = { isa = PBXGroup; children = ( - F7C55C7B2FB5AEF7004A974F /* UniformTypeIdentifiers.framework */, ); name = Frameworks; sourceTree = ""; @@ -3330,7 +3339,6 @@ 2C33C48023E2C475005F963B /* Notification Service Extension */, F7346E1428B0EF5B006CE2D2 /* Widget */, F7C9739328F17131002C43E2 /* WidgetDashboardIntentHandler */, - F7C55CC82FB5CE74004A974F /* Action Assistant */, F7FC7D651DC1F98700BB2C6A /* Products */, F30A962A2A27A9C800D7BCFE /* Tests */, F771E3D020E2392D00AFB62D /* File Provider Extension.appex */, @@ -3343,7 +3351,6 @@ C0046CDA2A17B98400D87C9D /* NextcloudUITests.xctest */, F7F1FBA62E27D13700C79E20 /* Frameworks */, F31165012F9674A1009A1E37 /* AppIcon.icon */, - F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */, ); sourceTree = ""; }; @@ -3703,7 +3710,6 @@ F7346E1B28B0EF5E006CE2D2 /* PBXTargetDependency */, F7C9739828F17131002C43E2 /* PBXTargetDependency */, F70716EC2987F81500E72C1D /* PBXTargetDependency */, - F7C55C872FB5AEF7004A974F /* PBXTargetDependency */, ); name = Nextcloud; packageProductDependencies = ( @@ -3740,26 +3746,6 @@ productReference = F7CE8AFA1DC1F8D8009CAE48 /* Nextcloud.app */; productType = "com.apple.product-type.application"; }; - F7C55C792FB5AEF7004A974F /* Action Assistant */ = { - isa = PBXNativeTarget; - buildConfigurationList = F7C55C8C2FB5AEF7004A974F /* Build configuration list for PBXNativeTarget "Action Assistant" */; - buildPhases = ( - F7C55C762FB5AEF7004A974F /* Sources */, - F7C55C772FB5AEF7004A974F /* Frameworks */, - F7C55C782FB5AEF7004A974F /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "Action Assistant"; - packageProductDependencies = ( - F7C55C9E2FB5B83A004A974F /* NextcloudKit */, - ); - productName = "Action Assistant"; - productReference = F7C55C7A2FB5AEF7004A974F /* Action Assistant.appex */; - productType = "com.apple.product-type.app-extension"; - }; F7C9738F28F17131002C43E2 /* WidgetDashboardIntentHandler */ = { isa = PBXNativeTarget; buildConfigurationList = F7C9739A28F17132002C43E2 /* Build configuration list for PBXNativeTarget "WidgetDashboardIntentHandler" */; @@ -3792,7 +3778,7 @@ isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 2640; + LastSwiftUpdateCheck = 1430; LastUpgradeCheck = 2640; ORGANIZATIONNAME = "Marino Faggiana"; TargetAttributes = { @@ -3846,9 +3832,6 @@ }; }; }; - F7C55C792FB5AEF7004A974F = { - CreatedOnToolsVersion = 26.4; - }; F7C9738F28F17131002C43E2 = { CreatedOnToolsVersion = 14.0; }; @@ -3945,7 +3928,6 @@ F7346E0F28B0EF5B006CE2D2 /* Widget */, F7C9738F28F17131002C43E2 /* WidgetDashboardIntentHandler */, F71459B41D12E3B700CAFEEC /* Share */, - F7C55C792FB5AEF7004A974F /* Action Assistant */, F771E3CF20E2392D00AFB62D /* File Provider Extension */, F70716E22987F81400E72C1D /* File Provider Extension UI */, 2C33C47E23E2C475005F963B /* Notification Service Extension */, @@ -4064,9 +4046,14 @@ F765F73225237E3F00391DBE /* NCRecent.storyboard in Resources */, F31165022F9674A1009A1E37 /* AppIcon.icon in Resources */, F78F74342163757000C2ADAD /* NCTrash.storyboard in Resources */, + B5E2E6E72DAE59F000AB2EDD /* NCShareHeaderCustomCell.xib in Resources */, + B5E2E6E82DAE59F000AB2EDD /* NCShareTextInputCell.xib in Resources */, F702F30225EE5D2C008F8E80 /* english.txt in Resources */, F757CC8C29E82D0500F31428 /* NCGroupfolders.storyboard in Resources */, F79A65C32191D90F00FF6DCC /* NCSelect.storyboard in Resources */, + B5E2E6DD2DAE573B00AB2EDD /* NCFilePermissionEditCell.xib in Resources */, + B5E2E6ED2DAE66AF00AB2EDD /* NCShareEmailFieldCell.xib in Resources */, + B5E2E6DE2DAE573B00AB2EDD /* NCFilePermissionCell.xib in Resources */, F7226EDC1EE4089300EBECB1 /* Main.storyboard in Resources */, AF56C1DC2784856200D8BAE2 /* NCActivityCommentView.xib in Resources */, F7F4F10B27ECDBDB008676F9 /* Inconsolata-Light.ttf in Resources */, @@ -4086,9 +4073,11 @@ F723B3DD22FC6D1D00301EFE /* NCShareCommentsCell.xib in Resources */, F78ACD4B21903F850088454D /* NCTrashListCell.xib in Resources */, AF93471927E2361E002537EE /* NCShareAdvancePermissionFooter.xib in Resources */, + B5E2E6E22DAE59CD00AB2EDD /* NCShareAdvancePermissionHeader.xib in Resources */, F7725A61251F33BB00D125E0 /* NCFiles.storyboard in Resources */, F7CBC1232BAC8B0000EC1D55 /* NCSectionFirstHeaderEmptyData.xib in Resources */, F700510122DF63AC003A3356 /* NCShare.storyboard in Resources */, + B5E2E6D72DAE571200AB2EDD /* PasswordInputField.xib in Resources */, F787704F22E7019900F287A9 /* NCShareLinkCell.xib in Resources */, F70753F72542A9C000972D44 /* NCViewerMediaPage.storyboard in Resources */, F7F4F10627ECDBDB008676F9 /* Inconsolata-Medium.ttf in Resources */, @@ -4106,7 +4095,7 @@ F704B5E32430AA6F00632F5F /* NCCreateFormUploadConflict.storyboard in Resources */, F7EDE509262DA9D600414FE6 /* NCSelectCommandViewSelect.xib in Resources */, F732D23327CF8AED000B0F1B /* NCPlayerToolBar.xib in Resources */, - F73D11FA253C5F4800DF9BEC /* NCViewerDirectEditing.storyboard in Resources */, + F73D11FA253C5F4800DF9BEC /* NCViewerNextcloudText.storyboard in Resources */, F7EDE51B262DD0C400414FE6 /* NCSelectCommandViewCopyMove.xib in Resources */, F7FF2CB12842159500EBB7A1 /* NCSectionHeader.xib in Resources */, F7D1612023CF19E30039EBBF /* NCViewerRichWorkspace.storyboard in Resources */, @@ -4124,15 +4113,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F7C55C782FB5AEF7004A974F /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F7C55CCA2FB5CE74004A974F /* Images.xcassets in Resources */, - F7C55CCC2FB5CE74004A974F /* MainInterface.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; F7C9738E28F17131002C43E2 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4251,6 +4231,7 @@ F34BDB3C2F574A58007A222C /* BidiSafeFilenameTests.swift in Sources */, F372087D2BAB4C0F006B5430 /* TestConstants.swift in Sources */, F78E2D6C29AF02DB0024D4F3 /* Database.swift in Sources */, + B5E2E6D42DAE52B500AB2EDD /* SharingTest.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4403,7 +4384,6 @@ F7F878AF1FB9E3B900599E4F /* NCEndToEndMetadata.swift in Sources */, F7327E3B2B73B8D600A462C7 /* Array+Extension.swift in Sources */, F7D7A76C2DCDD437003D2007 /* NCManageDatabase+AutoUpload.swift in Sources */, - F7FFFCA02FB300440015441E /* NCAssistantSharedTextStore.swift in Sources */, F39170AF2CB82024006127BC /* FileAutoRenamer+Extensions.swift in Sources */, F7D496FC2EBFA541004F9823 /* NCRecommendationsCell.swift in Sources */, F799DF832C4B7DCC003410B5 /* NCSectionFooter.swift in Sources */, @@ -4425,7 +4405,6 @@ F763412D2EBE255B0056F538 /* NCNetworking+NextcloudKitDelegate.swift in Sources */, F72FD3B8297ED49A00075D28 /* NCManageDatabase+E2EE.swift in Sources */, F7A76DC8256A71CD00119AB3 /* UIImage+Extension.swift in Sources */, - F7FFFCA42FB3088E0015441E /* NCShareExtension+Assistant.swift in Sources */, F3E173C32C9B1067006D177A /* AwakeMode.swift in Sources */, F711A4E52AF9310500095DD8 /* NCUtility+Image.swift in Sources */, F73EF7AA2B0223900087E6E9 /* NCManageDatabase+Comments.swift in Sources */, @@ -4454,6 +4433,10 @@ F76B3CCF1EAE01BD00921AC9 /* NCBrand.swift in Sources */, F72944F32A84246400246839 /* NCEndToEndMetadataV2.swift in Sources */, F7BAADCC1ED5A87C00B7EAD4 /* NCManageDatabase.swift in Sources */, + B5BEBD1E2E93A74C0002C9E5 /* CreateLinkFooterView.swift in Sources */, + B5BEBD1F2E93A74C0002C9E5 /* NCPermissions.swift in Sources */, + B5BEBD202E93A74C0002C9E5 /* NCShareEmailLinkHeaderView.swift in Sources */, + B5BEBD212E93A74C0002C9E5 /* NoSharesFooterView.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4593,11 +4576,15 @@ F7DF7B3F2F1A2EF900514020 /* WarningBannerView.swift in Sources */, F768822C2C0DD1E7001CF441 /* NCPreferences.swift in Sources */, F7CAFE1D2F17A35F00DB35A5 /* NCNetworking+Actor.swift in Sources */, + F768822C2C0DD1E7001CF441 /* NCPreferences.swift in Sources */, + B5E2E6EE2DAE66AF00AB2EDD /* NCShareEmailFieldCell.swift in Sources */, F3754A7D2CF87D600009312E /* SetupPasscodeView.swift in Sources */, F73EF7D72B0226080087E6E9 /* NCManageDatabase+Tip.swift in Sources */, F3374A842D64AC31002A38F9 /* AssistantLabelStyle.swift in Sources */, F74BAE172C7E2F4E0028D4FA /* FileProviderDomain.swift in Sources */, F76882402C0DD30B001CF441 /* ViewOnAppear.swift in Sources */, + B5E2E6E92DAE59F000AB2EDD /* NCShareTextInputCell.swift in Sources */, + B5E2E6EA2DAE59F000AB2EDD /* NCShareHeaderCustomCell.swift in Sources */, F790110E21415BF600D7B136 /* NCViewerRichDocument.swift in Sources */, F78ACD4021903CC20088454D /* NCGridCell.swift in Sources */, F7D890752BD25C570050B8A6 /* NCCollectionViewCommon+DragDrop.swift in Sources */, @@ -4616,6 +4603,7 @@ F7BD0A002C468925003A4A6D /* NCMedia+CollectionViewDataSource.swift in Sources */, F76D3CF12428B40E005DFA87 /* NCViewerPDFSearch.swift in Sources */, F7245924289BB50C00474787 /* ThreadSafeDictionary.swift in Sources */, + B5BEBD192E93A5260002C9E5 /* Shareable.swift in Sources */, F702F2CF25EE5B5C008F8E80 /* NCGlobal.swift in Sources */, F794E13F2BBC0F70003693D7 /* SceneDelegate.swift in Sources */, F714A1472ED84AF90050A43B /* HudBannerView.swift in Sources */, @@ -4674,8 +4662,8 @@ F7D60CAF2C941ACB008FBFDD /* NCMediaPinchGesture.swift in Sources */, F71916142E2901FB00E13E96 /* NCNetworking+Upload.swift in Sources */, F704B5E92430C0B800632F5F /* NCCreateFormUploadConflictCell.swift in Sources */, - F72D404923D2082500A97FD0 /* NCViewerDirectEditing.swift in Sources */, - AFCE353927E5DE0500FEA6C2 /* Shareable.swift in Sources */, + F72D404923D2082500A97FD0 /* NCViewerNextcloudText.swift in Sources */, + AFCE353927E5DE0500FEA6C2 /* NCShare+Helper.swift in Sources */, F77BB746289984CA0090FC19 /* UIViewController+Extension.swift in Sources */, F700510522DF6A89003A3356 /* NCShare.swift in Sources */, F72D1007210B6882009C96B7 /* NCPushNotificationEncryption.m in Sources */, @@ -4731,7 +4719,6 @@ F78F74362163781100C2ADAD /* NCTrash.swift in Sources */, F71638922FA0C20C00A913B7 /* NCMoreView.swift in Sources */, AF2D7C7C2742556F00ADF566 /* NCShareLinkCell.swift in Sources */, - F7C55C512FB4A658004A974F /* NCAssistantInputModel.swift in Sources */, F7E41316294A19B300839300 /* UIView+Extension.swift in Sources */, F7C30E00291BD2610017149B /* NCNetworkingE2EERename.swift in Sources */, F74AF3A4247FB6AE00AC767B /* NCUtilityFileSystem.swift in Sources */, @@ -4739,6 +4726,7 @@ F73EF7BF2B02250B0087E6E9 /* NCManageDatabase+GPS.swift in Sources */, F39A1EE22D0AF8A400DAD522 /* Albums.swift in Sources */, F71F6D072B6A6A5E00F1EB15 /* ThreadSafeArray.swift in Sources */, + B5E2E6D82DAE571200AB2EDD /* PasswordInputField.swift in Sources */, F761856C29E98543006EB3B0 /* NCIntroCollectionViewCell.swift in Sources */, F75DD765290ABB25002EB562 /* Intent.intentdefinition in Sources */, F7D4BF012CA1831900A5E746 /* NCCollectionViewCommonPinchGesture.swift in Sources */, @@ -4791,6 +4779,10 @@ F76B3CCE1EAE01BD00921AC9 /* NCBrand.swift in Sources */, F7FA7FFC2C0F4EE40072FC60 /* NCViewerQuickLookView.swift in Sources */, F3A0479B2BD2668800658E7B /* NCAssistant.swift in Sources */, + B5BEBD222E93A74C0002C9E5 /* CreateLinkFooterView.swift in Sources */, + B5BEBD232E93A74C0002C9E5 /* NCPermissions.swift in Sources */, + B5BEBD242E93A74C0002C9E5 /* NCShareEmailLinkHeaderView.swift in Sources */, + B5BEBD252E93A74C0002C9E5 /* NoSharesFooterView.swift in Sources */, F359D8672A7D03420023F405 /* NCUtility+Exif.swift in Sources */, F7A03E352D427312007AA677 /* NCMainNavigationController.swift in Sources */, F769CA192966EA3C00039397 /* ComponentView.swift in Sources */, @@ -4801,6 +4793,7 @@ AF93474E27E3F212002537EE /* NCShareNewUserAddComment.swift in Sources */, F7C30DFD291BD0B80017149B /* NCNetworkingE2EEDelete.swift in Sources */, F77E8C232E79717D00EAE68F /* NCManageDatabase+LivePhoto.swift in Sources */, + B5E2E6F02DAE6E3F00AB2EDD /* ShareDownloadLimitNetwork.swift in Sources */, F76882302C0DD1E7001CF441 /* NCFileNameModel.swift in Sources */, F7CF06882E1127460063AD04 /* NCManageDatabase+CreateMetadata.swift in Sources */, F72FD3B5297ED49A00075D28 /* NCManageDatabase+E2EE.swift in Sources */, @@ -4840,6 +4833,8 @@ AF93471B27E2361E002537EE /* NCShareAdvancePermission.swift in Sources */, F77BC3ED293E528A005F2B08 /* NCConfigServer.swift in Sources */, F7A560422AE1593700BE8FD6 /* NCOperationSaveLivePhoto.swift in Sources */, + B5E2E6DF2DAE573B00AB2EDD /* NCFilePermissionCell.swift in Sources */, + B5E2E6E02DAE573B00AB2EDD /* NCFilePermissionEditCell.swift in Sources */, F7D1C4AC2C9484FD00EC6D44 /* NCMedia+CollectionViewDataSourcePrefetching.swift in Sources */, F7D368DF2DAFE19E0037E7C6 /* NCActivityNavigationController.swift in Sources */, F7A03E332D426115007AA677 /* NCMoreNavigationController.swift in Sources */, @@ -4862,6 +4857,7 @@ F77C97392953131000FDDD09 /* NCCameraRoll.swift in Sources */, F7CADEFD2EA159210057849E /* NCMetadataTranfersSuccess.swift in Sources */, F343A4B32A1E01FF00DDA874 /* PHAsset+Extension.swift in Sources */, + B55AFB5B2FE2936100A1A1A4 /* NCShare+Menu.swift in Sources */, F70968A424212C4E00ED60E5 /* NCLivePhoto.swift in Sources */, F7C30DFA291BCF790017149B /* NCNetworkingE2EECreateFolder.swift in Sources */, F72CA05C2F5051DB002E2F06 /* AlertActionBannerView.swift in Sources */, @@ -4891,7 +4887,6 @@ F3DDFE212F1F953000A784C8 /* NCAssistantChatConversations.swift in Sources */, F72EC7262F45C91E00A2135C /* NCContextMenuNavigation.swift in Sources */, F7E2B64F2DDCC5C30075B4D0 /* NCMedia+TransferDelegate.swift in Sources */, - F7FFFCA22FB300600015441E /* NCAssistantSharedTextStore.swift in Sources */, F3DDFE0F2F15453900A784C8 /* NCAssistantChat.swift in Sources */, F7D68FCC28CB9051009139F3 /* NCManageDatabase+DashboardWidget.swift in Sources */, F76882292C0DD1E7001CF441 /* NCManageE2EEModel.swift in Sources */, @@ -4921,7 +4916,6 @@ F7D61EA82EBF1694007F865B /* NCManageDatabase+TableCapabilities.swift in Sources */, F79FFB262A97C24A0055EEA4 /* NCNetworkingE2EEMarkFolder.swift in Sources */, F70D8D8124A4A9BF000A5756 /* NCNetworkingProcess.swift in Sources */, - F75A60552FB4493A00F8247E /* NCDirectEditorAdapter.swift in Sources */, F3A0479A2BD2668800658E7B /* NCAssistantTaskDetail.swift in Sources */, F71D2FB72E09BBD700B751CC /* NCAutoUploadModel.swift in Sources */, F38F71252B6BBDC300473CDC /* NCCollectionViewCommonSelectTabBar.swift in Sources */, @@ -4941,20 +4935,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - F7C55C762FB5AEF7004A974F /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - F7C55C8F2FB5B045004A974F /* NCBrand.swift in Sources */, - F7C55C8E2FB5B03D004A974F /* NCGlobal.swift in Sources */, - F7C55CC92FB5CE74004A974F /* ActionViewController.swift in Sources */, - F7C55C9B2FB5B1A7004A974F /* UIColor+Extension.swift in Sources */, - F75F4BC32FD008D7009E55ED /* Optional+Extension.swift in Sources */, - F7C55C8D2FB5B02C004A974F /* NCAssistantSharedTextStore.swift in Sources */, - F7C55C9A2FB5B127004A974F /* ThreadSafeDictionary.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; F7C9738C28F17131002C43E2 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -5055,11 +5035,6 @@ target = F771E3CF20E2392D00AFB62D /* File Provider Extension */; targetProxy = F771E3E920E2392E00AFB62D /* PBXContainerItemProxy */; }; - F7C55C872FB5AEF7004A974F /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = F7C55C792FB5AEF7004A974F /* Action Assistant */; - targetProxy = F7C55C862FB5AEF7004A974F /* PBXContainerItemProxy */; - }; F7C9739828F17131002C43E2 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = F7C9738F28F17131002C43E2 /* WidgetDashboardIntentHandler */; @@ -5238,14 +5213,6 @@ name = Intent.intentdefinition; sourceTree = ""; }; - F7C55CC72FB5CE74004A974F /* MainInterface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - F7C55CC62FB5CE74004A974F /* Base */, - ); - name = MainInterface.storyboard; - sourceTree = ""; - }; F7E70DE91A24DE4100E1B66A /* Localizable.strings */ = { isa = PBXVariantGroup; children = ( @@ -5659,7 +5626,7 @@ OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = it.twsweb.Nextcloud.Share; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; @@ -5683,7 +5650,7 @@ OTHER_LDFLAGS = ""; PRODUCT_BUNDLE_IDENTIFIER = it.twsweb.Nextcloud.Share; PRODUCT_NAME = "$(TARGET_NAME)"; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_XR_DESIGNED_FOR_IPHONE_IPAD = YES; @@ -5851,85 +5818,6 @@ }; name = Release; }; - F7C55C892FB5AEF7004A974F /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand/Action_Assistant.entitlements"; - CODE_SIGN_STYLE = Automatic; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_DYNAMIC_NO_PIC = NO; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "$(SRCROOT)/Brand/Action_Assistant.plist"; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "it.twsweb.Nextcloud.Action-Assistant"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) EXTENSION"; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - }; - name = Debug; - }; - F7C55C8A2FB5AEF7004A974F /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = "$(SRCROOT)/Brand/Action_Assistant.entitlements"; - CODE_SIGN_STYLE = Automatic; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_USER_SCRIPT_SANDBOXING = YES; - GCC_C_LANGUAGE_STANDARD = gnu17; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = "$(SRCROOT)/Brand/Action_Assistant.plist"; - LOCALIZATION_PREFERS_STRING_CATALOGS = YES; - MTL_ENABLE_DEBUG_INFO = NO; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = "it.twsweb.Nextcloud.Action-Assistant"; - PRODUCT_NAME = "$(TARGET_NAME)"; - SKIP_INSTALL = YES; - STRING_CATALOG_GENERATE_SYMBOLS = YES; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) EXTENSION"; - SWIFT_APPROACHABLE_CONCURRENCY = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; F7C9739B28F17132002C43E2 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -6066,7 +5954,6 @@ ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-v"; OTHER_LDFLAGS = ""; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) NC DEBUG"; SWIFT_OBJC_INTEROP_MODE = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -6132,7 +6019,6 @@ ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = "-v"; OTHER_LDFLAGS = ""; - SUPPORTED_PLATFORMS = "iphonesimulator iphoneos"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) NC"; SWIFT_COMPILATION_MODE = wholemodule; SWIFT_OBJC_INTEROP_MODE = objc; @@ -6226,15 +6112,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - F7C55C8C2FB5AEF7004A974F /* Build configuration list for PBXNativeTarget "Action Assistant" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - F7C55C892FB5AEF7004A974F /* Debug */, - F7C55C8A2FB5AEF7004A974F /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; F7C9739A28F17132002C43E2 /* Build configuration list for PBXNativeTarget "WidgetDashboardIntentHandler" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -6388,8 +6265,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/nextcloud/NextcloudKit"; requirement = { - branch = main; - kind = branch; + kind = exactVersion; + version = 7.3.1; }; }; F788ECC5263AAAF900ADC67F /* XCRemoteSwiftPackageReference "MarkdownKit" */ = { @@ -6831,11 +6708,6 @@ package = F7BB7E4527A18C56009B9F29 /* XCRemoteSwiftPackageReference "Parchment" */; productName = Parchment; }; - F7C55C9E2FB5B83A004A974F /* NextcloudKit */ = { - isa = XCSwiftPackageProductDependency; - package = F783034028B511D200B84583 /* XCRemoteSwiftPackageReference "NextcloudKit" */; - productName = NextcloudKit; - }; F7D4BF532CA2ED9D00A5E746 /* VLCKitSPM */ = { isa = XCSwiftPackageProductDependency; package = F7D4BF4E2CA2ECCB00A5E746 /* XCRemoteSwiftPackageReference "vlckit-spm" */; diff --git a/Tests/NextcloudUnitTests/SharingTest.swift b/Tests/NextcloudUnitTests/SharingTest.swift new file mode 100644 index 0000000000..984a4c0ac3 --- /dev/null +++ b/Tests/NextcloudUnitTests/SharingTest.swift @@ -0,0 +1,232 @@ +// +// SharingTest.swift +// NextcloudTests +// +// Created by A200020526 on 07/06/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import XCTest +@testable import Nextcloud +final class SharingTest: XCTestCase { + + var button: UIButton? + var ncShare: NCShare? + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + super.setUp() + button = UIButton() + ncShare = NCShare() + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + button = nil + ncShare = nil + super.tearDown() + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + self.measure { + // Put the code you want to measure the time of here. + } + } + + + //Date exntesion test case + func testTomorrow() { + let tomorrow = Date.tomorrow + let expectedTomorrow = Calendar.current.date(byAdding: .day, value: 1, to: Date())! + XCTAssertEqual(tomorrow.extendedIso8601String, expectedTomorrow.extendedIso8601String, "Tomorrow date should be correct.") + } + func testToday() { + let today = Date.today + let currentDate = Date() + XCTAssertEqual(today.extendedIso8601String, currentDate.extendedIso8601String, "Today date should be correct.") + } + + func testDayAfter() { + let date = Date() + let dayAfter = date.dayAfter + let expectedDayAfter = Calendar.current.date(byAdding: .day, value: 1, to: date)! + XCTAssertEqual(dayAfter.extendedIso8601String, expectedDayAfter.extendedIso8601String, "Day after date should be correct.") + } + + //Date Formatter extension Test Case + func testShareExpDate() { + let dateFormatter = DateFormatter.shareExpDate + + XCTAssertEqual(dateFormatter.formatterBehavior, .behavior10_4, "Formatter behavior should be correct.") + XCTAssertEqual(dateFormatter.dateStyle, .medium, "Date style should be correct.") + XCTAssertEqual(dateFormatter.dateFormat, NCShareAdvancePermission.displayDateFormat, "Date format should be correct.") + } + + //Button Extension test case + func testSetBackgroundColor() { + // Arrange + let color = UIColor.red + let state: UIControl.State = .normal + + // Act + button?.setBackgroundColor(color, for: state) + + // Assert + XCTAssertNotNil(button?.currentBackgroundImage, "Button background image not nil") + } + + func testSetBackgroundColorForDifferentStates() { + // Arrange + + let selectedColor = UIColor.green + + // Act + button?.isSelected = true + button?.setBackgroundColor(selectedColor, for: .selected) + + // Assert + XCTAssertNotNil(button?.currentBackgroundImage, "Button background image not nil") + button?.isSelected = false + XCTAssertNil(button?.currentBackgroundImage,"Button background image will be nil") + button?.isHighlighted = true + XCTAssertNil(button?.currentBackgroundImage, "Button background image will be nil") + } + + //UIView extension shadow test case + func testAddShadowWithLocation() { + // Create a UIView instance + let view = UIView() + + // Set the shadow with bottom location + view.addShadow(location: .bottom, height: 2, color: .red, opacity: 0.4, radius: 2) + + // Verify that the shadow offset is set correctly for the bottom location + let bottomShadowOffset = view.layer.shadowOffset + XCTAssertEqual(bottomShadowOffset, CGSize(width: 0, height: 2), "Shadow offset not set correctly for bottom location") + + // Verify that the shadow color is set correctly + let shadowColor = view.layer.shadowColor + XCTAssertEqual(shadowColor, UIColor.red.cgColor, "Shadow color not set correctly") + + // Verify that the shadow opacity is set correctly + let shadowOpacity = view.layer.shadowOpacity + XCTAssertEqual(shadowOpacity, 0.4, "Shadow opacity not set correctly") + + // Verify that the shadow radius is set correctly + let shadowRadius = view.layer.shadowRadius + XCTAssertEqual(shadowRadius, 2.0, "Shadow radius not set correctly") + } + + func testAddShadowWithOffset() { + // Create a UIView instance + let view = UIView() + + // Set the shadow with a custom offset + view.addShadow(offset: CGSize(width: 0, height: -4), color: .blue, opacity: 0.6, radius: 3) + + // Verify that the shadow offset is set correctly + let shadowOffset = view.layer.shadowOffset + XCTAssertEqual(shadowOffset, CGSize(width: 0, height: -4), "Shadow offset not set correctly") + + // Verify that the shadow color is set correctly + let shadowColor = view.layer.shadowColor + XCTAssertEqual(shadowColor, UIColor.blue.cgColor, "Shadow color not set correctly") + + // Verify that the shadow opacity is set correctly + let shadowOpacity = view.layer.shadowOpacity + XCTAssertEqual(shadowOpacity, 0.6, "Shadow opacity not set correctly") + + // Verify that the shadow radius is set correctly + let shadowRadius = view.layer.shadowRadius + XCTAssertEqual(shadowRadius, 3.0, "Shadow radius not set correctly") + } + + func testAddShadowForLocation() { + // Create a UIView instance + let view = UIView() + + // Add shadow to the bottom + view.addShadow(location: .bottom, color: UIColor.black) + + // Verify that the shadow properties are set correctly for the bottom location + XCTAssertEqual(view.layer.shadowOffset, CGSize(width: 0, height: 2), "Shadow offset not set correctly for bottom location") + XCTAssertEqual(view.layer.shadowColor, UIColor.black.cgColor, "Shadow color not set correctly for bottom location") + XCTAssertEqual(view.layer.shadowOpacity, 0.4, "Shadow opacity not set correctly for bottom location") + XCTAssertEqual(view.layer.shadowRadius, 2.0, "Shadow radius not set correctly for bottom location") + + // Add shadow to the top + view.addShadow(location: .top) + + // Verify that the shadow properties are set correctly for the top location + XCTAssertEqual(view.layer.shadowOffset, CGSize(width: 0, height: -2), "Shadow offset not set correctly for top location") + XCTAssertEqual(view.layer.shadowColor, NCBrandColor.shared.customerDarkGrey.cgColor, "Shadow color not set correctly for top location") + XCTAssertEqual(view.layer.shadowOpacity, 0.4, "Shadow opacity not set correctly for top location") + XCTAssertEqual(view.layer.shadowRadius, 2.0, "Shadow radius not set correctly for top location") + } + + func testAddShadowForOffset() { + // Create a UIView instance + let view = UIView() + + // Add shadow with custom offset + view.addShadow(offset: CGSize(width: 2, height: 2)) + + // Verify that the shadow properties are set correctly for the custom offset + XCTAssertEqual(view.layer.shadowOffset, CGSize(width: 2, height: 2), "Shadow offset not set correctly for custom offset") + XCTAssertEqual(view.layer.shadowColor, UIColor.black.cgColor, "Shadow color not set correctly for custom offset") + XCTAssertEqual(view.layer.shadowOpacity, 0.5, "Shadow opacity not set correctly for custom offset") + XCTAssertEqual(view.layer.shadowRadius, 5.0, "Shadow radius not set correctly for custom offset") + } + + + func testHasUploadPermission() { + // Create an instance of NCShare + let share = NCShare() + + // Define the input parameters + let tableShareWithUploadPermission = tableShare() + tableShareWithUploadPermission.permissions = NCGlobal.shared.permissionMaxFileShare + + let tableShareWithoutUploadPermission = tableShare() + tableShareWithoutUploadPermission.permissions = NCGlobal.shared.permissionReadShare + + // Call the hasUploadPermission function + let hasUploadPermission1 = share.hasUploadPermission(tableShare: tableShareWithUploadPermission) + let hasUploadPermission2 = share.hasUploadPermission(tableShare: tableShareWithoutUploadPermission) + + // Verify the results + XCTAssertTrue(hasUploadPermission1, "hasUploadPermission returned false for a tableShare with upload permission") + XCTAssertFalse(hasUploadPermission2, "hasUploadPermission returned true for a tableShare without upload permission") + } + + func testGetImageShareType() { + let sut = NCShareCommon // Replace with the actual class containing the getImageShareType function + + // Test case 1: SHARE_TYPE_USER + let shareType1 = sut.shareTypeUser + let result1 = sut.getImageShareType(shareType: shareType1) + XCTAssertEqual(result1, UIImage(named: "shareTypeEmail")?.imageColor(NCBrandColor.shared.label)) + + // Test case 2: SHARE_TYPE_GROUP + let shareType2 = sut.shareTypeGroup + let result2 = sut.getImageShareType(shareType: shareType2) + XCTAssertEqual(result2, UIImage(named: "shareTypeGroup")?.imageColor(NCBrandColor.shared.label)) + + // Test case 3: SHARE_TYPE_LINK + let shareType3 = sut.shareTypeLink + let result3 = sut.getImageShareType(shareType: shareType3) + XCTAssertEqual(result3, UIImage(named: "shareTypeLink")?.imageColor(NCBrandColor.shared.label)) + + // Test case 4: SHARE_TYPE_EMAIL (with isDropDown=false) + let shareType4 = sut.shareTypeEmail + let result4 = sut.getImageShareType(shareType: shareType4) + XCTAssertEqual(result4, UIImage(named: "shareTypeUser")?.imageColor(NCBrandColor.shared.label)) + + // Test case 5: SHARE_TYPE_EMAIL (with isDropDown=true) + let shareType5 = sut.shareTypeEmail + let isDropDown5 = true + let result5 = sut.getImageShareType(shareType: shareType5)//, isDropDown: isDropDown5) + XCTAssertEqual(result5, UIImage(named: "email")?.imageColor(NCBrandColor.shared.label)) + } +} diff --git a/iOSClient/Activity/NCActivity.swift b/iOSClient/Activity/NCActivity.swift index 10df054eb5..22a21f79b2 100644 --- a/iOSClient/Activity/NCActivity.swift +++ b/iOSClient/Activity/NCActivity.swift @@ -5,13 +5,14 @@ import UIKit import SwiftRichString import NextcloudKit +import SVGKit class NCActivity: UIViewController, NCSharePagingContent { @IBOutlet weak var viewContainerConstraint: NSLayoutConstraint! @IBOutlet weak var tableView: UITableView! var commentView: NCActivityCommentView? - var textField: UIView? { commentView?.newCommentField } + var textField: UITextField? { commentView?.newCommentField } var height: CGFloat = 0 var metadata: tableMetadata? var showComments: Bool = false @@ -48,7 +49,7 @@ class NCActivity: UIViewController, NCSharePagingContent { internal var windowScene: UIWindowScene? { SceneManager.shared.getWindowScene(controller: self.tabBarController as? NCMainTabBarController) } - + // MARK: - View Life Cycle override func viewDidLoad() { @@ -223,16 +224,15 @@ extension NCActivity: UITableViewDataSource { cell.indexPath = indexPath cell.tableComments = comment cell.delegate = self - cell.configureAvatarMenu() // Avatar let fileName = NCSession.shared.getFileName(urlBase: metadata.urlBase, user: comment.actorId) let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) if results.image == nil { - cell.avatarImage?.image = utility.loadUserImage(for: comment.actorId, displayName: comment.actorDisplayName, urlBase: NCSession.shared.getSession(account: account).urlBase) + cell.avatarImageView?.image = utility.loadUserImage(for: comment.actorId, displayName: comment.actorDisplayName, urlBase: NCSession.shared.getSession(account: account).urlBase) } else { - cell.avatarImage?.image = results.image + cell.avatarImageView?.image = results.image } if let tblAvatar = results.tblAvatar, @@ -253,7 +253,6 @@ extension NCActivity: UITableViewDataSource { // Button Menu if comment.actorId == metadata.userId { cell.buttonMenu.isHidden = false - cell.configureCommentMenu() } else { cell.buttonMenu.isHidden = true } @@ -271,7 +270,7 @@ extension NCActivity: UITableViewDataSource { cell.idActivity = activity.idActivity cell.account = activity.account - cell.index = indexPath + cell.indexPath = indexPath cell.avatar.image = nil cell.avatar.isHidden = true cell.didSelectItemEnable = self.didSelectItemEnable @@ -280,14 +279,31 @@ extension NCActivity: UITableViewDataSource { // icon if !activity.icon.isEmpty { - Task { - let results = await NCUtility().convertSVGtoPNGWriteToUserData(serverUrl: activity.icon, - rewrite: false, - account: activity.account, - id: activity.idActivity) - if let image = results.image, - cell.idActivity == results.id { - cell.icon.image = image + activity.icon = activity.icon.replacingOccurrences(of: ".png", with: ".svg") + let fileNameIcon = (activity.icon as NSString).lastPathComponent + let fileNameLocalPath = utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileNameIcon) + + if FileManager.default.fileExists(atPath: fileNameLocalPath) { + let image = fileNameIcon.contains(".svg") ? SVGKImage(contentsOfFile: fileNameLocalPath)?.uiImage : UIImage(contentsOfFile: fileNameLocalPath) + + if let image { + cell.icon.image = image.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) + } + } else { + NextcloudKit.shared.downloadContent(serverUrl: activity.icon, account: activity.account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.account, + path: activity.icon, + name: "downloadContent") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } completion: { _, responseData, error in + if error == .success, let data = responseData?.data { + do { + try data.write(to: NSURL(fileURLWithPath: fileNameLocalPath) as URL, options: .atomic) + self.tableView.reloadData() + } catch { return } + } } } } @@ -295,17 +311,16 @@ extension NCActivity: UITableViewDataSource { // avatar if !activity.user.isEmpty && activity.user != session.userId { cell.avatar.isHidden = false - cell.user = activity.user + cell.fileUser = activity.user cell.subjectLeadingConstraint.constant = 15 - cell.configureAvatarMenu() let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: activity.user) let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) if results.image == nil { - cell.avatar?.image = utility.loadUserImage(for: activity.user, displayName: nil, urlBase: session.urlBase) + cell.avatarImageView?.image = utility.loadUserImage(for: activity.user, displayName: nil, urlBase: session.urlBase) } else { - cell.avatar?.image = results.image + cell.avatarImageView?.image = results.image } if !(results.tblAvatar?.loaded ?? false), @@ -526,17 +541,97 @@ extension NCActivity { } extension NCActivity: NCShareCommentsCellDelegate { - func openProfileMenu(with tableComment: tableComments?) -> UIMenu? { - guard let tableComment = tableComment else { return nil } - return NCContextMenuProfile(userId: tableComment.actorId, session: session, viewController: self).viewMenu() + func showProfile(with tableComment: tableComments?, sender: Any) { + guard let tableComment = tableComment else { + return + } + self.showProfileMenu(userId: tableComment.actorId, session: session, sender: sender) } - func openCommentMenu(with tableComments: tableComments?) -> UIMenu? { - guard let tableComments, let metadata else { return nil } - return NCContextMenuComment( - tableComments: tableComments, - metadata: metadata, - viewController: self - ).viewMenu() + func tapMenu(with tableComments: tableComments?, sender: Any) { + toggleMenu(with: tableComments, sender: sender) + } + + func toggleMenu(with tableComments: tableComments?, sender: Any) { + var actions = [NCMenuAction]() + + actions.append( + NCMenuAction( + title: NSLocalizedString("_edit_comment_", comment: ""), + icon: utility.loadImage(named: "pencil", colors: [NCBrandColor.shared.iconImageColor]), + sender: sender, + action: { _ in + guard let metadata = self.metadata, + let tableComments = tableComments else { + return + } + + let alert = UIAlertController(title: NSLocalizedString("_edit_comment_", comment: ""), message: nil, preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel, handler: nil)) + + alert.addTextField(configurationHandler: { textField in + textField.placeholder = NSLocalizedString("_new_comment_", comment: "") + }) + + alert.addAction(UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default, handler: { _ in + guard let message = alert.textFields?.first?.text, !message.isEmpty else { return } + + NextcloudKit.shared.updateComments(fileId: metadata.fileId, messageId: tableComments.messageId, message: message, account: metadata.account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account, + path: metadata.fileId, + name: "updateComments") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } completion: { _, _, error in + if error == .success { + self.loadComments() + } else { + Task {@MainActor in + await showErrorBanner(windowScene: self.windowScene, + text: error.errorDescription, + errorCode: error.errorCode) + } + } + } + })) + + self.present(alert, animated: true) + } + ) + ) + + actions.append( + NCMenuAction( + title: NSLocalizedString("_delete_comment_", comment: ""), + destructive: true, + icon: utility.loadImage(named: "trash", colors: [.red]), + sender: sender, + action: { _ in + guard let metadata = self.metadata, let tableComments = tableComments else { return } + + NextcloudKit.shared.deleteComments(fileId: metadata.fileId, messageId: tableComments.messageId, account: metadata.account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account, + path: metadata.fileId, + name: "deleteComments") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } completion: { _, _, error in + if error == .success { + self.loadComments() + } else { + Task { + await showErrorBanner(windowScene: self.windowScene, + text: error.errorDescription, + errorCode: error.errorCode) + } + } + } + } + ) + ) + + presentMenu(with: actions, sender: sender) } } diff --git a/iOSClient/Extensions/DateFormatter+Extension.swift b/iOSClient/Extensions/DateFormatter+Extension.swift index dceb24e7e7..8f14117050 100644 --- a/iOSClient/Extensions/DateFormatter+Extension.swift +++ b/iOSClient/Extensions/DateFormatter+Extension.swift @@ -31,4 +31,66 @@ extension DateFormatter { dateFormatter.dateStyle = .medium return dateFormatter }() + +// static func formattedExpiryDate(_ date: Date) -> String { +// // Get the app language +// let appLanguage = Locale.preferredLanguages.first?.prefix(2) ?? "en" +// let locale = Locale(identifier: "\(appLanguage)_\(appLanguage.uppercased())") +// +// // Extract components +// let calendar = Calendar.current +// let day = calendar.component(.day, from: date) +// +// // Get month name abbreviation in the correct locale +// let monthFormatter = DateFormatter() +// monthFormatter.locale = locale +// monthFormatter.dateFormat = "MMM" // abbreviated month +// var month = monthFormatter.string(from: date) +// +// // Capitalize first letter (German months are lowercase normally) +// month = month.prefix(1).uppercased() + month.dropFirst() +// +// // Remove trailing period if present (common in German abbreviations) +// month = month.replacingOccurrences(of: ".", with: "") +// +// // Get year +// let year = calendar.component(.year, from: date) +// +// return String(format: "%02d.%@.%d", day, month, year) +// } + + /// Formats a given Date object into the specific string format "dd. MMM. yyyy" + /// required for the expiry date display (e.g., "01. Dez. 2026"). + static func formattedExpiryDate(_ date: Date) -> String { + + // 1. Determine the correct locale to use based on the app's preferences + let appLanguageCode = Locale.preferredLanguages.first?.prefix(2) ?? "en" + // Use a standard locale identifier for consistency across devices + let localeIdentifier = (appLanguageCode == "de" || appLanguageCode == "en") ? appLanguageCode : "en" + let locale = Locale(identifier: String(localeIdentifier)) + + let calendar = Calendar.current + let day = calendar.component(.day, from: date) + let year = calendar.component(.year, from: date) + + // 2. Get the month abbreviation using the correct locale + let monthFormatter = DateFormatter() + monthFormatter.locale = locale + monthFormatter.dateFormat = "MMM" // e.g., "Dez." (German locale adds a period) or "Dec" (English locale adds no period) + var month = monthFormatter.string(from: date) + + // 3. CRITICAL FIXES: + // A. Remove any trailing period the locale might have added + month = month.replacingOccurrences(of: ".", with: "") + + // B. Capitalize the first letter (German defaults to lowercase 'dez') + month = month.prefix(1).uppercased() + month.dropFirst() + + // 4. Use the String(format:) with correct spacing to guarantee the output structure + // This line guarantees a SINGLE period after the day and a SINGLE period after the month abbreviation. + let formattedDateString = String(format: "%02d. %@. %d", day, month, year) + + return formattedDateString + } + } diff --git a/iOSClient/Extensions/UIView+Extension.swift b/iOSClient/Extensions/UIView+Extension.swift index b7c0564314..ce28c998b5 100644 --- a/iOSClient/Extensions/UIView+Extension.swift +++ b/iOSClient/Extensions/UIView+Extension.swift @@ -25,32 +25,86 @@ import Foundation import UIKit extension UIView { + + // Source + // https://stackoverflow.com/questions/18680028/prevent-screen-capture-in-an-ios-app/67054892#67054892 + // + // private weak var scrollView: UIScrollView! (it's an outlet) + // self.view.preventScreenshot(for: self.scrollView) + // + func preventScreenshot(for view: UIView) { + let textField = UITextField() + textField.isSecureTextEntry = true + textField.isUserInteractionEnabled = false + guard let hiddenView = textField.layer.sublayers?.first?.delegate as? UIView else { + return + } + hiddenView.subviews.forEach { $0.removeFromSuperview() } + hiddenView.translatesAutoresizingMaskIntoConstraints = false + self.addSubview(hiddenView) + hiddenView.fillSuperview() + hiddenView.addSubview(view) + } + + func addBlur(style: UIBlurEffect.Style) { + let blur = UIBlurEffect(style: style) + let blurredEffectView = UIVisualEffectView(effect: blur) + blurredEffectView.frame = self.bounds + blurredEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + blurredEffectView.isUserInteractionEnabled = false + self.addSubview(blurredEffectView) + } + + func insertBlur(style: UIBlurEffect.Style) { + let blur = UIBlurEffect(style: style) + let blurredEffectView = UIVisualEffectView(effect: blur) + blurredEffectView.frame = self.bounds + blurredEffectView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + blurredEffectView.isUserInteractionEnabled = false + self.insertSubview(blurredEffectView, at: 0) + } + func makeCircularBackground(withColor backgroundColor: UIColor) { self.backgroundColor = backgroundColor self.layer.cornerRadius = self.frame.size.width / 2 self.layer.masksToBounds = true } - /// Splits a filename into base name + extension across two labels to prevent - /// Unicode bidi override attacks from visually disguising the real file extension. - func setBidiSafeFilename( - _ filename: String, - isDirectory: Bool, - titleLabel: UILabel?, - extensionLabel: UILabel? - ) { - let nsName = filename as NSString - let ext = nsName.pathExtension - let base = nsName.deletingPathExtension - - if isDirectory || ext.isEmpty || base.isEmpty { - titleLabel?.text = filename - extensionLabel?.text = "" - extensionLabel?.isHidden = true - } else { - titleLabel?.text = base - extensionLabel?.text = "." + ext - extensionLabel?.isHidden = false + var parentTabBarController: UITabBarController? { + var responder: UIResponder? = self + while let nextResponder = responder?.next { + if let tabBarController = nextResponder as? UITabBarController { + return tabBarController + } + responder = nextResponder } + return nil + } + + func addBlur(style: UIBlurEffect.Style, alpha: CGFloat = 1.0) { + let blurEffect = UIBlurEffect(style: style) + let blurView = UIVisualEffectView(effect: blurEffect) + blurView.frame = bounds + blurView.autoresizingMask = [.flexibleWidth, .flexibleHeight] + blurView.alpha = alpha + blurView.layer.masksToBounds = true + insertSubview(blurView, at: 0) + } + + func addBlurBackground(style: UIBlurEffect.Style, alpha: CGFloat = 1) { + let blur = UIBlurEffect(style: style) + let blurView = UIVisualEffectView(effect: blur) + blurView.isUserInteractionEnabled = false + blurView.alpha = alpha + blurView.layer.masksToBounds = true + blurView.translatesAutoresizingMaskIntoConstraints = false + insertSubview(blurView, at: 0) + + NSLayoutConstraint.activate([ + blurView.topAnchor.constraint(equalTo: topAnchor), + blurView.leadingAnchor.constraint(equalTo: leadingAnchor), + blurView.trailingAnchor.constraint(equalTo: trailingAnchor), + blurView.bottomAnchor.constraint(equalTo: bottomAnchor) + ]) } } diff --git a/iOSClient/Menu/NCShare+Menu.swift b/iOSClient/Menu/NCShare+Menu.swift new file mode 100644 index 0000000000..baddf64fbe --- /dev/null +++ b/iOSClient/Menu/NCShare+Menu.swift @@ -0,0 +1,212 @@ +// +// NCShare+Menu.swift +// Nextcloud +// +// Created by Henrik Storch on 16.03.22. +// Copyright © 2022 Henrik Storch. All rights reserved. +// +// Author Henrik Storch +// +// 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 . +// + +import Foundation +import UIKit +import NextcloudKit + +extension NCShare { + func toggleShareMenu(for share: tableShare, sendMail: Bool, folder: Bool, sender: Any) { + let capabilities = NCNetworking.shared.capabilities[self.metadata.account] ?? NKCapabilities.Capabilities() + var actions = [NCMenuAction]() + + if share.shareType == NKShare.ShareType.publicLink.rawValue, canReshare { + actions.append( + NCMenuAction( + title: NSLocalizedString("_share_add_sharelink_", comment: ""), + icon: utility.loadImage(named: "plus", colors: [NCBrandColor.shared.iconImageColor]), + sender: sender, + action: { _ in + self.makeNewLinkShare() + } + ) + ) + } + + actions.append( + NCMenuAction( +// title: NSLocalizedString("_details_", comment: ""), +// icon: utility.loadImage(named: "pencil", colors: [NCBrandColor.shared.iconImageColor]), + title: NSLocalizedString("_advance_permissions_", comment: ""), + icon: utility.loadImage(named: "rename", colors: [NCBrandColor.shared.brandElement]), + accessibilityIdentifier: "shareMenu/details", + sender: sender, + action: { _ in + guard + let advancePermission = UIStoryboard(name: "NCShare", bundle: nil).instantiateViewController(withIdentifier: "NCShareAdvancePermission") as? NCShareAdvancePermission, + let navigationController = self.navigationController, !share.isInvalidated else { return } + advancePermission.networking = self.networking + advancePermission.share = tableShare(value: share) + advancePermission.oldTableShare = tableShare(value: share) + advancePermission.metadata = self.metadata + + if let downloadLimit = try? self.database.getDownloadLimit(byAccount: self.metadata.account, shareToken: share.token) { + advancePermission.downloadLimit = .limited(limit: downloadLimit.limit, count: downloadLimit.count) + } + + navigationController.pushViewController(advancePermission, animated: true) + } + ) + ) + + if sendMail { + actions.append( + NCMenuAction( + title: NSLocalizedString("_send_new_email_", comment: ""), + icon: utility.loadImage(named: "email", colors: [NCBrandColor.shared.brandElement]), + sender: sender, + action: { menuAction in + let storyboard = UIStoryboard(name: "NCShare", bundle: nil) + guard let viewNewUserComment = storyboard.instantiateViewController(withIdentifier: "NCShareNewUserAddComment") as? NCShareNewUserAddComment else { return } + viewNewUserComment.metadata = self.metadata + viewNewUserComment.share = tableShare(value: share) + viewNewUserComment.networking = self.networking + viewNewUserComment.isFromMenu = true + self.navigationController?.pushViewController(viewNewUserComment, animated: true) + } + ) + ) + } + + actions.append( + NCMenuAction( + title: NSLocalizedString("_share_unshare_", comment: ""), + icon: utility.loadImage(named: "trashIcon", colors: [NCBrandColor.shared.brandElement]), + sender: sender, + action: { _ in + Task { + if share.shareType != NKShare.ShareType.publicLink.rawValue, let metadata = self.metadata, metadata.e2eEncrypted && NCGlobal.shared.isE2eeVersion2(capabilities.e2EEApiVersion) { + if await NCNetworkingE2EE().isInUpload(account: metadata.account, serverUrl: metadata.serverUrlFileName) { + let error = NKError(errorCode: NCGlobal.shared.errorE2EEUploadInProgress, errorDescription: NSLocalizedString("_e2e_in_upload_", comment: "")) + return NCContentPresenter().showInfo(error: error) + } + let error = await NCNetworkingE2EE().uploadMetadata(serverUrl: metadata.serverUrlFileName, addUserId: nil, removeUserId: share.shareWith, account: metadata.account) + if error != .success { + return NCContentPresenter().showError(error: error) + } + } + self.networking?.unShare(idShare: share.idShare) + } + } + ) + ) + + self.presentMenu(with: actions, sender: sender) + } + + func toggleQuickPermissionsMenu(isDirectory: Bool, share: tableShare, sender: Any?) { + var actions = [NCMenuAction]() + + actions.append(contentsOf: + [NCMenuAction( + title: NSLocalizedString("_share_read_only_", comment: ""), + icon: utility.loadImage(named: "eye", colors: [NCBrandColor.shared.iconImageColor]), + selected: share.permissions == (NKShare.Permission.read.rawValue + NKShare.Permission.share.rawValue) || share.permissions == NKShare.Permission.read.rawValue, + on: false, + sender: sender, + action: { _ in + let permissions = NCSharePermissions.getPermissionValue(canCreate: false, canEdit: false, canDelete: false, canShare: false, isDirectory: isDirectory) + self.updateSharePermissions(share: share, permissions: permissions) + } + ), + NCMenuAction( + title: NSLocalizedString("_share_editing_", comment: ""), + icon: utility.loadImage(named: "pencil", colors: [NCBrandColor.shared.iconImageColor]), + selected: hasUploadPermission(tableShare: share), + on: false, + sender: sender, + action: { _ in + let permissions = NCSharePermissions.getPermissionValue(canCreate: true, canEdit: true, canDelete: true, canShare: true, isDirectory: isDirectory) + self.updateSharePermissions(share: share, permissions: permissions) + } + ), +// NCMenuAction( +// title: NSLocalizedString("_custom_permissions_", comment: ""), +// icon: utility.loadImage(named: "ellipsis", colors: [NCBrandColor.shared.iconImageColor]), +// sender: sender, +// action: { _ in +// guard +// let advancePermission = UIStoryboard(name: "NCShare", bundle: nil).instantiateViewController(withIdentifier: "NCShareAdvancePermission") as? NCShareAdvancePermission, +// let navigationController = self.navigationController, !share.isInvalidated else { return } +// advancePermission.networking = self.networking +// advancePermission.share = tableShare(value: share) +// advancePermission.oldTableShare = tableShare(value: share) +// advancePermission.metadata = self.metadata +// +// if let downloadLimit = try? self.database.getDownloadLimit(byAccount: self.metadata.account, shareToken: share.token) { +// advancePermission.downloadLimit = .limited(limit: downloadLimit.limit, count: downloadLimit.count) +// } +// +// navigationController.pushViewController(advancePermission, animated: true) +// } +// ) + ] + ) + + if isDirectory && (share.shareType == NKShare.ShareType.publicLink.rawValue /* public link */ || share.shareType == NKShare.ShareType.email.rawValue) { + actions.insert(NCMenuAction( + title: NSLocalizedString("_share_file_drop_", comment: ""), + icon: utility.loadImage(named: "arrow.up.document", colors: [NCBrandColor.shared.iconImageColor]), + selected: share.permissions == NKShare.Permission.create.rawValue, + on: false, + sender: sender, + action: { _ in + let permissions = NCSharePermissions.getPermissionValue(canRead: false, canCreate: true, canEdit: false, canDelete: false, canShare: false, isDirectory: isDirectory) + self.updateSharePermissions(share: share, permissions: permissions) + } + ), at: 2) + } + + self.presentMenu(with: actions, sender: sender) + } + + fileprivate func hasUploadPermission(tableShare: tableShare) -> Bool { + let uploadPermissions = [ + NCSharePermissions.permissionMaxFileShare, + NCSharePermissions.permissionMaxFolderShare, + NCSharePermissions.permissionDefaultFileRemoteShareNoSupportShareOption, + NCSharePermissions.permissionDefaultFolderRemoteShareNoSupportShareOption] + return uploadPermissions.contains(tableShare.permissions) + } + + func updateSharePermissions(share: tableShare, permissions: Int) { + let updatedShare = tableShare(value: share) + updatedShare.permissions = permissions + + var downloadLimit: DownloadLimitViewModel = .unlimited + + do { + if let model = try database.getDownloadLimit(byAccount: metadata.account, shareToken: updatedShare.token) { + downloadLimit = .limited(limit: model.limit, count: model.count) + } + if let model = try database.getDownloadLimit(byAccount: metadata.account, shareToken: updatedShare.token) { + downloadLimit = .limited(limit: model.limit, count: model.count) + } + } catch { + nkLog(error: "Failed to get download limit from database!") + return + } + + networking?.updateShare(updatedShare, downloadLimit: downloadLimit) + } +} diff --git a/iOSClient/NCGlobal.swift b/iOSClient/NCGlobal.swift index 813c75d117..e99860e7c9 100644 --- a/iOSClient/NCGlobal.swift +++ b/iOSClient/NCGlobal.swift @@ -40,9 +40,29 @@ final class NCGlobal: Sendable { let twoFactorNotificatioName = "twofactor_nextcloud_notification" let termsOfServiceName = "terms_of_service" + // Nextcloud version + // + let nextcloudVersion18: Int = 18 + let nextcloudVersion20: Int = 20 + let nextcloudVersion23: Int = 23 + let nextcloudVersion24: Int = 24 + let nextcloudVersion25: Int = 25 + let nextcloudVersion26: Int = 26 + let nextcloudVersion27: Int = 27 + let nextcloudVersion28: Int = 28 + let nextcloudVersion30: Int = 30 + let nextcloudVersion31: Int = 31 + let nextcloudVersionFuture: Int = 99999 + + + // Nextcloud unsupported + // + let nextcloud_unsupported_version: Int = 20 + // Intro selector // let introLogin: Int = 0 + let introSignup: Int = 1 let introSignUpWithProvider: Int = 1 // Avatar @@ -77,6 +97,14 @@ final class NCGlobal: Sendable { // E2EE // let e2eePassphraseTest = "more over television factory tendency independence international intellectual impress interest sentence pony" + let e2eeCompatibleVersions = ["1.1", "1.2", "2.0", "2.1"] + + func isE2eeVersion2(_ version: String) -> Bool { + if version == "2.0" || version == "2.1" { + return true + } + return false + } // CHUNK let chunkSizeMBCellular = 10000000 @@ -103,6 +131,7 @@ final class NCGlobal: Sendable { let layoutViewOffline = "LayoutOffline" let layoutViewFavorite = "LayoutFavorite" let layoutViewFiles = "LayoutFiles" + let layoutViewTransfers = "LayoutTransfers" let layoutViewRecent = "LayoutRecent" let layoutViewShares = "LayoutShares" let layoutViewShareExtension = "LayoutShareExtension" @@ -113,7 +142,34 @@ final class NCGlobal: Sendable { // let buttonMoreMore = "more" let buttonMoreLock = "moreLock" - + let buttonMoreStop = "stop" + + // Standard height sections header/footer + // + let heightButtonsView: CGFloat = 50 + let heightHeaderTransfer: CGFloat = 50 + let heightSection: CGFloat = 30 + let heightFooter: CGFloat = 1 + let heightFooterButton: CGFloat = 30 + let endHeightFooter: CGFloat = 85 + + // Text - OnlyOffice - Collabora - QuickLook + // + let editorText = "text" + let editorOnlyoffice = "onlyoffice" + let editorCollabora = "collabora" + let editorQuickLook = "quicklook" + + let onlyofficeDocx = "onlyoffice_docx" + let onlyofficeXlsx = "onlyoffice_xlsx" + let onlyofficePptx = "onlyoffice_pptx" + + // Template + // + let templateDocument = "document" + let templateSpreadsheet = "spreadsheet" + let templatePresentation = "presentation" + // Rich Workspace // let fileNameRichWorkspace = "Readme.md" @@ -125,6 +181,7 @@ final class NCGlobal: Sendable { // Error // + let errorRequestExplicityCancelled: Int = 15 let errorNotModified: Int = 304 let errorBadRequest: Int = 400 let errorUnauthorized401: Int = 401 @@ -146,6 +203,7 @@ final class NCGlobal: Sendable { let errorInternalError: Int = -99999 let errorFileNotSaved: Int = -99998 let errorOfflineNotAllowed: Int = -99997 + let errorOffline: Int = -99997 let errorCharactersForbidden: Int = -99996 let errorCreationFile: Int = -99995 let errorReadFile: Int = -99994 @@ -155,8 +213,6 @@ final class NCGlobal: Sendable { let errorIncorrectFileName: Int = -99990 let errorVersionMismatch: Int = -99989 let errorNCSessionNotFound: Int = -99988 - let errorNotPermission: Int = -99987 - // E2EE let errorE2EENotEnabled: Int = -98000 @@ -198,7 +254,9 @@ final class NCGlobal: Sendable { let selectorSaveAsScan = "saveAsScan" let selectorOpenDetail = "openDetail" let selectorSynchronizationOffline = "synchronizationOffline" - + let selectorPrint = "print" + let selectorDeleteFile = "deleteFile" + // Metadata : Status // // 0 normal @@ -231,8 +289,7 @@ final class NCGlobal: Sendable { let metadataStatusTransfers = [-2, -3, 2, 3, 10, 11, 12, 13, 14, 15] let metadatasStatusInWaiting = [-1, 1, 10, 11, 12, 13, 14, 15] - let metadatasStatusInWaitingDownloadUpload = [-1, 1] - let metadatasStatusDownloadingUploading = [-2, 2] + let metadatasStatusInProgress = [-2, 2] // Auto upload subfolder granularity // @@ -252,6 +309,16 @@ final class NCGlobal: Sendable { let notificationCenterCheckUserDelaultErrorDone = "checkUserDelaultErrorDone" // userInfo: account, controller let notificationCenterServerDidUpdate = "serverDidUpdate" // userInfo: account let notificationCenterNetworkReachability = "networkReachability" + let notificationCenterDidCreateShareLink = "didCreateShareLink" + + let notificationCenterDeleteFile = "deleteFile" // userInfo: [ocId], error + let notificationCenterCopyMoveFile = "copyMoveFile" // userInfo: [ocId] serverUrl, account, dragdrop, type (copy, move) + let notificationCenterMoveFile = "moveFile" // userInfo: [ocId], [indexPath], error + let notificationCenterCopyFile = "copyFile" // userInfo: [ocId], [indexPath], error + let notificationCenterRenameFile = "renameFile" // userInfo: serverUrl, account, error + let notificationCenterFavoriteFile = "favoriteFile" // userInfo: ocId, serverUrl + let notificationCenterFileExists = "fileExists" // userInfo: ocId, fileExists + let notificationCenterReloadDataSource = "reloadDataSource" // userInfo: serverUrl?, clearDataSource let notificationCenterMenuSearchTextPDF = "menuSearchTextPDF" let notificationCenterMenuGotToPageInPDF = "menuGotToPageInPDF" @@ -259,6 +326,7 @@ final class NCGlobal: Sendable { let notificationCenterOpenMediaDetail = "openMediaDetail" // userInfo: ocId let notificationCenterDismissScanDocument = "dismissScanDocument" + let notificationCenterDismissUploadAssets = "dismissUploadAssets" let notificationCenterEnableSwipeGesture = "enableSwipeGesture" let notificationCenterDisableSwipeGesture = "disableSwipeGesture" @@ -286,6 +354,7 @@ final class NCGlobal: Sendable { let networkingStatusUploaded = "statusUploaded" let networkingStatusReloadAvatar = "statusReloadAvatar" + let notificationCenterUpdateIcons = "updateIcons" // TIP // @@ -368,7 +437,21 @@ final class NCGlobal: Sendable { // let taskDescriptionRetrievesProperties = "retrievesProperties" let taskDescriptionSynchronization = "synchronization" - + let taskDescriptionDeleteFileOrFolder = "deleteFileOrFolder" + + // MoEngage App Version + // + let moEngageAppVersion = 854 + + // Filename Mask and Type + // + let keyFileNameMask = "fileNameMask" + let keyFileNameType = "fileNameType" + let keyFileNameAutoUploadMask = "fileNameAutoUploadMask" + let keyFileNameAutoUploadType = "fileNameAutoUploadType" + let keyFileNameOriginal = "fileNameOriginal" + let keyFileNameOriginalAutoUpload = "fileNameOriginalAutoUpload" + // LOG TAG // let logTagTask = "BGT" @@ -387,6 +470,10 @@ final class NCGlobal: Sendable { // let udMigrationMultiDomains = "migrationMultiDomains" let udLastVersion = "lastVersion" + + // Album + // + let selectedTabIndexAlbum: Int = 3 } /** diff --git a/iOSClient/Networking/NCDownloadAction.swift b/iOSClient/Networking/NCDownloadAction.swift new file mode 100644 index 0000000000..f2acb8aa99 --- /dev/null +++ b/iOSClient/Networking/NCDownloadAction.swift @@ -0,0 +1,741 @@ +// SPDX-FileCopyrightText: Nextcloud GmbH +// SPDX-FileCopyrightText: 2020 Marino Faggiana +// SPDX-License-Identifier: GPL-3.0-or-later + +import UIKit +import NextcloudKit +import Queuer +import SVGKit +import Photos +import Alamofire + +class NCDownloadAction: NSObject, UIDocumentInteractionControllerDelegate, NCSelectDelegate, NCTransferDelegate { + static let shared = NCDownloadAction() + + var viewerQuickLook: NCViewerQuickLook? + var documentController: UIDocumentInteractionController? + let utilityFileSystem = NCUtilityFileSystem() + let utility = NCUtility() + let global = NCGlobal.shared + var sceneIdentifier: String = "" + + override private init() { } + + func setup(sceneIdentifier: String) { + self.sceneIdentifier = sceneIdentifier + + Task { + await NCNetworking.shared.transferDispatcher.addDelegate(self) + } + } + + // MARK: - Download + + func transferChange(status: String, metadata: tableMetadata, error: NKError) { + DispatchQueue.main.async { + switch status { + /// DOWNLOADED + case self.global.networkingStatusDownloaded: + self.downloadedFile(metadata: metadata, error: error) + default: + break + } + } + } + + func downloadedFile(metadata: tableMetadata, error: NKError) { + guard error == .success else { + return + } + /// Select UIWindowScene active in serverUrl + var controller: NCMainTabBarController? + let windowScenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + if windowScenes.count == 1 { + controller = UIApplication.shared.firstWindow?.rootViewController as? NCMainTabBarController + } else if let sceneIdentifier = metadata.sceneIdentifier, + let tabBarController = SceneManager.shared.getController(sceneIdentifier: sceneIdentifier) { + controller = tabBarController + } else { + for windowScene in windowScenes { + if let rootViewController = windowScene.keyWindow?.rootViewController as? NCMainTabBarController, + rootViewController.currentServerUrl() == metadata.serverUrl { + controller = rootViewController + break + } + } + } + guard let controller else { return } + + switch metadata.sessionSelector { + case NCGlobal.shared.selectorLoadFileQuickLook: + + let fileNamePath = self.utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase) + let fileNameTemp = NSTemporaryDirectory() + metadata.fileNameView + let viewerQuickLook = NCViewerQuickLook(with: URL(fileURLWithPath: fileNameTemp), isEditingEnabled: true, metadata: metadata) + if let image = UIImage(contentsOfFile: fileNamePath) { + if let data = image.jpegData(compressionQuality: 1) { + do { + try data.write(to: URL(fileURLWithPath: fileNameTemp)) + } catch { + return + } + } + let navigationController = UINavigationController(rootViewController: viewerQuickLook) + navigationController.modalPresentationStyle = .fullScreen + controller.present(navigationController, animated: true) + } else { + self.utilityFileSystem.copyFile(atPath: fileNamePath, toPath: fileNameTemp) + controller.present(viewerQuickLook, animated: true) + } + + case NCGlobal.shared.selectorLoadFileView: + guard !isAppInBackground + else { + return + } + + if metadata.contentType.contains("opendocument") && !self.utility.isTypeFileRichDocument(metadata) { + self.openActivityViewController(selectedMetadata: [metadata], controller: controller, sender: nil) + } else if metadata.classFile == NKTypeClassFile.compress.rawValue || metadata.classFile == NKTypeClassFile.unknow.rawValue { + self.openActivityViewController(selectedMetadata: [metadata], controller: controller, sender: nil) + } else { + if let viewController = controller.currentViewController() { + let image = self.utility.getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024, userId: metadata.userId, urlBase: metadata.urlBase) + Task { + if let vc = await NCViewer().getViewerController(metadata: metadata, image: image, delegate: viewController) { + await viewController.navigationController?.pushViewController(vc, animated: true) + } + } + } + } + + case NCGlobal.shared.selectorOpenIn: + guard !isAppInBackground + else { + return + } + + self.openActivityViewController(selectedMetadata: [metadata], controller: controller, sender: nil) + + case NCGlobal.shared.selectorPrint: + // waiting close menu + // https://github.com/nextcloud/ios/issues/2278 +// DispatchQueue.main.async { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + self.printDocument(metadata: metadata) + } + + case NCGlobal.shared.selectorSaveAlbum: + + self.saveAlbum(metadata: metadata, controller: controller) + + case NCGlobal.shared.selectorSaveAsScan: + + self.saveAsScan(metadata: metadata, controller: controller) + + case NCGlobal.shared.selectorOpenDetail: + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterOpenMediaDetail, userInfo: ["ocId": metadata.ocId]) + + default: + let applicationHandle = NCApplicationHandle() + applicationHandle.downloadedFile(selector: metadata.sessionSelector, metadata: metadata) + } + } + + // MARK: - + + func setMetadataAvalableOffline(_ metadata: tableMetadata, isOffline: Bool) async { + if isOffline { + if metadata.directory { + await NCManageDatabase.shared.setDirectoryAsync(serverUrl: metadata.serverUrlFileName, offline: false, metadata: metadata) + let predicate = NSPredicate(format: "account == %@ AND serverUrl BEGINSWITH %@ AND sessionSelector == %@ AND status == %d", metadata.account, metadata.serverUrlFileName, NCGlobal.shared.selectorSynchronizationOffline, NCGlobal.shared.metadataStatusWaitDownload) + if let metadatas = await NCManageDatabase.shared.getMetadatasAsync(predicate: predicate) { + await NCManageDatabase.shared.clearMetadatasSessionAsync(metadatas: metadatas) + } + } else { + await NCManageDatabase.shared.setOffLocalFileAsync(ocId: metadata.ocId) + } + } else if metadata.directory { + await NCManageDatabase.shared.cleanTablesOcIds(account: metadata.account, userId: metadata.userId, urlBase: metadata.urlBase) + await NCManageDatabase.shared.setDirectoryAsync(serverUrl: metadata.serverUrlFileName, offline: true, metadata: metadata) + await NCNetworking.shared.synchronization(account: metadata.account, serverUrl: metadata.serverUrlFileName, userId: metadata.userId, urlBase: metadata.urlBase, metadatasInDownload: nil) + } else { + var metadatasSynchronizationOffline: [tableMetadata] = [] + metadatasSynchronizationOffline.append(metadata) + if let metadata = await NCManageDatabase.shared.getMetadataLivePhotoAsync(metadata: metadata) { + metadatasSynchronizationOffline.append(metadata) + } + await NCManageDatabase.shared.addLocalFileAsync(metadata: metadata, offline: true) + for metadata in metadatasSynchronizationOffline { + await NCManageDatabase.shared.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, + session: NCNetworking.shared.sessionDownloadBackground, + selector: NCGlobal.shared.selectorSynchronizationOffline) + AnalyticsHelper.shared.trackEventWithMetadata(eventName: .EVENT__OFFLINE_AVAILABLE, metadata: metadata) + } + } + } + + // MARK: - + + @MainActor + func viewerFile(account: String, fileId: String, viewController: UIViewController) async { + var downloadRequest: DownloadRequest? + let hud = NCHud(viewController.tabBarController?.view) + + if let metadata = await NCManageDatabase.shared.getMetadataFromFileIdAsync(fileId) { + do { + let attr = try FileManager.default.attributesOfItem(atPath: utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase)) + let fileSize = attr[FileAttributeKey.size] as? UInt64 ?? 0 + if fileSize > 0 { + if let vc = await NCViewer().getViewerController(metadata: metadata, delegate: viewController) { + viewController.navigationController?.pushViewController(vc, animated: true) + } + return + } + } catch { + print("Error: \(error)") + } + } + + hud.ringProgress(tapToCancelDetailText: true) { + if let request = downloadRequest { + request.cancel() + } + } + + let resultsFile = await NextcloudKit.shared.getFileFromFileIdAsync(fileId: fileId, account: account) { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, + path: fileId, + name: "getFileFromFileId") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } + hud.dismiss() + guard resultsFile.error == .success, let file = resultsFile.file else { + NCContentPresenter().showError(error: resultsFile.error) + return + } + + let metadata = await NCManageDatabase.shared.convertFileToMetadataAsync(file) + await NCManageDatabase.shared.addMetadataAsync(metadata) + + let fileNameLocalPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase) + + if metadata.isAudioOrVideo { + if let vc = await NCViewer().getViewerController(metadata: metadata, delegate: viewController) { + viewController.navigationController?.pushViewController(vc, animated: true) + } + return + } + + hud.show() + let download = await NextcloudKit.shared.downloadAsync(serverUrlFileName: metadata.serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: account) { request in + downloadRequest = request + } taskHandler: { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: metadata.account, + path: metadata.serverUrlFileName, + name: "download") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + + let ocId = metadata.ocId + await NCManageDatabase.shared.setMetadataSessionAsync(ocId: ocId, + sessionTaskIdentifier: task.taskIdentifier, + status: self.global.metadataStatusDownloading) + } + } progressHandler: { progress in + hud.progress(progress.fractionCompleted) + } + + hud.dismiss() + await NCManageDatabase.shared.setMetadataSessionAsync(ocId: metadata.ocId, + session: "", + sessionTaskIdentifier: 0, + sessionError: "", + status: self.global.metadataStatusNormal, + etag: download.etag) + + if download.nkError == .success { + await NCManageDatabase.shared.addLocalFileAsync(metadata: metadata) + if let vc = await NCViewer().getViewerController(metadata: metadata, delegate: viewController) { + viewController.navigationController?.pushViewController(vc, animated: true) + } + } + } + + // MARK: - + + func openShare(viewController: UIViewController, metadata: tableMetadata, page: NCBrandOptions.NCInfoPagingTab) { + var page = page + let capabilities = NCNetworking.shared.capabilities[metadata.account] ?? NKCapabilities.Capabilities() + + NCActivityIndicator.shared.start(backgroundView: viewController.view) + NCNetworking.shared.readFile(serverUrlFileName: metadata.serverUrlFileName, account: metadata.account) { _, metadata, file, error in + Task { @MainActor in + NCActivityIndicator.shared.stop() + + if let metadata = metadata, error == .success { + let shareNavigationController = UIStoryboard(name: "NCShare", bundle: nil).instantiateInitialViewController() as? UINavigationController + let shareViewController = shareNavigationController?.topViewController as? NCShare + shareViewController?.metadata = metadata + shareNavigationController?.modalPresentationStyle = .formSheet + if let shareNavigationController = shareNavigationController { + viewController.present(shareNavigationController, animated: true, completion: nil) + } + } + } + } + } + + // MARK: - Open Activity [Share] ... + + func openActivityViewController(selectedMetadata: [tableMetadata], controller: NCMainTabBarController?, sender: Any?) { + guard let controller else { return } + let metadatas = selectedMetadata.filter({ !$0.directory }) + var urls: [URL] = [] + var downloadMetadata: [(tableMetadata, URL)] = [] + + for metadata in metadatas { + let fileURL = URL(fileURLWithPath: utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase)) + if utilityFileSystem.fileProviderStorageExists(metadata) { + urls.append(fileURL) + } else { + downloadMetadata.append((metadata, fileURL)) + } + } + + let processor = ParallelWorker(n: 5, titleKey: "_downloading_", totalTasks: downloadMetadata.count, controller: controller) + for (metadata, url) in downloadMetadata { + processor.execute { completion in + Task { + guard let metadata = await NCManageDatabase.shared.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, + session: NCNetworking.shared.sessionDownload, + selector: "", + sceneIdentifier: controller.sceneIdentifier) else { + return completion() + } + + await NCNetworking.shared.downloadFile(metadata: metadata) { _ in + } progressHandler: { progress in + processor.hud.progress(progress.fractionCompleted) + } + + if self.utilityFileSystem.fileProviderStorageExists(metadata) { + urls.append(url) + } + completion() + } + } + } + + processor.completeWork { + guard !urls.isEmpty else { return } + let activityViewController = UIActivityViewController(activityItems: urls, applicationActivities: nil) + + // iPad + if let popover = activityViewController.popoverPresentationController { + if let view = sender as? UIView { + popover.sourceView = view + popover.sourceRect = view.bounds + } else { + popover.sourceView = controller.view + popover.sourceRect = CGRect(x: controller.view.bounds.midX, + y: controller.view.bounds.midY, + width: 0, + height: 0) + popover.permittedArrowDirections = [] + } + } + + controller.present(activityViewController, animated: true) + } + } + + // MARK: - Save as scan + + func saveAsScan(metadata: tableMetadata, controller: NCMainTabBarController?) { + let fileNamePath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase) + let fileNameDestination = utilityFileSystem.createFileName("scan.png", fileDate: Date(), fileType: PHAssetMediaType.image, notUseMask: true) + let fileNamePathDestination = utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryScan, fileName: fileNameDestination) + + utilityFileSystem.copyFile(atPath: fileNamePath, toPath: fileNamePathDestination) + + if let navigationController = UIStoryboard(name: "NCScan", bundle: nil).instantiateInitialViewController() { + navigationController.modalPresentationStyle = UIModalPresentationStyle.pageSheet + let viewController = navigationController.presentedViewController as? NCScan + viewController?.serverUrl = controller?.currentServerUrl() + viewController?.controller = controller + controller?.present(navigationController, animated: true, completion: nil) + } + } + + // MARK: - Save photo + + func saveAlbum(metadata: tableMetadata, controller: NCMainTabBarController?) { + let fileNamePath = utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase) + + NCAskAuthorization().askAuthorizationPhotoLibrary(controller: controller) { hasPermission in + guard hasPermission else { + let error = NKError(errorCode: NCGlobal.shared.errorFileNotSaved, errorDescription: "_access_photo_not_enabled_msg_") + return NCContentPresenter().messageNotification("_access_photo_not_enabled_", error: error, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error) + } + + let errorSave = NKError(errorCode: NCGlobal.shared.errorFileNotSaved, errorDescription: "_file_not_saved_cameraroll_") + + do { + if metadata.isImage { + let data = try Data(contentsOf: URL(fileURLWithPath: fileNamePath)) + PHPhotoLibrary.shared().performChanges({ + let assetRequest = PHAssetCreationRequest.forAsset() + assetRequest.addResource(with: .photo, data: data, options: nil) + }) { success, _ in + if !success { + NCContentPresenter().messageNotification("_save_selected_files_", error: errorSave, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error) + } + } + } else if metadata.isVideo { + PHPhotoLibrary.shared().performChanges({ + PHAssetChangeRequest.creationRequestForAssetFromVideo(atFileURL: URL(fileURLWithPath: fileNamePath)) + }) { success, _ in + if !success { + NCContentPresenter().messageNotification("_save_selected_files_", error: errorSave, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error) + } + } + } else { + NCContentPresenter().messageNotification("_save_selected_files_", error: errorSave, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error) + return + } + } catch { + NCContentPresenter().messageNotification("_save_selected_files_", error: errorSave, delay: NCGlobal.shared.dismissAfterSecond, type: NCContentPresenter.messageType.error) + } + } + } + + // MARK: - Copy & Paste + + func copyPasteboard(pasteboardOcIds: [String], controller: NCMainTabBarController?) { + var items = [[String: Any]]() + let hudView = controller + + // getting file data can take some time and block the main queue + DispatchQueue.global(qos: .userInitiated).async { + var downloadMetadatas: [tableMetadata] = [] + for ocid in pasteboardOcIds { + guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocid) else { continue } + if let pasteboardItem = metadata.toPasteBoardItem() { + items.append(pasteboardItem) + } else { + downloadMetadatas.append(metadata) + } + } + + let processor = ParallelWorker(n: 5, titleKey: "_downloading_", totalTasks: downloadMetadatas.count, controller: controller) + for metadata in downloadMetadatas { + processor.execute { completion in + Task { + guard let metadata = await NCManageDatabase.shared.setMetadataSessionInWaitDownloadAsync(ocId: metadata.ocId, + session: NCNetworking.shared.sessionDownload, + selector: "", + sceneIdentifier: controller?.sceneIdentifier) else { + return completion() + } + + await NCNetworking.shared.downloadFile(metadata: metadata) { _ in + } progressHandler: { progress in + processor.hud.progress(progress.fractionCompleted) + } + + completion() + } + } + } + + processor.completeWork { + items.append(contentsOf: downloadMetadatas.compactMap({ $0.toPasteBoardItem() })) + UIPasteboard.general.setItems(items, options: [:]) + } + } + } + + func pastePasteboard(serverUrl: String, account: String, controller: NCMainTabBarController?) async { + var fractionCompleted: Float = 0 + let processor = ParallelWorker(n: 5, titleKey: "_status_uploading_", totalTasks: nil, controller: controller) + guard let tblAccount = await NCManageDatabase.shared.getTableAccountAsync(account: account) else { + return + } + + func uploadPastePasteboard(fileName: String, serverUrlFileName: String, fileNameLocalPath: String, serverUrl: String, completion: @escaping () -> Void) { + NextcloudKit.shared.upload(serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, account: account) { _ in + } taskHandler: { task in + Task { + let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: account, + path: serverUrlFileName, + name: "upload") + await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) + } + } progressHandler: { progress in + if Float(progress.fractionCompleted) > fractionCompleted || fractionCompleted == 0 { + processor.hud.progress(progress.fractionCompleted) + fractionCompleted = Float(progress.fractionCompleted) + } + } completionHandler: { account, ocId, etag, _, _, _, error in + if error == .success && etag != nil && ocId != nil { + let toPath = self.utilityFileSystem.getDirectoryProviderStorageOcId(ocId!, + fileName: fileName, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase) + self.utilityFileSystem.moveFile(atPath: fileNameLocalPath, toPath: toPath) + NCManageDatabase.shared.addLocalFile(account: account, etag: etag!, ocId: ocId!, fileName: fileName) + Task { + await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in + delegate.transferRequestData(serverUrl: serverUrl) + } + } + } else { + NCContentPresenter().showError(error: error) + } + fractionCompleted = 0 + completion() + } + } + + for (index, items) in UIPasteboard.general.items.enumerated() { + for item in items { + let capabilities = await NKCapabilities.shared.getCapabilities(for: account) + let results = NKFilePropertyResolver().resolve(inUTI: item.key, capabilities: capabilities) + guard let data = UIPasteboard.general.data(forPasteboardType: item.key, inItemSet: IndexSet([index]))?.first else { + continue + } + let fileName = results.name + "_" + NCPreferences().incrementalNumber + "." + results.ext + let serverUrlFileName = utilityFileSystem.createServerUrl(serverUrl: serverUrl, fileName: fileName) + let ocIdUpload = UUID().uuidString + let fileNameLocalPath = utilityFileSystem.getDirectoryProviderStorageOcId(ocIdUpload, + fileName: fileName, + userId: tblAccount.userId, + urlBase: tblAccount.urlBase) + do { try data.write(to: URL(fileURLWithPath: fileNameLocalPath)) } catch { continue } + processor.execute { completion in + uploadPastePasteboard(fileName: fileName, serverUrlFileName: serverUrlFileName, fileNameLocalPath: fileNameLocalPath, serverUrl: serverUrl, completion: completion) + } + } + } + processor.completeWork() + } + + // MARK: - + + func openFileViewInFolder(serverUrl: String, fileNameBlink: String?, fileNameOpen: String?, sceneIdentifier: String) { + guard let controller = SceneManager.shared.getController(sceneIdentifier: sceneIdentifier), + let navigationController = controller.viewControllers?.first as? UINavigationController + else { return } + let session = NCSession.shared.getSession(controller: controller) + var serverUrlPush = self.utilityFileSystem.getHomeServer(session: session) + + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + navigationController.popToRootViewController(animated: false) + controller.selectedIndex = 0 + if serverUrlPush == serverUrl, + let viewController = navigationController.topViewController as? NCFiles { + viewController.blinkCell(fileName: fileNameBlink) + viewController.openFile(fileName: fileNameOpen) + return + } + + let diffDirectory = serverUrl.replacingOccurrences(of: serverUrlPush, with: "") + var subDirs = diffDirectory.split(separator: "/") + + while serverUrlPush != serverUrl, !subDirs.isEmpty { + + guard let dir = subDirs.first else { + return + } + serverUrlPush = self.utilityFileSystem.createServerUrl(serverUrl: serverUrlPush, fileName: String(dir)) + + if let viewController = controller.navigationCollectionViewCommon.first(where: { $0.navigationController == navigationController && $0.serverUrl == serverUrlPush})?.viewController as? NCFiles, viewController.isViewLoaded { + viewController.fileNameBlink = fileNameBlink + viewController.fileNameOpen = fileNameOpen + navigationController.pushViewController(viewController, animated: false) + } else { + if let viewController: NCFiles = UIStoryboard(name: "NCFiles", bundle: nil).instantiateInitialViewController() as? NCFiles { + viewController.serverUrl = serverUrlPush + viewController.titleCurrentFolder = String(dir) + viewController.navigationItem.backButtonTitle = viewController.titleCurrentFolder + + controller.navigationCollectionViewCommon.append(NavigationCollectionViewCommon(serverUrl: serverUrlPush, navigationController: navigationController, viewController: viewController)) + + if serverUrlPush == serverUrl { + viewController.fileNameBlink = fileNameBlink + viewController.fileNameOpen = fileNameOpen + } + navigationController.pushViewController(viewController, animated: false) + } + } + subDirs.remove(at: 0) + } + } + } + + // MARK: - NCSelect + Delegate + + func dismissSelect(serverUrl: String?, metadata: tableMetadata?, type: String, items: [Any], overwrite: Bool, copy: Bool, move: Bool) {//, session: NCSession.Session) { + if let destination = serverUrl, !items.isEmpty { + if copy { + for case let metadata as tableMetadata in items { + if metadata.status != NCGlobal.shared.metadataStatusNormal, metadata.status != NCGlobal.shared.metadataStatusWaitCopy { + continue + } + + NCNetworking.shared.copyMetadata(metadata, destination: destination, overwrite: overwrite) + } + + } else if move { + for case let metadata as tableMetadata in items { + if metadata.status != NCGlobal.shared.metadataStatusNormal, metadata.status != NCGlobal.shared.metadataStatusWaitMove { + continue + } + + NCNetworking.shared.moveMetadata(metadata, destination: destination, overwrite: overwrite) + } + } + } + } + + func openSelectView(items: [tableMetadata], controller: NCMainTabBarController?) { + let session = NCSession.shared.getSession(controller: controller) + let navigationController = UIStoryboard(name: "NCSelect", bundle: nil).instantiateInitialViewController() as? UINavigationController + let topViewController = navigationController?.topViewController as? NCSelect + var listViewController = [NCSelect]() + var copyItems: [tableMetadata] = [] + let capabilities = NCNetworking.shared.capabilities[controller?.account ?? ""] ?? NKCapabilities.Capabilities() + + for item in items { + if let fileNameError = FileNameValidator.checkFileName(item.fileNameView, account: controller?.account, capabilities: capabilities) { + controller?.present(UIAlertController.warning(message: "\(fileNameError.errorDescription) \(NSLocalizedString("_please_rename_file_", comment: ""))"), animated: true) + return + } + copyItems.append(item) + } + + let home = utilityFileSystem.getHomeServer(session: session) + 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 { + // If not in the topmost directory, create a new view controller and set correct title. + // If in the topmost directory, use the default view controller as the base. + var viewController: NCSelect? + if serverUrl != home { + viewController = UIStoryboard(name: "NCSelect", bundle: nil).instantiateViewController(withIdentifier: "NCSelect.storyboard") as? NCSelect + if viewController == nil { + return + } + viewController!.titleCurrentFolder = (serverUrl as NSString).lastPathComponent + } else { + viewController = topViewController + } + guard let vc = viewController else { return } + + vc.delegate = self + vc.typeOfCommandView = .copyMove + vc.items = copyItems + vc.serverUrl = serverUrl + vc.session = session + + vc.navigationItem.backButtonTitle = vc.titleCurrentFolder + listViewController.insert(vc, at: 0) + + if serverUrl != home { + if let serverDirectoryUp = utilityFileSystem.serverDirectoryUp(serverUrl: serverUrl, home: home) { + serverUrl = serverDirectoryUp + } + } else { + break + } + } + + navigationController?.setViewControllers(listViewController, animated: false) + navigationController?.modalPresentationStyle = .formSheet + + if let navigationController = navigationController { + controller?.present(navigationController, animated: true, completion: nil) + } + } + + // MARK: - Print + + func printDocument(metadata: tableMetadata) { + + let fileNameURL = URL(fileURLWithPath: utilityFileSystem.getDirectoryProviderStorageOcId(metadata.ocId, + fileName: metadata.fileNameView, + userId: metadata.userId, + urlBase: metadata.urlBase)) + let printController = UIPrintInteractionController.shared + let printInfo = UIPrintInfo(dictionary: nil) + + printInfo.jobName = fileNameURL.lastPathComponent + printInfo.outputType = metadata.isImage ? .photo : .general + printController.printInfo = printInfo + printController.showsNumberOfCopies = true + + guard !UIPrintInteractionController.canPrint(fileNameURL) else { + printController.printingItem = fileNameURL + printController.present(animated: true) + return + } + + // can't print without data + guard let data = try? Data(contentsOf: fileNameURL) else { return } + + if let svg = SVGKImage(data: data) { + printController.printingItem = svg.uiImage + printController.present(animated: true) + return + } + + guard let text = String(data: data, encoding: .utf8) else { return } + let formatter = UISimpleTextPrintFormatter(text: text) + formatter.perPageContentInsets.top = 72 + formatter.perPageContentInsets.bottom = 72 + formatter.perPageContentInsets.left = 72 + formatter.perPageContentInsets.right = 72 + printController.printFormatter = formatter + printController.present(animated: true) + } +} + +fileprivate extension tableMetadata { + func toPasteBoardItem() -> [String: Any]? { + // Get Data + let fileUrl = URL(fileURLWithPath: NCUtilityFileSystem().getDirectoryProviderStorageOcId(ocId, + fileName: fileNameView, + userId: userId, + urlBase: urlBase)) + guard NCUtilityFileSystem().fileProviderStorageExists(self), + let data = try? Data(contentsOf: fileUrl) else { return nil } + + // Determine the UTI for the file + guard let fileUTI = UTType(filenameExtension: fileExtension)?.identifier else { return nil } + + // Pasteboard item + return [fileUTI: data] + } +} diff --git a/iOSClient/Scan document/PasswordInputField.swift b/iOSClient/Scan document/PasswordInputField.swift new file mode 100644 index 0000000000..43117f09f3 --- /dev/null +++ b/iOSClient/Scan document/PasswordInputField.swift @@ -0,0 +1,73 @@ +// +// PasswordInputField.swift +// Nextcloud +// +// Created by Sumit on 10/06/21. +// Copyright © 2021 Marino Faggiana. All rights reserved. +// + +import Foundation +import XLForm + +class PasswordInputField: XLFormBaseCell,UITextFieldDelegate { + + @IBOutlet weak var fileNameInputTextField: UITextField! + @IBOutlet weak var separatorBottom: UIView! + + override func awakeFromNib() { + super.awakeFromNib() + // Initialization code + fileNameInputTextField.isSecureTextEntry = true + fileNameInputTextField.delegate = self + separatorBottom.backgroundColor = NCBrandColor.shared.systemGray4 + self.selectionStyle = .none + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + + // Configure the view for the selected state + } + + override func configure() { + super.configure() + // fileNameInputTextField.isEnabled = false + + } + + override func update() { + super.update() + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + + if fileNameInputTextField == textField { + if let rowDescriptor = rowDescriptor, let text = self.fileNameInputTextField.text { + + if (text + " ").isEmpty == false { + rowDescriptor.value = self.fileNameInputTextField.text! + string + } else { + rowDescriptor.value = nil + } + } + } + + self.formViewController().textField(textField, shouldChangeCharactersIn: range, replacementString: string) + + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.formViewController()?.textFieldShouldReturn(fileNameInputTextField) + return true + } + + func textFieldShouldClear(_ textField: UITextField) -> Bool { + self.formViewController()?.textFieldShouldClear(fileNameInputTextField) + return true + } + + override class func formDescriptorCellHeight(for rowDescriptor: XLFormRowDescriptor!) -> CGFloat { + return 45 + } +} diff --git a/iOSClient/Scan document/PasswordInputField.xib b/iOSClient/Scan document/PasswordInputField.xib new file mode 100644 index 0000000000..26914e0760 --- /dev/null +++ b/iOSClient/Scan document/PasswordInputField.xib @@ -0,0 +1,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Share/Advanced/DownloadLimit/DownloadLimitViewModel.swift b/iOSClient/Share/Advanced/DownloadLimit/DownloadLimitViewModel.swift index a949484cae..4d7b7d54f0 100644 --- a/iOSClient/Share/Advanced/DownloadLimit/DownloadLimitViewModel.swift +++ b/iOSClient/Share/Advanced/DownloadLimit/DownloadLimitViewModel.swift @@ -20,3 +20,19 @@ enum DownloadLimitViewModel { /// case limited(limit: Int, count: Int) } + +extension DownloadLimitViewModel { + var limit: Int? { + if case let .limited(limit, _) = self { + return limit + } + return nil + } + + var count: Int? { + if case let .limited(_, count) = self { + return count + } + return nil + } +} diff --git a/iOSClient/Share/Advanced/NCFilePermissionCell.swift b/iOSClient/Share/Advanced/NCFilePermissionCell.swift new file mode 100644 index 0000000000..58eb66bdaa --- /dev/null +++ b/iOSClient/Share/Advanced/NCFilePermissionCell.swift @@ -0,0 +1,93 @@ +// +// NCFilePermissionCell.swift +// Nextcloud +// +// Created by T-systems on 17/08/21. +// Copyright © 2021 Marino Faggiana. All rights reserved. +// + +import UIKit +import XLForm + +class NCFilePermissionCell: XLFormButtonCell { + + @IBOutlet weak var seperator: UIView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var imageCheck: UIImageView! + @IBOutlet weak var seperatorBelow: UIView! + @IBOutlet weak var seperatorBelowFull: UIView! + @IBOutlet weak var titleLabelBottom: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + self.selectionStyle = .none + self.backgroundColor = NCBrandColor.shared.secondarySystemGroupedBackground + self.titleLabel.textColor = NCBrandColor.shared.label + NotificationCenter.default.addObserver(self, selector: #selector(changeTheming), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeTheming), object: nil) + } + + @objc func changeTheming() { + self.backgroundColor = NCBrandColor.shared.secondarySystemGroupedBackground + self.titleLabel.textColor = NCBrandColor.shared.label + self.titleLabelBottom.textColor = NCBrandColor.shared.iconImageColor + self.titleLabelBottom.textColor = NCBrandColor.shared.iconColor + } + + override func configure() { + super.configure() + } + + + override func update() { + super.update() + self.selectionStyle = .none + if rowDescriptor.tag == "NCFilePermissionCellSharing" || rowDescriptor.tag == "NCFilePermissionCellAdvanceTxt" { + self.seperator.isHidden = true + self.seperatorBelowFull.isHidden = true + self.seperatorBelow.isHidden = true + self.titleLabel.font = UIFont.boldSystemFont(ofSize: 17) + self.titleLabelBottom.font = UIFont.boldSystemFont(ofSize: 17) + } + if rowDescriptor.tag == "kNMCFilePermissionCellEditing" { + self.seperator.isHidden = true +// self.seperatorBelowFull.isHidden = true + } + + if rowDescriptor.tag == "NCFilePermissionCellFileDrop" { + self.seperator.isHidden = true + self.seperatorBelow.isHidden = false + self.seperatorBelowFull.isHidden = true + } + + if rowDescriptor.tag == "kNMCFilePermissionEditCellEditingCanShare" { + self.seperator.isHidden = true + self.seperatorBelowFull.isHidden = false + } + + if rowDescriptor.tag == "kNMCFilePermissionCellEditingMsg" { + self.seperator.isHidden = true + self.seperatorBelow.isHidden = true + self.seperatorBelowFull.isHidden = false + } + + if rowDescriptor.tag == "kNMCFilePermissionCellFiledropMessage" { + self.seperator.isHidden = true + self.seperatorBelow.isHidden = true + self.seperatorBelowFull.isHidden = false + self.imageCheck.isHidden = true + } + } + + @objc func switchChanged(mySwitch: UISwitch) { + self.rowDescriptor.value = mySwitch.isOn + } + + override func formDescriptorCellDidSelected(withForm controller: XLFormViewController!) { + self.selectionStyle = .none + } + + override class func formDescriptorCellHeight(for rowDescriptor: XLFormRowDescriptor!) -> CGFloat { + return 44.0 + } + +} diff --git a/iOSClient/Share/Advanced/NCFilePermissionCell.xib b/iOSClient/Share/Advanced/NCFilePermissionCell.xib new file mode 100644 index 0000000000..e40c22e14d --- /dev/null +++ b/iOSClient/Share/Advanced/NCFilePermissionCell.xib @@ -0,0 +1,95 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Share/Advanced/NCFilePermissionEditCell.swift b/iOSClient/Share/Advanced/NCFilePermissionEditCell.swift new file mode 100644 index 0000000000..4bca782e80 --- /dev/null +++ b/iOSClient/Share/Advanced/NCFilePermissionEditCell.swift @@ -0,0 +1,180 @@ +// +// NCFilePermissionEditCell.swift +// Nextcloud +// +// Created by T-systems on 10/08/21. +// Copyright © 2021 Marino Faggiana. All rights reserved. +// + +import UIKit +import XLForm + +class NCFilePermissionEditCell: XLFormBaseCell, UITextFieldDelegate { + + @IBOutlet weak var seperator: UIView! + @IBOutlet weak var seperatorMiddle: UIView! + @IBOutlet weak var seperatorBottom: UIView! + @IBOutlet weak var titleLabel: UILabel! + @IBOutlet weak var switchControl: UISwitch! + @IBOutlet weak var cellTextField: UITextField! + @IBOutlet weak var buttonLinkLabel: UIButton! + let datePicker = UIDatePicker() + var expirationDateText: String! + var expirationDate: NSDate! + + override func awakeFromNib() { + super.awakeFromNib() + self.cellTextField.delegate = self + self.cellTextField.isEnabled = false + self.selectionStyle = .none + switchControl.addTarget(self, action: #selector(switchChanged), for: UIControl.Event.valueChanged) + self.backgroundColor = NCBrandColor.shared.secondarySystemGroupedBackground + self.titleLabel.textColor = NCBrandColor.shared.label + self.cellTextField.attributedPlaceholder = NSAttributedString(string: "", + attributes: [NSAttributedString.Key.foregroundColor: NCBrandColor.shared.fileFolderName]) + self.cellTextField.textColor = NCBrandColor.shared.singleTitleColorButton + NotificationCenter.default.addObserver(self, selector: #selector(changeTheming), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterChangeTheming), object: nil) + } + + @objc func changeTheming() { + self.backgroundColor = NCBrandColor.shared.secondarySystemGroupedBackground + self.titleLabel.textColor = NCBrandColor.shared.iconImageColor + } + + override func configure() { + super.configure() + } + + override func update() { + super.update() + + if rowDescriptor.tag == "kNMCFilePermissionCellEditingCanShare" { + self.seperatorMiddle.isHidden = true + self.seperatorBottom.isHidden = true + self.cellTextField.isHidden = true + } + if rowDescriptor.tag == "kNMCFilePermissionEditCellLinkLabel" { + self.switchControl.isHidden = true + self.cellTextField.isEnabled = true + self.seperatorBottom.isHidden = true + } + if rowDescriptor.tag == "kNMCFilePermissionEditCellLinkLabel" { + self.switchControl.isHidden = true + } + + if rowDescriptor.tag == "kNMCFilePermissionEditCellExpiration" { + self.seperator.isHidden = true + setDatePicker(sender: self.cellTextField) + } + + if rowDescriptor.tag == "kNMCFilePermissionEditPasswordCellWithText" { + self.seperatorMiddle.isHidden = true + self.seperator.isHidden = true + } + + if rowDescriptor.tag == "kNMCFilePermissionEditCellHideDownload" { + self.seperator.isHidden = true + self.seperatorMiddle.isHidden = true + } + + if rowDescriptor.tag == "kNMCFilePermissionEditCellEditingCanShare" { + self.seperator.isHidden = true + self.seperatorBottom.isHidden = true + } + } + + @objc func switchChanged(mySwitch: UISwitch) { + let isOn = mySwitch.isOn + if isOn { + //on + self.rowDescriptor.value = isOn + self.cellTextField.isEnabled = true + cellTextField.delegate = self + } else { + self.rowDescriptor.value = isOn + self.cellTextField.isEnabled = false + if rowDescriptor.tag == "kNMCFilePermissionEditCellExpiration" || rowDescriptor.tag == "kNMCFilePermissionEditCellPassword" { + self.cellTextField.text = "" + } + } + if rowDescriptor.tag == "kNMCFilePermissionEditPasswordCellWithText" { + seperatorBottom.isHidden = isOn + seperatorMiddle.isHidden = !isOn + } + if rowDescriptor.tag == "kNMCFilePermissionEditCellExpiration" { + seperatorBottom.isHidden = isOn + seperatorMiddle.isHidden = !isOn + } + } + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if self.cellTextField == textField { + if let rowDescriptor = rowDescriptor, let text = self.cellTextField.text { + + if (text + " ").isEmpty == false { + rowDescriptor.value = self.cellTextField.text! + string + } else { + rowDescriptor.value = nil + } + } + } + + self.formViewController().textField(textField, shouldChangeCharactersIn: range, replacementString: string) + return true + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.formViewController()?.textFieldShouldReturn(textField) + return true + } + + func textFieldShouldClear(_ textField: UITextField) -> Bool { + self.formViewController()?.textFieldShouldClear(textField) + return true + } + + override class func formDescriptorCellHeight(for rowDescriptor: XLFormRowDescriptor!) -> CGFloat { + return 30 + } + + override func formDescriptorCellDidSelected(withForm controller: XLFormViewController!) { + self.selectionStyle = .none + } + + func setDatePicker(sender: UITextField) { + //Format Date + datePicker.datePickerMode = .date + datePicker.minimumDate = Date() + //ToolBar + let toolbar = UIToolbar(); + toolbar.sizeToFit() + let doneButton = UIBarButtonItem(title: NSLocalizedString("_done_", comment: ""), style: .plain, target: self, action: #selector(doneDatePicker)); + let spaceButton = UIBarButtonItem(barButtonSystemItem: UIBarButtonItem.SystemItem.flexibleSpace, target: nil, action: nil) + let cancelButton = UIBarButtonItem(title: NSLocalizedString("_cancel_", comment: ""), style: .plain, target: self, action: #selector(cancelDatePicker)); + + toolbar.setItems([doneButton,spaceButton,cancelButton], animated: false) + + sender.inputAccessoryView = toolbar + sender.inputView = datePicker + } + + @objc func doneDatePicker() { +// let dateFormatter = DateFormatter() +// dateFormatter.formatterBehavior = .behavior10_4 +// dateFormatter.dateStyle = .medium +// self.expirationDateText = dateFormatter.string(from: datePicker.date as Date) +// dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss" + + let expiryDateString = DateFormatter.formattedExpiryDate(datePicker.date) + + self.expirationDateText = expiryDateString + self.expirationDate = datePicker.date as NSDate + self.cellTextField.text = self.expirationDateText + self.rowDescriptor.value = self.expirationDate + self.cellTextField.endEditing(true) + } + + @objc func cancelDatePicker() { + self.cellTextField.endEditing(true) + } +} diff --git a/iOSClient/Share/Advanced/NCFilePermissionEditCell.xib b/iOSClient/Share/Advanced/NCFilePermissionEditCell.xib new file mode 100644 index 0000000000..2a7aa3ac63 --- /dev/null +++ b/iOSClient/Share/Advanced/NCFilePermissionEditCell.xib @@ -0,0 +1,108 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Share/Advanced/NCShareAdvancePermission.swift b/iOSClient/Share/Advanced/NCShareAdvancePermission.swift index 890c88a9bd..f36153afda 100644 --- a/iOSClient/Share/Advanced/NCShareAdvancePermission.swift +++ b/iOSClient/Share/Advanced/NCShareAdvancePermission.swift @@ -23,9 +23,10 @@ import UIKit import NextcloudKit +import SVGKit import CloudKit -class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFooterDelegate, NCShareNavigationTitleSetting { +class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFotterDelegate, NCShareNavigationTitleSetting { let database = NCManageDatabase.shared var oldTableShare: tableShare? @@ -60,7 +61,7 @@ class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFooterDeleg var windowScene: UIWindowScene? { SceneManager.shared.getWindowScene(controller: controller) } - + override func viewDidLoad() { super.viewDidLoad() self.shareConfig = NCShareConfig(parentMetadata: metadata, share: share) @@ -113,7 +114,7 @@ class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFooterDeleg guard let headerView = (Bundle.main.loadNibNamed("NCShareHeader", owner: self, options: nil)?.first as? NCShareHeader) else { return } headerView.setupUI(with: metadata) - let container = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 200)) + let container = UIView(frame: CGRect(x: 0, y: 0, width: view.frame.width, height: 220)) container.addSubview(headerView) tableView.tableHeaderView = container headerView.translatesAutoresizingMaskIntoConstraints = false @@ -189,9 +190,27 @@ class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFooterDeleg tableView.reloadData() return } +// let alertController = UIAlertController.password(titleKey: "_share_password_") { password in +// self.share.password = password ?? "" +// tableView.reloadData() +// } let alertController = UIAlertController.password(titleKey: "_share_password_") { password in - self.share.password = password ?? "" - tableView.reloadData() + let newPassword = password ?? "" + + // Enforce identical rules: minimum 6 characters + if newPassword.count >= 6 { + self.share.password = newPassword + tableView.reloadData() + } else if !newPassword.isEmpty { + // Optional: Show an error alert or toast for "too short" +// print(NSLocalizedString("_share_password_must_be_at_least_6_chars", comment: "")) +// NCContentPresenter().showInfo(title: "_share_password_must_be_at_least_6_chars") + Task { + await showErrorBanner(windowScene: self.windowScene, + text: "_share_password_must_be_at_least_6_chars", + errorCode: 0) + } + } } self.present(alertController, animated: true) case .note: @@ -200,6 +219,7 @@ class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFooterDeleg viewNewUserComment.metadata = self.metadata viewNewUserComment.share = self.share viewNewUserComment.onDismiss = tableView.reloadData + viewNewUserComment.networking = self.networking self.navigationController?.pushViewController(viewNewUserComment, animated: true) case .label: let alertController = UIAlertController.withTextField(titleKey: "_share_link_name_") { textField in @@ -245,8 +265,16 @@ class NCShareAdvancePermission: UITableViewController, NCShareAdvanceFooterDeleg share.permissions = share.permissions - NKShare.Permission.share.rawValue } +// guard share.permissions > 0 else { +// NCContentPresenter().showInfo(title: "_share_permission_should_not_be_empty_") +// return +// } + if isNewShare { - if share.shareType != NKShare.ShareType.publicLink.rawValue, metadata.e2eEncrypted { + let capabilities = await NKCapabilities.shared.getCapabilities(for: metadata.account) + + if share.shareType != NKShare.ShareType.publicLink.rawValue, metadata.e2eEncrypted, + NCGlobal.shared.isE2eeVersion2(capabilities.e2EEApiVersion) { if await NCNetworkingE2EE().isInUpload(account: metadata.account, serverUrl: metadata.serverUrlFileName) { await showErrorBanner(windowScene: windowScene, diff --git a/iOSClient/Share/Advanced/NCShareAdvancePermissionFooter.swift b/iOSClient/Share/Advanced/NCShareAdvancePermissionFooter.swift index e893f6875f..fe6f6c4e0c 100644 --- a/iOSClient/Share/Advanced/NCShareAdvancePermissionFooter.swift +++ b/iOSClient/Share/Advanced/NCShareAdvancePermissionFooter.swift @@ -23,7 +23,7 @@ import UIKit -protocol NCShareAdvanceFooterDelegate: AnyObject { +protocol NCShareAdvanceFotterDelegate: AnyObject { var isNewShare: Bool { get } func dismissShareAdvanceView(shouldSave: Bool) } @@ -31,14 +31,14 @@ protocol NCShareAdvanceFooterDelegate: AnyObject { class NCShareAdvancePermissionFooter: UIView { @IBOutlet weak var buttonCancel: UIButton! @IBOutlet weak var buttonNext: UIButton! - weak var delegate: NCShareAdvanceFooterDelegate? + weak var delegate: NCShareAdvanceFotterDelegate? - func setupUI(delegate: NCShareAdvanceFooterDelegate?, account: String) { + func setupUI(delegate: NCShareAdvanceFotterDelegate?, account: String) { self.delegate = delegate backgroundColor = .clear buttonCancel.setTitle(NSLocalizedString("_cancel_", comment: ""), for: .normal) - buttonCancel.layer.cornerRadius = 25 + buttonCancel.layer.cornerRadius = 10 buttonCancel.layer.masksToBounds = true buttonCancel.layer.borderWidth = 1 buttonCancel.layer.borderColor = NCBrandColor.shared.textColor2.cgColor @@ -47,7 +47,7 @@ class NCShareAdvancePermissionFooter: UIView { buttonCancel.setTitleColor(NCBrandColor.shared.textColor2, for: .normal) buttonNext.setTitle(NSLocalizedString(delegate?.isNewShare == true ? "_share_" : "_save_", comment: ""), for: .normal) - buttonNext.layer.cornerRadius = 25 + buttonNext.layer.cornerRadius = 10 buttonNext.layer.masksToBounds = true buttonNext.backgroundColor = NCBrandColor.shared.getElement(account: account) buttonNext.addTarget(self, action: #selector(nextClicked(_:)), for: .touchUpInside) diff --git a/iOSClient/Share/Advanced/NCShareAdvancePermissionFooter.xib b/iOSClient/Share/Advanced/NCShareAdvancePermissionFooter.xib index b4ec72ed06..36b09589ef 100644 --- a/iOSClient/Share/Advanced/NCShareAdvancePermissionFooter.xib +++ b/iOSClient/Share/Advanced/NCShareAdvancePermissionFooter.xib @@ -1,67 +1,60 @@ - + - + - - - + + - + + - + - - - - - - - + - + - - - - - - - + + + - - + + - + - - - - - - - - diff --git a/iOSClient/Share/Advanced/NCShareAdvancePermissionHeader.xib b/iOSClient/Share/Advanced/NCShareAdvancePermissionHeader.xib new file mode 100644 index 0000000000..7943c4041b --- /dev/null +++ b/iOSClient/Share/Advanced/NCShareAdvancePermissionHeader.xib @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Share/Advanced/NCShareCells.swift b/iOSClient/Share/Advanced/NCShareCells.swift index e6d3f38e31..9e826d8ece 100644 --- a/iOSClient/Share/Advanced/NCShareCells.swift +++ b/iOSClient/Share/Advanced/NCShareCells.swift @@ -69,7 +69,7 @@ enum NCUserPermission: CaseIterable, NCPermission { case .reshare: NKShare.Permission.share.rawValue case .edit: NKShare.Permission.update.rawValue case .create: NKShare.Permission.create.rawValue - case .delete: NKShare.Permission.delete.rawValue +// case .delete: NKShare.Permission.delete.rawValue } } @@ -83,13 +83,13 @@ enum NCUserPermission: CaseIterable, NCPermission { static func forDirectoryE2EE(account: String) -> [NCPermission] { let capabilities = NCNetworking.shared.capabilities[account] ?? NKCapabilities.Capabilities() - if capabilities.e2EEApiVersion.hasPrefix("2.") { + if NCGlobal.shared.isE2eeVersion2(capabilities.e2EEApiVersion) { return NCUserPermission.allCases } return [] } - case read, reshare, edit, create, delete + case read, reshare, edit, create//, delete static let forDirectory: [NCUserPermission] = NCUserPermission.allCases static let forFile: [NCUserPermission] = [.read, .reshare, .edit] @@ -99,7 +99,7 @@ enum NCUserPermission: CaseIterable, NCPermission { case .reshare: return NSLocalizedString("_share_can_reshare_", comment: "") case .edit: return NSLocalizedString("_share_can_change_", comment: "") case .create: return NSLocalizedString("_share_can_create_", comment: "") - case .delete: return NSLocalizedString("_share_can_delete_", comment: "") +// case .delete: return NSLocalizedString("_share_can_delete_", comment: "") } } } @@ -107,7 +107,7 @@ enum NCUserPermission: CaseIterable, NCPermission { enum NCLinkEmailPermission: CaseIterable, NCPermission { static func forDirectoryE2EE(account: String) -> [any NCPermission] { let capabilities = NCNetworking.shared.capabilities[account] ?? NKCapabilities.Capabilities() - if capabilities.e2EEApiVersion.hasPrefix("2.") { + if NCGlobal.shared.isE2eeVersion2(capabilities.e2EEApiVersion) { return NCUserPermission.allCases } return [] @@ -126,7 +126,7 @@ enum NCLinkEmailPermission: CaseIterable, NCPermission { case .read: NKShare.Permission.read.rawValue case .edit: NKShare.Permission.update.rawValue case .create: NKShare.Permission.create.rawValue - case .delete: NKShare.Permission.delete.rawValue +// case .delete: NKShare.Permission.delete.rawValue } } @@ -143,11 +143,11 @@ enum NCLinkEmailPermission: CaseIterable, NCPermission { case .read: return NSLocalizedString("_share_can_read_", comment: "") case .edit: return NSLocalizedString("_share_can_change_", comment: "") case .create: return NSLocalizedString("_share_can_create_", comment: "") - case .delete: return NSLocalizedString("_share_can_delete_", comment: "") +// case .delete: return NSLocalizedString("_share_can_delete_", comment: "") } } - case edit, read, create, delete + case edit, read, create//, delete static let forDirectory: [NCLinkEmailPermission] = NCLinkEmailPermission.allCases static let forFile: [NCLinkEmailPermission] = [.read, .edit] } @@ -207,7 +207,6 @@ enum NCAdvancedPermission: CaseIterable, NCShareCellConfig { } case label, hideDownload, limitDownload, expirationDate, password, note, downloadAndSync - static let forLink: [NCAdvancedPermission] = [.expirationDate, .hideDownload, .label, .limitDownload, .note, .password] static let forEmail: [NCAdvancedPermission] = [.expirationDate, .note, .downloadAndSync, .password] static let forUser: [NCAdvancedPermission] = [.expirationDate, .note, .downloadAndSync] @@ -229,20 +228,20 @@ struct NCShareConfig { self.permissions = parentMetadata.directory ? (parentMetadata.e2eEncrypted ? type.forDirectoryE2EE(account: parentMetadata.account) : type.forDirectory) : type.forFile switch share.shareType { - case NKShare.ShareType.publicLink.rawValue: - let capabilities = NCNetworking.shared.capabilities[parentMetadata.account] ?? NKCapabilities.Capabilities() - let hasDownloadLimitCapability = capabilities.fileSharingDownloadLimit - - if parentMetadata.isDirectory || hasDownloadLimitCapability == false { - self.advanced = NCAdvancedPermission.forLink.filter { $0 != .limitDownload } - } else { - self.advanced = NCAdvancedPermission.forLink - } - case NKShare.ShareType.email.rawValue: - self.advanced = NCAdvancedPermission.forEmail - default: - self.advanced = NCAdvancedPermission.forUser - } + case NKShare.ShareType.publicLink.rawValue: + let capabilities = NCNetworking.shared.capabilities[parentMetadata.account] ?? NKCapabilities.Capabilities() + let hasDownloadLimitCapability = capabilities.fileSharingDownloadLimit + + if parentMetadata.isDirectory || hasDownloadLimitCapability == false { + self.advanced = NCAdvancedPermission.forLink.filter { $0 != .limitDownload } + } else { + self.advanced = NCAdvancedPermission.forLink + } + case NKShare.ShareType.email.rawValue: + self.advanced = NCAdvancedPermission.forEmail + default: + self.advanced = NCAdvancedPermission.forUser + } } func cellFor(indexPath: IndexPath) -> UITableViewCell? { diff --git a/iOSClient/Share/Advanced/NCShareDateCell.swift b/iOSClient/Share/Advanced/NCShareDateCell.swift index df1c4fe4af..b4048630c5 100644 --- a/iOSClient/Share/Advanced/NCShareDateCell.swift +++ b/iOSClient/Share/Advanced/NCShareDateCell.swift @@ -20,9 +20,18 @@ class NCShareDateCell: UITableViewCell { picker.datePickerMode = .date picker.minimumDate = Calendar.current.date(byAdding: .day, value: 1, to: Date()) picker.preferredDatePickerStyle = .wheels + // Get the app's language (first language in the preferred languages array) + let appLanguage = Locale.preferredLanguages.first?.prefix(2) ?? "en" + + // Set the locale of the picker to the app's language + picker.locale = Locale(identifier: "\(appLanguage)_\(appLanguage.uppercased())") + + // Handle date changes +// picker.addTarget(self, action: #selector(dateChanged(_:)), for: .valueChanged) + picker.action(for: .valueChanged) { datePicker in guard let datePicker = datePicker as? UIDatePicker else { return } - self.detailTextLabel?.text = DateFormatter.shareExpDate.string(from: datePicker.date) + self.detailTextLabel?.text = DateFormatter.formattedExpiryDate(datePicker.date)//DateFormatter.shareExpDate.string(from: datePicker.date) } accessoryView = textField @@ -42,7 +51,7 @@ class NCShareDateCell: UITableViewCell { textField.inputView = picker if let expDate = share.expirationDate { - detailTextLabel?.text = DateFormatter.shareExpDate.string(from: expDate as Date) + detailTextLabel?.text = DateFormatter.formattedExpiryDate(expDate as Date) //DateFormatter.shareExpDate.string(from: expDate as Date) } } diff --git a/iOSClient/Share/Advanced/NCShareHeaderCustomCell.swift b/iOSClient/Share/Advanced/NCShareHeaderCustomCell.swift new file mode 100644 index 0000000000..9b77fc069a --- /dev/null +++ b/iOSClient/Share/Advanced/NCShareHeaderCustomCell.swift @@ -0,0 +1,31 @@ +// +// NCShareHeaderCustomCell.swift +// Nextcloud +// +// Created by T-systems on 11/08/21. +// Copyright © 2021 Marino Faggiana. All rights reserved. +// + +import UIKit +import XLForm + +class NCShareHeaderCustomCell: XLFormBaseCell { + + @IBOutlet weak var titleLabel: UILabel! + + override func awakeFromNib() { + super.awakeFromNib() + self.selectionStyle = .none + self.backgroundColor = .clear + } + + override func configure() { + super.configure() + } + + override func update() { + self.backgroundColor = .clear + self.titleLabel.textColor = NCBrandColor.shared.systemGray + super.update() + } +} diff --git a/iOSClient/Share/Advanced/NCShareHeaderCustomCell.xib b/iOSClient/Share/Advanced/NCShareHeaderCustomCell.xib new file mode 100644 index 0000000000..89ba3781e5 --- /dev/null +++ b/iOSClient/Share/Advanced/NCShareHeaderCustomCell.xib @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Share/Advanced/NCShareNewUserAddComment.swift b/iOSClient/Share/Advanced/NCShareNewUserAddComment.swift index 0c6bb586ef..4259357dcc 100644 --- a/iOSClient/Share/Advanced/NCShareNewUserAddComment.swift +++ b/iOSClient/Share/Advanced/NCShareNewUserAddComment.swift @@ -27,6 +27,9 @@ class NCShareNewUserAddComment: UIViewController, NCShareNavigationTitleSetting @IBOutlet weak var headerContainerView: UIView! @IBOutlet weak var sharingLabel: UILabel! @IBOutlet weak var noteTextField: UITextView! + @IBOutlet weak var btnCancel: UIButton! + @IBOutlet weak var btnSendShare: UIButton! + @IBOutlet weak var buttonContainerView: UIView! let contentInsets: CGFloat = 16 var onDismiss: (() -> Void)? @@ -34,13 +37,46 @@ class NCShareNewUserAddComment: UIViewController, NCShareNavigationTitleSetting public var share: Shareable! public var metadata: tableMetadata! + var isNewShare: Bool { share is NCTableShareOptions } + var networking: NCShareNetworking? + + var isFromMenu: Bool = false + /// + /// The possible download limit associated with this share. + /// + /// This can only be created after the share has been actually created due to its requirement of the share token provided by the server. + /// + var downloadLimit: DownloadLimitViewModel = .unlimited + var downloadLimitChanged: Bool = false + override func viewDidLoad() { super.viewDidLoad() self.setNavigationTitle() - NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) +// NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillHideNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(adjustForKeyboard), name: UIResponder.keyboardWillChangeFrameNotification, object: nil) + btnCancel.setTitleColor(NCBrandColor.shared.label, for: .normal) + btnCancel.layer.borderColor = NCBrandColor.shared.label.cgColor + btnCancel.backgroundColor = .clear + buttonContainerView.backgroundColor = NCBrandColor.shared.secondarySystemGroupedBackground + btnSendShare.backgroundColor = NCBrandColor.shared.customer //setBackgroundColor(NCBrandColor.shared.customer, for: .normal) + btnSendShare.setTitleColor(.white, for: .normal) + + btnCancel.setTitle(NSLocalizedString("_cancel_", comment: ""), for: .normal) + btnCancel.layer.cornerRadius = 10 + btnCancel.layer.masksToBounds = true + btnCancel.layer.borderWidth = 1 + btnSendShare.setTitle(NSLocalizedString("_send_share_", comment: ""), for: .normal) + btnSendShare.layer.cornerRadius = 10 + btnSendShare.layer.masksToBounds = true + + if !self.isFromMenu { + buttonContainerView.isHidden = true + } + sharingLabel.text = NSLocalizedString("_share_note_recipient_", comment: "") noteTextField.textContainerInset = UIEdgeInsets(top: contentInsets, left: contentInsets, bottom: contentInsets, right: contentInsets) @@ -52,7 +88,9 @@ class NCShareNewUserAddComment: UIViewController, NCShareNavigationTitleSetting } onDone: { self.noteTextField.resignFirstResponder() self.share.note = self.noteTextField.text - self.navigationController?.popViewController(animated: true) + if !self.isFromMenu { + self.navigationController?.popViewController(animated: true) + } } noteTextField.inputAccessoryView = toolbar.wrappedSafeAreaContainer @@ -75,6 +113,21 @@ class NCShareNewUserAddComment: UIViewController, NCShareNavigationTitleSetting onDismiss?() } + @IBAction func cancelClicked(_ sender: Any) { + self.navigationController?.popToRootViewController(animated: true) + } + + @IBAction func sendShareClicked(_ sender: Any) { + share.note = noteTextField.text + if isNewShare { + networking?.createShare(share, downloadLimit: self.downloadLimit) + } else { + networking?.updateShare(share, downloadLimit: self.downloadLimit, changeDownloadLimit: downloadLimitChanged) + } + + self.navigationController?.popToRootViewController(animated: true) + } + @objc func adjustForKeyboard(notification: Notification) { guard let keyboardValue = notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue, let globalTextViewFrame = noteTextField.superview?.convert(noteTextField.frame, to: nil) else { return } @@ -90,4 +143,18 @@ class NCShareNewUserAddComment: UIViewController, NCShareNavigationTitleSetting noteTextField.scrollIndicatorInsets = noteTextField.contentInset } + + @objc func keyboardWillShow(notification: Notification) { + if UIScreen.main.bounds.width <= 375 { + if view.frame.origin.y == 0 { + self.view.frame.origin.y -= 200 + } + } + } + + @objc func keyboardWillHide(notification: Notification) { + if view.frame.origin.y != 0 { + self.view.frame.origin.y = 0 + } + } } diff --git a/iOSClient/Share/Advanced/NCShareTextInputCell.swift b/iOSClient/Share/Advanced/NCShareTextInputCell.swift new file mode 100644 index 0000000000..f3534c975f --- /dev/null +++ b/iOSClient/Share/Advanced/NCShareTextInputCell.swift @@ -0,0 +1,162 @@ +// +// NCShareTextInputCell.swift +// Nextcloud +// +// Created by T-systems on 20/09/21. +// Copyright © 2021 Kunal. All rights reserved. +// + +import UIKit +import XLForm + +class NCShareTextInputCell: XLFormBaseCell, UITextFieldDelegate { + + @IBOutlet weak var seperator: UIView! + @IBOutlet weak var seperatorBottom: UIView! + @IBOutlet weak var cellTextField: UITextField! + @IBOutlet weak var calendarImageView: UIImageView! + + let datePicker = UIDatePicker() + var expirationDateText: String! + var expirationDate: NSDate! + + override func awakeFromNib() { + super.awakeFromNib() + self.cellTextField.delegate = self + self.cellTextField.isEnabled = true + calendarImageView.image = UIImage(named: "calender")//?.imageColor(NCBrandColor.shared.brandElement) + self.selectionStyle = .none + self.backgroundColor = NCBrandColor.shared.secondarySystemGroupedBackground + self.cellTextField.attributedPlaceholder = NSAttributedString( + string: "", + attributes: [NSAttributedString.Key.foregroundColor: NCBrandColor.shared.fileFolderName] + ) + self.cellTextField.textColor = NCBrandColor.shared.singleTitleColorButton + } + + override func configure() { + super.configure() + } + + override func update() { + super.update() + + calendarImageView.isHidden = rowDescriptor.tag != "NCShareTextInputCellExpiry" + + if rowDescriptor.tag == "NCShareTextInputCellExpiry" { + seperator.isHidden = true + setDatePicker(sender: self.cellTextField) + } else if rowDescriptor.tag == "NCShareTextInputCellDownloadLimit" { + seperator.isHidden = true + cellTextField.keyboardType = .numberPad + setDownloadLimitDoneButton(sender: self.cellTextField) + } + } + + // MARK: - TextField Delegate + + func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { + if self.cellTextField == textField, let text = self.cellTextField.text { + rowDescriptor.value = (text + string).isEmpty ? nil : text + string + } + self.formViewController().textField(textField, shouldChangeCharactersIn: range, replacementString: string) + return true + } + + func textFieldDidEndEditing(_ textField: UITextField) { + rowDescriptor.value = cellTextField.text + } + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + self.formViewController()?.textFieldShouldReturn(textField) + return true + } + + func textFieldShouldClear(_ textField: UITextField) -> Bool { + self.formViewController()?.textFieldShouldClear(textField) + return true + } + + override class func formDescriptorCellHeight(for rowDescriptor: XLFormRowDescriptor!) -> CGFloat { + return 30 + } + + override func formDescriptorCellDidSelected(withForm controller: XLFormViewController!) { + self.selectionStyle = .none + } + + // MARK: - Date Picker + + func setDatePicker(sender: UITextField) { + datePicker.datePickerMode = .date + datePicker.minimumDate = Date()//.tomorrow + if #available(iOS 13.4, *) { + datePicker.preferredDatePickerStyle = .wheels + datePicker.sizeToFit() + } + + let toolbar = UIToolbar() + toolbar.sizeToFit() + + let doneButton = UIBarButtonItem(title: NSLocalizedString("_done_", comment: ""), style: .plain, target: self, action: #selector(doneDatePicker)) + let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + let clearButton = UIBarButtonItem(title: NSLocalizedString("_clear_", comment: ""), style: .plain, target: self, action: #selector(clearDatePicker)) + + toolbar.setItems([clearButton, spaceButton, doneButton], animated: false) + sender.inputAccessoryView = toolbar + sender.inputView = datePicker + } + + @objc func doneDatePicker() { +// let expiryDateString = DateFormatter.formattedShareExpDate(from: datePicker.date) + let expiryDateString = DateFormatter.formattedExpiryDate(datePicker.date) + + self.expirationDateText = expiryDateString + self.expirationDate = datePicker.date as NSDate + + self.cellTextField.text = self.expirationDateText + self.rowDescriptor.value = self.expirationDate + self.cellTextField.endEditing(true) + } + + @objc func clearDatePicker() { + self.expirationDate = nil + self.cellTextField.text = "" + self.rowDescriptor.value = nil + self.cellTextField.endEditing(true) + } + + // MARK: - Download Limit + + func setDownloadLimitDoneButton(sender: UITextField) { + let toolbar = UIToolbar() + toolbar.sizeToFit() + + let doneButton = UIBarButtonItem(title: NSLocalizedString("_done_", comment: ""), style: .plain, target: self, action: #selector(doneDownloadLimit)) + let spaceButton = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + toolbar.setItems([spaceButton, doneButton], animated: false) + + sender.inputAccessoryView = toolbar + } + + @objc func doneDownloadLimit() { + if let text = cellTextField.text, let limit = Int(text) { + rowDescriptor.value = limit + } else { + rowDescriptor.value = 0 + } + cellTextField.endEditing(true) + } +} + +class NCSeparatorCell: XLFormBaseCell { + override func awakeFromNib() { + super.awakeFromNib() + selectionStyle = .none + } + + override func update() { + super.update() + selectionStyle = .none + } +} diff --git a/iOSClient/Share/Advanced/NCShareTextInputCell.xib b/iOSClient/Share/Advanced/NCShareTextInputCell.xib new file mode 100644 index 0000000000..eaa2f0cd13 --- /dev/null +++ b/iOSClient/Share/Advanced/NCShareTextInputCell.xib @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Share/CreateLinkFooterView.swift b/iOSClient/Share/CreateLinkFooterView.swift new file mode 100644 index 0000000000..fb07695cd0 --- /dev/null +++ b/iOSClient/Share/CreateLinkFooterView.swift @@ -0,0 +1,79 @@ +// +// CreateLinkFooterView.swift +// Nextcloud +// +// Created by A106551118 on 12/08/25. +// Copyright © 2025 Marino Faggiana. All rights reserved. +// + + +import UIKit + +class CreateLinkFooterView: UITableViewHeaderFooterView { + + static let reuseIdentifier = "CreateLinkFooterView" + + private let createButton: UIButton = { + let button = UIButton(type: .system) + button.setTitle(NSLocalizedString("_create_new_link_", comment: ""), for: .normal) + button.setTitleColor(UIColor.label, for: .normal) +// button.setTitleColor(NCBrandColor.shared.shareBlackColor, for: .normal) + button.titleLabel?.font = .systemFont(ofSize: 17, weight: .semibold) + button.layer.cornerRadius = 7 + button.layer.borderWidth = 1 + button.layer.borderColor = NCBrandColor.shared.label.cgColor + button.backgroundColor = .clear + button.translatesAutoresizingMaskIntoConstraints = false + return button + }() + + private let separator: UIView = { + let view = UIView() + view.backgroundColor = .lightGray + view.translatesAutoresizingMaskIntoConstraints = false + return view + }() + + var createButtonAction: (() -> Void)? + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + } + + override func layoutSubviews() { + super.layoutSubviews() + createButton.layer.borderColor = NCBrandColor.shared.label.cgColor + } + + private func setupUI() { + contentView.backgroundColor = .clear + + contentView.addSubview(createButton) + contentView.addSubview(separator) + + NSLayoutConstraint.activate([ + createButton.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), + createButton.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), + createButton.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15), + createButton.heightAnchor.constraint(equalToConstant: 40), + + separator.topAnchor.constraint(equalTo: createButton.bottomAnchor, constant: 20), + separator.leadingAnchor.constraint(equalTo: contentView.leadingAnchor), + separator.trailingAnchor.constraint(equalTo: contentView.trailingAnchor), + separator.heightAnchor.constraint(equalToConstant: 1), + separator.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) // ensures correct height + ]) + + createButton.addTarget(self, action: #selector(buttonTapped), for: .touchUpInside) + } + + @objc private func buttonTapped() { + createButtonAction?() + } +} diff --git a/iOSClient/Share/NCPermissions.swift b/iOSClient/Share/NCPermissions.swift new file mode 100644 index 0000000000..de40c00833 --- /dev/null +++ b/iOSClient/Share/NCPermissions.swift @@ -0,0 +1,108 @@ +// +// NCPermissions.swift +// Nextcloud +// +// Created by Marino Faggiana on 05/06/24. +// Copyright © 2024 Marino Faggiana. All rights reserved. +// +// Author Marino Faggiana +// +// 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 . +// + +import UIKit +import Foundation + +class NCPermissions: NSObject { + let permissionShared = "S" + let permissionCanShare = "R" + let permissionMounted = "M" + let permissionFileCanWrite = "W" + let permissionCanCreateFile = "C" + let permissionCanCreateFolder = "K" + let permissionCanDelete = "D" + let permissionCanRename = "N" + let permissionCanMove = "V" + + // Share permission + // permissions - (int) 1 = read; 2 = update; 4 = create; 8 = delete; 16 = share; 31 = all + // +// let permissionReadShare: Int = 1 +// let permissionUpdateShare: Int = 2 +// let permissionCreateShare: Int = 4 +// let permissionDeleteShare: Int = 8 +// let permissionShareShare: Int = 16 + @objc let permissionReadShare: Int = 1 + @objc let permissionUpdateShare: Int = 2 + @objc let permissionCreateShare: Int = 4 + @objc let permissionDeleteShare: Int = 8 + @objc let permissionShareShare: Int = 16 + // + let permissionMinFileShare: Int = 1 + let permissionMaxFileShare: Int = 19 + let permissionMinFolderShare: Int = 1 + let permissionMaxFolderShare: Int = 31 + let permissionDefaultFileRemoteShareNoSupportShareOption: Int = 3 + let permissionDefaultFolderRemoteShareNoSupportShareOption: Int = 15 + // ATTRIBUTES + let permissionDownloadShare: Int = 0 + + func isPermissionToRead(_ permission: Int) -> Bool { + return ((permission & permissionReadShare) > 0) + } + func isPermissionToCanDelete(_ permission: Int) -> Bool { + return ((permission & permissionDeleteShare) > 0) + } + func isPermissionToCanCreate(_ permission: Int) -> Bool { + return ((permission & permissionCreateShare) > 0) + } + func isPermissionToCanChange(_ permission: Int) -> Bool { + return ((permission & permissionUpdateShare) > 0) + } + func isPermissionToCanShare(_ permission: Int) -> Bool { + return ((permission & permissionShareShare) > 0) + } + func isAnyPermissionToEdit(_ permission: Int) -> Bool { + let canCreate = isPermissionToCanCreate(permission) + let canChange = isPermissionToCanChange(permission) + let canDelete = isPermissionToCanDelete(permission) + return canCreate || canChange || canDelete + } + func isPermissionToReadCreateUpdate(_ permission: Int) -> Bool { + let canRead = isPermissionToRead(permission) + let canCreate = isPermissionToCanCreate(permission) + let canChange = isPermissionToCanChange(permission) + return canCreate && canChange && canRead + } + func getPermission(canEdit: Bool, canCreate: Bool, canChange: Bool, canDelete: Bool, canShare: Bool, isDirectory: Bool) -> Int { + var permission = permissionReadShare + + if canEdit && !isDirectory { + permission = permission + permissionUpdateShare + } + if canCreate && isDirectory { + permission = permission + permissionCreateShare + } + if canChange && isDirectory { + permission = permission + permissionUpdateShare + } +// if canDelete && isDirectory { +// permission = permission + permissionDeleteShare +// } + if canShare { + permission = permission + permissionShareShare + } + return permission + } +} diff --git a/iOSClient/Share/NCSearchUserDropDownCell.xib b/iOSClient/Share/NCSearchUserDropDownCell.xib index 2ba38c8fa6..fbded3d17d 100755 --- a/iOSClient/Share/NCSearchUserDropDownCell.xib +++ b/iOSClient/Share/NCSearchUserDropDownCell.xib @@ -1,9 +1,8 @@ - + - - + @@ -29,12 +28,12 @@ - + @@ -58,12 +57,12 @@ + - @@ -73,12 +72,12 @@ - + - - + + diff --git a/iOSClient/Share/NCShare+Helper.swift b/iOSClient/Share/NCShare+Helper.swift new file mode 100644 index 0000000000..8fa315d483 --- /dev/null +++ b/iOSClient/Share/NCShare+Helper.swift @@ -0,0 +1,114 @@ +// +// NCShare+Helper.swift +// Nextcloud +// +// Created by Henrik Storch on 19.03.22. +// Copyright © 2022 Henrik Storch. All rights reserved. +// +// Author Henrik Storch +// +// 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 . +// + +import UIKit +import NextcloudKit + +extension tableShare: NCTableShareable { } +extension NKShare: NCTableShareable { } + +protocol NCTableShareable: AnyObject { + var shareType: Int { get set } + var permissions: Int { get set } + var idShare: Int { get set } + var shareWith: String { get set } + var hideDownload: Bool { get set } + var password: String { get set } + var label: String { get set } + var note: String { get set } + var expirationDate: NSDate? { get set } + var shareWithDisplayname: String { get set } + var attributes: String? { get set } +} + +extension NCTableShareable { + var expDateString: String? { + guard let date = expirationDate else { return nil } + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "YYYY-MM-dd HH:mm:ss" + return dateFormatter.string(from: date as Date) + } + + func hasChanges(comparedTo other: NCTableShareable) -> Bool { + return other.shareType != shareType + || other.permissions != permissions + || other.hideDownload != hideDownload + || other.password != password + || other.label != label + || other.note != note + || other.expirationDate != expirationDate + } +} + +class NCTableShareOptions: NCTableShareable { + + var shareType: Int + var permissions: Int + + var idShare: Int = 0 + var shareWith: String = "" + + var hideDownload: Bool = false + var password: String = "" + var label: String = "" + var note: String = "" + var expirationDate: NSDate? + var shareWithDisplayname: String = "" + + var attributes: String? + + private init(shareType: Int, metadata: tableMetadata, password: String?) async { + if metadata.e2eEncrypted, await NKCapabilities.shared.getCapabilities(for: metadata.account).e2EEApiVersion == "1.2" { + self.permissions = NCPermissions().permissionCreateShare + } else { + self.permissions = await NKCapabilities.shared.getCapabilities(for: metadata.account).fileSharingDefaultPermission & metadata.sharePermissionsCollaborationServices + } + self.shareType = shareType + if let password = password { + self.password = password + } + } + + convenience init(sharee: NKSharee, metadata: tableMetadata, password: String?) async { + await self.init(shareType: sharee.shareType, metadata: metadata, password: password) + self.shareWith = sharee.shareWith + self.shareWithDisplayname = sharee.label + } + + static func shareLink(metadata: tableMetadata, password: String?) async -> NCTableShareOptions { + return await NCTableShareOptions(shareType: NCShareCommon.shareTypeLink, metadata: metadata, password: password) + } +} + +protocol NCShareDetail { + var share: NCTableShareable! { get } +} + +extension NCShareDetail where Self: UIViewController { + func setNavigationTitle() { + title = NSLocalizedString("_sharing_", comment: "") + if share.shareType != NCShareCommon.shareTypeLink { + title! = share.shareWithDisplayname.isEmpty ? share.shareWith : share.shareWithDisplayname + } + } +} diff --git a/iOSClient/Share/NCShare+NCCellDelegate.swift b/iOSClient/Share/NCShare+NCCellDelegate.swift index c1480d67d2..46597aee63 100644 --- a/iOSClient/Share/NCShare+NCCellDelegate.swift +++ b/iOSClient/Share/NCShare+NCCellDelegate.swift @@ -22,6 +22,7 @@ // import UIKit +import NextcloudKit // MARK: - NCCell Delegates extension NCShare: NCShareLinkCellDelegate, NCShareUserCellDelegate { @@ -50,20 +51,20 @@ extension NCShare: NCShareLinkCellDelegate, NCShareUserCellDelegate { } func tapMenu(with tableShare: tableShare?, sender: Any) { - // Menu is now shown via native context menu on the button - // Only handle the case where there's no tableShare (add new link) - if tableShare == nil { + if let tableShare = tableShare { + self.toggleShareMenu(for: tableShare, sendMail: (tableShare.shareType != NKShare.ShareType.publicLink.rawValue), folder: metadata?.directory ?? false, sender: sender) + } else { self.makeNewLinkShare() } } - func tapProfileMenu(with tableShare: tableShare?) -> UIMenu? { - guard let tableShare else { return nil } - return NCContextMenuProfile(userId: tableShare.shareWith, session: session, viewController: self).viewMenu() + func showProfile(with tableShare: tableShare?, sender: Any) { + guard let tableShare else { return } + showProfileMenu(userId: tableShare.shareWith, session: session, sender: sender) } - func tapQuickStatus(with tableShare: tableShare?, sender: Any) { - guard let tableShare else { return } - presentQuickStatusActionSheet(for: tableShare, sender: sender) + func quickStatus(with tableShare: tableShare?, sender: Any) { + guard let tableShare, let metadata else { return } + self.toggleQuickPermissionsMenu(isDirectory: metadata.directory, share: tableShare, sender: sender) } } diff --git a/iOSClient/Share/NCShare.storyboard b/iOSClient/Share/NCShare.storyboard index 5bf01c2fb8..d07ec49440 100644 --- a/iOSClient/Share/NCShare.storyboard +++ b/iOSClient/Share/NCShare.storyboard @@ -1,185 +1,310 @@ - + - - - + - - - - - - - - - - - - - - - - - + - + - - + + - + - + - - + + - - + + + + + + + + + + + + + + + + + - - + + - - - - - - - - - - + + + + + + + + + + + + + + + - - + + - - - - - - - - - + + - - - - - - - - - - - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - + + + + + - - - - - - - - + - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - - + + - + - + - + - + - + - + @@ -191,10 +316,10 @@ - + - - - - - - - - + + + + + + + - - - - - + + + + + - - + + + + + + + + + + + + + + + + + + + + + - + - - - - - + + + + + + + + + + + + + + - + - - - + + + + + + - + - + - + - - + + - + - + - + - + - + - + - - - - + + + + - + - + - + - + - - + + - + - - + + - - + + - + - - + + - - + + - + - - + + - - - - + + + + - - - + + + - + - + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - diff --git a/iOSClient/Share/NCShare.swift b/iOSClient/Share/NCShare.swift index 2f2a765db8..895ef8275a 100644 --- a/iOSClient/Share/NCShare.swift +++ b/iOSClient/Share/NCShare.swift @@ -27,24 +27,33 @@ import UIKit import Parchment import DropDown import NextcloudKit +import MarqueeLabel import ContactsUI +enum ShareSection: Int, CaseIterable { + case header + case linkByEmail + case links + case emails +} + class NCShare: UIViewController, NCSharePagingContent { - @IBOutlet weak var viewContainerConstraint: NSLayoutConstraint! - @IBOutlet weak var sharedWithYouByView: UIView! - @IBOutlet weak var sharedWithYouByImage: UIImageView! - @IBOutlet weak var sharedWithYouByLabel: UILabel! - @IBOutlet weak var searchFieldTopConstraint: NSLayoutConstraint! - @IBOutlet weak var searchField: UISearchBar! - var textField: UIView? { searchField } +// @IBOutlet weak var viewContainerConstraint: NSLayoutConstraint! +// @IBOutlet weak var sharedWithYouByView: UIView! +// @IBOutlet weak var sharedWithYouByImage: UIImageView! +// @IBOutlet weak var sharedWithYouByLabel: UILabel! +// @IBOutlet weak var searchFieldTopConstraint: NSLayoutConstraint! +// @IBOutlet weak var searchField: UISearchBar! +// var textField: UIView? { searchField } + var textField: UITextField? { self.view.viewWithTag(Tag.searchField) as? UITextField } @IBOutlet weak var tableView: UITableView! - @IBOutlet weak var btnContact: UIButton! +// @IBOutlet weak var btnContact: UIButton! weak var appDelegate = UIApplication.shared.delegate as? AppDelegate - var metadata: tableMetadata! - var height: CGFloat = 0 + public var metadata: tableMetadata! + public var height: CGFloat = 0 var controller: NCMainTabBarController? let utilityFileSystem = NCUtilityFileSystem() @@ -54,10 +63,10 @@ class NCShare: UIViewController, NCSharePagingContent { var shareLinksCount = 0 var canReshare: Bool { + guard let metadata = metadata else { return true } return ((metadata.sharePermissionsCollaborationServices & NKShare.Permission.share.rawValue) != 0) } - @MainActor var session: NCSession.Session { NCSession.shared.getSession(account: metadata.account) } @@ -67,21 +76,27 @@ class NCShare: UIViewController, NCSharePagingContent { var capabilities = NKCapabilities.Capabilities() private var dropDown = DropDown() - private var avatarButton: UIButton! var networking: NCShareNetworking? + var isCurrentUser: Bool { + if let currentUser = NCManageDatabase.shared.getActiveTableAccount(), currentUser.userId == metadata?.ownerId { + return true + } + return false + } + var shareLinks: [tableShare] = [] + var shareEmails: [tableShare] = [] + var shareOthers: [tableShare] = [] + private var cachedHeader: NCShareAdvancePermissionHeader? + // MARK: - View Life Cycle override func viewDidLoad() { super.viewDidLoad() + navigationController?.setNavigationBarAppearance() view.backgroundColor = .systemBackground - - viewContainerConstraint.constant = height - searchFieldTopConstraint.constant = 0 - - searchField.placeholder = NSLocalizedString("_shareLinksearch_placeholder_", comment: "") - searchField.autocorrectionType = .no + title = NSLocalizedString("_details_", comment: "") tableView.dataSource = self tableView.delegate = self @@ -91,33 +106,61 @@ class NCShare: UIViewController, NCSharePagingContent { tableView.register(UINib(nibName: "NCShareLinkCell", bundle: nil), forCellReuseIdentifier: "cellLink") tableView.register(UINib(nibName: "NCShareUserCell", bundle: nil), forCellReuseIdentifier: "cellUser") + tableView.register(UINib(nibName: "NCShareEmailFieldCell", bundle: nil), forCellReuseIdentifier: "NCShareEmailFieldCell") + tableView.register(NCShareEmailLinkHeaderView.self, + forHeaderFooterViewReuseIdentifier: NCShareEmailLinkHeaderView.reuseIdentifier) + tableView.register(CreateLinkFooterView.self, forHeaderFooterViewReuseIdentifier: CreateLinkFooterView.reuseIdentifier) + tableView.register(NoSharesFooterView.self, forHeaderFooterViewReuseIdentifier: NoSharesFooterView.reuseIdentifier) + tableView.register(UINib(nibName: "NCShareAdvancePermissionHeader", bundle: nil), + forHeaderFooterViewReuseIdentifier: NCShareAdvancePermissionHeader.reuseIdentifier) NotificationCenter.default.addObserver(self, selector: #selector(reloadData), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterReloadDataNCShare), object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow), name: UIResponder.keyboardWillShowNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide), name: UIResponder.keyboardWillHideNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(appWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(reloadData), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterDidCreateShareLink), object: nil) +// NotificationCenter.default.addObserver(self, selector: #selector(handleFavoriteStatusChanged), name: NSNotification.Name(rawValue: NCGlobal.shared.notificationCenterFavoriteStatusChanged), object: nil) + guard let metadata = metadata else { return } + + reloadData() + Task { self.capabilities = await NKCapabilities.shared.getCapabilities(for: metadata.account) if metadata.e2eEncrypted { let metadataDirectory = await self.database.getMetadataDirectoryAsync(serverUrl: metadata.serverUrl, account: metadata.account) + if capabilities.e2EEApiVersion == "1.2" || - (capabilities.e2EEApiVersion.hasPrefix("2.") && metadataDirectory?.e2eEncrypted ?? false) { - searchFieldTopConstraint.constant = -50 - searchField.alpha = 0 - btnContact.alpha = 0 + (NCGlobal.shared.isE2eeVersion2(capabilities.e2EEApiVersion) && metadataDirectory?.e2eEncrypted ?? false) { +// searchFieldTopConstraint.constant = -50 +// searchField.alpha = 0 +// btnContact.alpha = 0 } } else { - checkSharedWithYou() +// checkSharedWithYou() } - reloadData() +// reloadData() networking = NCShareNetworking(metadata: metadata, view: self.view, delegate: self, session: session, controller: controller) let isVisible = (self.navigationController?.topViewController as? NCSharePaging)?.page == .sharing networking?.readShare(showLoadingIndicator: isVisible) - searchField.searchTextField.font = .systemFont(ofSize: 14) - searchField.delegate = self +// searchField.searchTextField.font = .systemFont(ofSize: 14) +// searchField.delegate = self } + + navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("_close_", comment: ""), style: .plain, target: self, action: #selector(exitTapped)) } + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + } + + @objc func exitTapped() { + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterUpdateIcons) + self.dismiss(animated: true, completion: nil) + } + func makeNewLinkShare() { guard let advancePermission = UIStoryboard(name: "NCShare", bundle: nil).instantiateViewController(withIdentifier: "NCShareAdvancePermission") as? NCShareAdvancePermission, @@ -126,91 +169,137 @@ class NCShare: UIViewController, NCSharePagingContent { advancePermission.networking = self.networking advancePermission.share = TransientShare.shareLink(metadata: self.metadata, password: password) advancePermission.metadata = self.metadata - advancePermission.controller = self.controller navigationController.pushViewController(advancePermission, animated: true) } } - // Shared with you by ... - func checkSharedWithYou() { - guard !metadata.ownerId.isEmpty, metadata.ownerId != session.userId else { return } + // MARK: - Notification Center + + @objc func openShareProfile(_ sender: UITapGestureRecognizer) { + self.showProfileMenu(userId: metadata.ownerId, session: session, sender: sender.view) + } - if !canReshare { - searchField.isUserInteractionEnabled = false - searchField.alpha = 0.5 - searchField.placeholder = NSLocalizedString("_share_reshare_disabled_", comment: "") - btnContact.isEnabled = false + private func scrollToTopIfNeeded() { + if tableView.numberOfSections > 0 && tableView.numberOfRows(inSection: 0) > 0 { + self.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) } + } - searchFieldTopConstraint.constant = 45 - sharedWithYouByView.isHidden = false - sharedWithYouByLabel.text = NSLocalizedString("_shared_with_you_by_", comment: "") + " " + metadata.ownerDisplayName - sharedWithYouByImage.image = utility.loadUserImage(for: metadata.ownerId, displayName: metadata.ownerDisplayName, urlBase: session.urlBase) - sharedWithYouByLabel.accessibilityHint = NSLocalizedString("_show_profile_", comment: "") - - avatarButton = UIButton(type: .system) - avatarButton.translatesAutoresizingMaskIntoConstraints = false - avatarButton.backgroundColor = .clear - sharedWithYouByView.addSubview(avatarButton) - NSLayoutConstraint.activate([ - avatarButton.topAnchor.constraint(equalTo: sharedWithYouByImage.topAnchor), - avatarButton.bottomAnchor.constraint(equalTo: sharedWithYouByImage.bottomAnchor), - avatarButton.leadingAnchor.constraint(equalTo: sharedWithYouByImage.leadingAnchor), - avatarButton.trailingAnchor.constraint(equalTo: sharedWithYouByLabel.trailingAnchor) - ]) - avatarButton.showsMenuAsPrimaryAction = true - avatarButton.menu = NCContextMenuProfile(userId: metadata.ownerId, session: session, viewController: self).viewMenu() - - let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: metadata.ownerId) - let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) - - if results.image == nil { - let etag = self.database.getTableAvatar(fileName: fileName)?.etag - let fileNameLocalPath = utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - - NextcloudKit.shared.downloadAvatar( - user: metadata.ownerId, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etag, - account: metadata.account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: self.metadata.account, - path: self.metadata.ownerId, - name: "downloadAvatar") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, imageAvatar, _, etag, _, error in - if error == .success, let etag = etag, let imageAvatar = imageAvatar { - self.database.addAvatar(fileName: fileName, etag: etag) - self.sharedWithYouByImage.image = imageAvatar - self.reloadData() - } else if error.errorCode == NCGlobal.shared.errorNotModified, let imageAvatar = self.database.setAvatarLoaded(fileName: fileName) { - self.sharedWithYouByImage.image = imageAvatar + @objc func keyboardWillShow(notification: Notification) { + if UIDevice.current.userInterfaceIdiom == .phone { + if UIScreen.main.bounds.width < 374 || UIDevice.current.orientation.isLandscape { + if let keyboardSize = (notification.userInfo?[UIResponder.keyboardFrameEndUserInfoKey] as? NSValue)?.cgRectValue { + if view.frame.origin.y == 0 { + scrollToTopIfNeeded() + self.view.frame.origin.y -= keyboardSize.height } } + } else if UIScreen.main.bounds.height < 850 { + if view.frame.origin.y == 0 { + scrollToTopIfNeeded() + self.view.frame.origin.y -= 70 + } + } else { + if view.frame.origin.y == 0 { + scrollToTopIfNeeded() + self.view.frame.origin.y -= 40 + } + } } - reloadData() + if UIDevice.current.userInterfaceIdiom == .pad, UIDevice.current.orientation.isLandscape { + if view.frame.origin.y == 0 { + if tableView.numberOfSections > 0 && tableView.numberOfRows(inSection: 0) > 0 { + self.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) + } + self.view.frame.origin.y -= 230 + } + } + + textField?.layer.borderColor = NCBrandColor.shared.brand.cgColor } - // MARK: - + + @objc func keyboardWillHide(notification: Notification) { + if view.frame.origin.y != 0 { + self.view.frame.origin.y = 0 + } + textField?.layer.borderColor = NCBrandColor.shared.label.cgColor + } + @objc func appWillEnterForeground(notification: Notification) { + reloadData() + } + + // MARK: - + @objc func reloadData() { + guard let metadata = metadata else { + return + } shares = self.database.getTableShares(metadata: metadata) - shareLinksCount = 0 + updateShareArrays() tableView.reloadData() } + + func updateShareArrays() { + shareLinks.removeAll() + shareEmails.removeAll() + + guard var allShares = shares.share else { return } + allShares = allShares.sorted(by: { $0.date?.compare($1.date as Date? ?? Date()) == .orderedAscending }) + if let firstLink = shares.firstShareLink { + // Remove if already exists to avoid duplication + allShares.removeAll { $0.idShare == firstLink.idShare } + allShares.insert(firstLink, at: 0) + } + + shares.share = allShares + + for item in allShares { + if item.shareType == NCShareCommon.shareTypeLink { + shareLinks.append(item) + } else { + shareEmails.append(item) + } + } + } + + @objc func handleFavoriteStatusChanged(notification: Notification) { + // Retrieve the updated metadata from the notification object + if let updatedMetadata = notification.object as? tableMetadata { + // Update the table view header with the new metadata + if let headerView = tableView.tableHeaderView as? NCShareAdvancePermissionHeader { + headerView.setupUI(with: updatedMetadata, linkCount: shareLinks.count, emailCount: shareEmails.count) // Update header UI with new metadata + } + } + } // MARK: - IBAction @IBAction func searchFieldDidEndOnExit(textField: UITextField) { +// // https://stackoverflow.com/questions/25471114/how-to-validate-an-e-mail-address-in-swift +// func isValidEmail(_ email: String) -> Bool { +// +// let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" +// let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx) +// return emailPred.evaluate(with: email) +// } guard let searchString = textField.text, !searchString.isEmpty else { return } if searchString.contains("@"), !isValidEmail(searchString) { return } networking?.getSharees(searchString: searchString) } - + + @IBAction func searchFieldDidChange(textField: UITextField) { + NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(searchSharees), object: nil) + guard let searchString = textField.text else {return} + if searchString.count == 0 { + dropDown.hide() + } else { + perform(#selector(searchSharees), with: nil, afterDelay: 0.5) + } + } + func checkEnforcedPassword(shareType: Int, completion: @escaping (String?) -> Void) { guard capabilities.fileSharingPubPasswdEnforced, shareType == NKShare.ShareType.publicLink.rawValue || shareType == NKShare.ShareType.email.rawValue @@ -227,91 +316,44 @@ class NCShare: UIViewController, NCSharePagingContent { cnPicker.predicateForSelectionOfProperty = NSPredicate(format: "emailAddresses.@count > 0") self.present(cnPicker, animated: true) } - - func presentQuickStatusActionSheet(for share: tableShare, sender: Any?) { + + @IBAction func createLinkClicked(_ sender: Any?) { + appDelegate?.adjust.trackEvent(TriggerEvent(CreateLink.rawValue)) + TealiumHelper.shared.trackEvent(title: "magentacloud-app.sharing.create", data: ["": ""]) +// self.touchUpInsideButtonMenu(sender) + self.touchUpInsideButtonMenu(sender as Any) + } + + @IBAction func touchUpInsideButtonMenu(_ sender: Any) { + guard let metadata = metadata else { return } - - let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - let isDirectory = metadata.directory - - // Read Only - let readOnlyAction = UIAlertAction(title: NSLocalizedString("_share_read_only_", comment: ""), style: .default) { [weak self] _ in - let permissions = NCSharePermissions.getPermissionValue(canCreate: false, canEdit: false, canDelete: false, canShare: false, isDirectory: isDirectory) - self?.updateSharePermissions(share: share, permissions: permissions) - } - alertController.addAction(readOnlyAction) - - // Editing - let editingAction = UIAlertAction(title: NSLocalizedString("_share_editing_", comment: ""), style: .default) { [weak self] _ in - let permissions = NCSharePermissions.getPermissionValue(canCreate: true, canEdit: true, canDelete: true, canShare: true, isDirectory: isDirectory) - self?.updateSharePermissions(share: share, permissions: permissions) - } - alertController.addAction(editingAction) - - // File Drop (only for directories with public link or email share) - if isDirectory && (share.shareType == NKShare.ShareType.publicLink.rawValue || share.shareType == NKShare.ShareType.email.rawValue) { - let fileDropAction = UIAlertAction(title: NSLocalizedString("_share_file_drop_", comment: ""), style: .default) { [weak self] _ in - let permissions = NCSharePermissions.getPermissionValue(canRead: false, canCreate: true, canEdit: false, canDelete: false, canShare: false, isDirectory: isDirectory) - self?.updateSharePermissions(share: share, permissions: permissions) + let capabilities = NCNetworking.shared.capabilities[metadata.account] ?? NKCapabilities.Capabilities() + let isFilesSharingPublicPasswordEnforced = capabilities.fileSharingPubPasswdEnforced + let shares = NCManageDatabase.shared.getTableShares(metadata: metadata) + + if isFilesSharingPublicPasswordEnforced && shares.firstShareLink == nil { + let alertController = UIAlertController(title: NSLocalizedString("_enforce_password_protection_", comment: ""), message: "", preferredStyle: .alert) + alertController.addTextField { (textField) in + textField.isSecureTextEntry = true } - alertController.addAction(fileDropAction) - } - - // Custom Permissions - let customAction = UIAlertAction(title: NSLocalizedString("_custom_permissions_", comment: ""), style: .default) { [weak self] _ in - self?.openAdvancePermission(for: share) - } - alertController.addAction(customAction) - - // Cancel - let cancelAction = UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel) - alertController.addAction(cancelAction) - - // iPad popover support - if let popover = alertController.popoverPresentationController, - let sourceView = sender as? UIView { - let barItem = UIBarButtonItem(customView: sourceView) - popover.sourceItem = barItem - } - - present(alertController, animated: true) - } - - private func openAdvancePermission(for share: tableShare) { - guard let advancePermission = UIStoryboard(name: "NCShare", bundle: nil).instantiateViewController(withIdentifier: "NCShareAdvancePermission") as? NCShareAdvancePermission, - !share.isInvalidated, - let metadata = metadata else { return } - - advancePermission.networking = networking - advancePermission.share = tableShare(value: share) - advancePermission.oldTableShare = tableShare(value: share) - advancePermission.metadata = metadata - advancePermission.controller = self.controller + alertController.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .default) { (action:UIAlertAction) in }) + let okAction = UIAlertAction(title: NSLocalizedString("_ok_", comment: ""), style: .default) {[weak self] (action:UIAlertAction) in + let password = alertController.textFields?.first?.text + self?.networking?.createShareLink(password: password ?? "") + } + + alertController.addAction(okAction) + + present(alertController, animated: true, completion:nil) + } else if shares.firstShareLink == nil { + networking?.createShareLink(password: "") - if let downloadLimit = try? NCManageDatabase.shared.getDownloadLimit(byAccount: metadata.account, shareToken: share.token) { - advancePermission.downloadLimit = .limited(limit: downloadLimit.limit, count: downloadLimit.count) + } else { + networking?.createShareLink(password: "") } - - navigationController?.pushViewController(advancePermission, animated: true) + } - func updateSharePermissions(share: tableShare, permissions: Int) { - let updatedShare = tableShare(value: share) - updatedShare.permissions = permissions - - var downloadLimit: DownloadLimitViewModel = .unlimited - - do { - if let model = try database.getDownloadLimit(byAccount: metadata.account, shareToken: updatedShare.token) { - downloadLimit = .limited(limit: model.limit, count: model.count) - } - } catch { - nkLog(error: "Failed to get download limit from database!") - return - } - - networking?.updateShare(updatedShare, downloadLimit: downloadLimit) - } } // MARK: - NCShareNetworkingDelegate @@ -341,15 +383,15 @@ extension NCShare: NCShareNetworkingDelegate { } // close keyboard - self.view.endEditing(true) +// self.view.endEditing(true) dropDown = DropDown() let appearance = DropDown.appearance() - // Setting up the blur effect - let blurEffect = UIBlurEffect(style: .light) // You can choose .dark, .extraLight, or .light - let blurEffectView = UIVisualEffectView(effect: blurEffect) - blurEffectView.frame = CGRect(x: 0, y: 0, width: 500, height: 20) +// // Setting up the blur effect +// let blurEffect = UIBlurEffect(style: .light) // You can choose .dark, .extraLight, or .light +// let blurEffectView = UIVisualEffectView(effect: blurEffect) +// blurEffectView.frame = CGRect(x: 0, y: 0, width: 500, height: 20) appearance.backgroundColor = .systemBackground appearance.cornerRadius = 10 @@ -357,7 +399,8 @@ extension NCShare: NCShareNetworkingDelegate { appearance.shadowOpacity = 0.2 appearance.shadowRadius = 30 appearance.animationduration = 0.25 - appearance.textColor = .darkGray + appearance.textColor = NCBrandColor.shared.label + appearance.setupMaskedCorners([.layerMaxXMaxYCorner, .layerMinXMaxYCorner]) let account = NCManageDatabase.shared.getTableAccount(account: metadata.account) let existingShares = NCManageDatabase.shared.getTableShares(metadata: metadata) @@ -365,7 +408,7 @@ extension NCShare: NCShareNetworkingDelegate { for sharee in sharees { if sharee.shareWith == account?.user { continue } // do not show your own account if let shares = existingShares.share, shares.contains(where: {$0.shareWith == sharee.shareWith}) { continue } // do not show already existing sharees - if metadata.ownerDisplayName == sharee.shareWith { continue } // do not show owner of the share + if metadata.ownerDisplayName == sharee.shareWith { continue } // do not show owner of the share var label = sharee.label if sharee.shareType == NKShare.ShareType.team.rawValue { label += " (\(sharee.circleInfo), \(sharee.circleOwner))" @@ -374,9 +417,9 @@ extension NCShare: NCShareNetworkingDelegate { dropDown.dataSource.append(label) } - dropDown.anchorView = searchField - dropDown.bottomOffset = CGPoint(x: 10, y: searchField.bounds.height) - dropDown.width = searchField.bounds.width - 20 + dropDown.anchorView = textField + dropDown.bottomOffset = CGPoint(x: 10, y: textField?.bounds.height ?? 0) + dropDown.width = (textField?.bounds.width ?? 0) - 20 dropDown.direction = .bottom dropDown.cellNib = UINib(nibName: "NCSearchUserDropDownCell", bundle: nil) @@ -387,6 +430,8 @@ extension NCShare: NCShareNetworkingDelegate { } dropDown.selectionAction = { index, _ in + self.textField?.text = "" + self.textField?.resignFirstResponder() let sharee = sharees[index] guard let advancePermission = UIStoryboard(name: "NCShare", bundle: nil).instantiateViewController(withIdentifier: "NCShareAdvancePermission") as? NCShareAdvancePermission, @@ -396,7 +441,6 @@ extension NCShare: NCShareNetworkingDelegate { advancePermission.share = shareOptions advancePermission.networking = self.networking advancePermission.metadata = self.metadata - advancePermission.controller = self.controller navigationController.pushViewController(advancePermission, animated: true) } } @@ -418,8 +462,25 @@ extension NCShare: NCShareNetworkingDelegate { extension NCShare: UITableViewDelegate { func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { - return 60 + guard let sectionType = ShareSection(rawValue: indexPath.section) else { return 0 } + + switch sectionType { + case .header: + return 210 + + case .linkByEmail: + let isPad = UIDevice.current.userInterfaceIdiom == .pad + if isCurrentUser { + return 130 + } else { + return isPad ? (canReshare ? 200 : 220) : 220 + } + + case .links, .emails: + return 60 + } } + } // MARK: - UITableViewDataSource @@ -427,85 +488,159 @@ extension NCShare: UITableViewDelegate { extension NCShare: UITableViewDataSource { func numberOfSections(in tableView: UITableView) -> Int { - return 2 + ShareSection.allCases.count } func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - var numRows = shares.share?.count ?? 0 - if section == 0 { - if metadata.e2eEncrypted, capabilities.e2EEApiVersion == "1.2" { - numRows = 1 - } else { - // don't allow link creation if reshare is disabled - numRows = shares.firstShareLink != nil || canReshare ? 2 : 1 - } + guard let sectionType = ShareSection(rawValue: section) else { return 0 } + + switch sectionType { + case .header: + return 0 + case .linkByEmail: + return 1 + case .links: + return shareLinks.count + case .emails: + return shareEmails.count } - return numRows } func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { - // Setup default share cells - guard indexPath.section != 0 else { - guard let cell = tableView.dequeueReusableCell(withIdentifier: "cellLink", for: indexPath) as? NCShareLinkCell - else { return UITableViewCell() } + guard let sectionType = ShareSection(rawValue: indexPath.section) else { return UITableViewCell() } + + switch sectionType { + case .header: + return UITableViewCell() // Empty row + case .linkByEmail: + guard let cell = tableView.dequeueReusableCell(withIdentifier: "NCShareEmailFieldCell", for: indexPath) as? NCShareEmailFieldCell else { + return UITableViewCell() + } + cell.searchField.addTarget(self, action: #selector(searchFieldDidEndOnExit(textField:)), for: .editingDidEndOnExit) + cell.searchField.addTarget(self, action: #selector(searchFieldDidChange(textField:)), for: .editingChanged) + cell.btnContact.addTarget(self, action: #selector(selectContactClicked(_:)), for: .touchUpInside) + cell.setupCell(with: metadata) + return cell + + case .links: + let tableShare = shareLinks[indexPath.row] + guard let cell = tableView.dequeueReusableCell(withIdentifier: "cellLink", for: indexPath) as? NCShareLinkCell else { + return UITableViewCell() + } cell.delegate = self - if metadata.e2eEncrypted, capabilities.e2EEApiVersion == "1.2" { - cell.tableShare = shares.firstShareLink + if indexPath.row == 0 { + cell.configure(with: tableShare, at: indexPath, isDirectory: metadata.directory, title: "") } else { - if indexPath.row == 0 { - cell.isInternalLink = true - } else if shares.firstShareLink?.isInvalidated != true { - cell.tableShare = shares.firstShareLink - } + let linkNumber = " \(indexPath.row + 1)" + cell.configure(with: tableShare, at: indexPath, isDirectory: metadata.directory, title: linkNumber) } - cell.isDirectory = metadata.directory - cell.setupCellUI() + return cell - if cell.tableShare != nil, let tableShare = shares.firstShareLink { - cell.menuButton.menu = NCContextMenuShare(share: tableShare, isDirectory: metadata.isDirectory, canReshare: canReshare, shareController: self, controller: controller).viewMenu() - cell.menuButton.showsMenuAsPrimaryAction = true + case .emails: + let tableShare = shareEmails[indexPath.row] + guard let cell = tableView.dequeueReusableCell(withIdentifier: "cellUser", for: indexPath) as? NCShareUserCell else { + return UITableViewCell() } - - shareLinksCount += 1 + cell.delegate = self + cell.configure(with: tableShare, at: indexPath, isDirectory: metadata.directory, userId: session.userId) return cell } + } - let orderedShares = shares.share?.sorted(by: { $0.date?.compare($1.date as Date? ?? Date()) == .orderedAscending }) - guard let tableShare = orderedShares?[indexPath.row] else { return UITableViewCell() } - - // LINK, EMAIL - if tableShare.shareType == NKShare.ShareType.publicLink.rawValue || tableShare.shareType == NKShare.ShareType.email.rawValue { - if let cell = tableView.dequeueReusableCell(withIdentifier: "cellLink", for: indexPath) as? NCShareLinkCell { - cell.indexPath = indexPath - cell.tableShare = tableShare - cell.isDirectory = metadata.directory - cell.delegate = self - cell.setupCellUI(titleAppendString: String(shareLinksCount)) - cell.menuButton.menu = NCContextMenuShare(share: tableShare, isDirectory: metadata.isDirectory, canReshare: canReshare, shareController: self, controller: controller).viewMenu() - cell.menuButton.showsMenuAsPrimaryAction = true - if tableShare.shareType == NKShare.ShareType.publicLink.rawValue { shareLinksCount += 1 } - return cell + func numberOfRows(in section: Int) -> Int { + return tableView(tableView, numberOfRowsInSection: section) + } + + func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { + guard let sectionType = ShareSection(rawValue: section) else { return nil } + + switch sectionType { + case .header: + let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: NCShareAdvancePermissionHeader.reuseIdentifier) as? NCShareAdvancePermissionHeader + headerView?.ocId = metadata.ocId + headerView?.shares = self.shares + headerView?.setupUI(with: metadata, linkCount: shareLinks.count, emailCount: shareEmails.count) + return headerView + + case .linkByEmail: + return nil + + case .links: + if isCurrentUser || canReshare { + let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "NCShareEmailLinkHeaderView") as? NCShareEmailLinkHeaderView + headerView?.configure(text: NSLocalizedString("_share_copy_link_", comment: "123")) + return headerView } - } else { - // USER / GROUP etc. - if let cell = tableView.dequeueReusableCell(withIdentifier: "cellUser", for: indexPath) as? NCShareUserCell { - cell.index = indexPath - cell.tableShare = tableShare - cell.isDirectory = metadata.directory - cell.delegate = self - cell.setupCellUI(userId: session.userId, session: session, metadata: metadata) - - cell.buttonMenu.menu = NCContextMenuShare(share: tableShare, isDirectory: metadata.isDirectory, canReshare: canReshare, shareController: self, controller: controller).viewMenu() - cell.buttonMenu.showsMenuAsPrimaryAction = true - - return cell + return nil + + case .emails: + if (isCurrentUser || canReshare) && numberOfRows(in: section) > 0 { + let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "NCShareEmailLinkHeaderView") as? NCShareEmailLinkHeaderView + headerView?.configure(text: NSLocalizedString("_share_shared_with_", comment: "")) + return headerView } + return nil } + } - return UITableViewCell() + func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { + guard let sectionType = ShareSection(rawValue: section) else { return 0 } + + switch sectionType { + case .header: + return 190 + case .linkByEmail: + return 0 + case .links: + return (isCurrentUser || canReshare) ? 44 : 0 + case .emails: + return ((isCurrentUser || canReshare) && numberOfRows(in: section) > 0) ? 44 : 0 + } + } + + func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? { + guard isCurrentUser || canReshare, + let sectionType = ShareSection(rawValue: section) else { + return nil + } + + switch sectionType { + case .links: + let footer = tableView.dequeueReusableHeaderFooterView(withIdentifier: CreateLinkFooterView.reuseIdentifier) as? CreateLinkFooterView + footer?.createButtonAction = { [weak self] in + self?.createLinkClicked(nil) + } + return footer + + case .emails: + if numberOfRows(in: section) == 0 { + return tableView.dequeueReusableHeaderFooterView(withIdentifier: NoSharesFooterView.reuseIdentifier) + } + return nil + case .header, .linkByEmail: + return nil + } } + + func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat { + guard isCurrentUser || canReshare, + let sectionType = ShareSection(rawValue: section) else { + return 0.001 + } + + switch sectionType { + case .links: + return 80 + case .emails: + return numberOfRows(in: section) == 0 ? 100 : 80 + case .header, .linkByEmail: + return 0.001 + } + } + } + // MARK: - CNContactPickerDelegate extension NCShare: CNContactPickerDelegate { @@ -513,32 +648,30 @@ extension NCShare: CNContactPickerDelegate { if contact.emailAddresses.count > 1 { showEmailList(arrEmail: contact.emailAddresses.map({$0.value as String}), sender: picker) } else if let email = contact.emailAddresses.first?.value as? String { - searchField?.text = email + textField?.text = email networking?.getSharees(searchString: email) } } func showEmailList(arrEmail: [String], sender: Any?) { - let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) - + var actions = [NCMenuAction]() for email in arrEmail { - alert.addAction(UIAlertAction(title: email, style: .default) { _ in - self.searchField?.text = email - self.networking?.getSharees(searchString: email) - }) - } - - alert.addAction(UIAlertAction(title: NSLocalizedString("_cancel_", comment: ""), style: .cancel)) - - // iPad popover support - if let popover = alert.popoverPresentationController { - popover.sourceView = self.view - popover.sourceRect = CGRect(x: self.view.bounds.midX, y: self.view.bounds.midY, width: 0, height: 0) - popover.permittedArrowDirections = [] + actions.append( + NCMenuAction( + title: email, + icon: utility.loadImage(named: "email", colors: [NCBrandColor.shared.iconImageColor]), + selected: false, + on: false, + sender: sender, + action: { _ in + self.textField?.text = email + self.networking?.getSharees(searchString: email) + } + ) + ) } - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { - self.present(alert, animated: true) + self.presentMenu(with: actions, sender: sender) } } } @@ -552,12 +685,21 @@ extension NCShare: UISearchBarDelegate { if searchText.isEmpty { dropDown.hide() } else { - perform(#selector(searchSharees(_:)), with: nil, afterDelay: 1) + perform(#selector(searchSharees(_:)), with: nil, afterDelay: 0.5) } } @objc private func searchSharees(_ sender: Any?) { - guard let searchString = searchField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !searchString.isEmpty else { return } +// // https://stackoverflow.com/questions/25471114/how-to-validate-an-e-mail-address-in-swift +// func isValidEmail(_ email: String) -> Bool { +// +// let emailRegEx = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" +// let emailPred = NSPredicate(format: "SELF MATCHES %@", emailRegEx) +// return emailPred.evaluate(with: email) +// } +// guard let searchString = textField?.text, !searchString.isEmpty else { return } + guard let searchString = textField?.text?.trimmingCharacters(in: .whitespacesAndNewlines), !searchString.isEmpty else { return } +// guard let searchString = searchField.text?.trimmingCharacters(in: .whitespacesAndNewlines), !searchString.isEmpty else { return } if searchString.contains("@"), !isValidEmail(searchString) { return } networking?.getSharees(searchString: searchString) } diff --git a/iOSClient/Share/NCShareCommentsCell.swift b/iOSClient/Share/NCShareCommentsCell.swift index 9bbf034968..9c12503cea 100644 --- a/iOSClient/Share/NCShareCommentsCell.swift +++ b/iOSClient/Share/NCShareCommentsCell.swift @@ -26,7 +26,7 @@ import NextcloudKit // MARK: - NCShareCommentsCell -class NCShareCommentsCell: UITableViewCell { +class NCShareCommentsCell: UITableViewCell, NCCellProtocol { @IBOutlet weak var imageItem: UIImageView! @IBOutlet weak var labelUser: UILabel! @@ -35,7 +35,6 @@ class NCShareCommentsCell: UITableViewCell { @IBOutlet weak var labelMessage: UILabel! private var index = IndexPath() - private var avatarButton: UIButton! var tableComments: tableComments? weak var delegate: NCShareCommentsCellDelegate? @@ -44,7 +43,7 @@ class NCShareCommentsCell: UITableViewCell { get { return index } set { index = newValue } } - var avatarImage: UIImageView? { + var avatarImageView: UIImageView? { return imageItem } var fileUser: String? { @@ -55,35 +54,20 @@ class NCShareCommentsCell: UITableViewCell { override func awakeFromNib() { super.awakeFromNib() - avatarButton = UIButton(type: .system) - avatarButton.translatesAutoresizingMaskIntoConstraints = false - avatarButton.backgroundColor = .clear - contentView.addSubview(avatarButton) - NSLayoutConstraint.activate([ - avatarButton.topAnchor.constraint(equalTo: imageItem.topAnchor), - avatarButton.bottomAnchor.constraint(equalTo: imageItem.bottomAnchor), - avatarButton.leadingAnchor.constraint(equalTo: imageItem.leadingAnchor), - avatarButton.trailingAnchor.constraint(equalTo: imageItem.trailingAnchor) - ]) - avatarButton.showsMenuAsPrimaryAction = true - - buttonMenu.showsMenuAsPrimaryAction = true + let tapGesture = UITapGestureRecognizer(target: self, action: #selector(tapAvatarImage(_:))) + imageItem?.addGestureRecognizer(tapGesture) } - func configureAvatarMenu() { - guard let tableComments = tableComments else { - avatarButton.menu = nil - return - } - avatarButton.menu = delegate?.openProfileMenu(with: tableComments) + @objc func tapAvatarImage(_ sender: UITapGestureRecognizer) { + self.delegate?.showProfile(with: tableComments, sender: sender) } - func configureCommentMenu() { - buttonMenu.menu = delegate?.openCommentMenu(with: tableComments) + @IBAction func touchUpInsideMenu(_ sender: Any) { + delegate?.tapMenu(with: tableComments, sender: sender) } } protocol NCShareCommentsCellDelegate: AnyObject { - func openCommentMenu(with tableComments: tableComments?) -> UIMenu? - func openProfileMenu(with tableComment: tableComments?) -> UIMenu? + func tapMenu(with tableComments: tableComments?, sender: Any) + func showProfile(with tableComment: tableComments?, sender: Any) } diff --git a/iOSClient/Share/NCShareCommentsCell.xib b/iOSClient/Share/NCShareCommentsCell.xib index 02a5c9efb6..243aa83341 100755 --- a/iOSClient/Share/NCShareCommentsCell.xib +++ b/iOSClient/Share/NCShareCommentsCell.xib @@ -1,8 +1,9 @@ - + - + + @@ -52,8 +53,8 @@ - diff --git a/iOSClient/Share/NCShareCommon.swift b/iOSClient/Share/NCShareCommon.swift index c79dfa9305..f96b33f55a 100644 --- a/iOSClient/Share/NCShareCommon.swift +++ b/iOSClient/Share/NCShareCommon.swift @@ -22,9 +22,19 @@ import UIKit import DropDown -import NextcloudKit enum NCShareCommon { + static let shareTypeUser = 0 + static let shareTypeGroup = 1 + static let shareTypeLink = 3 + static let shareTypeEmail = 4 + static let shareTypeContact = 5 + static let shareTypeFederated = 6 + static let shareTypeTeam = 7 + static let shareTypeGuest = 8 + static let shareTypeFederatedGroup = 9 + static let shareTypeRoom = 10 + static let itemTypeFile = "file" static let itemTypeFolder = "folder" @@ -58,24 +68,76 @@ enum NCShareCommon { } } - static func getImageShareType(shareType: Int) -> UIImage? { - typealias type = NKShare.ShareType + static func getImageShareType(shareType: Int, isDropDown:Bool = false) -> UIImage? { switch shareType { - case type.group.rawValue: + case NCShareCommon.shareTypeUser: + return UIImage(named: "shareTypeEmail")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) + case NCShareCommon.shareTypeGroup: return UIImage(named: "shareTypeGroup")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) - case type.publicLink.rawValue: + case NCShareCommon.shareTypeLink: return UIImage(named: "shareTypeLink")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) - case type.email.rawValue: - return UIImage(named: "shareTypeEmail")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) - case type.team.rawValue: - return UIImage(named: "shareTypeTeam")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) - case type.federatedGroup.rawValue: + case NCShareCommon.shareTypeEmail: + return UIImage(named: isDropDown ? "email" : "shareTypeUser")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) + case NCShareCommon.shareTypeContact: + return UIImage(named: "shareTypeUser")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) + case NCShareCommon.shareTypeFederated: +// return UIImage(named: "shareTypeUser")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) + return UIImage(named: isDropDown ? "shareTypeUser" : "shareTypeEmail")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) + case NCShareCommon.shareTypeTeam: +// return UIImage(named: "shareTypeTeam")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) + return UIImage(named: "shareTypeCircles")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) + case NCShareCommon.shareTypeGuest: + return UIImage(named: "shareTypeUser")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) + case NCShareCommon.shareTypeFederatedGroup: return UIImage(named: "shareTypeGroup")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) - case type.talkConversation.rawValue: + case NCShareCommon.shareTypeRoom: return UIImage(named: "shareTypeRoom")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) default: return UIImage(named: "shareTypeUser")?.withTintColor(NCBrandColor.shared.textColor, renderingMode: .alwaysOriginal) } } + + static func isLinkShare(shareType: Int) -> Bool { + return shareType == NCShareCommon.shareTypeLink + } + + static func isExternalUserShare(shareType: Int) -> Bool { + return shareType == NCShareCommon.shareTypeEmail + } + + static func isInternalUser(shareType: Int) -> Bool { + return shareType == NCShareCommon.shareTypeUser + } + + static func isFileTypeAllowedForEditing(fileExtension: String, shareType: Int) -> Bool { + if fileExtension == "md" || fileExtension == "txt" { + return true + } else { + return isInternalUser(shareType: shareType) + } + } + + static func isEditingEnabled(isDirectory: Bool, fileExtension: String, shareType: Int) -> Bool { + if !isDirectory {//file + return isFileTypeAllowedForEditing(fileExtension: fileExtension, shareType: shareType) + } else { + return true + } + } + + static func isFileDropOptionVisible(isDirectory: Bool, shareType: Int) -> Bool { + return (isDirectory && (isLinkShare(shareType: shareType) || isExternalUserShare(shareType: shareType))) + } + + static func isCurrentUserIsFileOwner(fileOwnerId: String) -> Bool { + if let currentUser = NCManageDatabase.shared.getActiveTableAccount(), currentUser.userId == fileOwnerId { + return true + } + return false + } + + static func canReshare(withPermission permission: String) -> Bool { + return permission.contains(NCPermissions().permissionCanShare) + } } diff --git a/iOSClient/Share/NCShareEmailFieldCell.swift b/iOSClient/Share/NCShareEmailFieldCell.swift new file mode 100644 index 0000000000..48cdcb1774 --- /dev/null +++ b/iOSClient/Share/NCShareEmailFieldCell.swift @@ -0,0 +1,188 @@ +// +// NCShareEmailFieldCell.swift +// Nextcloud +// +// Created by A200020526 on 01/06/23. +// Copyright © 2023 Marino Faggiana. All rights reserved. +// + +import UIKit +import MarqueeLabel +import NextcloudKit + +enum Tag { + static let searchField = 999 +} + +class NCShareEmailFieldCell: UITableViewCell { + + @IBOutlet weak var searchField: UITextField! + @IBOutlet weak var labelOrLink: UILabel! + @IBOutlet weak var btnContact: UIButton! + @IBOutlet weak var labelSeparator1: UILabel! + @IBOutlet weak var labelSeparator2: UILabel! + @IBOutlet weak var labelSendLinkByMail: UILabel! + @IBOutlet weak var labelSharedWithBy: UILabel! + @IBOutlet weak var labelResharingAllowed: UILabel! + @IBOutlet weak var topConstraintResharingView: NSLayoutConstraint! + @IBOutlet weak var viewOrLinkSeparator: UIView! + + var ocId = "" + + override func awakeFromNib() { + super.awakeFromNib() + } + + override func setSelected(_ selected: Bool, animated: Bool) { + super.setSelected(selected, animated: animated) + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle { + setupCellAppearance() + } + } + + func setupCell(with metadata: tableMetadata) { +// contentView.backgroundColor = NCBrandColor.shared.secondarySystemGroupedBackground + ocId = metadata.ocId + + setupCellAppearance() + updateCanReshareUI() + + setNeedsLayout() + layoutIfNeeded() + } + + func setupCellAppearance() { + configureSearchField() + configureContactButton() + configureLabels() + } + + private func configureSearchField() { + searchField.layer.cornerRadius = 5 + searchField.layer.masksToBounds = true + searchField.layer.borderWidth = 1 + searchField.layer.borderColor = NCBrandColor.shared.label.cgColor + searchField.text = "" + searchField.textColor = NCBrandColor.shared.label + searchField.attributedPlaceholder = NSAttributedString( + string: NSLocalizedString("_shareLinksearch_placeholder_", comment: ""), + attributes: [.foregroundColor: NCBrandColor.shared.gray60] + ) + searchField.tag = Tag.searchField + setDoneButton(sender: searchField) + } + + private func configureContactButton() { + btnContact.layer.cornerRadius = 5 + btnContact.layer.masksToBounds = true + btnContact.layer.borderWidth = 1 + btnContact.layer.borderColor = NCBrandColor.shared.label.cgColor + if let image = UIImage(named: "Contacts")?.withRenderingMode(.alwaysTemplate) { + btnContact.setImage(image, for: .normal) + } + btnContact.tintColor = NCBrandColor.shared.label + + } + + private func configureLabels() { + labelOrLink.text = NSLocalizedString("_share_or_", comment: "") + labelSendLinkByMail.text = NSLocalizedString("_share_send_link_by_mail_", comment: "") + labelSharedWithBy.text = NSLocalizedString("_share_received_shares_text_", comment: "") + labelResharingAllowed.text = NSLocalizedString("_share_reshare_allowed_", comment: "") + + labelSendLinkByMail.textColor = NCBrandColor.shared.label + labelSharedWithBy.textColor = NCBrandColor.shared.label + labelResharingAllowed.textColor = NCBrandColor.shared.label + } + + func updateCanReshareUI() { + guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { return } + + let isCurrentUser = NCShareCommon.isCurrentUserIsFileOwner(fileOwnerId: metadata.ownerId) + let canReshare = (metadata.sharePermissionsCollaborationServices & NCPermissions().permissionShareShare) != 0 + + labelSharedWithBy.isHidden = isCurrentUser + labelResharingAllowed.isHidden = isCurrentUser + + if !canReshare { + searchField.isUserInteractionEnabled = false + searchField.alpha = 0.5 + btnContact.isEnabled = false + btnContact.alpha = 0.5 + } + + if !isCurrentUser { + let ownerName = metadata.ownerDisplayName + let fullText = NSLocalizedString("_share_received_shares_text_", comment: "") + " " + ownerName + let attributed = NSMutableAttributedString(string: fullText) + + if let range = fullText.range(of: ownerName) { + let nsRange = NSRange(range, in: fullText) + attributed.addAttribute(.font, value: UIFont.boldSystemFont(ofSize: 16), range: nsRange) + } + + labelSharedWithBy.attributedText = attributed + labelSharedWithBy.numberOfLines = 0 + + labelResharingAllowed.text = canReshare + ? NSLocalizedString("_share_reshare_allowed_", comment: "") + : NSLocalizedString("_share_reshare_not_allowed_", comment: "") + + topConstraintResharingView.constant = 15 + } else { + topConstraintResharingView.constant = 0 + } + + viewOrLinkSeparator.isHidden = !canReshare + } + + func updateShareUI(ocId: String, count: Int) { + guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { return } + + let isCurrentUser = NCShareCommon.isCurrentUserIsFileOwner(fileOwnerId: metadata.ownerId) + let canReshare = (metadata.sharePermissionsCollaborationServices & NCPermissions().permissionShareShare) != 0 + + if !isCurrentUser { + if canReshare { + labelOrLink.isHidden = true + labelSeparator1.isHidden = true + labelSeparator2.isHidden = true + } + } + } + + @objc func cancelDatePicker() { + self.searchField.endEditing(true) + } + + private func setDoneButton(sender: UITextField) { + let toolbar = UIToolbar() + toolbar.sizeToFit() + let doneButton = UIBarButtonItem( + title: NSLocalizedString("_done_", comment: ""), + style: .plain, + target: self, + action: #selector(cancelDatePicker) + ) + let space = UIBarButtonItem(barButtonSystemItem: .flexibleSpace, target: nil, action: nil) + toolbar.setItems([space, doneButton], animated: false) + sender.inputAccessoryView = toolbar + } + + @IBAction func touchUpInsideFavorite(_ sender: UIButton) { + // Hook for favorite action if needed + } + + @IBAction func touchUpInsideDetails(_ sender: UIButton) { + // Hook for toggling detail visibility if needed + } + + @objc func longTap(_ sender: UIGestureRecognizer) { + let error = NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_copied_path_") + NCContentPresenter().showInfo(error: error) + } +} diff --git a/iOSClient/Share/NCShareEmailFieldCell.xib b/iOSClient/Share/NCShareEmailFieldCell.xib new file mode 100644 index 0000000000..8384397621 --- /dev/null +++ b/iOSClient/Share/NCShareEmailFieldCell.xib @@ -0,0 +1,186 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/iOSClient/Share/NCShareEmailLinkHeaderView.swift b/iOSClient/Share/NCShareEmailLinkHeaderView.swift new file mode 100644 index 0000000000..9502164891 --- /dev/null +++ b/iOSClient/Share/NCShareEmailLinkHeaderView.swift @@ -0,0 +1,43 @@ +// +// NCShareEmailLinkHeaderView.swift +// Nextcloud +// +// Created by A106551118 on 12/08/25. +// Copyright © 2025 Marino Faggiana. All rights reserved. +// + +import UIKit + +class NCShareEmailLinkHeaderView: UITableViewHeaderFooterView { + private let label = UILabel() + static let reuseIdentifier = "NCShareEmailLinkHeaderView" + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + setup() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setup() + } + + private func setup() { + contentView.backgroundColor = .clear + label.translatesAutoresizingMaskIntoConstraints = false + label.font = .systemFont(ofSize: 17, weight: .semibold) + label.textColor = NCBrandColor.shared.textColor + contentView.addSubview(label) + + NSLayoutConstraint.activate([ + label.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), + label.heightAnchor.constraint(equalToConstant: 30), + label.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), + label.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15), + ]) + } + + func configure(text: String) { + label.text = text + } +} diff --git a/iOSClient/Share/NCShareHeader.swift b/iOSClient/Share/NCShareHeader.swift index ea3fa20a9e..8601a84b24 100644 --- a/iOSClient/Share/NCShareHeader.swift +++ b/iOSClient/Share/NCShareHeader.swift @@ -23,25 +23,20 @@ import UIKit import TagListView -import SwiftUI import NextcloudKit class NCShareHeader: UIView { @IBOutlet weak var imageView: UIImageView! @IBOutlet weak var fileName: UILabel! - @IBOutlet weak var fileNameExtension: UILabel! @IBOutlet weak var info: UILabel! @IBOutlet weak var fullWidthImageView: UIImageView! @IBOutlet weak var fileNameTopConstraint: NSLayoutConstraint! @IBOutlet weak var tagListView: TagListView! - private var metadata = tableMetadata() - private var heightConstraintWithImage: NSLayoutConstraint? private var heightConstraintWithoutImage: NSLayoutConstraint? func setupUI(with metadata: tableMetadata) { - self.metadata = metadata.detachedCopy() let utilityFileSystem = NCUtilityFileSystem() if let image = NCUtility().getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024, userId: metadata.userId, urlBase: metadata.urlBase) { fullWidthImageView.image = image @@ -50,7 +45,7 @@ class NCShareHeader: UIView { imageView.isHidden = true } else { if metadata.directory { - imageView.image = metadata.e2eEncrypted ? NCImageCache.shared.getFolderEncrypted(account: metadata.account) : NCImageCache.shared.getFolder(account: metadata.account) + imageView.image = metadata.e2eEncrypted ? NCImageCache.shared.getFolderEncrypted() : NCImageCache.shared.getFolder() } else if !metadata.iconName.isEmpty { imageView.image = NCUtility().loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) } else { @@ -60,17 +55,12 @@ class NCShareHeader: UIView { fileNameTopConstraint.constant -= 45 } - fileName?.numberOfLines = 1 - fileNameExtension?.numberOfLines = 1 - setBidiSafeFilename(metadata.fileNameView, isDirectory: metadata.directory, titleLabel: fileName, extensionLabel: fileNameExtension) - + fileName.text = metadata.fileNameView fileName.textColor = NCBrandColor.shared.textColor - fileNameExtension?.textColor = NCBrandColor.shared.textColor info.textColor = NCBrandColor.shared.textColor2 info.text = utilityFileSystem.transformedSize(metadata.size) + ", " + NCUtility().getRelativeDateTitle(metadata.date as Date) - tagListView.marginY = 8 - refreshTags(metadata.tagNames, tagModels: metadata.tags.map(\.nkTag)) + tagListView.addTags(Array(metadata.tags)) setNeedsLayout() layoutIfNeeded() @@ -81,57 +71,118 @@ class NCShareHeader: UIView { imageView.isHidden = traitCollection.verticalSizeClass != .compact } } +} - func presentTagEditor(from sourceViewController: UIViewController, onApplied: (([NKTag]) -> Void)? = nil) { - let editor = NCTagEditorView( - metadata: metadata.detachedCopy(), - windowScene: sourceViewController.view.window?.windowScene, - onApplied: { [weak self] tags in - guard let self else { return } - self.metadata.tags.removeAll() - self.metadata.tags.append(objectsIn: tags, account: self.metadata.account) - self.refreshTags(tags.map(\.name), tagModels: tags) - onApplied?(tags) - } - ) +class NCShareAdvancePermissionHeader: UITableViewHeaderFooterView { + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var fileName: UILabel! + @IBOutlet weak var info: UILabel! + @IBOutlet weak var favorite: UIButton! + @IBOutlet weak var fullWidthImageView: UIImageView! - let hosting = UIHostingController(rootView: editor) - hosting.title = NSLocalizedString("_tags_", comment: "") - hosting.isModalInPresentation = true + static let reuseIdentifier = "NCShareAdvancePermissionHeader" - if let sheet = hosting.sheetPresentationController { - sheet.detents = [.medium(), .large()] - sheet.selectedDetentIdentifier = .medium - sheet.prefersGrabberVisible = true - } + var ocId = "" + let utility = NCUtility() + let utilityFileSystem = NCUtilityFileSystem() + var shares: (firstShareLink: tableShare?, share: [tableShare]?) = (nil, nil) - sourceViewController.present(hosting, animated: true) - } + func setupUI(with metadata: tableMetadata) { + fileName.textColor = NCBrandColor.shared.label + info.textColor = NCBrandColor.shared.textInfo - private func refreshTags(_ tags: [String], tagModels: [NKTag]? = nil) { - let tagModels = tagModels ?? [] + let isShare = metadata.permissions.contains(NCPermissions().permissionShared) - tagListView.removeAllTags() + if let image = NCUtility().getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024, userId: metadata.userId, urlBase: metadata.urlBase) { + fullWidthImageView.image = image + fullWidthImageView.contentMode = .scaleAspectFill + imageView.isHidden = true + } else { + imageView.isHidden = false + if metadata.e2eEncrypted { + imageView.image = NCImageCache.shared.getFolderEncrypted() + } else if isShare || !metadata.shareType.isEmpty { + imageView.image = NCImageCache.shared.getFolderPublic() + } else if !metadata.shareType.isEmpty { + imageView.image = metadata.shareType.contains(3) + ? NCImageCache.shared.getFolderPublic() + : NCImageCache.shared.getFolderSharedWithMe() + } else if metadata.permissions.contains("S"), (metadata.permissions.range(of: "S") != nil) { + imageView.image = NCImageCache.shared.getImageSharedWithMe() + } else if metadata.directory { + imageView.image = NCImageCache.shared.getFolder() + } else if !metadata.iconName.isEmpty { + imageView.image = NCUtility().loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) + } else { + imageView.image = NCImageCache.shared.getImageFile() + } + } - for tagName in tags { - let matchedTag = tagModels.first { $0.name == tagName } - let displayName = matchedTag?.name ?? tagName - let tagView = tagListView.addTag(displayName) + fileName.text = metadata.fileNameView + fileName.textColor = NCBrandColor.shared.fileFolderName - if let colorHex = matchedTag?.color, let color = UIColor(hex: colorHex) { - tagView.tagBackgroundColor = .clear - tagView.borderColor = color - tagView.textColor = color - tagView.selectedTextColor = color + updateFavoriteIcon(isFavorite: metadata.favorite) + info.text = utilityFileSystem.transformedSize(metadata.size) + ", " + utility.getRelativeDateTitle(metadata.date as Date) + } + + func setupUI(with metadata: tableMetadata, linkCount: Int, emailCount: Int) { + fileName.textColor = NCBrandColor.shared.label + info.textColor = NCBrandColor.shared.textInfo + + let isShare = metadata.permissions.contains(NCPermissions().permissionShared) + let isMounted = metadata.permissions.contains(NCPermissions().permissionMounted) + let hasEmailAndLinkShares = (linkCount > 0 && emailCount > 0) + + if let image = NCUtility().getImage(ocId: metadata.ocId, etag: metadata.etag, ext: NCGlobal.shared.previewExt1024, userId: metadata.userId, urlBase: metadata.urlBase) { + fullWidthImageView.image = image + fullWidthImageView.contentMode = .scaleAspectFill + imageView.isHidden = true + } else { + imageView.isHidden = false + if metadata.directory { + + if metadata.e2eEncrypted { + imageView.image = NCImageCache.shared.getFolderEncrypted() + } else if metadata.permissions.contains("S"), (metadata.permissions.range(of: "S") != nil) { + imageView.image = NCImageCache.shared.getFolderSharedWithMe() + } else if (!metadata.shareType.isEmpty || !(shares.share?.isEmpty ?? true) || (shares.firstShareLink != nil)) || isShare || hasEmailAndLinkShares { + imageView.image = NCImageCache.shared.getFolderPublic() + } else if metadata.mountType == "group" { + imageView.image = NCImageCache.shared.getFolderGroup() + } else if isMounted { + imageView.image = NCImageCache.shared.getFolderExternal() + } else { + imageView.image = NCImageCache.shared.getFolder() + } } else { - tagView.tagBackgroundColor = .clear - tagView.borderColor = .systemGray - tagView.textColor = .systemGray - tagView.selectedTextColor = .systemGray + if metadata.iconName.isEmpty { + imageView.image = NCImageCache.shared.getImageFile() + } else { + imageView.image = NCUtility().loadImage(named: metadata.iconName, useTypeIconFile: true, account: metadata.account) + } } + } - tagView.textFont = UIFont.boldSystemFont(ofSize: 12) + fileName.text = metadata.fileNameView + fileName.textColor = NCBrandColor.shared.fileFolderName + + updateFavoriteIcon(isFavorite: metadata.favorite) + info.text = utilityFileSystem.transformedSize(metadata.size) + ", " + utility.getRelativeDateTitle(metadata.date as Date) + } + + private func updateFavoriteIcon(isFavorite: Bool) { + favorite.setImage(NCUtility().loadImage(named: !isFavorite ? "star" : "star.fill", colors: [NCBrandColor.shared.yellowFavorite], size: 24), for: .normal) + } + + @IBAction func touchUpInsideFavorite(_ sender: UIButton) { + guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { return } + NCNetworking.shared.setStatusWaitFavorite(metadata) { error in + if error == .success { + guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(metadata.ocId) else { return } + self.updateFavoriteIcon(isFavorite: metadata.favorite) + } else { + NCContentPresenter().showError(error: error) + } } } - } diff --git a/iOSClient/Share/NCShareHeader.xib b/iOSClient/Share/NCShareHeader.xib index 50574f7299..88f3d5ea08 100644 --- a/iOSClient/Share/NCShareHeader.xib +++ b/iOSClient/Share/NCShareHeader.xib @@ -1,57 +1,52 @@ - - + + - + + - + - + - + - + - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + - - + + - - + + - - + + - - - - - - - + + + + + + diff --git a/iOSClient/Share/NCShareHeaderView.xib b/iOSClient/Share/NCShareHeaderView.xib index 44b83d3fc7..b7384fb2ec 100644 --- a/iOSClient/Share/NCShareHeaderView.xib +++ b/iOSClient/Share/NCShareHeaderView.xib @@ -1,149 +1,176 @@ - + - - + - - + + - - + + - - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + - + - - - - - - - - - - - - - - - + + diff --git a/iOSClient/Share/NCShareLinkCell.swift b/iOSClient/Share/NCShareLinkCell.swift index b1cf0a3495..737218e32b 100644 --- a/iOSClient/Share/NCShareLinkCell.swift +++ b/iOSClient/Share/NCShareLinkCell.swift @@ -21,25 +21,29 @@ // along with this program. If not, see . import UIKit -import NextcloudKit class NCShareLinkCell: UITableViewCell { - @IBOutlet private weak var imageItem: UIImageView! - @IBOutlet private weak var labelTitle: UILabel! - @IBOutlet private weak var descriptionLabel: UILabel! + @IBOutlet weak var labelTitle: UILabel! + @IBOutlet weak var buttonDetail: UIButton! + @IBOutlet weak var buttonCopy: UIButton! + @IBOutlet weak var btnQuickStatus: UIButton! + @IBOutlet weak var imagePermissionType: UIImageView! + @IBOutlet weak var imageExpiredDateSet: UIImageView! + @IBOutlet weak var imagePasswordSet: UIImageView! + @IBOutlet weak var imageAllowedPermission: UIImageView! + @IBOutlet weak var imageRightArrow: UIImageView! @IBOutlet weak var labelQuickStatus: UILabel! - @IBOutlet weak var statusStackView: UIStackView! - @IBOutlet weak var menuButton: UIButton! - @IBOutlet private weak var copyButton: UIButton! - @IBOutlet weak var imageDownArrow: UIImageView! + @IBOutlet weak var leadingContraintofImageRightArrow: NSLayoutConstraint! + + private let iconShareSize: CGFloat = 200 - var tableShare: tableShare? - var isDirectory = false weak var delegate: NCShareLinkCellDelegate? + + var tableShare: tableShare? var isInternalLink = false + var isDirectory = false var indexPath = IndexPath() - let utility = NCUtility() override func prepareForReuse() { super.prepareForReuse() @@ -47,92 +51,93 @@ class NCShareLinkCell: UITableViewCell { tableShare = nil } - func setupCellUI(titleAppendString: String? = nil) { - var menuImageName = "ellipsis" + override func awakeFromNib() { + super.awakeFromNib() + setupCellAppearance() + } + + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle { + setupCellAppearance() + } + } - menuButton.isHidden = isInternalLink - descriptionLabel.isHidden = !isInternalLink - copyButton.isHidden = !isInternalLink && tableShare == nil - statusStackView.isHidden = isInternalLink + func configure(with share: tableShare?, at indexPath: IndexPath, isDirectory: Bool, title: String) { + self.tableShare = share + self.indexPath = indexPath + self.isDirectory = isDirectory + setupCellAppearance(titleAppendString: title) - copyButton.setImage(UIImage(systemName: "doc.on.doc")?.withTintColor(.label, renderingMode: .alwaysOriginal), for: .normal) - copyButton.accessibilityLabel = NSLocalizedString("_copy_", comment: "") +// let shareLinksCountString = shareLinksCount > 0 ? String(shareLinksCount) : "" +// setupCellAppearance(titleAppendString: shareLinksCountString) +// setupCellAppearance(titleAppendString: String(shareLinksCount)) + } - menuButton.accessibilityLabel = NSLocalizedString("_more_", comment: "") - menuButton.accessibilityIdentifier = "showShareLinkDetails" + private func setupCellAppearance(titleAppendString: String? = nil) { +// contentView.backgroundColor = NCBrandColor.shared.secondarySystemGroupedBackground + labelTitle.textColor = NCBrandColor.shared.label + labelQuickStatus.textColor = NCBrandColor.shared.shareBlueColor - if isInternalLink { - labelTitle.text = NSLocalizedString("_share_internal_link_", comment: "") - descriptionLabel.text = NSLocalizedString("_share_internal_link_des_", comment: "") - imageItem.image = NCUtility().loadImage(named: "square.and.arrow.up.circle.fill", colors: [NCBrandColor.shared.iconImageColor2]) - } else { - labelTitle.text = NSLocalizedString("_share_link_", comment: "") + buttonDetail.setTitleColor(NCBrandColor.shared.shareBlackColor, for: .normal) + buttonCopy.setImage(UIImage(named: "share")?.image(color: NCBrandColor.shared.brand, size: 24), for: .normal) - if let titleAppendString { - labelTitle.text?.append(" (\(titleAppendString))") - } + imageRightArrow.image = UIImage(named: "rightArrow")?.image(color: NCBrandColor.shared.shareBlueColor) + imageExpiredDateSet.image = UIImage(named: "calenderNew")?.image(color: NCBrandColor.shared.shareBlueColor) + imagePasswordSet.image = UIImage(named: "lockNew")?.image(color: NCBrandColor.shared.shareBlueColor) - if let tableShare = tableShare { - if !tableShare.label.isEmpty { - labelTitle.text? += " (\(tableShare.label))" - } + buttonDetail.setTitle(NSLocalizedString("_share_details_", comment: ""), for: .normal) + labelTitle.text = NSLocalizedString("_share_link_", comment: "") + + if let tableShare = tableShare, let titleAppendString { + if !tableShare.label.isEmpty { + labelTitle.text? += " (\(tableShare.label))" } else { - menuImageName = "plus" - menuButton.accessibilityLabel = NSLocalizedString("_add_", comment: "") - menuButton.accessibilityIdentifier = "addShareLink" + labelTitle.text?.append(" \(titleAppendString)") } - - imageItem.image = NCUtility().loadImage(named: "link.circle.fill", colors: [NCBrandColor.shared.getElement(account: tableShare?.account)]) - menuButton.setImage(NCUtility().loadImage(named: menuImageName, colors: [NCBrandColor.shared.iconImageColor]), for: .normal) } + updatePermissionUI() + } - labelTitle.textColor = NCBrandColor.shared.textColor - - statusStackView.isHidden = true - - if let tableShare { - statusStackView.isHidden = false - labelQuickStatus.text = NSLocalizedString("_custom_permissions_", comment: "") + private func updatePermissionUI() { + guard let tableShare = tableShare else { return } - if NCSharePermissions.canEdit(tableShare.permissions, isDirectory: isDirectory) { // Can edit - labelQuickStatus.text = NSLocalizedString("_share_editing_", comment: "") - } - if NCSharePermissions.getPermissionValue(canRead: false, canCreate: true, canEdit: false, canDelete: false, canShare: false, isDirectory: isDirectory) == tableShare.permissions { // File request - labelQuickStatus.text = NSLocalizedString("_share_file_drop_", comment: "") - } - if NCSharePermissions.getPermissionValue(canCreate: false, canEdit: false, canDelete: false, canShare: true, isDirectory: isDirectory) == tableShare.permissions { // Read only - labelQuickStatus.text = NSLocalizedString("_share_read_only_", comment: "") - } + let permissions = NCPermissions() - if tableShare.shareType == NKShare.ShareType.email.rawValue { - labelTitle.text = tableShare.shareWithDisplayname - imageItem.image = NCUtility().loadImage(named: "envelope.circle.fill", colors: [NCBrandColor.shared.getElement(account: tableShare.account)]) - } + if tableShare.permissions == permissions.permissionCreateShare { + labelQuickStatus.text = NSLocalizedString("_share_quick_permission_everyone_can_just_upload_", comment: "") + imagePermissionType.image = UIImage(named: "upload")?.image(color: NCBrandColor.shared.shareBlueColor) + } else if permissions.isAnyPermissionToEdit(tableShare.permissions) { + labelQuickStatus.text = NSLocalizedString("_share_quick_permission_everyone_can_edit_", comment: "") + imagePermissionType.image = UIImage(named: "editNew")?.image(color: NCBrandColor.shared.shareBlueColor) + } else { + labelQuickStatus.text = NSLocalizedString("_share_quick_permission_everyone_can_only_view_", comment: "") + imagePermissionType.image = UIImage(named: "showPasswordNew")?.image(color: NCBrandColor.shared.shareBlueColor) } - statusStackView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(openQuickStatus))) - labelQuickStatus.textColor = NCBrandColor.shared.customer - imageDownArrow.image = utility.loadImage(named: "arrowtriangle.down.circle", colors: [NCBrandColor.shared.customer]) - - menuButton.menu = nil - menuButton.showsMenuAsPrimaryAction = true + imagePasswordSet.isHidden = tableShare.password.isEmpty + imageExpiredDateSet.isHidden = (tableShare.expirationDate == nil) + + leadingContraintofImageRightArrow.constant = (imagePasswordSet.isHidden && imageExpiredDateSet.isHidden) ? 0 : 5 } - @IBAction func touchUpCopy(_ sender: Any) { + // MARK: - Actions + + @IBAction func touchUpInsideCopy(_ sender: Any) { delegate?.tapCopy(with: tableShare, sender: sender) } - @IBAction func touchUpMenu(_ sender: Any) { + @IBAction func touchUpInsideDetail(_ sender: Any) { delegate?.tapMenu(with: tableShare, sender: sender) } - @objc func openQuickStatus(_ sender: UITapGestureRecognizer) { - delegate?.tapQuickStatus(with: tableShare, sender: sender.view ?? sender) + @IBAction func quickStatusClicked(_ sender: UIButton) { + delegate?.quickStatus(with: tableShare, sender: sender) } } protocol NCShareLinkCellDelegate: AnyObject { func tapCopy(with tableShare: tableShare?, sender: Any) func tapMenu(with tableShare: tableShare?, sender: Any) - func tapQuickStatus(with tableShare: tableShare?, sender: Any) + func quickStatus(with tableShare: tableShare?, sender: Any) } diff --git a/iOSClient/Share/NCShareLinkCell.xib b/iOSClient/Share/NCShareLinkCell.xib index 3311593091..0f88db8ca6 100755 --- a/iOSClient/Share/NCShareLinkCell.xib +++ b/iOSClient/Share/NCShareLinkCell.xib @@ -1,148 +1,159 @@ - + - - - + - - + + - - + + - - + + + + + + + - - - - + + + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - + + + + + + + + + + - + - - - - - - - - - - - - - + + + + diff --git a/iOSClient/Share/NCShareNavigationTitleSetting.swift b/iOSClient/Share/NCShareNavigationTitleSetting.swift index 927f21b97f..668d9a9782 100644 --- a/iOSClient/Share/NCShareNavigationTitleSetting.swift +++ b/iOSClient/Share/NCShareNavigationTitleSetting.swift @@ -18,12 +18,13 @@ extension NCShareNavigationTitleSetting where Self: UIViewController { /// Consolidated convenience method to set a view controller navigation title for a share. /// func setNavigationTitle() { - title = NSLocalizedString("_share_", comment: "") + " – " + title = NSLocalizedString("_share_", comment: "") //+ " – " if share.shareType == NKShare.ShareType.publicLink.rawValue { - title! += share.label.isEmpty ? NSLocalizedString("_share_link_", comment: "") : share.label - } else { - title! += share.shareWithDisplayname.isEmpty ? share.shareWith : share.shareWithDisplayname + title! += " – " + (share.label.isEmpty ? NSLocalizedString("_share_link_", comment: "") : share.label) } +// else { +// title! += share.shareWithDisplayname.isEmpty ? share.shareWith : share.shareWithDisplayname +// } } } diff --git a/iOSClient/Share/NCShareNetworking.swift b/iOSClient/Share/NCShareNetworking.swift index 10db2b8eb1..6c15799172 100644 --- a/iOSClient/Share/NCShareNetworking.swift +++ b/iOSClient/Share/NCShareNetworking.swift @@ -28,15 +28,15 @@ class NCShareNetworking: NSObject { let database = NCManageDatabase.shared weak var delegate: NCShareNetworkingDelegate? var view: UIView - let metadata: tableMetadata - let session: NCSession.Session + var metadata: tableMetadata + var session: NCSession.Session let controller: NCMainTabBarController? @MainActor internal var windowScene: UIWindowScene? { SceneManager.shared.getWindowScene(controller: controller) } - + init(metadata: tableMetadata, view: UIView, delegate: NCShareNetworkingDelegate?, @@ -51,6 +51,7 @@ class NCShareNetworking: NSObject { super.init() } + private func readDownloadLimit(account: String, token: String) async throws -> NKDownloadLimit? { return try await withCheckedThrowingContinuation { continuation in NextcloudKit.shared.getDownloadLimit(account: account, token: token) { limit, error in @@ -126,6 +127,39 @@ class NCShareNetworking: NSObject { } } } + + // MARK: - Create Share Link + func createShareLink(password: String?) { + NCActivityIndicator.shared.start(backgroundView: view) + let filenamePath = utilityFileSystem.getRelativeFilePath(metadata.fileName, serverUrl: metadata.serverUrl, session: session) + + NextcloudKit.shared.createShare(path: filenamePath, + shareType: NCShareCommon.shareTypeLink, + shareWith: "", + account: metadata.account) { [weak self] account, share, _, error in + guard let self = self else { return } + NCActivityIndicator.shared.stop() + + if error == .success, let share = share { + let home = self.utilityFileSystem.getHomeServer(session: self.session) + self.database.addShare(account: self.metadata.account, home: home, shares: [share]) + + if !self.metadata.contentType.contains("directory") { + AnalyticsHelper.shared.trackEventWithMetadata(eventName: .EVENT__SHARE_FILE, metadata: self.metadata) + } + + NotificationCenter.default.postOnMainThread(name: NCGlobal.shared.notificationCenterDidCreateShareLink) + // 🔄 ensure we sync DB + UI with server + self.readShare(showLoadingIndicator: false) + } else { + Task { + await showErrorBanner(windowScene: self.windowScene, error: error) + } + } + + self.delegate?.shareCompleted() + } + } func createShare(_ shareable: Shareable, downloadLimit: DownloadLimitViewModel) { NCActivityIndicator.shared.start(backgroundView: view) @@ -167,6 +201,10 @@ class NCShareNetworking: NSObject { self.setShareDownloadLimit(limit, token: share.token) } } + + if !self.metadata.contentType.contains("directory") { + AnalyticsHelper.shared.trackEventWithMetadata(eventName: .EVENT__SHARE_FILE, metadata: self.metadata) + } Task { await NCNetworking.shared.transferDispatcher.notifyAllDelegates { delegate in @@ -272,8 +310,7 @@ class NCShareNetworking: NSObject { self.delegate?.getSharees(sharees: sharees) } else { Task { - let windowScene = await SceneManager.shared.getWindowScene(controller: self.controller) - await showErrorBanner(windowScene: windowScene, error: error) + await showErrorBanner(windowScene: self.windowScene, error: error) } self.delegate?.getSharees(sharees: nil) } diff --git a/iOSClient/Share/NCSharePaging.swift b/iOSClient/Share/NCSharePaging.swift index 70f6bd7353..eae395e7b3 100644 --- a/iOSClient/Share/NCSharePaging.swift +++ b/iOSClient/Share/NCSharePaging.swift @@ -25,19 +25,20 @@ import UIKit import Parchment import NextcloudKit +import MarqueeLabel import TagListView protocol NCSharePagingContent { - var textField: UIView? { get } + var textField: UITextField? { get } } class NCSharePaging: UIViewController { private let pagingViewController = NCShareHeaderViewController() private weak var appDelegate = UIApplication.shared.delegate as? AppDelegate private var currentVC: NCSharePagingContent? + private let applicationHandle = NCApplicationHandle() var metadata = tableMetadata() - var controller: NCMainTabBarController? var pages: [NCBrandOptions.NCInfoPagingTab] = [] var page: NCBrandOptions.NCInfoPagingTab = .activity @@ -51,14 +52,6 @@ class NCSharePaging: UIViewController { navigationItem.leftBarButtonItem = UIBarButtonItem(title: NSLocalizedString("_close_", comment: ""), style: .plain, target: self, action: #selector(exitTapped(_:))) - let manageTagsAction = UIAction(title: NSLocalizedString("_edit_tags_", comment: ""), image: UIImage(systemName: "tag")) { [weak self] _ in - self?.editTagsTapped(nil) - } - - let moreButton = UIBarButtonItem(image: UIImage(systemName: "ellipsis"), style: .plain, target: nil, action: nil) - moreButton.menu = UIMenu(children: [manageTagsAction]) - navigationItem.rightBarButtonItem = moreButton - NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillShow(notification:)), name: UIResponder.keyboardWillShowNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(keyboardWillHide(notification:)), name: UIResponder.keyboardWillHideNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(applicationDidEnterBackground(notification:)), name: UIApplication.didEnterBackgroundNotification, object: nil) @@ -110,6 +103,10 @@ class NCSharePaging: UIViewController { override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) + + // Re-evaluate in-app messages after viewDidAppear + MoEngageAnalytics.shared.displayInAppNotificationSafely(reason: "viewDidAppear") + currentVC = pagingViewController.pageViewController.selectedViewController as? NCSharePagingContent } @@ -123,6 +120,10 @@ class NCSharePaging: UIViewController { if !capabilities.fileSharingApiEnabled && !capabilities.filesComments && capabilities.activity.isEmpty { self.dismiss(animated: false, completion: nil) } + +// pagingViewController.menuItemSize = .fixed( +// width: self.view.bounds.width / CGFloat(self.pages.count), +// height: 40) } override func viewWillDisappear(_ animated: Bool) { @@ -170,20 +171,6 @@ class NCSharePaging: UIViewController { self.dismiss(animated: true, completion: nil) } - @objc func editTagsTapped(_ sender: Any?) { - guard let header = (pagingViewController.view as? NCSharePagingView)?.header else { - return - } - - header.presentTagEditor(from: self) { [weak self] tags in - guard let self else { return } - self.metadata.tags.removeAll() - self.metadata.tags.append(objectsIn: tags, account: self.metadata.account) - self.pagingViewController.metadata.tags.removeAll() - self.pagingViewController.metadata.tags.append(objectsIn: tags, account: self.pagingViewController.metadata.account) - } - } - @objc func applicationDidEnterBackground(notification: Notification) { self.dismiss(animated: false, completion: nil) } @@ -222,10 +209,9 @@ extension NCSharePaging: PagingViewControllerDataSource { } viewController.metadata = metadata viewController.height = height - viewController.controller = controller return viewController } else { - return UIViewController() + return applicationHandle.pagingViewController(pagingViewController, viewControllerAt: index, metadata: metadata, topHeight: height) } } @@ -236,7 +222,7 @@ extension NCSharePaging: PagingViewControllerDataSource { } else if pages[index] == .sharing { return PagingIndexItem(index: index, title: NSLocalizedString("_sharing_", comment: "")) } else { - return PagingIndexItem(index: index, title: "") + return applicationHandle.pagingViewController(pagingViewController, pagingItemAt: index) } } @@ -315,3 +301,46 @@ class NCSharePagingView: PagingView { ]) } } + +class NCShareHeaderView: UIView { + + @IBOutlet weak var imageView: UIImageView! + @IBOutlet weak var path: MarqueeLabel! + @IBOutlet weak var info: UILabel! + @IBOutlet weak var creation: UILabel! + @IBOutlet weak var upload: UILabel! + @IBOutlet weak var favorite: UIButton! + @IBOutlet weak var details: UIButton! + @IBOutlet weak var tagListView: TagListView! + + var ocId = "" + + override func awakeFromNib() { + super.awakeFromNib() + let longGesture = UILongPressGestureRecognizer(target: self, action: #selector(longTap(_:))) + path.addGestureRecognizer(longGesture) + } + + @IBAction func touchUpInsideFavorite(_ sender: UIButton) { + guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(ocId) else { return } + NCNetworking.shared.setStatusWaitFavorite(metadata) { error in + if error == .success { + guard let metadata = NCManageDatabase.shared.getMetadataFromOcId(metadata.ocId) else { return } + self.favorite.setImage(NCUtility().loadImage(named: metadata.favorite ? "star" : "star.fill", colors: [NCBrandColor.shared.yellowFavorite], size: 20), for: .normal) + } else { + NCContentPresenter().showError(error: error) + } + } + } + + @IBAction func touchUpInsideDetails(_ sender: UIButton) { + creation.isHidden = !creation.isHidden + upload.isHidden = !upload.isHidden + } + + @objc func longTap(_ sender: UIGestureRecognizer) { + UIPasteboard.general.string = path.text + let error = NKError(errorCode: NCGlobal.shared.errorInternalError, errorDescription: "_copied_path_") + NCContentPresenter().showInfo(error: error) + } +} diff --git a/iOSClient/Share/NCSharePermissions.swift b/iOSClient/Share/NCSharePermissions.swift index de12f94a59..9f1bc74886 100644 --- a/iOSClient/Share/NCSharePermissions.swift +++ b/iOSClient/Share/NCSharePermissions.swift @@ -48,7 +48,7 @@ enum NCSharePermissions { let canCreate = hasPermissionToCreate(permission) let canEdit = hasPermissionToEdit(permission) let canDelete = hasPermissionToDelete(permission) - return canCreate || canEdit || canDelete + return canCreate || canEdit //|| canDelete } /// "Can edit" means it has can read, create, edit, and delete. @@ -57,7 +57,7 @@ enum NCSharePermissions { let canCreate = isDirectory ? hasPermissionToCreate(permission) : true let canEdit = hasPermissionToEdit(permission) let canDelete = isDirectory ? hasPermissionToDelete(permission) : true - return canCreate && canEdit && canRead && canDelete + return canCreate && canEdit && canRead //&& canDelete } /// Read permission is always true for a share, hence why it's not here. @@ -74,9 +74,9 @@ enum NCSharePermissions { if canEdit { permission = permission + NKShare.Permission.update.rawValue } - if canDelete && isDirectory { - permission = permission + NKShare.Permission.delete.rawValue - } +// if canDelete && isDirectory { +// permission = permission + NKShare.Permission.delete.rawValue +// } if canShare { permission = permission + NKShare.Permission.share.rawValue } diff --git a/iOSClient/Share/NCShareUserCell.swift b/iOSClient/Share/NCShareUserCell.swift index 023eba0edc..9eb20dad56 100644 --- a/iOSClient/Share/NCShareUserCell.swift +++ b/iOSClient/Share/NCShareUserCell.swift @@ -24,89 +24,135 @@ import UIKit import DropDown import NextcloudKit -class NCShareUserCell: UITableViewCell { +class NCShareUserCell: UITableViewCell, NCCellProtocol { + @IBOutlet weak var imageItem: UIImageView! @IBOutlet weak var labelTitle: UILabel! @IBOutlet weak var buttonMenu: UIButton! @IBOutlet weak var imageStatus: UIImageView! @IBOutlet weak var status: UILabel! - @IBOutlet weak var stackViewQuickStatus: UIStackView! + @IBOutlet weak var btnQuickStatus: UIButton! @IBOutlet weak var labelQuickStatus: UILabel! - @IBOutlet weak var imageDownArrow: UIImageView! - - var index = IndexPath() - var avatarButton: UIButton! - - var tableShare: tableShare? + @IBOutlet weak var imagePermissionType: UIImageView! + @IBOutlet weak var imageRightArrow: UIImageView! + @IBOutlet weak var imageExpiredDateSet: UIImageView! + @IBOutlet weak var imagePasswordSet: UIImageView! + @IBOutlet weak var imageAllowedPermission: UIImageView! + @IBOutlet weak var leadingContraintofImageRightArrow: NSLayoutConstraint! + + private var index = IndexPath() + + var tableShare: tableShare? { + didSet { + // When permissions or related fields change (e.g., via Advanced permissions), + // refresh the permission UI and accessory indicators. + updatePermissionUI() + // Also update title if display name changed. + if let share = tableShare { + labelTitle.text = share.shareWithDisplayname + imagePasswordSet.isHidden = share.password.isEmpty + imageExpiredDateSet.isHidden = (share.expirationDate == nil) + leadingContraintofImageRightArrow.constant = (imagePasswordSet.isHidden && imageExpiredDateSet.isHidden) ? 0 : 5 + } + } + } var isDirectory = false let utility = NCUtility() weak var delegate: NCShareUserCellDelegate? - func setupCellUI(userId: String, session: NCSession.Session, metadata: tableMetadata) { - guard let tableShare = tableShare else { - return - } - labelTitle.text = (tableShare.shareWithDisplayname.isEmpty ? tableShare.shareWith : tableShare.shareWithDisplayname) + var indexPath: IndexPath { + get { return index } + set { index = newValue } + } + var avatarImageView: UIImageView? { + return imageItem + } + var fileUser: String? { + get { return tableShare?.shareWith } + set {} + } - let type = getTypeString(tableShare) - if !type.isEmpty { - labelTitle.text?.append(" (\(type))") - } + // MARK: - Lifecycle + override func awakeFromNib() { + super.awakeFromNib() + setupCellUIAppearance() + } - labelTitle.lineBreakMode = .byTruncatingMiddle - labelTitle.textColor = NCBrandColor.shared.textColor - isUserInteractionEnabled = true - labelQuickStatus.isHidden = false - imageDownArrow.isHidden = false - buttonMenu.isHidden = false - buttonMenu.accessibilityLabel = NSLocalizedString("_more_", comment: "") - imageItem.image = NCShareCommon.getImageShareType(shareType: tableShare.shareType) + override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { + super.traitCollectionDidChange(previousTraitCollection) + if previousTraitCollection?.userInterfaceStyle != traitCollection.userInterfaceStyle { + setupCellUIAppearance() + } + } - let status = utility.getUserStatus(userIcon: tableShare.userIcon, userStatus: tableShare.userStatus, userMessage: tableShare.userMessage) - imageStatus.image = status.statusImage - self.status.text = status.statusMessage + // MARK: - Configure + func configure(with share: tableShare?, at indexPath: IndexPath, isDirectory: Bool, userId: String) { + self.indexPath = indexPath + self.tableShare = share + self.isDirectory = isDirectory + setupCellUI(userId: userId) + } - // If the initiator or the recipient is not the current user, show the list of sharees without any options to edit it. - if tableShare.uidOwner != userId && tableShare.uidFileOwner != userId { - isUserInteractionEnabled = false - labelQuickStatus.isHidden = true - imageDownArrow.isHidden = true - buttonMenu.isHidden = true - } + func refresh(with share: tableShare?, userId: String) { + self.tableShare = share + setupCellUI(userId: userId) + } - if NCSharePermissions.canEdit(tableShare.permissions, isDirectory: isDirectory) { // Can edit - labelQuickStatus.text = NSLocalizedString("_share_editing_", comment: "") - } else if tableShare.permissions == NKShare.Permission.read.rawValue { // Read only - labelQuickStatus.text = NSLocalizedString("_share_read_only_", comment: "") - } else { // Custom permissions - labelQuickStatus.text = NSLocalizedString("_custom_permissions_", comment: "") - } + // MARK: - UI Setup + private func setupCellUIAppearance() { +// contentView.backgroundColor = NCBrandColor.shared.secondarySystemGroupedBackground + buttonMenu.contentMode = .scaleAspectFill +// buttonMenu.setImage(NCImageCache.images.buttonMore.image(color: NCBrandColor.shared.brand, size: 24), for: .normal) + buttonMenu.setImage(NCImageCache.shared.getImageButtonMore().image(color: NCBrandColor.shared.brand, size: 24), for: .normal) + labelQuickStatus.textColor = NCBrandColor.shared.shareBlueColor + labelTitle.textColor = NCBrandColor.shared.label + imageRightArrow.image = UIImage(named: "rightArrow")?.image(color: NCBrandColor.shared.shareBlueColor) + imageExpiredDateSet.image = UIImage(named: "calenderNew")?.image(color: NCBrandColor.shared.shareBlueColor) + imagePasswordSet.image = UIImage(named: "lockNew")?.image(color: NCBrandColor.shared.shareBlueColor) + + imagePermissionType.image = imagePermissionType.image?.image(color: NCBrandColor.shared.shareBlueColor) + // Permission UI is updated via tableShare didSet or explicit refresh + } - let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: tableShare.shareWith) - let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) + private func updatePermissionUI() { + guard let tableShare = tableShare else { return } - imageItem.contentMode = .scaleAspectFill + let permissions = NCPermissions() - if tableShare.shareType == NKShare.ShareType.team.rawValue { - imageItem.image = utility.loadImage(named: "custom.person.3.circle.fill", colors: [NCBrandColor.shared.iconImageColor2]) - } else if results.image == nil { - imageItem.image = utility.loadUserImage(for: tableShare.shareWith, displayName: tableShare.shareWithDisplayname, urlBase: metadata.urlBase) + if tableShare.permissions == permissions.permissionCreateShare { + labelQuickStatus.text = NSLocalizedString("_share_quick_permission_everyone_can_just_upload_", comment: "") + imagePermissionType.image = UIImage(named: "upload")?.image(color: NCBrandColor.shared.shareBlueColor) + } else if permissions.isAnyPermissionToEdit(tableShare.permissions) { + labelQuickStatus.text = NSLocalizedString("_share_quick_permission_everyone_can_edit_", comment: "") + imagePermissionType.image = UIImage(named: "editNew")?.image(color: NCBrandColor.shared.shareBlueColor) } else { - imageItem.image = results.image + labelQuickStatus.text = NSLocalizedString("_share_quick_permission_everyone_can_only_view_", comment: "") + imagePermissionType.image = UIImage(named: "showPasswordNew")?.image(color: NCBrandColor.shared.shareBlueColor) } - if !(results.tblAvatar?.loaded ?? false), - NCNetworking.shared.downloadAvatarQueue.operations.filter({ ($0 as? NCOperationDownloadAvatar)?.fileName == fileName }).isEmpty { - NCNetworking.shared.downloadAvatarQueue.addOperation(NCOperationDownloadAvatar(user: tableShare.shareWith, fileName: fileName, account: metadata.account, view: self)) - } + imagePasswordSet.isHidden = tableShare.password.isEmpty + imageExpiredDateSet.isHidden = (tableShare.expirationDate == nil) + + leadingContraintofImageRightArrow.constant = (imagePasswordSet.isHidden && imageExpiredDateSet.isHidden) ? 0 : 5 + } - stackViewQuickStatus.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(openQuickStatus))) + private func setupCellUI(userId: String) { + guard let tableShare = tableShare else { return } - buttonMenu.menu = nil - buttonMenu.showsMenuAsPrimaryAction = true + labelTitle.text = tableShare.shareWithDisplayname - // Configure avatar menu - avatarButton.menu = delegate?.tapProfileMenu(with: tableShare) + let isOwner = tableShare.uidOwner == userId || tableShare.uidFileOwner == userId + isUserInteractionEnabled = isOwner + buttonMenu.isHidden = !isOwner + buttonMenu.accessibilityLabel = NSLocalizedString("_more_", comment: "") + + btnQuickStatus.setTitle("", for: .normal) + btnQuickStatus.isEnabled = true + btnQuickStatus.accessibilityHint = NSLocalizedString("_user_sharee_footer_", comment: "") + btnQuickStatus.contentHorizontalAlignment = .left + + setupCellUIAppearance() + updatePermissionUI() } private func getTypeString(_ tableShare: tableShareV2) -> String { @@ -122,58 +168,55 @@ class NCShareUserCell: UITableViewCell { } } - override func awakeFromNib() { - super.awakeFromNib() - - avatarButton = UIButton(type: .system) - avatarButton.translatesAutoresizingMaskIntoConstraints = false - avatarButton.backgroundColor = .clear - contentView.addSubview(avatarButton) - NSLayoutConstraint.activate([ - avatarButton.topAnchor.constraint(equalTo: imageItem.topAnchor), - avatarButton.bottomAnchor.constraint(equalTo: imageItem.bottomAnchor), - avatarButton.leadingAnchor.constraint(equalTo: imageItem.leadingAnchor), - avatarButton.trailingAnchor.constraint(equalTo: imageItem.trailingAnchor) - ]) - avatarButton.showsMenuAsPrimaryAction = true - - labelQuickStatus.textColor = NCBrandColor.shared.customer - imageDownArrow.image = utility.loadImage(named: "arrowtriangle.down.circle", colors: [NCBrandColor.shared.customer]) + @objc func tapAvatarImage(_ sender: UITapGestureRecognizer) { + delegate?.showProfile(with: tableShare, sender: sender) } @IBAction func touchUpInsideMenu(_ sender: Any) { delegate?.tapMenu(with: tableShare, sender: sender) } - @objc func openQuickStatus(_ sender: UIGestureRecognizer) { - delegate?.tapQuickStatus(with: tableShare, sender: sender.view ?? sender) + @IBAction func quickStatusClicked(_ sender: Any) { + delegate?.quickStatus(with: tableShare, sender: sender) } } protocol NCShareUserCellDelegate: AnyObject { func tapMenu(with tableShare: tableShare?, sender: Any) - func tapProfileMenu(with tableShare: tableShare?) -> UIMenu? - func tapQuickStatus(with tableShare: tableShare?, sender: Any) + func showProfile(with tableComment: tableShare?, sender: Any) + func quickStatus(with tableShare: tableShare?, sender: Any) } // MARK: - NCSearchUserDropDownCell -class NCSearchUserDropDownCell: DropDownCell { +class NCSearchUserDropDownCell: DropDownCell, NCCellProtocol { @IBOutlet weak var imageItem: UIImageView! @IBOutlet weak var imageStatus: UIImageView! @IBOutlet weak var status: UILabel! @IBOutlet weak var imageShareeType: UIImageView! - @IBOutlet weak var centerTitle: NSLayoutConstraint! + @IBOutlet weak var centerTitleConstraint: NSLayoutConstraint! - var user: String = "" - var index = IndexPath() + private var user: String = "" + private var index = IndexPath() private let utilityFileSystem = NCUtilityFileSystem() + var indexPath: IndexPath { + get { return index } + set { index = newValue } + } + var avatarImageView: UIImageView? { + return imageItem + } + var fileUser: String? { + get { return user } + set { user = newValue ?? "" } + } + func setupCell(sharee: NKSharee, session: NCSession.Session) { let utility = NCUtility() - imageItem.image = NCShareCommon.getImageShareType(shareType: sharee.shareType) - imageShareeType.image = NCShareCommon.getImageShareType(shareType: sharee.shareType) +// imageItem.image = NCShareCommon.getImageShareType(shareType: sharee.shareType) + imageShareeType.image = NCShareCommon.getImageShareType(shareType: sharee.shareType, isDropDown: true) let status = utility.getUserStatus(userIcon: sharee.userIcon, userStatus: sharee.userStatus, userMessage: sharee.userMessage) if let statusImage = status.statusImage { @@ -183,41 +226,10 @@ class NCSearchUserDropDownCell: DropDownCell { self.status.text = status.statusMessage if self.status.text?.count ?? 0 > 0 { - centerTitle.constant = -5 + centerTitleConstraint.constant = -5 } else { - centerTitle.constant = 0 - } - - imageItem.image = utility.loadUserImage(for: sharee.shareWith, displayName: nil, urlBase: session.urlBase) - - let fileName = NCSession.shared.getFileName(urlBase: session.urlBase, user: sharee.shareWith) - let results = NCManageDatabase.shared.getImageAvatarLoaded(fileName: fileName) - - if results.image == nil { - let etag = NCManageDatabase.shared.getTableAvatar(fileName: fileName)?.etag - let fileNameLocalPath = utilityFileSystem.createServerUrl(serverUrl: utilityFileSystem.directoryUserData, fileName: fileName) - - NextcloudKit.shared.downloadAvatar( - user: sharee.shareWith, - fileNameLocalPath: fileNameLocalPath, - sizeImage: NCGlobal.shared.avatarSize, - avatarSizeRounded: NCGlobal.shared.avatarSizeRounded, - etagResource: etag, - account: session.account) { task in - Task { - let identifier = await NCNetworking.shared.networkingTasks.createIdentifier(account: session.account, - path: sharee.shareWith, - name: "downloadAvatar") - await NCNetworking.shared.networkingTasks.track(identifier: identifier, task: task) - } - } completion: { _, imageAvatar, _, etag, _, error in - if error == .success, let etag = etag, let imageAvatar = imageAvatar { - NCManageDatabase.shared.addAvatar(fileName: fileName, etag: etag) - self.imageItem.image = imageAvatar - } else if error.errorCode == NCGlobal.shared.errorNotModified, let imageAvatar = NCManageDatabase.shared.setAvatarLoaded(fileName: fileName) { - self.imageItem.image = imageAvatar - } - } + centerTitleConstraint.constant = 0 } } } + diff --git a/iOSClient/Share/NCShareUserCell.xib b/iOSClient/Share/NCShareUserCell.xib index 0cc1819cb1..2a7e57bf77 100755 --- a/iOSClient/Share/NCShareUserCell.xib +++ b/iOSClient/Share/NCShareUserCell.xib @@ -1,149 +1,134 @@ - + - - - + - - + + - - + + - - + - - + + + + + + + + - - + + - - + + + - - - - - - - - - - - - - - + + - - + - - - - - - - - - - + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + - + - - - - - - - - - - - - + + + diff --git a/iOSClient/Share/NoSharesFooterView.swift b/iOSClient/Share/NoSharesFooterView.swift new file mode 100644 index 0000000000..636b319a83 --- /dev/null +++ b/iOSClient/Share/NoSharesFooterView.swift @@ -0,0 +1,63 @@ +// +// NoSharesFooterView.swift +// Nextcloud +// +// Created by A106551118 on 12/08/25. +// Copyright © 2025 Marino Faggiana. All rights reserved. +// + + +import UIKit + +class NoSharesFooterView: UITableViewHeaderFooterView { + + static let reuseIdentifier = "NoSharesFooterView" + + private let titleLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 17, weight: .semibold) + label.textColor = NCBrandColor.shared.textColor + label.text = NSLocalizedString("_share_shared_with_", comment: "") + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + private let infoLabel: UILabel = { + let label = UILabel() + label.font = .systemFont(ofSize: 15, weight: .regular) + label.textColor = NCBrandColor.shared.textColor + label.text = NSLocalizedString("_share_no_shares_text_", comment: "") + label.numberOfLines = 0 + label.lineBreakMode = .byWordWrapping + label.translatesAutoresizingMaskIntoConstraints = false + return label + }() + + override init(reuseIdentifier: String?) { + super.init(reuseIdentifier: reuseIdentifier) + setupUI() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + setupUI() + } + + private func setupUI() { + contentView.backgroundColor = .clear + contentView.addSubview(titleLabel) + contentView.addSubview(infoLabel) + + NSLayoutConstraint.activate([ + titleLabel.topAnchor.constraint(equalTo: contentView.topAnchor, constant: 20), + titleLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), + titleLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15), + titleLabel.heightAnchor.constraint(equalToConstant: 25), + + infoLabel.topAnchor.constraint(equalTo: titleLabel.bottomAnchor, constant: 15), + infoLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 15), + infoLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor, constant: -15), + infoLabel.bottomAnchor.constraint(equalTo: contentView.bottomAnchor) + ]) + } +} diff --git a/iOSClient/Share/ShareDownloadLimitNetwork.swift b/iOSClient/Share/ShareDownloadLimitNetwork.swift new file mode 100644 index 0000000000..18e334f3a4 --- /dev/null +++ b/iOSClient/Share/ShareDownloadLimitNetwork.swift @@ -0,0 +1,156 @@ +// +// ShareDownloadLimitNetwork.swift +// Nextcloud +// +// Created by A118830248 on 11/11/21. +// Copyright © 2021 Marino Faggiana. All rights reserved. +// + +import Foundation +import SwiftyJSON +import NextcloudKit +import Alamofire + +class NMCCommunication: NSObject, XMLParserDelegate { + + public static let shared: NMCCommunication = { + let instance = NMCCommunication() + return instance + }() + + var message = "" + var foundCharacters = ""; + var downloadLimit = DownloadLimit() + private lazy var appDelegate = UIApplication.shared.delegate as? AppDelegate + var controller: NCMainTabBarController! + var session: NCSession.Session { + NCSession.shared.getSession(controller: controller) + } + + func getDownloadLimit(token: String, completion: @escaping (_ downloadLimit: DownloadLimit?, _ errorDescription: String) -> Void) { + let baseUrl = session.urlBase // NCBrandOptions.shared.loginBaseUrl + + func getDownloadLimit(token: String, completion: @escaping (_ downloadLimit: DownloadLimit?, _ errorDescription: String) -> Void) { + let baseUrl = session.urlBase // NCBrandOptions.shared.loginBaseUrl + let endPoint = "/ocs/v2.php/apps/files_downloadlimit/\(token)/limit" + let path = baseUrl+endPoint + do { + var urlRequest = try URLRequest(url: URL(string: path)!, method: .get) + urlRequest.addValue("true", forHTTPHeaderField: "OCS-APIREQUEST") + + let sessionConfiguration = URLSessionConfiguration.default + let urlSession = URLSession(configuration: sessionConfiguration) + + let task = urlSession.dataTask(with: urlRequest) { [self] (data, response, error) in + guard error == nil else { + completion(nil, error?.localizedDescription ?? "") + return + } + + if let httpResponse = response as? HTTPURLResponse { + let statusCode = httpResponse.statusCode + print("url: \(String(describing: httpResponse.url))\nStatus Code: \(statusCode)") + if httpResponse.statusCode == 200 { + let parser = XMLParser(data: data!) + parser.delegate = self + parser.parse() + completion(self.downloadLimit, self.message) + } else { + completion(nil, "Invalid Response code: \(statusCode)") + } + } else { + completion(nil, error?.localizedDescription ?? "Invalid Response") + } + } + task.resume() + } catch { + completion(nil, error.localizedDescription) + } + } + + func setDownloadLimit(deleteLimit: Bool, limit: String, token: String, completion: @escaping (_ success: Bool, _ errorDescription: String) -> Void) { + let baseUrl = session.urlBase //NCBrandOptions.shared.loginBaseUrl + let baseUrl = appDelegate?.urlBase ?? "" //NCBrandOptions.shared.loginBaseUrl + let endPoint = "/ocs/v2.php/apps/files_downloadlimit/\(token)/limit" + let path = baseUrl+endPoint + do { + + let method = deleteLimit ? HTTPMethod.delete : .put + var urlRequest = try URLRequest(url: URL(string: path)!, method: method) + + urlRequest.addValue("true", forHTTPHeaderField: "OCS-APIREQUEST") + urlRequest.addValue(authorizationToken(), forHTTPHeaderField: "Authorization") + urlRequest.addValue("application/json", forHTTPHeaderField: "Content-Type") + + let parameters = ["token": token, "limit": limit] + + let encoder = JSONEncoder() + let jsonData = try encoder.encode(parameters) + urlRequest.httpBody = jsonData + + let sessionConfiguration = URLSessionConfiguration.default + let urlSession = URLSession(configuration: sessionConfiguration) + + let task = urlSession.dataTask(with: urlRequest) { (data, response, error) in + guard error == nil else { + completion(false, error?.localizedDescription ?? "") + return + } + + if let httpResponse = response as? HTTPURLResponse { + let statusCode = httpResponse.statusCode + print("url: \(String(describing: httpResponse.url))\nStatus Code: \(statusCode)") + if httpResponse.statusCode == 200 { + completion(true, error?.localizedDescription ?? "") + } else { + completion(false, "Invalid Response code: \(statusCode)") + } + } else { + completion(false, error?.localizedDescription ?? "Invalid Response") + } + } + task.resume() + } catch { + completion(false, error.localizedDescription) + } + } + + public func authorizationToken() -> String { + let accountDetails = NCManageDatabase.shared.getAllTableAccount().first + let accountDetails = NCManageDatabase.shared.getAllAccount().first + let password = NCKeychain().getPassword(account: accountDetails?.account ?? "") + let password = NCKeychain().getPassword(account: accountDetails?.account ?? "") + let username = accountDetails?.user ?? "" + let credential = Data("\(username):\(password)".utf8).base64EncodedString() + return ("Basic \(credential)") + } + + + // MARK:- XML Parser Delegate + public func parser(_ parser: XMLParser, didStartElement elementName: String, namespaceURI: String?, qualifiedName qName: String?, attributes attributeDict: [String : String] = [:]) { + + } + public func parser(_ parser: XMLParser, foundCharacters string: String) { + self.foundCharacters += string; + } + + public func parser(_ parser: XMLParser, didEndElement elementName: String, namespaceURI: String?, qualifiedName qName: String?) { + if elementName == "limit" { + let limit = self.foundCharacters.replacingOccurrences(of: "\n", with: "") + self.downloadLimit.limit = Int(limit.trimmingCharacters(in: .whitespaces)) + } + if elementName == "count" { + let count = self.foundCharacters.replacingOccurrences(of: "\n", with: "") + self.downloadLimit.count = Int(count.trimmingCharacters(in: .whitespaces)) + } + if elementName == "message"{ + self.message = self.foundCharacters + } + self.foundCharacters = "" + } +} + +//struct DownloadLimit: Codable { +// var limit: Int? +// var count: Int? +//} diff --git a/iOSClient/Utility/NCUtility.swift b/iOSClient/Utility/NCUtility.swift index e64b90735c..6aa3d3810e 100644 --- a/iOSClient/Utility/NCUtility.swift +++ b/iOSClient/Utility/NCUtility.swift @@ -39,32 +39,32 @@ final class NCUtility: NSObject, Sendable { } func editorsDirectEditing(account: String, contentType: String) -> [String] { - var identifiers: [String] = [] + var names: [String] = [] let capabilities = NCNetworking.shared.capabilities[account] capabilities?.directEditingEditors.forEach { editor in editor.mimetypes.forEach { mimetype in if mimetype == contentType { - identifiers.append(editor.identifier) + names.append(editor.name) } // HARDCODE // https://github.com/nextcloud/text/issues/913 if mimetype == "text/markdown" && contentType == "text/x-markdown" { - identifiers.append(editor.identifier) + names.append(editor.name) } if contentType == "text/html" { - identifiers.append(editor.identifier) + names.append(editor.name) } } editor.optionalMimetypes.forEach { mimetype in if mimetype == contentType { - identifiers.append(editor.identifier) + names.append(editor.name) } } } - return Array(Set(identifiers)) + return Array(Set(names)) } func getCustomUserAgentNCText() -> String { @@ -116,6 +116,13 @@ final class NCUtility: NSObject, Sendable { return zeros + fileId } + func getLivePhotoOcId(metadata: tableMetadata) -> String? { + if let instanceId = splitOcId(metadata.ocId).instanceId { + return paddedFileId(metadata.livePhotoFile) + instanceId + } + return nil + } + func getVersionBuild() -> String { if let dictionary = Bundle.main.infoDictionary, let version = dictionary["CFBundleShortVersionString"], @@ -285,4 +292,78 @@ final class NCUtility: NSObject, Sendable { return count.formatted(.number.notation(.compactName).locale(Locale(identifier: "en_US"))) } } + + // E-mail validations + // 1. Basic Email Validator (ASCII only) + func isValidEmail(_ email: String) -> Bool { + let emailRegex = "^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}$" + let predicate = NSPredicate(format: "SELF MATCHES[c] %@", emailRegex) + return predicate.evaluate(with: email) + } + + // 2. Manually Convert Unicode Domain to Punycode with German Char Support + func convertToPunycode(email: String) -> String? { + guard let atIndex = email.firstIndex(of: "@") else { return nil } + + let localPart = String(email[.. String? { + // Mapping of common German characters to their corresponding Punycode equivalents + var punycodeDomain = domain.lowercased() + + let germanCharToPunycode: [String: String] = [ + "ü": "xn--u-1fa", // ü → xn--u-1fa + "ä": "xn--a-1fa", // ä → xn--a-1fa + "ö": "xn--o-1fa", // ö → xn--o-1fa + "ß": "xn--ss-1fa", // ß → xn--ss-1fa + "é": "xn--e-1fa", // é → xn--e-1fa + "è": "xn--e-1f", // è → xn--e-1f + "à": "xn--a-1f", // à → xn--a-1f + ] + + // Replace each German character with the corresponding Punycode equivalent + for (char, punycode) in germanCharToPunycode { + punycodeDomain = punycodeDomain.replacingOccurrences(of: char, with: punycode) + } + + // If no change occurred, return the domain as it is (i.e., no Punycode needed) + return punycodeDomain + } + + // 4. IDN Email Validator (handles Unicode domain by converting to Punycode) + func isValidIDNEmail(_ email: String) -> Bool { + // Convert domain part to Punycode and validate using basic email regex + guard let punycodeEmail = convertToPunycode(email: email) else { + return false + } + + return isValidEmail(punycodeEmail) + } + + // 5. Unified Email Validation - Check for both basic and IDN emails + func validateEmail(_ email: String) -> Bool { + if isValidEmail(email) { + print("Valid ASCII email: \(email)") + return true + } else if isValidIDNEmail(email) { + print("Valid IDN email: \(email)") + return true + } else { + print("Invalid email: \(email)") + return false + } + } }