mirror of
https://github.com/paricafe/misskey.git
synced 2025-04-12 09:59:36 -05:00
Merge branch 'develop' into pari
This commit is contained in:
commit
3f7cd4a596
33 changed files with 353 additions and 248 deletions
.devcontainer
.github/workflows
CHANGELOG.mdlocales
packages
|
@ -7,7 +7,9 @@
|
|||
"ghcr.io/devcontainers/features/node:1": {
|
||||
"version": "22.11.0"
|
||||
},
|
||||
"ghcr.io/devcontainers-contrib/features/corepack:1": {}
|
||||
"ghcr.io/devcontainers-extra/features/corepack:1": {
|
||||
"version": "0.31.0"
|
||||
}
|
||||
},
|
||||
"forwardPorts": [3000],
|
||||
"postCreateCommand": "/bin/bash .devcontainer/init.sh",
|
||||
|
|
36
.github/workflows/ok-to-test.yml
vendored
36
.github/workflows/ok-to-test.yml
vendored
|
@ -1,36 +0,0 @@
|
|||
# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event
|
||||
name: Ok To Test
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
ok-to-test:
|
||||
runs-on: ubuntu-latest
|
||||
# Only run for PRs, not issue comments
|
||||
if: ${{ github.event.issue.pull_request }}
|
||||
steps:
|
||||
# Generate a GitHub App installation access token from an App ID and private key
|
||||
# To create a new GitHub App:
|
||||
# https://developer.github.com/apps/building-github-apps/creating-a-github-app/
|
||||
# See app.yml for an example app manifest
|
||||
- name: Generate token
|
||||
id: generate_token
|
||||
uses: tibdex/github-app-token@v2
|
||||
with:
|
||||
app_id: ${{ secrets.DEPLOYBOT_APP_ID }}
|
||||
private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }}
|
||||
|
||||
- name: Slash Command Dispatch
|
||||
uses: peter-evans/slash-command-dispatch@v4
|
||||
env:
|
||||
TOKEN: ${{ steps.generate_token.outputs.token }}
|
||||
with:
|
||||
token: ${{ env.TOKEN }} # GitHub App installation access token
|
||||
# token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work
|
||||
reaction-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
issue-type: pull-request
|
||||
commands: deploy
|
||||
named-args: true
|
||||
permission: write
|
92
.github/workflows/pr-preview-deploy.yml
vendored
92
.github/workflows/pr-preview-deploy.yml
vendored
|
@ -1,92 +0,0 @@
|
|||
# Run secret-dependent integration tests only after /deploy approval
|
||||
on:
|
||||
repository_dispatch:
|
||||
types: [deploy-command]
|
||||
|
||||
name: Deploy preview environment
|
||||
|
||||
jobs:
|
||||
# Repo owner has commented /deploy on a (fork-based) pull request
|
||||
deploy-preview-environment:
|
||||
runs-on: ubuntu-latest
|
||||
if:
|
||||
github.event.client_payload.slash_command.sha != '' &&
|
||||
contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
|
||||
steps:
|
||||
- uses: actions/github-script@v7.0.1
|
||||
id: check-id
|
||||
env:
|
||||
number: ${{ github.event.client_payload.pull_request.number }}
|
||||
job: ${{ github.job }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-encoding: string
|
||||
script: |
|
||||
const { data: pull } = await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number: process.env.number
|
||||
});
|
||||
const ref = pull.head.sha;
|
||||
|
||||
const { data: checks } = await github.rest.checks.listForRef({
|
||||
...context.repo,
|
||||
ref
|
||||
});
|
||||
|
||||
const check = checks.check_runs.filter(c => c.name === process.env.job);
|
||||
|
||||
return check[0].id;
|
||||
|
||||
- uses: actions/github-script@v7.0.1
|
||||
env:
|
||||
check_id: ${{ steps.check-id.outputs.result }}
|
||||
details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
await github.rest.checks.update({
|
||||
...context.repo,
|
||||
check_run_id: process.env.check_id,
|
||||
status: 'in_progress',
|
||||
details_url: process.env.details_url
|
||||
});
|
||||
|
||||
# Check out merge commit
|
||||
- name: Fork based /deploy checkout
|
||||
uses: actions/checkout@v4.1.1
|
||||
with:
|
||||
ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
|
||||
|
||||
# <insert integration tests needing secrets>
|
||||
- name: Context
|
||||
uses: okteto/context@latest
|
||||
with:
|
||||
token: ${{ secrets.OKTETO_TOKEN }}
|
||||
|
||||
- name: Deploy preview environment
|
||||
uses: ikuradon/deploy-preview@latest
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
with:
|
||||
name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo
|
||||
timeout: 15m
|
||||
|
||||
# Update check run called "integration-fork"
|
||||
- uses: actions/github-script@v7.0.1
|
||||
id: update-check-run
|
||||
if: ${{ always() }}
|
||||
env:
|
||||
# Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
|
||||
conclusion: ${{ job.status }}
|
||||
check_id: ${{ steps.check-id.outputs.result }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
script: |
|
||||
const { data: result } = await github.rest.checks.update({
|
||||
...context.repo,
|
||||
check_run_id: process.env.check_id,
|
||||
status: 'completed',
|
||||
conclusion: process.env.conclusion
|
||||
});
|
||||
|
||||
return result;
|
54
.github/workflows/pr-preview-destroy.yml
vendored
54
.github/workflows/pr-preview-destroy.yml
vendored
|
@ -1,54 +0,0 @@
|
|||
# file: .github/workflows/preview-closed.yaml
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- closed
|
||||
|
||||
name: Destroy preview environment
|
||||
|
||||
jobs:
|
||||
destroy-preview-environment:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/github-script@v7.0.1
|
||||
id: check-conclusion
|
||||
env:
|
||||
number: ${{ github.event.number }}
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
result-encoding: string
|
||||
script: |
|
||||
const { data: pull } = await github.rest.pulls.get({
|
||||
...context.repo,
|
||||
pull_number: process.env.number
|
||||
});
|
||||
const ref = pull.head.sha;
|
||||
|
||||
const { data: checks } = await github.rest.checks.listForRef({
|
||||
...context.repo,
|
||||
ref
|
||||
});
|
||||
|
||||
const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment');
|
||||
|
||||
if (check.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { data: result } = await github.rest.checks.get({
|
||||
...context.repo,
|
||||
check_run_id: check[0].id,
|
||||
});
|
||||
|
||||
return result.conclusion;
|
||||
- name: Context
|
||||
if: steps.check-conclusion.outputs.result == 'success'
|
||||
uses: okteto/context@latest
|
||||
with:
|
||||
token: ${{ secrets.OKTETO_TOKEN }}
|
||||
|
||||
- name: Destroy preview environment
|
||||
if: steps.check-conclusion.outputs.result == 'success'
|
||||
uses: okteto/destroy-preview@latest
|
||||
with:
|
||||
name: pr-${{ github.event.number }}-syuilo
|
|
@ -4,10 +4,12 @@
|
|||
-
|
||||
|
||||
### Client
|
||||
-
|
||||
- Enhance: 投稿フォームの「迷惑になる可能性があります」のダイアログを表示する条件においてCWを考慮するように
|
||||
- Enhance: アンテナ、リスト等の名前をカラム名のデフォルト値にするように `#13992`
|
||||
- Enhance: クライアントエラー画面の多言語対応
|
||||
|
||||
### Server
|
||||
-
|
||||
- Fix: `following/invalidate`でフォロワーを解除しようとしているユーザーの情報を返すように
|
||||
|
||||
|
||||
## 2025.2.0
|
||||
|
@ -45,6 +47,7 @@
|
|||
* β版として公開のため、旧画面も引き続き利用可能です
|
||||
|
||||
### Client
|
||||
- Feat: 投稿フォームで画像をプレビュー可能に
|
||||
- Enhance: PC画面でチャンネルが複数列で表示されるように
|
||||
(Cherry-picked from https://github.com/Otaku-Social/maniakey/pull/13)
|
||||
- Enhance: 照会に失敗した場合、その理由を表示するように
|
||||
|
|
46
locales/index.d.ts
vendored
46
locales/index.d.ts
vendored
|
@ -10969,6 +10969,52 @@ export interface Locale extends ILocale {
|
|||
};
|
||||
};
|
||||
};
|
||||
"_bootErrors": {
|
||||
/**
|
||||
* 読み込みに失敗しました
|
||||
*/
|
||||
"title": string;
|
||||
/**
|
||||
* 少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。
|
||||
*/
|
||||
"serverError": string;
|
||||
/**
|
||||
* 以下を行うと解決する可能性があります。
|
||||
*/
|
||||
"solution": string;
|
||||
/**
|
||||
* ブラウザおよびOSを最新バージョンに更新する
|
||||
*/
|
||||
"solution1": string;
|
||||
/**
|
||||
* アドブロッカーを無効にする
|
||||
*/
|
||||
"solution2": string;
|
||||
/**
|
||||
* ブラウザのキャッシュをクリアする
|
||||
*/
|
||||
"solution3": string;
|
||||
/**
|
||||
* (Tor Browser) dom.webaudio.enabledをtrueに設定する
|
||||
*/
|
||||
"solution4": string;
|
||||
/**
|
||||
* その他のオプション
|
||||
*/
|
||||
"otherOption": string;
|
||||
/**
|
||||
* クライアント設定とキャッシュを削除
|
||||
*/
|
||||
"otherOption1": string;
|
||||
/**
|
||||
* 簡易クライアントを起動
|
||||
*/
|
||||
"otherOption2": string;
|
||||
/**
|
||||
* 修復ツールを起動
|
||||
*/
|
||||
"otherOption3": string;
|
||||
};
|
||||
}
|
||||
declare const locales: {
|
||||
[lang: string]: Locale;
|
||||
|
|
|
@ -2979,3 +2979,16 @@ _captcha:
|
|||
_unknown:
|
||||
title: "CAPTCHAエラー"
|
||||
text: "想定外のエラーが発生しました。"
|
||||
|
||||
_bootErrors:
|
||||
title: "読み込みに失敗しました"
|
||||
serverError: "少し待ってからリロードしてもまだ問題が解決されない場合、以下のError IDを添えてサーバー管理者に連絡してください。"
|
||||
solution: "以下を行うと解決する可能性があります。"
|
||||
solution1: "ブラウザおよびOSを最新バージョンに更新する"
|
||||
solution2: "アドブロッカーを無効にする"
|
||||
solution3: "ブラウザのキャッシュをクリアする"
|
||||
solution4: "(Tor Browser) dom.webaudio.enabledをtrueに設定する"
|
||||
otherOption: "その他のオプション"
|
||||
otherOption1: "クライアント設定とキャッシュを削除"
|
||||
otherOption2: "簡易クライアントを起動"
|
||||
otherOption3: "修復ツールを起動"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"$schema": "https://swc.rs/schema.json",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
|
|
|
@ -96,7 +96,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
|||
|
||||
await this.userFollowingService.unfollow(follower, followee);
|
||||
|
||||
return await this.userEntityService.pack(followee.id, me);
|
||||
return await this.userEntityService.pack(follower.id, me);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -210,9 +210,15 @@ export function genOpenapiSpec(config: Config, includeSelfRef = false) {
|
|||
|
||||
spec.paths['/' + endpoint.name] = {
|
||||
...(endpoint.meta.allowGet ? {
|
||||
get: info,
|
||||
get: {
|
||||
...info,
|
||||
operationId: 'get___' + info.operationId,
|
||||
},
|
||||
} : {}),
|
||||
post: info,
|
||||
post: {
|
||||
...info,
|
||||
operationId: 'post___' + info.operationId,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -114,13 +114,17 @@
|
|||
if (document.readyState === 'loading') {
|
||||
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
||||
}
|
||||
|
||||
const locale = JSON.parse(localStorage.getItem('locale') || '{}');
|
||||
|
||||
const title = locale?._bootErrors?.title || 'Failed to initialize Misskey';
|
||||
const reload = locale?.reload || 'Reload';
|
||||
|
||||
document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
|
||||
<div class="message">読み込みに失敗しました</div>
|
||||
<div class="submessage">Failed to initialize Misskey</div>
|
||||
<div class="message">${title}</div>
|
||||
<div class="submessage">Error Code: ${code}</div>
|
||||
<button onclick="location.reload(!0)">
|
||||
<div>リロード</div>
|
||||
<div><small>Reload</small></div>
|
||||
<div>${reload}</div>
|
||||
</button>`;
|
||||
addStyle(`
|
||||
#misskey_app,
|
||||
|
|
|
@ -156,6 +156,22 @@
|
|||
await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
|
||||
}
|
||||
|
||||
const locale = JSON.parse(localStorage.getItem('locale') || '{}');
|
||||
|
||||
const messages = Object.assign({
|
||||
title: 'Failed to initialize Misskey',
|
||||
solution: 'The following actions may solve the problem.',
|
||||
solution1: 'Update your os and browser',
|
||||
solution2: 'Disable an adblocker',
|
||||
solution3: 'Clear the browser cache',
|
||||
solution4: '(Tor Browser) Set dom.webaudio.enabled to true',
|
||||
otherOption: 'Other options',
|
||||
otherOption1: 'Clear preferences and cache',
|
||||
otherOption2: 'Start the simple client',
|
||||
otherOption3: 'Start the repair tool',
|
||||
}, locale?._bootErrors || {});
|
||||
const reload = locale?.reload || 'Reload';
|
||||
|
||||
let errorsElement = document.getElementById('errors');
|
||||
|
||||
if (!errorsElement) {
|
||||
|
@ -165,32 +181,32 @@
|
|||
<path d="M12 9v2m0 4v.01"></path>
|
||||
<path d="M5 19h14a2 2 0 0 0 1.84 -2.75l-7.1 -12.25a2 2 0 0 0 -3.5 0l-7.1 12.25a2 2 0 0 0 1.75 2.75"></path>
|
||||
</svg>
|
||||
<h1>Failed to load<br>加載失敗しました</h1>
|
||||
<h1>${messages.title}</h1>
|
||||
<button class="button-big" onclick="location.reload(true);">
|
||||
<span class="button-label-big">Reload / 重新加載</span>
|
||||
<span class="button-label-big">${reload}</span>
|
||||
</button>
|
||||
<p><b>The following actions may solve the problem. / 執行以下操作可能會有所幫助。</b></p>
|
||||
<p>Clear the browser cache / 清除瀏覽器緩存</p>
|
||||
<p>Update your os and browser / 將您的瀏覽器和操作系統更新到最新版本</p>
|
||||
<p>Disable an adblocker / 禁用廣告攔截器</p>
|
||||
<p>(Tor Browser) Set dom.webaudio.enabled to true / 將dom.webaudio.enabled設定為true</p>
|
||||
<p><b>${messages.solution}</b></p>
|
||||
<p>${messages.solution1}</p>
|
||||
<p>${messages.solution2}</p>
|
||||
<p>${messages.solution3}</p>
|
||||
<p>${messages.solution4}</p>
|
||||
<details style="color: #e1aac6;">
|
||||
<summary>Other options / 其他選項</summary>
|
||||
<summary>${messages.otherOption}</summary>
|
||||
<a href="/flush">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Clear preferences and cache</span>
|
||||
<span class="button-label-small">${messages.otherOption1}</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/cli">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Start the simple client</span>
|
||||
<span class="button-label-small">${messages.otherOption2}</span>
|
||||
</button>
|
||||
</a>
|
||||
<br>
|
||||
<a href="/bios">
|
||||
<button class="button-small">
|
||||
<span class="button-label-small">Start the repair tool</span>
|
||||
<span class="button-label-small">${messages.otherOption3}</span>
|
||||
</button>
|
||||
</a>
|
||||
</details>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"$schema": "https://swc.rs/schema.json",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
|
|
|
@ -414,6 +414,7 @@ function toStories(component: string): Promise<string> {
|
|||
glob('src/components/MkSignupServerRules.vue'),
|
||||
glob('src/components/MkUserSetupDialog.vue'),
|
||||
glob('src/components/MkUserSetupDialog.*.vue'),
|
||||
glob('src/components/MkImgPreviewDialog.vue'),
|
||||
glob('src/components/MkInstanceCardMini.vue'),
|
||||
glob('src/components/MkInviteCode.vue'),
|
||||
glob('src/components/MkTagItem.vue'),
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { file } from '../../.storybook/fakes.js';
|
||||
import MkImgPreviewDialog from './MkImgPreviewDialog.vue';
|
||||
export const Default = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkImgPreviewDialog,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkImgPreviewDialog v-bind="props" />',
|
||||
};
|
||||
},
|
||||
args: {
|
||||
file: file(),
|
||||
},
|
||||
parameters: {
|
||||
chromatic: {
|
||||
// NOTE: ロードが終わるまで待つ
|
||||
delay: 3000,
|
||||
},
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkImgPreviewDialog>;
|
58
packages/frontend/src/components/MkImgPreviewDialog.vue
Normal file
58
packages/frontend/src/components/MkImgPreviewDialog.vue
Normal file
|
@ -0,0 +1,58 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<MkModalWindow
|
||||
ref="modal"
|
||||
:width="1800"
|
||||
:height="900"
|
||||
@close="close"
|
||||
@esc="close"
|
||||
@click="close"
|
||||
>
|
||||
<template #header>{{ file.name }}</template>
|
||||
<div :class="$style.container">
|
||||
<img :src="file.url" :alt="file.comment ?? file.name" :class="$style.img"/>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
<script lang="ts" setup>
|
||||
import { defineProps, ref } from 'vue';
|
||||
import MkModalWindow from './MkModalWindow.vue';
|
||||
import type * as Misskey from 'misskey-js';
|
||||
|
||||
defineProps<{
|
||||
file: Misskey.entities.DriveFile;
|
||||
}>();
|
||||
|
||||
const modal = ref<typeof MkModalWindow | null>(null);
|
||||
|
||||
function close() {
|
||||
modal.value?.close();
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss" module>
|
||||
.container {
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
|
||||
background-color: var(--MI_THEME-bg);
|
||||
background-size: auto auto;
|
||||
background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
|
||||
}
|
||||
|
||||
.img {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
</style>
|
|
@ -808,6 +808,14 @@ function deleteDraft() {
|
|||
miLocalStorage.setItem('drafts', JSON.stringify(draftData));
|
||||
}
|
||||
|
||||
function isAnnoying(text: string): boolean {
|
||||
return text.includes('$[x2') ||
|
||||
text.includes('$[x3') ||
|
||||
text.includes('$[x4') ||
|
||||
text.includes('$[scale') ||
|
||||
text.includes('$[position');
|
||||
}
|
||||
|
||||
async function post(ev?: MouseEvent) {
|
||||
if (useCw.value && (cw.value == null || cw.value.trim() === '')) {
|
||||
os.alert({
|
||||
|
@ -832,14 +840,10 @@ async function post(ev?: MouseEvent) {
|
|||
|
||||
if (props.mock) return;
|
||||
|
||||
const annoying =
|
||||
text.value.includes('$[x2') ||
|
||||
text.value.includes('$[x3') ||
|
||||
text.value.includes('$[x4') ||
|
||||
text.value.includes('$[scale') ||
|
||||
text.value.includes('$[position');
|
||||
|
||||
if (annoying && visibility.value === 'public') {
|
||||
if (visibility.value === 'public' && (
|
||||
(useCw.value && cw.value != null && cw.value.trim() !== '' && isAnnoying(cw.value)) || // CWが迷惑になる場合
|
||||
((!useCw.value || cw.value == null || cw.value.trim() === '') && text.value != null && text.value.trim() !== '' && isAnnoying(text.value)) // CWが無い かつ 本文が迷惑になる場合
|
||||
)) {
|
||||
const { canceled, result } = await os.actions({
|
||||
type: 'warning',
|
||||
text: i18n.ts.thisPostMayBeAnnoying,
|
||||
|
|
|
@ -22,20 +22,24 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
<p :class="[$style.remain, {
|
||||
[$style.exceeded]: props.modelValue.length > 16,
|
||||
}]">{{ 16 - props.modelValue.length }}/16</p>
|
||||
<p
|
||||
:class="[$style.remain, {
|
||||
[$style.exceeded]: props.modelValue.length > 16,
|
||||
}]"
|
||||
>
|
||||
{{ 16 - props.modelValue.length }}/16
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent, inject } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import type { MenuItem } from '@/types/menu';
|
||||
import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue';
|
||||
import * as os from '@/os.js';
|
||||
import { misskeyApi } from '@/scripts/misskey-api.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import type { MenuItem } from '@/types/menu.js';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
|
@ -168,6 +172,14 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
|
|||
text: i18n.ts.cropImage,
|
||||
icon: 'ti ti-crop',
|
||||
action: () : void => { crop(file); },
|
||||
}, {
|
||||
text: i18n.ts.preview,
|
||||
icon: 'ti ti-photo-search',
|
||||
action: () => {
|
||||
os.popup(defineAsyncComponent(() => import('@/components/MkImgPreviewDialog.vue')), {
|
||||
file: file,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -310,6 +310,21 @@ export function inputText(props: {
|
|||
} | {
|
||||
canceled: false; result: string;
|
||||
}>;
|
||||
// min lengthが指定されてたら result は null になり得ないことを保証する overload function
|
||||
export function inputText(props: {
|
||||
type?: 'text' | 'email' | 'password' | 'url';
|
||||
title?: string;
|
||||
text?: string;
|
||||
placeholder?: string | null;
|
||||
autocomplete?: string;
|
||||
default?: string;
|
||||
minLength: number;
|
||||
maxLength?: number;
|
||||
}): Promise<{
|
||||
canceled: true; result: undefined;
|
||||
} | {
|
||||
canceled: false; result: string;
|
||||
}>;
|
||||
export function inputText(props: {
|
||||
type?: 'text' | 'email' | 'password' | 'url';
|
||||
title?: string;
|
||||
|
|
|
@ -193,7 +193,7 @@ const addColumn = async (ev) => {
|
|||
addColumnToStore({
|
||||
type: column,
|
||||
id: uuid(),
|
||||
name: i18n.ts._deck._columns[column],
|
||||
name: null,
|
||||
width: 330,
|
||||
soundSetting: { type: null, volume: 1 },
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
|
||||
<template #header>
|
||||
<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name || antennaName || i18n.ts._deck._columns.antenna }}</span>
|
||||
</template>
|
||||
|
||||
<MkTimeline v-if="column.antennaId" ref="timeline" src="antenna" :antenna="column.antennaId" @note="onNote"/>
|
||||
|
@ -36,6 +36,7 @@ const props = defineProps<{
|
|||
|
||||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
|
||||
const antennaName = ref<string | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.column.antennaId == null) {
|
||||
|
@ -43,6 +44,13 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
|
||||
watch([() => props.column.name, () => props.column.antennaId], () => {
|
||||
if (!props.column.name && props.column.antennaId) {
|
||||
misskeyApi('antennas/show', { antennaId: props.column.antennaId })
|
||||
.then(value => antennaName.value = value.name);
|
||||
}
|
||||
});
|
||||
|
||||
watch(soundSetting, v => {
|
||||
updateColumn(props.column.id, { soundSetting: v });
|
||||
});
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
|
||||
<template #header>
|
||||
<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name || channel?.name || i18n.ts._deck._columns.channel }}</span>
|
||||
</template>
|
||||
|
||||
<template v-if="column.channelId">
|
||||
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, shallowRef, watch } from 'vue';
|
||||
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn } from './deck-store.js';
|
||||
|
@ -44,9 +44,18 @@ const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
|||
const channel = shallowRef<Misskey.entities.Channel>();
|
||||
const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
|
||||
|
||||
if (props.column.channelId == null) {
|
||||
setChannel();
|
||||
}
|
||||
onMounted(() => {
|
||||
if (props.column.channelId == null) {
|
||||
setChannel();
|
||||
}
|
||||
});
|
||||
|
||||
watch([() => props.column.name, () => props.column.channelId], () => {
|
||||
if (!props.column.name && props.column.channelId) {
|
||||
misskeyApi('channels/show', { channelId: props.column.channelId })
|
||||
.then(value => channel.value = value);
|
||||
}
|
||||
});
|
||||
|
||||
watch(soundSetting, v => {
|
||||
updateColumn(props.column.id, { soundSetting: v });
|
||||
|
|
|
@ -129,7 +129,8 @@ function getMenu() {
|
|||
icon: 'ti ti-settings',
|
||||
text: i18n.ts._deck.configureColumn,
|
||||
action: async () => {
|
||||
const { canceled, result } = await os.form(props.column.name, {
|
||||
const name = props.column.name ?? i18n.ts._deck._columns[props.column.type];
|
||||
const { canceled, result } = await os.form(name, {
|
||||
name: {
|
||||
type: 'string',
|
||||
label: i18n.ts.name,
|
||||
|
@ -144,7 +145,7 @@ function getMenu() {
|
|||
flexible: {
|
||||
type: 'boolean',
|
||||
label: i18n.ts._deck.flexible,
|
||||
default: props.column.flexible,
|
||||
default: props.column.flexible ?? null,
|
||||
},
|
||||
});
|
||||
if (canceled) return;
|
||||
|
|
|
@ -51,7 +51,7 @@ export type Column = {
|
|||
withReplies?: boolean;
|
||||
withSensitive?: boolean;
|
||||
onlyFiles?: boolean;
|
||||
soundSetting: SoundStore;
|
||||
soundSetting?: SoundStore;
|
||||
};
|
||||
|
||||
export const deckStore = markRaw(new Storage('deck', {
|
||||
|
@ -94,7 +94,7 @@ export const loadDeck = async () => {
|
|||
key: deckStore.state.profile,
|
||||
});
|
||||
} catch (err) {
|
||||
if (err.code === 'NO_SUCH_KEY') {
|
||||
if (typeof err === 'object' && err != null && 'code' in err && err.code === 'NO_SUCH_KEY') {
|
||||
// 後方互換性のため
|
||||
if (deckStore.state.profile === 'default') {
|
||||
saveDeck();
|
||||
|
@ -180,6 +180,7 @@ export function swapLeftColumn(id: Column['id']) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
@ -196,6 +197,7 @@ export function swapRightColumn(id: Column['id']) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
@ -216,6 +218,7 @@ export function swapUpColumn(id: Column['id']) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
@ -236,6 +239,7 @@ export function swapDownColumn(id: Column['id']) {
|
|||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
saveDeck();
|
||||
}
|
||||
|
@ -286,7 +290,8 @@ export function removeColumnWidget(id: Column['id'], widget: ColumnWidget) {
|
|||
const columns = deepClone(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (column == null || column.widgets == null) return;
|
||||
if (column == null) return;
|
||||
if (column.widgets == null) column.widgets = [];
|
||||
column.widgets = column.widgets.filter(w => w.id !== widget.id);
|
||||
columns[columnIndex] = column;
|
||||
deckStore.set('columns', columns);
|
||||
|
@ -308,7 +313,8 @@ export function updateColumnWidget(id: Column['id'], widgetId: string, widgetDat
|
|||
const columns = deepClone(deckStore.state.columns);
|
||||
const columnIndex = deckStore.state.columns.findIndex(c => c.id === id);
|
||||
const column = deepClone(deckStore.state.columns[columnIndex]);
|
||||
if (column == null || column.widgets == null) return;
|
||||
if (column == null) return;
|
||||
if (column.widgets == null) column.widgets = [];
|
||||
column.widgets = column.widgets.map(w => w.id === widgetId ? {
|
||||
...w,
|
||||
data: widgetData,
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
|
||||
<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.direct }}</template>
|
||||
|
||||
<MkNotes ref="tlComponent" :pagination="pagination"/>
|
||||
</XColumn>
|
||||
|
@ -16,6 +16,7 @@ import { ref } from 'vue';
|
|||
import XColumn from './column.vue';
|
||||
import type { Column } from './deck-store.js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
|
||||
defineProps<{
|
||||
column: Column;
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
|
||||
<template #header>
|
||||
<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ (column.name || listName) ?? i18n.ts._deck._columns.list }}</span>
|
||||
</template>
|
||||
|
||||
<MkTimeline v-if="column.listId" ref="timeline" src="list" :list="column.listId" :withRenotes="withRenotes" @note="onNote"/>
|
||||
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { watch, shallowRef, ref } from 'vue';
|
||||
import { watch, shallowRef, ref, onMounted } from 'vue';
|
||||
import type { entities as MisskeyEntities } from 'misskey-js';
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn } from './deck-store.js';
|
||||
|
@ -37,10 +37,20 @@ const props = defineProps<{
|
|||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
const withRenotes = ref(props.column.withRenotes ?? true);
|
||||
const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
|
||||
const listName = ref<string | null>(null);
|
||||
|
||||
if (props.column.listId == null) {
|
||||
setList();
|
||||
}
|
||||
onMounted(() => {
|
||||
if (props.column.listId == null) {
|
||||
setList();
|
||||
}
|
||||
});
|
||||
|
||||
watch([() => props.column.name, () => props.column.listId], () => {
|
||||
if (!props.column.name && props.column.listId) {
|
||||
misskeyApi('users/lists/show', { listId: props.column.listId })
|
||||
.then(value => listName.value = value.name);
|
||||
}
|
||||
});
|
||||
|
||||
watch(withRenotes, v => {
|
||||
updateColumn(props.column.id, {
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
|
||||
<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.mentions }}</template>
|
||||
|
||||
<MkNotes ref="tlComponent" :pagination="pagination"/>
|
||||
</XColumn>
|
||||
|
@ -16,6 +16,7 @@ import { ref } from 'vue';
|
|||
import XColumn from './column.vue';
|
||||
import type { Column } from './deck-store.js';
|
||||
import MkNotes from '@/components/MkNotes.vue';
|
||||
import { i18n } from '../../i18n.js';
|
||||
|
||||
defineProps<{
|
||||
column: Column;
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="async () => { await notificationsComponent?.reload() }">
|
||||
<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns.notifications }}</template>
|
||||
|
||||
<XNotifications ref="notificationsComponent" :excludeTypes="props.column.excludeTypes"/>
|
||||
</XColumn>
|
||||
|
|
|
@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template>
|
||||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
|
||||
<template #header>
|
||||
<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name || roleName || i18n.ts._deck._columns.roleTimeline }}</span>
|
||||
</template>
|
||||
|
||||
<MkTimeline v-if="column.roleId" ref="timeline" src="role" :role="column.roleId" @note="onNote"/>
|
||||
|
@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
||||
import XColumn from './column.vue';
|
||||
import { updateColumn } from './deck-store.js';
|
||||
import type { Column } from './deck-store.js';
|
||||
|
@ -34,6 +34,7 @@ const props = defineProps<{
|
|||
|
||||
const timeline = shallowRef<InstanceType<typeof MkTimeline>>();
|
||||
const soundSetting = ref<SoundStore>(props.column.soundSetting ?? { type: null, volume: 1 });
|
||||
const roleName = ref<string | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
if (props.column.roleId == null) {
|
||||
|
@ -41,6 +42,13 @@ onMounted(() => {
|
|||
}
|
||||
});
|
||||
|
||||
watch([() => props.column.name, () => props.column.roleId], () => {
|
||||
if (!props.column.name && props.column.roleId) {
|
||||
misskeyApi('roles/show', { roleId: props.column.roleId })
|
||||
.then(value => roleName.value = value.name);
|
||||
}
|
||||
});
|
||||
|
||||
watch(soundSetting, v => {
|
||||
updateColumn(props.column.id, { soundSetting: v });
|
||||
});
|
||||
|
|
|
@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="async () => { await timeline?.reloadTimeline() }">
|
||||
<template #header>
|
||||
<i v-if="column.tl != null" :class="basicTimelineIconClass(column.tl)"/>
|
||||
<span style="margin-left: 8px;">{{ column.name }}</span>
|
||||
<span style="margin-left: 8px;">{{ column.name || (column.tl ? i18n.ts._timelines[column.tl] : null) || i18n.ts._deck._columns.tl }}</span>
|
||||
</template>
|
||||
|
||||
<div v-if="!isAvailableBasicTimeline(column.tl)" :class="$style.disabled">
|
||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
|
||||
<template>
|
||||
<XColumn :menu="menu" :naked="true" :column="column" :isStacked="isStacked">
|
||||
<template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name }}</template>
|
||||
<template #header><i class="ti ti-apps" style="margin-right: 8px;"></i>{{ column.name || i18n.ts._deck._columns[props.column.type] }}</template>
|
||||
|
||||
<div :class="$style.root">
|
||||
<div v-if="!(column.widgets && column.widgets.length > 0) && !edit" :class="$style.intro">{{ i18n.ts._deck.widgetsIntroduction }}</div>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"$schema": "https://swc.rs/schema.json",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import { mkdir, writeFile } from 'fs/promises';
|
||||
import assert from 'assert';
|
||||
import { mkdir, readFile, writeFile } from 'fs/promises';
|
||||
import { OpenAPIV3_1 } from 'openapi-types';
|
||||
import { toPascal } from 'ts-case-convert';
|
||||
import OpenAPIParser from '@readme/openapi-parser';
|
||||
import openapiTS from 'openapi-typescript';
|
||||
import openapiTS, { OpenAPI3, OperationObject, PathItemObject } from 'openapi-typescript';
|
||||
|
||||
async function generateBaseTypes(
|
||||
openApiDocs: OpenAPIV3_1.Document,
|
||||
|
@ -20,7 +21,29 @@ async function generateBaseTypes(
|
|||
}
|
||||
lines.push('');
|
||||
|
||||
const generatedTypes = await openapiTS(openApiJsonPath, {
|
||||
// NOTE: Align `operationId` of GET and POST to avoid duplication of type definitions
|
||||
const openApi = JSON.parse(await readFile(openApiJsonPath, 'utf8')) as OpenAPI3;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
for (const [key, item] of Object.entries(openApi.paths!)) {
|
||||
assert('post' in item);
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
openApi.paths![key] = {
|
||||
...('get' in item ? {
|
||||
get: {
|
||||
...item.get,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
operationId: ((item as PathItemObject).get as OperationObject).operationId!.replaceAll('get___', ''),
|
||||
},
|
||||
} : {}),
|
||||
post: {
|
||||
...item.post,
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
operationId: ((item as PathItemObject).post as OperationObject).operationId!.replaceAll('post___', ''),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const generatedTypes = await openapiTS(openApi, {
|
||||
exportType: true,
|
||||
transform(schemaObject) {
|
||||
if ('format' in schemaObject && schemaObject.format === 'binary') {
|
||||
|
@ -78,7 +101,7 @@ async function generateEndpoints(
|
|||
for (const operation of postPathItems) {
|
||||
const path = operation._path_;
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const operationId = operation.operationId!;
|
||||
const operationId = operation.operationId!.replaceAll('get___', '').replaceAll('post___', '');
|
||||
const endpoint = new Endpoint(path);
|
||||
endpoints.push(endpoint);
|
||||
|
||||
|
@ -195,7 +218,7 @@ async function generateApiClientJSDoc(
|
|||
|
||||
for (const operation of postPathItems) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const operationId = operation.operationId!;
|
||||
const operationId = operation.operationId!.replaceAll('get___', '').replaceAll('post___', '');
|
||||
|
||||
if (operation.description) {
|
||||
endpoints.push({
|
||||
|
|
Loading…
Add table
Reference in a new issue