Merge branch 'misskey-dev:develop' into develop

This commit is contained in:
老兄 2023-12-22 22:52:27 +08:00 committed by GitHub
commit 1360cbe309
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
166 changed files with 1527 additions and 332 deletions

View File

@ -14,7 +14,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@ -37,7 +37,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -19,7 +19,7 @@ jobs:
with: with:
version: 8 version: 8
run_install: false run_install: false
- uses: actions/setup-node@v4.0.0 - uses: actions/setup-node@v4.0.1
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -46,7 +46,7 @@ jobs:
with: with:
version: 7 version: 7
run_install: false run_install: false
- uses: actions/setup-node@v4.0.0 - uses: actions/setup-node@v4.0.1
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'
@ -72,7 +72,7 @@ jobs:
with: with:
version: 7 version: 7
run_install: false run_install: false
- uses: actions/setup-node@v4.0.0 - uses: actions/setup-node@v4.0.1
with: with:
node-version-file: '.node-version' node-version-file: '.node-version'
cache: 'pnpm' cache: 'pnpm'

View File

@ -17,7 +17,7 @@ jobs:
services: services:
postgres: postgres:
image: postgres:13 image: postgres:15
ports: ports:
- 54312:5432 - 54312:5432
env: env:
@ -38,7 +38,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -25,7 +25,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'
@ -56,7 +56,7 @@ jobs:
services: services:
postgres: postgres:
image: postgres:13 image: postgres:15
ports: ports:
- 54312:5432 - 54312:5432
env: env:
@ -83,7 +83,7 @@ jobs:
version: 7 version: 7
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -26,7 +26,7 @@ jobs:
- run: corepack enable - run: corepack enable
- name: Setup Node.js ${{ matrix.node-version }} - name: Setup Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -28,7 +28,7 @@ jobs:
version: 8 version: 8
run_install: false run_install: false
- name: Use Node.js ${{ matrix.node-version }} - name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v4.0.0 uses: actions/setup-node@v4.0.1
with: with:
node-version: ${{ matrix.node-version }} node-version: ${{ matrix.node-version }}
cache: 'pnpm' cache: 'pnpm'

View File

