Compare commits

..

43 commits

Author SHA1 Message Date
Mizah a858ee31c6 Added doc comment to alert()
Some checks failed
Check SPDX-License-Identifier / check-spdx-license-id (push) Has been cancelled
Check copyright year / check_copyright_year (push) Has been cancelled
Publish Docker image (develop) / Build (linux/amd64) (push) Has been cancelled
Publish Docker image (develop) / Build (linux/arm64) (push) Has been cancelled
Dockle / dockle (push) Has been cancelled
Lint / pnpm_install (push) Has been cancelled
Storybook / build (push) Has been cancelled
Test (frontend) / vitest (20.16.0) (push) Has been cancelled
Test (frontend) / e2e (chrome, 20.16.0) (push) Has been cancelled
Test (production install and build) / production (20.16.0) (push) Has been cancelled
Publish Docker image (develop) / merge (push) Has been cancelled
Lint / lint (backend) (push) Has been cancelled
Lint / lint (frontend) (push) Has been cancelled
Lint / lint (frontend-embed) (push) Has been cancelled
Lint / lint (frontend-shared) (push) Has been cancelled
Lint / lint (misskey-bubble-game) (push) Has been cancelled
Lint / lint (misskey-js) (push) Has been cancelled
Lint / lint (misskey-reversi) (push) Has been cancelled
Lint / lint (sw) (push) Has been cancelled
Lint / typecheck (backend) (push) Has been cancelled
Lint / typecheck (misskey-js) (push) Has been cancelled
Lint / typecheck (sw) (push) Has been cancelled
2024-11-17 14:34:54 +02:00
Mizah 0bd7ed8191 Added doc comment to toast() 2024-11-17 14:34:47 +02:00
Mizah ee574ae154 Added doc comment to pageWindow 2024-11-17 14:34:39 +02:00
Mizah a505f36252 Added doc comment to popup 2024-11-17 14:34:31 +02:00
Mizah a516383c66 Added internal comments to popup() 2024-11-17 14:34:21 +02:00
Mizah 1613dafc39 Added docs to z-index generator. 2024-11-17 14:23:44 +02:00
Mizah 8457fa9b3b Added docs to popup list and popup id counter. 2024-11-17 14:23:31 +02:00
Mizah 2e51e779e7 Added docs idb proxy.
Some checks are pending
Check SPDX-License-Identifier / check-spdx-license-id (push) Waiting to run
Check copyright year / check_copyright_year (push) Waiting to run
Publish Docker image (develop) / Build (linux/amd64) (push) Waiting to run
Publish Docker image (develop) / Build (linux/arm64) (push) Waiting to run
Publish Docker image (develop) / merge (push) Blocked by required conditions
Dockle / dockle (push) Waiting to run
Storybook / build (push) Waiting to run
Test (production install and build) / production (20.16.0) (push) Waiting to run
2024-11-17 14:02:17 +02:00
Mizah 7f6b486976 Added docs to Keys for localStorage. 2024-11-17 14:02:11 +02:00
Mizah 8c1508fae4 Added docs to miLocalStorage. 2024-11-17 14:02:08 +02:00
Mizah aeb568664d Added docs (and observations) to notes count. 2024-11-17 14:02:02 +02:00
Mizah ecb990fb77 Added docs to $i, iAmModerator, iAmAdmin. 2024-11-17 14:01:53 +02:00
饺子w (Yumechi) a11b77a415
fix(backend): Webhook Test一致性 (#14863)
* fix(backend): Webhook Test一致性

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

* UserWebhookPayload<'followed'> 修正

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>

---------

Signed-off-by: eternal-flame-AD <yume@yumechi.jp>
2024-11-12 09:51:18 +09:00
syuilo 1bc4f400c0 Merge branch 'develop' of https://github.com/misskey-dev/misskey into develop 2024-11-11 16:35:23 +09:00
syuilo 458c72c153 Update about-misskey.vue 2024-11-11 16:35:13 +09:00
syuilo 6bd3ed2074
Update CHANGELOG.md 2024-11-10 15:10:04 +09:00
かっこかり 31e5f0bd09
fix(frontend): メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正 (#14928)
* fix(frontend): メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正

* Update MkSignupDialog.form.vue

* fix condition
2024-11-10 15:08:58 +09:00
かっこかり e0a83e9c9e
Update CHANGELOG.md (書き方を揃える) 2024-11-09 15:57:10 +09:00
かっこかり 1496700b37
Update CHANGELOG.md
たぶんリリースワークフローはこうしないと認識してくれない
2024-11-09 15:51:49 +09:00
syuilo 00cbf9fe80
Update CONTRIBUTING.md 2024-11-09 14:09:02 +09:00
github-actions[bot] cf09aa21f0 Bump version to 2024.11.0-alpha.0 2024-11-09 02:28:02 +00:00
github-actions[bot] 9f7d41eb47 Bump version to 2024.10.2-alpha.3 2024-11-09 02:25:42 +00:00
かっこかり 4a62051ce7
fix(backend): ローカルユーザーへのメンションを含むノートが連合される際に正しいURLに変換されないことがある問題を修正 (#14879)
* fix: make sure mentions of local users get rendered correctly during AP delivery (resolves #645)

* Update Changelog

* indent

---------

Co-authored-by: Laura Hausmann <laura@hausmann.dev>
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-11-09 10:58:09 +09:00
かっこかり 3a421837bf
refactor(frontend): 動画UIのフルスクリーン周りの調整 (#14877)
* refactor(frontend): フルスクリーン周りの調整

(cherry picked from commit 783032caec5853d78d5af3391e29cf364f2282e8)

* refactor(frontend): deviceKindの循環参照を除去

(cherry picked from commit 1ca471f57e968a1a6e2259bde4a7c6da1fe0c54e)

* fix

---------

Co-authored-by: taiyme <53635909+taiyme@users.noreply.github.com>
2024-11-09 10:57:04 +09:00
momoirodouhu a4c5ce1413
enhance(backend) : リモートユーザーの照会をオリジナルにリダイレクトするように (#12892) (#14897)
* enhance(backend) : リモートユーザーの照会をオリジナルにリダイレクトするように (#12892)

* オリジンリダイレクトのテストをtodoとして追加。

e2eテストにリモートユーザー考慮のテストがなさそうなので。

次のコマンドで動くことは確認済みです。
curl "http://localhost:3000/@foo@bar" -H "accept: application/activity+json" -L

* Acctのパースを既存のパーサーでするように修正

* lint
2024-11-09 10:54:44 +09:00
かっこかり e75b62f3f5
enhance(frontend): 個別お知らせページではmetaタグを出力するように (#14902)
* enhance(frontend): 個別お知らせページではmetaタグを出力するように

* Update Changelog
2024-11-09 10:53:09 +09:00
かっこかり 5b60ae810b
fix(frontend): 外部URLへのリダイレクトのバリデーションを強化 (#14919)
* Fix code scanning alert no. 25: Incomplete URL scheme check (MisskeyIO#799)

* Fix code scanning alert no. 26: Incomplete URL scheme check

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

* Fix code scanning alert no. 25: Incomplete URL scheme check

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>

---------

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
(cherry picked from commit 7d7552e076c0152a5966e919be0e9a60b3736208)

* ✌️

---------

Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-11-09 10:52:07 +09:00
かっこかり 98b4717c45
fix(backend): SQLのサニタイズを強化 (#14920)
* Fix code scanning alert no. 28: Incomplete string escaping or encoding (MisskeyIO#800)

Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
(cherry picked from commit 443335c662b14f609d6a81a8f3807e95709aebc1)

* ✌️

---------

Co-authored-by: あわわわとーにゅ <17376330+u1-liquid@users.noreply.github.com>
2024-11-09 10:51:28 +09:00
syuilo 8a4ce16e90
Update CONTRIBUTING.md 2024-11-08 18:00:55 +09:00
4ster1sk 794cb9ffe2
fix(backend): followedMessageではなくdescriptionになっていたのを修正 (#14908) 2024-11-07 17:16:51 +09:00
syuilo 0b976064ca
Update CHANGELOG.md 2024-11-07 15:10:38 +09:00
4ster1sk bca690f256
fix(backend): フォロワーへのメッセージの絵文字をemojisに含めるように (#14904) 2024-11-07 15:10:10 +09:00
Linca f1eb17f66c
chore: little type trick in pizzax.ts (#14891)
Make `makeGetterSetter` take the correct type associated with getter and setter
2024-11-06 22:01:58 +09:00
かっこかり b1c82213a3
fix(backend): FTT無効時にユーザーリストタイムラインが使用できない問題を修正 (#14878)
* fix: return getfromdb when FanoutTimeline is not enabled

* Update Changelog

* fix

---------

Co-authored-by: Lhc_fl <lhcfl@outlook.com>
2024-11-06 22:01:21 +09:00
かっこかり a896c39dbf
fix(frontend): ノート投稿ボタンにホバー時のスタイルが適用されていない (#14887)
* fix(frontend): ノート投稿ボタンにホバー時のスタイルが適用されていない (#305)

(cherry picked from commit 711ab846a967feeddbe0c908bee4b91646cec321)

* Update Changelog

---------

Co-authored-by: taiy <53635909+taiyme@users.noreply.github.com>
2024-11-06 15:15:28 +09:00
かっこかり 6718a54f6f
fix(backend): ノートを連合する際にリモートユーザーのacctの大小文字を区別して処理している問題を修正 (#14880)
* fix: make sure outgoing remote mentions get resolved correctly if referenced with non-canonical casing (resolves #646)

* Update Changelog

* Update Changelog

* indent

---------

Co-authored-by: Laura Hausmann <laura@hausmann.dev>
2024-11-03 08:26:51 +09:00
かっこかり d57b8bf2e2
fix(frontend): withSensitiveフィルタ周りの挙動修正 (#14884)
* fix(frontend): withSensitiveフィルタ周りの挙動修正

* Update MkNote.vue
2024-11-03 08:23:52 +09:00
syuilo 224bbd486f refactor 2024-10-31 13:50:50 +09:00
syuilo 724dea8136 lint 2024-10-31 13:47:47 +09:00
syuilo ceb60d61b0 refactor 2024-10-31 13:47:30 +09:00
かっこかり 17d9aca5a7
refactor(frontend): asとanyをすぐなおせる範囲で除去 (#14848)
* refactor(frontend): できるだけanyを除去

* refactor

* lint

* fix

* remove unused

* Update packages/frontend/src/components/MkReactionsViewer.details.vue

* Update packages/frontend/src/components/MkUsersTooltip.vue

---------

Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
2024-10-31 13:46:42 +09:00
かっこかり 7fc8a2a7b0
fix(frontend): 一部のノート表示で設定にかかわらずセンシティブなファイルを含むノートが最小化される問題を修正
Fix https://github.com/misskey-dev/misskey/pull/14772#discussion_r1821707117
2024-10-30 09:57:54 +09:00
github-actions[bot] a96f09cee3 Bump version to 2024.10.2-alpha.2 2024-10-28 12:23:59 +00:00
106 changed files with 808 additions and 456 deletions

View file

@ -1,8 +1,10 @@
## 2024.10.2 ## 2024.11.0
### General ### General
- Feat: コンテンツの表示にログインを必須にできるように - Feat: コンテンツの表示にログインを必須にできるように
- Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように - Feat: 過去のノートを非公開化/フォロワーのみ表示可能にできるように
- Enhance: 依存関係の更新
- Enhance: l10nの更新
### Client ### Client
- Enhance: Bull DashboardでRelationship Queueの状態も確認できるように - Enhance: Bull DashboardでRelationship Queueの状態も確認できるように
@ -15,22 +17,35 @@
- どのアカウントで認証しようとしているのかがわかるように - どのアカウントで認証しようとしているのかがわかるように
- 認証するアカウントを切り替えられるように - 認証するアカウントを切り替えられるように
- Enhance: Self-XSS防止用の警告を追加 - Enhance: Self-XSS防止用の警告を追加
- Enhance: カタルーニャ語 (ca-ES) に対応 - Enhance: カタルーニャ語 (ca-ES) に対応
- Enhance: 個別お知らせページではMetaタグを出力するように
- Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正 - Fix: 通知の範囲指定の設定項目が必要ない通知設定でも範囲指定の設定がでている問題を修正
- Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正 - Fix: Turnstileが失敗・期限切れした際にも成功扱いとなってしまう問題を修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768) (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/768)
- Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正 - Fix: デッキのタイムラインカラムで「センシティブなファイルを含むノートを表示」設定が使用できなかった問題を修正
- Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used - Fix: Encode RSS urls with escape sequences before fetching allowing query parameters to be used
- Fix: リンク切れを修正 - Fix: リンク切れを修正
= Fix: ノート投稿ボタンにホバー時のスタイルが適用されていないのを修正
(Cherry-picked from https://github.com/taiyme/misskey/pull/305)
- Fix: メールアドレス登録有効化時の「完了」ダイアログボックスの表示条件を修正
### Server ### Server
- Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように - Enhance: 起動前の疎通チェックで、DBとメイン以外のRedisの疎通確認も行うように
(Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588) (Based on https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/588)
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/715)
- Enhance: リモートユーザーの照会をオリジナルにリダイレクトするように
- Fix: フォロワーへのメッセージの絵文字をemojisに含めるように
- Fix: Nested proxy requestsを検出した際にブロックするように - Fix: Nested proxy requestsを検出した際にブロックするように
[ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236) [ghsa-gq5q-c77c-v236](https://github.com/misskey-dev/misskey/security/advisories/ghsa-gq5q-c77c-v236)
- Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正 - Fix: 招待コードの発行可能な残り数算出に使用すべきロールポリシーの値が違う問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706) (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/706)
- Fix: 連合への配信時に、acctの大小文字が区別されてしまい正しくメンションが処理されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/711)
- Fix: ローカルユーザーへのメンションを含むートが連合される際に正しいURLに変換されないことがある問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/712)
- Fix: FTT無効時にユーザーリストタイムラインが使用できない問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/709)
- Fix: User Webhookテスト機能のMock Payloadを修正
### Misskey.js ### Misskey.js
- Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正 - Fix: Stream初期化時、別途WebSocketを指定する場合の型定義を修正

View file

@ -83,6 +83,10 @@ One should not add property that has defined before by other implementation, or
## Reviewers guide ## Reviewers guide
Be willing to comment on the good points and not just the things you want fixed 💯 Be willing to comment on the good points and not just the things you want fixed 💯
読んでおくといいやつ
- https://blog.lacolaco.net/posts/1e2cf439b3c2/
- https://konifar-zatsu.hatenadiary.jp/entry/2024/11/05/192421
### Review perspective ### Review perspective
- Scope - Scope
- Are the goals of the PR clear? - Are the goals of the PR clear?

View file

@ -8,37 +8,37 @@ search: "Cercar"
notifications: "Notificacions" notifications: "Notificacions"
username: "Nom d'usuari" username: "Nom d'usuari"
password: "Contrasenya" password: "Contrasenya"
initialPasswordForSetup: "Contrasenya inicial per fer la primera configuració " initialPasswordForSetup: "Contrasenya inicial per la configuració inicial"
initialPasswordIsIncorrect: "La contrasenya no és correcta." initialPasswordIsIncorrect: "La contrasenya no és correcta."
initialPasswordForSetupDescription: "Fes servir la contrasenya que has fet servir al fitxer de configuració, si tu mateix has instal·lat Misskey.\nSi fas servir una empresa d'allotjament de Misskey, fes servir la contrasenya que t'han donat.\nSi no has posat cap contrasenya deixar l'espai en blanc." initialPasswordForSetupDescription: "Fes servir la contrasenya que has fet servir al fitxer de configuració, si tu mateix has instal·lat Misskey.\nSi fas servir una empresa d'allotjament de Misskey, fes servir la contrasenya que t'han donat.\nSi no has posat cap contrasenya deixar l'espai en blanc."
forgotPassword: "Restableix la contrasenya " forgotPassword: "Contrasenya oblidada"
fetchingAsApObject: "Cercant al Fediverse..." fetchingAsApObject: "Cercant en el Fediverse..."
ok: "OK" ok: "OK"
gotIt: "D'acord " gotIt: "Ho he entès!"
cancel: "Cancel·lar" cancel: "Cancel·lar"
noThankYou: "No, gràcies" noThankYou: "No, gràcies"
enterUsername: "Introdueix el teu nom d'usuari" enterUsername: "Introdueix el teu nom d'usuari"
renotedBy: "Impulsat per {user}" renotedBy: "Impulsat per {user}"
noNotes: "Cap nota" noNotes: "Cap nota"
noNotifications: "Cap notificació" noNotifications: "Cap notificació"
instance: "Instància " instance: "Servidor"
settings: "Preferències" settings: "Preferències"
notificationSettings: "Configurar les notificacions" notificationSettings: "Paràmetres de notificacions"
basicSettings: "Configuració bàsica" basicSettings: "Configuració bàsica"
otherSettings: "Altres configuracions" otherSettings: "Configuració avançada"
openInWindow: "Obrir en una finestra nova" openInWindow: "Obrir en una nova finestra"
profile: "Perfil" profile: "Perfil"
timeline: "Línia de temps" timeline: "Línia de temps"
noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia." noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia."
login: "Iniciar sessió" login: "Iniciar sessió"
loggingIn: "Iniciar la sessió " loggingIn: "Identificant-se"
logout: "Tancar la sessió" logout: "Tancar la sessió"
signup: "Registrar-se" signup: "Registrar-se"
uploading: "Pujant..." uploading: "Pujant..."
save: "Desa" save: "Desa"
users: "Usuaris" users: "Usuaris"
addUser: "Afegir un usuari" addUser: "Afegir un usuari"
favorite: "Afegeix als preferits" favorite: "Afegir a preferits"
favorites: "Favorits" favorites: "Favorits"
unfavorite: "Eliminar dels preferits" unfavorite: "Eliminar dels preferits"
favorited: "Afegit als preferits." favorited: "Afegit als preferits."
@ -50,26 +50,26 @@ copyContent: "Copiar el contingut"
copyLink: "Copiar l'enllaç" copyLink: "Copiar l'enllaç"
copyLinkRenote: "Copiar l'enllaç de la renota" copyLinkRenote: "Copiar l'enllaç de la renota"
delete: "Elimina" delete: "Elimina"
deleteAndEdit: "Eliminar i editar" deleteAndEdit: "Elimina i edita"
deleteAndEditConfirm: "Segur que vols eliminar aquesta publicació i editar-la? Perdràs totes les reaccions, impulsos i respostes." deleteAndEditConfirm: "Segur que vols eliminar aquesta publicació i editar-la? Perdràs totes les reaccions, impulsos i respostes."
addToList: "Afegir a una llista" addToList: "Afegir a una llista"
addToAntenna: "Afegir a una antena" addToAntenna: "Afegir a l'antena"
sendMessage: "Enviar un missatge" sendMessage: "Enviar un missatge"
copyRSS: "Copiar RSS" copyRSS: "Copiar RSS"
copyUsername: "Copiar nom d'usuari" copyUsername: "Copiar nom d'usuari"
copyUserId: "Copiar ID d'usuari" copyUserId: "Copiar ID d'usuari"
copyNoteId: "Copiar ID de la nota" copyNoteId: "Copiar ID de nota"
copyFileId: "Copiar ID de l'arxiu" copyFileId: "Copiar ID d'arxiu"
copyFolderId: "Copiar ID de la carpeta" copyFolderId: "Copiar ID de carpeta"
copyProfileUrl: "Copiar adreça URL del perfil" copyProfileUrl: "Copiar URL del perfil"
searchUser: "Cercar un usuari" searchUser: "Cercar un usuari"
searchThisUsersNotes: "Cercar les publicacions de l'usuari" searchThisUsersNotes: "Cerca les publicacions de l'usuari"
reply: "Respon" reply: "Respondre"
loadMore: "Carregar més" loadMore: "Carregar més"
showMore: "Veure més" showMore: "Veure més"
showLess: "Mostrar menys" showLess: "Mostra menys"
youGotNewFollower: "t'ha seguit" youGotNewFollower: "t'ha seguit"
receiveFollowRequest: "Has rebut una sol·licitud de seguiment" receiveFollowRequest: "Sol·licitud de seguiment rebuda"
followRequestAccepted: "Sol·licitud de seguiment acceptada" followRequestAccepted: "Sol·licitud de seguiment acceptada"
mention: "Menció" mention: "Menció"
mentions: "Mencions" mentions: "Mencions"
@ -78,25 +78,25 @@ importAndExport: "Importar / Exportar"
import: "Importar" import: "Importar"
export: "Exporta" export: "Exporta"
files: "Fitxers" files: "Fitxers"
download: "Descarregar" download: "Baixar"
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer també seran esborrades." driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer adjunt també se suprimiran."
unfollowConfirm: "Segur que vols deixar de seguir a {name}?" unfollowConfirm: "Estàs segur que vols deixar de seguir {name}?"
exportRequested: "Has sol·licitat una exportació de dades. Això pot trigar una estona. S'afegirà a la teva unitat de disc un cop estigui completada." exportRequested: "Has sol·licitat una exportació. Això pot trigar una estona. S'afegirà a la teva unitat un cop completat."
importRequested: "Has sol·licitat una importació de dades. Això pot trigar una estona." importRequested: "Has sol·licitat una importació. Això pot trigar una estona."
lists: "Llistes" lists: "Llistes"
noLists: "No tens cap llista" noLists: "No tens cap llista"
note: "Nota" note: "Nota"
notes: "Notes" notes: "Notes"
following: "Segueixes " following: "Seguint"
followers: "Seguidors" followers: "Seguidors"
followsYou: "Et segueix" followsYou: "Et segueix"
createList: "Crear llista" createList: "Crear llista"
manageLists: "Gestionar les llistes" manageLists: "Gestionar les llistes"
error: "Error" error: "Error"
somethingHappened: "S'ha produït un error" somethingHappened: "S'ha produït un error"
retry: "Torna-ho a provar" retry: "Torna-ho a intentar"
pageLoadError: "S'ha produït un error en carregar la pàgina" pageLoadError: "S'ha produït un error en carregar la pàgina"
pageLoadErrorDescription: "Això normalment és a causa d'errors a la xarxa o a la memòria cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després d'esperar un temps." pageLoadErrorDescription: "Això normalment es deu a errors de xarxa o a la memòria cau del navegador. Prova d'esborrar la memòria cau i torna-ho a provar després d'esperar una estona."
serverIsDead: "Aquest servidor no respon. Espera una estona i torna-ho a provar." serverIsDead: "Aquest servidor no respon. Espera una estona i torna-ho a provar."
youShouldUpgradeClient: "Per veure aquesta pàgina, actualitzeu-la per actualitzar el vostre client." youShouldUpgradeClient: "Per veure aquesta pàgina, actualitzeu-la per actualitzar el vostre client."
enterListName: "Introdueix un nom per a la llista" enterListName: "Introdueix un nom per a la llista"
@ -104,52 +104,52 @@ privacy: "Privadesa"
makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació" makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació"
defaultNoteVisibility: "Visibilitat per defecte" defaultNoteVisibility: "Visibilitat per defecte"
follow: "Seguint" follow: "Seguint"
followRequest: "Enviar sol·licitud de seguiment" followRequest: "Enviar la sol·licitud de seguiment"
followRequests: "Sol·licituds de seguiment" followRequests: "Sol·licituds de seguiment"
unfollow: "Deixar de seguir" unfollow: "Deixar de seguir"
followRequestPending: "Sol·licituds de seguiment pendents" followRequestPending: "Sol·licituds de seguiment pendents"
enterEmoji: "Introduir un emoji" enterEmoji: "Introduir un emoji"
renote: "Impulsar " renote: "Impulsa"
unrenote: "Anul·la l'impuls" unrenote: "Anul·la l'impuls"
renoted: "S'ha impulsat" renoted: "S'ha impulsat"
renotedToX: "Impulsat per {name}." renotedToX: "Impulsat per {name}."
cantRenote: "No es pot impulsar aquesta publicació" cantRenote: "No es pot impulsar aquesta publicació"
cantReRenote: "No es pot impulsar un impuls." cantReRenote: "No es pot impulsar l'impuls."
quote: "Cita" quote: "Cita"
inChannelRenote: "Impulsar només a un canal" inChannelRenote: "Renotar només al Canal"
inChannelQuote: "Citar només a un canal" inChannelQuote: "Citar només al Canal"
renoteToChannel: "Impulsar a un canal" renoteToChannel: "Impulsa a un canal"
renoteToOtherChannel: "Impulsar a un altre canal" renoteToOtherChannel: "Impulsa a un altre canal"
pinnedNote: "Nota fixada" pinnedNote: "Nota fixada"
pinned: "Fixar al perfil" pinned: "Fixar al perfil"
you: "Tu" you: "Tu"
clickToShow: "Fes clic per mostrar" clickToShow: "Fes clic per mostrar"
sensitive: "Sensible" sensitive: "NSFW"
add: "Afegir" add: "Afegir"
reaction: "Reacció " reaction: "Reaccions"
reactions: "Reaccions" reactions: "Reaccions"
emojiPicker: "Selector d'emojis" emojiPicker: "Selecció d'emojis"
pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb qui vols reaccionar" pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
pinnedEmojisSettingDescription: "Selecciona quins emojis vols deixar fixats i es mostrin en obrir el selector d'emojis" pinnedEmojisSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
emojiPickerDisplay: "Mostrar el selector d'emojis" emojiPickerDisplay: "Visualitza el selector d'emojis"
overwriteFromPinnedEmojisForReaction: "Reemplaça els emojis de la reacció" overwriteFromPinnedEmojisForReaction: "Reemplaça els emojis de la reacció"
overwriteFromPinnedEmojis: "Sobreescriu els emojis fixats al panel de reaccions" 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"
deleteFile: "Esborrar l'arxiu " deleteFile: "Esborrar l'arxiu "
markAsSensitive: "Marcar com a sensible" markAsSensitive: "Marcar com a NSFW"
unmarkAsSensitive: "Deixar de marcar com a sensible" unmarkAsSensitive: "Deixar de marcar com a sensible"
enterFileName: "Defineix nom del fitxer" enterFileName: "Defineix nom del fitxer"
mute: "Silencia" mute: "Silencia"
unmute: "Deixa de silenciar" unmute: "Deixa de silenciar"
renoteMute: "Silenciar impulsos" renoteMute: "Silenciar Renotes"
renoteUnmute: "Treure el silenci dels impulsos" renoteUnmute: "Treure el silenci de les renotes"
block: "Bloqueja" block: "Bloqueja"
unblock: "Desbloqueja" unblock: "Desbloqueja"
suspend: "Suspèn" suspend: "Suspèn"
unsuspend: "Deixa de suspendre" unsuspend: "Deixa de suspendre"
blockConfirm: "Vols bloquejar-lo?" blockConfirm: "Vols bloquejar?"
unblockConfirm: "Vols desbloquejar-lo?" unblockConfirm: "Vols desbloquejar-lo?"
suspendConfirm: "Estàs segur que vols suspendre aquest compte?" suspendConfirm: "Estàs segur que vols suspendre aquest compte?"
unsuspendConfirm: "Estàs segur que vols treure la suspensió d'aquest compte?" unsuspendConfirm: "Estàs segur que vols treure la suspensió d'aquest compte?"
@ -175,11 +175,11 @@ youCanCleanRemoteFilesCache: "Pots netejar la memòria cau fent clic al botó de
cacheRemoteSensitiveFiles: "Posar a la memòria cau arxius remots sensibles" cacheRemoteSensitiveFiles: "Posar a la memòria cau arxius remots sensibles"
cacheRemoteSensitiveFilesDescription: "Quan aquesta opció és desactiva, els arxius remots sensibles es carregant directament del servidor d'origen sense que es guardin a la memòria cau." cacheRemoteSensitiveFilesDescription: "Quan aquesta opció és desactiva, els arxius remots sensibles es carregant directament del servidor d'origen sense que es guardin a la memòria cau."
flagAsBot: "Marca aquest compte com a bot" flagAsBot: "Marca aquest compte com a bot"
flagAsBotDescription: "Activa aquesta opció si el compte el controla un programa. Si s'activa, actuarà com un senyal per altres desenvolupadors per prevenir cadenes d'interacció sense fi i ajustar els paràmetres interns de Misskey pe tractar el compte com un bot." flagAsBotDescription: "Marca aquest compte com a bot"
flagAsCat: "Marca aquest compte com a gat" flagAsCat: "Marca aquest compte com a gat"
flagAsCatDescription: "Activeu aquesta opció per marcar aquest compte com a gat." flagAsCatDescription: "Activeu aquesta opció per marcar aquest compte com a gat."
flagShowTimelineReplies: "Mostra les respostes a la línia de temps" flagShowTimelineReplies: "Mostra les respostes a la línia de temps"
flagShowTimelineRepliesDescription: "Mostra les respostes dels usuaris a les notes d'altres usuaris a la línia de temps." flagShowTimelineRepliesDescription: "Mostra les respostes a la línia de temps"
autoAcceptFollowed: "Aprova automàticament les sol·licituds de seguiment dels usuaris que segueixes" autoAcceptFollowed: "Aprova automàticament les sol·licituds de seguiment dels usuaris que segueixes"
addAccount: "Afegeix un compte" addAccount: "Afegeix un compte"
reloadAccountsList: "Recarregar la llista de contactes" reloadAccountsList: "Recarregar la llista de contactes"
@ -204,7 +204,7 @@ selectUser: "Selecciona usuari/a"
recipient: "Destinatari" recipient: "Destinatari"
annotation: "Comentaris" annotation: "Comentaris"
federation: "Federació" federation: "Federació"
instances: "Instàncies " instances: "Servidors"
registeredAt: "Registrat a" registeredAt: "Registrat a"
latestRequestReceivedAt: "Última petició rebuda" latestRequestReceivedAt: "Última petició rebuda"
latestStatus: "Últim estat" latestStatus: "Últim estat"
@ -213,7 +213,7 @@ charts: "Gràfics"
perHour: "Per hora" perHour: "Per hora"
perDay: "Per dia" perDay: "Per dia"
stopActivityDelivery: "Deixa d'enviar activitats" stopActivityDelivery: "Deixa d'enviar activitats"
blockThisInstance: "Bloca aquesta instància " blockThisInstance: "Deixa d'enviar activitats"
silenceThisInstance: "Silencia aquesta instància " silenceThisInstance: "Silencia aquesta instància "
mediaSilenceThisInstance: "Silenciar els arxius d'aquesta instància " mediaSilenceThisInstance: "Silenciar els arxius d'aquesta instància "
operations: "Accions" operations: "Accions"
@ -228,7 +228,7 @@ network: "Xarxa"
disk: "Disc" disk: "Disc"
instanceInfo: "Informació del fitxer d'instal·lació" instanceInfo: "Informació del fitxer d'instal·lació"
statistics: "Estadístiques" statistics: "Estadístiques"
clearQueue: "Esborra la cua de feina" clearQueue: "Esborrar la cua"
clearQueueConfirmTitle: "Esteu segur que voleu esborrar la cua?" clearQueueConfirmTitle: "Esteu segur que voleu esborrar la cua?"
clearQueueConfirmText: "Les notes no lliurades que quedin a la cua no es federaran. Normalment aquesta operació no és necessària." clearQueueConfirmText: "Les notes no lliurades que quedin a la cua no es federaran. Normalment aquesta operació no és necessària."
clearCachedFiles: "Esborra la memòria cau" clearCachedFiles: "Esborra la memòria cau"
@ -254,7 +254,7 @@ processing: "S'està processant..."
preview: "Vista prèvia" preview: "Vista prèvia"
default: "Per defecte" default: "Per defecte"
defaultValueIs: "Per defecte: {value}" defaultValueIs: "Per defecte: {value}"
noCustomEmojis: "No hi ha emojis personalitzats" noCustomEmojis: "Cap emoji personalitzat"
noJobs: "No hi ha feines" noJobs: "No hi ha feines"
federating: "Federant" federating: "Federant"
blocked: "Bloquejat" blocked: "Bloquejat"
@ -268,11 +268,11 @@ instanceFollowers: "Seguidors del servidor"
instanceUsers: "Usuaris del servidor" instanceUsers: "Usuaris del servidor"
changePassword: "Canvia la contrasenya" changePassword: "Canvia la contrasenya"
security: "Seguretat" security: "Seguretat"
retypedNotMatch: "Les entrades no coincideix" retypedNotMatch: "L'entrada no coincideix"
currentPassword: "Contrasenya actual" currentPassword: "Contrasenya actual"
newPassword: "Contrasenya nova" newPassword: "Contrasenya nova"
newPasswordRetype: "Contrasenya nova (repeteix-la)" newPasswordRetype: "Contrasenya nou (repeteix-la)"
attachFile: "Afegeix un arxiu" attachFile: "Adjunta fitxers"
more: "Més" more: "Més"
featured: "Destacat" featured: "Destacat"
usernameOrUserId: "Nom o ID d'usuari" usernameOrUserId: "Nom o ID d'usuari"
@ -282,25 +282,25 @@ announcements: "Anuncis"
imageUrl: "URL de la imatge" imageUrl: "URL de la imatge"
remove: "Eliminar" remove: "Eliminar"
removed: "Eliminat" removed: "Eliminat"
removeAreYouSure: "Segur que vols esborrar «{x}»?" removeAreYouSure: "Segur que voleu retirar «{x}»?"
deleteAreYouSure: "Segur que vols esborrar «{x}»?" deleteAreYouSure: "Segur que voleu retirar «{x}»?"
resetAreYouSure: "Segur que vols restablir-ho?" resetAreYouSure: "Segur que voleu restablir-ho?"
areYouSure: "Estàs segur?" areYouSure: "Està segur?"
saved: "S'ha desat" saved: "S'ha desat"
messaging: "Xat" messaging: "Xat"
upload: "Puja" upload: "Puja"
keepOriginalUploading: "Guarda la imatge original" keepOriginalUploading: "Guarda la imatge original"
keepOriginalUploadingDescription: "Guarda la imatge pujada sense modificar. Si està desactivat, es generarà una versió per visualitzar a la web en pujar la imatge." 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 del Disc" fromDrive: "Des de la unitat"
fromUrl: "Des d'un enllaç" fromUrl: "Des d'un enllaç"
uploadFromUrl: "Carrega des d'un enllaç" uploadFromUrl: "Carrega des d'un enllaç"
uploadFromUrlDescription: "Enllaç del fitxer que vols carregar" uploadFromUrlDescription: "Enllaç del fitxer que vols carregar"
uploadFromUrlRequested: "Càrrega sol·licitada" uploadFromUrlRequested: "Càrrega sol·licitada"
uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot trigar un temps" uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot prendre un temps"
explore: "Explora" explore: "Explora"
messageRead: "Vist" messageRead: "Vist"
noMoreHistory: "No hi ha res més per veure" noMoreHistory: "No hi resta més per veure"
startMessaging: "Comença a xatejar" startMessaging: "Començar a xatejar"
nUsersRead: "Vist per {n}" nUsersRead: "Vist per {n}"
agreeTo: "Accepto que {0}" agreeTo: "Accepto que {0}"
agree: "Hi estic d'acord" agree: "Hi estic d'acord"
@ -312,7 +312,7 @@ home: "Inici"
remoteUserCaution: "Ja que aquest usuari resideix a una instància remota, la informació mostrada es podria trobar incompleta." 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: "Imatge" image: "Imatges"
birthday: "Aniversari" birthday: "Aniversari"
yearsOld: "{age} anys" yearsOld: "{age} anys"
registeredDate: "Data de registre" registeredDate: "Data de registre"
@ -327,10 +327,10 @@ 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" drive: "Unitat"
fileName: "Nom del Fitxer" fileName: "Nom del Fitxer"
selectFile: "Selecciona un fitxer" selectFile: "Selecciona fitxers"
selectFiles: "Selecciona fitxers" selectFiles: "Selecciona fitxers"
selectFolder: "Selecció de carpeta" selectFolder: "Selecció de carpeta"
selectFolders: "Selecció de carpetes" selectFolders: "Selecció de carpeta"
fileNotSelected: "Cap fitxer seleccionat" fileNotSelected: "Cap fitxer seleccionat"
renameFile: "Canvia el nom del fitxer" renameFile: "Canvia el nom del fitxer"
folderName: "Nom de la carpeta" folderName: "Nom de la carpeta"
@ -359,9 +359,9 @@ reload: "Actualitza"
doNothing: "Ignora" doNothing: "Ignora"
reloadConfirm: "Vols recarregar?" reloadConfirm: "Vols recarregar?"
watch: "Veure" watch: "Veure"
unwatch: "Deixa de veure" unwatch: "Deixar de veure"
accept: "Acceptar" accept: "Acceptar"
reject: "Denega" reject: "Denegar"
normal: "Normal" normal: "Normal"
instanceName: "Nom del servidor" instanceName: "Nom del servidor"
instanceDescription: "Descripció del servidor" instanceDescription: "Descripció del servidor"
@ -382,7 +382,7 @@ enableLocalTimeline: "Activa la línia de temps local"
enableGlobalTimeline: "Activa la línia de temps global" enableGlobalTimeline: "Activa la línia de temps global"
disablingTimelinesInfo: "Fins i tot si aquestes línies de temps són desactivades, els administradors i els moderadors poden continuar visualitzant per conveniència." disablingTimelinesInfo: "Fins i tot si aquestes línies de temps són desactivades, els administradors i els moderadors poden continuar visualitzant per conveniència."
registration: "Registre" registration: "Registre"
enableRegistration: "Permet el registre de nous usuaris" enableRegistration: "Permet els registres d'usuaris"
invite: "Convida" invite: "Convida"
driveCapacityPerLocalAccount: "Capacitat del disc per usuaris locals" driveCapacityPerLocalAccount: "Capacitat del disc per usuaris locals"
driveCapacityPerRemoteAccount: "Capacitat del disc per usuaris remots" driveCapacityPerRemoteAccount: "Capacitat del disc per usuaris remots"
@ -393,20 +393,20 @@ basicInfo: "Informació bàsica"
pinnedUsers: "Usuaris fixats" pinnedUsers: "Usuaris fixats"
pinnedUsersDescription: "Llista d'usuaris, separats per salts de línia, que seran fixats a la pestanya \"Explorar\"." pinnedUsersDescription: "Llista d'usuaris, separats per salts de línia, que seran fixats a la pestanya \"Explorar\"."
pinnedPages: "Pàgines fixades" pinnedPages: "Pàgines fixades"
pinnedPagesDescription: "Escriu les adreces de les pàgines que vols fixar a la pàgina d'inici d'aquesta instància. Separades per salts de línia." pinnedPagesDescription: "Escriu els camins de les pàgines que vols fixar a la pàgina d'inici d'aquesta instància. Separades per salts de línia."
pinnedClipId: "ID del retall fixat" pinnedClipId: "ID del retall fixat"
pinnedNotes: "Nota fixada" pinnedNotes: "Nota fixada"
hcaptcha: "hCaptcha" hcaptcha: "hCaptcha"
enableHcaptcha: "Activa hCaptcha" enableHcaptcha: "Activar hCaptcha"
hcaptchaSiteKey: "Clau del lloc" hcaptchaSiteKey: "Clau del lloc"
hcaptchaSecretKey: "Clau secreta" hcaptchaSecretKey: "Clau secreta"
mcaptcha: "mCaptcha" mcaptcha: "mCaptcha"
enableMcaptcha: "Activa mCaptcha" enableMcaptcha: "Activar mCaptcha"
mcaptchaSiteKey: "Clau del lloc" mcaptchaSiteKey: "Clau del lloc"
mcaptchaSecretKey: "Clau secreta" mcaptchaSecretKey: "Clau secreta"
mcaptchaInstanceUrl: "Adreça URL del servidor mCaptcha" mcaptchaInstanceUrl: "Adreça URL del servidor mCaptcha"
recaptcha: "reCAPTCHA" recaptcha: "reCAPTCHA"
enableRecaptcha: "Activa reCAPTCHA" enableRecaptcha: "Activar reCAPTCHA"
recaptchaSiteKey: "Clau del lloc" recaptchaSiteKey: "Clau del lloc"
recaptchaSecretKey: "Clau secreta" recaptchaSecretKey: "Clau secreta"
turnstile: "Turnstile" turnstile: "Turnstile"
@ -448,14 +448,14 @@ aboutMisskey: "Quant a Misskey"
administrator: "Administrador/a" administrator: "Administrador/a"
token: "Codi de verificació" token: "Codi de verificació"
2fa: "Autenticació de doble factor" 2fa: "Autenticació de doble factor"
setupOf2fa: "Configura l'autenticació de doble factor" setupOf2fa: "Configurar l'autenticació de doble factor"
totp: "Aplicació d'autenticació" totp: "Aplicació d'autenticació"
totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d'autenticació" totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d'autenticació"
moderator: "Moderador/a" moderator: "Moderador/a"
moderation: "Moderació" moderation: "Moderació"
moderationNote: "Nota de moderació " moderationNote: "Nota de moderació "
moderationNoteDescription: "Pots escriure notes que es compartiran entre els moderadors." moderationNoteDescription: "Pots escriure notes que es compartiran entre els moderadors."
addModerationNote: "Afegeix una nota de moderació " addModerationNote: "Afegir una nota de moderació "
moderationLogs: "Registre de moderació " moderationLogs: "Registre de moderació "
nUsersMentioned: "{n} usuaris mencionats" nUsersMentioned: "{n} usuaris mencionats"
securityKeyAndPasskey: "Clau de seguretat / Clau de pas" securityKeyAndPasskey: "Clau de seguretat / Clau de pas"
@ -471,13 +471,13 @@ reduceUiAnimation: "Redueix les animacions de la interfície"
share: "Comparteix" share: "Comparteix"
notFound: "No s'ha trobat" notFound: "No s'ha trobat"
notFoundDescription: "No es troba cap pàgina que correspongui a aquesta adreça" notFoundDescription: "No es troba cap pàgina que correspongui a aquesta adreça"
uploadFolder: "Carpeta per defecte on desar els arxius pujats" uploadFolder: "Carpeta per defecte per pujades"
markAsReadAllNotifications: "Marca totes les notificacions com a llegides" markAsReadAllNotifications: "Marca totes les notificacions com a llegides"
markAsReadAllUnreadNotes: "Marca-ho tot com a llegit" markAsReadAllUnreadNotes: "Marca-ho tot com a llegit"
markAsReadAllTalkMessages: "Marcar tots els missatges com llegits" markAsReadAllTalkMessages: "Marcar tots els missatges com llegits"
help: "Ajuda" help: "Ajuda"
inputMessageHere: "Escriu aquí el teu missatge " inputMessageHere: "Escriu aquí el teu missatge "
close: "Tanca" close: "Tancar"
invites: "Convida" invites: "Convida"
members: "Membres" members: "Membres"
transfer: "Transferir" transfer: "Transferir"
@ -508,7 +508,7 @@ normalPassword: "Bona contrasenya"
strongPassword: "Contrasenya segura" strongPassword: "Contrasenya segura"
passwordMatched: "Correcte!" passwordMatched: "Correcte!"
passwordNotMatched: "No coincideix" passwordNotMatched: "No coincideix"
signinWith: "Inicia sessió amb {x}" signinWith: "Inicia sessió amb amb {x}"
signinFailed: "Autenticació sense èxit. Intenta-ho un altre cop utilitzant la contrasenya i el nom correctes." signinFailed: "Autenticació sense èxit. Intenta-ho un altre cop utilitzant la contrasenya i el nom correctes."
or: "O" or: "O"
language: "Idioma" language: "Idioma"
@ -591,7 +591,7 @@ chooseEmoji: "Tria un emoji"
unableToProcess: "L'operació no pot ser completada " unableToProcess: "L'operació no pot ser completada "
recentUsed: "Utilitzat recentment" recentUsed: "Utilitzat recentment"
install: "Instal·lació " install: "Instal·lació "
uninstall: "Desinstal·la" uninstall: "Desinstal·lar "
installedApps: "Aplicacions autoritzades " installedApps: "Aplicacions autoritzades "
nothing: "No hi ha res per veure aquí " nothing: "No hi ha res per veure aquí "
installedDate: "Data d'instal·lació" installedDate: "Data d'instal·lació"
@ -608,13 +608,13 @@ output: "Sortida"
script: "Script" script: "Script"
disablePagesScript: "Desactivar AiScript a les pàgines " disablePagesScript: "Desactivar AiScript a les pàgines "
updateRemoteUser: "Actualitzar la informació de l'usuari remot" updateRemoteUser: "Actualitzar la informació de l'usuari remot"
unsetUserAvatar: "Desactiva l'avatar " unsetUserAvatar: "Desactivar l'avatar "
unsetUserAvatarConfirm: "Segur que vols desactivar l'avatar?" unsetUserAvatarConfirm: "Segur que vols desactivar l'avatar?"
unsetUserBanner: "Desactiva el bàner " unsetUserBanner: "Desactivar el bàner "
unsetUserBannerConfirm: "Segur que vols desactivar el bàner?" unsetUserBannerConfirm: "Segur que vols desactivar el bàner?"
deleteAllFiles: "Esborra tots els arxius" deleteAllFiles: "Esborrar tots els arxius"
deleteAllFilesConfirm: "Segur que vols esborrar tots els arxius?" deleteAllFilesConfirm: "Segur que vols esborrar tots els arxius?"
removeAllFollowing: "Deixa de seguir tots els usuaris seguits" 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." 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" userSuspended: "Aquest usuari ha sigut suspès"
userSilenced: "Aquest usuari està sent silenciat" userSilenced: "Aquest usuari està sent silenciat"
@ -1183,8 +1183,8 @@ currentAnnouncements: "Informes actuals"
pastAnnouncements: "Informes passats" pastAnnouncements: "Informes passats"
youHaveUnreadAnnouncements: "Tens informes per llegir." youHaveUnreadAnnouncements: "Tens informes per llegir."
useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey." useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey."
replies: "Respon" replies: "Respondre"
renotes: "Impulsar " renotes: "Impulsa"
loadReplies: "Mostrar les respostes" loadReplies: "Mostrar les respostes"
loadConversation: "Mostrar la conversació " loadConversation: "Mostrar la conversació "
pinnedList: "Llista fixada" pinnedList: "Llista fixada"
@ -1299,7 +1299,6 @@ yourNameContainsProhibitedWordsDescription: "Si de veritat vols fer servir aques
thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autor requereix l'inici de sessió per poder veure" thisContentsAreMarkedAsSigninRequiredByAuthor: "L'autor requereix l'inici de sessió per poder veure"
lockdown: "Bloquejat" lockdown: "Bloquejat"
pleaseSelectAccount: "Seleccionar un compte" pleaseSelectAccount: "Seleccionar un compte"
availableRoles: "Roles disponibles "
_accountSettings: _accountSettings:
requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut" requireSigninToViewContents: "És obligatori l'inici de sessió per poder veure el contingut"
requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació." requireSigninToViewContentsDescription1: "Es requereix l'inici de sessió per poder veure totes les notes i el contingut que has creat. Amb això esperem evitar que els rastrejadors recopilin informació."

View file

@ -331,7 +331,6 @@ selectFile: "Select a file"
selectFiles: "Select files" selectFiles: "Select files"
selectFolder: "Select a folder" selectFolder: "Select a folder"
selectFolders: "Select folders" selectFolders: "Select folders"
fileNotSelected: "No file selected"
renameFile: "Rename file" renameFile: "Rename file"
folderName: "Folder name" folderName: "Folder name"
createFolder: "Create a folder" createFolder: "Create a folder"
@ -1299,7 +1298,6 @@ yourNameContainsProhibitedWordsDescription: "If you wish to use this name, pleas
thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require login to view" thisContentsAreMarkedAsSigninRequiredByAuthor: "Set by the author to require login to view"
lockdown: "Lockdown" lockdown: "Lockdown"
pleaseSelectAccount: "Select an account" pleaseSelectAccount: "Select an account"
availableRoles: "Available roles"
_accountSettings: _accountSettings:
requireSigninToViewContents: "Require sign-in to view contents" requireSigninToViewContents: "Require sign-in to view contents"
requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information." requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information."
@ -2732,9 +2730,3 @@ _embedCodeGen:
generateCode: "Generate embed code" generateCode: "Generate embed code"
codeGenerated: "The code has been generated" codeGenerated: "The code has been generated"
codeGeneratedDescription: "Paste the generated code into your website to embed the content." codeGeneratedDescription: "Paste the generated code into your website to embed the content."
_selfXssPrevention:
warning: "WARNING"
title: "\"Paste something on this screen\" is all a scam."
description1: "If you paste something here, a malicious user could hijack your account or steal your personal information."
description2: "If you do not understand exactly what you are trying to paste, %cstop working right now and close this window."
description3: "For more information, please refer to this. {link}"

View file

@ -42,7 +42,7 @@ favorite: "즐겨찾기"
favorites: "즐겨찾기" favorites: "즐겨찾기"
unfavorite: "즐겨찾기에서 제거" unfavorite: "즐겨찾기에서 제거"
favorited: "즐겨찾기에 등록했습니다." favorited: "즐겨찾기에 등록했습니다."
alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다." alreadyFavorited: "이미 즐겨찾기에 등록습니다."
cantFavorite: "즐겨찾기에 등록하지 못했습니다." cantFavorite: "즐겨찾기에 등록하지 못했습니다."
pin: "프로필에 고정" pin: "프로필에 고정"
unpin: "프로필에서 고정 해제" unpin: "프로필에서 고정 해제"
@ -947,9 +947,6 @@ oneHour: "1시간"
oneDay: "1일" oneDay: "1일"
oneWeek: "일주일" oneWeek: "일주일"
oneMonth: "1개월" oneMonth: "1개월"
threeMonths: "3개월"
oneYear: "1년"
threeDays: "3일"
reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다." reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다."
failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다" failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다"
rateLimitExceeded: "요청 제한 횟수를 초과하였습니다" rateLimitExceeded: "요청 제한 횟수를 초과하였습니다"
@ -1289,29 +1286,13 @@ signinWithPasskey: "패스키로 로그인"
unknownWebAuthnKey: "등록되지 않은 패스키입니다." unknownWebAuthnKey: "등록되지 않은 패스키입니다."
passkeyVerificationFailed: "패스키 검증을 실패했습니다." passkeyVerificationFailed: "패스키 검증을 실패했습니다."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다." passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
messageToFollower: "팔로워에 보낼 메시지" messageToFollower: "팔로워에 보낼 메시지"
target: "대상" target: "대상"
testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>" testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)" prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)"
prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다." prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다."
yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다." yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다."
yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요." yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요."
thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해야 볼 수 있도록 설정되어 있습니다."
lockdown: "잠금"
pleaseSelectAccount: "계정을 선택해주세요."
availableRoles: "사용 가능한 역할"
_accountSettings:
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
requireSigninToViewContentsDescription2: "URL 미리보기(OGP), 웹페이지에 삽입, 노트 인용을 지원하지 않는 서버에서 볼 수 없게 됩니다."
requireSigninToViewContentsDescription3: "원격 서버에 연합된 콘텐츠에는 이러한 제한이 적용되지 않을 수 있습니다."
makeNotesFollowersOnlyBefore: "과거 노트는 팔로워만 볼 수 있도록 설정하기"
makeNotesFollowersOnlyBeforeDescription: "이 기능이 활성화되어 있는 동안, 설정된 날짜 및 시간보다 과거 또는 설정된 시간이 지난 노트는 팔로워만 볼 수 있게 됩니다.비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
makeNotesHiddenBefore: "과거 노트 비공개로 전환하기"
makeNotesHiddenBeforeDescription: "이 기능이 활성화되어 있는 동안 설정한 날짜 및 시간보다 과거 또는 설정한 시간이 지난 노트는 본인만 볼 수 있게(비공개로 전환) 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
mayNotEffectForFederatedNotes: "원격 서버에 연합된 노트에는 효과가 없을 수도 있습니다."
notesHavePassedSpecifiedPeriod: "지정한 시간이 경과된 노트"
notesOlderThanSpecifiedDateAndTime: "지정된 날짜 및 시간 이전의 노트"
_abuseUserReport: _abuseUserReport:
forward: "전달" forward: "전달"
forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다." forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다."
@ -2176,11 +2157,8 @@ _auth:
permissionAsk: "이 앱은 다음의 권한을 요청합니다" permissionAsk: "이 앱은 다음의 권한을 요청합니다"
pleaseGoBack: "앱으로 돌아가서 시도해 주세요" pleaseGoBack: "앱으로 돌아가서 시도해 주세요"
callback: "앱으로 돌아갑니다" callback: "앱으로 돌아갑니다"
accepted: "접근 권한이 부여되었습니다."
denied: "접근이 거부되었습니다" denied: "접근이 거부되었습니다"
scopeUser: "다음 사용자로 활동하고 있습니다."
pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오." pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오."
byClickingYouWillBeRedirectedToThisUrl: "접근을 허용하면 자동으로 다음 URL로 이동합니다."
_antennaSources: _antennaSources:
all: "모든 노트" all: "모든 노트"
homeTimeline: "팔로우중인 유저의 노트" homeTimeline: "팔로우중인 유저의 노트"
@ -2732,9 +2710,3 @@ _embedCodeGen:
generateCode: "임베디드 코드를 만들기" generateCode: "임베디드 코드를 만들기"
codeGenerated: "코드를 만들었습니다." codeGenerated: "코드를 만들었습니다."
codeGeneratedDescription: "만들어진 코드를 웹 사이트에 붙여서 사용하세요." codeGeneratedDescription: "만들어진 코드를 웹 사이트에 붙여서 사용하세요."
_selfXssPrevention:
warning: "경고"
title: "“이 화면에 뭔가를 붙여넣어라\"는 것은 모두 사기입니다."
description1: "여기에 무언가를 붙여넣으면 악의적인 사용자에게 계정을 탈취당하거나 개인정보를 도용당할 수 있습니다."
description2: "붙여 넣으려는 항목이 무엇인지 정확히 이해하지 못하는 경우, %c지금 바로 작업을 중단하고 이 창을 닫으십시오."
description3: "자세한 내용은 여기를 확인해 주세요. {link}"

View file

@ -8,9 +8,6 @@ search: "Tìm kiếm"
notifications: "Thông báo" notifications: "Thông báo"
username: "Tên người dùng" username: "Tên người dùng"
password: "Mật khẩu" password: "Mật khẩu"
initialPasswordForSetup: "Mật khẩu ban đầu để thiết lập"
initialPasswordIsIncorrect: "Mật khẩu ban đầu đã nhập sai"
initialPasswordForSetupDescription: "Nếu bạn tự cài đặt Misskey, hãy sử dụng mật khẩu ban đầu của bạn đã nhập trong tệp cấu hình.\nNếu bạn đang sử dụng dịch vụ nào đó giống như dịch vụ lưu trữ của Misskey, hãy sử dụng mật khẩu ban đầu được cung cấp.\nNếu bạn chưa đặt mật khẩu ban đầu, vui lòng để trống và tiếp tục."
forgotPassword: "Quên mật khẩu" forgotPassword: "Quên mật khẩu"
fetchingAsApObject: "Đang nạp dữ liệu từ Fediverse..." fetchingAsApObject: "Đang nạp dữ liệu từ Fediverse..."
ok: "Đồng ý" ok: "Đồng ý"

View file

@ -1299,7 +1299,6 @@ yourNameContainsProhibitedWordsDescription: "用户名内含有违禁词。若
thisContentsAreMarkedAsSigninRequiredByAuthor: "根据发帖者的设定,需要登录才能显示" thisContentsAreMarkedAsSigninRequiredByAuthor: "根据发帖者的设定,需要登录才能显示"
lockdown: "锁定" lockdown: "锁定"
pleaseSelectAccount: "请选择帐户" pleaseSelectAccount: "请选择帐户"
availableRoles: "可用角色"
_accountSettings: _accountSettings:
requireSigninToViewContents: "需要登录才能显示内容" requireSigninToViewContents: "需要登录才能显示内容"
requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。" requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"

View file

@ -8,8 +8,8 @@ search: "搜尋"
notifications: "通知" notifications: "通知"
username: "使用者名稱" username: "使用者名稱"
password: "密碼" password: "密碼"
initialPasswordForSetup: "啟動初始設定的密碼" initialPasswordForSetup: "初始設定的密碼"
initialPasswordIsIncorrect: "啟動初始設定的密碼錯誤。" initialPasswordIsIncorrect: "初始設定的密碼錯誤。"
initialPasswordForSetupDescription: "如果您自己安裝了 Misskey請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼請將其留空並繼續。" initialPasswordForSetupDescription: "如果您自己安裝了 Misskey請使用您在設定檔中輸入的密碼。\n如果您使用 Misskey 的託管服務之類的服務,請使用提供的密碼。\n如果您尚未設定密碼請將其留空並繼續。"
forgotPassword: "忘記密碼" forgotPassword: "忘記密碼"
fetchingAsApObject: "從聯邦宇宙取得中..." fetchingAsApObject: "從聯邦宇宙取得中..."
@ -1299,7 +1299,6 @@ yourNameContainsProhibitedWordsDescription: "名稱中包含禁止使用的字
thisContentsAreMarkedAsSigninRequiredByAuthor: "作者將其設定為需要登入才能顯示。" thisContentsAreMarkedAsSigninRequiredByAuthor: "作者將其設定為需要登入才能顯示。"
lockdown: "鎖定" lockdown: "鎖定"
pleaseSelectAccount: "請選擇帳戶" pleaseSelectAccount: "請選擇帳戶"
availableRoles: "可用角色"
_accountSettings: _accountSettings:
requireSigninToViewContents: "須登入以顯示內容" requireSigninToViewContents: "須登入以顯示內容"
requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。" requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"

View file

@ -1,6 +1,6 @@
{ {
"name": "misskey", "name": "misskey",
"version": "2024.10.2-alpha.1", "version": "2024.11.0-alpha.0",
"codename": "nasubi", "codename": "nasubi",
"repository": { "repository": {
"type": "git", "type": "git",

View file

@ -406,8 +406,10 @@ export class MfmService {
mention: (node) => { mention: (node) => {
const a = doc.createElement('a'); const a = doc.createElement('a');
const { username, host, acct } = node.props; const { username, host, acct } = node.props;
const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username === username && remoteUser.host === host); const remoteUserInfo = mentionedRemoteUsers.find(remoteUser => remoteUser.username.toLowerCase() === username.toLowerCase() && remoteUser.host?.toLowerCase() === host?.toLowerCase());
a.setAttribute('href', remoteUserInfo ? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri) : `${this.config.url}/${acct}`); a.setAttribute('href', remoteUserInfo
? (remoteUserInfo.url ? remoteUserInfo.url : remoteUserInfo.uri)
: `${this.config.url}/${acct.endsWith(`@${this.config.url}`) ? acct.substring(0, acct.length - this.config.url.length - 1) : acct}`);
a.className = 'u-url mention'; a.className = 'u-url mention';
a.textContent = acct; a.textContent = acct;
return a; return a;

View file

@ -7,7 +7,7 @@ import { randomUUID } from 'node:crypto';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import type { IActivity } from '@/core/activitypub/type.js'; import type { IActivity } from '@/core/activitypub/type.js';
import type { MiDriveFile } from '@/models/DriveFile.js'; import type { MiDriveFile } from '@/models/DriveFile.js';
import type { MiWebhook, webhookEventTypes } from '@/models/Webhook.js'; import type { MiWebhook, WebhookEventTypes, webhookEventTypes } from '@/models/Webhook.js';
import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js'; import type { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import type { Config } from '@/config.js'; import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
@ -35,6 +35,7 @@ import type {
} from './QueueModule.js'; } from './QueueModule.js';
import type httpSignature from '@peertube/http-signature'; import type httpSignature from '@peertube/http-signature';
import type * as Bull from 'bullmq'; import type * as Bull from 'bullmq';
import { type UserWebhookPayload } from './UserWebhookService.js';
@Injectable() @Injectable()
export class QueueService { export class QueueService {
@ -468,10 +469,10 @@ export class QueueService {
* @see UserWebhookDeliverProcessorService * @see UserWebhookDeliverProcessorService
*/ */
@bindThis @bindThis
public userWebhookDeliver( public userWebhookDeliver<T extends WebhookEventTypes>(
webhook: MiWebhook, webhook: MiWebhook,
type: typeof webhookEventTypes[number], type: T,
content: unknown, content: UserWebhookPayload<T>,
opts?: { attempts?: number }, opts?: { attempts?: number },
) { ) {
const data: UserWebhookDeliverJobData = { const data: UserWebhookDeliverJobData = {

View file

@ -6,11 +6,23 @@
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis'; import * as Redis from 'ioredis';
import { type WebhooksRepository } from '@/models/_.js'; import { type WebhooksRepository } from '@/models/_.js';
import { MiWebhook } from '@/models/Webhook.js'; import { MiWebhook, WebhookEventTypes } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js'; import { GlobalEvents } from '@/core/GlobalEventService.js';
import type { OnApplicationShutdown } from '@nestjs/common'; import type { OnApplicationShutdown } from '@nestjs/common';
import type { Packed } from '@/misc/json-schema.js';
export type UserWebhookPayload<T extends WebhookEventTypes> =
T extends 'note' | 'reply' | 'renote' |'mention' ? {
note: Packed<'Note'>,
} :
T extends 'follow' | 'unfollow' ? {
user: Packed<'UserDetailedNotMe'>,
} :
T extends 'followed' ? {
user: Packed<'UserLite'>,
} : never;
@Injectable() @Injectable()
export class UserWebhookService implements OnApplicationShutdown { export class UserWebhookService implements OnApplicationShutdown {

View file

@ -10,7 +10,7 @@ import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWeb
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { Packed } from '@/misc/json-schema.js'; import { Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js'; import { type WebhookEventTypes } from '@/models/Webhook.js';
import { UserWebhookService } from '@/core/UserWebhookService.js'; import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { QueueService } from '@/core/QueueService.js'; import { QueueService } from '@/core/QueueService.js';
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js'; import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
@ -306,10 +306,10 @@ export class WebhookTestService {
* - on * - on
*/ */
@bindThis @bindThis
public async testUserWebhook( public async testUserWebhook<T extends WebhookEventTypes>(
params: { params: {
webhookId: MiWebhook['id'], webhookId: MiWebhook['id'],
type: WebhookEventTypes, type: T,
override?: Partial<Omit<MiWebhook, 'id'>>, override?: Partial<Omit<MiWebhook, 'id'>>,
}, },
sender: MiUser | null, sender: MiUser | null,
@ -321,7 +321,7 @@ export class WebhookTestService {
} }
const webhook = webhooks[0]; const webhook = webhooks[0];
const send = (contents: unknown) => { const send = <U extends WebhookEventTypes>(type: U, contents: UserWebhookPayload<U>) => {
const merged = { const merged = {
...webhook, ...webhook,
...params.override, ...params.override,
@ -329,7 +329,7 @@ export class WebhookTestService {
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図. // テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図.
// また、Jobの試行回数も1回だけ. // また、Jobの試行回数も1回だけ.
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 }); this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 });
}; };
const dummyNote1 = generateDummyNote({ const dummyNote1 = generateDummyNote({
@ -361,33 +361,40 @@ export class WebhookTestService {
switch (params.type) { switch (params.type) {
case 'note': { case 'note': {
send(toPackedNote(dummyNote1)); send('note', { note: toPackedNote(dummyNote1) });
break; break;
} }
case 'reply': { case 'reply': {
send(toPackedNote(dummyReply1)); send('reply', { note: toPackedNote(dummyReply1) });
break; break;
} }
case 'renote': { case 'renote': {
send(toPackedNote(dummyRenote1)); send('renote', { note: toPackedNote(dummyRenote1) });
break; break;
} }
case 'mention': { case 'mention': {
send(toPackedNote(dummyMention1)); send('mention', { note: toPackedNote(dummyMention1) });
break; break;
} }
case 'follow': { case 'follow': {
send(toPackedUserDetailedNotMe(dummyUser1)); send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
break; break;
} }
case 'followed': { case 'followed': {
send(toPackedUserLite(dummyUser2)); send('followed', { user: toPackedUserLite(dummyUser2) });
break; break;
} }
case 'unfollow': { case 'unfollow': {
send(toPackedUserDetailedNotMe(dummyUser3)); send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
break; break;
} }
// まだ実装されていない (#9485)
case 'reaction': return;
default: {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _exhaustiveAssertion: never = params.type;
return;
}
} }
} }

View file

@ -4,5 +4,5 @@
*/ */
export function sqlLikeEscape(s: string) { export function sqlLikeEscape(s: string) {
return s.replace(/([%_])/g, '\\$1'); return s.replace(/([\\%_])/g, '\\$1');
} }

View file

@ -29,6 +29,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { IActivity } from '@/core/activitypub/type.js'; import { IActivity } from '@/core/activitypub/type.js';
import { isQuote, isRenote } from '@/misc/is-renote.js'; import { isQuote, isRenote } from '@/misc/is-renote.js';
import * as Acct from '@/misc/acct.js';
import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify'; import type { FastifyInstance, FastifyRequest, FastifyReply, FastifyPluginOptions, FastifyBodyParser } from 'fastify';
import type { FindOptionsWhere } from 'typeorm'; import type { FindOptionsWhere } from 'typeorm';
@ -486,6 +487,16 @@ export class ActivityPubServerService {
return; return;
} }
// リモートだったらリダイレクト
if (user.host != null) {
if (user.uri == null || this.utilityService.isSelfHost(user.host)) {
reply.code(500);
return;
}
reply.redirect(user.uri, 301);
return;
}
reply.header('Cache-Control', 'public, max-age=180'); reply.header('Cache-Control', 'public, max-age=180');
this.setResponseType(request, reply); this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser))); return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
@ -654,19 +665,20 @@ export class ActivityPubServerService {
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
id: userId, id: userId,
host: IsNull(),
isSuspended: false, isSuspended: false,
}); });
return await this.userInfo(request, reply, user); return await this.userInfo(request, reply, user);
}); });
fastify.get<{ Params: { user: string; } }>('/@:user', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => { fastify.get<{ Params: { acct: string; } }>('/@:acct', { constraints: { apOrHtml: 'ap' } }, async (request, reply) => {
vary(reply.raw, 'Accept'); vary(reply.raw, 'Accept');
const acct = Acct.parse(request.params.acct);
const user = await this.usersRepository.findOneBy({ const user = await this.usersRepository.findOneBy({
usernameLower: request.params.user.toLowerCase(), usernameLower: acct.username,
host: IsNull(), host: acct.host ?? IsNull(),
isSuspended: false, isSuspended: false,
}); });

View file

@ -465,6 +465,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const newName = updates.name === undefined ? user.name : updates.name; const newName = updates.name === undefined ? user.name : updates.name;
const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description; const newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields; const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
if (newName != null) { if (newName != null) {
let hasProhibitedWords = false; let hasProhibitedWords = false;
@ -494,6 +495,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
]); ]);
} }
if (newFollowedMessage != null) {
const tokens = mfm.parse(newFollowedMessage);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
}
updates.emojis = emojis; updates.emojis = emojis;
updates.tags = tags; updates.tags = tags;

View file

@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.activeUsersChart.read(me); this.activeUsersChart.read(me);
await this.noteEntityService.packMany(timeline, me); return await this.noteEntityService.packMany(timeline, me);
} }
const timeline = await this.fanoutTimelineEndpointService.timeline({ const timeline = await this.fanoutTimelineEndpointService.timeline({

View file

@ -42,13 +42,26 @@ import { MetaEntityService } from '@/core/entities/MetaEntityService.js';
import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js'; import { GalleryPostEntityService } from '@/core/entities/GalleryPostEntityService.js';
import { ClipEntityService } from '@/core/entities/ClipEntityService.js'; import { ClipEntityService } from '@/core/entities/ClipEntityService.js';
import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js'; import { ChannelEntityService } from '@/core/entities/ChannelEntityService.js';
import type { ChannelsRepository, ClipsRepository, FlashsRepository, GalleryPostsRepository, MiMeta, NotesRepository, PagesRepository, ReversiGamesRepository, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import type {
AnnouncementsRepository,
ChannelsRepository,
ClipsRepository,
FlashsRepository,
GalleryPostsRepository,
MiMeta,
NotesRepository,
PagesRepository,
ReversiGamesRepository,
UserProfilesRepository,
UsersRepository,
} from '@/models/_.js';
import type Logger from '@/logger.js'; import type Logger from '@/logger.js';
import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js'; import { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { bindThis } from '@/decorators.js'; import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js'; import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { RoleService } from '@/core/RoleService.js'; import { RoleService } from '@/core/RoleService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js'; import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { FeedService } from './FeedService.js'; import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js'; import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js'; import { ClientLoggerService } from './ClientLoggerService.js';
@ -103,6 +116,9 @@ export class ClientServerService {
@Inject(DI.reversiGamesRepository) @Inject(DI.reversiGamesRepository)
private reversiGamesRepository: ReversiGamesRepository, private reversiGamesRepository: ReversiGamesRepository,
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
private flashEntityService: FlashEntityService, private flashEntityService: FlashEntityService,
private userEntityService: UserEntityService, private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService, private noteEntityService: NoteEntityService,
@ -112,6 +128,7 @@ export class ClientServerService {
private clipEntityService: ClipEntityService, private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService, private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService, private reversiGameEntityService: ReversiGameEntityService,
private announcementEntityService: AnnouncementEntityService,
private urlPreviewService: UrlPreviewService, private urlPreviewService: UrlPreviewService,
private feedService: FeedService, private feedService: FeedService,
private roleService: RoleService, private roleService: RoleService,
@ -776,6 +793,24 @@ export class ClientServerService {
return await renderBase(reply); return await renderBase(reply);
} }
}); });
// 個別お知らせページ
fastify.get<{ Params: { announcementId: string; } }>('/announcements/:announcementId', async (request, reply) => {
const announcement = await this.announcementsRepository.findOneBy({
id: request.params.announcementId,
});
if (announcement) {
const _announcement = await this.announcementEntityService.pack(announcement);
reply.header('Cache-Control', 'public, max-age=3600');
return await reply.view('announcement', {
announcement: _announcement,
...await this.generateCommonPugData(this.meta),
});
} else {
return await renderBase(reply);
}
});
//#endregion //#endregion
//#region noindex pages //#region noindex pages

View file

@ -0,0 +1,21 @@
extends ./base
block vars
- const title = announcement.title;
- const description = announcement.text.length > 100 ? announcement.text.slice(0, 100) + '…' : announcement.text;
- const url = `${config.url}/announcements/${announcement.id}`;
block title
= `${title} | ${instanceName}`
block desc
meta(name='description' content=description)
block og
meta(property='og:type' content='article')
meta(property='og:title' content= title)
meta(property='og:description' content= description)
meta(property='og:url' content= url)
if announcement.imageUrl
meta(property='og:image' content=announcement.imageUrl)
meta(property='twitter:card' content='summary_large_image')

View file

@ -2,6 +2,7 @@ block vars
block loadClientEntry block loadClientEntry
- const entry = config.frontendEntry; - const entry = config.frontendEntry;
- const baseUrl = config.url;
doctype html doctype html
@ -32,7 +33,7 @@ html
link(rel='icon' href= icon || '/favicon.ico') link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png') link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
link(rel='manifest' href='/manifest.json') link(rel='manifest' href='/manifest.json')
link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${url}/opensearch.xml`) link(rel='search' type='application/opensearchdescription+xml' title=(title || "Misskey") href=`${baseUrl}/opensearch.xml`)
link(rel='prefetch' href=serverErrorImageUrl) link(rel='prefetch' href=serverErrorImageUrl)
link(rel='prefetch' href=infoImageUrl) link(rel='prefetch' href=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl) link(rel='prefetch' href=notFoundImageUrl)

View file

@ -230,6 +230,7 @@ describe('Webリソース', () => {
path: path('xxxxxxxxxx'), path: path('xxxxxxxxxx'),
type: HTML, type: HTML,
})); }));
test.todo('HTMLとしてGETできる。(リモートユーザーでもリダイレクトせず)');
}); });
describe.each([ describe.each([
@ -249,6 +250,7 @@ describe('Webリソース', () => {
path: path('xxxxxxxxxx'), path: path('xxxxxxxxxx'),
accept, accept,
})); }));
test.todo('はオリジナルにリダイレクトされる。(リモートユーザー)');
}); });
}); });

View file

@ -7,7 +7,7 @@
import { Test, TestingModule } from '@nestjs/testing'; import { Test, TestingModule } from '@nestjs/testing';
import { beforeAll, describe, jest } from '@jest/globals'; import { beforeAll, describe, jest } from '@jest/globals';
import { WebhookTestService } from '@/core/WebhookTestService.js'; import { WebhookTestService } from '@/core/WebhookTestService.js';
import { UserWebhookService } from '@/core/UserWebhookService.js'; import { UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js'; import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { GlobalModule } from '@/GlobalModule.js'; import { GlobalModule } from '@/GlobalModule.js';
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js'; import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@ -122,7 +122,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('note'); expect(calls[1]).toBe('note');
expect((calls[2] as any).id).toBe('dummy-note-1'); expect((calls[2] as UserWebhookPayload<'note'>).note.id).toBe('dummy-note-1');
}); });
test('reply', async () => { test('reply', async () => {
@ -131,7 +131,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('reply'); expect(calls[1]).toBe('reply');
expect((calls[2] as any).id).toBe('dummy-reply-1'); expect((calls[2] as UserWebhookPayload<'reply'>).note.id).toBe('dummy-reply-1');
}); });
test('renote', async () => { test('renote', async () => {
@ -140,7 +140,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('renote'); expect(calls[1]).toBe('renote');
expect((calls[2] as any).id).toBe('dummy-renote-1'); expect((calls[2] as UserWebhookPayload<'renote'>).note.id).toBe('dummy-renote-1');
}); });
test('mention', async () => { test('mention', async () => {
@ -149,7 +149,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('mention'); expect(calls[1]).toBe('mention');
expect((calls[2] as any).id).toBe('dummy-mention-1'); expect((calls[2] as UserWebhookPayload<'mention'>).note.id).toBe('dummy-mention-1');
}); });
test('follow', async () => { test('follow', async () => {
@ -158,7 +158,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('follow'); expect(calls[1]).toBe('follow');
expect((calls[2] as any).id).toBe('dummy-user-1'); expect((calls[2] as UserWebhookPayload<'follow'>).user.id).toBe('dummy-user-1');
}); });
test('followed', async () => { test('followed', async () => {
@ -167,7 +167,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('followed'); expect(calls[1]).toBe('followed');
expect((calls[2] as any).id).toBe('dummy-user-2'); expect((calls[2] as UserWebhookPayload<'followed'>).user.id).toBe('dummy-user-2');
}); });
test('unfollow', async () => { test('unfollow', async () => {
@ -176,7 +176,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0]; const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook'); expect((calls[0] as any).id).toBe('dummy-webhook');
expect(calls[1]).toBe('unfollow'); expect(calls[1]).toBe('unfollow');
expect((calls[2] as any).id).toBe('dummy-user-3'); expect((calls[2] as UserWebhookPayload<'unfollow'>).user.id).toBe('dummy-user-3');
}); });
describe('NoSuchWebhookError', () => { describe('NoSuchWebhookError', () => {

View file

@ -22,23 +22,66 @@ type Account = Misskey.entities.MeDetailed & { token: string };
const accountData = miLocalStorage.getItem('account'); const accountData = miLocalStorage.getItem('account');
// TODO: 外部からはreadonlyに // TODO: 外部からはreadonlyに
/**
* Reactive state for the current account. "I" as in "I am logged in".
* Initialized from local storage if available, otherwise null.
*
* @type {Account | null}
*/
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null; export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
/**
* Whether the current account is a moderator.
*
* @type {boolean}
*/
export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true); export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
/**
* Whether the current account is an administrator.
*
* @type {boolean}
*/
export const iAmAdmin = $i != null && $i.isAdmin; export const iAmAdmin = $i != null && $i.isAdmin;
/**
* Whether it is necessary to sign in; checks if the current
* account is null and throws an error if so.
*
* @throws {Error} If the current account is null
* @returns {Account} The current account
*/
export function signinRequired() { export function signinRequired() {
if ($i == null) throw new Error('signin required'); if ($i == null) throw new Error('signin required');
return $i; return $i;
} }
/**
* Extracts the current number of notes from the current account.
*
* Note: This appears to only be used for the "notes1" achievement.
*
* Also, separating it like this might cause counts to get out-of-sync.
*/
export let notesCount = $i == null ? 0 : $i.notesCount; export let notesCount = $i == null ? 0 : $i.notesCount;
/**
* Increments the number of notes by one.
*
* Documentation TODO: What about $i.notesCount? Why not increment that?
*/
export function incNotesCount() { export function incNotesCount() {
notesCount++; notesCount++;
} }
export async function signout() { export async function signout() {
if (!$i) return;
// If we're not signed in, there's nothing to do.
if (!$i) {
// Error log:
console.error('signout() called when not signed in');
return;
}
waiting(); waiting();
miLocalStorage.removeItem('account'); miLocalStorage.removeItem('account');

View file

@ -15,7 +15,7 @@ import { updateI18n, i18n } from '@/i18n.js';
import { $i, refreshAccount, login } 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, updateDeviceKind } from '@/scripts/device-kind.js';
import { reloadChannel } from '@/scripts/unison-reload.js'; import { reloadChannel } from '@/scripts/unison-reload.js';
import { getUrlWithoutLoginId } from '@/scripts/login-id.js'; import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
import { getAccountFromId } from '@/scripts/get-account-from-id.js'; import { getAccountFromId } from '@/scripts/get-account-from-id.js';
@ -185,6 +185,10 @@ export async function common(createVue: () => App<Element>) {
} }
}); });
watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
updateDeviceKind(kind);
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffectForModal, v => { watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none'); document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true }); }, { immediate: true });

View file

@ -160,7 +160,7 @@ async function deleteAntenna() {
function addUser() { function addUser() {
os.selectUser({ includeSelf: true }).then(user => { os.selectUser({ includeSelf: true }).then(user => {
users.value = users.value.trim(); users.value = users.value.trim();
users.value += '\n@' + Misskey.acct.toString(user as any); users.value += '\n@' + Misskey.acct.toString(user);
users.value = users.value.trim(); users.value = users.value.trim();
}); });
} }

View file

@ -47,11 +47,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { miLocalStorage } from '@/local-storage.js'; import { miLocalStorage } from '@/local-storage.js';
const props = defineProps<{ const props = defineProps<{
channel: Record<string, any>; channel: Misskey.entities.Channel;
}>(); }>();
const getLastReadedAt = (): number | null => { const getLastReadedAt = (): number | null => {

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:withOkButton="true" :withOkButton="true"
@close="cancel()" @close="cancel()"
@ok="ok()" @ok="ok()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ i18n.ts.cropImage }}</template> <template #header>{{ i18n.ts.cropImage }}</template>
<template #default="{ width, height }"> <template #default="{ width, height }">

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<MkModalWindow ref="dialogEl" @close="cancel()" @closed="$emit('closed')"> <MkModalWindow ref="dialogEl" @close="cancel()" @closed="emit('closed')">
<template #header>:{{ emoji.name }}:</template> <template #header>:{{ emoji.name }}:</template>
<template #default> <template #default>
<MkSpacer> <MkSpacer>

View file

@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
</MkSelect> </MkSelect>
<div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons"> <div v-if="(showOkButton || showCancelButton) && !actions" :class="$style.buttons">
<MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton> <MkButton v-if="showOkButton" data-cy-modal-dialog-ok inline primary rounded :autofocus="!input && !select" :disabled="okButtonDisabledReason != null" @click="ok">{{ okText ?? ((showCancelButton || input || select) ? i18n.ts.ok : i18n.ts.gotIt) }}</MkButton>
<MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton> <MkButton v-if="showCancelButton || input || select" data-cy-modal-dialog-cancel inline rounded @click="cancel">{{ cancelText ?? i18n.ts.cancel }}</MkButton>
</div> </div>
<div v-if="actions" :class="$style.buttons"> <div v-if="actions" :class="$style.buttons">
@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
text: string; text: string;
primary?: boolean, primary?: boolean,
danger?: boolean, danger?: boolean,
callback: (...args: any[]) => void; callback: (...args: unknown[]) => void;
}[]; }[];
showOkButton?: boolean; showOkButton?: boolean;
showCancelButton?: boolean; showCancelButton?: boolean;

View file

@ -157,7 +157,7 @@ const ilFilesObserver = new IntersectionObserver(
(entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(), (entries) => entries.some((entry) => entry.isIntersecting) && !fetching.value && moreFiles.value && fetchMoreFiles(),
); );
const sortModeSelect = ref('+createdAt'); const sortModeSelect = ref<NonNullable<Misskey.entities.DriveFilesRequest['sort']>>('+createdAt');
watch(folder, () => emit('cd', folder.value)); watch(folder, () => emit('cd', folder.value));
watch(sortModeSelect, () => { watch(sortModeSelect, () => {
@ -198,7 +198,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
removeFolder(folderId); removeFolder(folderId);
} }
function onDragover(ev: DragEvent): any { function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return; if (!ev.dataTransfer) return;
// //
@ -243,7 +243,7 @@ function onDragleave() {
draghover.value = false; draghover.value = false;
} }
function onDrop(ev: DragEvent): any { function onDrop(ev: DragEvent) {
draghover.value = false; draghover.value = false;
if (!ev.dataTransfer) return; if (!ev.dataTransfer) return;
@ -332,7 +332,7 @@ function createFolder() {
title: i18n.ts.createFolder, title: i18n.ts.createFolder,
placeholder: i18n.ts.folderName, placeholder: i18n.ts.folderName,
}).then(({ canceled, result: name }) => { }).then(({ canceled, result: name }) => {
if (canceled) return; if (canceled || name == null) return;
misskeyApi('drive/folders/create', { misskeyApi('drive/folders/create', {
name: name, name: name,
parentId: folder.value ? folder.value.id : undefined, parentId: folder.value ? folder.value.id : undefined,

View file

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:scroll="false" :scroll="false"
:withOkButton="false" :withOkButton="false"
@close="cancel()" @close="cancel()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template> <template #header><i class="ti ti-code"></i> {{ i18n.ts._embedCodeGen.title }}</template>

View file

@ -90,7 +90,7 @@ function computeButtonTitle(ev: MouseEvent): void {
elm.title = getEmojiName(emoji); elm.title = getEmojiName(emoji);
} }
function nestedChosen(emoji: any, ev: MouseEvent) { function nestedChosen(emoji: string, ev: MouseEvent) {
emit('chosen', emoji, ev); emit('chosen', emoji, ev);
} }
</script> </script>

View file

@ -409,7 +409,7 @@ function computeButtonTitle(ev: MouseEvent): void {
elm.title = getEmojiName(emoji); elm.title = getEmojiName(emoji);
} }
function chosen(emoji: any, ev?: MouseEvent) { function chosen(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef, ev?: MouseEvent) {
const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined; const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
if (el) { if (el) {
const rect = el.getBoundingClientRect(); const rect = el.getBoundingClientRect();
@ -426,7 +426,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
// 使 // 使
if (!pinned.value?.includes(key)) { if (!pinned.value?.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis; let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== key); recents = recents.filter((emoji) => emoji !== key);
recents.unshift(key); recents.unshift(key);
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32)); defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
} }

View file

@ -73,7 +73,7 @@ export type Extension = {
author: string; author: string;
description?: string; description?: string;
permissions?: string[]; permissions?: string[];
config?: Record<string, any>; config?: Record<string, unknown>;
}; };
} | { } | {
type: 'theme'; type: 'theme';

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@click="cancel()" @click="cancel()"
@ok="ok()" @ok="ok()"
@close="cancel()" @close="cancel()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header> <template #header>
{{ title }} {{ title }}

View file

@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs } from 'vue'; import { onMounted, onUnmounted, nextTick, ref, shallowRef, watch, computed, toRefs, InputHTMLAttributes } from 'vue';
import { debounce } from 'throttle-debounce'; import { debounce } from 'throttle-debounce';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { useInterval } from '@@/js/use-interval.js'; import { useInterval } from '@@/js/use-interval.js';
@ -53,7 +53,7 @@ import { Autocomplete, SuggestionType } from '@/scripts/autocomplete.js';
const props = defineProps<{ const props = defineProps<{
modelValue: string | number | null; modelValue: string | number | null;
type?: 'text' | 'number' | 'password' | 'email' | 'url' | 'date' | 'time' | 'search' | 'datetime-local'; type?: InputHTMLAttributes['type'];
required?: boolean; required?: boolean;
readonly?: boolean; readonly?: boolean;
disabled?: boolean; disabled?: boolean;
@ -64,8 +64,8 @@ const props = defineProps<{
mfmAutocomplete?: boolean | SuggestionType[], mfmAutocomplete?: boolean | SuggestionType[],
autocapitalize?: string; autocapitalize?: string;
spellcheck?: boolean; spellcheck?: boolean;
inputmode?: 'none' | 'text' | 'search' | 'email' | 'url' | 'numeric' | 'tel' | 'decimal'; inputmode?: InputHTMLAttributes['inputmode'];
step?: any; step?: InputHTMLAttributes['step'];
datalist?: string[]; datalist?: string[];
min?: number; min?: number;
max?: number; max?: number;

View file

@ -118,7 +118,7 @@ import { hms } from '@/filters/hms.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { isFullscreenNotSupported } from '@/scripts/device-kind.js'; import { exitFullscreen, requestFullscreen } from '@/scripts/fullscreen.js';
import hasAudio from '@/scripts/media-has-audio.js'; import hasAudio from '@/scripts/media-has-audio.js';
import MkMediaRange from '@/components/MkMediaRange.vue'; import MkMediaRange from '@/components/MkMediaRange.vue';
import { $i, iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
@ -334,26 +334,21 @@ function togglePlayPause() {
} }
function toggleFullscreen() { function toggleFullscreen() {
if (isFullscreenNotSupported && videoEl.value) { if (playerEl.value == null || videoEl.value == null) return;
if (isFullscreen.value) { if (isFullscreen.value) {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment exitFullscreen({
//@ts-ignore videoEl: videoEl.value,
videoEl.value.webkitExitFullscreen(); });
isFullscreen.value = false; isFullscreen.value = false;
} else { } else {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment requestFullscreen({
//@ts-ignore videoEl: videoEl.value,
videoEl.value.webkitEnterFullscreen(); playerEl: playerEl.value,
isFullscreen.value = true; options: {
} navigationUI: 'hide',
} else if (playerEl.value) { },
if (isFullscreen.value) { });
document.exitFullscreen(); isFullscreen.value = true;
isFullscreen.value = false;
} else {
playerEl.value.requestFullscreen({ navigationUI: 'hide' });
isFullscreen.value = true;
}
} }
} }
@ -454,8 +449,10 @@ watch(loop, (to) => {
}); });
watch(hide, (to) => { watch(hide, (to) => {
if (to && isFullscreen.value) { if (videoEl.value && to && isFullscreen.value) {
document.exitFullscreen(); exitFullscreen({
videoEl: videoEl.value,
});
isFullscreen.value = false; isFullscreen.value = false;
} }
}); });

View file

@ -227,7 +227,7 @@ const emit = defineEmits<{
}>(); }>();
const inTimeline = inject<boolean>('inTimeline', false); const inTimeline = inject<boolean>('inTimeline', false);
const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(false)); const tl_withSensitive = inject<Ref<boolean>>('tl_withSensitive', ref(true));
const inChannel = inject('inChannel', null); const inChannel = inject('inChannel', null);
const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null); const currentClip = inject<Ref<Misskey.entities.Clip> | null>('currentClip', null);
@ -292,15 +292,18 @@ function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute'; function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly: false): boolean | 'sensitiveMute';
*/ */
function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' { function checkMute(noteToCheck: Misskey.entities.Note, mutedWords: Array<string | string[]> | undefined | null, checkOnly = false): boolean | 'sensitiveMute' {
if (mutedWords == null) return false; if (mutedWords != null) {
if (checkWordMute(noteToCheck, $i, mutedWords)) return true;
if (checkWordMute(noteToCheck, $i, mutedWords)) return true; if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true;
if (noteToCheck.reply && checkWordMute(noteToCheck.reply, $i, mutedWords)) return true; if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true;
if (noteToCheck.renote && checkWordMute(noteToCheck.renote, $i, mutedWords)) return true; }
if (checkOnly) return false; if (checkOnly) return false;
if (inTimeline && !tl_withSensitive.value && noteToCheck.files?.some((v) => v.isSensitive)) return 'sensitiveMute'; if (inTimeline && tl_withSensitive.value === false && noteToCheck.files?.some((v) => v.isSensitive)) {
return 'sensitiveMute';
}
return false; return false;
} }

View file

@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const typesMap: TypesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as any); const typesMap = notificationTypes.reduce((p, t) => ({ ...p, [t]: ref<boolean>(!props.excludeTypes.includes(t)) }), {} as TypesMap);
function ok() { function ok() {
emit('done', { emit('done', {

View file

@ -39,7 +39,7 @@ import number from '@/filters/number.js';
import XValue from '@/components/MkObjectView.value.vue'; import XValue from '@/components/MkObjectView.value.vue';
const props = defineProps<{ const props = defineProps<{
value: any; value: unknown;
}>(); }>();
const collapsed = reactive({}); const collapsed = reactive({});
@ -50,19 +50,19 @@ if (isObject(props.value)) {
} }
} }
function isObject(v): boolean { function isObject(v: unknown): v is Record<PropertyKey, unknown> {
return typeof v === 'object' && !Array.isArray(v) && v !== null; return typeof v === 'object' && !Array.isArray(v) && v !== null;
} }
function isArray(v): boolean { function isArray(v: unknown): v is unknown[] {
return Array.isArray(v); return Array.isArray(v);
} }
function isEmpty(v): boolean { function isEmpty(v: unknown): v is Record<PropertyKey, never> | never[] {
return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0); return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
} }
function collapsable(v): boolean { function collapsable(v: unknown): boolean {
return (isObject(v) || isArray(v)) && !isEmpty(v); return (isObject(v) || isArray(v)) && !isEmpty(v);
} }
</script> </script>

View file

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:buttonsLeft="buttonsLeft" :buttonsLeft="buttonsLeft"
:buttonsRight="buttonsRight" :buttonsRight="buttonsRight"
:contextmenu="contextmenu" :contextmenu="contextmenu"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header> <template #header>
<template v-if="pageMetadata"> <template v-if="pageMetadata">
@ -30,17 +30,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue'; import { computed, onMounted, onUnmounted, provide, ref, shallowRef } from 'vue';
import { url } from '@@/js/config.js';
import { getScrollContainer } from '@@/js/scroll.js';
import RouterView from '@/components/global/RouterView.vue'; import RouterView from '@/components/global/RouterView.vue';
import MkWindow from '@/components/MkWindow.vue'; import MkWindow from '@/components/MkWindow.vue';
import { popout as _popout } from '@/scripts/popout.js'; import { popout as _popout } from '@/scripts/popout.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js'; import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { url } from '@@/js/config.js';
import { useScrollPositionManager } from '@/nirax.js'; import { useScrollPositionManager } from '@/nirax.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js'; import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { openingWindowsCount } from '@/os.js'; import { openingWindowsCount } from '@/os.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { getScrollContainer } from '@@/js/scroll.js';
import { useRouterFactory } from '@/router/supplier.js'; import { useRouterFactory } from '@/router/supplier.js';
import { mainRouter } from '@/router/main.js'; import { mainRouter } from '@/router/main.js';
@ -48,7 +48,7 @@ const props = defineProps<{
initialPath: string; initialPath: string;
}>(); }>();
defineEmits<{ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
}>(); }>();
@ -58,7 +58,7 @@ const windowRouter = routerFactory(props.initialPath);
const contents = shallowRef<HTMLElement | null>(null); const contents = shallowRef<HTMLElement | null>(null);
const pageMetadata = ref<null | PageMetadata>(null); const pageMetadata = ref<null | PageMetadata>(null);
const windowEl = shallowRef<InstanceType<typeof MkWindow>>(); const windowEl = shallowRef<InstanceType<typeof MkWindow>>();
const history = ref<{ path: string; key: any; }[]>([{ const history = ref<{ path: string; key: string; }[]>([{
path: windowRouter.getCurrentPath(), path: windowRouter.getCurrentPath(),
key: windowRouter.getCurrentKey(), key: windowRouter.getCurrentKey(),
}]); }]);

View file

@ -19,7 +19,7 @@ defineProps<{
items: MenuItem[]; items: MenuItem[];
align?: 'center' | string; align?: 'center' | string;
width?: number; width?: number;
src?: any; src?: HTMLElement | null;
returnFocusTo?: HTMLElement | null; returnFocusTo?: HTMLElement | null;
}>(); }>();

View file

@ -129,25 +129,13 @@ import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js'; import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js'; import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js'; import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
import type { PostFormProps } from '@/types/post-form.js';
const $i = signinRequired(); const $i = signinRequired();
const modal = inject('modal'); const modal = inject('modal');
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<PostFormProps & {
reply?: Misskey.entities.Note;
renote?: Misskey.entities.Note;
channel?: Misskey.entities.Channel; // TODO
mention?: Misskey.entities.User;
specified?: Misskey.entities.UserDetailed;
initialText?: string;
initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.UserDetailed[];
initialNote?: Misskey.entities.Note;
instant?: boolean;
fixed?: boolean; fixed?: boolean;
autofocus?: boolean; autofocus?: boolean;
freezeAfterPosted?: boolean; freezeAfterPosted?: boolean;
@ -955,8 +943,8 @@ function showActions(ev: MouseEvent) {
action.handler({ action.handler({
text: text.value, text: text.value,
cw: cw.value, cw: cw.value,
}, (key, value: any) => { }, (key, value) => {
if (typeof key !== 'string') return; if (typeof key !== 'string' || typeof value !== 'string') return;
if (key === 'text') { text.value = value; } if (key === 'text') { text.value = value; }
if (key === 'cw') { useCw.value = value !== null; cw.value = value; } if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
}); });
@ -1120,7 +1108,7 @@ defineExpose({
&:focus-visible { &:focus-visible {
outline: none; outline: none;
.submitInner { > .submitInner {
outline: 2px solid var(--MI_THEME-fgOnAccent); outline: 2px solid var(--MI_THEME-fgOnAccent);
outline-offset: -4px; outline-offset: -4px;
} }
@ -1135,13 +1123,13 @@ defineExpose({
} }
&:not(:disabled):hover { &:not(:disabled):hover {
> .inner { > .submitInner {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
} }
} }
&:not(:disabled):active { &:not(:disabled):active {
> .inner { > .submitInner {
background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5))); background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
} }
} }

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<div v-show="props.modelValue.length != 0" :class="$style.root"> <div v-show="props.modelValue.length != 0" :class="$style.root">
<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)"> <Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
<template #item="{element}"> <template #item="{ element }">
<div <div
:class="$style.file" :class="$style.file"
role="button" role="button"
@ -38,14 +38,14 @@ import type { MenuItem } from '@/types/menu.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default)); const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const props = defineProps<{ const props = defineProps<{
modelValue: any[]; modelValue: Misskey.entities.DriveFile[];
detachMediaFn?: (id: string) => void; detachMediaFn?: (id: string) => void;
}>(); }>();
const mock = inject<boolean>('mock', false); const mock = inject<boolean>('mock', false);
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update:modelValue', value: any[]): void; (ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void;
(ev: 'detach', id: string): void; (ev: 'detach', id: string): void;
(ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void; (ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): void;
(ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void; (ev: 'changeName', file: Misskey.entities.DriveFile, newName: string): void;
@ -113,7 +113,7 @@ async function rename(file) {
}); });
} }
async function describe(file) { async function describe(file: Misskey.entities.DriveFile) {
if (mock) return; if (mock) return;
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), {

View file

@ -11,23 +11,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { shallowRef } from 'vue'; import { shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkModal from '@/components/MkModal.vue'; import MkModal from '@/components/MkModal.vue';
import MkPostForm from '@/components/MkPostForm.vue'; import MkPostForm from '@/components/MkPostForm.vue';
import type { PostFormProps } from '@/types/post-form.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<PostFormProps & {
reply?: Misskey.entities.Note;
renote?: Misskey.entities.Note;
channel?: any; // TODO
mention?: Misskey.entities.User;
specified?: Misskey.entities.UserDetailed;
initialText?: string;
initialCw?: string;
initialVisibility?: (typeof Misskey.noteVisibilities)[number];
initialFiles?: Misskey.entities.DriveFile[];
initialLocalOnly?: boolean;
initialVisibleUsers?: Misskey.entities.UserDetailed[];
initialNote?: Misskey.entities.Note;
instant?: boolean; instant?: boolean;
fixed?: boolean; fixed?: boolean;
autofocus?: boolean; autofocus?: boolean;

View file

@ -24,17 +24,17 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup generic="T extends unknown">
import { computed } from 'vue'; import { computed } from 'vue';
const props = defineProps<{ const props = defineProps<{
modelValue: any; modelValue: T;
value: any; value: T;
disabled?: boolean; disabled?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void; (ev: 'update:modelValue', value: T): void;
}>(); }>();
const checked = computed(() => props.modelValue === props.value); const checked = computed(() => props.modelValue === props.value);

View file

@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import { } from 'vue';
import * as Misskey from 'misskey-js';
import { getEmojiName } from '@@/js/emojilist.js'; import { getEmojiName } from '@@/js/emojilist.js';
import MkTooltip from './MkTooltip.vue'; import MkTooltip from './MkTooltip.vue';
import MkReactionIcon from '@/components/MkReactionIcon.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue';
@ -30,7 +31,7 @@ import MkReactionIcon from '@/components/MkReactionIcon.vue';
defineProps<{ defineProps<{
showing: boolean; showing: boolean;
reaction: string; reaction: string;
users: any[]; // TODO users: Misskey.entities.UserLite[];
count: number; count: number;
targetElement: HTMLElement; targetElement: HTMLElement;
}>(); }>();

View file

@ -277,7 +277,7 @@ async function onSubmit(): Promise<void> {
return null; return null;
}); });
if (res) { if (res && res.ok) {
if (res.status === 204 || instance.emailRequiredForSignup) { if (res.status === 204 || instance.emailRequiredForSignup) {
os.alert({ os.alert({
type: 'success', type: 'success',
@ -295,6 +295,8 @@ async function onSubmit(): Promise<void> {
await login(resJson.token); await login(resJson.token);
} }
} }
} else {
onSignupApiError();
} }
submitting.value = false; submitting.value = false;

View file

@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:width="500" :width="500"
:height="600" :height="600"
@close="onClose" @close="onClose"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ i18n.ts.signup }}</template> <template #header>{{ i18n.ts.signup }}</template>

View file

@ -28,11 +28,38 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts">
import { } from 'vue'; export type SuperMenuDef = {
title?: string;
items: ({
type: 'a';
href: string;
target?: string;
icon?: string;
text: string;
danger?: boolean;
active?: boolean;
} | {
type: 'button';
icon?: string;
text: string;
danger?: boolean;
active?: boolean;
action: (ev: MouseEvent) => void;
} | {
type: 'link';
to: string;
icon?: string;
text: string;
danger?: boolean;
active?: boolean;
})[];
};
</script>
<script lang="ts" setup>
defineProps<{ defineProps<{
def: any[]; def: SuperMenuDef[];
grid?: boolean; grid?: boolean;
}>(); }>();
</script> </script>

View file

@ -43,6 +43,7 @@ const props = withDefaults(defineProps<{
}>(), { }>(), {
withRenotes: true, withRenotes: true,
withReplies: false, withReplies: false,
withSensitive: true,
onlyFiles: false, onlyFiles: false,
}); });

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:okButtonDisabled="false" :okButtonDisabled="false"
:canClose="false" :canClose="false"
@close="dialog?.close()" @close="dialog?.close()"
@closed="$emit('closed')" @closed="emit('closed')"
@ok="ok()" @ok="ok()"
> >
<template #header>{{ title || i18n.ts.generateAccessToken }}</template> <template #header>{{ title || i18n.ts.generateAccessToken }}</template>

View file

@ -180,7 +180,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
sensitive.value = info.sensitive ?? false; sensitive.value = info.sensitive ?? false;
}); });
function adjustTweetHeight(message: any) { function adjustTweetHeight(message: MessageEvent) {
if (message.origin !== 'https://platform.twitter.com') return; if (message.origin !== 'https://platform.twitter.com') return;
const embed = message.data?.['twttr.embed']; const embed = message.data?.['twttr.embed'];
if (embed?.method !== 'twttr.private.resize') return; if (embed?.method !== 'twttr.private.resize') return;
@ -193,14 +193,16 @@ function openPlayer(): void {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), { const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
url: requestUrl.href, url: requestUrl.href,
}, { }, {
// TODO closed: () => {
dispose();
},
}); });
} }
(window as any).addEventListener('message', adjustTweetHeight); window.addEventListener('message', adjustTweetHeight);
onUnmounted(() => { onUnmounted(() => {
(window as any).removeEventListener('message', adjustTweetHeight); window.removeEventListener('message', adjustTweetHeight);
}); });
</script> </script>

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog" ref="dialog"
:width="400" :width="400"
@close="dialog?.close()" @close="dialog?.close()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template v-if="announcement" #header>:{{ announcement.title }}:</template> <template v-if="announcement" #header>:{{ announcement.title }}:</template>
<template v-else #header>New announcement</template> <template v-else #header>New announcement</template>
@ -62,9 +62,16 @@ import MkTextarea from '@/components/MkTextarea.vue';
import MkSwitch from '@/components/MkSwitch.vue'; import MkSwitch from '@/components/MkSwitch.vue';
import MkRadios from '@/components/MkRadios.vue'; import MkRadios from '@/components/MkRadios.vue';
type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; }
const props = defineProps<{ const props = defineProps<{
user: Misskey.entities.User, user: Misskey.entities.User,
announcement?: Misskey.entities.Announcement, announcement?: Required<AdminAnnouncementType>,
}>();
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: AdminAnnouncementType; created?: AdminAnnouncementType; }): void,
(ev: 'closed'): void
}>(); }>();
const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null); const dialog = ref<InstanceType<typeof MkModalWindow> | null>(null);
@ -74,11 +81,6 @@ const icon = ref(props.announcement ? props.announcement.icon : 'info');
const display = ref(props.announcement ? props.announcement.display : 'dialog'); const display = ref(props.announcement ? props.announcement.display : 'dialog');
const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false); const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
(ev: 'closed'): void
}>();
async function done() { async function done() {
const params = { const params = {
title: title.value, title: title.value,
@ -88,7 +90,7 @@ async function done() {
display: display.value, display: display.value,
needConfirmationToRead: needConfirmationToRead.value, needConfirmationToRead: needConfirmationToRead.value,
userId: props.user.id, userId: props.user.id,
}; } satisfies Misskey.entities.AdminAnnouncementsCreateRequest;
if (props.announcement) { if (props.announcement) {
await os.apiWithDialog('admin/announcements/update', { await os.apiWithDialog('admin/announcements/update', {

View file

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
@click="cancel()" @click="cancel()"
@close="cancel()" @close="cancel()"
@ok="ok()" @ok="ok()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template #header>{{ i18n.ts.selectUser }}</template> <template #header>{{ i18n.ts.selectUser }}</template>
<div> <div>

View file

@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { } from 'vue'; import * as Misskey from 'misskey-js';
import MkTooltip from './MkTooltip.vue'; import MkTooltip from './MkTooltip.vue';
defineProps<{ defineProps<{
showing: boolean; showing: boolean;
users: any[]; // TODO users: Misskey.entities.UserLite[];
count: number; count: number;
targetElement: HTMLElement; targetElement: HTMLElement;
}>(); }>();

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option> <option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
</MkSelect> </MkSelect>
<MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton> <MkButton inline primary data-cy-widget-add @click="addWidget"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
<MkButton inline @click="$emit('exit')">{{ i18n.ts.close }}</MkButton> <MkButton inline @click="emit('exit')">{{ i18n.ts.close }}</MkButton>
</header> </header>
<Sortable <Sortable
:modelValue="props.widgets" :modelValue="props.widgets"

View file

@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''" :enterFromClass="defaultStore.state.animation ? $style.transition_window_enterFrom : ''"
:leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''" :leaveToClass="defaultStore.state.animation ? $style.transition_window_leaveTo : ''"
appear appear
@afterLeave="$emit('closed')" @afterLeave="emit('closed')"
> >
<div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]"> <div v-if="showing" ref="rootEl" :class="[$style.root, { [$style.maximized]: maximized }]">
<div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown"> <div :class="$style.body" class="_shadow" @mousedown="onBodyMousedown" @keydown="onKeydown">
@ -60,6 +60,13 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
type WindowButton = {
title: string;
icon: string;
onClick: () => void;
highlighted?: boolean;
};
const minHeight = 50; const minHeight = 50;
const minWidth = 250; const minWidth = 250;
@ -87,8 +94,8 @@ const props = withDefaults(defineProps<{
mini?: boolean; mini?: boolean;
front?: boolean; front?: boolean;
contextmenu?: MenuItem[] | null; contextmenu?: MenuItem[] | null;
buttonsLeft?: any[]; buttonsLeft?: WindowButton[];
buttonsRight?: any[]; buttonsRight?: WindowButton[];
}>(), { }>(), {
initialWidth: 400, initialWidth: 400,
initialHeight: null, initialHeight: null,

View file

@ -18,19 +18,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup generic="T extends unknown">
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{
p: () => Promise<any>; p: () => Promise<T>;
}>(); }>();
const pending = ref(true); const pending = ref(true);
const resolved = ref(false); const resolved = ref(false);
const rejected = ref(false); const rejected = ref(false);
const result = ref<any>(null); const result = ref<T | null>(null);
const process = () => { const process = () => {
if (props.p == null) { if (props.p == null) {

View file

@ -467,8 +467,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
} }
default: { default: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // @ts-expect-error 存在しないASTタイプ
console.error('unrecognized ast type:', (token as any).type); console.error('unrecognized ast type:', token.type);
return []; return [];
} }

View file

@ -3,6 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
/**
* A typesafe enum of keys for localStorage.
*/
export type Keys = export type Keys =
'v' | 'v' |
'lastVersion' | 'lastVersion' |
@ -44,16 +47,45 @@ export type Keys =
// セッション毎に廃棄されるLocalStorage代替セーフモードなどで使用できそう // セッション毎に廃棄されるLocalStorage代替セーフモードなどで使用できそう
//const safeSessionStorage = new Map<Keys, string>(); //const safeSessionStorage = new Map<Keys, string>();
/**
* A utility object for interacting with the browser's localStorage.
*
* It's mostly a small wrapper around window.localStorage, but it validates
* keys with a typesafe enum, and provides a few convenience methods for JSON.
*/
export const miLocalStorage = { export const miLocalStorage = {
/**
* Retrieves an item from localStorage.
* @param {Keys} key - The key of the item to retrieve.
* @returns {string | null} The value of the item, or null if the item does not exist.
*/
getItem: (key: Keys): string | null => { getItem: (key: Keys): string | null => {
return window.localStorage.getItem(key); return window.localStorage.getItem(key);
}, },
/**
* Stores an item in localStorage.
* @param {Keys} key - The key of the item to store.
* @param {string} value - The value of the item to store.
*/
setItem: (key: Keys, value: string): void => { setItem: (key: Keys, value: string): void => {
window.localStorage.setItem(key, value); window.localStorage.setItem(key, value);
}, },
/**
* Removes an item from localStorage.
* @param {Keys} key - The key of the item to remove.
*/
removeItem: (key: Keys): void => { removeItem: (key: Keys): void => {
window.localStorage.removeItem(key); window.localStorage.removeItem(key);
}, },
/**
* Retrieves an item from localStorage and parses it as JSON.
* @param {Keys} key - The key of the item to retrieve.
* @returns {any | undefined} The parsed value of the item, or undefined if the item does not exist.
*/
getItemAsJson: (key: Keys): any | undefined => { getItemAsJson: (key: Keys): any | undefined => {
const item = miLocalStorage.getItem(key); const item = miLocalStorage.getItem(key);
if (item === null) { if (item === null) {
@ -61,6 +93,12 @@ export const miLocalStorage = {
} }
return JSON.parse(item); return JSON.parse(item);
}, },
/**
* Stores an item in localStorage as a JSON string.
* @param {Keys} key - The key of the item to store.
* @param {any} value - The value of the item to store.
*/
setItemAsJson: (key: Keys, value: any): void => { setItemAsJson: (key: Keys, value: any): void => {
miLocalStorage.setItem(key, JSON.stringify(value)); miLocalStorage.setItem(key, JSON.stringify(value));
}, },

View file

@ -36,6 +36,8 @@ interface RouteDefWithRedirect extends RouteDefBase {
export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect; export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
export type RouterFlag = 'forcePage';
type ParsedPath = (string | { type ParsedPath = (string | {
name: string; name: string;
startsWith?: string; startsWith?: string;
@ -107,7 +109,7 @@ export interface IRouter extends EventEmitter<RouterEvent> {
current: Resolved; current: Resolved;
currentRef: ShallowRef<Resolved>; currentRef: ShallowRef<Resolved>;
currentRoute: ShallowRef<RouteDef>; currentRoute: ShallowRef<RouteDef>;
navHook: ((path: string, flag?: any) => boolean) | null; navHook: ((path: string, flag?: RouterFlag) => boolean) | null;
/** /**
* eventListenerの定義後に必ず呼び出すこと * eventListenerの定義後に必ず呼び出すこと
@ -116,11 +118,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
resolve(path: string): Resolved | null; resolve(path: string): Resolved | null;
getCurrentPath(): any; getCurrentPath(): string;
getCurrentKey(): string; getCurrentKey(): string;
push(path: string, flag?: any): void; push(path: string, flag?: RouterFlag): void;
replace(path: string, key?: string | null): void; replace(path: string, key?: string | null): void;
@ -197,7 +199,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
private currentKey = Date.now().toString(); private currentKey = Date.now().toString();
private redirectCount = 0; private redirectCount = 0;
public navHook: ((path: string, flag?: any) => boolean) | null = null; public navHook: ((path: string, flag?: RouterFlag) => boolean) | null = null;
constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) { constructor(routes: Router['routes'], currentPath: Router['currentPath'], isLoggedIn: boolean, notFoundPageComponent: Component) {
super(); super();
@ -404,7 +406,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
return this.currentKey; return this.currentKey;
} }
public push(path: string, flag?: any) { public push(path: string, flag?: RouterFlag) {
const beforePath = this.currentPath; const beforePath = this.currentPath;
if (path === beforePath) { if (path === beforePath) {
this.emit('same'); this.emit('same');

View file

@ -28,12 +28,13 @@ import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js'; import { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
import { focusParent } from '@/scripts/focus.js'; import { focusParent } from '@/scripts/focus.js';
import type { PostFormProps } from '@/types/post-form.js';
export const openingWindowsCount = ref(0); export const openingWindowsCount = ref(0);
export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>( export const apiWithDialog = (<E extends keyof Misskey.Endpoints, P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req']>(
endpoint: E, endpoint: E,
data: P = {} as any, data: P,
token?: string | null | undefined, token?: string | null | undefined,
customErrors?: Record<string, { title?: string; text: string; }>, customErrors?: Record<string, { title?: string; text: string; }>,
) => { ) => {
@ -94,7 +95,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
export function promiseDialog<T extends Promise<any>>( export function promiseDialog<T extends Promise<any>>(
promise: T, promise: T,
onSuccess?: ((res: any) => void) | null, onSuccess?: ((res: Awaited<T>) => void) | null,
onFailure?: ((err: Misskey.api.APIError) => void) | null, onFailure?: ((err: Misskey.api.APIError) => void) | null,
text?: string, text?: string,
): T { ): T {
@ -135,20 +136,40 @@ export function promiseDialog<T extends Promise<any>>(
return promise; return promise;
} }
/**
* Counter for generating unique popup IDs.
* @type {number}
*/
let popupIdCount = 0; let popupIdCount = 0;
export const popups = ref([]) as Ref<{
/**
* A reactive list of the currently opened popups. This is used in a Vue component
* in a v-for loop to render the popups.
*/
export const popups = ref<{
id: number; id: number;
component: Component; component: Component;
props: Record<string, any>; props: Record<string, any>;
events: Record<string, any>; events: Record<string, any>;
}[]>; }[]>([]);
/**
* An object containing z-index values for different priority levels.
*/
const zIndexes = { const zIndexes = {
veryLow: 500000, veryLow: 500000,
low: 1000000, low: 1000000,
middle: 2000000, middle: 2000000,
high: 3000000, high: 3000000,
}; };
/**
* Claims a z-index value for a given priority level.
* Increments the z-index value for the specified priority by 100 and returns the new value.
*
* @param {keyof typeof zIndexes} [priority='low'] - The priority level for which to claim a z-index.
* @returns {number} The new z-index value for the specified priority.
*/
export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number { export function claimZIndex(priority: keyof typeof zIndexes = 'low'): number {
zIndexes[priority] += 100; zIndexes[priority] += 100;
return zIndexes[priority]; return zIndexes[priority];
@ -176,6 +197,15 @@ type EmitsExtractor<T> = {
[K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K]; [K in keyof T as K extends `onVnode${string}` ? never : K extends `on${infer E}` ? Uncapitalize<E> : K extends string ? never : K]: T[K];
}; };
/**
* Opens a popup with the specified component, props, and events.
*
* @template T - The type of the component.
* @param {T} component - The Vue component to display in the popup.
* @param {ComponentProps<T>} props - The props to pass to the component.
* @param {ComponentEmit<T>} [events={}] - The events to bind to the component.
* @returns {{ dispose: () => void }} An object containing a dispose function to close the popup.
*/
export function popup<T extends Component>( export function popup<T extends Component>(
component: T, component: T,
props: ComponentProps<T>, props: ComponentProps<T>,
@ -183,13 +213,18 @@ export function popup<T extends Component>(
): { dispose: () => void } { ): { dispose: () => void } {
markRaw(component); markRaw(component);
// Generate a unique ID for this popup.
const id = ++popupIdCount; const id = ++popupIdCount;
// On disposal, remove this popup from the list of open popups.
const dispose = () => { const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ // このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ
window.setTimeout(() => { window.setTimeout(() => {
popups.value = popups.value.filter(p => p.id !== id); popups.value = popups.value.filter(p => p.id !== id);
}, 0); }, 0);
}; };
// Bundle the component, props, and events into a state object.
const state = { const state = {
component, component,
props, props,
@ -197,13 +232,19 @@ export function popup<T extends Component>(
id, id,
}; };
// Add the popup to the list of open popups.
popups.value.push(state); popups.value.push(state);
// Return a function that can be called to close the popup.
return { return {
dispose, dispose,
}; };
} }
/**
* Open the page with the given path in a pop-up window.
* @param path The path of the page to open.
*/
export function pageWindow(path: string) { export function pageWindow(path: string) {
const { dispose } = popup(MkPageWindow, { const { dispose } = popup(MkPageWindow, {
initialPath: path, initialPath: path,
@ -212,6 +253,11 @@ export function pageWindow(path: string) {
}); });
} }
/**
* Displays a toast message to the user.
*
* @param {string} message - The message to display in the toast.
*/
export function toast(message: string) { export function toast(message: string) {
const { dispose } = popup(MkToast, { const { dispose } = popup(MkToast, {
message, message,
@ -220,6 +266,15 @@ export function toast(message: string) {
}); });
} }
/**
* Displays an alert dialog to the user.
*
* @param {Object} props - The properties for the alert dialog.
* @param {'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'} [props.type] - The type of the alert.
* @param {string} [props.title] - The title of the alert dialog.
* @param {string} [props.text] - The text content of the alert dialog.
* @returns {Promise<void>} A promise that resolves when the alert dialog is closed.
*/
export function alert(props: { export function alert(props: {
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question'; type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
title?: string; title?: string;
@ -458,7 +513,7 @@ type SelectItem<C> = {
}; };
// default が指定されていたら result は null になり得ないことを保証する overload function // default が指定されていたら result は null になり得ないことを保証する overload function
export function select<C = any>(props: { export function select<C = unknown>(props: {
title?: string; title?: string;
text?: string; text?: string;
default: string; default: string;
@ -471,7 +526,7 @@ export function select<C = any>(props: {
} | { } | {
canceled: false; result: C; canceled: false; result: C;
}>; }>;
export function select<C = any>(props: { export function select<C = unknown>(props: {
title?: string; title?: string;
text?: string; text?: string;
default?: string | null; default?: string | null;
@ -484,7 +539,7 @@ export function select<C = any>(props: {
} | { } | {
canceled: false; result: C | null; canceled: false; result: C | null;
}>; }>;
export function select<C = any>(props: { export function select<C = unknown>(props: {
title?: string; title?: string;
text?: string; text?: string;
default?: string | null; default?: string | null;
@ -687,13 +742,13 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
})); }));
} }
export function post(props: Record<string, any> = {}): Promise<void> { export function post(props: PostFormProps = {}): Promise<void> {
pleaseLogin({ pleaseLogin({
openOnRemote: (props.initialText || props.initialNote ? { openOnRemote: (props.initialText || props.initialNote ? {
type: 'share', type: 'share',
params: { params: {
text: props.initialText ?? props.initialNote.text, text: props.initialText ?? props.initialNote?.text ?? '',
visibility: props.initialVisibility ?? props.initialNote?.visibility, visibility: props.initialVisibility ?? props.initialNote?.visibility ?? 'public',
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0', localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
}, },
} : undefined), } : undefined),

View file

@ -272,6 +272,9 @@ const patronsWithIcon = [{
}, { }, {
name: 'Yatoigawa', name: 'Yatoigawa',
icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg', icon: 'https://assets.misskey-hub.net/patrons/505e3568885a4a488431a8f22b4553d0.jpg',
}, {
name: '秋瀬カヲル',
icon: 'https://assets.misskey-hub.net/patrons/0f22aeb866484f4fa51db6721e3f9847.jpg',
}]; }];
const patrons = [ const patrons = [
@ -380,6 +383,7 @@ const patrons = [
'ケモナーのケシン', 'ケモナーのケシン',
'こまつぶり', 'こまつぶり',
'まゆつな空高', 'まゆつな空高',
'asata',
]; ];
const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure')); const thereIsTreasure = ref($i && !claimedAchievements.includes('foundTreasure'));

View file

@ -99,19 +99,19 @@ async function addUser() {
const { canceled: canceled1, result: username } = await os.inputText({ const { canceled: canceled1, result: username } = await os.inputText({
title: i18n.ts.username, title: i18n.ts.username,
}); });
if (canceled1) return; if (canceled1 || username == null) return;
const { canceled: canceled2, result: password } = await os.inputText({ const { canceled: canceled2, result: password } = await os.inputText({
title: i18n.ts.password, title: i18n.ts.password,
type: 'password', type: 'password',
}); });
if (canceled2) return; if (canceled2 || password == null) return;
os.apiWithDialog('admin/accounts/create', { os.apiWithDialog('admin/accounts/create', {
username: username, username: username,
password: password, password: password,
}).then(res => { }).then(res => {
paginationComponent.value.reload(); paginationComponent.value?.reload();
}); });
} }

View file

@ -103,7 +103,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []); const headerTabs = computed(() => []);
definePageMetadata(() => ({ definePageMetadata(() => ({
title: announcement.value ? `${i18n.ts.announcements}: ${announcement.value.title}` : i18n.ts.announcements, title: announcement.value ? announcement.value.title : i18n.ts.announcements,
icon: 'ti ti-speakerphone', icon: 'ti ti-speakerphone',
})); }));
</script> </script>

View file

@ -62,7 +62,7 @@ function accepted() {
state.value = 'accepted'; state.value = 'accepted';
if (session.value && session.value.app.callbackUrl) { if (session.value && session.value.app.callbackUrl) {
const url = new URL(session.value.app.callbackUrl); const url = new URL(session.value.app.callbackUrl);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(url.protocol)) throw new Error('invalid url'); if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(url.protocol)) throw new Error('invalid url');
location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`; location.href = `${session.value.app.callbackUrl}?token=${session.value.token}`;
} }
} }

View file

@ -116,7 +116,7 @@ const selectAll = () => {
if (selectedEmojis.value.length > 0) { if (selectedEmojis.value.length > 0) {
selectedEmojis.value = []; selectedEmojis.value = [];
} else { } else {
selectedEmojis.value = Array.from(emojisPaginationComponent.value.items.values(), item => item.id); selectedEmojis.value = Array.from(emojisPaginationComponent.value?.items.values(), item => item.id);
} }
}; };
@ -133,7 +133,7 @@ const add = async (ev: MouseEvent) => {
}, { }, {
done: result => { done: result => {
if (result.created) { if (result.created) {
emojisPaginationComponent.value.prepend(result.created); emojisPaginationComponent.value?.prepend(result.created);
} }
}, },
closed: () => dispose(), closed: () => dispose(),
@ -146,12 +146,12 @@ const edit = (emoji) => {
}, { }, {
done: result => { done: result => {
if (result.updated) { if (result.updated) {
emojisPaginationComponent.value.updateItem(result.updated.id, (oldEmoji: any) => ({ emojisPaginationComponent.value?.updateItem(result.updated.id, (oldEmoji) => ({
...oldEmoji, ...oldEmoji,
...result.updated, ...result.updated,
})); }));
} else if (result.deleted) { } else if (result.deleted) {
emojisPaginationComponent.value.removeItem(emoji.id); emojisPaginationComponent.value?.removeItem(emoji.id);
} }
}, },
closed: () => dispose(), closed: () => dispose(),
@ -226,7 +226,7 @@ const setCategoryBulk = async () => {
ids: selectedEmojis.value, ids: selectedEmojis.value,
category: result, category: result,
}); });
emojisPaginationComponent.value.reload(); emojisPaginationComponent.value?.reload();
}; };
const setLicenseBulk = async () => { const setLicenseBulk = async () => {
@ -238,43 +238,43 @@ const setLicenseBulk = async () => {
ids: selectedEmojis.value, ids: selectedEmojis.value,
license: result, license: result,
}); });
emojisPaginationComponent.value.reload(); emojisPaginationComponent.value?.reload();
}; };
const addTagBulk = async () => { const addTagBulk = async () => {
const { canceled, result } = await os.inputText({ const { canceled, result } = await os.inputText({
title: 'Tag', title: 'Tag',
}); });
if (canceled) return; if (canceled || result == null) return;
await os.apiWithDialog('admin/emoji/add-aliases-bulk', { await os.apiWithDialog('admin/emoji/add-aliases-bulk', {
ids: selectedEmojis.value, ids: selectedEmojis.value,
aliases: result.split(' '), aliases: result.split(' '),
}); });
emojisPaginationComponent.value.reload(); emojisPaginationComponent.value?.reload();
}; };
const removeTagBulk = async () => { const removeTagBulk = async () => {
const { canceled, result } = await os.inputText({ const { canceled, result } = await os.inputText({
title: 'Tag', title: 'Tag',
}); });
if (canceled) return; if (canceled || result == null) return;
await os.apiWithDialog('admin/emoji/remove-aliases-bulk', { await os.apiWithDialog('admin/emoji/remove-aliases-bulk', {
ids: selectedEmojis.value, ids: selectedEmojis.value,
aliases: result.split(' '), aliases: result.split(' '),
}); });
emojisPaginationComponent.value.reload(); emojisPaginationComponent.value?.reload();
}; };
const setTagBulk = async () => { const setTagBulk = async () => {
const { canceled, result } = await os.inputText({ const { canceled, result } = await os.inputText({
title: 'Tag', title: 'Tag',
}); });
if (canceled) return; if (canceled || result == null) return;
await os.apiWithDialog('admin/emoji/set-aliases-bulk', { await os.apiWithDialog('admin/emoji/set-aliases-bulk', {
ids: selectedEmojis.value, ids: selectedEmojis.value,
aliases: result.split(' '), aliases: result.split(' '),
}); });
emojisPaginationComponent.value.reload(); emojisPaginationComponent.value?.reload();
}; };
const delBulk = async () => { const delBulk = async () => {
@ -286,7 +286,7 @@ const delBulk = async () => {
await os.apiWithDialog('admin/emoji/delete-bulk', { await os.apiWithDialog('admin/emoji/delete-bulk', {
ids: selectedEmojis.value, ids: selectedEmojis.value,
}); });
emojisPaginationComponent.value.reload(); emojisPaginationComponent.value?.reload();
}; };
const headerActions = computed(() => [{ const headerActions = computed(() => [{

View file

@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
:initialWidth="400" :initialWidth="400"
:initialHeight="500" :initialHeight="500"
:canResize="true" :canResize="true"
@close="windowEl.close()" @close="windowEl?.close()"
@closed="$emit('closed')" @closed="emit('closed')"
> >
<template v-if="emoji" #header>:{{ emoji.name }}:</template> <template v-if="emoji" #header>:{{ emoji.name }}:</template>
<template v-else #header>New emoji</template> <template v-else #header>New emoji</template>
@ -95,14 +95,19 @@ import { selectFile } from '@/scripts/select-file.js';
import MkRolePreview from '@/components/MkRolePreview.vue'; import MkRolePreview from '@/components/MkRolePreview.vue';
const props = defineProps<{ const props = defineProps<{
emoji?: any, emoji?: Misskey.entities.EmojiDetailed,
}>();
const emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: Misskey.entities.AdminEmojiUpdateRequest; created?: Misskey.entities.AdminEmojiUpdateRequest }): void,
(ev: 'closed'): void
}>(); }>();
const windowEl = ref<InstanceType<typeof MkWindow> | null>(null); const windowEl = ref<InstanceType<typeof MkWindow> | null>(null);
const name = ref<string>(props.emoji ? props.emoji.name : ''); const name = ref<string>(props.emoji ? props.emoji.name : '');
const category = ref<string>(props.emoji ? props.emoji.category : ''); const category = ref<string>(props.emoji?.category ? props.emoji.category : '');
const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : ''); const aliases = ref<string>(props.emoji ? props.emoji.aliases.join(' ') : '');
const license = ref<string>(props.emoji ? (props.emoji.license ?? '') : ''); const license = ref<string>(props.emoji?.license ? props.emoji.license : '');
const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false); const isSensitive = ref(props.emoji ? props.emoji.isSensitive : false);
const localOnly = ref(props.emoji ? props.emoji.localOnly : false); const localOnly = ref(props.emoji ? props.emoji.localOnly : false);
const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []); const roleIdsThatCanBeUsedThisEmojiAsReaction = ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
@ -115,12 +120,7 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => {
const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null); const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? `/emoji/${props.emoji.name}.webp` : null);
const emit = defineEmits<{ async function changeImage(ev: Event) {
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
(ev: 'closed'): void
}>();
async function changeImage(ev) {
file.value = await selectFile(ev.currentTarget ?? ev.target, null); file.value = await selectFile(ev.currentTarget ?? ev.target, null);
const candidate = file.value.name.replace(/\.(.+)$/, ''); const candidate = file.value.name.replace(/\.(.+)$/, '');
if (candidate.match(/^[a-z0-9_]+$/)) { if (candidate.match(/^[a-z0-9_]+$/)) {
@ -140,7 +140,7 @@ async function addRole() {
rolesThatCanBeUsedThisEmojiAsReaction.value.push(role); rolesThatCanBeUsedThisEmojiAsReaction.value.push(role);
} }
async function removeRole(role, ev) { async function removeRole(role: Misskey.entities.RoleLite, ev: Event) {
rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id); rolesThatCanBeUsedThisEmojiAsReaction.value = rolesThatCanBeUsedThisEmojiAsReaction.value.filter(x => x.id !== role.id);
} }
@ -172,7 +172,7 @@ async function done() {
}, },
}); });
windowEl.value.close(); windowEl.value?.close();
} else { } else {
const created = await os.apiWithDialog('admin/emoji/add', params); const created = await os.apiWithDialog('admin/emoji/add', params);
@ -180,11 +180,12 @@ async function done() {
created: created, created: created,
}); });
windowEl.value.close(); windowEl.value?.close();
} }
} }
async function del() { async function del() {
if (!props.emoji) return;
const { canceled } = await os.confirm({ const { canceled } = await os.confirm({
type: 'warning', type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: name.value }), text: i18n.tsx.removeAreYouSure({ x: name.value }),
@ -197,7 +198,7 @@ async function del() {
emit('done', { emit('done', {
deleted: true, deleted: true,
}); });
windowEl.value.close(); windowEl.value?.close();
}); });
} }
</script> </script>

View file

@ -55,13 +55,13 @@ const pagination = {
function accept(user) { function accept(user) {
misskeyApi('following/requests/accept', { userId: user.id }).then(() => { misskeyApi('following/requests/accept', { userId: user.id }).then(() => {
paginationComponent.value.reload(); paginationComponent.value?.reload();
}); });
} }
function reject(user) { function reject(user) {
misskeyApi('following/requests/reject', { userId: user.id }).then(() => { misskeyApi('following/requests/reject', { userId: user.id }).then(() => {
paginationComponent.value.reload(); paginationComponent.value?.reload();
}); });
} }

View file

@ -40,7 +40,7 @@ function fetch() {
return; return;
} }
let promise: Promise<any>; let promise: Promise<unknown>;
if (uri.startsWith('https://')) { if (uri.startsWith('https://')) {
promise = misskeyApi('ap/show', { promise = misskeyApi('ap/show', {

View file

@ -65,7 +65,7 @@ async function onAccept(token: string) {
if (props.callback && props.callback !== '') { if (props.callback && props.callback !== '') {
const cbUrl = new URL(props.callback); const cbUrl = new URL(props.callback);
if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:'].includes(cbUrl.protocol)) throw new Error('invalid url'); if (['javascript:', 'file:', 'data:', 'mailto:', 'tel:', 'vbscript:'].includes(cbUrl.protocol)) throw new Error('invalid url');
cbUrl.searchParams.set('session', props.session); cbUrl.searchParams.set('session', props.session);
location.href = cbUrl.toString(); location.href = cbUrl.toString();
} else { } else {

View file

@ -77,15 +77,15 @@ async function create() {
clipsCache.delete(); clipsCache.delete();
pagingComponent.value.reload(); pagingComponent.value?.reload();
} }
function onClipCreated() { function onClipCreated() {
pagingComponent.value.reload(); pagingComponent.value?.reload();
} }
function onClipDeleted() { function onClipDeleted() {
pagingComponent.value.reload(); pagingComponent.value?.reload();
} }
const headerActions = computed(() => []); const headerActions = computed(() => []);

View file

@ -110,7 +110,7 @@ function addUser() {
listId: list.value.id, listId: list.value.id,
userId: user.id, userId: user.id,
}).then(() => { }).then(() => {
paginationEl.value.reload(); paginationEl.value?.reload();
}); });
}); });
} }
@ -126,7 +126,7 @@ async function removeUser(item, ev) {
listId: list.value.id, listId: list.value.id,
userId: item.userId, userId: item.userId,
}).then(() => { }).then(() => {
paginationEl.value.removeItem(item.id); paginationEl.value?.removeItem(item.id);
}); });
}, },
}], ev.currentTarget ?? ev.target); }], ev.currentTarget ?? ev.target);

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<!-- eslint-disable vue/no-mutating-props --> <!-- eslint-disable vue/no-mutating-props -->
<XContainer :draggable="true" @remove="() => $emit('remove')"> <XContainer :draggable="true" @remove="() => emit('remove')">
<template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template> <template #header><i class="ti ti-photo"></i> {{ i18n.ts._pages.blocks.image }}</template>
<template #func> <template #func>
<button @click="choose()"> <button @click="choose()">
@ -30,11 +30,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{
modelValue: any modelValue: Misskey.entities.PageBlock & { type: 'image' };
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void; (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'image' }): void;
(ev: 'remove'): void;
}>(); }>();
const file = ref<Misskey.entities.DriveFile | null>(null); const file = ref<Misskey.entities.DriveFile | null>(null);

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<!-- eslint-disable vue/no-mutating-props --> <!-- eslint-disable vue/no-mutating-props -->
<XContainer :draggable="true" @remove="() => $emit('remove')"> <XContainer :draggable="true" @remove="() => emit('remove')">
<template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template> <template #header><i class="ti ti-note"></i> {{ i18n.ts._pages.blocks.note }}</template>
<section style="padding: 16px;" class="_gaps_s"> <section style="padding: 16px;" class="_gaps_s">
@ -34,19 +34,24 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
const props = defineProps<{ const props = defineProps<{
modelValue: any modelValue: Misskey.entities.PageBlock & { type: 'note' };
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void; (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'note' }): void;
}>(); }>();
const id = ref<any>(props.modelValue.note); const id = ref(props.modelValue.note);
const note = ref<Misskey.entities.Note | null>(null); const note = ref<Misskey.entities.Note | null>(null);
watch(id, async () => { watch(id, async () => {
if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) { if (id.value && (id.value.startsWith('http://') || id.value.startsWith('https://'))) {
id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop(); id.value = (id.value.endsWith('/') ? id.value.slice(0, -1) : id.value).split('/').pop() ?? null;
}
if (!id.value) {
note.value = null;
return;
} }
emit('update:modelValue', { emit('update:modelValue', {

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<!-- eslint-disable vue/no-mutating-props --> <!-- eslint-disable vue/no-mutating-props -->
<XContainer :draggable="true" @remove="() => $emit('remove')"> <XContainer :draggable="true" @remove="() => emit('remove')">
<template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template> <template #header><i class="ti ti-note"></i> {{ props.modelValue.title }}</template>
<template #func> <template #func>
<button class="_button" @click="rename()"> <button class="_button" @click="rename()">
@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue'; import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
import XContainer from '../page-editor.container.vue'; import XContainer from '../page-editor.container.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -33,14 +34,13 @@ import { getPageBlockList } from '@/pages/page-editor/common.js';
const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue')); const XBlocks = defineAsyncComponent(() => import('../page-editor.blocks.vue'));
const props = withDefaults(defineProps<{ const props = defineProps<{
modelValue: any, modelValue: Misskey.entities.PageBlock & { type: 'section'; },
}>(), { }>();
modelValue: {},
});
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void; (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'section' }): void;
(ev: 'remove'): void;
}>(); }>();
const children = ref(deepClone(props.modelValue.children ?? [])); const children = ref(deepClone(props.modelValue.children ?? []));

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<!-- eslint-disable vue/no-mutating-props --> <!-- eslint-disable vue/no-mutating-props -->
<XContainer :draggable="true" @remove="() => $emit('remove')"> <XContainer :draggable="true" @remove="() => emit('remove')">
<template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template> <template #header><i class="ti ti-align-left"></i> {{ i18n.ts._pages.blocks.text }}</template>
<section> <section>
@ -15,18 +15,19 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue'; import { watch, ref, shallowRef, onMounted, onUnmounted } from 'vue';
import * as Misskey from 'misskey-js';
import XContainer from '../page-editor.container.vue'; import XContainer from '../page-editor.container.vue';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { Autocomplete } from '@/scripts/autocomplete.js'; import { Autocomplete } from '@/scripts/autocomplete.js';
const props = defineProps<{ const props = defineProps<{
modelValue: any modelValue: Misskey.entities.PageBlock & { type: 'text' }
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'update:modelValue', value: any): void; (ev: 'update:modelValue', value: Misskey.entities.PageBlock & { type: 'text' }): void;
}>(); }>();
let autocomplete: Autocomplete; let autocomplete: Autocomplete;

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => $emit('update:modelValue', v)"> <Sortable :modelValue="modelValue" tag="div" itemKey="id" handle=".drag-handle" :group="{ name: 'blocks' }" :animation="150" :swapThreshold="0.5" @update:modelValue="v => emit('update:modelValue', v)">
<template #item="{element}"> <template #item="{element}">
<div :class="$style.item"> <div :class="$style.item">
<!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 --> <!-- divが無いとエラーになる https://github.com/SortableJS/vue.draggable.next/issues/189 -->

View file

@ -52,7 +52,7 @@ const props = defineProps<{
const scope = computed(() => props.path ? props.path.split('/') : []); const scope = computed(() => props.path ? props.path.split('/') : []);
const keys = ref<any>(null); const keys = ref<[string, string][]>([]);
function fetchKeys() { function fetchKeys() {
misskeyApi('i/registry/keys-with-type', { misskeyApi('i/registry/keys-with-type', {

View file

@ -132,7 +132,7 @@ const mapCategories = Array.from(new Set(Object.values(Reversi.maps).map(x => x.
const props = defineProps<{ const props = defineProps<{
game: Misskey.entities.ReversiGameDetailed; game: Misskey.entities.ReversiGameDetailed;
connection: Misskey.ChannelConnection; connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>;
}>(); }>();
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false }); const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
@ -217,14 +217,14 @@ function onChangeReadyStates(states) {
game.value.user2Ready = states.user2; game.value.user2Ready = states.user2;
} }
function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) { function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
props.connection.send('updateSettings', { props.connection.send('updateSettings', {
key: key, key: key,
value: game.value[key], value: game.value[key],
}); });
} }
function onUpdateSettings({ userId, key, value }: { userId: string; key: keyof Misskey.entities.ReversiGameDetailed; value: any; }) { function onUpdateSettings<K extends typeof Misskey.reversiUpdateKeys[number]>({ userId, key, value }: { userId: string; key: K; value: Misskey.entities.ReversiGameDetailed[K]; }) {
if (userId === $i.id) return; if (userId === $i.id) return;
if (game.value[key] === value) return; if (game.value[key] === value) return;
game.value[key] = value; game.value[key] = value;

View file

@ -76,7 +76,11 @@ import { claimAchievement } from '@/scripts/achievements.js';
const parser = new Parser(); const parser = new Parser();
let aiscript: Interpreter; let aiscript: Interpreter;
const code = ref(''); const code = ref('');
const logs = ref<any[]>([]); const logs = ref<{
id: number;
text: string;
print: boolean;
}[]>([]);
const root = ref<AsUiRoot>(); const root = ref<AsUiRoot>();
const components = ref<Ref<AsUiComponent>[]>([]); const components = ref<Ref<AsUiComponent>[]>([]);
const uiKey = ref(0); const uiKey = ref(0);

View file

@ -138,12 +138,13 @@ const token = ref<string | number | null>(null);
const backupCodes = ref<string[]>(); const backupCodes = ref<string[]>();
function cancel() { function cancel() {
dialog.value.close(); dialog.value?.close();
} }
async function tokenDone() { async function tokenDone() {
if (token.value == null) return;
const res = await os.apiWithDialog('i/2fa/done', { const res = await os.apiWithDialog('i/2fa/done', {
token: token.value.toString(), token: typeof token.value === 'string' ? token.value : token.value.toString(),
}); });
backupCodes.value = res.backupCodes; backupCodes.value = res.backupCodes;
@ -166,7 +167,7 @@ function downloadBackupCodes() {
} }
function allDone() { function allDone() {
dialog.value.close(); dialog.value?.close();
} }
</script> </script>

View file

@ -90,7 +90,7 @@ function createAccount() {
}); });
} }
async function switchAccount(account: any) { async function switchAccount(account: Misskey.entities.UserDetailed) {
const fetchedAccounts = await getAccounts(); const fetchedAccounts = await getAccounts();
const token = fetchedAccounts.find(x => x.id === account.id)!.token; const token = fetchedAccounts.find(x => x.id === account.id)!.token;
switchAccountWithToken(token); switchAccountWithToken(token);

View file

@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import FormPagination from '@/components/MkPagination.vue'; import FormPagination from '@/components/MkPagination.vue';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
@ -77,7 +78,7 @@ const pagination = {
function revoke(token) { function revoke(token) {
misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => { misskeyApi('i/revoke-token', { tokenId: token.id }).then(() => {
list.value.reload(); list.value?.reload();
}); });
} }

View file

@ -110,7 +110,7 @@ const decorationsForPreview = computed(() => {
}); });
function cancel() { function cancel() {
dialog.value.close(); dialog.value?.close();
} }
async function update() { async function update() {
@ -120,7 +120,7 @@ async function update() {
offsetX: offsetX.value, offsetX: offsetX.value,
offsetY: offsetY.value, offsetY: offsetY.value,
}); });
dialog.value.close(); dialog.value?.close();
} }
async function attach() { async function attach() {
@ -130,12 +130,12 @@ async function attach() {
offsetX: offsetX.value, offsetX: offsetX.value,
offsetY: offsetY.value, offsetY: offsetY.value,
}); });
dialog.value.close(); dialog.value?.close();
} }
async function detach() { async function detach() {
emit('detach'); emit('detach');
dialog.value.close(); dialog.value?.close();
} }
</script> </script>

