Merge branch 'develop' into feat-1714

This commit is contained in:
かっこかり 2024-07-11 17:06:22 +09:00 committed by GitHub
commit f826c5df7a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 414 additions and 232 deletions

View File

@ -3,7 +3,7 @@ name: "Release Manager: sync changelog with PR"
on: on:
push: push:
branches: branches:
- release/** - develop
paths: paths:
- 'CHANGELOG.md' - 'CHANGELOG.md'
@ -20,24 +20,29 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得 # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PR - name: Get PR
run: | run: |
echo "pr_number=$(gh pr list --limit 1 --head "$GITHUB_REF_NAME" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr id: get_pr
env:
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
- name: Get target version - name: Get target version
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v1 if: steps.get_pr.outputs.pr_number != ''
uses: misskey-dev/release-manager-actions/.github/actions/get-target-version@v2
id: v id: v
# CHANGELOG.mdの内容を取得 # CHANGELOG.mdの内容を取得
- name: Get changelog - name: Get changelog
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v1 if: steps.get_pr.outputs.pr_number != ''
uses: misskey-dev/release-manager-actions/.github/actions/get-changelog@v2
with: with:
version: ${{ steps.v.outputs.target_version }} version: ${{ steps.v.outputs.target_version }}
id: changelog id: changelog
# PRのnotesを更新 # PRのnotesを更新
- name: Update PR - name: Update PR
if: steps.get_pr.outputs.pr_number != ''
run: | run: |
gh pr edit "$PR_NUMBER" --body "$CHANGELOG" gh pr edit "$PR_NUMBER" --body "$CHANGELOG"
env: env:
CHANGELOG: ${{ steps.changelog.outputs.changelog }}
PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }} PR_NUMBER: ${{ steps.get_pr.outputs.pr_number }}
CHANGELOG: ${{ steps.changelog.outputs.changelog }}

View File

@ -33,18 +33,21 @@ jobs:
pr_number: ${{ steps.get_pr.outputs.pr_number }} pr_number: ${{ steps.get_pr.outputs.pr_number }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# headがrelease/かつopenのPRを1つ取得 # headが$GITHUB_REF_NAME, baseが$STABLE_BRANCHかつopenのPRを1つ取得
- name: Get PRs - name: Get PRs
run: | run: |
echo "pr_number=$(gh pr list --limit 1 --search "head:release/ is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT echo "pr_number=$(gh pr list --limit 1 --search "head:$GITHUB_REF_NAME base:$STABLE_BRANCH is:open" --json number --jq '.[] | .number')" >> $GITHUB_OUTPUT
id: get_pr id: get_pr
env:
STABLE_BRANCH: ${{ vars.STABLE_BRANCH }}
merge: merge:
uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v1 uses: misskey-dev/release-manager-actions/.github/workflows/merge.yml@v2
needs: get-pr needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }} if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge == true }}
with: with:
pr_number: ${{ needs.get-pr.outputs.pr_number }} pr_number: ${{ needs.get-pr.outputs.pr_number }}
user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
# Text to prepend to the changelog # Text to prepend to the changelog
# The first line must be `## Unreleased` # The first line must be `## Unreleased`
@ -65,15 +68,14 @@ jobs:
secrets: secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}
create-prerelease: create-prerelease:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1 uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
needs: get-pr needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }} if: ${{ needs.get-pr.outputs.pr_number != '' && inputs.merge != true }}
with: with:
pr_number: ${{ needs.get-pr.outputs.pr_number }} pr_number: ${{ needs.get-pr.outputs.pr_number }}
user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }} indent: ${{ vars.INDENT }}
@ -82,10 +84,11 @@ jobs:
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
create-target: create-target:
uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v1 uses: misskey-dev/release-manager-actions/.github/workflows/create-target.yml@v2
needs: get-pr needs: get-pr
if: ${{ needs.get-pr.outputs.pr_number == '' }} if: ${{ needs.get-pr.outputs.pr_number == '' }}
with: with:
user: 'github-actions[bot]'
# The script for version increment. # The script for version increment.
# process.env.CURRENT_VERSION: The current version. # process.env.CURRENT_VERSION: The current version.
# #
@ -118,8 +121,7 @@ jobs:
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }} indent: ${{ vars.INDENT }}
stable_branch: ${{ vars.STABLE_BRANCH }}
secrets: secrets:
RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }} RELEASE_APP_ID: ${{ secrets.RELEASE_APP_ID }}
RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} RELEASE_APP_PRIVATE_KEY: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
RULESET_EDIT_APP_ID: ${{ secrets.RULESET_EDIT_APP_ID }}
RULESET_EDIT_APP_PRIVATE_KEY: ${{ secrets.RULESET_EDIT_APP_PRIVATE_KEY }}

View File

@ -16,23 +16,26 @@ jobs:
check: check:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
ref: ${{ steps.get_pr.outputs.ref }} head: ${{ steps.get_pr.outputs.head }}
base: ${{ steps.get_pr.outputs.base }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
# PR情報を取得 # PR情報を取得
- name: Get PR - name: Get PR
run: | run: |
pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName) pr_json=$(gh pr view "$PR_NUMBER" --json isDraft,headRefName,baseRefName)
echo "ref=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT echo "head=$(echo $pr_json | jq -r '.headRefName')" >> $GITHUB_OUTPUT
echo "base=$(echo $pr_json | jq -r '.baseRefName')" >> $GITHUB_OUTPUT
id: get_pr id: get_pr
env: env:
PR_NUMBER: ${{ github.event.pull_request.number }} PR_NUMBER: ${{ github.event.pull_request.number }}
release: release:
uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v1 uses: misskey-dev/release-manager-actions/.github/workflows/create-prerelease.yml@v2
needs: check needs: check
if: startsWith(needs.check.outputs.ref, 'release/') if: needs.check.outputs.head == github.event.repository.default_branch && needs.check.outputs.base == vars.STABLE_BRANCH
with: with:
pr_number: ${{ github.event.pull_request.number }} pr_number: ${{ github.event.pull_request.number }}
user: 'github-actions[bot]'
package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }} package_jsons_to_rewrite: ${{ vars.PACKAGE_JSONS_TO_REWRITE }}
use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }} use_external_app_to_release: ${{ vars.USE_RELEASE_APP == 'true' }}
indent: ${{ vars.INDENT }} indent: ${{ vars.INDENT }}

View File

@ -8,12 +8,15 @@
### Client ### Client
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能 - Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
- 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です - 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正 - Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968) - Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
- Fix: リバーシの対局を正しく共有できないことがある問題を修正 - Fix: リバーシの対局を正しく共有できないことがある問題を修正
- Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正 - Fix: コントロールパネルでベースロールのポリシーを編集してもUI上では変更が反映されない問題を修正
- Fix: アンテナの編集画面のボタンに隙間を追加 - Fix: アンテナの編集画面のボタンに隙間を追加
- Fix: テーマプレビューが見れない問題を修正 - Fix: テーマプレビューが見れない問題を修正
- Fix: ショートカットキーが連打できる問題を修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/234)
### Server ### Server
- Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949) - Feat: レートリミット制限に引っかかったときに`Retry-After`ヘッダーを返すように (#13949)
@ -36,6 +39,7 @@
### Misskey.js ### Misskey.js
- Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応) - Feat: `/drive/files/create` のリクエストに対応(`multipart/form-data`に対応)
- Feat: `/admin/role/create` のロールポリシーの型を修正
## 2024.5.0 ## 2024.5.0

View File

@ -165,7 +165,7 @@ cp .github/misskey/test.yml .config/
``` ```
Prepare DB/Redis for testing. Prepare DB/Redis for testing.
``` ```
docker compose -f packages/backend/test/compose.yaml up docker compose -f packages/backend/test/compose.yml up
``` ```
Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`. Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.

View File

@ -0,0 +1,20 @@
<!DOCTYPE html>
<html>
<head>
<title>Misskey API</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<script
id="api-reference"
data-url="/api.json"></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>

View File

@ -1,24 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<title>Misskey API</title>
<!-- needed for adaptive design -->
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
<!--
ReDoc doesn't change outer page styles
-->
<style>
body {
margin: 0;
padding: 0;
}
</style>
</head>
<body>
<redoc spec-url="/api.json" expand-responses="200" expand-single-schema-field="true"></redoc>
<script src="https://cdn.redoc.ly/redoc/v2.1.3/bundles/redoc.standalone.js" integrity="sha256-u4DgqzYXoArvNF/Ymw3puKexfOC6lYfw0sfmeliBJ1I=" crossorigin="anonymous"></script>
</body>
</html>

View File

@ -25,7 +25,7 @@ export class OpenApiServerService {
public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) { public createServer(fastify: FastifyInstance, _options: FastifyPluginOptions, done: (err?: Error) => void) {
fastify.get('/api-doc', async (_request, reply) => { fastify.get('/api-doc', async (_request, reply) => {
reply.header('Cache-Control', 'public, max-age=86400'); reply.header('Cache-Control', 'public, max-age=86400');
return await reply.sendFile('/redoc.html', staticAssets); return await reply.sendFile('/api-doc.html', staticAssets);
}); });
fastify.get('/api.json', (_request, reply) => { fastify.get('/api.json', (_request, reply) => {
reply.header('Cache-Control', 'public, max-age=600'); reply.header('Cache-Control', 'public, max-age=600');

View File

@ -15,7 +15,6 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
info: { info: {
version: config.version, version: config.version,
title: 'Misskey API', title: 'Misskey API',
'x-logo': { url: '/static-assets/api-doc.png' },
}, },
externalDocs: { externalDocs: {

View File

@ -13,7 +13,6 @@ import * as sound from '@/scripts/sound.js';
import { $i, signout, updateAccount } from '@/account.js'; import { $i, signout, updateAccount } from '@/account.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js'; import { reactionPicker } from '@/scripts/reaction-picker.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js'; import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
@ -21,6 +20,7 @@ import { initializeSw } from '@/scripts/initialize-sw.js';
import { deckStore } from '@/ui/deck/deck-store.js'; import { deckStore } from '@/ui/deck/deck-store.js';
import { emojiPicker } from '@/scripts/emoji-picker.js'; import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mainRouter } from '@/router/main.js'; import { mainRouter } from '@/router/main.js';
import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
export async function mainBoot() { export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp( const { isClientUpdated } = await common(() => createApp(
@ -69,14 +69,6 @@ export async function mainBoot() {
}); });
} }
const hotkeys = {
'd': (): void => {
defaultStore.set('darkMode', !defaultStore.state.darkMode);
},
's': (): void => {
mainRouter.push('/search');
},
};
try { try {
if (defaultStore.state.enableSeasonalScreenEffect) { if (defaultStore.state.enableSeasonalScreenEffect) {
const month = new Date().getMonth() + 1; const month = new Date().getMonth() + 1;
@ -105,9 +97,6 @@ export async function mainBoot() {
} }
if ($i) { if ($i) {
// only add post shortcuts if logged in
hotkeys['p|n'] = post;
defaultStore.loaded.then(() => { defaultStore.loaded.then(() => {
if (defaultStore.state.accountSetupWizard !== -1) { if (defaultStore.state.accountSetupWizard !== -1) {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, { const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
@ -334,7 +323,19 @@ export async function mainBoot() {
} }
// shortcut // shortcut
document.addEventListener('keydown', makeHotkey(hotkeys)); const keymap = {
'p|n': () => {
if ($i == null) return;
post();
},
'd': () => {
defaultStore.set('darkMode', !defaultStore.state.darkMode);
},
's': () => {
mainRouter.push('/search');
},
} as const satisfies Keymap;
document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
initializeSw(); initializeSw();
} }

View File

@ -82,6 +82,7 @@ import type { MenuItem } from '@/types/menu.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { type Keymap } from '@/scripts/hotkey.js';
import bytes from '@/filters/bytes.js'; import bytes from '@/filters/bytes.js';
import { hms } from '@/filters/hms.js'; import { hms } from '@/filters/hms.js';
import MkMediaRange from '@/components/MkMediaRange.vue'; import MkMediaRange from '@/components/MkMediaRange.vue';
@ -94,29 +95,41 @@ const props = defineProps<{
const inEmbedPage = inject<boolean>('EMBED_PAGE', false); const inEmbedPage = inject<boolean>('EMBED_PAGE', false);
const keymap = { const keymap = {
'up': () => { 'up': {
if (inEmbedPage) return; allowRepeat: true,
if (hasFocus() && audioEl.value) { callback: () => {
volume.value = Math.min(volume.value + 0.1, 1); if (inEmbedPage) return;
} if (hasFocus() && audioEl.value) {
volume.value = Math.min(volume.value + 0.1, 1);
}
},
}, },
'down': () => { 'down': {
if (inEmbedPage) return; allowRepeat: true,
if (hasFocus() && audioEl.value) { callback: () => {
volume.value = Math.max(volume.value - 0.1, 0); if (inEmbedPage) return;
} if (hasFocus() && audioEl.value) {
volume.value = Math.max(volume.value - 0.1, 0);
}
},
}, },
'left': () => { 'left': {
if (inEmbedPage) return; allowRepeat: true,
if (hasFocus() && audioEl.value) { callback: () => {
audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0); if (inEmbedPage) return;
} if (hasFocus() && audioEl.value) {
audioEl.value.currentTime = Math.max(audioEl.value.currentTime - 5, 0);
}
},
}, },
'right': () => { 'right': {
if (inEmbedPage) return; allowRepeat: true,
if (hasFocus() && audioEl.value) { callback: () => {
audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration); if (inEmbedPage) return;
} if (hasFocus() && audioEl.value) {
audioEl.value.currentTime = Math.min(audioEl.value.currentTime + 5, audioEl.value.duration);
}
},
}, },
'space': () => { 'space': () => {
if (inEmbedPage) return; if (inEmbedPage) return;
@ -124,7 +137,7 @@ const keymap = {
togglePlayPause(); togglePlayPause();
} }
}, },
}; } as const satisfies Keymap;
// PlayerEl // PlayerEl
function hasFocus() { function hasFocus() {

View File

@ -113,6 +113,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, shallowRef, computed, watch, inject, onDeactivated, onActivated, onMounted } from 'vue'; import { ref, shallowRef, computed, watch, inject, onDeactivated, onActivated, onMounted } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import type { MenuItem } from '@/types/menu.js'; import type { MenuItem } from '@/types/menu.js';
import { type Keymap } from '@/scripts/hotkey.js';
import bytes from '@/filters/bytes.js'; import bytes from '@/filters/bytes.js';
import { hms } from '@/filters/hms.js'; import { hms } from '@/filters/hms.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
@ -130,29 +131,41 @@ const props = defineProps<{
const inEmbedPage = inject<boolean>('EMBED_PAGE', false); const inEmbedPage = inject<boolean>('EMBED_PAGE', false);
const keymap = { const keymap = {
'up': () => { 'up': {
if (inEmbedPage) return; allowRepeat: true,
if (hasFocus() && videoEl.value) { callback: () => {
volume.value = Math.min(volume.value + 0.1, 1); if (inEmbedPage) return;
} if (hasFocus() && videoEl.value) {
volume.value = Math.min(volume.value + 0.1, 1);
}
},
}, },
'down': () => { 'down': {
if (inEmbedPage) return; allowRepeat: true,
if (hasFocus() && videoEl.value) { callback: () => {
volume.value = Math.max(volume.value - 0.1, 0); if (inEmbedPage) return;
} if (hasFocus() && videoEl.value) {
volume.value = Math.max(volume.value - 0.1, 0);
}
},
}, },
'left': () => { 'left': {
if (inEmbedPage) return; allowRepeat: true,
if (hasFocus() && videoEl.value) { callback: () => {
videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0); if (inEmbedPage) return;
} if (hasFocus() && videoEl.value) {
videoEl.value.currentTime = Math.max(videoEl.value.currentTime - 5, 0);
}
},
}, },
'right': () => { 'right': {
if (inEmbedPage) return; allowRepeat: true,
if (hasFocus() && videoEl.value) { callback: () => {
videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration); if (inEmbedPage) return;
} if (hasFocus() && videoEl.value) {
videoEl.value.currentTime = Math.min(videoEl.value.currentTime + 5, videoEl.value.duration);
}
},
}, },
'space': () => { 'space': () => {
if (inEmbedPage) return; if (inEmbedPage) return;
@ -160,7 +173,7 @@ const keymap = {
togglePlayPause(); togglePlayPause();
} }
}, },
}; } as const satisfies Keymap;
// PlayerEl // PlayerEl
function hasFocus() { function hasFocus() {

View File

@ -98,6 +98,7 @@ import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { isTouchUsing } from '@/scripts/touch.js'; import { isTouchUsing } from '@/scripts/touch.js';
import { type Keymap } from '@/scripts/hotkey.js';
const childrenCache = new WeakMap<MenuParent, MenuItem[]>(); const childrenCache = new WeakMap<MenuParent, MenuItem[]>();
</script> </script>
@ -125,11 +126,20 @@ const items2 = ref<InnerMenuItem[]>();
const child = shallowRef<InstanceType<typeof XChild>>(); const child = shallowRef<InstanceType<typeof XChild>>();
const keymap = computed(() => ({ const keymap = {
'up|k|shift+tab': focusUp, 'up|k|shift+tab': {
'down|j|tab': focusDown, allowRepeat: true,
'esc': close, callback: () => focusUp(),
})); },
'down|j|tab': {
allowRepeat: true,
callback: () => focusDown(),
},
'esc': {
allowRepeat: true,
callback: () => close(false),
},
} as const satisfies Keymap;
const childShowingItem = ref<MenuItem | null>(); const childShowingItem = ref<MenuItem | null>();

View File

@ -47,6 +47,7 @@ import * as os from '@/os.js';
import { isTouchUsing } from '@/scripts/touch.js'; import { isTouchUsing } from '@/scripts/touch.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { deviceKind } from '@/scripts/device-kind.js'; import { deviceKind } from '@/scripts/device-kind.js';
import { type Keymap } from '@/scripts/hotkey.js';
function getFixedContainer(el: Element | null): Element | null { function getFixedContainer(el: Element | null): Element | null {
if (el == null || el.tagName === 'BODY') return null; if (el == null || el.tagName === 'BODY') return null;
@ -154,8 +155,11 @@ if (type.value === 'drawer') {
} }
const keymap = { const keymap = {
'esc': () => emit('esc'), 'esc': {
}; allowRepeat: true,
callback: () => emit('esc'),
},
} as const satisfies Keymap;
const MARGIN = 16; const MARGIN = 16;
const SCROLLBAR_THICKNESS = 16; const SCROLLBAR_THICKNESS = 16;

View File

@ -217,6 +217,7 @@ import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { shouldCollapsed } from '@/scripts/collapsed.js'; import { shouldCollapsed } from '@/scripts/collapsed.js';
import { isEnabledUrlPreview } from '@/instance.js'; import { isEnabledUrlPreview } from '@/instance.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { type Keymap } from '@/scripts/hotkey.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -314,15 +315,53 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
} }
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => {
'e|a|plus': () => react(true), if (renoteCollapsed.value) return;
'q': () => renote(true), reply();
'up|k|shift+tab': focusBefore, },
'down|j|tab': focusAfter, 'e|a|plus': () => {
'esc': blur, if (renoteCollapsed.value) return;
'm|o': () => showMenu(true), react();
's': () => showContent.value !== showContent.value, },
}; 'q': () => {
if (renoteCollapsed.value) return;
renote();
},
'm': () => {
if (renoteCollapsed.value) return;
showMenu();
},
'c': () => {
if (renoteCollapsed.value) return;
if (!defaultStore.state.showClipButtonInNoteFooter) return;
clip();
},
'o': () => {
if (renoteCollapsed.value) return;
showMenu();
},
'v|enter': () => {
if (renoteCollapsed.value) {
renoteCollapsed.value = false;
} else if (appearNote.value.cw != null) {
showContent.value = !showContent.value;
} else if (isLong) {
collapsed.value = !collapsed.value;
}
},
'esc': {
allowRepeat: true,
callback: () => blur(),
},
'up|k|shift+tab': {
allowRepeat: true,
callback: () => focusBefore(),
},
'down|j|tab': {
allowRepeat: true,
callback: () => focusAfter(),
},
} as const satisfies Keymap;
provide('react', (reaction: string) => { provide('react', (reaction: string) => {
misskeyApi('notes/reactions/create', { misskeyApi('notes/reactions/create', {

View File

@ -233,6 +233,7 @@ import MkPagination, { type Paging } from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { isEnabledUrlPreview } from '@/instance.js'; import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -294,13 +295,24 @@ const replies = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(),
'e|a|plus': () => react(true), 'e|a|plus': () => react(),
'q': () => renote(true), 'q': () => renote(),
'esc': blur, 'm': () => showMenu(),
'm|o': () => showMenu(true), 'c': () => {
's': () => showContent.value !== showContent.value, if (!defaultStore.state.showClipButtonInNoteFooter) return;
}; clip();
},
'v|enter': () => {
if (appearNote.value.cw != null) {
showContent.value = !showContent.value;
}
},
'esc': {
allowRepeat: true,
callback: () => blur(),
},
} as const satisfies Keymap;
provide('react', (reaction: string) => { provide('react', (reaction: string) => {
misskeyApi('notes/reactions/create', { misskeyApi('notes/reactions/create', {

View File

@ -4,7 +4,7 @@
*/ */
import { Directive } from 'vue'; import { Directive } from 'vue';
import { makeHotkey } from '../scripts/hotkey.js'; import { makeHotkey } from '@/scripts/hotkey.js';
export default { export default {
mounted(el, binding) { mounted(el, binding) {

View File

@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkStickyContainer> <MkStickyContainer>
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template> <template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="800"> <MkSpacer :contentMax="800">
<div ref="rootEl" v-hotkey.global="keymap"> <div ref="rootEl">
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div> <div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div :class="$style.tl"> <div :class="$style.tl">
<MkTimeline <MkTimeline
@ -44,9 +44,6 @@ const antenna = ref<Misskey.entities.Antenna | null>(null);
const queue = ref(0); const queue = ref(0);
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();
const tlEl = shallowRef<InstanceType<typeof MkTimeline>>(); const tlEl = shallowRef<InstanceType<typeof MkTimeline>>();
const keymap = computed(() => ({
't': focus,
}));
function queueUpdated(q) { function queueUpdated(q) {
queue.value = q; queue.value = q;

View File

@ -48,7 +48,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed, ref, watch } from 'vue'; import { computed, ref, watch, type StyleValue } from 'vue';
import tinycolor from 'tinycolor2'; import tinycolor from 'tinycolor2';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
@ -102,10 +102,10 @@ function fetchDriveInfo(): void {
}); });
} }
function genUsageBar(fsize: number): object { function genUsageBar(fsize: number): StyleValue {
return { return {
width: `${fsize / usage.value * 100}%`, width: `${fsize / usage.value * 100}%`,
background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }), background: tinycolor({ h: 180 - (fsize / usage.value * 180), s: 0.7, l: 0.5 }).toHslString(),
}; };
} }