@ -5,18 +5,18 @@
- -
### Client ### Client
- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正 -
- Fix: MFMでルビの中のテキストがnyaizeされない問題を修正
### Server ### Server
- -
--> -->
## 2023.x.x (unreleased) ## 2023.12.0
### Note ### Note
- Node.js 20.10.0が最小要件になりました - Node.js 20.10.0が最小要件になりました
- 絵文字の追加辞書を既にインストールしている場合は、お手数ですが再インストールのほどお願いします
- 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。 - 絵文字ピッカーにピン留め表示する絵文字設定が「リアクション用」と「絵文字入力用」に分かれました。以前の設定は「リアクション用」として使用されます。
**影響:** **影響:**
@ -31,9 +31,12 @@
- 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)
- Feat: TL上からートが見えなくなるワードミュートであるハードミュートを追加 - Feat: TL上からートが見えなくなるワードミュートであるハードミュートを追加
- Enhance: 公開ロールにアサインされたときに通知が作成されるように
- Enhance: アイコンデコレーションを複数設定できるように - Enhance: アイコンデコレーションを複数設定できるように
- Enhance: アイコンデコレーションの位置を微調整できるように - Enhance: アイコンデコレーションの位置を微調整できるように
- Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072 - Enhance: つながりの公開範囲をフォロー/フォロワーで個別に設定可能に #12072
- Enhance: ローカリゼーションの更新
- Enhance: 依存関係の更新
- Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正 - Fix: MFM `$[unixtime ]` に不正な値を入力した際に発生する各種エラーを修正
### Client ### Client
@ -59,12 +62,16 @@
- Enhance: ユーザー名、プロフィール、お知らせ、ページの編集画面でMFMや絵文字のオートコンプリートが使用できるように - Enhance: ユーザー名、プロフィール、お知らせ、ページの編集画面でMFMや絵文字のオートコンプリートが使用できるように
- Enhance: プロフィール、お知らせの編集画面でMFMのプレビューを表示できるように - Enhance: プロフィール、お知らせの編集画面でMFMのプレビューを表示できるように
- Enhance: 絵文字の詳細ページに記載される情報を追加 - Enhance: 絵文字の詳細ページに記載される情報を追加
- Enhance: リアクションの表示幅制限を設定可能に
- Enhance: Unicode 15.0のサポート - Enhance: Unicode 15.0のサポート
- Enhance: コードブロックのハイライト機能を利用するには言語を明示的に指定させるように - Enhance: コードブロックのハイライト機能を利用するには言語を明示的に指定させるように
- MFMでコードブロックを利用する際に意図しないハイライトが起こらないようになりました - MFMでコードブロックを利用する際に意図しないハイライトが起こらないようになりました
- 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります - 逆に、MFMでコードハイライトを利用したい際は言語を明示的に指定する必要があります
(例: ` ```js ` → Javascript, ` ```ais ` → AiScript (例: ` ```js ` → Javascript, ` ```ais ` → AiScript
- Enhance: 絵文字などのオートコンプリートでShift+Tabを押すと前の候補を選択できるように - Enhance: 絵文字などのオートコンプリートでShift+Tabを押すと前の候補を選択できるように
- Enhance: チャンネルに新規の投稿がある場合にバッジを表示させる
- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加
- Enhance: 設定したタグをトレンドに表示させないようにする項目を管理画面で設定できるように
- Fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正 - Fix: 「設定のバックアップ」で一部の項目がバックアップに含まれていなかった問題を修正
- Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367 - Fix: ウィジェットのジョブキューにて音声の発音方法変更に追従できていなかったのを修正 #12367
- Fix: コードエディタが正しく表示されない問題を修正 - Fix: コードエディタが正しく表示されない問題を修正
@ -80,10 +87,16 @@
- Fix: 投票のみ/画像のみの引用RNが、通知欄でただのRNとして判定されるバグを修正 - Fix: 投票のみ/画像のみの引用RNが、通知欄でただのRNとして判定されるバグを修正
- Fix: CWをつけて引用RNしても、普通のRNとして扱われてしまうバグを修正しました。 - Fix: CWをつけて引用RNしても、普通のRNとして扱われてしまうバグを修正しました。
- Fix: 「画像が1枚のみのメディアリストの高さ」を「デフォルト」以外に設定していると、CWの中などに添付された画像が見られないバグを修正 - Fix: 「画像が1枚のみのメディアリストの高さ」を「デフォルト」以外に設定していると、CWの中などに添付された画像が見られないバグを修正
- Fix: DeepL TranslationのPro accountトグルスイッチが表示されていなかったのを修正
- Fix: twitterの埋め込みカード内リンクからリンク先を開けない問題を修正
- Fix: WebKitブラウザー上でも「デバイスの画面を常にオンにする」機能が効くように
- Fix: ページ一覧ページの表示がモバイル環境において崩れているのを修正
- Fix: MFMでルビの中のテキストがnyaizeされない問題を修正
### Server ### Server
- Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように - Enhance: MFM `$[ruby ]` が他ソフトウェアと連合されるように
- Enhance: Meilisearchを有効にした検索で、ユーザーのミュートやブロックを考慮するように - Enhance: Meilisearchを有効にした検索で、ユーザーのミュートやブロックを考慮するように
- Enhance: カスタム絵文字のインポート時の動作を改善
- Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303 - Fix: 時間経過により無効化されたアンテナを再有効化したとき、サーバ再起動までその状況が反映されないのを修正 #12303
- Fix: ロールタイムラインが保存されない問題を修正 - Fix: ロールタイムラインが保存されない問題を修正
- Fix: api.jsonの生成ロジックを改善 #12402 - Fix: api.jsonの生成ロジックを改善 #12402
@ -98,6 +111,7 @@
- Fix: 「みつける」が年越し時に壊れる問題を修正 - Fix: 「みつける」が年越し時に壊れる問題を修正
- Fix: アカウントをブロックした際に、自身のユーザーのページでノートが相手に表示される問題を修正 - Fix: アカウントをブロックした際に、自身のユーザーのページでノートが相手に表示される問題を修正
- Fix: モデレーションログがモデレーターは閲覧できないように修正 - Fix: モデレーションログがモデレーターは閲覧できないように修正
- Fix: ハッシュタグのトレンド除外設定が即時に効果を持つように修正
- Fix: HTTP Digestヘッダのアルゴリズム部分に大文字の"SHA-256"しか使えない - Fix: HTTP Digestヘッダのアルゴリズム部分に大文字の"SHA-256"しか使えない
- Fix: 管理者用APIのアクセス権限が適切に設定されていない問題を修正 - Fix: 管理者用APIのアクセス権限が適切に設定されていない問題を修正
@ -119,7 +133,6 @@
- 例: `$[unixtime 1701356400]` - 例: `$[unixtime 1701356400]`
- Enhance: プラグインでエラーが発生した場合のハンドリングを強化 - Enhance: プラグインでエラーが発生した場合のハンドリングを強化
- Enhance: 細かなUIのブラッシュアップ - Enhance: 細かなUIのブラッシュアップ
- Enhance: サウンド設定に「サウンドを出力しない」と「Misskeyがアクティブな時のみサウンドを出力する」を追加
- Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339 - Fix: 効果音が再生されるとデバイスで再生している動画や音声が停止する問題を修正 #12339
- Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236 - Fix: デッキに表示されたチャンネルの表示先チャンネルを切り替えた際、即座に反映されない問題を修正 #12236
- Fix: プラグインでノートの表示を書き換えられない問題を修正 - Fix: プラグインでノートの表示を書き換えられない問題を修正
@ -147,7 +160,7 @@
### General ### General
- Feat: アイコンデコレーション機能 - Feat: アイコンデコレーション機能
- サーバーで用意された画像をアイコンに重ねることができます - サーバーで用意された画像をアイコンに重ねることができます
- 画像のテンプレートはこちらです: https://misskey-hub.net/avatar-decoration-template.png - 画像のテンプレートはこちらです: https://misskey-hub.net/brand-assets/
- 最大でも黄色いエリア内にデコレーションを収めることを推奨します。 - 最大でも黄色いエリア内にデコレーションを収めることを推奨します。
- 画像は512x512pxを推奨します。 - 画像は512x512pxを推奨します。
- Feat: チャンネル設定にリノート/引用リノートの可否を設定できる項目を追加 - Feat: チャンネル設定にリノート/引用リノートの可否を設定できる項目を追加
@ -164,7 +177,7 @@
### Client ### Client
- Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました - Feat: プラグイン・テーマを外部サイトから直接インストールできるようになりました
- 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください - 外部サイトでの実装が必要です。詳細は Misskey Hub をご覧ください
https://misskey-hub.net/docs/advanced/publish-on-your-website.html https://misskey-hub.net/docs/for-developers/publish-on-your-website/
- Feat: 通知をグルーピングして表示するオプション(オプトアウト) - Feat: 通知をグルーピングして表示するオプション(オプトアウト)
- Feat: Misskeyの基本的なチュートリアルを実装 - Feat: Misskeyの基本的なチュートリアルを実装
- Feat: スワイプしてタイムラインを再読込できるように - Feat: スワイプしてタイムラインを再読込できるように

View File

@ -7,10 +7,10 @@
--- ---
<a href="https://misskey-hub.net/instances.html"> <a href="https://misskey-hub.net/servers/">
<img src="https://custom-icon-badges.herokuapp.com/badge/find_an-instance-acea31?logoColor=acea31&style=for-the-badge&logo=misskey&labelColor=363B40" alt="find an instance"/></a> <img src="https://custom-icon-badges.herokuapp.com/badge/find_an-instance-acea31?logoColor=acea31&style=for-the-badge&logo=misskey&labelColor=363B40" alt="find an instance"/></a>
<a href="https://misskey-hub.net/docs/install.html"> <a href="https://misskey-hub.net/docs/for-admin/install/guides/">
<img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a> <img src="https://custom-icon-badges.herokuapp.com/badge/create_an-instance-FBD53C?logoColor=FBD53C&style=for-the-badge&logo=server&labelColor=363B40" alt="create an instance"/></a>
<a href="./CONTRIBUTING.md"> <a href="./CONTRIBUTING.md">
@ -51,7 +51,7 @@ With Misskey's built in drive, you get cloud storage right in your social media,
## Documentation ## Documentation
Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/), some of the links and graphics above also lead to specific portions of it. Misskey Documentation can be found at [Misskey Hub](https://misskey-hub.net/docs/), some of the links and graphics above also lead to specific portions of it.
## Sponsors ## Sponsors

View File

@ -27,7 +27,7 @@ spec:
ports: ports:
- containerPort: 3000 - containerPort: 3000
- name: postgres - name: postgres
image: postgres:14-alpine image: postgres:15-alpine
env: env:
- name: POSTGRES_USER - name: POSTGRES_USER
value: "example-misskey-user" value: "example-misskey-user"
@ -38,7 +38,7 @@ spec:
ports: ports:
- containerPort: 5432 - containerPort: 5432
- name: redis - name: redis
image: redis:alpine image: redis:7-alpine
ports: ports:
- containerPort: 6379 - containerPort: 6379
volumes: volumes:

View File

@ -121,6 +121,12 @@ sensitive: "NSFW"
add: "Afegir" add: "Afegir"
reaction: "Reaccions" reaction: "Reaccions"
reactions: "Reaccions" reactions: "Reaccions"
emojiPicker: "Selecció d'emojis"
pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
pinnedEmojisSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
emojiPickerDisplay: "Visualitza el selector d'emojis"
overwriteFromPinnedEmojisForReaction: "Reemplaça els emojis de la reacció"
overwriteFromPinnedEmojis: "Sobreescriu des dels emojis fixats"
reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir." reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes" rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
attachCancel: "Eliminar el fitxer adjunt" attachCancel: "Eliminar el fitxer adjunt"
@ -213,6 +219,9 @@ clearQueueConfirmText: "Les notes no lliurades que quedin a la cua no es federar
clearCachedFiles: "Esborra la memòria cau" clearCachedFiles: "Esborra la memòria cau"
clearCachedFilesConfirm: "Segur que voleu eliminar tots els fitxers de la memòria cau?" clearCachedFilesConfirm: "Segur que voleu eliminar tots els fitxers de la memòria cau?"
blockedInstances: "Instàncies bloquejades" blockedInstances: "Instàncies bloquejades"
blockedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols bloquejar separades per un salt de pàgina. Les instàncies llistades no podran comunicar-se amb aquesta instància."
silencedInstances: "Instàncies silenciades"
silencedInstancesDescription: "Llista els enllaços d'amfitrió de les instàncies que vols silenciar. Tots els comptes de les instàncies llistades s'establiran com silenciades i només podran fer sol·licitacions de seguiment, i no podran mencionar als comptes locals si no els segueixen. Això no afectarà les instàncies bloquejades."
muteAndBlock: "Silencia i bloca" muteAndBlock: "Silencia i bloca"
mutedUsers: "Usuaris silenciats" mutedUsers: "Usuaris silenciats"
blockedUsers: "Usuaris bloquejats" blockedUsers: "Usuaris bloquejats"
@ -227,9 +236,12 @@ preview: "Vista prèvia"
default: "Per defecte" default: "Per defecte"
defaultValueIs: "Per defecte: {value}" defaultValueIs: "Per defecte: {value}"
noCustomEmojis: "Cap emoji personalitzat" noCustomEmojis: "Cap emoji personalitzat"
noJobs: "No hi ha feines"
federating: "Federant" federating: "Federant"
blocked: "Bloquejat" blocked: "Bloquejat"
suspended: "Suspés" suspended: "Suspés"
all: "tot"
subscribing: "Subscrit a"
publishing: "S'està publicant" publishing: "S'està publicant"
notResponding: "Sense resposta" notResponding: "Sense resposta"
instanceFollowing: "Seguits del servidor" instanceFollowing: "Seguits del servidor"
@ -254,11 +266,31 @@ removed: "Eliminat"
removeAreYouSure: "Segur que voleu retirar «{x}»?" removeAreYouSure: "Segur que voleu retirar «{x}»?"
deleteAreYouSure: "Segur que voleu retirar «{x}»?" deleteAreYouSure: "Segur que voleu retirar «{x}»?"
resetAreYouSure: "Segur que voleu restablir-ho?" resetAreYouSure: "Segur que voleu restablir-ho?"
areYouSure: "Està segur?"
saved: "S'ha desat" saved: "S'ha desat"
messaging: "Xat" messaging: "Xat"
upload: "Puja" upload: "Puja"
keepOriginalUploading: "Guarda la imatge original"
keepOriginalUploadingDescription: "Guarda la imatge pujada com hi és. Si està apagat, una versió per a la visualització a la xarxa serà generada quan sigui pujada."
fromDrive: "Des de la unitat"
fromUrl: "Des d'un enllaç"
uploadFromUrl: "Carrega des d'un enllaç"
uploadFromUrlDescription: "Enllaç del fitxer que vols carregar"
uploadFromUrlRequested: "Càrrega sol·licitada"
uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot prendre un temps"
explore: "Explora"
messageRead: "Vist"
noMoreHistory: "No hi resta més per veure"
startMessaging: "Començar a xatejar"
nUsersRead: "Vist per {n}"
agreeTo: "Accepto que {0}"
agree: "Hi estic d'acord"
agreeBelow: "Hi estic d'acord amb el següent"
basicNotesBeforeCreateAccount: "Notes importants"
termsOfService: "Condicions d'ús"
start: "Comença" start: "Comença"
home: "Inici" home: "Inici"
remoteUserCaution: "Ja que aquest usuari resideix a una instància remota, la informació mostrada es podria trobar incompleta."
activity: "Activitat" activity: "Activitat"
images: "Imatges" images: "Imatges"
image: "Imatges" image: "Imatges"
@ -274,16 +306,34 @@ dark: "Fosc"
lightThemes: "Temes clars" lightThemes: "Temes clars"
darkThemes: "Temes foscos" darkThemes: "Temes foscos"
syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu" syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu"
drive: "Unitat"
fileName: "Nom del Fitxer"
selectFile: "Selecciona fitxers"
selectFiles: "Selecciona fitxers"
selectFolder: "Selecció de carpeta"
selectFolders: "Selecció de carpeta"
renameFile: "Canvia el nom del fitxer" renameFile: "Canvia el nom del fitxer"
folderName: "Nom de la carpeta" folderName: "Nom de la carpeta"
createFolder: "Crea una carpeta" createFolder: "Crea una carpeta"
renameFolder: "Canvia el nom de la carpeta" renameFolder: "Canvia el nom de la carpeta"
deleteFolder: "Elimina la carpeta" deleteFolder: "Elimina la carpeta"
folder: "Carpeta "
addFile: "Afegeix un fitxer" addFile: "Afegeix un fitxer"
emptyDrive: "La teva unitat és buida"
emptyFolder: "La carpeta està buida" emptyFolder: "La carpeta està buida"
unableToDelete: "No es pot eliminar" unableToDelete: "No es pot eliminar"
inputNewFileName: "Introduïu el nom de fitxer nou"
inputNewDescription: "Inserta una nova llegenda"
inputNewFolderName: "Introduïu el nom de la carpeta nova"
circularReferenceFolder: "La carpeta destinatària és una subcarpeta de la carpeta a la qual la desitges moure"
hasChildFilesOrFolders: "No és possible esborrar aquesta carpeta ja que no és buida"
copyUrl: "Copia l'URL" copyUrl: "Copia l'URL"
rename: "Canvia el nom" rename: "Canvia el nom"
avatar: "Icona"
banner: "Bàner"
displayOfSensitiveMedia: "Visualització de contingut sensible"
whenServerDisconnected: "Quan es perdi la connexió al servidor"
disconnectedFromServer: "Desconnectat pel servidor"
reload: "Actualitza" reload: "Actualitza"
doNothing: "Ignora" doNothing: "Ignora"
accept: "Accepta" accept: "Accepta"
@ -353,33 +403,132 @@ notFound: "No s'ha trobat"
markAsReadAllUnreadNotes: "Marca-ho tot com a llegit" markAsReadAllUnreadNotes: "Marca-ho tot com a llegit"
help: "Ajuda" help: "Ajuda"
invites: "Convida" invites: "Convida"
title: "Títol"
text: "Text"
enable: "Habilita"
next: "Següent" next: "Següent"
retype: "Torneu a introduir-la"
noteOf: "Publicació de: {user}" noteOf: "Publicació de: {user}"
quoteAttached: "Frase adjunta"
quoteQuestion: "Vols annexar-la com a cita?"
noMessagesYet: "Encara no hi ha missatges"
newMessageExists: "Has rebut un nou missatge"
onlyOneFileCanBeAttached: "Només pots adjuntar un fitxer a un missatge"
signinRequired: "Si us plau, Registra't o inicia la sessió abans de continuar"
invitations: "Convida" invitations: "Convida"
invitationCode: "Codi d'invitació"
checking: "Comprovació en curs..."
available: "Disponible"
unavailable: "No és disponible"
usernameInvalidFormat: "Pots fer servir lletres (majúscules i minúscules), números i barres baixes (\"_\")"
tooShort: "Massa curt"
tooLong: "Massa llarg"
weakPassword: "Contrasenya insegura"
normalPassword: "Bona contrasenya"
strongPassword: "Contrasenya segura"
passwordMatched: "Correcte!"
passwordNotMatched: "No coincideix"
signinWith: "Inicia sessió amb amb {x}"
signinFailed: "Autenticació sense èxit. Intenta-ho un altre cop utilitzant la contrasenya i el nom correctes."
or: "O"
language: "Idioma"
uiLanguage: "Idioma de l'interfície"
aboutX: "Respecte a {x}"
emojiStyle: "Estil d'emoji"
native: "Nadiu"
disableDrawer: "No mostrar els menús en calaixos"
showNoteActionsOnlyHover: "Només mostra accions de la nota en passar amb el cursor"
noHistory: "No hi ha un registre previ"
signinHistory: "Historial d'autenticacions"
enableAdvancedMfm: "Habilitar l'MFM avançat"
enableAnimatedMfm: "Habilitar l'MFM amb moviment"
doing: "Processant..."
category: "Categoria"
tags: "Etiquetes" tags: "Etiquetes"
docSource: "Font del document" docSource: "Font del document"
createAccount: "Crea un compte" createAccount: "Crea un compte"
existingAccount: "Compte existent" existingAccount: "Compte existent"
regenerate: "Regenera" regenerate: "Regenera"
fontSize: "Mida del text" fontSize: "Mida del text"
mediaListWithOneImageAppearance: "Altura de la llista de fitxers amb una única imatge"
limitTo: "Limita a {x}"
noFollowRequests: "No tens sol·licituds de seguiment" noFollowRequests: "No tens sol·licituds de seguiment"
openImageInNewTab: "Obre imatges a una nova pestanya"
dashboard: "Panell de control" dashboard: "Panell de control"
local: "Local" local: "Local"
remote: "Remot" remote: "Remot"
total: "Total" total: "Total"
weekOverWeekChanges: "Canvis l'última setmana"
dayOverDayChanges: "Canvis ahir"
appearance: "Aparença" appearance: "Aparença"
clientSettings: "Configuració del client" clientSettings: "Configuració del client"
accountSettings: "Configuració del compte" accountSettings: "Configuració del compte"
promotion: "Promocionat"
promote: "Promoure"
numberOfDays: "Nombre de dies"
hideThisNote: "Amaga la publicació" hideThisNote: "Amaga la publicació"
showFeaturedNotesInTimeline: "Mostra publicacions destacades en la línia de temps" showFeaturedNotesInTimeline: "Mostra publicacions destacades en la línia de temps"
objectStorage: "Emmagatzematge d'objectes\n"
useObjectStorage: "Utilitzar l'emmagatzematge d'objectes"
objectStorageBaseUrl: "Base d'enllaç"
objectStorageBaseUrlDesc: "Prefix d'enllaç utilitzat per a fer referencia als fitxers. Especifica l'enllaç del teu CDN o Proxy si n'estàs utilitzant qualsevol, en cas contrari, especifica l'enllaç al que es pot accedir públicament segons la guia de servei que vosté utilitza.\nPer l'ús d'S3 utilitza 'https://<bucket>.s3.amazonaws.com' I per a GCS o serveis equivalents utilitza 'https://storage.googleapis.com/<bucket>'."
newNoteRecived: "Hi ha publicacions noves" newNoteRecived: "Hi ha publicacions noves"
installedDate: "Data d'instal·lació" installedDate: "Data d'instal·lació"
state: "Estat" state: "Estat"
sort: "Ordena" sort: "Ordena"
ascendingOrder: "Ascendent" ascendingOrder: "Ascendent"
descendingOrder: "Descendent" descendingOrder: "Descendent"
removeAllFollowing: "Deixar de seguir tots els usuaris seguits"
removeAllFollowingDescription: "El fet d'executar això, et farà deixar de seguir a tots els usuaris de {host}. Si us plau, executa això si l'amfitrió, per exemple, ja no existeix."
userSuspended: "Aquest usuari ha sigut suspès"
userSilenced: "Aquest usuari està sent silenciat"
yourAccountSuspendedTitle: "Aquest compte és suspès"
yourAccountSuspendedDescription: "Aquest compte ha sigut suspès a causa de la violació de les condicions d'ús o similars. Contacta l'administrador si en vol saber més. Si us plau, no en faci un altre compte."
tokenRevoked: "Codi de seguretat no vàlid"
tokenRevokedDescription: "La petició més recent ha estat denegada perquè contenia un codi de seguretat no vàlid. Actualitza la pàgina i torna-ho a provar."
accountDeleted: "Compte eliminat amb èxit"
accountDeletedDescription: "Aquest compte ha sigut eliminat"
menu: "Menú"
divider: "Divisor"
addItem: "Afegir element"
rearrange: "Torna a ordenar"
relays: "Relés"
addRelay: "Afegeix relés"
inboxUrl: "Enllaç de la safata d'entrada"
addedRelays: "Relés afegits"
serviceworkerInfo: "És obligatòria l'activació per a obtenir notificacions push"
deletedNote: "Publicacions eliminades" deletedNote: "Publicacions eliminades"
invisibleNote: "Publicacions amagades" invisibleNote: "Publicacions amagades"
enableInfiniteScroll: "Carrega més automàticament\n"
visibility: "Visibilitat"
poll: "Enquesta"
useCw: "Amaga el contingut"
enablePlayer: "Obre el reproductor de vídeo"
disablePlayer: "Tanca el reproductor de vídeo"
expandTweet: "Expandir post"
themeEditor: "Editor de temes"
description: "Descripció"
describeFile: "Afegir subtitulació"
enterFileDescription: "Afegeix un títol"
author: "Autor"
leaveConfirm: "Hi ha canvis sense guardar. Els vols descartar?"
manage: "Administració"
plugins: "Extensions"
preferencesBackups: "Configuracions de les Còpies de seguretat"
deck: "Escriptori"
undeck: "Tanca l'escriptori"
useBlurEffectForModal: "Utilitzar l'efecte de difuminació a modals"
useFullReactionPicker: "Utilitza el cercador de reaccions d'escala sencera"
width: "Amplada"
height: "Alçària"
large: "Gran"
medium: "Mitjà"
small: "Petit"
generateAccessToken: "Genera codi d'accés"
permission: "Permisos"
enableAll: "Habilita tot"
disableAll: "Deshabilita tot"
tokenRequested: "Donar accés al compte"
smtpHost: "Amfitrió" smtpHost: "Amfitrió"
smtpUser: "Nom d'usuari" smtpUser: "Nom d'usuari"
smtpPass: "Contrasenya" smtpPass: "Contrasenya"
@ -389,12 +538,17 @@ clearCache: "Esborra la memòria cau"
showingPastTimeline: "Estàs veient una línia de temps antiga" showingPastTimeline: "Estàs veient una línia de temps antiga"
info: "Informació" info: "Informació"
user: "Usuaris" user: "Usuaris"
administration: "Administració"
middle: "Mitjà"
global: "Global" global: "Global"
searchByGoogle: "Cercar" searchByGoogle: "Cercar"
file: "Fitxers" file: "Fitxers"
icon: "Icona"
replies: "Respondre" replies: "Respondre"
renotes: "Impulsa" renotes: "Impulsa"
_role: _role:
_priority:
middle: "Mitjà"
_options: _options:
antennaMax: "Nombre màxim d'antenes" antennaMax: "Nombre màxim d'antenes"
_email: _email:
@ -403,9 +557,11 @@ _email:
_instanceMute: _instanceMute:
instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat." instanceMuteDescription: "Silencia tots els impulsos dels servidors seleccionats, també els usuaris que responen a altres d'un servidor silenciat."
_theme: _theme:
description: "Descripció"
keys: keys:
mention: "Menció" mention: "Menció"
renote: "Renotar" renote: "Renotar"
divider: "Divisor"
_sfx: _sfx:
note: "Notes" note: "Notes"
notification: "Notificacions" notification: "Notificacions"
@ -447,6 +603,8 @@ _timelines:
local: "Local" local: "Local"
social: "Social" social: "Social"
global: "Global" global: "Global"
_play:
summary: "Descripció"
_pages: _pages:
contents: "Contingut" contents: "Contingut"
blocks: blocks:

View File

@ -543,7 +543,7 @@ showInPage: "Show in page"
popout: "Pop-out" popout: "Pop-out"
volume: "Volume" volume: "Volume"
masterVolume: "Master volume" masterVolume: "Master volume"
notUseSound: "No sounds output." notUseSound: "Disable sound"
useSoundOnlyWhenActive: "Output sounds only if Misskey is active." useSoundOnlyWhenActive: "Output sounds only if Misskey is active."
details: "Details" details: "Details"
chooseEmoji: "Select an emoji" chooseEmoji: "Select an emoji"
@ -1167,6 +1167,7 @@ cwNotationRequired: "If \"Hide content\" is enabled, a description must be provi
doReaction: "Add reaction" doReaction: "Add reaction"
code: "Code" code: "Code"
reloadRequiredToApplySettings: "Reloading is required to apply the settings." reloadRequiredToApplySettings: "Reloading is required to apply the settings."
decorate: "Decorate"
_announcement: _announcement:
forExistingUsers: "Existing users only" forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
@ -1256,7 +1257,7 @@ _initialTutorial:
sensitiveSucceeded: "When attaching files, please set sensitivities in accordance with the server guidelines." sensitiveSucceeded: "When attaching files, please set sensitivities in accordance with the server guidelines."
doItToContinue: "Mark the attachment file as sensitive to proceed." doItToContinue: "Mark the attachment file as sensitive to proceed."
_done: _done:
title: "The tutorial is complete! 🎉" title: "You've completed the tutorial! 🎉"
description: "The functions introduced here are just a small part. For a more detailed understanding of using Misskey, please refer to {link}." description: "The functions introduced here are just a small part. For a more detailed understanding of using Misskey, please refer to {link}."
_timelineDescription: _timelineDescription:
home: "In the Home timeline, you can see notes from accounts you follow." home: "In the Home timeline, you can see notes from accounts you follow."
@ -2154,6 +2155,7 @@ _notification:
pollEnded: "Poll results have become available" pollEnded: "Poll results have become available"
newNote: "New note" newNote: "New note"
unreadAntennaNote: "Antenna {name}" unreadAntennaNote: "Antenna {name}"
roleAssigned: "Role given"
emptyPushNotificationMessage: "Push notifications have been updated" emptyPushNotificationMessage: "Push notifications have been updated"
achievementEarned: "Achievement unlocked" achievementEarned: "Achievement unlocked"
testNotification: "Test notification" testNotification: "Test notification"
@ -2175,6 +2177,7 @@ _notification:
pollEnded: "Polls ending" pollEnded: "Polls ending"
receiveFollowRequest: "Received follow requests" receiveFollowRequest: "Received follow requests"
followRequestAccepted: "Accepted follow requests" followRequestAccepted: "Accepted follow requests"
roleAssigned: "Role given"
achievementEarned: "Achievement unlocked" achievementEarned: "Achievement unlocked"
app: "Notifications from linked apps" app: "Notifications from linked apps"
_actions: _actions:

View File

@ -1899,6 +1899,7 @@ _notification:
yourFollowRequestAccepted: "Votre demande dabonnement a été accepté" yourFollowRequestAccepted: "Votre demande dabonnement a été accepté"
pollEnded: "Les résultats du sondage sont disponibles" pollEnded: "Les résultats du sondage sont disponibles"
unreadAntennaNote: "Antenne {name}" unreadAntennaNote: "Antenne {name}"
roleAssigned: "Rôle attribué"
emptyPushNotificationMessage: "Les notifications push ont été mises à jour" emptyPushNotificationMessage: "Les notifications push ont été mises à jour"
achievementEarned: "Accomplissement" achievementEarned: "Accomplissement"
testNotification: "Tester la notification" testNotification: "Tester la notification"

2
locales/index.d.ts vendored
View File

@ -2325,6 +2325,7 @@ export interface Locale {
"pollEnded": string; "pollEnded": string;
"newNote": string; "newNote": string;
"unreadAntennaNote": string; "unreadAntennaNote": string;
"roleAssigned": string;
"emptyPushNotificationMessage": string; "emptyPushNotificationMessage": string;
"achievementEarned": string; "achievementEarned": string;
"testNotification": string; "testNotification": string;
@ -2346,6 +2347,7 @@ export interface Locale {
"pollEnded": string; "pollEnded": string;
"receiveFollowRequest": string; "receiveFollowRequest": string;
"followRequestAccepted": string; "followRequestAccepted": string;
"roleAssigned": string;
"achievementEarned": string; "achievementEarned": string;
"app": string; "app": string;
}; };

View File

@ -2227,6 +2227,7 @@ _notification:
pollEnded: "アンケートの結果が出ました" pollEnded: "アンケートの結果が出ました"
newNote: "新しい投稿" newNote: "新しい投稿"
unreadAntennaNote: "アンテナ {name}" unreadAntennaNote: "アンテナ {name}"
roleAssigned: "ロールが付与されました"
emptyPushNotificationMessage: "プッシュ通知の更新をしました" emptyPushNotificationMessage: "プッシュ通知の更新をしました"
achievementEarned: "実績を獲得" achievementEarned: "実績を獲得"
testNotification: "通知テスト" testNotification: "通知テスト"
@ -2249,6 +2250,7 @@ _notification:
pollEnded: "アンケートが終了" pollEnded: "アンケートが終了"
receiveFollowRequest: "フォロー申請を受け取った" receiveFollowRequest: "フォロー申請を受け取った"
followRequestAccepted: "フォローが受理された" followRequestAccepted: "フォローが受理された"
roleAssigned: "ロールが付与された"
achievementEarned: "実績の獲得" achievementEarned: "実績の獲得"
app: "連携アプリからの通知" app: "連携アプリからの通知"

View File

@ -881,6 +881,8 @@ makeReactionsPublicDescription: "나의 리액션을 누구나 볼 수 있게
classic: "클래식" classic: "클래식"
muteThread: "글타래 뮤트" muteThread: "글타래 뮤트"
unmuteThread: "글타래 뮤트 해제" unmuteThread: "글타래 뮤트 해제"
followingVisibility: "팔로우의 공개 범위"
followersVisibility: "팔로워의 공개 범위"
continueThread: "글타래 더 보기" continueThread: "글타래 더 보기"
deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? " deleteAccountConfirm: "계정이 삭제되고 되돌릴 수 없게 됩니다. 계속하시겠습니까? "
incorrectPassword: "비밀번호가 올바르지 않습니다." incorrectPassword: "비밀번호가 올바르지 않습니다."
@ -1178,6 +1180,7 @@ reloadRequiredToApplySettings: "설정을 적용하려면 새로고침을 해야
remainingN: "나머지: {n}" remainingN: "나머지: {n}"
overwriteContentConfirm: "현재 내용을 덮어쓰기 합니다. 계속 진행하시겠습니까?" overwriteContentConfirm: "현재 내용을 덮어쓰기 합니다. 계속 진행하시겠습니까?"
seasonalScreenEffect: "철에 맞는 화면으로 꾸미기" seasonalScreenEffect: "철에 맞는 화면으로 꾸미기"
decorate: "장식하기"
_announcement: _announcement:
forExistingUsers: "기존 유저에게만 알림" forExistingUsers: "기존 유저에게만 알림"
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다." forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다."
@ -1837,8 +1840,8 @@ _soundSettings:
driveFileWarn: "드라이브에 있는 파일을 선택하세요." driveFileWarn: "드라이브에 있는 파일을 선택하세요."
driveFileTypeWarn: "이 파일은 지원되지 않습니다." driveFileTypeWarn: "이 파일은 지원되지 않습니다."
driveFileTypeWarnDescription: "오디오 파일을 선택하세요." driveFileTypeWarnDescription: "오디오 파일을 선택하세요."
driveFileDurationWarn: "오디오가 너무 길어요." driveFileDurationWarn: "오디오가 너무 깁니다"
driveFileDurationWarnDescription: "길은 오디오를 사용하시는 경우 미스키 사용에 지장이 갈 수도 있습니다. 그래도 괜찮습니까?" driveFileDurationWarnDescription: "긴 오디오로 설정할 경우 미스키 사용에 지장이 갈 수도 있습니다. 그래도 괜찮습니까?"
_ago: _ago:
future: "미래" future: "미래"
justNow: "방금 전" justNow: "방금 전"
@ -2168,6 +2171,7 @@ _notification:
pollEnded: "투표 결과가 발표되었습니다" pollEnded: "투표 결과가 발표되었습니다"
newNote: "새 게시물" newNote: "새 게시물"
unreadAntennaNote: "안테나 {name}" unreadAntennaNote: "안테나 {name}"
roleAssigned: "역할이 부여 되었습니다."
emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다" emptyPushNotificationMessage: "푸시 알림이 갱신되었습니다"
achievementEarned: "도전 과제를 달성했습니다" achievementEarned: "도전 과제를 달성했습니다"
testNotification: "알림 테스트" testNotification: "알림 테스트"

View File

@ -1217,7 +1217,7 @@ _initialTutorial:
skipAreYouSure: "結束教學模式?" skipAreYouSure: "結束教學模式?"
_landing: _landing:
title: "歡迎使用本教學課程" title: "歡迎使用本教學課程"
description: "在這裡您可以查看Misskey的基本使用方法和功能。" description: "在這裡您可以查看 Misskey 的基本使用方法和功能。"
_note: _note:
title: "什麼是貼文?" title: "什麼是貼文?"
description: "在Misskey上發布的內容稱為「貼文」。貼文在時間軸上按時間順序排列並即時更新。" description: "在Misskey上發布的內容稱為「貼文」。貼文在時間軸上按時間順序排列並即時更新。"
@ -2171,6 +2171,7 @@ _notification:
pollEnded: "問卷調查已產生結果" pollEnded: "問卷調查已產生結果"
newNote: "新的貼文" newNote: "新的貼文"
unreadAntennaNote: "天線 {name}" unreadAntennaNote: "天線 {name}"
roleAssigned: "已授予角色"
emptyPushNotificationMessage: "推送通知已更新" emptyPushNotificationMessage: "推送通知已更新"
achievementEarned: "獲得成就" achievementEarned: "獲得成就"
testNotification: "通知測試" testNotification: "通知測試"

View File

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2023.12.0-beta.5", "version": "2023.12.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -77,6 +77,17 @@ export class FeaturedService {
return Array.from(ranking.keys()); return Array.from(ranking.keys());
} }
@bindThis
private async removeFromRanking(name: string, windowRange: number, element: string): Promise<void> {
const currentWindow = this.getCurrentWindow(windowRange);
const previousWindow = currentWindow - 1;
const redisPipeline = this.redisClient.pipeline();
redisPipeline.zrem(`${name}:${currentWindow}`, element);
redisPipeline.zrem(`${name}:${previousWindow}`, element);
await redisPipeline.exec();
}
@bindThis @bindThis
public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> { public updateGlobalNotesRanking(noteId: MiNote['id'], score = 1): Promise<void> {
return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score); return this.updateRankingOf('featuredGlobalNotesRanking', GLOBAL_NOTES_RANKING_WINDOW, noteId, score);
@ -126,4 +137,9 @@ export class FeaturedService {
public getHashtagsRanking(threshold: number): Promise<string[]> { public getHashtagsRanking(threshold: number): Promise<string[]> {
return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold); return this.getRankingOf('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, threshold);
} }
@bindThis
public removeHashtagsFromRanking(hashtag: string): Promise<void> {
return this.removeFromRanking('featuredHashtagsRanking', HASHTAG_RANKING_WINDOW, hashtag);
}
} }

View File

@ -11,6 +11,7 @@ import { MiMeta } from '@/models/Meta.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
@Injectable() @Injectable()
@ -25,6 +26,7 @@ export class MetaService implements OnApplicationShutdown {
@Inject(DI.db) @Inject(DI.db)
private db: DataSource, private db: DataSource,
private featuredService: FeaturedService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
) { ) {
//this.onMessage = this.onMessage.bind(this); //this.onMessage = this.onMessage.bind(this);
@ -95,6 +97,8 @@ export class MetaService implements OnApplicationShutdown {
@bindThis @bindThis
public async update(data: Partial<MiMeta>): Promise<MiMeta> { public async update(data: Partial<MiMeta>): Promise<MiMeta> {
let before: MiMeta | undefined;
const updated = await this.db.transaction(async transactionalEntityManager => { const updated = await this.db.transaction(async transactionalEntityManager => {
const metas = await transactionalEntityManager.find(MiMeta, { const metas = await transactionalEntityManager.find(MiMeta, {
order: { order: {
@ -102,10 +106,10 @@ export class MetaService implements OnApplicationShutdown {
}, },
}); });
const meta = metas[0]; before = metas[0];
if (meta) { if (before) {
await transactionalEntityManager.update(MiMeta, meta.id, data); await transactionalEntityManager.update(MiMeta, before.id, data);
const metas = await transactionalEntityManager.find(MiMeta, { const metas = await transactionalEntityManager.find(MiMeta, {
order: { order: {
@ -119,6 +123,21 @@ export class MetaService implements OnApplicationShutdown {
} }
}); });
if (data.hiddenTags) {
process.nextTick(() => {
const hiddenTags = new Set<string>(data.hiddenTags);
if (before) {
for (const previousHiddenTag of before.hiddenTags) {
hiddenTags.delete(previousHiddenTag);
}
}
for (const hiddenTag of hiddenTags) {
this.featuredService.removeHashtagsFromRanking(hiddenTag);
}
});
}
this.globalEventService.publishInternalEvent('metaUpdated', updated); this.globalEventService.publishInternalEvent('metaUpdated', updated);
return updated; return updated;

View File

@ -293,7 +293,7 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
// Check blocking // Check blocking
if (data.renote && this.isQuote(data)) { if (this.isQuote(data)) {
if (data.renote.userHost === null) { if (data.renote.userHost === null) {
if (data.renote.userId !== user.id) { if (data.renote.userId !== user.id) {
const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id); const blocked = await this.userBlockingService.checkBlocked(data.renote.userId, user.id);
@ -730,8 +730,9 @@ export class NoteCreateService implements OnApplicationShutdown {
} }
@bindThis @bindThis
private isQuote(note: Option): boolean { private isQuote(note: Option): note is Option & { renote: MiNote } {
return !!note.text || !!note.cw || !!note.files || !!note.poll; // sync with misc/is-quote.ts
return !!note.renote && (!!note.text || !!note.cw || (!!note.files && !!note.files.length) || !!note.poll);
} }
@bindThis @bindThis
@ -799,7 +800,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private async renderNoteOrRenoteActivity(data: Option, note: MiNote) { private async renderNoteOrRenoteActivity(data: Option, note: MiNote) {
if (data.localOnly) return null; if (data.localOnly) return null;
const content = data.renote && this.isQuote(data) const content = data.renote && !this.isQuote(data)
? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note) ? this.apRendererService.renderAnnounce(data.renote.uri ? data.renote.uri : `${this.config.url}/notes/${data.renote.id}`, note)
: this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note); : this.apRendererService.renderCreate(await this.apRendererService.renderNote(note, false), note);

View File

@ -6,7 +6,14 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { In } from 'typeorm'; import { In } from 'typeorm';
import type { MiRole, MiRoleAssignment, RoleAssignmentsRepository, RolesRepository, UsersRepository } from '@/models/_.js'; import { ModuleRef } from '@nestjs/core';
import type {
MiRole,
MiRoleAssignment,
RoleAssignmentsRepository,
RolesRepository,
UsersRepository,
} from '@/models/_.js';
import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js'; import { MemoryKVCache, MemorySingleCache } from '@/misc/cache.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -16,12 +23,13 @@ import { CacheService } from '@/core/CacheService.js';
import type { RoleCondFormulaValue } from '@/models/Role.js'; import type { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { IdService } from '@/core/IdService.js';
import { ModerationLogService } from '@/core/ModerationLogService.js'; import { ModerationLogService } from '@/core/ModerationLogService.js';
import type { Packed } from '@/misc/json-schema.js'; import type { Packed } from '@/misc/json-schema.js';
import { FanoutTimelineService } from '@/core/FanoutTimelineService.js'; import { FanoutTimelineService } from '@/core/FanoutTimelineService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import { NotificationService } from '@/core/NotificationService.js';
import type { OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
export type RolePolicies = { export type RolePolicies = {
gtlAvailable: boolean; gtlAvailable: boolean;
@ -78,14 +86,17 @@ export const DEFAULT_POLICIES: RolePolicies = {
}; };
@Injectable() @Injectable()
export class RoleService implements OnApplicationShutdown { export class RoleService implements OnApplicationShutdown, OnModuleInit {
private rolesCache: MemorySingleCache<MiRole[]>; private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>; private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService;
public static AlreadyAssignedError = class extends Error {}; public static AlreadyAssignedError = class extends Error {};
public static NotAssignedError = class extends Error {}; public static NotAssignedError = class extends Error {};
constructor( constructor(
private moduleRef: ModuleRef,
@Inject(DI.redis) @Inject(DI.redis)
private redisClient: Redis.Redis, private redisClient: Redis.Redis,
@ -120,6 +131,10 @@ export class RoleService implements OnApplicationShutdown {
this.redisForSub.on('message', this.onMessage); this.redisForSub.on('message', this.onMessage);
} }
async onModuleInit() {
this.notificationService = this.moduleRef.get(NotificationService.name);
}
@bindThis @bindThis
private async onMessage(_: string, data: string): Promise<void> { private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data); const obj = JSON.parse(data);
@ -427,6 +442,12 @@ export class RoleService implements OnApplicationShutdown {
this.globalEventService.publishInternalEvent('userRoleAssigned', created); this.globalEventService.publishInternalEvent('userRoleAssigned', created);
if (role.isPublic) {
this.notificationService.createNotification(userId, 'roleAssigned', {
roleId: roleId,
});
}
if (moderator) { if (moderator) {
const user = await this.usersRepository.findOneByOrFail({ id: userId }); const user = await this.usersRepository.findOneByOrFail({ id: userId });
this.moderationLogService.log(moderator, 'assignRole', { this.moderationLogService.log(moderator, 'assignRole', {

View File

@ -3,30 +3,34 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { Inject, Injectable, OnApplicationShutdown } from '@nestjs/common'; import { Inject, Injectable, OnApplicationShutdown, OnModuleInit } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { ModuleRef } from '@nestjs/core';
import type { UserListMembershipsRepository } from '@/models/_.js'; import type { UserListMembershipsRepository } from '@/models/_.js';
import type { MiUser } from '@/models/User.js'; import type { MiUser } from '@/models/User.js';
import type { MiUserList } from '@/models/UserList.js'; import type { MiUserList } from '@/models/UserList.js';
import type { MiUserListMembership } from '@/models/UserListMembership.js'; import type { MiUserListMembership } from '@/models/UserListMembership.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { ProxyAccountService } from '@/core/ProxyAccountService.js'; import { ProxyAccountService } from '@/core/ProxyAccountService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { RoleService } from '@/core/RoleService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { RedisKVCache } from '@/misc/cache.js'; import { RedisKVCache } from '@/misc/cache.js';
import type { GlobalEvents } from '@/core/GlobalEventService.js'; import { RoleService } from '@/core/RoleService.js';
@Injectable() @Injectable()
export class UserListService implements OnApplicationShutdown { export class UserListService implements OnApplicationShutdown, OnModuleInit {
public static TooManyUsersError = class extends Error {}; public static TooManyUsersError = class extends Error {};
public membersCache: RedisKVCache<Set<string>>; public membersCache: RedisKVCache<Set<string>>;
private roleService: RoleService;
constructor( constructor(
private moduleRef: ModuleRef,
@Inject(DI.redis) @Inject(DI.redis)
private redisClient: Redis.Redis, private redisClient: Redis.Redis,
@ -38,7 +42,6 @@ export class UserListService implements OnApplicationShutdown {
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private idService: IdService, private idService: IdService,
private roleService: RoleService,
private globalEventService: GlobalEventService, private globalEventService: GlobalEventService,
private proxyAccountService: ProxyAccountService, private proxyAccountService: ProxyAccountService,
private queueService: QueueService, private queueService: QueueService,
@ -54,6 +57,10 @@ export class UserListService implements OnApplicationShutdown {
this.redisForSub.on('message', this.onMessage); this.redisForSub.on('message', this.onMessage);
} }
async onModuleInit() {
this.roleService = this.moduleRef.get(RoleService.name);
}
@bindThis @bindThis
private async onMessage(_: string, data: string): Promise<void> { private async onMessage(_: string, data: string): Promise<void> {
const obj = JSON.parse(data); const obj = JSON.parse(data);

View File

@ -15,8 +15,8 @@ import type { Packed } from '@/misc/json-schema.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { isNotNull } from '@/misc/is-not-null.js'; import { isNotNull } from '@/misc/is-not-null.js';
import { FilterUnionByProperty, notificationTypes } from '@/types.js'; import { FilterUnionByProperty, notificationTypes } from '@/types.js';
import { RoleEntityService } from './RoleEntityService.js';
import type { OnModuleInit } from '@nestjs/common'; import type { OnModuleInit } from '@nestjs/common';
import type { CustomEmojiService } from '../CustomEmojiService.js';
import type { UserEntityService } from './UserEntityService.js'; import type { UserEntityService } from './UserEntityService.js';
import type { NoteEntityService } from './NoteEntityService.js'; import type { NoteEntityService } from './NoteEntityService.js';
@ -27,7 +27,7 @@ const NOTE_REQUIRED_GROUPED_NOTIFICATION_TYPES = new Set(['note', 'mention', 're
export class NotificationEntityService implements OnModuleInit { export class NotificationEntityService implements OnModuleInit {
private userEntityService: UserEntityService; private userEntityService: UserEntityService;
private noteEntityService: NoteEntityService; private noteEntityService: NoteEntityService;
private customEmojiService: CustomEmojiService; private roleEntityService: RoleEntityService;
constructor( constructor(
private moduleRef: ModuleRef, private moduleRef: ModuleRef,
@ -43,14 +43,13 @@ export class NotificationEntityService implements OnModuleInit {
//private userEntityService: UserEntityService, //private userEntityService: UserEntityService,
//private noteEntityService: NoteEntityService, //private noteEntityService: NoteEntityService,
//private customEmojiService: CustomEmojiService,
) { ) {
} }
onModuleInit() { onModuleInit() {
this.userEntityService = this.moduleRef.get('UserEntityService'); this.userEntityService = this.moduleRef.get('UserEntityService');
this.noteEntityService = this.moduleRef.get('NoteEntityService'); this.noteEntityService = this.moduleRef.get('NoteEntityService');
this.customEmojiService = this.moduleRef.get('CustomEmojiService'); this.roleEntityService = this.moduleRef.get('RoleEntityService');
} }
@bindThis @bindThis
@ -81,6 +80,7 @@ export class NotificationEntityService implements OnModuleInit {
detail: false, detail: false,
}) })
) : undefined; ) : undefined;
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
return await awaitAll({ return await awaitAll({
id: notification.id, id: notification.id,
@ -92,6 +92,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'reaction' ? { ...(notification.type === 'reaction' ? {
reaction: notification.reaction, reaction: notification.reaction,
} : {}), } : {}),
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
...(notification.type === 'achievementEarned' ? { ...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement, achievement: notification.achievement,
} : {}), } : {}),
@ -216,6 +219,8 @@ export class NotificationEntityService implements OnModuleInit {
}); });
} }
const role = notification.type === 'roleAssigned' ? await this.roleEntityService.pack(notification.roleId) : undefined;
return await awaitAll({ return await awaitAll({
id: notification.id, id: notification.id,
createdAt: new Date(notification.createdAt).toISOString(), createdAt: new Date(notification.createdAt).toISOString(),
@ -226,6 +231,9 @@ export class NotificationEntityService implements OnModuleInit {
...(notification.type === 'reaction' ? { ...(notification.type === 'reaction' ? {
reaction: notification.reaction, reaction: notification.reaction,
} : {}), } : {}),
...(notification.type === 'roleAssigned' ? {
role: role,
} : {}),
...(notification.type === 'achievementEarned' ? { ...(notification.type === 'achievementEarned' ? {
achievement: notification.achievement, achievement: notification.achievement,
} : {}), } : {}),

View File

@ -7,5 +7,6 @@ import type { MiNote } from '@/models/Note.js';
// eslint-disable-next-line import/no-default-export // eslint-disable-next-line import/no-default-export
export default function(note: MiNote): boolean { export default function(note: MiNote): boolean {
// sync with NoteCreateService.isQuote
return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0)); return note.renoteId != null && (note.text != null || note.hasPoll || (note.fileIds != null && note.fileIds.length > 0));
} }

View File

@ -38,6 +38,7 @@ import { packedFlashSchema } from '@/models/json-schema/flash.js';
import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js'; import { packedAnnouncementSchema } from '@/models/json-schema/announcement.js';
import { packedSigninSchema } from '@/models/json-schema/signin.js'; import { packedSigninSchema } from '@/models/json-schema/signin.js';
import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js'; import { packedRoleLiteSchema, packedRoleSchema } from '@/models/json-schema/role.js';
import { packedAdSchema } from '@/models/json-schema/ad.js';
export const refs = { export const refs = {
UserLite: packedUserLiteSchema, UserLite: packedUserLiteSchema,
@ -49,6 +50,7 @@ export const refs = {
User: packedUserSchema, User: packedUserSchema,
UserList: packedUserListSchema, UserList: packedUserListSchema,
Ad: packedAdSchema,
Announcement: packedAnnouncementSchema, Announcement: packedAnnouncementSchema,
App: packedAppSchema, App: packedAppSchema,
Note: packedNoteSchema, Note: packedNoteSchema,

View File

@ -3,11 +3,10 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { notificationTypes } from '@/types.js';
import { MiUser } from './User.js'; import { MiUser } from './User.js';
import { MiNote } from './Note.js'; import { MiNote } from './Note.js';
import { MiFollowRequest } from './FollowRequest.js';
import { MiAccessToken } from './AccessToken.js'; import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
export type MiNotification = { export type MiNotification = {
type: 'note'; type: 'note';
@ -68,6 +67,11 @@ export type MiNotification = {
id: string; id: string;
createdAt: string; createdAt: string;
notifierId: MiUser['id']; notifierId: MiUser['id'];
} | {
type: 'roleAssigned';
id: string;
createdAt: string;
roleId: MiRole['id'];
} | { } | {
type: 'achievementEarned'; type: 'achievementEarned';
id: string; id: string;

View File

@ -0,0 +1,64 @@
/*
* SPDX-FileCopyrightText: syuilo and other misskey contributors
* SPDX-License-Identifier: AGPL-3.0-only
*/
export const packedAdSchema = {
type: 'object',
properties: {
id: {
type: 'string',
optional: false,
nullable: false,
format: 'id',
example: 'xxxxxxxxxx',
},
expiresAt: {
type: 'string',
optional: false,
nullable: false,
format: 'date-time',
},
startsAt: {
type: 'string',
optional: false,
nullable: false,
format: 'date-time',
},
place: {
type: 'string',
optional: false,
nullable: false,
},
priority: {
type: 'string',
optional: false,
nullable: false,
},
ratio: {
type: 'number',
optional: false,
nullable: false,
},
url: {
type: 'string',
optional: false,
nullable: false,
},
imageUrl: {
type: 'string',
optional: false,
nullable: false,
},
memo: {
type: 'string',
optional: false,
nullable: false,
},
dayOfWeek: {
type: 'integer',
optional: false,
nullable: false,
},
},
} as const;

View File

@ -554,9 +554,7 @@ export const packedMeDetailedOnlySchema = {
mention: notificationRecieveConfig, mention: notificationRecieveConfig,
reaction: notificationRecieveConfig, reaction: notificationRecieveConfig,
pollEnded: notificationRecieveConfig, pollEnded: notificationRecieveConfig,
achievementEarned: notificationRecieveConfig,
receiveFollowRequest: notificationRecieveConfig, receiveFollowRequest: notificationRecieveConfig,
followRequestAccepted: notificationRecieveConfig,
}, },
}, },
emailNotificationTypes: { emailNotificationTypes: {

View File

@ -153,8 +153,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker this.systemQueueWorker
.on('active', (job) => systemLogger.debug(`active id=${job.id}`)) .on('active', (job) => systemLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => systemLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => systemLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => systemLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => systemLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => systemLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => systemLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -191,8 +191,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.dbQueueWorker this.dbQueueWorker
.on('active', (job) => dbLogger.debug(`active id=${job.id}`)) .on('active', (job) => dbLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => dbLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => dbLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => dbLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => dbLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => dbLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => dbLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -215,8 +215,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.deliverQueueWorker this.deliverQueueWorker
.on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('active', (job) => deliverLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => deliverLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => deliverLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) .on('failed', (job, err) => deliverLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => deliverLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => deliverLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => deliverLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -239,8 +239,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.inboxQueueWorker this.inboxQueueWorker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`)) .on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) .on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -263,8 +263,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.webhookDeliverQueueWorker this.webhookDeliverQueueWorker
.on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('active', (job) => webhookLogger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`))
.on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => webhookLogger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`))
.on('failed', (job, err) => webhookLogger.warn(`failed(${err}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`)) .on('failed', (job, err) => webhookLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`))
.on('error', (err: Error) => webhookLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => webhookLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => webhookLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -292,8 +292,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.relationshipQueueWorker this.relationshipQueueWorker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`)) .on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
@ -315,8 +315,8 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.objectStorageQueueWorker this.objectStorageQueueWorker
.on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`)) .on('active', (job) => objectStorageLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`)) .on('completed', (job, result) => objectStorageLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => objectStorageLogger.warn(`failed(${err}) id=${job ? job.id : '-'}`, { job, e: renderError(err) })) .on('failed', (job, err) => objectStorageLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }))
.on('error', (err: Error) => objectStorageLogger.error(`error ${err}`, { e: renderError(err) })) .on('error', (err: Error) => objectStorageLogger.error(`error ${err.stack}`, { e: renderError(err) }))
.on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`)); .on('stalled', (jobId) => objectStorageLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion

View File

@ -25,6 +25,11 @@ export const meta = {
id: 'cb865949-8af5-4062-a88c-ef55e8786d1d', id: 'cb865949-8af5-4062-a88c-ef55e8786d1d',
}, },
}, },
res: {
type: 'object',
optional: false, nullable: false,
ref: 'User',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -17,6 +17,12 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
res: {
type: 'object',
optional: false,
nullable: false,
ref: 'Ad',
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -63,7 +69,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
ad: ad, ad: ad,
}); });
return ad; return {
id: ad.id,
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
url: ad.url,
imageUrl: ad.imageUrl,
priority: ad.priority,
ratio: ad.ratio,
place: ad.place,
memo: ad.memo,
};
}); });
} }
} }

View File

@ -16,6 +16,17 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
res: {
type: 'array',
optional: false,
nullable: false,
items: {
type: 'object',
optional: false,
nullable: false,
ref: 'Ad',
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -46,7 +57,18 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
} }
const ads = await query.limit(ps.limit).getMany(); const ads = await query.limit(ps.limit).getMany();
return ads; return ads.map(ad => ({
id: ad.id,
expiresAt: ad.expiresAt.toISOString(),
startsAt: ad.startsAt.toISOString(),
dayOfWeek: ad.dayOfWeek,
url: ad.url,
imageUrl: ad.imageUrl,
memo: ad.memo,
place: ad.place,
priority: ad.priority,
ratio: ad.ratio,
}));
}); });
} }
} }

View File

@ -31,6 +31,8 @@ export const meta = {
id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975', id: 'f7a3462c-4e6e-4069-8421-b9bd4f4c3975',
}, },
}, },
ref: 'EmojiDetailed',
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -15,6 +15,16 @@ export const meta = {
kind: 'read:admin', kind: 'read:admin',
tags: ['admin'], tags: ['admin'],
res: {
type: 'array',
items: {
type: 'object',
properties: {
tablename: { type: 'string' },
indexname: { type: 'string' },
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -16,6 +16,25 @@ export const meta = {
requireCredential: true, requireCredential: true,
requireModerator: true, requireModerator: true,
res: {
type: 'array',
optional: false,
nullable: false,
items: {
type: 'object',
optional: false,
nullable: false,
properties: {
ip: { type: 'string' },
createdAt: {
type: 'string',
optional: false,
nullable: false,
format: 'date-time',
},
},
},
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -28,6 +28,20 @@ export const meta = {
id: '224eff5e-2488-4b18-b3e7-f50d94421648', id: '224eff5e-2488-4b18-b3e7-f50d94421648',
}, },
}, },
res: {
type: 'array',
items: {
type: 'object',
properties: {
id: { type: 'string', format: 'misskey:id' },
createdAt: { type: 'string', format: 'date-time' },
user: { ref: 'UserDetailed' },
expiresAt: { type: 'string', format: 'date-time', nullable: true },
},
required: ['id', 'createdAt', 'user'],
},
}
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -80,7 +94,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: assign.id, id: assign.id,
createdAt: this.idService.parse(assign.id).date.toISOString(), createdAt: this.idService.parse(assign.id).date.toISOString(),
user: await this.userEntityService.pack(assign.user!, me, { detail: true }), user: await this.userEntityService.pack(assign.user!, me, { detail: true }),
expiresAt: assign.expiresAt, expiresAt: assign.expiresAt?.toISOString() ?? null,
}))); })));
}); });
} }

View File

@ -11,6 +11,23 @@ export const meta = {
requireCredential: false, requireCredential: false,
tags: ['meta'], tags: ['meta'],
res: {
type: 'object',
nullable: true,
properties: {
params: {
type: 'array',
items: {
type: 'object',
properties: {
name: { type: 'string' },
type: { type: 'string' },
},
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -18,6 +18,92 @@ export const meta = {
allowGet: true, allowGet: true,
cacheSec: 60 * 60, cacheSec: 60 * 60,
res: {
type: 'object',
optional: false,
nullable: false,
properties: {
topSubInstances: {
type: 'array',
optional: false,
nullable: false,
items: {
properties: {
id: { type: 'string' },
firstRetrievedAt: { type: 'string' },
host: { type: 'string' },
usersCount: { type: 'number' },
notesCount: { type: 'number' },
followingCount: { type: 'number' },
followersCount: { type: 'number' },
isNotResponding: { type: 'boolean' },
isSuspended: { type: 'boolean' },
isBlocked: { type: 'boolean' },
softwareName: { type: 'string' },
softwareVersion: { type: 'string' },
openRegistrations: { type: 'boolean' },
name: { type: 'string' },
description: { type: 'string' },
maintainerName: { type: 'string' },
maintainerEmail: { type: 'string' },
isSilenced: { type: 'boolean' },
iconUrl: { type: 'string' },
faviconUrl: { type: 'string' },
themeColor: { type: 'string' },
infoUpdatedAt: {
type: 'string',
nullable: true,
},
latestRequestReceivedAt: {
type: 'string',
nullable: true,
},
}
},
},
otherFollowersCount: { type: 'number' },
topPubInstances: {
type: 'array',
optional: false,
nullable: false,
items: {
properties: {
id: { type: 'string' },
firstRetrievedAt: { type: 'string' },
host: { type: 'string' },
usersCount: { type: 'number' },
notesCount: { type: 'number' },
followingCount: { type: 'number' },
followersCount: { type: 'number' },
isNotResponding: { type: 'boolean' },
isSuspended: { type: 'boolean' },
isBlocked: { type: 'boolean' },
softwareName: { type: 'string' },
softwareVersion: { type: 'string' },
openRegistrations: { type: 'boolean' },
name: { type: 'string' },
description: { type: 'string' },
maintainerName: { type: 'string' },
maintainerEmail: { type: 'string' },
isSilenced: { type: 'boolean' },
iconUrl: { type: 'string' },
faviconUrl: { type: 'string' },
themeColor: { type: 'string' },
infoUpdatedAt: {
type: 'string',
nullable: true,
},
latestRequestReceivedAt: {
type: 'string',
nullable: true,
},
}
},
},
otherFollowingCount: { type: 'number' },
},
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -32,6 +32,18 @@ export const meta = {
id: '693ba8ba-b486-40df-a174-72f8279b56a4', id: '693ba8ba-b486-40df-a174-72f8279b56a4',
}, },
}, },
res: {
type: 'object',
properties: {
type: {
type: 'string',
},
data: {
type: 'string',
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -16,6 +16,18 @@ export const meta = {
requireCredential: false, requireCredential: false,
allowGet: true, allowGet: true,
cacheSec: 60 * 3, cacheSec: 60 * 3,
res: {
type: 'object',
properties: {
items: {
type: 'array',
items: {
type: 'object',
},
}
}
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -27,6 +27,12 @@ export const meta = {
errors: { errors: {
}, },
res: {
type: 'object',
optional: false, nullable: false,
ref: 'Flash',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -16,6 +16,16 @@ export const meta = {
requireCredential: false, requireCredential: false,
allowGet: true, allowGet: true,
cacheSec: 60 * 1, cacheSec: 60 * 1,
res: {
type: 'object',
optional: false, nullable: false,
properties: {
count: {
type: 'number',
nullable: false,
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -32,6 +32,16 @@ export const meta = {
id: '798d6847-b1ed-4f9c-b1f9-163c42655995', id: '798d6847-b1ed-4f9c-b1f9-163c42655995',
}, },
}, },
res: {
type: 'object',
nullable: false,
optional: false,
properties: {
id: { type: 'string' },
name: { type: 'string' },
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -36,6 +36,140 @@ export const meta = {
id: 'bf32b864-449b-47b8-974e-f9a5468546f1', id: 'bf32b864-449b-47b8-974e-f9a5468546f1',
}, },
}, },
res: {
type: 'object',
nullable: false,
optional: false,
properties: {
rp: {
type: 'object',
properties: {
id: {
type: 'string',
nullable: true,
},
},
},
user: {
type: 'object',
properties: {
id: {
type: 'string',
},
name: {
type: 'string',
},
displayName: {
type: 'string',
},
},
},
challenge: {
type: 'string',
},
pubKeyCredParams: {
type: 'array',
items: {
type: 'object',
properties: {
type: {
type: 'string',
},
alg: {
type: 'number',
},
},
},
},
timeout: {
type: 'number',
nullable: true,
},
excludeCredentials: {
type: 'array',
nullable: true,
items: {
type: 'object',
properties: {
id: {
type: 'string',
},
type: {
type: 'string',
},
transports: {
type: 'array',
items: {
type: 'string',
enum: [
"ble",
"cable",
"hybrid",
"internal",
"nfc",
"smart-card",
"usb",
],
},
},
},
},
},
authenticatorSelection: {
type: 'object',
nullable: true,
properties: {
authenticatorAttachment: {
type: 'string',
enum: [
"cross-platform",
"platform",
],
},
requireResidentKey: {
type: 'boolean',
},
userVerification: {
type: 'string',
enum: [
"discouraged",
"preferred",
"required",
],
},
},
},
attestation: {
type: 'string',
nullable: true,
enum: [
"direct",
"enterprise",
"indirect",
"none",
],
},
extensions: {
type: 'object',
nullable: true,
properties: {
appid: {
type: 'string',
nullable: true,
},
credProps: {
type: 'boolean',
nullable: true,
},
hmacCreateSecret: {
type: 'boolean',
nullable: true,
},
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -26,6 +26,19 @@ export const meta = {
id: '78d6c839-20c9-4c66-b90a-fc0542168b48', id: '78d6c839-20c9-4c66-b90a-fc0542168b48',
}, },
}, },
res: {
type: 'object',
nullable: false,
optional: false,
properties: {
qr: { type: 'string' },
url: { type: 'string' },
secret: { type: 'string' },
label: { type: 'string' },
issuer: { type: 'string' },
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -13,6 +13,37 @@ export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
res: {
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
name: {
type: 'string',
},
createdAt: {
type: 'string',
format: 'date-time',
},
lastUsedAt: {
type: 'string',
format: 'date-time',
},
permission: {
type: 'array',
uniqueItems: true,
items: {
type: 'string'
},
}
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -50,7 +81,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
id: token.id, id: token.id,
name: token.name ?? token.app?.name, name: token.name ?? token.app?.name,
createdAt: this.idService.parse(token.id).date.toISOString(), createdAt: this.idService.parse(token.id).date.toISOString(),
lastUsedAt: token.lastUsedAt, lastUsedAt: token.lastUsedAt?.toISOString(),
permission: token.permission, permission: token.permission,
}))); })));
}); });

View File

@ -14,6 +14,36 @@ export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
res: {
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
name: {
type: 'string',
},
callbackUrl: {
type: 'string',
nullable: true,
},
permission: {
type: 'array',
uniqueItems: true,
items: {
type: 'string'
},
},
isAuthorized: {
type: 'boolean',
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -64,6 +64,10 @@ export const meta = {
id: 'b234a14e-9ebe-4581-8000-074b3c215962', id: 'b234a14e-9ebe-4581-8000-074b3c215962',
}, },
}, },
res: {
type: 'object',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -9,6 +9,10 @@ import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
res: {
type: 'object',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -18,6 +18,10 @@ export const meta = {
id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a', id: '97a1e8e7-c0f7-47d2-957a-92e61256e01a',
}, },
}, },
res: {
type: 'object',
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -18,6 +18,10 @@ export const meta = {
id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a', id: 'ac3ed68a-62f0-422b-a7bc-d5e09e8f6a6a',
}, },
}, },
res: {
type: 'object',
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -9,6 +9,10 @@ import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
res: {
type: 'object',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -10,6 +10,28 @@ import { RegistryApiService } from '@/core/RegistryApiService.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
secure: true, secure: true,
res: {
type: 'array',
items: {
type: 'object',
properties: {
scopes: {
type: 'array',
items: {
type: 'array',
items: {
type: 'string',
}
}
},
domain: {
type: 'string',
nullable: true,
},
},
},
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -40,6 +40,11 @@ export const meta = {
id: 'a2defefb-f220-8849-0af6-17f816099323', id: 'a2defefb-f220-8849-0af6-17f816099323',
}, },
}, },
res: {
type: 'object',
ref: 'UserDetailed',
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -27,6 +27,33 @@ export const meta = {
id: '87a9bb19-111e-4e37-81d3-a3e7426453b0', id: '87a9bb19-111e-4e37-81d3-a3e7426453b0',
}, },
}, },
res: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id'
},
userId: {
type: 'string',
format: 'misskey:id',
},
name: { type: 'string' },
on: {
type: 'array',
items: {
type: 'string',
enum: webhookEventTypes,
}
},
url: { type: 'string' },
secret: { type: 'string' },
active: { type: 'boolean' },
latestSentAt: { type: 'string', format: 'date-time', nullable: true },
latestStatus: { type: 'integer', nullable: true },
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -73,7 +100,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.globalEventService.publishInternalEvent('webhookCreated', webhook); this.globalEventService.publishInternalEvent('webhookCreated', webhook);
return webhook; return {
id: webhook.id,
userId: webhook.userId,
name: webhook.name,
on: webhook.on,
url: webhook.url,
secret: webhook.secret,
active: webhook.active,
latestSentAt: webhook.latestSentAt?.toISOString(),
latestStatus: webhook.latestStatus,
};
}); });
} }
} }

