Newer
Older
import { Component, defineAsyncComponent, markRaw, reactive, Ref, ref } from 'vue';
import { EventEmitter } from 'eventemitter3';
import MkPostFormDialog from '@/components/post-form-dialog.vue';
import { $i } from '@/account';
import { defaultStore } from '@/store';
export const windows = new Map();
export function api(endpoint: string, data: Record<string, any> = {}, token?: string | null | undefined) {
pendingApiRequestsCount.value++;
const onFinally = () => {
pendingApiRequestsCount.value--;
};
}) : null;
if (debug) {
apiRequests.value.push(log);
if (apiRequests.value.length > 128) apiRequests.value.shift();
const promise = new Promise((resolve, reject) => {
// Append a credential
if (token !== undefined) (data as any).i = token;
// Send request
fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, {
method: 'POST',
body: JSON.stringify(data),
credentials: 'omit',
cache: 'no-cache'
}).then(async (res) => {
const body = res.status === 204 ? null : await res.json();
if (res.status === 200) {
resolve(body);
log!.res = markRaw(body.error);
log!.state = 'failed';
}
if (defaultStore.state.reportError && !_DEV_) {
Sentry.withScope((scope) => {
scope.setTag('api_endpoint', endpoint);
scope.setContext('api params', data);
scope.setContext('api error info', body.info);
scope.setTag('api_error_id', body.id);
scope.setTag('api_error_code', body.code);
scope.setTag('api_error_kind', body.kind);
scope.setLevel(Sentry.Severity.Error);
Sentry.captureMessage('API error');
});
}
}).catch(reject);
});
promise.then(onFinally, onFinally);
return promise;
}
export function apiWithDialog(
endpoint: string,
data: Record<string, any> = {},
token?: string | null | undefined,
onSuccess?: (res: any) => void,
onFailure?: (e: Error) => void,
) {
const promise = api(endpoint, data, token);
promiseDialog(promise, onSuccess, onFailure ? onFailure : (e) => {
dialog({
type: 'error',
return promise;
}
export function promiseDialog<T extends Promise<any>>(
promise: T,
onSuccess?: ((res: any) => void) | null,
onFailure?: ((e: Error) => void) | null,
promise.then(res => {
if (onSuccess) {
showing.value = false;
onSuccess(res);
} else {
setTimeout(() => {
showing.value = false;
}, 1000);
}
}).catch(e => {
showing.value = false;
if (onFailure) {
onFailure(e);
} else {
dialog({
type: 'error',
text: e
});
}
});
// NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない)
popup(MkWaitingDialog, {
}, {}, 'closed');
return promise;
}
function isModule(x: any): x is typeof import('*.vue') {
return x.default != null;
}
export const popups = ref([]) as Ref<{
id: any;
component: any;
props: Record<string, any>;
}[]>;
export async function popup(component: Component | typeof import('*.vue') | Promise<Component | typeof import('*.vue')>, props: Record<string, any>, events = {}, disposeEvent?: string) {
if (component.then) component = await component;
if (isModule(component)) component = component.default;
markRaw(component);
const dispose = () => {
if (_DEV_) console.log('os:popup close', id, component, props, events);
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ?
setTimeout(() => {
popups.value = popups.value.filter(popup => popup.id !== id);
}, 0);
};
const state = {
component,
props,
events: disposeEvent ? {
...events,
[disposeEvent]: dispose
} : events,
id,
};
if (_DEV_) console.log('os:popup open', id, component, props, events);
popups.value.push(state);
return {
dispose,
};
}
export function pageWindow(path: string) {
const { component, props } = resolve(path);
initialComponent: markRaw(component),
initialProps: props,
}, {}, 'closed');
}
export function dialog(props: Record<string, any>) {
return new Promise((resolve, reject) => {
done: result => {
resolve(result ? result : { canceled: true });
},
}, 'closed');
});
}
export function success() {
return new Promise((resolve, reject) => {
const showing = ref(true);
setTimeout(() => {
showing.value = false;
}, 1000);
showing: showing
}, {
done: () => resolve(),
}, 'closed');
});
}
export function waiting() {
return new Promise((resolve, reject) => {
const showing = ref(true);
showing: showing
}, {
done: () => resolve(),
}, 'closed');
});
}
export function form(title, form) {
return new Promise((resolve, reject) => {
done: result => {
resolve(result);
},
}, 'closed');
});
}
export async function selectUser() {
return new Promise((resolve, reject) => {
ok: user => {
resolve(user);
},
}, 'closed');
});
}
export async function selectDriveFile(multiple: boolean) {
return new Promise((resolve, reject) => {
type: 'file',
multiple
}, {
done: files => {
if (files) {
resolve(multiple ? files : files[0]);
}
},
}, 'closed');
});
}
export async function selectDriveFolder(multiple: boolean) {
return new Promise((resolve, reject) => {
type: 'folder',
multiple
}, {
done: folders => {
if (folders) {
resolve(multiple ? folders : folders[0]);
}
},
}, 'closed');
});
}
}, {
done: emoji => {
resolve(emoji);
},
}, 'closed');
});
}
type AwaitType<T> =
T extends Promise<infer U> ? U :
T extends (...args: Array<any>) => Promise<infer V> ? V :
T;
let openingEmojiPicker: AwaitType<ReturnType<typeof popup>> | null = null;
let activeTextarea: HTMLTextAreaElement | HTMLInputElement | null = null;
export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: typeof activeTextarea) {
if (openingEmojiPicker) return;
activeTextarea = initialTextarea;
const textareas = document.querySelectorAll('textarea, input');
for (const textarea of Array.from(textareas)) {
textarea.addEventListener('focus', () => {
activeTextarea = textarea;
});
}
const observer = new MutationObserver(records => {
for (const record of records) {
for (const node of Array.from(record.addedNodes).filter(node => node instanceof HTMLElement) as HTMLElement[]) {
const textareas = node.querySelectorAll('textarea, input') as NodeListOf<NonNullable<typeof activeTextarea>>;
for (const textarea of Array.from(textareas).filter(textarea => textarea.dataset.preventEmojiInsert == null)) {
if (document.activeElement === textarea) activeTextarea = textarea;
textarea.addEventListener('focus', () => {
activeTextarea = textarea;
});
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
}
}
}
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: false,
characterData: false,
});
openingEmojiPicker = await popup(import('@/components/emoji-picker-window.vue'), {
src,
...opts
}, {
chosen: emoji => {
insertTextAtCursor(activeTextarea, emoji);
},
closed: () => {
openingEmojiPicker!.dispose();
openingEmojiPicker = null;
observer.disconnect();
}
});
}
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
let reactionPicker = null;
export async function pickReaction(src: HTMLElement, chosen, closed) {
if (reactionPicker) {
reactionPicker.src.value = src;
reactionPicker.manualShowing.value = true;
reactionPicker.chosen = chosen;
reactionPicker.closed = closed;
} else {
reactionPicker = {
src: ref(src),
manualShowing: ref(true),
chosen, closed
};
popup(import('@/components/emoji-picker-dialog.vue'), {
src: reactionPicker.src,
asReactionPicker: true,
manualShowing: reactionPicker.manualShowing
}, {
done: reaction => {
reactionPicker.chosen(reaction);
},
close: () => {
reactionPicker.manualShowing.value = false;
},
closed: () => {
reactionPicker.src.value = null;
reactionPicker.closed();
}
});
}
}
export function modalMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) {
return new Promise((resolve, reject) => {
items,
src,
align: options?.align,
viaKeyboard: options?.viaKeyboard
}, {
closed: () => {
resolve();
dispose();
},
});
});
}
export function contextMenu(items: any[], ev: MouseEvent) {
ev.preventDefault();
return new Promise((resolve, reject) => {
items,
ev,
}, {
closed: () => {
resolve();
dispose();
},
});
});
}
export function post(props: Record<string, any>) {
return new Promise((resolve, reject) => {
// NOTE: MkPostFormDialogをdynamic importするとiOSでテキストエリアに自動フォーカスできない
// NOTE: ただ、dynamic importしない場合、MkPostFormDialogインスタンスが使いまわされ、
// Vueが渡されたコンポーネントに内部的に__propsというプロパティを生やす影響で、
// 複数のpost formを開いたときに場合によってはエラーになる
// もちろん複数のpost formを開けること自体Misskeyサイドのバグなのだが
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
});
});
}
export const deckGlobalEvents = new EventEmitter();
export const uploads = ref([]);
export function upload(file: File, folder?: any, name?: string) {
if (folder && typeof folder == 'object') folder = folder.id;
return new Promise((resolve, reject) => {
const id = Math.random();
const reader = new FileReader();
reader.onload = (e) => {
const ctx = reactive({
id: id,
name: name || file.name || 'untitled',
progressMax: undefined,
progressValue: undefined,
img: window.URL.createObjectURL(file)
});
uploads.value.push(ctx);
const data = new FormData();
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
data.append('force', 'true');
data.append('file', file);
if (folder) data.append('folderId', folder);
if (name) data.append('name', name);
const xhr = new XMLHttpRequest();
xhr.open('POST', apiUrl + '/drive/files/create', true);
xhr.onload = (e: any) => {
const driveFile = JSON.parse(e.target.response);
resolve(driveFile);
uploads.value = uploads.value.filter(x => x.id != id);
};
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
ctx.progressMax = e.total;
ctx.progressValue = e.loaded;
}
};
xhr.send(data);
};
reader.readAsArrayBuffer(file);
});
}
/*
export function checkExistence(fileData: ArrayBuffer): Promise<any> {
return new Promise((resolve, reject) => {
const data = new FormData();
data.append('md5', getMD5(fileData));
os.api('drive/files/find-by-hash', {
md5: getMD5(fileData)
}).then(resp => {
resolve(resp.length > 0 ? resp[0] : null);
});
});
}*/