Merge branch 'develop' into fix-13913

This commit is contained in:
かっこかり 2025-09-06 14:53:12 +09:00 committed by GitHub
commit 9c0857e09c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
73 changed files with 701 additions and 529 deletions

View File

@ -1,10 +1,17 @@
## Unreleased
## 2025.9.0
### General
-
### Client
-
- Enhance: AiScriptAppウィジェットで構文エラーを検知してもダイアログではなくウィジェット内にエラーを表示するように
- Enhance: /flushページでサイトキャッシュをクリアできるようになりました
- Enhance: クリップ/リスト/アンテナ/ロール追加系メニュー項目において、表示件数を拡張
- Enhance: 「キャッシュを削除」ボタンでブラウザの内部キャッシュの削除も行えるように
- Fix: プッシュ通知を有効にできない問題を修正
- Fix: RSSティッカーウィジェットが正しく動作しない問題を修正
- Fix: プロファイルを復元後アカウントの切り替えができない問題を修正
- Fix: エラー画像が横に引き伸ばされてしまう問題に対応
### Server
-

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.8.0",
"version": "2025.9.0-alpha.1",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -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;

View File

@ -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;

View File

@ -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.

View File

@ -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');
});

View File

@ -6,41 +6,45 @@ html
const msg = document.getElementById('msg');
const successText = `\nSuccess Flush! <a href="/">Back to Misskey</a>\n成功しました。<a href="/">Misskeyを開き直してください。</a>`;
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. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
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. <a href="/flush">Please retry.</a>\n失敗しました。<a href="/flush">もう一度試してみてください。</a>`);
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', `<p>[${(new Date()).toString()}] ${text.replace(/\n/g,'<br>')}</p>`)

View File

@ -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<typeof MFM_TAGS[number], string[]> = {
tada: ['speed=', 'delay='],

View File

@ -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',

View File

@ -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();
},
});
},
};
}

View File

@ -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<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
export const clipsCache = new Cache<Misskey.entities.Clip[]>(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<Misskey.entities.UserList[]>(1000 * 60 * 30, () => misskeyApi('users/lists/list'));
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list'));
export const antennasCache = new Cache<Misskey.entities.Antenna[]>(1000 * 60 * 30, () => misskeyApi('antennas/list', { limit: 30 }));
export const favoritedChannelsCache = new Cache<Misskey.entities.Channel[]>(1000 * 60 * 30, () => misskeyApi('channels/my-favorites', { limit: 100 }));

View File

@ -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<{

View File

@ -8,7 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-for="v, k in paramDefs" :key="k">
<MkSwitch
v-if="v.type === 'boolean'"
v-model="params[k]">
v-model="params[k]"
>
<template #label>{{ v.label ?? k }}</template>
<template v-if="v.caption != null" #caption>{{ v.caption }}</template>
</MkSwitch>
@ -53,12 +54,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script setup lang="ts">
import type { ImageEffectorRGB, ImageEffectorFxParamDefs } from '@/utility/image-effector/ImageEffector.js';
import MkInput from '@/components/MkInput.vue';
import MkRadios from '@/components/MkRadios.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkRange from '@/components/MkRange.vue';
import { i18n } from '@/i18n.js';
import type { ImageEffectorRGB, ImageEffectorFxParamDefs } from '@/utility/image-effector/ImageEffector.js';
defineProps<{
paramDefs: ImageEffectorFxParamDefs;

View File

@ -44,10 +44,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts">
type SupportedTypes = 'text' | 'password' | 'email' | 'url' | 'tel' | 'number' | 'search';
type SupportedTypes = 'text' | 'password' | 'email' | 'url' | 'tel' | 'number' | 'search' | 'date' | 'time' | 'datetime-local' | 'color';
type ModelValueType<T extends SupportedTypes> =
T extends 'number' ? number :
T extends 'text' | 'password' | 'email' | 'url' | 'tel' | 'search' ? string :
T extends 'text' | 'password' | 'email' | 'url' | 'tel' | 'search' | 'date' | 'time' | 'datetime-local' | 'color' ? string :
never;
</script>

View File

@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, useTemplateRef } from 'vue';
import { notificationTypes } from '@@/js/const.js';
import { notificationTypes } from 'misskey-js';
import MkSwitch from './MkSwitch.vue';
import MkInfo from './MkInfo.vue';
import MkButton from './MkButton.vue';

View File

@ -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) {

View File

@ -191,7 +191,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script setup lang="ts">
import { computed, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { ROLE_POLICIES } from '@@/js/const.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import * as os from '@/os.js';

View File

@ -69,9 +69,11 @@ import MkInfo from '@/components/MkInfo.vue';
const props = withDefaults(defineProps<{
message?: string,
openOnRemote?: OpenOnRemoteOptions,
initialUsername?: string;
}>(), {
message: '',
openOnRemote: undefined,
initialUsername: undefined,
});
const emit = defineEmits<{
@ -81,7 +83,7 @@ const emit = defineEmits<{
const host = toUnicode(configHost);
const username = ref('');
const username = ref(props.initialUsername ?? '');
//#region Open on remote
function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {

View File

@ -20,6 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
key="input"
:message="message"
:openOnRemote="openOnRemote"
:initialUsername="initialUsername"
@usernameSubmitted="onUsernameSubmitted"
@passkeyClick="onPasskeyLogin"
@ -89,10 +90,12 @@ const props = withDefaults(defineProps<{
autoSet?: boolean;
message?: string,
openOnRemote?: OpenOnRemoteOptions,
initialUsername?: string;
}>(), {
autoSet: false,
message: '',
openOnRemote: undefined,
initialUsername: undefined,
});
const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input');

View File

@ -16,7 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<button :class="$style.closeButton" class="_button" @click="onClose"><i class="ti ti-x"></i></button>
</div>
<div :class="$style.content">
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
<MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" :initialUsername="initialUsername" @login="onLogin"/>
</div>
</div>
</MkModal>
@ -34,10 +34,12 @@ withDefaults(defineProps<{
autoSet?: boolean;
message?: string,
openOnRemote?: OpenOnRemoteOptions,
initialUsername?: string;
}>(), {
autoSet: false,
message: '',
openOnRemote: undefined,
initialUsername: undefined,
});
const emit = defineEmits<{

View File

@ -44,10 +44,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onUnmounted, onMounted, computed, useTemplateRef, TransitionGroup, markRaw, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { notificationTypes } from 'misskey-js';
import { useInterval } from '@@/js/use-interval.js';
import { useDocumentVisibility } from '@@/js/use-document-visibility.js';
import { getScrollContainer, scrollToTop } from '@@/js/scroll.js';
import type { notificationTypes } from '@@/js/const.js';
import XNotification from '@/components/MkNotification.vue';
import MkNote from '@/components/MkNote.vue';
import { useStream } from '@/stream.js';

View File

@ -41,8 +41,7 @@ const props = defineProps<{
.img {
vertical-align: bottom;
height: 128px;
aspect-ratio: 1;
margin-bottom: 16px;
margin: auto auto 16px;
border-radius: 16px;
}

View File

@ -1,3 +1,4 @@
@ -1,70 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
@ -12,8 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else>
<div :class="$style.error">
<div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</div>
<MkButton inline style="margin-top: 16px;" @click="retry"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
<slot name="error" :error="error">
<div><i class="ti ti-alert-triangle"></i> {{ i18n.ts.somethingHappened }}</div>
<div v-if="error">{{ JSON.stringify(error) }}</div>
<MkButton inline style="margin-top: 16px;" @click="retry"><i class="ti ti-reload"></i> {{ i18n.ts.retry }}</MkButton>
</slot>
</div>
</div>
</template>
@ -27,15 +31,17 @@ const props = defineProps<{
p: () => Promise<T>;
}>();
const emit = defineEmits<{
(ev: 'resolved', result: T): void;
}>();
const pending = ref(true);
const resolved = ref(false);
const rejected = ref(false);
const result = ref<T | null>(null);
const error = ref<any | null>(null);
const process = () => {
if (props.p == null) {
return;
}
const promise = props.p();
pending.value = true;
resolved.value = false;
@ -44,10 +50,12 @@ const process = () => {
pending.value = false;
resolved.value = true;
result.value = _result;
emit('resolved', _result);
});
promise.catch(() => {
promise.catch((_error) => {
pending.value = false;
rejected.value = true;
error.value = _error;
});
};

View File

@ -48,6 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-else-if="cellType === 'image'">
<img
v-if="cell.value && typeof cell.value === 'string'"
:src="cell.value"
:alt="cell.value"
:class="$style.viewImage"

View File

@ -20,6 +20,7 @@ import NestedRouterView from './global/NestedRouterView.vue';
import StackingRouterView from './global/StackingRouterView.vue';
import MkLoading from './global/MkLoading.vue';
import MkError from './global/MkError.vue';
import MkSuspense from './global/MkSuspense.vue';
import MkAd from './global/MkAd.vue';
import MkPageHeader from './global/MkPageHeader.vue';
import MkStickyContainer from './global/MkStickyContainer.vue';
@ -60,6 +61,7 @@ export const components = {
MkUrl: MkUrl,
MkLoading: MkLoading,
MkError: MkError,
MkSuspense: MkSuspense,
MkAd: MkAd,
MkPageHeader: MkPageHeader,
MkStickyContainer: MkStickyContainer,
@ -94,6 +96,7 @@ declare module '@vue/runtime-core' {
MkUrl: typeof MkUrl;
MkLoading: typeof MkLoading;
MkError: typeof MkError;
MkSuspense: typeof MkSuspense;
MkAd: typeof MkAd;
MkPageHeader: typeof MkPageHeader;
MkStickyContainer: typeof MkStickyContainer;

View File

@ -15,7 +15,7 @@ import * as Misskey from 'misskey-js';
import MkMediaList from '@/components/MkMediaList.vue';
const props = defineProps<{
block: Misskey.entities.PageBlock,
block: Extract<Misskey.entities.PageBlock, { type: 'image' }>,
page: Misskey.entities.Page,
}>();

View File

@ -18,7 +18,7 @@ import MkNoteDetailed from '@/components/MkNoteDetailed.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
const props = defineProps<{
block: Misskey.entities.PageBlock,
block: Extract<Misskey.entities.PageBlock, { type: 'note' }>,
page: Misskey.entities.Page,
}>();

View File

@ -29,7 +29,7 @@ import * as Misskey from 'misskey-js';
const XBlock = defineAsyncComponent(() => import('./page.block.vue'));
defineProps<{
block: Misskey.entities.PageBlock,
block: Extract<Misskey.entities.PageBlock, { type: 'section' }>,
h: number,
page: Misskey.entities.Page,
}>();

View File

@ -22,7 +22,7 @@ import { isEnabledUrlPreview } from '@/utility/url-preview.js';
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));
const props = defineProps<{
block: Misskey.entities.PageBlock,
block: Extract<Misskey.entities.PageBlock, { type: 'text' }>,
page: Misskey.entities.Page,
}>();

View File

@ -51,3 +51,9 @@ export async function fetchInstance(force = false): Promise<Misskey.entities.Met
return instance;
}
export type ClientOptions = {
entrancePageStyle: 'classic' | 'simple';
showTimelineForVisitor: boolean;
showActivitiesForVisitor: boolean;
};

View File

@ -96,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</FormSection>
<FormSuspense v-slot="{ result: stats }" :p="initStats">
<MkSuspense v-slot="{ result: stats }" :p="initStats">
<FormSection>
<template #label>{{ i18n.ts.statistics }}</template>
<FormSplit>
@ -110,7 +110,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkKeyValue>
</FormSplit>
</FormSection>
</FormSuspense>
</MkSuspense>
<FormSection>
<template #label>Well-known resources</template>
@ -134,7 +134,6 @@ import { misskeyApi } from '@/utility/misskey-api.js';
import FormLink from '@/components/form/link.vue';
import FormSection from '@/components/form/section.vue';
import FormSplit from '@/components/form/split.vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkLink from '@/components/MkLink.vue';

View File

@ -0,0 +1,182 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
<div v-if="file" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m">
<a class="thumbnail" :href="file.url" target="_blank">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
</a>
<div>
<MkKeyValue :copy="file.type" oneline style="margin: 1em 0;">
<template #key>MIME Type</template>
<template #value><span class="_monospace">{{ file.type }}</span></template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>Size</template>
<template #value><span class="_monospace">{{ bytes(file.size) }}</span></template>
</MkKeyValue>
<MkKeyValue :copy="file.id" oneline style="margin: 1em 0;">
<template #key>ID</template>
<template #value><span class="_monospace">{{ file.id }}</span></template>
</MkKeyValue>
<MkKeyValue :copy="file.md5" oneline style="margin: 1em 0;">
<template #key>MD5</template>
<template #value><span class="_monospace">{{ file.md5 }}</span></template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><span class="_monospace"><MkTime :time="file.createdAt" mode="detail" style="display: block;"/></span></template>
</MkKeyValue>
</div>
<MkA v-if="file.user" class="user" :to="`/admin/user/${file.user.id}`">
<MkUserCardMini :user="file.user"/>
</MkA>
<div>
<MkSwitch :modelValue="isSensitive" @update:modelValue="toggleSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
</div>
<div>
<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
<div v-else-if="tab === 'usage' && info" class="_gaps_m">
<MkTabs
v-model:tab="usageTab"
:tabs="[{
key: 'note',
title: 'Note',
}, {
key: 'chat',
title: 'Chat',
}]"
/>
<XNotes v-if="usageTab === 'note'" :fileId="props.file.id"/>
<XChat v-else-if="usageTab === 'chat'" :fileId="props.file.id"/>
</div>
<div v-else-if="tab === 'ip' && info" class="_gaps_m">
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
<MkKeyValue v-if="info.requestIp" class="_monospace" :copy="info.requestIp" oneline>
<template #key>IP</template>
<template #value>{{ info.requestIp }}</template>
</MkKeyValue>
<FormSection v-if="info.requestHeaders">
<template #label>Headers</template>
<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_monospace">
<template #key>{{ k }}</template>
<template #value>{{ v }}</template>
</MkKeyValue>
</FormSection>
</div>
<div v-else-if="tab === 'raw'" class="_gaps_m">
<MkObjectView v-if="info" tall :value="info">
</MkObjectView>
</div>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkObjectView from '@/components/MkObjectView.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import FormSection from '@/components/form/section.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue';
import bytes from '@/filters/bytes.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { iAmAdmin, iAmModerator } from '@/i.js';
import MkTabs from '@/components/MkTabs.vue';
const props = defineProps<{
file: Misskey.entities.DriveFile,
info: Misskey.entities.AdminDriveShowFileResponse,
}>();
const tab = ref('overview');
const isSensitive = ref(props.file.isSensitive);
const usageTab = ref<'note' | 'chat'>('note');
const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
const XChat = defineAsyncComponent(() => import('./admin-file.chat.vue'));
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: props.file.name }),
});
if (canceled) return;
os.apiWithDialog('drive/files/delete', {
fileId: props.file.id,
});
}
async function toggleSensitive() {
const { canceled } = await os.confirm({
type: 'warning',
text: isSensitive.value ? i18n.ts.unmarkAsSensitiveConfirm : i18n.ts.markAsSensitiveConfirm,
});
if (canceled) return;
isSensitive.value = !isSensitive.value;
os.apiWithDialog('drive/files/update', {
fileId: props.file.id,
isSensitive: !props.file.isSensitive,
});
}
const headerActions = computed(() => [{
text: i18n.ts.openInNewTab,
icon: 'ti ti-external-link',
handler: () => {
window.open(props.file.url, '_blank', 'noopener');
},
}]);
const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
icon: 'ti ti-info-circle',
}, iAmModerator ? {
key: 'usage',
title: i18n.ts._fileViewer.usage,
icon: 'ti ti-plus',
} : null, iAmModerator ? {
key: 'ip',
title: 'IP',
icon: 'ti ti-password',
} : null, {
key: 'raw',
title: 'Raw data',
icon: 'ti ti-code',
}].filter(x => x != null));
</script>
<style lang="scss" scoped>
.cxqhhsmd {
> .thumbnail {
display: block;
> .thumbnail {
height: 300px;
max-width: 100%;
}
}
> .user {
&:hover {
text-decoration: none;
}
}
}
</style>

View File

@ -4,197 +4,37 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader v-model:tab="tab" :actions="headerActions" :tabs="headerTabs">
<div v-if="file" class="_spacer" style="--MI_SPACER-w: 600px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<div v-if="tab === 'overview'" class="cxqhhsmd _gaps_m">
<a class="thumbnail" :href="file.url" target="_blank">
<MkDriveFileThumbnail class="thumbnail" :file="file" fit="contain"/>
</a>
<div>
<MkKeyValue :copy="file.type" oneline style="margin: 1em 0;">
<template #key>MIME Type</template>
<template #value><span class="_monospace">{{ file.type }}</span></template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>Size</template>
<template #value><span class="_monospace">{{ bytes(file.size) }}</span></template>
</MkKeyValue>
<MkKeyValue :copy="file.id" oneline style="margin: 1em 0;">
<template #key>ID</template>
<template #value><span class="_monospace">{{ file.id }}</span></template>
</MkKeyValue>
<MkKeyValue :copy="file.md5" oneline style="margin: 1em 0;">
<template #key>MD5</template>
<template #value><span class="_monospace">{{ file.md5 }}</span></template>
</MkKeyValue>
<MkKeyValue oneline style="margin: 1em 0;">
<template #key>{{ i18n.ts.createdAt }}</template>
<template #value><span class="_monospace"><MkTime :time="file.createdAt" mode="detail" style="display: block;"/></span></template>
</MkKeyValue>
</div>
<MkA v-if="file.user" class="user" :to="`/admin/user/${file.user.id}`">
<MkUserCardMini :user="file.user"/>
</MkA>
<div>
<MkSwitch :modelValue="isSensitive" @update:modelValue="toggleSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
</div>
<div>
<MkButton danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
<div v-else-if="tab === 'usage' && info" class="_gaps_m">
<MkTabs
v-model:tab="usageTab"
:tabs="[{
key: 'note',
title: 'Note',
}, {
key: 'chat',
title: 'Chat',
}]"
/>
<XNotes v-if="usageTab === 'note'" :fileId="fileId"/>
<XChat v-else-if="usageTab === 'chat'" :fileId="fileId"/>
</div>
<div v-else-if="tab === 'ip' && info" class="_gaps_m">
<MkInfo v-if="!iAmAdmin" warn>{{ i18n.ts.requireAdminForView }}</MkInfo>
<MkKeyValue v-if="info.requestIp" class="_monospace" :copy="info.requestIp" oneline>
<template #key>IP</template>
<template #value>{{ info.requestIp }}</template>
</MkKeyValue>
<FormSection v-if="info.requestHeaders">
<template #label>Headers</template>
<MkKeyValue v-for="(v, k) in info.requestHeaders" :key="k" class="_monospace">
<template #key>{{ k }}</template>
<template #value>{{ v }}</template>
</MkKeyValue>
</FormSection>
</div>
<div v-else-if="tab === 'raw'" class="_gaps_m">
<MkObjectView v-if="info" tall :value="info">
</MkObjectView>
</div>
</div>
</PageWithHeader>
<MkSuspense v-slot="{ result }" :p="_fetch_" @resolved="(result) => file = result.file">
<XRoot v-if="result.file != null && result.info != null" :file="result.file" :info="result.info"/>
</MkSuspense>
</template>
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue';
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkObjectView from '@/components/MkObjectView.vue';
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import FormSection from '@/components/form/section.vue';
import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkInfo from '@/components/MkInfo.vue';
import bytes from '@/filters/bytes.js';
import * as os from '@/os.js';
import XRoot from './admin-file.root.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
import { iAmAdmin, iAmModerator } from '@/i.js';
import MkTabs from '@/components/MkTabs.vue';
const tab = ref('overview');
const file = ref<Misskey.entities.DriveFile | null>(null);
const info = ref<Misskey.entities.AdminDriveShowFileResponse | null>(null);
const isSensitive = ref<boolean>(false);
const usageTab = ref<'note' | 'chat'>('note');
const XNotes = defineAsyncComponent(() => import('./drive.file.notes.vue'));
const XChat = defineAsyncComponent(() => import('./admin-file.chat.vue'));
const props = defineProps<{
fileId: string,
}>();
async function _fetch_() {
file.value = await misskeyApi('drive/files/show', { fileId: props.fileId });
info.value = await misskeyApi('admin/drive/show-file', { fileId: props.fileId });
isSensitive.value = file.value.isSensitive;
function _fetch_() {
return Promise.all([
misskeyApi('drive/files/show', { fileId: props.fileId }),
misskeyApi('admin/drive/show-file', { fileId: props.fileId }),
]).then((result) => ({
file: result[0],
info: result[1],
}));
}
_fetch_();
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: file.value.name }),
});
if (canceled) return;
os.apiWithDialog('drive/files/delete', {
fileId: file.value.id,
});
}
async function toggleSensitive() {
if (!file.value) return;
const { canceled } = await os.confirm({
type: 'warning',
text: isSensitive.value ? i18n.ts.unmarkAsSensitiveConfirm : i18n.ts.markAsSensitiveConfirm,
});
if (canceled) return;
isSensitive.value = !isSensitive.value;
os.apiWithDialog('drive/files/update', {
fileId: file.value.id,
isSensitive: !file.value.isSensitive,
});
}
const headerActions = computed(() => [{
text: i18n.ts.openInNewTab,
icon: 'ti ti-external-link',
handler: () => {
window.open(file.value.url, '_blank', 'noopener');
},
}]);
const headerTabs = computed(() => [{
key: 'overview',
title: i18n.ts.overview,
icon: 'ti ti-info-circle',
}, iAmModerator ? {
key: 'usage',
title: i18n.ts._fileViewer.usage,
icon: 'ti ti-plus',
} : null, iAmModerator ? {
key: 'ip',
title: 'IP',
icon: 'ti ti-password',
} : null, {
key: 'raw',
title: 'Raw data',
icon: 'ti ti-code',
}].filter(x => x != null));
const file = ref<Misskey.entities.DriveFile | null>(null);
definePage(() => ({
title: file.value ? `${i18n.ts.file}: ${file.value.name}` : i18n.ts.file,
icon: 'ti ti-file',
}));
</script>
<style lang="scss" scoped>
.cxqhhsmd {
> .thumbnail {
display: block;
> .thumbnail {
height: 300px;
max-width: 100%;
}
}
> .user {
&:hover {
text-decoration: none;
}
}
}
</style>

View File

@ -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<ClientOptions['entrancePageStyle']>(meta.clientOptions.entrancePageStyle ?? 'classic');
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const showTimelineForVisitor = ref<ClientOptions['showTimelineForVisitor']>(meta.clientOptions.showTimelineForVisitor ?? true);
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const showActivitiesForVisitor = ref<ClientOptions['showActivitiesForVisitor']>(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,

View File

@ -6,19 +6,18 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
<MkSuspense v-slot="{ result: database }" :p="databasePromiseFactory">
<MkKeyValue v-for="table in database" :key="table[0]" oneline style="margin: 1em 0;">
<template #key>{{ table[0] }}</template>
<template #value>{{ bytes(table[1].size) }} ({{ number(table[1].count) }} recs)</template>
</MkKeyValue>
</FormSuspense>
</MkSuspense>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { computed } from 'vue';
import FormSuspense from '@/components/form/suspense.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import bytes from '@/filters/bytes.js';

View File

@ -107,7 +107,7 @@ const smtpPass = ref(meta.smtpPass);
async function testEmail() {
const { canceled, result: destination } = await os.inputText({
title: i18n.ts.destination,
title: 'To',
type: 'email',
default: instance.maintainerEmail ?? '',
placeholder: 'test@example.com',

View File

@ -81,10 +81,10 @@ function onStats(stats: Misskey.entities.QueueStats) {
delayed.value = stats[props.domain].delayed;
waiting.value = stats[props.domain].waiting;
chartProcess.value.pushData(stats[props.domain].activeSincePrevTick);
chartActive.value.pushData(stats[props.domain].active);
chartDelayed.value.pushData(stats[props.domain].delayed);
chartWaiting.value.pushData(stats[props.domain].waiting);
if (chartProcess.value != null) chartProcess.value.pushData(stats[props.domain].activeSincePrevTick);
if (chartActive.value != null) chartActive.value.pushData(stats[props.domain].active);
if (chartDelayed.value != null) chartDelayed.value.pushData(stats[props.domain].delayed);
if (chartWaiting.value != null) chartWaiting.value.pushData(stats[props.domain].waiting);
}
function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
@ -100,10 +100,10 @@ function onStatsLog(statsLog: Misskey.entities.QueueStatsLog) {
dataWaiting.push(stats[props.domain].waiting);
}
chartProcess.value.setData(dataProcess);
chartActive.value.setData(dataActive);
chartDelayed.value.setData(dataDelayed);
chartWaiting.value.setData(dataWaiting);
if (chartProcess.value != null) chartProcess.value.setData(dataProcess);
if (chartActive.value != null) chartActive.value.setData(dataActive);
if (chartDelayed.value != null) chartDelayed.value.setData(dataDelayed);
if (chartWaiting.value != null) chartWaiting.value.setData(dataWaiting);
}
onMounted(() => {

View File

@ -109,7 +109,6 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInput from '@/components/MkInput.vue';
import FormSuspense from '@/components/form/suspense.vue';
import FormSplit from '@/components/form/split.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';

View File

@ -37,6 +37,8 @@ async function renderChart() {
chartInstance.destroy();
}
if (chartEl.value == null) return;
const getDate = (ago: number) => {
const y = now.getFullYear();
const m = now.getMonth();
@ -105,7 +107,6 @@ async function renderChart() {
type: 'time',
offset: true,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@ -149,7 +150,9 @@ async function renderChart() {
},
external: externalTooltipHandler,
},
gradient,
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor)],

View File

@ -42,6 +42,9 @@ const { handler: externalTooltipHandler } = useChartTooltip();
const { handler: externalTooltipHandler2 } = useChartTooltip();
onMounted(async () => {
if (chartEl.value == null) return;
if (chartEl2.value == null) return;
const now = isChromatic() ? new Date('2024-08-31T10:00:00Z') : new Date();
const getDate = (ago: number) => {
@ -122,7 +125,6 @@ onMounted(async () => {
stacked: true,
offset: false,
time: {
stepSize: 1,
unit: 'day',
},
grid: {
@ -144,7 +146,7 @@ onMounted(async () => {
ticks: {
display: true,
//mirror: true,
callback: (value, index, values) => value < 0 ? -value : value,
callback: (value, index, values) => (value as number) < 0 ? -value : value,
},
},
},
@ -173,7 +175,9 @@ onMounted(async () => {
label: context => `${context.dataset.label}: ${Math.abs(context.parsed.y)}`,
},
},
gradient,
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor)],
@ -213,7 +217,6 @@ onMounted(async () => {
type: 'time',
offset: false,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@ -260,7 +263,9 @@ onMounted(async () => {
},
external: externalTooltipHandler2,
},
gradient,
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor)],

View File

@ -828,8 +828,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { watch, ref, computed } from 'vue';
import { throttle } from 'throttle-debounce';
import { ROLE_POLICIES } from '@@/js/const.js';
import * as Misskey from 'misskey-js';
import RolesEditorFormula from './RolesEditorFormula.vue';
import type { GetMkSelectValueTypesFromDef, MkSelectItem } from '@/components/MkSelect.vue';
import MkInput from '@/components/MkInput.vue';
import MkColorInput from '@/components/MkColorInput.vue';
import MkSelect from '@/components/MkSelect.vue';
@ -854,7 +855,7 @@ const props = defineProps<{
const role = ref(deepClone(props.modelValue));
// fill missing policy
for (const ROLE_POLICY of ROLE_POLICIES) {
for (const ROLE_POLICY of Misskey.rolePolicies) {
if (role.value.policies[ROLE_POLICY] == null) {
role.value.policies[ROLE_POLICY] = {
useDefault: true,

View File

@ -331,7 +331,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, reactive, ref } from 'vue';
import { ROLE_POLICIES } from '@@/js/const.js';
import * as Misskey from 'misskey-js';
import MkInput from '@/components/MkInput.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkSwitch from '@/components/MkSwitch.vue';
@ -353,8 +353,8 @@ const baseRoleQ = ref('');
const roles = await misskeyApi('admin/roles/list');
const policies = reactive<Record<typeof ROLE_POLICIES[number], any>>({});
for (const ROLE_POLICY of ROLE_POLICIES) {
const policies = reactive<Record<typeof Misskey.rolePolicies[number], any>>({});
for (const ROLE_POLICY of Misskey.rolePolicies) {
policies[ROLE_POLICY] = instance.policies[ROLE_POLICY];
}

View File

@ -194,7 +194,7 @@ const sensitiveMediaDetectionForm = useForm({
state.sensitiveMediaDetectionSensitivity === 2 ? 'medium' :
state.sensitiveMediaDetectionSensitivity === 3 ? 'high' :
state.sensitiveMediaDetectionSensitivity === 4 ? 'veryHigh' :
0,
null as never,
setSensitiveFlagAutomatically: state.setSensitiveFlagAutomatically,
enableSensitiveMediaDetectionForVideos: state.enableSensitiveMediaDetectionForVideos,
});

View File

@ -138,9 +138,10 @@ async function addPinnedNote() {
const { canceled, result: value } = await os.inputText({
title: i18n.ts.noteIdOrUrl,
});
if (canceled) return;
if (canceled || value == null) return;
const fromUrl = value.includes('/') ? value.split('/').pop() : null;
const note = await os.apiWithDialog('notes/show', {
noteId: value.includes('/') ? value.split('/').pop() : value,
noteId: fromUrl ?? value,
});
pinnedNotes.value = [{
id: note.id,

View File

@ -48,8 +48,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div><b>{{ i18n.tsx.lastNDays({ n: 7 }) }} {{ i18n.ts.ranking }}</b> ({{ gameMode.toUpperCase() }})</div>
<div v-if="ranking" class="_gaps_s">
<div v-for="r in ranking" :key="r.id" :class="$style.rankingRecord">
<MkAvatar :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
<MkUserName :user="r.user" :nowrap="true"/>
<MkAvatar v-if="r.user" :link="true" style="width: 24px; height: 24px; margin-right: 4px;" :user="r.user"/>
<MkUserName v-if="r.user" :user="r.user" :nowrap="true"/>
<b style="margin-left: auto;">{{ r.score.toLocaleString() }} {{ getScoreUnit(gameMode) }}</b>
</div>
</div>
@ -87,6 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import XGame from './drop-and-fusion.game.vue';
import { definePage } from '@/page.js';
import MkButton from '@/components/MkButton.vue';
@ -98,7 +99,7 @@ import { misskeyApiGet } from '@/utility/misskey-api.js';
const gameMode = ref<'normal' | 'square' | 'yen' | 'sweets' | 'space'>('normal');
const gameStarted = ref(false);
const mute = ref(false);
const ranking = ref(null);
const ranking = ref<Misskey.entities.BubbleGameRankingResponse | null>(null);
watch(gameMode, async () => {
ranking.value = await misskeyApiGet('bubble-game/ranking', { gameMode: gameMode.value });

View File

@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="name" pattern="[a-z0-9_]" autocapitalize="off">
<template #label>{{ i18n.ts.name }}</template>
</MkInput>
<MkInput v-model="category" :datalist="customEmojiCategories">
<MkInput v-model="category" :datalist="customEmojiCategories.filter(x => x != null)">
<template #label>{{ i18n.ts.category }}</template>
</MkInput>
<MkInput v-model="aliases" autocapitalize="off">

View File

@ -0,0 +1,145 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<MkInput v-model="title">
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
<MkTextarea v-model="description" :max="500">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
<div class="_gaps_s">
<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : '' }">
<div class="name">{{ file.name }}</div>
<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button>
</div>
<MkButton primary @click="chooseFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton>
</div>
<MkSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</MkSwitch>
<div class="_buttons">
<MkButton v-if="props.post != null" primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-else primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.publish }}</MkButton>
<MkButton v-if="props.post != null" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</div>
</PageWithHeader>
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import { selectFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/utility/misskey-api.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
const router = useRouter();
const props = defineProps<{
post: Misskey.entities.GalleryPost | null;
}>();
const files = ref(props.post?.files ?? []);
const description = ref(props.post?.description ?? null);
const title = ref(props.post?.title ?? '');
const isSensitive = ref(props.post?.isSensitive ?? false);
function chooseFile(evt) {
selectFile({
anchorElement: evt.currentTarget ?? evt.target,
multiple: true,
}).then(selected => {
files.value = files.value.concat(selected);
});
}
function remove(file) {
files.value = files.value.filter(f => f.id !== file.id);
}
async function save() {
if (props.post != null) {
await os.apiWithDialog('gallery/posts/update', {
postId: props.post.id,
title: title.value,
description: description.value,
fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value,
});
router.push('/gallery/:postId', {
params: {
postId: props.post.id,
},
});
} else {
const created = await os.apiWithDialog('gallery/posts/create', {
title: title.value,
description: description.value,
fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value,
});
router.push('/gallery/:postId', {
params: {
postId: created.id,
},
});
}
}
async function del() {
if (props.post == null) return;
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
});
if (canceled) return;
await os.apiWithDialog('gallery/posts/delete', {
postId: props.post.id,
});
router.push('/gallery');
}
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
</script>
<style lang="scss" scoped>
.wqugxsfx {
height: 200px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
position: relative;
> .name {
position: absolute;
top: 8px;
left: 9px;
padding: 8px;
background: var(--MI_THEME-panel);
}
> .remove {
position: absolute;
top: 8px;
right: 9px;
padding: 8px;
background: var(--MI_THEME-panel);
}
}
</style>

View File

@ -4,162 +4,35 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<PageWithHeader :actions="headerActions" :tabs="headerTabs">
<div class="_spacer" style="--MI_SPACER-w: 800px; --MI_SPACER-min: 16px; --MI_SPACER-max: 32px;">
<FormSuspense :p="init" class="_gaps">
<MkInput v-model="title">
<template #label>{{ i18n.ts.title }}</template>
</MkInput>
<MkTextarea v-model="description" :max="500">
<template #label>{{ i18n.ts.description }}</template>
</MkTextarea>
<div class="_gaps_s">
<div v-for="file in files" :key="file.id" class="wqugxsfx" :style="{ backgroundImage: file ? `url(${ file.thumbnailUrl })` : '' }">
<div class="name">{{ file.name }}</div>
<button v-tooltip="i18n.ts.remove" class="remove _button" @click="remove(file)"><i class="ti ti-x"></i></button>
</div>
<MkButton primary @click="chooseFile"><i class="ti ti-plus"></i> {{ i18n.ts.attachFile }}</MkButton>
</div>
<MkSwitch v-model="isSensitive">{{ i18n.ts.markAsSensitive }}</MkSwitch>
<div class="_buttons">
<MkButton v-if="postId" primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.save }}</MkButton>
<MkButton v-else primary @click="save"><i class="ti ti-device-floppy"></i> {{ i18n.ts.publish }}</MkButton>
<MkButton v-if="postId" danger @click="del"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
</div>
</FormSuspense>
</div>
</PageWithHeader>
<MkSuspense v-slot="{ result }" :p="_fetch_">
<XRoot :post="result"/>
</MkSuspense>
</template>
<script lang="ts" setup>
import { computed, watch, ref } from 'vue';
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import FormSuspense from '@/components/form/suspense.vue';
import { selectFile } from '@/utility/drive.js';
import * as os from '@/os.js';
import XRoot from './edit.root.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import { definePage } from '@/page.js';
import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js';
const router = useRouter();
import { definePage } from '@/page.js';
const props = defineProps<{
postId?: string;
postId?: string,
}>();
const init = ref<(() => Promise<any>) | null>(null);
const files = ref<Misskey.entities.DriveFile[]>([]);
const description = ref<string | null>(null);
const title = ref<string | null>(null);
const isSensitive = ref(false);
function chooseFile(evt) {
selectFile({
anchorElement: evt.currentTarget ?? evt.target,
multiple: true,
}).then(selected => {
files.value = files.value.concat(selected);
});
}
function remove(file) {
files.value = files.value.filter(f => f.id !== file.id);
}
async function save() {
if (props.postId) {
await os.apiWithDialog('gallery/posts/update', {
postId: props.postId,
title: title.value,
description: description.value,
fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value,
});
router.push('/gallery/:postId', {
params: {
postId: props.postId,
},
});
function _fetch_() {
if (props.postId == null) {
return Promise.resolve(null);
} else {
const created = await os.apiWithDialog('gallery/posts/create', {
title: title.value,
description: description.value,
fileIds: files.value.map(file => file.id),
isSensitive: isSensitive.value,
});
router.push('/gallery/:postId', {
params: {
postId: created.id,
},
return misskeyApi('gallery/posts/show', {
postId: props.postId,
});
}
}
async function del() {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts.deleteConfirm,
});
if (canceled) return;
await os.apiWithDialog('gallery/posts/delete', {
postId: props.postId,
});
router.push('/gallery');
}
watch(() => props.postId, () => {
init.value = () => props.postId ? misskeyApi('gallery/posts/show', {
postId: props.postId,
}).then(post => {
files.value = post.files ?? [];
title.value = post.title;
description.value = post.description;
isSensitive.value = post.isSensitive;
}) : Promise.resolve(null);
}, { immediate: true });
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
definePage(() => ({
title: props.postId ? i18n.ts.edit : i18n.ts.postToGallery,
icon: 'ti ti-pencil',
}));
</script>
<style lang="scss" scoped>
.wqugxsfx {
height: 200px;
background-size: contain;
background-position: center;
background-repeat: no-repeat;
position: relative;
> .name {
position: absolute;
top: 8px;
left: 9px;
padding: 8px;
background: var(--MI_THEME-panel);
}
> .remove {
position: absolute;
top: 8px;
right: 9px;
padding: 8px;
background: var(--MI_THEME-panel);
}
}
</style>

View File

@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, markRaw, ref } from 'vue';
import { notificationTypes } from '@@/js/const.js';
import { notificationTypes } from 'misskey-js';
import MkStreamingNotificationsTimeline from '@/components/MkStreamingNotificationsTimeline.vue';
import MkNotesTimeline from '@/components/MkNotesTimeline.vue';
import * as os from '@/os.js';

View File

@ -24,8 +24,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { genId } from '@/utility/id.js';
import XContainer from '../page-editor.container.vue';
import { genId } from '@/utility/id.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { deepClone } from '@/utility/clone.js';
@ -35,11 +35,11 @@ import { getPageBlockList } from '@/pages/page-editor/common.js';
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
const props = defineProps<{
modelValue: Misskey.entities.PageBlock & { type: 'section'; },
modelValue: Extract<Misskey.entities.PageBlock, { type: 'section'; }>,
}>();
const emit = defineEmits<{
(ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
(ev: 'update:modelValue', value: Extract<Misskey.entities.PageBlock, { type: 'section'; }>): void;
(ev: 'remove'): void;
}>();
@ -59,7 +59,7 @@ async function rename() {
title: i18n.ts._pages.enterSectionTitle,
default: props.modelValue.title,
});
if (canceled) return;
if (canceled || title == null) return;
emit('update:modelValue', {
...props.modelValue,
title,

View File

@ -135,7 +135,7 @@ const emit = defineEmits<{
const dialog = useTemplateRef('dialog');
const page = ref(0);
const token = ref<string | number | null>(null);
const token = ref<string | null>(null);
const backupCodes = ref<string[]>();
function cancel() {
@ -145,7 +145,7 @@ function cancel() {
async function tokenDone() {
if (token.value == null) return;
const res = await os.apiWithDialog('i/2fa/done', {
token: typeof token.value === 'string' ? token.value : token.value.toString(),
token: token.value.toString(), // numbertoString
});
backupCodes.value = res.backupCodes;

View File

@ -65,7 +65,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { useTemplateRef, computed } from 'vue';
import { notificationTypes } from '@@/js/const.js';
import { notificationTypes } from 'misskey-js';
import XNotificationConfig from './notifications.notification-config.vue';
import type { NotificationConfig } from './notifications.notification-config.vue';
import FormLink from '@/components/form/link.vue';

View File

@ -110,7 +110,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPreferenceContainer>
</SearchMarker>
<!--
<SearchMarker :keywords="['auto', 'load', 'auto', 'more', 'scroll']">
<MkPreferenceContainer k="enableInfiniteScroll">
<MkSwitch v-model="enableInfiniteScroll">
@ -118,7 +117,6 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
</MkPreferenceContainer>
</SearchMarker>
-->
</div>
<SearchMarker :keywords="['emoji', 'style', 'native', 'system', 'fluent', 'twemoji']">

View File

@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onMounted, ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { genId } from '@/utility/id.js';
import XStatusbar from './statusbar.statusbar.vue';
import { genId } from '@/utility/id.js';
import MkFolder from '@/components/MkFolder.vue';
import MkButton from '@/components/MkButton.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
@ -39,6 +39,7 @@ onMounted(() => {
async function add() {
prefer.commit('statusbars', [...statusbars.value, {
id: genId(),
name: null,
type: null,
black: false,
size: 'medium',

View File

@ -117,7 +117,6 @@ async function renderChart() {
offset: true,
stacked: true,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@ -162,7 +161,9 @@ async function renderChart() {
},
external: externalTooltipHandler,
},
gradient,
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)],

View File

@ -116,7 +116,6 @@ async function renderChart() {
offset: true,
stacked: true,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@ -161,7 +160,9 @@ async function renderChart() {
},
external: externalTooltipHandler,
},
gradient,
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)],

View File

@ -117,7 +117,6 @@ async function renderChart() {
offset: true,
stacked: true,
time: {
stepSize: 1,
unit: 'day',
displayFormats: {
day: 'M/d',
@ -155,8 +154,6 @@ async function renderChart() {
display: true,
text: 'Unique/Natural PV',
padding: {
left: 0,
right: 0,
top: 0,
bottom: 12,
},
@ -172,7 +169,9 @@ async function renderChart() {
},
external: externalTooltipHandler,
},
gradient,
...({ // TS
gradient,
}),
},
},
plugins: [chartVLine(vLineColor), chartLegend(legendEl.value)],

View File

@ -209,7 +209,7 @@ const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue'));
const props = withDefaults(defineProps<{
user: Misskey.entities.UserDetailed;
/** Test only; MkNotesTimeline currently causes problems in vitest */
disableNotes: boolean;
disableNotes?: boolean;
}>(), {
disableNotes: false,
});

View File

@ -34,7 +34,7 @@ import { instance as meta } from '@/instance.js';
position: fixed;
top: 0;
right: 0;
width: 80vw; // 100%shape
width: 100vw;
height: 100vh;
}

View File

@ -205,13 +205,13 @@ type HandlerDef = {
handler: (note: Misskey.entities.Note) => void;
};
note_view_interruptor: {
handler: (note: Misskey.entities.Note) => unknown;
handler: (note: Misskey.entities.Note) => Misskey.entities.Note;
};
note_post_interruptor: {
handler: (note: FIXME) => unknown;
};
page_view_interruptor: {
handler: (page: Misskey.entities.Page) => unknown;
handler: (page: Misskey.entities.Page) => Misskey.entities.Page;
};
};

View File

@ -32,6 +32,15 @@ export type SoundStore = {
volume: number;
};
export type StatusbarStore = {
name: string | null;
id: string;
type: string | null;
size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
black: boolean;
props: Record<string, any>;
};
type OmitStrict<T, K extends keyof T> = T extends any ? Pick<T, Exclude<keyof T, K>> : never;
// NOTE: デフォルト値は他の設定の状態に依存してはならない(依存していた場合、ユーザーがその設定項目単体で「初期値にリセット」した場合不具合の原因になる)
@ -182,14 +191,7 @@ export const PREF_DEF = definePreferences({
],
},
statusbars: {
default: [] as {
name: string;
id: string;
type: string;
size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
black: boolean;
props: Record<string, any>;
}[],
default: [] as StatusbarStore[],
},
serverDisconnectedBehavior: {
default: 'quiet' as 'quiet' | 'reload' | 'dialog',

View File

@ -6,9 +6,11 @@
import type { Plugin } from 'chart.js';
import MkChartLegend from '@/components/MkChartLegend.vue';
export const chartLegend = (legend: InstanceType<typeof MkChartLegend>) => ({
export const chartLegend = (legend: InstanceType<typeof MkChartLegend> | null | undefined) => ({
id: 'htmlLegend',
afterUpdate(chart, args, options) {
if (legend == null) return;
// Reuse the built-in legendItems generator
const items = chart.options.plugins!.legend!.labels!.generateLabels!(chart);

View File

@ -4,6 +4,7 @@
*/
import { unisonReload } from '@/utility/unison-reload.js';
import { misskeyApiGet } from '@/utility/misskey-api.js';
import * as os from '@/os.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
@ -16,6 +17,9 @@ export async function clearCache() {
miLocalStorage.removeItem('theme');
miLocalStorage.removeItem('emojis');
miLocalStorage.removeItem('lastEmojisFetchedAt');
await misskeyApiGet('clear-browser-cache', {}).catch(() => {
// ignore
});
await fetchInstance(true);
await fetchCustomEmojis(true);
unisonReload();

View File

@ -7,25 +7,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkContainer :showHeader="widgetProps.showHeader" class="mkw-aiscriptApp">
<template #header>App</template>
<div :class="$style.root">
<MkAsUi v-if="root" :component="root" :components="components" size="small"/>
<div v-if="isSyntaxError">Syntax error :(</div>
<MkAsUi v-else-if="root" :component="root" :components="components" size="small"/>
</div>
</MkContainer>
</template>
<script lang="ts" setup>
import { onMounted, ref, watch } from 'vue';
import type { Ref } from 'vue';
import { Interpreter, Parser } from '@syuilo/aiscript';
import { useWidgetPropsManager } from './widget.js';
import type { Ref } from 'vue';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
import * as os from '@/os.js';
import { aiScriptReadline, createAiScriptEnv } from '@/aiscript/api.js';
import { $i } from '@/i.js';
import MkAsUi from '@/components/MkAsUi.vue';
import MkContainer from '@/components/MkContainer.vue';
import { registerAsUiLib } from '@/aiscript/ui.js';
import type { AsUiComponent, AsUiRoot } from '@/aiscript/ui.js';
const name = 'aiscriptApp';
@ -56,8 +57,11 @@ const parser = new Parser();
const root = ref<AsUiRoot>();
const components = ref<Ref<AsUiComponent>[]>([]);
const isSyntaxError = ref(false);
async function run() {
isSyntaxError.value = false;
const aiscript = new Interpreter({
...createAiScriptEnv({
storageKey: 'widget',
@ -80,10 +84,7 @@ async function run() {
try {
ast = parser.parse(widgetProps.script);
} catch (err) {
os.alert({
type: 'error',
text: 'Syntax error :(',
});
isSyntaxError.value = true;
return;
}
try {

View File

@ -52,7 +52,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name,
const parser = new Parser();
const run = async () => {
async function run() {
const aiscript = new Interpreter(createAiScriptEnv({
storageKey: 'widget',
token: $i?.token,
@ -84,7 +84,7 @@ const run = async () => {
text: err instanceof Error ? err.message : String(err),
});
}
};
}
defineExpose<WidgetComponentExpose>({
name,

View File

@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent } from 'vue';
import type { notificationTypes as notificationTypes_typeReferenceOnly } from '@@/js/const.js';
import { useWidgetPropsManager } from './widget.js';
import type { notificationTypes as notificationTypes_typeReferenceOnly } from 'misskey-js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import MkContainer from '@/components/MkContainer.vue';

View File

@ -31,7 +31,7 @@ import { ref, watch, computed } from 'vue';
import * as Misskey from 'misskey-js';
import { useWidgetPropsManager } from './widget.js';
import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
import MarqueeText from '@/components/MkMarqueeText.vue';
import MkMarqueeText from '@/components/MkMarqueeText.vue';
import type { FormWithDefault, GetFormResultType } from '@/utility/form.js';
import MkContainer from '@/components/MkContainer.vue';
import { shuffle } from '@/utility/shuffle.js';

View File

@ -1449,6 +1449,10 @@ export type Endpoints = Overwrite<Endpoints_2, {
}>;
res: AdminRolesCreateResponse;
};
'clear-browser-cache': {
req: EmptyRequest;
res: EmptyResponse;
};
}>;
// @public (undocumented)
@ -3210,7 +3214,7 @@ type Notification_2 = components['schemas']['Notification'];
type NotificationsCreateRequest = operations['notifications___create']['requestBody']['content']['application/json'];
// @public (undocumented)
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollVote", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned"];
export const notificationTypes: readonly ["note", "follow", "mention", "reply", "renote", "quote", "reaction", "pollEnded", "receiveFollowRequest", "followRequestAccepted", "groupInvited", "app", "roleAssigned", "chatRoomInvitationReceived", "achievementEarned", "exportCompleted", "test", "login", "createToken"];
// @public (undocumented)
export function nyaize(text: string): string;
@ -3424,6 +3428,9 @@ type RoleLite = components['schemas']['RoleLite'];
// @public (undocumented)
type RolePolicies = components['schemas']['RolePolicies'];
// @public (undocumented)
export const rolePolicies: readonly ["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"];
// @public (undocumented)
type RolesListResponse = operations['roles___list']['responses']['200']['content']['application/json'];

View File

@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
"version": "2025.8.0",
"version": "2025.9.0-alpha.1",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",

View File

@ -1,6 +1,12 @@
import { Endpoints as Gen } from './autogen/endpoint.js';
import { UserDetailed } from './autogen/models.js';
import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js';
import {
AdminRolesCreateRequest,
AdminRolesCreateResponse,
EmptyRequest,
EmptyResponse,
UsersShowRequest,
} from './autogen/entities.js';
import {
PartialRolePolicyOverride,
SigninFlowRequest,
@ -106,6 +112,10 @@ export type Endpoints = Overwrite<
'admin/roles/create': {
req: Overwrite<AdminRolesCreateRequest, { policies: PartialRolePolicyOverride }>;
res: AdminRolesCreateResponse;
}
},
'clear-browser-cache': {
req: EmptyRequest;
res: EmptyResponse;
},
}
>;

View File

@ -17,7 +17,27 @@ import type {
ChatRoom,
} from './autogen/models.js';
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'groupInvited', 'app', 'roleAssigned', 'chatRoomInvitationReceived', 'achievementEarned'] as const;
export const notificationTypes = [
'note',
'follow',
'mention',
'reply',
'renote',
'quote',
'reaction',
'pollEnded',
'receiveFollowRequest',
'followRequestAccepted',
'groupInvited',
'app',
'roleAssigned',
'chatRoomInvitationReceived',
'achievementEarned',
'exportCompleted',
'test',
'login',
'createToken',
] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;
@ -170,6 +190,46 @@ export const moderationLogTypes = [
'updateProxyAccountDescription',
] as const;
export const rolePolicies = [
'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 queueTypes = [
'system',
'endedPollNotification',

View File

@ -13,6 +13,7 @@ export const mutedNoteReasons = consts.mutedNoteReasons;
export const followingVisibilities = consts.followingVisibilities;
export const followersVisibilities = consts.followersVisibilities;
export const moderationLogTypes = consts.moderationLogTypes;
export const rolePolicies = consts.rolePolicies;
export const queueTypes = consts.queueTypes;
export const reversiUpdateKeys = consts.reversiUpdateKeys;

View File

@ -2276,6 +2276,7 @@ packages:
'@github/webauthn-json@2.1.1':
resolution: {integrity: sha512-XrftRn4z75SnaJOmZQbt7Mk+IIjqVHw+glDGOxuHwXkZBZh/MBoRS7MHjSZMDaLhT4RjN2VqiEU7EOYleuJWSQ==}
deprecated: 'Deprecated: Modern browsers support built-in WebAuthn JSON methods. Please use native browser methods instead. For more information, visit https://github.com/github/webauthn-json'
hasBin: true
'@hapi/boom@10.0.1':
@ -3693,6 +3694,7 @@ packages:
'@simplewebauthn/types@12.0.0':
resolution: {integrity: sha512-q6y8MkoV8V8jB4zzp18Uyj2I7oFp2/ONL8c3j8uT06AOWu3cIChc1au71QYHrP2b+xDapkGTiv+9lX7xkTlAsA==}
deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.
'@sinclair/typebox@0.27.8':
resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==}