Compare commits

..

No commits in common. "0e8e523d84b6d1158fd48e541dd26b60aff1c4a3" and "8090429e9af510a965d7bcc3532f3f9331c45406" have entirely different histories.

15 changed files with 54 additions and 169 deletions

View File

@ -54,7 +54,6 @@
- フロントエンドの読み込みサイズを軽量化しました - フロントエンドの読み込みサイズを軽量化しました
- ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。 - ほとんどの言語のハイライトは問題なく行えますが、互換性の問題により一部の言語が正常にハイライトできなくなる可能性があります。詳しくは https://shiki.style/references/engine-js-compat をご覧ください。
- Fix: チャットに動画ファイルを送付すると、動画の表示が崩れてしまい視聴出来ない問題を修正 - Fix: チャットに動画ファイルを送付すると、動画の表示が崩れてしまい視聴出来ない問題を修正
- Fix: アカウント依存かつ初期状態である設定値をサーバー同期しようとした際に正しくコンフリクト検出されない問題を修正
- Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正 - Fix: "時計"ウィジェット(Clock)において、Transparent設定が有効でも、その背景が透過されない問題を修正
- Fix: 一定時間操作がなかったら動画プレイヤーのコントロールを隠すように - Fix: 一定時間操作がなかったら動画プレイヤーのコントロールを隠すように
- Fix: Twitchのクリップがプレイヤーで再生できない問題を修正 - Fix: Twitchのクリップがプレイヤーで再生できない問題を修正
@ -72,7 +71,7 @@
- Fix: ミュート対象ユーザーが引用されているートがRNされたときにミュートを貫通してしまう問題を修正 #16009 - Fix: ミュート対象ユーザーが引用されているートがRNされたときにミュートを貫通してしまう問題を修正 #16009
- Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように - Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように
- Fix: コントロールパネルから招待コードを作成すると作成者の情報が記録されない問題を修正 - Fix: コントロールパネルから招待コードを作成すると作成者の情報が記録されない問題を修正
- Fix: コントロールパネルのジョブキューページからPausedなジョブ一覧を閲覧できない問題を修正
## 2025.5.0 ## 2025.5.0

4
locales/index.d.ts vendored
View File

