mirror of
https://github.com/misskey-dev/misskey.git
synced 2026-06-22 18:54:45 +02:00
Compare commits
12 Commits
2026.6.0
...
clean-pref
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
827c7a98a2 | ||
|
|
d54b948085 | ||
|
|
694e58ce28 | ||
|
|
f5806a0560 | ||
|
|
5d8c31b6e5 | ||
|
|
7af729deff | ||
|
|
fff87f6604 | ||
|
|
2e5e55e8e5 | ||
|
|
7a3e03411f | ||
|
|
6d89d479e2 | ||
|
|
ab73b8abe3 | ||
|
|
3b2a3f2c57 |
135
.github/scripts/frontend-js-size.mjs
vendored
135
.github/scripts/frontend-js-size.mjs
vendored
@@ -189,6 +189,13 @@ function removedKeys(before, after) {
|
||||
.filter((key) => after.chunks[key] == null);
|
||||
}
|
||||
|
||||
function rowChangeType(beforeEntry, afterEntry, beforeSize, afterSize) {
|
||||
if (beforeEntry == null) return 'added';
|
||||
if (afterEntry == null) return 'removed';
|
||||
if (beforeSize !== afterSize) return 'updated';
|
||||
return 'unchanged';
|
||||
}
|
||||
|
||||
function getChunkComparisonRows(keys, before, after) {
|
||||
return keys.map((key) => {
|
||||
const beforeEntry = before.chunks[key];
|
||||
@@ -201,11 +208,31 @@ function getChunkComparisonRows(keys, before, after) {
|
||||
chunkFile: beforeEntry?.file ?? afterEntry?.file,
|
||||
beforeSize,
|
||||
afterSize,
|
||||
changeType: rowChangeType(beforeEntry, afterEntry, beforeSize, afterSize),
|
||||
sortSize: Math.max(beforeSize, afterSize),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function summarizeChanges(rows) {
|
||||
return {
|
||||
updated: rows.filter((row) => row.changeType === 'updated').length,
|
||||
added: rows.filter((row) => row.changeType === 'added').length,
|
||||
removed: rows.filter((row) => row.changeType === 'removed').length,
|
||||
};
|
||||
}
|
||||
|
||||
function formatChangeSummary(label, summary) {
|
||||
return `${label} (${summary.updated} updated, ${summary.added} added, ${summary.removed} removed)`;
|
||||
}
|
||||
|
||||
function compareComparisonRows(a, b) {
|
||||
return Math.abs(b.afterSize - b.beforeSize) - Math.abs(a.afterSize - a.beforeSize)
|
||||
|| (b.afterSize - b.beforeSize) - (a.afterSize - a.beforeSize)
|
||||
|| b.sortSize - a.sortSize
|
||||
|| a.name.localeCompare(b.name);
|
||||
}
|
||||
|
||||
function markdownTable(rows, total) {
|
||||
if (rows.length === 0) return '_No data_';
|
||||
|
||||
@@ -218,32 +245,13 @@ function markdownTable(rows, total) {
|
||||
lines.push('| | | | | |');
|
||||
}
|
||||
for (const row of rows) {
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | ${formatDiffPercent(row.beforeSize, row.afterSize)} |`);
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
|
||||
function chunkRows(keys, report) {
|
||||
return keys.map((key) => {
|
||||
const entry = report.chunks[key];
|
||||
return {
|
||||
key,
|
||||
name: entryDisplayName(entry),
|
||||
chunkFile: entry.file,
|
||||
size: entry.size,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function markdownChunkTable(rows) {
|
||||
if (rows.length === 0) return '_No data_';
|
||||
|
||||
const lines = [
|
||||
'| Chunk | Size |',
|
||||
'| --- | ---: |',
|
||||
];
|
||||
for (const row of rows) {
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.size)} |`);
|
||||
if (row.changeType === 'added') {
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | $\\color{orange}{\\text{(+)}}$ |`);
|
||||
} else if (row.changeType === 'removed') {
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | $\\color{green}{\\text{(-)}}$ |`);
|
||||
} else {
|
||||
lines.push(`| <details><summary>\`${escapeCell(row.name)}\`</summary> \`${escapeCell(row.chunkFile)}\` </details> | ${formatBytes(row.beforeSize)} | ${formatBytes(row.afterSize)} | ${formatDiff(row.afterSize - row.beforeSize)} | ${formatDiffPercent(row.beforeSize, row.afterSize)} |`);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
@@ -258,26 +266,21 @@ const before = await collectReport(beforeDir);
|
||||
const after = await collectReport(afterDir);
|
||||
|
||||
const commonChunkKeys = commonKeys(before, after);
|
||||
const comparisonRows = getChunkComparisonRows(commonChunkKeys, before, after);
|
||||
|
||||
const diffRows = comparisonRows
|
||||
.filter((row) => row.beforeSize !== row.afterSize)
|
||||
.sort((a, b) => Math.abs(b.afterSize - b.beforeSize) - Math.abs(a.afterSize - a.beforeSize)
|
||||
|| (b.afterSize - b.beforeSize) - (a.afterSize - a.beforeSize)
|
||||
|| b.sortSize - a.sortSize
|
||||
|| a.name.localeCompare(b.name))
|
||||
.slice(0, 30);
|
||||
const allChunkKeys = [
|
||||
...commonChunkKeys,
|
||||
...addedKeys(before, after),
|
||||
...removedKeys(before, after),
|
||||
];
|
||||
//const comparisonRows = getChunkComparisonRows(commonChunkKeys, before, after);
|
||||
const allComparisonRows = getChunkComparisonRows(allChunkKeys, before, after);
|
||||
|
||||
const changedRows = allComparisonRows.filter((row) => row.changeType !== 'unchanged');
|
||||
const diffSummary = summarizeChanges(changedRows);
|
||||
const diffTotal = {
|
||||
beforeSize: comparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
|
||||
afterSize: comparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
|
||||
beforeSize: allComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
|
||||
afterSize: allComparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
|
||||
};
|
||||
|
||||
const addedRows = chunkRows(addedKeys(before, after), after)
|
||||
.sort((a, b) => b.size - a.size || a.name.localeCompare(b.name));
|
||||
|
||||
const removedRows = chunkRows(removedKeys(before, after), before)
|
||||
.sort((a, b) => b.size - a.size || a.name.localeCompare(b.name));
|
||||
const diffRows = changedRows.sort(compareComparisonRows).slice(0, 30); // TODO: 実際に30を超えて切り捨てられたrowがあった場合はその旨をmarkdown内に表示するようにする
|
||||
|
||||
const startupKeys = new Set([
|
||||
...before.startupKeys,
|
||||
@@ -285,46 +288,30 @@ const startupKeys = new Set([
|
||||
]);
|
||||
const startupComparisonRows = getChunkComparisonRows([...startupKeys], before, after);
|
||||
const startupRows = startupComparisonRows
|
||||
.sort((a, b) => Math.abs(b.afterSize - b.beforeSize) - Math.abs(a.afterSize - a.beforeSize)
|
||||
|| (b.afterSize - b.beforeSize) - (a.afterSize - a.beforeSize)
|
||||
|| b.sortSize - a.sortSize
|
||||
|| a.name.localeCompare(b.name));
|
||||
.sort(compareComparisonRows);
|
||||
const startupSummary = summarizeChanges(startupComparisonRows);
|
||||
const startupTotal = {
|
||||
beforeSize: startupComparisonRows.reduce((sum, row) => sum + row.beforeSize, 0),
|
||||
afterSize: startupComparisonRows.reduce((sum, row) => sum + row.afterSize, 0),
|
||||
};
|
||||
|
||||
const largeRows = comparisonRows
|
||||
.sort((a, b) => b.sortSize - a.sortSize || a.name.localeCompare(b.name))
|
||||
.slice(0, 30);
|
||||
//const largeRows = comparisonRows
|
||||
// .sort((a, b) => b.sortSize - a.sortSize || a.name.localeCompare(b.name))
|
||||
// .slice(0, 30);
|
||||
|
||||
const body = [
|
||||
marker,
|
||||
`## Frontend chunk report (${locale})`,
|
||||
`## Frontend Chunk Report`,
|
||||
'',
|
||||
'<details open>',
|
||||
`<summary>Diffs</summary>`,
|
||||
`<summary>${formatChangeSummary('Diffs', diffSummary)}</summary>`,
|
||||
'',
|
||||
markdownTable(diffRows, diffTotal),
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
`<summary>Added (${addedRows.length})</summary>`,
|
||||
'',
|
||||
markdownChunkTable(addedRows),
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
`<summary>Removed (${removedRows.length})</summary>`,
|
||||
'',
|
||||
markdownChunkTable(removedRows),
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
`<summary>Startup</summary>`,
|
||||
`<summary>${formatChangeSummary('Startup', startupSummary)}</summary>`,
|
||||
'',
|
||||
markdownTable(startupRows, startupTotal),
|
||||
'',
|
||||
@@ -332,13 +319,13 @@ const body = [
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
'<details>',
|
||||
`<summary>Largest</summary>`,
|
||||
'',
|
||||
markdownTable(largeRows),
|
||||
'',
|
||||
'</details>',
|
||||
'',
|
||||
//'<details>',
|
||||
//`<summary>Largest</summary>`,
|
||||
//'',
|
||||
//markdownTable(largeRows),
|
||||
//'',
|
||||
//'</details>',
|
||||
//'',
|
||||
].join('\n');
|
||||
|
||||
await fs.writeFile(outFile, body);
|
||||
|
||||
@@ -104,6 +104,9 @@ jobs:
|
||||
});
|
||||
const report = artifacts.find((artifact) => artifact.name === artifactName && !artifact.expired);
|
||||
if (report) return { done: true, run };
|
||||
|
||||
core.info(`Frontend bundle report run ${run.id} did not produce ${artifactName}.`);
|
||||
return { done: true, run: null };
|
||||
}
|
||||
|
||||
return { done: false, run: null };
|
||||
@@ -126,8 +129,30 @@ jobs:
|
||||
|
||||
core.warning(`Timed out waiting for ${artifactName} from ${workflow_id} for ${headSha}.`);
|
||||
|
||||
- name: Download bundle report from workflow_run
|
||||
- name: Find bundle report artifact
|
||||
if: github.event_name == 'workflow_run'
|
||||
id: find-report-artifact
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
script: |
|
||||
const artifactName = 'frontend-bundle-report';
|
||||
const { owner, repo } = context.repo;
|
||||
const artifacts = await github.paginate(github.rest.actions.listWorkflowRunArtifacts, {
|
||||
owner,
|
||||
repo,
|
||||
run_id: context.payload.workflow_run.id,
|
||||
per_page: 100,
|
||||
});
|
||||
const report = artifacts.find((artifact) => artifact.name === artifactName && !artifact.expired);
|
||||
if (report) {
|
||||
core.setOutput('exists', 'true');
|
||||
} else {
|
||||
core.info(`Workflow run ${context.payload.workflow_run.id} did not produce ${artifactName}.`);
|
||||
core.setOutput('exists', 'false');
|
||||
}
|
||||
|
||||
- name: Download bundle report from workflow_run
|
||||
if: github.event_name == 'workflow_run' && steps.find-report-artifact.outputs.exists == 'true'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: frontend-bundle-report
|
||||
@@ -147,7 +172,7 @@ jobs:
|
||||
run-id: ${{ steps.find-report-run.outputs.run-id }}
|
||||
|
||||
- name: Comment on pull request
|
||||
if: github.event_name == 'workflow_run' || steps.find-report-run.outputs.run-id != ''
|
||||
if: (github.event_name == 'workflow_run' && steps.find-report-artifact.outputs.exists == 'true') || steps.find-report-run.outputs.run-id != ''
|
||||
uses: actions/github-script@v9
|
||||
with:
|
||||
github-token: ${{ secrets.FRONTEND_BUNDLE_REPORT_COMMENT_TOKEN || secrets.FRONTEND_JS_SIZE_COMMENT_TOKEN || secrets.FRONTEND_BUNDLE_VISUALIZER_COMMENT_TOKEN || github.token }}
|
||||
@@ -165,7 +190,7 @@ jobs:
|
||||
const headShaPath = path.join(reportDir, 'head-sha.txt');
|
||||
const workflowRun = context.payload.workflow_run;
|
||||
const pullRequest = context.payload.pull_request;
|
||||
const headSha = workflowRun?.head_sha ?? pullRequest?.head?.sha ?? null;
|
||||
const eventHeadSha = workflowRun?.head_sha ?? pullRequest?.head?.sha ?? null;
|
||||
const { owner, repo } = context.repo;
|
||||
|
||||
if (!fs.existsSync(jsSizeReportPath)) {
|
||||
@@ -180,10 +205,10 @@ jobs:
|
||||
const artifactHeadSha = fs.existsSync(headShaPath)
|
||||
? fs.readFileSync(headShaPath, 'utf8').trim()
|
||||
: null;
|
||||
if (headSha != null && artifactHeadSha != null && artifactHeadSha !== headSha) {
|
||||
core.setFailed(`The artifact head SHA (${artifactHeadSha}) does not match the workflow head SHA (${headSha}).`);
|
||||
return;
|
||||
if (eventHeadSha != null && artifactHeadSha != null && artifactHeadSha !== eventHeadSha) {
|
||||
core.info(`The artifact head SHA (${artifactHeadSha}) differs from the event head SHA (${eventHeadSha}). Using artifact metadata for PR validation.`);
|
||||
}
|
||||
const reportHeadSha = artifactHeadSha ?? eventHeadSha;
|
||||
|
||||
const artifactPrNumber = fs.existsSync(prNumberPath)
|
||||
? Number(fs.readFileSync(prNumberPath, 'utf8').trim())
|
||||
@@ -203,25 +228,29 @@ jobs:
|
||||
}
|
||||
}
|
||||
|
||||
const pullRequestsForCommit = await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, {
|
||||
owner,
|
||||
repo,
|
||||
commit_sha: headSha,
|
||||
per_page: 100,
|
||||
});
|
||||
for (const pullRequest of pullRequestsForCommit) {
|
||||
associatedPullRequests.set(pullRequest.number, pullRequest);
|
||||
if (reportHeadSha != null) {
|
||||
const pullRequestsForCommit = await github.paginate(github.rest.repos.listPullRequestsAssociatedWithCommit, {
|
||||
owner,
|
||||
repo,
|
||||
commit_sha: reportHeadSha,
|
||||
per_page: 100,
|
||||
});
|
||||
for (const pullRequest of pullRequestsForCommit) {
|
||||
associatedPullRequests.set(pullRequest.number, pullRequest);
|
||||
}
|
||||
}
|
||||
|
||||
if (Number.isInteger(artifactPrNumber) && associatedPullRequests.has(artifactPrNumber)) {
|
||||
issue_number = artifactPrNumber;
|
||||
} else if (Number.isInteger(artifactPrNumber) && associatedPullRequests.size === 0) {
|
||||
issue_number = artifactPrNumber;
|
||||
} else if (!Number.isInteger(artifactPrNumber) && associatedPullRequests.size === 1) {
|
||||
issue_number = [...associatedPullRequests.keys()][0];
|
||||
} else if (Number.isInteger(artifactPrNumber)) {
|
||||
core.setFailed(`The artifact pull request number (${artifactPrNumber}) is not associated with ${headSha}.`);
|
||||
core.setFailed(`The artifact pull request number (${artifactPrNumber}) is not associated with ${reportHeadSha}.`);
|
||||
return;
|
||||
} else {
|
||||
core.setFailed(`Could not determine the pull request associated with ${headSha}.`);
|
||||
core.setFailed(`Could not determine the pull request associated with ${reportHeadSha}.`);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
@@ -229,6 +258,17 @@ jobs:
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPullRequest = await github.rest.pulls.get({
|
||||
owner,
|
||||
repo,
|
||||
pull_number: issue_number,
|
||||
});
|
||||
const currentHeadSha = currentPullRequest.data.head?.sha;
|
||||
if (reportHeadSha != null && currentHeadSha != null && reportHeadSha !== currentHeadSha) {
|
||||
core.info(`The report head SHA (${reportHeadSha}) is not the current pull request head SHA (${currentHeadSha}). Skipping stale frontend bundle report.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const jsSizeReport = fs.readFileSync(jsSizeReportPath, 'utf8').trim();
|
||||
if (!jsSizeReport.includes(jsSizeMarker)) {
|
||||
core.setFailed('The frontend JS size report is missing the expected marker.');
|
||||
|
||||
24
.github/workflows/frontend-bundle-report.yml
vendored
24
.github/workflows/frontend-bundle-report.yml
vendored
@@ -56,21 +56,25 @@ jobs:
|
||||
path: after
|
||||
submodules: true
|
||||
|
||||
- name: Backport visualizer tooling to base if needed
|
||||
- name: Check base visualizer support
|
||||
id: check-base-visualizer
|
||||
shell: bash
|
||||
run: |
|
||||
if ! grep -q 'FRONTEND_BUNDLE_VISUALIZER' before/packages/frontend/vite.config.ts; then
|
||||
cp after/packages/frontend/package.json before/packages/frontend/package.json
|
||||
cp after/packages/frontend/vite.config.ts before/packages/frontend/vite.config.ts
|
||||
cp after/pnpm-lock.yaml before/pnpm-lock.yaml
|
||||
if grep -q 'FRONTEND_BUNDLE_VISUALIZER' before/packages/frontend/vite.config.ts; then
|
||||
echo 'supported=true' >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo 'supported=false' >> "$GITHUB_OUTPUT"
|
||||
echo 'Base commit does not support frontend bundle visualizer. Skipping frontend bundle report.' >> "$GITHUB_STEP_SUMMARY"
|
||||
fi
|
||||
|
||||
- name: Setup pnpm
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
uses: pnpm/action-setup@v6.0.3
|
||||
with:
|
||||
package_json_file: after/package.json
|
||||
|
||||
- name: Setup Node.js
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
uses: actions/setup-node@v6.4.0
|
||||
with:
|
||||
node-version-file: after/.node-version
|
||||
@@ -80,17 +84,21 @@ jobs:
|
||||
after/pnpm-lock.yaml
|
||||
|
||||
- name: Install dependencies for base
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: before
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Build frontend dependencies for base
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: before
|
||||
run: pnpm --filter "frontend^..." run build
|
||||
|
||||
- name: Prepare report output
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
run: mkdir -p "$RUNNER_TEMP/frontend-bundle-report"
|
||||
|
||||
- name: Build frontend report for base
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: before
|
||||
env:
|
||||
FRONTEND_BUNDLE_VISUALIZER: 'true'
|
||||
@@ -99,14 +107,17 @@ jobs:
|
||||
run: pnpm --filter frontend run build
|
||||
|
||||
- name: Install dependencies for pull request
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: after
|
||||
run: pnpm i --frozen-lockfile
|
||||
|
||||
- name: Build frontend dependencies for pull request
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: after
|
||||
run: pnpm --filter "frontend^..." run build
|
||||
|
||||
- name: Build frontend report for pull request
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
working-directory: after
|
||||
env:
|
||||
FRONTEND_BUNDLE_VISUALIZER: 'true'
|
||||
@@ -115,6 +126,7 @@ jobs:
|
||||
run: pnpm --filter frontend run build
|
||||
|
||||
- name: Generate report markdown
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
shell: bash
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
||||
@@ -130,6 +142,7 @@ jobs:
|
||||
printf '%s\n' "${{ github.event.pull_request.html_url }}" > "$REPORT_DIR/pr-url.txt"
|
||||
|
||||
- name: Check report
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
run: |
|
||||
REPORT_DIR="$RUNNER_TEMP/frontend-bundle-report"
|
||||
test -s "$REPORT_DIR/before-stats.json"
|
||||
@@ -141,6 +154,7 @@ jobs:
|
||||
cat "$REPORT_DIR/frontend-bundle-visualizer-report.md" >> "$GITHUB_STEP_SUMMARY"
|
||||
|
||||
- name: Upload bundle report
|
||||
if: steps.check-base-visualizer.outputs.supported == 'true'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: frontend-bundle-report
|
||||
|
||||
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,3 +1,16 @@
|
||||
## Unreleased
|
||||
|
||||
### General
|
||||
-
|
||||
|
||||
### Client
|
||||
- 2025.4.0 以前の設定情報の移行処理が削除されました
|
||||
- 2025.4.0 から直接 2026.6.0 以上にアップデートする場合は設定が移行されませんので注意してください。移行したい場合は一度 2026.5.1 を経由してください。
|
||||
|
||||
### Server
|
||||
-
|
||||
|
||||
|
||||
## 2026.6.0
|
||||
|
||||
### General
|
||||
|
||||
@@ -1361,14 +1361,11 @@ information: "情報"
|
||||
chat: "チャット"
|
||||
directMessage: "ダイレクトメッセージ"
|
||||
directMessage_short: "メッセージ"
|
||||
migrateOldSettings: "旧設定情報を移行"
|
||||
migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。"
|
||||
compress: "圧縮"
|
||||
right: "右"
|
||||
bottom: "下"
|
||||
top: "上"
|
||||
embed: "埋め込み"
|
||||
settingsMigrating: "設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)"
|
||||
readonly: "読み取り専用"
|
||||
goToDeck: "デッキへ戻る"
|
||||
federationJobs: "連合ジョブ"
|
||||
|
||||
@@ -27,7 +27,6 @@ import { makeHotkey } from '@/utility/hotkey.js';
|
||||
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { updateCurrentAccountPartial } from '@/accounts.js';
|
||||
import { migrateOldSettings } from '@/pref-migrate.js';
|
||||
import { unisonReload } from '@/utility/unison-reload.js';
|
||||
import { isBirthday } from '@/utility/is-birthday.js';
|
||||
|
||||
@@ -69,14 +68,6 @@ export async function mainBoot() {
|
||||
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {
|
||||
closed: () => dispose(),
|
||||
});
|
||||
|
||||
// prefereces migration
|
||||
// TODO: そのうち消す
|
||||
if (lastVersion && (compareVersions('2025.3.2-alpha.0', lastVersion) === 1)) {
|
||||
console.log('Preferences migration');
|
||||
|
||||
migrateOldSettings();
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
|
||||
@@ -145,11 +145,6 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||
<MkButton v-if="storagePersistenceSupported && !storagePersisted" @click="enableStoragePersistence">{{ i18n.ts._settings.settingsPersistence_title }}</MkButton>
|
||||
|
||||
<MkButton @click="forceCloudBackup">{{ i18n.ts._preferencesBackup.forceBackup }}</MkButton>
|
||||
|
||||
<FormSlot>
|
||||
<MkButton danger @click="migrate"><i class="ti ti-refresh"></i> {{ i18n.ts.migrateOldSettings }}</MkButton>
|
||||
<template #caption>{{ i18n.ts.migrateOldSettings_description }}</template>
|
||||
</FormSlot>
|
||||
</div>
|
||||
</SearchMarker>
|
||||
</template>
|
||||
@@ -173,7 +168,6 @@ import FormSection from '@/components/form/section.vue';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import MkRolePreview from '@/components/MkRolePreview.vue';
|
||||
import { signout } from '@/signout.js';
|
||||
import { migrateOldSettings } from '@/pref-migrate.js';
|
||||
import { hideAllTips as _hideAllTips, resetAllTips as _resetAllTips } from '@/tips.js';
|
||||
import { suggestReload } from '@/utility/reload-suggest.js';
|
||||
import { cloudBackup } from '@/preferences/utility.js';
|
||||
@@ -219,10 +213,6 @@ async function deleteAccount() {
|
||||
await signout();
|
||||
}
|
||||
|
||||
function migrate() {
|
||||
migrateOldSettings();
|
||||
}
|
||||
|
||||
function resetAllTips() {
|
||||
_resetAllTips();
|
||||
os.success();
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import type { DeckProfile } from '@/deck.js';
|
||||
import { genId } from '@/utility/id.js';
|
||||
import { store } from '@/store.js';
|
||||
import { prefer } from '@/preferences.js';
|
||||
import { misskeyApi } from '@/utility/misskey-api.js';
|
||||
import { deckStore } from '@/ui/deck/deck-store.js';
|
||||
import { unisonReload } from '@/utility/unison-reload.js';
|
||||
import * as os from '@/os.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import type { SoundStore } from '@/preferences/def.js';
|
||||
|
||||
// TODO: そのうち消す
|
||||
export function migrateOldSettings() {
|
||||
os.waiting({ text: i18n.ts.settingsMigrating });
|
||||
|
||||
store.loaded.then(async () => {
|
||||
misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []).then((themes: any) => {
|
||||
if (themes.length > 0) {
|
||||
prefer.commit('themes', themes);
|
||||
}
|
||||
});
|
||||
|
||||
prefer.commit('deck.profile', deckStore.s.profile);
|
||||
misskeyApi('i/registry/keys', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
}).then(async keys => {
|
||||
const profiles: DeckProfile[] = [];
|
||||
for (const key of keys) {
|
||||
const deck = await misskeyApi('i/registry/get', {
|
||||
scope: ['client', 'deck', 'profiles'],
|
||||
key: key,
|
||||
});
|
||||
profiles.push({
|
||||
id: genId(),
|
||||
name: key,
|
||||
columns: deck.columns,
|
||||
layout: deck.layout,
|
||||
});
|
||||
}
|
||||
prefer.commit('deck.profiles', profiles);
|
||||
});
|
||||
|
||||
prefer.commit('emojiPalettes', [{
|
||||
id: 'reactions',
|
||||
name: '',
|
||||
emojis: store.s.reactions,
|
||||
}, {
|
||||
id: 'pinnedEmojis',
|
||||
name: '',
|
||||
emojis: store.s.pinnedEmojis,
|
||||
}]);
|
||||
prefer.commit('emojiPaletteForMain', 'pinnedEmojis');
|
||||
prefer.commit('emojiPaletteForReaction', 'reactions');
|
||||
prefer.commit('overridedDeviceKind', store.s.overridedDeviceKind);
|
||||
prefer.commit('widgets', store.s.widgets);
|
||||
prefer.commit('keepCw', store.s.keepCw);
|
||||
prefer.commit('collapseRenotes', store.s.collapseRenotes);
|
||||
prefer.commit('rememberNoteVisibility', store.s.rememberNoteVisibility);
|
||||
prefer.commit('uploadFolder', store.s.uploadFolder);
|
||||
prefer.commit('menu', [...store.s.menu, 'chat']);
|
||||
prefer.commit('statusbars', store.s.statusbars);
|
||||
prefer.commit('pinnedUserLists', store.s.pinnedUserLists);
|
||||
prefer.commit('serverDisconnectedBehavior', store.s.serverDisconnectedBehavior);
|
||||
prefer.commit('nsfw', store.s.nsfw);
|
||||
prefer.commit('highlightSensitiveMedia', store.s.highlightSensitiveMedia);
|
||||
prefer.commit('animation', store.s.animation);
|
||||
prefer.commit('animatedMfm', store.s.animatedMfm);
|
||||
prefer.commit('advancedMfm', store.s.advancedMfm);
|
||||
prefer.commit('showReactionsCount', store.s.showReactionsCount);
|
||||
prefer.commit('enableQuickAddMfmFunction', store.s.enableQuickAddMfmFunction);
|
||||
prefer.commit('loadRawImages', store.s.loadRawImages);
|
||||
prefer.commit('imageNewTab', store.s.imageNewTab);
|
||||
prefer.commit('disableShowingAnimatedImages', store.s.disableShowingAnimatedImages);
|
||||
prefer.commit('emojiStyle', store.s.emojiStyle);
|
||||
prefer.commit('menuStyle', store.s.menuStyle);
|
||||
prefer.commit('useBlurEffectForModal', store.s.useBlurEffectForModal);
|
||||
prefer.commit('useBlurEffect', store.s.useBlurEffect);
|
||||
prefer.commit('showFixedPostForm', store.s.showFixedPostForm);
|
||||
prefer.commit('showFixedPostFormInChannel', store.s.showFixedPostFormInChannel);
|
||||
prefer.commit('enableInfiniteScroll', store.s.enableInfiniteScroll);
|
||||
prefer.commit('useReactionPickerForContextMenu', store.s.useReactionPickerForContextMenu);
|
||||
prefer.commit('instanceTicker', store.s.instanceTicker);
|
||||
prefer.commit('emojiPickerScale', store.s.emojiPickerScale);
|
||||
prefer.commit('emojiPickerWidth', store.s.emojiPickerWidth);
|
||||
prefer.commit('emojiPickerHeight', store.s.emojiPickerHeight);
|
||||
prefer.commit('emojiPickerStyle', store.s.emojiPickerStyle);
|
||||
prefer.commit('reportError', store.s.reportError);
|
||||
prefer.commit('squareAvatars', store.s.squareAvatars);
|
||||
prefer.commit('showAvatarDecorations', store.s.showAvatarDecorations);
|
||||
prefer.commit('numberOfPageCache', store.s.numberOfPageCache);
|
||||
prefer.commit('showNoteActionsOnlyHover', store.s.showNoteActionsOnlyHover);
|
||||
prefer.commit('showClipButtonInNoteFooter', store.s.showClipButtonInNoteFooter);
|
||||
prefer.commit('reactionsDisplaySize', store.s.reactionsDisplaySize);
|
||||
prefer.commit('limitWidthOfReaction', store.s.limitWidthOfReaction);
|
||||
prefer.commit('forceShowAds', store.s.forceShowAds);
|
||||
prefer.commit('aiChanMode', store.s.aiChanMode);
|
||||
prefer.commit('devMode', store.s.devMode);
|
||||
prefer.commit('mediaListWithOneImageAppearance', store.s.mediaListWithOneImageAppearance);
|
||||
prefer.commit('notificationPosition', store.s.notificationPosition);
|
||||
prefer.commit('notificationStackAxis', store.s.notificationStackAxis);
|
||||
prefer.commit('enableCondensedLine', store.s.enableCondensedLine);
|
||||
prefer.commit('keepScreenOn', store.s.keepScreenOn);
|
||||
prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications);
|
||||
prefer.commit('dataSaver', {
|
||||
...prefer.s.dataSaver,
|
||||
media: store.s.dataSaver.media,
|
||||
avatar: store.s.dataSaver.avatar,
|
||||
urlPreviewThumbnail: store.s.dataSaver.urlPreview,
|
||||
code: store.s.dataSaver.code,
|
||||
});
|
||||
prefer.commit('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect);
|
||||
prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe);
|
||||
prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer);
|
||||
prefer.commit('keepOriginalFilename', store.s.keepOriginalFilename);
|
||||
prefer.commit('alwaysConfirmFollow', store.s.alwaysConfirmFollow);
|
||||
prefer.commit('confirmWhenRevealingSensitiveMedia', store.s.confirmWhenRevealingSensitiveMedia);
|
||||
prefer.commit('contextMenu', store.s.contextMenu);
|
||||
prefer.commit('skipNoteRender', store.s.skipNoteRender);
|
||||
prefer.commit('showSoftWordMutedWord', store.s.showSoftWordMutedWord);
|
||||
prefer.commit('confirmOnReact', store.s.confirmOnReact);
|
||||
prefer.commit('defaultFollowWithReplies', store.s.defaultWithReplies);
|
||||
prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
|
||||
prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
|
||||
prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
|
||||
prefer.commit('sound.on.note', store.s.sound_note as SoundStore);
|
||||
prefer.commit('sound.on.noteMy', store.s.sound_noteMy as SoundStore);
|
||||
prefer.commit('sound.on.notification', store.s.sound_notification as SoundStore);
|
||||
prefer.commit('sound.on.reaction', store.s.sound_reaction as SoundStore);
|
||||
prefer.commit('defaultNoteVisibility', store.s.defaultNoteVisibility);
|
||||
prefer.commit('defaultNoteLocalOnly', store.s.defaultNoteLocalOnly);
|
||||
|
||||
window.setTimeout(() => {
|
||||
unisonReload();
|
||||
}, 10000);
|
||||
});
|
||||
}
|
||||
@@ -118,352 +118,6 @@ export const store = markRaw(new Pizzax('base', {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
|
||||
//#region TODO: そのうち消す (preferに移行済み)
|
||||
defaultWithReplies: {
|
||||
where: 'account',
|
||||
default: false,
|
||||
},
|
||||
reactions: {
|
||||
where: 'account',
|
||||
default: ['👍', '❤️', '😆', '🤔', '😮', '🎉', '💢', '😥', '😇', '🍮'],
|
||||
},
|
||||
pinnedEmojis: {
|
||||
where: 'account',
|
||||
default: [],
|
||||
},
|
||||
widgets: {
|
||||
where: 'account',
|
||||
default: [] as {
|
||||
name: string;
|
||||
id: string;
|
||||
place: string | null;
|
||||
data: Record<string, any>;
|
||||
}[],
|
||||
},
|
||||
overridedDeviceKind: {
|
||||
where: 'device',
|
||||
default: null as DeviceKind | null,
|
||||
},
|
||||
defaultSideView: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
defaultNoteVisibility: {
|
||||
where: 'account',
|
||||
default: 'public' as (typeof Misskey.noteVisibilities)[number],
|
||||
},
|
||||
defaultNoteLocalOnly: {
|
||||
where: 'account',
|
||||
default: false,
|
||||
},
|
||||
keepCw: {
|
||||
where: 'account',
|
||||
default: true,
|
||||
},
|
||||
collapseRenotes: {
|
||||
where: 'account',
|
||||
default: true,
|
||||
},
|
||||
rememberNoteVisibility: {
|
||||
where: 'account',
|
||||
default: false,
|
||||
},
|
||||
uploadFolder: {
|
||||
where: 'account',
|
||||
default: null as string | null,
|
||||
},
|
||||
keepOriginalUploading: {
|
||||
where: 'account',
|
||||
default: false,
|
||||
},
|
||||
menu: {
|
||||
where: 'deviceAccount',
|
||||
default: [
|
||||
'notifications',
|
||||
'clips',
|
||||
'drive',
|
||||
'followRequests',
|
||||
'-',
|
||||
'explore',
|
||||
'announcements',
|
||||
'search',
|
||||
'-',
|
||||
'ui',
|
||||
],
|
||||
},
|
||||
statusbars: {
|
||||
where: 'deviceAccount',
|
||||
default: [] as {
|
||||
name: string;
|
||||
id: string;
|
||||
type: string;
|
||||
size: 'verySmall' | 'small' | 'medium' | 'large' | 'veryLarge';
|
||||
black: boolean;
|
||||
props: Record<string, any>;
|
||||
}[],
|
||||
},
|
||||
pinnedUserLists: {
|
||||
where: 'deviceAccount',
|
||||
default: [] as Misskey.entities.UserList[],
|
||||
},
|
||||
serverDisconnectedBehavior: {
|
||||
where: 'device',
|
||||
default: 'quiet' as 'quiet' | 'reload' | 'dialog',
|
||||
},
|
||||
nsfw: {
|
||||
where: 'device',
|
||||
default: 'respect' as 'respect' | 'force' | 'ignore',
|
||||
},
|
||||
highlightSensitiveMedia: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
animation: {
|
||||
where: 'device',
|
||||
default: !prefersReducedMotion,
|
||||
},
|
||||
animatedMfm: {
|
||||
where: 'device',
|
||||
default: !prefersReducedMotion,
|
||||
},
|
||||
advancedMfm: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
showReactionsCount: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
enableQuickAddMfmFunction: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
loadRawImages: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
imageNewTab: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
disableShowingAnimatedImages: {
|
||||
where: 'device',
|
||||
default: prefersReducedMotion,
|
||||
},
|
||||
emojiStyle: {
|
||||
where: 'device',
|
||||
default: 'twemoji' as 'twemoji' | 'fluentEmoji' | 'native',
|
||||
},
|
||||
menuStyle: {
|
||||
where: 'device',
|
||||
default: 'auto' as 'auto' | 'popup' | 'drawer',
|
||||
},
|
||||
useBlurEffectForModal: {
|
||||
where: 'device',
|
||||
default: DEFAULT_DEVICE_KIND === 'desktop',
|
||||
},
|
||||
useBlurEffect: {
|
||||
where: 'device',
|
||||
default: DEFAULT_DEVICE_KIND === 'desktop',
|
||||
},
|
||||
showFixedPostForm: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
showFixedPostFormInChannel: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
enableInfiniteScroll: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
useReactionPickerForContextMenu: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
showGapBetweenNotesInTimeline: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
instanceTicker: {
|
||||
where: 'device',
|
||||
default: 'remote' as 'none' | 'remote' | 'always',
|
||||
},
|
||||
emojiPickerScale: {
|
||||
where: 'device',
|
||||
default: 1,
|
||||
},
|
||||
emojiPickerWidth: {
|
||||
where: 'device',
|
||||
default: 1,
|
||||
},
|
||||
emojiPickerHeight: {
|
||||
where: 'device',
|
||||
default: 2,
|
||||
},
|
||||
emojiPickerStyle: {
|
||||
where: 'device',
|
||||
default: 'auto' as 'auto' | 'popup' | 'drawer',
|
||||
},
|
||||
reportError: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
squareAvatars: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
showAvatarDecorations: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
numberOfPageCache: {
|
||||
where: 'device',
|
||||
default: 3,
|
||||
},
|
||||
showNoteActionsOnlyHover: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
showClipButtonInNoteFooter: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
reactionsDisplaySize: {
|
||||
where: 'device',
|
||||
default: 'medium' as 'small' | 'medium' | 'large',
|
||||
},
|
||||
limitWidthOfReaction: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
forceShowAds: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
aiChanMode: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
devMode: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
mediaListWithOneImageAppearance: {
|
||||
where: 'device',
|
||||
default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3',
|
||||
},
|
||||
notificationPosition: {
|
||||
where: 'device',
|
||||
default: 'rightBottom' as 'leftTop' | 'leftBottom' | 'rightTop' | 'rightBottom',
|
||||
},
|
||||
notificationStackAxis: {
|
||||
where: 'device',
|
||||
default: 'horizontal' as 'vertical' | 'horizontal',
|
||||
},
|
||||
enableCondensedLine: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
keepScreenOn: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
useGroupedNotifications: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
dataSaver: {
|
||||
where: 'device',
|
||||
default: {
|
||||
media: false,
|
||||
avatar: false,
|
||||
urlPreview: false,
|
||||
code: false,
|
||||
},
|
||||
},
|
||||
enableSeasonalScreenEffect: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
enableHorizontalSwipe: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
useNativeUIForVideoAudioPlayer: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
keepOriginalFilename: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
alwaysConfirmFollow: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
confirmWhenRevealingSensitiveMedia: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
contextMenu: {
|
||||
where: 'device',
|
||||
default: 'app' as 'app' | 'appWithShift' | 'native',
|
||||
},
|
||||
skipNoteRender: {
|
||||
where: 'device',
|
||||
default: true,
|
||||
},
|
||||
showSoftWordMutedWord: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
confirmOnReact: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
hemisphere: {
|
||||
where: 'device',
|
||||
default: hemisphere as 'N' | 'S',
|
||||
},
|
||||
sound_masterVolume: {
|
||||
where: 'device',
|
||||
default: 0.3,
|
||||
},
|
||||
sound_notUseSound: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
sound_useSoundOnlyWhenActive: {
|
||||
where: 'device',
|
||||
default: false,
|
||||
},
|
||||
sound_note: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/n-aec', volume: 1 },
|
||||
},
|
||||
sound_noteMy: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/n-cea-4va', volume: 1 },
|
||||
},
|
||||
sound_notification: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/n-ea', volume: 1 },
|
||||
},
|
||||
sound_reaction: {
|
||||
where: 'device',
|
||||
default: { type: 'syuilo/bubble2', volume: 1 },
|
||||
},
|
||||
dropAndFusion: {
|
||||
where: 'device',
|
||||
default: {
|
||||
bgmVolume: 0.25,
|
||||
sfxVolume: 1,
|
||||
},
|
||||
},
|
||||
//#endregion
|
||||
}));
|
||||
|
||||
// TODO: 他のタブと永続化されたstateを同期
|
||||
|
||||
@@ -5456,14 +5456,6 @@ export interface Locale extends ILocale {
|
||||
* メッセージ
|
||||
*/
|
||||
"directMessage_short": string;
|
||||
/**
|
||||
* 旧設定情報を移行
|
||||
*/
|
||||
"migrateOldSettings": string;
|
||||
/**
|
||||
* 通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。
|
||||
*/
|
||||
"migrateOldSettings_description": string;
|
||||
/**
|
||||
* 圧縮
|
||||
*/
|
||||
@@ -5484,10 +5476,6 @@ export interface Locale extends ILocale {
|
||||
* 埋め込み
|
||||
*/
|
||||
"embed": string;
|
||||
/**
|
||||
* 設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)
|
||||
*/
|
||||
"settingsMigrating": string;
|
||||
/**
|
||||
* 読み取り専用
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user