Compare commits

...

8 Commits

Author SHA1 Message Date
syuilo 23542530e1 feat(frontend): モバイルデバイスで折りたたまれたUIの展開表示に全画面ページを使用できるように 2025-05-22 14:57:35 +09:00
syuilo 65c2adee36 clean up 2025-05-22 13:06:49 +09:00
syuilo e4db9b64df refactor(frontend): better type defs 2025-05-22 13:05:28 +09:00
syuilo 74c857e23d refactor(frontend): src -> anchorElement 2025-05-22 12:24:52 +09:00
github-actions[bot] aa55663ef7 Bump version to 2025.5.1-alpha.4 2025-05-22 03:06:08 +00:00
かっこかり 000ed1f51f
fix(frontend): ジョブキューインスペクタの型エラー解消 (#16020)
* fix(frontend): ジョブキューインスペクタの型エラー解消

* fix

* fix

* fix

* fix
2025-05-22 12:06:07 +09:00
かっこかり c7318f5803
fix(backend): 連合モードが「なし」の場合はactivity jsonへのリンクタグを省略するように (#16074)
* fix(backend): 連合モードが「なし」の場合はactivity jsonへのリンクタグを省略するように

* Update Changelog

* flip condition
2025-05-22 12:02:01 +09:00
syuilo 8ad6ffc2b3 fix(frontend): UIのアニメーションをオフにするとページネーションが表示されない問題を修正
Fix #16078
2025-05-22 09:35:42 +09:00
44 changed files with 815 additions and 180 deletions

View File

@ -32,6 +32,7 @@
- チャットなど、一部の機能は引き続き設定に関わらずWebsocket接続が行われます
- Feat: 絵文字をミュート可能にする機能
- 絵文字(ユニコードの絵文字・カスタム絵文字)毎にミュートし、不可視化することができるようになりました
- Feat: モバイルデバイスで折りたたまれたUIの展開表示に全画面ページを使用できるように(実験的)
- Enhance: メモリ使用量を軽減しました
- Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加
- Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように
@ -50,6 +51,7 @@
- Fix: チャットルームが削除された場合・チャットルームから抜けた場合に、未読状態が残り続けることがあるのを修正
- Fix: ユーザ除外アンテナをインポートできない問題を修正
- Fix: アンテナのセンシティブなチャンネルのノートを含むかどうかの情報がエクスポートされない問題を修正
- Fix: 連合モードが「なし」の場合に、生成されるHTML内のactivity jsonへのリンクタグを省略するように
## 2025.5.0

View File

@ -1,6 +1,6 @@
{
"name": "misskey",
"version": "2025.5.1-alpha.3",
"version": "2025.5.1-alpha.4",
"codename": "nasubi",
"repository": {
"type": "git",

View File

@ -39,6 +39,7 @@ import type {
} from './QueueModule.js';
import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq';
import type { Packed } from '@/misc/json-schema.js';
export const QUEUE_TYPES = [
'system',
@ -774,13 +775,13 @@ export class QueueService {
}
@bindThis
private packJobData(job: Bull.Job) {
private packJobData(job: Bull.Job): Packed<'QueueJob'> {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
const stacktrace = job.stacktrace ? job.stacktrace.filter(Boolean) : [];
stacktrace.reverse();
return {
id: job.id,
id: job.id!,
name: job.name,
data: job.data,
opts: job.opts,

View File

@ -31,7 +31,11 @@ import { packedChannelSchema } from '@/models/json-schema/channel.js';
import { packedAntennaSchema } from '@/models/json-schema/antenna.js';
import { packedClipSchema } from '@/models/json-schema/clip.js';
import { packedFederationInstanceSchema } from '@/models/json-schema/federation-instance.js';
import { packedQueueCountSchema } from '@/models/json-schema/queue.js';
import {
packedQueueCountSchema,
packedQueueMetricsSchema,
packedQueueJobSchema,
} from '@/models/json-schema/queue.js';
import { packedGalleryPostSchema } from '@/models/json-schema/gallery-post.js';
import {
packedEmojiDetailedAdminSchema,
@ -100,6 +104,8 @@ export const refs = {
PageBlock: packedPageBlockSchema,
Channel: packedChannelSchema,
QueueCount: packedQueueCountSchema,
QueueMetrics: packedQueueMetricsSchema,
QueueJob: packedQueueJobSchema,
Antenna: packedAntennaSchema,
Clip: packedClipSchema,
FederationInstance: packedFederationInstanceSchema,

View File

@ -28,3 +28,110 @@ export const packedQueueCountSchema = {
},
},
} as const;
// Bull.Metrics
export const packedQueueMetricsSchema = {
type: 'object',
properties: {
meta: {
type: 'object',
optional: false, nullable: false,
properties: {
count: {
type: 'number',
optional: false, nullable: false,
},
prevTS: {
type: 'number',
optional: false, nullable: false,
},
prevCount: {
type: 'number',
optional: false, nullable: false,
},
},
},
data: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'number',
optional: false, nullable: false,
},
},
count: {
type: 'number',
optional: false, nullable: false,
},
},
} as const;
export const packedQueueJobSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false, nullable: false,
},
name: {
type: 'string',
optional: false, nullable: false,
},
data: {
type: 'object',
optional: false, nullable: false,
},
opts: {
type: 'object',
optional: false, nullable: false,
},
timestamp: {
type: 'number',
optional: false, nullable: false,
},
processedOn: {
type: 'number',
optional: true, nullable: false,
},
processedBy: {
type: 'string',
optional: true, nullable: false,
},
finishedOn: {
type: 'number',
optional: true, nullable: false,
},
progress: {
type: 'object',
optional: false, nullable: false,
},
attempts: {
type: 'number',
optional: false, nullable: false,
},
delay: {
type: 'number',
optional: false, nullable: false,
},
failedReason: {
type: 'string',
optional: false, nullable: false,
},
stacktrace: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'string',
optional: false, nullable: false,
},
},
returnValue: {
type: 'object',
optional: false, nullable: false,
},
isFailed: {
type: 'boolean',
optional: false, nullable: false,
},
},
} as const;

View File

@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@ -14,6 +13,15 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
type: 'array',
optional: false, nullable: false,
items: {
optional: false, nullable: false,
ref: 'QueueJob',
},
},
} as const;
export const paramDef = {

View File

@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@ -14,6 +13,118 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
type: 'object',
optional: false, nullable: false,
properties: {
name: {
type: 'string',
optional: false, nullable: false,
enum: QUEUE_TYPES,
},
qualifiedName: {
type: 'string',
optional: false, nullable: false,
},
counts: {
type: 'object',
optional: false, nullable: false,
additionalProperties: {
type: 'number',
},
},
isPaused: {
type: 'boolean',
optional: false, nullable: false,
},
metrics: {
type: 'object',
optional: false, nullable: false,
properties: {
completed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
failed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
},
},
db: {
type: 'object',
optional: false, nullable: false,
properties: {
version: {
type: 'string',
optional: false, nullable: false,
},
mode: {
type: 'string',
optional: false, nullable: false,
enum: ['cluster', 'standalone', 'sentinel'],
},
runId: {
type: 'string',
optional: false, nullable: false,
},
processId: {
type: 'string',
optional: false, nullable: false,
},
port: {
type: 'number',
optional: false, nullable: false,
},
os: {
type: 'string',
optional: false, nullable: false,
},
uptime: {
type: 'number',
optional: false, nullable: false,
},
memory: {
type: 'object',
optional: false, nullable: false,
properties: {
total: {
type: 'number',
optional: false, nullable: false,
},
used: {
type: 'number',
optional: false, nullable: false,
},
fragmentationRatio: {
type: 'number',
optional: false, nullable: false,
},
peak: {
type: 'number',
optional: false, nullable: false,
},
},
},
clients: {
type: 'object',
optional: false, nullable: false,
properties: {
blocked: {
type: 'number',
optional: false, nullable: false,
},
connected: {
type: 'number',
optional: false, nullable: false,
},
},
},
},
}
},
},
} as const;
export const paramDef = {

View File

@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@ -14,6 +13,47 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
type: 'array',
optional: false, nullable: false,
items: {
type: 'object',
optional: false, nullable: false,
properties: {
name: {
type: 'string',
optional: false, nullable: false,
enum: QUEUE_TYPES,
},
counts: {
type: 'object',
optional: false, nullable: false,
additionalProperties: {
type: 'number',
},
},
isPaused: {
type: 'boolean',
optional: false, nullable: false,
},
metrics: {
type: 'object',
optional: false, nullable: false,
properties: {
completed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
failed: {
optional: false, nullable: false,
ref: 'QueueMetrics',
},
},
},
},
},
},
} as const;
export const paramDef = {

View File

@ -5,7 +5,6 @@
import { Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { QUEUE_TYPES, QueueService } from '@/core/QueueService.js';
export const meta = {
@ -14,6 +13,11 @@ export const meta = {
requireCredential: true,
requireModerator: true,
kind: 'read:admin:queue',
res: {
optional: false, nullable: false,
ref: 'QueueJob',
},
} as const;
export const paramDef = {
@ -28,7 +32,6 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
private moderationLogService: ModerationLogService,
private queueService: QueueService,
) {
super(meta, paramDef, async (ps, me) => {

View File

@ -212,6 +212,7 @@ export class ClientServerService {
instanceUrl: this.config.url,
metaJson: htmlSafeJsonStringify(await this.metaEntityService.packDetailed(meta)),
now: Date.now(),
federationEnabled: this.meta.federation !== 'none',
};
}

View File

@ -55,7 +55,8 @@ block meta
if note.next
link(rel='next' href=`${config.url}/notes/${note.next}`)
if !user.host
link(rel='alternate' href=url type='application/activity+json')
if note.uri
link(rel='alternate' href=note.uri type='application/activity+json')
if federationEnabled
if !user.host
link(rel='alternate' href=url type='application/activity+json')
if note.uri
link(rel='alternate' href=note.uri type='application/activity+json')

View File

@ -32,12 +32,13 @@ block meta
meta(name='twitter:creator' content=`@${profile.twitter.screenName}`)
if !sub
if !user.host
link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
if user.uri
link(rel='alternate' href=user.uri type='application/activity+json')
if profile.url
link(rel='alternate' href=profile.url type='text/html')
if federationEnabled
if !user.host
link(rel='alternate' href=`${config.url}/users/${user.id}` type='application/activity+json')
if user.uri
link(rel='alternate' href=user.uri type='application/activity+json')
if profile.url
link(rel='alternate' href=profile.url type='text/html')
each m in me
link(rel='me' href=`${m}`)

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:hasInteractionWithOtherFocusTrappedEls="true"
:transparentBg="true"
:manualShowing="manualShowing"
:src="src"
:anchorElement="anchorElement"
@click="modal?.close()"
@esc="modal?.close()"
@opening="opening"
@ -44,7 +44,7 @@ import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
manualShowing?: boolean | null;
src?: HTMLElement;
anchorElement?: HTMLElement;
showPinned?: boolean;
pinnedEmojis?: string[],
asReactionPicker?: boolean;

View File

@ -19,13 +19,42 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div :class="$style.headerRight">
<span :class="$style.headerRightText"><slot name="suffix"></slot></span>
<i v-if="opened" class="ti ti-chevron-up icon"></i>
<i v-if="asPage" class="ti ti-chevron-right icon"></i>
<i v-else-if="opened" class="ti ti-chevron-up icon"></i>
<i v-else class="ti ti-chevron-down icon"></i>
</div>
</button>
</template>
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
<div v-if="asPage">
<Teleport v-if="opened" defer :to="`#v-${pageId}-header`">
<slot name="label"></slot>
</Teleport>
<Teleport v-if="opened" defer :to="`#v-${pageId}-body`">
<MkStickyContainer>
<template #header>
<div v-if="$slots.header" :class="$style.inBodyHeader">
<slot name="header"></slot>
</div>
</template>
<div v-if="withSpacer" class="_spacer" :style="{ '--MI_SPACER-min': props.spacerMin + 'px', '--MI_SPACER-max': props.spacerMax + 'px' }">
<slot></slot>
</div>
<div v-else>
<slot></slot>
</div>
<template #footer>
<div v-if="$slots.footer" :class="$style.inBodyFooter">
<slot name="footer"></slot>
</div>
</template>
</MkStickyContainer>
</Teleport>
</div>
<div v-else-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : undefined, overflow: maxHeight ? `auto` : undefined }" :aria-hidden="!opened">
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
@ -70,6 +99,9 @@ SPDX-License-Identifier: AGPL-3.0-only
import { nextTick, onMounted, ref, useTemplateRef } from 'vue';
import { prefer } from '@/preferences.js';
import { getBgColor } from '@/utility/get-bg-color.js';
import { pageFolderTeleportCount, popup } from '@/os.js';
import MkFolderPage from '@/components/MkFolderPage.vue';
import { deviceKind } from '@/utility/device-kind.js';
const props = withDefaults(defineProps<{
defaultOpen?: boolean;
@ -77,18 +109,21 @@ const props = withDefaults(defineProps<{
withSpacer?: boolean;
spacerMin?: number;
spacerMax?: number;
canPage?: boolean;
}>(), {
defaultOpen: false,
maxHeight: null,
withSpacer: true,
spacerMin: 14,
spacerMax: 22,
canPage: true,
});
const rootEl = useTemplateRef('rootEl');
const asPage = props.canPage && deviceKind === 'smartphone' && prefer.s['experimental.enableFolderPageView'];
const bgSame = ref(false);
const opened = ref(props.defaultOpen);
const openedAtLeastOnce = ref(props.defaultOpen);
const opened = ref(asPage ? false : props.defaultOpen);
const openedAtLeastOnce = ref(opened.value);
//#region interpolate-sizeTODO:
function enter(el: Element) {
@ -126,7 +161,22 @@ function afterLeave(el: Element) {
}
//#endregion
function toggle() {
let pageId = pageFolderTeleportCount.value;
pageFolderTeleportCount.value += 1000;
async function toggle() {
if (asPage && !opened.value) {
pageId++;
const { dispose } = await popup(MkFolderPage, {
pageId,
}, {
closed: () => {
opened.value = false;
dispose();
},
});
}
if (!opened.value) {
openedAtLeastOnce.value = true;
}

View File

@ -0,0 +1,157 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<Transition
name="x"
:enterActiveClass="prefer.s.animation ? $style.transition_x_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_x_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_x_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_x_leaveTo : ''"
:duration="300" appear @afterLeave="onClosed"
>
<div v-show="showing" :class="[$style.root]" :style="{ zIndex }">
<div :class="[$style.bg]" :style="{ zIndex }"></div>
<div :class="[$style.content]" :style="{ zIndex }">
<div :class="$style.header">
<button :class="$style.back" class="_button" @click="closePage"><i class="ti ti-chevron-left"></i></button>
<div :id="`v-${pageId}-header`" :class="$style.title"></div>
<div :class="$style.spacer"></div>
</div>
<div :id="`v-${pageId}-body`"></div>
</div>
</div>
</Transition>
</template>
<script lang="ts" setup>
import { onMounted, ref } from 'vue';
import { claimZIndex } from '@/os.js';
import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
pageId: number,
}>(), {
pageId: 0,
});
const emit = defineEmits<{
(_: 'closed'): void
}>();
const zIndex = claimZIndex('middle');
const showing = ref(true);
function closePage() {
showing.value = false;
}
function onClosed() {
emit('closed');
}
</script>
<style lang="scss" module>
.transition_x_enterActive {
> .bg {
transition: opacity 0.3s !important;
}
> .content {
transition: transform 0.3s cubic-bezier(0,0,.25,1) !important;
}
}
.transition_x_leaveActive {
> .bg {
transition: opacity 0.3s !important;
}
> .content {
transition: transform 0.3s cubic-bezier(0,0,.25,1) !important;
}
}
.transition_x_enterFrom,
.transition_x_leaveTo {
> .bg {
opacity: 0;
}
> .content {
pointer-events: none;
transform: translateX(100%);
}
}
.root {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: clip;
}
.bg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--MI_THEME-modalBg);
}
.content {
position: fixed;
bottom: 0;
left: 0;
right: 0;
width: 100%;
height: 100%;
margin: auto;
background: var(--MI_THEME-bg);
container-type: size;
overflow: auto;
overscroll-behavior: contain;
}
.header {
--height: 48px;
position: sticky;
top: 0;
left: 0;
height: var(--height);
z-index: 1;
display: flex;
align-items: center;
background: color(from var(--MI_THEME-panel) srgb r g b / 0.75);
-webkit-backdrop-filter: var(--MI-blur, blur(15px));
backdrop-filter: var(--MI-blur, blur(15px));
border-bottom: solid 0.5px var(--MI_THEME-divider);
}
.back {
display: flex;
align-items: center;
justify-content: center;
width: var(--height);
height: var(--height);
font-size: 16px;
color: var(--MI_THEME-accent);
}
.title {
margin: 0 auto;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
.spacer {
width: var(--height);
height: var(--height);
}
</style>

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
<MkModal ref="modal" v-slot="{ type, maxHeight }" :preferType="preferedModalType" :anchor="anchor" :transparentBg="true" :anchorElement="anchorElement" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
<div class="szkkfdyq _popup _shadow" :class="{ asDrawer: type === 'drawer' }" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : '' }">
<div class="main">
<template v-for="item in items" :key="item.text">
@ -34,7 +34,7 @@ import { deviceKind } from '@/utility/device-kind.js';
import { prefer } from '@/preferences.js';
const props = withDefaults(defineProps<{
src?: HTMLElement;
anchorElement?: HTMLElement;
anchor?: { x: string; y: string; };
}>(), {
anchor: () => ({ x: 'right', y: 'center' }),
@ -44,7 +44,7 @@ const emit = defineEmits<{
(ev: 'closed'): void;
}>();
const preferedModalType = (deviceKind === 'desktop' && props.src != null) ? 'popup' :
const preferedModalType = (deviceKind === 'desktop' && props.anchorElement != null) ? 'popup' :
deviceKind === 'smartphone' ? 'drawer' :
'dialog';

View File

@ -67,7 +67,7 @@ type ModalTypes = 'popup' | 'dialog' | 'drawer';
const props = withDefaults(defineProps<{
manualShowing?: boolean | null;
anchor?: { x: string; y: string; };
src?: HTMLElement | null;
anchorElement?: HTMLElement | null;
preferType?: ModalTypes | 'auto';
zPriority?: 'low' | 'middle' | 'high';
noOverlap?: boolean;
@ -76,7 +76,7 @@ const props = withDefaults(defineProps<{
returnFocusTo?: HTMLElement | null;
}>(), {
manualShowing: null,
src: null,
anchorElement: null,
anchor: () => ({ x: 'center', y: 'bottom' }),
preferType: 'auto',
zPriority: 'low',
@ -110,7 +110,7 @@ const type = computed<ModalTypes>(() => {
if ((prefer.s.menuStyle === 'drawer') || (prefer.s.menuStyle === 'auto' && isTouchUsing && deviceKind === 'smartphone')) {
return 'drawer';
} else {
return props.src != null ? 'popup' : 'dialog';
return props.anchorElement != null ? 'popup' : 'dialog';
}
} else {
return props.preferType!;
@ -149,7 +149,7 @@ function close(opts: { useSendAnimation?: boolean } = {}) {
}
// eslint-disable-next-line vue/no-mutating-props
if (props.src) props.src.style.pointerEvents = 'auto';
if (props.anchorElement) props.anchorElement.style.pointerEvents = 'auto';
showing.value = false;
emit('close');
}
@ -174,13 +174,13 @@ const MARGIN = 16;
const SCROLLBAR_THICKNESS = 16;
const align = () => {
if (props.src == null) return;
if (props.anchorElement == null) return;
if (type.value === 'drawer') return;
if (type.value === 'dialog') return;
if (content.value == null) return;
const srcRect = props.src.getBoundingClientRect();
const anchorRect = props.anchorElement.getBoundingClientRect();
const width = content.value!.offsetWidth;
const height = content.value!.offsetHeight;
@ -188,15 +188,15 @@ const align = () => {
let left;
let top;
const x = srcRect.left + (fixed.value ? 0 : window.scrollX);
const y = srcRect.top + (fixed.value ? 0 : window.scrollY);
const x = anchorRect.left + (fixed.value ? 0 : window.scrollX);
const y = anchorRect.top + (fixed.value ? 0 : window.scrollY);
if (props.anchor.x === 'center') {
left = x + (props.src.offsetWidth / 2) - (width / 2);
left = x + (props.anchorElement.offsetWidth / 2) - (width / 2);
} else if (props.anchor.x === 'left') {
// TODO
} else if (props.anchor.x === 'right') {
left = x + props.src.offsetWidth;
left = x + props.anchorElement.offsetWidth;
}
if (props.anchor.y === 'center') {
@ -204,7 +204,7 @@ const align = () => {
} else if (props.anchor.y === 'top') {
// TODO
} else if (props.anchor.y === 'bottom') {
top = y + props.src.offsetHeight;
top = y + props.anchorElement.offsetHeight;
}
if (fixed.value) {
@ -214,7 +214,7 @@ const align = () => {
}
const underSpace = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - top;
const upperSpace = (srcRect.top - MARGIN);
const upperSpace = (anchorRect.top - MARGIN);
//
if (top + height > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
@ -238,7 +238,7 @@ const align = () => {
}
const underSpace = ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN) - (top - window.scrollY);
const upperSpace = (srcRect.top - MARGIN);
const upperSpace = (anchorRect.top - MARGIN);
//
if (top + height - window.scrollY > ((window.innerHeight - SCROLLBAR_THICKNESS) - MARGIN)) {
@ -268,15 +268,15 @@ const align = () => {
let transformOriginX = 'center';
let transformOriginY = 'center';
if (top >= srcRect.top + props.src.offsetHeight + (fixed.value ? 0 : window.scrollY)) {
if (top >= anchorRect.top + props.anchorElement.offsetHeight + (fixed.value ? 0 : window.scrollY)) {
transformOriginY = 'top';
} else if ((top + height) <= srcRect.top + (fixed.value ? 0 : window.scrollY)) {
} else if ((top + height) <= anchorRect.top + (fixed.value ? 0 : window.scrollY)) {
transformOriginY = 'bottom';
}
if (left >= srcRect.left + props.src.offsetWidth + (fixed.value ? 0 : window.scrollX)) {
if (left >= anchorRect.left + props.anchorElement.offsetWidth + (fixed.value ? 0 : window.scrollX)) {
transformOriginX = 'left';
} else if ((left + width) <= srcRect.left + (fixed.value ? 0 : window.scrollX)) {
} else if ((left + width) <= anchorRect.left + (fixed.value ? 0 : window.scrollX)) {
transformOriginX = 'right';
}
@ -317,12 +317,12 @@ const alignObserver = new ResizeObserver((entries, observer) => {
});
onMounted(() => {
watch(() => props.src, async () => {
if (props.src) {
watch(() => props.anchorElement, async () => {
if (props.anchorElement) {
// eslint-disable-next-line vue/no-mutating-props
props.src.style.pointerEvents = 'none';
props.anchorElement.style.pointerEvents = 'none';
}
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.src) != null);
fixed.value = (type.value === 'drawer') || (getFixedContainer(props.anchorElement) != null);
await nextTick();
@ -339,7 +339,7 @@ onMounted(() => {
}
} else {
releaseFocusTrap?.();
focusParent(props.returnFocusTo ?? props.src, true, false);
focusParent(props.returnFocusTo ?? props.anchorElement, true, false);
}
}, { immediate: true });

View File

@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkPagination>
</template>
<script lang="ts" setup>
<script lang="ts" setup generic="T extends PagingCtx">
import { useTemplateRef } from 'vue';
import type { PagingCtx } from '@/composables/use-pagination.js';
import MkNote from '@/components/MkNote.vue';
@ -41,7 +41,7 @@ import { globalEvents, useGlobalEvent } from '@/events.js';
import { isSeparatorNeeded, getSeparatorInfo } from '@/utility/timeline-date-separate.js';
const props = withDefaults(defineProps<{
pagination: PagingCtx;
pagination: T;
noGap?: boolean;
disableAutoLoad?: boolean;
pullToRefresh?: boolean;

View File

@ -5,12 +5,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<component :is="prefer.s.enablePullToRefresh && pullToRefresh ? MkPullToRefresh : 'div'" :refresher="() => paginator.reload()">
<!-- :css="prefer.s.animation" にしたいけどバグる(おそらくvueのバグ) https://github.com/misskey-dev/misskey/issues/16078 -->
<Transition
:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
:css="prefer.s.animation"
mode="out-in"
>
<MkLoading v-if="paginator.fetching.value"/>
@ -40,16 +40,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</component>
</template>
<script lang="ts" setup>
<script lang="ts" setup generic="T extends PagingCtx">
import type { PagingCtx } from '@/composables/use-pagination.js';
import type { UnwrapRef } from 'vue';
import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js';
import { prefer } from '@/preferences.js';
import { usePagination } from '@/composables/use-pagination.js';
import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
type Paginator = ReturnType<typeof usePagination<T['endpoint']>>;
const props = withDefaults(defineProps<{
pagination: PagingCtx;
pagination: T;
disableAutoLoad?: boolean;
displayLimit?: number;
pullToRefresh?: boolean;
@ -58,7 +61,7 @@ const props = withDefaults(defineProps<{
pullToRefresh: true,
});
const paginator = usePagination({
const paginator: Paginator = usePagination({
ctx: props.pagination,
});
@ -70,6 +73,11 @@ function appearFetchMore() {
paginator.fetchOlder();
}
defineSlots<{
empty: () => void;
default: (props: { items: UnwrapRef<Paginator['items']> }) => void;
}>();
defineExpose({
paginator: paginator,
});

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :src="src" :transparentBg="true" :returnFocusTo="returnFocusTo" @click="click" @close="onModalClose" @closed="onModalClosed">
<MkModal ref="modal" v-slot="{ type, maxHeight }" :manualShowing="manualShowing" :zPriority="'high'" :anchorElement="anchorElement" :transparentBg="true" :returnFocusTo="returnFocusTo" @click="click" @close="onModalClose" @closed="onModalClosed">
<MkMenu :items="items" :align="align" :width="width" :max-height="maxHeight" :asDrawer="type === 'drawer'" :returnFocusTo="returnFocusTo" :class="{ [$style.drawer]: type === 'drawer' }" @close="onMenuClose" @hide="hide"/>
</MkModal>
</template>
@ -19,7 +19,7 @@ defineProps<{
items: MenuItem[];
align?: 'center' | string;
width?: number;
src?: HTMLElement | null;
anchorElement?: HTMLElement | null;
returnFocusTo?: HTMLElement | null;
}>();

View File

@ -470,7 +470,7 @@ function setVisibility() {
currentVisibility: visibility.value,
isSilenced: $i.isSilenced,
localOnly: localOnly.value,
src: visibilityButton.value,
anchorElement: visibilityButton.value,
...(props.reply ? { isReplyVisibilitySpecified: props.reply.visibility === 'specified' } : {}),
}, {
changeVisibility: v => {

View File

@ -21,15 +21,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
<script lang="ts" setup>
<script lang="ts">
export type TlEvent<E = any> = {
id: string;
timestamp: number;
data: E;
};
</script>
<script lang="ts" setup generic="T extends unknown">
import { computed } from 'vue';
const props = defineProps<{
events: {
id: string;
timestamp: number;
data: any;
}[];
events: TlEvent<T>[];
}>();
const events = computed(() => {
@ -44,12 +48,12 @@ function getDateText(dateInstance: Date) {
return `${year.toString()}/${month.toString()}/${date.toString()} ${hour.toString().padStart(2, '0')}:00:00`;
}
const items = computed<({
type TlItem<T> = ({
id: string;
type: 'event';
timestamp: number;
delta: number;
data: any;
delta: number
data: T;
} | {
id: string;
type: 'date';
@ -57,8 +61,10 @@ const items = computed<({
prevText: string;
next: Date | null;
nextText: string;
})[]>(() => {
const results = [];
});
const items = computed<TlItem<T>[]>(() => {
const results: TlItem<T>[] = [];
for (let i = 0; i < events.value.length; i++) {
const item = events.value[i];
@ -97,19 +103,12 @@ const items = computed<({
</script>
<style lang="scss" module>
.root {
}
.items {
display: grid;
grid-template-columns: max-content 18px 1fr;
gap: 0 8px;
}
.item {
}
.center {
position: relative;
@ -140,6 +139,7 @@ const items = computed<({
height: 100%;
background: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-bg) 75%);
}
.centerPoint {
position: absolute;
top: 0;

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :src="src" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
<MkModal ref="modal" v-slot="{ type }" :zPriority="'high'" :anchorElement="anchorElement" @click="modal?.close()" @closed="emit('closed')" @esc="modal?.close()">
<div class="_popup" :class="{ [$style.root]: true, [$style.asDrawer]: type === 'drawer' }">
<div :class="[$style.label, $style.item]">
{{ i18n.ts.visibility }}
@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{
currentVisibility: typeof Misskey.noteVisibilities[number];
isSilenced: boolean;
localOnly: boolean;
src?: HTMLElement;
anchorElement?: HTMLElement;
isReplyVisibilitySpecified?: boolean;
}>(), {
});

View File

@ -40,7 +40,7 @@ export type PagingCtx<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoint
canFetchDetection?: 'safe' | 'limit';
};
export function usePagination<Endpoint extends keyof Misskey.Endpoints, T = Misskey.Endpoints[Endpoint]['res'] extends (infer I)[] ? I : never>(props: {
export function usePagination<Endpoint extends keyof Misskey.Endpoints, T extends { id: string; } = (Misskey.Endpoints[Endpoint]['res'] extends (infer I)[] ? I extends { id: string } ? I : { id: string } : { id: string })>(props: {
ctx: PagingCtx<Endpoint>;
autoInit?: boolean;
autoReInit?: boolean;

View File

@ -609,10 +609,10 @@ export async function selectRole(params: ComponentProps<typeof MkRoleSelectDialo
});
}
export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog_TypeReferenceOnly>): Promise<string> {
export async function pickEmoji(anchorElement: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog_TypeReferenceOnly>): Promise<string> {
return new Promise(resolve => {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src,
anchorElement,
...opts,
}, {
done: emoji => {
@ -639,20 +639,20 @@ export async function cropImageFile(imageFile: File | Blob, options: {
});
}
export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | null, options?: {
export function popupMenu(items: MenuItem[], anchorElement?: HTMLElement | EventTarget | null, options?: {
align?: string;
width?: number;
onClosing?: () => void;
}): Promise<void> {
if (!(src instanceof HTMLElement)) {
src = null;
if (!(anchorElement instanceof HTMLElement)) {
anchorElement = null;
}
let returnFocusTo = getHTMLElementOrNull(src) ?? getHTMLElementOrNull(window.document.activeElement);
let returnFocusTo = getHTMLElementOrNull(anchorElement) ?? getHTMLElementOrNull(window.document.activeElement);
return new Promise(resolve => nextTick(() => {
const { dispose } = popup(MkPopupMenu, {
items,
src,
anchorElement,
width: options?.width,
align: options?.align,
returnFocusTo,
@ -790,3 +790,5 @@ export function launchUploader(
});
});
}
export const pageFolderTeleportCount = ref(0);

View File

@ -48,6 +48,8 @@ watch(() => props.dataSet, () => {
});
onMounted(() => {
if (chartEl.value == null) return;
const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
chartInstance = new Chart(chartEl.value, {

View File

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #suffix>
<MkTime :time="job.finishedOn ?? job.processedOn ?? job.timestamp" mode="relative"/>
<span v-if="job.progress != null && job.progress > 0" style="margin-left: 1em;">{{ Math.floor(job.progress * 100) }}%</span>
<span v-if="job.progress != null && typeof job.progress === 'number' && job.progress > 0" style="margin-left: 1em;">{{ Math.floor(job.progress * 100) }}%</span>
<span v-if="job.opts.attempts != null && job.opts.attempts > 0 && job.attempts > 1" style="margin-left: 1em; color: var(--MI_THEME-warn); font-variant-numeric: diagonal-fractions;">{{ job.attempts }}/{{ job.opts.attempts }}</span>
<span v-if="job.isFailed && job.finishedOn != null" style="margin-left: 1em; color: var(--MI_THEME-error)"><i class="ti ti-circle-x"></i></span>
<span v-else-if="job.isFailed" style="margin-left: 1em; color: var(--MI_THEME-warn)"><i class="ti ti-alert-triangle"></i></span>
@ -61,7 +61,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton rounded @click="copyRaw()"><i class="ti ti-copy"></i> Copy raw</MkButton>
<MkButton rounded @click="refresh()"><i class="ti ti-reload"></i> Refresh view</MkButton>
<MkButton rounded @click="promoteJob()"><i class="ti ti-player-track-next"></i> Promote</MkButton>
<MkButton rounded @click="moveJob"><i class="ti ti-arrow-right"></i> Move to</MkButton>
<!-- <MkButton rounded @click="moveJob"><i class="ti ti-arrow-right"></i> Move to</MkButton> -->
<MkButton danger rounded style="margin-left: auto;" @click="removeJob()"><i class="ti ti-trash"></i> Remove</MkButton>
</div>
</template>
@ -96,7 +96,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #key>Attempts</template>
<template #value>{{ job.attempts }} of {{ job.opts.attempts }}</template>
</MkKeyValue>
<MkKeyValue v-if="job.progress != null && job.progress > 0">
<MkKeyValue v-if="job.progress != null && typeof job.progress === 'number' && job.progress > 0">
<template #key>Progress</template>
<template #value>{{ Math.floor(job.progress * 100) }}%</template>
</MkKeyValue>
@ -150,7 +150,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton><i class="ti ti-device-floppy"></i> Update</MkButton>
</div>
<div v-else-if="tab === 'result'">
<MkCode :code="job.returnValue"/>
<MkCode :code="String(job.returnValue)"/>
</div>
<div v-else-if="tab === 'error'" class="_gaps_s">
<MkCode v-for="log in job.stacktrace" :code="log" lang="stacktrace"/>
@ -159,22 +159,20 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import JSON5 from 'json5';
import type { Ref } from 'vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import MkTabs from '@/components/MkTabs.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkCode from '@/components/MkCode.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import MkTl from '@/components/MkTl.vue';
import kmg from '@/filters/kmg.js';
import bytes from '@/filters/bytes.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
import type { TlEvent } from '@/components/MkTl.vue';
function msSMH(v: number | null) {
if (v == null) return 'N/A';
@ -189,25 +187,34 @@ function msSMH(v: number | null) {
}
const props = defineProps<{
job: any;
queueType: string;
job: Misskey.entities.QueueJob;
queueType: typeof Misskey.queueTypes[number];
}>();
const emit = defineEmits<{
(ev: 'needRefresh'): void,
(ev: 'needRefresh'): void;
}>();
const tab = ref('info');
const editData = ref(JSON5.stringify(props.job.data, null, '\t'));
const canEdit = true;
type TlType = TlEvent<{
type: 'created' | 'processed' | 'finished';
} | {
type: 'attempt';
attempt: number;
}>;
const timeline = computed(() => {
const events = [{
const events: TlType[] = [{
id: 'created',
timestamp: props.job.timestamp,
data: {
type: 'created',
},
}];
if (props.job.attempts > 1) {
for (let i = 1; i < props.job.attempts; i++) {
events.push({
@ -261,9 +268,10 @@ async function removeJob() {
os.apiWithDialog('admin/queue/remove-job', { queue: props.queueType, jobId: props.job.id });
}
function moveJob() {
// TODO
}
// TODO
// function moveJob() {
//
// }
function refresh() {
emit('needRefresh');

View File

@ -37,9 +37,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #footer>
<div class="_buttons">
<MkButton rounded @click="promoteAllJobs"><i class="ti ti-player-track-next"></i> Promote all jobs</MkButton>
<MkButton rounded @click="createJob"><i class="ti ti-plus"></i> Add job</MkButton>
<MkButton v-if="queueInfo.isPaused" rounded @click="resumeQueue"><i class="ti ti-player-play"></i> Resume queue</MkButton>
<MkButton v-else rounded danger @click="pauseQueue"><i class="ti ti-player-pause"></i> Pause queue</MkButton>
<!-- <MkButton rounded @click="createJob"><i class="ti ti-plus"></i> Add job</MkButton> -->
<!-- <MkButton v-if="queueInfo.isPaused" rounded @click="resumeQueue"><i class="ti ti-player-play"></i> Resume queue</MkButton> -->
<!-- <MkButton v-else rounded danger @click="pauseQueue"><i class="ti ti-player-pause"></i> Pause queue</MkButton> -->
<MkButton rounded danger @click="clearQueue"><i class="ti ti-trash"></i> Empty queue</MkButton>
</div>
</template>
@ -172,12 +172,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import JSON5 from 'json5';
import * as Misskey from 'misskey-js';
import { debounce } from 'throttle-debounce';
import { useInterval } from '@@/js/use-interval.js';
import XChart from './job-queue.chart.vue';
import XJob from './job-queue.job.vue';
import type { Ref } from 'vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { definePage } from '@/page.js';
@ -185,32 +184,18 @@ import MkButton from '@/components/MkButton.vue';
import { misskeyApi } from '@/utility/misskey-api.js';
import MkTabs from '@/components/MkTabs.vue';
import MkFolder from '@/components/MkFolder.vue';
import MkCode from '@/components/MkCode.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkTl from '@/components/MkTl.vue';
import kmg from '@/filters/kmg.js';
import MkInput from '@/components/MkInput.vue';
import bytes from '@/filters/bytes.js';
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
const QUEUE_TYPES = [
'system',
'endedPollNotification',
'deliver',
'inbox',
'db',
'relationship',
'objectStorage',
'userWebhookDeliver',
'systemWebhookDeliver',
] as const;
const tab: Ref<typeof QUEUE_TYPES[number] | '-'> = ref('-');
const jobState = ref('all');
const jobs = ref([]);
const tab = ref<typeof Misskey.queueTypes[number] | '-'>('-');
const jobState = ref<'all' | 'latest' | 'completed' | 'failed' | 'active' | 'delayed' | 'wait' | 'paused'>('all');
const jobs = ref<Misskey.entities.QueueJob[]>([]);
const jobsFetching = ref(true);
const queueInfos = ref([]);
const queueInfo = ref();
const queueInfos = ref<Misskey.entities.AdminQueueQueuesResponse>([]);
const queueInfo = ref<Misskey.entities.AdminQueueQueueStatsResponse | null>(null);
const searchQuery = ref('');
async function fetchQueues() {
@ -230,11 +215,11 @@ async function fetchJobs() {
queue: tab.value,
state: state === 'all' ? ['completed', 'failed', 'active', 'delayed', 'wait'] : state === 'latest' ? ['completed', 'failed'] : [state],
search: searchQuery.value.trim() === '' ? undefined : searchQuery.value,
}).then(res => {
}).then((res: Misskey.entities.AdminQueueJobsResponse) => {
if (state === 'all') {
res.sort((a, b) => (a.processedOn ?? a.timestamp) > (b.processedOn ?? b.timestamp) ? -1 : 1);
} else if (state === 'latest') {
res.sort((a, b) => a.processedOn > b.processedOn ? -1 : 1);
res.sort((a, b) => a.processedOn! > b.processedOn! ? -1 : 1);
} else if (state === 'delayed') {
res.sort((a, b) => (a.processedOn ?? a.timestamp) > (b.processedOn ?? b.timestamp) ? -1 : 1);
}
@ -276,6 +261,8 @@ useInterval(() => {
});
async function clearQueue() {
if (tab.value === '-') return;
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.ts.areYouSure,
@ -289,6 +276,8 @@ async function clearQueue() {
}
async function promoteAllJobs() {
if (tab.value === '-') return;
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.ts.areYouSure,
@ -302,13 +291,15 @@ async function promoteAllJobs() {
}
async function removeJobs() {
if (tab.value === '-' || jobState.value === 'latest') return;
const { canceled } = await os.confirm({
type: 'warning',
title: i18n.ts.areYouSure,
});
if (canceled) return;
os.apiWithDialog('admin/queue/clear', { queue: tab.value, state: jobState.value });
os.apiWithDialog('admin/queue/clear', { queue: tab.value, state: jobState.value === 'all' ? '*' : jobState.value });
fetchCurrentQueue();
fetchJobs();
@ -324,16 +315,18 @@ async function refreshJob(jobId: string) {
const headerActions = computed(() => []);
const headerTabs = computed(() =>
[{
key: '-',
title: i18n.ts.overview,
icon: 'ti ti-dashboard',
}].concat(QUEUE_TYPES.map((t) => ({
key: t,
title: t,
}))),
);
const headerTabs = computed<{
key: string;
title: string;
icon?: string;
}[]>(() => [{
key: '-',
title: i18n.ts.jobQueue,
icon: 'ti ti-list-check',
}, ...Misskey.queueTypes.map((q) => ({
key: q,
title: q,
}))]);
definePage(() => ({
title: i18n.ts.jobQueue,

View File

@ -96,6 +96,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="stackingRouterView">
<template #label>Enable stacking router view</template>
</MkSwitch>
<MkSwitch v-model="enableFolderPageView">
<template #label>Enable folder page view</template>
</MkSwitch>
</div>
</MkFolder>
</SearchMarker>
@ -157,6 +160,7 @@ const enableCondensedLine = prefer.model('enableCondensedLine');
const skipNoteRender = prefer.model('skipNoteRender');
const devMode = prefer.model('devMode');
const stackingRouterView = prefer.model('experimental.stackingRouterView');
const enableFolderPageView = prefer.model('experimental.enableFolderPageView');
watch(skipNoteRender, async () => {
await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });

View File

@ -417,4 +417,7 @@ export const PREF_DEF = {
'experimental.stackingRouterView': {
default: false,
},
'experimental.enableFolderPageView': {
default: false,
},
} satisfies PreferencesDefinition;

View File

@ -73,7 +73,7 @@ const otherNavItemIndicated = computed<boolean>(() => {
function more(ev: MouseEvent) {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
src: ev.currentTarget ?? ev.target,
anchorElement: ev.currentTarget ?? ev.target,
anchor: { x: 'center', y: 'bottom' },
}, {
closed: () => dispose(),

View File

@ -180,7 +180,7 @@ function more(ev: MouseEvent) {
const target = getHTMLElementOrNull(ev.currentTarget ?? ev.target);
if (!target) return;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkLaunchPad.vue')), {
src: target,
anchorElement: target,
}, {
closed: () => dispose(),
});

View File

@ -197,7 +197,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
});
}
function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
function select(anchorElement: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => {
os.popupMenu([label ? {
text: label,
@ -214,16 +214,16 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul
text: i18n.ts.fromUrl,
icon: 'ti ti-link',
action: () => chooseFileFromUrl().then(file => res([file])),
}], src);
}], anchorElement);
});
}
export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
return select(src, label, false).then(files => files[0]);
export function selectFile(anchorElement: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
return select(anchorElement, label, false).then(files => files[0]);
}
export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
return select(src, label, true);
export function selectFiles(anchorElement: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
return select(anchorElement, label, true);
}
export async function createCroppedImageDriveFileFromImageDriveFile(imageDriveFile: Misskey.entities.DriveFile, options: {

View File

@ -15,7 +15,7 @@ import { prefer } from '@/preferences.js';
* 使
*/
class EmojiPicker {
private src: Ref<HTMLElement | null> = ref(null);
private anchorElement: Ref<HTMLElement | null> = ref(null);
private manualShowing = ref(false);
private onChosen?: (emoji: string) => void;
private onClosed?: () => void;
@ -34,7 +34,7 @@ class EmojiPicker {
});
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
anchorElement: this.anchorElement,
pinnedEmojis: emojisRef,
asReactionPicker: false,
manualShowing: this.manualShowing,
@ -47,18 +47,18 @@ class EmojiPicker {
this.manualShowing.value = false;
},
closed: () => {
this.src.value = null;
this.anchorElement.value = null;
if (this.onClosed) this.onClosed();
},
});
}
public show(
src: HTMLElement,
anchorElement: HTMLElement,
onChosen?: EmojiPicker['onChosen'],
onClosed?: EmojiPicker['onClosed'],
) {
this.src.value = src;
this.anchorElement.value = anchorElement;
this.manualShowing.value = true;
this.onChosen = onChosen;
this.onClosed = onClosed;

View File

@ -4,20 +4,20 @@
*/
import { nextTick } from 'vue';
import { MFM_TAGS } from '@@/js/const.js';
import type { Ref } from 'vue';
import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { MFM_TAGS } from '@@/js/const.js';
import type { MenuItem } from '@/types/menu.js';
/**
* MFMの装飾のリストを表示する
*/
export function mfmFunctionPicker(src: HTMLElement | EventTarget | null, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
export function mfmFunctionPicker(anchorElement: HTMLElement | EventTarget | null, textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>) {
os.popupMenu([{
text: i18n.ts.addMfmFunction,
type: 'label',
}, ...getFunctionList(textArea, textRef)], src);
}, ...getFunctionList(textArea, textRef)], anchorElement);
}
function getFunctionList(textArea: HTMLInputElement | HTMLTextAreaElement, textRef: Ref<string>): MenuItem[] {

View File

@ -10,7 +10,7 @@ import { popup } from '@/os.js';
import { prefer } from '@/preferences.js';
class ReactionPicker {
private src: Ref<HTMLElement | null> = ref(null);
private anchorElement: Ref<HTMLElement | null> = ref(null);
private manualShowing = ref(false);
private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
private onChosen?: (reaction: string) => void;
@ -30,7 +30,7 @@ class ReactionPicker {
});
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
anchorElement: this.anchorElement,
pinnedEmojis: reactionsRef,
asReactionPicker: true,
targetNote: this.targetNote,
@ -43,14 +43,14 @@ class ReactionPicker {
this.manualShowing.value = false;
},
closed: () => {
this.src.value = null;
this.anchorElement.value = null;
if (this.onClosed) this.onClosed();
},
});
}
public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
this.src.value = src;
public show(anchorElement: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {
this.anchorElement.value = anchorElement;
this.targetNote.value = targetNote;
this.manualShowing.value = true;
this.onChosen = onChosen;

View File

@ -275,12 +275,21 @@ type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed'
// @public (undocumented)
type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueJobsResponse = operations['admin___queue___jobs']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueQueuesResponse = operations['admin___queue___queues']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminQueueQueueStatsRequest = operations['admin___queue___queue-stats']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueQueueStatsResponse = operations['admin___queue___queue-stats']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json'];
@ -290,6 +299,9 @@ type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['reques
// @public (undocumented)
type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json'];
// @public (undocumented)
type AdminQueueShowJobResponse = operations['admin___queue___show-job']['responses']['200']['content']['application/json'];
// @public (undocumented)
type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
@ -1532,11 +1544,15 @@ declare namespace entities {
AdminQueueDeliverDelayedResponse,
AdminQueueInboxDelayedResponse,
AdminQueueJobsRequest,
AdminQueueJobsResponse,
AdminQueuePromoteJobsRequest,
AdminQueueQueueStatsRequest,
AdminQueueQueueStatsResponse,
AdminQueueQueuesResponse,
AdminQueueRemoveJobRequest,
AdminQueueRetryJobRequest,
AdminQueueShowJobRequest,
AdminQueueShowJobResponse,
AdminQueueStatsResponse,
AdminRelaysAddRequest,
AdminRelaysAddResponse,
@ -2117,6 +2133,8 @@ declare namespace entities {
PageBlock,
Channel,
QueueCount,
QueueMetrics,
QueueJob,
Antenna,
Clip,
FederationInstance,
@ -3205,6 +3223,12 @@ type PureRenote = Omit<Note, 'renote' | 'renoteId' | 'reply' | 'replyId' | 'text
// @public (undocumented)
type QueueCount = components['schemas']['QueueCount'];
// @public (undocumented)
type QueueJob = components['schemas']['QueueJob'];
// @public (undocumented)
type QueueMetrics = components['schemas']['QueueMetrics'];
// @public (undocumented)
type QueueStats = {
deliver: {
@ -3224,6 +3248,9 @@ type QueueStats = {
// @public (undocumented)
type QueueStatsLog = QueueStats[];
// @public (undocumented)
export const queueTypes: readonly ["system", "endedPollNotification", "deliver", "inbox", "db", "relationship", "objectStorage", "userWebhookDeliver", "systemWebhookDeliver"];
// @public (undocumented)
type RenoteMuteCreateRequest = operations['renote-mute___create']['requestBody']['content']['application/json'];

View File

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

View File

@ -79,11 +79,15 @@ import type {
AdminQueueDeliverDelayedResponse,
AdminQueueInboxDelayedResponse,
AdminQueueJobsRequest,
AdminQueueJobsResponse,
AdminQueuePromoteJobsRequest,
AdminQueueQueueStatsRequest,
AdminQueueQueueStatsResponse,
AdminQueueQueuesResponse,
AdminQueueRemoveJobRequest,
AdminQueueRetryJobRequest,
AdminQueueShowJobRequest,
AdminQueueShowJobResponse,
AdminQueueStatsResponse,
AdminRelaysAddRequest,
AdminRelaysAddResponse,
@ -694,13 +698,13 @@ export type Endpoints = {
'admin/queue/clear': { req: AdminQueueClearRequest; res: EmptyResponse };
'admin/queue/deliver-delayed': { req: EmptyRequest; res: AdminQueueDeliverDelayedResponse };
'admin/queue/inbox-delayed': { req: EmptyRequest; res: AdminQueueInboxDelayedResponse };
'admin/queue/jobs': { req: AdminQueueJobsRequest; res: EmptyResponse };
'admin/queue/jobs': { req: AdminQueueJobsRequest; res: AdminQueueJobsResponse };
'admin/queue/promote-jobs': { req: AdminQueuePromoteJobsRequest; res: EmptyResponse };
'admin/queue/queue-stats': { req: AdminQueueQueueStatsRequest; res: EmptyResponse };
'admin/queue/queues': { req: EmptyRequest; res: EmptyResponse };
'admin/queue/queue-stats': { req: AdminQueueQueueStatsRequest; res: AdminQueueQueueStatsResponse };
'admin/queue/queues': { req: EmptyRequest; res: AdminQueueQueuesResponse };
'admin/queue/remove-job': { req: AdminQueueRemoveJobRequest; res: EmptyResponse };
'admin/queue/retry-job': { req: AdminQueueRetryJobRequest; res: EmptyResponse };
'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: EmptyResponse };
'admin/queue/show-job': { req: AdminQueueShowJobRequest; res: AdminQueueShowJobResponse };
'admin/queue/stats': { req: EmptyRequest; res: AdminQueueStatsResponse };
'admin/relays/add': { req: AdminRelaysAddRequest; res: AdminRelaysAddResponse };
'admin/relays/list': { req: EmptyRequest; res: AdminRelaysListResponse };

View File

@ -82,11 +82,15 @@ export type AdminQueueClearRequest = operations['admin___queue___clear']['reques
export type AdminQueueDeliverDelayedResponse = operations['admin___queue___deliver-delayed']['responses']['200']['content']['application/json'];
export type AdminQueueInboxDelayedResponse = operations['admin___queue___inbox-delayed']['responses']['200']['content']['application/json'];
export type AdminQueueJobsRequest = operations['admin___queue___jobs']['requestBody']['content']['application/json'];
export type AdminQueueJobsResponse = operations['admin___queue___jobs']['responses']['200']['content']['application/json'];
export type AdminQueuePromoteJobsRequest = operations['admin___queue___promote-jobs']['requestBody']['content']['application/json'];
export type AdminQueueQueueStatsRequest = operations['admin___queue___queue-stats']['requestBody']['content']['application/json'];
export type AdminQueueQueueStatsResponse = operations['admin___queue___queue-stats']['responses']['200']['content']['application/json'];
export type AdminQueueQueuesResponse = operations['admin___queue___queues']['responses']['200']['content']['application/json'];
export type AdminQueueRemoveJobRequest = operations['admin___queue___remove-job']['requestBody']['content']['application/json'];
export type AdminQueueRetryJobRequest = operations['admin___queue___retry-job']['requestBody']['content']['application/json'];
export type AdminQueueShowJobRequest = operations['admin___queue___show-job']['requestBody']['content']['application/json'];
export type AdminQueueShowJobResponse = operations['admin___queue___show-job']['responses']['200']['content']['application/json'];
export type AdminQueueStatsResponse = operations['admin___queue___stats']['responses']['200']['content']['application/json'];
export type AdminRelaysAddRequest = operations['admin___relays___add']['requestBody']['content']['application/json'];
export type AdminRelaysAddResponse = operations['admin___relays___add']['responses']['200']['content']['application/json'];

View File

@ -29,6 +29,8 @@ export type Page = components['schemas']['Page'];
export type PageBlock = components['schemas']['PageBlock'];
export type Channel = components['schemas']['Channel'];
export type QueueCount = components['schemas']['QueueCount'];
export type QueueMetrics = components['schemas']['QueueMetrics'];
export type QueueJob = components['schemas']['QueueJob'];
export type Antenna = components['schemas']['Antenna'];
export type Clip = components['schemas']['Clip'];
export type FederationInstance = components['schemas']['FederationInstance'];

View File

@ -4946,6 +4946,32 @@ export type components = {
failed: number;
delayed: number;
};
QueueMetrics: {
meta: {
count: number;
prevTS: number;
prevCount: number;
};
data: number[];
count: number;
};
QueueJob: {
id: string;
name: string;
data: Record<string, never>;
opts: Record<string, never>;
timestamp: number;
processedOn?: number;
processedBy?: string;
finishedOn?: number;
progress: Record<string, never>;
attempts: number;
delay: number;
failedReason: string;
stacktrace: string[];
returnValue: Record<string, never>;
isFailed: boolean;
};
Antenna: {
/** Format: id */
id: string;
@ -9049,9 +9075,11 @@ export type operations = {
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
/** @description OK (with results) */
200: {
content: {
'application/json': components['schemas']['QueueJob'][];
};
};
/** @description Client error */
400: {
@ -9153,9 +9181,43 @@ export type operations = {
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
/** @description OK (with results) */
200: {
content: {
'application/json': {
/** @enum {string} */
name: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver';
qualifiedName: string;
counts: {
[key: string]: number;
};
isPaused: boolean;
metrics: {
completed: components['schemas']['QueueMetrics'];
failed: components['schemas']['QueueMetrics'];
};
db: {
version: string;
/** @enum {string} */
mode: 'cluster' | 'standalone' | 'sentinel';
runId: string;
processId: string;
port: number;
os: string;
uptime: number;
memory: {
total: number;
used: number;
fragmentationRatio: number;
peak: number;
};
clients: {
blocked: number;
connected: number;
};
};
};
};
};
/** @description Client error */
400: {
@ -9197,9 +9259,22 @@ export type operations = {
*/
admin___queue___queues: {
responses: {
/** @description OK (without any results) */
204: {
content: never;
/** @description OK (with results) */
200: {
content: {
'application/json': ({
/** @enum {string} */
name: 'system' | 'endedPollNotification' | 'deliver' | 'inbox' | 'db' | 'relationship' | 'objectStorage' | 'userWebhookDeliver' | 'systemWebhookDeliver';
counts: {
[key: string]: number;
};
isPaused: boolean;
metrics: {
completed: components['schemas']['QueueMetrics'];
failed: components['schemas']['QueueMetrics'];
};
})[];
};
};
/** @description Client error */
400: {
@ -9356,9 +9431,11 @@ export type operations = {
};
};
responses: {
/** @description OK (without any results) */
204: {
content: never;
/** @description OK (with results) */
200: {
content: {
'application/json': components['schemas']['QueueJob'];
};
};
/** @description Client error */
400: {

View File

@ -169,6 +169,18 @@ export const moderationLogTypes = [
'deleteChatRoom',
] as const;
export const queueTypes = [
'system',
'endedPollNotification',
'deliver',
'inbox',
'db',
'relationship',
'objectStorage',
'userWebhookDeliver',
'systemWebhookDeliver',
] as const;
// See: packages/backend/src/core/ReversiService.ts@L410
export const reversiUpdateKeys = [
'map',

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 queueTypes = consts.queueTypes;
export const reversiUpdateKeys = consts.reversiUpdateKeys;
// api extractor not supported yet