View File

@ -95,7 +95,7 @@ const meterStyle = computed(() => {
h: 180 - (usage.value / capacity.value * 180), h: 180 - (usage.value / capacity.value * 180),
s: 0.7, s: 0.7,
l: 0.5, l: 0.5,
}), }).toHslString(),
}; };
}); });

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template> <template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
<MkSpacer :contentMax="800"> <MkSpacer :contentMax="800">
<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin"> <MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
<div :key="src" ref="rootEl" v-hotkey.global="keymap"> <div :key="src" ref="rootEl">
<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()"> <MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
{{ i18n.ts._timelineDescription[src] }} {{ i18n.ts._timelineDescription[src] }}
</MkInfo> </MkInfo>
@ -58,9 +58,6 @@ provide('shouldOmitHeaderTitle', true);
const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable); const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable); const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
const keymap = {
't': focus,
};
const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>(); const tlComponent = shallowRef<InstanceType<typeof MkTimeline>>();
const rootEl = shallowRef<HTMLElement>(); const rootEl = shallowRef<HTMLElement>();

View File

@ -3,93 +3,132 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import keyCode from './keycode.js'; //#region types
export type Keymap = Record<string, CallbackFunction | CallbackObject>;
type Callback = (ev: KeyboardEvent) => void; type CallbackFunction = (ev: KeyboardEvent) => unknown;
type Keymap = Record<string, Callback>; type CallbackObject = {
callback: CallbackFunction;
allowRepeat?: boolean;
};
type Pattern = { type Pattern = {
which: string[]; which: string[];
ctrl?: boolean; ctrl: boolean;
shift?: boolean; alt: boolean;
alt?: boolean; shift: boolean;
}; };
type Action = { type Action = {
patterns: Pattern[]; patterns: Pattern[];
callback: Callback; callback: CallbackFunction;
allowRepeat: boolean; options: Required<Omit<CallbackObject, 'callback'>>;
};
//#endregion
//#region consts
const KEY_ALIASES = {
'esc': 'Escape',
'enter': ['Enter', 'NumpadEnter'],
'space': [' ', 'Spacebar'],
'up': 'ArrowUp',
'down': 'ArrowDown',
'left': 'ArrowLeft',
'right': 'ArrowRight',
'plus': ['+', ';'],
}; };
const parseKeymap = (keymap: Keymap) => Object.entries(keymap).map(([patterns, callback]): Action => { const MODIFIER_KEYS = ['ctrl', 'alt', 'shift'];
const result = {
patterns: [],
callback,
allowRepeat: true,
} as Action;
if (patterns.match(/^\(.*\)$/) !== null) { const IGNORE_ELEMENTS = ['input', 'textarea'];
result.allowRepeat = false; //#endregion
patterns = patterns.slice(1, -1);
}
result.patterns = patterns.split('|').map(part => {
const pattern = {
which: [],
ctrl: false,
alt: false,
shift: false,
} as Pattern;
const keys = part.trim().split('+').map(x => x.trim().toLowerCase());
for (const key of keys) {
switch (key) {
case 'ctrl': pattern.ctrl = true; break;
case 'alt': pattern.alt = true; break;
case 'shift': pattern.shift = true; break;
default: pattern.which = keyCode(key).map(k => k.toLowerCase());
}
}
return pattern;
});
return result;
});
const ignoreElements = ['input', 'textarea'];
function match(ev: KeyboardEvent, patterns: Action['patterns']): boolean {
const key = ev.key.toLowerCase();
return patterns.some(pattern => pattern.which.includes(key) &&
pattern.ctrl === ev.ctrlKey &&
pattern.shift === ev.shiftKey &&
pattern.alt === ev.altKey &&
!ev.metaKey,
);
}
//#region impl
export const makeHotkey = (keymap: Keymap) => { export const makeHotkey = (keymap: Keymap) => {
const actions = parseKeymap(keymap); const actions = parseKeymap(keymap);
return (ev: KeyboardEvent) => { return (ev: KeyboardEvent) => {
if (document.activeElement) { if ('pswp' in window && window.pswp != null) return;
if (ignoreElements.some(el => document.activeElement!.matches(el))) return; if (document.activeElement != null) {
if (document.activeElement.attributes['contenteditable']) return; if (IGNORE_ELEMENTS.includes(document.activeElement.tagName.toLowerCase())) return;
if ((document.activeElement as HTMLElement).isContentEditable) return;
} }
for (const { patterns, callback, options } of actions) {
for (const action of actions) { if (matchPatterns(ev, patterns, options)) {
const matched = match(ev, action.patterns);
if (matched) {
if (!action.allowRepeat && ev.repeat) return;
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
action.callback(ev); callback(ev);
break;
} }
} }
}; };
}; };
const parseKeymap = (keymap: Keymap) => {
return Object.entries(keymap).map(([rawPatterns, rawCallback]) => {
const patterns = parsePatterns(rawPatterns);
const callback = parseCallback(rawCallback);
const options = parseOptions(rawCallback);
return { patterns, callback, options } as const satisfies Action;
});
};
const parsePatterns = (rawPatterns: keyof Keymap) => {
return rawPatterns.split('|').map(part => {
const keys = part.split('+').map(trimLower);
const which = parseKeyCode(keys.findLast(x => !MODIFIER_KEYS.includes(x)));
const ctrl = keys.includes('ctrl');
const alt = keys.includes('alt');
const shift = keys.includes('shift');
return { which, ctrl, alt, shift } as const satisfies Pattern;
});
};
const parseCallback = (rawCallback: Keymap[keyof Keymap]) => {
if (typeof rawCallback === 'object') {
return rawCallback.callback;
}
return rawCallback;
};
const parseOptions = (rawCallback: Keymap[keyof Keymap]) => {
const defaultOptions = {
allowRepeat: false,
} as const satisfies Action['options'];
if (typeof rawCallback === 'object') {
const { callback, ...rawOptions } = rawCallback;
const options = { ...defaultOptions, ...rawOptions };
return { ...options } as const satisfies Action['options'];
}
return { ...defaultOptions } as const satisfies Action['options'];
};
const matchPatterns = (ev: KeyboardEvent, patterns: Action['patterns'], options: Action['options']) => {
if (ev.repeat && !options.allowRepeat) return false;
const key = ev.key.toLowerCase();
return patterns.some(({ which, ctrl, shift, alt }) => {
if (!which.includes(key)) return false;
if (ctrl !== (ev.ctrlKey || ev.metaKey)) return false;
if (alt !== ev.altKey) return false;
if (shift !== ev.shiftKey) return false;
return true;
});
};
const parseKeyCode = (input?: string | null) => {
if (input == null) return [];
const raw = getValueByKey(KEY_ALIASES, input);
if (raw == null) return [input];
if (typeof raw === 'string') return [trimLower(raw)];
return raw.map(trimLower);
};
const getValueByKey = <
T extends Record<keyof any, unknown>,
K extends keyof T | keyof any,
R extends K extends keyof T ? T[K] : T[keyof T] | undefined,
>(obj: T, key: K) => {
return obj[key] as R;
};
const trimLower = (str: string) => str.trim().toLowerCase();
//#endregion

