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"
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ó "
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ó "
tooManyActiveAnnouncementDescription: "Tenir massa notificacions actives pot empitjorar l'experiència de l'usuari. Considera finalitzar els anuncis que siguin antics."
readConfirmTitle: "Marcar com llegida?"

View File

@ -1189,7 +1189,7 @@ _announcement:
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."
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"
tooManyActiveAnnouncementDescription: "Zu viele aktive Ankündigungen können die Benutzerfreundlichkeit verschlechtern. Es wird empfohlen, veraltete Ankündigungen zu archivieren."
readConfirmTitle: "Als gelesen markieren?"

View File

@ -138,6 +138,9 @@ mute: "Mute"
unmute: "Unmute"
renoteMute: "Mute Renotes"
renoteUnmute: "Unmute Renotes"
mutedReactions: "Mute reactions"
muteThisReaction: "Mute this reaction"
unmuteThisReaction: "Unmute this reaction"
block: "Block"
unblock: "Unblock"
suspend: "Suspend"
@ -1333,7 +1336,9 @@ _announcement:
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."
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"
tooManyActiveAnnouncementDescription: "Having too many active announcements may worsen the user experience. Please consider archiving announcements that have become obsolete."
readConfirmTitle: "Mark as read?"

View File

@ -1252,7 +1252,7 @@ _announcement:
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."
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"
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?"

View File

@ -1237,7 +1237,7 @@ _bubbleGame:
_announcement:
forExistingUsers: "Pour les utilisateurs existants seulement"
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"
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 ?"

View File

@ -1254,7 +1254,7 @@ _announcement:
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."
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"
tooManyActiveAnnouncementDescription: "Terlalu banyak pengumuman dapat memperburuk pengalaman pengguna. Mohon pertimbangkan untuk mengarsipkan pengumuman yang sudah usang/tidak relevan."
readConfirmTitle: "Tandai telah dibaca?"

30
locales/index.d.ts vendored
View File

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

View File

