diff options
| author | syuilo <Syuilotan@yahoo.co.jp> | 2022-07-07 21:23:03 +0900 |
|---|---|---|
| committer | syuilo <Syuilotan@yahoo.co.jp> | 2022-07-07 21:23:03 +0900 |
| commit | 84d984bd31fa9863c3fe2e1aeb672ad0e2e8de4b (patch) | |
| tree | a182502a5192992d873e7a7fcbf01662bb0dfca2 /packages/client/src/scripts | |
| parent | Merge pull request #8821 from misskey-dev/develop (diff) | |
| parent | 12.112.1 (diff) | |
| download | misskey-84d984bd31fa9863c3fe2e1aeb672ad0e2e8de4b.tar.gz misskey-84d984bd31fa9863c3fe2e1aeb672ad0e2e8de4b.tar.bz2 misskey-84d984bd31fa9863c3fe2e1aeb672ad0e2e8de4b.zip | |
Merge branch 'develop'
Diffstat (limited to 'packages/client/src/scripts')
24 files changed, 438 insertions, 279 deletions
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..3ef6224175 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; 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/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 78749ad6bb..632143f514 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 } from 'vue'; +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,13 +8,14 @@ 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'; export function getNoteMenu(props: { note: misskey.entities.Note; menuButton: Ref<HTMLElement>; translation: Ref<any>; translating: Ref<boolean>; + isDeleted: Ref<boolean>; + currentClipPage?: Ref<misskey.entities.Clip>; }) { const isRenote = ( props.note.renote != null && @@ -32,7 +34,7 @@ export function getNoteMenu(props: { if (canceled) return; os.api('notes/delete', { - noteId: appearNote.id + noteId: appearNote.id, }); }); } @@ -45,7 +47,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 }); @@ -54,19 +56,19 @@ export function getNoteMenu(props: { function toggleFavorite(favorite: boolean): void { os.apiWithDialog(favorite ? 'notes/favorites/create' : 'notes/favorites/delete', { - noteId: appearNote.id + 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, }); } @@ -82,12 +84,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, }); } }); @@ -102,35 +104,60 @@ 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: () => { - os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); - } + os.promiseDialog( + os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), + null, + async (err) => { + if (err.id === '734806c4-542c-463a-9311-15c512803965') { + const confirm = await os.confirm({ + type: 'warning', + text: i18n.t('confirmToUnclipAlreadyClippedNote', { name: clip.name }), + }); + if (!confirm.canceled) { + os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); + if (props.currentClipPage?.value.id === clip.id) props.isDeleted.value = true; + } + } else { + os.alert({ + type: 'error', + text: err.message + '\n' + err.id, + }); + } + }, + ); + }, }))], props.menuButton.value, { }).then(focus); } + async function unclip(): Promise<void> { + os.apiWithDialog('clips/remove-note', { clipId: props.currentClipPage.value.id, noteId: appearNote.id }); + props.isDeleted.value = true; + } + async function promote(): Promise<void> { const { canceled, result: days } = await os.inputNumber({ title: i18n.ts.numberOfDays, @@ -166,77 +193,86 @@ export function getNoteMenu(props: { let menu; if ($i) { const statePromise = os.api('notes/state', { - noteId: appearNote.id + noteId: appearNote.id, }); - menu = [{ - 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, - /* + 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, + /* ...($i.isModerator || $i.isAdmin ? [ null, { @@ -246,52 +282,52 @@ 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/abuse-report-window.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); } else { menu = [{ 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); } @@ -302,7 +338,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 091338efd6..25bcd90e9f 100644 --- a/packages/client/src/scripts/get-user-menu.ts +++ b/packages/client/src/scripts/get-user-menu.ts @@ -1,12 +1,12 @@ +import * as Acct from 'misskey-js/built/acct'; +import { defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n'; import copyToClipboard from '@/scripts/copy-to-clipboard'; import { host } from '@/config'; -import * as Acct from 'misskey-js/built/acct'; import * as os from '@/os'; import { userActions } from '@/store'; -import { router } from '@/router'; import { $i, iAmModerator } from '@/account'; -import { defineAsyncComponent } from 'vue'; +import { mainRouter } from '@/router'; export function getUserMenu(user) { const meId = $i ? $i.id : null; @@ -17,20 +17,20 @@ export function getUserMenu(user) { if (lists.length === 0) { os.alert({ type: 'error', - text: i18n.ts.youHaveNoLists + text: i18n.ts.youHaveNoLists, }); return; } const { canceled, result: listId } = await os.select({ title: t, items: lists.map(list => ({ - value: list.id, text: list.name - })) + value: list.id, text: list.name, + })), }); if (canceled) return; os.apiWithDialog('users/lists/push', { listId: listId, - userId: user.id + userId: user.id, }); } @@ -39,20 +39,20 @@ export function getUserMenu(user) { if (groups.length === 0) { os.alert({ type: 'error', - text: i18n.ts.youHaveNoGroups + text: i18n.ts.youHaveNoGroups, }); return; } const { canceled, result: groupId } = await os.select({ title: i18n.ts.group, items: groups.map(group => ({ - value: group.id, text: group.name - })) + value: group.id, text: group.name, + })), }); if (canceled) return; os.apiWithDialog('users/groups/invite', { groupId: groupId, - userId: user.id + userId: user.id, }); } @@ -101,7 +101,7 @@ export function getUserMenu(user) { if (!await getConfirmed(user.isBlocking ? i18n.ts.unblockConfirm : i18n.ts.blockConfirm)) return; os.apiWithDialog(user.isBlocking ? 'blocking/delete' : 'blocking/create', { - userId: user.id + userId: user.id, }).then(() => { user.isBlocking = !user.isBlocking; }); @@ -111,7 +111,7 @@ export function getUserMenu(user) { if (!await getConfirmed(i18n.t(user.isSilenced ? 'unsilenceConfirm' : 'silenceConfirm'))) return; os.apiWithDialog(user.isSilenced ? 'admin/unsilence-user' : 'admin/silence-user', { - userId: user.id + userId: user.id, }).then(() => { user.isSilenced = !user.isSilenced; }); @@ -121,7 +121,7 @@ export function getUserMenu(user) { if (!await getConfirmed(i18n.t(user.isSuspended ? 'unsuspendConfirm' : 'suspendConfirm'))) return; os.apiWithDialog(user.isSuspended ? 'admin/unsuspend-user' : 'admin/suspend-user', { - userId: user.id + userId: user.id, }).then(() => { user.isSuspended = !user.isSuspended; }); @@ -145,7 +145,7 @@ export function getUserMenu(user) { async function invalidateFollow() { os.apiWithDialog('following/invalidate', { - userId: user.id + userId: user.id, }).then(() => { user.isFollowed = !user.isFollowed; }); @@ -156,19 +156,19 @@ export function getUserMenu(user) { text: i18n.ts.copyUsername, action: () => { copyToClipboard(`@${user.username}@${user.host || host}`); - } + }, }, { icon: 'fas fa-info-circle', text: i18n.ts.info, action: () => { os.pageWindow(`/user-info/${user.id}`); - } + }, }, { icon: 'fas fa-envelope', text: i18n.ts.sendMessage, action: () => { os.post({ specified: user }); - } + }, }, meId !== user.id ? { type: 'link', icon: 'fas fa-comments', @@ -177,47 +177,47 @@ export function getUserMenu(user) { } : undefined, null, { icon: 'fas fa-list-ul', text: i18n.ts.addToList, - action: pushList + action: pushList, }, meId !== user.id ? { icon: 'fas fa-users', text: i18n.ts.inviteToGroup, - action: inviteGroup + action: inviteGroup, } : undefined] as any; if ($i && meId !== user.id) { menu = menu.concat([null, { icon: user.isMuted ? 'fas fa-eye' : 'fas fa-eye-slash', text: user.isMuted ? i18n.ts.unmute : i18n.ts.mute, - action: toggleMute + action: toggleMute, }, { icon: 'fas fa-ban', text: user.isBlocking ? i18n.ts.unblock : i18n.ts.block, - action: toggleBlock + action: toggleBlock, }]); if (user.isFollowed) { menu = menu.concat([{ icon: 'fas fa-unlink', text: i18n.ts.breakFollow, - action: invalidateFollow + action: invalidateFollow, }]); } menu = menu.concat([null, { icon: 'fas fa-exclamation-circle', text: i18n.ts.reportAbuse, - action: reportAbuse + action: reportAbuse, }]); if (iAmModerator) { menu = menu.concat([null, { icon: 'fas fa-microphone-slash', text: user.isSilenced ? i18n.ts.unsilence : i18n.ts.silence, - action: toggleSilence + action: toggleSilence, }, { icon: 'fas fa-snowflake', text: user.isSuspended ? i18n.ts.unsuspend : i18n.ts.suspend, - action: toggleSuspend + action: toggleSuspend, }]); } } @@ -227,8 +227,8 @@ export function getUserMenu(user) { icon: 'fas fa-pencil-alt', text: i18n.ts.editProfile, action: () => { - router.push('/settings/profile'); - } + mainRouter.push('/settings/profile'); + }, }]); } @@ -238,7 +238,7 @@ export function getUserMenu(user) { text: action.title, action: () => { action.handler(user); - } + }, }))]); } 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/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/i18n.ts b/packages/client/src/scripts/i18n.ts index 3fe88e5514..54184386da 100644 --- a/packages/client/src/scripts/i18n.ts +++ b/packages/client/src/scripts/i18n.ts @@ -11,13 +11,13 @@ export class I18n<T extends Record<string, any>> { // string にしているのは、ドット区切りでのパス指定を許可するため // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - public t(key: string, args?: Record<string, string>): string { + public t(key: string, args?: Record<string, string | number>): string { try { let str = key.split('.').reduce((o, i) => o[i], this.ts) as unknown as string; if (args) { for (const [k, v] of Object.entries(args)) { - str = str.replace(`{${k}}`, v); + str = str.replace(`{${k}}`, v.toString()); } } return str; diff --git a/packages/client/src/scripts/navigate.ts b/packages/client/src/scripts/navigate.ts deleted file mode 100644 index 08b891ec5b..0000000000 --- a/packages/client/src/scripts/navigate.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { inject } from 'vue'; -import { router } from '@/router'; -import { defaultStore } from '@/store'; - -export type Navigate = (path: string, record?: boolean) => void; - -export class MisskeyNavigator { - public readonly navHook: Navigate | null = null; - public readonly sideViewHook: Navigate | null = null; - - // It should be constructed during vue creating in order for inject function to work - constructor() { - this.navHook = inject<Navigate | null>('navHook', null); - this.sideViewHook = inject<Navigate | null>('sideViewHook', null); - } - - // Use this method instead of router.push() - public push(path: string, record = true) { - if (this.navHook) { - this.navHook(path, record); - } else { - if (defaultStore.state.defaultSideView && this.sideViewHook && path !== '/') { - return this.sideViewHook(path, record); - } - - if (router.currentRoute.value.path === path) { - window.scroll({ top: 0, behavior: 'smooth' }); - } else { - if (record) router.push(path); - else router.replace(path); - } - } - } -} diff --git a/packages/client/src/scripts/page-metadata.ts b/packages/client/src/scripts/page-metadata.ts new file mode 100644 index 0000000000..0db8369f9d --- /dev/null +++ b/packages/client/src/scripts/page-metadata.ts @@ -0,0 +1,41 @@ +import * as misskey from 'misskey-js'; +import { ComputedRef, inject, isRef, onActivated, onMounted, provide, ref, Ref } from 'vue'; + +export const setPageMetadata = Symbol('setPageMetadata'); +export const pageMetadataProvider = Symbol('pageMetadataProvider'); + +export type PageMetadata = { + title: string; + subtitle?: string; + icon?: string | null; + avatar?: misskey.entities.User | null; + userName?: misskey.entities.User | null; + bg?: string; +}; + +export function definePageMetadata(metadata: PageMetadata | null | Ref<PageMetadata | null> | ComputedRef<PageMetadata | null>): void { + const _metadata = isRef(metadata) ? metadata : ref(metadata); + + provide(pageMetadataProvider, _metadata); + + const set = inject(setPageMetadata) as any; + if (set) { + set(_metadata); + + onMounted(() => { + set(_metadata); + }); + + onActivated(() => { + set(_metadata); + }); + } +} + +export function provideMetadataReceiver(callback: (info: ComputedRef<PageMetadata>) => void): void { + provide(setPageMetadata, callback); +} + +export function injectPageMetadata(): PageMetadata | undefined { + return inject(pageMetadataProvider); +} diff --git a/packages/client/src/scripts/please-login.ts b/packages/client/src/scripts/please-login.ts index e21a6d2ed3..1f38061841 100644 --- a/packages/client/src/scripts/please-login.ts +++ b/packages/client/src/scripts/please-login.ts @@ -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/scroll.ts b/packages/client/src/scripts/scroll.ts index 621fe88105..f5bc6bf9ce 100644 --- a/packages/client/src/scripts/scroll.ts +++ b/packages/client/src/scripts/scroll.ts @@ -1,9 +1,9 @@ type ScrollBehavior = 'auto' | 'smooth' | 'instant'; -export function getScrollContainer(el: Element | null): Element | null { - if (el == null || el.tagName === 'BODY') return null; - const overflow = window.getComputedStyle(el).getPropertyValue('overflow'); - if (overflow.endsWith('auto')) { // xとyを個別に指定している場合、hidden auto みたいな値になる +export function getScrollContainer(el: HTMLElement | null): HTMLElement | null { + if (el == null || el.tagName === 'HTML') return null; + const overflow = window.getComputedStyle(el).getPropertyValue('overflow-y'); + if (overflow === 'scroll' || overflow === 'auto') { return el; } else { return getScrollContainer(el.parentElement); @@ -22,6 +22,11 @@ export function isTopVisible(el: Element | null): boolean { return scrollTop <= topPosition; } +export function isBottomVisible(el: HTMLElement, tolerance = 1, container = getScrollContainer(el)) { + if (container) return el.scrollHeight <= container.clientHeight + Math.abs(container.scrollTop) + tolerance; + return el.scrollHeight <= window.innerHeight + window.scrollY + tolerance; +} + export function onScrollTop(el: Element, cb) { const container = getScrollContainer(el) || window; const onScroll = ev => { diff --git a/packages/client/src/scripts/search.ts b/packages/client/src/scripts/search.ts index 0aedee9c98..64914d3d65 100644 --- a/packages/client/src/scripts/search.ts +++ b/packages/client/src/scripts/search.ts @@ -1,6 +1,6 @@ import * as os from '@/os'; import { i18n } from '@/i18n'; -import { router } from '@/router'; +import { mainRouter } from '@/router'; export async function search() { const { canceled, result: query } = await os.inputText({ @@ -11,12 +11,12 @@ export async function search() { const q = query.trim(); if (q.startsWith('@') && !q.includes(' ')) { - router.push(`/${q}`); + mainRouter.push(`/${q}`); return; } if (q.startsWith('#')) { - router.push(`/tags/${encodeURIComponent(q.substr(1))}`); + mainRouter.push(`/tags/${encodeURIComponent(q.substr(1))}`); return; } @@ -36,14 +36,14 @@ export async function search() { //v.$root.$emit('warp', date); os.alert({ icon: 'fas fa-history', - iconOnly: true, autoClose: true + iconOnly: true, autoClose: true, }); return; } if (q.startsWith('https://')) { const promise = os.api('ap/show', { - uri: q + uri: q, }); os.promiseDialog(promise, null, null, i18n.ts.fetchingAsApObject); @@ -51,13 +51,13 @@ export async function search() { const res = await promise; if (res.type === 'User') { - router.push(`/@${res.object.username}@${res.object.host}`); + mainRouter.push(`/@${res.object.username}@${res.object.host}`); } else if (res.type === 'Note') { - router.push(`/notes/${res.object.id}`); + mainRouter.push(`/notes/${res.object.id}`); } return; } - router.push(`/search?q=${encodeURIComponent(q)}`); + mainRouter.push(`/search?q=${encodeURIComponent(q)}`); } 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/twemoji-base.ts b/packages/client/src/scripts/twemoji-base.ts index cd50311b15..638aae3284 100644 --- a/packages/client/src/scripts/twemoji-base.ts +++ b/packages/client/src/scripts/twemoji-base.ts @@ -1 +1,12 @@ export const twemojiSvgBase = '/twemoji'; + +export function char2fileName(char: string): string { + let codes = Array.from(char).map(x => x.codePointAt(0)?.toString(16)); + if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); + codes = codes.filter(x => x && x.length); + return codes.join('-'); +} + +export function char2filePath(char: string): string { + return `${twemojiSvgBase}/${char2fileName(char)}.svg`; +} 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..ab57165694 --- /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/chart-tooltip.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 33eea6b522..a93b84d1fe 100644 --- a/packages/client/src/scripts/use-leave-guard.ts +++ b/packages/client/src/scripts/use-leave-guard.ts @@ -1,9 +1,9 @@ import { inject, onUnmounted, Ref } from 'vue'; -import { onBeforeRouteLeave } from 'vue-router'; import { i18n } from '@/i18n'; import * as os from '@/os'; export function useLeaveGuard(enabled: Ref<boolean>) { + /* TODO const setLeaveGuard = inject('setLeaveGuard'); if (setLeaveGuard) { @@ -29,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 = () => { |