diff options
Diffstat (limited to 'packages/frontend/src/scripts')
99 files changed, 1190 insertions, 312 deletions
diff --git a/packages/frontend/src/scripts/achievements.ts b/packages/frontend/src/scripts/achievements.ts index e7585fcf81..f5d0ab559f 100644 --- a/packages/frontend/src/scripts/achievements.ts +++ b/packages/frontend/src/scripts/achievements.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { $i } from '@/account.js'; export const ACHIEVEMENT_TYPES = [ @@ -83,6 +83,8 @@ export const ACHIEVEMENT_TYPES = [ 'brainDiver', 'smashTestNotificationButton', 'tutorialCompleted', + 'bubbleGameExplodingHead', + 'bubbleGameDoubleExplodingHead', ] as const; export const ACHIEVEMENT_BADGES = { @@ -466,6 +468,16 @@ export const ACHIEVEMENT_BADGES = { bg: 'linear-gradient(0deg, rgb(220 223 225), rgb(172 192 207))', frame: 'bronze', }, + 'bubbleGameExplodingHead': { + img: '/fluent-emoji/1f92f.png', + bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', + frame: 'bronze', + }, + 'bubbleGameDoubleExplodingHead': { + img: '/fluent-emoji/1f92f.png', + bg: 'linear-gradient(0deg, rgb(255 77 77), rgb(247 155 214))', + frame: 'silver', + }, /* @see <https://github.com/misskey-dev/misskey/pull/10365#discussion_r1155511107> } as const satisfies Record<typeof ACHIEVEMENT_TYPES[number], { img: string; @@ -489,7 +501,7 @@ export async function claimAchievement(type: typeof ACHIEVEMENT_TYPES[number]) { window.setTimeout(() => { claimingQueue.delete(type); }, 500); - os.api('i/claim-achievement', { name: type }); + misskeyApi('i/claim-achievement', { name: type }); } if (_DEV_) { diff --git a/packages/frontend/src/scripts/aiscript/api.ts b/packages/frontend/src/scripts/aiscript/api.ts index 038ae23109..98a0c61752 100644 --- a/packages/frontend/src/scripts/aiscript/api.ts +++ b/packages/frontend/src/scripts/aiscript/api.ts @@ -1,16 +1,27 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import { utils, values } from '@syuilo/aiscript'; 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<string> { + return new Promise(ok => { + os.inputText({ + title: q, + }).then(({ result: a }) => { + ok(a ?? ''); + }); + }); +} + export function createAiScriptEnv(opts) { return { USER_ID: $i ? values.STR($i.id) : values.NULL, @@ -44,7 +55,7 @@ export function createAiScriptEnv(opts) { if (typeof token.value !== 'string') throw new Error('invalid token'); } const actualToken: string|null = token?.value ?? opts.token ?? null; - return os.api(ep.value, utils.valToJs(param), actualToken).then(res => { + return misskeyApi(ep.value, utils.valToJs(param), actualToken).then(res => { return utils.jsToVal(res); }, err => { return values.ERROR('request_failed', utils.jsToVal(err)); diff --git a/packages/frontend/src/scripts/aiscript/ui.ts b/packages/frontend/src/scripts/aiscript/ui.ts index 08ba1e6d9b..f2493264d3 100644 --- a/packages/frontend/src/scripts/aiscript/ui.ts +++ b/packages/frontend/src/scripts/aiscript/ui.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -218,7 +218,7 @@ function getTextOptions(def: values.Value | undefined): Omit<AsUiText, 'id' | 't }; } -function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'type'> { +function getMfmOptions(def: values.Value | undefined, call: (fn: values.VFn, args: values.Value[]) => Promise<values.Value>): Omit<AsUiMfm, 'id' | 'type'> { utils.assertObject(def); const text = def.value.get('text'); @@ -241,7 +241,7 @@ function getMfmOptions(def: values.Value | undefined): Omit<AsUiMfm, 'id' | 'typ color: color?.value, font: font?.value, onClickEv: (evId: string) => { - if (onClickEv) call(onClickEv, values.STR(evId)); + if (onClickEv) call(onClickEv, [values.STR(evId)]); }, }; } diff --git a/packages/frontend/src/scripts/array.ts b/packages/frontend/src/scripts/array.ts index 082703a450..b3d76e149f 100644 --- a/packages/frontend/src/scripts/array.ts +++ b/packages/frontend/src/scripts/array.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/autocomplete.ts b/packages/frontend/src/scripts/autocomplete.ts index 2a9a42ace5..9fc8f7843e 100644 --- a/packages/frontend/src/scripts/autocomplete.ts +++ b/packages/frontend/src/scripts/autocomplete.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,18 +8,18 @@ import getCaretCoordinates from 'textarea-caret'; import { toASCII } from 'punycode/'; import { popup } from '@/os.js'; -export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag'; +export type SuggestionType = 'user' | 'hashtag' | 'emoji' | 'mfmTag' | 'mfmParam'; export class Autocomplete { private suggestion: { x: Ref<number>; y: Ref<number>; - q: Ref<string | null>; + q: Ref<any>; close: () => void; } | null; private textarea: HTMLInputElement | HTMLTextAreaElement; private currentType: string; - private textRef: Ref<string>; + private textRef: Ref<string | number | null>; private opening: boolean; private onlyType: SuggestionType[]; @@ -38,7 +38,7 @@ export class Autocomplete { /** * 対象のテキストエリアを与えてインスタンスを初期化します。 */ - constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>, onlyType?: SuggestionType[]) { + constructor(textarea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string | number | null>, onlyType?: SuggestionType[]) { //#region BIND this.onInput = this.onInput.bind(this); this.complete = this.complete.bind(this); @@ -49,7 +49,7 @@ export class Autocomplete { this.textarea = textarea; this.textRef = textRef; this.opening = false; - this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag']; + this.onlyType = onlyType ?? ['user', 'hashtag', 'emoji', 'mfmTag', 'mfmParam']; this.attach(); } @@ -80,6 +80,7 @@ export class Autocomplete { const hashtagIndex = text.lastIndexOf('#'); const emojiIndex = text.lastIndexOf(':'); const mfmTagIndex = text.lastIndexOf('$'); + const mfmParamIndex = text.lastIndexOf('.'); const max = Math.max( mentionIndex, @@ -92,9 +93,12 @@ export class Autocomplete { return; } + const afterLastMfmParam = text.split(/\$\[[a-zA-Z]+/).pop(); + const isMention = mentionIndex !== -1; const isHashtag = hashtagIndex !== -1; - const isMfmTag = mfmTagIndex !== -1; + const isMfmParam = mfmParamIndex !== -1 && afterLastMfmParam?.includes('.') && !afterLastMfmParam?.includes(' '); + const isMfmTag = mfmTagIndex !== -1 && !isMfmParam; const isEmoji = emojiIndex !== -1 && text.split(/:[a-z0-9_+\-]+:/).pop()!.includes(':'); let opened = false; @@ -134,6 +138,17 @@ export class Autocomplete { } } + if (isMfmParam && !opened && this.onlyType.includes('mfmParam')) { + const mfmParam = text.substring(mfmParamIndex + 1); + if (!mfmParam.includes(' ')) { + this.open('mfmParam', { + tag: text.substring(mfmTagIndex + 2, mfmParamIndex), + params: mfmParam.split(','), + }); + opened = true; + } + } + if (!opened) { this.close(); } @@ -142,7 +157,7 @@ export class Autocomplete { /** * サジェストを提示します。 */ - private async open(type: string, q: string | null) { + private async open(type: string, q: any) { if (type !== this.currentType) { this.close(); } @@ -280,6 +295,22 @@ export class Autocomplete { const pos = trimmedBefore.length + (value.length + 3); this.textarea.setSelectionRange(pos, pos); }); + } else if (type === 'mfmParam') { + const source = this.text; + + const before = source.substring(0, caret); + const trimmedBefore = before.substring(0, before.lastIndexOf('.')); + const after = source.substring(caret); + + // 挿入 + this.text = `${trimmedBefore}.${value}${after}`; + + // キャレットを戻す + nextTick(() => { + this.textarea.focus(); + const pos = trimmedBefore.length + (value.length + 1); + this.textarea.setSelectionRange(pos, pos); + }); } } } diff --git a/packages/frontend/src/scripts/boost-quote.ts b/packages/frontend/src/scripts/boost-quote.ts new file mode 100644 index 0000000000..4e025f5d4f --- /dev/null +++ b/packages/frontend/src/scripts/boost-quote.ts @@ -0,0 +1,81 @@ +/* + * SPDX-FileCopyrightText: dakkar and other misskey contributors + * SPDX-License-Identifier: AGPL-3.0-only +*/ + +import { ref, Ref } from 'vue'; +import * as Misskey from 'misskey-js'; +import { i18n } from '@/i18n.js'; +import { defaultStore } from '@/store.js'; +import { MenuItem } from '@/types/menu.js'; + +/* + this script should eventually contain all Sharkey-specific bits of + boosting and quoting that we would otherwise have to replicate in + `{M,S}kNote{,Detailed,Sub}.vue` + */ + +export type Visibility = 'public' | 'home' | 'followers' | 'specified'; + +export function smallerVisibility(a: Visibility | string, b: Visibility | string): Visibility { + if (a === 'specified' || b === 'specified') return 'specified'; + if (a === 'followers' || b === 'followers') return 'followers'; + if (a === 'home' || b === 'home') return 'home'; + // if (a === 'public' || b === 'public') + return 'public'; +} + +export function visibilityIsAtLeast(a: Visibility | string, b: Visibility | string): boolean { + return smallerVisibility(a, b) === b; +} + +export function boostMenuItems(appearNote: Ref<Misskey.entities.Note>, renote: (v: Visibility, l: boolean) => void): MenuItem[] { + const localOnly = ref(defaultStore.state.rememberNoteVisibility ? defaultStore.state.localOnly : defaultStore.state.defaultNoteLocalOnly); + const effectiveVisibility = ( + appearNote.value.channel?.isSensitive + ? smallerVisibility(appearNote.value.visibility, 'home') + : appearNote.value.visibility + ); + + const menuItems: MenuItem[] = []; + if (visibilityIsAtLeast(effectiveVisibility, 'public')) { + menuItems.push({ + type: 'button', + icon: 'ph-globe-hemisphere-west ph-bold ph-lg', + text: i18n.ts._visibility['public'], + action: () => { + renote('public', localOnly.value); + }, + } as MenuItem); + } + if (visibilityIsAtLeast(effectiveVisibility, 'home')) { + menuItems.push({ + type: 'button', + icon: 'ph-house ph-bold ph-lg', + text: i18n.ts._visibility['home'], + action: () => { + renote('home', localOnly.value); + }, + } as MenuItem); + } + if (visibilityIsAtLeast(effectiveVisibility, 'followers')) { + menuItems.push({ + type: 'button', + icon: 'ph-lock ph-bold ph-lg', + text: i18n.ts._visibility['followers'], + action: () => { + renote('followers', localOnly.value); + }, + } as MenuItem); + } + + return [ + ...menuItems, + { + type: 'switch', + icon: 'ph-planet ph-bold ph-lg', + text: i18n.ts._timelines.local, + ref: localOnly, + } as MenuItem, + ]; +} diff --git a/packages/frontend/src/scripts/cache.ts b/packages/frontend/src/scripts/cache.ts index 12347cf4b1..0fbdf34d5d 100644 --- a/packages/frontend/src/scripts/cache.ts +++ b/packages/frontend/src/scripts/cache.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/chart-legend.ts b/packages/frontend/src/scripts/chart-legend.ts index e91908e0cb..2d534f60c1 100644 --- a/packages/frontend/src/scripts/chart-legend.ts +++ b/packages/frontend/src/scripts/chart-legend.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/chart-vline.ts b/packages/frontend/src/scripts/chart-vline.ts index 336ec6cfbb..24e41245e7 100644 --- a/packages/frontend/src/scripts/chart-vline.ts +++ b/packages/frontend/src/scripts/chart-vline.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/check-animated-mfm.ts b/packages/frontend/src/scripts/check-animated-mfm.ts index 8e3ef7ee46..eac18738ee 100644 --- a/packages/frontend/src/scripts/check-animated-mfm.ts +++ b/packages/frontend/src/scripts/check-animated-mfm.ts @@ -1,4 +1,4 @@ -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; export function checkAnimationFromMfm(nodes: mfm.MfmNode[]): boolean { const animatedNodes = mfm.extract(nodes, (node) => { diff --git a/packages/frontend/src/scripts/check-reaction-permissions.ts b/packages/frontend/src/scripts/check-reaction-permissions.ts new file mode 100644 index 0000000000..e7b473dd75 --- /dev/null +++ b/packages/frontend/src/scripts/check-reaction-permissions.ts @@ -0,0 +1,12 @@ +import * as Misskey from 'misskey-js'; +import { UnicodeEmojiDef } from './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絵文字であることには変わりないので常にリアクション可能とする; + if ('char' in emoji) return true; // UnicodeEmojiDefなら常にリアクション可能 + + const roleIdsThatCanBeUsedThisEmojiAsReaction = emoji.roleIdsThatCanBeUsedThisEmojiAsReaction ?? []; + return !(emoji.localOnly && note.user.host !== me.host) + && !(emoji.isSensitive && (note.reactionAcceptance === 'nonSensitiveOnly' || note.reactionAcceptance === 'nonSensitiveOnlyForLocalLikeOnlyForRemote')) + && (roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0 || me.roles.some(role => roleIdsThatCanBeUsedThisEmojiAsReaction.includes(role.id))); +} diff --git a/packages/frontend/src/scripts/check-word-mute.ts b/packages/frontend/src/scripts/check-word-mute.ts index 5ac19c8d5b..67e896b4b9 100644 --- a/packages/frontend/src/scripts/check-word-mute.ts +++ b/packages/frontend/src/scripts/check-word-mute.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/chiptune2.ts b/packages/frontend/src/scripts/chiptune2.ts index e52adb07d1..5a5b1d6c24 100644 --- a/packages/frontend/src/scripts/chiptune2.ts +++ b/packages/frontend/src/scripts/chiptune2.ts @@ -150,6 +150,27 @@ ChiptuneJsPlayer.prototype.getRow = function () { return 0; }; +ChiptuneJsPlayer.prototype.getNumPatterns = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_num_patterns(this.currentPlayingNode.modulePtr); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.getCurrentSpeed = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_current_speed(this.currentPlayingNode.modulePtr); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.getCurrentTempo = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_current_tempo(this.currentPlayingNode.modulePtr); + } + return 0; +}; + ChiptuneJsPlayer.prototype.getPatternNumRows = function (pattern: number) { if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { return libopenmpt._openmpt_module_get_pattern_num_rows(this.currentPlayingNode.modulePtr, pattern); @@ -164,6 +185,20 @@ ChiptuneJsPlayer.prototype.getPatternRowChannel = function (pattern: number, row return ''; }; +ChiptuneJsPlayer.prototype.getCtls = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_module_get_ctls(this.currentPlayingNode.modulePtr); + } + return 0; +}; + +ChiptuneJsPlayer.prototype.version = function () { + if (this.currentPlayingNode && this.currentPlayingNode.modulePtr) { + return libopenmpt._openmpt_get_library_version(); + } + return 0; +}; + ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: object) { const maxFramesPerChunk = 4096; const processNode = this.audioContext.createScriptProcessor(2048, 0, 2); @@ -178,6 +213,7 @@ ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: obje processNode.paused = false; processNode.leftBufferPtr = libopenmpt._malloc(4 * maxFramesPerChunk); processNode.rightBufferPtr = libopenmpt._malloc(4 * maxFramesPerChunk); + processNode.perf = { 'current': 0, 'max': 0 }; processNode.cleanup = function () { if (this.modulePtr !== 0) { libopenmpt._openmpt_module_destroy(this.modulePtr); @@ -205,7 +241,13 @@ ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: obje processNode.togglePause = function () { this.paused = !this.paused; }; + processNode.getProcessTime = function() { + const max = this.perf.max; + this.perf.max = 0; + return { 'current': this.perf.current, 'max': max }; + }; processNode.onaudioprocess = function (e) { + let startTimeP1 = performance.now(); const outputL = e.outputBuffer.getChannelData(0); const outputR = e.outputBuffer.getChannelData(1); let framesToRender = outputL.length; @@ -231,11 +273,13 @@ ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: obje const currentPattern = libopenmpt._openmpt_module_get_current_pattern(this.modulePtr); const currentRow = libopenmpt._openmpt_module_get_current_row(this.modulePtr); + startTimeP1 = startTimeP1 - performance.now(); if (currentPattern !== this.patternIndex) { processNode.player.fireEvent('onPatternChange'); } processNode.player.fireEvent('onRowChange', { index: currentRow }); + const startTimeP2 = performance.now(); while (framesToRender > 0) { const framesPerChunk = Math.min(framesToRender, maxFramesPerChunk); const actualFramesPerChunk = libopenmpt._openmpt_module_read_float_stereo(this.modulePtr, this.context.sampleRate, framesPerChunk, this.leftBufferPtr, this.rightBufferPtr); @@ -262,6 +306,8 @@ ChiptuneJsPlayer.prototype.createLibopenmptNode = function (buffer, config: obje this.cleanup(); error ? processNode.player.fireEvent('onError', { type: 'openmpt' }) : processNode.player.fireEvent('onEnded'); } + this.perf.current = performance.now() - startTimeP2 + startTimeP1; + if (this.perf.current > this.perf.max) this.perf.max = this.perf.current; }; return processNode; }; diff --git a/packages/frontend/src/scripts/clear-cache.ts b/packages/frontend/src/scripts/clear-cache.ts index f2db87c4fb..b20109ec72 100644 --- a/packages/frontend/src/scripts/clear-cache.ts +++ b/packages/frontend/src/scripts/clear-cache.ts @@ -2,14 +2,18 @@ import { unisonReload } from '@/scripts/unison-reload.js'; import * as os from '@/os.js'; import { miLocalStorage } from '@/local-storage.js'; import { fetchCustomEmojis } from '@/custom-emojis.js'; +import { fetchInstance } from '@/instance.js'; export async function clearCache() { os.waiting(); + miLocalStorage.removeItem('instance'); + miLocalStorage.removeItem('instanceCachedAt'); miLocalStorage.removeItem('locale'); miLocalStorage.removeItem('localeVersion'); miLocalStorage.removeItem('theme'); miLocalStorage.removeItem('emojis'); miLocalStorage.removeItem('lastEmojisFetchedAt'); + await fetchInstance(true); await fetchCustomEmojis(true); unisonReload(); } diff --git a/packages/frontend/src/scripts/clicker-game.ts b/packages/frontend/src/scripts/clicker-game.ts index 5ad076e5ef..f9c4bc1829 100644 --- a/packages/frontend/src/scripts/clicker-game.ts +++ b/packages/frontend/src/scripts/clicker-game.ts @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import { ref, computed } from 'vue'; -import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; type SaveData = { gameVersion: number; @@ -23,7 +23,7 @@ let prev = ''; export async function load() { try { - saveData.value = await os.api('i/registry/get', { + saveData.value = await misskeyApi('i/registry/get', { scope: ['clickerGame'], key: 'saveData', }); @@ -63,7 +63,7 @@ export async function save() { const current = JSON.stringify(saveData.value); if (current === prev) return; - await os.api('i/registry/set', { + await misskeyApi('i/registry/set', { scope: ['clickerGame'], key: 'saveData', value: saveData.value, diff --git a/packages/frontend/src/scripts/clone.ts b/packages/frontend/src/scripts/clone.ts index 96b53684f3..ea8eea14b5 100644 --- a/packages/frontend/src/scripts/clone.ts +++ b/packages/frontend/src/scripts/clone.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,15 +8,15 @@ // あと、Vue RefをIndexedDBに保存しようとしてstructredCloneを使ったらエラーになった // https://github.com/misskey-dev/misskey/pull/8098#issuecomment-1114144045 -type Cloneable = string | number | boolean | null | { [key: string]: Cloneable } | Cloneable[]; +export type Cloneable = string | number | boolean | null | undefined | { [key: string]: Cloneable } | { [key: number]: Cloneable } | { [key: symbol]: Cloneable } | Cloneable[]; export function deepClone<T extends Cloneable>(x: T): T { if (typeof x === 'object') { if (x === null) return x; if (Array.isArray(x)) return x.map(deepClone) as T; - const obj = {} as Record<string, Cloneable>; + const obj = {} as Record<string | number | symbol, Cloneable>; for (const [k, v] of Object.entries(x)) { - obj[k] = deepClone(v); + obj[k] = v === undefined ? undefined : deepClone(v); } return obj as T; } else { diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts index 957669122e..2733897bab 100644 --- a/packages/frontend/src/scripts/code-highlighter.ts +++ b/packages/frontend/src/scripts/code-highlighter.ts @@ -1,10 +1,51 @@ -import { setWasm, setCDN, Highlighter, getHighlighter as _getHighlighter } from 'shiki'; - -setWasm('/assets/shiki/dist/onig.wasm'); -setCDN('/assets/shiki/'); +import { bundledThemesInfo } from 'shiki'; +import { getHighlighterCore, loadWasm } from 'shiki/core'; +import darkPlus from 'shiki/themes/dark-plus.mjs'; +import { unique } from './array.js'; +import { deepClone } from './clone.js'; +import { deepMerge } from './merge.js'; +import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki'; +import { ColdDeviceStorage } from '@/store.js'; +import lightTheme from '@/themes/_light.json5'; +import darkTheme from '@/themes/_dark.json5'; let _highlighter: Highlighter | null = null; +export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>; +export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>; +export async function getTheme(mode: 'light' | 'dark', getName = false): Promise<ThemeRegistration | ThemeRegistrationRaw | string | null> { + const theme = deepClone(ColdDeviceStorage.get(mode === 'light' ? 'lightTheme' : 'darkTheme')); + + if (theme.base) { + const base = [lightTheme, darkTheme].find(x => x.id === theme.base); + if (base && base.codeHighlighter) theme.codeHighlighter = Object.assign({}, base.codeHighlighter, theme.codeHighlighter); + } + + if (theme.codeHighlighter) { + let _res: ThemeRegistration = {}; + if (theme.codeHighlighter.base === '_none_') { + _res = deepClone(theme.codeHighlighter.overrides); + } else { + const base = await bundledThemesInfo.find(t => t.id === theme.codeHighlighter!.base)?.import() ?? darkPlus; + _res = deepMerge(theme.codeHighlighter.overrides ?? {}, 'default' in base ? base.default : base); + } + if (_res.name == null) { + _res.name = theme.id; + } + _res.type = mode; + + if (getName) { + return _res.name; + } + return _res; + } + + if (getName) { + return 'dark-plus'; + } + return darkPlus; +} + export async function getHighlighter(): Promise<Highlighter> { if (!_highlighter) { return await initHighlighter(); @@ -13,16 +54,36 @@ export async function getHighlighter(): Promise<Highlighter> { } export async function initHighlighter() { - const highlighter = await _getHighlighter({ - theme: 'dark-plus', - langs: ['js'], + const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json'); + + await loadWasm(import('shiki/onig.wasm?init')); + + // テーマの重複を消す + const themes = unique([ + darkPlus, + ...(await Promise.all([getTheme('light'), getTheme('dark')])), + ]); + + const highlighter = await getHighlighterCore({ + themes, + langs: [ + import('shiki/langs/javascript.mjs'), + aiScriptGrammar.default as unknown as LanguageRegistration, + ], + }); + + ColdDeviceStorage.watch('lightTheme', async () => { + const newTheme = await getTheme('light'); + if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { + highlighter.loadTheme(newTheme); + } }); - await highlighter.loadLanguage({ - path: 'languages/aiscript.tmLanguage.json', - id: 'aiscript', - scopeName: 'source.aiscript', - aliases: ['is', 'ais'], + ColdDeviceStorage.watch('darkTheme', async () => { + const newTheme = await getTheme('dark'); + if (newTheme.name && !highlighter.getLoadedThemes().includes(newTheme.name)) { + highlighter.loadTheme(newTheme); + } }); _highlighter = highlighter; diff --git a/packages/frontend/src/scripts/collapsed.ts b/packages/frontend/src/scripts/collapsed.ts index 57e6ecf5b5..237bd37c7a 100644 --- a/packages/frontend/src/scripts/collapsed.ts +++ b/packages/frontend/src/scripts/collapsed.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/collect-page-vars.ts b/packages/frontend/src/scripts/collect-page-vars.ts index 79356e60eb..5096c0669e 100644 --- a/packages/frontend/src/scripts/collect-page-vars.ts +++ b/packages/frontend/src/scripts/collect-page-vars.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/color.ts b/packages/frontend/src/scripts/color.ts index 25ef41d9b7..a11255ffd1 100644 --- a/packages/frontend/src/scripts/color.ts +++ b/packages/frontend/src/scripts/color.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/confetti.ts b/packages/frontend/src/scripts/confetti.ts index b394ba3e2a..8e53a6ceeb 100644 --- a/packages/frontend/src/scripts/confetti.ts +++ b/packages/frontend/src/scripts/confetti.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/contains.ts b/packages/frontend/src/scripts/contains.ts index b50ce4128c..6137c06e85 100644 --- a/packages/frontend/src/scripts/contains.ts +++ b/packages/frontend/src/scripts/contains.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/copy-to-clipboard.ts b/packages/frontend/src/scripts/copy-to-clipboard.ts index 3884d4a20a..216c0464b3 100644 --- a/packages/frontend/src/scripts/copy-to-clipboard.ts +++ b/packages/frontend/src/scripts/copy-to-clipboard.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/device-kind.ts b/packages/frontend/src/scripts/device-kind.ts index 3843052a24..7c33f8ccee 100644 --- a/packages/frontend/src/scripts/device-kind.ts +++ b/packages/frontend/src/scripts/device-kind.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -11,6 +11,13 @@ const ua = navigator.userAgent.toLowerCase(); const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700); const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua); +const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; +// navigator.platform may be deprecated but this check is still required +const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; +const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; + +export const isFullscreenNotSupported = isIPhone || isIos; + export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind : isSmartphone ? 'smartphone' : isTablet ? 'tablet' diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend/src/scripts/emoji-base.ts index 46a13462a1..16a5a6aa5b 100644 --- a/packages/frontend/src/scripts/emoji-base.ts +++ b/packages/frontend/src/scripts/emoji-base.ts @@ -1,10 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ const twemojiSvgBase = '/twemoji'; const fluentEmojiPngBase = '/fluent-emoji'; +const tossfaceSvgBase = '/tossface'; export function char2twemojiFilePath(char: string): string { let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); @@ -23,3 +24,14 @@ export function char2fluentEmojiFilePath(char: string): string { const fileName = codes.map(x => x!.padStart(4, '0')).join('-'); return `${fluentEmojiPngBase}/${fileName}.png`; } + +export function char2tossfaceFilePath(char: string): string { + let codes = Array.from(char, x => x.codePointAt(0)?.toString(16)); + // Twemoji is the only emoji font which still supports the shibuya 50 emoji to this day + if (codes[0]?.startsWith('e50a')) return char2twemojiFilePath(char); + // Tossface does not use the fe0f modifier + codes = codes.filter(x => x !== 'fe0f'); + codes = codes.filter(x => x && x.length); + const fileName = codes.join('-'); + return `${tossfaceSvgBase}/${fileName}.svg`; +} diff --git a/packages/frontend/src/scripts/emoji-picker.ts b/packages/frontend/src/scripts/emoji-picker.ts index f87c3f6fb2..14b5cbf35e 100644 --- a/packages/frontend/src/scripts/emoji-picker.ts +++ b/packages/frontend/src/scripts/emoji-picker.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts index 8885bf4b7f..6565feba97 100644 --- a/packages/frontend/src/scripts/emojilist.ts +++ b/packages/frontend/src/scripts/emojilist.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -20,6 +20,10 @@ export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({ category: unicodeEmojiCategories[x[2]], })); +const unicodeEmojisMap = new Map<string, UnicodeEmojiDef>( + emojilist.map(x => [x.char, x]), +); + const _indexByChar = new Map<string, number>(); const _charGroupByCategory = new Map<string, string[]>(); for (let i = 0; i < emojilist.length; i++) { @@ -35,15 +39,33 @@ for (let i = 0; i < emojilist.length; i++) { export const emojiCharByCategory = _charGroupByCategory; -export function getEmojiName(char: string): string | null { - const idx = _indexByChar.get(char); - if (idx == null) { - return null; +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; diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts index 57b296ab2a..992f6e9a16 100644 --- a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts +++ b/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/extract-mentions.ts b/packages/frontend/src/scripts/extract-mentions.ts index fe60f9a851..89a5ce1df8 100644 --- a/packages/frontend/src/scripts/extract-mentions.ts +++ b/packages/frontend/src/scripts/extract-mentions.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ // test is located in test/extract-mentions -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; export function extractMentions(nodes: mfm.MfmNode[]): mfm.MfmMention['props'][] { // TODO: 重複を削除 diff --git a/packages/frontend/src/scripts/extract-url-from-mfm.ts b/packages/frontend/src/scripts/extract-url-from-mfm.ts index d2fbfbcc00..a4c84aa740 100644 --- a/packages/frontend/src/scripts/extract-url-from-mfm.ts +++ b/packages/frontend/src/scripts/extract-url-from-mfm.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import * as mfm from '@sharkey/sfm-js'; +import * as mfm from '@transfem-org/sfm-js'; import { unique } from '@/scripts/array.js'; // unique without hash diff --git a/packages/frontend/src/scripts/focus.ts b/packages/frontend/src/scripts/focus.ts index 6a31ebd431..ea6ee61c88 100644 --- a/packages/frontend/src/scripts/focus.ts +++ b/packages/frontend/src/scripts/focus.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/form.ts b/packages/frontend/src/scripts/form.ts index 222fd9b0b7..b0db404f28 100644 --- a/packages/frontend/src/scripts/form.ts +++ b/packages/frontend/src/scripts/form.ts @@ -1,36 +1,48 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -type EnumItem = string | {label: string; value: string;}; +type EnumItem = string | { + label: string; + value: string; +}; + export type FormItem = { label?: string; type: 'string'; default: string | null; + description?: string; + required?: boolean; hidden?: boolean; multiline?: boolean; + treatAsMfm?: boolean; } | { label?: string; type: 'number'; default: number | null; + description?: string; + required?: boolean; hidden?: boolean; step?: number; } | { label?: string; type: 'boolean'; default: boolean | null; + description?: string; hidden?: boolean; } | { label?: string; type: 'enum'; default: string | null; + required?: boolean; hidden?: boolean; enum: EnumItem[]; } | { label?: string; type: 'radio'; default: unknown | null; + required?: boolean; hidden?: boolean; options: { label: string; @@ -38,14 +50,28 @@ export type FormItem = { }[]; } | { label?: string; + type: 'range'; + default: number | null; + description?: string; + required?: boolean; + step?: number; + min: number; + max: number; + textConverter?: (value: number) => string; +} | { + label?: string; type: 'object'; default: Record<string, unknown> | null; - hidden: true; + hidden: boolean; } | { label?: string; type: 'array'; default: unknown[] | null; - hidden: true; + hidden: boolean; +} | { + type: 'button'; + content?: string; + action: (ev: MouseEvent, v: any) => void; }; export type Form = Record<string, FormItem>; @@ -55,6 +81,7 @@ type GetItemType<Item extends FormItem> = Item['type'] extends 'number' ? number : Item['type'] extends 'boolean' ? boolean : Item['type'] extends 'radio' ? unknown : + Item['type'] extends 'range' ? number : Item['type'] extends 'enum' ? string : Item['type'] extends 'array' ? unknown[] : Item['type'] extends 'object' ? Record<string, unknown> diff --git a/packages/frontend/src/scripts/format-time-string.ts b/packages/frontend/src/scripts/format-time-string.ts index 918996dd10..35ad77d982 100644 --- a/packages/frontend/src/scripts/format-time-string.ts +++ b/packages/frontend/src/scripts/format-time-string.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/gen-search-query.ts b/packages/frontend/src/scripts/gen-search-query.ts index 54654980f2..60884d08d3 100644 --- a/packages/frontend/src/scripts/gen-search-query.ts +++ b/packages/frontend/src/scripts/gen-search-query.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -18,7 +18,7 @@ export async function genSearchQuery(v: any, q: string) { host = at; } } else { - const user = await v.os.api('users/show', Misskey.acct.parse(at)).catch(x => null); + const user = await v.api('users/show', Misskey.acct.parse(at)).catch(x => null); if (user) { userId = user.id; } else { diff --git a/packages/frontend/src/scripts/get-account-from-id.ts b/packages/frontend/src/scripts/get-account-from-id.ts index 346d283572..40afa10f2d 100644 --- a/packages/frontend/src/scripts/get-account-from-id.ts +++ b/packages/frontend/src/scripts/get-account-from-id.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/get-drive-file-menu.ts b/packages/frontend/src/scripts/get-drive-file-menu.ts index d6a5b00c0b..a883404307 100644 --- a/packages/frontend/src/scripts/get-drive-file-menu.ts +++ b/packages/frontend/src/scripts/get-drive-file-menu.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -8,6 +8,7 @@ import { defineAsyncComponent } from 'vue'; import { i18n } from '@/i18n.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { MenuItem } from '@/types/menu.js'; import { defaultStore } from '@/store.js'; @@ -18,7 +19,7 @@ function rename(file: Misskey.entities.DriveFile) { default: file.name, }).then(({ canceled, result: name }) => { if (canceled) return; - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, name: name, }); @@ -31,7 +32,7 @@ function describe(file: Misskey.entities.DriveFile) { file: file, }, { done: caption => { - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, comment: caption.length === 0 ? null : caption, }); @@ -40,7 +41,7 @@ function describe(file: Misskey.entities.DriveFile) { } function toggleSensitive(file: Misskey.entities.DriveFile) { - os.api('drive/files/update', { + misskeyApi('drive/files/update', { fileId: file.id, isSensitive: !file.isSensitive, }).catch(err => { @@ -65,11 +66,11 @@ function addApp() { async function deleteFile(file: Misskey.entities.DriveFile) { const { canceled } = await os.confirm({ type: 'warning', - text: i18n.t('driveFileDeleteConfirm', { name: file.name }), + text: i18n.tsx.driveFileDeleteConfirm({ name: file.name }), }); if (canceled) return; - os.api('drive/files/delete', { + misskeyApi('drive/files/delete', { fileId: file.id, }); } @@ -103,7 +104,7 @@ export function getDriveFileMenu(file: Misskey.entities.DriveFile, folder?: Miss }), }] : [], { type: 'divider' }, { text: i18n.ts.createNoteFromTheFile, - icon: 'ph-pencil ph-bold ph-lg', + icon: 'ph-pencil-simple ph-bold ph-lg', action: () => os.post({ initialFiles: [file], }), diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts index a409f1b775..40359a88bb 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -1,15 +1,16 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import { defineAsyncComponent, Ref } from 'vue'; +import { defineAsyncComponent, Ref, ShallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import { claimAchievement } from './achievements.js'; import { $i } from '@/account.js'; import { i18n } from '@/i18n.js'; import { instance } from '@/instance.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { url } from '@/config.js'; import { defaultStore, noteActions } from '@/store.js'; @@ -35,18 +36,18 @@ export async function getNoteClipMenu(props: { const appearNote = isRenote ? props.note.renote as Misskey.entities.Note : props.note; const clips = await clipsCache.fetch(); - return [...clips.map(clip => ({ + const menu: MenuItem[] = [...clips.map(clip => ({ text: clip.name, action: () => { claimAchievement('noteClipped1'); os.promiseDialog( - os.api('clips/add-note', { clipId: clip.id, noteId: appearNote.id }), + misskeyApi('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 }), + text: i18n.tsx.confirmToUnclipAlreadyClippedNote({ name: clip.name }), }); if (!confirm.canceled) { os.apiWithDialog('clips/remove-note', { clipId: clip.id, noteId: appearNote.id }); @@ -92,6 +93,8 @@ export async function getNoteClipMenu(props: { os.apiWithDialog('clips/add-note', { clipId: clip.id, noteId: appearNote.id }); }, }]; + + return menu; } export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): MenuItem { @@ -99,10 +102,13 @@ export function getAbuseNoteMenu(note: Misskey.entities.Note, text: string): Men icon: 'ph-warning-circle ph-bold ph-lg', text, action: (): void => { - const u = note.url ?? note.uri ?? `${url}/notes/${note.id}`; + const localUrl = `${url}/notes/${note.id}`; + let noteInfo = ''; + if (note.url ?? note.uri != null) noteInfo = `Note: ${note.url ?? note.uri}\n`; + noteInfo += `Local Note: ${localUrl}\n`; os.popup(defineAsyncComponent(() => import('@/components/MkAbuseReportWindow.vue')), { user: note.user, - initialComment: `Note: ${u}\n-----\n`, + initialComment: `${noteInfo}-----\n`, }, {}, 'closed'); }, }; @@ -132,7 +138,6 @@ export function getCopyNoteOriginLinkMenu(note: misskey.entities.Note, text: str export function getNoteMenu(props: { note: Misskey.entities.Note; - menuButton: Ref<HTMLElement>; translation: Ref<Misskey.entities.NotesTranslateResponse | null>; translating: Ref<boolean>; isDeleted: Ref<boolean>; @@ -156,7 +161,7 @@ export function getNoteMenu(props: { }).then(({ canceled }) => { if (canceled) return; - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: appearNote.id, }); @@ -173,7 +178,7 @@ export function getNoteMenu(props: { }).then(({ canceled }) => { if (canceled) return; - os.api('notes/delete', { + misskeyApi('notes/delete', { noteId: appearNote.id, }); @@ -252,7 +257,7 @@ export function getNoteMenu(props: { function share(): void { navigator.share({ - title: i18n.t('noteOf', { user: appearNote.user.name }), + title: i18n.tsx.noteOf({ user: appearNote.user.name }), text: appearNote.text, url: `${url}/notes/${appearNote.id}`, }); @@ -265,7 +270,7 @@ export function getNoteMenu(props: { async function translate(): Promise<void> { if (props.translation.value != null) return; props.translating.value = true; - const res = await os.api('notes/translate', { + const res = await misskeyApi('notes/translate', { noteId: appearNote.id, targetLang: miLocalStorage.getItem('lang') ?? navigator.language, }); @@ -275,7 +280,7 @@ export function getNoteMenu(props: { let menu: MenuItem[]; if ($i) { - const statePromise = os.api('notes/state', { + const statePromise = misskeyApi('notes/state', { noteId: appearNote.id, }); @@ -296,7 +301,7 @@ export function getNoteMenu(props: { text: i18n.ts.copyContent, action: copyContent, }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? + , (appearNote.url || appearNote.uri) ? getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') : undefined, (appearNote.url || appearNote.uri) ? { @@ -355,7 +360,7 @@ export function getNoteMenu(props: { icon: 'ph-user ph-bold ph-lg', text: i18n.ts.user, children: async () => { - const user = appearNote.userId === $i?.id ? $i : await os.api('users/show', { userId: appearNote.userId }); + const user = appearNote.userId === $i?.id ? $i : await misskeyApi('users/show', { userId: appearNote.userId }); const { menu, cleanup } = getUserMenu(user); cleanups.push(cleanup); return menu; @@ -377,19 +382,55 @@ export function getNoteMenu(props: { ] : [] ), + ...(appearNote.channel && (appearNote.channel.userId === $i.id || $i.isModerator || $i.isAdmin) ? [ + { type: 'divider' }, + { + type: 'parent' as const, + icon: 'ti ti-device-tv', + text: i18n.ts.channel, + children: async () => { + const channelChildMenu = [] as MenuItem[]; + + const channel = await misskeyApi('channels/show', { channelId: appearNote.channel!.id }); + + if (channel.pinnedNoteIds.includes(appearNote.id)) { + channelChildMenu.push({ + icon: 'ti ti-pinned-off', + text: i18n.ts.unpin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: channel.pinnedNoteIds.filter(id => id !== appearNote.id), + }), + }); + } else { + channelChildMenu.push({ + icon: 'ti ti-pin', + text: i18n.ts.pin, + action: () => os.apiWithDialog('channels/update', { + channelId: appearNote.channel!.id, + pinnedNoteIds: [...channel.pinnedNoteIds, appearNote.id], + }), + }); + } + return channelChildMenu; + }, + }, + ] + : [] + ), ...(appearNote.userId === $i.id || $i.isModerator || $i.isAdmin ? [ { type: 'divider' }, appearNote.userId === $i.id ? { - icon: 'ph-pencil ph-bold ph-lg', + icon: 'ph-pencil-simple ph-bold ph-lg', text: i18n.ts.edit, action: edit, } : undefined, { - icon: 'ph-pencil-line ph-bold ph-lg', + icon: 'ph-pencil-simple-line ph-bold ph-lg', text: i18n.ts.deleteAndEdit, danger: true, action: delEdit, - }, + }, { icon: 'ph-trash ph-bold ph-lg', text: i18n.ts.delete, @@ -409,7 +450,7 @@ export function getNoteMenu(props: { text: i18n.ts.copyContent, action: copyContent, }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink) - , (appearNote.url || appearNote.uri) ? + , (appearNote.url || appearNote.uri) ? getCopyNoteOriginLinkMenu(appearNote, 'Copy link (Origin)') : undefined, (appearNote.url || appearNote.uri) ? { @@ -468,7 +509,7 @@ function smallerVisibility(a: Visibility | string, b: Visibility | string): Visi export function getRenoteMenu(props: { note: Misskey.entities.Note; - renoteButton: Ref<HTMLElement>; + renoteButton: ShallowRef<HTMLElement | undefined>; mock?: boolean; }) { const isRenote = ( @@ -488,7 +529,7 @@ export function getRenoteMenu(props: { text: i18n.ts.inChannelRenote, icon: 'ti ti-repeat', action: () => { - const el = props.renoteButton.value as HTMLElement | null | undefined; + const el = props.renoteButton.value; if (el) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); @@ -497,7 +538,7 @@ export function getRenoteMenu(props: { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { renoteId: appearNote.id, channelId: appearNote.channelId, }).then(() => { @@ -524,7 +565,7 @@ export function getRenoteMenu(props: { text: i18n.ts.renote, icon: 'ti ti-repeat', action: () => { - const el = props.renoteButton.value as HTMLElement | null | undefined; + const el = props.renoteButton.value; if (el) { const rect = el.getBoundingClientRect(); const x = rect.left + (el.offsetWidth / 2); @@ -542,7 +583,7 @@ export function getRenoteMenu(props: { } if (!props.mock) { - os.api('notes/create', { + misskeyApi('notes/create', { localOnly, visibility, renoteId: appearNote.id, @@ -564,7 +605,7 @@ export function getRenoteMenu(props: { const renoteItems = [ ...normalRenoteItems, - ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] : [], + ...(channelRenoteItems.length > 0 && normalRenoteItems.length > 0) ? [{ type: 'divider' }] as MenuItem[] : [], ...channelRenoteItems, ]; diff --git a/packages/frontend/src/scripts/get-note-summary.ts b/packages/frontend/src/scripts/get-note-summary.ts index 1fd9f04d46..6fd9947ac1 100644 --- a/packages/frontend/src/scripts/get-note-summary.ts +++ b/packages/frontend/src/scripts/get-note-summary.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,7 +10,11 @@ import { i18n } from '@/i18n.js'; * 投稿を表す文字列を取得します。 * @param {*} note (packされた)投稿 */ -export const getNoteSummary = (note: Misskey.entities.Note): string => { +export const getNoteSummary = (note?: Misskey.entities.Note | null): string => { + if (note == null) { + return ''; + } + if (note.deletedAt) { return `(${i18n.ts.deletedNote})`; } @@ -30,7 +34,7 @@ export const getNoteSummary = (note: Misskey.entities.Note): string => { // ファイルが添付されているとき if ((note.files || []).length !== 0) { - summary += ` (${i18n.t('withNFiles', { n: note.files.length })})`; + summary += ` (${i18n.tsx.withNFiles({ n: note.files.length })})`; } // 投票が添付されているとき diff --git a/packages/frontend/src/scripts/get-note-versions-menu.ts b/packages/frontend/src/scripts/get-note-versions-menu.ts index 46e3bab3a7..84292f1277 100644 --- a/packages/frontend/src/scripts/get-note-versions-menu.ts +++ b/packages/frontend/src/scripts/get-note-versions-menu.ts @@ -2,6 +2,7 @@ import { Ref, defineAsyncComponent } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from './misskey-api.js'; import { MenuItem } from '@/types/menu.js'; import { dateTimeFormat } from './intl-const.js'; @@ -30,7 +31,7 @@ export async function getNoteVersionsMenu(props: { } const menu: MenuItem[] = []; - const statePromise = os.api('notes/versions', { + const statePromise = misskeyApi('notes/versions', { noteId: appearNote.id, }); @@ -39,9 +40,9 @@ export async function getNoteVersionsMenu(props: { const _time = edit.oldDate == null ? NaN : typeof edit.oldDate === 'number' ? edit.oldDate : (edit.oldDate instanceof Date ? edit.oldDate : new Date(edit.oldDate)).getTime(); - + menu.push({ - icon: 'ph-pencil ph-bold ph-lg', + icon: 'ph-pencil-simple ph-bold ph-lg', text: _time ? dateTimeFormat.format(_time) : dateTimeFormat.format(new Date(edit.updatedAt)), action: () => openVersion(edit), }); diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts index 67bc781aef..61f9a453dc 100644 --- a/packages/frontend/src/scripts/get-user-menu.ts +++ b/packages/frontend/src/scripts/get-user-menu.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,13 +10,14 @@ import { i18n } from '@/i18n.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import { host, url } from '@/config.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { defaultStore, userActions } from '@/store.js'; import { $i, iAmModerator } from '@/account.js'; -import { mainRouter } from '@/router.js'; -import { Router } from '@/nirax.js'; +import { IRouter } from '@/nirax.js'; import { antennasCache, rolesCache, userListsCache } from '@/cache.js'; +import { mainRouter } from '@/router/main.js'; -export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router = mainRouter) { +export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) { const meId = $i ? $i.id : null; const cleanups = [] as (() => void)[]; @@ -131,7 +132,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router } async function editMemo(): Promise<void> { - const userDetailed = await os.api('users/show', { + const userDetailed = await misskeyApi('users/show', { userId: user.id, }); const { canceled, result } = await os.form(i18n.ts.editMemo, { @@ -169,20 +170,21 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router action: () => { copyToClipboard(`${user.host ?? host}/@${user.username}.atom`); }, - }, { + }, ...(user.host != null && user.url != null ? [{ + icon: 'ph-share ph-bold ph-lg', + text: i18n.ts.showOnRemote, + action: () => { + if (user.url == null) return; + window.open(user.url, '_blank', 'noopener'); + }, + }] : []), { icon: 'ph-share-network ph-bold ph-lg', text: i18n.ts.copyProfileUrl, action: () => { const canonical = user.host === null ? `@${user.username}` : `@${user.username}@${toUnicode(user.host)}`; copyToClipboard(`${url}/${canonical}`); }, - }, ...(user.host ? [{ - icon: 'ph-share ph-bold ph-lg', - text: i18n.ts.openRemoteProfile, - action: () => { - open(`${user.uri}`, '_blank'); - }, - }] : []), { + }, { icon: 'ph-envelope ph-bold ph-lg', text: i18n.ts.sendMessage, action: () => { @@ -190,7 +192,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router os.post({ specified: user, initialText: `${canonical} ` }); }, }, { type: 'divider' }, { - icon: 'ph-pencil ph-bold ph-lg', + icon: 'ph-pencil-simple ph-bold ph-lg', text: i18n.ts.editMemo, action: () => { editMemo(); @@ -362,7 +364,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: Router if ($i && meId === user.id) { menu = menu.concat([{ type: 'divider' }, { - icon: 'ph-pencil ph-bold ph-lg', + icon: 'ph-pencil-simple ph-bold ph-lg', text: i18n.ts.editProfile, action: () => { router.push('/settings/profile'); diff --git a/packages/frontend/src/scripts/get-user-name.ts b/packages/frontend/src/scripts/get-user-name.ts index 3ae80d7fc3..56e91abba0 100644 --- a/packages/frontend/src/scripts/get-user-name.ts +++ b/packages/frontend/src/scripts/get-user-name.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/hotkey.ts b/packages/frontend/src/scripts/hotkey.ts index 48c80c066b..0600bff893 100644 --- a/packages/frontend/src/scripts/hotkey.ts +++ b/packages/frontend/src/scripts/hotkey.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend/src/scripts/i18n.ts index 8e5f17f38a..c2f44a33cc 100644 --- a/packages/frontend/src/scripts/i18n.ts +++ b/packages/frontend/src/scripts/i18n.ts @@ -1,34 +1,294 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ +import type { ILocale, ParameterizedString } from '../../../../locales/index.js'; -export class I18n<T extends Record<string, any>> { - public ts: T; +type FlattenKeys<T extends ILocale, TPrediction> = keyof { + [K in keyof T as T[K] extends ILocale + ? FlattenKeys<T[K], TPrediction> extends infer C extends string + ? `${K & string}.${C}` + : never + : T[K] extends TPrediction + ? K + : never]: T[K]; +}; - constructor(locale: T) { - this.ts = locale; +type ParametersOf<T extends ILocale, TKey extends FlattenKeys<T, ParameterizedString>> = TKey extends `${infer K}.${infer C}` + // @ts-expect-error -- C は明らかに FlattenKeys<T[K], ParameterizedString> になるが、型システムはここでは TKey がドット区切りであることのコンテキストを持たないので、型システムに合法にて示すことはできない。 + ? ParametersOf<T[K], C> + : TKey extends keyof T + ? T[TKey] extends ParameterizedString<infer P> + ? P + : never + : never; +type Tsx<T extends ILocale> = { + readonly [K in keyof T as T[K] extends string ? never : K]: T[K] extends ParameterizedString<infer P> + ? (arg: { readonly [_ in P]: string | number }) => string + // @ts-expect-error -- 証明省略 + : Tsx<T[K]>; +}; + +export class I18n<T extends ILocale> { + private tsxCache?: Tsx<T>; + + constructor(public locale: T) { //#region BIND this.t = this.t.bind(this); //#endregion } - // string にしているのは、ドット区切りでのパス指定を許可するため - // なるべくこのメソッド使うよりもlocale直接参照の方がvueのキャッシュ効いてパフォーマンスが良いかも - 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; + public get ts(): T { + if (_DEV_) { + class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> { + get(target: TTarget, p: string | symbol): unknown { + const value = target[p as keyof TTarget]; + + if (typeof value === 'object') { + return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>()); + } + + 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<T> { + if (_DEV_) { + if (this.tsxCache) { + return this.tsxCache; + } + + class Handler<TTarget extends ILocale> implements ProxyHandler<TTarget> { + get(target: TTarget, p: string | symbol): unknown { + const value = target[p as keyof TTarget]; + + if (typeof value === 'object') { + return new Proxy(value, new Handler<TTarget[keyof TTarget] & ILocale>()); + } + + 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<T>; + } + + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + if (this.tsxCache) { + return this.tsxCache; + } + + function build(target: ILocale): Tsx<T> { + const result = {} as Tsx<T>; + + 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<TKey extends FlattenKeys<T, string>>(key: TKey): string; + /** + * @deprecated なるべくこのメソッド使うよりも tsx 直接参照の方が vue のキャッシュ効いてパフォーマンスが良いかも + */ + public t<TKey extends FlattenKeys<T, ParameterizedString>>(key: TKey, args: { readonly [_ in ParametersOf<T, TKey>]: 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 (args) { - for (const [k, v] of Object.entries(args)) { - str = str.replace(`{${k}}`, v.toString()); + if (_DEV_) { + if (typeof str === 'undefined') { + console.error(`Unexpected locale key: ${key}`); + return key; } } - return str; - } catch (err) { - console.warn(`missing localization '${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; } } + +if (import.meta.vitest) { + const { describe, expect, it } = import.meta.vitest; + + describe('i18n', () => { + it('t', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.t('foo')).toBe('foo'); + expect(i18n.t('bar.baz')).toBe('baz'); + expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); + expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); + }); + it('ts', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.ts.foo).toBe('foo'); + expect(i18n.ts.bar.baz).toBe('baz'); + }); + it('tsx', () => { + const i18n = new I18n({ + foo: 'foo', + bar: { + baz: 'baz', + qux: 'qux {0}' as unknown as ParameterizedString<'0'>, + quux: 'quux {0} {1}' as unknown as ParameterizedString<'0' | '1'>, + }, + }); + + expect(i18n.tsx.bar.qux({ 0: 'hoge' })).toBe('qux hoge'); + expect(i18n.tsx.bar.quux({ 0: 'hoge', 1: 'fuga' })).toBe('quux hoge fuga'); + }); + }); +} diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts index a20cfcb1d0..1ca0990ba9 100644 --- a/packages/frontend/src/scripts/idb-proxy.ts +++ b/packages/frontend/src/scripts/idb-proxy.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/idle-render.ts b/packages/frontend/src/scripts/idle-render.ts index ac1be50c73..6adfedcb9f 100644 --- a/packages/frontend/src/scripts/idle-render.ts +++ b/packages/frontend/src/scripts/idle-render.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts index ebf27667d7..2465a14703 100644 --- a/packages/frontend/src/scripts/init-chart.ts +++ b/packages/frontend/src/scripts/init-chart.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/initialize-sw.ts b/packages/frontend/src/scripts/initialize-sw.ts index 007fc0f2f7..1517e4e1e8 100644 --- a/packages/frontend/src/scripts/initialize-sw.ts +++ b/packages/frontend/src/scripts/initialize-sw.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/install-plugin.ts b/packages/frontend/src/scripts/install-plugin.ts index 1310a0dc73..15b0cedc79 100644 --- a/packages/frontend/src/scripts/install-plugin.ts +++ b/packages/frontend/src/scripts/install-plugin.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,6 +10,7 @@ import { Interpreter, Parser, utils } from '@syuilo/aiscript'; import type { Plugin } from '@/store.js'; import { ColdDeviceStorage } from '@/store.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; export type AiScriptPluginMeta = { @@ -63,7 +64,11 @@ export async function parsePluginMeta(code: string): Promise<AiScriptPluginMeta> try { ast = parser.parse(code); } catch (err) { - throw new Error('Aiscript syntax error'); + if (err instanceof Error) { + throw new Error(`Aiscript syntax error\n${(err as Error).message}`); + } else { + throw new Error('Aiscript syntax error'); + } } const meta = Interpreter.collectMetadata(ast); @@ -110,7 +115,7 @@ export async function installPlugin(code: string, meta?: AiScriptPluginMeta) { }, { done: async result => { const { name, permissions } = result; - const { token } = await os.api('miauth/gen-token', { + const { token } = await misskeyApi('miauth/gen-token', { session: null, name: name, permission: permissions, diff --git a/packages/frontend/src/scripts/install-theme.ts b/packages/frontend/src/scripts/install-theme.ts index 394b642bf4..866f1225bf 100644 --- a/packages/frontend/src/scripts/install-theme.ts +++ b/packages/frontend/src/scripts/install-theme.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/intl-const.ts b/packages/frontend/src/scripts/intl-const.ts index ea16c9c2ae..aaa4f0a86e 100644 --- a/packages/frontend/src/scripts/intl-const.ts +++ b/packages/frontend/src/scripts/intl-const.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -33,6 +33,10 @@ try { } export const dateTimeFormat = _dateTimeFormat; +export const timeZone = dateTimeFormat.resolvedOptions().timeZone; + +export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N'; + let _numberFormat: Intl.NumberFormat; try { _numberFormat = new Intl.NumberFormat(versatileLang); diff --git a/packages/frontend/src/scripts/is-device-darkmode.ts b/packages/frontend/src/scripts/is-device-darkmode.ts index badc295726..4f487c7cb9 100644 --- a/packages/frontend/src/scripts/is-device-darkmode.ts +++ b/packages/frontend/src/scripts/is-device-darkmode.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/isFfVisibleForMe.ts b/packages/frontend/src/scripts/isFfVisibleForMe.ts index dc0e90d20a..406404c462 100644 --- a/packages/frontend/src/scripts/isFfVisibleForMe.ts +++ b/packages/frontend/src/scripts/isFfVisibleForMe.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/keycode.ts b/packages/frontend/src/scripts/keycode.ts index 57bc4d19ba..bc1f485f5e 100644 --- a/packages/frontend/src/scripts/keycode.ts +++ b/packages/frontend/src/scripts/keycode.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/langmap.ts b/packages/frontend/src/scripts/langmap.ts index 3912d58d82..b32de15963 100644 --- a/packages/frontend/src/scripts/langmap.ts +++ b/packages/frontend/src/scripts/langmap.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/login-id.ts b/packages/frontend/src/scripts/login-id.ts index fe0e17e66e..b52735caa0 100644 --- a/packages/frontend/src/scripts/login-id.ts +++ b/packages/frontend/src/scripts/login-id.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/lookup-user.ts b/packages/frontend/src/scripts/lookup-user.ts index a35fe898e4..efc9132e75 100644 --- a/packages/frontend/src/scripts/lookup-user.ts +++ b/packages/frontend/src/scripts/lookup-user.ts @@ -1,11 +1,12 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; export async function lookupUser() { const { canceled, result } = await os.inputText({ @@ -17,8 +18,8 @@ export async function lookupUser() { os.pageWindow(`/admin/user/${user.id}`); }; - const usernamePromise = os.api('users/show', Misskey.acct.parse(result)); - const idPromise = os.api('users/show', { userId: result }); + const usernamePromise = misskeyApi('users/show', Misskey.acct.parse(result)); + const idPromise = misskeyApi('users/show', { userId: result }); let _notFound = false; const notFound = () => { if (_notFound) { diff --git a/packages/frontend/src/scripts/lookup.ts b/packages/frontend/src/scripts/lookup.ts index 979f40f038..7f020b15cc 100644 --- a/packages/frontend/src/scripts/lookup.ts +++ b/packages/frontend/src/scripts/lookup.ts @@ -1,12 +1,13 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { i18n } from '@/i18n.js'; -import { mainRouter } from '@/router.js'; import { Router } from '@/nirax.js'; +import { mainRouter } from '@/router/main.js'; export async function lookup(router?: Router) { const _router = router ?? mainRouter; @@ -28,7 +29,7 @@ export async function lookup(router?: Router) { } if (query.startsWith('https://')) { - const promise = os.api('ap/show', { + const promise = misskeyApi('ap/show', { uri: query, }); diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts index 559e61211d..099a22163a 100644 --- a/packages/frontend/src/scripts/media-proxy.ts +++ b/packages/frontend/src/scripts/media-proxy.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/merge.ts b/packages/frontend/src/scripts/merge.ts new file mode 100644 index 0000000000..4e39a0fa06 --- /dev/null +++ b/packages/frontend/src/scripts/merge.ts @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +import { deepClone } from './clone.js'; +import type { Cloneable } from './clone.js'; + +type DeepPartial<T> = { + [P in keyof T]?: T[P] extends Record<string | number | symbol, unknown> ? DeepPartial<T[P]> : T[P]; +}; + +function isPureObject(value: unknown): value is Record<string | number | symbol, unknown> { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** + * valueにないキーをdefからもらう(再帰的)\ + * nullはそのまま、undefinedはdefの値 + **/ +export function deepMerge<X extends Record<string | number | symbol, unknown>>(value: DeepPartial<X>, def: X): X { + if (isPureObject(value) && isPureObject(def)) { + const result = deepClone(value as Cloneable) as X; + for (const [k, v] of Object.entries(def) as [keyof X, X[keyof X]][]) { + if (!Object.prototype.hasOwnProperty.call(value, k) || value[k] === undefined) { + result[k] = v; + } else if (isPureObject(v) && isPureObject(result[k])) { + const child = deepClone(result[k] as Cloneable) as DeepPartial<X[keyof X] & Record<string | number | symbol, unknown>>; + result[k] = deepMerge<typeof v>(child, v); + } + } + return result; + } + throw new Error('deepMerge: value and def must be pure objects'); +} diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts index 6e25cc856c..36de146c27 100644 --- a/packages/frontend/src/scripts/mfm-function-picker.ts +++ b/packages/frontend/src/scripts/mfm-function-picker.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/api.ts b/packages/frontend/src/scripts/misskey-api.ts index 8f3a163938..49fb6f9e59 100644 --- a/packages/frontend/src/scripts/api.ts +++ b/packages/frontend/src/scripts/misskey-api.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -10,12 +10,17 @@ import { $i } from '@/account.js'; export const pendingApiRequestsCount = ref(0); // Implements Misskey.api.ApiClient.request -export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>( +export function misskeyApi< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, +>( endpoint: E, data: P = {} as any, token?: string | null | undefined, signal?: AbortSignal, -): Promise<Misskey.api.SwitchCaseResponseType<E, P>> { +): Promise<_ResT> { if (endpoint.includes('://')) throw new Error('invalid endpoint'); pendingApiRequestsCount.value++; @@ -23,7 +28,7 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin pendingApiRequestsCount.value--; }; - const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => { + const promise = new Promise<_ResT>((resolve, reject) => { // Append a credential if ($i) (data as any).i = $i.token; if (token !== undefined) (data as any).i = token; @@ -44,7 +49,7 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin if (res.status === 200) { resolve(body); } else if (res.status === 204) { - resolve(); + resolve(undefined as _ResT); // void -> undefined } else { reject(body.error); } @@ -57,10 +62,15 @@ export function api<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoin } // Implements Misskey.api.ApiClient.request -export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req']>( +export function misskeyApiGet< + ResT = void, + E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, + P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'], + _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, +>( endpoint: E, data: P = {} as any, -): Promise<Misskey.api.SwitchCaseResponseType<E, P>> { +): Promise<_ResT> { pendingApiRequestsCount.value++; const onFinally = () => { @@ -69,7 +79,7 @@ export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endp const query = new URLSearchParams(data as any); - const promise = new Promise<Misskey.Endpoints[E]['res'] | void>((resolve, reject) => { + const promise = new Promise<_ResT>((resolve, reject) => { // Send request window.fetch(`${apiUrl}/${endpoint}?${query}`, { method: 'GET', @@ -81,7 +91,7 @@ export function apiGet<E extends keyof Misskey.Endpoints, P extends Misskey.Endp if (res.status === 200) { resolve(body); } else if (res.status === 204) { - resolve(); + resolve(undefined as _ResT); // void -> undefined } else { reject(body.error); } diff --git a/packages/frontend/src/scripts/navigator.ts b/packages/frontend/src/scripts/navigator.ts index b13186a10e..ffc0a457f4 100644 --- a/packages/frontend/src/scripts/navigator.ts +++ b/packages/frontend/src/scripts/navigator.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/nyaize.ts b/packages/frontend/src/scripts/nyaize.ts index 62833b4de3..58ed88fed1 100644 --- a/packages/frontend/src/scripts/nyaize.ts +++ b/packages/frontend/src/scripts/nyaize.ts @@ -1,23 +1,28 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * 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; +function ifAfter(prefix, fn) { + const preLen = prefix.length; + const regex = new RegExp(prefix,'i'); + return (x,pos,string) => { + return pos > 0 && string.substring(pos-preLen,pos).match(regex) ? fn(x) : x; + }; +} + 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') + .replace(/a/gi, ifAfter('n', x => x === 'A' ? 'YA' : 'ya')) + .replace(/ing/gi, ifAfter('morn', x => x === 'ING' ? 'YAN' : 'yan')) + .replace(/one/gi, ifAfter('every', x => x === 'ONE' ? 'NYAN' : 'nyan')) // ko-KR .replace(koRegex1, match => String.fromCharCode( match.charCodeAt(0)! + '냐'.charCodeAt(0) - '나'.charCodeAt(0), diff --git a/packages/frontend/src/scripts/page-metadata.ts b/packages/frontend/src/scripts/page-metadata.ts index 369e46aae1..0e3b093ecf 100644 --- a/packages/frontend/src/scripts/page-metadata.ts +++ b/packages/frontend/src/scripts/page-metadata.ts @@ -1,13 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ 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'); +import { MaybeRefOrGetter, Ref, inject, isRef, onActivated, onBeforeUnmount, provide, ref, toValue, watch } from 'vue'; export type PageMetadata = { title: string; @@ -18,29 +15,56 @@ export type PageMetadata = { needWideArea?: boolean; }; -export function definePageMetadata(metadata: PageMetadata | null | Ref<PageMetadata | null> | ComputedRef<PageMetadata | null>): void { - const _metadata = isRef(metadata) ? metadata : ref(metadata); +type PageMetadataGetter = () => PageMetadata; +type PageMetadataReceiver = (getter: PageMetadataGetter) => void; - provide(pageMetadataProvider, _metadata); +const RECEIVER_KEY = Symbol('ReceiverKey'); +const setReceiver = (v: PageMetadataReceiver): void => { + provide<PageMetadataReceiver>(RECEIVER_KEY, v); +}; +const getReceiver = (): PageMetadataReceiver | undefined => { + return inject<PageMetadataReceiver>(RECEIVER_KEY); +}; - const set = inject(setPageMetadata) as any; - if (set) { - set(_metadata); +const METADATA_KEY = Symbol('MetadataKey'); +const setMetadata = (v: Ref<PageMetadata | null>): void => { + provide<Ref<PageMetadata | null>>(METADATA_KEY, v); +}; +const getMetadata = (): Ref<PageMetadata | null> | undefined => { + return inject<Ref<PageMetadata | null>>(METADATA_KEY); +}; - onMounted(() => { - set(_metadata); - }); +export const definePageMetadata = (maybeRefOrGetterMetadata: MaybeRefOrGetter<PageMetadata>): void => { + const metadataRef = ref(toValue(maybeRefOrGetterMetadata)); + const metadataGetter = () => metadataRef.value; + const receiver = getReceiver(); - onActivated(() => { - set(_metadata); - }); - } -} + // setup handler + receiver?.(metadataGetter); -export function provideMetadataReceiver(callback: (info: ComputedRef<PageMetadata>) => void): void { - provide(setPageMetadata, callback); -} + // update handler + onBeforeUnmount(watch( + () => toValue(maybeRefOrGetterMetadata), + (metadata) => { + metadataRef.value = metadata; + receiver?.(metadataGetter); + }, + { deep: true }, + )); + onActivated(() => { + receiver?.(metadataGetter); + }); +}; -export function injectPageMetadata(): PageMetadata | undefined { - return inject(pageMetadataProvider); -} +export const provideMetadataReceiver = (receiver: PageMetadataReceiver): void => { + setReceiver(receiver); +}; + +export const provideReactiveMetadata = (metadataRef: Ref<PageMetadata | null>): void => { + setMetadata(metadataRef); +}; + +export const injectReactiveMetadata = (): Ref<PageMetadata | null> => { + const metadataRef = getMetadata(); + return isRef(metadataRef) ? metadataRef : ref(null); +}; diff --git a/packages/frontend/src/scripts/physics.ts b/packages/frontend/src/scripts/physics.ts index cf9fad70eb..8a4e9319b3 100644 --- a/packages/frontend/src/scripts/physics.ts +++ b/packages/frontend/src/scripts/physics.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/please-login.ts b/packages/frontend/src/scripts/please-login.ts index e6c08dfbc0..9e51272791 100644 --- a/packages/frontend/src/scripts/please-login.ts +++ b/packages/frontend/src/scripts/please-login.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts index 0c2ff16992..1caa2dfc21 100644 --- a/packages/frontend/src/scripts/popout.ts +++ b/packages/frontend/src/scripts/popout.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/popup-position.ts b/packages/frontend/src/scripts/popup-position.ts index 0a799c5665..8c9e3c02c3 100644 --- a/packages/frontend/src/scripts/popup-position.ts +++ b/packages/frontend/src/scripts/popup-position.ts @@ -1,10 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ export function calcPopupPosition(el: HTMLElement, props: { - anchorElement: HTMLElement | null; + anchorElement?: HTMLElement | null; innerMargin: number; direction: 'top' | 'bottom' | 'left' | 'right'; align: 'top' | 'bottom' | 'left' | 'right' | 'center'; diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts index 80441caf15..31a9ac1ad9 100644 --- a/packages/frontend/src/scripts/post-message.ts +++ b/packages/frontend/src/scripts/post-message.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/reaction-picker.ts b/packages/frontend/src/scripts/reaction-picker.ts index 9b13e794f5..7aec05c0cf 100644 --- a/packages/frontend/src/scripts/reaction-picker.ts +++ b/packages/frontend/src/scripts/reaction-picker.ts @@ -1,8 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ +import * as Misskey from 'misskey-js'; import { defineAsyncComponent, Ref, ref } from 'vue'; import { popup } from '@/os.js'; import { defaultStore } from '@/store.js'; @@ -10,6 +11,7 @@ import { defaultStore } from '@/store.js'; class ReactionPicker { private src: Ref<HTMLElement | null> = ref(null); private manualShowing = ref(false); + private targetNote: Ref<Misskey.entities.Note | null> = ref(null); private onChosen?: (reaction: string) => void; private onClosed?: () => void; @@ -23,6 +25,7 @@ class ReactionPicker { src: this.src, pinnedEmojis: reactionsRef, asReactionPicker: true, + targetNote: this.targetNote, manualShowing: this.manualShowing, }, { done: reaction => { @@ -38,8 +41,9 @@ class ReactionPicker { }); } - public show(src: HTMLElement, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) { + public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) { this.src.value = src; + this.targetNote.value = targetNote; this.manualShowing.value = true; this.onChosen = onChosen; this.onClosed = onClosed; diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts new file mode 100644 index 0000000000..6bfcef6c36 --- /dev/null +++ b/packages/frontend/src/scripts/safe-parse.ts @@ -0,0 +1,11 @@ +/* + * 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 index 625d8c34a7..0edf4e9eba 100644 --- a/packages/frontend/src/scripts/safe-uri-decode.ts +++ b/packages/frontend/src/scripts/safe-uri-decode.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend/src/scripts/scroll.ts index 1f626e4c0d..8edb6fca05 100644 --- a/packages/frontend/src/scripts/scroll.ts +++ b/packages/frontend/src/scripts/scroll.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/search-emoji.ts b/packages/frontend/src/scripts/search-emoji.ts new file mode 100644 index 0000000000..4192a2df8f --- /dev/null +++ b/packages/frontend/src/scripts/search-emoji.ts @@ -0,0 +1,106 @@ +/* + * SPDX-FileCopyrightText: syuilo and misskey-project + * SPDX-License-Identifier: AGPL-3.0-only + */ + +export type EmojiDef = { + emoji: string; + name: string; + url: string; + aliasOf?: string; +} | { + emoji: string; + name: string; + aliasOf?: string; + isCustomEmoji?: true; +}; +type EmojiScore = { emoji: EmojiDef, score: number }; + +export function searchEmoji(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] { + if (!query) { + return []; + } + + const matched = new Map<string, EmojiScore>(); + // 完全一致(エイリアスなし) + emojiDb.some(x => { + if (x.name.toLowerCase() === query && !x.aliasOf) { + matched.set(x.name, { emoji: x, score: query.length + 3 }); + } + return matched.size === max; + }); + + // 完全一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.toLowerCase() === query && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length + 2 }); + } + return matched.size === max; + }); + } + + // 前方一致(エイリアスなし) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.toLowerCase().startsWith(query) && !x.aliasOf && !matched.has(x.name)) { + matched.set(x.name, { emoji: x, score: query.length + 1 }); + } + return matched.size === max; + }); + } + + // 前方一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.toLowerCase().startsWith(query) && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length }); + } + return matched.size === max; + }); + } + + // 部分一致(エイリアス込み) + if (matched.size < max) { + emojiDb.some(x => { + if (x.name.toLowerCase().includes(query) && !matched.has(x.aliasOf ?? x.name)) { + matched.set(x.aliasOf ?? x.name, { emoji: x, score: query.length - 1 }); + } + return matched.size === max; + }); + } + + // 簡易あいまい検索(3文字以上) + if (matched.size < max && query.length > 3) { + const queryChars = [...query]; + const hitEmojis = new Map<string, EmojiScore>(); + + for (const x of emojiDb) { + // 文字列の位置を進めながら、クエリの文字を順番に探す + + let pos = 0; + let hit = 0; + for (const c of queryChars) { + pos = x.name.indexOf(c, pos); + if (pos <= -1) break; + hit++; + } + + // 半分以上の文字が含まれていればヒットとする + if (hit > Math.ceil(queryChars.length / 2) && hit - 2 > (matched.get(x.aliasOf ?? x.name)?.score ?? 0)) { + hitEmojis.set(x.aliasOf ?? x.name, { emoji: x, score: hit - 2 }); + } + } + + // ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分) + [...hitEmojis.values()] + .sort((x, y) => y.score - x.score) + .slice(0, 6) + .forEach(it => matched.set(it.emoji.name, it)); + } + + return [...matched.values()] + .sort((x, y) => y.score - x.score) + .slice(0, max) + .map(it => it.emoji); +} diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index 674c762fac..fd7cfc697b 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -1,11 +1,12 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import { ref } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os.js'; +import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; import { defaultStore } from '@/store.js'; @@ -65,7 +66,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> { } }); - os.api('drive/files/upload-from-url', { + misskeyApi('drive/files/upload-from-url', { url: url, folderId: defaultStore.state.uploadFolder, marker, diff --git a/packages/frontend/src/scripts/show-moved-dialog.ts b/packages/frontend/src/scripts/show-moved-dialog.ts index b4defbfe7d..35b3ef79d8 100644 --- a/packages/frontend/src/scripts/show-moved-dialog.ts +++ b/packages/frontend/src/scripts/show-moved-dialog.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/show-suspended-dialog.ts b/packages/frontend/src/scripts/show-suspended-dialog.ts index a2fd5db453..8b89dbb936 100644 --- a/packages/frontend/src/scripts/show-suspended-dialog.ts +++ b/packages/frontend/src/scripts/show-suspended-dialog.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/shuffle.ts b/packages/frontend/src/scripts/shuffle.ts index d9d5bb1037..fed16bc71c 100644 --- a/packages/frontend/src/scripts/shuffle.ts +++ b/packages/frontend/src/scripts/shuffle.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/snowfall-effect.ts b/packages/frontend/src/scripts/snowfall-effect.ts index a09f02cec0..11fcaa0716 100644 --- a/packages/frontend/src/scripts/snowfall-effect.ts +++ b/packages/frontend/src/scripts/snowfall-effect.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ @@ -17,20 +17,20 @@ export class SnowfallEffect { uniform vec3 u_worldSize; uniform float u_gravity; uniform float u_wind; + uniform float u_spin_factor; + uniform float u_turbulence; void main() { v_color = a_color; - v_rotation = a_rotation.x + u_time * a_rotation.y; + v_rotation = a_rotation.x + (u_time * u_spin_factor) * a_rotation.y; vec3 pos = a_position.xyz; - float turbulence = 1.0; - pos.x = mod(pos.x + u_time + u_wind * a_speed.x, u_worldSize.x * 2.0) - u_worldSize.x; pos.y = mod(pos.y - u_time * a_speed.y * u_gravity, u_worldSize.y * 2.0) - u_worldSize.y; - pos.x += sin(u_time * a_speed.z * turbulence) * a_rotation.z; - pos.z += cos(u_time * a_speed.z * turbulence) * a_rotation.z; + pos.x += sin(u_time * a_speed.z * u_turbulence) * a_rotation.z; + pos.z += cos(u_time * a_speed.z * u_turbulence) * a_rotation.z; gl_Position = u_projection * vec4(pos.xyz, a_position.w); gl_PointSize = (a_size / gl_Position.w) * 100.0; @@ -105,6 +105,7 @@ export class SnowfallEffect { private opacity = 1; private size = 4; private snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAAAXNSR0IArs4c6QAAAAlwSFlzAAALEwAACxMBAJqcGAAAAVlpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IlhNUCBDb3JlIDUuNC4wIj4KICAgPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICAgICAgPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iPgogICAgICAgICA8dGlmZjpPcmllbnRhdGlvbj4xPC90aWZmOk9yaWVudGF0aW9uPgogICAgICA8L3JkZjpEZXNjcmlwdGlvbj4KICAgPC9yZGY6UkRGPgo8L3g6eG1wbWV0YT4KTMInWQAAErRJREFUeAHdmgnYlmPax5MShaxRKRElPmXJXpaSsRxDU0bTZ+kt65RloiRDltEMQsxYKmS+zzYjxCCamCzV2LchResMIxFRQ1G93+93Pdf5dL9v7zuf4/hm0fc/jt9znddy3/e1nNd53c/7vHXq/AtVWVnZA/bzkaQjoWG298DeMdvrmP6/EIOqC4fBsbAx7Arz4TaYBPXgWVDnO2jSBrB2T0IMIA9mCmmoE8aonPkR6WPZHlp9xSlfeyeBzq9bHBD5feEdUGfDXBgBqnde+a2wvw/dYdNctvZNAp1PnTaFttA6JgP7eVgBM0CNzgO9HNvy0AcYDda6SaDTdXOnz8X+IkZDugAGQmOYA+ob6Ah/MIOMDRPhJjgJ6uV7pXtWt81/50SnY/Wvwn4ZDHAvwJ9ATYcxyaqsnEnqZCyCPaE80BgYZXG/5A3VyyP/b08LHa11z9KmFUwA5eqruRBHYX1s8WSI1Xcbme8Mt8PWUCU+kF8XbFN+dtH+p06OD4IU8EjD/VOZ5bnezq0XHcHuC2oV7BDlkVIWq56uIX8UjAO31GRIMYW0Vo/xXtSXJyTuXVO6xk1qalRTmQ9AfqzEvog2XYpllnsd6Qr4unCPT7NtByu0uU7vuAaOoy1JuvfXpJdTvSX0gI1gCXwGZdFmEFxoQb7Wid8s7lNu+I8wuHGsTqz2zpQ9DAa5R6HC55A2gvCMXthvwi25bjx26H0M9/9f4Rnok9s0zulFlC2HzzP9cnld8nH/p7DVrbmuIfYs6JLz9U3/z+KGadDeCDsmwre7GyEifn/su8HVSsL2HeBn8CK8AW+B7u9R5yrPgyOjvSn5DWAaXAG2UU7CE9Ayt4k4sR1lX4LaLdd9gn2ftsL+Vtuh1Dp/elH1C8lvCdUj8kDK3gbP8XdhCnSC86rcsNSR9pQvhc/gVlB9bUfqoFNAy/mLrUROrpMwCtpBxBbTtLqkF4K6IF9rf57I9pnYekx5AS0P1VhopXso9pR5buC7+kewU86nFcB+BT4EXdIvNO73sRBubGTXLZtTtgp+DEb++bACdqBuJOlAaMMzLVM3whegNznQDtCb+pW5b8YY76euB5+7pxm0IbzCfS8m3Zf2q4T8/+4JNArXGoptpxz8LqDmQJq0Qnostt/sfIn5GygD4/Zeq7B7wljQO2yjB/QGj0Pjxz4wGdqXrkjXtCT/ISyDa6EPpHrSraFjvnecFpMoMx40Br3xSlD262rYObevddHTs2kYwWUG9uP5It/f1eU5Xw9btwoXPALbwYXcg+unG/KB3Rq8n9ddAOpn4Kr8BAaBcltcDo9D7Ouavig1o34x7F94xqPk74eLQH0MH8HvwS3SLPe9iheEG6f70KiuLpZv6sxG/Va5bFJOabaO7ucAvGEbeAH+AN1hV7iDOidQFz4A2oJb6D1YDhXZHkTqpL8EbqHDYRtwW20AsdIb8syl5N2e6dTAPB2mWYa+hE4Qk7I59iMwFZ70GlJlfyuTVfygs7Hyw7HbwI0w3Tak14BqEtdg7wVdIx8pZbtBUbrjZeA3vUPBANkU+sEehev8O4Db6QpwYm+D8II0KPKHwUFeQ3oLDIMN4WgID1yOPQ+MAXMhNAtju3ztmtuAypiAw7EXwo/Am+0NfUG5mknYc6GfGVIjsoFNuyuoh8COuDcd2LmwA9jWE8bB3Q7N4XrwWAz5XOXR+Tx4n6FgdHeB6sF/w2QwhlSXdXvl/jixx4NH8GW5LDzb7GrR4ES4F5QddB99CieAwStOAPegdUZ2B71F3AXbQSn3vJ1bYaYWrayh3NUPTcbYFExVW3CfXwlvgfoavMbnDAY9dxGo6dCt0LeaB54H4UydDEPA2R4PDlrFLB9XuNmTlO+Xr7X9ZNBr9J4+EN8AMcv6ButpMND9FM6EnTOHkLrSnvtzwbbq3vwMB2ow/qWFSC8ZC++ZQaldbquH2afQWbl8TdcvVtC6LtipifAuOKt6gA9Tzqgzb5R2gP1hX3DVtZVHVvdklY5DA5beIkVPuZn8LOgAnWEfeAaUkxCan/voBNkfF+U5cFu5z5XlxZU20OmZtgm1K45VO4naNCukrcBZVk/CD+E/YBjoYjXJY8Zg9DxsDrbbBHTRotxOrug4eBs+hHgWZtKzfHrdXHBi9gDvqzxFHNA5KVfyBCf0ExgB7nkXStLLEKkniNf0AzUs5+ublkVFKiC9FBZAvGxshT0NnN3zoSUYSJQPcjAvm0HmjcIPemNS96F6E36drFLwugx7EEzNZV/l9IjoEPkW4B7eFtYH9QKcBcfA/aCWgpPQOT+zMbb9fS3nDbYR2MdgV0S5aVlUhLs0w45IHi7sqnnGJ2E7CXqHWgZXgJ1y8KqpDUmfSLmSV5yB/XrpDqVP8ofmehNdOv7I0ShfP4yyJdl2a4SchI1gCXgkHgljYfvc1i3cs/SU1A9jQRpfri/b0Sal1RrtSj4ULyHprY5C6+6E1+EBULq0E+DK7A96iwqX0z4td8B3dCdob5gD3UB3j9fUcNuDKFOvgc+bZAZFf4Zgu/q/AGPMgfm+5ShPWay+k6I31BwAvVDRYL2cuqfUVTkfnTqvVFx5ai7/MXn3tp1UrtRkDWRsaAMjzaD08uJ1irz7+8ps/6ZYj90V3FKrQBkvmubULbN7vs7tZRyJV9w0ePLbQ4PcJspqXnkbhbgoGk/AVptZRxpB0hU7Mpc1x34cdgKPm1dzeTts9XPwlFAO5Au4BDbO7ZycO7J9A/Zh2b4A2+ucALefWpTrflDKVq4kHQBOoi9PO1qvsDeGd6AxXAJbQ5VxlFrW8EnDcJlTsOPcjElxL7WNy7AduC4f2+A/rSN/Hyg7YMBTxgqPUT3F2HAqtIb58GvQW86GqyG+ff4UWz0FBuH4UhaTal1vmAGfg98dfP4d4HPGwmwYAg+D2/J7uU0ap/YaolHZVbBj5d1DaSK8ADsmqiH2JIhgNRhbPZrbhSdZ5heVJGw7477VfYuaagMK2sM8iMloga1HXAt/AeWELgQnR/0Z7k3W6pe3xTn/JamTFPGnPMZSj6p90rA8YOziwHcnH/EgTovJlJ0LPSHkyrTKmZNJ+8KrYKBsCQeB0pWdBFNleieMgzjL44jejTK1CPSY0CiMdyOT09g6ni5O3Ceg51U4VNLaPSA3SDNEwwiKFdgHgANNrpjb7UVejYTYCuZ92DR42HYh8gfDJfAMqBi4dqxk+RrKGkD0YXNsA6AT5qCUXhBe5CR0gPCC4dhqKFwI1m1qX0hr94CotDE4aAd3PCyBX4Jyn+sNL5tBDsRAp3S7b5KVYwa2A0nHaO5AXBeDtnlMxizsW+HomLh8zX9R5sTeBSEn/cqc2Tvak9eDXCyP2PgbYWzn2gefHxT7+0Qu/h18DO7XmPWYcYqSXuHz2myb6G7RNs7meLgeMxXugbiPA3clQx0xtgNPGN819L7+oCzvm6zSx+EkI+Du3Pe0LbOd/jqc7dhG9Wib+mJ5jaJBuL8e4B5aAMpAomKlb8d+KZWUVnw+dgzKSdDtvKaLDyJ1ReZB7O0J2EV5Xwd8OsTJExNpu7Q1SJ8zgy7K93UCX4P4mr4udoyhPGDKygOP+tomIFarMw2d+cfgF2DnDVAGoBvzw33YTHgPDoXQ7Fx/Wy6YkdMrcrmrehO4Pz3WvP90cIVPgonwITg4973yu0XTZK0+ZQaQd+K816twVAwKO71ZRj9zeg7lcVzXHghpVN4n2G3BAHQ1NILx4MBjoppgLwL3Ww8IHZsf6vGk3O8fwx9heK7rhD0o2zdg75JtT6GzQQ8KzcZwElSr3M5J85ktYCzEG+Gx2NNzm/Cm5pSp+K2gfLrZbg3RcB2IQcZN1qPM3+l06SjbAltX/TiXe1wtg7+AdR+AcgIs7xUPw94XxuTrnOD4E1bEoe9Rptw+DWGOGeQi7JOs1SfKKfk+epcakPNxbI8uFVdem8vT6aJdq7jASYjOFPdQDP4Q6t+Em8HVutmbkbYH9Tv4LcQW+H6ujy9Wrtxc6A7vQnznb5TbHUPZ0mw7CeoaOBAegmfBIKw8WZzs34M/oNiPGPzB2KHdrVMUlD29VFLLpw2jMWmnaIbdDNxXur+dWgVumTMglI4zMgbUEV5LmjqW7XnRkDS9qhbu/xZlZ8LWuc3UfM22Of80aVcYDJ/lstdIWxXu0TGXm/TO19vveHWuOglUxOo6iMfyBe7JOEp01ech9puuuBCMA8pVcUUNUB5lqgMYwJyE1oXOGTh9v1gO6kmogKEwHtREMHYofz5zAl3lJ2AWqJfgfohJiKB8HWWfg54YA9Zr1fn5Xmm80SdvHhNwVmq2umF8vWxA+WRwwE9BPNhOulrq0nxz97j6Go6DF8HYcBfYyer6MwWuoINeDG6roq4iE97QCtsJuxWc2JrkCeKEbgX7waOgnLiavxdQEWfohtgRwCrygIoxoQv1K0FNgR7gAKPTB+dr5lAWMliqmbAb7AzbgCs42vYK21NmOiwHJ9atpdxqDlhdA75QdYJT4XUYDfbBiVRe5ySoZTAbBpeekp6T4lo5uFnBz0fpJ6P8E9SJufEdXHipdRA/mw2hzmvfhrfgfjCKPwJnwn2g3igldb4hNaD5a6/fz7eHVuAb2wPwPs+4DB7E/hTagd64BbgoC6Ab9IAfgn+OX0p/ppAaGxZjnw6+Ep8DK8Cj0IDrmHw3GaeN9EZ/AlxFfk1RuVGUYu8K00D9Fa6EvrAUVKzO29gXg9vC1VW3g540w0xBcU2hKJnz+FxYvTCXWaduK/StuTZlLcD6JjnfEvsb6A56m32z78q4FMGw1gA4lEa60WmwMeiSnsljIBSDmEOBE3RdfvggbMuMIbNhItgJtbyUpE9ddjA0Bid1sderXDaQ1OdPAO9zH6hDcpuG2Ml7SQfArHRx6Xpf3JTluySrsrIP6Seg9/iMqsEvF6YZoXIDeAZCRmpneAHEnnLQnaEuXATX53schR3n/e7YyuvOT1bpnyV107Io3xZ6QWs4EirAyXkEqqvK3xa9CQ0c5C5xQ+zN8kWjcr2xZxTsBHfmsipbP671ZmW3wHYA58DdEPobhtwVF2HfBE9H3pT8xjkdja3iiDK4PQBO8Dx4B9wiH8JKeANcKTUW9IITwKNMeYrcArfDhVDsb1pVyty26le5D97/zWzrzVUGXyVjI0WjHUgq4CjoAuGiRuuJkN7mSJX7cn+uaZNyfBBgDHZqXvqsU2cZ6aPwChgE/ap8M9wLbSH+0DKOaw18z8N12GPAyf4BfADbwBmwCbxAHY9NvxQXx2GgVLZXPvurZDE0rqk5+NmAm8U2aIbdH9yDalgpSS80ltlB29fPqW9c8XLUHnsIuGquqt8gN7edwtazrOsAn4MysLryX8BD4Ap3y+0dZROIwPsl9h/hHjgit4lXdrdvHN8dc91wyk7JdvIS7VpF46Jb2ZGz4WJIRyBpBKQW3oR8lZuSvwQMhKtAfQUpYuf27cgbNx6EEeDAzgMHPwYMYi2gEcSfxC7B9qicDMoo/1vQI8p9IG88WAY/yeVpYrJdHpf5vytu4Ky7X46xIamrvjDb52OrG3K+HrZt4xq9wYEZPGPVfp7bhsdE2os2ylV6J1n5mbYPUX4S7AkGX+OAk2t6mm1Iw3PtQ+O4LuooK26RYvW3s7nBLZDiAGlbUHYiRV/S5AWk28DTEFqB4eo+B+n1M55Ivhu4kspj92uYCm6Px0Gv61lor0fcDQNBrQQnOr71lVeYsm894L/bkBuFe/u93eBngJtJMlwTDIDKyfDt6n3se8Dt8jHoNU0o70waq34obZ8lPx4coG+LbifrP6Pt0aQvwn65LFzcAHY8ZUtgAnwExp2WoMpeQLvaA12p7bf/pLPFmS3a/ajr750cfE43wX4YYmU9wi7IddHBCsrc69vm8uuwQydYVhQVvmsUn7s+ebfD0GhXrI+yf2jqA4oPKdo+iHxMwHbYRmgjta4cUTqCWXkg0UHatIR4SxxWKK9PeXhgKiZfxWOthzXuGff4p6b54bH3Y3W3pNxJcK8ebgdI44iys0G0N/8qKGOAGg9Ni50n3yjy2GkxSKtMRtT/21I7Fg/H9lRIX6qK5YX6zSjvDL4BGiBfBnUNmFdzwfKX4Ct40OtJv1sDj0Hlzrk6xbM3tob7uCf4amyk96VHvQg7gltGzQG9wpcwX6BCesfJ3/kJiMmgs+Gm4errUeZqF+Up4IoOzoWLcmqETyLve/2BsKkFpGUvK7VYCz6j06RbQx+ogHhN3Qdb3QF+a/wVKF94OhSHR77sWcXytcKm82usHGW9QE2B3skq/QB7APaqnJ9NuvaufnF1GIhxYH3LSAeA+hM0hMfgNzATdHvjgDHDv+qkP8gW77XW2gwmYsJe2F3zZDgxI7NteTo+/1WD/B9Au3Zjh2RyrgAAAABJRU5ErkJggg=='; + private mode = 'snow'; private INITIAL_BUFFERS = () => ({ position: { size: 3, value: [] }, @@ -119,6 +120,8 @@ export class SnowfallEffect { worldSize: { type: 'vec3', value: [0, 0, 0] }, gravity: { type: 'float', value: this.gravity }, wind: { type: 'float', value: 0 }, + spin_factor: { type: 'float', value: this.mode === 'sakura' ? 8 : 1 }, + turbulence: { type: 'float', value: this.mode === 'sakura' ? 2 : 1 }, projection: { type: 'mat4', value: [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], @@ -153,7 +156,16 @@ export class SnowfallEffect { easing: 0.0005, }; - constructor() { + constructor(options: { + sakura?: boolean; + }) { + if (options.sakura) { + this.mode = 'sakura'; + this.snowflake = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAFEmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iWE1QIENvcmUgNS41LjAiPgogPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4KICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIgogICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgeG1sbnM6ZXhpZj0iaHR0cDovL25zLmFkb2JlLmNvbS9leGlmLzEuMC8iCiAgICB4bWxuczp0aWZmPSJodHRwOi8vbnMuYWRvYmUuY29tL3RpZmYvMS4wLyIKICAgIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIgogICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgeG1wOkNyZWF0ZURhdGU9IjIwMjQtMDItMDFUMTQ6Mzk6NTYrMDkwMCIKICAgeG1wOk1vZGlmeURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHhtcDpNZXRhZGF0YURhdGU9IjIwMjQtMDItMDFUMTQ6NDU6MzQrMDk6MDAiCiAgIHBob3Rvc2hvcDpEYXRlQ3JlYXRlZD0iMjAyNC0wMi0wMVQxNDozOTo1NiswOTAwIgogICBwaG90b3Nob3A6Q29sb3JNb2RlPSIzIgogICBwaG90b3Nob3A6SUNDUHJvZmlsZT0ic1JHQiBJRUM2MTk2Ni0yLjEiCiAgIGV4aWY6UGl4ZWxYRGltZW5zaW9uPSI2NCIKICAgZXhpZjpQaXhlbFlEaW1l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'; + this.size = 10; + this.density = 1 / 280; + } + const canvas = this.initCanvas(); const gl = canvas.getContext('webgl2', { antialias: true }); if (gl == null) throw new Error('Failed to get WebGL context'); diff --git a/packages/frontend/src/scripts/sound.ts b/packages/frontend/src/scripts/sound.ts index 2f7545ef0d..fcd59510df 100644 --- a/packages/frontend/src/scripts/sound.ts +++ b/packages/frontend/src/scripts/sound.ts @@ -1,11 +1,10 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import type { SoundStore } from '@/store.js'; import { defaultStore } from '@/store.js'; -import * as os from '@/os.js'; let ctx: AudioContext; const cache = new Map<string, AudioBuffer>(); @@ -89,63 +88,33 @@ export type OperationType = typeof operationTypes[number]; /** * 音声を読み込む - * @param soundStore サウンド設定 + * @param url url * @param options `useCache`: デフォルトは`true` 一度再生した音声はキャッシュする */ -export async function loadAudio(soundStore: SoundStore, options?: { useCache?: boolean; }) { - if (_DEV_) console.log('loading audio. opts:', options); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { - return; - } +export async function loadAudio(url: string, options?: { useCache?: boolean; }) { // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (ctx == null) { ctx = new AudioContext(); } if (options?.useCache ?? true) { - if (soundStore.type === '_driveFile_' && cache.has(soundStore.fileId)) { - if (_DEV_) console.log('use cache'); - return cache.get(soundStore.fileId) as AudioBuffer; - } else if (cache.has(soundStore.type)) { - if (_DEV_) console.log('use cache'); - return cache.get(soundStore.type) as AudioBuffer; + if (cache.has(url)) { + return cache.get(url) as AudioBuffer; } } let response: Response; - if (soundStore.type === '_driveFile_') { - try { - response = await fetch(soundStore.fileUrl); - } catch (err) { - try { - // URLが変わっている可能性があるのでドライブ側からURLを取得するフォールバック - const apiRes = await os.api('drive/files/show', { - fileId: soundStore.fileId, - }); - response = await fetch(apiRes.url); - } catch (fbErr) { - // それでも無理なら諦める - return; - } - } - } else { - try { - response = await fetch(`/client-assets/sounds/${soundStore.type}.mp3`); - } catch (err) { - return; - } + try { + response = await fetch(url); + } catch (err) { + return; } const arrayBuffer = await response.arrayBuffer(); const audioBuffer = await ctx.decodeAudioData(arrayBuffer); if (options?.useCache ?? true) { - if (soundStore.type === '_driveFile_') { - cache.set(soundStore.fileId, audioBuffer); - } else { - cache.set(soundStore.type, audioBuffer); - } + cache.set(url, audioBuffer); } return audioBuffer; @@ -155,13 +124,12 @@ export async function loadAudio(soundStore: SoundStore, options?: { useCache?: b * 既定のスプライトを再生する * @param type スプライトの種類を指定 */ -export function play(operationType: OperationType) { +export function playMisskeySfx(operationType: OperationType) { const sound = defaultStore.state[`sound_${operationType}`]; - if (_DEV_) console.log('play', operationType, sound); - if (sound.type == null || !canPlay) return; + if (sound.type == null || !canPlay || ('userActivation' in navigator && !navigator.userActivation.hasBeenActive)) return; canPlay = false; - playFile(sound).finally(() => { + playMisskeySfxFile(sound).finally(() => { // ごく短時間に音が重複しないように setTimeout(() => { canPlay = true; @@ -173,26 +141,59 @@ export function play(operationType: OperationType) { * サウンド設定形式で指定された音声を再生する * @param soundStore サウンド設定 */ -export async function playFile(soundStore: SoundStore) { - const buffer = await loadAudio(soundStore); +export async function playMisskeySfxFile(soundStore: SoundStore) { + if (soundStore.type === null || (soundStore.type === '_driveFile_' && !soundStore.fileUrl)) { + return; + } + const masterVolume = defaultStore.state.sound_masterVolume; + if (isMute() || masterVolume === 0 || soundStore.volume === 0) { + return; + } + const url = soundStore.type === '_driveFile_' ? soundStore.fileUrl : `/client-assets/sounds/${soundStore.type}.mp3`; + const buffer = await loadAudio(url); if (!buffer) return; - createSourceNode(buffer, soundStore.volume)?.start(); + const volume = soundStore.volume * masterVolume; + createSourceNode(buffer, { volume }).soundSource.start(); } -export function createSourceNode(buffer: AudioBuffer, volume: number) : AudioBufferSourceNode | null { - const masterVolume = defaultStore.state.sound_masterVolume; - if (isMute() || masterVolume === 0 || volume === 0) { - return null; +export async function playUrl(url: string, opts: { + volume?: number; + pan?: number; + playbackRate?: number; +}) { + if (opts.volume === 0) { + return; } + const buffer = await loadAudio(url); + if (!buffer) return; + createSourceNode(buffer, opts).soundSource.start(); +} + +export function createSourceNode(buffer: AudioBuffer, opts: { + volume?: number; + pan?: number; + playbackRate?: number; +}): { + soundSource: AudioBufferSourceNode; + panNode: StereoPannerNode; + gainNode: GainNode; +} { + const panNode = ctx.createStereoPanner(); + panNode.pan.value = opts.pan ?? 0; const gainNode = ctx.createGain(); - gainNode.gain.value = masterVolume * volume; + + gainNode.gain.value = opts.volume ?? 1; const soundSource = ctx.createBufferSource(); soundSource.buffer = buffer; - soundSource.connect(gainNode).connect(ctx.destination); + soundSource.playbackRate.value = opts.playbackRate ?? 1; + soundSource + .connect(panNode) + .connect(gainNode) + .connect(ctx.destination); - return soundSource; + return { soundSource, panNode, gainNode }; } /** diff --git a/packages/frontend/src/scripts/sticky-sidebar.ts b/packages/frontend/src/scripts/sticky-sidebar.ts index f233c3648e..50f1e6ecc8 100644 --- a/packages/frontend/src/scripts/sticky-sidebar.ts +++ b/packages/frontend/src/scripts/sticky-sidebar.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/test-utils.ts b/packages/frontend/src/scripts/test-utils.ts index 1b42811faa..52bb2d94e0 100644 --- a/packages/frontend/src/scripts/test-utils.ts +++ b/packages/frontend/src/scripts/test-utils.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/theme-editor.ts b/packages/frontend/src/scripts/theme-editor.ts index 275f4bcdaa..0092af1640 100644 --- a/packages/frontend/src/scripts/theme-editor.ts +++ b/packages/frontend/src/scripts/theme-editor.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts index a174f51756..c49593ed42 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -1,11 +1,12 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import { ref } from 'vue'; import tinycolor from 'tinycolor2'; import { deepClone } from './clone.js'; +import type { BuiltinTheme } from 'shiki'; import { globalEvents } from '@/events.js'; import lightTheme from '@/themes/_light.json5'; import darkTheme from '@/themes/_dark.json5'; @@ -18,6 +19,13 @@ export type Theme = { desc?: string; base?: 'dark' | 'light'; props: Record<string, string>; + codeHighlighter?: { + base: BuiltinTheme; + overrides?: Record<string, any>; + } | { + base: '_none_'; + overrides: Record<string, any>; + }; }; export const themeProps = Object.keys(lightTheme.props).filter(key => !key.startsWith('X')); @@ -57,7 +65,7 @@ export const getBuiltinThemesRef = () => { const themeFontFaceName = 'sharkey-theme-font-face'; -let timeout = null; +let timeout: number | null = null; export function applyTheme(theme: Theme, persist = true) { if (timeout) window.clearTimeout(timeout); diff --git a/packages/frontend/src/scripts/time.ts b/packages/frontend/src/scripts/time.ts index 4479db1081..275b67ed00 100644 --- a/packages/frontend/src/scripts/time.ts +++ b/packages/frontend/src/scripts/time.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/timezones.ts b/packages/frontend/src/scripts/timezones.ts index 55f9be393f..c7582e06da 100644 --- a/packages/frontend/src/scripts/timezones.ts +++ b/packages/frontend/src/scripts/timezones.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/touch.ts b/packages/frontend/src/scripts/touch.ts index 05f379e4aa..13c9d648dc 100644 --- a/packages/frontend/src/scripts/touch.ts +++ b/packages/frontend/src/scripts/touch.ts @@ -1,8 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ +import { ref } from 'vue'; import { deviceKind } from '@/scripts/device-kind.js'; const isTouchSupported = 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; @@ -16,3 +17,6 @@ if (isTouchSupported && !isTouchUsing) { isTouchUsing = true; }, { passive: true }); } + +/** (MkHorizontalSwipe) 横スワイプ中か? */ +export const isHorizontalSwipeSwiping = ref(false); diff --git a/packages/frontend/src/scripts/unison-reload.ts b/packages/frontend/src/scripts/unison-reload.ts index 65fc090888..a24941d02e 100644 --- a/packages/frontend/src/scripts/unison-reload.ts +++ b/packages/frontend/src/scripts/unison-reload.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index b896376ec8..6c46b2bc1b 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import { reactive, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { readAndCompressImage } from 'browser-image-resizer'; +import { readAndCompressImage } from '@misskey-dev/browser-image-resizer'; import { getCompressionConfig } from './upload/compress-config.js'; import { defaultStore } from '@/store.js'; import { apiUrl } from '@/config.js'; diff --git a/packages/frontend/src/scripts/upload/compress-config.ts b/packages/frontend/src/scripts/upload/compress-config.ts index 2deb9cbb81..3046b7f518 100644 --- a/packages/frontend/src/scripts/upload/compress-config.ts +++ b/packages/frontend/src/scripts/upload/compress-config.ts @@ -1,11 +1,11 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ import isAnimated from 'is-file-animated'; import { isWebpSupported } from './isWebpSupported.js'; -import type { BrowserImageResizerConfig } from 'browser-image-resizer'; +import type { BrowserImageResizerConfigWithConvertedOutput } from '@misskey-dev/browser-image-resizer'; const compressTypeMap = { 'image/jpeg': { quality: 0.90, mimeType: 'image/webp' }, @@ -21,7 +21,7 @@ const compressTypeMapFallback = { 'image/svg+xml': { quality: 1, mimeType: 'image/png' }, } as const; -export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfig | undefined> { +export async function getCompressionConfig(file: File): Promise<BrowserImageResizerConfigWithConvertedOutput | undefined> { const imgConfig = (isWebpSupported() ? compressTypeMap : compressTypeMapFallback)[file.type]; if (!imgConfig || await isAnimated(file)) { return; diff --git a/packages/frontend/src/scripts/upload/isWebpSupported.ts b/packages/frontend/src/scripts/upload/isWebpSupported.ts index 185c3e6b40..2511236ecc 100644 --- a/packages/frontend/src/scripts/upload/isWebpSupported.ts +++ b/packages/frontend/src/scripts/upload/isWebpSupported.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend/src/scripts/url.ts index 625f4ce057..e3072b3b7d 100644 --- a/packages/frontend/src/scripts/url.ts +++ b/packages/frontend/src/scripts/url.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/use-chart-tooltip.ts b/packages/frontend/src/scripts/use-chart-tooltip.ts index 3d6489c3b8..7e4bf5c9c6 100644 --- a/packages/frontend/src/scripts/use-chart-tooltip.ts +++ b/packages/frontend/src/scripts/use-chart-tooltip.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend/src/scripts/use-document-visibility.ts index a9e2512eb3..a8f4d5e03a 100644 --- a/packages/frontend/src/scripts/use-document-visibility.ts +++ b/packages/frontend/src/scripts/use-document-visibility.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend/src/scripts/use-interval.ts index b8c5431fb6..b50e78c3cc 100644 --- a/packages/frontend/src/scripts/use-interval.ts +++ b/packages/frontend/src/scripts/use-interval.ts @@ -1,9 +1,9 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import { onMounted, onUnmounted } from 'vue'; +import { onActivated, onDeactivated, onMounted, onUnmounted } from 'vue'; export function useInterval(fn: () => void, interval: number, options: { immediate: boolean; @@ -28,6 +28,16 @@ export function useInterval(fn: () => void, interval: number, options: { intervalId = null; }; + onActivated(() => { + if (intervalId) return; + if (options.immediate) fn(); + intervalId = window.setInterval(fn, interval); + }); + + onDeactivated(() => { + clear(); + }); + onUnmounted(() => { clear(); }); diff --git a/packages/frontend/src/scripts/use-leave-guard.ts b/packages/frontend/src/scripts/use-leave-guard.ts index c9750c3923..5f7e56e8a9 100644 --- a/packages/frontend/src/scripts/use-leave-guard.ts +++ b/packages/frontend/src/scripts/use-leave-guard.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index bcdba5455a..3baa45d50f 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -1,16 +1,17 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ -import { onUnmounted, Ref } from 'vue'; +import { onUnmounted, Ref, ShallowRef } from 'vue'; import * as Misskey from 'misskey-js'; import { useStream } from '@/stream.js'; import { $i } from '@/account.js'; import * as os from '@/os.js'; +import { misskeyApi } from './misskey-api.js'; export function useNoteCapture(props: { - rootEl: Ref<HTMLElement>; + rootEl: ShallowRef<HTMLElement | undefined>; note: Ref<Misskey.entities.Note>; pureNote: Ref<Misskey.entities.Note>; isDeletedRef: Ref<boolean>; @@ -32,7 +33,7 @@ export function useNoteCapture(props: { // notes/show may throw if the current user can't see the note try { - const replyNote = await os.api('notes/show', { + const replyNote = await misskeyApi('notes/show', { noteId: body.id, }); @@ -100,7 +101,7 @@ export function useNoteCapture(props: { case 'updated': { try { - const editedNote = await os.api('notes/show', { + const editedNote = await misskeyApi('notes/show', { noteId: id, }); @@ -121,7 +122,7 @@ export function useNoteCapture(props: { function capture(withHandler = false): void { if (connection) { // TODO: このノートがストリーミング経由で流れてきた場合のみ sr する - connection.send(document.body.contains(props.rootEl.value) ? 'sr' : 's', { id: note.value.id }); + connection.send(document.body.contains(props.rootEl.value ?? null as Node | null) ? 'sr' : 's', { id: note.value.id }); if (pureNote.value.id !== note.value.id) connection.send('s', { id: pureNote.value.id }); if (withHandler) connection.on('noteUpdated', onStreamNoteUpdated); } diff --git a/packages/frontend/src/scripts/use-tooltip.ts b/packages/frontend/src/scripts/use-tooltip.ts index aaf0a0285a..a26d08cce7 100644 --- a/packages/frontend/src/scripts/use-tooltip.ts +++ b/packages/frontend/src/scripts/use-tooltip.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ diff --git a/packages/frontend/src/scripts/worker-multi-dispatch.ts b/packages/frontend/src/scripts/worker-multi-dispatch.ts index 7686b687c5..6b3fcd9383 100644 --- a/packages/frontend/src/scripts/worker-multi-dispatch.ts +++ b/packages/frontend/src/scripts/worker-multi-dispatch.ts @@ -1,5 +1,5 @@ /* - * SPDX-FileCopyrightText: syuilo and other misskey contributors + * SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-License-Identifier: AGPL-3.0-only */ |