@ -1252,7 +1252,7 @@ _announcement:
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."
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"
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?"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1254,7 +1254,7 @@ _announcement:
forExistingUsers: "僅限既有的使用者"
forExistingUsersDescription: "啟用代表僅向現存使用者顯示;停用代表張貼後註冊的新使用者也會看到。"
needConfirmationToRead: "必須確認才能標記為已讀"
needConfirmationToReadDescription: "啟用代表此公告將顯示對話方塊以確認是否標記為已讀,同時不會受「標記所有公告為已讀」功能影響。"
needConfirmationToReadDescription: "啟用代表此公告將顯示對話方塊以確認是否標記為已讀。"
end: "結束公告"
tooManyActiveAnnouncementDescription: "有過多公告可能會影響使用者體驗。請考慮歸檔已結束的公告。"
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;
redisForEndedPollNotificationQueue?: RedisOptionsSource;
redisForDeliverQueues?: Array<RedisOptionsSource>;
redisForInboxQueue?: RedisOptionsSource;
redisForInboxQueues?: Array<RedisOptionsSource>;
redisForDbQueue?: RedisOptionsSource;
redisForRelationshipQueue?: RedisOptionsSource;
redisForRelationshipQueues?: Array<RedisOptionsSource>;
redisForObjectStorageQueue?: RedisOptionsSource;
redisForWebhookDeliverQueue?: RedisOptionsSource;
redisForTimelines?: RedisOptionsSource;
@ -221,9 +221,9 @@ export type Config = {
redisForSystemQueue: RedisOptions & RedisOptionsSource;
redisForEndedPollNotificationQueue: RedisOptions & RedisOptionsSource;
redisForDeliverQueues: Array<RedisOptions & RedisOptionsSource>;
redisForInboxQueue: RedisOptions & RedisOptionsSource;
redisForInboxQueues: Array<RedisOptions & RedisOptionsSource>;
redisForDbQueue: RedisOptions & RedisOptionsSource;
redisForRelationshipQueue: RedisOptions & RedisOptionsSource;
redisForRelationshipQueues: Array<RedisOptions & RedisOptionsSource>;
redisForObjectStorageQueue: RedisOptions & RedisOptionsSource;
redisForWebhookDeliverQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
@ -297,9 +297,9 @@ export function loadConfig(): Config {
redisForSystemQueue: config.redisForSystemQueue ? convertRedisOptions(config.redisForSystemQueue, host) : redisForJobQueue,
redisForEndedPollNotificationQueue: config.redisForEndedPollNotificationQueue ? convertRedisOptions(config.redisForEndedPollNotificationQueue, 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,
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,
redisForWebhookDeliverQueue: config.redisForWebhookDeliverQueue ? convertRedisOptions(config.redisForWebhookDeliverQueue, host) : redisForJobQueue,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,

View File

@ -91,6 +91,7 @@ export class AnnouncementService {
forExistingUsers: values.forExistingUsers,
silence: values.silence,
needConfirmationToRead: values.needConfirmationToRead,
needEnrollmentTutorialToRead: values.needEnrollmentTutorialToRead,
closeDuration: values.closeDuration,
displayOrder: values.displayOrder,
userId: values.userId,
@ -200,6 +201,7 @@ export class AnnouncementService {
icon: values.icon,
forExistingUsers: values.forExistingUsers,
needConfirmationToRead: values.needConfirmationToRead,
needEnrollmentTutorialToRead: values.needEnrollmentTutorialToRead,
closeDuration: values.closeDuration,
displayOrder: values.displayOrder,
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 EndedPollNotificationQueue = Bull.Queue<EndedPollNotificationJobData>;
export type DeliverQueue = Queues<DeliverJobData>;
export type InboxQueue = Bull.Queue<InboxJobData>;
export type InboxQueue = Queues<InboxJobData>;
export type DbQueue = Bull.Queue;
export type RelationshipQueue = Bull.Queue<RelationshipJobData>;
export type RelationshipQueue = Queues<RelationshipJobData>;
export type ObjectStorageQueue = Bull.Queue;
export type WebhookDeliverQueue = Bull.Queue<WebhookDeliverJobData>;
@ -42,7 +42,7 @@ const $deliver: Provider = {
const $inbox: Provider = {
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],
};
@ -54,7 +54,7 @@ const $db: Provider = {
const $relationship: Provider = {
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],
};

View File

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

View File

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

View File

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

View File

@ -79,9 +79,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
private systemQueueWorker: Bull.Worker;
private dbQueueWorker: Bull.Worker;
private deliverQueueWorkers: Bull.Worker[];
private inboxQueueWorker: Bull.Worker;
private inboxQueueWorkers: Bull.Worker[];
private webhookDeliverQueueWorker: Bull.Worker;
private relationshipQueueWorker: Bull.Worker;
private relationshipQueueWorkers: Bull.Worker[];
private objectStorageQueueWorker: Bull.Worker;
private endedPollNotificationQueueWorker: Bull.Worker;
@ -243,8 +243,10 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#endregion
//#region inbox
this.inboxQueueWorker = new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
...baseWorkerOptions(this.config.redisForInboxQueue, this.config.bullmqWorkerOptions, QUEUE.INBOX),
this.inboxQueueWorkers = this.config.redisForInboxQueues
.filter((_, index) => process.env.QUEUE_WORKER_INDEX == null || index === Number.parseInt(process.env.QUEUE_WORKER_INDEX, 10))
.map(config => new Bull.Worker(QUEUE.INBOX, (job) => this.inboxProcessorService.process(job), {
...baseWorkerOptions(config, this.config.bullmqWorkerOptions, QUEUE.INBOX),
autorun: false,
concurrency: this.config.inboxJobConcurrency ?? 16,
limiter: {
@ -254,16 +256,18 @@ export class QueueProcessorService implements OnApplicationShutdown {
settings: {
backoffStrategy: httpRelatedBackoff,
},
});
}));
const inboxLogger = this.logger.createSubLogger('inbox');
this.inboxQueueWorkers.forEach((worker, index) => {
const inboxLogger = this.logger.createSubLogger(`inbox-${index}`);
this.inboxQueueWorker
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}`));
});
//#endregion
//#region webhook deliver
@ -291,7 +295,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
//#endregion
//#region relationship
this.relationshipQueueWorker = new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
this.relationshipQueueWorkers = this.config.redisForRelationshipQueues
.filter((_, index) => process.env.QUEUE_WORKER_INDEX == null || index === Number.parseInt(process.env.QUEUE_WORKER_INDEX, 10))
.map(config => new Bull.Worker(QUEUE.RELATIONSHIP, (job) => {
switch (job.name) {
case 'follow': return this.relationshipProcessorService.processFollow(job);
case 'unfollow': return this.relationshipProcessorService.processUnfollow(job);
@ -300,23 +306,25 @@ export class QueueProcessorService implements OnApplicationShutdown {
default: throw new Error(`unrecognized job type ${job.name} for relationship`);
}
}, {
...baseWorkerOptions(this.config.redisForRelationshipQueue, this.config.bullmqWorkerOptions, QUEUE.RELATIONSHIP),
...baseWorkerOptions(config, this.config.bullmqWorkerOptions, QUEUE.RELATIONSHIP),
autorun: false,
concurrency: this.config.relationshipJobConcurrency ?? 16,
limiter: {
max: this.config.relationshipJobPerSec ?? 64,
duration: 1000,
},
});
}));
const relationshipLogger = this.logger.createSubLogger('relationship');
this.relationshipQueueWorkers.forEach((worker, index) => {
const relationshipLogger = this.logger.createSubLogger(`relationship-${index}`);
this.relationshipQueueWorker
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}`));
});
//#endregion
//#region object storage
@ -356,9 +364,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker.run(),
this.dbQueueWorker.run(),
...this.deliverQueueWorkers.map(worker => worker.run()),
this.inboxQueueWorker.run(),
this.inboxQueueWorkers.map(worker => worker.run()),
this.webhookDeliverQueueWorker.run(),
this.relationshipQueueWorker.run(),
this.relationshipQueueWorkers.map(worker => worker.run()),
this.objectStorageQueueWorker.run(),
this.endedPollNotificationQueueWorker.run(),
]);
@ -370,9 +378,9 @@ export class QueueProcessorService implements OnApplicationShutdown {
this.systemQueueWorker.close(),
this.dbQueueWorker.close(),
...this.deliverQueueWorkers.map(worker => worker.close()),
this.inboxQueueWorker.close(),
this.inboxQueueWorkers.map(worker => worker.close()),
this.webhookDeliverQueueWorker.close(),
this.relationshipQueueWorker.close(),
this.relationshipQueueWorkers.map(worker => worker.close()),
this.objectStorageQueueWorker.close(),
this.endedPollNotificationQueueWorker.close(),
]);

