diff --git a/CHANGELOG.md b/CHANGELOG.md index a88ba3aca0..3b6a1f6f66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 2025.9.0 + +### Client +- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように +- Enhance: /flushページでサイトキャッシュをクリアできるようになりました +- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張 +- Enhance: 「キャッシュを削除」ボタンでブラウザの内部キャッシュの削除も行えるように +- Enhance: Ctrlキー(Commandキー)を押下しながらリンクをクリックすると新しいタブで開くように +- Fix: プッシュ通知を有効にできない問題を修正 +- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正 +- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正 +- Fix: エラー画像が横に引き伸ばされてしまう問題に対応 + +### Server +- Fix: webpなどの画像に対してセンシティブなメディアの検出が適用されていなかった問題を修正 + ## 2025.8.0 ### Note diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index 4b6c8b97c3..63878bf1b7 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -1644,7 +1644,7 @@ _serverSettings: reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat." remoteNotesCleaning: "Neteja automàtica de notes remotes" remoteNotesCleaning_description: "Quan activis aquesta opció, periòdicament es netejaran les notes remotes que no es consultin, això evitarà que la base de dades se" - remoteNotesCleaningMaxProcessingDuration: "D'oració màxima del temps de funcionament del procés de neteja" + remoteNotesCleaningMaxProcessingDuration: "Duració màxima del temps de funcionament del procés de neteja" remoteNotesCleaningExpiryDaysForEachNotes: "Duració mínima de conservació de les notes" inquiryUrl: "URL de consulta " inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació." diff --git a/locales/es-ES.yml b/locales/es-ES.yml index ac983aae37..8a1d2c458b 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -2137,7 +2137,7 @@ _aboutMisskey: _displayOfSensitiveMedia: respect: "Esconder medios marcados como sensibles" ignore: "Mostrar medios marcados como sensibles" - force: "Esconder todala multimedia" + force: "Esconder toda la multimedia" _instanceTicker: none: "No mostrar" remote: "Mostrar a usuarios remotos" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index aea69f6f24..f633e1488f 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1215,6 +1215,7 @@ privacyPolicyUrl: "Ссылка на Политику Конфиденциаль tosAndPrivacyPolicy: "Условия использования и политика конфиденциальности" avatarDecorations: "Украшения для аватара" attach: "Прикрепить" +detachAll: "Убрать всё" angle: "Угол" flip: "Переворот" showAvatarDecorations: "Показать украшения для аватара" @@ -1253,7 +1254,7 @@ clipNoteLimitExceeded: "К этому клипу больше нельзя до performance: "Производительность" modified: "Изменено" signinWithPasskey: "Войдите в систему, используя свой пароль" -unknownWebAuthnKey: "Не известный ключ " +unknownWebAuthnKey: "Неизвестный ключ" passkeyVerificationFailed: "Ошибка проверка ключа доступа " messageToFollower: "Сообщение подписчикам" testCaptchaWarning: "Эта функция предназначена для тестирования CAPTCHA. Не использовать это в рабочей среде" @@ -1268,8 +1269,11 @@ availableRoles: "Доступные роли" federationDisabled: "Федерация отключена для этого сервера. Вы не можете взаимодействовать с пользователями на других серверах." draft: "Черновик" markAsSensitiveConfirm: "Отметить контент как чувствительный?" +preferences: "Основное" resetToDefaultValue: "Сбросить настройки до стандартных" +syncBetweenDevices: "Синхронизировать между устройствами" postForm: "Форма отправки" +textCount: "Количество символов" information: "Описание" inMinutes: "мин" inDays: "сут" @@ -1281,6 +1285,11 @@ _chat: send: "Отправить" _settings: webhook: "Вебхук" + preferencesBanner: "Вы можете настроить общее поведение клиента по вашим предпочтениям" + timelineAndNote: "Лента и заметки" + _chat: + showSenderName: "Показывать имя отправителя" + sendOnEnter: "Использовать Enter для отправки" _delivery: stop: "Заморожено" _type: @@ -1557,6 +1566,12 @@ _achievements: title: "Brain Diver" description: "Опубликована ссылка на песню «Brain Diver»" flavor: "Мисски-Мисски Ла-Ту-Ма" + _bubbleGameExplodingHead: + title: "🤯" + description: "Самый большой объект в Bubble game" + _bubbleGameDoubleExplodingHead: + title: "Двойной🤯" + description: "Два самых больших объекта в Bubble game одновременно!" _role: new: "Новая роль" edit: "Изменить роль" diff --git a/package.json b/package.json index e4cec6aea6..1e7c8507cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2025.8.0", + "version": "2025.9.0-alpha.2", "codename": "nasubi", "repository": { "type": "git", @@ -67,6 +67,7 @@ }, "devDependencies": { "@misskey-dev/eslint-plugin": "2.1.0", + "@types/js-yaml": "4.0.9", "@types/node": "22.17.2", "@typescript-eslint/eslint-plugin": "8.40.0", "@typescript-eslint/parser": "8.40.0", diff --git a/packages/backend/src/core/AiService.ts b/packages/backend/src/core/AiService.ts index 248a9b8979..23ab8082ed 100644 --- a/packages/backend/src/core/AiService.ts +++ b/packages/backend/src/core/AiService.ts @@ -29,7 +29,7 @@ export class AiService { } @bindThis - public async detectSensitive(path: string): Promise { + public async detectSensitive(source: string | Buffer): Promise { try { if (isSupportedCpu === undefined) { isSupportedCpu = await this.computeIsSupportedCpu(); @@ -51,7 +51,7 @@ export class AiService { }); } - const buffer = await fs.promises.readFile(path); + const buffer = source instanceof Buffer ? source : await fs.promises.readFile(source); const image = await tf.node.decodeImage(buffer, 3) as any; try { const predictions = await this.model.classify(image); diff --git a/packages/backend/src/core/FileInfoService.ts b/packages/backend/src/core/FileInfoService.ts index 6250d4d3a1..62a7d24afb 100644 --- a/packages/backend/src/core/FileInfoService.ts +++ b/packages/backend/src/core/FileInfoService.ts @@ -21,6 +21,7 @@ import { LoggerService } from '@/core/LoggerService.js'; import type Logger from '@/logger.js'; import { bindThis } from '@/decorators.js'; import type { PredictionType } from 'nsfwjs'; +import { isMimeImage } from '@/misc/is-mime-image.js'; export type FileInfo = { size: number; @@ -204,16 +205,7 @@ export class FileInfoService { return [sensitive, porn]; } - if ([ - 'image/jpeg', - 'image/png', - 'image/webp', - ].includes(mime)) { - const result = await this.aiService.detectSensitive(source); - if (result) { - [sensitive, porn] = judgePrediction(result); - } - } else if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) { + if (analyzeVideo && (mime === 'image/apng' || mime.startsWith('video/'))) { const [outDir, disposeOutDir] = await createTempDir(); try { const command = FFmpeg() @@ -281,6 +273,23 @@ export class FileInfoService { } finally { disposeOutDir(); } + } else if (isMimeImage(mime, 'sharp-convertible-image-with-bmp')) { + /* + * tfjs-node は限られた画像形式しか受け付けないため、sharp で PNG に変換する + * せっかくなので内部処理で使われる最大サイズの299x299に事前にリサイズする + */ + const png = await (await sharpBmp(source, mime)) + .resize(299, 299, { + withoutEnlargement: false, + }) + .rotate() + .flatten({ background: { r: 119, g: 119, b: 119 } }) // 透過部分を18%グレーで塗りつぶす + .png() + .toBuffer(); + const result = await this.aiService.detectSensitive(png); + if (result) { + [sensitive, porn] = judgePrediction(result); + } } return [sensitive, porn]; diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 3df7ee69ee..7dc07ef4dd 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -31,6 +31,7 @@ import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { NotificationService } from '@/core/NotificationService.js'; import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common'; +// misskey-js の rolePolicies と同期すべし export type RolePolicies = { gtlAvailable: boolean; ltlAvailable: boolean; diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts index 5764a307b0..0b4eeb3455 100644 --- a/packages/backend/src/models/Notification.ts +++ b/packages/backend/src/models/Notification.ts @@ -10,6 +10,7 @@ import { MiAccessToken } from './AccessToken.js'; import { MiRole } from './Role.js'; import { MiDriveFile } from './DriveFile.js'; +// misskey-js の notificationTypes と同期すべし export type MiNotification = { type: 'note'; id: string; diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts index 32818003ad..57d74ef2b1 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -176,6 +176,17 @@ export class ApiServerService { } }); + fastify.all('/clear-browser-cache', (request, reply) => { + if (['GET', 'POST'].includes(request.method)) { + reply.header('Clear-Site-Data', '"cache", "prefetchCache", "prerenderCache", "executionContexts"'); + reply.code(204); + reply.send(); + } else { + reply.code(405); + reply.send(); + } + }); + // Make sure any unknown path under /api returns HTTP 404 Not Found, // because otherwise ClientServerService will return the base client HTML // page with HTTP 200. diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts index b515a0c0c8..3cd83efa1a 100644 --- a/packages/backend/src/server/web/ClientServerService.ts +++ b/packages/backend/src/server/web/ClientServerService.ts @@ -201,6 +201,8 @@ export class ClientServerService { @bindThis public createServer(fastify: FastifyInstance, options: FastifyPluginOptions, done: (err?: Error) => void) { + const configUrl = new URL(this.config.url); + fastify.register(fastifyView, { root: _dirname + '/views', engine: { @@ -239,7 +241,6 @@ export class ClientServerService { done(); }); } else { - const configUrl = new URL(this.config.url); const urlOriginWithoutPort = configUrl.origin.replace(/:\d+$/, ''); const port = (process.env.VITE_PORT ?? '5173'); @@ -887,6 +888,22 @@ export class ClientServerService { [, ...target.split('/').filter(x => x), ...source.split('/').filter(x => x).splice(depth)].join('/'); fastify.get('/flush', async (request, reply) => { + let sendHeader = true; + + if (request.headers['origin']) { + const originURL = new URL(request.headers['origin']); + if (originURL.protocol !== 'https:') { // Clear-Site-Data only supports https + sendHeader = false; + } + if (originURL.host !== configUrl.host) { + sendHeader = false; + } + } + + if (sendHeader) { + reply.header('Clear-Site-Data', '"*"'); + } + reply.header('Set-Cookie', 'http-flush-failed=1; Path=/flush; Max-Age=60'); return await reply.view('flush'); }); diff --git a/packages/backend/src/server/web/views/flush.pug b/packages/backend/src/server/web/views/flush.pug index a73a45212f..7884495d08 100644 --- a/packages/backend/src/server/web/views/flush.pug +++ b/packages/backend/src/server/web/views/flush.pug @@ -6,41 +6,45 @@ html const msg = document.getElementById('msg'); const successText = `\nSuccess Flush! Back to Misskey\n成功しました。Misskeyを開き直してください。`; - message('Start flushing.'); + if (!document.cookie) { + message('Your site data is fully cleared by your browser.'); + message(successText); + } else { + message('Your browser does not support Clear-Site-Data header. Start opportunistic flushing.'); + (async function() { + try { + localStorage.clear(); + message('localStorage cleared.'); - (async function() { - try { - localStorage.clear(); - message('localStorage cleared.'); + const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => { + const delidb = indexedDB.deleteDatabase(name); + delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`)); + delidb.onerror = e => rej(e) + })); - const idbPromises = ['MisskeyClient', 'keyval-store'].map((name, i, arr) => new Promise((res, rej) => { - const delidb = indexedDB.deleteDatabase(name); - delidb.onsuccess = () => res(message(`indexedDB "${name}" cleared. (${i + 1}/${arr.length})`)); - delidb.onerror = e => rej(e) - })); + await Promise.all(idbPromises); - await Promise.all(idbPromises); + if (navigator.serviceWorker.controller) { + navigator.serviceWorker.controller.postMessage('clear'); + await navigator.serviceWorker.getRegistrations() + .then(registrations => { + return Promise.all(registrations.map(registration => registration.unregister())); + }) + .catch(e => { throw new Error(e) }); + } - if (navigator.serviceWorker.controller) { - navigator.serviceWorker.controller.postMessage('clear'); - await navigator.serviceWorker.getRegistrations() - .then(registrations => { - return Promise.all(registrations.map(registration => registration.unregister())); - }) - .catch(e => { throw new Error(e) }); + message(successText); + } catch (e) { + message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`); + message(`\nIf you retry more than 3 times, try manually clearing the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを手動で消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`) + + console.error(e); + setTimeout(() => { + location = '/'; + }, 10000) } - - message(successText); - } catch (e) { - message(`\n${e}\n\nFlush Failed. Please retry.\n失敗しました。もう一度試してみてください。`); - message(`\nIf you retry more than 3 times, clear the browser cache or contact to instance admin.\n3回以上試しても失敗する場合、ブラウザのキャッシュを消去し、それでもだめならインスタンス管理者に連絡してみてください。\n`) - - console.error(e); - setTimeout(() => { - location = '/'; - }, 10000) - } - })(); + })(); + } function message(text) { msg.insertAdjacentHTML('beforeend', `

