Compare commits

..

18 Commits

Author SHA1 Message Date
01a804cd9d
enhance: block indicator
All checks were successful
Publish Docker Image (Misskey) TeamCity build finished
2024-10-20 22:08:54 +09:00
2dc84bafa2
fix(user-menu): confirm text i18n 2024-10-20 21:34:19 +09:00
2e38ad3d90
fix(MkRemoteCaution): apply info styles 2024-10-20 21:22:10 +09:00
2f6f1a7158
Merge upstream 2024-10-20 21:15:43 +09:00
b2ec0852e6
enhance(user): remove user suspended indicator and raw page 2024-10-20 21:04:27 +09:00
あわわわとーにゅ
046eab7496
fix(frontend/reactions): ローカルのカスタム絵文字のミュートが正常に機能しない問題を修正 (MisskeyIO#765)
MisskeyIO#762
2024-10-20 12:41:36 +09:00
あわわわとーにゅ
4d928915f6
i18n: MisskeyIO#758 MisskeyIO#762 (MisskeyIO#763) 2024-10-20 11:34:46 +09:00
まっちゃてぃー。
75b424192e
feat(frontend/reactions): リモートのリアクションのミュートの方法を変えた (MisskeyIO#762)
Co-authored-by: kabo2468 <28654659+kabo2468@users.noreply.github.com>
2024-10-20 11:19:33 +09:00
あわわわとーにゅ
762cc679a9
Bump up version to 2024.5.0-io.3b (MisskeyIO#760) 2024-10-20 07:33:45 +09:00
あわわわとーにゅ
8f66f9ca59
enhance(SSO): ユーザーに対話型プロンプトが表示されないように設定できるように (MisskeyIO#759) 2024-10-20 07:33:27 +09:00
まっちゃてぃー。
a73a09a999
feat(frontend/reactions): リアクションミュート機能を追加しました (MisskeyIO#758) 2024-10-20 05:31:35 +09:00
まっちゃてぃー。
65f0138bd7
enhance(frontend): 絵文字のフォールバック先を文字じゃなくて画像に (MisskeyIO#757) 2024-10-19 23:57:29 +09:00
あわわわとーにゅ
61b758e3dc
Bump up version to 2024.5.0-io.3a (MisskeyIO#755) 2024-10-19 03:33:37 +09:00
あわわわとーにゅ
edeca403e1
spec(frontend): アカウント新規作成時のOOBE向上 (MisskeyIO#754)
寄付のポップアップと重なってglitch感が出るので削除
2024-10-19 03:33:17 +09:00
あわわわとーにゅ
d005daae84
feat(announcement): お知らせの既読処理にチュートリアル受講を要求できるように (MisskeyIO#753) 2024-10-19 03:22:05 +09:00
riku6460
c47140eab7
enhance(queue): inbox と relationship にも MisskeyIO#745 を適用 (MisskeyIO#752) 2024-10-18 23:06:33 +09:00
riku6460
a8bbccbefa
spec(frontend): Bull Dashboard に relationship queue を追加 (MisskeyIO#751) 2024-10-18 22:37:04 +09:00
riku6460
3fdcf99011
perf(backend): queue の delayed の件数が増えた際に deliver-delayed と inbox-delayed が返ってこなくなる問題を修正 (MisskeyIO#750) 2024-10-18 22:35:33 +09:00
53 changed files with 435 additions and 150 deletions

View File

@ -1234,7 +1234,7 @@ _announcement:
forExistingUsers: "Anunci per usuaris registrats" forExistingUsers: "Anunci per usuaris registrats"
forExistingUsersDescription: "Aquest avís només es mostrarà als usuaris existents fins al moment de la publicació. Si no també es mostrarà als usuaris que es registrin després de la publicació." forExistingUsersDescription: "Aquest avís només es mostrarà als usuaris existents fins al moment de la publicació. Si no també es mostrarà als usuaris que es registrin després de la publicació."
needConfirmationToRead: "Es necessita confirmació de lectura de la notificació " needConfirmationToRead: "Es necessita confirmació de lectura de la notificació "
needConfirmationToReadDescription: "Si s'activa es mostrarà un diàleg per confirmar la lectura d'aquesta notificació. A més aquesta notificació serà exclosa de qualsevol funcionalitat com \"Marcar tot com a llegit\"." needConfirmationToReadDescription: "Si s'activa es mostrarà un diàleg per confirmar la lectura d'aquesta notificació."
end: "Final de la notificació " end: "Final de la notificació "
tooManyActiveAnnouncementDescription: "Tenir massa notificacions actives pot empitjorar l'experiència de l'usuari. Considera finalitzar els anuncis que siguin antics." tooManyActiveAnnouncementDescription: "Tenir massa notificacions actives pot empitjorar l'experiència de l'usuari. Considera finalitzar els anuncis que siguin antics."
readConfirmTitle: "Marcar com llegida?" readConfirmTitle: "Marcar com llegida?"

View File

@ -1189,7 +1189,7 @@ _announcement:
forExistingUsers: "Nur für existierende Nutzer" forExistingUsers: "Nur für existierende Nutzer"
forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt." forExistingUsersDescription: "Ist diese Option aktiviert, wird diese Ankündigung nur Nutzern angezeigt, die zum Zeitpunkt der Ankündigung bereits registriert sind. Ist sie deaktiviert, wird sie auch Nutzern, die sich nach dessen Veröffentlichung registrieren, angezeigt."
needConfirmationToRead: "Separate Lesebestätigung erfordern" needConfirmationToRead: "Separate Lesebestätigung erfordern"
needConfirmationToReadDescription: "Ist dies aktiviert, so wird beim Markieren dieser Ankündigung als gelesen ein separates Bestätigungsfenster angezeigt. Auch wird sie von der \"Alle als gelesen markieren\"-Funktion ausgenommen." needConfirmationToReadDescription: "Ist dies aktiviert, so wird beim Markieren dieser Ankündigung als gelesen ein separates Bestätigungsfenster angezeigt."
end: "Ankündigung archivieren" end: "Ankündigung archivieren"
tooManyActiveAnnouncementDescription: "Zu viele aktive Ankündigungen können die Benutzerfreundlichkeit verschlechtern. Es wird empfohlen, veraltete Ankündigungen zu archivieren." tooManyActiveAnnouncementDescription: "Zu viele aktive Ankündigungen können die Benutzerfreundlichkeit verschlechtern. Es wird empfohlen, veraltete Ankündigungen zu archivieren."
readConfirmTitle: "Als gelesen markieren?" readConfirmTitle: "Als gelesen markieren?"

View File

@ -138,6 +138,9 @@ mute: "Mute"
unmute: "Unmute" unmute: "Unmute"
renoteMute: "Mute Renotes" renoteMute: "Mute Renotes"
renoteUnmute: "Unmute Renotes" renoteUnmute: "Unmute Renotes"
mutedReactions: "Mute reactions"
muteThisReaction: "Mute this reaction"
unmuteThisReaction: "Unmute this reaction"
block: "Block" block: "Block"
unblock: "Unblock" unblock: "Unblock"
suspend: "Suspend" suspend: "Suspend"
@ -1333,7 +1336,9 @@ _announcement:
forExistingUsers: "Existing users only" forExistingUsers: "Existing users only"
forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it." forExistingUsersDescription: "This announcement will only be shown to users existing at the point of publishment if enabled. If disabled, those newly signing up after it has been posted will also see it."
needConfirmationToRead: "Require separate read confirmation" needConfirmationToRead: "Require separate read confirmation"
needConfirmationToReadDescription: "A separate prompt to confirm marking this announcement as read will be displayed if enabled. This announcement will also be excluded from any \"Mark all as read\" functionality." needConfirmationToReadDescription: "A separate prompt to confirm marking this announcement as read will be displayed if enabled."
needEnrollmentTutorialToRead: "Require tutorial completion to read"
needEnrollmentTutorialToReadDescription: "Users must complete the tutorial to read this announcement if enabled."
end: "Archive announcement" end: "Archive announcement"
tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete." tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete."
readConfirmTitle: "Mark as read?" readConfirmTitle: "Mark as read?"

View File

@ -1252,7 +1252,7 @@ _announcement:
forExistingUsers: "Solo para usuarios registrados" forExistingUsers: "Solo para usuarios registrados"
forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán." forExistingUsersDescription: "Este anuncio solo se mostrará a aquellos usuarios registrados en el momento de su publicación. Si se deshabilita esta opción, aquellos usuarios que se registren tras su publicación también lo verán."
needConfirmationToRead: "Requerir confirmación de lectura aparte" needConfirmationToRead: "Requerir confirmación de lectura aparte"
needConfirmationToReadDescription: "Si se habilita esta opción, se pedirá una confirmación de lectura aparte. Además, este anuncio será excluido de cualquier funcionalidad de \"Marcar todos como leídos\"." needConfirmationToReadDescription: "Si se habilita esta opción, se pedirá una confirmación de lectura aparte."
end: "Anuncios archivados" end: "Anuncios archivados"
tooManyActiveAnnouncementDescription: "Tener demasiados anuncios activos empeora la experiencia de usuario. Por favor, considera archivar aquellos anuncios que hayan quedado obsoletos." tooManyActiveAnnouncementDescription: "Tener demasiados anuncios activos empeora la experiencia de usuario. Por favor, considera archivar aquellos anuncios que hayan quedado obsoletos."
readConfirmTitle: "¿Marcar como leído?" readConfirmTitle: "¿Marcar como leído?"

View File

@ -1237,7 +1237,7 @@ _bubbleGame:
_announcement: _announcement:
forExistingUsers: "Pour les utilisateurs existants seulement" forExistingUsers: "Pour les utilisateurs existants seulement"
needConfirmationToRead: "Exiger la confirmation de la lecture" needConfirmationToRead: "Exiger la confirmation de la lecture"
needConfirmationToReadDescription: "Si activé, afficher un dialogue de confirmation quand l'annonce est marquée comme lue. Aussi, elle sera exclue de « marquer tout comme lu » ." needConfirmationToReadDescription: "Si activé, afficher un dialogue de confirmation quand l'annonce est marquée comme lue."
end: "Archiver l'annonce" end: "Archiver l'annonce"
tooManyActiveAnnouncementDescription: "Un grand nombre d'annonces actives peut baisser l'expérience utilisateur. Considérez d'archiver les annonces obsolètes." tooManyActiveAnnouncementDescription: "Un grand nombre d'annonces actives peut baisser l'expérience utilisateur. Considérez d'archiver les annonces obsolètes."
readConfirmTitle: "Marquer comme lu ?" readConfirmTitle: "Marquer comme lu ?"

View File

@ -1254,7 +1254,7 @@ _announcement:
forExistingUsers: "Hanya pengguna yang telah ada" forExistingUsers: "Hanya pengguna yang telah ada"
forExistingUsersDescription: "Pengumuman ini akan dimunculkan ke pengguna yang sudah ada dari titik waktu publikasi jika dinyalakan. Apabila dimatikan, mereka yang baru mendaftar setelah publikasi ini akan juga melihatnya." forExistingUsersDescription: "Pengumuman ini akan dimunculkan ke pengguna yang sudah ada dari titik waktu publikasi jika dinyalakan. Apabila dimatikan, mereka yang baru mendaftar setelah publikasi ini akan juga melihatnya."
needConfirmationToRead: "Membutuhkan konfirmasi terpisah bahwa telah dibaca" needConfirmationToRead: "Membutuhkan konfirmasi terpisah bahwa telah dibaca"
needConfirmationToReadDescription: "Permintaan terpisah untuk mengonfirmasi menandai pengumuman ini telah dibaca akan ditampilkan apabila fitur ini dinyalakan. Pengumuman ini juga akan dikecualikan dari fungsi \"Tandai semua telah dibaca\"." needConfirmationToReadDescription: "Permintaan terpisah untuk mengonfirmasi menandai pengumuman ini telah dibaca akan ditampilkan apabila fitur ini dinyalakan."
end: "Arsipkan pengumuman" end: "Arsipkan pengumuman"
tooManyActiveAnnouncementDescription: "Terlalu banyak pengumuman dapat memperburuk pengalaman pengguna. Mohon pertimbangkan untuk mengarsipkan pengumuman yang sudah usang/tidak relevan." tooManyActiveAnnouncementDescription: "Terlalu banyak pengumuman dapat memperburuk pengalaman pengguna. Mohon pertimbangkan untuk mengarsipkan pengumuman yang sudah usang/tidak relevan."
readConfirmTitle: "Tandai telah dibaca?" readConfirmTitle: "Tandai telah dibaca?"

30
locales/index.d.ts vendored
View File

@ -360,6 +360,14 @@ export interface Locale extends ILocale {
* *
*/ */
"followsYou": string; "followsYou": string;
/**
*
*/
"youAreBlocking": string;
/**
*
*/
"youAreBlocked": string;
/** /**
* *
*/ */
@ -580,6 +588,18 @@ export interface Locale extends ILocale {
* *
*/ */
"renoteUnmute": string; "renoteUnmute": string;
/**
*
*/
"mutedReactions": string;
/**
*
*/
"muteThisReaction": string;
/**
*
*/
"unmuteThisReaction": string;
/** /**
* *
*/ */
@ -5445,9 +5465,17 @@ export interface Locale extends ILocale {
*/ */
"needConfirmationToRead": string; "needConfirmationToRead": string;
/** /**
* *
*/ */
"needConfirmationToReadDescription": string; "needConfirmationToReadDescription": string;
/**
*
*/
"needEnrollmentTutorialToRead": string;
/**
*
*/
"needEnrollmentTutorialToReadDescription": string;
/** /**
* *
*/ */

View File

@ -1252,7 +1252,7 @@ _announcement:
forExistingUsers: "Solo ai profili attuali" forExistingUsers: "Solo ai profili attuali"
forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio."
needConfirmationToRead: "Richiede la conferma di lettura" needConfirmationToRead: "Richiede la conferma di lettura"
needConfirmationToReadDescription: "Sarà visualizzata una finestra di dialogo che richiede la conferma di lettura. Inoltre, non è soggetto a conferme di lettura massicce." needConfirmationToReadDescription: "Sarà visualizzata una finestra di dialogo che richiede la conferma di lettura."
end: "Archivia l'annuncio" end: "Archivia l'annuncio"
tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi." tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi."
readConfirmTitle: "Segnare come già letto?" readConfirmTitle: "Segnare come già letto?"

View File

@ -86,6 +86,8 @@ notes: "ノート"
following: "フォロー" following: "フォロー"
followers: "フォロワー" followers: "フォロワー"
followsYou: "フォローされています" followsYou: "フォローされています"
youAreBlocking: "ブロックしています"
youAreBlocked: "ブロックされています"
createList: "リスト作成" createList: "リスト作成"
manageLists: "リストの管理" manageLists: "リストの管理"
error: "エラー" error: "エラー"
@ -141,6 +143,9 @@ mute: "ミュート"
unmute: "ミュート解除" unmute: "ミュート解除"
renoteMute: "リノートをミュート" renoteMute: "リノートをミュート"
renoteUnmute: "リノートのミュートを解除" renoteUnmute: "リノートのミュートを解除"
mutedReactions: "リアクションのミュート"
muteThisReaction: "このリアクションをミュートする"
unmuteThisReaction: "このリアクションのミュートを解除する"
block: "ブロック" block: "ブロック"
unblock: "ブロック解除" unblock: "ブロック解除"
suspend: "凍結" suspend: "凍結"
@ -1361,7 +1366,9 @@ _announcement:
forExistingUsers: "既存ユーザーのみ" forExistingUsers: "既存ユーザーのみ"
forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。" forExistingUsersDescription: "有効にすると、このお知らせ作成時点で存在するユーザーにのみお知らせが表示されます。無効にすると、このお知らせ作成後にアカウントを作成したユーザーにもお知らせが表示されます。"
needConfirmationToRead: "既読にするのに確認が必要" needConfirmationToRead: "既読にするのに確認が必要"
needConfirmationToReadDescription: "有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。また、一括既読操作の対象になりません。" needConfirmationToReadDescription: "有効にすると、このお知らせを既読にする際に確認ダイアログが表示されます。"
needEnrollmentTutorialToRead: "チュートリアルの受講が必要"
needEnrollmentTutorialToReadDescription: "有効にすると、このお知らせを既読にするためにはチュートリアルの受講が必要です。"
end: "お知らせを終了" end: "お知らせを終了"
tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。" tooManyActiveAnnouncementDescription: "アクティブなお知らせが多いため、UXが低下する可能性があります。終了したお知らせはアーカイブすることを検討してください。"
readConfirmTitle: "既読にしますか?" readConfirmTitle: "既読にしますか?"

View File

@ -1254,7 +1254,7 @@ _announcement:
forExistingUsers: "もうおるユーザーのみ" forExistingUsers: "もうおるユーザーのみ"
forExistingUsersDescription: "オンにしたらこのお知らせができた時点でおる人らにだけお知らせが行くで。切ったらこの知らせが行ったあとにアカウント作った人にもちゃんとお知らせが行くで。" forExistingUsersDescription: "オンにしたらこのお知らせができた時点でおる人らにだけお知らせが行くで。切ったらこの知らせが行ったあとにアカウント作った人にもちゃんとお知らせが行くで。"
needConfirmationToRead: "既読にするんやったら確認してや" needConfirmationToRead: "既読にするんやったら確認してや"
needConfirmationToReadDescription: "オンにしたら、このお知らせを既読にする時に確認するで。ついでに、一括既読しても既読扱いにならへんで。" needConfirmationToReadDescription: "オンにしたら、このお知らせを既読にする時に確認するで。"
end: "お知らせやめる" end: "お知らせやめる"
tooManyActiveAnnouncementDescription: "お知らせが多すぎてUXが落ちそうや。終わったお知らせはアーカイブに突っ込んだほうがええかも。" tooManyActiveAnnouncementDescription: "お知らせが多すぎてUXが落ちそうや。終わったお知らせはアーカイブに突っ込んだほうがええかも。"
readConfirmTitle: "既読にしてええんやな?" readConfirmTitle: "既読にしてええんやな?"

View File

@ -85,7 +85,9 @@ note: "노트"
notes: "노트" notes: "노트"
following: "팔로잉" following: "팔로잉"
followers: "팔로워" followers: "팔로워"
followsYou: "나를 팔로우 합니다" followsYou: "나를 팔로우하는 중"
youAreBlocking: "내가 차단한 유저"
youAreBlocked: "나를 차단하는 중"
createList: "리스트 만들기" createList: "리스트 만들기"
manageLists: "리스트 관리" manageLists: "리스트 관리"
error: "오류" error: "오류"
@ -138,6 +140,9 @@ mute: "뮤트"
unmute: "뮤트 해제" unmute: "뮤트 해제"
renoteMute: "리노트 뮤트하기" renoteMute: "리노트 뮤트하기"
renoteUnmute: "리노트 뮤트 해제" renoteUnmute: "리노트 뮤트 해제"
mutedReactions: "리액션 뮤트"
muteThisReaction: "이 리액션을 뮤트하기"
unmuteThisReaction: "이 리액션의 뮤트를 해제하기"
block: "차단" block: "차단"
unblock: "차단 해제" unblock: "차단 해제"
suspend: "정지" suspend: "정지"
@ -1348,7 +1353,9 @@ _announcement:
forExistingUsers: "기존 유저에게만 알림" forExistingUsers: "기존 유저에게만 알림"
forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다." forExistingUsersDescription: "활성화하면 이 공지사항을 게시한 시점에서 이미 가입한 유저에게만 표시합니다. 비활성화하면 게시 후에 가입한 유저에게도 표시합니다."
needConfirmationToRead: "읽음으로 표시하기 전에 확인하기" needConfirmationToRead: "읽음으로 표시하기 전에 확인하기"
needConfirmationToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 확인 알림창을 띄웁니다. '모두 읽음'의 대상에서도 제외됩니다." needConfirmationToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 확인 알림창을 띄웁니다."
needEnrollmentTutorialToRead: "읽음으로 표시하기 전에 튜토리얼 진행하기"
needEnrollmentTutorialToReadDescription: "활성화하면 이 공지사항을 읽음으로 표시하기 전에 사용자에게 튜토리얼을 진행하도록 요구합니다."
end: "공지에서 내리기" end: "공지에서 내리기"
tooManyActiveAnnouncementDescription: "공지사항이 너무 많을 경우, 유저 경험에 영향을 끼칠 가능성이 있습니다. 오래된 공지사항은 아카이브하시는 것을 권장드립니다." tooManyActiveAnnouncementDescription: "공지사항이 너무 많을 경우, 유저 경험에 영향을 끼칠 가능성이 있습니다. 오래된 공지사항은 아카이브하시는 것을 권장드립니다."
readConfirmTitle: "읽음으로 표시합니까?" readConfirmTitle: "읽음으로 표시합니까?"

View File

@ -1254,7 +1254,7 @@ _announcement:
forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น" forExistingUsers: "ผู้ใช้งานที่มีอยู่เท่านั้น"
forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน" forExistingUsersDescription: "การประกาศนี้จะแสดงต่อผู้ใช้ที่มีอยู่ ณ จุดที่เผยแพร่นั้นๆถ้าหากเปิดใช้งาน ถ้าหากปิดใช้งานผู้ที่กำลังสมัครใหม่หลังจากโพสต์แล้วนั้นก็จะเห็นเช่นกัน"
needConfirmationToRead: "จำเป็นต้องยืนยันว่าอ่านแล้ว" needConfirmationToRead: "จำเป็นต้องยืนยันว่าอ่านแล้ว"
needConfirmationToReadDescription: "กล่องโต้ตอบการยืนยันจะปรากฏขึ้นเมื่อจะทำเครื่องหมายว่าอ่านแล้ว นอกจากนี้ยังทำให้ประกาศนี้ยังไม่ถูกอ่านเมื่อใช้ฟังก์ชั่น “ทำเครื่องหมายฯ ทั้งหมดว่าอ่านแล้ว”" needConfirmationToReadDescription: "กล่องโต้ตอบการยืนยันจะปรากฏขึ้นเมื่อจะทำเครื่องหมายว่าอ่านแล้ว"
end: "เก็บประกาศ" end: "เก็บประกาศ"
tooManyActiveAnnouncementDescription: "การมีประกาศที่ใช้งานมากเกินไปนั้นอาจจะทำให้ประสบการณ์ของผู้ใช้งานนั้นดูแย่ลง โปรดกรุณาพิจารณาการเก็บประกาศที่ล้าสมัยด้วยนะค่ะ" tooManyActiveAnnouncementDescription: "การมีประกาศที่ใช้งานมากเกินไปนั้นอาจจะทำให้ประสบการณ์ของผู้ใช้งานนั้นดูแย่ลง โปรดกรุณาพิจารณาการเก็บประกาศที่ล้าสมัยด้วยนะค่ะ"
readConfirmTitle: "ทำเครื่องหมายว่าอ่านแล้วเลยไหม?" readConfirmTitle: "ทำเครื่องหมายว่าอ่านแล้วเลยไหม?"

View File

@ -1275,7 +1275,7 @@ _announcement:
forExistingUsers: "仅限现有用户" forExistingUsers: "仅限现有用户"
forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。" forExistingUsersDescription: "若启用,该公告将仅对创建此公告时存在的用户可见。 如果禁用,则在创建此公告后注册的用户也可以看到该公告。"
needConfirmationToRead: "需要确认才能标记为已读" needConfirmationToRead: "需要确认才能标记为已读"
needConfirmationToReadDescription: "若启用,则会在标记已读时会显示确认对话框。此外,它也会不受批量已读操作的影响。" needConfirmationToReadDescription: "若启用,则会在标记已读时会显示确认对话框。"
end: "结束公告" end: "结束公告"
tooManyActiveAnnouncementDescription: "若有大量活动公告,可能会造成用户体验下降。请考虑归档已完成的公告。" tooManyActiveAnnouncementDescription: "若有大量活动公告,可能会造成用户体验下降。请考虑归档已完成的公告。"
readConfirmTitle: "标记为已读?" readConfirmTitle: "标记为已读?"

View File

@ -1254,7 +1254,7 @@ _announcement:
forExistingUsers: "僅限既有的使用者" forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。" forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
needConfirmationToRead: "必須確認才能標記為已讀" needConfirmationToRead: "必須確認才能標記為已讀"
needConfirmationToReadDescription: "啟用代表此公告將顯示對話方塊以確認是否標記為已讀,同時不會受「標記所有公告為已讀」功能影響。" needConfirmationToReadDescription: "啟用代表此公告將顯示對話方塊以確認是否標記為已讀。"
end: "結束公告" end: "結束公告"
tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。" tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。"
readConfirmTitle: "標記為已讀嗎?" readConfirmTitle: "標記為已讀嗎?"

View File

@ -0,0 +1,11 @@
export class AnnouncementNeedEnrollmentTutorialToRead1729270181417 {
name = 'AnnouncementNeedEnrollmentTutorialToRead1729270181417'
async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "announcement" ADD "needEnrollmentTutorialToRead" boolean NOT NULL DEFAULT false`);
}
async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "announcement" DROP COLUMN "needEnrollmentTutorialToRead"`);
}
}

View File

@ -51,9 +51,9 @@ type Source = {
redisForSystemQueue?: RedisOptionsSource; redisForSystemQueue?: RedisOptionsSource;
redisForEndedPollNotificationQueue?: RedisOptionsSource; redisForEndedPollNotificationQueue?: RedisOptionsSource;
redisForDeliverQueues?: Array<RedisOptionsSource>; redisForDeliverQueues?: Array<RedisOptionsSource>;
redisForInboxQueue?: RedisOptionsSource; redisForInboxQueues?: Array<RedisOptionsSource>;
redisForDbQueue?: RedisOptionsSource; redisForDbQueue?: RedisOptionsSource;
redisForRelationshipQueue?: RedisOptionsSource; redisForRelationshipQueues?: Array<RedisOptionsSource>;
redisForObjectStorageQueue?: RedisOptionsSource; redisForObjectStorageQueue?: RedisOptionsSource;
redisForWebhookDeliverQueue?: RedisOptionsSource; redisForWebhookDeliverQueue?: RedisOptionsSource;
redisForTimelines?: RedisOptionsSource; redisForTimelines?: RedisOptionsSource;
@ -221,9 +221,9 @@ export type Config = {
redisForSystemQueue: RedisOptions & RedisOptionsSource; redisForSystemQueue: RedisOptions & RedisOptionsSource;
redisForEndedPollNotificationQueue: RedisOptions & RedisOptionsSource; redisForEndedPollNotificationQueue: RedisOptions & RedisOptionsSource;
redisForDeliverQueues: Array<RedisOptions & RedisOptionsSource>; redisForDeliverQueues: Array<RedisOptions & RedisOptionsSource>;
redisForInboxQueue: RedisOptions & RedisOptionsSource; redisForInboxQueues: Array<RedisOptions & RedisOptionsSource>;
redisForDbQueue: RedisOptions & RedisOptionsSource; redisForDbQueue: RedisOptions & RedisOptionsSource;
redisForRelationshipQueue: RedisOptions & RedisOptionsSource; redisForRelationshipQueues: Array<RedisOptions & RedisOptionsSource>;
redisForObjectStorageQueue: RedisOptions & RedisOptionsSource; redisForObjectStorageQueue: RedisOptions & RedisOptionsSource;
redisForWebhookDeliverQueue: RedisOptions & RedisOptionsSource; redisForWebhookDeliverQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource; redisForTimelines: RedisOptions & RedisOptionsSource;
@ -297,9 +297,9 @@ export function loadConfig(): Config {
redisForSystemQueue: config.redisForSystemQueue ? convertRedisOptions(config.redisForSystemQueue, host) : redisForJobQueue, redisForSystemQueue: config.redisForSystemQueue ? convertRedisOptions(config.redisForSystemQueue, host) : redisForJobQueue,
redisForEndedPollNotificationQueue: config.redisForEndedPollNotificationQueue ? convertRedisOptions(config.redisForEndedPollNotificationQueue, host) : redisForJobQueue, redisForEndedPollNotificationQueue: config.redisForEndedPollNotificationQueue ? convertRedisOptions(config.redisForEndedPollNotificationQueue, host) : redisForJobQueue,
redisForDeliverQueues: config.redisForDeliverQueues ? config.redisForDeliverQueues.map(config => convertRedisOptions(config, host)) : [redisForJobQueue], redisForDeliverQueues: config.redisForDeliverQueues ? config.redisForDeliverQueues.map(config => convertRedisOptions(config, host)) : [redisForJobQueue],
redisForInboxQueue: config.redisForInboxQueue ? convertRedisOptions(config.redisForInboxQueue, host) : redisForJobQueue, redisForInboxQueues: config.redisForInboxQueues ? config.redisForInboxQueues.map(config => convertRedisOptions(config, host)) : [redisForJobQueue],
redisForDbQueue: config.redisForDbQueue ? convertRedisOptions(config.redisForDbQueue, host) : redisForJobQueue, redisForDbQueue: config.redisForDbQueue ? convertRedisOptions(config.redisForDbQueue, host) : redisForJobQueue,
redisForRelationshipQueue: config.redisForRelationshipQueue ? convertRedisOptions(config.redisForRelationshipQueue, host) : redisForJobQueue, redisForRelationshipQueues: config.redisForRelationshipQueues ? config.redisForRelationshipQueues.map(config => convertRedisOptions(config, host)) : [redisForJobQueue],
redisForObjectStorageQueue: config.redisForObjectStorageQueue ? convertRedisOptions(config.redisForObjectStorageQueue, host) : redisForJobQueue, redisForObjectStorageQueue: config.redisForObjectStorageQueue ? convertRedisOptions(config.redisForObjectStorageQueue, host) : redisForJobQueue,
redisForWebhookDeliverQueue: config.redisForWebhookDeliverQueue ? convertRedisOptions(config.redisForWebhookDeliverQueue, host) : redisForJobQueue, redisForWebhookDeliverQueue: config.redisForWebhookDeliverQueue ? convertRedisOptions(config.redisForWebhookDeliverQueue, host) : redisForJobQueue,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis, redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,

View File

@ -91,6 +91,7 @@ export class AnnouncementService {
forExistingUsers: values.forExistingUsers, forExistingUsers: values.forExistingUsers,
silence: values.silence, silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead, needConfirmationToRead: values.needConfirmationToRead,
needEnrollmentTutorialToRead: values.needEnrollmentTutorialToRead,
closeDuration: values.closeDuration, closeDuration: values.closeDuration,
displayOrder: values.displayOrder, displayOrder: values.displayOrder,
userId: values.userId, userId: values.userId,
@ -200,6 +201,7 @@ export class AnnouncementService {
icon: values.icon, icon: values.icon,
forExistingUsers: values.forExistingUsers, forExistingUsers: values.forExistingUsers,
needConfirmationToRead: values.needConfirmationToRead, needConfirmationToRead: values.needConfirmationToRead,
needEnrollmentTutorialToRead: values.needEnrollmentTutorialToRead,
closeDuration: values.closeDuration, closeDuration: values.closeDuration,
displayOrder: values.displayOrder, displayOrder: values.displayOrder,
silence: values.silence, silence: values.silence,

View File

@ -16,9 +16,9 @@ import type { DeliverJobData, InboxJobData, EndedPollNotificationJobData, Webhoo
export type SystemQueue = Bull.Queue<Record<string, unknown>>; export type SystemQueue = Bull.Queue<Record<string, unknown>>;
export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>; export type EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type DeliverQueue = Queues<DeliverJobData>; export type DeliverQueue = Queues<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>; export type InboxQueue = Queues<InboxJobData>;
export type DbQueue = Bull.Queue; export type DbQueue = Bull.Queue;
export type RelationshipQueue = Bull.Queue<RelationshipJobData>; export type RelationshipQueue = Queues<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue; export type ObjectStorageQueue = Bull.Queue;
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>; export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
@ -42,7 +42,7 @@ const $deliver: Provider = {
const $inbox: Provider = { const $inbox: Provider = {
provide: 'queue:inbox', provide: 'queue:inbox',
useFactory: (config: Config) => new Bull.Queue(QUEUE.INBOX, baseQueueOptions(config.redisForInboxQueue, config.bullmqQueueOptions, QUEUE.INBOX)), useFactory: (config: Config) => new Queues(config.redisForInboxQueues.map(queueConfig => new Bull.Queue(QUEUE.INBOX, baseQueueOptions(queueConfig, config.bullmqQueueOptions, QUEUE.INBOX)))),
inject: [DI.config], inject: [DI.config],
}; };
@ -54,7 +54,7 @@ const $db: Provider = {
const $relationship: Provider = { const $relationship: Provider = {
provide: 'queue:relationship', provide: 'queue:relationship',
useFactory: (config: Config) => new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(config.redisForRelationshipQueue, config.bullmqQueueOptions, QUEUE.RELATIONSHIP)), useFactory: (config: Config) => new Queues(config.redisForRelationshipQueues.map(queueConfig => new Bull.Queue(QUEUE.RELATIONSHIP, baseQueueOptions(queueConfig, config.bullmqQueueOptions, QUEUE.RELATIONSHIP)))),
inject: [DI.config], inject: [DI.config],
}; };

View File

@ -54,6 +54,7 @@ export class AnnouncementEntityService {
display: announcement.display, display: announcement.display,
forYou: announcement.userId === me?.id, forYou: announcement.userId === me?.id,
needConfirmationToRead: announcement.needConfirmationToRead, needConfirmationToRead: announcement.needConfirmationToRead,
needEnrollmentTutorialToRead: announcement.needEnrollmentTutorialToRead,
closeDuration: announcement.closeDuration, closeDuration: announcement.closeDuration,
displayOrder: announcement.displayOrder, displayOrder: announcement.displayOrder,
silence: announcement.silence, silence: announcement.silence,

View File

@ -61,6 +61,11 @@ export class MiAnnouncement {
}) })
public needConfirmationToRead: boolean; public needConfirmationToRead: boolean;
@Column('boolean', {
default: false,
})
public needEnrollmentTutorialToRead: boolean;
@Column('integer', { @Column('integer', {
nullable: false, nullable: false,
default: 0, default: 0,

View File

@ -48,6 +48,10 @@ export const packedAnnouncementSchema = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
needEnrollmentTutorialToRead: {
type: 'boolean',
optional: false, nullable: false,
},
forYou: { forYou: {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,

View File

@ -79,9 +79,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
private systemQueueWorker: Bull.Worker; private systemQueueWorker: Bull.Worker;
private dbQueueWorker: Bull.Worker; private dbQueueWorker: Bull.Worker;
private deliverQueueWorkers: Bull.Worker[]; private deliverQueueWorkers: Bull.Worker[];
private inboxQueueWorker: Bull.Worker; private inboxQueueWorkers: Bull.Worker[];
private webhookDeliverQueueWorker: Bull.Worker; private webhookDeliverQueueWorker: Bull.Worker;
private relationshipQueueWorker: Bull.Worker; private relationshipQueueWorkers: Bull.Worker[];
private objectStorageQueueWorker: Bull.Worker; private objectStorageQueueWorker: Bull.Worker;
private endedPollNotificationQueueWorker: Bull.Worker; private endedPollNotificationQueueWorker: Bull.Worker;
@ -243,27 +243,31 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#endregion //#endregion
//#region inbox //#region inbox
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), { this.inboxQueueWorkers = this.config.redisForInboxQueues
...baseWorkerOptions(this.config.redisForInboxQueue, this.config.bullmqWorkerOptions, QUEUE.INBOX), .filter((_, index) => process.env.QUEUE_WORKER_INDEX == null || index === Number.parseInt(process.env.QUEUE_WORKER_INDEX, 10))
autorun: false, .map(config => new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
concurrency: this.config.inboxJobConcurrency ?? 16, ...baseWorkerOptions(config, this.config.bullmqWorkerOptions, QUEUE.INBOX),
limiter: { autorun: false,
max: this.config.inboxJobPerSec ?? 32, concurrency: this.config.inboxJobConcurrency ?? 16,
duration: 1000, limiter: {
}, max: this.config.inboxJobPerSec ?? 32,
settings: { duration: 1000,
backoffStrategy: httpRelatedBackoff, },
}, settings: {
backoffStrategy: httpRelatedBackoff,
},
}));
this.inboxQueueWorkers.forEach((worker, index) => {
const inboxLogger = this.logger.createSubLogger(`inbox-${index}`);
worker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, error: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { error: renderError(err) }))
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
}); });
const inboxLogger = this.logger.createSubLogger('inbox');
this.inboxQueueWorker
.on('active', (job) => inboxLogger.debug(`active ${getJobInfo(job, true)}`))
.on('completed', (job, result) => inboxLogger.debug(`completed(${result}) ${getJobInfo(job, true)}`))
.on('failed', (job, err) => inboxLogger.warn(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, error: renderError(err) }))
.on('error', (err: Error) => inboxLogger.error(`error ${err.stack}`, { error: renderError(err) }))
.on('stalled', (jobId) => inboxLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
//#region webhook deliver //#region webhook deliver
@ -291,32 +295,36 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#endregion //#endregion
//#region relationship //#region relationship
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => { this.relationshipQueueWorkers = this.config.redisForRelationshipQueues
switch (job.name) { .filter((_, index) => process.env.QUEUE_WORKER_INDEX == null || index === Number.parseInt(process.env.QUEUE_WORKER_INDEX, 10))
case 'follow': return this.relationshipProcessorService.processFollow(job); .map(config => new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
case 'unfollow': return this.relationshipProcessorService.processUnfollow(job); switch (job.name) {
case 'block': return this.relationshipProcessorService.processBlock(job); case 'follow': return this.relationshipProcessorService.processFollow(job);
case 'unblock': return this.relationshipProcessorService.processUnblock(job); case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
default: throw new Error(`unrecognized job type ${job.name} for relationship`); case 'block': return this.relationshipProcessorService.processBlock(job);
} case 'unblock': return this.relationshipProcessorService.processUnblock(job);
}, { default: throw new Error(`unrecognized job type ${job.name} for relationship`);
...baseWorkerOptions(this.config.redisForRelationshipQueue, this.config.bullmqWorkerOptions, QUEUE.RELATIONSHIP), }
autorun: false, }, {
concurrency: this.config.relationshipJobConcurrency ?? 16, ...baseWorkerOptions(config, this.config.bullmqWorkerOptions, QUEUE.RELATIONSHIP),
limiter: { autorun: false,
max: this.config.relationshipJobPerSec ?? 64, concurrency: this.config.relationshipJobConcurrency ?? 16,
duration: 1000, limiter: {
}, max: this.config.relationshipJobPerSec ?? 64,
duration: 1000,
},
}));
this.relationshipQueueWorkers.forEach((worker, index) => {
const relationshipLogger = this.logger.createSubLogger(`relationship-${index}`);
worker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, error: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { error: renderError(err) }))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
}); });
const relationshipLogger = this.logger.createSubLogger('relationship');
this.relationshipQueueWorker
.on('active', (job) => relationshipLogger.debug(`active id=${job.id}`))
.on('completed', (job, result) => relationshipLogger.debug(`completed(${result}) id=${job.id}`))
.on('failed', (job, err) => relationshipLogger.warn(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, error: renderError(err) }))
.on('error', (err: Error) => relationshipLogger.error(`error ${err.stack}`, { error: renderError(err) }))
.on('stalled', (jobId) => relationshipLogger.warn(`stalled id=${jobId}`));
//#endregion //#endregion
//#region object storage //#region object storage
@ -356,9 +364,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker.run(), this.systemQueueWorker.run(),
this.dbQueueWorker.run(), this.dbQueueWorker.run(),
...this.deliverQueueWorkers.map(worker => worker.run()), ...this.deliverQueueWorkers.map(worker => worker.run()),
this.inboxQueueWorker.run(), this.inboxQueueWorkers.map(worker => worker.run()),
this.webhookDeliverQueueWorker.run(), this.webhookDeliverQueueWorker.run(),
this.relationshipQueueWorker.run(), this.relationshipQueueWorkers.map(worker => worker.run()),
this.objectStorageQueueWorker.run(), this.objectStorageQueueWorker.run(),
this.endedPollNotificationQueueWorker.run(), this.endedPollNotificationQueueWorker.run(),
]); ]);
@ -370,9 +378,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker.close(), this.systemQueueWorker.close(),
this.dbQueueWorker.close(), this.dbQueueWorker.close(),
...this.deliverQueueWorkers.map(worker => worker.close()), ...this.deliverQueueWorkers.map(worker => worker.close()),
this.inboxQueueWorker.close(), this.inboxQueueWorkers.map(worker => worker.close()),
this.webhookDeliverQueueWorker.close(), this.webhookDeliverQueueWorker.close(),
this.relationshipQueueWorker.close(), this.relationshipQueueWorkers.map(worker => worker.close()),
this.objectStorageQueueWorker.close(), this.objectStorageQueueWorker.close(),
this.endedPollNotificationQueueWorker.close(), this.endedPollNotificationQueueWorker.close(),
]); ]);

View File

@ -62,6 +62,10 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
needEnrollmentTutorialToRead: {
type: 'boolean',
optional: false, nullable: false,
},
closeDuration: { closeDuration: {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,
@ -92,6 +96,7 @@ export const paramDef = {
display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' }, display: { type: 'string', enum: ['normal', 'banner', 'dialog'], default: 'normal' },
forExistingUsers: { type: 'boolean', default: false }, forExistingUsers: { type: 'boolean', default: false },
needConfirmationToRead: { type: 'boolean', default: false }, needConfirmationToRead: { type: 'boolean', default: false },
needEnrollmentTutorialToRead: { type: 'boolean', default: false },
closeDuration: { type: 'number', default: 0 }, closeDuration: { type: 'number', default: 0 },
displayOrder: { type: 'number', default: 0 }, displayOrder: { type: 'number', default: 0 },
silence: { type: 'boolean', default: false }, silence: { type: 'boolean', default: false },
@ -115,6 +120,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
display: ps.display, display: ps.display,
forExistingUsers: ps.forExistingUsers, forExistingUsers: ps.forExistingUsers,
needConfirmationToRead: ps.needConfirmationToRead, needConfirmationToRead: ps.needConfirmationToRead,
needEnrollmentTutorialToRead: ps.needEnrollmentTutorialToRead,
closeDuration: ps.closeDuration, closeDuration: ps.closeDuration,
displayOrder: ps.displayOrder, displayOrder: ps.displayOrder,
silence: ps.silence, silence: ps.silence,

View File

@ -72,6 +72,10 @@ export const meta = {
type: 'boolean', type: 'boolean',
optional: false, nullable: false, optional: false, nullable: false,
}, },
needEnrollmentTutorialToRead: {
type: 'boolean',
optional: false, nullable: false,
},
closeDuration: { closeDuration: {
type: 'number', type: 'number',
optional: false, nullable: false, optional: false, nullable: false,
@ -139,6 +143,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
isActive: announcement.isActive, isActive: announcement.isActive,
forExistingUsers: announcement.forExistingUsers, forExistingUsers: announcement.forExistingUsers,
needConfirmationToRead: announcement.needConfirmationToRead, needConfirmationToRead: announcement.needConfirmationToRead,
needEnrollmentTutorialToRead: announcement.needEnrollmentTutorialToRead,
closeDuration: announcement.closeDuration, closeDuration: announcement.closeDuration,
displayOrder: announcement.displayOrder, displayOrder: announcement.displayOrder,
silence: announcement.silence, silence: announcement.silence,

View File

@ -37,6 +37,7 @@ export const paramDef = {
display: { type: 'string', enum: ['normal', 'banner', 'dialog'] }, display: { type: 'string', enum: ['normal', 'banner', 'dialog'] },
forExistingUsers: { type: 'boolean' }, forExistingUsers: { type: 'boolean' },
needConfirmationToRead: { type: 'boolean' }, needConfirmationToRead: { type: 'boolean' },
needEnrollmentTutorialToRead: { type: 'boolean' },
closeDuration: { type: 'number', default: 0 }, closeDuration: { type: 'number', default: 0 },
displayOrder: { type: 'number', default: 0 }, displayOrder: { type: 'number', default: 0 },
silence: { type: 'boolean' }, silence: { type: 'boolean' },
@ -68,6 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
icon: ps.icon, icon: ps.icon,
forExistingUsers: ps.forExistingUsers, forExistingUsers: ps.forExistingUsers,
needConfirmationToRead: ps.needConfirmationToRead, needConfirmationToRead: ps.needConfirmationToRead,
needEnrollmentTutorialToRead: ps.needEnrollmentTutorialToRead,
closeDuration: ps.closeDuration, closeDuration: ps.closeDuration,
displayOrder: ps.displayOrder, displayOrder: ps.displayOrder,
silence: ps.silence, silence: ps.silence,

View File

@ -56,7 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const jobs = await this.deliverQueue.getJobs(['delayed']); const jobs = await this.deliverQueue.getJobs(['delayed']);
const res = [] as [string, number][]; const res = new Map<string, number>();
for (const job of jobs) { for (const job of jobs) {
let host: string; let host: string;
@ -68,17 +68,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
continue; continue;
} }
const found = res.find(x => x[0] === host); const found = res.get(host);
if (found) { if (found) {
found[1]++; res.set(host, found + 1);
} else { } else {
res.push([host, 1]); res.set(host, 1);
} }
} }
res.sort((a, b) => b[1] - a[1]); return Array.from(res.entries()).sort((a, b) => b[1] - a[1]);
return res;
}); });
} }
} }