View File

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

View File

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

View File

@ -37,6 +37,7 @@ export const paramDef = {
display: { type: 'string', enum: ['normal', 'banner', 'dialog'] },
forExistingUsers: { type: 'boolean' },
needConfirmationToRead: { type: 'boolean' },
needEnrollmentTutorialToRead: { type: 'boolean' },
closeDuration: { type: 'number', default: 0 },
displayOrder: { type: 'number', default: 0 },
silence: { type: 'boolean' },
@ -68,6 +69,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
icon: ps.icon,
forExistingUsers: ps.forExistingUsers,
needConfirmationToRead: ps.needConfirmationToRead,
needEnrollmentTutorialToRead: ps.needEnrollmentTutorialToRead,
closeDuration: ps.closeDuration,
displayOrder: ps.displayOrder,
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) => {
const jobs = await this.deliverQueue.getJobs(['delayed']);
const res = [] as [string, number][];
const res = new Map<string, number>();
for (const job of jobs) {
let host: string;
@ -68,17 +68,15 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
continue;
}
const found = res.find(x => x[0] === host);
const found = res.get(host);
if (found) {
found[1]++;
res.set(host, found + 1);
} else {
res.push([host, 1]);
res.set(host, 1);
}
}
res.sort((a, b) => b[1] - a[1]);
return res;
return Array.from(res.entries()).sort((a, b) => b[1] - a[1]);
});
}
}

View File

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

View File

