diff --git a/Privitty.framework/Headers/Privitty.h b/Privitty.framework/Headers/Privitty.h index 0fe051d7c..cc8a19cf6 100644 --- a/Privitty.framework/Headers/Privitty.h +++ b/Privitty.framework/Headers/Privitty.h @@ -131,6 +131,77 @@ FOUNDATION_EXPORT const unsigned char PrivittyVersionString[]; - (nullable NSDictionary*)getConfigWithKey:(NSString*)key; +// ============================================================================= +// FORWARD ACCESS CONTROL (Three-party Trust Model) +// ============================================================================= + +/** + * Forward peer add request - Add a forwardee to forward file access + * @param chatId Chat identifier with the relay/owner + * @param forwardeeChatId Chat identifier with the forwardee + * @param prvFile Path to the .prv file to forward + * @return Result dictionary with PDU (nullable on error) + */ +- (nullable NSDictionary*)processInitForwardPeerAddRequestWithChatId:(NSString*)chatId + forwardeeChatId:(NSString*)forwardeeChatId + prvFile:(NSString*)prvFile; + +/** + * Forwardee initiates forward access request - Request access to a forwarded file + * @param chatId Chat identifier with the relay + * @param filePath Path to the .prv file + * @return Result dictionary with PDU (nullable on error) + */ +- (nullable NSDictionary*)processInitForwardAccessRequestWithChatId:(NSString*)chatId + filePath:(NSString*)filePath; + +/** + * Original owner accepts relay forward access request + * @param chatId Chat identifier with the relay + * @param filePath Path to the .prv file + * @param contactId ID of the forwarder/contact requesting access + * @param accessDuration Duration in seconds for access validity (0 for unlimited) + * @param allowDownload Whether to allow download of the file + * @return Result dictionary with PDU (nullable on error) + */ +- (nullable NSDictionary*)processInitRevertRelayForwardAccessAcceptWithChatId:(NSString*)chatId + filePath:(NSString*)filePath + contactId:(NSString*)contactId + accessDuration:(NSInteger)accessDuration + allowDownload:(BOOL)allowDownload; + +/** + * Original owner denies relay forward access request + * @param chatId Chat identifier with the relay + * @param filePath Path to the .prv file + * @param contactId ID of the forwarder/contact requesting access + * @param denialReason Optional reason for denial (can be nil) + * @return Result dictionary with PDU (nullable on error) + */ +- (nullable NSDictionary*)processInitRevertRelayForwardAccessDeniedWithChatId:(NSString*)chatId + filePath:(NSString*)filePath + contactId:(NSString*)contactId + denialReason:(nullable NSString*)denialReason; + +/** + * Decrypt a forwarded file (file that was forwarded from another peer) + * @param fileId File identifier (actually the prv_file path) + * @param forwarderPeer Peer who forwarded the file (actually the chat_id) + * @return Result dictionary with decryption result (nullable on error) + */ +- (nullable NSDictionary*)processForwardedFileDecryptRequestWithFileId:(NSString*)fileId + forwarderPeer:(NSString*)forwarderPeer; + +/** + * Get detailed file access status list with owner, shared, and forwarded information + * Returns comprehensive view of the file's access control chain in three-party trust model + * @param chatId Chat identifier + * @param filePath Path to the .prv file + * @return Result dictionary with owner_info, shared_info, and forwarded_list (nullable on error) + */ +- (nullable NSDictionary*)getFileAccessStatusListWithChatId:(NSString*)chatId + filePath:(NSString*)filePath; + // ============================================================================= // UNIFIED MESSAGE PROCESSING (PRIMARY METHOD) // ============================================================================= diff --git a/Privitty.framework/Info.plist b/Privitty.framework/Info.plist index 434f3947d..1aa38094f 100644 --- a/Privitty.framework/Info.plist +++ b/Privitty.framework/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.0.3 + 0.0.4 CFBundleVersion 1 MinimumOSVersion diff --git a/Privitty.framework/Privitty b/Privitty.framework/Privitty index 329ba126d..ca25a1bdf 100644 Binary files a/Privitty.framework/Privitty and b/Privitty.framework/Privitty differ diff --git a/Privitty.framework/_CodeSignature/CodeDirectory b/Privitty.framework/_CodeSignature/CodeDirectory index 0380bd218..835f638b6 100644 Binary files a/Privitty.framework/_CodeSignature/CodeDirectory and b/Privitty.framework/_CodeSignature/CodeDirectory differ diff --git a/Privitty.framework/_CodeSignature/CodeRequirements-1 b/Privitty.framework/_CodeSignature/CodeRequirements-1 index 473d0a688..7dfd3417e 100644 Binary files a/Privitty.framework/_CodeSignature/CodeRequirements-1 and b/Privitty.framework/_CodeSignature/CodeRequirements-1 differ diff --git a/Privitty.framework/_CodeSignature/CodeResources b/Privitty.framework/_CodeSignature/CodeResources index 8794f0466..def9a0785 100644 --- a/Privitty.framework/_CodeSignature/CodeResources +++ b/Privitty.framework/_CodeSignature/CodeResources @@ -6,11 +6,11 @@ Headers/Privitty.h - XOZiDTCe+NAWBhmcV7UHd7cQLlA= + 7XJwNRZwtCUxomO8O7IL7fBgT74= Info.plist - gVvxK6BoiqDGhhpTsPceMUMZ6Fg= + g3lk23mWzYzi7ThM/ASFGdIBWjw= Modules/module.modulemap @@ -23,11 +23,11 @@ hash - XOZiDTCe+NAWBhmcV7UHd7cQLlA= + 7XJwNRZwtCUxomO8O7IL7fBgT74= hash2 - lOUboneq2t71vqTNSqqXPH0sq0AwaZ1Nj9DQ6cW0Di8= + Ctj7MZKa+vxsHYPazciSO4F29ZdWOQJWl4BdECBZvS0= Modules/module.modulemap diff --git a/Privitty.framework/_CodeSignature/CodeSignature b/Privitty.framework/_CodeSignature/CodeSignature index 707354834..8d2b472e1 100644 Binary files a/Privitty.framework/_CodeSignature/CodeSignature and b/Privitty.framework/_CodeSignature/CodeSignature differ diff --git a/deltachat-ios.xcodeproj/project.pbxproj b/deltachat-ios.xcodeproj/project.pbxproj index 4c0222134..3e06117da 100644 --- a/deltachat-ios.xcodeproj/project.pbxproj +++ b/deltachat-ios.xcodeproj/project.pbxproj @@ -12,12 +12,12 @@ 0D481EC62ED3EEF700CFB244 /* poppins_semibold.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0D481EC32ED3EEF700CFB244 /* poppins_semibold.ttf */; }; 0D481EC72ED3EEF700CFB244 /* poppins_regular.ttf in Resources */ = {isa = PBXBuildFile; fileRef = 0D481EC22ED3EEF700CFB244 /* poppins_regular.ttf */; }; 0D481EC92ED3F1EB00CFB244 /* AppFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0D481EC82ED3F1EB00CFB244 /* AppFont.swift */; }; + 0D6B54552EEB3B5B006BFEF5 /* Privitty.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F437DF72E94F68900297EED /* Privitty.framework */; }; 0DACEEFC2EE47A7400043D27 /* ContentDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DACEEFB2EE47A7400043D27 /* ContentDetailsViewController.swift */; }; 0DACEF672EE4A7C600043D27 /* ContentDetailsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0DACEF662EE4A7C600043D27 /* ContentDetailsViewController.swift */; }; 21D54500299415B9008B54D5 /* Character+Extentions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D544FF299415B9008B54D5 /* Character+Extentions.swift */; }; 21D6C941260623F500D0755A /* NotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 21D6C9392606190600D0755A /* NotificationManager.swift */; }; 2F11E3E62E9FABD900CA4BB4 /* PrvContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F11E3E52E9FABD900CA4BB4 /* PrvContext.swift */; }; - 2F2A09F22EC24FCC00A37097 /* Privitty.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2F437DF72E94F68900297EED /* Privitty.framework */; }; 2F2A09F32EC24FCC00A37097 /* Privitty.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 2F437DF72E94F68900297EED /* Privitty.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 3008CB7224F93EB900E6A617 /* AudioMessageCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7124F93EB900E6A617 /* AudioMessageCell.swift */; }; 3008CB7424F9436C00E6A617 /* AudioPlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3008CB7324F9436C00E6A617 /* AudioPlayerView.swift */; }; @@ -733,7 +733,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 2F2A09F22EC24FCC00A37097 /* Privitty.framework in Frameworks */, + 0D6B54552EEB3B5B006BFEF5 /* Privitty.framework in Frameworks */, 8A19190635E24C1DF7F0A35D /* Pods_deltachat_ios.framework in Frameworks */, D89EC91A2D4BDEB800352298 /* PhotosUI.framework in Frameworks */, ); diff --git a/deltachat-ios/Chat/ChatViewController.swift b/deltachat-ios/Chat/ChatViewController.swift index 6936ff732..9802c1a10 100644 --- a/deltachat-ios/Chat/ChatViewController.swift +++ b/deltachat-ios/Chat/ChatViewController.swift @@ -1873,27 +1873,27 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate { } private func stageVCard(url: URL) { - draft.setAttachment(viewType: DC_MSG_VCARD, path: url.relativePath) + draft.setAttachment(viewType: DC_MSG_VCARD, path: url.path) configureDraftArea(draft: draft) focusInputTextView() - FileHelper.deleteFileAsync(atPath: url.relativePath) + // Note: File cleanup handled by Delta Chat core } private func stageDocument(url: NSURL) { DispatchQueue.main.async { [weak self] in guard let self else { return } - self.draft.setAttachment(viewType: url.pathExtension == "xdc" ? DC_MSG_WEBXDC : DC_MSG_FILE, path: url.relativePath) + self.draft.setAttachment(viewType: url.pathExtension == "xdc" ? DC_MSG_WEBXDC : DC_MSG_FILE, path: url.path) self.configureDraftArea(draft: self.draft) self.focusInputTextView() - FileHelper.deleteFileAsync(atPath: url.relativePath) + // Note: File cleanup handled by Delta Chat core } } private func stageVideo(url: NSURL) { - self.draft.setAttachment(viewType: DC_MSG_VIDEO, path: url.relativePath) + self.draft.setAttachment(viewType: DC_MSG_VIDEO, path: url.path) self.configureDraftArea(draft: self.draft) self.focusInputTextView() - FileHelper.deleteFileAsync(atPath: url.relativePath) + // Note: File cleanup handled by Delta Chat core } private func stageImage(url: NSURL) { @@ -1920,7 +1920,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate { } self.configureDraftArea(draft: self.draft) self.focusInputTextView() - FileHelper.deleteFileAsync(atPath: pathInCachesDir) + // Note: File cleanup will be handled by Delta Chat core after message is sent } } } @@ -1931,7 +1931,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate { guard let self else { return } if let path = ImageFormat.saveImage(image: image, directory: .cachesDirectory) { self.sendAttachmentMessage(viewType: DC_MSG_IMAGE, filePath: path, message: message) - FileHelper.deleteFileAsync(atPath: path) + // Note: File cleanup handled by Delta Chat core after message is sent } } } @@ -1939,8 +1939,8 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate { private func sendVideo(url: URL, message: String? = nil) { DispatchQueue.global().async { [weak self] in guard let self else { return } - self.sendAttachmentMessage(viewType: DC_MSG_VIDEO, filePath: url.relativePath, message: message) - FileHelper.deleteFileAsync(atPath: url.relativePath) + self.sendAttachmentMessage(viewType: DC_MSG_VIDEO, filePath: url.path, message: message) + // Note: File cleanup handled by Delta Chat core after message is sent } } @@ -1957,7 +1957,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate { } self.sendAttachmentMessage(viewType: DC_MSG_STICKER, filePath: path, message: nil, quoteMessage: self.draft.quoteMessage) - FileHelper.deleteFileAsync(atPath: path) + // Note: File cleanup handled by Delta Chat core self.draft.clear() DispatchQueue.main.async { self.draftArea.quotePreview.cancel() @@ -2081,7 +2081,7 @@ class ChatViewController: UITableViewController, UITableViewDropDelegate { if let quoteMessage = self.draft.quoteMessage { msg.quoteMessage = quoteMessage } - msg.setFile(filepath: url.relativePath, mimeType: "audio/m4a") + msg.setFile(filepath: url.path, mimeType: "audio/m4a") self.dcContext.sendMessage(chatId: self.chatId, message: msg) DispatchQueue.main.async { self.draft.setQuote(quotedMsg: nil) @@ -2375,17 +2375,18 @@ extension ChatViewController { ) } - if !dcChat.isSelfTalk && message.canSave { - if message.savedMessageId != 0 { - children.append( - UIAction.menuAction(localizationKey: "unsave", systemImageName: "bookmark.slash.fill", with: messageId, action: toggleSave) - ) - } else { - children.append( - UIAction.menuAction(localizationKey: "save_desktop", systemImageName: "bookmark", with: messageId, action: toggleSave) - ) - } - } + // MARK: - Save menu hidden for Privitty + // if !dcChat.isSelfTalk && message.canSave { + // if message.savedMessageId != 0 { + // children.append( + // UIAction.menuAction(localizationKey: "unsave", systemImageName: "bookmark.slash.fill", with: messageId, action: toggleSave) + // ) + // } else { + // children.append( + // UIAction.menuAction(localizationKey: "save_desktop", systemImageName: "bookmark", with: messageId, action: toggleSave) + // ) + // } + // } if let link = isLinkTapped(indexPath: indexPath, point: point) { children.append( @@ -2534,6 +2535,7 @@ extension ChatViewController { !indexPaths.isEmpty { editingBar.isEnabled = true evaluateMoreButton() + evaluateForwardButton() } else { editingBar.isEnabled = false } @@ -2570,7 +2572,26 @@ extension ChatViewController { } private func evaluateMoreButton() { - editingBar.moreButton.isEnabled = canSaveOrUnsaveMultiple().0 || canResend() + // Only enable more button for resend (save functionality is hidden) + editingBar.moreButton.isEnabled = canResend() + } + + private func hasFileInSelection() -> Bool { + guard let rows = tableView.indexPathsForSelectedRows else { return false } + let msgIds = rows.compactMap { messageIds[$0.row] } + for msgId in msgIds { + let msg = dcContext.getMessage(id: msgId) + if msg.file != nil { + return true + } + } + return false + } + + private func evaluateForwardButton() { + // Hide forward button if any selected message contains a file + editingBar.forwardButton.isHidden = hasFileInSelection() + editingBar.forwardButton.isEnabled = !hasFileInSelection() } func setEditing(isEditing: Bool, selectedAtIndexPath: IndexPath? = nil) { @@ -2580,6 +2601,13 @@ extension ChatViewController { if let indexPath = selectedAtIndexPath { _ = handleSelection(indexPath: indexPath) } + + // Reset forward button visibility when exiting edit mode + if !isEditing { + editingBar.forwardButton.isHidden = false + editingBar.forwardButton.isEnabled = true + } + self.updateTitle() if refreshMessagesAfterEditing && isEditing == false { refreshMessages() @@ -3174,9 +3202,23 @@ extension ChatViewController: MediaPickerDelegate { if let typeIdentifier = itemProvider.registeredTypeIdentifiers.first { itemProvider.loadFileRepresentation(forTypeIdentifier: typeIdentifier) { [weak self] url, error in if let url { - var copyURL = URL.temporaryDirectory.appendingPathComponent(url.lastPathComponent) - copyURL = FileHelper.copyIfPossible(src: url, dest: copyURL) - self?.stageDocument(url: copyURL as NSURL) + // Check if this is an iOS temporary photo file (.pvt) + if url.pathExtension.lowercased() == "pvt" || url.path.contains("/tmp/") && url.lastPathComponent.hasPrefix("IMG_") { + // Load as image instead of treating as file + if let image = ImageFormat.loadImageFrom(url: url) { + self?.stageImage(image) + } else { + // Failed to load as image, fall back to file handling + var copyURL = URL.temporaryDirectory.appendingPathComponent(url.lastPathComponent) + copyURL = FileHelper.copyIfPossible(src: url, dest: copyURL) + self?.stageDocument(url: copyURL as NSURL) + } + } else { + // Regular file, copy and stage normally + var copyURL = URL.temporaryDirectory.appendingPathComponent(url.lastPathComponent) + copyURL = FileHelper.copyIfPossible(src: url, dest: copyURL) + self?.stageDocument(url: copyURL as NSURL) + } } else if let error { self?.logAndAlert(error: error.localizedDescription) } @@ -3323,12 +3365,13 @@ extension ChatViewController: ChatEditingDelegate { self?.onResendActionPressed() }) } - let (canSaveOrUnsave, doSave) = canSaveOrUnsaveMultiple() - if canSaveOrUnsave { - actions.append(UIAction(title: String.localized(doSave ? "save_desktop" : "unsave"), image: UIImage(systemName: doSave ? "bookmark" : "bookmark.slash.fill")) { [weak self] _ in - self?.onMultipleSaveOrUnsave(doSave: doSave) - }) - } + // MARK: - Save menu hidden for Privitty + // let (canSaveOrUnsave, doSave) = canSaveOrUnsaveMultiple() + // if canSaveOrUnsave { + // actions.append(UIAction(title: String.localized(doSave ? "save_desktop" : "unsave"), image: UIImage(systemName: doSave ? "bookmark" : "bookmark.slash.fill")) { [weak self] _ in + // self?.onMultipleSaveOrUnsave(doSave: doSave) + // }) + // } return UIMenu(children: actions) } @@ -3559,9 +3602,9 @@ extension ChatViewController: BackButtonUpdateable { extension ChatViewController: AppPickerViewControllerDelegate { func pickedApp(_ viewController: AppPickerViewController, fileURL url: URL) { - draft.setAttachment(viewType: DC_MSG_WEBXDC, path: url.relativePath) + draft.setAttachment(viewType: DC_MSG_WEBXDC, path: url.path) configureDraftArea(draft: draft) focusInputTextView() - FileHelper.deleteFileAsync(atPath: url.relativePath) + // Note: File cleanup handled by Delta Chat core } } diff --git a/deltachat-ios/Chat/DraftModel.swift b/deltachat-ios/Chat/DraftModel.swift index f47b7b7d5..1517bdb52 100644 --- a/deltachat-ios/Chat/DraftModel.swift +++ b/deltachat-ios/Chat/DraftModel.swift @@ -16,7 +16,7 @@ public class DraftModel { return draftMsg?.quoteText } var attachment: String? { - return draftMsg?.fileURL?.relativePath + return draftMsg?.fileURL?.path } var attachmentMimeType: String? { return draftMsg?.filemime diff --git a/deltachat-ios/Chat/Views/FileView.swift b/deltachat-ios/Chat/Views/FileView.swift index f0f806be8..5dfc71ebb 100644 --- a/deltachat-ios/Chat/Views/FileView.swift +++ b/deltachat-ios/Chat/Views/FileView.swift @@ -176,36 +176,36 @@ public class FileView: UIView { outerBoxView.bottomAnchor.constraint(equalTo: bottomAnchor), // File type label at top of outer box (leave room for bell) - fileTypeLabel.topAnchor.constraint(equalTo: outerBoxView.topAnchor, constant: 8), + fileTypeLabel.topAnchor.constraint(equalTo: outerBoxView.topAnchor, constant: 6), fileTypeLabel.leadingAnchor.constraint(equalTo: outerBoxView.leadingAnchor, constant: 12), fileTypeLabel.trailingAnchor.constraint(equalTo: bellIconView.leadingAnchor, constant: -8), // Bell icon at top-right corner, aligned with file type label bellIconView.centerYAnchor.constraint(equalTo: fileTypeLabel.centerYAnchor), bellIconView.trailingAnchor.constraint(equalTo: outerBoxView.trailingAnchor, constant: -12), - bellIconView.widthAnchor.constraint(equalToConstant: 20), - bellIconView.heightAnchor.constraint(equalToConstant: 20), + bellIconView.widthAnchor.constraint(equalToConstant: 18), + bellIconView.heightAnchor.constraint(equalToConstant: 18), // Inner box below file type label - innerBoxView.topAnchor.constraint(equalTo: fileTypeLabel.bottomAnchor, constant: 6), + innerBoxView.topAnchor.constraint(equalTo: fileTypeLabel.bottomAnchor, constant: 4), innerBoxView.leadingAnchor.constraint(equalTo: outerBoxView.leadingAnchor, constant: 8), innerBoxView.trailingAnchor.constraint(equalTo: outerBoxView.trailingAnchor, constant: -8), // Inner stack fills inner box with padding - innerStackView.topAnchor.constraint(equalTo: innerBoxView.topAnchor, constant: 8), + innerStackView.topAnchor.constraint(equalTo: innerBoxView.topAnchor, constant: 6), innerStackView.leadingAnchor.constraint(equalTo: innerBoxView.leadingAnchor, constant: 8), innerStackView.trailingAnchor.constraint(equalTo: innerBoxView.trailingAnchor, constant: -8), - innerStackView.bottomAnchor.constraint(equalTo: innerBoxView.bottomAnchor, constant: -8), + innerStackView.bottomAnchor.constraint(equalTo: innerBoxView.bottomAnchor, constant: -6), // File icon size - fileImageView.widthAnchor.constraint(equalToConstant: 32), - fileImageView.heightAnchor.constraint(equalToConstant: 32), + fileImageView.widthAnchor.constraint(equalToConstant: 28), + fileImageView.heightAnchor.constraint(equalToConstant: 28), // Access until label below inner box - accessUntilLabel.topAnchor.constraint(equalTo: innerBoxView.bottomAnchor, constant: 6), + accessUntilLabel.topAnchor.constraint(equalTo: innerBoxView.bottomAnchor, constant: 4), accessUntilLabel.leadingAnchor.constraint(equalTo: outerBoxView.leadingAnchor, constant: 12), accessUntilLabel.trailingAnchor.constraint(equalTo: outerBoxView.trailingAnchor, constant: -12), - accessUntilLabel.bottomAnchor.constraint(equalTo: outerBoxView.bottomAnchor, constant: -8) + accessUntilLabel.bottomAnchor.constraint(equalTo: outerBoxView.bottomAnchor, constant: -6) ]) } diff --git a/deltachat-ios/Controller/AccountSetup/WelcomeViewController.swift b/deltachat-ios/Controller/AccountSetup/WelcomeViewController.swift index 661855068..c6cee9ba9 100644 --- a/deltachat-ios/Controller/AccountSetup/WelcomeViewController.swift +++ b/deltachat-ios/Controller/AccountSetup/WelcomeViewController.swift @@ -638,7 +638,7 @@ extension WelcomeViewController: MediaPickerDelegate { securityScopedResource = url } - if let selectedBackupFilePath = url.relativePath { + if let selectedBackupFilePath = url.path { addProgressHudBackupListener(importByFile: true) importBackup(at: selectedBackupFilePath) } else { diff --git a/deltachat-ios/Controller/ContentDetailsViewController.swift b/deltachat-ios/Controller/ContentDetailsViewController.swift index 49c3414b8..9150bd34b 100644 --- a/deltachat-ios/Controller/ContentDetailsViewController.swift +++ b/deltachat-ios/Controller/ContentDetailsViewController.swift @@ -181,6 +181,186 @@ class ContentDetailsViewController: UIViewController { super.viewDidLoad() setupUI() configureContent() + fetchDetailedAccessStatus() + } + + // MARK: - Fetch Detailed Access Status + + private func fetchDetailedAccessStatus() { + // Get file path from message + guard let filePath = message.file else { + logger.warning("ContentDetails: No file path in message") + return + } + + let chatId = String(message.chatId) + + logger.info("========================================") + logger.info("ContentDetails: Fetching detailed access status") + logger.info("Chat ID: \(chatId)") + logger.info("File Path: \(filePath)") + logger.info("========================================") + + // Call the new method + guard let result = PrvContext.shared.getFileAccessStatusListWithChatId( + chatId: chatId, + filePath: filePath + ) else { + logger.error("ContentDetails: getFileAccessStatusListWithChatId returned nil") + return + } + + // Print result to console + print("========================================") + print("📋 RESULT FROM getFileAccessStatusListWithChatId:") + print(result) + print("========================================") + + logger.info("========================================") + logger.info("ContentDetails: RAW RESPONSE FROM NATIVE LIBRARY:") + logger.info("\(result)") + logger.info("========================================") + + // Check if method is not implemented + if let error = result["error"] as? String, + error.contains("not implemented") { + logger.warning("ContentDetails: Method not yet implemented in framework") + DispatchQueue.main.async { [weak self] in + let alert = UIAlertController( + title: "Feature Not Available", + message: "This feature requires an updated version of the Privitty framework. Please contact your administrator.", + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "OK", style: .default)) + self?.present(alert, animated: true) + } + return + } + + // Check success + if let success = result["success"] as? Bool, success { + logger.info("ContentDetails: ✅ Success = true") + + // Parse message + if let message = result["message"] as? String { + logger.info("ContentDetails: Message: \(message)") + } + + // Parse data + if let data = result["data"] as? [String: Any] { + logger.info("========================================") + logger.info("ContentDetails: DATA OBJECT:") + logger.info("\(data)") + logger.info("========================================") + + // Parse top-level fields + if let chatId = data["chat_id"] as? String { + logger.info("ContentDetails: - chat_id: \(chatId)") + } + if let fileName = data["file_name"] as? String { + logger.info("ContentDetails: - file_name: \(fileName)") + } + + // Parse file object + if let file = data["file"] as? [String: Any] { + logger.info("========================================") + logger.info("ContentDetails: FILE OBJECT:") + logger.info("\(file)") + logger.info("========================================") + + // Parse owner_info + if let ownerInfo = file["owner_info"] as? [String: Any] { + logger.info("========================================") + logger.info("ContentDetails: OWNER INFO:") + if let contactName = ownerInfo["contact_name"] as? String { + logger.info(" 📧 Original Owner: \(contactName)") + } + if let contactId = ownerInfo["contact_id"] as? String { + logger.info(" 📧 Owner ID: \(contactId)") + } + logger.info("========================================") + } else { + logger.info("ContentDetails: ⚠️ No owner_info found") + } + + // Parse shared_info + if let sharedInfo = file["shared_info"] as? [String: Any] { + logger.info("========================================") + logger.info("ContentDetails: SHARED INFO (I am):") + if let contactName = sharedInfo["contact_name"] as? String { + logger.info(" 👤 My Name: \(contactName)") + } + if let contactId = sharedInfo["contact_id"] as? String { + logger.info(" 📧 My ID: \(contactId)") + } + if let status = sharedInfo["status"] as? String { + logger.info(" 📊 Status: \(status)") + } + if let expiryTime = sharedInfo["expiry_time"] as? String { + logger.info(" ⏰ Expiry: \(expiryTime)") + } + if let accessDuration = sharedInfo["access_duration"] as? String { + logger.info(" ⏱️ Duration: \(accessDuration)s") + } + if let downloadAllowed = sharedInfo["download_allowed"] as? Bool { + logger.info(" ⬇️ Download Allowed: \(downloadAllowed)") + } + if let forwardAllowed = sharedInfo["forward_allowed"] as? Bool { + logger.info(" ↗️ Forward Allowed: \(forwardAllowed)") + } + logger.info("========================================") + } else { + logger.info("ContentDetails: ⚠️ No shared_info found") + } + + // Parse forwarded_list + if let forwardedList = file["forwarded_list"] as? [[String: Any]] { + logger.info("========================================") + logger.info("ContentDetails: FORWARDED LIST:") + logger.info(" 📤 I have forwarded to \(forwardedList.count) people") + logger.info("========================================") + + for (index, forwardee) in forwardedList.enumerated() { + logger.info(" [\(index + 1)]:") + if let name = forwardee["contact_name"] as? String { + logger.info(" 👤 Name: \(name)") + } + if let contactId = forwardee["contact_id"] as? String { + logger.info(" 📧 ID: \(contactId)") + } + if let status = forwardee["status"] as? String { + logger.info(" 📊 Status: \(status)") + } + if let expiryTime = forwardee["expiry_time"] as? String { + logger.info(" ⏰ Expiry: \(expiryTime)") + } + if let downloadAllowed = forwardee["download_allowed"] as? Bool { + logger.info(" ⬇️ Download: \(downloadAllowed)") + } + logger.info(" ────────────────────────────────────") + } + } else { + logger.info("ContentDetails: ⚠️ No forwarded_list found or empty") + } + } else { + logger.warning("ContentDetails: ⚠️ No 'file' object in data") + } + } else { + logger.warning("ContentDetails: ⚠️ No 'data' object in response") + } + } else { + logger.error("ContentDetails: ❌ Success = false") + if let error = result["error"] as? String { + logger.error("ContentDetails: Error: \(error)") + } + if let message = result["message"] as? String { + logger.error("ContentDetails: Message: \(message)") + } + } + + logger.info("========================================") + logger.info("ContentDetails: END OF DETAILED ACCESS STATUS") + logger.info("========================================") } // MARK: - Setup diff --git a/deltachat-ios/DC/PrvContext.swift b/deltachat-ios/DC/PrvContext.swift index efc31d709..35800f979 100644 --- a/deltachat-ios/DC/PrvContext.swift +++ b/deltachat-ios/DC/PrvContext.swift @@ -22,12 +22,7 @@ public class PrvContext { public func initialize() -> Bool { // Check if already initialized if let existingCore = core { - logger.debug("Core object exists, checking if initialized...") - - // Safely check initialization let isInit = existingCore.isInitialized() - logger.debug("Core isInitialized: \(isInit)") - if isInit { logger.info("Privitty Core already initialized") return true @@ -35,19 +30,15 @@ public class PrvContext { } logger.info("Creating new Privitty Core instance...") - logger.debug("Base directory: \(documentsPath)") guard let newCore = PrivittyCore(baseDirectory: documentsPath) else { logger.error("Failed to create PrivittyCore object") return false } - // Verify it initialized properly let isInit = newCore.isInitialized() - logger.debug("New core isInitialized: \(isInit)") if isInit { - // Store the core only if it's initialized core = newCore // Get and print version information @@ -55,8 +46,6 @@ public class PrvContext { let success = versionResponse["success"] as? Int, success == 1, let data = versionResponse["data"] as? [String: Any] { - logger.debug("Raw version response: \(versionResponse)") - if let coreVersion = data["core_version"] as? String { logger.info("Privitty Core Version: \(coreVersion)") } @@ -96,13 +85,10 @@ public class PrvContext { /// Check if core is initialized public func isInitialized() -> Bool { guard let existingCore = core else { - logger.debug("isInitialized: core is nil") return false } - let isInit = existingCore.isInitialized() - logger.debug("isInitialized: \(isInit)") - return isInit + return existingCore.isInitialized() } /// Shutdown core @@ -136,13 +122,10 @@ public class PrvContext { // Check if user is already selected if let currentUser = core.getCurrentUser(), currentUser == username { - logger.debug("User '\(username)' already selected") return true } logger.info("Switching to user profile: \(username)") - logger.debug("User email: \(useremail.isEmpty ? "(none)" : useremail)") - logger.debug("User ID: \(userid.isEmpty ? "(none)" : userid)") if core.switchProfile(withUsername: username, useremail: useremail, userid: userid) { logger.info("Successfully switched to user: \(username)") @@ -297,7 +280,6 @@ public class PrvContext { let selfEmail = dcContext.getConfig("configured_addr") ?? "" logger.info("Creating/switching Privitty user: \(userName)") - logger.debug("Email: \(selfEmail)") // Call switchProfile with all three parameters (userid is empty string) if ensureUserSetup(username: userName, useremail: selfEmail, userid: "") { @@ -328,9 +310,6 @@ public class PrvContext { } logger.info("Creating peer add request...") - logger.debug("Current user: \(currentUser)") - logger.debug("Chat ID: \(chatId)") - logger.debug("Peer: \(peerName)") let result = core.createPeerAddRequest( withChatId: chatId, @@ -349,7 +328,6 @@ public class PrvContext { let data = resultDict["data"] as? [String: Any], let pdu = data["pdu"] as? String { logger.info("Peer add request created successfully") - logger.debug("PDU length: \(pdu.count) characters") return (true, pdu, nil) } else if let error = resultDict["error"] as? String { logger.error("Failed to create peer add request: \(error)") @@ -373,7 +351,6 @@ public class PrvContext { } logger.info("Processing incoming message for user: \(currentUser)") - logger.debug("Chat ID: \(chatId), Direction: \(direction), PDU length: \(pdu.count) characters") let eventDataJson = """ { @@ -411,9 +388,7 @@ public class PrvContext { return false } - let isProtected = core.isChatProtected(chatId) - logger.debug("Chat \(chatId) protection status: \(isProtected)") - return isProtected + return core.isChatProtected(chatId) } /// Check if a message is a Privitty message @@ -448,11 +423,6 @@ public class PrvContext { } logger.info("Requesting file encryption for user: \(currentUser)") - logger.debug("File: \(filePath)") - logger.debug("Chat ID: \(chatId)") - logger.debug("Access Time: \(accessTime) seconds") - logger.debug("Allow Download: \(allowDownload)") - logger.debug("Allow Forward: \(allowForward)") let result = core.processFileEncryptRequest( withFilePath: filePath, @@ -495,8 +465,6 @@ public class PrvContext { } logger.info("Requesting file decryption for user: \(currentUser)") - logger.debug("PRV file: \(prvFile)") - logger.debug("Chat ID: \(chatId)") let result = core.processFileDecryptRequest(withPrvFile: prvFile, chatId: chatId) @@ -535,10 +503,6 @@ public class PrvContext { return (false, nil, "No user selected") } - logger.debug("Requesting file access status for user: \(currentUser)") - logger.debug("File path: \(filePath)") - logger.debug("Chat ID: \(chatId)") - guard let result = core.getFileAccessStatus(withChatId: chatId, filePath: filePath) else { logger.error("Failed to get file access status response") return (false, nil, "No response returned") @@ -567,6 +531,59 @@ public class PrvContext { logger.error("File access status check failed: \(errorMessage)") return (false, nil, errorMessage) } + + /// Retrieve detailed file access status list including owner, shared, and forwarded information + /// Returns raw dictionary with nested structure: owner_info, shared_info, forwarded_list + public func getFileAccessStatusListWithChatId(chatId: String, + filePath: String) -> [String: Any]? { + guard let core = getCore() else { + logger.error("Cannot check file access status list: Core not initialized") + return nil + } + + guard let currentUser = getCurrentUser() else { + logger.error("Cannot check file access status list: No user selected") + return nil + } + + // Safety check: Verify the method exists in the native framework + let selector = #selector(PrivittyCore.getFileAccessStatusList(withChatId:filePath:)) + guard core.responds(to: selector) else { + logger.warning("getFileAccessStatusList method not implemented in native framework yet") + logger.warning("Please update Privitty.framework to the latest version") + return [ + "success": false, + "error": "Method not implemented in native framework", + "message": "Please update Privitty.framework to the latest version" + ] + } + + guard let result = core.getFileAccessStatusList(withChatId: chatId, filePath: filePath) else { + logger.error("Failed to get file access status list response") + return nil + } + + // Check success + let successValue: Bool + if let value = result["success"] as? Bool { + successValue = value + } else if let value = result["success"] as? NSNumber { + successValue = value.boolValue + } else if let value = result["success"] as? Int { + successValue = value != 0 + } else { + successValue = false + } + + if successValue { + logger.info("File access status list retrieved successfully") + return result as? [String: Any] + } else { + let errorMessage = (result["error"] as? String) ?? (result["message"] as? String) ?? "Unknown error" + logger.error("File access status list check failed: \(errorMessage)") + return result as? [String: Any] + } + } /// Request access to a Privitty encrypted file (sends request to owner) public func processInitAccessGrantRequest(chatId: String, @@ -582,8 +599,6 @@ public class PrvContext { } logger.info("Requesting file access grant for user: \(currentUser)") - logger.debug("File path: \(filePath)") - logger.debug("Chat ID: \(chatId)") guard let result = core.processInitAccessGrantRequest(withChatId: chatId, filePath: filePath) else { logger.error("Failed to get access grant request response") diff --git a/deltachat-ios/Helper/FileHelper.swift b/deltachat-ios/Helper/FileHelper.swift index db71285a6..24c8f1b3c 100644 --- a/deltachat-ios/Helper/FileHelper.swift +++ b/deltachat-ios/Helper/FileHelper.swift @@ -54,7 +54,7 @@ public class FileHelper { // write data do { try data.write(to: path) - return path.relativePath + return path.path } catch { print("err: \(error.localizedDescription)") return nil @@ -90,6 +90,52 @@ public class FileHelper { static func copyIfPossible(src: URL, dest: URL) -> URL { guard src != dest else { return src } + + // Check if source is a directory + var isDirectory: ObjCBool = false + let srcExists = FileManager.default.fileExists(atPath: src.path, isDirectory: &isDirectory) + + // Handle iOS .pvt photo directories + if isDirectory.boolValue && src.pathExtension.lowercased() == "pvt" { + // Look for the actual image file inside the directory + do { + let contents = try FileManager.default.contentsOfDirectory(at: src, includingPropertiesForKeys: [.isRegularFileKey], options: []) + + // Find the largest file (likely the actual image) + var largestFile: URL? + var largestSize: Int64 = 0 + + for fileURL in contents { + if let attrs = try? FileManager.default.attributesOfItem(atPath: fileURL.path), + let fileType = attrs[.type] as? FileAttributeType, + fileType == .typeRegular, + let size = attrs[.size] as? Int64, + size > largestSize { + largestSize = size + largestFile = fileURL + } + } + + if let actualImageFile = largestFile { + // Create destination with proper extension + var properDest = dest.deletingPathExtension() + if !actualImageFile.pathExtension.isEmpty { + properDest = properDest.appendingPathExtension(actualImageFile.pathExtension) + } + + // Copy the actual image file + if FileManager.default.fileExists(atPath: properDest.path) { + try FileManager.default.removeItem(at: properDest) + } + try FileManager.default.copyItem(at: actualImageFile, to: properDest) + return properDest + } + } catch { + logger.error("cannot extract from .pvt directory: \(error)") + } + } + + // Regular file copy do { if FileManager.default.fileExists(atPath: dest.path) { try FileManager.default.removeItem(at: dest)