diff options
| author | tamaina <tamaina@hotmail.co.jp> | 2022-11-17 23:35:55 +0900 |
|---|---|---|
| committer | tamaina <tamaina@hotmail.co.jp> | 2022-11-17 23:35:55 +0900 |
| commit | 764da890b6ad3d53808ec592099a93d9d39d7b08 (patch) | |
| tree | b3e9b08bfafa2bbbb5f657af3adb60bcc9510b67 /packages/client/src/scripts | |
| parent | fix (diff) | |
| parent | Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop (diff) | |
| download | sharkey-764da890b6ad3d53808ec592099a93d9d39d7b08.tar.gz sharkey-764da890b6ad3d53808ec592099a93d9d39d7b08.tar.bz2 sharkey-764da890b6ad3d53808ec592099a93d9d39d7b08.zip | |
Merge branch 'develop' into pizzax-indexeddb
Diffstat (limited to 'packages/client/src/scripts')
32 files changed, 616 insertions, 248 deletions
diff --git a/packages/client/src/scripts/aiscript/api.ts b/packages/client/src/scripts/aiscript/api.ts index 01b8fd05fe..6debcb8a13 100644 --- a/packages/client/src/scripts/aiscript/api.ts +++ b/packages/client/src/scripts/aiscript/api.ts @@ -27,7 +27,7 @@ export function createAiScriptEnv(opts) { if (token) utils.assertString(token); apiRequests++; if (apiRequests > 16) return values.NULL; - const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token || null)); + const res = await os.api(ep.value, utils.valToJs(param), token ? token.value : (opts.token ?? null)); return utils.jsToVal(res); }), 'Mk:save': values.FN_NATIVE(([key, value]) => { diff --git a/packages/client/src/scripts/array.ts b/packages/client/src/scripts/array.ts index 29d027de14..26c6195d66 100644 --- a/packages/client/src/scripts/array.ts +++ b/packages/client/src/scripts/array.ts @@ -98,7 +98,7 @@ export function groupOn<T, S>(f: (x: T) => S, xs: T[]): T[][] { export function groupByX<T>(collections: T[], keySelector: (x: T) => string) { return collections.reduce((obj: Record<string, T[]>, item: T) => { const key = keySelector(item); - if (!obj.hasOwnProperty(key)) { + if (typeof obj[key] === 'undefined') { obj[key] = []; } diff --git a/packages/client/src/scripts/autocomplete.ts b/packages/client/src/scripts/autocomplete.ts index 8d9bdee8f5..206724de9e 100644 --- a/packages/client/src/scripts/autocomplete.ts +++ b/packages/client/src/scripts/autocomplete.ts @@ -8,7 +8,7 @@ export class Autocomplete { x: Ref<number>; y: Ref<number>; q: Ref<string | null>; - close: Function; + close: () => void; } | null; private textarea: HTMLInputElement | HTMLTextAreaElement; private currentType: string; @@ -157,7 +157,7 @@ export class Autocomplete { const _y = ref(y); const _q = ref(q); - const { dispose } = await popup(defineAsyncComponent(() => import('@/components/autocomplete.vue')), { + const { dispose } = await popup(defineAsyncComponent(() => import('@/components/MkAutocomplete.vue')), { textarea: this.textarea, close: this.close, type: type, diff --git a/packages/client/src/scripts/check-word-mute.ts b/packages/client/src/scripts/check-word-mute.ts index fa74c09939..35d40a6e08 100644 --- a/packages/client/src/scripts/check-word-mute.ts +++ b/packages/client/src/scripts/check-word-mute.ts @@ -3,7 +3,9 @@ export function checkWordMute(note: Record<string, any>, me: Record<string, any> if (me && (note.userId === me.id)) return false; if (mutedWords.length > 0) { - if (note.text == null) return false; + const text = ((note.cw ?? '') + '\n' + (note.text ?? '')).trim(); + + if (text === '') return false; const matched = mutedWords.some(filter => { if (Array.isArray(filter)) { @@ -11,7 +13,7 @@ export function checkWordMute(note: Record<string, any>, me: Record<string, any> const filteredFilter = filter.filter(keyword => keyword !== ''); if (filteredFilter.length === 0) return false; - return filteredFilter.every(keyword => note.text!.includes(keyword)); + return filteredFilter.every(keyword => text.includes(keyword)); } else { // represents RegExp const regexp = filter.match(/^\/(.+)\/(.*)$/); @@ -20,7 +22,7 @@ export function checkWordMute(note: Record<string, any>, me: Record<string, any> if (!regexp) return false; try { - return new RegExp(regexp[1], regexp[2]).test(note.text!); + return new RegExp(regexp[1], regexp[2]).test(text); } catch (err) { // This should never happen due to input sanitisation. return false; diff --git a/packages/client/src/scripts/clone.ts b/packages/client/src/scripts/clone.ts new file mode 100644 index 0000000000..16fad24129 --- /dev/null +++ b/packages/client/src/scripts/clone.ts @@ -0,0 +1,18 @@ +// structredCloneが遅いため +// SEE: http://var.blog.jp/archives/86038606.html + +type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; + +export function deepClone<T extends Cloneable>(x: T): T { + if (typeof x === 'object') { + if (x === null) return x; + if (Array.isArray(x)) return x.map(deepClone) as T; + const obj = {} as Record<string, Cloneable>; + for (const [k, v] of Object.entries(x)) { + obj[k] = deepClone(v); + } + return obj as T; + } else { + return x; + } +} diff --git a/packages/client/src/scripts/emojilist.ts b/packages/client/src/scripts/emojilist.ts index 4196170d24..4ce63dc7e7 100644 --- a/packages/client/src/scripts/emojilist.ts +++ b/packages/client/src/scripts/emojilist.ts @@ -8,4 +8,6 @@ export type UnicodeEmojiDef = { } // initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -export const emojilist = (await import('../emojilist.json')).default as UnicodeEmojiDef[]; +import _emojilist from '../emojilist.json'; + +export const emojilist = _emojilist as UnicodeEmojiDef[]; diff --git a/packages/client/src/scripts/gen-search-query.ts b/packages/client/src/scripts/gen-search-query.ts index 57a06c280c..b413cbbab1 100644 --- a/packages/client/src/scripts/gen-search-query.ts +++ b/packages/client/src/scripts/gen-search-query.ts @@ -21,7 +21,6 @@ export async function genSearchQuery(v: any, q: string) { } } } - } return { query: q.split(' ').filter(x => !x.startsWith('/') && !x.startsWith('@')).join(' '), diff --git a/packages/client/src/scripts/get-note-menu.ts b/packages/client/src/scripts/get-note-menu.ts index 283c90362c..4826cd70fd 100644 --- a/packages/client/src/scripts/get-note-menu.ts +++ b/packages/client/src/scripts/get-note-menu.ts @@ -1,5 +1,6 @@ import { defineAsyncComponent, Ref, inject } from 'vue'; import * as misskey from 'misskey-js'; +import { pleaseLogin } from './please-login'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { instance } from '@/instance'; @@ -7,7 +8,7 @@ import * as os from '@/os'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import { url } from '@/config'; import { noteActions } from '@/store'; -import { pleaseLogin } from './please-login'; +import { notePage } from '@/filters/note'; export function getNoteMenu(props: { note: misskey.entities.Note; @@ -34,7 +35,7 @@ export function getNoteMenu(props: { if (canceled) return; os.api('notes/delete', { - noteId: appearNote.id + noteId: appearNote.id, }); }); } @@ -47,7 +48,7 @@ export function getNoteMenu(props: { if (canceled) return; os.api('notes/delete', { - noteId: appearNote.id + noteId: appearNote.id, }); os.post({ initialNote: appearNote, renote: appearNote.renote, reply: appearNote.reply, channel: appearNote.channel }); @@ -56,19 +57,13 @@ export function getNoteMenu(props: { function toggleFavorite(favorite: boolean): void { os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { - noteId: appearNote.id - }); - } - - function toggleWatch(watch: boolean): void { - os.apiWithDialog(watch ? 'notes/watching/create' : 'notes/watching/delete', { - noteId: appearNote.id + noteId: appearNote.id, }); } function toggleThreadMute(mute: boolean): void { os.apiWithDialog(mute ? 'notes/thread-muting/create' : 'notes/thread-muting/delete', { - noteId: appearNote.id + noteId: appearNote.id, }); } @@ -84,12 +79,12 @@ export function getNoteMenu(props: { function togglePin(pin: boolean): void { os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', { - noteId: appearNote.id + noteId: appearNote.id, }, undefined, null, res => { if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') { os.alert({ type: 'error', - text: i18n.ts.pinLimitExceeded + text: i18n.ts.pinLimitExceeded, }); } }); @@ -104,26 +99,26 @@ export function getNoteMenu(props: { const { canceled, result } = await os.form(i18n.ts.createNewClip, { name: { type: 'string', - label: i18n.ts.name + label: i18n.ts.name, }, description: { type: 'string', required: false, multiline: true, - label: i18n.ts.description + label: i18n.ts.description, }, isPublic: { type: 'boolean', label: i18n.ts.public, - default: false - } + default: false, + }, }); if (canceled) return; const clip = await os.apiWithDialog('clips/create', result); os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); - } + }, }, null, ...clips.map(clip => ({ text: clip.name, action: () => { @@ -146,9 +141,9 @@ export function getNoteMenu(props: { text: err.message + '\n' + err.id, }); } - } + }, ); - } + }, }))], props.menuButton.value, { }).then(focus); } @@ -178,7 +173,9 @@ export function getNoteMenu(props: { url: `${url}/notes/${appearNote.id}`, }); } - + function notedetails(): void { + os.pageWindow(`/notes/${appearNote.id}`); + } async function translate(): Promise<void> { if (props.translation.value != null) return; props.translating.value = true; @@ -193,86 +190,80 @@ export function getNoteMenu(props: { let menu; if ($i) { const statePromise = os.api('notes/state', { - noteId: appearNote.id + noteId: appearNote.id, }); menu = [ - ...( - props.currentClipPage?.value.userId === $i.id ? [{ - icon: 'fas fa-circle-minus', - text: i18n.ts.unclip, - danger: true, - action: unclip, - }, null] : [] - ), - { - icon: 'fas fa-copy', - text: i18n.ts.copyContent, - action: copyContent - }, { - icon: 'fas fa-link', - text: i18n.ts.copyLink, - action: copyLink - }, (appearNote.url || appearNote.uri) ? { - icon: 'fas fa-external-link-square-alt', - text: i18n.ts.showOnRemote, - action: () => { - window.open(appearNote.url || appearNote.uri, '_blank'); - } - } : undefined, - { - icon: 'fas fa-share-alt', - text: i18n.ts.share, - action: share - }, - instance.translatorAvailable ? { - icon: 'fas fa-language', - text: i18n.ts.translate, - action: translate - } : undefined, - null, - statePromise.then(state => state.isFavorited ? { - icon: 'fas fa-star', - text: i18n.ts.unfavorite, - action: () => toggleFavorite(false) - } : { - icon: 'fas fa-star', - text: i18n.ts.favorite, - action: () => toggleFavorite(true) - }), - { - icon: 'fas fa-paperclip', - text: i18n.ts.clip, - action: () => clip() - }, - (appearNote.userId !== $i.id) ? statePromise.then(state => state.isWatching ? { - icon: 'fas fa-eye-slash', - text: i18n.ts.unwatch, - action: () => toggleWatch(false) - } : { - icon: 'fas fa-eye', - text: i18n.ts.watch, - action: () => toggleWatch(true) - }) : undefined, - statePromise.then(state => state.isMutedThread ? { - icon: 'fas fa-comment-slash', - text: i18n.ts.unmuteThread, - action: () => toggleThreadMute(false) - } : { - icon: 'fas fa-comment-slash', - text: i18n.ts.muteThread, - action: () => toggleThreadMute(true) - }), - appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { - icon: 'fas fa-thumbtack', - text: i18n.ts.unpin, - action: () => togglePin(false) - } : { - icon: 'fas fa-thumbtack', - text: i18n.ts.pin, - action: () => togglePin(true) - } : undefined, - /* + ...( + props.currentClipPage?.value.userId === $i.id ? [{ + icon: 'fas fa-circle-minus', + text: i18n.ts.unclip, + danger: true, + action: unclip, + }, null] : [] + ), { + icon: 'fas fa-external-link-alt', + text: i18n.ts.details, + action: notedetails, + }, { + icon: 'fas fa-copy', + text: i18n.ts.copyContent, + action: copyContent, + }, { + icon: 'fas fa-link', + text: i18n.ts.copyLink, + action: copyLink, + }, (appearNote.url || appearNote.uri) ? { + icon: 'fas fa-external-link-square-alt', + text: i18n.ts.showOnRemote, + action: () => { + window.open(appearNote.url || appearNote.uri, '_blank'); + }, + } : undefined, + { + icon: 'fas fa-share-alt', + text: i18n.ts.share, + action: share, + }, + instance.translatorAvailable ? { + icon: 'fas fa-language', + text: i18n.ts.translate, + action: translate, + } : undefined, + null, + statePromise.then(state => state.isFavorited ? { + icon: 'fas fa-star', + text: i18n.ts.unfavorite, + action: () => toggleFavorite(false), + } : { + icon: 'fas fa-star', + text: i18n.ts.favorite, + action: () => toggleFavorite(true), + }), + { + icon: 'fas fa-paperclip', + text: i18n.ts.clip, + action: () => clip(), + }, + statePromise.then(state => state.isMutedThread ? { + icon: 'fas fa-comment-slash', + text: i18n.ts.unmuteThread, + action: () => toggleThreadMute(false), + } : { + icon: 'fas fa-comment-slash', + text: i18n.ts.muteThread, + action: () => toggleThreadMute(true), + }), + appearNote.userId === $i.id ? ($i.pinnedNoteIds || []).includes(appearNote.id) ? { + icon: 'fas fa-thumbtack', + text: i18n.ts.unpin, + action: () => togglePin(false), + } : { + icon: 'fas fa-thumbtack', + text: i18n.ts.pin, + action: () => togglePin(true), + } : undefined, + /* ...($i.isModerator || $i.isAdmin ? [ null, { @@ -282,54 +273,58 @@ export function getNoteMenu(props: { }] : [] ),*/ - ...(appearNote.userId !== $i.id ? [ - null, - { - icon: 'fas fa-exclamation-circle', - text: i18n.ts.reportAbuse, - action: () => { - const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`; - os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), { - user: appearNote.user, - initialComment: `Note: ${u}\n-----\n` - }, {}, 'closed'); - } - }] + ...(appearNote.userId !== $i.id ? [ + null, + { + icon: 'fas fa-exclamation-circle', + text: i18n.ts.reportAbuse, + action: () => { + const u = appearNote.url || appearNote.uri || `${url}/notes/${appearNote.id}`; + os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { + user: appearNote.user, + initialComment: `Note: ${u}\n-----\n`, + }, {}, 'closed'); + }, + }] : [] - ), - ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ - null, - appearNote.userId === $i.id ? { - icon: 'fas fa-edit', - text: i18n.ts.deleteAndEdit, - action: delEdit - } : undefined, - { - icon: 'fas fa-trash-alt', - text: i18n.ts.delete, - danger: true, - action: del - }] + ), + ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ + null, + appearNote.userId === $i.id ? { + icon: 'fas fa-edit', + text: i18n.ts.deleteAndEdit, + action: delEdit, + } : undefined, + { + icon: 'fas fa-trash-alt', + text: i18n.ts.delete, + danger: true, + action: del, + }] : [] - )] - .filter(x => x !== undefined); + )] + .filter(x => x !== undefined); } else { menu = [{ + icon: 'fas fa-external-link-alt', + text: i18n.ts.detailed, + action: openDetail, + }, { icon: 'fas fa-copy', text: i18n.ts.copyContent, - action: copyContent + action: copyContent, }, { icon: 'fas fa-link', text: i18n.ts.copyLink, - action: copyLink + action: copyLink, }, (appearNote.url || appearNote.uri) ? { icon: 'fas fa-external-link-square-alt', text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url || appearNote.uri, '_blank'); - } + }, } : undefined] - .filter(x => x !== undefined); + .filter(x => x !== undefined); } if (noteActions.length > 0) { @@ -338,7 +333,7 @@ export function getNoteMenu(props: { text: action.title, action: () => { action.handler(appearNote); - } + }, }))]); } diff --git a/packages/client/src/scripts/get-user-menu.ts b/packages/client/src/scripts/get-user-menu.ts index 25bcd90e9f..4a5a2d42f0 100644 --- a/packages/client/src/scripts/get-user-menu.ts +++ b/packages/client/src/scripts/get-user-menu.ts @@ -7,8 +7,9 @@ import * as os from '@/os'; import { userActions } from '@/store'; import { $i, iAmModerator } from '@/account'; import { mainRouter } from '@/router'; +import { Router } from '@/nirax'; -export function getUserMenu(user) { +export function getUserMenu(user, router: Router = mainRouter) { const meId = $i ? $i.id : null; async function pushList() { @@ -128,7 +129,7 @@ export function getUserMenu(user) { } function reportAbuse() { - os.popup(defineAsyncComponent(() => import('@/components/abuse-report-window.vue')), { + os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: user, }, {}, 'closed'); } @@ -161,7 +162,7 @@ export function getUserMenu(user) { icon: 'fas fa-info-circle', text: i18n.ts.info, action: () => { - os.pageWindow(`/user-info/${user.id}`); + router.push(`/user-info/${user.id}`); }, }, { icon: 'fas fa-envelope', @@ -227,7 +228,7 @@ export function getUserMenu(user) { icon: 'fas fa-pencil-alt', text: i18n.ts.editProfile, action: () => { - mainRouter.push('/settings/profile'); + router.push('/settings/profile'); }, }]); } diff --git a/packages/client/src/scripts/hotkey.ts b/packages/client/src/scripts/hotkey.ts index fd9c74f6c8..bd8c3b6cab 100644 --- a/packages/client/src/scripts/hotkey.ts +++ b/packages/client/src/scripts/hotkey.ts @@ -1,6 +1,8 @@ import keyCode from './keycode'; -type Keymap = Record<string, Function>; +type Callback = (ev: KeyboardEvent) => void; + +type Keymap = Record<string, Callback>; type Pattern = { which: string[]; @@ -11,14 +13,14 @@ type Pattern = { type Action = { patterns: Pattern[]; - callback: Function; + callback: Callback; allowRepeat: boolean; }; const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { const result = { patterns: [], - callback: callback, + callback, allowRepeat: true } as Action; diff --git a/packages/client/src/scripts/hpml/evaluator.ts b/packages/client/src/scripts/hpml/evaluator.ts index 8106687b61..10023edffb 100644 --- a/packages/client/src/scripts/hpml/evaluator.ts +++ b/packages/client/src/scripts/hpml/evaluator.ts @@ -159,7 +159,6 @@ export class Hpml { @autobind private evaluate(expr: Expr, scope: HpmlScope): any { - if (isLiteralValue(expr)) { if (expr.type === null) { return null; diff --git a/packages/client/src/scripts/hpml/expr.ts b/packages/client/src/scripts/hpml/expr.ts index 00e3ed118b..18c7c2a14b 100644 --- a/packages/client/src/scripts/hpml/expr.ts +++ b/packages/client/src/scripts/hpml/expr.ts @@ -16,7 +16,7 @@ export type TextValue = ExprBase & { value: string; }; -export type MultiLineTextValue = ExprBase & { +export type MultiLineTextValue = ExprBase & { type: 'multiLineText'; value: string; }; diff --git a/packages/client/src/scripts/hpml/index.ts b/packages/client/src/scripts/hpml/index.ts index ac81eac2d9..7cf88d5961 100644 --- a/packages/client/src/scripts/hpml/index.ts +++ b/packages/client/src/scripts/hpml/index.ts @@ -14,13 +14,13 @@ export type Fn = { export type Type = 'string' | 'number' | 'boolean' | 'stringArray' | null; export const literalDefs: Record<string, { out: any; category: string; icon: any; }> = { - text: { out: 'string', category: 'value', icon: 'fas fa-quote-right', }, - multiLineText: { out: 'string', category: 'value', icon: 'fas fa-align-left', }, - textList: { out: 'stringArray', category: 'value', icon: 'fas fa-list', }, - number: { out: 'number', category: 'value', icon: 'fas fa-sort-numeric-up', }, - ref: { out: null, category: 'value', icon: 'fas fa-magic', }, - aiScriptVar: { out: null, category: 'value', icon: 'fas fa-magic', }, - fn: { out: 'function', category: 'value', icon: 'fas fa-square-root-alt', }, + text: { out: 'string', category: 'value', icon: 'fas fa-quote-right', }, + multiLineText: { out: 'string', category: 'value', icon: 'fas fa-align-left', }, + textList: { out: 'stringArray', category: 'value', icon: 'fas fa-list', }, + number: { out: 'number', category: 'value', icon: 'fas fa-sort-numeric-up', }, + ref: { out: null, category: 'value', icon: 'fas fa-magic', }, + aiScriptVar: { out: null, category: 'value', icon: 'fas fa-magic', }, + fn: { out: 'function', category: 'value', icon: 'fas fa-square-root-alt', }, }; export const blockDefs = [ diff --git a/packages/client/src/scripts/hpml/lib.ts b/packages/client/src/scripts/hpml/lib.ts index 01a44ffcdf..cab467a920 100644 --- a/packages/client/src/scripts/hpml/lib.ts +++ b/packages/client/src/scripts/hpml/lib.ts @@ -125,55 +125,56 @@ export function initAiLib(hpml: Hpml) { } }); */ - }) + }), }; } export const funcDefs: Record<string, { in: any[]; out: any; category: string; icon: any; }> = { - if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'fas fa-share-alt', }, - for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'fas fa-recycle', }, - not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag', }, - or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag', }, - and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag', }, - add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-plus', }, - subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-minus', }, - multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-times', }, - divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide', }, - mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide', }, - round: { in: ['number'], out: 'number', category: 'operation', icon: 'fas fa-calculator', }, - eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-equals', }, - notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-not-equal', }, - gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than', }, - lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than', }, - gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than-equal', }, - ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than-equal', }, - strLen: { in: ['string'], out: 'number', category: 'text', icon: 'fas fa-quote-right', }, - strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'fas fa-quote-right', }, - strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right', }, - strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'fas fa-quote-right', }, - join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right', }, - stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'fas fa-exchange-alt', }, - numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'fas fa-exchange-alt', }, - splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'fas fa-exchange-alt', }, - pick: { in: [null, 'number'], out: null, category: 'list', icon: 'fas fa-indent', }, - listLen: { in: [null], out: 'number', category: 'list', icon: 'fas fa-indent', }, - rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice', }, - dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice', }, - seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice', }, - random: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice', }, - dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice', }, - seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'fas fa-dice', }, - randomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice', }, - dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice', }, - seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'fas fa-dice', }, - DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'fas fa-dice', }, // dailyRandomPickWithProbabilityMapping + if: { in: ['boolean', 0, 0], out: 0, category: 'flow', icon: 'fas fa-share-alt' }, + for: { in: ['number', 'function'], out: null, category: 'flow', icon: 'fas fa-recycle' }, + not: { in: ['boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, + or: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, + and: { in: ['boolean', 'boolean'], out: 'boolean', category: 'logical', icon: 'fas fa-flag' }, + add: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-plus' }, + subtract: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-minus' }, + multiply: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-times' }, + divide: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' }, + mod: { in: ['number', 'number'], out: 'number', category: 'operation', icon: 'fas fa-divide' }, + round: { in: ['number'], out: 'number', category: 'operation', icon: 'fas fa-calculator' }, + eq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-equals' }, + notEq: { in: [0, 0], out: 'boolean', category: 'comparison', icon: 'fas fa-not-equal' }, + gt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than' }, + lt: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than' }, + gtEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-greater-than-equal' }, + ltEq: { in: ['number', 'number'], out: 'boolean', category: 'comparison', icon: 'fas fa-less-than-equal' }, + strLen: { in: ['string'], out: 'number', category: 'text', icon: 'fas fa-quote-right' }, + strPick: { in: ['string', 'number'], out: 'string', category: 'text', icon: 'fas fa-quote-right' }, + strReplace: { in: ['string', 'string', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right' }, + strReverse: { in: ['string'], out: 'string', category: 'text', icon: 'fas fa-quote-right' }, + join: { in: ['stringArray', 'string'], out: 'string', category: 'text', icon: 'fas fa-quote-right' }, + stringToNumber: { in: ['string'], out: 'number', category: 'convert', icon: 'fas fa-exchange-alt' }, + numberToString: { in: ['number'], out: 'string', category: 'convert', icon: 'fas fa-exchange-alt' }, + splitStrByLine: { in: ['string'], out: 'stringArray', category: 'convert', icon: 'fas fa-exchange-alt' }, + pick: { in: [null, 'number'], out: null, category: 'list', icon: 'fas fa-indent' }, + listLen: { in: [null], out: 'number', category: 'list', icon: 'fas fa-indent' }, + rannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, + dailyRannum: { in: ['number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, + seedRannum: { in: [null, 'number', 'number'], out: 'number', category: 'random', icon: 'fas fa-dice' }, + random: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, + dailyRandom: { in: ['number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, + seedRandom: { in: [null, 'number'], out: 'boolean', category: 'random', icon: 'fas fa-dice' }, + randomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' }, + dailyRandomPick: { in: [0], out: 0, category: 'random', icon: 'fas fa-dice' }, + seedRandomPick: { in: [null, 0], out: 0, category: 'random', icon: 'fas fa-dice' }, + DRPWPM: { in: ['stringArray'], out: 'string', category: 'random', icon: 'fas fa-dice' }, // dailyRandomPickWithProbabilityMapping }; export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, visitor?: any) { - const date = new Date(); const day = `${visitor ? visitor.id : ''} ${date.getFullYear()}/${date.getMonth() + 1}/${date.getDate()}`; + // SHOULD be fine to ignore since it's intended + function shape isn't defined + // eslint-disable-next-line @typescript-eslint/ban-types const funcs: Record<string, Function> = { not: (a: boolean) => !a, or: (a: boolean, b: boolean) => a || b, @@ -189,7 +190,7 @@ export function initHpmlLib(expr: Expr, scope: HpmlScope, randomSeed: string, vi const result: any[] = []; for (let i = 0; i < times; i++) { result.push(fn.exec({ - [fn.slots[0]]: i + 1 + [fn.slots[0]]: i + 1, })); } return result; diff --git a/packages/client/src/scripts/hpml/type-checker.ts b/packages/client/src/scripts/hpml/type-checker.ts index 9633b3cd01..24c9ed8bcb 100644 --- a/packages/client/src/scripts/hpml/type-checker.ts +++ b/packages/client/src/scripts/hpml/type-checker.ts @@ -1,7 +1,9 @@ import autobind from 'autobind-decorator'; -import { Type, envVarsDef, PageVar } from '.'; -import { Expr, isLiteralValue, Variable } from './expr'; +import { isLiteralValue } from './expr'; import { funcDefs } from './lib'; +import { envVarsDef } from '.'; +import type { Type, PageVar } from '.'; +import type { Expr, Variable } from './expr'; type TypeError = { arg: number; @@ -44,14 +46,14 @@ export class HpmlTypeChecker { return { arg: i, expect: generic[arg], - actual: type + actual: type, }; } } else if (type !== arg) { return { arg: i, expect: arg, - actual: type + actual: type, }; } } @@ -81,7 +83,7 @@ export class HpmlTypeChecker { } if (typeof def.in[slot] === 'number') { - return generic[def.in[slot]] || null; + return generic[def.in[slot]] ?? null; } else { return def.in[slot]; } diff --git a/packages/client/src/scripts/idb-proxy.ts b/packages/client/src/scripts/idb-proxy.ts index d462a0d7ce..77bb84463c 100644 --- a/packages/client/src/scripts/idb-proxy.ts +++ b/packages/client/src/scripts/idb-proxy.ts @@ -11,16 +11,15 @@ const fallbackName = (key: string) => `idbfallback::${key}`; let idbAvailable = typeof window !== 'undefined' ? !!window.indexedDB : true; if (idbAvailable) { - try { - await iset('idb-test', 'test'); - } catch (err) { + iset('idb-test', 'test').catch(err => { console.error('idb error', err); + console.error('indexedDB is unavailable. It will use localStorage.'); idbAvailable = false; - } + }); +} else { + console.error('indexedDB is unavailable. It will use localStorage.'); } -if (!idbAvailable) console.error('indexedDB is unavailable. It will use localStorage.'); - export async function get(key: string) { if (idbAvailable) return iget(key); return JSON.parse(localStorage.getItem(fallbackName(key))); diff --git a/packages/client/src/scripts/media-proxy.ts b/packages/client/src/scripts/media-proxy.ts new file mode 100644 index 0000000000..76e20786f4 --- /dev/null +++ b/packages/client/src/scripts/media-proxy.ts @@ -0,0 +1,13 @@ +import { query } from '@/scripts/url'; +import { url } from '@/config'; + +export function getProxiedImageUrl(imageUrl: string): string { + return `${url}/proxy/image.webp?${query({ + url: imageUrl, + })}`; +} + +export function getProxiedImageUrlNullable(imageUrl: string | null | undefined): string | null { + if (imageUrl == null) return null; + return getProxiedImageUrl(imageUrl); +} diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts index e21a6d2ed3..3323968f71 100644 --- a/packages/client/src/scripts/please-login.ts +++ b/packages/client/src/scripts/please-login.ts @@ -6,7 +6,7 @@ import { popup } from '@/os'; export function pleaseLogin(path?: string) { if ($i) return; - popup(defineAsyncComponent(() => import('@/components/signin-dialog.vue')), { + popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { autoSet: true, message: i18n.ts.signinRequired }, { @@ -17,5 +17,5 @@ export function pleaseLogin(path?: string) { }, }, 'closed'); - throw new Error('signin required'); + if (!path) throw new Error('signin required'); } diff --git a/packages/client/src/scripts/popup-position.ts b/packages/client/src/scripts/popup-position.ts new file mode 100644 index 0000000000..e84eebf103 --- /dev/null +++ b/packages/client/src/scripts/popup-position.ts @@ -0,0 +1,158 @@ +import { Ref } from 'vue'; + +export function calcPopupPosition(el: HTMLElement, props: { + anchorElement: HTMLElement | null; + innerMargin: number; + direction: 'top' | 'bottom' | 'left' | 'right'; + align: 'top' | 'bottom' | 'left' | 'right' | 'center'; + alignOffset?: number; + x?: number; + y?: number; +}): { top: number; left: number; transformOrigin: string; } { + const contentWidth = el.offsetWidth; + const contentHeight = el.offsetHeight; + + let rect: DOMRect; + + if (props.anchorElement) { + rect = props.anchorElement.getBoundingClientRect(); + } + + const calcPosWhenTop = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset - contentHeight) - props.innerMargin; + } else { + left = props.x; + top = (props.y - contentHeight) - props.innerMargin; + } + + left -= (el.offsetWidth / 2); + + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } + + return [left, top]; + }; + + const calcPosWhenBottom = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = rect.left + window.pageXOffset + (props.anchorElement.offsetWidth / 2); + top = (rect.top + window.pageYOffset + props.anchorElement.offsetHeight) + props.innerMargin; + } else { + left = props.x; + top = (props.y) + props.innerMargin; + } + + left -= (el.offsetWidth / 2); + + if (left + contentWidth - window.pageXOffset > window.innerWidth) { + left = window.innerWidth - contentWidth + window.pageXOffset - 1; + } + + return [left, top]; + }; + + const calcPosWhenLeft = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = (rect.left + window.pageXOffset - contentWidth) - props.innerMargin; + top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); + } else { + left = (props.x - contentWidth) - props.innerMargin; + top = props.y; + } + + top -= (el.offsetHeight / 2); + + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; + } + + return [left, top]; + }; + + const calcPosWhenRight = () => { + let left: number; + let top: number; + + if (props.anchorElement) { + left = (rect.left + props.anchorElement.offsetWidth + window.pageXOffset) + props.innerMargin; + + if (props.align === 'top') { + top = rect.top + window.pageYOffset; + if (props.alignOffset != null) top += props.alignOffset; + } else if (props.align === 'bottom') { + // TODO + } else { // center + top = rect.top + window.pageYOffset + (props.anchorElement.offsetHeight / 2); + top -= (el.offsetHeight / 2); + } + } else { + left = props.x + props.innerMargin; + top = props.y; + top -= (el.offsetHeight / 2); + } + + if (top + contentHeight - window.pageYOffset > window.innerHeight) { + top = window.innerHeight - contentHeight + window.pageYOffset - 1; + } + + return [left, top]; + }; + + const calc = (): { + left: number; + top: number; + transformOrigin: string; + } => { + switch (props.direction) { + case 'top': { + const [left, top] = calcPosWhenTop(); + + // ツールチップを上に向かって表示するスペースがなければ下に向かって出す + if (top - window.pageYOffset < 0) { + const [left, top] = calcPosWhenBottom(); + return { left, top, transformOrigin: 'center top' }; + } + + return { left, top, transformOrigin: 'center bottom' }; + } + + case 'bottom': { + const [left, top] = calcPosWhenBottom(); + // TODO: ツールチップを下に向かって表示するスペースがなければ上に向かって出す + return { left, top, transformOrigin: 'center top' }; + } + + case 'left': { + const [left, top] = calcPosWhenLeft(); + + // ツールチップを左に向かって表示するスペースがなければ右に向かって出す + if (left - window.pageXOffset < 0) { + const [left, top] = calcPosWhenRight(); + return { left, top, transformOrigin: 'left center' }; + } + + return { left, top, transformOrigin: 'right center' }; + } + + case 'right': { + const [left, top] = calcPosWhenRight(); + // TODO: ツールチップを右に向かって表示するスペースがなければ左に向かって出す + return { left, top, transformOrigin: 'left center' }; + } + } + }; + + return calc(); +} diff --git a/packages/client/src/scripts/reaction-picker.ts b/packages/client/src/scripts/reaction-picker.ts index b7699cae4a..a6d0940a40 100644 --- a/packages/client/src/scripts/reaction-picker.ts +++ b/packages/client/src/scripts/reaction-picker.ts @@ -12,7 +12,7 @@ class ReactionPicker { } public async init() { - await popup(defineAsyncComponent(() => import('@/components/emoji-picker-dialog.vue')), { + await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), { src: this.src, asReactionPicker: true, manualShowing: this.manualShowing diff --git a/packages/client/src/scripts/safe-uri-decode.ts b/packages/client/src/scripts/safe-uri-decode.ts new file mode 100644 index 0000000000..301b56d7fd --- /dev/null +++ b/packages/client/src/scripts/safe-uri-decode.ts @@ -0,0 +1,7 @@ +export function safeURIDecode(str: string): string { + try { + return decodeURIComponent(str); + } catch { + return str; + } +} diff --git a/packages/client/src/scripts/scroll.ts b/packages/client/src/scripts/scroll.ts index 0643bad2fb..f5bc6bf9ce 100644 --- a/packages/client/src/scripts/scroll.ts +++ b/packages/client/src/scripts/scroll.ts @@ -2,12 +2,8 @@ type ScrollBehavior = 'auto' | 'smooth' | 'instant'; export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { if (el == null || el.tagName === 'HTML') return null; - const overflow = window.getComputedStyle(el).getPropertyValue('overflow'); - if ( - // xとyを個別に指定している場合、`hidden scroll`みたいな値になる - overflow.endsWith('scroll') || - overflow.endsWith('auto') - ) { + const overflow = window.getComputedStyle(el).getPropertyValue('overflow-y'); + if (overflow === 'scroll' || overflow === 'auto') { return el; } else { return getScrollContainer(el.parentElement); diff --git a/packages/client/src/scripts/select-file.ts b/packages/client/src/scripts/select-file.ts index 461d613b42..17e31d96f1 100644 --- a/packages/client/src/scripts/select-file.ts +++ b/packages/client/src/scripts/select-file.ts @@ -1,9 +1,9 @@ import { ref } from 'vue'; +import { DriveFile } from 'misskey-js/built/entities'; import * as os from '@/os'; import { stream } from '@/stream'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; -import { DriveFile } from 'misskey-js/built/entities'; import { uploadFile } from '@/scripts/upload'; function select(src: any, label: string | null, multiple: boolean): Promise<DriveFile | DriveFile[]> { @@ -20,10 +20,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv Promise.all(promises).then(driveFiles => { res(multiple ? driveFiles : driveFiles[0]); }).catch(err => { - os.alert({ - type: 'error', - text: err - }); + // アップロードのエラーは uploadFile 内でハンドリングされているためアラートダイアログを出したりはしてはいけない }); // 一応廃棄 @@ -47,7 +44,7 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv os.inputText({ title: i18n.ts.uploadFromUrl, type: 'url', - placeholder: i18n.ts.uploadFromUrlDescription + placeholder: i18n.ts.uploadFromUrlDescription, }).then(({ canceled, result: url }) => { if (canceled) return; @@ -64,35 +61,35 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Driv os.api('drive/files/upload-from-url', { url: url, folderId: defaultStore.state.uploadFolder, - marker + marker, }); os.alert({ title: i18n.ts.uploadFromUrlRequested, - text: i18n.ts.uploadFromUrlMayTakeTime + text: i18n.ts.uploadFromUrlMayTakeTime, }); }); }; os.popupMenu([label ? { text: label, - type: 'label' + type: 'label', } : undefined, { type: 'switch', text: i18n.ts.keepOriginalUploading, - ref: keepOriginal + ref: keepOriginal, }, { text: i18n.ts.upload, icon: 'fas fa-upload', - action: chooseFileFromPc + action: chooseFileFromPc, }, { text: i18n.ts.fromDrive, icon: 'fas fa-cloud', - action: chooseFileFromDrive + action: chooseFileFromDrive, }, { text: i18n.ts.fromUrl, icon: 'fas fa-link', - action: chooseFileFromUrl + action: chooseFileFromUrl, }], src); }); } diff --git a/packages/client/src/scripts/shuffle.ts b/packages/client/src/scripts/shuffle.ts new file mode 100644 index 0000000000..05e6cdfbcf --- /dev/null +++ b/packages/client/src/scripts/shuffle.ts @@ -0,0 +1,19 @@ +/** + * 配列をシャッフル (破壊的) + */ +export function shuffle<T extends any[]>(array: T): T { + let currentIndex = array.length, randomIndex; + + // While there remain elements to shuffle. + while (currentIndex !== 0) { + // Pick a remaining element. + randomIndex = Math.floor(Math.random() * currentIndex); + currentIndex--; + + // And swap it with the current element. + [array[currentIndex], array[randomIndex]] = [ + array[randomIndex], array[currentIndex]]; + } + + return array; +} diff --git a/packages/client/src/scripts/theme.ts b/packages/client/src/scripts/theme.ts index dec9fb355c..62a2b9459a 100644 --- a/packages/client/src/scripts/theme.ts +++ b/packages/client/src/scripts/theme.ts @@ -1,6 +1,6 @@ import { ref } from 'vue'; -import { globalEvents } from '@/events'; import tinycolor from 'tinycolor2'; +import { globalEvents } from '@/events'; export type Theme = { id: string; @@ -13,6 +13,7 @@ export type Theme = { import lightTheme from '@/themes/_light.json5'; import darkTheme from '@/themes/_dark.json5'; +import { deepClone } from './clone'; export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); @@ -25,17 +26,19 @@ export const getBuiltinThemes = () => Promise.all( 'l-vivid', 'l-cherry', 'l-sushi', + 'l-u0', 'd-dark', 'd-persimmon', 'd-astro', 'd-future', 'd-botanical', + 'd-green-lime', + 'd-green-orange', 'd-cherry', 'd-ice', - 'd-pumpkin', - 'd-black', - ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)) + 'd-u0', + ].map(name => import(`../themes/${name}.json5`).then(({ default: _default }): Theme => _default)), ); export const getBuiltinThemesRef = () => { @@ -55,8 +58,10 @@ export function applyTheme(theme: Theme, persist = true) { document.documentElement.classList.remove('_themeChanging_'); }, 1000); + const colorSchema = theme.base === 'dark' ? 'dark' : 'light'; + // Deep copy - const _theme = JSON.parse(JSON.stringify(theme)); + const _theme = deepClone(theme); if (_theme.base) { const base = [lightTheme, darkTheme].find(x => x.id === _theme.base); @@ -76,8 +81,11 @@ export function applyTheme(theme: Theme, persist = true) { document.documentElement.style.setProperty(`--${k}`, v.toString()); } + document.documentElement.style.setProperty('color-schema', colorSchema); + if (persist) { localStorage.setItem('theme', JSON.stringify(props)); + localStorage.setItem('colorSchema', colorSchema); } // 色計算など再度行えるようにクライアント全体に通知 diff --git a/packages/client/src/scripts/timezones.ts b/packages/client/src/scripts/timezones.ts new file mode 100644 index 0000000000..8ce07323f6 --- /dev/null +++ b/packages/client/src/scripts/timezones.ts @@ -0,0 +1,49 @@ +export const timezones = [{ + name: 'UTC', + abbrev: 'UTC', + offset: 0, +}, { + name: 'Europe/Berlin', + abbrev: 'CET', + offset: 60, +}, { + name: 'Asia/Tokyo', + abbrev: 'JST', + offset: 540, +}, { + name: 'Asia/Seoul', + abbrev: 'KST', + offset: 540, +}, { + name: 'Asia/Shanghai', + abbrev: 'CST', + offset: 480, +}, { + name: 'Australia/Sydney', + abbrev: 'AEST', + offset: 600, +}, { + name: 'Australia/Darwin', + abbrev: 'ACST', + offset: 570, +}, { + name: 'Australia/Perth', + abbrev: 'AWST', + offset: 480, +}, { + name: 'America/New_York', + abbrev: 'EST', + offset: -300, +}, { + name: 'America/Mexico_City', + abbrev: 'CST', + offset: -360, +}, { + name: 'America/Phoenix', + abbrev: 'MST', + offset: -420, +}, { + name: 'America/Los_Angeles', + abbrev: 'PST', + offset: -480, +}]; diff --git a/packages/client/src/scripts/upload.ts b/packages/client/src/scripts/upload.ts index 2f7b30b58d..51f1c1b86f 100644 --- a/packages/client/src/scripts/upload.ts +++ b/packages/client/src/scripts/upload.ts @@ -1,10 +1,11 @@ import { reactive, ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import { readAndCompressImage } from 'browser-image-resizer'; import { defaultStore } from '@/store'; import { apiUrl } from '@/config'; -import * as Misskey from 'misskey-js'; import { $i } from '@/account'; -import { readAndCompressImage } from 'browser-image-resizer'; import { alert } from '@/os'; +import { i18n } from '@/i18n'; type Uploading = { id: string; @@ -31,7 +32,7 @@ export function uploadFile( file: File, folder?: any, name?: string, - keepOriginal: boolean = defaultStore.state.keepOriginalUploading + keepOriginal: boolean = defaultStore.state.keepOriginalUploading, ): Promise<Misskey.entities.DriveFile> { if (folder && typeof folder === 'object') folder = folder.id; @@ -45,7 +46,7 @@ export function uploadFile( name: name || file.name || 'untitled', progressMax: undefined, progressValue: undefined, - img: window.URL.createObjectURL(file) + img: window.URL.createObjectURL(file), }); uploads.value.push(ctx); @@ -80,14 +81,37 @@ export function uploadFile( xhr.open('POST', apiUrl + '/drive/files/create', true); xhr.onload = (ev) => { if (xhr.status !== 200 || ev.target == null || ev.target.response == null) { - // TODO: 消すのではなくて再送できるようにしたい + // TODO: 消すのではなくて(ネットワーク的なエラーなら)再送できるようにしたい uploads.value = uploads.value.filter(x => x.id !== id); - alert({ - type: 'error', - title: 'Failed to upload', - text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}` - }); + if (ev.target?.response) { + const res = JSON.parse(ev.target.response); + if (res.error?.id === 'bec5bd69-fba3-43c9-b4fb-2894b66ad5d2') { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseInappropriate, + }); + } else if (res.error?.id === 'd08dbc37-a6a9-463a-8c47-96c32ab5f064') { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: i18n.ts.cannotUploadBecauseNoFreeSpace, + }); + } else { + alert({ + type: 'error', + title: i18n.ts.failedToUpload, + text: `${res.error?.message}\n${res.error?.code}\n${res.error?.id}`, + }); + } + } else { + alert({ + type: 'error', + title: 'Failed to upload', + text: `${JSON.stringify(ev.target?.response)}, ${JSON.stringify(xhr.response)}`, + }); + } reject(); return; diff --git a/packages/client/src/scripts/url.ts b/packages/client/src/scripts/url.ts index 542b00e0f0..86735de9f0 100644 --- a/packages/client/src/scripts/url.ts +++ b/packages/client/src/scripts/url.ts @@ -1,4 +1,4 @@ -export function query(obj: {}): string { +export function query(obj: Record<string, any>): string { const params = Object.entries(obj) .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) .reduce((a, [k, v]) => (a[k] = v, a), {} as Record<string, any>); diff --git a/packages/client/src/scripts/use-chart-tooltip.ts b/packages/client/src/scripts/use-chart-tooltip.ts new file mode 100644 index 0000000000..91c27585f3 --- /dev/null +++ b/packages/client/src/scripts/use-chart-tooltip.ts @@ -0,0 +1,50 @@ +import { onUnmounted, ref } from 'vue'; +import * as os from '@/os'; +import MkChartTooltip from '@/components/MkChartTooltip.vue'; + +export function useChartTooltip() { + const tooltipShowing = ref(false); + const tooltipX = ref(0); + const tooltipY = ref(0); + const tooltipTitle = ref(null); + const tooltipSeries = ref(null); + let disposeTooltipComponent; + + os.popup(MkChartTooltip, { + showing: tooltipShowing, + x: tooltipX, + y: tooltipY, + title: tooltipTitle, + series: tooltipSeries, + }, {}).then(({ dispose }) => { + disposeTooltipComponent = dispose; + }); + + onUnmounted(() => { + if (disposeTooltipComponent) disposeTooltipComponent(); + }); + + function handler(context) { + if (context.tooltip.opacity === 0) { + tooltipShowing.value = false; + return; + } + + tooltipTitle.value = context.tooltip.title[0]; + tooltipSeries.value = context.tooltip.body.map((b, i) => ({ + backgroundColor: context.tooltip.labelColors[i].backgroundColor, + borderColor: context.tooltip.labelColors[i].borderColor, + text: b.lines[0], + })); + + const rect = context.chart.canvas.getBoundingClientRect(); + + tooltipShowing.value = true; + tooltipX.value = rect.left + window.pageXOffset + context.tooltip.caretX; + tooltipY.value = rect.top + window.pageYOffset + context.tooltip.caretY; + } + + return { + handler, + }; +} diff --git a/packages/client/src/scripts/use-interval.ts b/packages/client/src/scripts/use-interval.ts new file mode 100644 index 0000000000..201ba417ef --- /dev/null +++ b/packages/client/src/scripts/use-interval.ts @@ -0,0 +1,24 @@ +import { onMounted, onUnmounted } from 'vue'; + +export function useInterval(fn: () => void, interval: number, options: { + immediate: boolean; + afterMounted: boolean; +}): void { + if (Number.isNaN(interval)) return; + + let intervalId: number | null = null; + + if (options.afterMounted) { + onMounted(() => { + if (options.immediate) fn(); + intervalId = window.setInterval(fn, interval); + }); + } else { + if (options.immediate) fn(); + intervalId = window.setInterval(fn, interval); + } + + onUnmounted(() => { + if (intervalId) window.clearInterval(intervalId); + }); +} diff --git a/packages/client/src/scripts/use-leave-guard.ts b/packages/client/src/scripts/use-leave-guard.ts index 379a7e577c..a93b84d1fe 100644 --- a/packages/client/src/scripts/use-leave-guard.ts +++ b/packages/client/src/scripts/use-leave-guard.ts @@ -3,6 +3,7 @@ import { i18n } from '@/i18n'; import * as os from '@/os'; export function useLeaveGuard(enabled: Ref<boolean>) { + /* TODO const setLeaveGuard = inject('setLeaveGuard'); if (setLeaveGuard) { @@ -28,6 +29,7 @@ export function useLeaveGuard(enabled: Ref<boolean>) { return !canceled; }); } + */ /* function onBeforeLeave(ev: BeforeUnloadEvent) { diff --git a/packages/client/src/scripts/use-tooltip.ts b/packages/client/src/scripts/use-tooltip.ts index bc8f27a038..1f6e0fb6ce 100644 --- a/packages/client/src/scripts/use-tooltip.ts +++ b/packages/client/src/scripts/use-tooltip.ts @@ -3,6 +3,7 @@ import { Ref, ref, watch, onUnmounted } from 'vue'; export function useTooltip( elRef: Ref<HTMLElement | { $el: HTMLElement } | null | undefined>, onShow: (showing: Ref<boolean>) => void, + delay = 300, ): void { let isHovering = false; @@ -40,7 +41,7 @@ export function useTooltip( if (isHovering) return; if (shouldIgnoreMouseover) return; isHovering = true; - timeoutId = window.setTimeout(open, 300); + timeoutId = window.setTimeout(open, delay); }; const onMouseleave = () => { @@ -54,7 +55,7 @@ export function useTooltip( shouldIgnoreMouseover = true; if (isHovering) return; isHovering = true; - timeoutId = window.setTimeout(open, 300); + timeoutId = window.setTimeout(open, delay); }; const onTouchend = () => { |