From 2cbe1d1210a5745787f37069ecb59b8f6c03c224 Mon Sep 17 00:00:00 2001 From: かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Mon, 9 Sep 2024 20:57:36 +0900 Subject: feat(frontend): ノート・ユーザータイムライン埋め込み (#13929) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix * navhookをbootに移動 * サーバーサイドのbootも分けるように * 埋め込みページかどうかの判定は最初の一回だけに * tooltipは出せるように * fix design * 埋め込み独自のtooltipを削除 * ロジックの分岐が多かったMkNoteDetailedを分離 * fix indent * プレビュー用iframeにフォーカスが当たるのを修正 * popupの制御を出す側で行うように * パラメータが逆になっていたのを修正 * Update MkEmbedCodeGenDialog.vue * fix * eliminate misskey-js lint warns * fix * add appropriate attributes to embed html * enhance: サーバーサイドのembed系をさらに分離 * enhance: embed routerを分離(route定義をboot時に変更できるようにする改修を含む) * type * lint * fix indent * server-side styleを完全に分離 * Revert "refactor: 画面サイズのしきい値をconstにまとめる" This reverts commit 05ca36f400889456981e89489ae0ae242fa09b67. * fix * revert all changes in base.pug * embedドメインをまとめた * embedドメインをまとめた * prevent calling contextmenu in embed page by stopping at the caller * fix import * fix import * improve directory structure * fix import * register timeline ui as a container * wa- * rename * wa- * Update EmMediaList.vue * Update EmMediaList.vue * Update EmMediaList.vue * Update EmMediaImage.vue * Update EmNote.vue * revert mkmedialist changes * 戻し漏れ * wip * tweak embed media ui * revert original media components * Update boot.embed.js * rename * wip * Update MkNote.vue * wip * Update MkSubNoteContent.vue * Update EmNote.vue * Update packages/frontend/src/router/definition.ts * Revert "Update packages/frontend/src/router/definition.ts" This reverts commit 937ae44521cdb0f250796943b20142b65f8ed944. * refactor EmMediaImage * fix import * remove unused imports * Update router.ts * wip * Update boot.ts * wip * wip * wip * wip * Update EmNote.vue * Update EmNote.vue * Create EmA.vue * Create EmAvatar.vue * Update EmAvatar.vue * wip * wip * wip * Create EmImgWithBlurhash.vue * Update EmImgWithBlurhash.vue * Create EmPagination.vue * wip * Update boot.ts * wip * wip * wi@p * wip * wip * wiop * wip * wip * wip * wip * wip * wip * wip * wip * wip * wip * Update boot.ts * wip * Update MkMisskeyFlavoredMarkdown.ts * wip * wip * wip * wip * wip * Update post-message.ts * wip * Update EmNoteDetailed.vue * Update EmNoteDetailed.vue * Create instance.ts * Update EmNoteDetailed.vue * wip * Update EmNoteDetailed.vue * wip * wip * wip * Update pnpm-lock.yaml * wip * wip * wp * wip * Update ClientServerService.ts * wip * Update boot.ts * Update vite.config.local-dev.ts * Update vite.config.ts * Create index.html * wa- * wip * Update boot.ts * wip * wip * wip * wip * wip * wip * wip * wip * wip * Create EmLink.vue * Create EmMention.vue * Update EmMfm.ts * wip * wip * wip * wip * Update vite.config.ts * Update boot.ts * Update EmA.vue * うぃp * wip * wip * Create EmError.vue * wip * Update MkEmbedCodeGenDialog.vue * Update EmNote.vue * wip * wip * Update user-timeline.vue * Update check-spdx-license-id.yml * wip * wip * style(frontend-shared): lint fixes on build.js * fix(frontend-shared): include `*.{js,json}` files in js-built * wip * use alias * refactor * refactor * Update scroll.ts * refactor * refactor * refactor * wip * wip * wip * wip * Update roles.vue * Update branding.vue * wip * wip * wip * Update page.vue * wip * fix import * add missing css variables * 絵文字をtwemojiに変更 クライアントデフォルトにあわせるため * force empoll readonly * fix compiler error * fix broken imports * tweak button style * run api extractor * fix storybook theme preloads * fix storybook instance imports * Update preview.ts * Update preview.ts * Update preview.ts * Revert "Update preview.ts" This reverts commit 12bab1c6fbd3baf753515df760ff19d027b85155. * Revert "Update preview.ts" This reverts commit 5c0ce01dbdf2194ffe94aba950f747a9968f29c4. * Revert "Update preview.ts" This reverts commit f4863524d7e5ca0f25470808849c24a72bea000a. * Revert "fix storybook instance imports" This reverts commit ed8eabb246edf731d31adffbe3c77c539e53ae9e. * Revert "wip" This reverts commit d3c1926519878155193a1654f49141e515d49683. * Revert "Update page.vue" This reverts commit 27c7900b0c1ae296b56075e8a9c22585d9cd744b. * Revert "Update branding.vue" This reverts commit c08ccb65ba66774c3e2b3dcfc6153004b5c0aa16. * Revert "Update roles.vue" This reverts commit 1488b670660cb1803d17d8f5c78f2d79e59fa52d. * Revert "wip" This reverts commit aab1c769814b08c257cad3025422a0eea3bfba4f. * refactor: use common media proxy * fix imports * fix * fix: MediaProxyの初期化を保証する(storybook対策?) * enhance(frontend-embed): improve embedParams provide * fix(backend): MK_DEV_PREFER=backendのときにembed viteが読み込めないのを修正 * fix * embed-pageを共通化 * fix import * fix import * fix import * const.jsを共通化 (たぶんrevertしすぎた) * fix type error * fix duplicated import * fix lint * fix * コメントとして残す * sharedとembedをlint対象にする * lint * attempt to fix eslint (frontend-shared) * lint fixes --------- Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> Co-authored-by: zyoshoka <107108195+zyoshoka@users.noreply.github.com> --- packages/frontend/src/scripts/aiscript/api.ts | 4 +- .../src/scripts/check-reaction-permissions.ts | 2 +- packages/frontend/src/scripts/code-highlighter.ts | 4 +- packages/frontend/src/scripts/emoji-base.ts | 25 --- packages/frontend/src/scripts/emojilist.ts | 73 ------ .../src/scripts/extract-avg-color-from-blurhash.ts | 14 -- packages/frontend/src/scripts/focus.ts | 2 +- packages/frontend/src/scripts/get-embed-code.ts | 87 ++++++++ packages/frontend/src/scripts/get-note-menu.ts | 22 +- packages/frontend/src/scripts/get-user-menu.ts | 13 +- packages/frontend/src/scripts/i18n.ts | 245 --------------------- packages/frontend/src/scripts/idb-proxy.ts | 11 +- packages/frontend/src/scripts/is-link.ts | 12 + packages/frontend/src/scripts/media-proxy.ts | 51 ++--- .../frontend/src/scripts/mfm-function-picker.ts | 2 +- packages/frontend/src/scripts/nyaize.ts | 27 --- packages/frontend/src/scripts/popout.ts | 2 +- packages/frontend/src/scripts/post-message.ts | 2 +- packages/frontend/src/scripts/safe-parse.ts | 11 - packages/frontend/src/scripts/safe-uri-decode.ts | 12 - packages/frontend/src/scripts/scroll.ts | 144 ------------ packages/frontend/src/scripts/stream-mock.ts | 81 +++++++ packages/frontend/src/scripts/theme.ts | 4 +- packages/frontend/src/scripts/url.ts | 28 --- .../src/scripts/use-document-visibility.ts | 24 -- packages/frontend/src/scripts/use-interval.ts | 46 ---- 26 files changed, 243 insertions(+), 705 deletions(-) delete mode 100644 packages/frontend/src/scripts/emoji-base.ts delete mode 100644 packages/frontend/src/scripts/emojilist.ts delete mode 100644 packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts create mode 100644 packages/frontend/src/scripts/get-embed-code.ts delete mode 100644 packages/frontend/src/scripts/i18n.ts create mode 100644 packages/frontend/src/scripts/is-link.ts delete mode 100644 packages/frontend/src/scripts/nyaize.ts delete mode 100644 packages/frontend/src/scripts/safe-parse.ts delete mode 100644 packages/frontend/src/scripts/safe-uri-decode.ts delete mode 100644 packages/frontend/src/scripts/scroll.ts create mode 100644 packages/frontend/src/scripts/stream-mock.ts delete mode 100644 packages/frontend/src/scripts/url.ts delete mode 100644 packages/frontend/src/scripts/use-document-visibility.ts delete mode 100644 packages/frontend/src/scripts/use-interval.ts (limited to 'packages/frontend/src/scripts') diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 98a0c61752..417ba08c3f 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -4,13 +4,13 @@ */ import { utils, values } from '@syuilo/aiscript'; +import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; import { miLocalStorage } from '@/local-storage.js'; import { customEmojis } from '@/custom-emojis.js'; import { url, lang } from '@/config.js'; -import { nyaize } from '@/scripts/nyaize.js'; export function aiScriptReadline(q: string): Promise { return new Promise(ok => { @@ -87,7 +87,7 @@ export function createAiScriptEnv(opts) { }), 'Mk:nyaize': values.FN_NATIVE(([text]) => { utils.assertString(text); - return values.STR(nyaize(text.value)); + return values.STR(Misskey.nyaize(text.value)); }), }; } diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts index 8fc857f84f..c3c3f419a9 100644 --- a/packages/frontend/src/scripts/check-reaction-permissions.ts +++ b/packages/frontend/src/scripts/check-reaction-permissions.ts @@ -4,7 +4,7 @@ */ import * as Misskey from 'misskey-js'; -import { UnicodeEmojiDef } from './emojilist.js'; +import { UnicodeEmojiDef } from '@@/js/emojilist.js'; export function checkReactionPermissions(me: Misskey.entities.MeDetailed, note: Misskey.entities.Note, emoji: Misskey.entities.EmojiSimple | UnicodeEmojiDef | string): boolean { if (typeof emoji === 'string') return true; // UnicodeEmojiDefにも無い絵文字であれば文字列で来る。Unicode絵文字であることには変わりないので常にリアクション可能とする; diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index e94027d302..b0ffac93d7 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -7,13 +7,13 @@ import { getHighlighterCore, loadWasm } from 'shiki/core'; import darkPlus from 'shiki/themes/dark-plus.mjs'; import { bundledThemesInfo } from 'shiki/themes'; import { bundledLanguagesInfo } from 'shiki/langs'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import { unique } from './array.js'; import { deepClone } from './clone.js'; import { deepMerge } from './merge.js'; import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core'; import { ColdDeviceStorage } from '@/store.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; let _highlighter: HighlighterCore | null = null; diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend/src/scripts/emoji-base.ts deleted file mode 100644 index a01540a3e4..0000000000 --- a/packages/frontend/src/scripts/emoji-base.ts +++ /dev/null @@ -1,25 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const twemojiSvgBase = '/twemoji'; -const fluentEmojiPngBase = '/fluent-emoji'; - -export function char2twemojiFilePath(char: string): string { - let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); - if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.join('-'); - return `${twemojiSvgBase}/${fileName}.svg`; -} - -export function char2fluentEmojiFilePath(char: string): string { - let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); - // Fluent Emojiは国旗非対応 https://github.com/microsoft/fluentui-emoji/issues/25 - if (codes[0]?.startsWith('1f1')) return char2twemojiFilePath(char); - if (!codes.includes('200d')) codes = codes.filter(x => x !== 'fe0f'); - codes = codes.filter(x => x && x.length); - const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); - return `${fluentEmojiPngBase}/${fileName}.png`; -} diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts deleted file mode 100644 index 6565feba97..0000000000 --- a/packages/frontend/src/scripts/emojilist.ts +++ /dev/null @@ -1,73 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export const unicodeEmojiCategories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags'] as const; - -export type UnicodeEmojiDef = { - name: string; - char: string; - category: typeof unicodeEmojiCategories[number]; -} - -// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb -import _emojilist from '../emojilist.json'; - -export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ - name: x[1] as string, - char: x[0] as string, - category: unicodeEmojiCategories[x[2]], -})); - -const unicodeEmojisMap = new Map( - emojilist.map(x => [x.char, x]), -); - -const _indexByChar = new Map(); -const _charGroupByCategory = new Map(); -for (let i = 0; i < emojilist.length; i++) { - const emo = emojilist[i]; - _indexByChar.set(emo.char, i); - - if (_charGroupByCategory.has(emo.category)) { - _charGroupByCategory.get(emo.category)?.push(emo.char); - } else { - _charGroupByCategory.set(emo.category, [emo.char]); - } -} - -export const emojiCharByCategory = _charGroupByCategory; - -export function getUnicodeEmoji(char: string): UnicodeEmojiDef | string { - // Colorize it because emojilist.json assumes that - return unicodeEmojisMap.get(colorizeEmoji(char)) - // カラースタイル絵文字がjsonに無い場合はテキストスタイル絵文字にフォールバックする - ?? unicodeEmojisMap.get(char) - // それでも見つからない場合はそのまま返す(絵文字情報がjsonに無い場合、このフォールバックが無いとレンダリングに失敗する) - ?? char; -} - -export function getEmojiName(char: string): string { - // Colorize it because emojilist.json assumes that - const idx = _indexByChar.get(colorizeEmoji(char)) ?? _indexByChar.get(char); - if (idx === undefined) { - // 絵文字情報がjsonに無い場合は名前の取得が出来ないのでそのまま返すしか無い - return char; - } else { - return emojilist[idx].name; - } -} - -/** - * テキストスタイル絵文字(U+260Eなどの1文字で表現される絵文字)をカラースタイル絵文字に変換します(VS16:U+FE0Fを付与)。 - */ -export function colorizeEmoji(char: string) { - return char.length === 1 ? `${char}\uFE0F` : char; -} - -export interface CustomEmojiFolderTree { - value: string; - category: string; - children: CustomEmojiFolderTree[]; -} diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts deleted file mode 100644 index 992f6e9a16..0000000000 --- a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function extractAvgColorFromBlurhash(hash: string) { - return typeof hash === 'string' - ? '#' + [...hash.slice(2, 6)] - .map(x => '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~'.indexOf(x)) - .reduce((a, c) => a * 83 + c, 0) - .toString(16) - .padStart(6, '0') - : undefined; -} diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts index eb2da5ad86..81278b17ea 100644 --- a/packages/frontend/src/scripts/focus.ts +++ b/packages/frontend/src/scripts/focus.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@/scripts/scroll.js'; +import { getScrollPosition, getScrollContainer, getStickyBottom, getStickyTop } from '@@/js/scroll.js'; import { getElementOrNull, getNodeOrNull } from '@/scripts/get-dom-node-or-null.js'; type MaybeHTMLElement = EventTarget | Node | Element | HTMLElement; diff --git a/packages/frontend/src/scripts/get-embed-code.ts b/packages/frontend/src/scripts/get-embed-code.ts new file mode 100644 index 0000000000..007cd6561b --- /dev/null +++ b/packages/frontend/src/scripts/get-embed-code.ts @@ -0,0 +1,87 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ +import { defineAsyncComponent } from 'vue'; +import { v4 as uuid } from 'uuid'; +import type { EmbedParams, EmbeddableEntity } from '@@/js/embed-page.js'; +import { url } from '@/config.js'; +import * as os from '@/os.js'; +import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; +import { defaultEmbedParams, embedRouteWithScrollbar } from '@@/js/embed-page.js'; + +const MOBILE_THRESHOLD = 500; + +/** + * パラメータを正規化する(埋め込みコード作成用) + * @param params パラメータ + * @returns 正規化されたパラメータ + */ +export function normalizeEmbedParams(params: EmbedParams): Record { + // paramsのvalueをすべてstringに変換。undefinedやnullはプロパティごと消す + const normalizedParams: Record = {}; + for (const key in params) { + // デフォルトの値と同じならparamsに含めない + if (params[key] == null || params[key] === defaultEmbedParams[key]) { + continue; + } + switch (typeof params[key]) { + case 'number': + normalizedParams[key] = params[key].toString(); + break; + case 'boolean': + normalizedParams[key] = params[key] ? 'true' : 'false'; + break; + default: + normalizedParams[key] = params[key]; + break; + } + } + return normalizedParams; +} + +/** + * 埋め込みコードを生成(iframe IDの発番もやる) + */ +export function getEmbedCode(path: string, params?: EmbedParams): string { + const iframeId = 'v1_' + uuid(); // 将来embed.jsのバージョンが上がったとき用にv1_を付けておく + + let paramString = ''; + if (params) { + const searchParams = new URLSearchParams(normalizeEmbedParams(params)); + paramString = searchParams.toString() === '' ? '' : '?' + searchParams.toString(); + } + + const iframeCode = [ + ``, + ``, + ]; + return iframeCode.join('\n'); +} + +/** + * 埋め込みコードを生成してコピーする(カスタマイズ機能つき) + * + * カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください + */ +export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) { + const _params = { ...params }; + + if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) { + _params.maxHeight = 700; + } + + // PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー + if (window.innerWidth < MOBILE_THRESHOLD) { + copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params)); + os.success(); + } else { + const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), { + entity, + id, + params: _params, + }, { + closed: () => dispose(), + }); + } +} diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index b5d7350a41..e0ccea813d 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -21,6 +21,7 @@ import { MenuItem } from '@/types/menu.js'; import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { isSupportShare } from '@/scripts/navigator.js'; import { getAppearNote } from '@/scripts/get-appear-note.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; export async function getNoteClipMenu(props: { note: Misskey.entities.Note; @@ -156,6 +157,19 @@ export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string): }; } +function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined { + if (note.url != null || note.uri != null) return undefined; + if (['specified', 'followers'].includes(note.visibility)) return undefined; + + return { + icon: 'ti ti-code', + text, + action: (): void => { + genEmbedCode('notes', note.id); + }, + }; +} + export function getNoteMenu(props: { note: Misskey.entities.Note; translation: Ref; @@ -310,7 +324,7 @@ export function getNoteMenu(props: { action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - } : undefined, + } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode), ...(isSupportShare() ? [{ icon: 'ti ti-share', text: i18n.ts.share, @@ -443,14 +457,14 @@ export function getNoteMenu(props: { icon: 'ti ti-copy', text: i18n.ts.copyContent, action: copyContent, - }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? { + }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink), + (appearNote.url || appearNote.uri) ? { icon: 'ti ti-external-link', text: i18n.ts.showOnRemote, action: () => { window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener'); }, - } : undefined] + } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)] .filter(x => x !== undefined); } diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 33f16a68aa..035abc7bd0 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -17,6 +17,7 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-pe import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; import { mainRouter } from '@/router/main.js'; +import { genEmbedCode } from '@/scripts/get-embed-code.js'; import { MenuItem } from '@/types/menu.js'; export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { @@ -179,7 +180,17 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter if (user.url == null) return; window.open(user.url, '_blank', 'noopener'); }, - }] : []), { + }] : [{ + icon: 'ti ti-code', + text: i18n.ts.genEmbedCode, + type: 'parent' as const, + children: [{ + text: i18n.ts.noteOfThisUser, + action: () => { + genEmbedCode('user-timeline', user.id); + }, + }], // TODO: ユーザーカードの埋め込みなど + }]), { icon: 'ti ti-share', text: i18n.ts.copyProfileUrl, action: () => { diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts deleted file mode 100644 index b258a2a678..0000000000 --- a/packages/frontend/src/scripts/i18n.ts +++ /dev/null @@ -1,245 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ -import type { ILocale, ParameterizedString } from '../../../../locales/index.js'; - -type FlattenKeys = keyof { - [K in keyof T as T[K] extends ILocale - ? FlattenKeys extends infer C extends string - ? `${K & string}.${C}` - : never - : T[K] extends TPrediction - ? K - : never]: T[K]; -}; - -type ParametersOf> = TKey extends `${infer K}.${infer C}` - // @ts-expect-error -- C は明らかに FlattenKeys になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。 - ? ParametersOf - : TKey extends keyof T - ? T[TKey] extends ParameterizedString - ? P - : never - : never; - -type Tsx = { - readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString - ? (arg: { readonly [_ in P]: string | number }) => string - // @ts-expect-error -- 証明省略 - : Tsx; -}; - -export class I18n { - private tsxCache?: Tsx; - - constructor(public locale: T) { - //#region BIND - this.t = this.t.bind(this); - //#endregion - } - - public get ts(): T { - if (_DEV_) { - class Handler implements ProxyHandler { - get(target: TTarget, p: string | symbol): unknown { - const value = target[p as keyof TTarget]; - - if (typeof value === 'object') { - return new Proxy(value, new Handler()); - } - - if (typeof value === 'string') { - const parameters = Array.from(value.matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter); - - if (parameters.length) { - console.error(`Missing locale parameters: ${parameters.join(', ')} at ${String(p)}`); - } - - return value; - } - - console.error(`Unexpected locale key: ${String(p)}`); - - return p; - } - } - - return new Proxy(this.locale, new Handler()); - } - - return this.locale; - } - - public get tsx(): Tsx { - if (_DEV_) { - if (this.tsxCache) { - return this.tsxCache; - } - - class Handler implements ProxyHandler { - get(target: TTarget, p: string | symbol): unknown { - const value = target[p as keyof TTarget]; - - if (typeof value === 'object') { - return new Proxy(value, new Handler()); - } - - if (typeof value === 'string') { - const quasis: string[] = []; - const expressions: string[] = []; - let cursor = 0; - - while (~cursor) { - const start = value.indexOf('{', cursor); - - if (!~start) { - quasis.push(value.slice(cursor)); - break; - } - - quasis.push(value.slice(cursor, start)); - - const end = value.indexOf('}', start); - - expressions.push(value.slice(start + 1, end)); - - cursor = end + 1; - } - - if (!expressions.length) { - console.error(`Unexpected locale key: ${String(p)}`); - - return () => value; - } - - return (arg) => { - let str = quasis[0]; - - for (let i = 0; i < expressions.length; i++) { - if (!Object.hasOwn(arg, expressions[i])) { - console.error(`Missing locale parameters: ${expressions[i]} at ${String(p)}`); - } - - str += arg[expressions[i]] + quasis[i + 1]; - } - - return str; - }; - } - - console.error(`Unexpected locale key: ${String(p)}`); - - return p; - } - } - - return this.tsxCache = new Proxy(this.locale, new Handler()) as unknown as Tsx; - } - - if (this.tsxCache) { - return this.tsxCache; - } - - function build(target: ILocale): Tsx { - const result = {} as Tsx; - - for (const k in target) { - if (!Object.hasOwn(target, k)) { - continue; - } - - const value = target[k as keyof typeof target]; - - if (typeof value === 'object') { - result[k] = build(value as ILocale); - } else if (typeof value === 'string') { - const quasis: string[] = []; - const expressions: string[] = []; - let cursor = 0; - - while (~cursor) { - const start = value.indexOf('{', cursor); - - if (!~start) { - quasis.push(value.slice(cursor)); - break; - } - - quasis.push(value.slice(cursor, start)); - - const end = value.indexOf('}', start); - - expressions.push(value.slice(start + 1, end)); - - cursor = end + 1; - } - - if (!expressions.length) { - continue; - } - - result[k] = (arg) => { - let str = quasis[0]; - - for (let i = 0; i < expressions.length; i++) { - str += arg[expressions[i]] + quasis[i + 1]; - } - - return str; - }; - } - } - return result; - } - - return this.tsxCache = build(this.locale); - } - - /** - * @deprecated なるべくこのメソッド使うよりも ts 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも - */ - public t>(key: TKey): string; - /** - * @deprecated なるべくこのメソッド使うよりも tsx 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも - */ - public t>(key: TKey, args: { readonly [_ in ParametersOf]: string | number }): string; - public t(key: string, args?: { readonly [_: string]: string | number }) { - let str: string | ParameterizedString | ILocale = this.locale; - - for (const k of key.split('.')) { - str = str[k]; - - if (_DEV_) { - if (typeof str === 'undefined') { - console.error(`Unexpected locale key: ${key}`); - return key; - } - } - } - - if (args) { - if (_DEV_) { - const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter)); - - if (missing.length) { - console.error(`Missing locale parameters: ${missing.join(', ')} at ${key}`); - } - } - - for (const [k, v] of Object.entries(args)) { - const search = `{${k}}`; - - if (_DEV_) { - if (!(str as string).includes(search)) { - console.error(`Unexpected locale parameter: ${k} at ${key}`); - } - } - - str = (str as string).replace(search, v.toString()); - } - } - - return str; - } -} diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index 6b511f2a5f..20f51660c7 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -10,10 +10,11 @@ import { set as iset, del as idel, } from 'idb-keyval'; +import { miLocalStorage } from '@/local-storage.js'; -const fallbackName = (key: string) => `idbfallback::${key}`; +const PREFIX = 'idbfallback::'; -let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true; +let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true; // iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。 // バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと @@ -38,15 +39,15 @@ if (idbAvailable) { export async function get(key: string) { if (idbAvailable) return iget(key); - return JSON.parse(window.localStorage.getItem(fallbackName(key))); + return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); } export async function set(key: string, val: any) { if (idbAvailable) return iset(key, val); - return window.localStorage.setItem(fallbackName(key), JSON.stringify(val)); + return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); } export async function del(key: string) { if (idbAvailable) return idel(key); - return window.localStorage.removeItem(fallbackName(key)); + return miLocalStorage.removeItem(`${PREFIX}${key}`); } diff --git a/packages/frontend/src/scripts/is-link.ts b/packages/frontend/src/scripts/is-link.ts new file mode 100644 index 0000000000..946f86400e --- /dev/null +++ b/packages/frontend/src/scripts/is-link.ts @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export function isLink(el: HTMLElement) { + if (el.tagName === 'A') return true; + if (el.parentElement) { + return isLink(el.parentElement); + } + return false; +} diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 099a22163a..68a5a1dcf8 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -3,51 +3,32 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { query } from '@/scripts/url.js'; +import { MediaProxy } from '@@/js/media-proxy.js'; import { url } from '@/config.js'; import { instance } from '@/instance.js'; -export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string { - const localProxy = `${url}/proxy`; +let _mediaProxy: MediaProxy | null = null; - if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) { - // もう既にproxyっぽそうだったらurlを取り出す - imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl; +export function getProxiedImageUrl(...args: Parameters): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${mustOrigin ? localProxy : instance.mediaProxy}/${ - type === 'preview' ? 'preview.webp' - : 'image.webp' - }?${query({ - url: imageUrl, - ...(!noFallback ? { 'fallback': '1' } : {}), - ...(type ? { [type]: '1' } : {}), - ...(mustOrigin ? { origin: '1' } : {}), - })}`; + return _mediaProxy.getProxiedImageUrl(...args); } -export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null { - if (imageUrl == null) return null; - return getProxiedImageUrl(imageUrl, type); -} - -export function getStaticImageUrl(baseUrl: string): string { - const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url); - - if (u.href.startsWith(`${url}/emoji/`)) { - // もう既にemojiっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; +export function getProxiedImageUrlNullable(...args: Parameters): string | null { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - if (u.href.startsWith(instance.mediaProxy + '/')) { - // もう既にproxyっぽそうだったらsearchParams付けるだけ - u.searchParams.set('static', '1'); - return u.href; + return _mediaProxy.getProxiedImageUrlNullable(...args); +} + +export function getStaticImageUrl(...args: Parameters): string { + if (_mediaProxy == null) { + _mediaProxy = new MediaProxy(instance, url); } - return `${instance.mediaProxy}/static.webp?${query({ - url: u.href, - static: '1', - })}`; + return _mediaProxy.getStaticImageUrl(...args); } diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts index 9938e534c1..bf59fe98a0 100644 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -6,7 +6,7 @@ import { Ref, nextTick } from 'vue'; import * as os from '@/os.js'; import { i18n } from '@/i18n.js'; -import { MFM_TAGS } from '@/const.js'; +import { MFM_TAGS } from '@@/js/const.js'; import type { MenuItem } from '@/types/menu.js'; /** diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/frontend/src/scripts/nyaize.ts deleted file mode 100644 index abc8ada461..0000000000 --- a/packages/frontend/src/scripts/nyaize.ts +++ /dev/null @@ -1,27 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -const enRegex1 = /(?<=n)a/gi; -const enRegex2 = /(?<=morn)ing/gi; -const enRegex3 = /(?<=every)one/gi; -const koRegex1 = /[나-낳]/g; -const koRegex2 = /(다$)|(다(?=\.))|(다(?= ))|(다(?=!))|(다(?=\?))/gm; -const koRegex3 = /(야(?=\?))|(야$)|(야(?= ))/gm; - -export function nyaize(text: string): string { - return text - // ja-JP - .replaceAll('な', 'にゃ').replaceAll('ナ', 'ニャ').replaceAll('ナ', 'ニャ') - // en-US - .replace(enRegex1, x => x === 'A' ? 'YA' : 'ya') - .replace(enRegex2, x => x === 'ING' ? 'YAN' : 'yan') - .replace(enRegex3, x => x === 'ONE' ? 'NYAN' : 'nyan') - // ko-KR - .replace(koRegex1, match => String.fromCharCode( - match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0), - )) - .replace(koRegex2, '다냥') - .replace(koRegex3, '냥'); -} diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts index 1caa2dfc21..ed49611b4f 100644 --- a/packages/frontend/src/scripts/popout.ts +++ b/packages/frontend/src/scripts/popout.ts @@ -3,7 +3,7 @@ * SPDX-License-Identifier: AGPL-3.0-only */ -import { appendQuery } from './url.js'; +import { appendQuery } from '@@/js/url.js'; import * as config from '@/config.js'; export function popout(path: string, w?: HTMLElement) { diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts index 31a9ac1ad9..11b6f52ddd 100644 --- a/packages/frontend/src/scripts/post-message.ts +++ b/packages/frontend/src/scripts/post-message.ts @@ -18,7 +18,7 @@ export type MiPostMessageEvent = { * 親フレームにイベントを送信 */ export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void { - window.postMessage({ + window.parent.postMessage({ type, payload, }, '*'); diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts deleted file mode 100644 index 6bfcef6c36..0000000000 --- a/packages/frontend/src/scripts/safe-parse.ts +++ /dev/null @@ -1,11 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeParseFloat(str: unknown): number | null { - if (typeof str !== 'string' || str === '') return null; - const num = parseFloat(str); - if (isNaN(num)) return null; - return num; -} diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts deleted file mode 100644 index 0edf4e9eba..0000000000 --- a/packages/frontend/src/scripts/safe-uri-decode.ts +++ /dev/null @@ -1,12 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -export function safeURIDecode(str: string): string { - try { - return decodeURIComponent(str); - } catch { - return str; - } -} diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts deleted file mode 100644 index f0274034b5..0000000000 --- a/packages/frontend/src/scripts/scroll.ts +++ /dev/null @@ -1,144 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -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-y'); - if (overflow === 'scroll' || overflow === 'auto') { - return el; - } else { - return getScrollContainer(el.parentElement); - } -} - -export function getStickyTop(el: HTMLElement, container: HTMLElement | null = null, top = 0) { - if (!el.parentElement) return top; - const data = el.dataset.stickyContainerHeaderHeight; - const newTop = data ? Number(data) + top : top; - if (el === container) return newTop; - return getStickyTop(el.parentElement, container, newTop); -} - -export function getStickyBottom(el: HTMLElement, container: HTMLElement | null = null, bottom = 0) { - if (!el.parentElement) return bottom; - const data = el.dataset.stickyContainerFooterHeight; - const newBottom = data ? Number(data) + bottom : bottom; - if (el === container) return newBottom; - return getStickyBottom(el.parentElement, container, newBottom); -} - -export function getScrollPosition(el: HTMLElement | null): number { - const container = getScrollContainer(el); - return container == null ? window.scrollY : container.scrollTop; -} - -export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { - // とりあえず評価してみる - if (el.isConnected && isTopVisible(el)) { - cb(); - if (once) return null; - } - - const container = getScrollContainer(el) ?? window; - - const onScroll = ev => { - if (!document.body.contains(el)) return; - if (isTopVisible(el, tolerance)) { - cb(); - if (once) removeListener(); - } - }; - - function removeListener() { container.removeEventListener('scroll', onScroll); } - - container.addEventListener('scroll', onScroll, { passive: true }); - return removeListener; -} - -export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1, once = false) { - const container = getScrollContainer(el); - - // とりあえず評価してみる - if (el.isConnected && isBottomVisible(el, tolerance, container)) { - cb(); - if (once) return null; - } - - const containerOrWindow = container ?? window; - const onScroll = ev => { - if (!document.body.contains(el)) return; - if (isBottomVisible(el, 1, container)) { - cb(); - if (once) removeListener(); - } - }; - - function removeListener() { - containerOrWindow.removeEventListener('scroll', onScroll); - } - - containerOrWindow.addEventListener('scroll', onScroll, { passive: true }); - return removeListener; -} - -export function scroll(el: HTMLElement, options: ScrollToOptions | undefined) { - const container = getScrollContainer(el); - if (container == null) { - window.scroll(options); - } else { - container.scroll(options); - } -} - -/** - * Scroll to Top - * @param el Scroll container element - * @param options Scroll options - */ -export function scrollToTop(el: HTMLElement, options: { behavior?: ScrollBehavior; } = {}) { - scroll(el, { top: 0, ...options }); -} - -/** - * Scroll to Bottom - * @param el Content element - * @param options Scroll options - * @param container Scroll container element - */ -export function scrollToBottom( - el: HTMLElement, - options: ScrollToOptions = {}, - container = getScrollContainer(el), -) { - if (container) { - container.scroll({ top: el.scrollHeight - container.clientHeight + getStickyTop(el, container) || 0, ...options }); - } else { - window.scroll({ - top: (el.scrollHeight - window.innerHeight + getStickyTop(el, container) + (window.innerWidth <= 500 ? 96 : 0)) || 0, - ...options, - }); - } -} - -export function isTopVisible(el: HTMLElement, tolerance = 1): boolean { - const scrollTop = getScrollPosition(el); - return scrollTop <= tolerance; -} - -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; -} - -// https://ja.javascript.info/size-and-scroll-window#ref-932 -export function getBodyScrollHeight() { - return Math.max( - document.body.scrollHeight, document.documentElement.scrollHeight, - document.body.offsetHeight, document.documentElement.offsetHeight, - document.body.clientHeight, document.documentElement.clientHeight, - ); -} diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts new file mode 100644 index 0000000000..cb0e607fcb --- /dev/null +++ b/packages/frontend/src/scripts/stream-mock.ts @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { EventEmitter } from 'eventemitter3'; +import * as Misskey from 'misskey-js'; +import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js'; + +type AnyOf> = T[keyof T]; +type OmitFirst = T extends [any, ...infer R] ? R : never; + +/** + * Websocket無効化時に使うStreamのモック(なにもしない) + */ +export class StreamMock extends EventEmitter implements IStream { + public readonly state = 'initializing'; + + constructor(...args: ConstructorParameters) { + super(); + // do nothing + } + + public useChannel(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock { + return new ChannelConnectionMock(this, channel, name); + } + + public removeSharedConnection(connection: any): void { + // do nothing + } + + public removeSharedConnectionPool(pool: any): void { + // do nothing + } + + public disconnectToChannel(): void { + // do nothing + } + + public send(typeOrPayload: string): void + public send(typeOrPayload: string, payload: any): void + public send(typeOrPayload: Record | any[]): void + public send(typeOrPayload: string | Record | any[], payload?: any): void { + // do nothing + } + + public ping(): void { + // do nothing + } + + public heartbeat(): void { + // do nothing + } + + public close(): void { + // do nothing + } +} + +class ChannelConnectionMock = any> extends EventEmitter implements IChannelConnection { + public id = ''; + public name?: string; // for debug + public inCount = 0; // for debug + public outCount = 0; // for debug + public channel: string; + + constructor(stream: IStream, ...args: OmitFirst>>) { + super(); + + this.channel = args[0]; + this.name = args[1]; + } + + public send(type: T, body: Channel['receives'][T]): void { + // do nothing + } + + public dispose(): void { + // do nothing + } +} diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index c7f8b3d596..9b9f1f030c 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -5,11 +5,11 @@ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; +import lightTheme from '@@/themes/_light.json5'; +import darkTheme from '@@/themes/_dark.json5'; import { deepClone } from './clone.js'; import type { BundledTheme } from 'shiki/themes'; import { globalEvents } from '@/events.js'; -import lightTheme from '@/themes/_light.json5'; -import darkTheme from '@/themes/_dark.json5'; import { miLocalStorage } from '@/local-storage.js'; export type Theme = { diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend/src/scripts/url.ts deleted file mode 100644 index 5a8265af9e..0000000000 --- a/packages/frontend/src/scripts/url.ts +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -/* objを検査して - * 1. 配列に何も入っていない時はクエリを付けない - * 2. プロパティがundefinedの時はクエリを付けない - * (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない) - */ -export function query(obj: Record): 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); - - return Object.entries(params) - .map((p) => `${p[0]}=${encodeURIComponent(p[1])}`) - .join('&'); -} - -export function appendQuery(url: string, query: string): string { - return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`; -} - -export function extractDomain(url: string) { - const match = url.match(/^(?:https?:)?(?:\/\/)?(?:[^@\n]+@)?([^:\/\n]+)/im); - return match ? match[1] : null; -} diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend/src/scripts/use-document-visibility.ts deleted file mode 100644 index a8f4d5e03a..0000000000 --- a/packages/frontend/src/scripts/use-document-visibility.ts +++ /dev/null @@ -1,24 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onMounted, onUnmounted, ref, Ref } from 'vue'; - -export function useDocumentVisibility(): Ref { - const visibility = ref(document.visibilityState); - - const onChange = (): void => { - visibility.value = document.visibilityState; - }; - - onMounted(() => { - document.addEventListener('visibilitychange', onChange); - }); - - onUnmounted(() => { - document.removeEventListener('visibilitychange', onChange); - }); - - return visibility; -} diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend/src/scripts/use-interval.ts deleted file mode 100644 index b50e78c3cc..0000000000 --- a/packages/frontend/src/scripts/use-interval.ts +++ /dev/null @@ -1,46 +0,0 @@ -/* - * SPDX-FileCopyrightText: syuilo and misskey-project - * SPDX-License-Identifier: AGPL-3.0-only - */ - -import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'; - -export function useInterval(fn: () => void, interval: number, options: { - immediate: boolean; - afterMounted: boolean; -}): (() => void) | undefined { - 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); - } - - const clear = () => { - if (intervalId) window.clearInterval(intervalId); - intervalId = null; - }; - - onActivated(() => { - if (intervalId) return; - if (options.immediate) fn(); - intervalId = window.setInterval(fn, interval); - }); - - onDeactivated(() => { - clear(); - }); - - onUnmounted(() => { - clear(); - }); - - return clear; -} -- cgit v1.2.3-freya