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

View file

@ -83,6 +83,10 @@ One should not add property that has defined before by other implementation, or
## Reviewers guide
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
- Scope
- Are the goals of the PR clear?

View file

@ -8,37 +8,37 @@ search: "Cercar"
notifications: "Notificacions"
username: "Nom d'usuari"
password: "Contrasenya"
initialPasswordForSetup: "Contrasenya inicial per fer la primera configuració "
initialPasswordForSetup: "Contrasenya inicial per la configuració inicial"
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."
forgotPassword: "Restableix la contrasenya "
fetchingAsApObject: "Cercant al Fediverse..."
forgotPassword: "Contrasenya oblidada"
fetchingAsApObject: "Cercant en el Fediverse..."
ok: "OK"
gotIt: "D'acord "
gotIt: "Ho he entès!"
cancel: "Cancel·lar"
noThankYou: "No, gràcies"
enterUsername: "Introdueix el teu nom d'usuari"
renotedBy: "Impulsat per {user}"
noNotes: "Cap nota"
noNotifications: "Cap notificació"
instance: "Instància "
instance: "Servidor"
settings: "Preferències"
notificationSettings: "Configurar les notificacions"
notificationSettings: "Paràmetres de notificacions"
basicSettings: "Configuració bàsica"
otherSettings: "Altres configuracions"
openInWindow: "Obrir en una finestra nova"
otherSettings: "Configuració avançada"
openInWindow: "Obrir en una nova finestra"
profile: "Perfil"
timeline: "Línia de temps"
noAccountDescription: "Aquest usuari encara no ha escrit la seva biografia."
login: "Iniciar sessió"
loggingIn: "Iniciar la sessió "
loggingIn: "Identificant-se"
logout: "Tancar la sessió"
signup: "Registrar-se"
uploading: "Pujant..."
save: "Desa"
users: "Usuaris"
addUser: "Afegir un usuari"
favorite: "Afegeix als preferits"
favorite: "Afegir a preferits"
favorites: "Favorits"
unfavorite: "Eliminar dels preferits"
favorited: "Afegit als preferits."
@ -50,26 +50,26 @@ copyContent: "Copiar el contingut"
copyLink: "Copiar l'enllaç"
copyLinkRenote: "Copiar l'enllaç de la renota"
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."
addToList: "Afegir a una llista"
addToAntenna: "Afegir a una antena"
addToAntenna: "Afegir a l'antena"
sendMessage: "Enviar un missatge"
copyRSS: "Copiar RSS"
copyUsername: "Copiar nom d'usuari"
copyUserId: "Copiar ID d'usuari"
copyNoteId: "Copiar ID de la nota"
copyFileId: "Copiar ID de l'arxiu"
copyFolderId: "Copiar ID de la carpeta"
copyProfileUrl: "Copiar adreça URL del perfil"
copyNoteId: "Copiar ID de nota"
copyFileId: "Copiar ID d'arxiu"
copyFolderId: "Copiar ID de carpeta"
copyProfileUrl: "Copiar URL del perfil"
searchUser: "Cercar un usuari"
searchThisUsersNotes: "Cercar les publicacions de l'usuari"
reply: "Respon"
searchThisUsersNotes: "Cerca les publicacions de l'usuari"
reply: "Respondre"
loadMore: "Carregar més"
showMore: "Veure més"
showLess: "Mostrar menys"
showLess: "Mostra menys"
youGotNewFollower: "t'ha seguit"
receiveFollowRequest: "Has rebut una sol·licitud de seguiment"
receiveFollowRequest: "Sol·licitud de seguiment rebuda"
followRequestAccepted: "Sol·licitud de seguiment acceptada"
mention: "Menció"
mentions: "Mencions"
@ -78,25 +78,25 @@ importAndExport: "Importar / Exportar"
import: "Importar"
export: "Exporta"
files: "Fitxers"
download: "Descarregar"
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer també seran esborrades."
unfollowConfirm: "Segur que vols deixar de seguir a {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."
importRequested: "Has sol·licitat una importació de dades. Això pot trigar una estona."
download: "Baixar"
driveFileDeleteConfirm: "Estàs segur que vols suprimir el fitxer \"{name}\"? Les notes associades a aquest fitxer adjunt també se suprimiran."
unfollowConfirm: "Estàs segur que vols deixar de seguir {name}?"
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ó. Això pot trigar una estona."
lists: "Llistes"
noLists: "No tens cap llista"
note: "Nota"
notes: "Notes"
following: "Segueixes "
following: "Seguint"
followers: "Seguidors"
followsYou: "Et segueix"
createList: "Crear llista"
manageLists: "Gestionar les llistes"
error: "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"
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."
youShouldUpgradeClient: "Per veure aquesta pàgina, actualitzeu-la per actualitzar el vostre client."
enterListName: "Introdueix un nom per a la llista"
@ -104,52 +104,52 @@ privacy: "Privadesa"
makeFollowManuallyApprove: "Les sol·licituds de seguiment requereixen aprovació"
defaultNoteVisibility: "Visibilitat per defecte"
follow: "Seguint"
followRequest: "Enviar sol·licitud de seguiment"
followRequest: "Enviar la sol·licitud de seguiment"
followRequests: "Sol·licituds de seguiment"
unfollow: "Deixar de seguir"
followRequestPending: "Sol·licituds de seguiment pendents"
enterEmoji: "Introduir un emoji"
renote: "Impulsar "
renote: "Impulsa"
unrenote: "Anul·la l'impuls"
renoted: "S'ha impulsat"
renotedToX: "Impulsat per {name}."
cantRenote: "No es pot impulsar aquesta publicació"
cantReRenote: "No es pot impulsar un impuls."
cantReRenote: "No es pot impulsar l'impuls."
quote: "Cita"
inChannelRenote: "Impulsar només a un canal"
inChannelQuote: "Citar només a un canal"
renoteToChannel: "Impulsar a un canal"
renoteToOtherChannel: "Impulsar a un altre canal"
inChannelRenote: "Renotar només al Canal"
inChannelQuote: "Citar només al Canal"
renoteToChannel: "Impulsa a un canal"
renoteToOtherChannel: "Impulsa a un altre canal"
pinnedNote: "Nota fixada"
pinned: "Fixar al perfil"
you: "Tu"
clickToShow: "Fes clic per mostrar"
sensitive: "Sensible"
sensitive: "NSFW"
add: "Afegir"
reaction: "Reacció "
reaction: "Reaccions"
reactions: "Reaccions"
emojiPicker: "Selector d'emojis"
pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb qui vols reaccionar"
pinnedEmojisSettingDescription: "Selecciona quins emojis vols deixar fixats i es mostrin en obrir el selector d'emojis"
emojiPickerDisplay: "Mostrar el selector d'emojis"
emojiPicker: "Selecció d'emojis"
pinnedEmojisForReactionSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
pinnedEmojisSettingDescription: "Selecciona l'emoji amb el qual reaccionar"
emojiPickerDisplay: "Visualitza el selector d'emojis"
overwriteFromPinnedEmojisForReaction: "Reemplaça els emojis de la reacció"
overwriteFromPinnedEmojis: "Sobreescriu els emojis fixats al panel de reaccions"
overwriteFromPinnedEmojis: "Sobreescriu des dels emojis fixats"
reactionSettingDescription2: "Arrossega per reordenar, fes clic per suprimir, prem \"+\" per afegir."
rememberNoteVisibility: "Recorda la configuració de visibilitat de les notes"
attachCancel: "Eliminar el fitxer adjunt"
deleteFile: "Esborrar l'arxiu "
markAsSensitive: "Marcar com a sensible"
markAsSensitive: "Marcar com a NSFW"
unmarkAsSensitive: "Deixar de marcar com a sensible"
enterFileName: "Defineix nom del fitxer"
mute: "Silencia"
unmute: "Deixa de silenciar"
renoteMute: "Silenciar impulsos"
renoteUnmute: "Treure el silenci dels impulsos"
renoteMute: "Silenciar Renotes"
renoteUnmute: "Treure el silenci de les renotes"
block: "Bloqueja"
unblock: "Desbloqueja"
suspend: "Suspèn"
unsuspend: "Deixa de suspendre"
blockConfirm: "Vols bloquejar-lo?"
blockConfirm: "Vols bloquejar?"
unblockConfirm: "Vols desbloquejar-lo?"
suspendConfirm: "Estàs segur que vols suspendre 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"
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"
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"
flagAsCatDescription: "Activeu aquesta opció per marcar aquest compte com a gat."
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"
addAccount: "Afegeix un compte"
reloadAccountsList: "Recarregar la llista de contactes"
@ -204,7 +204,7 @@ selectUser: "Selecciona usuari/a"
recipient: "Destinatari"
annotation: "Comentaris"
federation: "Federació"
instances: "Instàncies "
instances: "Servidors"
registeredAt: "Registrat a"
latestRequestReceivedAt: "Última petició rebuda"
latestStatus: "Últim estat"
@ -213,7 +213,7 @@ charts: "Gràfics"
perHour: "Per hora"
perDay: "Per dia"
stopActivityDelivery: "Deixa d'enviar activitats"
blockThisInstance: "Bloca aquesta instància "
blockThisInstance: "Deixa d'enviar activitats"
silenceThisInstance: "Silencia aquesta instància "
mediaSilenceThisInstance: "Silenciar els arxius d'aquesta instància "
operations: "Accions"
@ -228,7 +228,7 @@ network: "Xarxa"
disk: "Disc"
instanceInfo: "Informació del fitxer d'instal·lació"
statistics: "Estadístiques"
clearQueue: "Esborra la cua de feina"
clearQueue: "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."
clearCachedFiles: "Esborra la memòria cau"
@ -254,7 +254,7 @@ processing: "S'està processant..."
preview: "Vista prèvia"
default: "Per defecte"
defaultValueIs: "Per defecte: {value}"
noCustomEmojis: "No hi ha emojis personalitzats"
noCustomEmojis: "Cap emoji personalitzat"
noJobs: "No hi ha feines"
federating: "Federant"
blocked: "Bloquejat"
@ -268,11 +268,11 @@ instanceFollowers: "Seguidors del servidor"
instanceUsers: "Usuaris del servidor"
changePassword: "Canvia la contrasenya"
security: "Seguretat"
retypedNotMatch: "Les entrades no coincideix"
retypedNotMatch: "L'entrada no coincideix"
currentPassword: "Contrasenya actual"
newPassword: "Contrasenya nova"
newPasswordRetype: "Contrasenya nova (repeteix-la)"
attachFile: "Afegeix un arxiu"
newPasswordRetype: "Contrasenya nou (repeteix-la)"
attachFile: "Adjunta fitxers"
more: "Més"
featured: "Destacat"
usernameOrUserId: "Nom o ID d'usuari"
@ -282,25 +282,25 @@ announcements: "Anuncis"
imageUrl: "URL de la imatge"
remove: "Eliminar"
removed: "Eliminat"
removeAreYouSure: "Segur que vols esborrar «{x}»?"
deleteAreYouSure: "Segur que vols esborrar «{x}»?"
resetAreYouSure: "Segur que vols restablir-ho?"
areYouSure: "Estàs segur?"
removeAreYouSure: "Segur que voleu retirar «{x}»?"
deleteAreYouSure: "Segur que voleu retirar «{x}»?"
resetAreYouSure: "Segur que voleu restablir-ho?"
areYouSure: "Està segur?"
saved: "S'ha desat"
messaging: "Xat"
upload: "Puja"
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."
fromDrive: "Des del Disc"
keepOriginalUploadingDescription: "Guarda la imatge pujada com hi és. Si està apagat, una versió per a la visualització a la xarxa serà generada quan sigui pujada."
fromDrive: "Des de la unitat"
fromUrl: "Des d'un enllaç"
uploadFromUrl: "Carrega des d'un enllaç"
uploadFromUrlDescription: "Enllaç del fitxer que vols carregar"
uploadFromUrlRequested: "Càrrega sol·licitada"
uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot trigar un temps"
uploadFromUrlMayTakeTime: "La càrrega des de l'enllaç pot prendre un temps"
explore: "Explora"
messageRead: "Vist"
noMoreHistory: "No hi ha res més per veure"
startMessaging: "Comença a xatejar"
noMoreHistory: "No hi resta més per veure"
startMessaging: "Començar a xatejar"
nUsersRead: "Vist per {n}"
agreeTo: "Accepto que {0}"
agree: "Hi estic d'acord"
@ -312,7 +312,7 @@ home: "Inici"
remoteUserCaution: "Ja que aquest usuari resideix a una instància remota, la informació mostrada es podria trobar incompleta."
activity: "Activitat"
images: "Imatges"
image: "Imatge"
image: "Imatges"
birthday: "Aniversari"
yearsOld: "{age} anys"
registeredDate: "Data de registre"
@ -327,10 +327,10 @@ darkThemes: "Temes foscos"
syncDeviceDarkMode: "Sincronitza el mode fosc amb la configuració del dispositiu"
drive: "Unitat"
fileName: "Nom del Fitxer"
selectFile: "Selecciona un fitxer"
selectFile: "Selecciona fitxers"
selectFiles: "Selecciona fitxers"
selectFolder: "Selecció de carpeta"
selectFolders: "Selecció de carpetes"
selectFolders: "Selecció de carpeta"
fileNotSelected: "Cap fitxer seleccionat"
renameFile: "Canvia el nom del fitxer"
folderName: "Nom de la carpeta"
@ -359,9 +359,9 @@ reload: "Actualitza"
doNothing: "Ignora"
reloadConfirm: "Vols recarregar?"
watch: "Veure"
unwatch: "Deixa de veure"
unwatch: "Deixar de veure"
accept: "Acceptar"
reject: "Denega"
reject: "Denegar"
normal: "Normal"
instanceName: "Nom 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"
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"
enableRegistration: "Permet el registre de nous usuaris"
enableRegistration: "Permet els registres d'usuaris"
invite: "Convida"
driveCapacityPerLocalAccount: "Capacitat del disc per usuaris locals"
driveCapacityPerRemoteAccount: "Capacitat del disc per usuaris remots"
@ -393,20 +393,20 @@ basicInfo: "Informació bàsica"
pinnedUsers: "Usuaris fixats"
pinnedUsersDescription: "Llista d'usuaris, separats per salts de línia, que seran fixats a la pestanya \"Explorar\"."
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"
pinnedNotes: "Nota fixada"
hcaptcha: "hCaptcha"
enableHcaptcha: "Activa hCaptcha"
enableHcaptcha: "Activar hCaptcha"
hcaptchaSiteKey: "Clau del lloc"
hcaptchaSecretKey: "Clau secreta"
mcaptcha: "mCaptcha"
enableMcaptcha: "Activa mCaptcha"
enableMcaptcha: "Activar mCaptcha"
mcaptchaSiteKey: "Clau del lloc"
mcaptchaSecretKey: "Clau secreta"
mcaptchaInstanceUrl: "Adreça URL del servidor mCaptcha"
recaptcha: "reCAPTCHA"
enableRecaptcha: "Activa reCAPTCHA"
enableRecaptcha: "Activar reCAPTCHA"
recaptchaSiteKey: "Clau del lloc"
recaptchaSecretKey: "Clau secreta"
turnstile: "Turnstile"
@ -448,14 +448,14 @@ aboutMisskey: "Quant a Misskey"
administrator: "Administrador/a"
token: "Codi de verificació"
2fa: "Autenticació de doble factor"
setupOf2fa: "Configura l'autenticació de doble factor"
setupOf2fa: "Configurar l'autenticació de doble factor"
totp: "Aplicació d'autenticació"
totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d'autenticació"
moderator: "Moderador/a"
moderation: "Moderació"
moderationNote: "Nota de moderació "
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ó "
nUsersMentioned: "{n} usuaris mencionats"
securityKeyAndPasskey: "Clau de seguretat / Clau de pas"
@ -471,13 +471,13 @@ reduceUiAnimation: "Redueix les animacions de la interfície"
share: "Comparteix"
notFound: "No s'ha trobat"
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"
markAsReadAllUnreadNotes: "Marca-ho tot com a llegit"
markAsReadAllTalkMessages: "Marcar tots els missatges com llegits"
help: "Ajuda"
inputMessageHere: "Escriu aquí el teu missatge "
close: "Tanca"
close: "Tancar"
invites: "Convida"
members: "Membres"
transfer: "Transferir"
@ -508,7 +508,7 @@ normalPassword: "Bona contrasenya"
strongPassword: "Contrasenya segura"
passwordMatched: "Correcte!"
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."
or: "O"
language: "Idioma"
@ -591,7 +591,7 @@ chooseEmoji: "Tria un emoji"
unableToProcess: "L'operació no pot ser completada "
recentUsed: "Utilitzat recentment"
install: "Instal·lació "
uninstall: "Desinstal·la"
uninstall: "Desinstal·lar "
installedApps: "Aplicacions autoritzades "
nothing: "No hi ha res per veure aquí "
installedDate: "Data d'instal·lació"
@ -608,13 +608,13 @@ output: "Sortida"
script: "Script"
disablePagesScript: "Desactivar AiScript a les pàgines "
updateRemoteUser: "Actualitzar la informació de l'usuari remot"
unsetUserAvatar: "Desactiva l'avatar "
unsetUserAvatar: "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?"
deleteAllFiles: "Esborra tots els arxius"
deleteAllFiles: "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."
userSuspended: "Aquest usuari ha sigut suspès"
userSilenced: "Aquest usuari està sent silenciat"
@ -1183,8 +1183,8 @@ currentAnnouncements: "Informes actuals"
pastAnnouncements: "Informes passats"
youHaveUnreadAnnouncements: "Tens informes per llegir."
useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey."
replies: "Respon"
renotes: "Impulsar "
replies: "Respondre"
renotes: "Impulsa"
loadReplies: "Mostrar les respostes"
loadConversation: "Mostrar la conversació "
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"
lockdown: "Bloquejat"
pleaseSelectAccount: "Seleccionar un compte"
availableRoles: "Roles disponibles "
_accountSettings:
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ó."

