Compare commits

..

7 Commits

Author SHA1 Message Date
github-actions[bot] f68da80ebd Bump version to 2026.7.0-beta.5 2026-07-24 00:37:34 +00:00
かっこかり fcb4ced33d enhance(frontend): tagcanvas実装を変更 (#17753)
* enhance(frontend): tagcanvas実装を変更

* fix indent

* fix

* fix

* review fixes
2026-07-24 09:30:35 +09:00
おさむのひと 85e810533b enhance: 旧Loggerを拡張して構造化ログを直接受け取れるように (#17780)
* enhance: 旧Loggerを拡張して構造化ログを直接受け取れるように

* fix lint
2026-07-24 09:28:56 +09:00
かっこかり 661838c89e fix(backend): 初期設定で作成したアカウント以外でアカウント作成APIが利用できない問題を修正 (#17783)
* fix(backend): 初期設定で作成したアカウント以外でアカウント作成APIが利用できない問題を修正

* Update Changelog

* fix

* fix
2026-07-24 09:27:40 +09:00
かっこかり 80d14231c5 fix(frontend): Lightbox上で強くスワイプしたときに一時的に画面が操作不能になる問題を修正 (#17785)
* fix(frontend): Lightbox上で強くスワイプしたときに一時的に画面が操作不能になる問題を修正

* fix

* update comment
2026-07-23 21:55:20 +09:00
syuilo 28153f7b5f Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2026-07-23 21:46:20 +09:00
syuilo 6d81c81ab2 chore(gh): fix tests 2026-07-23 21:46:19 +09:00
20 changed files with 249 additions and 129 deletions
+1
View File
@@ -70,6 +70,7 @@
- Fix: Sentry 使用環境下にて、外部送信リクエストへ `sentry-trace` / `baggage` ヘッダーが既定で付与されないように
- Fix: フォロワー限定投稿へのリプライをホーム投稿に出来る問題を修正
- Fix: ファイルをアップロードするAPIにて、処理終了後に一時ファイルが削除されないことがある問題を修正
- Fix: 初期設定で作成したアカウント以外でアカウント作成APIが使用できない問題を修正
## 2026.6.0
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2026.7.0-beta.4",
"version": "2026.7.0-beta.5",
"codename": "nasubi",
"repository": {
"type": "git",
@@ -64,7 +64,7 @@ test('throws when filtering leaves fewer than two heap snapshots per side', asyn
})).toThrow('At least two samples per side are required');
});
test('reports the difference of medians and leaves a paired-looking PSS delta uncoloured', async () => {
test('omits a paired-looking PSS delta that stays within observed noise', async () => {
const base = replacePssSamples(await loadFixture('base'), [290_000, 292_900, 295_800, 298_700, 301_600]);
const head = replacePssSamples(await loadFixture('head'), [292_900, 296_300, 298_700, 301_600, 290_000]);
@@ -78,16 +78,10 @@ test('reports the difference of medians and leaves a paired-looking PSS delta un
baseHeapSnapshotUrl: 'https://example.invalid/base',
headHeapSnapshotUrl: 'https://example.invalid/head',
});
const row = findMetricRow(markdown, 'PSS');
expect(row).toContain('295.8 MB <br> ± 2.9 MB');
expect(row).toContain('296.3 MB <br> ± 3.4 MB');
expect(row).toContain('$\\text{+0.5 MB}$');
expect(row).toContain('4.5 MB');
expect(row.split('|')).toHaveLength(7);
expect(row).not.toMatch(/within noise|increase|decrease|inconclusive/);
expect(row).not.toContain('\\color{orange}');
expect(findMetricRow(markdown, 'USS')).not.toContain('\\color{orange}');
expect(markdown).not.toContain('| **PSS** |');
expect(markdown).toContain('<small><i>Only metrics showing significant changes are displayed.</i></small>');
expect(markdown).not.toContain('⚠️ **Warning**: Memory usage (PSS)');
});
test('keeps the convergence warning without suppressing table colour or the PSS warning', async () => {
@@ -251,7 +251,7 @@ test('renders a directional encoded byte delta at the absolute threshold', async
const row = requireMetricRow(await renderReport(null, { base, head }), 'Encoded network');
expect(row).toContain('$\\color{orange}{\\text{+10 KB}}$');
expect(row).toContain('$\\color{orange}{\\text{+10 KB}}$');
});
test('colours absolute and relative deltas together when the row is significant', async () => {
@@ -268,9 +268,9 @@ test('colours absolute and relative deltas together when the row is significant'
const row = requireMetricRow(await renderReport(null, { base, head }), 'Encoded network');
expect(row).toContain('100 MB <br> ± 0 B');
expect(row).toContain('$\\color{orange}{\\text{+20 KB}}$<br>$\\color{orange}{\\text{+0\\\\%}}$');
expect(row).toContain('| 0 B |');
expect(row).toContain('100 MB <br> ± 0 B');
expect(row).toContain('$\\color{orange}{\\text{+20 KB}}$<br>$\\color{orange}{\\text{+0\\\\%}}$');
expect(row).toContain('| 0 B |');
expect(row.split('|')).toHaveLength(7);
});
@@ -20,15 +20,15 @@ import {
describe('formatBytes', () => {
// 1024ではなく1000区切り。単位が2桁以上なら小数を落とす
test.each([
[0, '0 B'],
[999, '999 B'],
[1_000, '1 KB'],
[1_500, '1.5 KB'],
[15_000, '15 KB'],
[1_234_567, '1.2 MB'],
[12_345_678, '12 MB'],
[1_500_000_000, '1.5 GB'],
[1_500_000_000_000, '1,500 GB'],
[0, '0 B'],
[999, '999 B'],
[1_000, '1 KB'],
[1_500, '1.5 KB'],
[15_000, '15 KB'],
[1_234_567, '1.2 MB'],
[12_345_678, '12 MB'],
[1_500_000_000, '1.5 GB'],
[1_500_000_000_000, '1,500 GB'],
])('formats %i as %s', (input, expected) => {
expect(formatBytes(input)).toBe(expected);
});
@@ -45,7 +45,7 @@ describe('renderHeapSnapshotTable', () => {
report([1_100_000, 1_200_000, 1_300_000]),
));
expect(row).toContain('$\\text{+100 KB}$');
expect(row).toContain('$\\text{+100 KB}$');
expect(row).not.toContain('within noise');
expect(row).not.toContain('\\color{orange}');
});
@@ -56,7 +56,7 @@ describe('renderHeapSnapshotTable', () => {
report([1_200_000, 1_200_000, 1_200_000]),
));
expect(row).toContain('$\\color{orange}{\\text{+200 KB}}$');
expect(row).toContain('$\\color{orange}{\\text{+200 KB}}$');
expect(row).toContain('$\\color{orange}{\\text{+20\\\\%}}$');
expect(row).not.toContain('increase');
});
@@ -67,7 +67,7 @@ describe('renderHeapSnapshotTable', () => {
report([1_050_000, 1_050_000, 1_050_000]),
));
expect(row).toContain('$\\text{+50 KB}$<br>$\\text{+5\\\\%}$');
expect(row).toContain('$\\text{+50 KB}$<br>$\\text{+5\\\\%}$');
expect(row.split('|')[4]).not.toContain('\\color{');
});
@@ -33,7 +33,7 @@ describe('renderMetricComparisonTable', () => {
expect(table.split('\n')).toStrictEqual([
'| Metric | @&nbsp;Base | @&nbsp;Head | Δ | MAD |',
'| --- | ---: | ---: | ---: | ---: |',
'| <strong>Metric</strong> | 100 units <br> ± 0 units | 120 units <br> ± 0 units | $\\color{orange}{\\text{+20 units}}$<br>$\\color{orange}{\\text{+20\\\\%}}$ | 0 units |',
'| <strong>Metric</strong> | 100 units <br> ± 0 units | 120 units <br> ± 0 units | $\\color{orange}{\\text{+20~units}}$<br>$\\color{orange}{\\text{+20\\\\%}}$ | 0 units |',
]);
});
@@ -52,7 +52,7 @@ describe('renderMetricComparisonTable', () => {
expect(table.split('\n')).toStrictEqual([
'| Metric | @&nbsp;Base | @&nbsp;Head | Δ | MAD |',
'| --- | ---: | ---: | ---: | ---: |',
'| <strong>Metric</strong> | 100 units | 120 units | $\\color{orange}{\\text{+20 units}}$ | 0 units |',
'| <strong>Metric</strong> | 100 units | 120 units | $\\color{orange}{\\text{+20~units}}$ | 0 units |',
'| | | | | |',
]);
});
@@ -62,7 +62,7 @@ describe('renderMetricComparisonTable', () => {
const head = samples(109, 109, 109);
const table = renderMetricComparisonTable(base, head, [defaultRow]);
expect(table).toContain('$\\text{+9 units}$<br>$\\text{+9\\\\%}$');
expect(table).toContain('$\\text{+9~units}$<br>$\\text{+9\\\\%}$');
expect(table).not.toContain('\\color{');
expect(renderMetricComparisonTable(base, head, [defaultRow], {
onlySignificantChanges: true,
@@ -85,7 +85,7 @@ describe('renderMetricComparisonTable', () => {
{ onlySignificantChanges: true },
);
expect(table).toContain('$\\color{orange}{\\text{+10 units}}$');
expect(table).toContain('$\\color{orange}{\\text{+10~units}}$');
expect(table).toContain('$\\color{orange}{\\text{+10\\\\%}}$');
});
@@ -106,11 +106,11 @@ describe('renderMetricComparisonTable', () => {
[defaultRow],
);
expect(inside).toContain('$\\text{+9 units}$');
expect(inside).toContain('$\\text{+9~units}$');
expect(inside).not.toContain('\\color{');
expect(boundary).toContain('$\\text{+30 units}$');
expect(boundary).toContain('$\\text{+30~units}$');
expect(boundary).not.toContain('\\color{');
expect(outside).toContain('$\\color{orange}{\\text{+31 units}}$');
expect(outside).toContain('$\\color{orange}{\\text{+31~units}}$');
expect(outside).toContain('$\\color{orange}{\\text{+31\\\\%}}$');
});
@@ -121,7 +121,7 @@ describe('renderMetricComparisonTable', () => {
[defaultRow],
);
expect(table).toContain('$\\color{green}{\\text{-20 units}}$');
expect(table).toContain('$\\color{green}{\\text{-20~units}}$');
expect(table).toContain('$\\color{green}{\\text{-16.7\\\\%}}$');
});
@@ -132,7 +132,7 @@ describe('renderMetricComparisonTable', () => {
[defaultRow],
);
expect(table).toContain('$\\color{orange}{\\text{+20 units}}$<br>-');
expect(table).toContain('$\\color{orange}{\\text{+20~units}}$<br>-');
});
test('propagates the minimum sample contract', () => {
+60 -11
View File
@@ -5,9 +5,12 @@
import { bindThis } from '@/decorators.js';
import { logManager } from './logging/logging-runtime.js';
import type { LogLevel, LoggerContext, LogWriteInput } from './logging/types.js';
import type { LogEntryInput, LogLevel, LoggerContext, LogWriteInput } from './logging/types.js';
import type { Keyword } from 'color-convert';
// 旧APIのdataは表示用の任意値を受け取り、Errorや配列も既存呼び出しで使用されています。
type LegacyData = Record<string, any> | null;
/**
* ロガー名の階層と従来の公開APIを提供する薄い窓口です。
* 出力条件の判断や整形はLogManagerとLogBackendへ委譲します。
@@ -52,6 +55,15 @@ export default class Logger {
});
}
/** level別メソッドの構造化入力にlevelとLoggerのcontextを付けて渡します。 */
@bindThis
private logStructured(level: LogLevel, input: LogEntryInput): void {
this.write({
...input,
level,
});
}
/** 構造化ログをLoggerのcontext付きでLogManagerへ渡します。 */
@bindThis
public write(input: LogWriteInput): void {
@@ -62,24 +74,35 @@ export default class Logger {
}
/** 処理を継続できない状況を記録します。 */
public error(input: LogEntryInput): void;
public error(error: Error, data?: LegacyData, important?: boolean): void;
public error(message: string, data?: LegacyData, important?: boolean): void;
public error(errorOrMessage: string | Error, data?: LegacyData, important?: boolean): void;
@bindThis
public error(x: string | Error, data?: Record<string, any> | null, important = false): void {
public error(x: LogEntryInput | string | Error, data?: LegacyData, important = false): void {
if (x instanceof Error) {
// エラー本体も第2引数へ残し、従来どおりスタックなどを確認できるようにします。
data = data ?? {};
data.e = x;
this.log('error', x.toString(), data, important, undefined, x);
} else if (typeof x === 'object') {
this.log('error', `${(x as any).message ?? (x as any).name ?? x}`, data, important);
} else if (typeof x === 'string') {
this.log('error', x, data, important);
} else {
this.log('error', `${x}`, data, important);
this.logStructured('error', x);
}
}
/** 処理は継続できるものの、改善が必要な状況を記録します。 */
public warn(input: LogEntryInput): void;
public warn(message: string): void;
public warn(message: string, data?: LegacyData, important?: boolean): void;
@bindThis
public warn(message: string, data?: Record<string, any> | null, important = false): void {
this.log('warn', message, data, important);
public warn(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
if (typeof inputOrMessage === 'string') {
this.log('warn', inputOrMessage, data, important);
} else {
this.logStructured('warn', inputOrMessage);
}
}
/** 処理が成功したことを、従来のDONE表示で記録します。 */
@@ -89,14 +112,40 @@ export default class Logger {
}
/** 開発者向けの調査情報を記録します。 */
public debug(input: LogEntryInput): void;
public debug(message: string): void;
public debug(message: string, data?: LegacyData, important?: boolean): void;
@bindThis
public debug(message: string, data?: Record<string, any> | null, important = false): void {
this.log('debug', message, data, important);
public debug(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
if (typeof inputOrMessage === 'string') {
this.log('debug', inputOrMessage, data, important);
} else {
this.logStructured('debug', inputOrMessage);
}
}
/** 通常の動作状況を記録します。 */
public info(input: LogEntryInput): void;
public info(message: string): void;
public info(message: string, data?: LegacyData, important?: boolean): void;
@bindThis
public info(message: string, data?: Record<string, any> | null, important = false): void {
this.log('info', message, data, important);
public info(inputOrMessage: LogEntryInput | string, data?: LegacyData, important = false): void {
if (typeof inputOrMessage === 'string') {
this.log('info', inputOrMessage, data, important);
} else {
this.logStructured('info', inputOrMessage);
}
}
/** 致命的な状況を構造化ログとして記録します。 */
public fatal(input: LogEntryInput): void;
public fatal(message: string): void;
@bindThis
public fatal(inputOrMessage: LogEntryInput | string): void {
if (typeof inputOrMessage === 'string') {
this.logStructured('fatal', { message: inputOrMessage });
} else {
this.logStructured('fatal', inputOrMessage);
}
}
}
+7 -3
View File
@@ -52,15 +52,19 @@ export type LogTraceContext = {
/** LogManagerが出力直前に呼び出すTrace Context取得処理です。 */
export type LogTraceContextProvider = () => LogTraceContext | undefined;
/** ロガーの呼び出し側が構造化ログとして指定する入力です。 */
export type LogWriteInput = {
readonly level: LogLevel;
/** level別のLoggerメソッドが受け取る構造化ログの共通入力です。 */
export type LogEntryInput = {
readonly message: string;
readonly eventName?: string;
readonly attributes?: Readonly<Record<string, unknown>>;
readonly error?: unknown;
};
/** 動的なlevelを含めてLogManagerへ渡す構造化ログの入力です。 */
export type LogWriteInput = LogEntryInput & {
readonly level: LogLevel;
};
/**
* ロガー名を構成する一要素です。
* 色は見やすい形式での表示だけに使い、ログの意味には影響させません。
@@ -7,6 +7,7 @@ import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import type { MiMeta, UsersRepository } from '@/models/_.js';
import { SignupService } from '@/core/SignupService.js';
import { RoleService } from '@/core/RoleService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
import { DI } from '@/di-symbols.js';
@@ -77,6 +78,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private userEntityService: UserEntityService,
private signupService: SignupService,
private roleService: RoleService,
) {
super(meta, paramDef, async (ps, _me, token) => {
const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
@@ -93,7 +95,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
// 初期パスワードが設定されていないのに初期パスワードが入力された場合
throw new ApiError(meta.errors.wrongInitialPassword);
}
} else if ((this.serverSettings.rootUserId != null && (this.serverSettings.rootUserId !== me?.id)) || token !== null) {
} else if (token !== null || !(await this.roleService.isAdministrator(me))) {
// 初回セットアップではなく、管理者でない場合 or 外部トークンを使用している場合
throw new ApiError(meta.errors.accessDenied);
}
@@ -76,6 +76,91 @@ describe('Logger', () => {
}));
});
test('supports structured input through every level-specific method', () => {
const logger = new Logger('root').createSubLogger('child');
const error = new Error('broken');
const input = {
eventName: 'example.failed',
message: 'failed',
attributes: { id: 'id' },
error,
};
logger.debug(input);
logger.info(input);
logger.warn(input);
logger.error(input);
logger.fatal(input);
expect(mocks.write.mock.calls.map(([entry]) => entry.level)).toEqual([
'debug',
'info',
'warn',
'error',
'fatal',
]);
for (const [entry] of mocks.write.mock.calls) {
expect(entry).toMatchObject({
...input,
context: [
{ name: 'root', color: undefined },
{ name: 'child', color: undefined },
],
});
expect(entry).not.toHaveProperty('compatibility');
}
});
test('level-specific methods own the level even for runtime-invalid input', () => {
const logger = new Logger('root');
// @ts-expect-error level is selected by the method rather than the input object
logger.warn({ level: 'error', message: 'warning' });
expect(mocks.write.mock.calls[0][0]).toMatchObject({
level: 'warn',
message: 'warning',
});
});
test('preserves the legacy string signatures for non-error levels', () => {
const logger = new Logger('root');
logger.debug('debug', { source: 'debug' }, true);
logger.info('info', null, true);
logger.warn('warn', { source: 'warn' });
expect(mocks.write.mock.calls.map(([entry]) => entry.compatibility)).toEqual([
{ legacyLevel: undefined, important: true, data: { source: 'debug' } },
{ legacyLevel: undefined, important: true, data: null },
{ legacyLevel: undefined, important: false, data: { source: 'warn' } },
]);
});
test('records a fatal string through the structured API', () => {
new Logger('root').fatal('fatal message');
expect(mocks.write).toHaveBeenCalledWith({
level: 'fatal',
message: 'fatal message',
context: [{ name: 'root', color: undefined }],
});
});
test('preserves the legacy error string signature', () => {
new Logger('root').error('failed', { requestId: 'request' }, true);
expect(mocks.write).toHaveBeenCalledWith({
level: 'error',
message: 'failed',
context: [{ name: 'root', color: undefined }],
compatibility: {
legacyLevel: undefined,
important: true,
data: { requestId: 'request' },
},
});
});
test('uses Error.toString and adds the Error to existing data', () => {
const logger = new Logger('root');
const error = new TypeError('broken');
-5
View File
@@ -13,8 +13,3 @@ declare const _PERF_PREFIX_: string;
// for dev-mode
declare const _LANGS_FULL_: string[][];
// TagCanvas
interface Window {
TagCanvas: any;
}
-6
View File
@@ -14,9 +14,3 @@ declare const _PERF_PREFIX_: string;
// for dev-mode
declare const _LANGS_FULL_: string[][];
// TagCanvas
interface Window {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
TagCanvas: any;
}
-5
View File
@@ -13,8 +13,3 @@ declare const _PERF_PREFIX_: string;
// for dev-mode
declare const _LANGS_FULL_: string[][];
// TagCanvas
interface Window {
TagCanvas: any;
}
File diff suppressed because one or more lines are too long
+1
View File
@@ -22,6 +22,7 @@
"@mcaptcha/core-glue": "0.1.0-alpha-5",
"@misskey-dev/browser-image-resizer": "2024.1.0",
"@misskey-dev/emoji-data": "17.0.3",
"@misskey-dev/tagcanvas-es": "0.1.2",
"@sentry/vue": "10.65.0",
"@simplewebauthn/browser": "13.3.0",
"@syuilo/aiscript": "1.2.1",
@@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@pointerup.passive="onPointerup"
@pointercancel.passive="cancelPointerGesture"
@touchstart.passive="onTouchstart"
@touchmove.passive="onTouchmove"
@touchmove="onTouchmove"
@touchcancel.passive="cancelPointerGesture"
@contextmenu="cancelPointerGesture"
@wheel="onWheel"
@@ -721,6 +721,13 @@ function onTouchstart(ev: TouchEvent) {
function onTouchmove(ev: TouchEvent) {
doubleTapDetector.onTouchmove(ev);
// preventDefault
// touch-action: none
//
if (isVerticalSwiping || isHorizontalSwiping || isZooming.value || pointerEventCache.size > 1) {
ev.preventDefault();
}
}
//#region inertia
+45 -39
View File
@@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div ref="rootEl" :class="$style.root">
<canvas :id="idForCanvas" ref="canvasEl" style="display: block;" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas>
<div :id="idForTags" ref="tagsEl" :class="$style.tags">
<canvas ref="canvasEl" style="display: block;" :width="width" height="300" @contextmenu.prevent="() => {}"></canvas>
<div ref="tagsEl" :class="$style.tags">
<ul>
<slot></slot>
</ul>
@@ -15,62 +15,68 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { onMounted, watch, onBeforeUnmount, ref, useTemplateRef } from 'vue';
import { onMounted, onBeforeUnmount, nextTick, ref, shallowRef, useTemplateRef } from 'vue';
import { themeManager } from '@/theme.js';
import tinycolor from 'tinycolor2';
import { TagCanvas } from '@misskey-dev/tagcanvas-es';
const loaded = !!window.TagCanvas;
const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
const available = ref(false);
const rootEl = useTemplateRef('rootEl');
const canvasEl = useTemplateRef('canvasEl');
const tagsEl = useTemplateRef('tagsEl');
const tagCanvas = shallowRef<TagCanvas | null>(null);
const width = ref(300);
watch(available, () => {
try {
window.TagCanvas.Start(idForCanvas, idForTags, {
textColour: '#ffffff',
outlineColour: tinycolor(themeManager.currentCompiledTheme!.accent).toHexString(),
outlineRadius: 10,
initial: [-0.030, -0.010],
frontSelect: true,
imageRadius: 8,
//dragControl: true,
dragThreshold: 3,
wheelZoom: false,
reverse: true,
depth: 0.5,
maxSpeed: 0.2,
minSpeed: 0.003,
stretchX: 0.8,
stretchY: 0.8,
});
} catch (err) {}
});
function createTagCanvas() {
if (tagCanvas.value) {
tagCanvas.value.destroy();
tagCanvas.value = null;
}
if (tagsEl.value == null || canvasEl.value == null) return;
if (tagsEl.value.children[0].children.length === 0) return;
tagCanvas.value = new TagCanvas(canvasEl.value, {
tagContainer: tagsEl.value,
textColor: '#ffffff',
outlineColor: tinycolor(themeManager.currentCompiledTheme!.accent).toHexString(),
outlineRadius: 10,
initial: [-0.030, -0.010],
frontSelect: true,
imageRadius: 8,
// dragControl: true,
dragThreshold: 3,
wheelZoom: false,
reverse: true,
depth: 0.5,
maxSpeed: 0.2,
minSpeed: 0.003,
stretchX: 0.8,
stretchY: 0.8,
});
}
onMounted(() => {
if (rootEl.value) width.value = rootEl.value.offsetWidth;
if (loaded) {
available.value = true;
} else {
window.document.head.appendChild(Object.assign(window.document.createElement('script'), {
async: true,
src: '/client-assets/tagcanvas.min.js',
})).addEventListener('load', () => available.value = true);
}
nextTick(() => {
createTagCanvas();
});
});
onBeforeUnmount(() => {
if (window.TagCanvas) window.TagCanvas.Delete(idForCanvas);
if (tagCanvas.value) {
tagCanvas.value.destroy();
tagCanvas.value = null;
}
});
defineExpose({
update: () => {
window.TagCanvas.Update(idForCanvas);
if (tagCanvas.value) {
tagCanvas.value.update();
} else {
createTagCanvas();
}
},
});
</script>
+1 -1
View File
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2026.7.0-beta.4",
"version": "2026.7.0-beta.5",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
+8
View File
@@ -673,6 +673,9 @@ importers:
'@misskey-dev/emoji-data':
specifier: 17.0.3
version: 17.0.3
'@misskey-dev/tagcanvas-es':
specifier: 0.1.2
version: 0.1.2
'@sentry/vue':
specifier: 10.65.0
version: 10.65.0(vue@3.5.39(typescript@6.0.2))
@@ -2447,6 +2450,9 @@ packages:
'@misskey-dev/summaly@5.5.1':
resolution: {integrity: sha512-CfpVtmFgltjNS1FAS5+bOluKAF/irZuQnUxAw66ydnp/uesiNNu62ltf9gEgwutT5S+itGtlnP87AOx6DDn/1A==}
'@misskey-dev/tagcanvas-es@0.1.2':
resolution: {integrity: sha512-SGqyu30/CCQyIC95Zcmj3k1vYXwfX66/arsxpl1UUulUqvlRpTpuhB7kEAAps1lqXin0rQzXknb31EeveNKsaw==}
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4':
resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==}
cpu: [arm64]
@@ -10505,6 +10511,8 @@ snapshots:
optionalDependencies:
fastify: 5.8.5
'@misskey-dev/tagcanvas-es@0.1.2': {}
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4':
optional: true