Merge branch 'dev' into fix/atomic-reentrancy

pull/1058/head
Morgan Pretty 4 months ago
commit c6365e597d

@ -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;

@ -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.") }

@ -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
}

@ -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)

@ -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) {

@ -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)
}

@ -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

@ -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" : {

@ -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(

@ -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
)
}
}
)
)

@ -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()

@ -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)
}

@ -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))

@ -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),

@ -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(

@ -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(

@ -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)

@ -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 {

@ -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? {

@ -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)
}
}

Loading…
Cancel
Save