Merge pull request MisskeyIO#260 from MisskeyIO/merge-upstream
Merge misskey-dev/develop
This commit is contained in:
commit
b7db189307
186
.github/workflows/get-api-diff.yml
vendored
186
.github/workflows/get-api-diff.yml
vendored
|
@ -1,186 +0,0 @@
|
||||||
# this name is used in report-api-diff.yml so be careful when change name
|
|
||||||
name: Get api.json from Misskey
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- develop
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
get-base:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [20.5.1]
|
|
||||||
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
image: postgres:13
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
env:
|
|
||||||
POSTGRES_DB: misskey
|
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
|
||||||
POSTGRES_USER: example-misskey-user
|
|
||||||
POSTGRESS_PASS: example-misskey-pass
|
|
||||||
redis:
|
|
||||||
image: redis:7
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4.1.1
|
|
||||||
with:
|
|
||||||
repository: ${{ github.event.pull_request.base.repo.full_name }}
|
|
||||||
ref: ${{ github.base_ref }}
|
|
||||||
submodules: true
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
run_install: false
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v4.0.0
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
cache: 'pnpm'
|
|
||||||
- run: corepack enable
|
|
||||||
- run: pnpm i --frozen-lockfile
|
|
||||||
- name: Check pnpm-lock.yaml
|
|
||||||
run: git diff --exit-code pnpm-lock.yaml
|
|
||||||
- name: Copy Configure
|
|
||||||
run: cp .config/example.yml .config/default.yml
|
|
||||||
- name: Build
|
|
||||||
run: pnpm build
|
|
||||||
- name : Migrate
|
|
||||||
run: pnpm migrate
|
|
||||||
- name: Launch misskey
|
|
||||||
run: |
|
|
||||||
screen -S misskey -dm pnpm run dev
|
|
||||||
sleep 30s
|
|
||||||
- name: Wait for Misskey to be ready
|
|
||||||
run: |
|
|
||||||
MAX_RETRIES=12
|
|
||||||
RETRY_DELAY=5
|
|
||||||
count=0
|
|
||||||
until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do
|
|
||||||
printf '.'
|
|
||||||
sleep $RETRY_DELAY
|
|
||||||
count=$((count + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ $count -eq $MAX_RETRIES ]]; then
|
|
||||||
echo "Failed to connect to Misskey after $MAX_RETRIES attempts."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- id: fetch
|
|
||||||
name: Get api.json from Misskey
|
|
||||||
run: |
|
|
||||||
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
|
|
||||||
echo $RESULT > api-base.json
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: api-artifact
|
|
||||||
path: api-base.json
|
|
||||||
- name: Kill Misskey Job
|
|
||||||
run: screen -S misskey -X quit
|
|
||||||
|
|
||||||
get-head:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
node-version: [20.5.1]
|
|
||||||
|
|
||||||
services:
|
|
||||||
db:
|
|
||||||
image: postgres:13
|
|
||||||
ports:
|
|
||||||
- 5432:5432
|
|
||||||
env:
|
|
||||||
POSTGRES_DB: misskey
|
|
||||||
POSTGRES_HOST_AUTH_METHOD: trust
|
|
||||||
POSTGRES_USER: example-misskey-user
|
|
||||||
POSTGRESS_PASS: example-misskey-pass
|
|
||||||
redis:
|
|
||||||
image: redis:7
|
|
||||||
ports:
|
|
||||||
- 6379:6379
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v4.1.1
|
|
||||||
with:
|
|
||||||
repository: ${{ github.event.pull_request.head.repo.full_name }}
|
|
||||||
ref: ${{ github.head_ref }}
|
|
||||||
submodules: true
|
|
||||||
- name: Install pnpm
|
|
||||||
uses: pnpm/action-setup@v2
|
|
||||||
with:
|
|
||||||
version: 8
|
|
||||||
run_install: false
|
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
|
||||||
uses: actions/setup-node@v4.0.0
|
|
||||||
with:
|
|
||||||
node-version: ${{ matrix.node-version }}
|
|
||||||
cache: 'pnpm'
|
|
||||||
- run: corepack enable
|
|
||||||
- run: pnpm i --frozen-lockfile
|
|
||||||
- name: Check pnpm-lock.yaml
|
|
||||||
run: git diff --exit-code pnpm-lock.yaml
|
|
||||||
- name: Copy Configure
|
|
||||||
run: cp .config/example.yml .config/default.yml
|
|
||||||
- name: Build
|
|
||||||
run: pnpm build
|
|
||||||
- name : Migrate
|
|
||||||
run: pnpm migrate
|
|
||||||
- name: Launch misskey
|
|
||||||
run: |
|
|
||||||
screen -S misskey -dm pnpm run dev
|
|
||||||
sleep 30s
|
|
||||||
- name: Wait for Misskey to be ready
|
|
||||||
run: |
|
|
||||||
MAX_RETRIES=12
|
|
||||||
RETRY_DELAY=5
|
|
||||||
count=0
|
|
||||||
until $(curl --output /dev/null --silent --head --fail http://localhost:3000) || [[ $count -eq $MAX_RETRIES ]]; do
|
|
||||||
printf '.'
|
|
||||||
sleep $RETRY_DELAY
|
|
||||||
count=$((count + 1))
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ $count -eq $MAX_RETRIES ]]; then
|
|
||||||
echo "Failed to connect to Misskey after $MAX_RETRIES attempts."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
- id: fetch
|
|
||||||
name: Get api.json from Misskey
|
|
||||||
run: |
|
|
||||||
RESULT=$(curl --retry 5 --retry-delay 5 --retry-max-time 60 http://localhost:3000/api.json)
|
|
||||||
echo $RESULT > api-head.json
|
|
||||||
- name: Upload Artifact
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: api-artifact
|
|
||||||
path: api-head.json
|
|
||||||
- name: Kill Misskey Job
|
|
||||||
run: screen -S misskey -X quit
|
|
||||||
|
|
||||||
save-pr-number:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Save PR number
|
|
||||||
env:
|
|
||||||
PR_NUMBER: ${{ github.event.number }}
|
|
||||||
run: |
|
|
||||||
echo "$PR_NUMBER" > ./pr_number
|
|
||||||
- uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: api-artifact
|
|
||||||
path: pr_number
|
|
85
.github/workflows/report-api-diff.yml
vendored
85
.github/workflows/report-api-diff.yml
vendored
|
@ -1,85 +0,0 @@
|
||||||
name: Report API Diff
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_run:
|
|
||||||
types: [completed]
|
|
||||||
workflows:
|
|
||||||
- Get api.json from Misskey # get-api-diff.yml
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
compare-diff:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
# api-artifact
|
|
||||||
steps:
|
|
||||||
- name: Download artifact
|
|
||||||
uses: actions/github-script@v7
|
|
||||||
with:
|
|
||||||
script: |
|
|
||||||
let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
run_id: context.payload.workflow_run.id,
|
|
||||||
});
|
|
||||||
let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => {
|
|
||||||
return artifact.name == "api-artifact"
|
|
||||||
})[0];
|
|
||||||
let download = await github.rest.actions.downloadArtifact({
|
|
||||||
owner: context.repo.owner,
|
|
||||||
repo: context.repo.repo,
|
|
||||||
artifact_id: matchArtifact.id,
|
|
||||||
archive_format: 'zip',
|
|
||||||
});
|
|
||||||
let fs = require('fs');
|
|
||||||
fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/api-artifact.zip`, Buffer.from(download.data));
|
|
||||||
- name: Extract artifact
|
|
||||||
run: unzip api-artifact.zip -d artifacts
|
|
||||||
- name: Load PR Number
|
|
||||||
id: load-pr-num
|
|
||||||
run: echo "pr-number=$(cat artifacts/pr_number)" >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Output base
|
|
||||||
run: cat ./artifacts/api-base.json
|
|
||||||
- name: Output head
|
|
||||||
run: cat ./artifacts/api-head.json
|
|
||||||
- name: Arrange json files
|
|
||||||
run: |
|
|
||||||
jq '.' ./artifacts/api-base.json > ./api-base.json
|
|
||||||
jq '.' ./artifacts/api-head.json > ./api-head.json
|
|
||||||
- name: Get diff of 2 files
|
|
||||||
run: diff -u --label=base --label=head ./api-base.json ./api-head.json | cat > api.json.diff
|
|
||||||
- name: Get full diff
|
|
||||||
run: diff --label=base --label=head --new-line-format='+%L' --old-line-format='-%L' --unchanged-line-format=' %L' ./api-base.json ./api-head.json | cat > api-full.json.diff
|
|
||||||
- name: Echo full diff
|
|
||||||
run: cat ./api-full.json.diff
|
|
||||||
- name: Upload full diff to Artifact
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: api-artifact
|
|
||||||
path: |
|
|
||||||
api-full.json.diff
|
|
||||||
api-base.json
|
|
||||||
api-head.json
|
|
||||||
- id: out-diff
|
|
||||||
name: Build diff Comment
|
|
||||||
run: |
|
|
||||||
cat <<- EOF > ./output.md
|
|
||||||
このPRによるapi.jsonの差分
|
|
||||||
<details>
|
|
||||||
<summary>差分はこちら</summary>
|
|
||||||
|
|
||||||
\`\`\`diff
|
|
||||||
$(cat ./api.json.diff)
|
|
||||||
\`\`\`
|
|
||||||
</details>
|
|
||||||
|
|
||||||
[Get diff files from Workflow Page](https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID})
|
|
||||||
EOF
|
|
||||||
- uses: thollander/actions-comment-pull-request@v2
|
|
||||||
with:
|
|
||||||
pr_number: ${{ steps.load-pr-num.outputs.pr-number }}
|
|
||||||
comment_tag: show_diff
|
|
||||||
filePath: ./output.md
|
|
|
@ -17,14 +17,21 @@
|
||||||
### General
|
### General
|
||||||
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
|
- Feat: メールアドレスの認証にverifymail.ioを使えるように (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/971ba07a44550f68d2ba31c62066db2d43a0caed)
|
||||||
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
|
- Feat: モデレーターがユーザーのアイコンもしくはバナー画像を未設定状態にできる機能を追加 (cherry-pick from https://github.com/TeamNijimiss/misskey/commit/e0eb5a752f6e5616d6312bb7c9790302f9dbff83)
|
||||||
|
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
|
||||||
|
|
||||||
### Client
|
### Client
|
||||||
|
- Enhance: 絵文字のオートコンプリート機能強化 #12364
|
||||||
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
|
- fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
|
||||||
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
|
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
|
||||||
|
- Fix: コードエディタが正しく表示されない問題を修正
|
||||||
|
- Fix: プロフィールの「ファイル」にセンシティブな画像がある際のデザインを修正
|
||||||
|
|
||||||
### Server
|
### Server
|
||||||
|
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
|
||||||
- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303
|
- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303
|
||||||
- Fix: ロールタイムラインが保存されない問題を修正
|
- Fix: ロールタイムラインが保存されない問題を修正
|
||||||
|
- Fix: api.jsonの生成ロジックを改善 #12402
|
||||||
|
- Fix: 招待コードが使い回せる問題を修正
|
||||||
|
|
||||||
## 2023.11.1
|
## 2023.11.1
|
||||||
|
|
||||||
|
|
|
@ -641,6 +641,7 @@ smtpSecure: "Use implicit SSL/TLS for SMTP connections"
|
||||||
smtpSecureInfo: "Turn this off when using STARTTLS"
|
smtpSecureInfo: "Turn this off when using STARTTLS"
|
||||||
testEmail: "Test email delivery"
|
testEmail: "Test email delivery"
|
||||||
wordMute: "Word mute"
|
wordMute: "Word mute"
|
||||||
|
hardWordMute: "Hard Word mute"
|
||||||
regexpError: "Regular Expression error"
|
regexpError: "Regular Expression error"
|
||||||
regexpErrorDescription: "An error occurred in the regular expression on line {line} of your {tab} word mutes:"
|
regexpErrorDescription: "An error occurred in the regular expression on line {line} of your {tab} word mutes:"
|
||||||
instanceMute: "Instance Mutes"
|
instanceMute: "Instance Mutes"
|
||||||
|
|
1
locales/index.d.ts
vendored
1
locales/index.d.ts
vendored
|
@ -644,6 +644,7 @@ export interface Locale {
|
||||||
"smtpSecureInfo": string;
|
"smtpSecureInfo": string;
|
||||||
"testEmail": string;
|
"testEmail": string;
|
||||||
"wordMute": string;
|
"wordMute": string;
|
||||||
|
"hardWordMute": string;
|
||||||
"regexpError": string;
|
"regexpError": string;
|
||||||
"regexpErrorDescription": string;
|
"regexpErrorDescription": string;
|
||||||
"instanceMute": string;
|
"instanceMute": string;
|
||||||
|
|
|
@ -641,6 +641,7 @@ smtpSecure: "SMTP 接続に暗黙的なSSL/TLSを使用する"
|
||||||
smtpSecureInfo: "STARTTLS使用時はオフにします。"
|
smtpSecureInfo: "STARTTLS使用時はオフにします。"
|
||||||
testEmail: "配信テスト"
|
testEmail: "配信テスト"
|
||||||
wordMute: "ワードミュート"
|
wordMute: "ワードミュート"
|
||||||
|
hardWordMute: "ハードワードミュート"
|
||||||
regexpError: "正規表現エラー"
|
regexpError: "正規表現エラー"
|
||||||
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
|
regexpErrorDescription: "{tab}ワードミュートの{line}行目の正規表現にエラーが発生しました:"
|
||||||
instanceMute: "サーバーミュート"
|
instanceMute: "サーバーミュート"
|
||||||
|
@ -1864,7 +1865,7 @@ _ago:
|
||||||
weeksAgo: "{n}週間前"
|
weeksAgo: "{n}週間前"
|
||||||
monthsAgo: "{n}ヶ月前"
|
monthsAgo: "{n}ヶ月前"
|
||||||
yearsAgo: "{n}年前"
|
yearsAgo: "{n}年前"
|
||||||
invalid: "ありません"
|
invalid: "日時の解析に失敗"
|
||||||
|
|
||||||
_timeIn:
|
_timeIn:
|
||||||
seconds: "{n}秒後"
|
seconds: "{n}秒後"
|
||||||
|
|
8
packages/backend/generate_api_json.js
Normal file
8
packages/backend/generate_api_json.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import { loadConfig } from './built/config.js'
|
||||||
|
import { genOpenapiSpec } from './built/server/api/openapi/gen-spec.js'
|
||||||
|
import { writeFileSync } from "node:fs";
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
const spec = genOpenapiSpec(config);
|
||||||
|
|
||||||
|
writeFileSync('./built/api.json', JSON.stringify(spec), 'utf-8');
|
11
packages/backend/migration/1700383825690-hard-mute.js
Normal file
11
packages/backend/migration/1700383825690-hard-mute.js
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export class HardMute1700383825690 {
|
||||||
|
name = 'HardMute1700383825690'
|
||||||
|
|
||||||
|
async up(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" ADD "hardMutedWords" jsonb NOT NULL DEFAULT '[]'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
async down(queryRunner) {
|
||||||
|
await queryRunner.query(`ALTER TABLE "user_profile" DROP COLUMN "hardMutedWords"`);
|
||||||
|
}
|
||||||
|
}
|
|
@ -23,7 +23,8 @@
|
||||||
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
|
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit",
|
||||||
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
|
||||||
"test": "pnpm jest",
|
"test": "pnpm jest",
|
||||||
"test-and-coverage": "pnpm jest-and-coverage"
|
"test-and-coverage": "pnpm jest-and-coverage",
|
||||||
|
"generate-api-json": "node ./generate_api_json.js"
|
||||||
},
|
},
|
||||||
"optionalDependencies": {
|
"optionalDependencies": {
|
||||||
"@swc/core-android-arm64": "1.3.11",
|
"@swc/core-android-arm64": "1.3.11",
|
||||||
|
|
|
@ -250,6 +250,12 @@ export class MfmService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function fnDefault(node: mfm.MfmFn) {
|
||||||
|
const el = doc.createElement('i');
|
||||||
|
appendChildren(node.children, el);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
const handlers: { [K in mfm.MfmNode['type']]: (node: mfm.NodeType<K>) => any } = {
|
||||||
bold: (node) => {
|
bold: (node) => {
|
||||||
const el = doc.createElement('b');
|
const el = doc.createElement('b');
|
||||||
|
@ -276,17 +282,68 @@ export class MfmService {
|
||||||
},
|
},
|
||||||
|
|
||||||
fn: (node) => {
|
fn: (node) => {
|
||||||
if (node.props.name === 'unixtime') {
|
switch (node.props.name) {
|
||||||
const text = node.children[0]!.type === 'text' ? node.children[0].props.text : '';
|
case 'unixtime': {
|
||||||
const date = new Date(parseInt(text, 10) * 1000);
|
const text = node.children[0].type === 'text' ? node.children[0].props.text : '';
|
||||||
const el = doc.createElement('time');
|
try {
|
||||||
el.setAttribute('datetime', date.toISOString());
|
const date = new Date(parseInt(text, 10) * 1000);
|
||||||
el.textContent = date.toISOString();
|
const el = doc.createElement('time');
|
||||||
return el;
|
el.setAttribute('datetime', date.toISOString());
|
||||||
} else {
|
el.textContent = date.toISOString();
|
||||||
const el = doc.createElement('i');
|
return el;
|
||||||
appendChildren(node.children, el);
|
} catch (err) {
|
||||||
return el;
|
return fnDefault(node);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'ruby': {
|
||||||
|
if (node.children.length === 1) {
|
||||||
|
const child = node.children[0];
|
||||||
|
const text = child.type === 'text' ? child.props.text : '';
|
||||||
|
const rubyEl = doc.createElement('ruby');
|
||||||
|
const rtEl = doc.createElement('rt');
|
||||||
|
|
||||||
|
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
|
||||||
|
const rpStartEl = doc.createElement('rp');
|
||||||
|
rpStartEl.appendChild(doc.createTextNode('('));
|
||||||
|
const rpEndEl = doc.createElement('rp');
|
||||||
|
rpEndEl.appendChild(doc.createTextNode(')'));
|
||||||
|
|
||||||
|
rubyEl.appendChild(doc.createTextNode(text.split(' ')[0]));
|
||||||
|
rtEl.appendChild(doc.createTextNode(text.split(' ')[1]));
|
||||||
|
rubyEl.appendChild(rpStartEl);
|
||||||
|
rubyEl.appendChild(rtEl);
|
||||||
|
rubyEl.appendChild(rpEndEl);
|
||||||
|
return rubyEl;
|
||||||
|
} else {
|
||||||
|
const rt = node.children.at(-1);
|
||||||
|
|
||||||
|
if (!rt) {
|
||||||
|
return fnDefault(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
const text = rt.type === 'text' ? rt.props.text : '';
|
||||||
|
const rubyEl = doc.createElement('ruby');
|
||||||
|
const rtEl = doc.createElement('rt');
|
||||||
|
|
||||||
|
// ruby未対応のHTMLサニタイザーを通したときにルビが「劉備(りゅうび)」となるようにする
|
||||||
|
const rpStartEl = doc.createElement('rp');
|
||||||
|
rpStartEl.appendChild(doc.createTextNode('('));
|
||||||
|
const rpEndEl = doc.createElement('rp');
|
||||||
|
rpEndEl.appendChild(doc.createTextNode(')'));
|
||||||
|
|
||||||
|
appendChildren(node.children.slice(0, node.children.length - 1), rubyEl);
|
||||||
|
rtEl.appendChild(doc.createTextNode(text.trim()));
|
||||||
|
rubyEl.appendChild(rpStartEl);
|
||||||
|
rubyEl.appendChild(rtEl);
|
||||||
|
rubyEl.appendChild(rpEndEl);
|
||||||
|
return rubyEl;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return fnDefault(node);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -473,6 +473,7 @@ export class UserEntityService implements OnModuleInit {
|
||||||
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
hasPendingReceivedFollowRequest: this.getHasPendingReceivedFollowRequest(user.id),
|
||||||
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
unreadNotificationsCount: notificationsInfo?.unreadCount,
|
||||||
mutedWords: profile!.mutedWords,
|
mutedWords: profile!.mutedWords,
|
||||||
|
hardMutedWords: profile!.hardMutedWords,
|
||||||
mutedInstances: profile!.mutedInstances,
|
mutedInstances: profile!.mutedInstances,
|
||||||
mutingNotificationTypes: [], // 後方互換性のため
|
mutingNotificationTypes: [], // 後方互換性のため
|
||||||
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
notificationRecieveConfig: profile!.notificationRecieveConfig,
|
||||||
|
|
|
@ -215,7 +215,12 @@ export class MiUserProfile {
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
})
|
})
|
||||||
public mutedWords: string[][];
|
public mutedWords: (string[] | string)[];
|
||||||
|
|
||||||
|
@Column('jsonb', {
|
||||||
|
default: [],
|
||||||
|
})
|
||||||
|
public hardMutedWords: (string[] | string)[];
|
||||||
|
|
||||||
@Column('jsonb', {
|
@Column('jsonb', {
|
||||||
default: [],
|
default: [],
|
||||||
|
|
|
@ -534,6 +534,18 @@ export const packedMeDetailedOnlySchema = {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
hardMutedWords: {
|
||||||
|
type: 'array',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
items: {
|
||||||
|
type: 'array',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
items: {
|
||||||
|
type: 'string',
|
||||||
|
nullable: false, optional: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
mutedInstances: {
|
mutedInstances: {
|
||||||
type: 'array',
|
type: 'array',
|
||||||
nullable: true, optional: false,
|
nullable: true, optional: false,
|
||||||
|
|
|
@ -126,7 +126,7 @@ export class SignupApiService {
|
||||||
code: invitationCode,
|
code: invitationCode,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (ticket == null) {
|
if (ticket == null || ticket.usedById != null) {
|
||||||
reply.code(400);
|
reply.code(400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -16,12 +16,9 @@ export const meta = {
|
||||||
requireCredential: false,
|
requireCredential: false,
|
||||||
|
|
||||||
res: {
|
res: {
|
||||||
oneOf: [{
|
type: 'object',
|
||||||
type: 'object',
|
optional: false, nullable: true,
|
||||||
ref: 'FederationInstance',
|
ref: 'FederationInstance',
|
||||||
}, {
|
|
||||||
type: 'null',
|
|
||||||
}],
|
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
|
|
@ -124,6 +124,11 @@ export const meta = {
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const muteWordsType = { type: 'array', items: { oneOf: [
|
||||||
|
{ type: 'array', items: { type: 'string' } },
|
||||||
|
{ type: 'string' }
|
||||||
|
] } } as const;
|
||||||
|
|
||||||
export const paramDef = {
|
export const paramDef = {
|
||||||
type: 'object',
|
type: 'object',
|
||||||
properties: {
|
properties: {
|
||||||
|
@ -172,7 +177,8 @@ export const paramDef = {
|
||||||
autoSensitive: { type: 'boolean' },
|
autoSensitive: { type: 'boolean' },
|
||||||
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
ffVisibility: { type: 'string', enum: ['public', 'followers', 'private'] },
|
||||||
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
pinnedPageId: { type: 'string', format: 'misskey:id', nullable: true },
|
||||||
mutedWords: { type: 'array' },
|
mutedWords: muteWordsType,
|
||||||
|
hardMutedWords: muteWordsType,
|
||||||
mutedInstances: { type: 'array', items: {
|
mutedInstances: { type: 'array', items: {
|
||||||
type: 'string',
|
type: 'string',
|
||||||
} },
|
} },
|
||||||
|
@ -235,15 +241,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
if (ps.location !== undefined) profileUpdates.location = ps.location;
|
||||||
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
if (ps.birthday !== undefined) profileUpdates.birthday = ps.birthday;
|
||||||
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
|
if (ps.ffVisibility !== undefined) profileUpdates.ffVisibility = ps.ffVisibility;
|
||||||
if (ps.mutedWords !== undefined) {
|
|
||||||
const length = ps.mutedWords.length;
|
function checkMuteWordCount(mutedWords: (string[] | string)[], limit: number) {
|
||||||
if (length > (await this.roleService.getUserPolicies(user.id)).wordMuteLimit) {
|
const length = mutedWords.length;
|
||||||
|
if (length > limit) {
|
||||||
throw new ApiError(meta.errors.tooManyMutedWords);
|
throw new ApiError(meta.errors.tooManyMutedWords);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// validate regular expression syntax
|
function validateMuteWordRegex(mutedWords: (string[] | string)[]) {
|
||||||
ps.mutedWords.filter(x => !Array.isArray(x)).forEach(x => {
|
for (const mutedWord of mutedWords) {
|
||||||
const regexp = x.match(/^\/(.+)\/(.*)$/);
|
if (typeof mutedWord !== "string") continue;
|
||||||
|
|
||||||
|
const regexp = mutedWord.match(/^\/(.+)\/(.*)$/);
|
||||||
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
if (!regexp) throw new ApiError(meta.errors.invalidRegexp);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
@ -251,11 +261,21 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
throw new ApiError(meta.errors.invalidRegexp);
|
throw new ApiError(meta.errors.invalidRegexp);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ps.mutedWords !== undefined) {
|
||||||
|
checkMuteWordCount(ps.mutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
||||||
|
validateMuteWordRegex(ps.mutedWords);
|
||||||
|
|
||||||
profileUpdates.mutedWords = ps.mutedWords;
|
profileUpdates.mutedWords = ps.mutedWords;
|
||||||
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
profileUpdates.enableWordMute = ps.mutedWords.length > 0;
|
||||||
}
|
}
|
||||||
|
if (ps.hardMutedWords !== undefined) {
|
||||||
|
checkMuteWordCount(ps.hardMutedWords, (await this.roleService.getUserPolicies(user.id)).wordMuteLimit);
|
||||||
|
validateMuteWordRegex(ps.hardMutedWords);
|
||||||
|
profileUpdates.hardMutedWords = ps.hardMutedWords;
|
||||||
|
}
|
||||||
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
if (ps.mutedInstances !== undefined) profileUpdates.mutedInstances = ps.mutedInstances;
|
||||||
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
|
if (ps.notificationRecieveConfig !== undefined) profileUpdates.notificationRecieveConfig = ps.notificationRecieveConfig;
|
||||||
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
if (typeof ps.isLocked === 'boolean') updates.isLocked = ps.isLocked;
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Config } from '@/config.js';
|
import type { Config } from '@/config.js';
|
||||||
import endpoints from '../endpoints.js';
|
import endpoints, { IEndpoint } from '../endpoints.js';
|
||||||
import { errors as basicErrors } from './errors.js';
|
import { errors as basicErrors } from './errors.js';
|
||||||
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
|
import { schemas, convertSchemaToOpenApiSchema } from './schemas.js';
|
||||||
|
|
||||||
|
@ -33,16 +33,17 @@ export function genOpenapiSpec(config: Config) {
|
||||||
schemas: schemas,
|
schemas: schemas,
|
||||||
|
|
||||||
securitySchemes: {
|
securitySchemes: {
|
||||||
ApiKeyAuth: {
|
bearerAuth: {
|
||||||
type: 'apiKey',
|
type: 'http',
|
||||||
in: 'body',
|
scheme: 'bearer',
|
||||||
name: 'i',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
for (const endpoint of endpoints.filter(ep => !ep.meta.secure)) {
|
// 書き換えたりするのでディープコピーしておく。そのまま編集するとメモリ上の値が汚れて次回以降の出力に影響する
|
||||||
|
const copiedEndpoints = JSON.parse(JSON.stringify(endpoints)) as IEndpoint[];
|
||||||
|
for (const endpoint of copiedEndpoints.filter(ep => !ep.meta.secure)) {
|
||||||
const errors = {} as any;
|
const errors = {} as any;
|
||||||
|
|
||||||
if (endpoint.meta.errors) {
|
if (endpoint.meta.errors) {
|
||||||
|
@ -79,6 +80,13 @@ export function genOpenapiSpec(config: Config) {
|
||||||
schema.required = [...schema.required ?? [], 'file'];
|
schema.required = [...schema.required ?? [], 'file'];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (schema.required && schema.required.length <= 0) {
|
||||||
|
// 空配列は許可されない
|
||||||
|
schema.required = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasBody = (schema.type === 'object' && schema.properties && Object.keys(schema.properties).length >= 1);
|
||||||
|
|
||||||
const info = {
|
const info = {
|
||||||
operationId: endpoint.name,
|
operationId: endpoint.name,
|
||||||
summary: endpoint.name,
|
summary: endpoint.name,
|
||||||
|
@ -92,17 +100,19 @@ export function genOpenapiSpec(config: Config) {
|
||||||
} : {}),
|
} : {}),
|
||||||
...(endpoint.meta.requireCredential ? {
|
...(endpoint.meta.requireCredential ? {
|
||||||
security: [{
|
security: [{
|
||||||
ApiKeyAuth: [],
|
bearerAuth: [],
|
||||||
}],
|
}],
|
||||||
} : {}),
|
} : {}),
|
||||||
requestBody: {
|
...(hasBody ? {
|
||||||
required: true,
|
requestBody: {
|
||||||
content: {
|
required: true,
|
||||||
[requestType]: {
|
content: {
|
||||||
schema,
|
[requestType]: {
|
||||||
|
schema,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
} : {}),
|
||||||
responses: {
|
responses: {
|
||||||
...(endpoint.meta.res ? {
|
...(endpoint.meta.res ? {
|
||||||
'200': {
|
'200': {
|
||||||
|
@ -118,6 +128,11 @@ export function genOpenapiSpec(config: Config) {
|
||||||
description: 'OK (without any results)',
|
description: 'OK (without any results)',
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
...(endpoint.meta.res?.optional === true || endpoint.meta.res?.nullable === true ? {
|
||||||
|
'204': {
|
||||||
|
description: 'OK (without any results)',
|
||||||
|
},
|
||||||
|
} : {}),
|
||||||
'400': {
|
'400': {
|
||||||
description: 'Client error',
|
description: 'Client error',
|
||||||
content: {
|
content: {
|
||||||
|
@ -190,6 +205,7 @@ export function genOpenapiSpec(config: Config) {
|
||||||
};
|
};
|
||||||
|
|
||||||
spec.paths['/' + endpoint.name] = {
|
spec.paths['/' + endpoint.name] = {
|
||||||
|
...(endpoint.meta.allowGet ? { get: info } : {}),
|
||||||
post: info,
|
post: info,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,10 +7,16 @@ import type { Schema } from '@/misc/json-schema.js';
|
||||||
import { refs } from '@/misc/json-schema.js';
|
import { refs } from '@/misc/json-schema.js';
|
||||||
|
|
||||||
export function convertSchemaToOpenApiSchema(schema: Schema) {
|
export function convertSchemaToOpenApiSchema(schema: Schema) {
|
||||||
const res: any = schema;
|
// optional, refはスキーマ定義に含まれないので分離しておく
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { optional, ref, ...res }: any = schema;
|
||||||
|
|
||||||
if (schema.type === 'object' && schema.properties) {
|
if (schema.type === 'object' && schema.properties) {
|
||||||
res.required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
|
const required = Object.entries(schema.properties).filter(([k, v]) => !v.optional).map(([k]) => k);
|
||||||
|
if (required.length > 0) {
|
||||||
|
// 空配列は許可されない
|
||||||
|
res.required = required;
|
||||||
|
}
|
||||||
|
|
||||||
for (const k of Object.keys(schema.properties)) {
|
for (const k of Object.keys(schema.properties)) {
|
||||||
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
|
res.properties[k] = convertSchemaToOpenApiSchema(schema.properties[k]);
|
||||||
|
|
|
@ -169,6 +169,7 @@ describe('ユーザー', () => {
|
||||||
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
|
hasPendingReceivedFollowRequest: user.hasPendingReceivedFollowRequest,
|
||||||
unreadAnnouncements: user.unreadAnnouncements,
|
unreadAnnouncements: user.unreadAnnouncements,
|
||||||
mutedWords: user.mutedWords,
|
mutedWords: user.mutedWords,
|
||||||
|
hardMutedWords: user.hardMutedWords,
|
||||||
mutedInstances: user.mutedInstances,
|
mutedInstances: user.mutedInstances,
|
||||||
mutingNotificationTypes: user.mutingNotificationTypes,
|
mutingNotificationTypes: user.mutingNotificationTypes,
|
||||||
notificationRecieveConfig: user.notificationRecieveConfig,
|
notificationRecieveConfig: user.notificationRecieveConfig,
|
||||||
|
|
|
@ -242,29 +242,7 @@ function exec() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const matched: EmojiDef[] = [];
|
emojis.value = emojiAutoComplete(props.q, emojiDb.value);
|
||||||
const max = 30;
|
|
||||||
|
|
||||||
emojiDb.value.some(x => {
|
|
||||||
if (x.name.startsWith(props.q ?? '') && !x.aliasOf && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
|
||||||
return matched.length === max;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (matched.length < max) {
|
|
||||||
emojiDb.value.some(x => {
|
|
||||||
if (x.name.startsWith(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
|
||||||
return matched.length === max;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (matched.length < max) {
|
|
||||||
emojiDb.value.some(x => {
|
|
||||||
if (x.name.includes(props.q ?? '') && !matched.some(y => y.emoji === x.emoji)) matched.push(x);
|
|
||||||
return matched.length === max;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
emojis.value = matched;
|
|
||||||
} else if (props.type === 'mfmTag') {
|
} else if (props.type === 'mfmTag') {
|
||||||
if (!props.q || props.q === '') {
|
if (!props.q || props.q === '') {
|
||||||
mfmTags.value = MFM_TAGS;
|
mfmTags.value = MFM_TAGS;
|
||||||
|
@ -275,6 +253,82 @@ function exec() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type EmojiScore = { emoji: EmojiDef, score: number };
|
||||||
|
|
||||||
|
function emojiAutoComplete(query: string | null, emojiDb: EmojiDef[], max = 30): EmojiDef[] {
|
||||||
|
if (!query) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const matched = new Map<string, EmojiScore>();
|
||||||
|
|
||||||
|
// 前方一致(エイリアスなし)
|
||||||
|
emojiDb.some(x => {
|
||||||
|
if (x.name.startsWith(query) && !x.aliasOf) {
|
||||||
|
matched.set(x.name, { emoji: x, score: query.length });
|
||||||
|
}
|
||||||
|
return matched.size === max;
|
||||||
|
});
|
||||||
|
|
||||||
|
// 前方一致(エイリアス込み)
|
||||||
|
if (matched.size < max) {
|
||||||
|
emojiDb.some(x => {
|
||||||
|
if (x.name.startsWith(query)) {
|
||||||
|
matched.set(x.name, { emoji: x, score: query.length });
|
||||||
|
}
|
||||||
|
return matched.size === max;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 部分一致(エイリアス込み)
|
||||||
|
if (matched.size < max) {
|
||||||
|
emojiDb.some(x => {
|
||||||
|
if (x.name.includes(query)) {
|
||||||
|
matched.set(x.name, { emoji: x, score: query.length });
|
||||||
|
}
|
||||||
|
return matched.size === max;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 簡易あいまい検索
|
||||||
|
if (matched.size < max) {
|
||||||
|
const queryChars = [...query];
|
||||||
|
const hitEmojis = new Map<string, EmojiScore>();
|
||||||
|
|
||||||
|
for (const x of emojiDb) {
|
||||||
|
// クエリ文字列の1文字単位で絵文字名にヒットするかを見る
|
||||||
|
// ただし、過剰に検出されるのを防ぐためクエリ文字列に登場する順番で絵文字名を走査する
|
||||||
|
|
||||||
|
let queryCharHitPos = 0;
|
||||||
|
let queryCharHitCount = 0;
|
||||||
|
for (let idx = 0; idx < queryChars.length; idx++) {
|
||||||
|
queryCharHitPos = x.name.indexOf(queryChars[idx], queryCharHitPos);
|
||||||
|
if (queryCharHitPos <= -1) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
queryCharHitCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ヒット数が少なすぎると検索結果が汚れるので調節する
|
||||||
|
if (queryCharHitCount > 2) {
|
||||||
|
hitEmojis.set(x.name, { emoji: x, score: queryCharHitCount });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ヒットしたものを全部追加すると雑多になるので、先頭の6件程度だけにしておく(6件=オートコンプリートのポップアップのサイズ分)
|
||||||
|
[...hitEmojis.values()]
|
||||||
|
.sort((x, y) => y.score - x.score)
|
||||||
|
.slice(0, 6)
|
||||||
|
.forEach(it => matched.set(it.emoji.name, it));
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...matched.values()]
|
||||||
|
.sort((x, y) => y.score - x.score)
|
||||||
|
.slice(0, max)
|
||||||
|
.map(it => it.emoji);
|
||||||
|
}
|
||||||
|
|
||||||
function onMousedown(event: Event) {
|
function onMousedown(event: Event) {
|
||||||
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
|
if (!contains(rootEl.value, event.target) && (rootEl.value !== event.target)) props.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -141,6 +141,10 @@ watch(v, () => {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.textarea, .codeEditorHighlighter {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.textarea {
|
.textarea {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
@ -156,6 +160,8 @@ watch(v, () => {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
border: 0;
|
border: 0;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
|
min-width: calc(100% - 24px);
|
||||||
|
height: calc(100% - 24px);
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
line-height: 1.5em;
|
line-height: 1.5em;
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
|
|
|
@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
v-if="!muted"
|
v-if="!hardMuted && !muted"
|
||||||
v-show="!isDeleted"
|
v-show="!isDeleted"
|
||||||
ref="el"
|
ref="el"
|
||||||
v-hotkey="keymap"
|
v-hotkey="keymap"
|
||||||
|
@ -133,7 +133,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
</div>
|
</div>
|
||||||
</article>
|
</article>
|
||||||
</div>
|
</div>
|
||||||
<div v-else :class="$style.muted" @click="muted = false">
|
<div v-else-if="!hardMuted" :class="$style.muted" @click="muted = false">
|
||||||
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
<I18n :src="i18n.ts.userSaysSomething" tag="small">
|
||||||
<template #name>
|
<template #name>
|
||||||
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
<MkA v-user-preview="appearNote.userId" :to="userPage(appearNote.user)">
|
||||||
|
@ -183,6 +183,7 @@ const props = withDefaults(defineProps<{
|
||||||
note: Misskey.entities.Note;
|
note: Misskey.entities.Note;
|
||||||
pinned?: boolean;
|
pinned?: boolean;
|
||||||
mock?: boolean;
|
mock?: boolean;
|
||||||
|
withHardMute?: boolean;
|
||||||
}>(), {
|
}>(), {
|
||||||
mock: false,
|
mock: false,
|
||||||
});
|
});
|
||||||
|
@ -239,13 +240,23 @@ const urls = $computed(() => parsed ? extractUrlFromMfm(parsed) : null);
|
||||||
const isLong = shouldCollapsed(appearNote, urls ?? []);
|
const isLong = shouldCollapsed(appearNote, urls ?? []);
|
||||||
const collapsed = ref(appearNote.cw == null && isLong);
|
const collapsed = ref(appearNote.cw == null && isLong);
|
||||||
const isDeleted = ref(false);
|
const isDeleted = ref(false);
|
||||||
const muted = ref($i ? checkWordMute(appearNote, $i, $i.mutedWords) : false);
|
const muted = ref(checkMute(appearNote, $i?.mutedWords));
|
||||||
|
const hardMuted = ref(props.withHardMute && checkMute(appearNote, $i?.hardMutedWords));
|
||||||
const translation = ref<any>(null);
|
const translation = ref<any>(null);
|
||||||
const translating = ref(false);
|
const translating = ref(false);
|
||||||
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultStore.state.instanceTicker === 'remote' && appearNote.user.instance);
|
||||||
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
|
const canRenote = computed(() => ['public', 'home'].includes(appearNote.visibility) || (appearNote.visibility === 'followers' && appearNote.userId === $i.id));
|
||||||
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
let renoteCollapsed = $ref(defaultStore.state.collapseRenotes && isRenote && (($i && ($i.id === note.userId || $i.id === appearNote.userId)) || (appearNote.myReaction != null)));
|
||||||
|
|
||||||
|
function checkMute(note: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null): boolean {
|
||||||
|
if (mutedWords == null) return false;
|
||||||
|
|
||||||
|
if (checkWordMute(note, $i, mutedWords)) return true;
|
||||||
|
if (note.reply && checkWordMute(note.reply, $i, mutedWords)) return true;
|
||||||
|
if (note.renote && checkWordMute(note.renote, $i, mutedWords)) return true;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
const keymap = {
|
const keymap = {
|
||||||
'r': () => reply(true),
|
'r': () => reply(true),
|
||||||
'e|a|plus': () => react(true),
|
'e|a|plus': () => react(true),
|
||||||
|
|
|
@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:ad="true"
|
:ad="true"
|
||||||
:class="$style.notes"
|
:class="$style.notes"
|
||||||
>
|
>
|
||||||
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note"/>
|
<MkNote :key="note._featuredId_ || note._prId_ || note.id" :class="$style.note" :note="note" :withHardMute="true"/>
|
||||||
</MkDateSeparatedList>
|
</MkDateSeparatedList>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<template #default="{ items: notifications }">
|
<template #default="{ items: notifications }">
|
||||||
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
|
||||||
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
|
<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note" :withHardMute="true"/>
|
||||||
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
|
<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel"/>
|
||||||
</MkDateSeparatedList>
|
</MkDateSeparatedList>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -28,12 +28,25 @@ const props = withDefaults(defineProps<{
|
||||||
mode: 'relative',
|
mode: 'relative',
|
||||||
});
|
});
|
||||||
|
|
||||||
const _time = props.time == null ? NaN :
|
function getDateSafe(n: Date | string | number) {
|
||||||
typeof props.time === 'number' ? props.time :
|
try {
|
||||||
(props.time instanceof Date ? props.time : new Date(props.time)).getTime();
|
if (n instanceof Date) {
|
||||||
|
return n;
|
||||||
|
}
|
||||||
|
return new Date(n);
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
getTime: () => NaN,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||||
|
const _time = props.time == null ? NaN : getDateSafe(props.time).getTime();
|
||||||
const invalid = Number.isNaN(_time);
|
const invalid = Number.isNaN(_time);
|
||||||
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
const absolute = !invalid ? dateTimeFormat.format(_time) : i18n.ts._ago.invalid;
|
||||||
|
|
||||||
|
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||||
let now = $ref((props.origin ?? new Date()).getTime());
|
let now = $ref((props.origin ?? new Date()).getTime());
|
||||||
const ago = $computed(() => (now - _time) / 1000/*ms*/);
|
const ago = $computed(() => (now - _time) / 1000/*ms*/);
|
||||||
|
|
||||||
|
|
|
@ -65,9 +65,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar">
|
<img src="https://avatars.githubusercontent.com/u/67428053?v=4" :class="$style.contributorAvatar">
|
||||||
<span :class="$style.contributorUsername">@kakkokari-gtyih</span>
|
<span :class="$style.contributorUsername">@kakkokari-gtyih</span>
|
||||||
</a>
|
</a>
|
||||||
<a href="https://github.com/taichanNE30" target="_blank" :class="$style.contributor">
|
<a href="https://github.com/tai-cha" target="_blank" :class="$style.contributor">
|
||||||
<img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar">
|
<img src="https://avatars.githubusercontent.com/u/40626578?v=4" :class="$style.contributorAvatar">
|
||||||
<span :class="$style.contributorUsername">@taichanNE30</span>
|
<span :class="$style.contributorUsername">@tai-cha</span>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</FormSection>
|
</FormSection>
|
||||||
|
|
|
@ -9,7 +9,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<template #icon><i class="ti ti-message-off"></i></template>
|
<template #icon><i class="ti ti-message-off"></i></template>
|
||||||
<template #label>{{ i18n.ts.wordMute }}</template>
|
<template #label>{{ i18n.ts.wordMute }}</template>
|
||||||
|
|
||||||
<XWordMute/>
|
<XWordMute :muted="$i!.mutedWords" @save="saveMutedWords"/>
|
||||||
|
</MkFolder>
|
||||||
|
|
||||||
|
<MkFolder>
|
||||||
|
<template #icon><i class="ti ti-message-off"></i></template>
|
||||||
|
<template #label>{{ i18n.ts.hardWordMute }}</template>
|
||||||
|
|
||||||
|
<XWordMute :muted="$i!.hardMutedWords" @save="saveHardMutedWords"/>
|
||||||
</MkFolder>
|
</MkFolder>
|
||||||
|
|
||||||
<MkFolder>
|
<MkFolder>
|
||||||
|
@ -129,6 +136,7 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
|
||||||
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
import MkUserCardMini from '@/components/MkUserCardMini.vue';
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import { infoImageUrl } from '@/instance.js';
|
import { infoImageUrl } from '@/instance.js';
|
||||||
|
import { $i } from '@/account.js';
|
||||||
import MkFolder from '@/components/MkFolder.vue';
|
import MkFolder from '@/components/MkFolder.vue';
|
||||||
|
|
||||||
const renoteMutingPagination = {
|
const renoteMutingPagination = {
|
||||||
|
@ -207,6 +215,14 @@ async function toggleBlockItem(item) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function saveMutedWords(mutedWords: (string | string[])[]) {
|
||||||
|
await os.api('i/update', { mutedWords });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveHardMutedWords(hardMutedWords: (string | string[])[]) {
|
||||||
|
await os.api('i/update', { hardMutedWords });
|
||||||
|
}
|
||||||
|
|
||||||
const headerActions = $computed(() => []);
|
const headerActions = $computed(() => []);
|
||||||
|
|
||||||
const headerTabs = $computed(() => []);
|
const headerTabs = $computed(() => []);
|
||||||
|
|
|
@ -18,16 +18,17 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { ref, watch } from 'vue';
|
import { ref, watch } from 'vue';
|
||||||
import MkTextarea from '@/components/MkTextarea.vue';
|
import MkTextarea from '@/components/MkTextarea.vue';
|
||||||
import MkKeyValue from '@/components/MkKeyValue.vue';
|
|
||||||
import MkButton from '@/components/MkButton.vue';
|
import MkButton from '@/components/MkButton.vue';
|
||||||
import MkInfo from '@/components/MkInfo.vue';
|
|
||||||
import MkTab from '@/components/MkTab.vue';
|
|
||||||
import * as os from '@/os.js';
|
import * as os from '@/os.js';
|
||||||
import number from '@/filters/number.js';
|
|
||||||
import { defaultStore } from '@/store.js';
|
|
||||||
import { $i } from '@/account.js';
|
|
||||||
import { i18n } from '@/i18n.js';
|
import { i18n } from '@/i18n.js';
|
||||||
import { definePageMetadata } from '@/scripts/page-metadata.js';
|
|
||||||
|
const props = defineProps<{
|
||||||
|
muted: (string[] | string)[];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(ev: 'save', value: (string[] | string)[]): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
const render = (mutedWords) => mutedWords.map(x => {
|
const render = (mutedWords) => mutedWords.map(x => {
|
||||||
if (Array.isArray(x)) {
|
if (Array.isArray(x)) {
|
||||||
|
@ -37,8 +38,7 @@ const render = (mutedWords) => mutedWords.map(x => {
|
||||||
}
|
}
|
||||||
}).join('\n');
|
}).join('\n');
|
||||||
|
|
||||||
const tab = ref('soft');
|
const mutedWords = ref(render(props.muted));
|
||||||
const mutedWords = ref(render($i!.mutedWords));
|
|
||||||
const changed = ref(false);
|
const changed = ref(false);
|
||||||
|
|
||||||
watch(mutedWords, () => {
|
watch(mutedWords, () => {
|
||||||
|
@ -85,9 +85,7 @@ async function save() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await os.api('i/update', {
|
emit('save', parsed);
|
||||||
mutedWords: parsed,
|
|
||||||
});
|
|
||||||
|
|
||||||
changed.value = false;
|
changed.value = false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,14 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
<MkLoading v-if="fetching"/>
|
<MkLoading v-if="fetching"/>
|
||||||
<div v-if="!fetching && files.length > 0" :class="$style.stream">
|
<div v-if="!fetching && files.length > 0" :class="$style.stream">
|
||||||
<template v-for="file in files" :key="file.note.id + file.file.id">
|
<template v-for="file in files" :key="file.note.id + file.file.id">
|
||||||
<div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.sensitive" @click="showingFiles.push(file.file.id)">
|
<div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)">
|
||||||
<div>
|
<!-- TODO: 画像以外のファイルに対応 -->
|
||||||
<b style="display: block;"><i class="ti ti-eye-exclamation"/> {{ i18n.ts.sensitive }}</b>
|
<ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/>
|
||||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
<div :class="$style.sensitive">
|
||||||
|
<div>
|
||||||
|
<div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div>
|
||||||
|
<div>{{ i18n.ts.clickToShow }}</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MkA v-else :class="$style.img" :to="notePage(file.note)">
|
<MkA v-else :class="$style.img" :to="notePage(file.note)">
|
||||||
|
@ -88,6 +92,7 @@ onMounted(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.img {
|
.img {
|
||||||
|
position: relative;
|
||||||
height: 128px;
|
height: 128px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
overflow: clip;
|
overflow: clip;
|
||||||
|
@ -99,16 +104,25 @@ onMounted(() => {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sensitive {
|
.sensitiveImg {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 128px;
|
height: 100%;
|
||||||
display: flex;
|
filter: brightness(0.7);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sensitive {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
font-size: 0.8em;
|
font-size: 0.8em;
|
||||||
text-align: center;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: rgba(0, 0, 0, 0.5);
|
cursor: pointer;
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -1587,7 +1587,8 @@ export type Endpoints = {
|
||||||
injectFeaturedNote?: boolean;
|
injectFeaturedNote?: boolean;
|
||||||
receiveAnnouncementEmail?: boolean;
|
receiveAnnouncementEmail?: boolean;
|
||||||
alwaysMarkNsfw?: boolean;
|
alwaysMarkNsfw?: boolean;
|
||||||
mutedWords?: string[][];
|
mutedWords?: (string[] | string)[];
|
||||||
|
hardMutedWords?: (string[] | string)[];
|
||||||
notificationRecieveConfig?: any;
|
notificationRecieveConfig?: any;
|
||||||
emailNotificationTypes?: string[];
|
emailNotificationTypes?: string[];
|
||||||
alsoKnownAs?: string[];
|
alsoKnownAs?: string[];
|
||||||
|
@ -2543,7 +2544,8 @@ type MeDetailed = UserDetailed & {
|
||||||
integrations: Record<string, any>;
|
integrations: Record<string, any>;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
isExplorable: boolean;
|
isExplorable: boolean;
|
||||||
mutedWords: string[][];
|
mutedWords: (string[] | string)[];
|
||||||
|
hardMutedWords: (string[] | string)[];
|
||||||
notificationRecieveConfig: {
|
notificationRecieveConfig: {
|
||||||
[notificationType in typeof notificationTypes_2[number]]?: {
|
[notificationType in typeof notificationTypes_2[number]]?: {
|
||||||
type: 'all';
|
type: 'all';
|
||||||
|
@ -3082,9 +3084,9 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
|
||||||
//
|
//
|
||||||
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
|
||||||
// src/api.types.ts:638:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
// src/api.types.ts:639:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
|
||||||
// src/entities.ts:119:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:120:2 - (ae-forgotten-export) The symbol "notificationTypes_2" needs to be exported by the entry point index.d.ts
|
||||||
// src/entities.ts:637:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
// src/entities.ts:638:2 - (ae-forgotten-export) The symbol "ModerationLogPayloads" needs to be exported by the entry point index.d.ts
|
||||||
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
// src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
|
||||||
|
|
||||||
// (No @packageDocumentation comment for this package)
|
// (No @packageDocumentation comment for this package)
|
||||||
|
|
|
@ -436,7 +436,8 @@ export type Endpoints = {
|
||||||
injectFeaturedNote?: boolean;
|
injectFeaturedNote?: boolean;
|
||||||
receiveAnnouncementEmail?: boolean;
|
receiveAnnouncementEmail?: boolean;
|
||||||
alwaysMarkNsfw?: boolean;
|
alwaysMarkNsfw?: boolean;
|
||||||
mutedWords?: string[][];
|
mutedWords?: (string[] | string)[];
|
||||||
|
hardMutedWords?: (string[] | string)[];
|
||||||
notificationRecieveConfig?: any;
|
notificationRecieveConfig?: any;
|
||||||
emailNotificationTypes?: string[];
|
emailNotificationTypes?: string[];
|
||||||
alsoKnownAs?: string[];
|
alsoKnownAs?: string[];
|
||||||
|
|
|
@ -115,7 +115,8 @@ export type MeDetailed = UserDetailed & {
|
||||||
integrations: Record<string, any>;
|
integrations: Record<string, any>;
|
||||||
isDeleted: boolean;
|
isDeleted: boolean;
|
||||||
isExplorable: boolean;
|
isExplorable: boolean;
|
||||||
mutedWords: string[][];
|
mutedWords: (string[] | string)[];
|
||||||
|
hardMutedWords: (string[] | string)[];
|
||||||
notificationRecieveConfig: {
|
notificationRecieveConfig: {
|
||||||
[notificationType in typeof notificationTypes[number]]?: {
|
[notificationType in typeof notificationTypes[number]]?: {
|
||||||
type: 'all';
|
type: 'all';
|
||||||
|
|
Loading…
Reference in a new issue