react-native-attachment-kit 1.1.2 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -1
- package/README.md +75 -4
- package/android/src/main/libs/rnattachmentkit-release.aar +0 -0
- package/android/src/newarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +21 -1
- package/android/src/oldarch/java/com/rnattachmentkit/RNAttachmentKitModule.kt +21 -1
- package/ios/RNAttachmentKit.swift +4 -3
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.abi.json +7072 -4567
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.private.swiftinterface +35 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios.swiftinterface +35 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeResources +12 -12
- package/ios/RNAttachmentKit.xcframework/ios-arm64/RNAttachmentKit.framework/_CodeSignature/CodeSignature +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Assets.car +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.abi.json +7072 -4567
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +35 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/arm64-apple-ios-simulator.swiftinterface +35 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.abi.json +7072 -4567
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +35 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/Modules/RNAttachmentKit.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +35 -1
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/RNAttachmentKit +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeDirectory +0 -0
- package/ios/RNAttachmentKit.xcframework/ios-arm64_x86_64-simulator/RNAttachmentKit.framework/_CodeSignature/CodeResources +22 -22
- package/package.json +1 -1
- package/src/index.ts +24 -3
- package/src/types.ts +55 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## 1.2.2 — 2026-11-14
|
|
4
4
|
|
|
5
|
+
- **Viewer: a video opened with the UI hidden no longer flashes the centre button while it starts (iOS + Android)**. Opening a remote video with `startWithUiHidden: true` (e.g. from a feed card, with `startTime`) showed the 64 pt glass button for about half a second before playback began, as the loading circle with ⏸ showing through it.
|
|
6
|
+
- iOS: while seeking to `startTime` the player is still paused, so the "stopped → show the button" rule showed it at once. Now, from the moment the viewer starts the video with the UI hidden until it actually plays (seeking to the start position included), the button stays hidden; only if loading takes longer than **1.5 s** (instead of 0.4 s) does the loading circle appear, without the ⏸/▶ icon. It ends as soon as the video plays, stops, ends, fails, or the user taps to show the UI — after that everything works as before (buffering mid-playback still shows the circle after 0.4 s). Also applies to a video page reached by swiping while the UI is hidden.
|
|
7
|
+
- iOS: `ViewerPlayButton` — changing the icon (`setPause`) reset the icon's alpha, so ⏸/▶ showed through the loading circle whenever the icon changed during loading (also with the UI visible). The button now keeps the icon hidden while loading.
|
|
8
|
+
- Android (`ViewerVideoController`): the ⏸ flash did not happen (the player starts with `playWhenReady`), but a slow stream showed the loading circle after 0.4 s; with the UI hidden the first start now uses the same 1.5 s threshold.
|
|
9
|
+
- Checked on the iOS simulator (harness `-viewerResumeTest -resumeHiddenUI -resumeLogButton`, local server with a throttled 12 s mp4, button opacity logged every 0.05 s): `startTime` 7.5 s on a fast link — before: button visible 0.45 → 1.45 s with circle + ⏸; after: never visible. Slow link (start takes ~2 s): only the circle appears at 1.5 s after loading began, fades out when playback starts, no ⏸. From the beginning without `startTime`: no button on the fast link. UI shown: unchanged (circle while loading, ⏸ once playing — the icon no longer shows through the circle). Android: compiled only, not run on a device (harness gained `--ez resumeHiddenUI true`).
|
|
10
|
+
|
|
11
|
+
## 1.2.1 — 2026-11-14
|
|
12
|
+
|
|
13
|
+
- **Viewer: `ViewerVideo.startTime` — open a video page at a given position, and `onViewerWillClose` tells where each video stopped (iOS + Android `useOverlayViewer`)**. For a feed that autoplays a muted preview in the card and opens the full video in the viewer: pass the card's position as `startTime` (seconds) and the viewer continues from there; on close, `onViewerWillClose({ currentIndex, videoPositions })` gives the last position of every video page played (keys are page indexes), so the card can continue from there. Without `startTime` nothing changes, and code that calls `onViewerWillClose` without reading the argument keeps working.
|
|
14
|
+
- Only the first time the page is shown; swiping away and back starts from the beginning as before. 0, negative, or ≥ the length (the given `duration`, or the real length once loaded) = from the start. The progress pill starts at that position.
|
|
15
|
+
- iOS: the player waits for `readyToPlay`, seeks exactly (zero tolerance), then plays; until then the page shows the poster (`ViewerVideoItemView.startHeld`) and the centre button shows loading, so neither the first frames nor the first sound come through. `ViewerVideoItem.startTime`, `RemoteImageViewerViewController.willCloseEventBody()` (the event body, also usable from native screens).
|
|
16
|
+
- Android: `ExoPlayer.setMediaItem / setMediaSource(…, startPositionMs)` — the first rendered frame is already the start position (the poster hides on the first frame as before). `ViewerVideoSource.startTimeMs` / `durationMs`, bundle keys `videoStartTimes` / `videoDurations`; `VIEWER_WILL_CLOSE` carries `currentIndex` and a `videoPositions` bundle.
|
|
17
|
+
- JS: `onViewerWillClose` now runs once per close (on Android the event can arrive through both emitters).
|
|
18
|
+
- Checked on the iOS simulator and an Android emulator with a 12 s test video that shows its own time in every frame, recorded on screen: start 7.5 s streaming (download into `cacheFile` still running, 1 MB/s) and from the cached file — the first video frame on screen is 07.50 on both platforms, no earlier frame shows; positions reported on close match the last frame on screen within 0.05 s; no `startTime` starts at 00.00 as before; `startTime: 20` on the 12 s video starts at 00.00.
|
|
19
|
+
|
|
20
|
+
## 1.2.0 — 2026-11-14
|
|
21
|
+
|
|
22
|
+
- **Video: `video.preview` — a small silent preview mp4 next to the video you send (iOS + Android)**. For feeds that autoplay videos in a list: `preview: true` or `{ height, bitrate, maxFps, maxDurationSec, codec }` (defaults 360 short side, 450 000 bps, 24 fps, full length, h264). The preview is encoded **from the finished send file** (so trim, crop, rotation, drawing and mute are already in it; a remuxed send file works too), video only, keyframe every second, moov before mdat. Result fields `previewUri`, `previewWidth`, `previewHeight`, `previewDuration`, `previewFileSize`, `previewBitrate`; `previewSkipped: "SOURCE_SMALL"` when the send file is already as small (short side ≤ `height`, or its file ≤ the expected preview size); `previewError: "ENCODE_FAILED"` when only the preview failed (the send file is returned as usual). Also in `onImageProcessed`. Progress stays one value per video (send file 0–0.9, preview 0.9–1) and the HUD closes after the preview; cancelling (`cancelVideoProcessing`, HUD cancel) deletes the send file, poster and preview. File `<baseName>.preview.mp4` in the same folder. Without `preview` nothing changes.
|
|
23
|
+
- iOS: `VideoPreviewOptions` / `VideoPreviewResult` / `VideoExporter.makePreview(from:options:to:progress:)` (AVAssetReader + Writer, `shouldOptimizeForNetworkUse`), `previewWorthMaking`; `AttachmentResult.preview*`.
|
|
24
|
+
- Android: same names in `VideoExporter.kt` (`makePreview` with media3 Transformer — `setRemoveAudio`, `Presentation`, `setiFrameIntervalSeconds(1)`, CBR bitrate mode with a VBR retry when the encoder has no CBR; `maxFps` is not applied — media3 1.4.1's `FrameDropEffect` did not drop frames in Transformer; portrait previews carry a 90° rotation tag like the Android send file), and `Mp4FastStart` moves the moov box to the front (MediaMuxer writes it at the end); `AttachmentProcessor.Result.preview*`. The existing transcode now shares `runTransformer` with the preview (same settings as before).
|
|
25
|
+
- `createVideoPreview(fileUri)` (stand-alone, step 2 of the request) is not included.
|
|
26
|
+
|
|
27
|
+
- **Gallery / editor: the "video too long" message says seconds when the limit is under a minute or not whole minutes (iOS + Android)**. The message was always built as minutes, rounded and at least 1, so a 15 s limit said "up to 1 minute". New string `video_too_long_seconds` (`turbo_video_too_long_seconds` on Android) in all 59 languages; the choice lives in one helper per platform, `VideoExporter.tooLongText` (seconds if the limit is under 60 s or not a multiple of 60, otherwise minutes as before). Used by the gallery's too-long alert / toast and the editor's trim-bar warning and send-issue text.
|
|
5
28
|
- **Viewer (iOS): a video page with no poster image now shows the video**. The player layer sits inside the page's image view, and that view only got a size from a loaded image, so with an empty poster URL or a failed poster download the layer stayed 0×0: the video played (sound, position moving) on an empty page. `ViewerVideoItemView` now sizes the area from `presentationSize` (rotation metadata included), or the page size until that is known. Once a poster arrives its size is used, as before.
|
|
6
29
|
- Checked on the iOS simulator (test harness): empty poster and 404 poster with local / remote / download-to-`cacheFile` videos, landscape, portrait and 90°-rotation-metadata files. Poster pages have the same frames as before; a poster arriving late, swiping away and back, pinch zoom, and the zoom close from the current frame all work.
|
|
7
30
|
|
package/README.md
CHANGED
|
@@ -8,6 +8,7 @@ A fully native attachment toolkit for React Native chat apps: a Telegram-style p
|
|
|
8
8
|
|
|
9
9
|
- **Pure native UI** (Swift / Kotlin) — smooth on both platforms, New Architecture (Turbo Modules) and old architecture supported.
|
|
10
10
|
- **Photo picker** — bottom-sheet gallery, single or multi selection, albums, camera cell, optional editor step, profile (1:1) crop mode.
|
|
11
|
+
- In-app camera: tap the shutter for a photo, **hold it to record a video** (when `mediaType` is `'all'` / `'video'`); a ratio pill (Full / 9:16 / 3:4 / 1:1) sets the framing for both, and a resolution pill sets the video quality (HD / FHD default / 4K when supported). The screen stays portrait; holding the phone sideways captures a landscape photo or video (only the controls rotate). A timer (3s/10s), tap-to-focus with an exposure handle, and a ⚙ menu (grid, level, stabilization, selfie mirror, 30/60 fps) are built in; the front camera uses the screen as a flash. Captures go to the photo library and are inserted at the top of the grid, selected.
|
|
11
12
|
- **Attach menu** — Gallery / **Location** / **File** tabs under the gallery (Telegram style).
|
|
12
13
|
- Location: map with a center pin, "send my current location", place search, "Share my location" switch, map snapshot image.
|
|
13
14
|
- File: system file picker, files copied into the app's temp folder.
|
|
@@ -65,6 +66,9 @@ Rebuild the app after installing or updating (the module ships compiled native c
|
|
|
65
66
|
```xml
|
|
66
67
|
<key>NSCameraUsageDescription</key>
|
|
67
68
|
<string>We need access to your camera to take photos.</string>
|
|
69
|
+
<!-- In-app camera: hold the shutter to record a video with sound (without this key the recording has no audio) -->
|
|
70
|
+
<key>NSMicrophoneUsageDescription</key>
|
|
71
|
+
<string>We need the microphone to record sound in videos.</string>
|
|
68
72
|
<key>NSPhotoLibraryUsageDescription</key>
|
|
69
73
|
<string>We need access to your photo library to select images.</string>
|
|
70
74
|
<key>NSPhotoLibraryAddUsageDescription</key>
|
|
@@ -72,8 +76,18 @@ Rebuild the app after installing or updating (the module ships compiled native c
|
|
|
72
76
|
<!-- Attach menu → Location ("send my current location", my-location dot) -->
|
|
73
77
|
<key>NSLocationWhenInUseUsageDescription</key>
|
|
74
78
|
<string>We use your location to share it in a chat.</string>
|
|
79
|
+
<!-- Map viewer → Directions: lets it tell whether each map app is installed -->
|
|
80
|
+
<key>LSApplicationQueriesSchemes</key>
|
|
81
|
+
<array>
|
|
82
|
+
<string>comgooglemaps</string>
|
|
83
|
+
<string>kakaomap</string>
|
|
84
|
+
<string>nmap</string>
|
|
85
|
+
<string>tmap</string>
|
|
86
|
+
</array>
|
|
75
87
|
```
|
|
76
88
|
|
|
89
|
+
The Directions menu shows Apple Maps and Google Maps, plus Kakao Map, Naver Map and TMAP when `languageCode` is Korean. Without the schemes above it still works: an app that isn't installed falls back to the web map (TMAP: App Store), and cancelling the system "Open in …?" prompt opens nothing.
|
|
90
|
+
|
|
77
91
|
### Android — Google Maps key
|
|
78
92
|
|
|
79
93
|
The Location tab, the map viewer and `renderLocationThumbnail` use Google Maps. The library ships **no key**; add your own to the app's `AndroidManifest.xml`:
|
|
@@ -155,7 +169,7 @@ Opens the gallery bottom sheet. Resolves with the picked images (`[]` when the u
|
|
|
155
169
|
| `onProcessingStart` | `(e: ProcessingStartEvent) => void` | — | Once, right after Send, before any processing. |
|
|
156
170
|
| `onImageProcessed` | `(e) => void` | — | Per item `{ index, total, image }` as soon as it is ready. |
|
|
157
171
|
| `mediaType` | `'photo' \| 'all' \| 'video'` | `'photo'` | `'all'` mixes photos and videos. |
|
|
158
|
-
| `video` | `VideoOptions` | — | `maxDurationSec`, `maxFileSizeMB`, `qualities`, `defaultQuality`, `codec`, `onProgress`, `showProgressHUD`. |
|
|
172
|
+
| `video` | `VideoOptions` | — | `maxDurationSec`, `maxFileSizeMB`, `qualities`, `defaultQuality`, `codec`, `posterFormat`, `preview`, `onProgress`, `showProgressHUD`. |
|
|
159
173
|
| `openDuration` / `closeDuration` | `number` (ms) | native | Animation durations. |
|
|
160
174
|
| `showAttachMenu` … | | | Attach menu options — see [Attach menu](#attach-menu-location--file). |
|
|
161
175
|
|
|
@@ -175,6 +189,25 @@ Opens the gallery bottom sheet. Resolves with the picked images (`[]` when the u
|
|
|
175
189
|
| `fileName?` / `fileExtension?` / `fileSize?` | | Output file info (bytes). |
|
|
176
190
|
| `thumbnailUri?` / `thumbnailWidth?` / `thumbnailHeight?` | | Photo: the thumbnail when the `thumbnail` option is set. Video: the poster (always, see `video.posterFormat`). |
|
|
177
191
|
| `thumbnailFileSize?` | `number` | Photo thumbnail file size (bytes). |
|
|
192
|
+
| `previewUri?` / `previewWidth?` / `previewHeight?` / `previewDuration?` / `previewFileSize?` / `previewBitrate?` | | Video with `video.preview`: the small silent preview mp4. |
|
|
193
|
+
| `previewError?` / `previewSkipped?` | `string` | Preview failed (`"ENCODE_FAILED"`, the video itself is fine) / not made (`"SOURCE_SMALL"`). |
|
|
194
|
+
|
|
195
|
+
#### Video previews (`video.preview`, 1.2.0)
|
|
196
|
+
|
|
197
|
+
For feeds that autoplay videos silently in a list, ask for a small preview file next to the one you send:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
const items = await AttachmentKit.openGallery({ mediaType: 'all', video: { preview: true } })
|
|
201
|
+
// or tune it: video: { preview: { height: 360, bitrate: 450_000, maxFps: 24, maxDurationSec: 0, codec: 'h264' } }
|
|
202
|
+
// items[i].previewUri → file://….preview.mp4 (no audio, 360p, ~450 kbps, moov first, keyframe every second)
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- Made **from the video you send** after it is ready, so trims, crops, drawings and mute are all in it.
|
|
206
|
+
- `height` is the short side (default 360), `bitrate` default 450 000 bps, `maxFps` default 24 (iOS only — on Android media3 1.4.1 keeps the source frame rate; the size is still set by `bitrate`), `maxDurationSec` 0 = full length, `codec` default `h264`.
|
|
207
|
+
- Not made when the sent video is already as small (short side ≤ `height`, or its file ≤ the expected preview size): `previewSkipped: "SOURCE_SMALL"` — use the sent file.
|
|
208
|
+
- A failed preview never fails the video: `uri` is returned and `previewError: "ENCODE_FAILED"` is set.
|
|
209
|
+
- Progress stays one 0 → 1 value per video (send file 0–0.9, preview 0.9–1); the progress HUD closes after the preview. Cancelling removes the sent file, poster and preview.
|
|
210
|
+
- Saved next to the sent file as `<name>.preview.mp4`.
|
|
178
211
|
|
|
179
212
|
#### Photo thumbnails
|
|
180
213
|
|
|
@@ -210,7 +243,7 @@ Selection events outside `openGallery`; returns an unsubscribe function.
|
|
|
210
243
|
|
|
211
244
|
A chat can show every sent item at once and fill it in as it finishes (photos are resized, videos are encoded), the same way for photos and videos:
|
|
212
245
|
|
|
213
|
-
1. **`onProcessingStart`** `{ total, items }`: once, right after Send. Each item has `index`, `mediaType` (`'image' | 'video'`), `originalUri`, and the expected output `width` / `height` (for the aspect ratio). Videos also have `duration` (seconds, after trim) and `thumbnailUri` / `thumbnailWidth` / `thumbnailHeight` (a poster
|
|
246
|
+
1. **`onProcessingStart`** `{ total, items }`: once, right after Send. Each item has `index`, `mediaType` (`'image' | 'video'`), `originalUri`, and the expected output `width` / `height` (for the aspect ratio). Videos also have `duration` (seconds, after trim) and `thumbnailUri` / `thumbnailWidth` / `thumbnailHeight` (a poster image with crop and drawing applied — **webp** by default; set `video.posterFormat: 'jpg'` for jpg). Create the placeholders here.
|
|
214
247
|
2. **`onVideoProgress`** `{ index, total, progress }`: video encoding 0…1 (at most every 1% / 0.1 s). Draw a progress ring on that placeholder.
|
|
215
248
|
3. **`onImageProcessed`** `{ index, total, image }`: the item is ready. Replace the placeholder with `image.uri`. A video the app cancelled comes as `{ error: 'CANCELLED', cancelled: true }`; a failed one has `error`.
|
|
216
249
|
|
|
@@ -332,6 +365,24 @@ ref.current?.measure((x, y, width, height, pageX, pageY) => {
|
|
|
332
365
|
| `animationType` / `closeAnimationType` | `'zoom' \| 'fade' \| 'slide'` | `zoom` with `sourceRect`, else `slide` | Open / close animation. |
|
|
333
366
|
| `openDuration` / `closeDuration` | `number` (ms) | native | Animation durations. |
|
|
334
367
|
|
|
368
|
+
**Videos** — iOS viewer and Android `useOverlayViewer: true` only (the other Android viewers show the poster)
|
|
369
|
+
|
|
370
|
+
| Option | Type | Description |
|
|
371
|
+
|---|---|---|
|
|
372
|
+
| `videos` | `(ViewerVideo \| null)[]` | Same order as `images`. A non-null entry makes that page a video; `images[i]` is then its **poster** (shown until the first frame, and for the open animation). `ViewerVideo = { uri, duration?, cacheFile?, startTime? }` — `uri` is `https://` (mp4/mov, or an HLS `.m3u8` stream) or a local file; `cacheFile` works like `cacheFiles`: if the file exists it is played without network, otherwise the video streams and is downloaded there in the background for next time. `startTime` (seconds, 1.2.1): the first time that page is shown, play from there instead of the start — the poster stays until the player has moved to that point, so the opening frames never flash; 0, negative or ≥ the length = from the start; leaving the page and coming back starts from the beginning as before. Streaming and cached files both. |
|
|
373
|
+
|
|
374
|
+
```ts
|
|
375
|
+
AttachmentKit.openViewer({
|
|
376
|
+
images: [photoUrl, videoPosterUrl],
|
|
377
|
+
videos: [null, { uri: videoUrl, duration: 12.4, cacheFile: `${cacheDir}/v_123.mp4`, startTime: card.position }],
|
|
378
|
+
useOverlayViewer: true,
|
|
379
|
+
// where the user stopped watching (seconds), e.g. to continue the card's preview from there
|
|
380
|
+
onViewerWillClose: (e) => { const t = e?.videoPositions?.[1]; if (t != null) card.seek(t) },
|
|
381
|
+
})
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
On a video page: autoplay from the start (leaving the page pauses, no loop — at the end it pauses and shows ▶), a centre ▶/❚❚ button (spinner while buffering), a progress pill `0:03 ━━●── 0:12` (drag to seek), ⚙ playback speed (`0.5x` / Normal / `1.5x` / `2x`, kept while the viewer is open) and ⛶ full screen for landscape videos (the app's orientation is never changed — the video and its controls are rotated 90° to fill the screen; stand the phone up or tap ⛶ / Android back to leave). Tap toggles the controls (auto-hide after 3 s while playing); no pinch zoom; pull-to-dismiss moves only the video and closes on the current frame. Save / share handle videos as files: saved to the photo library as a video, shared as an mp4 (a remote video is downloaded first, with a small progress label; HLS streams cannot be saved). The sound follows the silent switch on iOS (default audio session).
|
|
385
|
+
|
|
335
386
|
**Sticky transition**
|
|
336
387
|
|
|
337
388
|
| Option | Type | Description |
|
|
@@ -360,7 +411,7 @@ ref.current?.measure((x, y, width, height, pageX, pageY) => {
|
|
|
360
411
|
| Option | Platform | Description |
|
|
361
412
|
|---|---|---|
|
|
362
413
|
| `onPageSelected(index)` | both | Page changed. |
|
|
363
|
-
| `onViewerWillClose()` | both | Close animation starts. |
|
|
414
|
+
| `onViewerWillClose(e?)` | both | Close animation starts. 1.2.1: `e = { currentIndex, videoPositions? }` — `videoPositions` maps each video page played in this viewer to its last position in seconds (the length if it played to the end); pages never played are absent. Old code that ignores the argument still works. On Android only the `useOverlayViewer` viewer sends `e` (the other Android viewers call it with no argument). |
|
|
364
415
|
| `onViewerOpened()` | Android | Open animation finished (safe moment for background scroll corrections). |
|
|
365
416
|
|
|
366
417
|
**Android viewer modes**
|
|
@@ -483,7 +534,7 @@ A native grid built for thousands of photos. It opens like the picker (same bott
|
|
|
483
534
|
|
|
484
535
|
- **Native data source (recommended)**: implement `MediaGalleryDataSource` in your app's native code and register it with `MediaGalleryRegistry.register("name", source)` (iOS Swift / Android Kotlin). Pass `dataSource: "name"` and `dataSourceParams`. Pages are loaded natively, no JS round trip. Built-in demo: `dataSource: "turboDemo", dataSourceParams: { count: 10000 }`.
|
|
485
536
|
- **JS data source**: pass `initialItems`, `hasMore`, `nextCursor`; answer `onNeedPage` with `appendGalleryItems({ galleryId, items, nextCursor, hasMore })`.
|
|
486
|
-
- `MediaGalleryItem`: `{ id, groupId?, timestamp?, thumbUri? (file:// only — no base64), imageUri, cacheFile?, isMine?, width?, height? }`.
|
|
537
|
+
- `MediaGalleryItem`: `{ id, groupId?, timestamp?, thumbUri? (file:// only — no base64), imageUri, cacheFile?, isMine?, width?, height?, mediaType?, videoUri?, videoCacheFile?, duration? }`. A video item has `mediaType: 'video'` and `videoUri` (`imageUri` / `thumbUri` are its poster); the grid shows a "▶ 0:12" badge (from `duration`, seconds) and the viewer plays it (see `openViewer` → Videos).
|
|
487
538
|
- `title` (required), `gridSize: '3x3' | '4x4' | '5x5'` (default until the user picks one; the user's choice is saved on the device and wins), `spacing`, `theme`, `appearance`, `languageCode`, `emptyTitle`, `emptySubtitle`, `countFormat`.
|
|
488
539
|
- `viewer`: `{ themeColor, deleteOptionsForMine, deleteOptionsForOthers, titleFormat: 'date' | 'none', …openViewer text options }`.
|
|
489
540
|
- Events: `onItemDeleted({ galleryId, id, groupId, option })` (the module already removed that group from the screen — delete it in the app), `onNeedPage`, `onGridSizeChanged`, `onClosed`.
|
|
@@ -596,6 +647,26 @@ app.post('/ai/generate', auth, express.json({ limit: '20mb' }), async (req, res)
|
|
|
596
647
|
| Photo thumbnails (`thumbnail` option) | `Library/Caches/RNAttachmentKit/turbo_picker_thumb_*` | `cache/turbo_picker_thumb_*` | Deleted after 24 h (next gallery open). |
|
|
597
648
|
| Location thumbnails | `Library/Caches/RNAttachmentKit/LocationThumbs/` | `cache/RNAttachmentKitLocationThumbs/` | 300 max → 250, least recently used. `clearLocationThumbnailCache()`. |
|
|
598
649
|
| Reverse-geocode results | `Library/Caches/RNAttachmentKit/Geocode/` | `cache/RNAttachmentKitGeocode/` | 1000 max → 800, least recently used. `clearReverseGeocodeCache()`. |
|
|
650
|
+
| Viewer videos downloaded for save/share (no `cacheFile` given) | `Library/Caches/RNAttachmentKit/viewer_video/` | `cache/RNAttachmentKit/viewer_video/` | Reused for playback next time; the OS may purge. With `cacheFile` the file goes there instead (app-owned). |
|
|
651
|
+
| Viewer share copies (Android) | — | `<external files>/Pictures/viewer_share/` | Overwritten names per share; same folder as shared photos. |
|
|
652
|
+
|
|
653
|
+
---
|
|
654
|
+
|
|
655
|
+
## 🔧 Maintainers (source project)
|
|
656
|
+
|
|
657
|
+
This folder is the **source** project. Apps use the compiled distribution `../RNAttachmentKit-Dist` (published to npm as `react-native-attachment-kit`), where Kotlin/Swift are shipped as `.aar` / `.xcframework`.
|
|
658
|
+
|
|
659
|
+
- **Edit code only here.** After changing native code or `src/*.ts`, rebuild the distribution — apps see nothing until then:
|
|
660
|
+
```bash
|
|
661
|
+
cd .. && bash -l ./run_deploy.command # make_framework.rb + deploy.rb, log in ../deploy_last.log
|
|
662
|
+
cd RNAttachmentKit/ios && ruby restore_app.rb # back to app mode for the iOS harness
|
|
663
|
+
```
|
|
664
|
+
Hand-edit in Dist only `package.json`, the podspec, `react-native.config.js` and `README.md`. Keep this README and the Dist README in sync (same content; this section is source-only).
|
|
665
|
+
- **Native test apps**: iOS — `ios/RNAttachmentKit.xcodeproj` in app mode (`ruby restore_app.rb`; `ruby make_framework.rb` before building the framework). Android — open `android/` and run `app` (its Maps key lives in git-ignored `android/local.properties`, for the test app only). RN example app: `../RNAttachmentKitApp` (`portal:` to Dist).
|
|
666
|
+
- **Translations**: all strings live in `scripts/i18n/strings.json` (every language must have every key). Run `python3 scripts/i18n/generate.py` to regenerate `ios/RNAttachmentKit/Localizer.swift` and Android `res/values*/strings*.xml`.
|
|
667
|
+
- **Editor icons**: single source `scripts/editor_icons/icons.py` (Android vectors + iOS `ToolbarIcons` webp).
|
|
668
|
+
- **Docs**: version history in `CHANGELOG.md`; location-message integration notes in `docs/location-api.md`; old diagnostics in `docs/history/` (not authoritative).
|
|
669
|
+
- **Publishing**: bump `version` in both `package.json` files, rebuild Dist, then run `../publish_npm.sh` from a terminal (npm login + OTP).
|
|
599
670
|
|
|
600
671
|
---
|
|
601
672
|
|
|
Binary file
|
|
@@ -89,12 +89,24 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
89
89
|
// 채로 다음 이미지를 열면, 두 구독이 동시에 반응해서 "이전 이미지의 위치 정보"로
|
|
90
90
|
// updateSourceRect()가 잘못 호출되어 닫기 애니메이션이 엉뚱한(이전) 위치로 돌아가는
|
|
91
91
|
// 버그의 원인이었습니다.
|
|
92
|
+
/** 1.2.1: VIEWER_WILL_CLOSE 의 currentIndex·videoPositions → JS 본문 (오버레이 뷰어만 보냄, 다른 뷰어는 빈 본문) */
|
|
93
|
+
private fun viewerWillCloseBody(intent: android.content.Intent): com.facebook.react.bridge.WritableMap {
|
|
94
|
+
val body = com.facebook.react.bridge.Arguments.createMap()
|
|
95
|
+
if (intent.hasExtra("currentIndex")) body.putInt("currentIndex", intent.getIntExtra("currentIndex", 0))
|
|
96
|
+
intent.getBundleExtra("videoPositions")?.let { b ->
|
|
97
|
+
val m = com.facebook.react.bridge.Arguments.createMap()
|
|
98
|
+
for (k in b.keySet()) m.putDouble(k, b.getDouble(k))
|
|
99
|
+
body.putMap("videoPositions", m)
|
|
100
|
+
}
|
|
101
|
+
return body
|
|
102
|
+
}
|
|
103
|
+
|
|
92
104
|
private val viewerWillCloseReceiver = object : android.content.BroadcastReceiver() {
|
|
93
105
|
override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
|
|
94
106
|
if (intent?.action == "com.rnattachmentkit.VIEWER_WILL_CLOSE") {
|
|
95
107
|
reactApplicationContext
|
|
96
108
|
.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
97
|
-
.emit("onViewerWillClose",
|
|
109
|
+
.emit("onViewerWillClose", viewerWillCloseBody(intent))
|
|
98
110
|
}
|
|
99
111
|
}
|
|
100
112
|
}
|
|
@@ -715,13 +727,21 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
715
727
|
if (videoArray != null) {
|
|
716
728
|
val uris = ArrayList<String>()
|
|
717
729
|
val caches = ArrayList<String>()
|
|
730
|
+
val starts = DoubleArray(videoArray.size())
|
|
731
|
+
val durations = DoubleArray(videoArray.size())
|
|
732
|
+
fun num(m: com.facebook.react.bridge.ReadableMap?, key: String): Double =
|
|
733
|
+
if (m != null && m.hasKey(key) && m.getType(key) == com.facebook.react.bridge.ReadableType.Number) m.getDouble(key) else 0.0
|
|
718
734
|
for (i in 0 until videoArray.size()) {
|
|
719
735
|
val m = if (videoArray.getType(i) == com.facebook.react.bridge.ReadableType.Map) videoArray.getMap(i) else null
|
|
720
736
|
uris.add(if (m != null && m.hasKey("uri") && !m.isNull("uri")) m.getString("uri") ?: "" else "")
|
|
721
737
|
caches.add(if (m != null && m.hasKey("cacheFile") && !m.isNull("cacheFile")) m.getString("cacheFile") ?: "" else "")
|
|
738
|
+
starts[i] = num(m, "startTime")
|
|
739
|
+
durations[i] = num(m, "duration")
|
|
722
740
|
}
|
|
723
741
|
putStringArrayList(com.rnattachmentkit.ViewerVideoSource.ARG_URIS, uris)
|
|
724
742
|
putStringArrayList(com.rnattachmentkit.ViewerVideoSource.ARG_CACHE_FILES, caches)
|
|
743
|
+
putDoubleArray(com.rnattachmentkit.ViewerVideoSource.ARG_START_TIMES, starts)
|
|
744
|
+
putDoubleArray(com.rnattachmentkit.ViewerVideoSource.ARG_DURATIONS, durations)
|
|
725
745
|
}
|
|
726
746
|
}
|
|
727
747
|
// 상단바 저장/공유/삭제 버튼과 그 문구들 (iOS와 동일한 옵션 키). 문구를 안 넘기면
|
|
@@ -78,12 +78,24 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
78
78
|
// Keep: Required for RN built in Event Emitter Calls.
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/** 1.2.1: VIEWER_WILL_CLOSE 의 currentIndex·videoPositions → JS 본문 (오버레이 뷰어만 보냄, 다른 뷰어는 빈 본문) */
|
|
82
|
+
private fun viewerWillCloseBody(intent: android.content.Intent): com.facebook.react.bridge.WritableMap {
|
|
83
|
+
val body = com.facebook.react.bridge.Arguments.createMap()
|
|
84
|
+
if (intent.hasExtra("currentIndex")) body.putInt("currentIndex", intent.getIntExtra("currentIndex", 0))
|
|
85
|
+
intent.getBundleExtra("videoPositions")?.let { b ->
|
|
86
|
+
val m = com.facebook.react.bridge.Arguments.createMap()
|
|
87
|
+
for (k in b.keySet()) m.putDouble(k, b.getDouble(k))
|
|
88
|
+
body.putMap("videoPositions", m)
|
|
89
|
+
}
|
|
90
|
+
return body
|
|
91
|
+
}
|
|
92
|
+
|
|
81
93
|
private val viewerWillCloseReceiver = object : android.content.BroadcastReceiver() {
|
|
82
94
|
override fun onReceive(context: android.content.Context?, intent: android.content.Intent?) {
|
|
83
95
|
if (intent?.action == "com.rnattachmentkit.VIEWER_WILL_CLOSE") {
|
|
84
96
|
reactApplicationContext
|
|
85
97
|
.getJSModule(com.facebook.react.modules.core.DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
86
|
-
.emit("onViewerWillClose",
|
|
98
|
+
.emit("onViewerWillClose", viewerWillCloseBody(intent))
|
|
87
99
|
}
|
|
88
100
|
}
|
|
89
101
|
}
|
|
@@ -724,13 +736,21 @@ class RNAttachmentKitModule(reactContext: ReactApplicationContext) :
|
|
|
724
736
|
if (videoArray != null) {
|
|
725
737
|
val uris = ArrayList<String>()
|
|
726
738
|
val caches = ArrayList<String>()
|
|
739
|
+
val starts = DoubleArray(videoArray.size())
|
|
740
|
+
val durations = DoubleArray(videoArray.size())
|
|
741
|
+
fun num(m: com.facebook.react.bridge.ReadableMap?, key: String): Double =
|
|
742
|
+
if (m != null && m.hasKey(key) && m.getType(key) == com.facebook.react.bridge.ReadableType.Number) m.getDouble(key) else 0.0
|
|
727
743
|
for (i in 0 until videoArray.size()) {
|
|
728
744
|
val m = if (videoArray.getType(i) == com.facebook.react.bridge.ReadableType.Map) videoArray.getMap(i) else null
|
|
729
745
|
uris.add(if (m != null && m.hasKey("uri") && !m.isNull("uri")) m.getString("uri") ?: "" else "")
|
|
730
746
|
caches.add(if (m != null && m.hasKey("cacheFile") && !m.isNull("cacheFile")) m.getString("cacheFile") ?: "" else "")
|
|
747
|
+
starts[i] = num(m, "startTime")
|
|
748
|
+
durations[i] = num(m, "duration")
|
|
731
749
|
}
|
|
732
750
|
putStringArrayList(com.rnattachmentkit.ViewerVideoSource.ARG_URIS, uris)
|
|
733
751
|
putStringArrayList(com.rnattachmentkit.ViewerVideoSource.ARG_CACHE_FILES, caches)
|
|
752
|
+
putDoubleArray(com.rnattachmentkit.ViewerVideoSource.ARG_START_TIMES, starts)
|
|
753
|
+
putDoubleArray(com.rnattachmentkit.ViewerVideoSource.ARG_DURATIONS, durations)
|
|
734
754
|
}
|
|
735
755
|
}
|
|
736
756
|
// 상단바 저장/공유/삭제 버튼과 그 문구들 (iOS와 동일한 옵션 키). 문구를 안 넘기면
|
|
@@ -612,8 +612,9 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
612
612
|
// 공유된 zoomAnimator.sourceRect를 덮어써버렸습니다 - "스와이프 후 닫으면 전혀
|
|
613
613
|
// 다른 그룹 이미지 위치로 애니메이션되는" 버그의 근본 원인이었습니다. 이제 실제로
|
|
614
614
|
// 뷰어가 닫히기 시작할 때 이벤트를 보내서 JS가 제때 구독을 정리하도록 합니다.
|
|
615
|
-
|
|
616
|
-
|
|
615
|
+
// 1.2.1: { currentIndex, videoPositions } — 앱은 인자를 무시해도 됨
|
|
616
|
+
viewerVC.onViewerWillClose = { [weak self, weak viewerVC] () -> Void in
|
|
617
|
+
self?.sendEvent(withName: "onViewerWillClose", body: viewerVC?.willCloseEventBody())
|
|
617
618
|
}
|
|
618
619
|
viewerVC.onImageDeleted = { [weak self] (index: Int, url: String, option: String?) -> Void in
|
|
619
620
|
guard let self = self else { return }
|
|
@@ -1122,7 +1123,7 @@ class RNAttachmentKit: RCTEventEmitter {
|
|
|
1122
1123
|
self.processingJobs[processingId] = nil
|
|
1123
1124
|
if aborted {
|
|
1124
1125
|
// 변환 취소: 이미 만든 파일은 버림
|
|
1125
|
-
results.
|
|
1126
|
+
results.flatMap { [$0.path, $0.previewPath].compactMap { $0 } }.forEach { try? FileManager.default.removeItem(atPath: $0) }
|
|
1126
1127
|
reject("CANCELLED", "User cancelled video conversion", nil)
|
|
1127
1128
|
} else {
|
|
1128
1129
|
resolve(results.map { $0.dictionary })
|