@ -12041,10 +12041,6 @@ export interface Locale extends ILocale {
* *
*/ */
"tip": string; "tip": string;
/**
*
*/
"quitWithoutSaveConfirm": string;
/** /**
* *
*/ */

View File

@ -3225,7 +3225,6 @@ watermark: "ウォーターマーク"
defaultPreset: "デフォルトのプリセット" defaultPreset: "デフォルトのプリセット"
_watermarkEditor: _watermarkEditor:
tip: "画像にクレジット情報などのウォーターマークを追加することができます。" tip: "画像にクレジット情報などのウォーターマークを追加することができます。"
quitWithoutSaveConfirm: "保存せずに終了しますか?"
title: "ウォーターマークの編集" title: "ウォーターマークの編集"
cover: "全体に被せる" cover: "全体に被せる"
repeat: "敷き詰める" repeat: "敷き詰める"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2025.5.1-beta.6", "version": "2025.5.1-beta.5",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -28,7 +28,7 @@ export const paramDef = {
type: 'object', type: 'object',
properties: { properties: {
queue: { type: 'string', enum: QUEUE_TYPES }, queue: { type: 'string', enum: QUEUE_TYPES },
state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed', 'paused'] } }, state: { type: 'array', items: { type: 'string', enum: ['active', 'wait', 'delayed', 'completed', 'failed'] } },
search: { type: 'string' }, search: { type: 'string' },
}, },
required: ['queue', 'state'], required: ['queue', 'state'],

View File

@ -126,8 +126,8 @@ onMounted(async () => {
imageBitmap = await window.createImageBitmap(props.image); imageBitmap = await window.createImageBitmap(props.image);
const MAX_W = 1000; const MAX_W = 500;
const MAX_H = 1000; const MAX_H = 500;
let w = imageBitmap.width; let w = imageBitmap.width;
let h = imageBitmap.height; let h = imageBitmap.height;

View File

@ -88,12 +88,7 @@ const emit = defineEmits<{
const dialog = useTemplateRef('dialog'); const dialog = useTemplateRef('dialog');
async function cancel() { function cancel() {
const { canceled } = await os.confirm({
text: i18n.ts._watermarkEditor.quitWithoutSaveConfirm,
});
if (canceled) return;
emit('cancel'); emit('cancel');
dialog.value?.close(); dialog.value?.close();
} }

View File

@ -547,28 +547,14 @@ export function success(): Promise<void> {
}); });
} }
export function waiting(options: { text?: string } = {}) { export function waiting(text?: string | null): () => void {
window.document.body.setAttribute('inert', 'true'); window.document.body.setAttribute('inert', 'true');
const showing = ref(true); const showing = ref(true);
const isSuccess = ref(false);
function done(doneOptions: { success?: boolean } = {}) {
if (doneOptions.success) {
isSuccess.value = true;
window.setTimeout(() => {
showing.value = false;
}, 1000);
} else {
showing.value = false;
}
}
// NOTE: dynamic importすると挙動がおかしくなる(showingの変更が伝播しない)
const { dispose } = popup(MkWaitingDialog, { const { dispose } = popup(MkWaitingDialog, {
success: isSuccess, success: false,
showing: showing, showing: showing,
text: options.text, text,
}, { }, {
closed: () => { closed: () => {
window.document.body.removeAttribute('inert'); window.document.body.removeAttribute('inert');
@ -576,7 +562,9 @@ export function waiting(options: { text?: string } = {}) {
}, },
}); });
return done; return () => {
showing.value = false;
};
} }
export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> { export function form<F extends Form>(title: string, f: F): Promise<{ canceled: true, result?: undefined } | { canceled?: false, result: GetFormResultType<F> }> {

View File

@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder> </MkFolder>
<div class="_buttonsCenter"> <div class="_buttonsCenter">
<MkButton primary rounded @click="onFileSelectClicked">{{ i18n.ts.upload }}</MkButton> <MkButton primary rounded @click="onFileSelectClicked">{{ i18n.ts.uplaod }}</MkButton>
<MkButton primary rounded @click="onDriveSelectClicked">{{ i18n.ts.fromDrive }}</MkButton> <MkButton primary rounded @click="onDriveSelectClicked">{{ i18n.ts.fromDrive }}</MkButton>
</div> </div>

View File

@ -149,7 +149,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-if="matchQuery([i18n.ts._role._options.uploadableFileTypes, 'uploadableFileTypes'])"> <MkFolder v-if="matchQuery([i18n.ts._role._options.uploadableFileTypes, 'uploadableFileTypes'])">
<template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template> <template #label>{{ i18n.ts._role._options.uploadableFileTypes }}</template>
<template #suffix>...</template> <template #suffix>...</template>
<MkTextarea :modelValue="policies.uploadableFileTypes.join('\n')" @update:modelValue="v => policies.uploadableFileTypes = v.split('\n')"> <MkTextarea :modelValue="policies.uploadableFileTypes.join('\n')">
<template #caption> <template #caption>
<div>{{ i18n.ts._role._options.uploadableFileTypes_caption }}</div> <div>{{ i18n.ts._role._options.uploadableFileTypes_caption }}</div>
<div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }}</div> <div><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i> {{ i18n.tsx._role._options.uploadableFileTypes_caption2({ x: 'application/octet-stream' }) }}</div>

View File

@ -15,7 +15,7 @@ import { i18n } from '@/i18n.js';
// TODO: そのうち消す // TODO: そのうち消す
export function migrateOldSettings() { export function migrateOldSettings() {
os.waiting({ text: i18n.ts.settingsMigrating }); os.waiting(i18n.ts.settingsMigrating);
store.loaded.then(async () => { store.loaded.then(async () => {
misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []).then((themes: any) => { misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []).then((themes: any) => {

View File

@ -14,7 +14,6 @@ import type { DeckProfile } from '@/deck.js';
import type { PreferencesDefinition } from './manager.js'; import type { PreferencesDefinition } from './manager.js';
import type { WatermarkPreset } from '@/utility/watermark.js'; import type { WatermarkPreset } from '@/utility/watermark.js';
import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js'; import { DEFAULT_DEVICE_KIND } from '@/utility/device-kind.js';
import { deepEqual } from '@/utility/deep-equal.js';
/** サウンド設定 */ /** サウンド設定 */
export type SoundStore = { export type SoundStore = {
@ -89,20 +88,9 @@ export const PREF_DEF = {
emojis: string[]; emojis: string[];
}[], }[],
mergeStrategy: (a, b) => { mergeStrategy: (a, b) => {
const mergedItems = [] as (typeof a)[]; const sameIdExists = a.some(x => b.some(y => x.id === y.id));
for (const x of a.concat(b)) { if (sameIdExists) throw new Error();
const sameIdItem = mergedItems.find(y => y.id === x.id); return a.concat(b);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
}, },
}, },
emojiPaletteForReaction: { emojiPaletteForReaction: {
@ -120,20 +108,9 @@ export const PREF_DEF = {
themes: { themes: {
default: [] as Theme[], default: [] as Theme[],
mergeStrategy: (a, b) => { mergeStrategy: (a, b) => {
const mergedItems = [] as (typeof a)[]; const sameIdExists = a.some(x => b.some(y => x.id === y.id));
for (const x of a.concat(b)) { if (sameIdExists) throw new Error();
const sameIdItem = mergedItems.find(y => y.id === x.id); return a.concat(b);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
}, },
}, },
lightTheme: { lightTheme: {
@ -398,20 +375,9 @@ export const PREF_DEF = {
accountDependent: true, accountDependent: true,
default: [] as WatermarkPreset[], default: [] as WatermarkPreset[],
mergeStrategy: (a, b) => { mergeStrategy: (a, b) => {
const mergedItems = [] as (typeof a)[]; const sameIdExists = a.some(x => b.some(y => x.id === y.id));
for (const x of a.concat(b)) { if (sameIdExists) throw new Error();
const sameIdItem = mergedItems.find(y => y.id === x.id); return a.concat(b);
if (sameIdItem != null) {
if (deepEqual(x, sameIdItem)) { // 完全な重複は無視
continue;
} else { // IDは同じなのに内容が違う場合はマージ不可とする
throw new Error();
}
} else {
mergedItems.push(x);
}
}
return mergedItems;
}, },
}, },
defaultWatermarkPresetId: { defaultWatermarkPresetId: {

View File

@ -104,8 +104,6 @@ export function getInitialPrefValue<K extends keyof PREF>(k: K): ValueOf<K> {
} }
} }
// TODO: PreferencesManagerForGuest のような非ログイン専用のクラスを分離すれば$iのnullチェックやaccountがnullであるスコープのレコード挿入などが不要になり綺麗になるかもしれない
// NOTE: accountDependentな設定は初期状態であってもアカウントごとのスコープでレコードを作成しておかないと、サーバー同期する際に正しく動作しなくなる
export class PreferencesManager { export class PreferencesManager {
private storageProvider: StorageProvider; private storageProvider: StorageProvider;
public profile: PreferencesProfile; public profile: PreferencesProfile;
@ -141,11 +139,11 @@ export class PreferencesManager {
// TODO: 定期的にクラウドの値をフェッチ // TODO: 定期的にクラウドの値をフェッチ
} }
private static isAccountDependentKey<K extends keyof PREF>(key: K): boolean { private isAccountDependentKey<K extends keyof PREF>(key: K): boolean {
return (PREF_DEF as PreferencesDefinition)[key].accountDependent === true; return (PREF_DEF as PreferencesDefinition)[key].accountDependent === true;
} }
private static isServerDependentKey<K extends keyof PREF>(key: K): boolean { private isServerDependentKey<K extends keyof PREF>(key: K): boolean {
return (PREF_DEF as PreferencesDefinition)[key].serverDependent === true; return (PREF_DEF as PreferencesDefinition)[key].serverDependent === true;
} }
@ -168,7 +166,7 @@ export class PreferencesManager {
const record = this.getMatchedRecordOf(key); const record = this.getMatchedRecordOf(key);
if (parseScope(record[0]).account == null && PreferencesManager.isAccountDependentKey(key)) { if (parseScope(record[0]).account == null && this.isAccountDependentKey(key)) {
this.profile.preferences[key].push([makeScope({ this.profile.preferences[key].push([makeScope({
server: host, server: host,
account: $i!.id, account: $i!.id,
@ -177,7 +175,7 @@ export class PreferencesManager {
return; return;
} }
if (parseScope(record[0]).server == null && PreferencesManager.isServerDependentKey(key)) { if (parseScope(record[0]).server == null && this.isServerDependentKey(key)) {
this.profile.preferences[key].push([makeScope({ this.profile.preferences[key].push([makeScope({
server: host, server: host,
}), v, {}]); }), v, {}]);
@ -278,19 +276,7 @@ export class PreferencesManager {
public static newProfile(): PreferencesProfile { public static newProfile(): PreferencesProfile {
const data = {} as PreferencesProfile['preferences']; const data = {} as PreferencesProfile['preferences'];
for (const key in PREF_DEF) { for (const key in PREF_DEF) {
const v = getInitialPrefValue(key as keyof typeof PREF_DEF); data[key] = [[makeScope({}), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]];
if (PreferencesManager.isAccountDependentKey(key as keyof typeof PREF_DEF)) {
data[key] = $i ? [[makeScope({}), v, {}], [makeScope({
server: host,
account: $i.id,
}), v, {}]] : [[makeScope({}), v, {}]];
} else if (PreferencesManager.isServerDependentKey(key as keyof typeof PREF_DEF)) {
data[key] = [[makeScope({
server: host,
}), v, {}]];
} else {
data[key] = [[makeScope({}), v, {}]];
}
} }
return { return {
id: uuid(), id: uuid(),
@ -307,36 +293,18 @@ export class PreferencesManager {
for (const key in PREF_DEF) { for (const key in PREF_DEF) {
const records = profileLike.preferences[key]; const records = profileLike.preferences[key];
if (records == null || records.length === 0) { if (records == null || records.length === 0) {
const v = getInitialPrefValue(key as keyof typeof PREF_DEF); data[key] = [[makeScope({}), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]];
if (PreferencesManager.isAccountDependentKey(key as keyof typeof PREF_DEF)) {
data[key] = $i ? [[makeScope({}), v, {}], [makeScope({
server: host,
account: $i.id,
}), v, {}]] : [[makeScope({}), v, {}]];
} else if (PreferencesManager.isServerDependentKey(key as keyof typeof PREF_DEF)) {
data[key] = [[makeScope({
server: host,
}), v, {}]];
} else {
data[key] = [[makeScope({}), v, {}]];
}
continue; continue;
} else { } else {
if ($i && PreferencesManager.isAccountDependentKey(key as keyof typeof PREF_DEF) && !records.some(([scope]) => parseScope(scope).server === host && parseScope(scope).account === $i!.id)) {
data[key] = records.concat([[makeScope({
server: host,
account: $i.id,
}), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]]);
continue;
}
if ($i && PreferencesManager.isServerDependentKey(key as keyof typeof PREF_DEF) && !records.some(([scope]) => parseScope(scope).server === host)) {
data[key] = records.concat([[makeScope({
server: host,
}), getInitialPrefValue(key as keyof typeof PREF_DEF), {}]]);
continue;
}
data[key] = records; data[key] = records;
// alpha段階ではmetaが無かったのでマイグレート
// TODO: そのうち消す
for (const record of data[key] as any[][]) {
if (record.length === 2) {
record.push({});
}
}
} }
} }
@ -374,7 +342,7 @@ export class PreferencesManager {
public setAccountOverride<K extends keyof PREF>(key: K) { public setAccountOverride<K extends keyof PREF>(key: K) {
if ($i == null) return; if ($i == null) return;
if (PreferencesManager.isAccountDependentKey(key)) throw new Error('already account-dependent'); if (this.isAccountDependentKey(key)) throw new Error('already account-dependent');
if (this.isAccountOverrided(key)) return; if (this.isAccountOverrided(key)) return;
const records = this.profile.preferences[key]; const records = this.profile.preferences[key];
@ -388,7 +356,7 @@ export class PreferencesManager {
public clearAccountOverride<K extends keyof PREF>(key: K) { public clearAccountOverride<K extends keyof PREF>(key: K) {
if ($i == null) return; if ($i == null) return;
if (PreferencesManager.isAccountDependentKey(key)) throw new Error('cannot clear override for this account-dependent property'); if (this.isAccountDependentKey(key)) throw new Error('cannot clear override for this account-dependent property');
const records = this.profile.preferences[key]; const records = this.profile.preferences[key];
@ -409,12 +377,14 @@ export class PreferencesManager {
public async enableSync<K extends keyof PREF>(key: K): Promise<{ enabled: boolean; } | null> { public async enableSync<K extends keyof PREF>(key: K): Promise<{ enabled: boolean; } | null> {
if (this.isSyncEnabled(key)) return Promise.resolve(null); if (this.isSyncEnabled(key)) return Promise.resolve(null);
// undefined ... cancel const record = this.getMatchedRecordOf(key);
async function resolveConflict(local: ValueOf<K>, remote: ValueOf<K>): Promise<ValueOf<K> | undefined> {
const existing = await this.storageProvider.cloudGet({ key, scope: record[0] });
if (existing != null && !deepEqual(existing.value, record[1])) {
const merge = (PREF_DEF as PreferencesDefinition)[key].mergeStrategy; const merge = (PREF_DEF as PreferencesDefinition)[key].mergeStrategy;
let mergedValue: ValueOf<K> | undefined = undefined; // null と区別したいため let mergedValue: ValueOf<K> | undefined = undefined; // null と区別したいため
try { try {
if (merge != null) mergedValue = merge(local, remote); if (merge != null) mergedValue = merge(record[1], existing.value);
} catch (err) { } catch (err) {
// nop // nop
} }
@ -436,51 +406,23 @@ export class PreferencesManager {
}], }],
default: mergedValue !== undefined ? 'merge' : 'remote', default: mergedValue !== undefined ? 'merge' : 'remote',
}); });
if (canceled || choice == null) return undefined; if (canceled || choice == null) return { enabled: false };
if (choice === 'remote') { if (choice === 'remote') {
return remote; this.commit(key, existing.value);
} else if (choice === 'local') { } else if (choice === 'local') {
return local; // nop
} else if (choice === 'merge') { } else if (choice === 'merge') {
return mergedValue!; this.commit(key, mergedValue!);
} }
} }
const record = this.getMatchedRecordOf(key);
let newValue = record[1];
const existing = await this.storageProvider.cloudGet({ key, scope: record[0] });
if (existing != null && !deepEqual(record[1], existing.value)) {
const resolvedValue = await resolveConflict(record[1], existing.value);
if (resolvedValue === undefined) return { enabled: false }; // canceled
newValue = resolvedValue;
}
this.commit(key, newValue);
const done = os.waiting();
try {
await this.storageProvider.cloudSet({ key, scope: record[0], value: newValue });
} catch (err) {
done();
os.alert({
type: 'error',
title: i18n.ts.somethingHappened,
text: err,
});
return { enabled: false };
}
done({ success: true });
record[2].sync = true; record[2].sync = true;
this.save(); this.save();
// awaitの必要性は無い
this.storageProvider.cloudSet({ key, scope: record[0], value: this.s[key] });
return { enabled: true }; return { enabled: true };
} }

View File

@ -1,7 +1,7 @@
{ {
"type": "module", "type": "module",
"name": "misskey-js", "name": "misskey-js",
"version": "2025.5.1-beta.6", "version": "2025.5.1-beta.5",
"description": "Misskey SDK for JavaScript", "description": "Misskey SDK for JavaScript",
"license": "MIT", "license": "MIT",
"main": "./built/index.js", "main": "./built/index.js",

View File

@ -9074,7 +9074,7 @@ export type operations = {
'application/json': { 'application/json': {
/** @enum {string} */ /** @enum {string} */
queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver'; queue: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver';
state: ('active' | 'wait' | 'delayed' | 'completed' | 'failed' | 'paused')[]; state: ('active' | 'wait' | 'delayed' | 'completed' | 'failed')[];
search?: string; search?: string;
}; };
}; };