mirror of
https://github.com/Novaloop-AG/element-x-ios.git
synced 2026-11-04 01:20:12 +00:00
Recognise dragged iOS screenshots as images instead of raw data
When dragging the floating screenshot preview into a room, iOS hands us a provider whose `suggestedName` is e.g. `Screenshot 2026-05-22 at 14.49.40`. `NSString.pathExtension` returns `"40"`, so the previous `!pathExtension.isEmpty` check treated the timestamp's trailing segment as a real extension. The file got written without `.jpg`/`.png`, and the `MediaUploadingPreprocessor` then fell back to `application/octet-stream` which is why screenshots showed up as a "data" tile.
This commit is contained in:
committed by
Stefan Ceriu
parent
ff4fe6c728
commit
2082e71888
@@ -221,6 +221,7 @@
|
|||||||
22C5483D01EEB290B8339817 /* HomeScreenInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */; };
|
22C5483D01EEB290B8339817 /* HomeScreenInviteCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D8FC33C3F6BF597E095CE9FA /* HomeScreenInviteCell.swift */; };
|
||||||
2335D1AB954C151FD8779F45 /* RoomPermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0096BC5DA86AF6B6E5742AC /* RoomPermissionsTests.swift */; };
|
2335D1AB954C151FD8779F45 /* RoomPermissionsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0096BC5DA86AF6B6E5742AC /* RoomPermissionsTests.swift */; };
|
||||||
23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */; };
|
23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */; };
|
||||||
|
5C0EE5C0EE5C0EE5C0EE5C01 /* NSItemProviderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C0EE5C0EE5C0EE5C0EE5C02 /* NSItemProviderTests.swift */; };
|
||||||
237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; };
|
237FC70AA257B935F53316BA /* SessionVerificationControllerProxy.swift in Sources */ = {isa = PBXBuildFile; fileRef = C55D7E514F9DE4E3D72FDCAD /* SessionVerificationControllerProxy.swift */; };
|
||||||
238D561CA231339C6D4D06F3 /* ClientBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */; };
|
238D561CA231339C6D4D06F3 /* ClientBuilder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A1C33355FFB0F0953C35036 /* ClientBuilder.swift */; };
|
||||||
23E23736B53946785431A74F /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FD22AEFFA20065494ED2333 /* AppAppearance.swift */; };
|
23E23736B53946785431A74F /* AppAppearance.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0FD22AEFFA20065494ED2333 /* AppAppearance.swift */; };
|
||||||
@@ -2622,6 +2623,7 @@
|
|||||||
AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenCoordinator.swift; sourceTree = "<group>"; };
|
AD9CB3B9DFA353AB2B7CD9F8 /* NotificationSettingsEditScreenCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationSettingsEditScreenCoordinator.swift; sourceTree = "<group>"; };
|
||||||
ADCB8A232D3A8FB3E16A7303 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
ADCB8A232D3A8FB3E16A7303 /* uk */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = uk; path = uk.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessorTests.swift; sourceTree = "<group>"; };
|
AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MediaUploadingPreprocessorTests.swift; sourceTree = "<group>"; };
|
||||||
|
5C0EE5C0EE5C0EE5C0EE5C02 /* NSItemProviderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NSItemProviderTests.swift; sourceTree = "<group>"; };
|
||||||
AE40D4A5DD857AC16EED945A /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = "<group>"; };
|
AE40D4A5DD857AC16EED945A /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = "<group>"; };
|
||||||
AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableTask.swift; sourceTree = "<group>"; };
|
AE52983FAFB4E0998C00EE8A /* CancellableTask.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CancellableTask.swift; sourceTree = "<group>"; };
|
||||||
AE5DDBEBBA17973ED4638823 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
AE5DDBEBBA17973ED4638823 /* de */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = de; path = de.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
@@ -4968,6 +4970,7 @@
|
|||||||
2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */,
|
2D7A2C4A3A74F0D2FFE9356A /* MediaPlayerProviderTests.swift */,
|
||||||
AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */,
|
AE203026B9AD3DB412439866 /* MediaUploadingPreprocessorTests.swift */,
|
||||||
03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */,
|
03FABD73FD8086EFAB699F42 /* MediaUploadPreviewScreenViewModelTests.swift */,
|
||||||
|
5C0EE5C0EE5C0EE5C0EE5C02 /* NSItemProviderTests.swift */,
|
||||||
6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */,
|
6F6E6EDC4BBF962B2ED595A4 /* MessageForwardingScreenViewModelTests.swift */,
|
||||||
F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */,
|
F875D71347DC81EAE7687446 /* NavigationRootCoordinatorTests.swift */,
|
||||||
78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */,
|
78913D6E120D46138E97C107 /* NavigationSplitCoordinatorTests.swift */,
|
||||||
@@ -7952,6 +7955,7 @@
|
|||||||
167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */,
|
167D00CAA13FAFB822298021 /* MediaProviderTests.swift in Sources */,
|
||||||
B9A8C34A00D03094C0CF56F3 /* MediaUploadPreviewScreenViewModelTests.swift in Sources */,
|
B9A8C34A00D03094C0CF56F3 /* MediaUploadPreviewScreenViewModelTests.swift in Sources */,
|
||||||
23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */,
|
23701DE32ACD6FD40AA992C3 /* MediaUploadingPreprocessorTests.swift in Sources */,
|
||||||
|
5C0EE5C0EE5C0EE5C0EE5C01 /* NSItemProviderTests.swift in Sources */,
|
||||||
F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */,
|
F777C6FEE7D106136E2ED2B2 /* MessageForwardingScreenViewModelTests.swift in Sources */,
|
||||||
1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */,
|
1146E9EDCF8344F7D6E0D553 /* MockCoder.swift in Sources */,
|
||||||
DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */,
|
DC68E866D6E664B0D2B06E74 /* MockImageCache.swift in Sources */,
|
||||||
|
|||||||
@@ -196,8 +196,15 @@ extension NSItemProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension NSString {
|
extension NSString {
|
||||||
|
/// `NSString.pathExtension` returns the trailing dot-segment regardless of whether
|
||||||
|
/// it's a real extension, and `UTType(filenameExtension:)` synthesises a `dyn.*`
|
||||||
|
/// placeholder for unknown ones instead of returning nil — so check both.
|
||||||
var hasPathExtension: Bool {
|
var hasPathExtension: Bool {
|
||||||
!pathExtension.isEmpty
|
guard !pathExtension.isEmpty,
|
||||||
|
let type = UTType(filenameExtension: pathExtension) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return !type.isDynamic
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// Copyright 2026 Element Creations Ltd.
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
// Please see LICENSE files in the repository root for full details.
|
||||||
|
//
|
||||||
|
|
||||||
|
@testable import ElementX
|
||||||
|
import Foundation
|
||||||
|
import Testing
|
||||||
|
|
||||||
|
struct NSItemProviderTests {
|
||||||
|
// MARK: - hasPathExtension
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func hasPathExtensionRecognisesRegisteredExtensions() {
|
||||||
|
#expect(("IMG_1234.jpg" as NSString).hasPathExtension)
|
||||||
|
#expect(("photo.png" as NSString).hasPathExtension)
|
||||||
|
#expect(("clip.heic" as NSString).hasPathExtension)
|
||||||
|
#expect(("doc.pdf" as NSString).hasPathExtension)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func hasPathExtensionRejectsDateLikeTrailingSegments() {
|
||||||
|
// iOS screenshot drags expose a `suggestedName` like
|
||||||
|
// `Screenshot 2026-05-22 at 14.49.40`, whose `pathExtension` is `"40"`.
|
||||||
|
// `UTType(filenameExtension:)` synthesises a `dyn.*` placeholder for that —
|
||||||
|
// it must not be treated as a real extension.
|
||||||
|
#expect(!("Screenshot 2026-05-22 at 14.49.40" as NSString).hasPathExtension)
|
||||||
|
#expect(!("report 2025.10" as NSString).hasPathExtension)
|
||||||
|
#expect(!("v1.2.3" as NSString).hasPathExtension)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func hasPathExtensionRejectsAbsentExtension() {
|
||||||
|
#expect(!("plainname" as NSString).hasPathExtension)
|
||||||
|
#expect(!("" as NSString).hasPathExtension)
|
||||||
|
#expect(!("trailing dot." as NSString).hasPathExtension)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - storeData()
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func storeDataAppendsExtensionWhenSuggestedNameLooksLikeAScreenshot() async throws {
|
||||||
|
let imageURL = try #require(Bundle(for: BundleAnchor.self).url(forResource: "test_image.png", withExtension: nil),
|
||||||
|
"Failed retrieving fixture")
|
||||||
|
let provider = try #require(NSItemProvider(contentsOf: imageURL))
|
||||||
|
provider.suggestedName = "Screenshot 2026-05-22 at 14.49.40"
|
||||||
|
|
||||||
|
let storedURL = try #require(await provider.storeData())
|
||||||
|
|
||||||
|
#expect(storedURL.pathExtension == "png")
|
||||||
|
#expect(storedURL.deletingPathExtension().lastPathComponent == "Screenshot 2026-05-22 at 14.49.40")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
func storeDataPreservesSuggestedNameWithRealExtension() async throws {
|
||||||
|
let imageURL = try #require(Bundle(for: BundleAnchor.self).url(forResource: "test_image.png", withExtension: nil),
|
||||||
|
"Failed retrieving fixture")
|
||||||
|
let provider = try #require(NSItemProvider(contentsOf: imageURL))
|
||||||
|
provider.suggestedName = "IMG_1234.png"
|
||||||
|
|
||||||
|
let storedURL = try #require(await provider.storeData())
|
||||||
|
|
||||||
|
#expect(storedURL.lastPathComponent == "IMG_1234.png")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Bundle(for:)` requires a class; the enclosing test type is a struct, so we keep
|
||||||
|
/// a private anchor to scope the lookup to the unit test bundle.
|
||||||
|
private final class BundleAnchor { }
|
||||||
Reference in New Issue
Block a user