View File

@ -1,24 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
export default (input: string): string[] => {
if (Object.keys(aliases).some(a => a.toLowerCase() === input.toLowerCase())) {
const codes = aliases[input];
return Array.isArray(codes) ? codes : [codes];
} else {
return [input];
}
};
export const aliases = {
'esc': 'Escape',
'enter': ['Enter', 'NumpadEnter'],
'space': [' ', 'Spacebar'],
'up': 'ArrowUp',
'down': 'ArrowDown',
'left': 'ArrowLeft',
'right': 'ArrowRight',
'plus': ['NumpadAdd', 'Semicolon'],
};

View File

@ -1160,6 +1160,12 @@ export type Endpoints = Overwrite<Endpoints_2, {
req: SigninRequest; req: SigninRequest;
res: SigninResponse; res: SigninResponse;
}; };
'admin/roles/create': {
req: Overwrite<AdminRolesCreateRequest, {
policies: PartialRolePolicyOverride;
}>;
res: AdminRolesCreateResponse;
};
}>; }>;
// @public (undocumented) // @public (undocumented)
@ -1185,6 +1191,7 @@ declare namespace entities {
SignupPendingResponse, SignupPendingResponse,
SigninRequest, SigninRequest,
SigninResponse, SigninResponse,
PartialRolePolicyOverride,
EmptyRequest, EmptyRequest,
EmptyResponse, EmptyResponse,
AdminMetaResponse, AdminMetaResponse,
@ -2777,6 +2784,15 @@ type PagesUpdateRequest = operations['pages___update']['requestBody']['content']
// @public (undocumented) // @public (undocumented)
function parse(acct: string): Acct; function parse(acct: string): Acct;
// Warning: (ae-forgotten-export) The symbol "Values" needs to be exported by the entry point index.d.ts
//
// @public (undocumented)
type PartialRolePolicyOverride = Partial<{
[k in keyof RolePolicies]: Omit<Values<Role['policies']>, 'value'> & {
value: RolePolicies[k];
};
}>;
// @public (undocumented) // @public (undocumented)
export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"]; export const permissions: readonly ["read:account", "write:account", "read:blocks", "write:blocks", "read:drive", "write:drive", "read:favorites", "write:favorites", "read:following", "write:following", "read:messaging", "write:messaging", "read:mutes", "write:mutes", "write:notes", "read:notifications", "write:notifications", "read:reactions", "write:reactions", "write:votes", "read:pages", "write:pages", "write:page-likes", "read:page-likes", "read:user-groups", "write:user-groups", "read:channels", "write:channels", "read:gallery", "write:gallery", "read:gallery-likes", "write:gallery-likes", "read:flash", "write:flash", "read:flash-likes", "write:flash-likes", "read:admin:abuse-user-reports", "write:admin:delete-account", "write:admin:delete-all-files-of-a-user", "read:admin:index-stats", "read:admin:table-stats", "read:admin:user-ips", "read:admin:meta", "write:admin:reset-password", "write:admin:resolve-abuse-user-report", "write:admin:send-email", "read:admin:server-info", "read:admin:show-moderation-log", "read:admin:show-user", "write:admin:suspend-user", "write:admin:unset-user-avatar", "write:admin:unset-user-banner", "write:admin:unsuspend-user", "write:admin:meta", "write:admin:user-note", "write:admin:roles", "read:admin:roles", "write:admin:relays", "read:admin:relays", "write:admin:invite-codes", "read:admin:invite-codes", "write:admin:announcements", "read:admin:announcements", "write:admin:avatar-decorations", "read:admin:avatar-decorations", "write:admin:federation", "write:admin:account", "read:admin:account", "write:admin:emoji", "read:admin:emoji", "write:admin:queue", "read:admin:queue", "write:admin:promo", "write:admin:drive", "read:admin:drive", "write:admin:ad", "read:admin:ad", "write:invite-codes", "read:invite-codes", "write:clip-favorite", "read:clip-favorite", "read:federation", "write:report-abuse"];
@ -3265,7 +3281,7 @@ type UsersUpdateMemoRequest = operations['users___update-memo']['requestBody']['
// Warnings were encountered during analysis: // Warnings were encountered during analysis:
// //
// src/entities.ts:25:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts // src/entities.ts:34:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
// (No @packageDocumentation comment for this package) // (No @packageDocumentation comment for this package)

View File

@ -1,7 +1,8 @@
import { Endpoints as Gen } from './autogen/endpoint.js'; import { Endpoints as Gen } from './autogen/endpoint.js';
import { UserDetailed } from './autogen/models.js'; import { UserDetailed } from './autogen/models.js';
import { UsersShowRequest } from './autogen/entities.js'; import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js';
import { import {
PartialRolePolicyOverride,
SigninRequest, SigninRequest,
SigninResponse, SigninResponse,
SignupPendingRequest, SignupPendingRequest,
@ -79,5 +80,9 @@ export type Endpoints = Overwrite<
req: SigninRequest; req: SigninRequest;
res: SigninResponse; res: SigninResponse;
}, },
'admin/roles/create': {
req: Overwrite<AdminRolesCreateRequest, { policies: PartialRolePolicyOverride }>;
res: AdminRolesCreateResponse;
}
} }
> >