[${(new Date()).toString()}] ${text.replace(/\n/g,'
')}

`) diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index b2d83fff8b..c8c437afe9 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -54,68 +54,6 @@ https://github.com/sindresorhus/file-type/blob/main/core.js https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Containers */ -export const notificationTypes = [ - 'note', - 'follow', - 'mention', - 'reply', - 'renote', - 'quote', - 'reaction', - 'pollEnded', - 'receiveFollowRequest', - 'followRequestAccepted', - 'roleAssigned', - 'chatRoomInvitationReceived', - 'achievementEarned', - 'exportCompleted', - 'login', - 'createToken', - 'test', - 'app', -] as const; -export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; - -export const ROLE_POLICIES = [ - 'gtlAvailable', - 'ltlAvailable', - 'canPublicNote', - 'mentionLimit', - 'canInvite', - 'inviteLimit', - 'inviteLimitCycle', - 'inviteExpirationTime', - 'canManageCustomEmojis', - 'canManageAvatarDecorations', - 'canSearchNotes', - 'canSearchUsers', - 'canUseTranslator', - 'canHideAds', - 'driveCapacityMb', - 'maxFileSizeMb', - 'alwaysMarkNsfw', - 'canUpdateBioMedia', - 'pinLimit', - 'antennaLimit', - 'wordMuteLimit', - 'webhookLimit', - 'clipLimit', - 'noteEachClipsLimit', - 'userListLimit', - 'userEachUserListsLimit', - 'rateLimitFactor', - 'avatarDecorationLimit', - 'canImportAntennas', - 'canImportBlocking', - 'canImportFollowing', - 'canImportMuting', - 'canImportUserLists', - 'chatAvailability', - 'uploadableFileTypes', - 'noteDraftLimit', - 'watermarkAvailable', -] as const; - export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime']; export const MFM_PARAMS: Record = { tada: ['speed=', 'delay='], diff --git a/packages/frontend/.storybook/fakes.ts b/packages/frontend/.storybook/fakes.ts index 91ef41eedf..ed29c63471 100644 --- a/packages/frontend/.storybook/fakes.ts +++ b/packages/frontend/.storybook/fakes.ts @@ -127,7 +127,7 @@ export function galleryPost(isSensitive = false) { } } -export function file(isSensitive = false) { +export function file(isSensitive = false): entities.DriveFile { return { id: 'somefileid', createdAt: '2016-12-28T22:49:51.000Z', @@ -207,6 +207,7 @@ export function federationInstance(): entities.FederationInstance { isSuspended: false, suspensionState: 'none', isBlocked: false, + isMediaSilenced: false, softwareName: 'misskey', softwareVersion: '2024.5.0', openRegistrations: false, @@ -311,6 +312,8 @@ export function userDetailed(id = 'someuserid', username = 'miskist', host: enti alsoKnownAs: null, notify: 'none', memo: null, + canChat: true, + chatScope: 'everyone', }; } @@ -378,6 +381,7 @@ export function role(params: { asBadge: params.asBadge ?? true, canEditMembersByModerator: params.canEditMembersByModerator ?? false, usersCount: params.usersCount ?? 10, + preserveAssignmentOnMoveAccount: false, condFormula: { id: '', type: 'or', diff --git a/packages/frontend/src/accounts.ts b/packages/frontend/src/accounts.ts index afa2ecb911..60f7cd0b4b 100644 --- a/packages/frontend/src/accounts.ts +++ b/packages/frontend/src/accounts.ts @@ -251,13 +251,30 @@ export async function openAccountMenu(opts: { } }, }; - } else { + } else { // プロファイルを復元した場合などはアカウントのトークンや詳細情報はstoreにキャッシュされていない return { type: 'button' as const, text: username, active: opts.active != null ? opts.active === id : false, action: async () => { - // TODO + const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), { + initialUsername: username, + }, { + done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => { + store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i }); + + if (callback) { + fetchAccount(res.i, id).then(account => { + callback(account); + }); + } else { + switchAccount(host, id); + } + }, + closed: () => { + dispose(); + }, + }); }, }; } diff --git a/packages/frontend/src/aiscript/api.ts b/packages/frontend/src/aiscript/api.ts index a876e94ee8..0549ab76a0 100644 --- a/packages/frontend/src/aiscript/api.ts +++ b/packages/frontend/src/aiscript/api.ts @@ -86,7 +86,7 @@ export function createAiScriptEnv(opts: { storageKey: string, token?: string }) throw new errors.AiScriptRuntimeError('expected param'); } utils.assertObject(param); - return misskeyApi(ep.value, utils.valToJs(param) as object, actualToken).then(res => { + return misskeyApi(ep.value as keyof Misskey.Endpoints, utils.valToJs(param) as object, actualToken).then(res => { return utils.jsToVal(res); }, err => { return values.ERROR('request_failed', utils.jsToVal(err)); diff --git a/packages/frontend/src/cache.ts b/packages/frontend/src/cache.ts index 70078b410d..39cf73feb8 100644 --- a/packages/frontend/src/cache.ts +++ b/packages/frontend/src/cache.ts @@ -7,8 +7,8 @@ import * as Misskey from 'misskey-js'; import { Cache } from '@/utility/cache.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -export const clipsCache = new Cache(1000 * 60 * 30, () => misskeyApi('clips/list')); -export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list')); +export const clipsCache = new Cache(1000 * 60 * 30, () => misskeyApi('clips/list', { limit: 30 })); +export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list', { limit: 30 })); export const userListsCache = new Cache(1000 * 60 * 30, () => misskeyApi('users/lists/list')); -export const antennasCache = new Cache(1000 * 60 * 30, () => misskeyApi('antennas/list')); +export const antennasCache = new Cache(1000 * 60 * 30, () => misskeyApi('antennas/list', { limit: 30 })); export const favoritedChannelsCache = new Cache(1000 * 60 * 30, () => misskeyApi('channels/my-favorites', { limit: 100 })); diff --git a/packages/frontend/src/components/MkAuthConfirm.vue b/packages/frontend/src/components/MkAuthConfirm.vue index b3331d742b..8744b50926 100644 --- a/packages/frontend/src/components/MkAuthConfirm.vue +++ b/packages/frontend/src/components/MkAuthConfirm.vue @@ -167,9 +167,13 @@ async function init() { for (const user of usersRes) { if (users.value.has(user.id)) continue; + const account = accounts.find(a => a.id === user.id); + + if (!account || account.token == null) continue; + users.value.set(user.id, { ...user, - token: accounts.find(a => a.id === user.id)!.token, + token: account.token, }); } } diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index e5b9533cd7..cf5d95e11b 100644 --- a/packages/frontend/src/components/MkAutocomplete.vue +++ b/packages/frontend/src/components/MkAutocomplete.vue @@ -25,7 +25,7 @@ SPDX-License-Identifier: AGPL-3.0-only - + ({{ emoji.aliasOf }}) @@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
    -
  1. +
  2. {{ param }}
@@ -194,6 +194,11 @@ const mfmParams = ref([]); const select = ref(-1); const zIndex = os.claimZIndex('high'); +function completeMfmParam(param: string) { + if (props.type !== 'mfmParam') throw new Error('Invalid type'); + complete('mfmParam', props.q.params.toSpliced(-1, 1, param).join(',')); +} + function complete(type: T, value: CompleteInfo[T]['payload']) { emit('done', { type, value }); emit('closed'); diff --git a/packages/frontend/src/components/MkChartTooltip.vue b/packages/frontend/src/components/MkChartTooltip.vue index 51081ede23..b9d2c8219a 100644 --- a/packages/frontend/src/components/MkChartTooltip.vue +++ b/packages/frontend/src/components/MkChartTooltip.vue @@ -25,12 +25,12 @@ defineProps<{ showing: boolean; x: number; y: number; - title?: string; + title?: string | null; series?: { backgroundColor: string; borderColor: string; text: string; - }[]; + }[] | null; }>(); const emit = defineEmits<{ diff --git a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts index bd6733f9a8..7ac3e2a2cd 100644 --- a/packages/frontend/src/components/MkCropperDialog.stories.impl.ts +++ b/packages/frontend/src/components/MkCropperDialog.stories.impl.ts @@ -38,7 +38,7 @@ export const Default = { }; }, args: { - file: file(), + imageFile: file(), aspectRatio: NaN, }, parameters: { diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 9f1364aec4..19c98c3738 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -699,7 +699,7 @@ useGlobalEvent('driveFoldersDeleted', (folders) => { } }); -let connection: Misskey.ChannelConnection | null = null; +let connection: Misskey.IChannelConnection | null = null; onMounted(() => { if (store.s.realtimeMode) { diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue index d18fe0ed0c..17823deb85 100644 --- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue +++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue @@ -160,7 +160,7 @@ const embedPreviewUrl = computed(() => { const isEmbedWithScrollbar = computed(() => embedRouteWithScrollbar.includes(props.entity)); const header = ref(props.params?.header ?? true); -const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? undefined : 500); +const maxHeight = ref(props.params?.maxHeight !== 0 ? props.params?.maxHeight ?? null : 500); const colorMode = ref<'light' | 'dark' | 'auto'>(props.params?.colorMode ?? 'auto'); const rounded = ref(props.params?.rounded ?? true); diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue index 6ac4441cac..8d697499a5 100644 --- a/packages/frontend/src/components/MkFormDialog.vue +++ b/packages/frontend/src/components/MkFormDialog.vue @@ -41,11 +41,11 @@ SPDX-License-Identifier: AGPL-3.0-only - + - + @@ -77,7 +77,7 @@ import MkRange from './MkRange.vue'; import MkButton from './MkButton.vue'; import MkRadios from './MkRadios.vue'; import XFile from './MkFormDialog.file.vue'; -import type { Form } from '@/utility/form.js'; +import type { EnumItem, Form, RadioFormItem } from '@/utility/form.js'; import MkModalWindow from '@/components/MkModalWindow.vue'; import { i18n } from '@/i18n.js'; @@ -99,7 +99,11 @@ const dialog = useTemplateRef('dialog'); const values = reactive({}); for (const item in props.form) { - values[item] = props.form[item].default ?? null; + if ('default' in props.form[item]) { + values[item] = props.form[item].default ?? null; + } else { + values[item] = null; + } } function ok() { @@ -115,4 +119,20 @@ function cancel() { }); dialog.value?.close(); } + +function getEnumLabel(e: EnumItem) { + return typeof e === 'string' ? e : e.label; +} + +function getEnumValue(e: EnumItem) { + return typeof e === 'string' ? e : e.value; +} + +function getEnumKey(e: EnumItem) { + return typeof e === 'string' ? e : typeof e.value === 'string' ? e.value : JSON.stringify(e.value); +} + +function getRadioKey(e: RadioFormItem['options'][number]) { + return typeof e.value === 'string' ? e.value : JSON.stringify(e.value); +} diff --git a/packages/frontend/src/components/MkImageEffectorFxForm.vue b/packages/frontend/src/components/MkImageEffectorFxForm.vue index d7ab620132..e581b1f743 100644 --- a/packages/frontend/src/components/MkImageEffectorFxForm.vue +++ b/packages/frontend/src/components/MkImageEffectorFxForm.vue @@ -8,7 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
+ v-model="params[k]" + > @@ -53,12 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/components/MkPushNotificationAllowButton.vue b/packages/frontend/src/components/MkPushNotificationAllowButton.vue index c651d3a3f5..9c37eb5e72 100644 --- a/packages/frontend/src/components/MkPushNotificationAllowButton.vue +++ b/packages/frontend/src/components/MkPushNotificationAllowButton.vue @@ -78,7 +78,7 @@ function subscribe() { // SEE: https://developer.mozilla.org/en-US/docs/Web/API/PushManager/subscribe#Parameters return promiseDialog(registration.value.pushManager.subscribe({ userVisibleOnly: true, - applicationServerKey: urlBase64ToBase64(instance.swPublickey), + applicationServerKey: urlBase64ToUint8Array(instance.swPublickey), }) .then(async subscription => { pushSubscription.value = subscription; @@ -131,16 +131,22 @@ function encode(buffer: ArrayBuffer | null) { } /** - * Convert the URL safe base64 string to a base64 string + * Convert the URL safe base64 string to a Uint8Array * @param base64String base64 string */ -function urlBase64ToBase64(base64String: string): string { +function urlBase64ToUint8Array(base64String: string): Uint8Array { const padding = '='.repeat((4 - base64String.length % 4) % 4); const base64 = (base64String + padding) .replace(/-/g, '+') .replace(/_/g, '/'); - return base64; + const rawData = window.atob(base64); + const outputArray = new Uint8Array(rawData.length); + + for (let i = 0; i < rawData.length; ++i) { + outputArray[i] = rawData.charCodeAt(i); + } + return outputArray; } if (navigator.serviceWorker == null) { diff --git a/packages/frontend/src/components/MkServerSetupWizard.vue b/packages/frontend/src/components/MkServerSetupWizard.vue index 1d2dfed297..8e3b41e754 100644 --- a/packages/frontend/src/components/MkServerSetupWizard.vue +++ b/packages/frontend/src/components/MkServerSetupWizard.vue @@ -134,7 +134,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._serverSettings.entrancePageStyle }}:
-
{{ serverSettings.clientOptions.entrancePageStyle }}
+
{{ serverSettings.clientOptions?.entrancePageStyle }}
@@ -191,7 +191,6 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/pages/admin-file.vue b/packages/frontend/src/pages/admin-file.vue index 052829ffe2..608a77c06b 100644 --- a/packages/frontend/src/pages/admin-file.vue +++ b/packages/frontend/src/pages/admin-file.vue @@ -4,197 +4,37 @@ SPDX-License-Identifier: AGPL-3.0-only --> - - diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue index f78a4f27bd..e5e0f087e1 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -152,6 +152,7 @@ SPDX-License-Identifier: AGPL-3.0-only import { ref, computed } from 'vue'; import JSON5 from 'json5'; import { host } from '@@/js/config.js'; +import type { ClientOptions } from '@/instance.js'; import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import * as os from '@/os.js'; @@ -166,9 +167,13 @@ import MkSwitch from '@/components/MkSwitch.vue'; const meta = await misskeyApi('admin/meta'); -const entrancePageStyle = ref(meta.clientOptions.entrancePageStyle ?? 'classic'); -const showTimelineForVisitor = ref(meta.clientOptions.showTimelineForVisitor ?? true); -const showActivitiesForVisitor = ref(meta.clientOptions.showActivitiesForVisitor ?? true); +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +const entrancePageStyle = ref(meta.clientOptions.entrancePageStyle ?? 'classic'); +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +const showTimelineForVisitor = ref(meta.clientOptions.showTimelineForVisitor ?? true); +// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition +const showActivitiesForVisitor = ref(meta.clientOptions.showActivitiesForVisitor ?? true); + const iconUrl = ref(meta.iconUrl); const app192IconUrl = ref(meta.app192IconUrl); const app512IconUrl = ref(meta.app512IconUrl); @@ -186,11 +191,11 @@ const manifestJsonOverride = ref(meta.manifestJsonOverride === '' ? '{}' : JSON. function save() { os.apiWithDialog('admin/update-meta', { - clientOptions: { + clientOptions: ({ entrancePageStyle: entrancePageStyle.value, showTimelineForVisitor: showTimelineForVisitor.value, showActivitiesForVisitor: showActivitiesForVisitor.value, - }, + } as ClientOptions) as any, iconUrl: iconUrl.value, app192IconUrl: app192IconUrl.value, app512IconUrl: app512IconUrl.value, diff --git a/packages/frontend/src/pages/admin/database.vue b/packages/frontend/src/pages/admin/database.vue index d51f43c098..f41967d3bd 100644 --- a/packages/frontend/src/pages/admin/database.vue +++ b/packages/frontend/src/pages/admin/database.vue @@ -6,19 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only + + diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index 3fd462e0b9..12d1a37390 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -4,162 +4,35 @@ SPDX-License-Identifier: AGPL-3.0-only --> - - diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue index 71c957460c..5d308e6b29 100644 --- a/packages/frontend/src/pages/notifications.vue +++ b/packages/frontend/src/pages/notifications.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only