View file

@ -44,7 +44,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton> <MkButton transparent :class="$style.testButton" :disabled="!(active && event_renote)" @click="test('renote')"><i class="ti ti-send"></i></MkButton>
</div> </div>
<div :class="$style.switchBox"> <div :class="$style.switchBox">
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton> <MkButton transparent :class="$style.testButton" :disabled="!(active && event_reaction)" @click="test('reaction')"><i class="ti ti-send"></i></MkButton>
</div> </div>
<div :class="$style.switchBox"> <div :class="$style.switchBox">

View file

@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch> <MkSwitch v-model="event_note">{{ i18n.ts._webhookSettings._events.note }}</MkSwitch>
<MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch> <MkSwitch v-model="event_reply">{{ i18n.ts._webhookSettings._events.reply }}</MkSwitch>
<MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch> <MkSwitch v-model="event_renote">{{ i18n.ts._webhookSettings._events.renote }}</MkSwitch>
<MkSwitch v-model="event_reaction">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch> <MkSwitch v-model="event_reaction" :disabled="true">{{ i18n.ts._webhookSettings._events.reaction }}</MkSwitch>
<MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch> <MkSwitch v-model="event_mention">{{ i18n.ts._webhookSettings._events.mention }}</MkSwitch>
</div> </div>
</FormSection> </FormSection>

