* fix(app): preserve image attachment MIME types Image intake discarded clipboard and desktop path metadata, then guessed JPEG when a later file object had no type. Resolve MIME once at the platform boundary and require it downstream. * fix(app): honor native picker MIME metadata
93 lines
2.8 KiB
TypeScript
93 lines
2.8 KiB
TypeScript
import { useCallback, useRef } from "react";
|
|
import { Alert } from "react-native";
|
|
import * as ImagePicker from "expo-image-picker";
|
|
import { useTranslation } from "react-i18next";
|
|
import { getDesktopHost, isElectronRuntime } from "@/desktop/host";
|
|
import {
|
|
normalizePickedImageAssets,
|
|
pickImagesWithDesktopDialog,
|
|
type PickedImageAttachmentInput,
|
|
} from "@/hooks/image-attachment-picker";
|
|
import { isWeb } from "@/constants/platform";
|
|
|
|
interface UseImageAttachmentPickerResult {
|
|
pickImages: () => Promise<PickedImageAttachmentInput[] | null>;
|
|
}
|
|
|
|
export function useImageAttachmentPicker(): UseImageAttachmentPickerResult {
|
|
const { t } = useTranslation();
|
|
const [mediaPermission, requestMediaPermission] = ImagePicker.useMediaLibraryPermissions();
|
|
const isPickingRef = useRef(false);
|
|
|
|
const ensurePermission = useCallback(async () => {
|
|
let currentPermission = mediaPermission;
|
|
|
|
if (
|
|
!currentPermission ||
|
|
currentPermission.status === ImagePicker.PermissionStatus.UNDETERMINED
|
|
) {
|
|
currentPermission = await requestMediaPermission();
|
|
} else if (!currentPermission.granted) {
|
|
currentPermission = await requestMediaPermission();
|
|
}
|
|
|
|
if (!currentPermission?.granted) {
|
|
Alert.alert(
|
|
t("imageAttachmentPicker.permissionTitle"),
|
|
t("imageAttachmentPicker.permissionMessage"),
|
|
);
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}, [mediaPermission, requestMediaPermission, t]);
|
|
|
|
const pickImages = useCallback(async () => {
|
|
if (isPickingRef.current) {
|
|
return null;
|
|
}
|
|
|
|
isPickingRef.current = true;
|
|
|
|
try {
|
|
if (isWeb && isElectronRuntime()) {
|
|
const selectedImages = await pickImagesWithDesktopDialog(getDesktopHost()?.dialog);
|
|
if (selectedImages.length === 0) {
|
|
return null;
|
|
}
|
|
return selectedImages;
|
|
}
|
|
|
|
const hasPermission = await ensurePermission();
|
|
if (!hasPermission) {
|
|
return null;
|
|
}
|
|
|
|
const pendingResult = await ImagePicker.getPendingResultAsync();
|
|
if (pendingResult && "canceled" in pendingResult && !pendingResult.canceled) {
|
|
return await normalizePickedImageAssets(pendingResult.assets);
|
|
}
|
|
|
|
const result = await ImagePicker.launchImageLibraryAsync({
|
|
mediaTypes: ["images"] as ImagePicker.MediaType[],
|
|
allowsMultipleSelection: true,
|
|
quality: 0.8,
|
|
});
|
|
|
|
if (result.canceled) {
|
|
return null;
|
|
}
|
|
|
|
return await normalizePickedImageAssets(result.assets);
|
|
} catch (error) {
|
|
console.error("[ImageAttachmentPicker] Failed to pick image:", error);
|
|
Alert.alert(t("imageAttachmentPicker.errorTitle"), t("imageAttachmentPicker.failedToSelect"));
|
|
return null;
|
|
} finally {
|
|
isPickingRef.current = false;
|
|
}
|
|
}, [ensurePermission, t]);
|
|
|
|
return { pickImages };
|
|
}
|