View File

@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { webhookEventTypes } from '@/models/Webhook.js';
import type { WebhooksRepository } from '@/models/_.js'; import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -14,6 +15,36 @@ export const meta = {
requireCredential: true, requireCredential: true,
kind: 'read:account', kind: 'read:account',
res: {
type: 'array',
items: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id'
},
userId: {
type: 'string',
format: 'misskey:id',
},
name: { type: 'string' },
on: {
type: 'array',
items: {
type: 'string',
enum: webhookEventTypes,
}
},
url: { type: 'string' },
secret: { type: 'string' },
active: { type: 'boolean' },
latestSentAt: { type: 'string', format: 'date-time', nullable: true },
latestStatus: { type: 'integer', nullable: true },
},
}
}
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -33,7 +64,19 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
userId: me.id, userId: me.id,
}); });
return webhooks; return webhooks.map(webhook => (
{
id: webhook.id,
userId: webhook.userId,
name: webhook.name,
on: webhook.on,
url: webhook.url,
secret: webhook.secret,
active: webhook.active,
latestSentAt: webhook.latestSentAt?.toISOString(),
latestStatus: webhook.latestStatus,
}
));
}); });
} }
} }

View File

@ -5,6 +5,7 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js'; import { Endpoint } from '@/server/api/endpoint-base.js';
import { webhookEventTypes } from '@/models/Webhook.js';
import type { WebhooksRepository } from '@/models/_.js'; import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js'; import { ApiError } from '../../../error.js';
@ -23,6 +24,33 @@ export const meta = {
id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098', id: '50f614d9-3047-4f7e-90d8-ad6b2d5fb098',
}, },
}, },
res: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id'
},
userId: {
type: 'string',
format: 'misskey:id',
},
name: { type: 'string' },
on: {
type: 'array',
items: {
type: 'string',
enum: webhookEventTypes,
}
},
url: { type: 'string' },
secret: { type: 'string' },
active: { type: 'boolean' },
latestSentAt: { type: 'string', format: 'date-time', nullable: true },
latestStatus: { type: 'integer', nullable: true },
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {
@ -49,7 +77,17 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchWebhook); throw new ApiError(meta.errors.noSuchWebhook);
} }
return webhook; return {
id: webhook.id,
userId: webhook.userId,
name: webhook.name,
on: webhook.on,
url: webhook.url,
secret: webhook.secret,
active: webhook.active,
latestSentAt: webhook.latestSentAt?.toISOString(),
latestStatus: webhook.latestStatus,
};
}); });
} }
} }