View File

@ -1,5 +1,14 @@
import { ModerationLogPayloads } from './consts.js'; import { ModerationLogPayloads } from './consts.js';
import { Announcement, EmojiDetailed, MeDetailed, Page, User, UserDetailedNotMe } from './autogen/models.js'; import {
Announcement,
EmojiDetailed,
MeDetailed,
Page,
Role,
RolePolicies,
User,
UserDetailedNotMe
} from './autogen/models.js';
export * from './autogen/entities.js'; export * from './autogen/entities.js';
export * from './autogen/models.js'; export * from './autogen/models.js';
@ -236,3 +245,7 @@ export type SigninResponse = {
id: User['id'], id: User['id'],
i: string, i: string,
}; };
type Values<T extends Record<PropertyKey, unknown>> = T[keyof T];
export type PartialRolePolicyOverride = Partial<{[k in keyof RolePolicies]: Omit<Values<Role['policies']>, 'value'> & { value: RolePolicies[k] }}>;

View File

@ -259,4 +259,42 @@ describe('API', () => {
expect(isAPIError(e)).toEqual(false); expect(isAPIError(e)).toEqual(false);
} }
}); });
test('admin/roles/create の型が合う', async() => {
fetchMock.resetMocks();
fetchMock.mockResponse(async () => {
return {
// 本来返すべき値は`Role`型だが、テストなのでお茶を濁す
status: 200,
body: '{}'
};
});
const cli = new APIClient({
origin: 'https://misskey.test',
credential: 'TOKEN',
});
await cli.request('admin/roles/create', {
name: 'aaa',
asBadge: false,
canEditMembersByModerator: false,
color: '#123456',
condFormula: {},
description: '',
displayOrder: 0,
iconUrl: '',
isAdministrator: false,
isExplorable: false,
isModerator: false,
isPublic: false,
policies: {
ltlAvailable: {
value: true,
priority: 0,
useDefault: false,
},
},
target: 'manual',
});
})
}); });