Skip to content
Open
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: 9 additions & 3 deletions packages/backend/src/server/web/ClientServerService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,13 +178,19 @@ export class ClientServerService {
'purpose': 'any',
}],
'share_target': {
'action': '/share/',
'method': 'GET',
'enctype': 'application/x-www-form-urlencoded',
'action': '/sw/share',
'method': 'POST',
'enctype': 'multipart/form-data',
'params': {
'title': 'title',
'text': 'text',
'url': 'url',
'files': [
{
'name': 'files',
'accept': '*/*',
},
],
},
},
'shortcuts': [{
Expand Down
14 changes: 10 additions & 4 deletions packages/backend/src/server/web/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@
}
],
"share_target": {
"action": "/share/",
"method": "GET",
"enctype": "application/x-www-form-urlencoded",
"action": "/sw/share",
"method": "POST",
"enctype": "multipart/form-data",
"params": {
"title": "title",
"text": "text",
"url": "url"
"url": "url",
"files": [
{
"name": "files",
"accept": "*/*"
}
]
}
},
"shortcuts": [
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/src/components/MkPostForm.vue
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,10 @@ uploader.events.on('itemUploaded', ctx => {
uploader.removeItem(ctx.item);
});

if (props.initialLocalFiles) {
uploader.addFiles(props.initialLocalFiles);
}

const draftKey = computed((): string => {
let key = targetChannel.value ? `channel:${targetChannel.value.id}` : '';

Expand Down
62 changes: 61 additions & 1 deletion packages/frontend/src/pages/share.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:initialText="initialText"
:initialVisibility="visibility"
:initialFiles="files"
:initialLocalFiles="tempFiles"
:initialLocalOnly="localOnly"
:reply="reply"
:renote="renote"
Expand All @@ -33,6 +34,7 @@ SPDX-License-Identifier: AGPL-3.0-only

import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { get, del } from 'idb-keyval';
import MkButton from '@/components/MkButton.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import * as os from '@/os.js';
Expand All @@ -41,7 +43,19 @@ import { definePage } from '@/page.js';
import { postMessageToParentWindow } from '@/utility/post-message.js';
import { i18n } from '@/i18n.js';

//#region parameters
const urlParams = new URLSearchParams(window.location.search);
// merge hash parameters
try {
const hashParams = new URLSearchParams(window.location.hash.slice(1));
for (const [key, value] of hashParams.entries()) {
urlParams.set(key, value);
}
} catch (e) {
console.error('Failed to parse hash parameters:', e);
}
//#endregion

const localOnlyQuery = urlParams.get('localOnly');
const visibilityQuery = urlParams.get('visibility') as typeof Misskey.noteVisibilities[number];

Expand All @@ -56,6 +70,7 @@ const visibility = ref(Misskey.noteVisibilities.includes(visibilityQuery) ? visi
const localOnly = ref(localOnlyQuery === '0' ? false : localOnlyQuery === '1' ? true : undefined);
const files = ref([] as Misskey.entities.DriveFile[]);
const visibleUsers = ref([] as Misskey.entities.UserDetailed[]);
const tempFiles = ref([] as File[]);

async function init() {
let noteText = '';
Expand Down Expand Up @@ -181,6 +196,30 @@ async function init() {
});
}

//#region Local files
// If the browser supports IndexedDB, try to get the temporary files from temp.
if (typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true) {
const filesFromIdb = await get<File[]>('share-files-temp');
if (Array.isArray(filesFromIdb) && filesFromIdb.length > 0 && filesFromIdb.every(file => file instanceof Blob)) {
tempFiles.value = filesFromIdb;
}
}

const fileData = urlParams.get('file');
if (fileData && fileData.startsWith('data:')) {
try {
const file = await window.fetch(fileData).then(res => res.blob());
if (file instanceof Blob) {
tempFiles.value.push(file as File);
} else {
console.error('Fetched file is not a Blob:', file);
}
} catch (e) {
console.error('Failed to fetch file:', e);
}
}
//#endregion

state.value = 'writing';
}

Expand All @@ -201,10 +240,31 @@ function goToMisskey(): void {

function onPosted(): void {
state.value = 'posted';
// SWが保存したファイルは投稿が完了するまでIndexedDBに保持
del('share-files-temp');
postMessageToParentWindow('misskey:shareForm:shareCompleted');
}

const headerActions = computed(() => []);
const headerActions = computed(() => [
{
icon: 'ti ti-dots',
text: i18n.ts.menu,
handler: (ev: MouseEvent) => {
os.popupMenu([
{
icon: 'ti ti-home',
text: i18n.ts.goToMisskey,
action: () => goToMisskey(),
},
{
icon: 'ti ti-x',
text: i18n.ts.close,
action: () => close(),
},
], ev.currentTarget ?? ev.target);
},
},
]);

const headerTabs = computed(() => []);

Expand Down
1 change: 1 addition & 0 deletions packages/frontend/src/types/post-form.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export interface PostFormProps {
initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[];
initialLocalFiles?: File[];
initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.UserDetailed[];
initialNote?: Misskey.entities.Note;
Expand Down
31 changes: 30 additions & 1 deletion packages/sw/src/sw.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/

import { get } from 'idb-keyval';
import { get, set } from 'idb-keyval';
import * as Misskey from 'misskey-js';
import { FETCH_TIMEOUT_MS } from '@/const.js';
import type { PushNotificationDataMap } from '@/types.js';
Expand Down Expand Up @@ -89,6 +89,34 @@ globalThis.addEventListener('activate', ev => {
});

globalThis.addEventListener('fetch', ev => {
//#region /sw/share
const url = new URL(ev.request.url);
if (url.pathname === '/sw/share') {
ev.respondWith((async () => {
const responseUrl = new URL(ev.request.url);
responseUrl.pathname = '/share';
const formData = await ev.request.formData();

// とりあえず初期化 (IndexedDBの削除は時間がかかる可能性があるため空の配列をセット)
await set('share-url-temp', []);
if (formData.has('files')) {
const files = formData.getAll('files');
if (files.length > 0 && files.every(file => file instanceof Blob)) {
set('share-files-temp', files);
}
}

formData.delete('files');
for (const [key, value] of formData.entries()) {
responseUrl.searchParams.set(key, value.toString());
}

return Response.redirect(responseUrl, 303);
})());
return;
}

//#region others
let isHTMLRequest = false;
if (ev.request.headers.get('sec-fetch-dest') === 'document') {
isHTMLRequest = true;
Expand All @@ -100,6 +128,7 @@ globalThis.addEventListener('fetch', ev => {

if (!isHTMLRequest) return;
ev.respondWith(respondToNavigation(ev.request));
//#endregion
});

globalThis.addEventListener('push', ev => {
Expand Down
Loading