From bede92404bef4557bb4c2f11e63072e4a6eed84f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Horv=C3=A1th=20B=C3=A1lint?= <40771359+horvbalint@users.noreply.github.com> Date: Sat, 14 Jan 2023 21:58:52 +0100 Subject: [PATCH] feat: add support for the Web Share Target API (#1100) Co-authored-by: userquin --- components/publish/PublishWidget.vue | 14 +++++- composables/web-share-target.ts | 24 +++++++++++ locales/en-GB.json | 5 +++ locales/en-US.json | 5 +++ locales/es-ES.json | 5 +++ middleware/auth.ts | 8 +++- modules/pwa/i18n.ts | 54 +++++++++++++++++++++-- pages/share-target.vue | 38 +++++++++++++++++ plugins/pwa.client.ts | 7 +++ service-worker/share-target.ts | 64 ++++++++++++++++++++++++++++ service-worker/sw.ts | 4 +- 11 files changed, 221 insertions(+), 7 deletions(-) create mode 100644 composables/web-share-target.ts create mode 100644 pages/share-target.vue create mode 100644 service-worker/share-target.ts diff --git a/components/publish/PublishWidget.vue b/components/publish/PublishWidget.vue index 5dfe1c97..d086559e 100644 --- a/components/publish/PublishWidget.vue +++ b/components/publish/PublishWidget.vue @@ -1,7 +1,6 @@ + + diff --git a/plugins/pwa.client.ts b/plugins/pwa.client.ts index ac0f9932..dfa60334 100644 --- a/plugins/pwa.client.ts +++ b/plugins/pwa.client.ts @@ -5,6 +5,12 @@ export default defineNuxtPlugin(() => { const registrationError = ref(false) const swActivated = ref(false) + // https://thomashunter.name/posts/2021-12-11-detecting-if-pwa-twa-is-installed + const ua = navigator.userAgent + const ios = ua.match(/iPhone|iPad|iPod/) + const standalone = window.matchMedia('(display-mode: standalone)').matches + const isInstalled = !!(standalone || (ios && !ua.match(/Safari/))) + const registerPeriodicSync = (swUrl: string, r: ServiceWorkerRegistration) => { setInterval(async () => { if (!online.value) @@ -54,6 +60,7 @@ export default defineNuxtPlugin(() => { return { provide: { pwa: reactive({ + isInstalled, swActivated, registrationError, needRefresh, diff --git a/service-worker/share-target.ts b/service-worker/share-target.ts new file mode 100644 index 00000000..694bf9d5 --- /dev/null +++ b/service-worker/share-target.ts @@ -0,0 +1,64 @@ +/// +declare const self: ServiceWorkerGlobalScope + +const clientResolves: { [key: string]: Function } = {} + +self.addEventListener('message', (event) => { + if (event.data.action !== 'ready-to-receive') + return + + const id: string | undefined = (event.source as any)?.id ?? undefined + + if (id && clientResolves[id] !== undefined) + clientResolves[id]() +}) + +export const onShareTarget = (event: FetchEvent) => { + if (!event.request.url.endsWith('/web-share-target') || event.request.method !== 'POST') + return + + event.waitUntil(handleSharedTarget(event)) +} + +async function handleSharedTarget(event: FetchEvent) { + event.respondWith(Response.redirect('/home?share-target=true')) + await waitForClientToGetReady(event.resultingClientId) + + const [client, formData] = await getClientAndFormData(event) + if (client === undefined) + return + + await sendShareTargetMessage(client, formData) +} + +async function sendShareTargetMessage(client: Client, data: FormData) { + const sharedData: { text?: string; files?: File[] } = {} + + const text = data.get('text') + if (text !== null) + sharedData.text = text.toString() + + const files: File[] = [] + for (const [name, file] of data.entries()) { + if (name === 'files' && file instanceof File) + files.push(file) + } + + if (files.length !== 0) + sharedData.files = files + + client.postMessage({ data: sharedData, action: 'compose-with-shared-data' }) +} + +function waitForClientToGetReady(clientId: string) { + return new Promise((resolve) => { + clientResolves[clientId] = resolve + }) +} + +function getClientAndFormData(event: FetchEvent): Promise<[client: Client | undefined, formData: FormData]> { + return Promise.all([ + self.clients.get(event.resultingClientId), + event.request.formData(), + ]) +} diff --git a/service-worker/sw.ts b/service-worker/sw.ts index 5d139b09..c72e3384 100644 --- a/service-worker/sw.ts +++ b/service-worker/sw.ts @@ -7,6 +7,7 @@ import { StaleWhileRevalidate } from 'workbox-strategies' import { ExpirationPlugin } from 'workbox-expiration' import { onNotificationClick, onPush } from './web-push-notifications' +import { onShareTarget } from './share-target' declare const self: ServiceWorkerGlobalScope @@ -32,7 +33,7 @@ if (import.meta.env.DEV) // deny api and server page calls let denylist: undefined | RegExp[] if (import.meta.env.PROD) - denylist = [/^\/api\//, /^\/login\//, /^\/oauth\//, /^\/signin\//] + denylist = [/^\/api\//, /^\/login\//, /^\/oauth\//, /^\/signin\//, /^\/web-share-target\//] // only cache pages and external assets on local build + start or in production if (import.meta.env.PROD) { @@ -90,3 +91,4 @@ registerRoute(new NavigationRoute( self.addEventListener('push', onPush) self.addEventListener('notificationclick', onNotificationClick) +self.addEventListener('fetch', onShareTarget)