feat: add PWA shortcuts for compose/notifications (#2019)
* feat: add PWA shortcuts for compose/notifications Fixes #2012 * fix: fix icon path
This commit is contained in:
parent
65733ce68a
commit
d044e12aee
|
@ -3,24 +3,22 @@ import { importShowComposeDialog } from '../_components/dialog/asyncDialogs/impo
|
|||
import { database } from '../_database/database'
|
||||
import { doMediaUpload } from './media'
|
||||
|
||||
export async function showShareDialogIfNecessary () {
|
||||
// show a compose dialog, typically invoked by the Web Share API or a PWA shortcut
|
||||
export async function showComposeDialog () {
|
||||
const { isUserLoggedIn } = store.get()
|
||||
if (!isUserLoggedIn) {
|
||||
return
|
||||
}
|
||||
const importShowComposeDialogPromise = importShowComposeDialog() // start promise early
|
||||
|
||||
const data = await database.getWebShareData()
|
||||
if (!data) {
|
||||
return
|
||||
|
||||
if (data) {
|
||||
await database.deleteWebShareData() // only need this data once; it came from Web Share (service worker)
|
||||
}
|
||||
|
||||
// delete from IDB and import the dialog in parallel
|
||||
const [showComposeDialog] = await Promise.all([
|
||||
importShowComposeDialog(),
|
||||
database.deleteWebShareData()
|
||||
])
|
||||
|
||||
console.log('share data', data)
|
||||
const { title, text, url, file } = data
|
||||
const { title, text, url, file } = (data || {})
|
||||
|
||||
// url is currently ignored on Android, but one can dream
|
||||
// https://web.dev/web-share-target/#verifying-shared-content
|
||||
|
@ -30,6 +28,7 @@ export async function showShareDialogIfNecessary () {
|
|||
store.setComposeData('dialog', { text: composeText })
|
||||
store.save()
|
||||
|
||||
const showComposeDialog = await importShowComposeDialogPromise
|
||||
showComposeDialog()
|
||||
if (file) { // start the upload once the dialog is in view so it shows the loading spinner and everything
|
||||
/* no await */ doMediaUpload('dialog', file)
|
|
@ -7,7 +7,7 @@ import { customScrollbarObservers } from './customScrollbarObservers'
|
|||
import { customEmojiObservers } from './customEmojiObservers'
|
||||
import { cleanup } from './cleanup'
|
||||
import { wordFilterObservers } from './wordFilterObservers'
|
||||
import { showShareDialogObservers } from './showShareDialogObservers'
|
||||
import { showComposeDialogObservers } from './showComposeDialogObservers'
|
||||
import { badgeObservers } from './badgeObservers'
|
||||
|
||||
// These observers can be lazy-loaded when the user is actually logged in.
|
||||
|
@ -21,7 +21,7 @@ export function loggedInObservers () {
|
|||
notificationPermissionObservers()
|
||||
customScrollbarObservers()
|
||||
customEmojiObservers()
|
||||
showShareDialogObservers()
|
||||
showComposeDialogObservers()
|
||||
badgeObservers()
|
||||
cleanup()
|
||||
}
|
||||
|
|
|
@ -1,18 +1,18 @@
|
|||
import { store } from '../store'
|
||||
import { showShareDialogIfNecessary } from '../../_actions/showShareDialogIfNecessary'
|
||||
import { showComposeDialog } from '../../_actions/showComposeDialog'
|
||||
|
||||
// If the user is logged in, and if the Service Worker handled a POST and set special data
|
||||
// in IndexedDB, then we want to handle it on the home page.
|
||||
export function showShareDialogObservers () {
|
||||
export function showComposeDialogObservers () {
|
||||
let observedOnce = false
|
||||
store.observe('currentVerifyCredentials', verifyCredentials => {
|
||||
store.observe('currentVerifyCredentials', async verifyCredentials => {
|
||||
if (verifyCredentials && !observedOnce) {
|
||||
// when the verifyCredentials object is available, we can check to see
|
||||
// if the user is trying to share something, then share it
|
||||
// if the user is trying to share something (or we got here from a shortcut), then share it
|
||||
observedOnce = true
|
||||
const { currentPage } = store.get()
|
||||
if (currentPage === 'home') {
|
||||
/* no await */ showShareDialogIfNecessary()
|
||||
if (currentPage === 'home' && new URLSearchParams(location.search).get('compose') === 'true') {
|
||||
await showComposeDialog()
|
||||
}
|
||||
}
|
||||
})
|
|
@ -116,7 +116,7 @@ self.addEventListener('fetch', event => {
|
|||
await setWebShareData({ title, text, url, file })
|
||||
await closeKeyValIDBConnection() // don't need to keep the IDB connection open
|
||||
return Response.redirect(
|
||||
'/?pwa=true', // same as start_url in manifest.json. This can only be invoked from PWAs
|
||||
'/?pwa=true&compose=true', // pwa=true because this can only be invoked from a PWA
|
||||
303 // 303 recommended by https://web.dev/web-share-target/
|
||||
)
|
||||
}
|
||||
|
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 4.1 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 KiB |
|
@ -112,6 +112,32 @@
|
|||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Write a toot",
|
||||
"short_name": "New toot",
|
||||
"description": "Start composing a new toot",
|
||||
"url": "/?pwa=true&compose=true",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-shortcut-fa-pencil.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "View notifications",
|
||||
"short_name": "Notifications",
|
||||
"description": "View your new notifications",
|
||||
"url": "/notifications?pwa=true",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icon-shortcut-fa-bell.png",
|
||||
"sizes": "192x192"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"screenshots": [
|
||||
{
|
||||
"src": "screenshot-540-720-1.png",
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
import {
|
||||
composeModalInput, getComposeModalNthMediaListItem,
|
||||
getUrl, modalDialogContents, simulateWebShare
|
||||
} from '../utils'
|
||||
import { loginAsFoobar } from '../roles'
|
||||
import { ONE_TRANSPARENT_PIXEL } from '../../src/routes/_static/media'
|
||||
|
||||
fixture`027-web-share-and-web-shortcuts.js`
|
||||
.page`http://localhost:4002`
|
||||
|
||||
test('Can take a shortcut directly to a compose dialog', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
.navigateTo('http://localhost:4002/?compose=true')
|
||||
.expect(modalDialogContents.exists).ok()
|
||||
.expect(composeModalInput.value).eql('')
|
||||
.expect(getComposeModalNthMediaListItem(1).exists).notOk()
|
||||
})
|
||||
|
||||
test('Can share title/text using Web Share', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
await (simulateWebShare({ title: 'my title', url: undefined, text: 'my text' })())
|
||||
await t
|
||||
.navigateTo('http://localhost:4002/?compose=true')
|
||||
.expect(modalDialogContents.exists).ok()
|
||||
.expect(composeModalInput.value).eql('my title\n\nmy text')
|
||||
.expect(getComposeModalNthMediaListItem(1).exists).notOk()
|
||||
})
|
||||
|
||||
test('Can share a file using Web Share', async t => {
|
||||
await loginAsFoobar(t)
|
||||
await t
|
||||
.expect(getUrl()).eql('http://localhost:4002/')
|
||||
await (simulateWebShare({ title: undefined, url: undefined, text: undefined, file: ONE_TRANSPARENT_PIXEL })())
|
||||
await t
|
||||
.navigateTo('http://localhost:4002/?compose=true')
|
||||
.expect(modalDialogContents.exists).ok()
|
||||
.expect(composeModalInput.value).eql('')
|
||||
.expect(getComposeModalNthMediaListItem(1).exists).ok()
|
||||
.expect(getComposeModalNthMediaListItem(1).getAttribute('aria-label')).eql('media')
|
||||
})
|
|
@ -265,6 +265,50 @@ export const uploadKittenImage = i => (exec(() => {
|
|||
}
|
||||
}))
|
||||
|
||||
export const simulateWebShare = ({ title, text, url, file }) => (exec(() => {
|
||||
let blob
|
||||
return Promise.resolve().then(() => {
|
||||
if (file) {
|
||||
return fetch(file).then(resp => resp.blob()).then(theBlob => {
|
||||
blob = theBlob
|
||||
})
|
||||
}
|
||||
}).then(() => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open('keyval-store')
|
||||
request.onerror = (event) => {
|
||||
console.error(event)
|
||||
reject(new Error('idb error'))
|
||||
}
|
||||
request.onupgradeneeded = () => {
|
||||
request.result.createObjectStore('keyval')
|
||||
}
|
||||
request.onsuccess = (event) => {
|
||||
const db = event.target.result
|
||||
const txn = db.transaction('keyval', 'readwrite')
|
||||
txn.onerror = () => reject(new Error('idb error'))
|
||||
txn.oncomplete = () => {
|
||||
db.close()
|
||||
resolve()
|
||||
}
|
||||
txn.objectStore('keyval').put({
|
||||
title,
|
||||
text,
|
||||
url,
|
||||
file: blob
|
||||
}, 'web-share-data')
|
||||
}
|
||||
})
|
||||
})
|
||||
}, {
|
||||
dependencies: {
|
||||
title,
|
||||
text,
|
||||
url,
|
||||
file
|
||||
}
|
||||
}))
|
||||
|
||||
export const focus = (selector) => (exec(() => {
|
||||
document.querySelector(selector).focus()
|
||||
}, {
|
||||
|
|
Loading…
Reference in New Issue