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)