Files
native/src/lib/share-intent.ts
T
Linus Rath cd4ce9d08b fix: upload files shared into the app with their real name and type
Sharing a photo or document from another app into Bulwark failed every
time: the upload task was handed the share's `content://` URI, but it
can only stream from a file path, so it failed with "Directory for
'/external/images/media/…' doesn't exist". The attachment was also named
after the URI's last segment (a MediaStore row id such as "1000000027"),
and SEND_MULTIPLE shares only carried a wildcard type like `image/*`.

- `uploadBlob` copies `content://` URIs into the cache, uploads the copy
  and deletes it afterwards, also when the upload fails
- `ShareIntentStore` asks the providing app for the display name, size
  and MIME type when the intent arrives, falling back to the URI segment
  plus an extension for the MIME type
- the composer gets the real name, type and size, so the size limit
  check now applies to shared files too
2026-09-24 18:43:43 +02:00

60 lines
2.0 KiB
TypeScript

import { NativeEventEmitter, NativeModules, Platform } from 'react-native';
import type { SharePayload } from '../navigation/linking';
// Android share sheet (ACTION_SEND / ACTION_SEND_MULTIPLE) → composer.
// MainActivity stashes the intent in ShareIntentStore; JS drains it here on
// launch and listens for later shares while running.
type ShareNative = {
getInitialShare?: () => Promise<SharePayload | null>;
};
function native(): ShareNative | null {
if (Platform.OS !== 'android') return null;
return ((NativeModules as Record<string, unknown>).BulwarkFcm as ShareNative | undefined) ?? null;
}
export async function getInitialShare(): Promise<SharePayload | null> {
const mod = native();
if (!mod?.getInitialShare) return null;
try {
return (await mod.getInitialShare()) ?? null;
} catch {
return null;
}
}
export function addShareListener(listener: (payload: SharePayload) => void): () => void {
if (Platform.OS !== 'android' || !NativeModules.BulwarkFcm) return () => undefined;
const emitter = new NativeEventEmitter(NativeModules.BulwarkFcm);
const sub = emitter.addListener('app:share', listener);
return () => sub.remove();
}
/** Attachment descriptors for the composer's `prefillAttachments`. */
export function shareAttachments(
share: SharePayload,
): Array<{ uri: string; name: string; type: string; size?: number }> {
const uris = share.uris ?? [];
return uris.map((uri, i) => {
const mimeType = share.mimeTypes?.[i];
const type = mimeType && !mimeType.includes('*') ? mimeType : 'application/octet-stream';
const size = share.sizes?.[i];
return {
uri,
name: share.names?.[i]?.trim() || nameFromUri(uri, i),
type,
...(typeof size === 'number' && size >= 0 ? { size } : {}),
};
});
}
function nameFromUri(uri: string, i: number): string {
const last = uri.split('/').filter(Boolean).pop() ?? `shared-${i + 1}`;
try {
return decodeURIComponent(last);
} catch {
return last;
}
}