Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions src/web/public/image-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,16 @@ Object.assign(CodemanApp.prototype, {
/** What a pick or a paste may upload: 'image', or 'video' for the camera
* roll's clips (the server streams those to disk under their own cap), else
* null. The one filter behind Attach, drop, the Compose and paste-dialog
* pickers and the clipboard handlers. The MIME type decides; some Android
* file managers hand over a File with none, so the name's extension is the
* fallback, mirroring the server's own allowlists. */
* pickers and the clipboard handlers. The MIME type decides, but a video MIME
* only for the containers the server admits; some Android file managers hand
* over a File with none, so the name's extension is the fallback, mirroring
* the server's own allowlists. */
_promptAttachKind(file) {
if (file.type.startsWith('image/')) return 'image';
if (file.type.startsWith('video/')) return 'video';
// A video MIME counts only for the five containers the server accepts on
// ?kind=video (its own set in session-routes.ts); a 3gp or mkv falls to the
// extension check below and is refused up front, not after a 1GB upload and a 400.
if (/^video\/(mp4|quicktime|x-m4v|webm|ogg)$/i.test(file.type)) return 'video';
const ext = (file.name || '').toLowerCase().match(/\.([a-z0-9]+)$/)?.[1] || '';
if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'bmp', 'heic', 'heif'].includes(ext)) return 'image';
if (['mp4', 'webm', 'mov', 'm4v', 'ogv'].includes(ext)) return 'video';
Expand Down
15 changes: 15 additions & 0 deletions test/toolbar-attach.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,21 @@ describe('phone toolbar Attach button', () => {
expect(kind('clip.ogv', 'image/jpeg')).toBe('image'); // the MIME type wins when present
});

it('counts a video MIME only for the containers the server admits on ?kind=video', () => {
const { app, window } = loadToolbar();
const kind = (type: string, name = 'clip') => app._promptAttachKind(new window.File(['x'], name, { type }));
// The five the server's ?kind=video branch accepts (session-routes.ts).
for (const type of ['video/mp4', 'video/quicktime', 'video/x-m4v', 'video/webm', 'video/ogg']) {
expect(kind(type)).toBe('video');
}
// A video MIME outside that set is refused client-side (no full upload then a
// 400) when the name's extension is not one the server admits either.
expect(kind('video/3gpp', 'clip.3gp')).toBeNull();
expect(kind('video/x-matroska', 'clip.mkv')).toBeNull();
// An admitted extension still rescues an odd MIME, as the server's path does.
expect(kind('video/3gpp', 'clip.mp4')).toBe('video');
});

it('flags the upload as a video by name as well as by MIME type', async () => {
const { app, window, fetchMock } = loadToolbar();
await app._uploadPasteImage('session-1', new window.File(['v'], 'clip.ogv', { type: '' }));
Expand Down
Loading