From d556677d5fbac31665b5236a8c0f6730f26d8851 Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Fri, 27 Mar 2026 16:49:35 +0530 Subject: [PATCH 01/13] adding new component for video quick action picker --- .../video-quick-action-picker-config.js | 109 +++++++++ .../video-quick-action-picker.css | 215 +++++++++++++++++ .../video-quick-action-picker.js | 225 ++++++++++++++++++ express/code/icons/edit-video.svg | 12 + 4 files changed, 561 insertions(+) create mode 100644 express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js create mode 100644 express/code/blocks/video-quick-action-picker/video-quick-action-picker.css create mode 100644 express/code/blocks/video-quick-action-picker/video-quick-action-picker.js create mode 100644 express/code/icons/edit-video.svg diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js new file mode 100644 index 000000000..c30e87f26 --- /dev/null +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js @@ -0,0 +1,109 @@ +import { getLibs } from '../../scripts/utils.js'; +import { QA_CONFIGS } from '../../scripts/utils/frictionless-utils.js'; + +export const ACTION_TYPES = { + QUICK_ACTION: 'quick-action', // launches SDK inline with the uploaded file + APP_INSTALL: 'app-install', // navigates to app install URL (same tab) +}; + +// Max duration in seconds per quick action (source: Adobe Express help docs) +const MAX_DURATION = { + 'convert-to-gif': 60, // 1 minute + 'caption-video': 300, // 5 minutes +}; + +let replaceKey; +let getConfig; + +export async function loadPlaceholders() { + const [utils, placeholders] = await Promise.all([ + import(`${getLibs()}/utils/utils.js`), + import(`${getLibs()}/features/placeholders.js`), + ]); + ({ getConfig } = utils); + ({ replaceKey } = placeholders); +} + +async function resolveKey(key, fallback) { + const resolved = await replaceKey(key, getConfig()); + if (resolved === key.replaceAll('-', ' ')) return fallback; + return resolved; +} + +export async function getLocalizedStrings() { + return { + editVideo: await resolveKey('edit-video', 'Edit video'), + appOnly: await resolveKey('ios-app-only', 'iOS App only'), + convertVideoToGif: await resolveKey('convert-video-to-gif', 'Convert video to GIF'), + cropVideo: await resolveKey('crop-video', 'Crop video'), + trimVideo: await resolveKey('trim-video', 'Trim video'), + resizeVideo: await resolveKey('resize-video', 'Resize video'), + convertVideoToMp4: await resolveKey('convert-video-to-mp4', 'Convert video to MP4'), + captionVideo: await resolveKey('caption-video', 'Caption video'), + openingPreview: await resolveKey('opening-preview', 'Opening preview'), + uploadedVideo: await resolveKey('uploaded-video', 'Uploaded video'), + previewUnavailable: await resolveKey('preview-unavailable', 'Preview unavailable'), + startFromYourVideo: await resolveKey('start-from-your-video', 'Start from your video'), + closeDialog: await resolveKey('close-dialog', 'Close dialog'), + }; +} + +export function getVideoActions(strings, videoFile, videoDuration) { + const actions = [ + { + id: 'edit-video', + label: strings.editVideo, + badge: strings.appOnly, + type: ACTION_TYPES.APP_INSTALL, + icon: 'edit-video', + }, + { + id: 'convert-to-gif', + label: strings.convertVideoToGif, + type: ACTION_TYPES.QUICK_ACTION, + icon: 'ax-convert-to-gif-22', + }, + { + id: 'crop-video', + label: strings.cropVideo, + type: ACTION_TYPES.QUICK_ACTION, + icon: 'ax-crop-image-22', // TODO: replace with crop-video-22 icon when available + }, + { + id: 'trim-video', + label: strings.trimVideo, + type: ACTION_TYPES.QUICK_ACTION, + icon: 'trim-video-22', + }, + { + id: 'resize-video', + label: strings.resizeVideo, + type: ACTION_TYPES.QUICK_ACTION, + icon: 'ax-resize-video-22', + }, + { + id: 'convert-to-mp4', + label: strings.convertVideoToMp4, + type: ACTION_TYPES.QUICK_ACTION, + icon: 'ax-convert-22', // TODO: replace with convert-video-to-mp4-22 icon when available + }, + { + id: 'caption-video', + label: strings.captionVideo, + type: ACTION_TYPES.QUICK_ACTION, + icon: 'ax-caption-video-22', + }, + ]; + + return actions.filter((action) => { + // No point offering convert-to-mp4 if the file is already an MP4 + if (action.id === 'convert-to-mp4' && videoFile.type === 'video/mp4') return false; + const config = QA_CONFIGS[action.id]; + if (!config) return false; + if (!config.input_check(videoFile.type)) return false; + if (videoFile.size > config.max_size) return false; + const maxDuration = MAX_DURATION[action.id]; + if (maxDuration && videoDuration > maxDuration) return false; + return true; + }); +} diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.css b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.css new file mode 100644 index 000000000..02c0a845b --- /dev/null +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.css @@ -0,0 +1,215 @@ +/* --- Fullscreen Dialog --- */ +.vqap-dialog { + position: fixed; + inset: 0; + --vqap-mobile-header-bar-height: 53px; + --vqap-hero-inline-mobile-height: 282px; + background: var(--body-background-color); + display: flex; + flex-direction: column; + z-index: 9999; + overflow: hidden; +} + +/* --- Close Button --- */ +.vqap-close-btn { + position: absolute; + inset-block-start: calc(var(--spacing-75) + env(safe-area-inset-top, 0px)); + inset-inline-end: var(--spacing-300); + width: 50px; + height: 50px; + border-radius: 100%; + border: 0; + background: transparent; + color: var(--color-white); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 1; +} + +/* --- Hero Container --- */ +.vqap-hero { + background: var(--color-gray-100); + height: var(--vqap-hero-inline-mobile-height); + width: 100%; + flex-shrink: 0; + margin-block-end: var(--spacing-400); +} + +/* --- Header Bar --- */ + +.vqap-header-bar { + height: var(--vqap-mobile-header-bar-height); + background-color: var(--color-gray-800-variant); + flex-shrink: 0; +} + +/* --- Preview Area --- */ +.vqap-preview-container { + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; + background-color: var(--color-gray-100); + box-sizing: border-box; + height: calc(100% - var(--vqap-mobile-header-bar-height)); + width: 100%; + padding: var(--spacing-300); + display: flex; + align-items: center; + justify-content: center; +} + +.vqap-preview-container video { + width: 100%; + height: auto; + max-height: 100%; + max-width: 100%; + display: block; + object-fit: contain; + box-sizing: border-box; +} + +/* --- Loading Spinner --- */ +.vqap-loading { + display: flex; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +} + +.vqap-spinner { + width: 48px; + height: 48px; + border: 4px solid var(--color-light-gray); + border-top-color: var(--color-gray-800-variant); + border-radius: 50%; + animation: vqap-spin 0.8s linear infinite; +} + +@keyframes vqap-spin { + to { transform: rotate(360deg); } +} + +/* --- Error State --- */ +.vqap-error { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: var(--spacing-100); + width: 100%; + height: 100%; + color: var(--color-default-font); + font-size: var(--ax-body-xs-size, 14px); +} + +/* --- Scrollable Body & Content Container --- */ +.vqap-body { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-height: 0; +} + +.vqap-content-container { + padding-inline: var(--spacing-300); + flex: 1 1 auto; + overflow-y: auto; + -webkit-overflow-scrolling: touch; + display: flex; + flex-direction: column; + gap: var(--spacing-400); + padding-block: var(--spacing-50); + padding-bottom: calc(var(--spacing-50) + env(safe-area-inset-bottom, 0px)); +} + +.vqap-content-container > .vqap-action-card:first-child { + margin-block-start: var(--spacing-75); +} + +.vqap-content-container > .vqap-action-card:last-child { + margin-block-end: var(--spacing-300); +} + +/* --- Action Card --- */ +.vqap-action-card { + -webkit-touch-callout: none; + -webkit-user-select: none; + user-select: none; + display: block; + width: 100%; + border: var(--border-width-2) solid var(--color-light-gray); + border-radius: var(--border-radius-10); + padding: var(--spacing-400) var(--spacing-300); + cursor: pointer; + background: var(--body-background-color); + box-sizing: border-box; + touch-action: manipulation; +} + +.vqap-action-card:hover, +.vqap-action-card:active { + border-color: var(--color-gray-250); +} + +/* --- Card Inner (mirrors .action-title) --- */ +.vqap-action-title { + align-items: center; + display: flex; + gap: var(--spacing-200); + padding-block: var(--spacing-75); + font-family: var(--body-font-family); + font-size: var(--ax-heading-xs-size, 16px); + line-height: var(--ax-heading-xs-lh, 20px); +} + +.vqap-action-title span { + align-items: center; + display: flex; + gap: var(--spacing-100); + flex-wrap: wrap-reverse; +} + +.vqap-action-title strong { + font-weight: var(--ax-heading-weight, 700); + color: var(--heading-color, var(--color-default-font)); +} + +.vqap-card-icon { + flex-shrink: 0; +} + +.vqap-card-icon img { + width: 28px; + height: 28px; +} + +.vqap-close-btn img { + width: 18px; + height: 18px; +} + +/* --- Badge --- */ +.vqap-badge { + display: flex; + align-items: center; + box-sizing: border-box; + min-height: 26px; + border: 1px solid transparent; + border-radius: 4px; + background: rgb(229, 240, 254); + flex-shrink: 0; + cursor: pointer; + text-align: center; +} + +.vqap-badge strong { + font-size: var(--ax-body-xs-size); + line-height: 1.3; + color: var(--color-black); + padding-block: var(--spacing-50); + padding-inline: var(--spacing-80); +} diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js new file mode 100644 index 000000000..b99c6fadd --- /dev/null +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -0,0 +1,225 @@ +import { createTag } from '../../scripts/utils.js'; +import { createMetadataMap } from '../../scripts/utils/mobile-fork-button-utils.js'; +import { + ACTION_TYPES, + loadPlaceholders, + getLocalizedStrings, + getVideoActions, +} from './video-quick-action-picker-config.js'; + +const STYLES_URL = '/express/code/blocks/video-quick-action-picker/video-quick-action-picker.css'; + +function loadStyles() { + if (document.querySelector(`link[href="${STYLES_URL}"]`)) return; + const link = document.createElement('link'); + link.rel = 'stylesheet'; + link.href = STYLES_URL; + document.head.append(link); +} + +const ICONS_BASE = '/express/code/icons'; + +function getAppInstallLink() { + const metadataMap = createMetadataMap(); + return metadataMap['fork-cta-1-link'] || metadataMap['fork-cta-1-link-frictionless'] || ''; +} + +function lockBodyScroll() { + document.body.style.overflow = 'hidden'; + document.body.style.position = 'fixed'; + document.body.style.width = '100%'; + document.body.dataset.vqapScrollY = window.scrollY; + document.body.style.top = `-${window.scrollY}px`; +} + +function unlockBodyScroll() { + document.body.style.overflow = ''; + document.body.style.position = ''; + document.body.style.width = ''; + document.body.style.top = ''; + const scrollY = document.body.dataset.vqapScrollY; + if (scrollY) window.scrollTo(0, parseInt(scrollY, 10)); + delete document.body.dataset.vqapScrollY; +} + +function setupDialogKeyboard(dialog, onEscape) { + const focusableSelector = 'button, [role="link"][tabindex="0"], [tabindex="0"]'; + + function handleKeydown(e) { + if (e.key === 'Escape') { onEscape(); return; } + if (e.key === 'Tab') { + const focusable = [...dialog.querySelectorAll(focusableSelector)]; + if (!focusable.length) return; + const first = focusable[0]; + const last = focusable[focusable.length - 1]; + + if (e.shiftKey && document.activeElement === first) { + e.preventDefault(); + last.focus(); + } else if (!e.shiftKey && document.activeElement === last) { + e.preventDefault(); + first.focus(); + } + } + } + + document.addEventListener('keydown', handleKeydown); + return () => document.removeEventListener('keydown', handleKeydown); +} + +function createVideoPreview(file, strings) { + const previewContainer = createTag('div', { class: 'vqap-preview-container' }); + + const loading = createTag('div', { + class: 'vqap-loading', + role: 'status', + 'aria-label': strings.openingPreview, + }); + loading.append(createTag('div', { class: 'vqap-spinner' })); + previewContainer.append(loading); + + const video = createTag('video', { + autoplay: '', + muted: '', + playsinline: '', + 'aria-label': strings.uploadedVideo, + }); + video.style.display = 'none'; + video.src = URL.createObjectURL(file); + + // Resolve duration from the same video element to avoid a separate load + const durationPromise = new Promise((resolve) => { + video.addEventListener('loadedmetadata', () => resolve(video.duration), { once: true }); + video.addEventListener('error', () => resolve(0), { once: true }); + }); + + video.addEventListener('loadeddata', () => { + loading.remove(); + video.style.display = 'block'; + }, { once: true }); + + video.addEventListener('error', () => { + loading.remove(); + video.remove(); + const errorEl = createTag('div', { class: 'vqap-error' }); + errorEl.textContent = strings.previewUnavailable; + previewContainer.append(errorEl); + }, { once: true }); + + previewContainer.append(video); + return { previewContainer, video, durationPromise }; +} + +function buildActionCard(action) { + const card = createTag('button', { + class: 'vqap-action-card', + 'aria-label': action.label, + }); + + const actionTitle = createTag('div', { class: 'vqap-action-title' }); + + const iconWrapper = createTag('span', { class: 'vqap-card-icon' }); + if (action.icon) { + const icon = createTag('img', { + src: `${ICONS_BASE}/${action.icon}.svg`, + alt: '', + 'aria-hidden': 'true', + width: '28', + height: '28', + }); + iconWrapper.append(icon); + } + + const textSpan = createTag('span'); + const title = createTag('strong', { 'aria-hidden': 'true' }); + title.textContent = action.label; + textSpan.append(title); + + if (action.badge) { + const badge = createTag('span', { class: 'vqap-badge' }); + const badgeText = createTag('strong'); + badgeText.textContent = action.badge; + badge.append(badgeText); + textSpan.append(badge); + } + + actionTitle.append(iconWrapper, textSpan); + card.append(actionTitle); + return card; +} + +/** + * @param {File} videoFile - the selected video blob + * @param {HTMLElement} block - the frictionless-quick-action-mobile block + * @param {Object} sdkHandlers - { startSDKWithUnconvertedFiles } + */ +export async function showVideoQuickActionPicker(videoFile, block, sdkHandlers) { + loadStyles(); + await loadPlaceholders(); + const strings = await getLocalizedStrings(); + const { startSDKWithUnconvertedFiles } = sdkHandlers; + + const dialog = createTag('div', { + class: 'vqap-dialog', + role: 'dialog', + 'aria-modal': 'true', + 'aria-label': strings.startFromYourVideo, + }); + + const hero = createTag('div', { class: 'vqap-hero' }); + const headerBar = createTag('div', { class: 'vqap-header-bar' }); + + const closeBtn = createTag('button', { + class: 'vqap-close-btn', + 'aria-label': strings.closeDialog, + }); + const closeIcon = createTag('img', { + src: `${ICONS_BASE}/close-white.svg`, + alt: '', + 'aria-hidden': 'true', + width: '24', + height: '24', + }); + closeBtn.append(closeIcon); + + const { previewContainer, video, durationPromise } = createVideoPreview(videoFile, strings); + hero.append(headerBar, previewContainer); + + const body = createTag('div', { class: 'vqap-body' }); + const contentContainer = createTag('div', { class: 'vqap-content-container' }); + + function closeDialog() { + removeFocusTrap(); + unlockBodyScroll(); + if (video.src) URL.revokeObjectURL(video.src); + dialog.remove(); + } + + const videoDuration = await durationPromise; + const videoActions = getVideoActions(strings, videoFile, videoDuration); + videoActions.forEach((action) => { + const card = buildActionCard(action); + card.addEventListener('click', () => { + closeDialog(); + if (action.type === ACTION_TYPES.APP_INSTALL) { + const appLink = getAppInstallLink(); + if (appLink) window.location.href = appLink; + } else { + startSDKWithUnconvertedFiles([videoFile], action.id, block); + } + }); + contentContainer.append(card); + }); + + body.append(contentContainer); + + closeBtn.addEventListener('click', closeDialog); + + dialog.append(closeBtn, hero, body); + document.body.append(dialog); + + lockBodyScroll(); + const removeFocusTrap = setupDialogKeyboard(dialog, closeDialog); + + closeBtn.focus(); +} diff --git a/express/code/icons/edit-video.svg b/express/code/icons/edit-video.svg new file mode 100644 index 000000000..e6e171d8a --- /dev/null +++ b/express/code/icons/edit-video.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + From de97ff0421a972e0ae07873f81f7aebef79c7aaa Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Fri, 27 Mar 2026 17:03:59 +0530 Subject: [PATCH 02/13] minor fixes --- .../video-quick-action-picker/video-quick-action-picker.css | 3 --- .../video-quick-action-picker/video-quick-action-picker.js | 4 ++-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.css b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.css index 02c0a845b..284201025 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.css +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.css @@ -196,14 +196,11 @@ .vqap-badge { display: flex; align-items: center; - box-sizing: border-box; min-height: 26px; border: 1px solid transparent; border-radius: 4px; background: rgb(229, 240, 254); flex-shrink: 0; - cursor: pointer; - text-align: center; } .vqap-badge strong { diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js index b99c6fadd..d34085aa4 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -177,8 +177,8 @@ export async function showVideoQuickActionPicker(videoFile, block, sdkHandlers) src: `${ICONS_BASE}/close-white.svg`, alt: '', 'aria-hidden': 'true', - width: '24', - height: '24', + width: '18', + height: '18', }); closeBtn.append(closeIcon); From 9bcc0ced1547e6c641ada66579cfd976f1678933 Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Wed, 1 Apr 2026 11:57:03 +0530 Subject: [PATCH 03/13] lint fixes --- .../video-quick-action-picker-config.js | 6 ++-- .../video-quick-action-picker.js | 34 +++---------------- 2 files changed, 7 insertions(+), 33 deletions(-) diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js index c30e87f26..ca809c3e6 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js @@ -3,13 +3,13 @@ import { QA_CONFIGS } from '../../scripts/utils/frictionless-utils.js'; export const ACTION_TYPES = { QUICK_ACTION: 'quick-action', // launches SDK inline with the uploaded file - APP_INSTALL: 'app-install', // navigates to app install URL (same tab) + APP_INSTALL: 'app-install', // navigates to app install URL (same tab) }; // Max duration in seconds per quick action (source: Adobe Express help docs) const MAX_DURATION = { - 'convert-to-gif': 60, // 1 minute - 'caption-video': 300, // 5 minutes + 'convert-to-gif': 60, // 1 minute + 'caption-video': 300, // 5 minutes }; let replaceKey; diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js index d34085aa4..4a7c05677 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -42,31 +42,6 @@ function unlockBodyScroll() { delete document.body.dataset.vqapScrollY; } -function setupDialogKeyboard(dialog, onEscape) { - const focusableSelector = 'button, [role="link"][tabindex="0"], [tabindex="0"]'; - - function handleKeydown(e) { - if (e.key === 'Escape') { onEscape(); return; } - if (e.key === 'Tab') { - const focusable = [...dialog.querySelectorAll(focusableSelector)]; - if (!focusable.length) return; - const first = focusable[0]; - const last = focusable[focusable.length - 1]; - - if (e.shiftKey && document.activeElement === first) { - e.preventDefault(); - last.focus(); - } else if (!e.shiftKey && document.activeElement === last) { - e.preventDefault(); - first.focus(); - } - } - } - - document.addEventListener('keydown', handleKeydown); - return () => document.removeEventListener('keydown', handleKeydown); -} - function createVideoPreview(file, strings) { const previewContainer = createTag('div', { class: 'vqap-preview-container' }); @@ -153,7 +128,7 @@ function buildActionCard(action) { * @param {HTMLElement} block - the frictionless-quick-action-mobile block * @param {Object} sdkHandlers - { startSDKWithUnconvertedFiles } */ -export async function showVideoQuickActionPicker(videoFile, block, sdkHandlers) { +export default async function showVideoQuickActionPicker(videoFile, block, sdkHandlers) { loadStyles(); await loadPlaceholders(); const strings = await getLocalizedStrings(); @@ -189,7 +164,6 @@ export async function showVideoQuickActionPicker(videoFile, block, sdkHandlers) const contentContainer = createTag('div', { class: 'vqap-content-container' }); function closeDialog() { - removeFocusTrap(); unlockBodyScroll(); if (video.src) URL.revokeObjectURL(video.src); dialog.remove(); @@ -219,7 +193,7 @@ export async function showVideoQuickActionPicker(videoFile, block, sdkHandlers) document.body.append(dialog); lockBodyScroll(); - const removeFocusTrap = setupDialogKeyboard(dialog, closeDialog); - - closeBtn.focus(); + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape') closeDialog(); + }, { once: true }); } From 2bf74698f07213b383abec52231f53f0d1c8f079 Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Wed, 8 Apr 2026 11:40:28 +0530 Subject: [PATCH 04/13] updating vqa icons and integrated for iOS pages video editor and create video pages --- .../frictionless-quick-action-mobile.js | 13 +++++++++++-- .../video-quick-action-picker-config.js | 16 +++++++++------- .../video-quick-action-picker.js | 8 ++++---- express/code/icons/convert-to-mp4.svg | 10 ++++++++++ express/code/icons/edit-video.svg | 15 ++++----------- express/code/icons/vqa-crop-video.svg | 6 ++++++ express/code/icons/vqa-resize-video.svg | 10 ++++++++++ express/code/icons/vqa-trim-video.svg | 6 ++++++ express/code/scripts/utils/frictionless-utils.js | 8 +++++++- 9 files changed, 67 insertions(+), 25 deletions(-) create mode 100644 express/code/icons/convert-to-mp4.svg create mode 100644 express/code/icons/vqa-crop-video.svg create mode 100644 express/code/icons/vqa-resize-video.svg create mode 100644 express/code/icons/vqa-trim-video.svg diff --git a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js index e9e7d4be7..a6c64e909 100644 --- a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js +++ b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js @@ -19,6 +19,7 @@ import { processFilesForQuickAction, loadAndInitializeCCEverywhere, getErrorMsg, + shouldShowVideoQuickActionPickerForMobile, } from '../../scripts/utils/frictionless-utils.js'; let replaceKey; let getConfig; @@ -283,8 +284,16 @@ export default async function decorate(block) { accept: QA_CONFIGS[quickAction].accept, ...(quickAction === 'merge-videos' && { multiple: true }), }); - inputElement.onchange = () => { - if (quickAction === 'merge-videos' && inputElement.files.length > 1) { + inputElement.onchange = async () => { + const file = inputElement.files[0]; + if (shouldShowVideoQuickActionPickerForMobile(quickAction, file)) { + if (!file) return; + const { default: showVideoQuickActionPicker } = await import( + '../video-quick-action-picker/video-quick-action-picker.js' + ); + showVideoQuickActionPicker(file, block, { startSDKWithUnconvertedFiles }); + inputElement.value = ''; + } else if (quickAction === 'merge-videos' && inputElement.files.length > 1) { startSDKWithUnconvertedFiles(inputElement.files, quickAction, block); } else { const file = inputElement.files[0]; diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js index ca809c3e6..17a3fe1ab 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js @@ -1,6 +1,8 @@ import { getLibs } from '../../scripts/utils.js'; import { QA_CONFIGS } from '../../scripts/utils/frictionless-utils.js'; +const ICONS_BASE = '/express/code/icons'; + export const ACTION_TYPES = { QUICK_ACTION: 'quick-action', // launches SDK inline with the uploaded file APP_INSTALL: 'app-install', // navigates to app install URL (same tab) @@ -55,43 +57,43 @@ export function getVideoActions(strings, videoFile, videoDuration) { label: strings.editVideo, badge: strings.appOnly, type: ACTION_TYPES.APP_INSTALL, - icon: 'edit-video', + iconPath: `${ICONS_BASE}/edit-video.svg`, }, { id: 'convert-to-gif', label: strings.convertVideoToGif, type: ACTION_TYPES.QUICK_ACTION, - icon: 'ax-convert-to-gif-22', + iconPath: `${ICONS_BASE}/ax-convert-to-gif-22.svg`, }, { id: 'crop-video', label: strings.cropVideo, type: ACTION_TYPES.QUICK_ACTION, - icon: 'ax-crop-image-22', // TODO: replace with crop-video-22 icon when available + iconPath: `${ICONS_BASE}/vqa-crop-video.svg`, }, { id: 'trim-video', label: strings.trimVideo, type: ACTION_TYPES.QUICK_ACTION, - icon: 'trim-video-22', + iconPath: `${ICONS_BASE}/vqa-trim-video.svg`, }, { id: 'resize-video', label: strings.resizeVideo, type: ACTION_TYPES.QUICK_ACTION, - icon: 'ax-resize-video-22', + iconPath: `${ICONS_BASE}/vqa-resize-video.svg`, }, { id: 'convert-to-mp4', label: strings.convertVideoToMp4, type: ACTION_TYPES.QUICK_ACTION, - icon: 'ax-convert-22', // TODO: replace with convert-video-to-mp4-22 icon when available + iconPath: `${ICONS_BASE}/convert-to-mp4.svg`, }, { id: 'caption-video', label: strings.captionVideo, type: ACTION_TYPES.QUICK_ACTION, - icon: 'ax-caption-video-22', + iconPath: `${ICONS_BASE}/ax-caption-video-22.svg`, }, ]; diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js index 4a7c05677..42c643e20 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -17,7 +17,7 @@ function loadStyles() { document.head.append(link); } -const ICONS_BASE = '/express/code/icons'; +const CLOSE_ICON_PATH = '/express/code/icons/close-white.svg'; function getAppInstallLink() { const metadataMap = createMetadataMap(); @@ -94,9 +94,9 @@ function buildActionCard(action) { const actionTitle = createTag('div', { class: 'vqap-action-title' }); const iconWrapper = createTag('span', { class: 'vqap-card-icon' }); - if (action.icon) { + if (action.iconPath) { const icon = createTag('img', { - src: `${ICONS_BASE}/${action.icon}.svg`, + src: action.iconPath, alt: '', 'aria-hidden': 'true', width: '28', @@ -149,7 +149,7 @@ export default async function showVideoQuickActionPicker(videoFile, block, sdkHa 'aria-label': strings.closeDialog, }); const closeIcon = createTag('img', { - src: `${ICONS_BASE}/close-white.svg`, + src: CLOSE_ICON_PATH, alt: '', 'aria-hidden': 'true', width: '18', diff --git a/express/code/icons/convert-to-mp4.svg b/express/code/icons/convert-to-mp4.svg new file mode 100644 index 000000000..e998247a7 --- /dev/null +++ b/express/code/icons/convert-to-mp4.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/express/code/icons/edit-video.svg b/express/code/icons/edit-video.svg index e6e171d8a..c58847022 100644 --- a/express/code/icons/edit-video.svg +++ b/express/code/icons/edit-video.svg @@ -1,12 +1,5 @@ - - - - - - - - - - - + + + + diff --git a/express/code/icons/vqa-crop-video.svg b/express/code/icons/vqa-crop-video.svg new file mode 100644 index 000000000..ff4e89b41 --- /dev/null +++ b/express/code/icons/vqa-crop-video.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/express/code/icons/vqa-resize-video.svg b/express/code/icons/vqa-resize-video.svg new file mode 100644 index 000000000..458ab3bdc --- /dev/null +++ b/express/code/icons/vqa-resize-video.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/express/code/icons/vqa-trim-video.svg b/express/code/icons/vqa-trim-video.svg new file mode 100644 index 000000000..036b4eef4 --- /dev/null +++ b/express/code/icons/vqa-trim-video.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/express/code/scripts/utils/frictionless-utils.js b/express/code/scripts/utils/frictionless-utils.js index 3168260e7..d30847b4d 100644 --- a/express/code/scripts/utils/frictionless-utils.js +++ b/express/code/scripts/utils/frictionless-utils.js @@ -1,4 +1,4 @@ -import { getLibs } from '../utils.js'; +import { getLibs, getMobileOperatingSystem } from '../utils.js'; // Shared constants and configurations for frictionless quick actions const JPG = 'jpg'; @@ -157,6 +157,12 @@ export const AUTH_FRICTIONLESS_UPLOAD_QUICK_ACTIONS = { removeBackground: 'remove-background', }; +export function shouldShowVideoQuickActionPickerForMobile(quickAction, file) { + return quickAction === FRICTIONLESS_UPLOAD_QUICK_ACTIONS.videoEditor + && getMobileOperatingSystem() === 'iOS' + && file?.type?.startsWith('video/'); +} + // Route paths map corresponding to the express routes export const EXPRESS_ROUTE_PATHS = { loggedOutEditor: '/new', From 1191901cbda815b440ce68fcc80ce7977774caf0 Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Wed, 8 Apr 2026 12:08:54 +0530 Subject: [PATCH 05/13] fix linter --- .../frictionless-quick-action-mobile.js | 1 - .../video-quick-action-picker.js | 12 ++++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js index a6c64e909..f17440eff 100644 --- a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js +++ b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js @@ -296,7 +296,6 @@ export default async function decorate(block) { } else if (quickAction === 'merge-videos' && inputElement.files.length > 1) { startSDKWithUnconvertedFiles(inputElement.files, quickAction, block); } else { - const file = inputElement.files[0]; startSDKWithUnconvertedFiles([file], quickAction, block); } }; diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js index 42c643e20..6c6ee29ed 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -60,7 +60,8 @@ function createVideoPreview(file, strings) { 'aria-label': strings.uploadedVideo, }); video.style.display = 'none'; - video.src = URL.createObjectURL(file); + const blobUrl = URL.createObjectURL(file); + video.setAttribute('src', blobUrl); // Resolve duration from the same video element to avoid a separate load const durationPromise = new Promise((resolve) => { @@ -163,7 +164,12 @@ export default async function showVideoQuickActionPicker(videoFile, block, sdkHa const body = createTag('div', { class: 'vqap-body' }); const contentContainer = createTag('div', { class: 'vqap-content-container' }); + function handleKeydown(e) { + if (e.key === 'Escape') closeDialog(); + } + function closeDialog() { + document.removeEventListener('keydown', handleKeydown); unlockBodyScroll(); if (video.src) URL.revokeObjectURL(video.src); dialog.remove(); @@ -193,7 +199,5 @@ export default async function showVideoQuickActionPicker(videoFile, block, sdkHa document.body.append(dialog); lockBodyScroll(); - document.addEventListener('keydown', (e) => { - if (e.key === 'Escape') closeDialog(); - }, { once: true }); + document.addEventListener('keydown', handleKeydown); } From 9b2086fdb4caef38eee689b54fa1a09998c10cdb Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Wed, 8 Apr 2026 16:27:39 +0530 Subject: [PATCH 06/13] fixed linter --- .../frictionless-quick-action-mobile.js | 10 ++++++++-- .../video-quick-action-picker.js | 18 +++++++----------- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js index f17440eff..7ace20a42 100644 --- a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js +++ b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js @@ -288,11 +288,17 @@ export default async function decorate(block) { const file = inputElement.files[0]; if (shouldShowVideoQuickActionPickerForMobile(quickAction, file)) { if (!file) return; + const blobUrl = URL.createObjectURL(file); + inputElement.value = ''; + if (!blobUrl.startsWith('blob:')) { + URL.revokeObjectURL(blobUrl); + showErrorToast(block, await getErrorMsg('file-type-not-supported')); + return; + } const { default: showVideoQuickActionPicker } = await import( '../video-quick-action-picker/video-quick-action-picker.js' ); - showVideoQuickActionPicker(file, block, { startSDKWithUnconvertedFiles }); - inputElement.value = ''; + showVideoQuickActionPicker(file, block, { startSDKWithUnconvertedFiles, blobUrl }); } else if (quickAction === 'merge-videos' && inputElement.files.length > 1) { startSDKWithUnconvertedFiles(inputElement.files, quickAction, block); } else { diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js index 6c6ee29ed..266e2d69e 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -42,7 +42,7 @@ function unlockBodyScroll() { delete document.body.dataset.vqapScrollY; } -function createVideoPreview(file, strings) { +function createVideoPreview(blobUrl, strings) { const previewContainer = createTag('div', { class: 'vqap-preview-container' }); const loading = createTag('div', { @@ -60,8 +60,7 @@ function createVideoPreview(file, strings) { 'aria-label': strings.uploadedVideo, }); video.style.display = 'none'; - const blobUrl = URL.createObjectURL(file); - video.setAttribute('src', blobUrl); + video.src = blobUrl; // Resolve duration from the same video element to avoid a separate load const durationPromise = new Promise((resolve) => { @@ -77,13 +76,10 @@ function createVideoPreview(file, strings) { video.addEventListener('error', () => { loading.remove(); video.remove(); - const errorEl = createTag('div', { class: 'vqap-error' }); - errorEl.textContent = strings.previewUnavailable; - previewContainer.append(errorEl); }, { once: true }); previewContainer.append(video); - return { previewContainer, video, durationPromise }; + return { previewContainer, durationPromise }; } function buildActionCard(action) { @@ -127,13 +123,13 @@ function buildActionCard(action) { /** * @param {File} videoFile - the selected video blob * @param {HTMLElement} block - the frictionless-quick-action-mobile block - * @param {Object} sdkHandlers - { startSDKWithUnconvertedFiles } + * @param {Object} sdkHandlers - { startSDKWithUnconvertedFiles, blobUrl } */ export default async function showVideoQuickActionPicker(videoFile, block, sdkHandlers) { loadStyles(); await loadPlaceholders(); const strings = await getLocalizedStrings(); - const { startSDKWithUnconvertedFiles } = sdkHandlers; + const { startSDKWithUnconvertedFiles, blobUrl } = sdkHandlers; const dialog = createTag('div', { class: 'vqap-dialog', @@ -158,7 +154,7 @@ export default async function showVideoQuickActionPicker(videoFile, block, sdkHa }); closeBtn.append(closeIcon); - const { previewContainer, video, durationPromise } = createVideoPreview(videoFile, strings); + const { previewContainer, durationPromise } = createVideoPreview(blobUrl, strings); hero.append(headerBar, previewContainer); const body = createTag('div', { class: 'vqap-body' }); @@ -171,7 +167,7 @@ export default async function showVideoQuickActionPicker(videoFile, block, sdkHa function closeDialog() { document.removeEventListener('keydown', handleKeydown); unlockBodyScroll(); - if (video.src) URL.revokeObjectURL(video.src); + URL.revokeObjectURL(blobUrl); dialog.remove(); } From b4385527dacd74e36ea3b1f727fe4b771682ac07 Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Wed, 8 Apr 2026 16:39:44 +0530 Subject: [PATCH 07/13] fixed linter 2 --- .../frictionless-quick-action-mobile.js | 5 ----- .../video-quick-action-picker/video-quick-action-picker.js | 4 ++++ 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js index 7ace20a42..34ffca9c5 100644 --- a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js +++ b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js @@ -290,11 +290,6 @@ export default async function decorate(block) { if (!file) return; const blobUrl = URL.createObjectURL(file); inputElement.value = ''; - if (!blobUrl.startsWith('blob:')) { - URL.revokeObjectURL(blobUrl); - showErrorToast(block, await getErrorMsg('file-type-not-supported')); - return; - } const { default: showVideoQuickActionPicker } = await import( '../video-quick-action-picker/video-quick-action-picker.js' ); diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js index 266e2d69e..462727a2b 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -60,6 +60,7 @@ function createVideoPreview(blobUrl, strings) { 'aria-label': strings.uploadedVideo, }); video.style.display = 'none'; + if (typeof blobUrl !== 'string' || !blobUrl.startsWith('blob:')) return { previewContainer, durationPromise: Promise.resolve(0) }; video.src = blobUrl; // Resolve duration from the same video element to avoid a separate load @@ -171,6 +172,9 @@ export default async function showVideoQuickActionPicker(videoFile, block, sdkHa dialog.remove(); } + function handleKeydown(e) { + if (e.key === 'Escape') closeDialog(); + } const videoDuration = await durationPromise; const videoActions = getVideoActions(strings, videoFile, videoDuration); videoActions.forEach((action) => { From c09f40f65cc6e8bdcb4efaa9d3d31ffe15088d0c Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Wed, 8 Apr 2026 17:04:25 +0530 Subject: [PATCH 08/13] fix linter 3 --- .../frictionless-quick-action-mobile.js | 5 +++++ .../video-quick-action-picker/video-quick-action-picker.js | 7 +------ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js index 34ffca9c5..7419d3cbd 100644 --- a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js +++ b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js @@ -290,6 +290,11 @@ export default async function decorate(block) { if (!file) return; const blobUrl = URL.createObjectURL(file); inputElement.value = ''; + if (typeof blobUrl !== 'string' || !blobUrl.startsWith('blob:')) { + URL.revokeObjectURL(blobUrl); + showErrorToast(block, await getErrorMsg('file-type-not-supported')); + return; + } const { default: showVideoQuickActionPicker } = await import( '../video-quick-action-picker/video-quick-action-picker.js' ); diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js index 462727a2b..fa51026b4 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -57,11 +57,10 @@ function createVideoPreview(blobUrl, strings) { autoplay: '', muted: '', playsinline: '', + src: safeSrc, 'aria-label': strings.uploadedVideo, }); video.style.display = 'none'; - if (typeof blobUrl !== 'string' || !blobUrl.startsWith('blob:')) return { previewContainer, durationPromise: Promise.resolve(0) }; - video.src = blobUrl; // Resolve duration from the same video element to avoid a separate load const durationPromise = new Promise((resolve) => { @@ -161,10 +160,6 @@ export default async function showVideoQuickActionPicker(videoFile, block, sdkHa const body = createTag('div', { class: 'vqap-body' }); const contentContainer = createTag('div', { class: 'vqap-content-container' }); - function handleKeydown(e) { - if (e.key === 'Escape') closeDialog(); - } - function closeDialog() { document.removeEventListener('keydown', handleKeydown); unlockBodyScroll(); From a3284da4100e8d5ee946ccf9d516127e3b940f20 Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Wed, 8 Apr 2026 17:19:06 +0530 Subject: [PATCH 09/13] fixed linter 5 --- .../video-quick-action-picker/video-quick-action-picker.js | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js index fa51026b4..bbb31c060 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -57,7 +57,7 @@ function createVideoPreview(blobUrl, strings) { autoplay: '', muted: '', playsinline: '', - src: safeSrc, + src: blobUrl, 'aria-label': strings.uploadedVideo, }); video.style.display = 'none'; @@ -161,15 +161,11 @@ export default async function showVideoQuickActionPicker(videoFile, block, sdkHa const contentContainer = createTag('div', { class: 'vqap-content-container' }); function closeDialog() { - document.removeEventListener('keydown', handleKeydown); unlockBodyScroll(); URL.revokeObjectURL(blobUrl); dialog.remove(); } - function handleKeydown(e) { - if (e.key === 'Escape') closeDialog(); - } const videoDuration = await durationPromise; const videoActions = getVideoActions(strings, videoFile, videoDuration); videoActions.forEach((action) => { @@ -194,5 +190,4 @@ export default async function showVideoQuickActionPicker(videoFile, block, sdkHa document.body.append(dialog); lockBodyScroll(); - document.addEventListener('keydown', handleKeydown); } From d3ea5e5a369fdbb39e0af0f233d81f1548549e9b Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Wed, 8 Apr 2026 18:34:19 +0530 Subject: [PATCH 10/13] removed the unused string --- .../video-quick-action-picker-config.js | 1 - 1 file changed, 1 deletion(-) diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js index 17a3fe1ab..80d4f10e1 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker-config.js @@ -44,7 +44,6 @@ export async function getLocalizedStrings() { captionVideo: await resolveKey('caption-video', 'Caption video'), openingPreview: await resolveKey('opening-preview', 'Opening preview'), uploadedVideo: await resolveKey('uploaded-video', 'Uploaded video'), - previewUnavailable: await resolveKey('preview-unavailable', 'Preview unavailable'), startFromYourVideo: await resolveKey('start-from-your-video', 'Start from your video'), closeDialog: await resolveKey('close-dialog', 'Close dialog'), }; From 2cca9e96be6a1f255712ec49b61fe29f5bd65e3f Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Wed, 8 Apr 2026 23:17:10 +0530 Subject: [PATCH 11/13] review comments --- .../frictionless-quick-action-mobile.js | 16 +- .../video-quick-action-picker.js | 138 +++++++++++------- 2 files changed, 89 insertions(+), 65 deletions(-) diff --git a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js index 7419d3cbd..e62e648f6 100644 --- a/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js +++ b/express/code/blocks/frictionless-quick-action-mobile/frictionless-quick-action-mobile.js @@ -288,17 +288,15 @@ export default async function decorate(block) { const file = inputElement.files[0]; if (shouldShowVideoQuickActionPickerForMobile(quickAction, file)) { if (!file) return; - const blobUrl = URL.createObjectURL(file); inputElement.value = ''; - if (typeof blobUrl !== 'string' || !blobUrl.startsWith('blob:')) { - URL.revokeObjectURL(blobUrl); - showErrorToast(block, await getErrorMsg('file-type-not-supported')); - return; + try { + const { default: showVideoQuickActionPicker } = await import( + '../video-quick-action-picker/video-quick-action-picker.js' + ); + await showVideoQuickActionPicker(file, block, { startSDKWithUnconvertedFiles }); + } catch (e) { + showErrorToast(block, await replaceKey('upload-media-error', getConfig())); } - const { default: showVideoQuickActionPicker } = await import( - '../video-quick-action-picker/video-quick-action-picker.js' - ); - showVideoQuickActionPicker(file, block, { startSDKWithUnconvertedFiles, blobUrl }); } else if (quickAction === 'merge-videos' && inputElement.files.length > 1) { startSDKWithUnconvertedFiles(inputElement.files, quickAction, block); } else { diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js index bbb31c060..9e7d98398 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -54,12 +54,12 @@ function createVideoPreview(blobUrl, strings) { previewContainer.append(loading); const video = createTag('video', { - autoplay: '', - muted: '', + autoplay: true, + muted: true, playsinline: '', - src: blobUrl, 'aria-label': strings.uploadedVideo, }); + video.src = blobUrl; video.style.display = 'none'; // Resolve duration from the same video element to avoid a separate load @@ -123,71 +123,97 @@ function buildActionCard(action) { /** * @param {File} videoFile - the selected video blob * @param {HTMLElement} block - the frictionless-quick-action-mobile block - * @param {Object} sdkHandlers - { startSDKWithUnconvertedFiles, blobUrl } + * @param {Object} sdkHandlers - { startSDKWithUnconvertedFiles } */ export default async function showVideoQuickActionPicker(videoFile, block, sdkHandlers) { loadStyles(); await loadPlaceholders(); const strings = await getLocalizedStrings(); - const { startSDKWithUnconvertedFiles, blobUrl } = sdkHandlers; + const { startSDKWithUnconvertedFiles } = sdkHandlers; + const blobUrl = URL.createObjectURL(videoFile); + let dialog; + let handleKeydown; + + try { + const parsedBlobUrl = new URL(blobUrl, window.location.href); + if (parsedBlobUrl.protocol !== 'blob:') { + throw new Error('Invalid video preview URL'); + } + + dialog = createTag('div', { + class: 'vqap-dialog', + role: 'dialog', + 'aria-modal': 'true', + 'aria-label': strings.startFromYourVideo, + }); - const dialog = createTag('div', { - class: 'vqap-dialog', - role: 'dialog', - 'aria-modal': 'true', - 'aria-label': strings.startFromYourVideo, - }); + const hero = createTag('div', { class: 'vqap-hero' }); + const headerBar = createTag('div', { class: 'vqap-header-bar' }); - const hero = createTag('div', { class: 'vqap-hero' }); - const headerBar = createTag('div', { class: 'vqap-header-bar' }); + const closeBtn = createTag('button', { + class: 'vqap-close-btn', + 'aria-label': strings.closeDialog, + }); + const closeIcon = createTag('img', { + src: CLOSE_ICON_PATH, + alt: '', + 'aria-hidden': 'true', + width: '18', + height: '18', + }); + closeBtn.append(closeIcon); - const closeBtn = createTag('button', { - class: 'vqap-close-btn', - 'aria-label': strings.closeDialog, - }); - const closeIcon = createTag('img', { - src: CLOSE_ICON_PATH, - alt: '', - 'aria-hidden': 'true', - width: '18', - height: '18', - }); - closeBtn.append(closeIcon); + // The preview URL is browser-generated from the selected file and restricted to blob:. + const { previewContainer, durationPromise } = createVideoPreview(parsedBlobUrl.href, strings); + hero.append(headerBar, previewContainer); - const { previewContainer, durationPromise } = createVideoPreview(blobUrl, strings); - hero.append(headerBar, previewContainer); + const body = createTag('div', { class: 'vqap-body' }); + const contentContainer = createTag('div', { class: 'vqap-content-container' }); - const body = createTag('div', { class: 'vqap-body' }); - const contentContainer = createTag('div', { class: 'vqap-content-container' }); + function closeDialog() { + document.removeEventListener('keydown', handleKeydown); + unlockBodyScroll(); + URL.revokeObjectURL(blobUrl); + dialog.remove(); + } - function closeDialog() { - unlockBodyScroll(); - URL.revokeObjectURL(blobUrl); - dialog.remove(); - } - - const videoDuration = await durationPromise; - const videoActions = getVideoActions(strings, videoFile, videoDuration); - videoActions.forEach((action) => { - const card = buildActionCard(action); - card.addEventListener('click', () => { - closeDialog(); - if (action.type === ACTION_TYPES.APP_INSTALL) { - const appLink = getAppInstallLink(); - if (appLink) window.location.href = appLink; - } else { - startSDKWithUnconvertedFiles([videoFile], action.id, block); + handleKeydown = (e) => { + if (e.key === 'Escape') { + closeDialog(); } + }; + + closeBtn.addEventListener('click', closeDialog); + document.addEventListener('keydown', handleKeydown); + + dialog.append(closeBtn, hero, body); + document.body.append(dialog); + lockBodyScroll(); + closeBtn.focus(); + + const videoDuration = await durationPromise; + const videoActions = getVideoActions(strings, videoFile, videoDuration); + videoActions.forEach((action) => { + const card = buildActionCard(action); + card.addEventListener('click', () => { + closeDialog(); + if (action.type === ACTION_TYPES.APP_INSTALL) { + const appLink = getAppInstallLink(); + if (appLink) window.location.href = appLink; + } else { + startSDKWithUnconvertedFiles([videoFile], action.id, block); + } + }); + contentContainer.append(card); }); - contentContainer.append(card); - }); - - body.append(contentContainer); - closeBtn.addEventListener('click', closeDialog); - - dialog.append(closeBtn, hero, body); - document.body.append(dialog); - - lockBodyScroll(); + body.append(contentContainer); + } catch (error) { + document.removeEventListener('keydown', handleKeydown); + if (dialog?.isConnected) { + dialog.remove(); + } + URL.revokeObjectURL(blobUrl); + throw error; + } } From d2ea3fec4ecf9c1dc89b3b9373363185257fef82 Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Wed, 8 Apr 2026 23:22:24 +0530 Subject: [PATCH 12/13] fixed lints --- .../video-quick-action-picker/video-quick-action-picker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js index 9e7d98398..88a84ed2b 100644 --- a/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js +++ b/express/code/blocks/video-quick-action-picker/video-quick-action-picker.js @@ -170,12 +170,12 @@ export default async function showVideoQuickActionPicker(videoFile, block, sdkHa const body = createTag('div', { class: 'vqap-body' }); const contentContainer = createTag('div', { class: 'vqap-content-container' }); - function closeDialog() { + const closeDialog = () => { document.removeEventListener('keydown', handleKeydown); unlockBodyScroll(); URL.revokeObjectURL(blobUrl); dialog.remove(); - } + }; handleKeydown = (e) => { if (e.key === 'Escape') { From 65b3bb8f534e61165f72e45f2ac2f1bbc130f19a Mon Sep 17 00:00:00 2001 From: Shairil Kansal Date: Thu, 9 Apr 2026 22:03:03 +0530 Subject: [PATCH 13/13] added new values to showwith section metadata to separate iOS and android devices the frictionless behvaiour --- express/code/scripts/utils.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/express/code/scripts/utils.js b/express/code/scripts/utils.js index f70274048..83af6c9a4 100644 --- a/express/code/scripts/utils.js +++ b/express/code/scripts/utils.js @@ -420,12 +420,24 @@ export function hideQuickActionsOnDevices(userAgent) { } // latest setup that supports safari frictionless, enabled by metadata // fqa-non-qualified: always removed. (before: safari) - // fqa-qualified-mobile: mobile only. (before: non-safari mobile) + // fqa-qualified-ios: iOS mobile only. + // fqa-qualified-android: Android mobile only. + // fqa-qualified-mobile: mobile only. (before: non-safari mobile) — kept for backwards compat // fqa-qualified-desktop: desktop only. (before: non-safari desktop) const audienceFqaMeta = document.createElement('meta'); audienceFqaMeta.setAttribute('content', 'on'); if (getMetadata('frictionless-safari')?.toLowerCase() === 'on' || isQualifiedBrowser) { - audienceFqaMeta.setAttribute('name', `fqa-qualified-${isMobile ? 'mobile' : 'desktop'}`); + if (isMobile) { + const os = getMobileOperatingSystem(); + audienceFqaMeta.setAttribute('name', os === 'Android' ? 'fqa-qualified-android' : 'fqa-qualified-ios'); + // also inject fqa-qualified-mobile so sections targeting both iOS + Android still show + const mobileFqaMeta = document.createElement('meta'); + mobileFqaMeta.setAttribute('name', 'fqa-qualified-mobile'); + mobileFqaMeta.setAttribute('content', 'on'); + document.head.append(mobileFqaMeta); + } else { + audienceFqaMeta.setAttribute('name', 'fqa-qualified-desktop'); + } } else { audienceFqaMeta.setAttribute('name', 'fqa-non-qualified'); } @@ -453,7 +465,7 @@ export function preDecorateSections(area) { || urlParams.get(`${sectionMeta.showwith}`); } const showwith = sectionMeta.showwith.toLowerCase(); - if (['fqa-off', 'fqa-on', 'fqa-non-qualified', 'fqa-qualified-mobile', 'fqa-qualified-desktop'].includes(showwith)) hideQuickActionsOnDevices(navigator.userAgent); + if (['fqa-off', 'fqa-on', 'fqa-non-qualified', 'fqa-qualified-mobile', 'fqa-qualified-desktop', 'fqa-qualified-ios', 'fqa-qualified-android'].includes(showwith)) hideQuickActionsOnDevices(navigator.userAgent); sectionRemove = showWithSearchParam !== null ? showWithSearchParam !== 'on' : getMetadata(showwith) !== 'on'; } if (sectionRemove) section.remove();