View File

@ -56,7 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
super(meta, paramDef, async (ps, me) => { super(meta, paramDef, async (ps, me) => {
const jobs = await this.inboxQueue.getJobs(['delayed']); const jobs = await this.inboxQueue.getJobs(['delayed']);
const res = [] as [string, number][]; const res = new Map<string, number>();
for (const job of jobs) { for (const job of jobs) {
let host: string; let host: string;
@ -68,17 +68,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
continue; continue;
} }
const found = res.find(x => x[0] === host); const found = res.get(host);
if (found) { if (found) {
found[1]++; res.set(host, found + 1);
} else { } else {
res.push([host, 1]); res.set(host, 1);
} }
} }
res.sort((a, b) => b[1] - a[1]); return Array.from(res.entries()).sort((a, b) => b[1] - a[1]);
return res;
}); });
} }
} }

View File

@ -64,11 +64,12 @@ export class JWTIdentifyProviderService {
fastify.all<{ fastify.all<{
Params: { serviceId: string }; Params: { serviceId: string };
Querystring?: { serviceurl?: string, return_to?: string }; Querystring?: { serviceurl?: string, return_to?: string, prompt?: string };
Body?: { serviceurl?: string, return_to?: string }; Body?: { serviceurl?: string, return_to?: string, prompt?: string };
}>('/:serviceId', async (request, reply) => { }>('/:serviceId', async (request, reply) => {
const serviceId = request.params.serviceId; const serviceId = request.params.serviceId;
const returnTo = request.query?.return_to ?? request.query?.serviceurl ?? request.body?.return_to ?? request.body?.serviceurl; const returnTo = request.query?.return_to ?? request.query?.serviceurl ?? request.body?.return_to ?? request.body?.serviceurl;
const prompt = request.query?.prompt ?? request.body?.prompt ?? 'consent';
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'jwt' }); const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'jwt' });
if (!ssoServiceProvider) { if (!ssoServiceProvider) {
@ -101,6 +102,7 @@ export class JWTIdentifyProviderService {
transactionId: transactionId, transactionId: transactionId,
serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer, serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer,
kind: 'jwt', kind: 'jwt',
prompt: prompt,
}); });
}); });