View file

@ -331,7 +331,6 @@ selectFile: "Select a file"
selectFiles: "Select files"
selectFolder: "Select a folder"
selectFolders: "Select folders"
fileNotSelected: "No file selected"
renameFile: "Rename file"
folderName: "Folder name"
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"
lockdown: "Lockdown"
pleaseSelectAccount: "Select an account"
availableRoles: "Available roles"
_accountSettings:
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."
@ -2732,9 +2730,3 @@ _embedCodeGen:
generateCode: "Generate embed code"
codeGenerated: "The code has been generated"
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: "즐겨찾기"
unfavorite: "즐겨찾기에서 제거"
favorited: "즐겨찾기에 등록했습니다."
alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다."
alreadyFavorited: "이미 즐겨찾기에 등록습니다."
cantFavorite: "즐겨찾기에 등록하지 못했습니다."
pin: "프로필에 고정"
unpin: "프로필에서 고정 해제"
@ -947,9 +947,6 @@ oneHour: "1시간"
oneDay: "1일"
oneWeek: "일주일"
oneMonth: "1개월"
threeMonths: "3개월"
oneYear: "1년"
threeDays: "3일"
reflectMayTakeTime: "반영되기까지 시간이 걸릴 수 있습니다."
failedToFetchAccountInformation: "계정 정보를 가져오지 못했습니다"
rateLimitExceeded: "요청 제한 횟수를 초과하였습니다"
@ -1289,29 +1286,13 @@ signinWithPasskey: "패스키로 로그인"
unknownWebAuthnKey: "등록되지 않은 패스키입니다."
passkeyVerificationFailed: "패스키 검증을 실패했습니다."
passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다."
messageToFollower: "팔로워에 보낼 메시지"
messageToFollower: "팔로워에 보낼 메시지"
target: "대상"
testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>"
prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)"
prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다."
yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다."
yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요."
thisContentsAreMarkedAsSigninRequiredByAuthor: "게시자에 의해 로그인해야 볼 수 있도록 설정되어 있습니다."
lockdown: "잠금"
pleaseSelectAccount: "계정을 선택해주세요."
availableRoles: "사용 가능한 역할"
_accountSettings:
requireSigninToViewContents: "콘텐츠 열람을 위해 로그인으 필수로 설정하기"
requireSigninToViewContentsDescription1: "자신이 작성한 모든 노트 등의 콘텐츠를 보기 위해 로그인을 필수로 설정합니다. 크롤러가 정보 수집하는 것을 방지하는 효과를 기대할 수 있습니다."
requireSigninToViewContentsDescription2: "URL 미리보기(OGP), 웹페이지에 삽입, 노트 인용을 지원하지 않는 서버에서 볼 수 없게 됩니다."
requireSigninToViewContentsDescription3: "원격 서버에 연합된 콘텐츠에는 이러한 제한이 적용되지 않을 수 있습니다."
makeNotesFollowersOnlyBefore: "과거 노트는 팔로워만 볼 수 있도록 설정하기"
makeNotesFollowersOnlyBeforeDescription: "이 기능이 활성화되어 있는 동안, 설정된 날짜 및 시간보다 과거 또는 설정된 시간이 지난 노트는 팔로워만 볼 수 있게 됩니다.비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
makeNotesHiddenBefore: "과거 노트 비공개로 전환하기"
makeNotesHiddenBeforeDescription: "이 기능이 활성화되어 있는 동안 설정한 날짜 및 시간보다 과거 또는 설정한 시간이 지난 노트는 본인만 볼 수 있게(비공개로 전환) 됩니다. 비활성화하면 노트의 공개 상태도 원래대로 돌아갑니다."
mayNotEffectForFederatedNotes: "원격 서버에 연합된 노트에는 효과가 없을 수도 있습니다."
notesHavePassedSpecifiedPeriod: "지정한 시간이 경과된 노트"
notesOlderThanSpecifiedDateAndTime: "지정된 날짜 및 시간 이전의 노트"
_abuseUserReport:
forward: "전달"
forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다."
@ -2176,11 +2157,8 @@ _auth:
permissionAsk: "이 앱은 다음의 권한을 요청합니다"
pleaseGoBack: "앱으로 돌아가서 시도해 주세요"
callback: "앱으로 돌아갑니다"
accepted: "접근 권한이 부여되었습니다."
denied: "접근이 거부되었습니다"
scopeUser: "다음 사용자로 활동하고 있습니다."
pleaseLogin: "어플리케이션의 접근을 허가하려면 로그인하십시오."
byClickingYouWillBeRedirectedToThisUrl: "접근을 허용하면 자동으로 다음 URL로 이동합니다."
_antennaSources:
all: "모든 노트"
homeTimeline: "팔로우중인 유저의 노트"
@ -2732,9 +2710,3 @@ _embedCodeGen:
generateCode: "임베디드 코드를 만들기"
codeGenerated: "코드를 만들었습니다."
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"
username: "Tên người dùng"
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"
fetchingAsApObject: "Đang nạp dữ liệu từ Fediverse..."
ok: "Đồng ý"

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -6,11 +6,23 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
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 { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
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()
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 { Packed } from '@/misc/json-schema.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 { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
@ -306,10 +306,10 @@ export class WebhookTestService {
* - on
*/
@bindThis
public async testUserWebhook(
public async testUserWebhook<T extends WebhookEventTypes>(
params: {
webhookId: MiWebhook['id'],
type: WebhookEventTypes,
type: T,
override?: Partial<Omit<MiWebhook, 'id'>>,
},
sender: MiUser | null,
@ -321,7 +321,7 @@ export class WebhookTestService {
}
const webhook = webhooks[0];
const send = (contents: unknown) => {
const send = <U extends WebhookEventTypes>(type: U, contents: UserWebhookPayload<U>) => {
const merged = {
...webhook,
...params.override,
@ -329,7 +329,7 @@ export class WebhookTestService {
// テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加するチェック処理などをスキップする意図.
// また、Jobの試行回数も1回だけ.
this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
this.queueService.userWebhookDeliver(merged, type, contents, { attempts: 1 });
};
const dummyNote1 = generateDummyNote({
@ -361,33 +361,40 @@ export class WebhookTestService {
switch (params.type) {
case 'note': {
send(toPackedNote(dummyNote1));
send('note', { note: toPackedNote(dummyNote1) });
break;
}
case 'reply': {
send(toPackedNote(dummyReply1));
send('reply', { note: toPackedNote(dummyReply1) });
break;
}
case 'renote': {
send(toPackedNote(dummyRenote1));
send('renote', { note: toPackedNote(dummyRenote1) });
break;
}
case 'mention': {
send(toPackedNote(dummyMention1));
send('mention', { note: toPackedNote(dummyMention1) });
break;
}
case 'follow': {
send(toPackedUserDetailedNotMe(dummyUser1));
send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
break;
}
case 'followed': {
send(toPackedUserLite(dummyUser2));
send('followed', { user: toPackedUserLite(dummyUser2) });
break;
}
case 'unfollow': {
send(toPackedUserDetailedNotMe(dummyUser3));
send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
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) {
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 { IActivity } from '@/core/activitypub/type.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 { FindOptionsWhere } from 'typeorm';
@ -486,6 +487,16 @@ export class ActivityPubServerService {
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');
this.setResponseType(request, reply);
return (this.apRendererService.addContext(await this.apRendererService.renderPerson(user as MiLocalUser)));
@ -654,19 +665,20 @@ export class ActivityPubServerService {
const user = await this.usersRepository.findOneBy({
id: userId,
host: IsNull(),
isSuspended: false,
});
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');
const acct = Acct.parse(request.params.acct);
const user = await this.usersRepository.findOneBy({
usernameLower: request.params.user.toLowerCase(),
host: IsNull(),
usernameLower: acct.username,
host: acct.host ?? IsNull(),
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 newDescription = profileUpdates.description === undefined ? profile.description : profileUpdates.description;
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
const newFollowedMessage = profileUpdates.followedMessage === undefined ? profile.followedMessage : profileUpdates.followedMessage;
if (newName != null) {
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.tags = tags;

View file

@ -112,7 +112,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
this.activeUsersChart.read(me);
await this.noteEntityService.packMany(timeline, me);
return await this.noteEntityService.packMany(timeline, me);
}
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 { ClipEntityService } from '@/core/entities/ClipEntityService.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 { handleRequestRedirectToOmitSearch } from '@/misc/fastify-hook-handlers.js';
import { bindThis } from '@/decorators.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { RoleService } from '@/core/RoleService.js';
import { ReversiGameEntityService } from '@/core/entities/ReversiGameEntityService.js';
import { AnnouncementEntityService } from '@/core/entities/AnnouncementEntityService.js';
import { FeedService } from './FeedService.js';
import { UrlPreviewService } from './UrlPreviewService.js';
import { ClientLoggerService } from './ClientLoggerService.js';
@ -103,6 +116,9 @@ export class ClientServerService {
@Inject(DI.reversiGamesRepository)
private reversiGamesRepository: ReversiGamesRepository,
@Inject(DI.announcementsRepository)
private announcementsRepository: AnnouncementsRepository,
private flashEntityService: FlashEntityService,
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
@ -112,6 +128,7 @@ export class ClientServerService {
private clipEntityService: ClipEntityService,
private channelEntityService: ChannelEntityService,
private reversiGameEntityService: ReversiGameEntityService,
private announcementEntityService: AnnouncementEntityService,
private urlPreviewService: UrlPreviewService,
private feedService: FeedService,
private roleService: RoleService,
@ -776,6 +793,24 @@ export class ClientServerService {
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
//#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
- const entry = config.frontendEntry;
- const baseUrl = config.url;
doctype html
@ -32,7 +33,7 @@ html
link(rel='icon' href= icon || '/favicon.ico')
link(rel='apple-touch-icon' href= appleTouchIcon || '/apple-touch-icon.png')
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=infoImageUrl)
link(rel='prefetch' href=notFoundImageUrl)

View file

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

View file

@ -7,7 +7,7 @@
import { Test, TestingModule } from '@nestjs/testing';
import { beforeAll, describe, jest } from '@jest/globals';
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 { GlobalModule } from '@/GlobalModule.js';
import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
@ -122,7 +122,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
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 () => {
@ -131,7 +131,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
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 () => {
@ -140,7 +140,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
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 () => {
@ -149,7 +149,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
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 () => {
@ -158,7 +158,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
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 () => {
@ -167,7 +167,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
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 () => {
@ -176,7 +176,7 @@ describe('WebhookTestService', () => {
const calls = queueService.userWebhookDeliver.mock.calls[0];
expect((calls[0] as any).id).toBe('dummy-webhook');
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', () => {

View file

@ -22,23 +22,66 @@ type Account = Misskey.entities.MeDetailed & { token: string };
const accountData = miLocalStorage.getItem('account');
// 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;
/**
* Whether the current account is a moderator.
*
* @type {boolean}
*/
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;
/**
* 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() {
if ($i == null) throw new Error('signin required');
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;
/**
* Increments the number of notes by one.
*
* Documentation TODO: What about $i.notesCount? Why not increment that?
*/
export function incNotesCount() {
notesCount++;
}
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();
miLocalStorage.removeItem('account');

View file

@ -15,7 +15,7 @@ import { updateI18n, i18n } from '@/i18n.js';
import { $i, refreshAccount, login } from '@/account.js';
import { defaultStore, ColdDeviceStorage } from '@/store.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 { getUrlWithoutLoginId } from '@/scripts/login-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 => {
document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });

View file

@ -160,7 +160,7 @@ async function deleteAntenna() {
function addUser() {
os.selectUser({ includeSelf: true }).then(user => {
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();
});
}

View file

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

View file

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

View file

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

View file

@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
</MkSelect>
<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>
</div>
<div v-if="actions" :class="$style.buttons">
@ -98,7 +98,7 @@ const props = withDefaults(defineProps<{
text: string;
primary?: boolean,
danger?: boolean,
callback: (...args: any[]) => void;
callback: (...args: unknown[]) => void;
}[];
showOkButton?: boolean;
showCancelButton?: boolean;

View file

@ -157,7 +157,7 @@ const ilFilesObserver = new IntersectionObserver(
(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(sortModeSelect, () => {
@ -198,7 +198,7 @@ function onStreamDriveFolderDeleted(folderId: string) {
removeFolder(folderId);
}
function onDragover(ev: DragEvent): any {
function onDragover(ev: DragEvent) {
if (!ev.dataTransfer) return;
//
@ -243,7 +243,7 @@ function onDragleave() {
draghover.value = false;
}
function onDrop(ev: DragEvent): any {
function onDrop(ev: DragEvent) {
draghover.value = false;
if (!ev.dataTransfer) return;
@ -332,7 +332,7 @@ function createFolder() {
title: i18n.ts.createFolder,
placeholder: i18n.ts.folderName,
}).then(({ canceled, result: name }) => {
if (canceled) return;
if (canceled || name == null) return;
misskeyApi('drive/folders/create', {
name: name,
parentId: folder.value ? folder.value.id : undefined,

View file

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:scroll="false"
:withOkButton="false"
@close="cancel()"
@closed="$emit('closed')"
@closed="emit('closed')"
>
<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);
}
function nestedChosen(emoji: any, ev: MouseEvent) {
function nestedChosen(emoji: string, ev: MouseEvent) {
emit('chosen', emoji, ev);
}
</script>

View file

@ -409,7 +409,7 @@ function computeButtonTitle(ev: MouseEvent): void {
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;
if (el) {
const rect = el.getBoundingClientRect();
@ -426,7 +426,7 @@ function chosen(emoji: any, ev?: MouseEvent) {
// 使
if (!pinned.value?.includes(key)) {
let recents = defaultStore.state.recentlyUsedEmojis;
recents = recents.filter((emoji: any) => emoji !== key);
recents = recents.filter((emoji) => emoji !== key);
recents.unshift(key);
defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
}

View file

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

View file

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

View file

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

View file

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

View file

@ -227,7 +227,7 @@ const emit = defineEmits<{
}>();
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 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' {
if (mutedWords == null) return false;
if (checkWordMute(noteToCheck, $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 (mutedWords != null) {
if (checkWordMute(noteToCheck, $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 (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;
}

View file

@ -53,7 +53,7 @@ const props = withDefaults(defineProps<{
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() {
emit('done', {

View file

@ -39,7 +39,7 @@ import number from '@/filters/number.js';
import XValue from '@/components/MkObjectView.value.vue';
const props = defineProps<{
value: any;
value: unknown;
}>();
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;
}
function isArray(v): boolean {
function isArray(v: unknown): v is unknown[] {
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);
}
function collapsable(v): boolean {
function collapsable(v: unknown): boolean {
return (isObject(v) || isArray(v)) && !isEmpty(v);
}
</script>

View file

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

View file

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

View file

@ -129,25 +129,13 @@ import { miLocalStorage } from '@/local-storage.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mfmFunctionPicker } from '@/scripts/mfm-function-picker.js';
import type { PostFormProps } from '@/types/post-form.js';
const $i = signinRequired();
const modal = inject('modal');
const props = withDefaults(defineProps<{
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;
const props = withDefaults(defineProps<PostFormProps & {
fixed?: boolean;
autofocus?: boolean;
freezeAfterPosted?: boolean;
@ -955,8 +943,8 @@ function showActions(ev: MouseEvent) {
action.handler({
text: text.value,
cw: cw.value,
}, (key, value: any) => {
if (typeof key !== 'string') return;
}, (key, value) => {
if (typeof key !== 'string' || typeof value !== 'string') return;
if (key === 'text') { text.value = value; }
if (key === 'cw') { useCw.value = value !== null; cw.value = value; }
});
@ -1120,7 +1108,7 @@ defineExpose({
&:focus-visible {
outline: none;
.submitInner {
> .submitInner {
outline: 2px solid var(--MI_THEME-fgOnAccent);
outline-offset: -4px;
}
@ -1135,13 +1123,13 @@ defineExpose({
}
&: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)));
}
}
&: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)));
}
}

View file

@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<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)">
<template #item="{element}">
<template #item="{ element }">
<div
:class="$style.file"
role="button"
@ -38,14 +38,14 @@ import type { MenuItem } from '@/types/menu.js';
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
const props = defineProps<{
modelValue: any[];
modelValue: Misskey.entities.DriveFile[];
detachMediaFn?: (id: string) => void;
}>();
const mock = inject<boolean>('mock', false);
const emit = defineEmits<{
(ev: 'update:modelValue', value: any[]): void;
(ev: 'update:modelValue', value: Misskey.entities.DriveFile[]): void;
(ev: 'detach', id: string): void;
(ev: 'changeSensitive', file: Misskey.entities.DriveFile, isSensitive: boolean): 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;
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>
import { shallowRef } from 'vue';
import * as Misskey from 'misskey-js';
import MkModal from '@/components/MkModal.vue';
import MkPostForm from '@/components/MkPostForm.vue';
import type { PostFormProps } from '@/types/post-form.js';
const props = withDefaults(defineProps<{
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;
const props = withDefaults(defineProps<PostFormProps & {
instant?: boolean;
fixed?: boolean;
autofocus?: boolean;

View file

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

View file

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

View file

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

View file

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

View file

@ -28,11 +28,38 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</template>
<script lang="ts" setup>
import { } from 'vue';
<script lang="ts">
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<{
def: any[];
def: SuperMenuDef[];
grid?: boolean;
}>();
</script>

View file

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

View file

@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:okButtonDisabled="false"
:canClose="false"
@close="dialog?.close()"
@closed="$emit('closed')"
@closed="emit('closed')"
@ok="ok()"
>
<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;
});
function adjustTweetHeight(message: any) {
function adjustTweetHeight(message: MessageEvent) {
if (message.origin !== 'https://platform.twitter.com') return;
const embed = message.data?.['twttr.embed'];
if (embed?.method !== 'twttr.private.resize') return;
@ -193,14 +193,16 @@ function openPlayer(): void {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkYouTubePlayer.vue')), {
url: requestUrl.href,
}, {
// TODO
closed: () => {
dispose();
},
});
}
(window as any).addEventListener('message', adjustTweetHeight);
window.addEventListener('message', adjustTweetHeight);
onUnmounted(() => {
(window as any).removeEventListener('message', adjustTweetHeight);
window.removeEventListener('message', adjustTweetHeight);
});
</script>

View file

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
ref="dialog"
:width="400"
@close="dialog?.close()"
@closed="$emit('closed')"
@closed="emit('closed')"
>
<template v-if="announcement" #header>:{{ announcement.title }}:</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 MkRadios from '@/components/MkRadios.vue';
type AdminAnnouncementType = Misskey.entities.AdminAnnouncementsCreateRequest & { id: string; }
const props = defineProps<{
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);
@ -74,11 +81,6 @@ const icon = ref(props.announcement ? props.announcement.icon : 'info');
const display = ref(props.announcement ? props.announcement.display : 'dialog');
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() {
const params = {
title: title.value,
@ -88,7 +90,7 @@ async function done() {
display: display.value,
needConfirmationToRead: needConfirmationToRead.value,
userId: props.user.id,
};
} satisfies Misskey.entities.AdminAnnouncementsCreateRequest;
if (props.announcement) {
await os.apiWithDialog('admin/announcements/update', {

View file

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

View file

@ -16,12 +16,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { } from 'vue';
import * as Misskey from 'misskey-js';
import MkTooltip from './MkTooltip.vue';
defineProps<{
showing: boolean;
users: any[]; // TODO
users: Misskey.entities.UserLite[];
count: number;
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>
</MkSelect>
<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>
<Sortable
:modelValue="props.widgets"

View file

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

View file

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

View file

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

View file

@ -3,6 +3,9 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
/**
* A typesafe enum of keys for localStorage.
*/
export type Keys =
'v' |
'lastVersion' |
@ -44,16 +47,45 @@ export type Keys =
// セッション毎に廃棄されるLocalStorage代替セーフモードなどで使用できそう
//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 = {
/**
* 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 => {
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 => {
window.localStorage.setItem(key, value);
},
/**
* Removes an item from localStorage.
* @param {Keys} key - The key of the item to remove.
*/
removeItem: (key: Keys): void => {
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 => {
const item = miLocalStorage.getItem(key);
if (item === null) {
@ -61,6 +93,12 @@ export const miLocalStorage = {
}
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 => {
miLocalStorage.setItem(key, JSON.stringify(value));
},

View file

@ -36,6 +36,8 @@ interface RouteDefWithRedirect extends RouteDefBase {
export type RouteDef = RouteDefWithComponent | RouteDefWithRedirect;
export type RouterFlag = 'forcePage';
type ParsedPath = (string | {
name: string;
startsWith?: string;
@ -107,7 +109,7 @@ export interface IRouter extends EventEmitter<RouterEvent> {
current: Resolved;
currentRef: ShallowRef<Resolved>;
currentRoute: ShallowRef<RouteDef>;
navHook: ((path: string, flag?: any) => boolean) | null;
navHook: ((path: string, flag?: RouterFlag) => boolean) | null;
/**
* eventListenerの定義後に必ず呼び出すこと
@ -116,11 +118,11 @@ export interface IRouter extends EventEmitter<RouterEvent> {
resolve(path: string): Resolved | null;
getCurrentPath(): any;
getCurrentPath(): string;
getCurrentKey(): string;
push(path: string, flag?: any): void;
push(path: string, flag?: RouterFlag): 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 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) {
super();
@ -404,7 +406,7 @@ export class Router extends EventEmitter<RouterEvent> implements IRouter {
return this.currentKey;
}
public push(path: string, flag?: any) {
public push(path: string, flag?: RouterFlag) {
const beforePath = this.currentPath;
if (path === beforePath) {
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 { getHTMLElementOrNull } from '@/scripts/get-dom-node-or-null.js';
import { focusParent } from '@/scripts/focus.js';
import type { PostFormProps } from '@/types/post-form.js';
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,
data: P = {} as any,
data: P,
token?: string | null | undefined,
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>>(
promise: T,
onSuccess?: ((res: any) => void) | null,
onSuccess?: ((res: Awaited<T>) => void) | null,
onFailure?: ((err: Misskey.api.APIError) => void) | null,
text?: string,
): T {
@ -135,20 +136,40 @@ export function promiseDialog<T extends Promise<any>>(
return promise;
}
/**
* Counter for generating unique popup IDs.
* @type {number}
*/
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;
component: Component;
props: Record<string, any>;
events: Record<string, any>;
}[]>;
}[]>([]);
/**
* An object containing z-index values for different priority levels.
*/
const zIndexes = {
veryLow: 500000,
low: 1000000,
middle: 2000000,
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 {
zIndexes[priority] += 100;
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];
};
/**
* 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>(
component: T,
props: ComponentProps<T>,
@ -183,13 +213,18 @@ export function popup<T extends Component>(
): { dispose: () => void } {
markRaw(component);
// Generate a unique ID for this popup.
const id = ++popupIdCount;
// On disposal, remove this popup from the list of open popups.
const dispose = () => {
// このsetTimeoutが無いと挙動がおかしくなる(autocompleteが閉じなくなる)。Vueのバグ
window.setTimeout(() => {
popups.value = popups.value.filter(p => p.id !== id);
}, 0);
};
// Bundle the component, props, and events into a state object.
const state = {
component,
props,
@ -197,13 +232,19 @@ export function popup<T extends Component>(
id,
};
// Add the popup to the list of open popups.
popups.value.push(state);
// Return a function that can be called to close the popup.
return {
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) {
const { dispose } = popup(MkPageWindow, {
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) {
const { dispose } = popup(MkToast, {
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: {
type?: 'error' | 'info' | 'success' | 'warning' | 'waiting' | 'question';
title?: string;
@ -458,7 +513,7 @@ type SelectItem<C> = {
};
// default が指定されていたら result は null になり得ないことを保証する overload function
export function select<C = any>(props: {
export function select<C = unknown>(props: {
title?: string;
text?: string;
default: string;
@ -471,7 +526,7 @@ export function select<C = any>(props: {
} | {
canceled: false; result: C;
}>;
export function select<C = any>(props: {
export function select<C = unknown>(props: {
title?: string;
text?: string;
default?: string | null;
@ -484,7 +539,7 @@ export function select<C = any>(props: {
} | {
canceled: false; result: C | null;
}>;
export function select<C = any>(props: {
export function select<C = unknown>(props: {
title?: string;
text?: string;
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({
openOnRemote: (props.initialText || props.initialNote ? {
type: 'share',
params: {
text: props.initialText ?? props.initialNote.text,
visibility: props.initialVisibility ?? props.initialNote?.visibility,
text: props.initialText ?? props.initialNote?.text ?? '',
visibility: props.initialVisibility ?? props.initialNote?.visibility ?? 'public',
localOnly: (props.initialLocalOnly || props.initialNote?.localOnly) ? '1' : '0',
},
} : undefined),

View file

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

View file

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

View file

@ -103,7 +103,7 @@ const headerActions = computed(() => []);
const headerTabs = computed(() => []);
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',
}));
</script>

View file

@ -62,7 +62,7 @@ function accepted() {
state.value = 'accepted';
if (session.value && 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}`;
}
}

View file

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

View file

@ -9,8 +9,8 @@ SPDX-License-Identifier: AGPL-3.0-only
:initialWidth="400"
:initialHeight="500"
:canResize="true"
@close="windowEl.close()"
@closed="$emit('closed')"
@close="windowEl?.close()"
@closed="emit('closed')"
>
<template v-if="emoji" #header>:{{ emoji.name }}:</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';
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 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 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 localOnly = ref(props.emoji ? props.emoji.localOnly : false);
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 emit = defineEmits<{
(ev: 'done', v: { deleted?: boolean; updated?: any; created?: any }): void,
(ev: 'closed'): void
}>();
async function changeImage(ev) {
async function changeImage(ev: Event) {
file.value = await selectFile(ev.currentTarget ?? ev.target, null);
const candidate = file.value.name.replace(/\.(.+)$/, '');
if (candidate.match(/^[a-z0-9_]+$/)) {
@ -140,7 +140,7 @@ async function addRole() {
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);
}
@ -172,7 +172,7 @@ async function done() {
},
});
windowEl.value.close();
windowEl.value?.close();
} else {
const created = await os.apiWithDialog('admin/emoji/add', params);
@ -180,11 +180,12 @@ async function done() {
created: created,
});
windowEl.value.close();
windowEl.value?.close();
}
}
async function del() {
if (!props.emoji) return;
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.removeAreYouSure({ x: name.value }),
@ -197,7 +198,7 @@ async function del() {
emit('done', {
deleted: true,
});
windowEl.value.close();
windowEl.value?.close();
});
}
</script>

View file

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

View file

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

View file

@ -65,7 +65,7 @@ async function onAccept(token: string) {
if (props.callback && 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);
location.href = cbUrl.toString();
} else {

View file

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

View file

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

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- 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 #func>
<button @click="choose()">
@ -30,11 +30,12 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
modelValue: any
modelValue: Misskey.entities.PageBlock & { type: 'image' };
}>();
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);

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- 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>
<section style="padding: 16px;" class="_gaps_s">
@ -34,19 +34,24 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
const props = defineProps<{
modelValue: any
modelValue: Misskey.entities.PageBlock & { type: 'note' };
}>();
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);
watch(id, async () => {
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', {

View file

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- 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 #func>
<button class="_button" @click="rename()">
@ -21,8 +21,9 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
/* eslint-disable vue/no-mutating-props */
import { defineAsyncComponent, inject, onMounted, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
import { v4 as uuid } from 'uuid';
import XContainer from '../page-editor.container.vue';
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 props = withDefaults(defineProps<{
modelValue: any,
}>(), {
modelValue: {},
});
const props = defineProps<{
modelValue: Misskey.entities.PageBlock & { type: 'section'; },
}>();
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 ?? []));

View file

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

View file

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<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}">
<div :class="$style.item">
<!-- 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 keys = ref<any>(null);
const keys = ref<[string, string][]>([]);
function fetchKeys() {
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<{
game: Misskey.entities.ReversiGameDetailed;
connection: Misskey.ChannelConnection;
connection: Misskey.ChannelConnection<Misskey.Channels['reversiGame']>;
}>();
const shareWhenStart = defineModel<boolean>('shareWhenStart', { default: false });
@ -217,14 +217,14 @@ function onChangeReadyStates(states) {
game.value.user2Ready = states.user2;
}
function updateSettings(key: keyof Misskey.entities.ReversiGameDetailed) {
function updateSettings(key: typeof Misskey.reversiUpdateKeys[number]) {
props.connection.send('updateSettings', {
key: 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 (game.value[key] === value) return;
game.value[key] = value;

View file

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

View file

@ -138,12 +138,13 @@ const token = ref<string | number | null>(null);
const backupCodes = ref<string[]>();
function cancel() {
dialog.value.close();
dialog.value?.close();
}
async function tokenDone() {
if (token.value == null) return;
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;
@ -166,7 +167,7 @@ function downloadBackupCodes() {
}
function allDone() {
dialog.value.close();
dialog.value?.close();
}
</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 token = fetchedAccounts.find(x => x.id === account.id)!.token;
switchAccountWithToken(token);

View file

@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
import * as Misskey from 'misskey-js';
import FormPagination from '@/components/MkPagination.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
@ -77,7 +78,7 @@ const pagination = {
function revoke(token) {
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() {
dialog.value.close();
dialog.value?.close();
}
async function update() {
@ -120,7 +120,7 @@ async function update() {
offsetX: offsetX.value,
offsetY: offsetY.value,
});
dialog.value.close();
dialog.value?.close();
}
async function attach() {
@ -130,12 +130,12 @@ async function attach() {
offsetX: offsetX.value,
offsetY: offsetY.value,
});
dialog.value.close();
dialog.value?.close();
}
async function detach() {
emit('detach');
dialog.value.close();
dialog.value?.close();
}
</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>
</div>
<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>
</div>
<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_reply">{{ i18n.ts._webhookSettings._events.reply }}</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>
</div>
</FormSection>

View file

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

View file

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

View file

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

View file

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

View file

@ -4,7 +4,7 @@
*/
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';
@ -79,7 +79,7 @@ class MainRouterProxy implements IRouter {
return this.supplier().currentRoute;
}
get navHook(): ((path: string, flag?: any) => boolean) | null {
get navHook(): ((path: string, flag?: RouterFlag) => boolean) | null {
return this.supplier().navHook;
}
@ -91,11 +91,11 @@ class MainRouterProxy implements IRouter {
return this.supplier().getCurrentKey();
}
getCurrentPath(): any {
getCurrentPath(): string {
return this.supplier().getCurrentPath();
}
push(path: string, flag?: any): void {
push(path: string, flag?: RouterFlag): void {
this.supplier().push(path, flag);
}

View file

@ -2,8 +2,9 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* 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;

View file

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

View file

@ -15,7 +15,7 @@ type Hidden = boolean | ((v: any) => boolean);
export type FormItem = {
label?: string;
type: 'string';
default: string | null;
default?: string | null;
description?: string;
required?: boolean;
hidden?: Hidden;
@ -24,7 +24,7 @@ export type FormItem = {
} | {
label?: string;
type: 'number';
default: number | null;
default?: number | null;
description?: string;
required?: boolean;
hidden?: Hidden;
@ -32,20 +32,20 @@ export type FormItem = {
} | {
label?: string;
type: 'boolean';
default: boolean | null;
default?: boolean | null;
description?: string;
hidden?: Hidden;
} | {
label?: string;
type: 'enum';
default: string | null;
default?: string | null;
required?: boolean;
hidden?: Hidden;
enum: EnumItem[];
} | {
label?: string;
type: 'radio';
default: unknown | null;
default?: unknown | null;
required?: boolean;
hidden?: Hidden;
options: {
@ -55,7 +55,7 @@ export type FormItem = {
} | {
label?: string;
type: 'range';
default: number | null;
default?: number | null;
description?: string;
required?: boolean;
step?: number;
@ -66,12 +66,12 @@ export type FormItem = {
} | {
label?: string;
type: 'object';
default: Record<string, unknown> | null;
default?: Record<string, unknown> | null;
hidden: Hidden;
} | {
label?: string;
type: 'array';
default: unknown[] | null;
default?: unknown[] | null;
hidden: Hidden;
} | {
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.');
}
// Check for the availability of indexedDB.
if (idbAvailable) {
await iset('idb-test', 'test')
.catch(err => {
@ -37,16 +38,36 @@ if (idbAvailable) {
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) {
if (idbAvailable) return iget(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) {
if (idbAvailable) return iset(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) {
if (idbAvailable) return idel(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,
>(
endpoint: E,
data: P = {} as any,
data: P & { i?: string | null; } = {} as any,
token?: string | null | undefined,
signal?: AbortSignal,
): Promise<_ResT> {
@ -30,8 +30,8 @@ export function misskeyApi<
const promise = new Promise<_ResT>((resolve, reject) => {
// Append a credential
if ($i) (data as any).i = $i.token;
if (token !== undefined) (data as any).i = token;
if ($i) data.i = $i.token;
if (token !== undefined) data.i = token;
// Send request
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) => {
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]);
}
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);
}

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