@ -64,11 +64,12 @@ export class JWTIdentifyProviderService {
fastify.all<{
Params: { serviceId: string };
Querystring?: { serviceurl?: string, return_to?: string };
Body?: { serviceurl?: string, return_to?: string };
Querystring?: { serviceurl?: string, return_to?: string, prompt?: string };
Body?: { serviceurl?: string, return_to?: string, prompt?: string };
}>('/:serviceId', async (request, reply) => {
const serviceId = request.params.serviceId;
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' });
if (!ssoServiceProvider) {
@ -101,6 +102,7 @@ export class JWTIdentifyProviderService {
transactionId: transactionId,
serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer,
kind: 'jwt',
prompt: prompt,
});
});

View File

@ -201,13 +201,14 @@ export class SAMLIdentifyProviderService {
fastify.all<{
Params: { serviceId: string };
Querystring?: { SAMLRequest?: string; RelayState?: string };
Body?: { SAMLRequest?: string; RelayState?: string };
Querystring?: { SAMLRequest?: string; RelayState?: string, prompt?: string };
Body?: { SAMLRequest?: string; RelayState?: string, prompt?: string };
}>('/:serviceId', async (request, reply) => {
const serviceId = request.params.serviceId;
const binding = request.query?.SAMLRequest ? 'redirect' : 'post';
const samlRequest = request.query?.SAMLRequest ?? request.body?.SAMLRequest;
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()) });
if (!ssoServiceProvider) {
@ -268,6 +269,7 @@ export class SAMLIdentifyProviderService {
transactionId: transactionId,
serviceName: ssoServiceProvider.name ?? ssoServiceProvider.issuer,
kind: 'saml',
prompt: prompt,
});
} catch (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 * as Acct from '@/misc/acct.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 { NoteEntityService } from '@/core/entities/NoteEntityService.js';
import { PageEntityService } from '@/core/entities/PageEntityService.js';
@ -110,6 +119,7 @@ export class ClientServerService {
@Inject('queue:deliver') public deliverQueue: DeliverQueue,
@Inject('queue:inbox') public inboxQueue: InboxQueue,
@Inject('queue:db') public dbQueue: DbQueue,
@Inject('queue:relationship') public relationshipQueue: RelationshipQueue,
@Inject('queue:objectStorage') public objectStorageQueue: ObjectStorageQueue,
@Inject('queue:webhookDeliver') public webhookDeliverQueue: WebhookDeliverQueue,
) {
@ -235,12 +245,13 @@ export class ClientServerService {
queues: [
this.systemQueue,
this.endedPollNotificationQueue,
this.inboxQueue,
this.dbQueue,
this.objectStorageQueue,
this.webhookDeliverQueue,
].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,
});

View File

@ -4,3 +4,4 @@ block meta
meta(name='misskey:sso:transaction-id' content=transactionId)
meta(name='misskey:sso:service-name' content=serviceName)
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 * as sound from '@/scripts/sound.js';
import { $i, signout, updateAccount } from '@/account.js';
import { instance } from '@/instance.js';
import { ColdDeviceStorage, defaultStore } from '@/store.js';
import { makeHotkey } from '@/scripts/hotkey.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
@ -237,10 +236,10 @@ export async function mainBoot() {
}
}
const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
}
// const modifiedVersionMustProminentlyOfferInAgplV3Section13Read = miLocalStorage.getItem('modifiedVersionMustProminentlyOfferInAgplV3Section13Read');
// if (modifiedVersionMustProminentlyOfferInAgplV3Section13Read !== 'true' && instance.repositoryUrl !== 'https://github.com/misskey-dev/misskey') {
// popup(defineAsyncComponent(() => import('@/components/MkSourceCodeAvailablePopup.vue')), {}, {}, 'closed');
// }
if ('Notification' in window) {
// 許可を得ていなかったらリクエスト

View File

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

View File

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

View File

@ -19,13 +19,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<Mfm :text="announcement.text"/>
<img v-if="announcement.imageUrl" :src="announcement.imageUrl"/>
</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>
</MkModal>
</template>
<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 os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
@ -46,6 +49,18 @@ const secVisible = ref(true);
const sec = ref(props.announcement.closeDuration);
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) {
const confirm = await os.confirm({
type: 'question',

View File

@ -6,12 +6,15 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<button
class="_button"
:class="[$style.root, { [$style.wait]: wait, [$style.active]: userDetailed.isFollowing || userDetailed.hasPendingFollowRequestFromYou, [$style.full]: full, [$style.large]: large }]"
:disabled="wait"
: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 || userDetailed.isBlocked"
@click="onClick"
>
<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>
</template>
<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() {
wait.value = true;
try {
if (userDetailed.value.isFollowing) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.tsx.unfollowConfirm({ name: (userDetailed.value.name || userDetailed.value.username) ?? i18n.ts.user }),
});
if (canceled) return;
if (!await getConfirmed(i18n.tsx.unfollowConfirm({ name: (userDetailed.value.name || userDetailed.value.username) ?? i18n.ts.user }))) return;
await misskeyApi('following/delete', {
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 {
if (defaultStore.state.alwaysConfirmFollow) {
const { canceled } = await os.confirm({
type: 'question',
text: i18n.tsx.followConfirm({ name: props.user.name || props.user.username }),
});
const confirmed = await getConfirmed(i18n.tsx.followConfirm({ name: props.user.name || props.user.username }));
if (canceled) {
if (!confirmed) {
wait.value = false;
return;
}
@ -214,6 +233,12 @@ onBeforeUnmount(() => {
}
}
&.activeBlocked {
color: var(--fgOnAccent);
border-color: var(--error);
background: var(--error);
}
&.wait {
cursor: wait !important;
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>
</button>
</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">
<template #default="{ items }">
<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 replies = ref<Misskey.entities.Note[]>([]);
const canRenote = computed(() => ['public', 'home'].includes(appearNote.value.visibility) || appearNote.value.userId === $i?.id);
const mutedReactions = ref<string[]>(defaultStore.state.mutedReactions);
const keymap = {
'r': () => reply(true),
@ -316,6 +321,7 @@ provide('react', (reaction: string) => {
const tab = ref(props.initialTab);
const reactionTabType = ref<string | null>(null);
const reactionTabTypeTrimLocal = computed(() => reactionTabType.value?.replace('@.', '') ?? null);
const renotesPagination = computed<Paging>(() => ({
endpoint: 'notes/renotes',
@ -497,6 +503,18 @@ async function translate(): Promise<void> {
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 {
if (!isMyRenote) return;
pleaseLogin();
@ -782,6 +800,10 @@ function loadConversation() {
margin-bottom: 8px;
}
.reactionMuteButton {
margin-bottom: 8px;
}
.reactionTab {
padding: 4px 6px;
border: solid 1px var(--divider);

View File

@ -22,6 +22,7 @@ import * as Misskey from 'misskey-js';
import { inject, watch, ref } from 'vue';
import XReaction from '@/components/MkReactionsViewer.reaction.vue';
import { defaultStore } from '@/store.js';
import { $i } from '@/account.js';
const props = withDefaults(defineProps<{
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];
}
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) {
if (!mock) return;
@ -80,7 +88,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
newReactions.push([props.note.myReaction, newSource[props.note.myReaction]]);
}
reactions.value = newReactions;
reactions.value = newReactions.filter(shouldDisplayReaction);
}, { immediate: true, deep: true });
</script>
@ -104,6 +112,7 @@ watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumbe
flex-wrap: wrap;
align-items: center;
margin: 4px -2px 0 -2px;
max-width: 100%;
&:empty {
display: none;

View File

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

View File

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

View File

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

View File

@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<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
v-else
:class="[$style.root, { [$style.normal]: normal, [$style.noStyle]: noStyle }]"
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<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 { defaultStore } from '@/store.js';
import { customEmojisMap } from '@/custom-emojis.js';
@ -73,6 +73,10 @@ const url = computed(() => {
: proxied;
});
watch(url, (newValue) => {
errored.value = (newValue === undefined);
});
const alt = computed(() => `:${customEmojiName.value}:`);
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">
{{ i18n.ts._announcement.needConfirmationToRead }}
</MkSwitch>
<MkSwitch v-model="announcement.needEnrollmentTutorialToRead" :helpText="i18n.ts._announcement.needEnrollmentTutorialToReadDescription">
{{ i18n.ts._announcement.needEnrollmentTutorialToRead }}
</MkSwitch>
<MkInput v-model="announcement.closeDuration" type="number">
<template #label>{{ i18n.ts.dialogCloseDuration }}</template>
<template #suffix>{{ i18n.ts._time.second }}</template>

View File

@ -34,7 +34,10 @@
</MkA>
</div>
<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>
<MkError v-else-if="error" @retry="fetch()"/>
@ -45,7 +48,7 @@
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, defineAsyncComponent } from 'vue';
import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import * as os from '@/os.js';
@ -75,6 +78,17 @@ function fetch() {
}
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) {
const confirm = await os.confirm({
type: 'question',

View File

@ -36,7 +36,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
</div>
<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>
</section>
</MkPagination>
@ -47,7 +50,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import { ref, computed, watch, defineAsyncComponent } from 'vue';
import MkPagination from '@/components/MkPagination.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
@ -81,6 +84,17 @@ const paginationEl = ref<InstanceType<typeof MkPagination>>();
const tab = ref('current');
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) {
const confirm = await os.confirm({
type: 'question',

View File

@ -12,6 +12,23 @@ SPDX-License-Identifier: AGPL-3.0-only
<XWordMute :muted="$i.mutedWords" @save="saveMutedWords"/>
</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>
<template #icon><i class="ti ti-planet-off"></i></template>
<template #label>{{ i18n.ts.instanceMute }}</template>
@ -119,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<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 XWordMute from './mute-block.word-mute.vue';
import MkPagination from '@/components/MkPagination.vue';
@ -132,6 +149,9 @@ import { misskeyApi } from '@/scripts/misskey-api.js';
import { infoImageUrl } from '@/instance.js';
import { signinRequired } from '@/account.js';
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();
@ -154,6 +174,37 @@ const expandedRenoteMuteItems = ref([]);
const expandedMuteItems = 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) {
os.popupMenu([{
text: i18n.ts.renoteUnmute,
@ -263,4 +314,9 @@ definePageMetadata(() => ({
transform: rotateX(180deg);
}
}
.emojisItem{
display: inline-block;
padding: 8px;
}
</style>

View File

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

View File

@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<script lang="ts" setup>
import { ref, nextTick } from 'vue';
import { ref, nextTick, onMounted } from 'vue';
import MkSignin from '@/components/MkSignin.vue';
import MkButton from '@/components/MkButton.vue';
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 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 postBindingForm = ref<HTMLFormElement | null>(null);
@ -90,6 +91,12 @@ async function authorize(): Promise<void> {
}
}
onMounted(() => {
if ($i && prompt === 'none') {
onAccept();
}
});
definePageMetadata(() => ({
title: 'Single Sign-On',
icon: 'ti ti-apps',

View File

@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="profile _gaps">
<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"/>
<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"/>
<div class="bottom">
<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.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">
@ -32,10 +31,12 @@ SPDX-License-Identifier: AGPL-3.0-only
</button>
</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">
<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>
<MkAvatar class="avatar" :user="user" indicator/>
@ -43,7 +44,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkUserName :user="user" :nowrap="false" class="name"/>
<div class="bottom">
<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.isBot" :title="i18n.ts.isBot"><i class="ti ti-robot"></i></span>
</div>
@ -186,7 +186,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div class="contents _gaps">
<div v-if="user.isBlocked !== true" class="contents _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"/>
</div>
@ -438,8 +438,8 @@ onUnmounted(() => {
> .punished {
font-size: 0.8em;
padding: 16px;
background: var(--infoWarnBg);
color: var(--infoWarnFg);
background: var(--infoBg);
color: var(--infoFg);
border-radius: var(--radius);
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"/>
<XFlashs v-else-if="tab === 'flashs'" key="flashs" :user="user"/>
<XGallery v-else-if="tab === 'gallery'" key="gallery" :user="user"/>
<XRaw v-else-if="tab === 'raw'" key="raw" :user="user"/>
</MkHorizontalSwipe>
</div>
<div v-else-if="error">
<MkError @retry="fetchUser()"/>
</div>
<div v-else-if="userstatus">
<MkUserNotFound v-if="userstatus === 'notfound'"/>
<MkUserSuspended v-else-if="userstatus === 'suspended'"/>
<MkUserNotFound v-if="userstatus === 'notfound' || userstatus === 'suspended'"/>
</div>
<MkLoading v-else/>
</div>
@ -45,7 +43,6 @@ import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
import { $i, iAmModerator } from '@/account.js';
import MkUserNotFound from '@/components/MkUserNotFound.vue';
import MkUserSuspended from '@/components/MkUserSuspended.vue';
import MkHorizontalSwipe from '@/components/MkHorizontalSwipe.vue';
const XHome = defineAsyncComponent(() => import('./home.vue'));
@ -58,7 +55,6 @@ const XLists = defineAsyncComponent(() => import('./lists.vue'));
const XPages = defineAsyncComponent(() => import('./pages.vue'));
const XFlashs = defineAsyncComponent(() => import('./flashs.vue'));
const XGallery = defineAsyncComponent(() => import('./gallery.vue'));
const XRaw = defineAsyncComponent(() => import('./raw.vue'));
const props = withDefaults(defineProps<{
acct: string;
@ -135,10 +131,6 @@ const headerTabs = computed(() => user.value ? [{
key: 'gallery',
title: i18n.ts.gallery,
icon: 'ti ti-icons',
}, {
key: 'raw',
title: 'Raw',
icon: 'ti ti-code',
}] : []);
definePageMetadata(() => ({

View File

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

View File

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

View File

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