View File

@ -201,13 +201,14 @@ export class SAMLIdentifyProviderService {
fastify.all<{ fastify.all<{
Params: { serviceId: string }; Params: { serviceId: string };
Querystring?: { SAMLRequest?: string; RelayState?: string }; Querystring?: { SAMLRequest?: string; RelayState?: string, prompt?: string };
Body?: { SAMLRequest?: string; RelayState?: string }; Body?: { SAMLRequest?: string; RelayState?: string, prompt?: string };
}>('/:serviceId', async (request, reply) => { }>('/:serviceId', async (request, reply) => {
const serviceId = request.params.serviceId; const serviceId = request.params.serviceId;
const binding = request.query?.SAMLRequest ? 'redirect' : 'post'; const binding = request.query?.SAMLRequest ? 'redirect' : 'post';
const samlRequest = request.query?.SAMLRequest ?? request.body?.SAMLRequest; const samlRequest = request.query?.SAMLRequest ?? request.body?.SAMLRequest;
const relayState = request.query?.RelayState ?? request.body?.RelayState; const relayState = request.query?.RelayState ?? request.body?.RelayState;
const prompt = request.query?.prompt ?? request.body?.prompt ?? 'consent';
const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'saml', privateKey: Not(IsNull()) }); const ssoServiceProvider = await this.singleSignOnServiceProviderRepository.findOneBy({ id: serviceId, type: 'saml', privateKey: Not(IsNull()) });
if (!ssoServiceProvider) { if (!ssoServiceProvider) {
@ -268,6 +269,7 @@ export class SAMLIdentifyProviderService {
transactionId: transactionId, transactionId: transactionId,
serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer, serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer,
kind: 'saml', kind: 'saml',
prompt: prompt,
}); });
} catch (err) { } catch (err) {
this.#logger.error('Failed to parse SAML request', { error: err }); this.#logger.error('Failed to parse SAML request', { error: err });

View File

@ -25,7 +25,16 @@ import { getNoteSummary } from '@/misc/get-note-summary.js';
import { DI } from '@/di-symbols.js'; import { DI } from '@/di-symbols.js';
import * as Acct from '@/misc/acct.js'; import * as Acct from '@/misc/acct.js';
import { MetaService } from '@/core/MetaService.js'; import { MetaService } from '@/core/MetaService.js';
import type { DbQueue, DeliverQueue, EndedPollNotificationQueue, InboxQueue, ObjectStorageQueue, SystemQueue, WebhookDeliverQueue } from '@/core/QueueModule.js'; import type {
DbQueue,
DeliverQueue,
EndedPollNotificationQueue,
InboxQueue,
ObjectStorageQueue,
RelationshipQueue,
SystemQueue,
WebhookDeliverQueue,
} from '@/core/QueueModule.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js'; import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { NoteEntityService } from '@/core/entities/NoteEntityService.js'; import { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js'; import { PageEntityService } from '@/core/entities/PageEntityService.js';
@ -110,6 +119,7 @@ export class ClientServerService {
@Inject('queue:deliver') public deliverQueue: DeliverQueue, @Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue, @Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue, @Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue, @Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue, @Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
) { ) {
@ -235,12 +245,13 @@ export class ClientServerService {
queues: [ queues: [
this.systemQueue, this.systemQueue,
this.endedPollNotificationQueue, this.endedPollNotificationQueue,
this.inboxQueue,
this.dbQueue, this.dbQueue,
this.objectStorageQueue, this.objectStorageQueue,
this.webhookDeliverQueue, this.webhookDeliverQueue,
].map(q => new BullMQAdapter(q)) ].map(q => new BullMQAdapter(q))
.concat(this.deliverQueue.queues.map((q, index) => new BullMQAdapter(q, { prefix: `${index}-` }))), .concat(this.deliverQueue.queues.map((q, index) => new BullMQAdapter(q, { prefix: `${index}-` })))
.concat(this.inboxQueue.queues.map((q, index) => new BullMQAdapter(q, { prefix: `${index}-` })))
.concat(this.relationshipQueue.queues.map((q, index) => new BullMQAdapter(q, { prefix: `${index}-` }))),
serverAdapter, serverAdapter,
}); });

View File

@ -4,3 +4,4 @@ block meta
meta(name='misskey:sso:transaction-id' content=transactionId) meta(name='misskey:sso:transaction-id' content=transactionId)
meta(name='misskey:sso:service-name' content=serviceName) meta(name='misskey:sso:service-name' content=serviceName)
meta(name='misskey:sso:kind' content=kind) meta(name='misskey:sso:kind' content=kind)
meta(name='misskey:sso:prompt' content=prompt)

View File

@ -11,7 +11,6 @@ import { alert, confirm, popup, post, toast } from '@/os.js';
import { useStream } from '@/stream.js'; import { useStream } from '@/stream.js';
import * as sound from '@/scripts/sound.js'; import * as sound from '@/scripts/sound.js';
import { $i, signout, updateAccount } from '@/account.js'; import { $i, signout, updateAccount } from '@/account.js';
import { instance } from '@/instance.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js'; import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js'; import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js'; import { reactionPicker } from '@/scripts/reaction-picker.js';
@ -97,7 +96,7 @@ export async function mainBoot() {
}).render(); }).render();
} }
} }
} }
} catch (error) { } catch (error) {
// console.error(error); // console.error(error);
console.error('Failed to initialise the seasonal screen effect canvas context:', error); console.error('Failed to initialise the seasonal screen effect canvas context:', error);
@ -237,10 +236,10 @@ export async function mainBoot() {
} }
} }
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read'); // const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') { // if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed'); // popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
} // }
if ('Notification' in window) { if ('Notification' in window) {
// 許可を得ていなかったらリクエスト // 許可を得ていなかったらリクエスト

View File

@ -34,8 +34,8 @@ misskeyApi('users/show', { userId: props.movedTo ?? props.movedFrom }).then(u =>
.root { .root {
padding: 16px; padding: 16px;
font-size: 90%; font-size: 90%;
background: var(--infoWarnBg); background: var(--infoBg);
color: var(--error); color: var(--infoFg);
border-radius: var(--radius); border-radius: var(--radius);
} }

View File

@ -46,6 +46,7 @@ export const Default = {
imageUrl: null, imageUrl: null,
display: 'dialog', display: 'dialog',
needConfirmationToRead: false, needConfirmationToRead: false,
needEnrollmentTutorialToRead: false,
silence: false, silence: false,
forYou: true, forYou: true,
}, },

View File

@ -19,13 +19,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="announcement.text"/> <Mfm :text="announcement.text"/>
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/> <img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
</div> </div>
<MkButton :class="$style.gotIt" primary full :disabled="gotItDisabled" @click="gotIt">{{ i18n.ts.gotIt }}<span v-if="secVisible"> ({{ sec }})</span></MkButton> <MkButton :class="$style.gotIt" primary gradate full :disabled="gotItDisabled" @click="gotIt">
{{ !announcement.needEnrollmentTutorialToRead ? i18n.ts.gotIt : i18n.ts._initialAccountSetting.startTutorial }}
<span v-if="secVisible"> ({{ sec }})</span>
</MkButton>
</div> </div>
</MkModal> </MkModal>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue'; import { defineAsyncComponent, onMounted, ref, shallowRef } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import * as os from '@/os.js'; import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js'; import { misskeyApi } from '@/scripts/misskey-api.js';
@ -46,6 +49,18 @@ const secVisible = ref(true);
const sec = ref(props.announcement.closeDuration); const sec = ref(props.announcement.closeDuration);
async function gotIt(): Promise<void> { async function gotIt(): Promise<void> {
if (props.announcement.needEnrollmentTutorialToRead) {
modal.value?.close();
const tutorialCompleted = await (new Promise<boolean>(resolve => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
done: () => {
resolve(true);
},
}, 'closed');
}));
if (!tutorialCompleted) return;
}
if (props.announcement.needConfirmationToRead) { if (props.announcement.needConfirmationToRead) {
const confirm = await os.confirm({ const confirm = await os.confirm({
type: 'question', type: 'question',

View File

@ -6,12 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template> <template>
<button <button
class="_button" class="_button"
:class="[$style.root, { [$style.wait]: wait, [$style.active]: userDetailed.isFollowing || userDetailed.hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]" :class="[$style.root, { [$style.wait]: wait, [$style.active]: userDetailed.isFollowing || userDetailed.hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large, [$style.activeBlocked]: userDetailed.isBlocking || userDetailed.isBlocked }]"
:disabled="wait" :disabled="wait || userDetailed.isBlocked"
@click="onClick" @click="onClick"
> >
<template v-if="!wait"> <template v-if="!wait">
<template v-if="userDetailed.hasPendingFollowRequestFromYou && userDetailed.isLocked"> <template v-if="userDetailed.isBlocking || userDetailed.isBlocked">
<span v-if="full" :class="$style.text">{{ i18n.ts.blocked }}</span><i class="ti ti-ban"></i>
</template>
<template v-else-if="userDetailed.hasPendingFollowRequestFromYou && userDetailed.isLocked">
<span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i> <span v-if="full" :class="$style.text">{{ i18n.ts.followRequestPending }}</span><i class="ti ti-hourglass-empty"></i>
</template> </template>
<template v-else-if="userDetailed.hasPendingFollowRequestFromYou && !userDetailed.isLocked"> <template v-else-if="userDetailed.hasPendingFollowRequestFromYou && !userDetailed.isLocked">
@ -78,29 +81,45 @@ function onFollowChange(user: Misskey.entities.UserDetailed) {
} }
} }
async function getConfirmed(text: string): Promise<boolean> {
const confirm = await os.confirm({
type: 'warning',
title: i18n.ts.areYouSure,
text,
});
return !confirm.canceled;
}
async function onClick() { async function onClick() {
wait.value = true; wait.value = true;
try { try {
if (userDetailed.value.isFollowing) { if (userDetailed.value.isFollowing) {
const { canceled } = await os.confirm({ if (!await getConfirmed(i18n.tsx.unfollowConfirm({ name: (userDetailed.value.name || userDetailed.value.username) ?? i18n.ts.user }))) return;
type: 'warning',
text: i18n.tsx.unfollowConfirm({ name: (userDetailed.value.name || userDetailed.value.username) ?? i18n.ts.user }),
});
if (canceled) return;
await misskeyApi('following/delete', { await misskeyApi('following/delete', {
userId: props.user.id, userId: props.user.id,
}); });
} else if (userDetailed.value.isBlocking) {
if (!await getConfirmed(i18n.ts.unblockConfirm)) return;
os.apiWithDialog('blocking/delete', {
userId: userDetailed.value.id,
}).then(() => {
misskeyApi('users/show', {
userId: props.user.id,
})
.then(onFollowChange)
.then(() => {
wait.value = false;
});
});
} else { } else {
if (defaultStore.state.alwaysConfirmFollow) { if (defaultStore.state.alwaysConfirmFollow) {
const { canceled } = await os.confirm({ const confirmed = await getConfirmed(i18n.tsx.followConfirm({ name: props.user.name || props.user.username }));
type: 'question',
text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }),
});
if (canceled) { if (!confirmed) {
wait.value = false; wait.value = false;
return; return;
} }
@ -214,6 +233,12 @@ onBeforeUnmount(() => {
} }
} }
&.activeBlocked {
color: var(--fgOnAccent);
border-color: var(--error);
background: var(--error);
}
&.wait { &.wait {
cursor: wait !important; cursor: wait !important;
opacity: 0.7; opacity: 0.7;

View File

@ -174,6 +174,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<span style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span> <span style="margin-left: 4px;">{{ appearNote.reactions[reaction] }}</span>
</button> </button>
</div> </div>
<MkButton v-if="reactionTabType" :class="$style.reactionMuteButton" @click="reactionMuteToggle(reactionTabTypeTrimLocal)">
<i :class="!mutedReactions.includes(reactionTabTypeTrimLocal) ? 'ti ti-mood-happy' : 'ti ti-mood-off'"/>
{{ !mutedReactions.includes(reactionTabTypeTrimLocal) ? i18n.ts.muteThisReaction : i18n.ts.unmuteThisReaction }}
</MkButton>
<MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true"> <MkPagination v-if="reactionTabType" :key="reactionTabType" :pagination="reactionsPagination" :disableAutoLoad="true">
<template #default="{ items }"> <template #default="{ items }">
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;"> <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); grid-gap: 12px;">
@ -297,6 +301,7 @@ const showTicker = (defaultStore.state.instanceTicker === 'always') || (defaultS
const conversation = ref<Misskey.entities.Note[]>([]); const conversation = ref<Misskey.entities.Note[]>([]);
const replies = ref<Misskey.entities.Note[]>([]); const replies = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id); const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
const mutedReactions = ref<string[]>(defaultStore.state.mutedReactions);
const keymap = { const keymap = {
'r': () => reply(true), 'r': () => reply(true),
@ -316,6 +321,7 @@ provide('react', (reaction: string) => {
const tab = ref(props.initialTab); const tab = ref(props.initialTab);
const reactionTabType = ref<string | null>(null); const reactionTabType = ref<string | null>(null);
const reactionTabTypeTrimLocal = computed(() => reactionTabType.value?.replace('@.', '') ?? null);
const renotesPagination = computed<Paging>(() => ({ const renotesPagination = computed<Paging>(() => ({
endpoint: 'notes/renotes', endpoint: 'notes/renotes',
@ -497,6 +503,18 @@ async function translate(): Promise<void> {
translation.value = res; translation.value = res;
} }
async function reactionMuteToggle(reactionName: string | null) {
if (reactionName == null) return;
if (!mutedReactions.value.includes(reactionName)) {
mutedReactions.value.push(reactionName);
defaultStore.set('mutedReactions', mutedReactions.value);
} else {
mutedReactions.value = mutedReactions.value.filter(x => x !== reactionName);
defaultStore.set('mutedReactions', mutedReactions.value);
}
}
function showRenoteMenu(viaKeyboard = false): void { function showRenoteMenu(viaKeyboard = false): void {
if (!isMyRenote) return; if (!isMyRenote) return;
pleaseLogin(); pleaseLogin();
@ -782,6 +800,10 @@ function loadConversation() {
margin-bottom: 8px; margin-bottom: 8px;
} }
.reactionMuteButton {
margin-bottom: 8px;
}
.reactionTab { .reactionTab {
padding: 4px 6px; padding: 4px 6px;
border: solid 1px var(--divider); border: solid 1px var(--divider);

View File

@ -22,6 +22,7 @@ import * as Misskey from 'misskey-js';
import { inject, watch, ref } from 'vue'; import { inject, watch, ref } from 'vue';
import XReaction from '@/components/MkReactionsViewer.reaction.vue'; import XReaction from '@/components/MkReactionsViewer.reaction.vue';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
note: Misskey.entities.Note; note: Misskey.entities.Note;
@ -45,6 +46,13 @@ if (props.note.myReaction && !Object.keys(reactions.value).includes(props.note.m
reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction]; reactions.value[props.note.myReaction] = props.note.reactions[props.note.myReaction];
} }
function shouldDisplayReaction([reaction]: [string, number]): boolean {
if (!$i) return true; //
if (reaction === props.note.myReaction) return true; //
if (!defaultStore.state.mutedReactions.includes(reaction.replace('@.', ''))) return true; // @. suffix
return false;
}
function onMockToggleReaction(emoji: string, count: number) { function onMockToggleReaction(emoji: string, count: number) {
if (!mock) return; if (!mock) return;
@ -80,7 +88,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]); newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
} }
reactions.value = newReactions; reactions.value = newReactions.filter(shouldDisplayReaction);
}, { immediate: true, deep: true }); }, { immediate: true, deep: true });
</script> </script>
@ -104,6 +112,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
flex-wrap: wrap; flex-wrap: wrap;
align-items: center; align-items: center;
margin: 4px -2px 0 -2px; margin: 4px -2px 0 -2px;
max-width: 100%;
&:empty { &:empty {
display: none; display: none;

View File

@ -19,8 +19,8 @@ defineProps<{
.root { .root {
font-size: 0.8em; font-size: 0.8em;
padding: 16px; padding: 16px;
background: var(--infoWarnBg); background: var(--infoBg);
color: var(--infoWarnFg); color: var(--infoFg);
border-radius: var(--radius); border-radius: var(--radius);
overflow: clip; overflow: clip;
} }

View File

@ -40,6 +40,7 @@ const props = defineProps<{
const emit = defineEmits<{ const emit = defineEmits<{
(ev: 'closed'): void; (ev: 'closed'): void;
(ev: 'done'): void;
}>(); }>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
@ -49,6 +50,10 @@ const page = ref(props.initialPage ?? 0);
function handlePageChange(to: number) { function handlePageChange(to: number) {
page.value = to; page.value = to;
if (page.value === 9) {
emit('done');
}
} }
async function close(skip?: boolean) { async function close(skip?: boolean) {

View File

@ -42,6 +42,10 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._announcement.needConfirmationToRead }} {{ i18n.ts._announcement.needConfirmationToRead }}
<template #caption>{{ i18n.ts._announcement.needConfirmationToReadDescription }}</template> <template #caption>{{ i18n.ts._announcement.needConfirmationToReadDescription }}</template>
</MkSwitch> </MkSwitch>
<MkSwitch v-model="needEnrollmentTutorialToRead">
{{ i18n.ts._announcement.needEnrollmentTutorialToRead }}
<template #caption>{{ i18n.ts._announcement.needEnrollmentTutorialToReadDescription }}</template>
</MkSwitch>
<MkInput v-model="closeDuration" type="number"> <MkInput v-model="closeDuration" type="number">
<template #label>{{ i18n.ts.dialogCloseDuration }}</template> <template #label>{{ i18n.ts.dialogCloseDuration }}</template>
<template #suffix>{{ i18n.ts._time.second }}</template> <template #suffix>{{ i18n.ts._time.second }}</template>
@ -90,6 +94,7 @@ const text = ref<string>(props.announcement ? props.announcement.text : '');
const icon = ref<string>(props.announcement ? props.announcement.icon : 'info'); const icon = ref<string>(props.announcement ? props.announcement.icon : 'info');
const display = ref<string>(props.announcement ? props.announcement.display : 'dialog'); const display = ref<string>(props.announcement ? props.announcement.display : 'dialog');
const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false); const needConfirmationToRead = ref(props.announcement ? props.announcement.needConfirmationToRead : false);
const needEnrollmentTutorialToRead = ref(props.announcement ? props.announcement.needEnrollmentTutorialToRead : false);
const closeDuration = ref<number>(props.announcement ? props.announcement.closeDuration : 0); const closeDuration = ref<number>(props.announcement ? props.announcement.closeDuration : 0);
const displayOrder = ref<number>(props.announcement ? props.announcement.displayOrder : 0); const displayOrder = ref<number>(props.announcement ? props.announcement.displayOrder : 0);
const silence = ref<boolean>(props.announcement ? props.announcement.silence : false); const silence = ref<boolean>(props.announcement ? props.announcement.silence : false);

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
--> -->
<template> <template>
<span v-if="errored">:{{ customEmojiName }}:</span> <img v-if="errored" src="/client-assets/dummy.png" :alt="alt" :title="alt" decoding="async" :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"/>
<img <img
v-else v-else
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]" :class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, inject, ref } from 'vue'; import { computed, inject, ref, watch } from 'vue';
import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js'; import { getProxiedImageUrl, getStaticImageUrl } from '@/scripts/media-proxy.js';
import { defaultStore } from '@/store.js'; import { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js'; import { customEmojisMap } from '@/custom-emojis.js';
@ -73,6 +73,10 @@ const url = computed(() => {
: proxied; : proxied;
}); });
watch(url, (newValue) => {
errored.value = (newValue === undefined);
});
const alt = computed(() => `:${customEmojiName.value}:`); const alt = computed(() => `:${customEmojiName.value}:`);
const errored = ref(url.value == null); const errored = ref(url.value == null);

View File

@ -66,6 +66,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription"> <MkSwitch v-model="announcement.needConfirmationToRead" :helpText="i18n.ts._announcement.needConfirmationToReadDescription">
{{ i18n.ts._announcement.needConfirmationToRead }} {{ i18n.ts._announcement.needConfirmationToRead }}
</MkSwitch> </MkSwitch>
<MkSwitch v-model="announcement.needEnrollmentTutorialToRead" :helpText="i18n.ts._announcement.needEnrollmentTutorialToReadDescription">
{{ i18n.ts._announcement.needEnrollmentTutorialToRead }}
</MkSwitch>
<MkInput v-model="announcement.closeDuration" type="number"> <MkInput v-model="announcement.closeDuration" type="number">
<template #label>{{ i18n.ts.dialogCloseDuration }}</template> <template #label>{{ i18n.ts.dialogCloseDuration }}</template>
<template #suffix>{{ i18n.ts._time.second }}</template> <template #suffix>{{ i18n.ts._time.second }}</template>

View File

@ -34,7 +34,10 @@
</MkA> </MkA>
</div> </div>
<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer"> <div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer">
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton> <MkButton primary gradate @click="read(announcement)">
<i :class="!announcement.needEnrollmentTutorialToRead ? 'ti ti-check' : 'ti ti-presentation'"/>
{{ !announcement.needEnrollmentTutorialToRead ? i18n.ts.gotIt : i18n.ts._initialAccountSetting.startTutorial }}
</MkButton>
</div> </div>
</div> </div>
<MkError v-else-if="error" @retry="fetch()"/> <MkError v-else-if="error" @retry="fetch()"/>
@ -45,7 +48,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, watch } from 'vue'; import { ref, computed, watch, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js'; import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js'; import * as os from '@/os.js';
@ -75,6 +78,17 @@ function fetch() {
} }
async function read(announcement): Promise<void> { async function read(announcement): Promise<void> {
if (announcement.needEnrollmentTutorialToRead) {
const tutorialCompleted = await (new Promise<boolean>(resolve => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
done: () => {
resolve(true);
},
}, 'closed');
}));
if (!tutorialCompleted) return;
}
if (announcement.needConfirmationToRead) { if (announcement.needConfirmationToRead) {
const confirm = await os.confirm({ const confirm = await os.confirm({
type: 'question', type: 'question',

View File

@ -36,7 +36,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA> </MkA>
</div> </div>
<div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer"> <div v-if="$i && !announcement.silence && !announcement.isRead" :class="$style.footer">
<MkButton primary @click="read(announcement)"><i class="ti ti-check"></i> {{ i18n.ts.gotIt }}</MkButton> <MkButton primary gradate @click="read(announcement)">
<i :class="!announcement.needEnrollmentTutorialToRead ? 'ti ti-check' : 'ti ti-presentation'"/>
{{ !announcement.needEnrollmentTutorialToRead ? i18n.ts.gotIt : i18n.ts._initialAccountSetting.startTutorial }}
</MkButton>
</div> </div>
</section> </section>
</MkPagination> </MkPagination>
@ -47,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed, watch } from 'vue'; import { ref, computed, watch, defineAsyncComponent } from 'vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue'; import MkInfo from '@/components/MkInfo.vue';
@ -81,6 +84,17 @@ const paginationEl = ref<InstanceType<typeof MkPagination>>();
const tab = ref('current'); const tab = ref('current');
async function read(announcement): Promise<void> { async function read(announcement): Promise<void> {
if (announcement.needEnrollmentTutorialToRead) {
const tutorialCompleted = await (new Promise<boolean>(resolve => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {}, {
done: () => {
resolve(true);
},
}, 'closed');
}));
if (!tutorialCompleted) return;
}
if (announcement.needConfirmationToRead) { if (announcement.needConfirmationToRead) {
const confirm = await os.confirm({ const confirm = await os.confirm({
type: 'question', type: 'question',

View File

@ -12,6 +12,23 @@ SPDX-License-Identifier: AGPL-3.0-only
<XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/> <XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/>
</MkFolder> </MkFolder>
<MkFolder>
<template #icon><i class="ti ti-message-off"></i></template>
<template #label>{{ i18n.ts.mutedReactions }}</template>
<div class="_gaps">
<div v-panel style="border-radius: var(--radius); padding: var(--margin);">
<button v-for="emoji in mutedReactions" class="_button" :class="$style.emojisItem" @click="removeReaction(emoji, $event)">
<MkCustomEmoji v-if="emoji && emoji[0] === ':'" :name="emoji"/>
<MkEmoji v-else :emoji="emoji ? emoji : 'null'"/>
</button>
<button class="_button" @click="chooseReaction">
<i class="ti ti-plus"></i>
</button>
</div>
</div>
</MkFolder>
<MkFolder> <MkFolder>
<template #icon><i class="ti ti-planet-off"></i></template> <template #icon><i class="ti ti-planet-off"></i></template>
<template #label>{{ i18n.ts.instanceMute }}</template> <template #label>{{ i18n.ts.instanceMute }}</template>
@ -119,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, computed } from 'vue'; import { ref, computed, watch, Ref } from 'vue';
import XInstanceMute from './mute-block.instance-mute.vue'; import XInstanceMute from './mute-block.instance-mute.vue';
import XWordMute from './mute-block.word-mute.vue'; import XWordMute from './mute-block.word-mute.vue';
import MkPagination from '@/components/MkPagination.vue'; import MkPagination from '@/components/MkPagination.vue';
@ -132,6 +149,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { infoImageUrl } from '@/instance.js'; import { infoImageUrl } from '@/instance.js';
import { signinRequired } from '@/account.js'; import { signinRequired } from '@/account.js';
import MkFolder from '@/components/MkFolder.vue'; import MkFolder from '@/components/MkFolder.vue';
import MkCustomEmoji from '@/components/global/MkCustomEmoji.vue';
import MkEmoji from '@/components/global/MkEmoji.vue';
import { defaultStore } from '@/store.js';
const $i = signinRequired(); const $i = signinRequired();
@ -154,6 +174,37 @@ const expandedRenoteMuteItems = ref([]);
const expandedMuteItems = ref([]); const expandedMuteItems = ref([]);
const expandedBlockItems = ref([]); const expandedBlockItems = ref([]);
const mutedReactions = ref<string[]>(defaultStore.state.mutedReactions);
watch(mutedReactions, () => {
defaultStore.set('mutedReactions', mutedReactions.value);
}, {
deep: true,
});
const chooseReaction = (ev: MouseEvent) => pickEmoji(mutedReactions, ev);
const removeReaction = (reaction: string, ev: MouseEvent) => remove(mutedReactions, reaction, ev);
function remove(itemsRef: Ref<string[]>, reaction: string, ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts.remove,
action: () => {
itemsRef.value = itemsRef.value.filter(x => x !== reaction);
},
}], ev.currentTarget ?? ev.target);
}
async function pickEmoji(itemsRef: Ref<string[]>, ev: MouseEvent) {
os.pickEmoji(ev.currentTarget ?? ev.target, {
showPinned: false,
}).then(it => {
const emoji = it;
if (!itemsRef.value.includes(emoji)) {
itemsRef.value.push(emoji);
}
});
}
async function unrenoteMute(user, ev) { async function unrenoteMute(user, ev) {
os.popupMenu([{ os.popupMenu([{
text: i18n.ts.renoteUnmute, text: i18n.ts.renoteUnmute,
@ -263,4 +314,9 @@ definePageMetadata(() => ({
transform: rotateX(180deg); transform: rotateX(180deg);
} }
} }
.emojisItem{
display: inline-block;
padding: 8px;
}
</style> </style>

View File

@ -120,6 +120,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'sound_notification', 'sound_notification',
'sound_antenna', 'sound_antenna',
'sound_channel', 'sound_channel',
'mutedReactions',
]; ];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [ const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
'lightTheme', 'lightTheme',

View File

@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, nextTick } from 'vue'; import { ref, nextTick, onMounted } from 'vue';
import MkSignin from '@/components/MkSignin.vue'; import MkSignin from '@/components/MkSignin.vue';
import MkButton from '@/components/MkButton.vue'; import MkButton from '@/components/MkButton.vue';
import { $i, login } from '@/account.js'; import { $i, login } from '@/account.js';
@ -47,6 +47,7 @@ if (transactionIdMeta) {
} }
const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:service-name"]')?.content; const name = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:service-name"]')?.content;
const kind = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:kind"]')?.content; const kind = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:kind"]')?.content;
const prompt = document.querySelector<HTMLMetaElement>('meta[name="misskey:sso:prompt"]')?.content;
const loading = ref(false); const loading = ref(false);
const postBindingForm = ref<HTMLFormElement | null>(null); const postBindingForm = ref<HTMLFormElement | null>(null);
@ -90,6 +91,12 @@ async function authorize(): Promise<void> {
} }
} }
onMounted(() => {
if ($i && prompt === 'none') {
onAccept();
}
});
definePageMetadata(() => ({ definePageMetadata(() => ({
title: 'Single Sign-On', title: 'Single Sign-On',
icon: 'ti ti-apps', icon: 'ti ti-apps',

View File

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="profile _gaps"> <div class="profile _gaps">
<MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/> <MkAccountMoved v-if="user.movedTo" :movedTo="user.movedTo"/>
<MkAccountMoved v-if="movedFromLog" :movedFrom="movedFromLog[0]?.movedFromId"/> <MkAccountMoved v-if="movedFromLog && iAmModerator" :movedFrom="movedFromLog[0]?.movedFromId"/>
<MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/> <MkRemoteCaution v-if="user.host != null" :href="user.url ?? user.uri!" class="warn"/>
<div :key="user.id" class="main _panel"> <div :key="user.id" class="main _panel">
@ -24,7 +24,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName class="name" :user="user" :nowrap="true"/> <MkUserName class="name" :user="user" :nowrap="true"/>
<div class="bottom"> <div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true"/></span> <span class="username"><MkAcct :user="user" :detail="true"/></span>
<span v-if="user.isAdmin" v-tooltip.noDelay="i18n.ts.administrator" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
<span v-if="user.isLocked" v-tooltip.noDelay="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isLocked" v-tooltip.noDelay="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
<span v-if="user.isBot" v-tooltip.noDelay="i18n.ts.isBot"><i class="ti ti-robot"></i></span> <span v-if="user.isBot" v-tooltip.noDelay="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea"> <button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
@ -32,10 +31,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</button> </button>
</div> </div>
</div> </div>
<span v-if="$i && $i.id != user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span> <span v-if="$i && $i.id !== user.id && user.isFollowed" class="followed">{{ i18n.ts.followsYou }}</span>
<span v-if="$i && $i.id !== user.id && user.isBlocking" class="followed">{{ i18n.ts.youAreBlocking }}</span>
<span v-if="$i && $i.id !== user.id && user.isBlocked" class="followed">{{ i18n.ts.youAreBlocked }}</span>
<div v-if="$i" class="actions"> <div v-if="$i" class="actions">
<button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button> <button class="menu _button" @click="menu"><i class="ti ti-dots"></i></button>
<MkFollowButton v-if="$i.id != user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/> <MkFollowButton v-if="$i && $i.id !== user.id" v-model:user="user" :inline="true" :transparent="false" :full="true" class="koudoku"/>
</div> </div>
</div> </div>
<MkAvatar class="avatar" :user="user" indicator/> <MkAvatar class="avatar" :user="user" indicator/>
@ -43,7 +44,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName :user="user" :nowrap="false" class="name"/> <MkUserName :user="user" :nowrap="false" class="name"/>
<div class="bottom"> <div class="bottom">
<span class="username"><MkAcct :user="user" :detail="true"/></span> <span class="username"><MkAcct :user="user" :detail="true"/></span>
<span v-if="user.isAdmin" :title="i18n.ts.administrator" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
<span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span> <span v-if="user.isLocked" :title="i18n.ts.isLocked"><i class="ti ti-lock"></i></span>
<span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span> <span v-if="user.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
</div> </div>
@ -186,7 +186,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div> </div>
</div> </div>
<div class="contents _gaps"> <div v-if="user.isBlocked !== true" class="contents _gaps">
<div v-if="user.pinnedNotes.length > 0" class="_gaps"> <div v-if="user.pinnedNotes.length > 0" class="_gaps">
<MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/> <MkNote v-for="note in user.pinnedNotes" :key="note.id" class="note _panel" :note="note" :pinned="true"/>
</div> </div>
@ -438,8 +438,8 @@ onUnmounted(() => {
> .punished { > .punished {
font-size: 0.8em; font-size: 0.8em;
padding: 16px; padding: 16px;
background: var(--infoWarnBg); background: var(--infoBg);
color: var(--infoWarnFg); color: var(--infoFg);
border-radius: var(--radius); border-radius: var(--radius);
overflow: clip; overflow: clip;
} }

View File

@ -21,15 +21,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<XPages v-else-if="tab === 'pages'" key="pages" :user="user"/> <XPages v-else-if="tab === 'pages'" key="pages" :user="user"/>
<XFlashs v-else-if="tab === 'flashs'" key="flashs" :user="user"/> <XFlashs v-else-if="tab === 'flashs'" key="flashs" :user="user"/>
<XGallery v-else-if="tab === 'gallery'" key="gallery" :user="user"/> <XGallery v-else-if="tab === 'gallery'" key="gallery" :user="user"/>
<XRaw v-else-if="tab === 'raw'" key="raw" :user="user"/>
</MkHorizontalSwipe> </MkHorizontalSwipe>
</div> </div>
<div v-else-if="error"> <div v-else-if="error">
<MkError @retry="fetchUser()"/> <MkError @retry="fetchUser()"/>
</div> </div>
<div v-else-if="userstatus"> <div v-else-if="userstatus">
<MkUserNotFound v-if="userstatus === 'notfound'"/> <MkUserNotFound v-if="userstatus === 'notfound' || userstatus === 'suspended'"/>
<MkUserSuspended v-else-if="userstatus === 'suspended'"/>
</div> </div>
<MkLoading v-else/> <MkLoading v-else/>
</div> </div>
@ -45,7 +43,6 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js'; import { i18n } from '@/i18n.js';
import { $i, iAmModerator } from '@/account.js'; import { $i, iAmModerator } from '@/account.js';
import MkUserNotFound from '@/components/MkUserNotFound.vue'; import MkUserNotFound from '@/components/MkUserNotFound.vue';
import MkUserSuspended from '@/components/MkUserSuspended.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue'; import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const XHome = defineAsyncComponent(() => import('./home.vue')); const XHome = defineAsyncComponent(() => import('./home.vue'));
@ -58,7 +55,6 @@ const XLists = defineAsyncComponent(() => import('./lists.vue'));
const XPages = defineAsyncComponent(() => import('./pages.vue')); const XPages = defineAsyncComponent(() => import('./pages.vue'));
const XFlashs = defineAsyncComponent(() => import('./flashs.vue')); const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
const XGallery = defineAsyncComponent(() => import('./gallery.vue')); const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
acct: string; acct: string;
@ -135,10 +131,6 @@ const headerTabs = computed(() => user.value ? [{
key: 'gallery', key: 'gallery',
title: i18n.ts.gallery, title: i18n.ts.gallery,
icon: 'ti ti-icons', icon: 'ti ti-icons',
}, {
key: 'raw',
title: 'Raw',
icon: 'ti ti-code',
}] : []); }] : []);
definePageMetadata(() => ({ definePageMetadata(() => ({

View File

@ -108,7 +108,7 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
async function getConfirmed(text: string): Promise<boolean> { async function getConfirmed(text: string): Promise<boolean> {
const confirm = await os.confirm({ const confirm = await os.confirm({
type: 'warning', type: 'warning',
title: 'confirm', title: i18n.ts.areYouSure,
text, text,
}); });

View File

@ -543,6 +543,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device', where: 'device',
default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore, default: { type: 'syuilo/bubble2', volume: 1 } as SoundStore,
}, },
mutedReactions: {
where: 'account',
default: [] as string[],
},
})); }));
// TODO: 他のタブと永続化されたstateを同期 // TODO: 他のタブと永続化されたstateを同期

View File

@ -4204,6 +4204,7 @@ export type components = {
/** @enum {string} */ /** @enum {string} */
display: 'dialog' | 'normal' | 'banner'; display: 'dialog' | 'normal' | 'banner';
needConfirmationToRead: boolean; needConfirmationToRead: boolean;
needEnrollmentTutorialToRead: boolean;
forYou: boolean; forYou: boolean;
closeDuration: number; closeDuration: number;
displayOrder: number; displayOrder: number;
@ -6086,6 +6087,8 @@ export type operations = {
forExistingUsers?: boolean; forExistingUsers?: boolean;
/** @default false */ /** @default false */
needConfirmationToRead?: boolean; needConfirmationToRead?: boolean;
/** @default false */
needEnrollmentTutorialToRead?: boolean;
/** @default 0 */ /** @default 0 */
closeDuration?: number; closeDuration?: number;
/** @default 0 */ /** @default 0 */
@ -6121,6 +6124,7 @@ export type operations = {
display: string; display: string;
forYou: boolean; forYou: boolean;
needConfirmationToRead: boolean; needConfirmationToRead: boolean;
needEnrollmentTutorialToRead: boolean;
closeDuration: number; closeDuration: number;
displayOrder: number; displayOrder: number;
silence: boolean; silence: boolean;
@ -6253,6 +6257,7 @@ export type operations = {
display: string; display: string;
forExistingUsers: boolean; forExistingUsers: boolean;
needConfirmationToRead: boolean; needConfirmationToRead: boolean;
needEnrollmentTutorialToRead: boolean;
closeDuration: number; closeDuration: number;
displayOrder: number; displayOrder: number;
silence: boolean; silence: boolean;
@ -6317,6 +6322,7 @@ export type operations = {
display?: 'normal' | 'banner' | 'dialog'; display?: 'normal' | 'banner' | 'dialog';
forExistingUsers?: boolean; forExistingUsers?: boolean;
needConfirmationToRead?: boolean; needConfirmationToRead?: boolean;
needEnrollmentTutorialToRead?: boolean;
/** @default 0 */ /** @default 0 */
closeDuration?: number; closeDuration?: number;
/** @default 0 */ /** @default 0 */