View File

@ -24,6 +24,25 @@ export const meta = {
id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5', id: '30aaaee3-4792-48dc-ab0d-cf501a575ac5',
}, },
}, },
res: {
type: 'array',
items: {
type: 'object',
nullable: false,
properties: {
id: {
type: 'string',
format: 'misskey:id'
},
user: {
type: 'object',
ref: 'User'
},
},
required: ['id', 'user'],
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -15,6 +15,53 @@ export const meta = {
cacheSec: 60 * 1, cacheSec: 60 * 1,
tags: ['meta'], tags: ['meta'],
res: {
type: 'object',
optional: false, nullable: false,
properties: {
machine: {
type: 'string',
nullable: false,
},
cpu: {
type: 'object',
nullable: false,
properties: {
model: {
type: 'string',
nullable: false,
},
cores: {
type: 'number',
nullable: false,
},
},
},
mem: {
type: 'object',
properties: {
total: {
type: 'number',
nullable: false,
},
},
},
fs: {
type: 'object',
nullable: false,
properties: {
total: {
type: 'number',
nullable: false,
},
used: {
type: 'number',
nullable: false,
},
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -12,6 +12,30 @@ export const meta = {
description: 'Endpoint for testing input validation.', description: 'Endpoint for testing input validation.',
requireCredential: false, requireCredential: false,
res: {
type: 'object',
properties: {
id: {
type: 'string',
format: 'misskey:id'
},
required: {
type: 'boolean',
},
string: {
type: 'string',
},
default: {
type: 'string',
},
nullableDefault: {
type: 'string',
default: 'hello',
nullable: true,
},
}
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -10,6 +10,21 @@ import { DI } from '@/di-symbols.js';
export const meta = { export const meta = {
requireCredential: true, requireCredential: true,
res: {
type: 'array',
items: {
type: 'object',
properties: {
name: {
type: 'string',
},
unlockedAt: {
type: 'number',
},
},
},
}
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -25,6 +25,35 @@ export const meta = {
id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686', id: '7bc05c21-1d7a-41ae-88f1-66820f4dc686',
}, },
}, },
res: {
type: 'array',
items: {
type: 'object',
nullable: false,
properties: {
id: {
type: 'string',
format: 'misskey:id',
},
createdAt: {
type: 'string',
format: 'date-time',
},
userId: {
type: 'string',
format: 'misskey:id',
},
user: {
type: 'object',
ref: 'User',
},
withReplies: {
type: 'boolean',
},
},
},
},
} as const; } as const;
export const paramDef = { export const paramDef = {

View File

@ -14,11 +14,26 @@
* pollEnded - * pollEnded -
* receiveFollowRequest - * receiveFollowRequest -
* followRequestAccepted - * followRequestAccepted -
* roleAssigned -
* achievementEarned - * achievementEarned -
* app - * app -
* test - * test -
*/ */
export const notificationTypes = ['note', 'follow', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollEnded', 'receiveFollowRequest', 'followRequestAccepted', 'achievementEarned', 'app', 'test'] as const; export const notificationTypes = [
'note',
'follow',
'mention',
'reply',
'renote',
'quote',
'reaction',
'pollEnded',
'receiveFollowRequest',
'followRequestAccepted',
'roleAssigned',
'achievementEarned',
'app',
'test'] as const;
export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const; export const obsoleteNotificationTypes = ['pollVote', 'groupInvited'] as const;
export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const; export const noteVisibilities = ['public', 'home', 'followers', 'specified'] as const;

View File

@ -7,7 +7,7 @@ services:
- "127.0.0.1:56312:6379" - "127.0.0.1:56312:6379"
dbtest: dbtest:
image: postgres:13 image: postgres:15
ports: ports:
- "127.0.0.1:54312:5432" - "127.0.0.1:54312:5432"
environment: environment:

View File

@ -10,9 +10,8 @@ process.env.NODE_ENV = 'test';
process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true'; process.env.FORCE_FOLLOW_REMOTE_USER_FOR_TESTING = 'true';
import * as assert from 'assert'; import * as assert from 'assert';
import { signup, api, post, react, startServer, waitFire, sleep, uploadUrl, randomString } from '../utils.js'; import { api, post, randomString, signup, sleep, startServer, uploadUrl } from '../utils.js';
import type { INestApplicationContext } from '@nestjs/common'; import type { INestApplicationContext } from '@nestjs/common';
import type * as misskey from 'misskey-js';
function genHost() { function genHost() {
return randomString() + '.example.com'; return randomString() + '.example.com';
@ -366,8 +365,8 @@ describe('Timelines', () => {
await api('/following/create', { userId: bob.id }, alice); await api('/following/create', { userId: bob.id }, alice);
await sleep(1000); await sleep(1000);
const [bobFile, carolFile] = await Promise.all([ const [bobFile, carolFile] = await Promise.all([
uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'), uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'), uploadUrl(carol, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png'),
]); ]);
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [bobFile.id] }); const bobNote2 = await post(bob, { fileIds: [bobFile.id] });
@ -666,7 +665,7 @@ describe('Timelines', () => {
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [file.id] }); const bobNote2 = await post(bob, { fileIds: [file.id] });
@ -804,7 +803,7 @@ describe('Timelines', () => {
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [file.id] }); const bobNote2 = await post(bob, { fileIds: [file.id] });
@ -999,7 +998,7 @@ describe('Timelines', () => {
const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body); const list = await api('/users/lists/create', { name: 'list' }, alice).then(res => res.body);
await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice); await api('/users/lists/push', { listId: list.id, userId: bob.id }, alice);
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [file.id] }); const bobNote2 = await post(bob, { fileIds: [file.id] });
@ -1158,7 +1157,7 @@ describe('Timelines', () => {
test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => { test.concurrent('[withFiles: true] ファイル付きノートのみ含まれる', async () => {
const [alice, bob] = await Promise.all([signup(), signup()]); const [alice, bob] = await Promise.all([signup(), signup()]);
const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/icon.png'); const file = await uploadUrl(bob, 'https://raw.githubusercontent.com/misskey-dev/assets/main/public/icon.png');
const bobNote1 = await post(bob, { text: 'hi' }); const bobNote1 = await post(bob, { text: 'hi' });
const bobNote2 = await post(bob, { fileIds: [file.id] }); const bobNote2 = await post(bob, { fileIds: [file.id] });

View File

@ -19,6 +19,7 @@ import { CacheService } from '@/core/CacheService.js';
import { IdService } from '@/core/IdService.js'; import { IdService } from '@/core/IdService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js'; import { GlobalEventService } from '@/core/GlobalEventService.js';
import { secureRndstr } from '@/misc/secure-rndstr.js'; import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js';
import { sleep } from '../utils.js'; import { sleep } from '../utils.js';
import type { TestingModule } from '@nestjs/testing'; import type { TestingModule } from '@nestjs/testing';
import type { MockFunctionMetadata } from 'jest-mock'; import type { MockFunctionMetadata } from 'jest-mock';
@ -32,6 +33,7 @@ describe('RoleService', () => {
let rolesRepository: RolesRepository; let rolesRepository: RolesRepository;
let roleAssignmentsRepository: RoleAssignmentsRepository; let roleAssignmentsRepository: RoleAssignmentsRepository;
let metaService: jest.Mocked<MetaService>; let metaService: jest.Mocked<MetaService>;
let notificationService: jest.Mocked<NotificationService>;
let clock: lolex.InstalledClock; let clock: lolex.InstalledClock;
function createUser(data: Partial<MiUser> = {}) { function createUser(data: Partial<MiUser> = {}) {
@ -71,6 +73,16 @@ describe('RoleService', () => {
CacheService, CacheService,
IdService, IdService,
GlobalEventService, GlobalEventService,
{
provide: NotificationService,
useFactory: () => ({
createNotification: jest.fn(),
}),
},
{
provide: NotificationService.name,
useExisting: NotificationService,
},
], ],
}) })
.useMocker((token) => { .useMocker((token) => {
@ -93,6 +105,9 @@ describe('RoleService', () => {
roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository); roleAssignmentsRepository = app.get<RoleAssignmentsRepository>(DI.roleAssignmentsRepository);
metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>; metaService = app.get<MetaService>(MetaService) as jest.Mocked<MetaService>;
notificationService = app.get<NotificationService>(NotificationService) as jest.Mocked<NotificationService>;
await roleService.onModuleInit();
}); });
afterEach(async () => { afterEach(async () => {
@ -273,4 +288,57 @@ describe('RoleService', () => {
expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true); expect(resultAfter25hAgain.canManageCustomEmojis).toBe(true);
}); });
}); });
describe('assign', () => {
test('公開ロールの場合は通知される', async () => {
const user = await createUser();
const role = await createRole({
isPublic: true,
name: 'a',
});
await roleService.assign(user.id, role.id);
clock.uninstall();
await sleep(100);
const assignments = await roleAssignmentsRepository.find({
where: {
userId: user.id,
roleId: role.id,
},
});
expect(assignments).toHaveLength(1);
expect(notificationService.createNotification).toHaveBeenCalled();
expect(notificationService.createNotification.mock.lastCall![0]).toBe(user.id);
expect(notificationService.createNotification.mock.lastCall![1]).toBe('roleAssigned');
expect(notificationService.createNotification.mock.lastCall![2]).toEqual({
roleId: role.id,
});
});
test('非公開ロールの場合は通知されない', async () => {
const user = await createUser();
const role = await createRole({
isPublic: false,
name: 'a',
});
await roleService.assign(user.id, role.id);
clock.uninstall();
await sleep(100);
const assignments = await roleAssignmentsRepository.find({
where: {
userId: user.id,
roleId: role.id,
},
});
expect(assignments).toHaveLength(1);
expect(notificationService.createNotification).not.toHaveBeenCalled();
});
});
}); });

View File

@ -3,16 +3,16 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue'; import { computed, watch, version as vueVersion, App } from 'vue';
import { compareVersions } from 'compare-versions'; import { compareVersions } from 'compare-versions';
import widgets from '@/widgets/index.js'; import widgets from '@/widgets/index.js';
import directives from '@/directives/index.js'; import directives from '@/directives/index.js';
import components from '@/components/index.js'; import components from '@/components/index.js';
import { version, ui, lang, updateLocale, locale } from '@/config.js'; import { version, lang, updateLocale, locale } from '@/config.js';
import { applyTheme } from '@/scripts/theme.js'; import { applyTheme } from '@/scripts/theme.js';
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js'; import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
import { i18n, updateI18n } from '@/i18n.js'; import { updateI18n } from '@/i18n.js';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js'; import { $i, refreshAccount, login } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js';
import { fetchInstance, instance } from '@/instance.js'; import { fetchInstance, instance } from '@/instance.js';
import { deviceKind } from '@/scripts/device-kind.js'; import { deviceKind } from '@/scripts/device-kind.js';

View File

@ -3,14 +3,14 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; import { createApp, markRaw, defineAsyncComponent } from 'vue';
import { common } from './common.js'; import { common } from './common.js';
import { version, ui, lang, updateLocale } from '@/config.js'; import { ui } from '@/config.js';
import { i18n, updateI18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { confirm, alert, post, popup, toast } from '@/os.js'; import { confirm, alert, post, popup, toast } from '@/os.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { $i, refreshAccount, login, updateAccount, signout } from '@/account.js'; import { $i, updateAccount, signout } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.js'; import { defaultStore, ColdDeviceStorage } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js'; import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js'; import { reactionPicker } from '@/scripts/reaction-picker.js';
@ -20,7 +20,6 @@ import { mainRouter } from '@/router.js';
import { initializeSw } from '@/scripts/initialize-sw.js'; import { initializeSw } from '@/scripts/initialize-sw.js';
import { deckStore } from '@/ui/deck/deck-store.js'; import { deckStore } from '@/ui/deck/deck-store.js';
import { emojiPicker } from '@/scripts/emoji-picker.js'; import { emojiPicker } from '@/scripts/emoji-picker.js';
import { SnowfallEffect } from '@/scripts/snowfall-effect.js';
export async function mainBoot() { export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp( const { isClientUpdated } = await common(() => createApp(
@ -79,6 +78,7 @@ export async function mainBoot() {
if (defaultStore.state.enableSeasonalScreenEffect) { if (defaultStore.state.enableSeasonalScreenEffect) {
const month = new Date().getMonth() + 1; const month = new Date().getMonth() + 1;
if (month === 12 || month === 1) { if (month === 12 || month === 1) {
const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
new SnowfallEffect().render(); new SnowfallEffect().render();
} }
} }

View File

@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; import { createApp, defineAsyncComponent } from 'vue';
import { common } from './common.js'; import { common } from './common.js';
export async function subBoot() { export async function subBoot() {

View File

@ -4,49 +4,70 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1"> <div style="position: relative;">
<div class="banner" :style="bannerStyle"> <MkA :to="`/channels/${channel.id}`" class="eftoefju _panel" tabindex="-1" @click="updateLastReadedAt">
<div class="fade"></div> <div class="banner" :style="bannerStyle">
<div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div> <div class="fade"></div>
<div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div> <div class="name"><i class="ti ti-device-tv"></i> {{ channel.name }}</div>
<div class="status"> <div v-if="channel.isSensitive" class="sensitiveIndicator">{{ i18n.ts.sensitive }}</div>
<div> <div class="status">
<i class="ti ti-users ti-fw"></i> <div>
<I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;"> <i class="ti ti-users ti-fw"></i>
<template #n> <I18n :src="i18n.ts._channel.usersCount" tag="span" style="margin-left: 4px;">
<b>{{ channel.usersCount }}</b> <template #n>
</template> <b>{{ channel.usersCount }}</b>
</I18n> </template>
</div> </I18n>
<div> </div>
<i class="ti ti-pencil ti-fw"></i> <div>
<I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;"> <i class="ti ti-pencil ti-fw"></i>
<template #n> <I18n :src="i18n.ts._channel.notesCount" tag="span" style="margin-left: 4px;">
<b>{{ channel.notesCount }}</b> <template #n>
</template> <b>{{ channel.notesCount }}</b>
</I18n> </template>
</I18n>
</div>
</div> </div>
</div> </div>
</div> <article v-if="channel.description">
<article v-if="channel.description"> <p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p>
<p :title="channel.description">{{ channel.description.length > 85 ? channel.description.slice(0, 85) + '…' : channel.description }}</p> </article>
</article> <footer>
<footer> <span v-if="channel.lastNotedAt">
<span v-if="channel.lastNotedAt"> {{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/>
{{ i18n.ts.updatedAt }}: <MkTime :time="channel.lastNotedAt"/> </span>
</span> </footer>
</footer> </MkA>
</MkA> <div
v-if="channel.lastNotedAt && (channel.isFavorited || channel.isFollowing) && (!lastReadedAt || Date.parse(channel.lastNotedAt) > lastReadedAt)"
class="indicator"
></div>
</div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue'; import { computed, ref, watch } from 'vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js';
const props = defineProps<{ const props = defineProps<{
channel: Record<string, any>; channel: Record<string, any>;
}>(); }>();
const getLastReadedAt = (): number | null => {
return miLocalStorage.getItemAsJson(`channelLastReadedAt:${props.channel.id}`) ?? null;
};
const lastReadedAt = ref(getLastReadedAt());
watch(() => props.channel.id, () => {
lastReadedAt.value = getLastReadedAt();
});
const updateLastReadedAt = () => {
lastReadedAt.value = props.channel.lastNotedAt ? Date.parse(props.channel.lastNotedAt) : Date.now();
};
const bannerStyle = computed(() => { const bannerStyle = computed(() => {
if (props.channel.bannerUrl) { if (props.channel.bannerUrl) {
return { backgroundImage: `url(${props.channel.bannerUrl})` }; return { backgroundImage: `url(${props.channel.bannerUrl})` };
@ -170,4 +191,17 @@ const bannerStyle = computed(() => {
} }
} }
.indicator {
position: absolute;
top: 0;
right: 0;
transform: translate(25%, -25%);
background-color: var(--accent);
border: solid var(--bg) 4px;
border-radius: 100%;
width: 1.5rem;
height: 1.5rem;
aspect-ratio: 1 / 1;
}
</style> </style>

View File

@ -24,8 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; import { ref, shallowRef, toRefs } from 'vue';
import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{
modelValue: string | null; modelValue: string | null;

View File

@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
</I18n> </I18n>
<div style="margin-top: 0.2em;"> <div style="margin-top: 0.2em;">
<MkLink target="_blank" url="https://misskey-hub.net/docs/donate.html">{{ i18n.ts.learnMore }}</MkLink> <MkLink target="_blank" url="https://misskey-hub.net/docs/for-users/resources/donate/">{{ i18n.ts.learnMore }}</MkLink>
</div> </div>
</div> </div>
<div class="_buttons"> <div class="_buttons">

View File

@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, shallowRef, watch, ref } from 'vue'; import { shallowRef, watch, ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';

View File

@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-for="(item, i) in items2"> <template v-for="(item, i) in items2">
<div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div> <div v-if="item.type === 'divider'" role="separator" :class="$style.divider"></div>
<span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]"> <span v-else-if="item.type === 'label'" role="menuitem" :class="[$style.label, $style.item]">
<span>{{ item.text }}</span> <span style="opacity: 0.7;">{{ item.text }}</span>
</span> </span>
<span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]"> <span v-else-if="item.type === 'pending'" role="menuitem" :tabindex="i" :class="[$style.pending, $style.item]">
<span><MkEllipsis/></span> <span><MkEllipsis/></span>
@ -23,32 +23,44 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <MkA v-else-if="item.type === 'link'" role="menuitem" :to="item.to" :tabindex="i" class="_button" :class="$style.item" @click.passive="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<span>{{ item.text }}</span> <div :class="$style.item_content">
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> <span :class="$style.item_content_text">{{ item.text }}</span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</div>
</MkA> </MkA>
<a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <a v-else-if="item.type === 'a'" role="menuitem" :href="item.href" :target="item.target" :download="item.download" :tabindex="i" class="_button" :class="$style.item" @click="close(true)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<span>{{ item.text }}</span> <div :class="$style.item_content">
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> <span :class="$style.item_content_text">{{ item.text }}</span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</div>
</a> </a>
<button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <button v-else-if="item.type === 'user'" role="menuitem" :tabindex="i" class="_button" :class="[$style.item, { [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/> <MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> <div v-if="item.indicate" :class="$style.item_content">
<span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</div>
</button> </button>
<button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <button v-else-if="item.type === 'switch'" role="menuitemcheckbox" :tabindex="i" class="_button" :class="[$style.item, $style.switch, { [$style.switchDisabled]: item.disabled } ]" @click="switchItem(item)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/> <MkSwitchButton :class="$style.switchButton" :checked="item.ref" :disabled="item.disabled" @toggle="switchItem(item)"/>
<span :class="$style.switchText">{{ item.text }}</span> <div :class="$style.item_content">
<span :class="[$style.item_content_text, $style.switchText]">{{ item.text }}</span>
</div>
</button> </button>
<button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)"> <button v-else-if="item.type === 'parent'" class="_button" role="menuitem" :tabindex="i" :class="[$style.item, $style.parent, { [$style.childShowing]: childShowingItem === item }]" @mouseenter="preferClick ? null : showChildren(item, $event)" @click="!preferClick ? null : showChildren(item, $event)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]" style="pointer-events: none;"></i>
<span style="pointer-events: none;">{{ item.text }}</span> <div :class="$style.item_content">
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span> <span :class="$style.item_content_text" style="pointer-events: none;">{{ item.text }}</span>
<span :class="$style.caret" style="pointer-events: none;"><i class="ti ti-chevron-right ti-fw"></i></span>
</div>
</button> </button>
<button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)"> <button v-else :tabindex="i" class="_button" role="menuitem" :class="[$style.item, { [$style.danger]: item.danger, [$style.active]: item.active }]" :disabled="item.active" @click="clicked(item.action, $event)" @mouseenter.passive="onItemMouseEnter(item)" @mouseleave.passive="onItemMouseLeave(item)">
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i> <i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/> <MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<span>{{ item.text }}</span> <div :class="$style.item_content">
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span> <span :class="$style.item_content_text">{{ item.text }}</span>
<span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
</div>
</button> </button>
</template> </template>
<span v-if="items2.length === 0" :class="[$style.none, $style.item]"> <span v-if="items2.length === 0" :class="[$style.none, $style.item]">
@ -62,7 +74,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts"> <script lang="ts">
import { Ref, computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue'; import { computed, defineAsyncComponent, nextTick, onBeforeUnmount, onMounted, ref, shallowRef, watch } from 'vue';
import { focusPrev, focusNext } from '@/scripts/focus.js'; import { focusPrev, focusNext } from '@/scripts/focus.js';
import MkSwitchButton from '@/components/MkSwitch.button.vue'; import MkSwitchButton from '@/components/MkSwitch.button.vue';
import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu'; import { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuParent } from '@/types/menu';
@ -228,6 +240,7 @@ onBeforeUnmount(() => {
.root { .root {
padding: 8px 0; padding: 8px 0;
box-sizing: border-box; box-sizing: border-box;
max-width: 100vw;
min-width: 200px; min-width: 200px;
overflow: auto; overflow: auto;
overscroll-behavior: contain; overscroll-behavior: contain;
@ -267,7 +280,8 @@ onBeforeUnmount(() => {
} }
.item { .item {
display: block; display: flex;
align-items: center;
position: relative; position: relative;
padding: 5px 16px; padding: 5px 16px;
width: 100%; width: 100%;
@ -340,10 +354,6 @@ onBeforeUnmount(() => {
pointer-events: none; pointer-events: none;
font-size: 0.7em; font-size: 0.7em;
padding-bottom: 4px; padding-bottom: 4px;
> span {
opacity: 0.7;
}
} }
&.pending { &.pending {
@ -373,6 +383,22 @@ onBeforeUnmount(() => {
} }
} }
.item_content {
width: 100%;
max-width: 100vw;
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
text-overflow: ellipsis;
}
.item_content_text {
max-width: calc(100vw - 4rem);
text-overflow: ellipsis;
overflow: hidden;
}
.switch { .switch {
position: relative; position: relative;
display: flex; display: flex;
@ -406,6 +432,7 @@ onBeforeUnmount(() => {
.icon { .icon {
margin-right: 8px; margin-right: 8px;
line-height: 1;
} }
.caret { .caret {
@ -419,9 +446,8 @@ onBeforeUnmount(() => {
} }
.indicator { .indicator {
position: absolute; display: flex;
top: 5px; align-items: center;
left: 13px;
color: var(--indicator); color: var(--indicator);
font-size: 12px; font-size: 12px;
animation: blink 1s infinite; animation: blink 1s infinite;

View File

@ -151,7 +151,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue'; import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } from 'vue';
import * as mfm from 'mfm-js'; import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkNoteSub from '@/components/MkNoteSub.vue'; import MkNoteSub from '@/components/MkNoteSub.vue';

View File

@ -221,11 +221,10 @@ import { useNoteCapture } from '@/scripts/use-note-capture.js';
import { deepClone } from '@/scripts/clone.js'; import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js'; import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue'; import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import MkUserCardMini from '@/components/MkUserCardMini.vue'; import MkUserCardMini from '@/components/MkUserCardMini.vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';

View File

@ -27,7 +27,6 @@ import * as Misskey from 'misskey-js';
import MkNoteHeader from '@/components/MkNoteHeader.vue'; import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkSubNoteContent from '@/components/MkSubNoteContent.vue'; import MkSubNoteContent from '@/components/MkSubNoteContent.vue';
import MkCwButton from '@/components/MkCwButton.vue'; import MkCwButton from '@/components/MkCwButton.vue';
import { $i } from '@/account.js';
const props = defineProps<{ const props = defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;

View File

@ -51,7 +51,6 @@ import { i18n } from '@/i18n.js';
import { $i } from '@/account.js'; import { $i } from '@/account.js';
import { userPage } from '@/filters/user.js'; import { userPage } from '@/filters/user.js';
import { checkWordMute } from '@/scripts/check-word-mute.js'; import { checkWordMute } from '@/scripts/check-word-mute.js';
import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;

View File

@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.head"> <div :class="$style.head">
<MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-if="notification.type === 'pollEnded'" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/> <MkAvatar v-else-if="notification.type === 'note'" :class="$style.icon" :user="notification.note.user" link preview/>
<MkAvatar v-else-if="notification.type === 'roleAssigned'" :class="$style.icon" :user="$i" link preview/>
<MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/> <MkAvatar v-else-if="notification.type === 'achievementEarned'" :class="$style.icon" :user="$i" link preview/>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ti ti-plus" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div> <div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
@ -36,6 +37,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i> <i v-else-if="notification.type === 'quote'" class="ti ti-quote"></i>
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i> <i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i> <i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
<img v-else-if="notification.type === 'roleAssigned'" :src="notification.role.iconUrl" alt=""/>
<!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 --> <!-- notification.reaction null になることはまずないがここでoptional chaining使うと一部ブラウザで刺さるので念の為 -->
<MkReactionIcon <MkReactionIcon
v-else-if="notification.type === 'reaction'" v-else-if="notification.type === 'reaction'"
@ -50,6 +52,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<header :class="$style.header"> <header :class="$style.header">
<span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span> <span v-if="notification.type === 'pollEnded'">{{ i18n.ts._notification.pollEnded }}</span>
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span> <span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span> <span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span> <span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA> <MkA v-else-if="notification.user" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
@ -86,6 +89,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/> <Mfm :text="getNoteSummary(notification.note)" :plain="true" :nowrap="true" :author="notification.note.user"/>
<i class="ti ti-quote" :class="$style.quote"></i> <i class="ti ti-quote" :class="$style.quote"></i>
</MkA> </MkA>
<div v-else-if="notification.type === 'roleAssigned'" :class="$style.text">
{{ notification.role.name }}
</div>
<MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements"> <MkA v-else-if="notification.type === 'achievementEarned'" :class="$style.text" to="/my/achievements">
{{ i18n.ts._achievements._types['_' + notification.achievement].title }} {{ i18n.ts._achievements._types['_' + notification.achievement].title }}
</MkA> </MkA>
@ -130,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, shallowRef } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
import MkFollowButton from '@/components/MkFollowButton.vue'; import MkFollowButton from '@/components/MkFollowButton.vue';

View File

@ -24,13 +24,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated, watch } from 'vue'; import { onUnmounted, onDeactivated, onMounted, computed, shallowRef, onActivated } from 'vue';
import MkPagination, { Paging } from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import XNotification from '@/components/MkNotification.vue'; import XNotification from '@/components/MkNotification.vue';
import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
import MkNote from '@/components/MkNote.vue'; import MkNote from '@/components/MkNote.vue';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import { $i } from '@/account.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { notificationTypes } from '@/const.js'; import { notificationTypes } from '@/const.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';

View File

@ -23,8 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, watch, ref, shallowRef } from 'vue'; import { onMounted, onUnmounted, ref, shallowRef } from 'vue';
import { deviceKind } from '@/scripts/device-kind.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { getScrollContainer } from '@/scripts/scroll.js'; import { getScrollContainer } from '@/scripts/scroll.js';

View File

@ -80,7 +80,6 @@ import { ref, computed } from 'vue';
import { toUnicode } from 'punycode/'; import { toUnicode } from 'punycode/';
import MkButton from './MkButton.vue'; import MkButton from './MkButton.vue';
import MkInput from './MkInput.vue'; import MkInput from './MkInput.vue';
import MkSwitch from './MkSwitch.vue';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue'; import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
import * as config from '@/config.js'; import * as config from '@/config.js';
import * as os from '@/os.js'; import * as os from '@/os.js';

View File

@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template> <template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template> <template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template>
<a href="https://misskey-hub.net/docs/notes.html" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a> <a href="https://misskey-hub.net/docs/for-users/onboarding/warning/" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
<MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch> <MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch>
</MkFolder> </MkFolder>
@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref, watch } from 'vue'; import { computed, ref } from 'vue';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';

View File

@ -39,7 +39,6 @@ import XSignup from '@/components/MkSignupDialog.form.vue';
import XServerRules from '@/components/MkSignupDialog.rules.vue'; import XServerRules from '@/components/MkSignupDialog.rules.vue';
import MkModalWindow from '@/components/MkModalWindow.vue'; import MkModalWindow from '@/components/MkModalWindow.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
autoSet?: boolean; autoSet?: boolean;

View File

@ -35,7 +35,6 @@ import * as Misskey from 'misskey-js';
import MkMediaList from '@/components/MkMediaList.vue'; import MkMediaList from '@/components/MkMediaList.vue';
import MkPoll from '@/components/MkPoll.vue'; import MkPoll from '@/components/MkPoll.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import { shouldCollapsed } from '@/scripts/collapsed.js'; import { shouldCollapsed } from '@/scripts/collapsed.js';
const props = defineProps<{ const props = defineProps<{

View File

@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.divider"></div> <div :class="$style.divider"></div>
<I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;"> <I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;">
<template #link> <template #link>
<a href="https://misskey-hub.net/docs/features/timeline.html" target="_blank" class="_link">{{ i18n.ts.help }}</a> <a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template> </template>
</I18n> </I18n>

View File

@ -130,7 +130,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div> <div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;"> <I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;">
<template #link> <template #link>
<a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a> <a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template> </template>
</I18n> </I18n>
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div> <div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>

View File

@ -27,7 +27,7 @@ const modal = shallowRef<InstanceType<typeof MkModal>>();
const whatIsNew = () => { const whatIsNew = () => {
modal.value.close(); modal.value.close();
window.open(`https://misskey-hub.net/docs/releases.html#_${version.replace(/\./g, '-')}`, '_blank'); window.open(`https://misskey-hub.net/docs/releases/#_${version.replace(/\./g, '')}`, '_blank');
}; };
onMounted(() => { onMounted(() => {

View File

@ -34,15 +34,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue'; import XUser from '@/components/MkUserSetupDialog.User.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { $i } from '@/account.js';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
const pinnedUsers = { endpoint: 'pinned-users', noPaging: true }; const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };

View File

@ -44,14 +44,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { $i } from '@/account.js';
const isLocked = ref(false); const isLocked = ref(false);
const hideOnlineStatus = ref(false); const hideOnlineStatus = ref(false);

View File

@ -30,8 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { instance } from '@/instance.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue'; import MkInput from '@/components/MkInput.vue';

View File

@ -29,7 +29,6 @@ import * as Misskey from 'misskey-js';
import { ref } from 'vue'; import { ref } from 'vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i } from '@/account.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
const props = defineProps<{ const props = defineProps<{

View File

@ -53,7 +53,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue'; import { ref } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import XTimeline from './welcome.timeline.vue';
import XSigninDialog from '@/components/MkSigninDialog.vue'; import XSigninDialog from '@/components/MkSigninDialog.vue';
import XSignupDialog from '@/components/MkSignupDialog.vue'; import XSignupDialog from '@/components/MkSignupDialog.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
@ -63,7 +62,6 @@ import { instanceName } from '@/config.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js'; import { instance } from '@/instance.js';
import number from '@/filters/number.js';
import MkNumber from '@/components/MkNumber.vue'; import MkNumber from '@/components/MkNumber.vue';
import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue'; import XActiveUsersChart from '@/components/MkVisitorDashboard.ActiveUsersChart.vue';
@ -125,13 +123,13 @@ function showMenu(ev) {
text: i18n.ts.help, text: i18n.ts.help,
icon: 'ti ti-help-circle', icon: 'ti ti-help-circle',
action: () => { action: () => {
window.open('https://misskey-hub.net/help.md', '_blank', 'noopener'); window.open('https://misskey-hub.net/docs/for-users/', '_blank', 'noopener');
}, },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);
} }
function exploreOtherServers() { function exploreOtherServers() {
window.open('https://join.misskey.page/instances', '_blank', 'noopener'); window.open('https://misskey-hub.net/servers/', '_blank', 'noopener');
} }
</script> </script>

View File

@ -21,7 +21,6 @@ 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 MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{

View File

@ -14,7 +14,6 @@ import { computed } from 'vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
import copyToClipboard from '@/scripts/copy-to-clipboard.js'; import copyToClipboard from '@/scripts/copy-to-clipboard.js';
import { url } from '@/config.js'; import { url } from '@/config.js';
import { popout as popout_ } from '@/scripts/popout.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { useRouter } from '@/router.js'; import { useRouter } from '@/router.js';

View File

@ -4,11 +4,8 @@
*/ */
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest';
import { userEvent, waitFor, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import MkAd from './MkAd.vue'; import MkAd from './MkAd.vue';
import { i18n } from '@/i18n.js';
let lock: Promise<undefined> | undefined; let lock: Promise<undefined> | undefined;

View File

@ -53,7 +53,7 @@ import { PageHeaderItem } from '@/types/page-header.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
tabs?: Tab[]; tabs?: Tab[];
tab?: string; tab?: string;
actions?: PageHeaderItem[]; actions?: PageHeaderItem[] | null;
thin?: boolean; thin?: boolean;
displayMyAvatar?: boolean; displayMyAvatar?: boolean;
}>(), { }>(), {

View File

@ -5,7 +5,6 @@
/* eslint-disable @typescript-eslint/explicit-function-return-type */ /* eslint-disable @typescript-eslint/explicit-function-return-type */
import { expect } from '@storybook/jest'; import { expect } from '@storybook/jest';
import { userEvent, within } from '@storybook/testing-library';
import { StoryObj } from '@storybook/vue3'; import { StoryObj } from '@storybook/vue3';
import { userDetailed } from '../../../.storybook/fakes'; import { userDetailed } from '../../../.storybook/fakes';
import MkUserName from './MkUserName.vue'; import MkUserName from './MkUserName.vue';

View File

@ -16,7 +16,6 @@ import * as mfm from 'mfm-js';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import { TextBlock } from './block.type'; import { TextBlock } from './block.type';
import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js'; import { extractUrlFromMfm } from '@/scripts/extract-url-from-mfm.js';
import { $i } from '@/account.js';
const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue')); const MkUrlPreview = defineAsyncComponent(() => import('@/components/MkUrlPreview.vue'));

Some files were not shown because too many files have changed in this diff Show More