View file

@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.tl"> <div :class="$style.tl">
<MkTimeline <MkTimeline
ref="tlComponent" ref="tlComponent"
:key="src + withRenotes + withReplies + onlyFiles" :key="src + withRenotes + withReplies + onlyFiles + withSensitive"
:src="src.split(':')[0]" :src="src.split(':')[0]"
:list="src.split(':')[1]" :list="src.split(':')[1]"
:withRenotes="withRenotes" :withRenotes="withRenotes"

View file

@ -257,7 +257,7 @@ function parallaxLoop() {
} }
function parallax() { function parallax() {
const banner = bannerEl.value as any; const banner = bannerEl.value;
if (banner == null) return; if (banner == null) return;
const top = getScrollPosition(rootEl.value); const top = getScrollPosition(rootEl.value);

View file

@ -241,9 +241,13 @@ export class Storage<T extends StateDef> {
* getter/setterを作ります * getter/setterを作ります
* vue上で設定コントロールのmodelとして使う用 * vue上で設定コントロールのmodelとして使う用
*/ */
public makeGetterSetter<K extends keyof T>(key: K, getter?: (v: T[K]) => unknown, setter?: (v: unknown) => T[K]): { public makeGetterSetter<K extends keyof T, R = T[K]['default']>(
get: () => T[K]['default']; key: K,
set: (value: T[K]['default']) => void; getter?: (v: T[K]['default']) => R,
setter?: (v: R) => T[K]['default'],
): {
get: () => R;
set: (value: R) => void;
} { } {
const valueRef = ref(this.state[key]); const valueRef = ref(this.state[key]);
@ -265,7 +269,7 @@ export class Storage<T extends StateDef> {
return valueRef.value; return valueRef.value;
} }
}, },
set: (value: unknown) => { set: (value) => {
const val = setter ? setter(value) : value; const val = setter ? setter(value) : value;
this.set(key, val); this.set(key, val);
valueRef.value = val; valueRef.value = val;

View file

@ -10,7 +10,7 @@ import { $i, iAmModerator } from '@/account.js';
import MkLoading from '@/pages/_loading_.vue'; import MkLoading from '@/pages/_loading_.vue';
import MkError from '@/pages/_error_.vue'; import MkError from '@/pages/_error_.vue';
export const page = (loader: AsyncComponentLoader<any>) => defineAsyncComponent({ export const page = (loader: AsyncComponentLoader) => defineAsyncComponent({
loader: loader, loader: loader,
loadingComponent: MkLoading, loadingComponent: MkLoading,
errorComponent: MkError, errorComponent: MkError,

View file

@ -4,7 +4,7 @@
*/ */
import { EventEmitter } from 'eventemitter3'; import { EventEmitter } from 'eventemitter3';
import { IRouter, Resolved, RouteDef, RouterEvent } from '@/nirax.js'; import { IRouter, Resolved, RouteDef, RouterEvent, RouterFlag } from '@/nirax.js';
import type { App, ShallowRef } from 'vue'; import type { App, ShallowRef } from 'vue';
@ -79,7 +79,7 @@ class MainRouterProxy implements IRouter {
return this.supplier().currentRoute; return this.supplier().currentRoute;
} }
get navHook(): ((path: string, flag?: any) => boolean) | null { get navHook(): ((path: string, flag?: RouterFlag) => boolean) | null {
return this.supplier().navHook; return this.supplier().navHook;
} }
@ -91,11 +91,11 @@ class MainRouterProxy implements IRouter {
return this.supplier().getCurrentKey(); return this.supplier().getCurrentKey();
} }
getCurrentPath(): any { getCurrentPath(): string {
return this.supplier().getCurrentPath(); return this.supplier().getCurrentPath();
} }
push(path: string, flag?: any): void { push(path: string, flag?: RouterFlag): void {
this.supplier().push(path, flag); this.supplier().push(path, flag);
} }

View file

@ -2,8 +2,9 @@
* SPDX-FileCopyrightText: syuilo and misskey-project * SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import * as Misskey from 'misskey-js';
export function checkWordMute(note: Record<string, any>, me: Record<string, any> | null | undefined, mutedWords: Array<string | string[]>): boolean { export function checkWordMute(note: Misskey.entities.Note, me: Misskey.entities.UserLite | null | undefined, mutedWords: Array<string | string[]>): boolean {
// 自分自身 // 自分自身
if (me && (note.userId === me.id)) return false; if (me && (note.userId === me.id)) return false;

View file

@ -3,22 +3,22 @@
* SPDX-License-Identifier: AGPL-3.0-only * SPDX-License-Identifier: AGPL-3.0-only
*/ */
import { defaultStore } from '@/store.js'; export type DeviceKind = 'smartphone' | 'tablet' | 'desktop';
await defaultStore.ready;
const ua = navigator.userAgent.toLowerCase(); const ua = navigator.userAgent.toLowerCase();
const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700); const isTablet = /ipad/.test(ua) || (/mobile|iphone|android/.test(ua) && window.innerWidth > 700);
const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua); const isSmartphone = !isTablet && /mobile|iphone|android/.test(ua);
const isIPhone = /iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; export const DEFAULT_DEVICE_KIND: DeviceKind = (
// navigator.platform may be deprecated but this check is still required isSmartphone
const isIPadOS = navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1; ? 'smartphone'
const isIos = /ipad|iphone|ipod/gi.test(ua) && navigator.maxTouchPoints > 1; : isTablet
? 'tablet'
: 'desktop'
);
export const isFullscreenNotSupported = isIPhone || isIos; export let deviceKind: DeviceKind = DEFAULT_DEVICE_KIND;
export const deviceKind: 'smartphone' | 'tablet' | 'desktop' = defaultStore.state.overridedDeviceKind ? defaultStore.state.overridedDeviceKind export function updateDeviceKind(kind: DeviceKind | null) {
: isSmartphone ? 'smartphone' deviceKind = kind ?? DEFAULT_DEVICE_KIND;
: isTablet ? 'tablet' }
: 'desktop';

View file

@ -15,7 +15,7 @@ type Hidden = boolean | ((v: any) => boolean);
export type FormItem = { export type FormItem = {
label?: string; label?: string;
type: 'string'; type: 'string';
default: string | null; default?: string | null;
description?: string; description?: string;
required?: boolean; required?: boolean;
hidden?: Hidden; hidden?: Hidden;
@ -24,7 +24,7 @@ export type FormItem = {
} | { } | {
label?: string; label?: string;
type: 'number'; type: 'number';
default: number | null; default?: number | null;
description?: string; description?: string;
required?: boolean; required?: boolean;
hidden?: Hidden; hidden?: Hidden;
@ -32,20 +32,20 @@ export type FormItem = {
} | { } | {
label?: string; label?: string;
type: 'boolean'; type: 'boolean';
default: boolean | null; default?: boolean | null;
description?: string; description?: string;
hidden?: Hidden; hidden?: Hidden;
} | { } | {
label?: string; label?: string;
type: 'enum'; type: 'enum';
default: string | null; default?: string | null;
required?: boolean; required?: boolean;
hidden?: Hidden; hidden?: Hidden;
enum: EnumItem[]; enum: EnumItem[];
} | { } | {
label?: string; label?: string;
type: 'radio'; type: 'radio';
default: unknown | null; default?: unknown | null;
required?: boolean; required?: boolean;
hidden?: Hidden; hidden?: Hidden;
options: { options: {
@ -55,7 +55,7 @@ export type FormItem = {
} | { } | {
label?: string; label?: string;
type: 'range'; type: 'range';
default: number | null; default?: number | null;
description?: string; description?: string;
required?: boolean; required?: boolean;
step?: number; step?: number;
@ -66,12 +66,12 @@ export type FormItem = {
} | { } | {
label?: string; label?: string;
type: 'object'; type: 'object';
default: Record<string, unknown> | null; default?: Record<string, unknown> | null;
hidden: Hidden; hidden: Hidden;
} | { } | {
label?: string; label?: string;
type: 'array'; type: 'array';
default: unknown[] | null; default?: unknown[] | null;
hidden: Hidden; hidden: Hidden;
} | { } | {
type: 'button'; type: 'button';

View file

@ -0,0 +1,46 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
type PartiallyPartial<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
type VideoEl = PartiallyPartial<HTMLVideoElement, 'requestFullscreen'> & {
webkitEnterFullscreen?(): void;
webkitExitFullscreen?(): void;
};
type PlayerEl = PartiallyPartial<HTMLElement, 'requestFullscreen'>;
type RequestFullscreenProps = {
readonly videoEl: VideoEl;
readonly playerEl: PlayerEl;
readonly options?: FullscreenOptions | null;
};
type ExitFullscreenProps = {
readonly videoEl: VideoEl;
};
export const requestFullscreen = ({ videoEl, playerEl, options }: RequestFullscreenProps) => {
if (playerEl.requestFullscreen != null) {
playerEl.requestFullscreen(options ?? undefined);
return;
}
if (videoEl.webkitEnterFullscreen != null) {
videoEl.webkitEnterFullscreen();
return;
}
};
export const exitFullscreen = ({ videoEl }: ExitFullscreenProps) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (document.exitFullscreen != null) {
document.exitFullscreen();
return;
}
if (videoEl.webkitExitFullscreen != null) {
videoEl.webkitExitFullscreen();
return;
}
};

View file

@ -26,6 +26,7 @@ if (window.Cypress) {
console.log('Cypress detected. It will use localStorage.'); console.log('Cypress detected. It will use localStorage.');
} }
// Check for the availability of indexedDB.
if (idbAvailable) { if (idbAvailable) {
await iset('idb-test', 'test') await iset('idb-test', 'test')
.catch(err => { .catch(err => {
@ -37,16 +38,36 @@ if (idbAvailable) {
console.error('indexedDB is unavailable. It will use localStorage.'); console.error('indexedDB is unavailable. It will use localStorage.');
} }
/**
* Get a value from indexedDB (or localStorage as a fallback).
*
* @param key The key of the item to retrieve.
*
* @returns The value of the item.
*/
export async function get(key: string) { export async function get(key: string) {
if (idbAvailable) return iget(key); if (idbAvailable) return iget(key);
return miLocalStorage.getItemAsJson(`${PREFIX}${key}`); return miLocalStorage.getItemAsJson(`${PREFIX}${key}`);
} }
/**
* Set a value in indexedDB (or localStorage as a fallback).
*
* @param {string} key - The key of the item to set.
* @param {any} val - The value of the item to set.
* @returns {Promise<void>} - A promise that resolves when the value has been set.`
*/
export async function set(key: string, val: any) { export async function set(key: string, val: any) {
if (idbAvailable) return iset(key, val); if (idbAvailable) return iset(key, val);
return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val); return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val);
} }
/**
* Delete a value from indexedDB (or localStorage as a fallback).
*
* @param {string} key - The key of the item to delete.
* @returns {Promise<void>} - A promise that resolves when the value has been deleted.
*/
export async function del(key: string) { export async function del(key: string) {
if (idbAvailable) return idel(key); if (idbAvailable) return idel(key);
return miLocalStorage.removeItem(`${PREFIX}${key}`); return miLocalStorage.removeItem(`${PREFIX}${key}`);

View file

@ -17,7 +17,7 @@ export function misskeyApi<
_ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT, _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType<E, P> : ResT,
>( >(
endpoint: E, endpoint: E,
data: P = {} as any, data: P & { i?: string | null; } = {} as any,
token?: string | null | undefined, token?: string | null | undefined,
signal?: AbortSignal, signal?: AbortSignal,
): Promise<_ResT> { ): Promise<_ResT> {
@ -30,8 +30,8 @@ export function misskeyApi<
const promise = new Promise<_ResT>((resolve, reject) => { const promise = new Promise<_ResT>((resolve, reject) => {
// Append a credential // Append a credential
if ($i) (data as any).i = $i.token; if ($i) data.i = $i.token;
if (token !== undefined) (data as any).i = token; if (token !== undefined) data.i = token;
// Send request // Send request
window.fetch(`${apiUrl}/${endpoint}`, { window.fetch(`${apiUrl}/${endpoint}`, {

View file

@ -80,7 +80,7 @@ export function chooseFileFromUrl(): Promise<Misskey.entities.DriveFile> {
}); });
} }
function select(src: any, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> { function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise<Misskey.entities.DriveFile[]> {
return new Promise((res, rej) => { return new Promise((res, rej) => {
const keepOriginal = ref(defaultStore.state.keepOriginalUploading); const keepOriginal = ref(defaultStore.state.keepOriginalUploading);
@ -107,10 +107,10 @@ function select(src: any, label: string | null, multiple: boolean): Promise<Miss
}); });
} }
export function selectFile(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile> { export function selectFile(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile> {
return select(src, label, false).then(files => files[0]); return select(src, label, false).then(files => files[0]);
} }
export function selectFiles(src: any, label: string | null = null): Promise<Misskey.entities.DriveFile[]> { export function selectFiles(src: HTMLElement | EventTarget | null, label: string | null = null): Promise<Misskey.entities.DriveFile[]> {
return select(src, label, true); return select(src, label, true);
} }

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