diff --git a/Session.xcodeproj/project.pbxproj b/Session.xcodeproj/project.pbxproj index 33f202ec9..63a7fd7f4 100644 --- a/Session.xcodeproj/project.pbxproj +++ b/Session.xcodeproj/project.pbxproj @@ -7683,7 +7683,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 507; + CURRENT_PROJECT_VERSION = 517; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; @@ -7720,7 +7720,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.8.3; + MARKETING_VERSION = 2.8.4; ONLY_ACTIVE_ARCH = YES; OTHER_CFLAGS = ( "-fobjc-arc-exceptions", @@ -7762,7 +7762,7 @@ CLANG_WARN__ARC_BRIDGE_CAST_NONARC = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 507; + CURRENT_PROJECT_VERSION = 517; ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_NO_COMMON_BLOCKS = YES; @@ -7794,7 +7794,7 @@ GCC_WARN_UNUSED_VARIABLE = YES; HEADER_SEARCH_PATHS = ""; IPHONEOS_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 2.8.3; + MARKETING_VERSION = 2.8.4; ONLY_ACTIVE_ARCH = NO; OTHER_CFLAGS = ( "-DNS_BLOCK_ASSERTIONS=1", @@ -7825,7 +7825,6 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 513; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7863,7 +7862,6 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.8.4; OTHER_LDFLAGS = "$(inherited)"; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; @@ -7896,7 +7894,6 @@ CODE_SIGN_ENTITLEMENTS = Session/Meta/Signal.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 513; DEVELOPMENT_TEAM = SUQ8J2PCT7; FRAMEWORK_SEARCH_PATHS = ( "$(inherited)", @@ -7934,7 +7931,6 @@ "$(SRCROOT)", ); LLVM_LTO = NO; - MARKETING_VERSION = 2.8.4; OTHER_LDFLAGS = "$(inherited)"; PRODUCT_BUNDLE_IDENTIFIER = "com.loki-project.loki-messenger"; PRODUCT_NAME = Session; diff --git a/Session/Conversations/ConversationVC+Interaction.swift b/Session/Conversations/ConversationVC+Interaction.swift index 4be22de74..b6de32d3a 100644 --- a/Session/Conversations/ConversationVC+Interaction.swift +++ b/Session/Conversations/ConversationVC+Interaction.swift @@ -378,7 +378,7 @@ extension ConversationVC: } let fileName = urlResourceValues.name ?? "attachment".localized() - guard let dataSource = DataSourcePath(fileUrl: url, shouldDeleteOnDeinit: false) else { + guard let dataSource = DataSourcePath(fileUrl: url, sourceFilename: urlResourceValues.name, shouldDeleteOnDeinit: false) else { DispatchQueue.main.async { [weak self] in self?.viewModel.showToast(text: "attachmentsErrorLoad".localized()) } @@ -412,7 +412,7 @@ extension ConversationVC: func showAttachmentApprovalDialogAfterProcessingVideo(at url: URL, with fileName: String) { ModalActivityIndicatorViewController.present(fromViewController: self, canCancel: true, message: nil) { [weak self, dependencies = viewModel.dependencies] modalActivityIndicator in - let dataSource = DataSourcePath(fileUrl: url, shouldDeleteOnDeinit: false)! + let dataSource = DataSourcePath(fileUrl: url, sourceFilename: fileName, shouldDeleteOnDeinit: false)! dataSource.sourceFilename = fileName SignalAttachment @@ -2591,7 +2591,7 @@ extension ConversationVC: } // Get data - let dataSourceOrNil = DataSourcePath(fileUrl: audioRecorder.url, shouldDeleteOnDeinit: true) + let dataSourceOrNil = DataSourcePath(fileUrl: audioRecorder.url, sourceFilename: nil, shouldDeleteOnDeinit: true) self.audioRecorder = nil guard let dataSource = dataSourceOrNil else { return SNLog("Couldn't load recorded data.") } diff --git a/Session/Conversations/Message Cells/Content Views/MediaView.swift b/Session/Conversations/Message Cells/Content Views/MediaView.swift index 53f32643f..fc3a72a6c 100644 --- a/Session/Conversations/Message Cells/Content Views/MediaView.swift +++ b/Session/Conversations/Message Cells/Content Views/MediaView.swift @@ -372,6 +372,8 @@ public class MediaView: UIView { return } icon = asset + self.isAccessibilityElement = true + self.accessibilityIdentifier = "Media retry" case .invalid: guard let asset = UIImage(named: "media_invalid") else { @@ -379,6 +381,8 @@ public class MediaView: UIView { return } icon = asset + self.isAccessibilityElement = true + self.accessibilityIdentifier = "Media invalid" case .missing: return } diff --git a/Session/Conversations/Settings/ThreadSettingsViewModel.swift b/Session/Conversations/Settings/ThreadSettingsViewModel.swift index 8288efd39..fc9b90791 100644 --- a/Session/Conversations/Settings/ThreadSettingsViewModel.swift +++ b/Session/Conversations/Settings/ThreadSettingsViewModel.swift @@ -790,18 +790,19 @@ class ThreadSettingsViewModel: SessionTableViewModel, NavigationItemSource, Navi ) .save(db) + let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() + let destinationDisappearingMessagesConfiguration: DisappearingMessagesConfiguration? = try? DisappearingMessagesConfiguration + .filter(id: userId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .fetchOne(db) let interaction: Interaction = try Interaction( threadId: thread.id, threadVariant: thread.variant, authorId: currentUserSessionId, variant: .standardOutgoing, - timestampMs: SnodeAPI.currentOffsetTimestampMs(), - expiresInSeconds: try? DisappearingMessagesConfiguration - .select(.durationSeconds) - .filter(id: userId) - .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) - .asRequest(of: TimeInterval.self) - .fetchOne(db), + timestampMs: sentTimestampMs, + expiresInSeconds: destinationDisappearingMessagesConfiguration?.durationSeconds, + expiresStartedAtMs: (destinationDisappearingMessagesConfiguration?.type == .disappearAfterSend ? Double(sentTimestampMs) : nil), linkPreviewUrl: communityUrl ) .inserted(db) diff --git a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift index 56fedbab5..c7c389ecd 100644 --- a/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift +++ b/Session/Media Viewing & Editing/GIFs/GifPickerViewController.swift @@ -380,7 +380,7 @@ class GifPickerViewController: OWSViewController, UISearchBarDelegate, UICollect return } - let dataSource = DataSourcePath(filePath: asset.filePath, shouldDeleteOnDeinit: false) + let dataSource = DataSourcePath(filePath: asset.filePath, sourceFilename: URL(fileURLWithPath: asset.filePath).pathExtension, shouldDeleteOnDeinit: false) let attachment = SignalAttachment.attachment(dataSource: dataSource, type: rendition.type, imageQuality: .medium) self?.dismiss(animated: true) { diff --git a/Session/Media Viewing & Editing/PhotoCapture.swift b/Session/Media Viewing & Editing/PhotoCapture.swift index 33c0bc223..291ab6875 100644 --- a/Session/Media Viewing & Editing/PhotoCapture.swift +++ b/Session/Media Viewing & Editing/PhotoCapture.swift @@ -441,7 +441,7 @@ extension PhotoCapture: CaptureOutputDelegate { Log.debug("[PhotoCapture] Ignoring error, since capture succeeded.") } - let dataSource = DataSourcePath(fileUrl: outputFileURL, shouldDeleteOnDeinit: true) + let dataSource = DataSourcePath(fileUrl: outputFileURL, sourceFilename: nil, shouldDeleteOnDeinit: true) let attachment = SignalAttachment.attachment(dataSource: dataSource, type: .mpeg4Movie) delegate?.photoCapture(self, didFinishProcessingAttachment: attachment) } diff --git a/Session/Media Viewing & Editing/PhotoLibrary.swift b/Session/Media Viewing & Editing/PhotoLibrary.swift index e31f1c604..a6617060e 100644 --- a/Session/Media Viewing & Editing/PhotoLibrary.swift +++ b/Session/Media Viewing & Editing/PhotoLibrary.swift @@ -196,7 +196,7 @@ class PhotoCollectionContents { guard exportSession?.status == .completed, - let dataSource = DataSourcePath(fileUrl: exportURL, shouldDeleteOnDeinit: true) + let dataSource = DataSourcePath(fileUrl: exportURL, sourceFilename: nil, shouldDeleteOnDeinit: true) else { resolver(Result.failure(PhotoLibraryError.assertionError(description: "Failed to build data source for exported video URL"))) return diff --git a/Session/Meta/Translations/Localizable.xcstrings b/Session/Meta/Translations/Localizable.xcstrings index ec8c5a80a..13816aa3f 100644 --- a/Session/Meta/Translations/Localizable.xcstrings +++ b/Session/Meta/Translations/Localizable.xcstrings @@ -12563,6 +12563,17 @@ } } }, + "adminPromotionNotSent" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promotion not sent" + } + } + } + }, "adminPromotionSent" : { "extractionState" : "manual", "localizations" : { @@ -13048,6 +13059,17 @@ } } }, + "adminPromotionStatusUnknown" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Promotion status unknown" + } + } + } + }, "adminRemove" : { "extractionState" : "manual", "localizations" : { @@ -193854,6 +193876,17 @@ } } }, + "groupInviteNotSent" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invite not sent" + } + } + } + }, "groupInviteReinvite" : { "extractionState" : "manual", "localizations" : { @@ -194576,6 +194609,17 @@ } } }, + "groupInviteStatusUnknown" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Invite status unknown" + } + } + } + }, "groupInviteSuccessful" : { "extractionState" : "manual", "localizations" : { diff --git a/Session/Notifications/AppNotifications.swift b/Session/Notifications/AppNotifications.swift index 8e4e48add..f83cd18e6 100644 --- a/Session/Notifications/AppNotifications.swift +++ b/Session/Notifications/AppNotifications.swift @@ -512,14 +512,21 @@ class NotificationActionHandler { return Storage.shared .writePublisher { db in + let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() + let destinationDisappearingMessagesConfiguration: DisappearingMessagesConfiguration? = try? DisappearingMessagesConfiguration + .filter(id: threadId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .fetchOne(db) let interaction: Interaction = try Interaction( threadId: threadId, threadVariant: thread.variant, authorId: getUserHexEncodedPublicKey(db), variant: .standardOutgoing, body: replyText, - timestampMs: SnodeAPI.currentOffsetTimestampMs(), - hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText) + timestampMs: sentTimestampMs, + hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: replyText), + expiresInSeconds: destinationDisappearingMessagesConfiguration?.durationSeconds, + expiresStartedAtMs: (destinationDisappearingMessagesConfiguration?.type == .disappearAfterSend ? Double(sentTimestampMs) : nil) ).inserted(db) try Interaction.markAsRead( diff --git a/Session/Settings/HelpViewModel.swift b/Session/Settings/HelpViewModel.swift index 2530fd329..f7ba3506d 100644 --- a/Session/Settings/HelpViewModel.swift +++ b/Session/Settings/HelpViewModel.swift @@ -169,13 +169,15 @@ class HelpViewModel: SessionTableViewModel, NavigatableStateHolder, ObservableTa cancelTitle: "Share", cancelStyle: .alert_text, onConfirm: { _ in UIPasteboard.general.string = latestLogFilePath }, - onCancel: { _ in - HelpViewModel.shareLogsInternal( - viewControllerToDismiss: viewControllerToDismiss, - targetView: targetView, - animated: animated, - onShareComplete: onShareComplete - ) + onCancel: { modal in + modal.dismiss(animated: true) { + HelpViewModel.shareLogsInternal( + viewControllerToDismiss: viewControllerToDismiss, + targetView: targetView, + animated: animated, + onShareComplete: onShareComplete + ) + } } ) ) diff --git a/SessionMessagingKit/Database/Models/Attachment.swift b/SessionMessagingKit/Database/Models/Attachment.swift index 6e4263274..ebc24ffd6 100644 --- a/SessionMessagingKit/Database/Models/Attachment.swift +++ b/SessionMessagingKit/Database/Models/Attachment.swift @@ -638,7 +638,10 @@ extension Attachment { // If the filename has not file extension, deduce one // from the MIME type. if targetFileExtension.isEmpty { - targetFileExtension = (UTType(sessionMimeType: mimeType)?.sessionFileExtension ?? UTType.fileExtensionDefault) + targetFileExtension = ( + UTType(sessionMimeType: mimeType)?.sessionFileExtension(sourceFilename: sourceFilename) ?? + UTType.fileExtensionDefault + ) } targetFileExtension = targetFileExtension.lowercased() @@ -657,7 +660,7 @@ extension Attachment { } let targetFileExtension: String = ( - UTType(sessionMimeType: mimeType)?.sessionFileExtension ?? + UTType(sessionMimeType: mimeType)?.sessionFileExtension(sourceFilename: sourceFilename) ?? UTType.fileExtensionDefault ).lowercased() diff --git a/SessionMessagingKit/Database/Models/LinkPreview.swift b/SessionMessagingKit/Database/Models/LinkPreview.swift index 0c7e861e5..9e421076f 100644 --- a/SessionMessagingKit/Database/Models/LinkPreview.swift +++ b/SessionMessagingKit/Database/Models/LinkPreview.swift @@ -132,12 +132,16 @@ public extension LinkPreview { static func generateAttachmentIfPossible(imageData: Data?, type: UTType) throws -> Attachment? { guard let imageData: Data = imageData, !imageData.isEmpty else { return nil } - guard let fileExtension: String = type.sessionFileExtension else { return nil } + guard let fileExtension: String = type.sessionFileExtension(sourceFilename: nil) else { return nil } guard let mimeType: String = type.preferredMIMEType else { return nil } let filePath = FileSystem.temporaryFilePath(fileExtension: fileExtension) try imageData.write(to: NSURL.fileURL(withPath: filePath), options: .atomicWrite) - let dataSource: DataSourcePath = DataSourcePath(filePath: filePath, shouldDeleteOnDeinit: true) + let dataSource: DataSourcePath = DataSourcePath( + filePath: filePath, + sourceFilename: nil, + shouldDeleteOnDeinit: true + ) return Attachment(contentType: mimeType, dataSource: dataSource) } diff --git a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift index a7f7c3c39..ce8531edb 100644 --- a/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift +++ b/SessionMessagingKit/Sending & Receiving/Attachments/SignalAttachment.swift @@ -269,7 +269,7 @@ public class SignalAttachment: Equatable { // can be identified. public var mimeType: String { guard - let fileExtension: String = sourceFilename.map({ $0 as NSString })?.pathExtension, + let fileExtension: String = sourceFilename.map({ URL(fileURLWithPath: $0) })?.pathExtension, !fileExtension.isEmpty, let fileExtensionMimeType: String = UTType(sessionFileExtension: fileExtension)?.preferredMIMEType else { return (dataType.preferredMIMEType ?? UTType.mimeTypeDefault) } @@ -306,9 +306,9 @@ public class SignalAttachment: Equatable { // can be identified. public var fileExtension: String? { guard - let fileExtension: String = sourceFilename.map({ $0 as NSString })?.pathExtension, + let fileExtension: String = sourceFilename.map({ URL(fileURLWithPath: $0) })?.pathExtension, !fileExtension.isEmpty - else { return dataType.sessionFileExtension } + else { return dataType.sessionFileExtension(sourceFilename: sourceFilename) } return fileExtension.filteredFilename } @@ -803,7 +803,7 @@ public class SignalAttachment: Equatable { let baseFilename = dataSource.sourceFilename let mp4Filename = baseFilename?.filenameWithoutExtension.appendingFileExtension("mp4") - guard let dataSource = DataSourcePath(fileUrl: exportURL, shouldDeleteOnDeinit: true) else { + guard let dataSource = DataSourcePath(fileUrl: exportURL, sourceFilename: baseFilename, shouldDeleteOnDeinit: true) else { let attachment = SignalAttachment(dataSource: DataSourceValue.empty, dataType: type) attachment.error = .couldNotConvertToMpeg4 resolver(Result.success(attachment)) diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift index 8d6df1bb6..ccee66d0a 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageReceiver+ClosedGroups.swift @@ -142,6 +142,7 @@ extension MessageReceiver { // approved contact (to prevent spam via closed groups getting around message requests if users are // on old or modified clients) var hasApprovedAdmin: Bool = false + let receivedTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) for adminId in admins { if let contact: Contact = try? Contact.fetchOne(db, id: adminId), contact.isApproved { @@ -154,6 +155,36 @@ extension MessageReceiver { // doesn't matter if we have an approved admin - we should add it regardless guard hasApprovedAdmin || forceApprove else { return } + // Create the disappearing config + let disappearingConfig: DisappearingMessagesConfiguration = DisappearingMessagesConfiguration + .defaultWith(groupPublicKey) + .with( + isEnabled: (expirationTimer > 0), + durationSeconds: TimeInterval(expirationTimer), + type: (expirationTimer > 0 ? .disappearAfterSend : .unknown) + ) + + /// Update `libSession` first + /// + /// **Note:** This **MUST** happen before we call `SessionThread.upsert` as we won't add the group + /// if it already exists in `libSession` and upserting the thread results in an update to `libSession` to set + /// the `priority` + if configTriggeringChange == nil { + try? LibSession.add( + db, + groupPublicKey: groupPublicKey, + name: name, + joinedAt: (TimeInterval(formationTimestampMs) / 1000), + latestKeyPairPublicKey: Data(encryptionKeyPair.publicKey), + latestKeyPairSecretKey: Data(encryptionKeyPair.secretKey), + latestKeyPairReceivedTimestamp: receivedTimestamp, + disappearingConfig: disappearingConfig, + members: members.asSet(), + admins: admins.asSet(), + using: dependencies + ) + } + // Create the group let thread: SessionThread = try SessionThread.upsert( db, @@ -196,20 +227,11 @@ extension MessageReceiver { } // Update the DisappearingMessages config - var disappearingConfig = DisappearingMessagesConfiguration.defaultWith(thread.id) if (try? thread.disappearingMessagesConfiguration.fetchOne(db)) == nil { - let isEnabled: Bool = (expirationTimer > 0) - disappearingConfig = try disappearingConfig - .with( - isEnabled: isEnabled, - durationSeconds: TimeInterval(expirationTimer), - type: isEnabled ? .disappearAfterSend : .unknown - ) - .saved(db) + try disappearingConfig.upsert(db) } // Store the key pair if it doesn't already exist - let receivedTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) let newKeyPair: ClosedGroupKeyPair = ClosedGroupKeyPair( threadId: groupPublicKey, publicKey: Data(encryptionKeyPair.publicKey), diff --git a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift index 8cf00ff5a..31e338f0a 100644 --- a/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift +++ b/SessionMessagingKit/Sending & Receiving/Message Handling/MessageSender+ClosedGroups.swift @@ -39,6 +39,25 @@ extension MessageSender { let adminsAsData: [Data] = admins.map { Data(hex: $0) } let formationTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) + /// Update `libSession` first + /// + /// **Note:** This **MUST** happen before we call `SessionThread.upsert` as we won't add the group + /// if it already exists in `libSession` and upserting the thread results in an update to `libSession` to set + /// the `priority` + try LibSession.add( + db, + groupPublicKey: groupPublicKey, + name: name, + joinedAt: formationTimestamp, + latestKeyPairPublicKey: Data(encryptionKeyPair.publicKey), + latestKeyPairSecretKey: Data(encryptionKeyPair.secretKey), + latestKeyPairReceivedTimestamp: formationTimestamp, + disappearingConfig: DisappearingMessagesConfiguration.defaultWith(groupPublicKey), + members: members, + admins: admins, + using: dependencies + ) + // Create the relevant objects in the database let thread: SessionThread = try SessionThread.upsert( db, @@ -54,12 +73,11 @@ extension MessageSender { ).insert(db) // Store the key pair - let latestKeyPairReceivedTimestamp: TimeInterval = (TimeInterval(SnodeAPI.currentOffsetTimestampMs()) / 1000) try ClosedGroupKeyPair( threadId: groupPublicKey, publicKey: Data(encryptionKeyPair.publicKey), secretKey: Data(encryptionKeyPair.secretKey), - receivedTimestamp: latestKeyPairReceivedTimestamp + receivedTimestamp: formationTimestamp ).insert(db) // Create the member objects @@ -81,21 +99,6 @@ extension MessageSender { ).save(db) } - // Update libSession - try LibSession.add( - db, - groupPublicKey: groupPublicKey, - name: name, - joinedAt: formationTimestamp, - latestKeyPairPublicKey: Data(encryptionKeyPair.publicKey), - latestKeyPairSecretKey: Data(encryptionKeyPair.secretKey), - latestKeyPairReceivedTimestamp: latestKeyPairReceivedTimestamp, - disappearingConfig: DisappearingMessagesConfiguration.defaultWith(groupPublicKey), - members: members, - admins: admins, - using: dependencies - ) - let memberSendData: [MessageSender.PreparedSendData] = try members .map { memberId -> MessageSender.PreparedSendData in try MessageSender.preparedSendData( diff --git a/SessionShareExtension/ShareNavController.swift b/SessionShareExtension/ShareNavController.swift index 1daca9477..36ce0d95e 100644 --- a/SessionShareExtension/ShareNavController.swift +++ b/SessionShareExtension/ShareNavController.swift @@ -272,16 +272,14 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { // // NOTE: SharingThreadPickerViewController will try to unpack them // and send them as normal text messages if possible. - case (_, true): return DataSourcePath(fileUrl: url, shouldDeleteOnDeinit: false) + case (_, true): + return DataSourcePath(fileUrl: url, sourceFilename: customFileName, shouldDeleteOnDeinit: false) default: - guard let dataSource = DataSourcePath(fileUrl: url, shouldDeleteOnDeinit: false) else { + guard let dataSource = DataSourcePath(fileUrl: url, sourceFilename: customFileName, shouldDeleteOnDeinit: false) else { return nil } - // Fallback to the last part of the URL - dataSource.sourceFilename = (customFileName ?? url.lastPathComponent) - return dataSource } } @@ -427,7 +425,7 @@ final class ShareNavController: UINavigationController, ShareViewDelegate { switch value { case let data as Data: let customFileName = "Contact.vcf" // stringlint:ignore - let customFileExtension: String? = srcType.sessionFileExtension + let customFileExtension: String? = srcType.sessionFileExtension(sourceFilename: nil) guard let tempFilePath = try? FileSystem.write(data: data, toTemporaryFileWithExtension: customFileExtension) else { resolver( diff --git a/SessionShareExtension/ThreadPickerVC.swift b/SessionShareExtension/ThreadPickerVC.swift index 8c9e9add1..18e686e68 100644 --- a/SessionShareExtension/ThreadPickerVC.swift +++ b/SessionShareExtension/ThreadPickerVC.swift @@ -280,14 +280,21 @@ final class ThreadPickerVC: UIViewController, UITableViewDataSource, UITableView } // Create the interaction + let sentTimestampMs: Int64 = SnodeAPI.currentOffsetTimestampMs() + let destinationDisappearingMessagesConfiguration: DisappearingMessagesConfiguration? = try? DisappearingMessagesConfiguration + .filter(id: threadId) + .filter(DisappearingMessagesConfiguration.Columns.isEnabled == true) + .fetchOne(db) let interaction: Interaction = try Interaction( threadId: threadId, threadVariant: threadVariant, authorId: getUserHexEncodedPublicKey(db), variant: .standardOutgoing, body: body, - timestampMs: SnodeAPI.currentOffsetTimestampMs(), + timestampMs: sentTimestampMs, hasMention: Interaction.isUserMentioned(db, threadId: threadId, body: body), + expiresInSeconds: destinationDisappearingMessagesConfiguration?.durationSeconds, + expiresStartedAtMs: (destinationDisappearingMessagesConfiguration?.type == .disappearAfterSend ? Double(sentTimestampMs) : nil), linkPreviewUrl: (isSharingUrl ? attachments.first?.linkPreviewDraft?.urlString : nil) ).inserted(db) diff --git a/SessionUtilitiesKit/Media/DataSource.swift b/SessionUtilitiesKit/Media/DataSource.swift index 1cc3bf20d..e603b8434 100644 --- a/SessionUtilitiesKit/Media/DataSource.swift +++ b/SessionUtilitiesKit/Media/DataSource.swift @@ -97,7 +97,7 @@ public class DataSourceValue: DataSource { } public convenience init?(data: Data?, dataType: UTType) { - guard let fileExtension: String = dataType.sessionFileExtension else { return nil } + guard let fileExtension: String = dataType.sessionFileExtension(sourceFilename: nil) else { return nil } self.init(data: data, fileExtension: fileExtension) } @@ -210,15 +210,28 @@ public class DataSourcePath: DataSource { // MARK: - Initialization - public init(filePath: String, shouldDeleteOnDeinit: Bool) { + public init( + filePath: String, + sourceFilename: String?, + shouldDeleteOnDeinit: Bool + ) { self.filePath = filePath + self.sourceFilename = sourceFilename self.shouldDeleteOnDeinit = shouldDeleteOnDeinit } - public convenience init?(fileUrl: URL?, shouldDeleteOnDeinit: Bool) { + public convenience init?( + fileUrl: URL?, + sourceFilename: String?, + shouldDeleteOnDeinit: Bool + ) { guard let fileUrl: URL = fileUrl, fileUrl.isFileURL else { return nil } - self.init(filePath: fileUrl.path, shouldDeleteOnDeinit: shouldDeleteOnDeinit) + self.init( + filePath: fileUrl.path, + sourceFilename: (sourceFilename ?? fileUrl.lastPathComponent), + shouldDeleteOnDeinit: shouldDeleteOnDeinit + ) } deinit { diff --git a/SessionUtilitiesKit/Media/UTType+Utilities.swift b/SessionUtilitiesKit/Media/UTType+Utilities.swift index 64aef20b7..59353ead1 100644 --- a/SessionUtilitiesKit/Media/UTType+Utilities.swift +++ b/SessionUtilitiesKit/Media/UTType+Utilities.swift @@ -107,26 +107,6 @@ public extension UTType { var isMicrosoftDoc: Bool { UTType.supportedMicrosoftDocTypes.contains(self) } var isVisualMedia: Bool { isImage || isVideo || isAnimated } - var sessionFileExtension: String? { - // Special-case the "aac" filetype we use for voice messages (for legacy reasons) - // to use a .m4a file extension, not .aac, since AVAudioPlayer can't handle .aac - // properly. Doesn't affect file contents. - guard identifier != "public.aac-audio" else { return "m4a" } - - // Try to deduce the file extension by using a lookup table. - // - // This should be more accurate than deducing the file extension by - // converting to a UTI type. For example, .m4a files will have a - // UTI type of kUTTypeMPEG4Audio which incorrectly yields the file - // extension .mp4 instead of .m4a. - guard - let mimeType: String = preferredMIMEType, - let fileExtension: String = UTType.genericMimeTypesToExtensionTypes[mimeType] - else { return preferredFilenameExtension } - - return fileExtension - } - // MARK: - Initialization init?(sessionFileExtension: String) { @@ -188,6 +168,38 @@ public extension UTType { return (UTType(sessionMimeType: mimeType) ?? .invalid).isVisualMedia } + func sessionFileExtension(sourceFilename: String?) -> String? { + // First try to check if the file extension on `sourceFilename` is valid + // for the `preferredMIMEType` as we want to keep that if so (eg. use `.log` + // instead of `.txt`) + if + let sourceFileExtension: String = sourceFilename.map({ URL(fileURLWithPath: $0) })?.pathExtension, + let mimeType: String = preferredMIMEType, + let sourceExtensionMimeType: String = UTType.genericMimeTypesToExtensionTypes[sourceFileExtension], + UTType(mimeType: sourceExtensionMimeType)?.preferredMIMEType == mimeType + { + return sourceFileExtension + } + + // Special-case the "aac" filetype we use for voice messages (for legacy reasons) + // to use a .m4a file extension, not .aac, since AVAudioPlayer can't handle .aac + // properly. Doesn't affect file contents. + guard identifier != "public.aac-audio" else { return "m4a" } + + // Try to deduce the file extension by using a lookup table. + // + // This should be more accurate than deducing the file extension by + // converting to a UTI type. For example, .m4a files will have a + // UTI type of kUTTypeMPEG4Audio which incorrectly yields the file + // extension .mp4 instead of .m4a. + guard + let mimeType: String = preferredMIMEType, + let fileExtension: String = UTType.genericMimeTypesToExtensionTypes[mimeType] + else { return preferredFilenameExtension } + + return fileExtension + } + // MARK: - Lookup Table static func sessionMimeType(for fileExtension: String) -> String? { diff --git a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift index 904a26bf9..63f40c158 100644 --- a/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift +++ b/SignalUtilitiesKit/Media Viewing & Editing/Attachment Approval/AttachmentApprovalViewController.swift @@ -533,7 +533,7 @@ public class AttachmentApprovalViewController: UIPageViewController, UIPageViewC // Rewrite the filename's extension to reflect the output file format. var filename: String? = attachmentItem.attachment.sourceFilename if let sourceFilename = attachmentItem.attachment.sourceFilename { - if let fileExtension: String = dataType.sessionFileExtension { + if let fileExtension: String = dataType.sessionFileExtension(sourceFilename: sourceFilename) { filename = (sourceFilename as NSString).deletingPathExtension.appendingFileExtension(fileExtension) } }