summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.config/cypress-devcontainer.yml13
-rw-r--r--.config/example.yml14
-rw-r--r--.gitignore2
-rw-r--r--CHANGELOG.md55
-rw-r--r--CONTRIBUTING.md51
-rw-r--r--cypress/e2e/basic.cy.ts17
-rw-r--r--cypress/support/commands.ts7
-rw-r--r--idea/MkAbuseReport.stories.impl.ts (renamed from packages/frontend/src/components/MkAbuseReport.stories.impl.ts)4
-rw-r--r--idea/MkDisableSection.vue2
-rw-r--r--package.json2
-rw-r--r--packages/backend/assets/tabler-badges/login-2.pngbin0 -> 3770 bytes
-rw-r--r--packages/backend/eslint.config.js2
-rw-r--r--packages/backend/jest.config.fed.cjs13
-rw-r--r--packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js16
-rw-r--r--packages/backend/migration/1728085812127-refine-abuse-user-report.js18
-rw-r--r--packages/backend/migration/1728550878802-testcaptcha.js16
-rw-r--r--packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js14
-rw-r--r--packages/backend/package.json36
-rw-r--r--packages/backend/src/config.ts4
-rw-r--r--packages/backend/src/core/AbuseReportNotificationService.ts43
-rw-r--r--packages/backend/src/core/AbuseReportService.ts94
-rw-r--r--packages/backend/src/core/AccountMoveService.ts16
-rw-r--r--packages/backend/src/core/AnnouncementService.ts7
-rw-r--r--packages/backend/src/core/CaptchaService.ts13
-rw-r--r--packages/backend/src/core/CoreModule.ts5
-rw-r--r--packages/backend/src/core/CustomEmojiService.ts29
-rw-r--r--packages/backend/src/core/FederatedInstanceService.ts20
-rw-r--r--packages/backend/src/core/FetchInstanceMetadataService.ts2
-rw-r--r--packages/backend/src/core/FlashService.ts40
-rw-r--r--packages/backend/src/core/NoteCreateService.ts24
-rw-r--r--packages/backend/src/core/NoteDeleteService.ts27
-rw-r--r--packages/backend/src/core/QueueService.ts7
-rw-r--r--packages/backend/src/core/RoleService.ts75
-rw-r--r--packages/backend/src/core/SignupService.ts4
-rw-r--r--packages/backend/src/core/SystemWebhookService.ts9
-rw-r--r--packages/backend/src/core/UserFollowingService.ts60
-rw-r--r--packages/backend/src/core/WebhookTestService.ts39
-rw-r--r--packages/backend/src/core/activitypub/models/ApPersonService.ts16
-rw-r--r--packages/backend/src/core/entities/AbuseUserReportEntityService.ts2
-rw-r--r--packages/backend/src/core/entities/FlashEntityService.ts41
-rw-r--r--packages/backend/src/core/entities/MetaEntityService.ts1
-rw-r--r--packages/backend/src/core/entities/NoteEntityService.ts32
-rw-r--r--packages/backend/src/core/entities/UserEntityService.ts13
-rw-r--r--packages/backend/src/misc/is-renote.ts2
-rw-r--r--packages/backend/src/models/AbuseUserReport.ts18
-rw-r--r--packages/backend/src/models/Flash.ts5
-rw-r--r--packages/backend/src/models/Meta.ts15
-rw-r--r--packages/backend/src/models/Notification.ts6
-rw-r--r--packages/backend/src/models/SystemWebhook.ts4
-rw-r--r--packages/backend/src/models/json-schema/meta.ts4
-rw-r--r--packages/backend/src/models/json-schema/notification.ts10
-rw-r--r--packages/backend/src/models/json-schema/user.ts42
-rw-r--r--packages/backend/src/queue/QueueProcessorModule.ts3
-rw-r--r--packages/backend/src/queue/QueueProcessorService.ts5
-rw-r--r--packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts292
-rw-r--r--packages/backend/src/queue/processors/DeliverProcessorService.ts31
-rw-r--r--packages/backend/src/queue/processors/InboxProcessorService.ts22
-rw-r--r--packages/backend/src/server/api/ApiServerService.ts19
-rw-r--r--packages/backend/src/server/api/EndpointsModule.ts8
-rw-r--r--packages/backend/src/server/api/SigninApiService.ts104
-rw-r--r--packages/backend/src/server/api/SigninService.ts26
-rw-r--r--packages/backend/src/server/api/SignupApiService.ts7
-rw-r--r--packages/backend/src/server/api/endpoints.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts16
-rw-r--r--packages/backend/src/server/api/endpoints/admin/accounts/create.ts93
-rw-r--r--packages/backend/src/server/api/endpoints/admin/emoji/update.ts33
-rw-r--r--packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts55
-rw-r--r--packages/backend/src/server/api/endpoints/admin/meta.ts18
-rw-r--r--packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/show-users.ts4
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts58
-rw-r--r--packages/backend/src/server/api/endpoints/admin/update-meta.ts20
-rw-r--r--packages/backend/src/server/api/endpoints/flash/featured.ts22
-rw-r--r--packages/backend/src/server/api/endpoints/i/update.ts22
-rw-r--r--packages/backend/src/server/web/boot.js2
-rw-r--r--packages/backend/src/server/web/style.css8
-rw-r--r--packages/backend/src/server/web/style.embed.css10
-rw-r--r--packages/backend/src/types.ts17
-rw-r--r--packages/backend/test-federation/.config/example.conf70
-rw-r--r--packages/backend/test-federation/.config/example.default.yml25
-rw-r--r--packages/backend/test-federation/.config/example.docker.env5
-rw-r--r--packages/backend/test-federation/.gitignore6
-rw-r--r--packages/backend/test-federation/README.md24
-rw-r--r--packages/backend/test-federation/compose.a.yml64
-rw-r--r--packages/backend/test-federation/compose.b.yml64
-rw-r--r--packages/backend/test-federation/compose.override.yaml117
-rw-r--r--packages/backend/test-federation/compose.tpl.yml101
-rw-r--r--packages/backend/test-federation/compose.yml133
-rw-r--r--packages/backend/test-federation/daemon.ts38
-rw-r--r--packages/backend/test-federation/eslint.config.js21
-rw-r--r--packages/backend/test-federation/setup.sh35
-rw-r--r--packages/backend/test-federation/test/abuse-report.test.ts52
-rw-r--r--packages/backend/test-federation/test/block.test.ts224
-rw-r--r--packages/backend/test-federation/test/drive.test.ts175
-rw-r--r--packages/backend/test-federation/test/emoji.test.ts97
-rw-r--r--packages/backend/test-federation/test/move.test.ts52
-rw-r--r--packages/backend/test-federation/test/note.test.ts317
-rw-r--r--packages/backend/test-federation/test/notification.test.ts107
-rw-r--r--packages/backend/test-federation/test/timeline.test.ts328
-rw-r--r--packages/backend/test-federation/test/user.test.ts560
-rw-r--r--packages/backend/test-federation/test/utils.ts309
-rw-r--r--packages/backend/test-federation/tsconfig.json114
-rw-r--r--packages/backend/test/e2e/2fa.ts98
-rw-r--r--packages/backend/test/e2e/endpoints.ts8
-rw-r--r--packages/backend/test/e2e/synalio/abuse-report.ts6
-rw-r--r--packages/backend/test/e2e/users.ts15
-rw-r--r--packages/backend/test/unit/AbuseReportNotificationService.ts6
-rw-r--r--packages/backend/test/unit/FetchInstanceMetadataService.ts20
-rw-r--r--packages/backend/test/unit/FlashService.ts152
-rw-r--r--packages/backend/test/unit/RoleService.ts150
-rw-r--r--packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts379
-rw-r--r--packages/frontend-embed/package.json12
-rw-r--r--packages/frontend-embed/src/components/EmCustomEmoji.vue2
-rw-r--r--packages/frontend-embed/src/components/EmLoading.vue2
-rw-r--r--packages/frontend-embed/src/components/EmMediaBanner.vue12
-rw-r--r--packages/frontend-embed/src/components/EmMediaImage.vue18
-rw-r--r--packages/frontend-embed/src/components/EmMediaVideo.vue8
-rw-r--r--packages/frontend-embed/src/components/EmMention.vue4
-rw-r--r--packages/frontend-embed/src/components/EmMfm.ts19
-rw-r--r--packages/frontend-embed/src/components/EmNote.vue48
-rw-r--r--packages/frontend-embed/src/components/EmNoteDetailed.vue24
-rw-r--r--packages/frontend-embed/src/components/EmNoteHeader.vue2
-rw-r--r--packages/frontend-embed/src/components/EmNoteSimple.vue2
-rw-r--r--packages/frontend-embed/src/components/EmNoteSub.vue4
-rw-r--r--packages/frontend-embed/src/components/EmNotes.vue4
-rw-r--r--packages/frontend-embed/src/components/EmPoll.vue14
-rw-r--r--packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue10
-rw-r--r--packages/frontend-embed/src/components/EmSubNoteContent.vue14
-rw-r--r--packages/frontend-embed/src/components/EmTime.vue4
-rw-r--r--packages/frontend-embed/src/components/EmTimelineContainer.vue4
-rw-r--r--packages/frontend-embed/src/pages/clip.vue6
-rw-r--r--packages/frontend-embed/src/pages/note.vue2
-rw-r--r--packages/frontend-embed/src/pages/tag.vue6
-rw-r--r--packages/frontend-embed/src/pages/user-timeline.vue2
-rw-r--r--packages/frontend-embed/src/style.scss72
-rw-r--r--packages/frontend-embed/src/theme.ts2
-rw-r--r--packages/frontend-embed/src/ui.vue6
-rw-r--r--packages/frontend-embed/vite.config.ts5
-rw-r--r--packages/frontend-shared/js/const.ts1
-rw-r--r--packages/frontend-shared/themes/_dark.json53
-rw-r--r--packages/frontend-shared/themes/_light.json53
-rw-r--r--packages/frontend-shared/themes/d-astro.json53
-rw-r--r--packages/frontend-shared/themes/d-u0.json53
-rw-r--r--packages/frontend-shared/themes/l-u0.json53
-rw-r--r--packages/frontend-shared/themes/l-vivid.json53
-rw-r--r--packages/frontend/.storybook/generate.tsx13
-rw-r--r--packages/frontend/assets/testcaptcha.pngbin0 -> 2634 bytes
-rw-r--r--packages/frontend/package.json55
-rw-r--r--packages/frontend/src/_dev_boot_.ts2
-rw-r--r--packages/frontend/src/account.ts8
-rw-r--r--packages/frontend/src/boot/common.ts12
-rw-r--r--packages/frontend/src/components/MkAbuseReport.vue214
-rw-r--r--packages/frontend/src/components/MkAccountMoved.vue6
-rw-r--r--packages/frontend/src/components/MkAnalogClock.vue6
-rw-r--r--packages/frontend/src/components/MkAnnouncementDialog.vue10
-rw-r--r--packages/frontend/src/components/MkAntennaEditor.vue2
-rw-r--r--packages/frontend/src/components/MkAsUi.vue4
-rw-r--r--packages/frontend/src/components/MkAutocomplete.vue6
-rw-r--r--packages/frontend/src/components/MkButton.vue22
-rw-r--r--packages/frontend/src/components/MkCaptcha.vue28
-rw-r--r--packages/frontend/src/components/MkChannelFollowButton.vue16
-rw-r--r--packages/frontend/src/components/MkChannelPreview.vue12
-rw-r--r--packages/frontend/src/components/MkChart.vue4
-rw-r--r--packages/frontend/src/components/MkChartLegend.vue4
-rw-r--r--packages/frontend/src/components/MkClipPreview.vue6
-rw-r--r--packages/frontend/src/components/MkCode.core.vue2
-rw-r--r--packages/frontend/src/components/MkCode.vue6
-rw-r--r--packages/frontend/src/components/MkCodeEditor.vue14
-rw-r--r--packages/frontend/src/components/MkCodeInline.vue2
-rw-r--r--packages/frontend/src/components/MkColorInput.vue14
-rw-r--r--packages/frontend/src/components/MkContainer.vue15
-rw-r--r--packages/frontend/src/components/MkCropperDialog.vue6
-rw-r--r--packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue10
-rw-r--r--packages/frontend/src/components/MkDateSeparatedList.vue21
-rw-r--r--packages/frontend/src/components/MkDialog.vue8
-rw-r--r--packages/frontend/src/components/MkDivider.vue2
-rw-r--r--packages/frontend/src/components/MkDonation.vue6
-rw-r--r--packages/frontend/src/components/MkDrive.file.vue8
-rw-r--r--packages/frontend/src/components/MkDrive.folder.vue16
-rw-r--r--packages/frontend/src/components/MkDrive.vue6
-rw-r--r--packages/frontend/src/components/MkDriveFileThumbnail.vue4
-rw-r--r--packages/frontend/src/components/MkEmbedCodeGenDialog.vue8
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.section.vue4
-rw-r--r--packages/frontend/src/components/MkEmojiPicker.vue24
-rw-r--r--packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts83
-rw-r--r--packages/frontend/src/components/MkExtensionInstaller.vue146
-rw-r--r--packages/frontend/src/components/MkFileListForAdmin.vue2
-rw-r--r--packages/frontend/src/components/MkFlashPreview.vue5
-rw-r--r--packages/frontend/src/components/MkFoldableSection.vue10
-rw-r--r--packages/frontend/src/components/MkFolder.vue41
-rw-r--r--packages/frontend/src/components/MkFollowButton.vue16
-rw-r--r--packages/frontend/src/components/MkFormDialog.file.vue2
-rw-r--r--packages/frontend/src/components/MkFormFooter.vue2
-rw-r--r--packages/frontend/src/components/MkFukidashi.vue100
-rw-r--r--packages/frontend/src/components/MkGalleryPostPreview.vue2
-rw-r--r--packages/frontend/src/components/MkGoogle.vue4
-rw-r--r--packages/frontend/src/components/MkInfo.vue10
-rw-r--r--packages/frontend/src/components/MkInput.vue14
-rw-r--r--packages/frontend/src/components/MkInstanceCardMini.vue6
-rw-r--r--packages/frontend/src/components/MkInstanceStats.vue6
-rw-r--r--packages/frontend/src/components/MkInviteCode.vue4
-rw-r--r--packages/frontend/src/components/MkLaunchPad.vue11
-rw-r--r--packages/frontend/src/components/MkMediaAudio.vue12
-rw-r--r--packages/frontend/src/components/MkMediaBanner.vue1
-rw-r--r--packages/frontend/src/components/MkMediaImage.vue22
-rw-r--r--packages/frontend/src/components/MkMediaList.vue14
-rw-r--r--packages/frontend/src/components/MkMediaRange.vue4
-rw-r--r--packages/frontend/src/components/MkMediaVideo.vue18
-rw-r--r--packages/frontend/src/components/MkMention.vue15
-rw-r--r--packages/frontend/src/components/MkMenu.vue45
-rw-r--r--packages/frontend/src/components/MkMiniChart.vue2
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue26
-rw-r--r--packages/frontend/src/components/MkNote.vue59
-rw-r--r--packages/frontend/src/components/MkNoteDetailed.vue38
-rw-r--r--packages/frontend/src/components/MkNoteHeader.vue22
-rw-r--r--packages/frontend/src/components/MkNoteSimple.vue2
-rw-r--r--packages/frontend/src/components/MkNoteSub.vue4
-rw-r--r--packages/frontend/src/components/MkNotes.vue8
-rw-r--r--packages/frontend/src/components/MkNotification.vue27
-rw-r--r--packages/frontend/src/components/MkNotifications.vue2
-rw-r--r--packages/frontend/src/components/MkNumberDiff.vue4
-rw-r--r--packages/frontend/src/components/MkObjectView.value.vue8
-rw-r--r--packages/frontend/src/components/MkOmit.vue8
-rw-r--r--packages/frontend/src/components/MkPagePreview.vue15
-rw-r--r--packages/frontend/src/components/MkPageWindow.vue4
-rw-r--r--packages/frontend/src/components/MkPoll.vue14
-rw-r--r--packages/frontend/src/components/MkPostForm.vue30
-rw-r--r--packages/frontend/src/components/MkPostFormAttaches.vue2
-rw-r--r--packages/frontend/src/components/MkRadio.vue20
-rw-r--r--packages/frontend/src/components/MkRadios.vue2
-rw-r--r--packages/frontend/src/components/MkRange.vue14
-rw-r--r--packages/frontend/src/components/MkReactionEffect.vue2
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.details.vue2
-rw-r--r--packages/frontend/src/components/MkReactionsViewer.reaction.vue10
-rw-r--r--packages/frontend/src/components/MkRemoteCaution.vue8
-rw-r--r--packages/frontend/src/components/MkRetentionLineChart.vue2
-rw-r--r--packages/frontend/src/components/MkRippleEffect.vue4
-rw-r--r--packages/frontend/src/components/MkRolePreview.vue8
-rw-r--r--packages/frontend/src/components/MkSelect.vue14
-rw-r--r--packages/frontend/src/components/MkSignin.input.vue206
-rw-r--r--packages/frontend/src/components/MkSignin.passkey.vue92
-rw-r--r--packages/frontend/src/components/MkSignin.password.vue188
-rw-r--r--packages/frontend/src/components/MkSignin.totp.vue74
-rw-r--r--packages/frontend/src/components/MkSignin.vue579
-rw-r--r--packages/frontend/src/components/MkSigninDialog.vue85
-rw-r--r--packages/frontend/src/components/MkSignupDialog.form.vue133
-rw-r--r--packages/frontend/src/components/MkSignupDialog.rules.vue16
-rw-r--r--packages/frontend/src/components/MkSignupDialog.vue4
-rw-r--r--packages/frontend/src/components/MkSourceCodeAvailablePopup.vue6
-rw-r--r--packages/frontend/src/components/MkSubNoteContent.vue14
-rw-r--r--packages/frontend/src/components/MkSuperMenu.vue16
-rw-r--r--packages/frontend/src/components/MkSwitch.button.vue12
-rw-r--r--packages/frontend/src/components/MkSwitch.vue8
-rw-r--r--packages/frontend/src/components/MkSystemWebhookEditor.vue28
-rw-r--r--packages/frontend/src/components/MkTab.vue8
-rw-r--r--packages/frontend/src/components/MkTagCloud.vue2
-rw-r--r--packages/frontend/src/components/MkTextarea.vue14
-rw-r--r--packages/frontend/src/components/MkTokenGenerateWindow.vue8
-rw-r--r--packages/frontend/src/components/MkTooltip.vue2
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.Note.vue10
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.PostNote.vue12
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.Sensitive.vue14
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.Timeline.vue12
-rw-r--r--packages/frontend/src/components/MkTutorialDialog.vue8
-rw-r--r--packages/frontend/src/components/MkUpdated.vue4
-rw-r--r--packages/frontend/src/components/MkUrlPreview.vue11
-rw-r--r--packages/frontend/src/components/MkUserAnnouncementEditDialog.vue12
-rw-r--r--packages/frontend/src/components/MkUserCardMini.vue6
-rw-r--r--packages/frontend/src/components/MkUserInfo.vue12
-rw-r--r--packages/frontend/src/components/MkUserList.vue2
-rw-r--r--packages/frontend/src/components/MkUserOnlineIndicator.vue2
-rw-r--r--packages/frontend/src/components/MkUserPopup.vue10
-rw-r--r--packages/frontend/src/components/MkUserSelectDialog.vue4
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.vue2
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Profile.vue5
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.User.vue6
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.vue10
-rw-r--r--packages/frontend/src/components/MkVisibilityPicker.vue2
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue2
-rw-r--r--packages/frontend/src/components/MkVisitorDashboard.vue10
-rw-r--r--packages/frontend/src/components/MkWaitingDialog.vue6
-rw-r--r--packages/frontend/src/components/MkWidgets.vue4
-rw-r--r--packages/frontend/src/components/MkWindow.vue22
-rw-r--r--packages/frontend/src/components/form/link.vue10
-rw-r--r--packages/frontend/src/components/form/section.vue6
-rw-r--r--packages/frontend/src/components/form/slot.vue2
-rw-r--r--packages/frontend/src/components/global/MkAd.vue28
-rw-r--r--packages/frontend/src/components/global/MkLoading.vue2
-rw-r--r--packages/frontend/src/components/global/MkMfm.ts19
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.tabs.vue2
-rw-r--r--packages/frontend/src/components/global/MkPageHeader.vue12
-rw-r--r--packages/frontend/src/components/global/MkStickyContainer.vue59
-rw-r--r--packages/frontend/src/components/global/MkTime.vue4
-rw-r--r--packages/frontend/src/components/global/RouterView.vue3
-rw-r--r--packages/frontend/src/components/page/page.dynamic.vue6
-rw-r--r--packages/frontend/src/components/page/page.image.vue4
-rw-r--r--packages/frontend/src/components/page/page.note.vue4
-rw-r--r--packages/frontend/src/directives/adaptive-bg.ts2
-rw-r--r--packages/frontend/src/directives/adaptive-border.ts2
-rw-r--r--packages/frontend/src/directives/panel.ts6
-rw-r--r--packages/frontend/src/os.ts8
-rw-r--r--packages/frontend/src/pages/about.federation.vue2
-rw-r--r--packages/frontend/src/pages/about.overview.vue8
-rw-r--r--packages/frontend/src/pages/admin-user.vue33
-rw-r--r--packages/frontend/src/pages/admin/RolesEditorFormula.vue6
-rw-r--r--packages/frontend/src/pages/admin/_header_.vue10
-rw-r--r--packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue8
-rw-r--r--packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue4
-rw-r--r--packages/frontend/src/pages/admin/abuses.vue17
-rw-r--r--packages/frontend/src/pages/admin/ads.vue2
-rw-r--r--packages/frontend/src/pages/admin/announcements.vue12
-rw-r--r--packages/frontend/src/pages/admin/bot-protection.vue13
-rw-r--r--packages/frontend/src/pages/admin/branding.vue4
-rw-r--r--packages/frontend/src/pages/admin/email-settings.vue4
-rw-r--r--packages/frontend/src/pages/admin/federation.vue2
-rw-r--r--packages/frontend/src/pages/admin/files.vue4
-rw-r--r--packages/frontend/src/pages/admin/index.vue4
-rw-r--r--packages/frontend/src/pages/admin/moderation.vue25
-rw-r--r--packages/frontend/src/pages/admin/modlog.ModLog.vue13
-rw-r--r--packages/frontend/src/pages/admin/modlog.vue11
-rw-r--r--packages/frontend/src/pages/admin/object-storage.vue4
-rw-r--r--packages/frontend/src/pages/admin/overview.ap-requests.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.federation.vue4
-rw-r--r--packages/frontend/src/pages/admin/overview.pie.vue2
-rw-r--r--packages/frontend/src/pages/admin/overview.queue.vue4
-rw-r--r--packages/frontend/src/pages/admin/overview.stats.vue4
-rw-r--r--packages/frontend/src/pages/admin/performance.vue16
-rw-r--r--packages/frontend/src/pages/admin/queue.chart.vue4
-rw-r--r--packages/frontend/src/pages/admin/relays.vue4
-rw-r--r--packages/frontend/src/pages/admin/roles.edit.vue4
-rw-r--r--packages/frontend/src/pages/admin/roles.role.vue2
-rw-r--r--packages/frontend/src/pages/admin/server-rules.vue10
-rw-r--r--packages/frontend/src/pages/admin/settings.vue2
-rw-r--r--packages/frontend/src/pages/admin/system-webhook.item.vue7
-rw-r--r--packages/frontend/src/pages/announcement.vue6
-rw-r--r--packages/frontend/src/pages/announcements.vue6
-rw-r--r--packages/frontend/src/pages/antenna-timeline.vue10
-rw-r--r--packages/frontend/src/pages/avatar-decorations.vue8
-rw-r--r--packages/frontend/src/pages/channel-editor.vue2
-rw-r--r--packages/frontend/src/pages/channel.vue14
-rw-r--r--packages/frontend/src/pages/clip.vue2
-rw-r--r--packages/frontend/src/pages/custom-emojis-manager.vue16
-rw-r--r--packages/frontend/src/pages/drive.file.info.vue22
-rw-r--r--packages/frontend/src/pages/drop-and-fusion.game.vue2
-rw-r--r--packages/frontend/src/pages/emoji-edit-dialog.vue8
-rw-r--r--packages/frontend/src/pages/emojis.emoji.vue4
-rw-r--r--packages/frontend/src/pages/explore.featured.vue2
-rw-r--r--packages/frontend/src/pages/explore.users.vue2
-rw-r--r--packages/frontend/src/pages/favorites.vue4
-rw-r--r--packages/frontend/src/pages/flash/flash-edit.vue6
-rw-r--r--packages/frontend/src/pages/flash/flash-index.vue3
-rw-r--r--packages/frontend/src/pages/flash/flash.vue4
-rw-r--r--packages/frontend/src/pages/gallery/edit.vue4
-rw-r--r--packages/frontend/src/pages/gallery/index.vue2
-rw-r--r--packages/frontend/src/pages/gallery/post.vue16
-rw-r--r--packages/frontend/src/pages/games.vue2
-rw-r--r--packages/frontend/src/pages/install-extensions.vue125
-rw-r--r--packages/frontend/src/pages/instance-info.vue1
-rw-r--r--packages/frontend/src/pages/list.vue2
-rw-r--r--packages/frontend/src/pages/my-antennas/index.vue4
-rw-r--r--packages/frontend/src/pages/my-lists/index.vue4
-rw-r--r--packages/frontend/src/pages/my-lists/list.vue10
-rw-r--r--packages/frontend/src/pages/note.vue8
-rw-r--r--packages/frontend/src/pages/notifications.vue2
-rw-r--r--packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue2
-rw-r--r--packages/frontend/src/pages/page-editor/page-editor.container.vue6
-rw-r--r--packages/frontend/src/pages/page.vue30
-rw-r--r--packages/frontend/src/pages/reversi/game.board.vue16
-rw-r--r--packages/frontend/src/pages/reversi/game.setting.vue16
-rw-r--r--packages/frontend/src/pages/reversi/index.vue20
-rw-r--r--packages/frontend/src/pages/scratchpad.vue6
-rw-r--r--packages/frontend/src/pages/search.note.vue4
-rw-r--r--packages/frontend/src/pages/settings/2fa.vue2
-rw-r--r--packages/frontend/src/pages/settings/accounts.vue18
-rw-r--r--packages/frontend/src/pages/settings/apps.vue73
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.decoration.vue6
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.dialog.vue6
-rw-r--r--packages/frontend/src/pages/settings/avatar-decoration.vue2
-rw-r--r--packages/frontend/src/pages/settings/drive-cleaner.vue2
-rw-r--r--packages/frontend/src/pages/settings/email.vue2
-rw-r--r--packages/frontend/src/pages/settings/emoji-picker.vue8
-rw-r--r--packages/frontend/src/pages/settings/index.vue2
-rw-r--r--packages/frontend/src/pages/settings/mute-block.vue2
-rw-r--r--packages/frontend/src/pages/settings/navbar.vue2
-rw-r--r--packages/frontend/src/pages/settings/other.vue8
-rw-r--r--packages/frontend/src/pages/settings/preferences-backups.vue2
-rw-r--r--packages/frontend/src/pages/settings/profile.vue52
-rw-r--r--packages/frontend/src/pages/settings/security.vue6
-rw-r--r--packages/frontend/src/pages/settings/sounds.sound.vue4
-rw-r--r--packages/frontend/src/pages/settings/theme.vue14
-rw-r--r--packages/frontend/src/pages/settings/webhook.edit.vue2
-rw-r--r--packages/frontend/src/pages/settings/webhook.vue4
-rw-r--r--packages/frontend/src/pages/signup-complete.vue6
-rw-r--r--packages/frontend/src/pages/tag.vue8
-rw-r--r--packages/frontend/src/pages/theme-editor.vue2
-rw-r--r--packages/frontend/src/pages/timeline.vue16
-rw-r--r--packages/frontend/src/pages/user-list-timeline.vue10
-rw-r--r--packages/frontend/src/pages/user/clips.vue2
-rw-r--r--packages/frontend/src/pages/user/follow-list.vue2
-rw-r--r--packages/frontend/src/pages/user/gallery.vue2
-rw-r--r--packages/frontend/src/pages/user/home.vue67
-rw-r--r--packages/frontend/src/pages/user/index.timeline.vue8
-rw-r--r--packages/frontend/src/pages/user/lists.vue4
-rw-r--r--packages/frontend/src/pages/user/raw.vue12
-rw-r--r--packages/frontend/src/pages/user/reactions.vue2
-rw-r--r--packages/frontend/src/pages/welcome.entrance.a.vue12
-rw-r--r--packages/frontend/src/pages/welcome.setup.vue34
-rw-r--r--packages/frontend/src/pages/welcome.timeline.note.vue4
-rw-r--r--packages/frontend/src/pages/welcome.timeline.vue4
-rw-r--r--packages/frontend/src/scripts/get-note-menu.ts11
-rw-r--r--packages/frontend/src/scripts/init-chart.ts2
-rw-r--r--packages/frontend/src/scripts/theme.ts2
-rw-r--r--packages/frontend/src/store.ts10
-rw-r--r--packages/frontend/src/style.scss129
-rw-r--r--packages/frontend/src/ui/_common_/announcements.vue12
-rw-r--r--packages/frontend/src/ui/_common_/common.vue14
-rw-r--r--packages/frontend/src/ui/_common_/navbar-for-mobile.vue33
-rw-r--r--packages/frontend/src/ui/_common_/navbar.vue76
-rw-r--r--packages/frontend/src/ui/_common_/statusbars.vue4
-rw-r--r--packages/frontend/src/ui/_common_/stream-indicator.vue4
-rw-r--r--packages/frontend/src/ui/_common_/upload.vue4
-rw-r--r--packages/frontend/src/ui/classic.header.vue15
-rw-r--r--packages/frontend/src/ui/classic.sidebar.vue13
-rw-r--r--packages/frontend/src/ui/classic.vue29
-rw-r--r--packages/frontend/src/ui/deck.vue41
-rw-r--r--packages/frontend/src/ui/deck/column.vue36
-rw-r--r--packages/frontend/src/ui/deck/widgets-column.vue6
-rw-r--r--packages/frontend/src/ui/universal.vue71
-rw-r--r--packages/frontend/src/ui/visitor.vue10
-rw-r--r--packages/frontend/src/ui/zen.vue12
-rw-r--r--packages/frontend/src/widgets/WidgetAiscript.vue6
-rw-r--r--packages/frontend/src/widgets/WidgetBirthdayFollowings.vue6
-rw-r--r--packages/frontend/src/widgets/WidgetCalendar.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetFederation.vue4
-rw-r--r--packages/frontend/src/widgets/WidgetJobQueue.vue8
-rw-r--r--packages/frontend/src/widgets/WidgetMemo.vue4
-rw-r--r--packages/frontend/src/widgets/WidgetOnlineUsers.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetRss.vue2
-rw-r--r--packages/frontend/src/widgets/WidgetRssTicker.vue4
-rw-r--r--packages/frontend/src/widgets/WidgetTrends.vue4
-rw-r--r--packages/frontend/vite.config.ts5
-rw-r--r--packages/misskey-js/etc/misskey-js.api.md79
-rw-r--r--packages/misskey-js/package.json3
-rw-r--r--packages/misskey-js/src/api.types.ts27
-rw-r--r--packages/misskey-js/src/autogen/apiClientJSDoc.ts22
-rw-r--r--packages/misskey-js/src/autogen/endpoint.ts7
-rw-r--r--packages/misskey-js/src/autogen/entities.ts3
-rw-r--r--packages/misskey-js/src/autogen/types.ts188
-rw-r--r--packages/misskey-js/src/consts.ts15
-rw-r--r--packages/misskey-js/src/entities.ts44
-rw-r--r--packages/shared/eslint.config.js7
-rw-r--r--packages/sw/src/scripts/create-notification.ts6
-rw-r--r--packages/sw/src/types.ts3
-rw-r--r--pnpm-lock.yaml1469
454 files changed, 9710 insertions, 3345 deletions
diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml
index d8013a1c95..8848e837b7 100644
--- a/.config/cypress-devcontainer.yml
+++ b/.config/cypress-devcontainer.yml
@@ -2,6 +2,19 @@
# Misskey configuration
#â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”â”
+# ┌────────────────────────â”
+#───┘ Initial Setup Password └─────────────────────────────────────────────────────
+
+# Password to initiate setting up admin account.
+# It will not be used after the initial setup is complete.
+#
+# Be sure to change this when you set up Misskey via the Internet.
+#
+# The provider of the service who sets up Misskey on behalf of the customer should
+# set this value to something unique when generating the Misskey config file,
+# and provide it to the customer.
+setupPassword: example_password_please_change_this_or_you_will_get_hacked
+
# ┌─────â”
#───┘ URL └─────────────────────────────────────────────────────
diff --git a/.config/example.yml b/.config/example.yml
index 0062b6670c..c9bd20b9c6 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -59,6 +59,20 @@
#
# publishTarballInsteadOfProvideRepositoryUrl: true
+# ┌────────────────────────â”
+#───┘ Initial Setup Password └─────────────────────────────────────────────────────
+
+# Password to initiate setting up admin account.
+# It will not be used after the initial setup is complete.
+#
+# Be sure to change this when you set up Misskey via the Internet.
+#
+# The provider of the service who sets up Misskey on behalf of the customer should
+# set this value to something unique when generating the Misskey config file,
+# and provide it to the customer.
+#
+# setupPassword: example_password_please_change_this_or_you_will_get_hacked
+
# ┌─────â”
#───┘ URL └─────────────────────────────────────────────────────
diff --git a/.gitignore b/.gitignore
index 7cc7354a4a..77ae92c023 100644
--- a/.gitignore
+++ b/.gitignore
@@ -40,7 +40,7 @@ coverage
!/.config/docker_example.env
!/.config/cypress-devcontainer.yml
docker-compose.yml
-compose.yml
+./compose.yml
.devcontainer/compose.yml
!/.devcontainer/compose.yml
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cf0437e51a..7e32576d45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,58 @@
+## 2024.10.1
+
+### Note
+- スパム対策ã¨ã—ã¦ã€ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿æ¨©é™ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティãŒ7日以上確èªã§ããªã„å ´åˆã¯è‡ªå‹•çš„ã«æ‹›å¾…制ã¸ã¨åˆ‡ã‚Šæ›¿ãˆï¼ˆã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ«ãƒ‘ãƒãƒ« -> モデレーション -> "誰ã§ã‚‚æ–°è¦ç™»éŒ²ã§ãるよã†ã«ã™ã‚‹"をオフã«å¤‰æ›´ï¼‰ã‚‹ã‚ˆã†ã«ãªã‚Šã¾ã—ãŸã€‚ ( #13437 )
+ - 切り替ã‚ã£ãŸéš›ã¯ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ã¸ãŠçŸ¥ã‚‰ã›ã¨ã—ã¦é€šçŸ¥ã•れã¾ã™ã€‚登録をオープンãªçŠ¶æ…‹ã§ç¶™ç¶šã—ãŸã„å ´åˆã¯ã€ã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ«ãƒ‘ãƒãƒ«ã‹ã‚‰å†åº¦è¨­å®šã‚’行ã£ã¦ãã ã•ã„。
+
+### General
+- Feat: ユーザーã®åå‰ã«ç¦æ­¢ãƒ¯ãƒ¼ãƒ‰ã‚’設定ã§ãるよã†ã«
+
+### Client
+- Enhance: タイムライン表示時ã®ãƒ‘フォーマンスをå‘上
+- Enhance: アーカイブã—ãŸå€‹äººå®›ã®ãŠçŸ¥ã‚‰ã›ã‚’表示・編集ã§ãるよã†ã«
+- Enhance: l10nã®æ›´æ–°
+- Fix: メールアドレスä¸è¦ã§CaptchaãŒæœ‰åйãªå ´åˆã«ã‚¢ã‚«ã‚¦ãƒ³ãƒˆç™»éŒ²å®Œäº†å¾Œè‡ªå‹•ã§ã®ãƒ­ã‚°ã‚¤ãƒ³ã«å¤±æ•—ã™ã‚‹å•題を修正
+
+### Server
+- Feat: モデレータ権é™ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ãŒå…¨å“¡7日間活動ã—ãªã‹ã£ãŸå ´åˆã¯è‡ªå‹•çš„ã«æ‹›å¾…制ã¸ã¨åˆ‡ã‚Šæ›¿ãˆã‚‹ã‚ˆã†ã« ( #13437 )
+- Enhance: 個人宛ã®ãŠçŸ¥ã‚‰ã›ã¯ã€Œã‚ã‹ã£ãŸã€ã‚’押ã™ã¨è‡ªå‹•çš„ã«ã‚¢ãƒ¼ã‚«ã‚¤ãƒ–ã•れるよã†ã«
+- Fix: `admin/emoji/update`エンドãƒã‚¤ãƒ³ãƒˆã®idã®ã¿æŒ‡å®šã—ãŸæ™‚䏿­£ãªã‚¨ãƒ©ãƒ¼ãŒç™ºç”Ÿã™ã‚‹ãƒã‚°ã‚’修正
+- Fix: RBT有効時ã€ãƒªãƒŽãƒ¼ãƒˆã®ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ãŒå映ã•れãªã„å•題を修正
+- Fix: キューã®ã‚¨ãƒ©ãƒ¼ãƒ­ã‚°ã‚’簡略化ã™ã‚‹ã‚ˆã†ã«
+ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/649)
+
+## 2024.10.0
+
+### Note
+- セキュリティå‘上ã®ãŸã‚ã€ã‚µãƒ¼ãƒãƒ¼åˆæœŸè¨­å®šæ™‚ã«ä½¿ç”¨ã™ã‚‹åˆæœŸãƒ‘スワードを設定ã§ãるよã†ã«ãªã‚Šã¾ã—ãŸã€‚今後Misskeyサーãƒãƒ¼ã‚’æ–°ãŸã«è¨­ç½®ã™ã‚‹éš›ã«ã¯ã€åˆå›žã®èµ·å‹•å‰ã«ã‚³ãƒ³ãƒ•ィグファイルã®`setupPassword`をコメントアウトã—ã€åˆæœŸãƒ‘スワードを設定ã™ã‚‹ã“ã¨ã‚’ãŠã™ã™ã‚ã—ã¾ã™ã€‚(ã™ã§ã«åˆæœŸè¨­å®šã‚’完了ã—ã¦ã„るサーãƒãƒ¼ã«ã¤ã„ã¦ã¯ã€ã“ã®å¤‰æ›´ã«ä¼´ã„対応ã™ã‚‹å¿…è¦ã¯ã‚りã¾ã›ã‚“)
+ - ホスティングサービスをé‹å–¶ã—ã¦ã„ã‚‹å ´åˆã¯ã€ã‚³ãƒ³ãƒ•ィグファイルを構築ã™ã‚‹éš›ã«`setupPassword`をランダムãªå€¤ã«è¨­å®šã—ã€ãƒ¦ãƒ¼ã‚¶ãƒ¼ã«é€šçŸ¥ã™ã‚‹ã‚ˆã†ã«ã‚·ã‚¹ãƒ†ãƒ ã‚’æ›´æ–°ã™ã‚‹ã“ã¨ã‚’ãŠã™ã™ã‚ã—ã¾ã™ã€‚
+ - ãªãŠã€åˆæœŸãƒ‘スワードãŒè¨­å®šã•れã¦ã„ãªã„å ´åˆã§ã‚‚åˆæœŸè¨­å®šã‚’行ã†ã“ã¨ãŒå¯èƒ½ã§ã™ï¼ˆUI上ã§åˆæœŸãƒ‘スワードã®å…¥åŠ›æ¬„ã‚’ç©ºæ¬„ã«ã™ã‚‹ã¨ç¶šè¡Œã§ãã¾ã™ï¼‰ã€‚
+- ユーザーデータを読ã¿è¾¼ã‚€éš›ã®åž‹ãŒä¸€éƒ¨å¤‰æ›´ã•れã¾ã—ãŸã€‚
+ - `twoFactorEnabled`, `usePasswordLessLogin`, `securityKeys`: 自分ã¨ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ä»¥å¤–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰ã¯å–å¾—ã§ããªããªã‚Šã¾ã—ãŸ
+
+### General
+- Feat: サーãƒãƒ¼åˆæœŸè¨­å®šæ™‚ã«åˆæœŸãƒ‘スワードを設定ã§ãるよã†ã«
+- Feat: 通報ã«ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ãƒŽãƒ¼ãƒˆã‚’残ã›ã‚‹ã‚ˆã†ã«
+- Feat: 通報ã®è§£æ±ºç¨®åˆ¥ã‚’設定ã§ãるよã†ã«
+- Enhance: 通報ã®è§£æ±ºã¨è»¢é€ã‚’個別ã«è¡Œãˆã‚‹ã‚ˆã†ã«
+- Enhance: セキュリティå‘上ã®ãŸã‚ã€ã‚µã‚¤ãƒ³ã‚¤ãƒ³æ™‚ã‚‚CAPTCHAを求ã‚るよã†ã«ãªã‚Šã¾ã—ãŸ
+- Enhance: ä¾å­˜é–¢ä¿‚ã®æ›´æ–°
+- Enhance: l10nã®æ›´æ–°
+- Enhance: Playã®ã€Œäººæ°—ã€ã‚¿ãƒ–ã§10件以上表示å¯èƒ½ã« #14399
+- Fix: 連åˆã®ãƒ›ãƒ¯ã‚¤ãƒˆãƒªã‚¹ãƒˆãŒæ­£å¸¸ã«ç™»éŒ²ã•れãªã„å•題を修正
+
+### Client
+- Enhance: デザインã®èª¿æ•´
+- Enhance: ログイン画é¢ã®èªè¨¼ãƒ•ローを改善
+- Fix: クライアント上ã§ã®æ™‚間ベースã®å®Ÿç¸¾ç²å¾—動作ãŒå®Ÿç¸¾ç²å¾—後も発動ã—ã¦ã„ãŸå•題を修正
+ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/657)
+
+### Server
+- Enhance: セキュリティå‘上ã®ãŸã‚ã€ãƒ­ã‚°ã‚¤ãƒ³æ™‚ã«ãƒ¡ãƒ¼ãƒ«é€šçŸ¥ã‚’行ã†ã‚ˆã†ã«
+- Enhance: 自分ã¨ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ä»¥å¤–ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼ã‹ã‚‰äºŒè¦ç´ èªè¨¼é–¢é€£ã®ãƒ‡ãƒ¼ã‚¿ãŒå–å¾—ã§ããªã„よã†ã«
+- Enhance: 通報ãŠã‚ˆã³é€šå ±è§£æ±ºæ™‚ã«é€å‡ºã•れるSystemWebhookã«ãƒ¦ãƒ¼ã‚¶æƒ…報をå«ã‚るよã†ã« ( #14697 )
+- Fix: `admin/abuse-user-reports`エンドãƒã‚¤ãƒ³ãƒˆã®ã‚¹ã‚­ãƒ¼ãƒžãŒé–“é•ã£ã¦ã„ãŸå•題を修正
+
## 2024.9.0
### General
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index f2e48ec61d..3a80d55900 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -181,31 +181,46 @@ MK_DEV_PREFER=backend pnpm dev
- HMR may not work in some environments such as Windows.
## Testing
-- Test codes are located in [`/packages/backend/test`](packages/backend/test).
-### Run test
-Create a config file.
+You can run non-backend tests by executing following commands:
+```sh
+pnpm --filter frontend test
+pnpm --filter misskey-js test
```
+
+Backend tests require manual preparation of servers. See the next section for more on this.
+
+### Backend
+There are three types of test codes for the backend:
+- Unit tests: [`/packages/backend/test/unit`](/packages/backend/test/unit)
+- Single-server E2E tests: [`/packages/backend/test/e2e`](/packages/backend/test/e2e)
+- Multiple-server E2E tests: [`/packages/backend/test-federation`](/packages/backend/test-federation)
+
+#### Running Unit Tests or Single-server E2E Tests
+1. Create a config file:
+```sh
cp .github/misskey/test.yml .config/
```
-Prepare DB/Redis for testing.
-```
-docker compose -f packages/backend/test/compose.yml up
-```
-Alternatively, prepare an empty (data can be erased) DB and edit `.config/test.yml`.
-Run all test.
-```
-pnpm test
+2. Start DB and Redis servers for testing:
+```sh
+docker compose -f packages/backend/test/compose.yml up
```
+Instead, you can prepare an empty (data can be erased) DB and edit `.config/test.yml` appropriately.
-#### Run specify test
+3. Run all tests:
+```sh
+pnpm --filter backend test # unit tests
+pnpm --filter backend test:e2e # single-server E2E tests
```
-pnpm jest -- foo.ts
+If you want to run a specific test, run as a following command:
+```sh
+pnpm --filter backend test -- packages/backend/test/unit/activitypub.ts
+pnpm --filter backend test:e2e -- packages/backend/test/e2e/nodeinfo.ts
```
-### e2e tests
-TODO
+#### Running Multiple-server E2E Tests
+See [`/packages/backend/test-federation/README.md`](/packages/backend/test-federation/README.md).
## Environment Variable
@@ -579,19 +594,19 @@ ESMã§ã¯ãƒ‡ã‚£ãƒ¬ã‚¯ãƒˆãƒªã‚¤ãƒ³ãƒãƒ¼ãƒˆã¯å»ƒæ­¢ã•れã¦ã„ã‚‹ã®ã¨ã€ãƒ‡ã‚
### Lighten CSS vars
``` css
-color: hsl(from var(--accent) h s calc(l + 10));
+color: hsl(from var(--MI_THEME-accent) h s calc(l + 10));
```
### Darken CSS vars
``` css
-color: hsl(from var(--accent) h s calc(l - 10));
+color: hsl(from var(--MI_THEME-accent) h s calc(l - 10));
```
### Add alpha to CSS vars
``` css
-color: color(from var(--accent) srgb r g b / 0.5);
+color: color(from var(--MI_THEME-accent) srgb r g b / 0.5);
```
## Merging from Misskey into Sharkey
diff --git a/cypress/e2e/basic.cy.ts b/cypress/e2e/basic.cy.ts
index d2525e0a7d..d2efbf709c 100644
--- a/cypress/e2e/basic.cy.ts
+++ b/cypress/e2e/basic.cy.ts
@@ -23,6 +23,7 @@ describe('Before setup instance', () => {
cy.intercept('POST', '/api/admin/accounts/create').as('signup');
+ cy.get('[data-cy-admin-initial-password] input').type('example_password_please_change_this_or_you_will_get_hacked');
cy.get('[data-cy-admin-username] input').type('admin');
cy.get('[data-cy-admin-password] input').type('admin1234');
cy.get('[data-cy-admin-ok]').click();
@@ -119,11 +120,16 @@ describe('After user signup', () => {
it('signin', () => {
cy.visitHome();
- cy.intercept('POST', '/api/signin').as('signin');
+ cy.intercept('POST', '/api/signin-flow').as('signin');
cy.get('[data-cy-signin]').click();
- cy.get('[data-cy-signin-username] input').type('alice');
- // Enterキーã§ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã§ãã‚‹ã‹ã®ç¢ºèªã‚‚å…¼ã­ã‚‹
+
+ cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
+ // Enterキーã§ç¶šè¡Œã§ãã‚‹ã‹ã®ç¢ºèªã‚‚å…¼ã­ã‚‹
+ cy.get('[data-cy-signin-username] input').type('alice{enter}');
+
+ cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
+ // Enterキーã§ç¶šè¡Œã§ãã‚‹ã‹ã®ç¢ºèªã‚‚å…¼ã­ã‚‹
cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
cy.wait('@signin');
@@ -138,8 +144,9 @@ describe('After user signup', () => {
cy.visitHome();
cy.get('[data-cy-signin]').click();
- cy.get('[data-cy-signin-username] input').type('alice');
- cy.get('[data-cy-signin-password] input').type('alice1234{enter}');
+
+ cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
+ cy.get('[data-cy-signin-username] input').type('alice{enter}');
// TODO: cypressã«ãƒ–ラウザã®è¨€èªžæŒ‡å®šã§ãる機能ãŒå®Ÿè£…ã•れ次第英語ã®ã¿ãƒ†ã‚¹ãƒˆã™ã‚‹ã‚ˆã†ã«ã™ã‚‹
cy.contains(/アカウントãŒå‡çµã•れã¦ã„ã¾ã™|This account has been suspended due to/gi);
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 281f2e6ccd..197ff963ac 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -48,16 +48,19 @@ Cypress.Commands.add('registerUser', (username, password, isAdmin = false) => {
cy.request('POST', route, {
username: username,
password: password,
+ ...(isAdmin ? { setupPassword: 'example_password_please_change_this_or_you_will_get_hacked' } : {}),
}).its('body').as(username);
});
Cypress.Commands.add('login', (username, password) => {
cy.visitHome();
- cy.intercept('POST', '/api/signin').as('signin');
+ cy.intercept('POST', '/api/signin-flow').as('signin');
cy.get('[data-cy-signin]').click();
- cy.get('[data-cy-signin-username] input').type(username);
+ cy.get('[data-cy-signin-page-input]').should('be.visible', { timeout: 1000 });
+ cy.get('[data-cy-signin-username] input').type(`${username}{enter}`);
+ cy.get('[data-cy-signin-page-password]').should('be.visible', { timeout: 10000 });
cy.get('[data-cy-signin-password] input').type(`${password}{enter}`);
cy.wait('@signin').as('signedIn');
diff --git a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts b/idea/MkAbuseReport.stories.impl.ts
index cf09c96fd4..717bceb23d 100644
--- a/packages/frontend/src/components/MkAbuseReport.stories.impl.ts
+++ b/idea/MkAbuseReport.stories.impl.ts
@@ -7,8 +7,8 @@
import { action } from '@storybook/addon-actions';
import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
-import { abuseUserReport } from '../../.storybook/fakes.js';
-import { commonHandlers } from '../../.storybook/mocks.js';
+import { abuseUserReport } from '../packages/frontend/.storybook/fakes.js';
+import { commonHandlers } from '../packages/frontend/.storybook/mocks.js';
import MkAbuseReport from './MkAbuseReport.vue';
export const Default = {
render(args) {
diff --git a/idea/MkDisableSection.vue b/idea/MkDisableSection.vue
index d177886569..360705071b 100644
--- a/idea/MkDisableSection.vue
+++ b/idea/MkDisableSection.vue
@@ -34,7 +34,7 @@ defineProps<{
width: 100%;
height: 100%;
cursor: not-allowed;
- --color: color(from var(--error) srgb r g b / 0.25);
+ --color: color(from var(--MI_THEME-error) srgb r g b / 0.25);
background-size: auto auto;
background-image: repeating-linear-gradient(135deg, transparent, transparent 10px, var(--color) 4px, var(--color) 14px);
}
diff --git a/package.json b/package.json
index 6e84882440..3b4893fe2d 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "sharkey",
- "version": "2024.10.0-dev",
+ "version": "2024.10.3-rc",
"codename": "shonk",
"repository": {
"type": "git",
diff --git a/packages/backend/assets/tabler-badges/login-2.png b/packages/backend/assets/tabler-badges/login-2.png
new file mode 100644
index 0000000000..f3ca8de3dd
--- /dev/null
+++ b/packages/backend/assets/tabler-badges/login-2.png
Binary files differ
diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js
index 452045bc3e..ef5a0ccec1 100644
--- a/packages/backend/eslint.config.js
+++ b/packages/backend/eslint.config.js
@@ -12,7 +12,7 @@ export default [
languageOptions: {
parserOptions: {
parser: tsParser,
- project: ['./tsconfig.json', './test/tsconfig.json'],
+ project: ['./tsconfig.json', './test/tsconfig.json', './test-federation/tsconfig.json'],
sourceType: 'module',
tsconfigRootDir: import.meta.dirname,
},
diff --git a/packages/backend/jest.config.fed.cjs b/packages/backend/jest.config.fed.cjs
new file mode 100644
index 0000000000..fae187bc23
--- /dev/null
+++ b/packages/backend/jest.config.fed.cjs
@@ -0,0 +1,13 @@
+/*
+ * For a detailed explanation regarding each configuration property and type check, visit:
+ * https://jestjs.io/docs/en/configuration.html
+ */
+
+const base = require('./jest.config.cjs');
+
+module.exports = {
+ ...base,
+ testMatch: [
+ '<rootDir>/test-federation/test/**/*.test.ts',
+ ],
+};
diff --git a/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js b/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js
new file mode 100644
index 0000000000..4ff520172b
--- /dev/null
+++ b/packages/backend/migration/1727318020265-enableStatsForFederatedInstances.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class EnableStatsForFederatedInstances1727318020265 {
+ name = 'EnableStatsForFederatedInstances1727318020265'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "enableStatsForFederatedInstances" boolean NOT NULL DEFAULT true`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableStatsForFederatedInstances"`);
+ }
+}
diff --git a/packages/backend/migration/1728085812127-refine-abuse-user-report.js b/packages/backend/migration/1728085812127-refine-abuse-user-report.js
new file mode 100644
index 0000000000..57cbfdcf6d
--- /dev/null
+++ b/packages/backend/migration/1728085812127-refine-abuse-user-report.js
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class RefineAbuseUserReport1728085812127 {
+ name = 'RefineAbuseUserReport1728085812127'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "moderationNote" character varying(8192) NOT NULL DEFAULT ''`);
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" ADD "resolvedAs" character varying(128)`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "resolvedAs"`);
+ await queryRunner.query(`ALTER TABLE "abuse_user_report" DROP COLUMN "moderationNote"`);
+ }
+}
diff --git a/packages/backend/migration/1728550878802-testcaptcha.js b/packages/backend/migration/1728550878802-testcaptcha.js
new file mode 100644
index 0000000000..d8d987c0c1
--- /dev/null
+++ b/packages/backend/migration/1728550878802-testcaptcha.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class Testcaptcha1728550878802 {
+ name = 'Testcaptcha1728550878802'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "enableTestcaptcha" boolean NOT NULL DEFAULT false`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableTestcaptcha"`);
+ }
+}
diff --git a/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js b/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js
new file mode 100644
index 0000000000..36e698d120
--- /dev/null
+++ b/packages/backend/migration/1728634286056-prohibitedWordsForNameOfUser.js
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class ProhibitedWordsForNameOfUser1728634286056 {
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "prohibitedWordsForNameOfUser" character varying(1024) array NOT NULL DEFAULT '{}'`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "prohibitedWordsForNameOfUser"`);
+ }
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 19547c5033..ba601a569d 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -19,16 +19,18 @@
"watch": "node ./scripts/watch.mjs",
"restart": "pnpm build && pnpm start",
"dev": "node ./scripts/dev.mjs",
- "typecheck": "pnpm --filter megalodon build && tsc --noEmit && tsc -p test --noEmit",
- "eslint": "eslint --quiet \"{src,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache",
+ "typecheck": "pnpm --filter megalodon build && tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit",
+ "eslint": "eslint --quiet \"{src,test-federation,test,js,@types}/**/*.{js,jsx,ts,tsx,vue}\" --cache",
"lint": "pnpm typecheck && pnpm eslint",
"jest": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.unit.cjs",
"jest:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.e2e.cjs",
+ "jest:fed": "node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --forceExit --config jest.config.fed.cjs",
"jest-and-coverage": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.unit.cjs",
"jest-and-coverage:e2e": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --coverage --forceExit --config jest.config.e2e.cjs",
"jest-clear": "cross-env NODE_ENV=test node --experimental-vm-modules --experimental-import-meta-resolve node_modules/jest/bin/jest.js --clearCache",
"test": "pnpm jest",
"test:e2e": "pnpm build && pnpm build:test && pnpm jest:e2e",
+ "test:fed": "pnpm jest:fed",
"test-and-coverage": "pnpm jest-and-coverage",
"test-and-coverage:e2e": "pnpm build && pnpm build:test && pnpm jest-and-coverage:e2e",
"generate-api-json": "node ./scripts/generate_api_json.js"
@@ -69,20 +71,20 @@
"@bull-board/fastify": "6.0.0",
"@bull-board/ui": "6.0.0",
"@discordapp/twemoji": "15.1.0",
- "@fastify/accepts": "5.0.0",
- "@fastify/cookie": "10.0.0",
- "@fastify/cors": "10.0.0",
- "@fastify/express": "4.0.0",
+ "@fastify/accepts": "5.0.1",
+ "@fastify/cookie": "10.0.1",
+ "@fastify/cors": "10.0.1",
+ "@fastify/express": "4.0.1",
"@fastify/http-proxy": "10.0.0",
- "@fastify/multipart": "9.0.0",
- "@fastify/static": "8.0.0",
- "@fastify/view": "10.0.0",
+ "@fastify/multipart": "9.0.1",
+ "@fastify/static": "8.0.1",
+ "@fastify/view": "10.0.1",
"@misskey-dev/sharp-read-bmp": "1.2.0",
"@misskey-dev/summaly": "5.1.0",
"@napi-rs/canvas": "0.1.56",
- "@nestjs/common": "10.4.3",
- "@nestjs/core": "10.4.3",
- "@nestjs/testing": "10.4.3",
+ "@nestjs/common": "10.4.4",
+ "@nestjs/core": "10.4.4",
+ "@nestjs/testing": "10.4.4",
"@peertube/http-signature": "1.7.0",
"@sentry/node": "8.20.0",
"@sentry/profiling-node": "8.20.0",
@@ -101,7 +103,7 @@
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
"body-parser": "1.20.3",
- "bullmq": "5.13.2",
+ "bullmq": "5.15.0",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
"chalk": "5.3.0",
@@ -151,7 +153,7 @@
"oauth2orize": "1.12.0",
"oauth2orize-pkce": "0.1.2",
"os-utils": "0.0.14",
- "otpauth": "9.3.2",
+ "otpauth": "9.3.4",
"parse5": "7.1.2",
"pg": "8.13.0",
"pkce-challenge": "4.1.0",
@@ -169,7 +171,7 @@
"rename": "1.0.4",
"rss-parser": "3.13.0",
"rxjs": "7.8.1",
- "sanitize-html": "2.13.0",
+ "sanitize-html": "2.13.1",
"secure-json-parse": "2.7.0",
"sharp": "0.33.5",
"slacc": "0.0.10",
@@ -191,14 +193,14 @@
},
"devDependencies": {
"@jest/globals": "29.7.0",
- "@nestjs/platform-express": "10.4.3",
+ "@nestjs/platform-express": "10.4.4",
"@simplewebauthn/types": "10.0.0",
"@swc/jest": "0.2.36",
"@types/accepts": "1.3.7",
"@types/archiver": "6.0.2",
"@types/bcryptjs": "2.4.6",
"@types/body-parser": "1.19.5",
- "@types/color-convert": "2.0.3",
+ "@types/color-convert": "2.0.4",
"@types/content-disposition": "0.5.8",
"@types/fluent-ffmpeg": "2.1.26",
"@types/htmlescape": "1.1.3",
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index 3dc49c7eb6..d71c5568ef 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -65,6 +65,8 @@ type Source = {
publishTarballInsteadOfProvideRepositoryUrl?: boolean;
+ setupPassword?: string;
+
proxy?: string;
proxySmtp?: string;
proxyBypassHosts?: string[];
@@ -179,6 +181,7 @@ export type Config = {
version: string;
publishTarballInsteadOfProvideRepositoryUrl: boolean;
+ setupPassword: string | undefined;
host: string;
hostname: string;
scheme: string;
@@ -280,6 +283,7 @@ export function loadConfig(): Config {
return {
version,
publishTarballInsteadOfProvideRepositoryUrl: !!config.publishTarballInsteadOfProvideRepositoryUrl,
+ setupPassword: config.setupPassword,
url: url.origin,
port: config.port ?? parseInt(process.env.PORT ?? '3000', 10),
socket: config.socket,
diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts
index fe2c63e7d6..25e265f2b1 100644
--- a/packages/backend/src/core/AbuseReportNotificationService.ts
+++ b/packages/backend/src/core/AbuseReportNotificationService.ts
@@ -22,6 +22,7 @@ import { RoleService } from '@/core/RoleService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { ModerationLogService } from '@/core/ModerationLogService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { IdService } from './IdService.js';
@Injectable()
@@ -42,6 +43,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
private emailService: EmailService,
private moderationLogService: ModerationLogService,
private globalEventService: GlobalEventService,
+ private userEntityService: UserEntityService,
) {
this.redisForSub.on('message', this.onMessage);
}
@@ -59,7 +61,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
return;
}
- const moderatorIds = await this.roleService.getModeratorIds(true, true);
+ const moderatorIds = await this.roleService.getModeratorIds({
+ includeAdmins: true,
+ excludeExpire: true,
+ });
for (const moderatorId of moderatorIds) {
for (const abuseReport of abuseReports) {
@@ -135,6 +140,26 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
return;
}
+ const usersMap = await this.userEntityService.packMany(
+ [
+ ...new Set([
+ ...abuseReports.map(it => it.reporter ?? it.reporterId),
+ ...abuseReports.map(it => it.targetUser ?? it.targetUserId),
+ ...abuseReports.map(it => it.assignee ?? it.assigneeId),
+ ].filter(x => x != null)),
+ ],
+ null,
+ { schema: 'UserLite' },
+ ).then(it => new Map(it.map(it => [it.id, it])));
+ const convertedReports = abuseReports.map(it => {
+ return {
+ ...it,
+ reporter: usersMap.get(it.reporterId),
+ targetUser: usersMap.get(it.targetUserId),
+ assignee: it.assigneeId ? usersMap.get(it.assigneeId) : null,
+ };
+ });
+
const recipientWebhookIds = await this.fetchWebhookRecipients()
.then(it => it
.filter(it => it.isActive && it.systemWebhookId && it.method === 'webhook')
@@ -142,7 +167,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.filter(x => x != null));
for (const webhookId of recipientWebhookIds) {
await Promise.all(
- abuseReports.map(it => {
+ convertedReports.map(it => {
return this.systemWebhookService.enqueueSystemWebhook(
webhookId,
type,
@@ -263,8 +288,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.log(updater, 'createAbuseReportNotificationRecipient', {
recipientId: id,
recipient: created,
- })
- .then();
+ });
return created;
}
@@ -302,8 +326,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
recipientId: params.id,
before: beforeEntity,
after: afterEntity,
- })
- .then();
+ });
return afterEntity;
}
@@ -324,8 +347,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
.log(updater, 'deleteAbuseReportNotificationRecipient', {
recipientId: id,
recipient: entity,
- })
- .then();
+ });
}
/**
@@ -348,7 +370,10 @@ export class AbuseReportNotificationService implements OnApplicationShutdown {
}
// モデレータ権é™ã®æœ‰ç„¡ã§é€šçŸ¥å…ˆè¨­å®šã‚’振り分ã‘ã‚‹
- const authorizedUserIds = await this.roleService.getModeratorIds(true, true);
+ const authorizedUserIds = await this.roleService.getModeratorIds({
+ includeAdmins: true,
+ excludeExpire: true,
+ });
const authorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
const unauthorizedUserRecipients = Array.of<MiAbuseReportNotificationRecipient>();
for (const recipient of userRecipients) {
diff --git a/packages/backend/src/core/AbuseReportService.ts b/packages/backend/src/core/AbuseReportService.ts
index 007c3f1bf9..0b022d3b08 100644
--- a/packages/backend/src/core/AbuseReportService.ts
+++ b/packages/backend/src/core/AbuseReportService.ts
@@ -20,8 +20,10 @@ export class AbuseReportService {
constructor(
@Inject(DI.abuseUserReportsRepository)
private abuseUserReportsRepository: AbuseUserReportsRepository,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
+
private idService: IdService,
private abuseReportNotificationService: AbuseReportNotificationService,
private queueService: QueueService,
@@ -77,62 +79,98 @@ export class AbuseReportService {
* - SystemWebhook
*
* @param params 通報内容. ã‚‚ã—複数件ã®é€šå ±ã«å¯¾å¿œã—ãŸæ™‚ã®ãŸã‚ã«ã€ã‚らã‹ã˜ã‚複数件を処ç†ã§ãã‚‹å‰æã§è€ƒãˆã‚‹
- * @param operator 通報を処ç†ã—ãŸãƒ¦ãƒ¼ã‚¶
+ * @param moderator 通報を処ç†ã—ãŸãƒ¦ãƒ¼ã‚¶
* @see AbuseReportNotificationService.notify
*/
@bindThis
public async resolve(
params: {
reportId: string;
- forward: boolean;
+ resolvedAs: MiAbuseUserReport['resolvedAs'];
}[],
- operator: MiUser,
+ moderator: MiUser,
) {
const paramsMap = new Map(params.map(it => [it.reportId, it]));
const reports = await this.abuseUserReportsRepository.findBy({
id: In(params.map(it => it.reportId)),
});
- const targetUserMap = new Map();
- for (const report of reports) {
- const shouldForward = paramsMap.get(report.id)!.forward;
-
- if (shouldForward && report.targetUserHost != null) {
- targetUserMap.set(report.id, await this.usersRepository.findOneByOrFail({ id: report.targetUserId }));
- } else {
- targetUserMap.set(report.id, null);
- }
- }
-
for (const report of reports) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const ps = paramsMap.get(report.id)!;
await this.abuseUserReportsRepository.update(report.id, {
resolved: true,
- assigneeId: operator.id,
- forwarded: ps.forward && report.targetUserHost !== null,
+ assigneeId: moderator.id,
+ resolvedAs: ps.resolvedAs,
});
- const targetUser = targetUserMap.get(report.id)!;
- if (targetUser != null) {
- const actor = await this.instanceActorService.getInstanceActor();
-
- // eslint-disable-next-line
- const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
- const contextAssignedFlag = this.apRendererService.addContext(flag);
- this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
- }
-
this.moderationLogService
- .log(operator, 'resolveAbuseReport', {
+ .log(moderator, 'resolveAbuseReport', {
reportId: report.id,
report: report,
- forwarded: ps.forward && report.targetUserHost !== null,
+ resolvedAs: ps.resolvedAs,
});
}
return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) })
.then(reports => this.abuseReportNotificationService.notifySystemWebhook(reports, 'abuseReportResolved'));
}
+
+ @bindThis
+ public async forward(
+ reportId: MiAbuseUserReport['id'],
+ moderator: MiUser,
+ ) {
+ const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
+
+ if (report.targetUserHost == null) {
+ throw new Error('The target user host is null.');
+ }
+
+ if (report.forwarded) {
+ throw new Error('The report has already been forwarded.');
+ }
+
+ await this.abuseUserReportsRepository.update(report.id, {
+ forwarded: true,
+ });
+
+ const actor = await this.instanceActorService.getInstanceActor();
+ const targetUser = await this.usersRepository.findOneByOrFail({ id: report.targetUserId });
+
+ const flag = this.apRendererService.renderFlag(actor, targetUser.uri!, report.comment);
+ const contextAssignedFlag = this.apRendererService.addContext(flag);
+ this.queueService.deliver(actor, contextAssignedFlag, targetUser.inbox, false);
+
+ this.moderationLogService
+ .log(moderator, 'forwardAbuseReport', {
+ reportId: report.id,
+ report: report,
+ });
+ }
+
+ @bindThis
+ public async update(
+ reportId: MiAbuseUserReport['id'],
+ params: {
+ moderationNote?: MiAbuseUserReport['moderationNote'];
+ },
+ moderator: MiUser,
+ ) {
+ const report = await this.abuseUserReportsRepository.findOneByOrFail({ id: reportId });
+
+ await this.abuseUserReportsRepository.update(report.id, {
+ moderationNote: params.moderationNote,
+ });
+
+ if (params.moderationNote != null && report.moderationNote !== params.moderationNote) {
+ this.moderationLogService.log(moderator, 'updateAbuseReportNote', {
+ reportId: report.id,
+ report: report,
+ before: report.moderationNote,
+ after: params.moderationNote,
+ });
+ }
+ }
}
diff --git a/packages/backend/src/core/AccountMoveService.ts b/packages/backend/src/core/AccountMoveService.ts
index 6e3125044c..24d11f29ff 100644
--- a/packages/backend/src/core/AccountMoveService.ts
+++ b/packages/backend/src/core/AccountMoveService.ts
@@ -274,13 +274,15 @@ export class AccountMoveService {
}
// Update instance stats by decreasing remote followers count by the number of local followers who were following the old account.
- if (this.userEntityService.isRemoteUser(oldAccount)) {
- this.federatedInstanceService.fetch(oldAccount.host).then(async i => {
- this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
- if (this.meta.enableChartsForFederatedInstances) {
- this.instanceChart.updateFollowers(i.host, false);
- }
- });
+ if (this.meta.enableStatsForFederatedInstances) {
+ if (this.userEntityService.isRemoteUser(oldAccount)) {
+ this.federatedInstanceService.fetchOrRegister(oldAccount.host).then(async i => {
+ this.instancesRepository.decrement({ id: i.id }, 'followersCount', localFollowerIds.length);
+ if (this.meta.enableChartsForFederatedInstances) {
+ this.instanceChart.updateFollowers(i.host, false);
+ }
+ });
+ }
}
// FIXME: expensive?
diff --git a/packages/backend/src/core/AnnouncementService.ts b/packages/backend/src/core/AnnouncementService.ts
index 40a9db01c0..d4fcf19439 100644
--- a/packages/backend/src/core/AnnouncementService.ts
+++ b/packages/backend/src/core/AnnouncementService.ts
@@ -209,6 +209,13 @@ export class AnnouncementService {
return;
}
+ const announcement = await this.announcementsRepository.findOneBy({ id: announcementId });
+ if (announcement != null && announcement.userId === user.id) {
+ await this.announcementsRepository.update(announcementId, {
+ isActive: false,
+ });
+ }
+
if ((await this.getUnreadAnnouncements(user)).length === 0) {
this.globalEventService.publishMainStream(user.id, 'readAllAnnouncements');
}
diff --git a/packages/backend/src/core/CaptchaService.ts b/packages/backend/src/core/CaptchaService.ts
index 4be45dabb8..5b1ab00cfe 100644
--- a/packages/backend/src/core/CaptchaService.ts
+++ b/packages/backend/src/core/CaptchaService.ts
@@ -149,5 +149,18 @@ export class CaptchaService {
throw new Error(`turnstile-failed: ${errorCodes}`);
}
}
+
+ @bindThis
+ public async verifyTestcaptcha(response: string | null | undefined): Promise<void> {
+ if (response == null) {
+ throw new Error('testcaptcha-failed: no response provided');
+ }
+
+ const success = response === 'testcaptcha-passed';
+
+ if (!success) {
+ throw new Error('testcaptcha-failed');
+ }
+ }
}
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index c083068392..8b42408535 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -14,6 +14,7 @@ import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationSe
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { FlashService } from '@/core/FlashService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AnnouncementService } from './AnnouncementService.js';
@@ -220,6 +221,7 @@ const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useEx
const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
+const $FlashService: Provider = { provide: 'FlashService', useExisting: FlashService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
const $ClipService: Provider = { provide: 'ClipService', useExisting: ClipService };
const $FeaturedService: Provider = { provide: 'FeaturedService', useExisting: FeaturedService };
@@ -373,6 +375,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
WebhookTestService,
UtilityService,
FileInfoService,
+ FlashService,
SearchService,
ClipService,
FeaturedService,
@@ -522,6 +525,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
$WebhookTestService,
$UtilityService,
$FileInfoService,
+ $FlashService,
$SearchService,
$ClipService,
$FeaturedService,
@@ -672,6 +676,7 @@ const $SponsorsService: Provider = { provide: 'SponsorsService', useExisting: Sp
WebhookTestService,
UtilityService,
FileInfoService,
+ FlashService,
SearchService,
ClipService,
FeaturedService,
diff --git a/packages/backend/src/core/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts
index cd906a72af..cc33fb5c0b 100644
--- a/packages/backend/src/core/CustomEmojiService.ts
+++ b/packages/backend/src/core/CustomEmojiService.ts
@@ -112,19 +112,33 @@ export class CustomEmojiService implements OnApplicationShutdown {
}
@bindThis
- public async update(id: MiEmoji['id'], data: {
+ public async update(data: (
+ { id: MiEmoji['id'], name?: string; } | { name: string; id?: MiEmoji['id'], }
+ ) & {
driveFile?: MiDriveFile;
- name?: string;
category?: string | null;
aliases?: string[];
license?: string | null;
isSensitive?: boolean;
localOnly?: boolean;
roleIdsThatCanBeUsedThisEmojiAsReaction?: MiRole['id'][];
- }, moderator?: MiUser): Promise<void> {
- const emoji = await this.emojisRepository.findOneByOrFail({ id: id });
- const sameNameEmoji = await this.emojisRepository.findOneBy({ name: data.name, host: IsNull() });
- if (sameNameEmoji != null && sameNameEmoji.id !== id) throw new Error('name already exists');
+ }, moderator?: MiUser): Promise<
+ null
+ | 'NO_SUCH_EMOJI'
+ | 'SAME_NAME_EMOJI_EXISTS'
+ > {
+ const emoji = data.id
+ ? await this.getEmojiById(data.id)
+ : await this.getEmojiByName(data.name!);
+ if (emoji === null) return 'NO_SUCH_EMOJI';
+ const id = emoji.id;
+
+ // IDã¨çµµæ–‡å­—åãŒä¸¡æ–¹æŒ‡å®šã•れã¦ã„ã‚‹å ´åˆã¯çµµæ–‡å­—åã®å¤‰æ›´ã‚’行ã†ãŸã‚é‡è¤‡ãƒã‚§ãƒƒã‚¯ãŒå¿…è¦
+ const doNameUpdate = data.id && data.name && (data.name !== emoji.name);
+ if (doNameUpdate) {
+ const isDuplicate = await this.checkDuplicate(data.name!);
+ if (isDuplicate) return 'SAME_NAME_EMOJI_EXISTS';
+ }
await this.emojisRepository.update(emoji.id, {
updatedAt: new Date(),
@@ -151,7 +165,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
const packed = await this.emojiEntityService.packDetailed(emoji.id);
- if (emoji.name === data.name) {
+ if (!doNameUpdate) {
this.globalEventService.publishBroadcastStream('emojiUpdated', {
emojis: [packed],
});
@@ -173,6 +187,7 @@ export class CustomEmojiService implements OnApplicationShutdown {
after: updated,
});
}
+ return null;
}
@bindThis
diff --git a/packages/backend/src/core/FederatedInstanceService.ts b/packages/backend/src/core/FederatedInstanceService.ts
index 7ec565557c..fca3ad847a 100644
--- a/packages/backend/src/core/FederatedInstanceService.ts
+++ b/packages/backend/src/core/FederatedInstanceService.ts
@@ -49,7 +49,7 @@ export class FederatedInstanceService implements OnApplicationShutdown {
}
@bindThis
- public async fetch(host: string): Promise<MiInstance> {
+ public async fetchOrRegister(host: string): Promise<MiInstance> {
host = this.utilityService.toPuny(host);
const cached = await this.federatedInstanceCache.get(host);
@@ -86,6 +86,24 @@ export class FederatedInstanceService implements OnApplicationShutdown {
}
@bindThis
+ public async fetch(host: string): Promise<MiInstance | null> {
+ host = this.utilityService.toPuny(host);
+
+ const cached = await this.federatedInstanceCache.get(host);
+ if (cached !== undefined) return cached;
+
+ const index = await this.instancesRepository.findOneBy({ host });
+
+ if (index == null) {
+ this.federatedInstanceCache.set(host, null);
+ return null;
+ } else {
+ this.federatedInstanceCache.set(host, index);
+ return index;
+ }
+ }
+
+ @bindThis
public async update(id: MiInstance['id'], data: Partial<MiInstance>): Promise<void> {
const result = await this.instancesRepository.createQueryBuilder().update()
.set(data)
diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index aa16468ecb..987999bce7 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -82,7 +82,7 @@ export class FetchInstanceMetadataService {
try {
if (!force) {
- const _instance = await this.federatedInstanceService.fetch(host);
+ const _instance = await this.federatedInstanceService.fetchOrRegister(host);
const now = Date.now();
if (_instance && _instance.infoUpdatedAt && (now - _instance.infoUpdatedAt.getTime() < 1000 * 60 * 60 * 24)) {
// unlock at the finally caluse
diff --git a/packages/backend/src/core/FlashService.ts b/packages/backend/src/core/FlashService.ts
new file mode 100644
index 0000000000..2a98225382
--- /dev/null
+++ b/packages/backend/src/core/FlashService.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { DI } from '@/di-symbols.js';
+import { type FlashsRepository } from '@/models/_.js';
+
+/**
+ * MisskeyPlay関係ã®Service
+ */
+@Injectable()
+export class FlashService {
+ constructor(
+ @Inject(DI.flashsRepository)
+ private flashRepository: FlashsRepository,
+ ) {
+ }
+
+ /**
+ * 人気ã®ã‚ã‚‹Play一覧をå–å¾—ã™ã‚‹.
+ */
+ public async featured(opts?: { offset?: number, limit: number }) {
+ const builder = this.flashRepository.createQueryBuilder('flash')
+ .andWhere('flash.likedCount > 0')
+ .andWhere('flash.visibility = :visibility', { visibility: 'public' })
+ .addOrderBy('flash.likedCount', 'DESC')
+ .addOrderBy('flash.updatedAt', 'DESC')
+ .addOrderBy('flash.id', 'DESC');
+
+ if (opts?.offset) {
+ builder.skip(opts.offset);
+ }
+
+ builder.take(opts?.limit ?? 10);
+
+ return await builder.getMany();
+ }
+}
diff --git a/packages/backend/src/core/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts
index 1bc4599a60..3f282655f1 100644
--- a/packages/backend/src/core/NoteCreateService.ts
+++ b/packages/backend/src/core/NoteCreateService.ts
@@ -222,7 +222,7 @@ export class NoteCreateService implements OnApplicationShutdown {
private cacheService: CacheService,
private latestNoteService: LatestNoteService,
) {
- this.updateNotesCountQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseNotesCount, this.performUpdateNotesCount);
+ this.updateNotesCountQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseNotesCount, this.performUpdateNotesCount);
}
@bindThis
@@ -563,17 +563,17 @@ export class NoteCreateService implements OnApplicationShutdown {
}
// Register host
- if (this.userEntityService.isRemoteUser(user)) {
- this.federatedInstanceService.fetch(user.host).then(async i => {
- if (note.renote && note.text) {
- this.updateNotesCountQueue.enqueue(i.id, 1);
- } else if (!note.renote) {
- this.updateNotesCountQueue.enqueue(i.id, 1);
- }
- if (this.meta.enableChartsForFederatedInstances) {
- this.instanceChart.updateNote(i.host, note, true);
- }
- });
+ if (this.meta.enableStatsForFederatedInstances) {
+ if (this.userEntityService.isRemoteUser(user)) {
+ this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
+ if (note.renote && note.text || !note.renote) {
+ this.updateNotesCountQueue.enqueue(i.id, 1);
+ }
+ if (this.meta.enableChartsForFederatedInstances) {
+ this.instanceChart.updateNote(i.host, note, true);
+ }
+ });
+ }
}
// ãƒãƒƒã‚·ãƒ¥ã‚¿ã‚°æ›´æ–°
diff --git a/packages/backend/src/core/NoteDeleteService.ts b/packages/backend/src/core/NoteDeleteService.ts
index 285db9f152..b51a3143c9 100644
--- a/packages/backend/src/core/NoteDeleteService.ts
+++ b/packages/backend/src/core/NoteDeleteService.ts
@@ -115,25 +115,22 @@ export class NoteDeleteService {
this.perUserNotesChart.update(user, note, false);
}
- if (note.renoteId && note.text) {
- // Decrement notes count (user)
- this.decNotesCountOfUser(user);
- } else if (!note.renoteId) {
+ if (note.renoteId && note.text || !note.renoteId) {
// Decrement notes count (user)
this.decNotesCountOfUser(user);
}
- if (this.userEntityService.isRemoteUser(user)) {
- this.federatedInstanceService.fetch(user.host).then(async i => {
- if (note.renoteId && note.text) {
- this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
- } else if (!note.renoteId) {
- this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
- }
- if (this.meta.enableChartsForFederatedInstances) {
- this.instanceChart.updateNote(i.host, note, false);
- }
- });
+ if (this.meta.enableStatsForFederatedInstances) {
+ if (this.userEntityService.isRemoteUser(user)) {
+ this.federatedInstanceService.fetchOrRegister(user.host).then(async i => {
+ if (note.renoteId && note.text || !note.renoteId) {
+ this.instancesRepository.decrement({ id: i.id }, 'notesCount', 1);
+ }
+ if (this.meta.enableChartsForFederatedInstances) {
+ this.instanceChart.updateNote(i.host, note, false);
+ }
+ });
+ }
}
}
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index dc13aa21bf..341eb26c99 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -94,6 +94,13 @@ export class QueueService {
repeat: { pattern: '0 0 * * *' },
removeOnComplete: true,
});
+
+ this.systemQueue.add('checkModeratorsActivity', {
+ }, {
+ // 毎時30分ã«èµ·å‹•
+ repeat: { pattern: '30 * * * *' },
+ removeOnComplete: true,
+ });
}
@bindThis
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 64f7539031..a1a461ab96 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -105,6 +105,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
@Injectable()
export class RoleService implements OnApplicationShutdown, OnModuleInit {
+ private rootUserIdCache: MemorySingleCache<MiUser['id']>;
private rolesCache: MemorySingleCache<MiRole[]>;
private roleAssignmentByUserIdCache: MemoryKVCache<MiRoleAssignment[]>;
private notificationService: NotificationService;
@@ -140,6 +141,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
private moderationLogService: ModerationLogService,
private fanoutTimelineService: FanoutTimelineService,
) {
+ this.rootUserIdCache = new MemorySingleCache<MiUser['id']>(1000 * 60 * 60 * 24 * 7); // 1week. rootユーザã®IDã¯ä¸å¤‰ãªã®ã§é•·ã‚ã«
this.rolesCache = new MemorySingleCache<MiRole[]>(1000 * 60 * 60); // 1h
this.roleAssignmentByUserIdCache = new MemoryKVCache<MiRoleAssignment[]>(1000 * 60 * 5); // 5m
@@ -422,49 +424,78 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
}
@bindThis
- public async isExplorable(role: { id: MiRole['id']} | null): Promise<boolean> {
+ public async isExplorable(role: { id: MiRole['id'] } | null): Promise<boolean> {
if (role == null) return false;
const check = await this.rolesRepository.findOneBy({ id: role.id });
if (check == null) return false;
return check.isExplorable;
}
+ /**
+ * モデレーター権é™ã®ãƒ­ãƒ¼ãƒ«ãŒå‰²ã‚Šå½“ã¦ã‚‰ã‚Œã¦ã„るユーザID一覧をå–å¾—ã™ã‚‹.
+ *
+ * @param opts.includeAdmins 管ç†è€…権é™ã‚‚å«ã‚ã‚‹ã‹(デフォルト: true)
+ * @param opts.includeRoot rootユーザもå«ã‚ã‚‹ã‹(デフォルト: false)
+ * @param opts.excludeExpire 期é™åˆ‡ã‚Œã®ãƒ­ãƒ¼ãƒ«ã‚’除外ã™ã‚‹ã‹(デフォルト: false)
+ */
@bindThis
- public async getModeratorIds(includeAdmins = true, excludeExpire = false): Promise<MiUser['id'][]> {
+ public async getModeratorIds(opts?: {
+ includeAdmins?: boolean,
+ includeRoot?: boolean,
+ excludeExpire?: boolean,
+ }): Promise<MiUser['id'][]> {
+ const includeAdmins = opts?.includeAdmins ?? true;
+ const includeRoot = opts?.includeRoot ?? false;
+ const excludeExpire = opts?.excludeExpire ?? false;
+
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
const moderatorRoles = includeAdmins
? roles.filter(r => r.isModerator || r.isAdministrator)
: roles.filter(r => r.isModerator);
- // TODO: isRootãªã‚¢ã‚«ã‚¦ãƒ³ãƒˆã‚‚å«ã‚ã‚‹
const assigns = moderatorRoles.length > 0
? await this.roleAssignmentsRepository.findBy({ roleId: In(moderatorRoles.map(r => r.id)) })
: [];
+ // Setを経由ã—ã¦é‡è¤‡ã‚’除去(ユーザIDã¯é‡è¤‡ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ã®ã§ï¼‰
const now = Date.now();
- const result = [
- // Setを経由ã—ã¦é‡è¤‡ã‚’除去(ユーザIDã¯é‡è¤‡ã™ã‚‹å¯èƒ½æ€§ãŒã‚ã‚‹ã®ã§ï¼‰
- ...new Set(
- assigns
- .filter(it =>
- (excludeExpire)
- ? (it.expiresAt == null || it.expiresAt.getTime() > now)
- : true,
- )
- .map(a => a.userId),
- ),
- ];
+ const resultSet = new Set(
+ assigns
+ .filter(it =>
+ (excludeExpire)
+ ? (it.expiresAt == null || it.expiresAt.getTime() > now)
+ : true,
+ )
+ .map(a => a.userId),
+ );
+
+ if (includeRoot) {
+ const rootUserId = await this.rootUserIdCache.fetch(async () => {
+ const it = await this.usersRepository.createQueryBuilder('users')
+ .select('id')
+ .where({ isRoot: true })
+ .getRawOne<{ id: string }>();
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ return it!.id;
+ });
+ resultSet.add(rootUserId);
+ }
- return result.sort((x, y) => x.localeCompare(y));
+ return [...resultSet].sort((x, y) => x.localeCompare(y));
}
@bindThis
- public async getModerators(includeAdmins = true): Promise<MiUser[]> {
- const ids = await this.getModeratorIds(includeAdmins);
- const users = ids.length > 0 ? await this.usersRepository.findBy({
- id: In(ids),
- }) : [];
- return users;
+ public async getModerators(opts?: {
+ includeAdmins?: boolean,
+ includeRoot?: boolean,
+ excludeExpire?: boolean,
+ }): Promise<MiUser[]> {
+ const ids = await this.getModeratorIds(opts);
+ return ids.length > 0
+ ? await this.usersRepository.findBy({
+ id: In(ids),
+ })
+ : [];
}
@bindThis
diff --git a/packages/backend/src/core/SignupService.ts b/packages/backend/src/core/SignupService.ts
index 1b0b1e5bbd..a1e23a49c1 100644
--- a/packages/backend/src/core/SignupService.ts
+++ b/packages/backend/src/core/SignupService.ts
@@ -155,8 +155,8 @@ export class SignupService {
}));
});
- this.usersChart.update(account, true).then();
- this.userService.notifySystemWebhook(account, 'userCreated').then();
+ this.usersChart.update(account, true);
+ this.userService.notifySystemWebhook(account, 'userCreated');
return { account, secret };
}
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
index bb7c6b8c0e..db6407dcb3 100644
--- a/packages/backend/src/core/SystemWebhookService.ts
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -101,8 +101,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
.log(updater, 'createSystemWebhook', {
systemWebhookId: webhook.id,
webhook: webhook,
- })
- .then();
+ });
return webhook;
}
@@ -139,8 +138,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
systemWebhookId: beforeEntity.id,
before: beforeEntity,
after: afterEntity,
- })
- .then();
+ });
return afterEntity;
}
@@ -158,8 +156,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
.log(updater, 'deleteSystemWebhook', {
systemWebhookId: webhook.id,
webhook,
- })
- .then();
+ });
}
/**
diff --git a/packages/backend/src/core/UserFollowingService.ts b/packages/backend/src/core/UserFollowingService.ts
index 77e7b60bea..8963003057 100644
--- a/packages/backend/src/core/UserFollowingService.ts
+++ b/packages/backend/src/core/UserFollowingService.ts
@@ -305,20 +305,22 @@ export class UserFollowingService implements OnModuleInit {
//#endregion
//#region Update instance stats
- if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
- this.federatedInstanceService.fetch(follower.host).then(async i => {
- this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
- if (this.meta.enableChartsForFederatedInstances) {
- this.instanceChart.updateFollowing(i.host, true);
- }
- });
- } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
- this.federatedInstanceService.fetch(followee.host).then(async i => {
- this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
- if (this.meta.enableChartsForFederatedInstances) {
- this.instanceChart.updateFollowers(i.host, true);
- }
- });
+ if (this.meta.enableStatsForFederatedInstances) {
+ if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
+ this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
+ this.instancesRepository.increment({ id: i.id }, 'followingCount', 1);
+ if (this.meta.enableChartsForFederatedInstances) {
+ this.instanceChart.updateFollowing(i.host, true);
+ }
+ });
+ } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
+ this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
+ this.instancesRepository.increment({ id: i.id }, 'followersCount', 1);
+ if (this.meta.enableChartsForFederatedInstances) {
+ this.instanceChart.updateFollowers(i.host, true);
+ }
+ });
+ }
}
//#endregion
@@ -437,20 +439,22 @@ export class UserFollowingService implements OnModuleInit {
//#endregion
//#region Update instance stats
- if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
- this.federatedInstanceService.fetch(follower.host).then(async i => {
- this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
- if (this.meta.enableChartsForFederatedInstances) {
- this.instanceChart.updateFollowing(i.host, false);
- }
- });
- } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
- this.federatedInstanceService.fetch(followee.host).then(async i => {
- this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
- if (this.meta.enableChartsForFederatedInstances) {
- this.instanceChart.updateFollowers(i.host, false);
- }
- });
+ if (this.meta.enableStatsForFederatedInstances) {
+ if (this.userEntityService.isRemoteUser(follower) && this.userEntityService.isLocalUser(followee)) {
+ this.federatedInstanceService.fetchOrRegister(follower.host).then(async i => {
+ this.instancesRepository.decrement({ id: i.id }, 'followingCount', 1);
+ if (this.meta.enableChartsForFederatedInstances) {
+ this.instanceChart.updateFollowing(i.host, false);
+ }
+ });
+ } else if (this.userEntityService.isLocalUser(follower) && this.userEntityService.isRemoteUser(followee)) {
+ this.federatedInstanceService.fetchOrRegister(followee.host).then(async i => {
+ this.instancesRepository.decrement({ id: i.id }, 'followersCount', 1);
+ if (this.meta.enableChartsForFederatedInstances) {
+ this.instanceChart.updateFollowers(i.host, false);
+ }
+ });
+ }
}
//#endregion
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
index 4a31c1d17a..0304cae355 100644
--- a/packages/backend/src/core/WebhookTestService.ts
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -12,11 +12,18 @@ import { Packed } from '@/misc/json-schema.js';
import { type WebhookEventTypes } from '@/models/Webhook.js';
import { UserWebhookService } from '@/core/UserWebhookService.js';
import { QueueService } from '@/core/QueueService.js';
+import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
const oneDayMillis = 24 * 60 * 60 * 1000;
-function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUserReport {
- return {
+type AbuseUserReportDto = Omit<MiAbuseUserReport, 'targetUser' | 'reporter' | 'assignee'> & {
+ targetUser: Packed<'UserLite'> | null,
+ reporter: Packed<'UserLite'> | null,
+ assignee: Packed<'UserLite'> | null,
+};
+
+function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseUserReportDto {
+ const result: MiAbuseUserReport = {
id: 'dummy-abuse-report1',
targetUserId: 'dummy-target-user',
targetUser: null,
@@ -29,8 +36,17 @@ function generateAbuseReport(override?: Partial<MiAbuseUserReport>): MiAbuseUser
comment: 'This is a dummy report for testing purposes.',
targetUserHost: null,
reporterHost: null,
+ resolvedAs: null,
+ moderationNote: 'foo',
...override,
};
+
+ return {
+ ...result,
+ targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
+ reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
+ assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
+ };
}
function generateDummyUser(override?: Partial<MiUser>): MiUser {
@@ -287,7 +303,8 @@ const dummyUser3 = generateDummyUser({
@Injectable()
export class WebhookTestService {
- public static NoSuchWebhookError = class extends Error {};
+ public static NoSuchWebhookError = class extends Error {
+ };
constructor(
private userWebhookService: UserWebhookService,
@@ -449,6 +466,22 @@ export class WebhookTestService {
send(toPackedUserLite(dummyUser1));
break;
}
+ case 'inactiveModeratorsWarning': {
+ const dummyTime: ModeratorInactivityRemainingTime = {
+ time: 100000,
+ asDays: 1,
+ asHours: 24,
+ };
+
+ send({
+ remainingTime: dummyTime,
+ });
+ break;
+ }
+ case 'inactiveModeratorsInvitationOnlyChanged': {
+ send({});
+ break;
+ }
}
}
}
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index 2046dad099..ebe947f6b9 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -434,13 +434,15 @@ export class ApPersonService implements OnModuleInit {
this.cacheService.uriPersonCache.set(user.uri, user);
// Register host
- this.federatedInstanceService.fetch(host).then(i => {
- this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
- this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
- if (this.meta.enableChartsForFederatedInstances) {
- this.instanceChart.newUser(i.host);
- }
- });
+ if (this.meta.enableStatsForFederatedInstances) {
+ this.federatedInstanceService.fetchOrRegister(host).then(i => {
+ this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
+ if (this.meta.enableChartsForFederatedInstances) {
+ this.instanceChart.newUser(i.host);
+ }
+ this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
+ });
+ }
this.usersChart.update(user, true);
diff --git a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
index a13c244c19..70ead890ab 100644
--- a/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
+++ b/packages/backend/src/core/entities/AbuseUserReportEntityService.ts
@@ -53,6 +53,8 @@ export class AbuseUserReportEntityService {
schema: 'UserDetailedNotMe',
}) : null,
forwarded: report.forwarded,
+ resolvedAs: report.resolvedAs,
+ moderationNote: report.moderationNote,
});
}
diff --git a/packages/backend/src/core/entities/FlashEntityService.ts b/packages/backend/src/core/entities/FlashEntityService.ts
index 4aa7104c1e..7b0150f5b6 100644
--- a/packages/backend/src/core/entities/FlashEntityService.ts
+++ b/packages/backend/src/core/entities/FlashEntityService.ts
@@ -5,10 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import { DI } from '@/di-symbols.js';
-import type { FlashsRepository, FlashLikesRepository } from '@/models/_.js';
-import { awaitAll } from '@/misc/prelude/await-all.js';
+import type { FlashLikesRepository, FlashsRepository } from '@/models/_.js';
import type { Packed } from '@/misc/json-schema.js';
-import type { } from '@/models/Blocking.js';
import type { MiUser } from '@/models/User.js';
import type { MiFlash } from '@/models/Flash.js';
import { bindThis } from '@/decorators.js';
@@ -20,10 +18,8 @@ export class FlashEntityService {
constructor(
@Inject(DI.flashsRepository)
private flashsRepository: FlashsRepository,
-
@Inject(DI.flashLikesRepository)
private flashLikesRepository: FlashLikesRepository,
-
private userEntityService: UserEntityService,
private idService: IdService,
) {
@@ -34,25 +30,36 @@ export class FlashEntityService {
src: MiFlash['id'] | MiFlash,
me?: { id: MiUser['id'] } | null | undefined,
hint?: {
- packedUser?: Packed<'UserLite'>
+ packedUser?: Packed<'UserLite'>,
+ likedFlashIds?: MiFlash['id'][],
},
): Promise<Packed<'Flash'>> {
const meId = me ? me.id : null;
const flash = typeof src === 'object' ? src : await this.flashsRepository.findOneByOrFail({ id: src });
- return await awaitAll({
+ // { schema: 'UserDetailed' } ã™ã‚‹ã¨ç„¡é™ãƒ«ãƒ¼ãƒ—ã™ã‚‹ã®ã§æ³¨æ„
+ const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
+
+ let isLiked = undefined;
+ if (meId) {
+ isLiked = hint?.likedFlashIds
+ ? hint.likedFlashIds.includes(flash.id)
+ : await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } });
+ }
+
+ return {
id: flash.id,
createdAt: this.idService.parse(flash.id).date.toISOString(),
updatedAt: flash.updatedAt.toISOString(),
userId: flash.userId,
- user: hint?.packedUser ?? this.userEntityService.pack(flash.user ?? flash.userId, me), // { schema: 'UserDetailed' } ã™ã‚‹ã¨ç„¡é™ãƒ«ãƒ¼ãƒ—ã™ã‚‹ã®ã§æ³¨æ„
+ user: user,
title: flash.title,
summary: flash.summary,
script: flash.script,
visibility: flash.visibility,
likedCount: flash.likedCount,
- isLiked: meId ? await this.flashLikesRepository.exists({ where: { flashId: flash.id, userId: meId } }) : undefined,
- });
+ isLiked: isLiked,
+ };
}
@bindThis
@@ -63,7 +70,19 @@ export class FlashEntityService {
const _users = flashes.map(({ user, userId }) => user ?? userId);
const _userMap = await this.userEntityService.packMany(_users, me)
.then(users => new Map(users.map(u => [u.id, u])));
- return Promise.all(flashes.map(flash => this.pack(flash, me, { packedUser: _userMap.get(flash.userId) })));
+ const _likedFlashIds = me
+ ? await this.flashLikesRepository.createQueryBuilder('flashLike')
+ .select('flashLike.flashId')
+ .where('flashLike.userId = :userId', { userId: me.id })
+ .getRawMany<{ flashLike_flashId: string }>()
+ .then(likes => [...new Set(likes.map(like => like.flashLike_flashId))])
+ : [];
+ return Promise.all(
+ flashes.map(flash => this.pack(flash, me, {
+ packedUser: _userMap.get(flash.userId),
+ likedFlashIds: _likedFlashIds,
+ })),
+ );
}
}
diff --git a/packages/backend/src/core/entities/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts
index b2b9aebb79..7d7b4cbd81 100644
--- a/packages/backend/src/core/entities/MetaEntityService.ts
+++ b/packages/backend/src/core/entities/MetaEntityService.ts
@@ -100,6 +100,7 @@ export class MetaEntityService {
turnstileSiteKey: instance.turnstileSiteKey,
enableFC: instance.enableFC,
fcSiteKey: instance.fcSiteKey,
+ enableTestcaptcha: instance.enableTestcaptcha,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl ?? '/assets/ai.png',
diff --git a/packages/backend/src/core/entities/NoteEntityService.ts b/packages/backend/src/core/entities/NoteEntityService.ts
index 4dd17c5af3..1631daad88 100644
--- a/packages/backend/src/core/entities/NoteEntityService.ts
+++ b/packages/backend/src/core/entities/NoteEntityService.ts
@@ -23,6 +23,30 @@ import type { UserEntityService } from './UserEntityService.js';
import type { DriveFileEntityService } from './DriveFileEntityService.js';
import type { Config } from '@/config.js';
+// is-renote.tsã¨ã‚ˆã—ãªã«ãƒªãƒ³ã‚¯
+function isPureRenote(note: MiNote): note is MiNote & { renoteId: MiNote['id']; renote: MiNote } {
+ return (
+ note.renote != null &&
+ note.reply == null &&
+ note.text == null &&
+ note.cw == null &&
+ (note.fileIds == null || note.fileIds.length === 0) &&
+ !note.hasPoll
+ );
+}
+
+function getAppearNoteIds(notes: MiNote[]): Set<string> {
+ const appearNoteIds = new Set<string>();
+ for (const note of notes) {
+ if (isPureRenote(note)) {
+ appearNoteIds.add(note.renoteId);
+ } else {
+ appearNoteIds.add(note.id);
+ }
+ }
+ return appearNoteIds;
+}
+
@Injectable()
export class NoteEntityService implements OnModuleInit {
private userEntityService: UserEntityService;
@@ -94,7 +118,7 @@ export class NoteEntityService implements OnModuleInit {
hide = false;
} else {
// 指定ã•れã¦ã„ã‚‹ã‹ã©ã†ã‹
- const specified = packedNote.visibleUserIds!.some((id: any) => meId === id);
+ const specified = packedNote.visibleUserIds!.some(id => meId === id);
if (specified) {
hide = false;
@@ -244,7 +268,7 @@ export class NoteEntityService implements OnModuleInit {
return true;
} else {
// 指定ã•れã¦ã„ã‚‹ã‹ã©ã†ã‹
- return note.visibleUserIds.some((id: any) => meId === id);
+ return note.visibleUserIds.some(id => meId === id);
}
}
@@ -437,7 +461,7 @@ export class NoteEntityService implements OnModuleInit {
) {
if (notes.length === 0) return [];
- const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany(notes.map(x => x.id)) : null;
+ const bufferedReactions = this.meta.enableReactionsBuffering ? await this.reactionsBufferingService.getMany([...getAppearNoteIds(notes)]) : null;
const meId = me ? me.id : null;
const myReactionsMap = new Map<MiNote['id'], string | null>();
@@ -448,7 +472,7 @@ export class NoteEntityService implements OnModuleInit {
const oldId = this.idService.gen(Date.now() - 2000);
for (const note of notes) {
- if (note.renote && (note.text == null && note.fileIds.length === 0)) { // pure renote
+ if (isPureRenote(note)) {
const reactionsCount = Object.values(this.reactionsBufferingService.mergeReactions(note.renote.reactions, bufferedReactions?.get(note.renote.id)?.deltas ?? {})).reduce((a, b) => a + b, 0);
if (reactionsCount === 0) {
myReactionsMap.set(note.renote.id, null);
diff --git a/packages/backend/src/core/entities/UserEntityService.ts b/packages/backend/src/core/entities/UserEntityService.ts
index b1832ca0f5..a421e23195 100644
--- a/packages/backend/src/core/entities/UserEntityService.ts
+++ b/packages/backend/src/core/entities/UserEntityService.ts
@@ -597,11 +597,6 @@ export class UserEntityService implements OnModuleInit {
publicReactions: this.isLocalUser(user) ? profile!.publicReactions : false, // https://github.com/misskey-dev/misskey/issues/12964
followersVisibility: profile!.followersVisibility,
followingVisibility: profile!.followingVisibility,
- twoFactorEnabled: profile!.twoFactorEnabled,
- usePasswordLessLogin: profile!.usePasswordLessLogin,
- securityKeys: profile!.twoFactorEnabled
- ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
- : false,
roles: this.roleService.getUserRoles(user.id).then(roles => roles.filter(role => role.isPublic).sort((a, b) => b.displayOrder - a.displayOrder).map(role => ({
id: role.id,
name: role.name,
@@ -616,6 +611,14 @@ export class UserEntityService implements OnModuleInit {
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),
+ ...(isDetailed && (isMe || iAmModerator) ? {
+ twoFactorEnabled: profile!.twoFactorEnabled,
+ usePasswordLessLogin: profile!.usePasswordLessLogin,
+ securityKeys: profile!.twoFactorEnabled
+ ? this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1)
+ : false,
+ } : {}),
+
...(isDetailed && isMe ? {
avatarId: user.avatarId,
bannerId: user.bannerId,
diff --git a/packages/backend/src/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts
index c128fded14..43193d12f5 100644
--- a/packages/backend/src/misc/is-renote.ts
+++ b/packages/backend/src/misc/is-renote.ts
@@ -6,6 +6,8 @@
import type { MiNote } from '@/models/Note.js';
import type { Packed } from '@/misc/json-schema.js';
+// NoteEntityService.isPureRenote ã¨ã‚ˆã—ãªã«ãƒªãƒ³ã‚¯
+
type Renote =
MiNote & {
renoteId: NonNullable<MiNote['renoteId']>
diff --git a/packages/backend/src/models/AbuseUserReport.ts b/packages/backend/src/models/AbuseUserReport.ts
index 0615fd7eb5..cb5672e4ac 100644
--- a/packages/backend/src/models/AbuseUserReport.ts
+++ b/packages/backend/src/models/AbuseUserReport.ts
@@ -50,6 +50,9 @@ export class MiAbuseUserReport {
})
public resolved: boolean;
+ /**
+ * リモートサーãƒãƒ¼ã«è»¢é€ã—ãŸã‹ã©ã†ã‹
+ */
@Column('boolean', {
default: false,
})
@@ -60,6 +63,21 @@ export class MiAbuseUserReport {
})
public comment: string;
+ @Column('varchar', {
+ length: 8192, default: '',
+ })
+ public moderationNote: string;
+
+ /**
+ * accept æ˜¯èª ... é€šå ±å†…å®¹ãŒæ­£å½“ã§ã‚りã€è‚¯å®šçš„ã«å¯¾å¿œã•れãŸ
+ * reject å¦èª ... é€šå ±å†…å®¹ãŒæ­£å½“ã§ãªãã€å¦å®šçš„ã«å¯¾å¿œã•れãŸ
+ * null ... ãã®ä»–
+ */
+ @Column('varchar', {
+ length: 128, nullable: true,
+ })
+ public resolvedAs: 'accept' | 'reject' | null;
+
//#region Denormalized fields
@Index()
@Column('varchar', {
diff --git a/packages/backend/src/models/Flash.ts b/packages/backend/src/models/Flash.ts
index a1469a0d94..5db7dca992 100644
--- a/packages/backend/src/models/Flash.ts
+++ b/packages/backend/src/models/Flash.ts
@@ -7,6 +7,9 @@ import { Entity, Index, JoinColumn, Column, PrimaryColumn, ManyToOne } from 'typ
import { id } from './util/id.js';
import { MiUser } from './User.js';
+export const flashVisibility = ['public', 'private'] as const;
+export type FlashVisibility = typeof flashVisibility[number];
+
@Entity('flash')
export class MiFlash {
@PrimaryColumn(id())
@@ -63,5 +66,5 @@ export class MiFlash {
@Column('varchar', {
length: 512, default: 'public',
})
- public visibility: 'public' | 'private';
+ public visibility: FlashVisibility;
}
diff --git a/packages/backend/src/models/Meta.ts b/packages/backend/src/models/Meta.ts
index 0ea6765d6a..3fc3f273dd 100644
--- a/packages/backend/src/models/Meta.ts
+++ b/packages/backend/src/models/Meta.ts
@@ -84,6 +84,11 @@ export class MiMeta {
@Column('varchar', {
length: 1024, array: true, default: '{}',
})
+ public prohibitedWordsForNameOfUser: string[];
+
+ @Column('varchar', {
+ length: 1024, array: true, default: '{}',
+ })
public silencedHosts: string[];
@Column('varchar', {
@@ -286,6 +291,11 @@ export class MiMeta {
})
public fcSecretKey: string | null;
+ @Column('boolean', {
+ default: false,
+ })
+ public enableTestcaptcha: boolean;
+
// chaptcha系を追加ã—ãŸéš›ã«ã¯nodeinfoã®ãƒ¬ã‚¹ãƒãƒ³ã‚¹ã«è¿½åŠ ã™ã‚‹ã®ã‚’忘れãªã„よã†ã«ã™ã‚‹ã“ã¨
@Column('enum', {
@@ -570,6 +580,11 @@ export class MiMeta {
public enableChartsForFederatedInstances: boolean;
@Column('boolean', {
+ default: true,
+ })
+ public enableStatsForFederatedInstances: boolean;
+
+ @Column('boolean', {
default: false,
})
public enableServerMachineStats: boolean;
diff --git a/packages/backend/src/models/Notification.ts b/packages/backend/src/models/Notification.ts
index c4f046c565..7e835eb3ba 100644
--- a/packages/backend/src/models/Notification.ts
+++ b/packages/backend/src/models/Notification.ts
@@ -3,12 +3,12 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
+import { userExportableEntities } from '@/types.js';
import { MiUser } from './User.js';
import { MiNote } from './Note.js';
import { MiAccessToken } from './AccessToken.js';
import { MiRole } from './Role.js';
import { MiDriveFile } from './DriveFile.js';
-import { userExportableEntities } from '@/types.js';
export type MiNotification = {
type: 'note';
@@ -87,6 +87,10 @@ export type MiNotification = {
exportedEntity: typeof userExportableEntities[number];
fileId: MiDriveFile['id'];
} | {
+ type: 'login';
+ id: string;
+ createdAt: string;
+} | {
type: 'app';
id: string;
createdAt: string;
diff --git a/packages/backend/src/models/SystemWebhook.ts b/packages/backend/src/models/SystemWebhook.ts
index d6c27eae51..1a7ce4962b 100644
--- a/packages/backend/src/models/SystemWebhook.ts
+++ b/packages/backend/src/models/SystemWebhook.ts
@@ -14,6 +14,10 @@ export const systemWebhookEventTypes = [
'abuseReportResolved',
// ユーザãŒä½œæˆã•ã‚ŒãŸæ™‚
'userCreated',
+ // モデレータãŒä¸€å®šæœŸé–“ä¸åœ¨ã§ã‚る警告
+ 'inactiveModeratorsWarning',
+ // モデレータãŒä¸€å®šæœŸé–“ä¸åœ¨ã®ãŸã‚システムã«ã‚ˆã‚Šæ‹›å¾…制ã¸ã¨å¤‰æ›´ã•れãŸ
+ 'inactiveModeratorsInvitationOnlyChanged',
] as const;
export type SystemWebhookEventType = typeof systemWebhookEventTypes[number];
diff --git a/packages/backend/src/models/json-schema/meta.ts b/packages/backend/src/models/json-schema/meta.ts
index decdbd5650..5179e5d51c 100644
--- a/packages/backend/src/models/json-schema/meta.ts
+++ b/packages/backend/src/models/json-schema/meta.ts
@@ -139,6 +139,10 @@ export const packedMetaLiteSchema = {
type: 'boolean',
optional: false, nullable: true,
},
+ enableTestcaptcha: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
swPublickey: {
type: 'string',
optional: false, nullable: true,
diff --git a/packages/backend/src/models/json-schema/notification.ts b/packages/backend/src/models/json-schema/notification.ts
index 990e8957cf..4a43aece8d 100644
--- a/packages/backend/src/models/json-schema/notification.ts
+++ b/packages/backend/src/models/json-schema/notification.ts
@@ -329,6 +329,16 @@ export const packedNotificationSchema = {
type: {
type: 'string',
optional: false, nullable: false,
+ enum: ['login'],
+ },
+ },
+ }, {
+ type: 'object',
+ properties: {
+ ...baseSchema.properties,
+ type: {
+ type: 'string',
+ optional: false, nullable: false,
enum: ['app'],
},
body: {
diff --git a/packages/backend/src/models/json-schema/user.ts b/packages/backend/src/models/json-schema/user.ts
index d5e847cc40..a6517bfb98 100644
--- a/packages/backend/src/models/json-schema/user.ts
+++ b/packages/backend/src/models/json-schema/user.ts
@@ -392,21 +392,6 @@ export const packedUserDetailedNotMeOnlySchema = {
nullable: false, optional: false,
enum: ['public', 'followers', 'private'],
},
- twoFactorEnabled: {
- type: 'boolean',
- nullable: false, optional: false,
- default: false,
- },
- usePasswordLessLogin: {
- type: 'boolean',
- nullable: false, optional: false,
- default: false,
- },
- securityKeys: {
- type: 'boolean',
- nullable: false, optional: false,
- default: false,
- },
roles: {
type: 'array',
nullable: false, optional: false,
@@ -428,6 +413,18 @@ export const packedUserDetailedNotMeOnlySchema = {
type: 'string',
nullable: false, optional: true,
},
+ twoFactorEnabled: {
+ type: 'boolean',
+ nullable: false, optional: true,
+ },
+ usePasswordLessLogin: {
+ type: 'boolean',
+ nullable: false, optional: true,
+ },
+ securityKeys: {
+ type: 'boolean',
+ nullable: false, optional: true,
+ },
//#region relations
isFollowing: {
type: 'boolean',
@@ -689,6 +686,21 @@ export const packedMeDetailedOnlySchema = {
nullable: false, optional: false,
ref: 'RolePolicies',
},
+ twoFactorEnabled: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ default: false,
+ },
+ usePasswordLessLogin: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ default: false,
+ },
+ securityKeys: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ default: false,
+ },
//#region secrets
email: {
type: 'string',
diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts
index 7c6675b15d..dbb7a27460 100644
--- a/packages/backend/src/queue/QueueProcessorModule.ts
+++ b/packages/backend/src/queue/QueueProcessorModule.ts
@@ -6,6 +6,7 @@
import { Module } from '@nestjs/common';
import { CoreModule } from '@/core/CoreModule.js';
import { GlobalModule } from '@/GlobalModule.js';
+import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { QueueLoggerService } from './QueueLoggerService.js';
import { QueueProcessorService } from './QueueProcessorService.js';
import { DeliverProcessorService } from './processors/DeliverProcessorService.js';
@@ -84,6 +85,8 @@ import { RelationshipProcessorService } from './processors/RelationshipProcessor
DeliverProcessorService,
InboxProcessorService,
AggregateRetentionProcessorService,
+ CheckExpiredMutingsProcessorService,
+ CheckModeratorsActivityProcessorService,
QueueProcessorService,
],
exports: [
diff --git a/packages/backend/src/queue/QueueProcessorService.ts b/packages/backend/src/queue/QueueProcessorService.ts
index eaeb6d58df..28a74bbb4a 100644
--- a/packages/backend/src/queue/QueueProcessorService.ts
+++ b/packages/backend/src/queue/QueueProcessorService.ts
@@ -10,6 +10,7 @@ import type { Config } from '@/config.js';
import { DI } from '@/di-symbols.js';
import type Logger from '@/logger.js';
import { bindThis } from '@/decorators.js';
+import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
import { UserWebhookDeliverProcessorService } from './processors/UserWebhookDeliverProcessorService.js';
import { SystemWebhookDeliverProcessorService } from './processors/SystemWebhookDeliverProcessorService.js';
import { EndedPollNotificationProcessorService } from './processors/EndedPollNotificationProcessorService.js';
@@ -68,7 +69,7 @@ function getJobInfo(job: Bull.Job | undefined, increment = false): string {
// onActiveã¨ã‹onCompletedã®attemptsMadeãŒãªãœã‹0å§‹ã¾ã‚Šãªã®ã§ã‚¤ãƒ³ã‚¯ãƒªãƒ¡ãƒ³ãƒˆã™ã‚‹
const currentAttempts = job.attemptsMade + (increment ? 1 : 0);
- const maxAttempts = job.opts ? job.opts.attempts : 0;
+ const maxAttempts = job.opts.attempts ?? 0;
return `id=${job.id} attempts=${currentAttempts}/${maxAttempts} age=${formated}`;
}
@@ -124,6 +125,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
private aggregateRetentionProcessorService: AggregateRetentionProcessorService,
private checkExpiredMutingsProcessorService: CheckExpiredMutingsProcessorService,
private bakeBufferedReactionsProcessorService: BakeBufferedReactionsProcessorService,
+ private checkModeratorsActivityProcessorService: CheckModeratorsActivityProcessorService,
private cleanProcessorService: CleanProcessorService,
) {
this.logger = this.queueLoggerService.logger;
@@ -164,6 +166,7 @@ export class QueueProcessorService implements OnApplicationShutdown {
case 'aggregateRetention': return this.aggregateRetentionProcessorService.process();
case 'checkExpiredMutings': return this.checkExpiredMutingsProcessorService.process();
case 'bakeBufferedReactions': return this.bakeBufferedReactionsProcessorService.process();
+ case 'checkModeratorsActivity': return this.checkModeratorsActivityProcessorService.process();
case 'clean': return this.cleanProcessorService.process();
default: throw new Error(`unrecognized job type ${job.name} for system`);
}
diff --git a/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
new file mode 100644
index 0000000000..87183cb342
--- /dev/null
+++ b/packages/backend/src/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -0,0 +1,292 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { In } from 'typeorm';
+import type Logger from '@/logger.js';
+import { bindThis } from '@/decorators.js';
+import { MetaService } from '@/core/MetaService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { EmailService } from '@/core/EmailService.js';
+import { MiUser, type UserProfilesRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+import { QueueLoggerService } from '../QueueLoggerService.js';
+
+// モデレーターãŒä¸åœ¨ã¨åˆ¤æ–­ã™ã‚‹æ—¥ä»˜ã®é–¾å€¤
+const MODERATOR_INACTIVITY_LIMIT_DAYS = 7;
+// è­¦å‘Šé€šçŸ¥ã‚„ãƒ­ã‚°å‡ºåŠ›ã‚’è¡Œã†æ®‹æ—¥æ•°ã®é–¾å€¤
+const MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS = 2;
+// 期é™ã‹ã‚‰6時間ã”ã¨ã«é€šçŸ¥ã‚’行ã†
+const MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS = 6;
+const ONE_HOUR_MILLI_SEC = 1000 * 60 * 60;
+const ONE_DAY_MILLI_SEC = ONE_HOUR_MILLI_SEC * 24;
+
+export type ModeratorInactivityEvaluationResult = {
+ isModeratorsInactive: boolean;
+ inactiveModerators: MiUser[];
+ remainingTime: ModeratorInactivityRemainingTime;
+}
+
+export type ModeratorInactivityRemainingTime = {
+ time: number;
+ asHours: number;
+ asDays: number;
+};
+
+function generateModeratorInactivityMail(remainingTime: ModeratorInactivityRemainingTime) {
+ const subject = 'Moderator Inactivity Warning / モデレーターä¸åœ¨ã®é€šçŸ¥';
+
+ const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`;
+ const timeVariantJa = remainingTime.asDays === 0 ? `${remainingTime.asHours} 時間` : `${remainingTime.asDays} 日間`;
+ const message = [
+ 'To Moderators,',
+ '',
+ `A moderator has been inactive for a period of time. If there are ${timeVariant} of inactivity left, it will switch to invitation only.`,
+ 'If you do not wish to move to invitation only, you must log into Misskey and update your last active date and time.',
+ '',
+ '---------------',
+ '',
+ 'To モデレーターå„ä½',
+ '',
+ `モデレーターãŒä¸€å®šæœŸé–“活動ã—ã¦ã„ãªã„よã†ã§ã™ã€‚ã‚ã¨${timeVariantJa}活動ã—ã¦ã„ãªã„状態ãŒç¶šãã¨æ‹›å¾…制ã«åˆ‡ã‚Šæ›¿ã‚りã¾ã™ã€‚`,
+ '招待制ã«åˆ‡ã‚Šæ›¿ã‚ã‚‹ã“ã¨ã‚’望ã¾ãªã„å ´åˆã¯ã€Misskeyã«ãƒ­ã‚°ã‚¤ãƒ³ã—ã¦æœ€çµ‚アクティブ日時を更新ã—ã¦ãã ã•ã„。',
+ '',
+ ];
+
+ const html = message.join('<br>');
+ const text = message.join('\n');
+
+ return {
+ subject,
+ html,
+ text,
+ };
+}
+
+function generateInvitationOnlyChangedMail() {
+ const subject = 'Change to Invitation-Only / 招待制ã«å¤‰æ›´ã•れã¾ã—ãŸ';
+
+ const message = [
+ 'To Moderators,',
+ '',
+ `Changed to invitation only because no moderator activity was detected for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days.`,
+ 'To cancel the invitation only, you need to access the control panel.',
+ '',
+ '---------------',
+ '',
+ 'To モデレーターå„ä½',
+ '',
+ `ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ã®æ´»å‹•ãŒ${MODERATOR_INACTIVITY_LIMIT_DAYS}日間検出ã•れãªã‹ã£ãŸãŸã‚ã€æ‹›å¾…制ã«å¤‰æ›´ã•れã¾ã—ãŸã€‚`,
+ '招待制を解除ã™ã‚‹ã«ã¯ã€ã‚³ãƒ³ãƒˆãƒ­ãƒ¼ãƒ«ãƒ‘ãƒãƒ«ã«ã‚¢ã‚¯ã‚»ã‚¹ã™ã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚',
+ '',
+ ];
+
+ const html = message.join('<br>');
+ const text = message.join('\n');
+
+ return {
+ subject,
+ html,
+ text,
+ };
+}
+
+@Injectable()
+export class CheckModeratorsActivityProcessorService {
+ private logger: Logger;
+
+ constructor(
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+ private metaService: MetaService,
+ private roleService: RoleService,
+ private emailService: EmailService,
+ private announcementService: AnnouncementService,
+ private systemWebhookService: SystemWebhookService,
+ private queueLoggerService: QueueLoggerService,
+ ) {
+ this.logger = this.queueLoggerService.logger.createSubLogger('check-moderators-activity');
+ }
+
+ @bindThis
+ public async process(): Promise<void> {
+ this.logger.info('start.');
+
+ const meta = await this.metaService.fetch(false);
+ if (!meta.disableRegistration) {
+ await this.processImpl();
+ } else {
+ this.logger.info('is already invitation only.');
+ }
+
+ this.logger.succ('finish.');
+ }
+
+ @bindThis
+ private async processImpl() {
+ const evaluateResult = await this.evaluateModeratorsInactiveDays();
+ if (evaluateResult.isModeratorsInactive) {
+ this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`);
+
+ await this.changeToInvitationOnly();
+ await this.notifyChangeToInvitationOnly();
+ } else {
+ const remainingTime = evaluateResult.remainingTime;
+ if (remainingTime.asDays <= MODERATOR_INACTIVITY_WARNING_REMAINING_DAYS) {
+ const timeVariant = remainingTime.asDays === 0 ? `${remainingTime.asHours} hours` : `${remainingTime.asDays} days`;
+ this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${timeVariant}, it will switch to invitation only.`);
+
+ if (remainingTime.asHours % MODERATOR_INACTIVITY_WARNING_NOTIFY_INTERVAL_HOURS === 0) {
+ // ジョブã®å®Ÿè¡Œé »åº¦ã¨åŒç­‰ã ã¨é€šçŸ¥ãŒå¤šã™ãŽã‚‹ãŸã‚期é™ã‹ã‚‰6時間ã”ã¨ã«é€šçŸ¥ã™ã‚‹
+ // ã¤ã¾ã‚Šã€ã®ã“り2日を切ã£ãŸã‚‰6時間ã”ã¨ã«é€šçŸ¥ãŒé€ã‚‰ã‚Œã‚‹
+ await this.notifyInactiveModeratorsWarning(remainingTime);
+ }
+ }
+ }
+ }
+
+ /**
+ * モデレーターãŒä¸åœ¨ã§ã‚ã‚‹ã‹ã©ã†ã‹ã‚’確èªã™ã‚‹ã€‚trueã®å ´åˆã¯ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ãŒä¸åœ¨ã§ã‚る。
+ * isModerator, isAdministrator, isRootã®ã„ãšã‚Œã‹ãŒtrueã®ãƒ¦ãƒ¼ã‚¶ã‚’対象ã«ã€
+ * {@link MiUser.lastActiveDate}ã®å€¤ãŒå®Ÿè¡Œæ—¥æ™‚ã®{@link MODERATOR_INACTIVITY_LIMIT_DAYS}æ—¥å‰ã‚ˆã‚Šã‚‚å¤ã„ユーザãŒã„ã‚‹ã‹ã©ã†ã‹ã‚’確èªã™ã‚‹ã€‚
+ * {@link MiUser.lastActiveDate}ãŒnullã®å ´åˆã¯ã€ãã®ãƒ¦ãƒ¼ã‚¶ã¯ç¢ºèªã®å¯¾è±¡å¤–ã¨ã™ã‚‹ã€‚
+ *
+ * -----
+ *
+ * ### サンプルパターン
+ * - 実行日時: 2022-01-30 12:00:00
+ * - 判定基準: 2022-01-23 12:00:00(実行日時ã®{@link MODERATOR_INACTIVITY_LIMIT_DAYS}æ—¥å‰ï¼‰
+ *
+ * #### パターン①
+ * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト
+ * - モデレータB: lastActiveDate = 2022-01-23 12:00:00 ※セーフ(判定基準ã¨åŒå€¤ãªã®ã§ã‚®ãƒªã‚®ãƒªæ®‹ã‚Š0日)
+ * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日)
+ * - モデレータD: lastActiveDate = null
+ *
+ * ã“ã®å ´åˆã€ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿Bã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティã®ã¿åˆ¤å®šåŸºæº–日よりもå¤ããªã„ãŸã‚ã€ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ãŒåœ¨å¸­ã¨åˆ¤æ–­ã•れる。
+ *
+ * #### パターン②
+ * - モデレータA: lastActiveDate = 2022-01-20 00:00:00 ※アウト
+ * - モデレータB: lastActiveDate = 2022-01-22 12:00:00 ※アウト(残り-1日)
+ * - モデレータC: lastActiveDate = 2022-01-23 11:59:59 ※アウト(残り-1日)
+ * - モデレータD: lastActiveDate = null
+ *
+ * ã“ã®å ´åˆã€ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿A, B, Cã®ã‚¢ã‚¯ãƒ†ã‚£ãƒ“ティã¯åˆ¤å®šåŸºæº–日よりもå¤ã„ãŸã‚ã€ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ãŒä¸åœ¨ã¨åˆ¤æ–­ã•れる。
+ */
+ @bindThis
+ public async evaluateModeratorsInactiveDays(): Promise<ModeratorInactivityEvaluationResult> {
+ const today = new Date();
+ const inactivePeriod = new Date(today);
+ inactivePeriod.setDate(today.getDate() - MODERATOR_INACTIVITY_LIMIT_DAYS);
+
+ const moderators = await this.fetchModerators()
+ .then(it => it.filter(it => it.lastActiveDate != null));
+ const inactiveModerators = moderators
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ .filter(it => it.lastActiveDate!.getTime() < inactivePeriod.getTime());
+
+ // 残りã®çŒ¶äºˆã‚’示ã—ãŸã„ã®ã§ã€æœ€çµ‚アクティブ日時ãŒä¸€ç•ªè‹¥ã„ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ã®æ—¥æ•°ã‚’基準ã«çŒ¶äºˆã‚’計算ã™ã‚‹
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ const newestLastActiveDate = new Date(Math.max(...moderators.map(it => it.lastActiveDate!.getTime())));
+ const remainingTime = newestLastActiveDate.getTime() - inactivePeriod.getTime();
+ const remainingTimeAsDays = Math.floor(remainingTime / ONE_DAY_MILLI_SEC);
+ const remainingTimeAsHours = Math.floor((remainingTime / ONE_HOUR_MILLI_SEC));
+
+ return {
+ isModeratorsInactive: inactiveModerators.length === moderators.length,
+ inactiveModerators,
+ remainingTime: {
+ time: remainingTime,
+ asHours: remainingTimeAsHours,
+ asDays: remainingTimeAsDays,
+ },
+ };
+ }
+
+ @bindThis
+ private async changeToInvitationOnly() {
+ await this.metaService.update({ disableRegistration: true });
+ }
+
+ @bindThis
+ public async notifyInactiveModeratorsWarning(remainingTime: ModeratorInactivityRemainingTime) {
+ // -- モデレータã¸ã®ãƒ¡ãƒ¼ãƒ«é€ä¿¡
+
+ const moderators = await this.fetchModerators();
+ const moderatorProfiles = await this.userProfilesRepository
+ .findBy({ userId: In(moderators.map(it => it.id)) })
+ .then(it => new Map(it.map(it => [it.userId, it])));
+
+ const mail = generateModeratorInactivityMail(remainingTime);
+ for (const moderator of moderators) {
+ const profile = moderatorProfiles.get(moderator.id);
+ if (profile && profile.email && profile.emailVerified) {
+ this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text);
+ }
+ }
+
+ // -- SystemWebhook
+
+ const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
+ .then(it => it.filter(it => it.on.includes('inactiveModeratorsWarning')));
+ for (const systemWebhook of systemWebhooks) {
+ this.systemWebhookService.enqueueSystemWebhook(
+ systemWebhook,
+ 'inactiveModeratorsWarning',
+ { remainingTime: remainingTime },
+ );
+ }
+ }
+
+ @bindThis
+ public async notifyChangeToInvitationOnly() {
+ // -- モデレータã¸ã®ãƒ¡ãƒ¼ãƒ«ã¨ãŠçŸ¥ã‚‰ã›ï¼ˆå€‹äººå‘ã‘)é€ä¿¡
+
+ const moderators = await this.fetchModerators();
+ const moderatorProfiles = await this.userProfilesRepository
+ .findBy({ userId: In(moderators.map(it => it.id)) })
+ .then(it => new Map(it.map(it => [it.userId, it])));
+
+ const mail = generateInvitationOnlyChangedMail();
+ for (const moderator of moderators) {
+ this.announcementService.create({
+ title: mail.subject,
+ text: mail.text,
+ forExistingUsers: true,
+ needConfirmationToRead: true,
+ userId: moderator.id,
+ });
+
+ const profile = moderatorProfiles.get(moderator.id);
+ if (profile && profile.email && profile.emailVerified) {
+ this.emailService.sendEmail(profile.email, mail.subject, mail.html, mail.text);
+ }
+ }
+
+ // -- SystemWebhook
+
+ const systemWebhooks = await this.systemWebhookService.fetchActiveSystemWebhooks()
+ .then(it => it.filter(it => it.on.includes('inactiveModeratorsInvitationOnlyChanged')));
+ for (const systemWebhook of systemWebhooks) {
+ this.systemWebhookService.enqueueSystemWebhook(
+ systemWebhook,
+ 'inactiveModeratorsInvitationOnlyChanged',
+ {},
+ );
+ }
+ }
+
+ @bindThis
+ private async fetchModerators() {
+ // TODO: モデレーター以外ã«ã‚‚ç‰¹åˆ¥ãªæ¨©é™ã‚’æŒã¤ãƒ¦ãƒ¼ã‚¶ãƒ¼ãŒã„ã‚‹å ´åˆã¯è€ƒæ…®ã™ã‚‹
+ return this.roleService.getModerators({
+ includeAdmins: true,
+ includeRoot: true,
+ excludeExpire: true,
+ });
+ }
+}
diff --git a/packages/backend/src/queue/processors/DeliverProcessorService.ts b/packages/backend/src/queue/processors/DeliverProcessorService.ts
index 9590a4fe71..5a16496011 100644
--- a/packages/backend/src/queue/processors/DeliverProcessorService.ts
+++ b/packages/backend/src/queue/processors/DeliverProcessorService.ts
@@ -74,8 +74,17 @@ export class DeliverProcessorService {
try {
await this.apRequestService.signedPost(job.data.user, job.data.to, job.data.content, job.data.digest);
- // Update stats
- this.federatedInstanceService.fetch(host).then(i => {
+ this.apRequestChart.deliverSucc();
+ this.federationChart.deliverd(host, true);
+
+ // Update instance stats
+ process.nextTick(async () => {
+ const i = await (this.meta.enableStatsForFederatedInstances
+ ? this.federatedInstanceService.fetchOrRegister(host)
+ : this.federatedInstanceService.fetch(host));
+
+ if (i == null) return;
+
if (i.isNotResponding) {
this.federatedInstanceService.update(i.id, {
isNotResponding: false,
@@ -83,9 +92,9 @@ export class DeliverProcessorService {
});
}
- this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
- this.apRequestChart.deliverSucc();
- this.federationChart.deliverd(i.host, true);
+ if (this.meta.enableStatsForFederatedInstances) {
+ this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
+ }
if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, true);
@@ -94,8 +103,11 @@ export class DeliverProcessorService {
return 'Success';
} catch (res) {
- // Update stats
- this.federatedInstanceService.fetch(host).then(i => {
+ this.apRequestChart.deliverFail();
+ this.federationChart.deliverd(host, false);
+
+ // Update instance stats
+ this.federatedInstanceService.fetchOrRegister(host).then(i => {
if (!i.isNotResponding) {
this.federatedInstanceService.update(i.id, {
isNotResponding: true,
@@ -116,9 +128,6 @@ export class DeliverProcessorService {
});
}
- this.apRequestChart.deliverFail();
- this.federationChart.deliverd(i.host, false);
-
if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestSent(i.host, false);
}
@@ -129,7 +138,7 @@ export class DeliverProcessorService {
if (!res.isRetryable) {
// 相手ãŒé–‰éŽ–ã—ã¦ã„ã‚‹ã“ã¨ã‚’明示ã—ã¦ã„ã‚‹ãŸã‚ã€é…é€åœæ­¢ã™ã‚‹
if (job.data.isSharedInbox && res.statusCode === 410) {
- this.federatedInstanceService.fetch(host).then(i => {
+ this.federatedInstanceService.fetchOrRegister(host).then(i => {
this.federatedInstanceService.update(i.id, {
suspensionState: 'goneSuspended',
});
diff --git a/packages/backend/src/queue/processors/InboxProcessorService.ts b/packages/backend/src/queue/processors/InboxProcessorService.ts
index 11b00bb683..7a321e60f3 100644
--- a/packages/backend/src/queue/processors/InboxProcessorService.ts
+++ b/packages/backend/src/queue/processors/InboxProcessorService.ts
@@ -59,7 +59,7 @@ export class InboxProcessorService implements OnApplicationShutdown {
private queueLoggerService: QueueLoggerService,
) {
this.logger = this.queueLoggerService.logger.createSubLogger('inbox');
- this.updateInstanceQueue = new CollapsedQueue(60 * 1000 * 5, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
+ this.updateInstanceQueue = new CollapsedQueue(process.env.NODE_ENV !== 'test' ? 60 * 1000 * 5 : 0, this.collapseUpdateInstanceJobs, this.performUpdateInstance);
}
@bindThis
@@ -194,21 +194,27 @@ export class InboxProcessorService implements OnApplicationShutdown {
}
}
- // Update stats
- this.federatedInstanceService.fetch(authUser.user.host).then(i => {
+ this.apRequestChart.inbox();
+ this.federationChart.inbox(authUser.user.host);
+
+ // Update instance stats
+ process.nextTick(async () => {
+ const i = await (this.meta.enableStatsForFederatedInstances
+ ? this.federatedInstanceService.fetchOrRegister(authUser.user.host)
+ : this.federatedInstanceService.fetch(authUser.user.host));
+
+ if (i == null) return;
+
this.updateInstanceQueue.enqueue(i.id, {
latestRequestReceivedAt: new Date(),
shouldUnsuspend: i.suspensionState === 'autoSuspendedForNotResponding',
});
- this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
-
- this.apRequestChart.inbox();
- this.federationChart.inbox(i.host);
-
if (this.meta.enableChartsForFederatedInstances) {
this.instanceChart.requestReceived(i.host);
}
+
+ this.fetchInstanceMetadataService.fetchInstanceMetadata(i);
});
// アクティビティを処ç†
diff --git a/packages/backend/src/server/api/ApiServerService.ts b/packages/backend/src/server/api/ApiServerService.ts
index ac3b982742..0d77309537 100644
--- a/packages/backend/src/server/api/ApiServerService.ts
+++ b/packages/backend/src/server/api/ApiServerService.ts
@@ -119,25 +119,30 @@ export class ApiServerService {
'g-recaptcha-response'?: string;
'turnstile-response'?: string;
'frc-captcha-solution'?: string;
+ 'm-captcha-response'?: string;
+ 'testcaptcha-response'?: string;
}
}>('/signup', (request, reply) => this.signupApiService.signup(request, reply));
fastify.post<{
Body: {
username: string;
- password: string;
+ password?: string;
token?: string;
- signature?: string;
- authenticatorData?: string;
- clientDataJSON?: string;
- credentialId?: string;
- challengeId?: string;
+ credential?: AuthenticationResponseJSON;
+ 'hcaptcha-response'?: string;
+ 'g-recaptcha-response'?: string;
+ 'turnstile-response'?: string;
+ 'frc-captcha-solution'?: string;
+ 'm-captcha-response'?: string;
+ 'testcaptcha-response'?: string;
};
- }>('/signin', (request, reply) => this.signinApiService.signin(request, reply));
+ }>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply));
fastify.post<{
Body: {
credential?: AuthenticationResponseJSON;
+ context?: string;
};
}>('/signin-with-passkey', (request, reply) => this.signinWithPasskeyApiService.signin(request, reply));
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 5bdd7cf650..9a387e3f11 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -68,6 +68,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -470,6 +472,8 @@ const $admin_relays_list: Provider = { provide: 'ep:admin/relays/list', useClass
const $admin_relays_remove: Provider = { provide: 'ep:admin/relays/remove', useClass: ep___admin_relays_remove.default };
const $admin_resetPassword: Provider = { provide: 'ep:admin/reset-password', useClass: ep___admin_resetPassword.default };
const $admin_resolveAbuseUserReport: Provider = { provide: 'ep:admin/resolve-abuse-user-report', useClass: ep___admin_resolveAbuseUserReport.default };
+const $admin_forwardAbuseUserReport: Provider = { provide: 'ep:admin/forward-abuse-user-report', useClass: ep___admin_forwardAbuseUserReport.default };
+const $admin_updateAbuseUserReport: Provider = { provide: 'ep:admin/update-abuse-user-report', useClass: ep___admin_updateAbuseUserReport.default };
const $admin_sendEmail: Provider = { provide: 'ep:admin/send-email', useClass: ep___admin_sendEmail.default };
const $admin_serverInfo: Provider = { provide: 'ep:admin/server-info', useClass: ep___admin_serverInfo.default };
const $admin_showModerationLogs: Provider = { provide: 'ep:admin/show-moderation-logs', useClass: ep___admin_showModerationLogs.default };
@@ -876,6 +880,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove,
$admin_resetPassword,
$admin_resolveAbuseUserReport,
+ $admin_forwardAbuseUserReport,
+ $admin_updateAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
$admin_showModerationLogs,
@@ -1276,6 +1282,8 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_relays_remove,
$admin_resetPassword,
$admin_resolveAbuseUserReport,
+ $admin_forwardAbuseUserReport,
+ $admin_updateAbuseUserReport,
$admin_sendEmail,
$admin_serverInfo,
$admin_showModerationLogs,
diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts
index 64af7da7a6..1e7a2e80ef 100644
--- a/packages/backend/src/server/api/SigninApiService.ts
+++ b/packages/backend/src/server/api/SigninApiService.ts
@@ -6,12 +6,14 @@
import { Inject, Injectable } from '@nestjs/common';
import bcrypt from 'bcryptjs';
import * as argon2 from 'argon2';
-import * as OTPAuth from 'otpauth';
import { IsNull } from 'typeorm';
+import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js';
import type {
+ MiMeta,
SigninsRepository,
UserProfilesRepository,
+ UserSecurityKeysRepository,
UsersRepository,
} from '@/models/_.js';
import type { Config } from '@/config.js';
@@ -21,6 +23,8 @@ import { IdService } from '@/core/IdService.js';
import { bindThis } from '@/decorators.js';
import { WebAuthnService } from '@/core/WebAuthnService.js';
import { UserAuthService } from '@/core/UserAuthService.js';
+import { CaptchaService } from '@/core/CaptchaService.js';
+import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { RateLimiterService } from './RateLimiterService.js';
import { SigninService } from './SigninService.js';
import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
@@ -43,6 +47,9 @@ export class SigninApiService {
@Inject(DI.userProfilesRepository)
private userProfilesRepository: UserProfilesRepository,
+ @Inject(DI.userSecurityKeysRepository)
+ private userSecurityKeysRepository: UserSecurityKeysRepository,
+
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
@@ -51,6 +58,7 @@ export class SigninApiService {
private signinService: SigninService,
private userAuthService: UserAuthService,
private webAuthnService: WebAuthnService,
+ private captchaService: CaptchaService,
) {
}
@@ -59,9 +67,15 @@ export class SigninApiService {
request: FastifyRequest<{
Body: {
username: string;
- password: string;
+ password?: string;
token?: string;
credential?: AuthenticationResponseJSON;
+ 'hcaptcha-response'?: string;
+ 'g-recaptcha-response'?: string;
+ 'turnstile-response'?: string;
+ 'frc-captcha-solution'?: string;
+ 'm-captcha-response'?: string;
+ 'testcaptcha-response'?: string;
};
}>,
reply: FastifyReply,
@@ -98,11 +112,6 @@ export class SigninApiService {
return;
}
- if (typeof password !== 'string') {
- reply.code(400);
- return;
- }
-
if (token != null && typeof token !== 'string') {
reply.code(400);
return;
@@ -133,6 +142,27 @@ export class SigninApiService {
}
const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+ const securityKeysAvailable = await this.userSecurityKeysRepository.countBy({ userId: user.id }).then(result => result >= 1);
+
+ if (password == null) {
+ reply.code(200);
+ if (profile.twoFactorEnabled) {
+ return {
+ finished: false,
+ next: 'password',
+ } satisfies Misskey.entities.SigninFlowResponse;
+ } else {
+ return {
+ finished: false,
+ next: 'captcha',
+ } satisfies Misskey.entities.SigninFlowResponse;
+ }
+ }
+
+ if (typeof password !== 'string') {
+ reply.code(400);
+ return;
+ }
if (!user.approved && this.meta.approvalRequiredForSignup) {
reply.code(403);
@@ -148,7 +178,7 @@ export class SigninApiService {
// Compare password
const same = await argon2.verify(profile.password!, password) || bcrypt.compareSync(password, profile.password!);
- const fail = async (status?: number, failure?: { id: string }) => {
+ const fail = async (status?: number, failure?: { id: string; }) => {
// Append signin history
await this.signinsRepository.insert({
id: this.idService.gen(),
@@ -162,6 +192,44 @@ export class SigninApiService {
};
if (!profile.twoFactorEnabled) {
+ if (process.env.NODE_ENV !== 'test') {
+ if (this.meta.enableHcaptcha && this.meta.hcaptchaSecretKey) {
+ await this.captchaService.verifyHcaptcha(this.meta.hcaptchaSecretKey, body['hcaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableMcaptcha && this.meta.mcaptchaSecretKey && this.meta.mcaptchaSitekey && this.meta.mcaptchaInstanceUrl) {
+ await this.captchaService.verifyMcaptcha(this.meta.mcaptchaSecretKey, this.meta.mcaptchaSitekey, this.meta.mcaptchaInstanceUrl, body['m-captcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableFC && this.meta.fcSecretKey) {
+ await this.captchaService.verifyFriendlyCaptcha(this.meta.fcSecretKey, body['frc-captcha-solution']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableRecaptcha && this.meta.recaptchaSecretKey) {
+ await this.captchaService.verifyRecaptcha(this.meta.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableTurnstile && this.meta.turnstileSecretKey) {
+ await this.captchaService.verifyTurnstile(this.meta.turnstileSecretKey, body['turnstile-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+
+ if (this.meta.enableTestcaptcha) {
+ await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
+ }
+
if (same) {
if (profile.password!.startsWith('$2')) {
const newHash = await argon2.hash(password);
@@ -220,7 +288,7 @@ export class SigninApiService {
id: '93b86c4b-72f9-40eb-9815-798928603d1e',
});
}
- } else {
+ } else if (securityKeysAvailable) {
if (!same && !profile.usePasswordLessLogin) {
return await fail(403, {
id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
@@ -230,7 +298,23 @@ export class SigninApiService {
const authRequest = await this.webAuthnService.initiateAuthentication(user.id);
reply.code(200);
- return authRequest;
+ return {
+ finished: false,
+ next: 'passkey',
+ authRequest,
+ } satisfies Misskey.entities.SigninFlowResponse;
+ } else {
+ if (!same || !profile.twoFactorEnabled) {
+ return await fail(403, {
+ id: '932c904e-9460-45b7-9ce6-7ed33be7eb2c',
+ });
+ } else {
+ reply.code(200);
+ return {
+ finished: false,
+ next: 'totp',
+ } satisfies Misskey.entities.SigninFlowResponse;
+ }
}
// never get here
}
diff --git a/packages/backend/src/server/api/SigninService.ts b/packages/backend/src/server/api/SigninService.ts
index 70306c3113..640356b50c 100644
--- a/packages/backend/src/server/api/SigninService.ts
+++ b/packages/backend/src/server/api/SigninService.ts
@@ -4,13 +4,16 @@
*/
import { Inject, Injectable } from '@nestjs/common';
+import * as Misskey from 'misskey-js';
import { DI } from '@/di-symbols.js';
-import type { SigninsRepository } from '@/models/_.js';
+import type { SigninsRepository, UserProfilesRepository } from '@/models/_.js';
import { IdService } from '@/core/IdService.js';
import type { MiLocalUser } from '@/models/User.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { SigninEntityService } from '@/core/entities/SigninEntityService.js';
import { bindThis } from '@/decorators.js';
+import { EmailService } from '@/core/EmailService.js';
+import { NotificationService } from '@/core/NotificationService.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
@Injectable()
@@ -19,7 +22,12 @@ export class SigninService {
@Inject(DI.signinsRepository)
private signinsRepository: SigninsRepository,
+ @Inject(DI.userProfilesRepository)
+ private userProfilesRepository: UserProfilesRepository,
+
private signinEntityService: SigninEntityService,
+ private emailService: EmailService,
+ private notificationService: NotificationService,
private idService: IdService,
private globalEventService: GlobalEventService,
) {
@@ -28,7 +36,8 @@ export class SigninService {
@bindThis
public signin(request: FastifyRequest, reply: FastifyReply, user: MiLocalUser) {
setImmediate(async () => {
- // Append signin history
+ this.notificationService.createNotification(user.id, 'login', {});
+
const record = await this.signinsRepository.insertOne({
id: this.idService.gen(),
userId: user.id,
@@ -37,15 +46,22 @@ export class SigninService {
success: true,
});
- // Publish signin event
this.globalEventService.publishMainStream(user.id, 'signin', await this.signinEntityService.pack(record));
+
+ const profile = await this.userProfilesRepository.findOneByOrFail({ userId: user.id });
+ if (profile.email && profile.emailVerified) {
+ this.emailService.sendEmail(profile.email, 'New login / ログインãŒã‚りã¾ã—ãŸ',
+ 'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / æ–°ã—ã„ログインãŒã‚りã¾ã—ãŸã€‚ã“ã®ãƒ­ã‚°ã‚¤ãƒ³ã«å¿ƒå½“ãŸã‚ŠãŒãªã„å ´åˆã¯ã€ãƒ‘スワードを変更ã™ã‚‹ãªã©ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£çŠ¶æ…‹ã‚’æ›´æ–°ã—ã¦ãã ã•ã„。',
+ 'There is a new login. If you do not recognize this login, update the security status of your account, including changing your password. / æ–°ã—ã„ログインãŒã‚りã¾ã—ãŸã€‚ã“ã®ãƒ­ã‚°ã‚¤ãƒ³ã«å¿ƒå½“ãŸã‚ŠãŒãªã„å ´åˆã¯ã€ãƒ‘スワードを変更ã™ã‚‹ãªã©ã€ã‚¢ã‚«ã‚¦ãƒ³ãƒˆã®ã‚»ã‚­ãƒ¥ãƒªãƒ†ã‚£çŠ¶æ…‹ã‚’æ›´æ–°ã—ã¦ãã ã•ã„。');
+ }
});
reply.code(200);
return {
+ finished: true,
id: user.id,
- i: user.token,
- };
+ i: user.token!,
+ } satisfies Misskey.entities.SigninFlowResponse;
}
}
diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts
index db860d710a..7aea6a0e56 100644
--- a/packages/backend/src/server/api/SignupApiService.ts
+++ b/packages/backend/src/server/api/SignupApiService.ts
@@ -73,6 +73,7 @@ export class SignupApiService {
'turnstile-response'?: string;
'm-captcha-response'?: string;
'frc-captcha-solution'?: string;
+ 'testcaptcha-response'?: string;
}
}>,
reply: FastifyReply,
@@ -111,6 +112,12 @@ export class SignupApiService {
throw new FastifyReplyError(400, err);
});
}
+
+ if (this.meta.enableTestcaptcha) {
+ await this.captchaService.verifyTestcaptcha(body['testcaptcha-response']).catch(err => {
+ throw new FastifyReplyError(400, err);
+ });
+ }
}
const username = body['username'];
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 14e002929a..3dc287331c 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -74,6 +74,8 @@ import * as ep___admin_relays_list from './endpoints/admin/relays/list.js';
import * as ep___admin_relays_remove from './endpoints/admin/relays/remove.js';
import * as ep___admin_resetPassword from './endpoints/admin/reset-password.js';
import * as ep___admin_resolveAbuseUserReport from './endpoints/admin/resolve-abuse-user-report.js';
+import * as ep___admin_forwardAbuseUserReport from './endpoints/admin/forward-abuse-user-report.js';
+import * as ep___admin_updateAbuseUserReport from './endpoints/admin/update-abuse-user-report.js';
import * as ep___admin_sendEmail from './endpoints/admin/send-email.js';
import * as ep___admin_serverInfo from './endpoints/admin/server-info.js';
import * as ep___admin_showModerationLogs from './endpoints/admin/show-moderation-logs.js';
@@ -474,6 +476,8 @@ const eps = [
['admin/relays/remove', ep___admin_relays_remove],
['admin/reset-password', ep___admin_resetPassword],
['admin/resolve-abuse-user-report', ep___admin_resolveAbuseUserReport],
+ ['admin/forward-abuse-user-report', ep___admin_forwardAbuseUserReport],
+ ['admin/update-abuse-user-report', ep___admin_updateAbuseUserReport],
['admin/send-email', ep___admin_sendEmail],
['admin/server-info', ep___admin_serverInfo],
['admin/show-moderation-logs', ep___admin_showModerationLogs],
diff --git a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
index cf3f257ca6..0dbfaae054 100644
--- a/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
+++ b/packages/backend/src/server/api/endpoints/admin/abuse-user-reports.ts
@@ -71,9 +71,22 @@ export const meta = {
},
assignee: {
type: 'object',
- nullable: true, optional: true,
+ nullable: true, optional: false,
ref: 'UserDetailedNotMe',
},
+ forwarded: {
+ type: 'boolean',
+ nullable: false, optional: false,
+ },
+ resolvedAs: {
+ type: 'string',
+ nullable: true, optional: false,
+ enum: ['accept', 'reject', null],
+ },
+ moderationNote: {
+ type: 'string',
+ nullable: false, optional: false,
+ },
},
},
},
@@ -88,7 +101,6 @@ export const paramDef = {
state: { type: 'string', nullable: true, default: null },
reporterOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
targetUserOrigin: { type: 'string', enum: ['combined', 'local', 'remote'], default: 'combined' },
- forwarded: { type: 'boolean', default: false },
},
required: [],
} as const;
diff --git a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
index 7754899b95..d5d2e909a2 100644
--- a/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
+++ b/packages/backend/src/server/api/endpoints/admin/accounts/create.ts
@@ -10,6 +10,9 @@ import { SignupService } from '@/core/SignupService.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { InstanceActorService } from '@/core/InstanceActorService.js';
import { localUsernameSchema, passwordSchema } from '@/models/User.js';
+import { DI } from '@/di-symbols.js';
+import type { Config } from '@/config.js';
+import { ApiError } from '@/server/api/error.js';
import { Packed } from '@/misc/json-schema.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '@/server/api/error.js';
@@ -17,19 +20,19 @@ import { ApiError } from '@/server/api/error.js';
export const meta = {
tags: ['admin'],
- res: {
- type: 'object',
- optional: false, nullable: false,
- ref: 'MeDetailed',
- properties: {
- token: {
- type: 'string',
- optional: false, nullable: false,
- },
+ errors: {
+ accessDenied: {
+ message: 'Access denied.',
+ code: 'ACCESS_DENIED',
+ id: '1fb7cb09-d46a-4fff-b8df-057708cce513',
+ },
+
+ wrongInitialPassword: {
+ message: 'Initial password is incorrect.',
+ code: 'INCORRECT_INITIAL_PASSWORD',
+ id: '97147c55-1ae1-4f6f-91d6-e1c3e0e76d62',
},
- },
- errors: {
// From ApiCallService.ts
noCredential: {
message: 'Credential required.',
@@ -51,6 +54,18 @@ export const meta = {
},
},
+ res: {
+ type: 'object',
+ optional: false, nullable: false,
+ ref: 'MeDetailed',
+ properties: {
+ token: {
+ type: 'string',
+ optional: false, nullable: false,
+ },
+ },
+ },
+
// Required token permissions, but we need to check them manually.
// ApiCallService checks access in a way that would prevent creating the first account.
softPermissions: [
@@ -64,6 +79,7 @@ export const paramDef = {
properties: {
username: localUsernameSchema,
password: passwordSchema,
+ setupPassword: { type: 'string', nullable: true },
},
required: ['username', 'password'],
} as const;
@@ -71,13 +87,49 @@ export const paramDef = {
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.usersRepository)
+ private usersRepository: UsersRepository,
+
private roleService: RoleService,
private userEntityService: UserEntityService,
private signupService: SignupService,
private instanceActorService: InstanceActorService,
) {
super(meta, paramDef, async (ps, _me, token) => {
- await this.ensurePermissions(_me, token);
+ const me = _me ? await this.usersRepository.findOneByOrFail({ id: _me.id }) : null;
+ const realUsers = await this.instanceActorService.realLocalUsersPresent();
+
+ if (!realUsers && me == null && token == null) {
+ // åˆå›žã‚»ãƒƒãƒˆã‚¢ãƒƒãƒ—ã®å ´åˆ
+ if (this.config.setupPassword != null) {
+ // åˆæœŸãƒ‘スワードãŒè¨­å®šã•れã¦ã„ã‚‹å ´åˆ
+ if (ps.setupPassword !== this.config.setupPassword) {
+ // åˆæœŸãƒ‘スワードãŒé•ã†å ´åˆ
+ throw new ApiError(meta.errors.wrongInitialPassword);
+ }
+ } else if (ps.setupPassword != null && ps.setupPassword.trim() !== '') {
+ // åˆæœŸãƒ‘スワードãŒè¨­å®šã•れã¦ã„ãªã„ã®ã«åˆæœŸãƒ‘スワードãŒå…¥åŠ›ã•れãŸå ´åˆ
+ throw new ApiError(meta.errors.wrongInitialPassword);
+ }
+ } else {
+ if (token && !meta.softPermissions.every(p => token.permission.includes(p))) {
+ // Tokens have scoped permissions which may be *less* than the user's official role, so we need to check.
+ throw new ApiError(meta.errors.noPermission);
+ }
+
+ if (me && !await this.roleService.isAdministrator(me)) {
+ // Only administrators (including root) can create users.
+ throw new ApiError(meta.errors.noAdmin);
+ }
+
+ // Anonymous access is only allowed for initial instance setup (this check may be redundant)
+ if (!me && realUsers) {
+ throw new ApiError(meta.errors.noCredential);
+ }
+ }
const { account, secret } = await this.signupService.signup({
username: ps.username,
@@ -96,21 +148,4 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
return res;
});
}
-
- private async ensurePermissions(me: MiUser | null, token: MiAccessToken | null): Promise<void> {
- // Tokens have scoped permissions which may be *less* than the user's official role, so we need to check.
- if (token && !meta.softPermissions.every(p => token.permission.includes(p))) {
- throw new ApiError(meta.errors.noPermission);
- }
-
- // Only administrators (including root) can create users.
- if (me && !await this.roleService.isAdministrator(me)) {
- throw new ApiError(meta.errors.noAdmin);
- }
-
- // Anonymous access is only allowed for initial instance setup.
- if (!me && await this.instanceActorService.realLocalUsersPresent()) {
- throw new ApiError(meta.errors.noCredential);
- }
- }
}
diff --git a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
index 3caa0f84a3..071ddbef18 100644
--- a/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
+++ b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts
@@ -6,7 +6,7 @@
import { Inject, Injectable } from '@nestjs/common';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
-import type { DriveFilesRepository } from '@/models/_.js';
+import type { DriveFilesRepository, MiEmoji } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
@@ -79,25 +79,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (driveFile == null) throw new ApiError(meta.errors.noSuchFile);
}
- let emojiId;
- if (ps.id) {
- emojiId = ps.id;
- const emoji = await this.customEmojiService.getEmojiById(ps.id);
- if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
- if (nameNfc && (nameNfc !== emoji.name)) {
- const isDuplicate = await this.customEmojiService.checkDuplicate(nameNfc);
- if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists);
- }
- } else {
- if (!nameNfc) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.');
- const emoji = await this.customEmojiService.getEmojiByName(nameNfc);
- if (!emoji) throw new ApiError(meta.errors.noSuchEmoji);
- emojiId = emoji.id;
- }
+ // JSON schemeã®anyOfã®åž‹å¤‰æ›ãŒã†ã¾ãã„ã£ã¦ã„ãªã„らã—ã„
+ const required = { id: ps.id, name: nameNfc } as
+ | { id: MiEmoji['id']; name?: string }
+ | { id?: MiEmoji['id']; name: string };
- await this.customEmojiService.update(emojiId, {
+ const error = await this.customEmojiService.update({
+ ...required,
driveFile,
- name: nameNfc,
category: ps.category?.normalize('NFC'),
aliases: ps.aliases?.map(a => a.normalize('NFC')),
license: ps.license,
@@ -105,6 +94,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
localOnly: ps.localOnly,
roleIdsThatCanBeUsedThisEmojiAsReaction: ps.roleIdsThatCanBeUsedThisEmojiAsReaction,
}, me);
+
+ switch (error) {
+ case null: return;
+ case 'NO_SUCH_EMOJI': throw new ApiError(meta.errors.noSuchEmoji);
+ case 'SAME_NAME_EMOJI_EXISTS': throw new ApiError(meta.errors.sameNameEmojiExists);
+ }
+ // 網羅性ãƒã‚§ãƒƒã‚¯
+ const mustBeNever: never = error;
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
new file mode 100644
index 0000000000..3e42c91fed
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/forward-abuse-user-report.ts
@@ -0,0 +1,55 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: '8763e21b-d9bc-40be-acf6-54c1a6986493',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ reportId: { type: 'string', format: 'misskey:id' },
+ },
+ required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ private abuseReportService: AbuseReportService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
+ }
+
+ await this.abuseReportService.forward(report.id, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index 6e368eff43..6495e3b7da 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -81,6 +81,10 @@ export const meta = {
type: 'string',
optional: false, nullable: true,
},
+ enableTestcaptcha: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
swPublickey: {
type: 'string',
optional: false, nullable: true,
@@ -189,6 +193,13 @@ export const meta = {
type: 'string',
},
},
+ prohibitedWordsForNameOfUser: {
+ type: 'array',
+ optional: false, nullable: false,
+ items: {
+ type: 'string',
+ },
+ },
bannedEmailDomains: {
type: 'array',
optional: true, nullable: false,
@@ -368,6 +379,10 @@ export const meta = {
type: 'boolean',
optional: false, nullable: false,
},
+ enableStatsForFederatedInstances: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
enableServerMachineStats: {
type: 'boolean',
optional: false, nullable: false,
@@ -614,6 +629,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
turnstileSiteKey: instance.turnstileSiteKey,
enableFC: instance.enableFC,
fcSiteKey: instance.fcSiteKey,
+ enableTestcaptcha: instance.enableTestcaptcha,
swPublickey: instance.swPublicKey,
themeColor: instance.themeColor,
mascotImageUrl: instance.mascotImageUrl,
@@ -642,6 +658,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
mediaSilencedHosts: instance.mediaSilencedHosts,
sensitiveWords: instance.sensitiveWords,
prohibitedWords: instance.prohibitedWords,
+ prohibitedWordsForNameOfUser: instance.prohibitedWordsForNameOfUser,
preservedUsernames: instance.preservedUsernames,
bubbleInstances: instance.bubbleInstances,
hcaptchaSecretKey: instance.hcaptchaSecretKey,
@@ -688,6 +705,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
truemailAuthKey: instance.truemailAuthKey,
enableChartsForRemoteUser: instance.enableChartsForRemoteUser,
enableChartsForFederatedInstances: instance.enableChartsForFederatedInstances,
+ enableStatsForFederatedInstances: instance.enableStatsForFederatedInstances,
enableServerMachineStats: instance.enableServerMachineStats,
enableAchievements: instance.enableAchievements,
enableIdenticonGeneration: instance.enableIdenticonGeneration,
diff --git a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
index 9b79100fcf..554d324ff2 100644
--- a/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
+++ b/packages/backend/src/server/api/endpoints/admin/resolve-abuse-user-report.ts
@@ -32,7 +32,7 @@ export const paramDef = {
type: 'object',
properties: {
reportId: { type: 'string', format: 'misskey:id' },
- forward: { type: 'boolean', default: false },
+ resolvedAs: { type: 'string', enum: ['accept', 'reject', null], nullable: true },
},
required: ['reportId'],
} as const;
@@ -50,7 +50,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
throw new ApiError(meta.errors.noSuchAbuseReport);
}
- await this.abuseReportService.resolve([{ reportId: report.id, forward: ps.forward }], me);
+ await this.abuseReportService.resolve([{ reportId: report.id, resolvedAs: ps.resolvedAs ?? null }], me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/admin/show-users.ts b/packages/backend/src/server/api/endpoints/admin/show-users.ts
index 5f16519403..cc65ed2cf0 100644
--- a/packages/backend/src/server/api/endpoints/admin/show-users.ts
+++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts
@@ -72,13 +72,13 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
break;
}
case 'moderator': {
- const moderatorIds = await this.roleService.getModeratorIds(false);
+ const moderatorIds = await this.roleService.getModeratorIds({ includeAdmins: false });
if (moderatorIds.length === 0) return [];
query.where('user.id IN (:...moderatorIds)', { moderatorIds: moderatorIds });
break;
}
case 'adminOrModerator': {
- const adminOrModeratorIds = await this.roleService.getModeratorIds();
+ const adminOrModeratorIds = await this.roleService.getModeratorIds({ includeAdmins: true });
if (adminOrModeratorIds.length === 0) return [];
query.where('user.id IN (:...adminOrModeratorIds)', { adminOrModeratorIds: adminOrModeratorIds });
break;
diff --git a/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
new file mode 100644
index 0000000000..73d4b843f0
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/update-abuse-user-report.ts
@@ -0,0 +1,58 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import type { AbuseUserReportsRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { ApiError } from '@/server/api/error.js';
+import { AbuseReportService } from '@/core/AbuseReportService.js';
+
+export const meta = {
+ tags: ['admin'],
+
+ requireCredential: true,
+ requireModerator: true,
+ kind: 'write:admin:resolve-abuse-user-report',
+
+ errors: {
+ noSuchAbuseReport: {
+ message: 'No such abuse report.',
+ code: 'NO_SUCH_ABUSE_REPORT',
+ id: '15f51cf5-46d1-4b1d-a618-b35bcbed0662',
+ kind: 'server',
+ httpStatusCode: 404,
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ reportId: { type: 'string', format: 'misskey:id' },
+ moderationNote: { type: 'string' },
+ },
+ required: ['reportId'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
+ constructor(
+ @Inject(DI.abuseUserReportsRepository)
+ private abuseUserReportsRepository: AbuseUserReportsRepository,
+ private abuseReportService: AbuseReportService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ const report = await this.abuseUserReportsRepository.findOneBy({ id: ps.reportId });
+ if (!report) {
+ throw new ApiError(meta.errors.noSuchAbuseReport);
+ }
+
+ await this.abuseReportService.update(report.id, {
+ moderationNote: ps.moderationNote,
+ }, me);
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 98760bbcc3..72f428d85f 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -46,6 +46,11 @@ export const paramDef = {
type: 'string',
},
},
+ prohibitedWordsForNameOfUser: {
+ type: 'array', nullable: true, items: {
+ type: 'string',
+ },
+ },
themeColor: { type: 'string', nullable: true, pattern: '^#[0-9a-fA-F]{6}$' },
mascotImageUrl: { type: 'string', nullable: true },
bannerUrl: { type: 'string', nullable: true },
@@ -84,6 +89,7 @@ export const paramDef = {
enableFC: { type: 'boolean' },
fcSiteKey: { type: 'string', nullable: true },
fcSecretKey: { type: 'string', nullable: true },
+ enableTestcaptcha: { type: 'boolean' },
sensitiveMediaDetection: { type: 'string', enum: ['none', 'all', 'local', 'remote'] },
sensitiveMediaDetectionSensitivity: { type: 'string', enum: ['medium', 'low', 'high', 'veryLow', 'veryHigh'] },
setSensitiveFlagAutomatically: { type: 'boolean' },
@@ -140,6 +146,7 @@ export const paramDef = {
truemailAuthKey: { type: 'string', nullable: true },
enableChartsForRemoteUser: { type: 'boolean' },
enableChartsForFederatedInstances: { type: 'boolean' },
+ enableStatsForFederatedInstances: { type: 'boolean' },
enableServerMachineStats: { type: 'boolean' },
enableAchievements: { type: 'boolean' },
enableIdenticonGeneration: { type: 'boolean' },
@@ -230,6 +237,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
if (Array.isArray(ps.prohibitedWords)) {
set.prohibitedWords = ps.prohibitedWords.filter(Boolean);
}
+ if (Array.isArray(ps.prohibitedWordsForNameOfUser)) {
+ set.prohibitedWordsForNameOfUser = ps.prohibitedWordsForNameOfUser.filter(Boolean);
+ }
if (Array.isArray(ps.silencedHosts)) {
let lastValue = '';
set.silencedHosts = ps.silencedHosts.sort().filter((h) => {
@@ -390,6 +400,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableFC = ps.enableFC;
}
+ if (ps.enableTestcaptcha !== undefined) {
+ set.enableTestcaptcha = ps.enableTestcaptcha;
+ }
+
if (ps.fcSiteKey !== undefined) {
set.fcSiteKey = ps.fcSiteKey;
}
@@ -610,6 +624,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
}
+ if (ps.enableStatsForFederatedInstances !== undefined) {
+ set.enableStatsForFederatedInstances = ps.enableStatsForFederatedInstances;
+ }
+
if (ps.enableServerMachineStats !== undefined) {
set.enableServerMachineStats = ps.enableServerMachineStats;
}
@@ -709,7 +727,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
}
if (Array.isArray(ps.federationHosts)) {
- set.blockedHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
+ set.federationHosts = ps.federationHosts.filter(Boolean).map(x => x.toLowerCase());
}
const before = await this.metaService.fetch(true);
diff --git a/packages/backend/src/server/api/endpoints/flash/featured.ts b/packages/backend/src/server/api/endpoints/flash/featured.ts
index c2d6ab5085..9a0cb461f2 100644
--- a/packages/backend/src/server/api/endpoints/flash/featured.ts
+++ b/packages/backend/src/server/api/endpoints/flash/featured.ts
@@ -8,6 +8,7 @@ import type { FlashsRepository } from '@/models/_.js';
import { Endpoint } from '@/server/api/endpoint-base.js';
import { FlashEntityService } from '@/core/entities/FlashEntityService.js';
import { DI } from '@/di-symbols.js';
+import { FlashService } from '@/core/FlashService.js';
export const meta = {
tags: ['flash'],
@@ -27,26 +28,25 @@ export const meta = {
export const paramDef = {
type: 'object',
- properties: {},
+ properties: {
+ offset: { type: 'integer', minimum: 0, default: 0 },
+ limit: { type: 'integer', minimum: 1, maximum: 100, default: 10 },
+ },
required: [],
} as const;
@Injectable()
export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-disable-line import/no-default-export
constructor(
- @Inject(DI.flashsRepository)
- private flashsRepository: FlashsRepository,
-
+ private flashService: FlashService,
private flashEntityService: FlashEntityService,
) {
super(meta, paramDef, async (ps, me) => {
- const query = this.flashsRepository.createQueryBuilder('flash')
- .andWhere('flash.likedCount > 0')
- .orderBy('flash.likedCount', 'DESC');
-
- const flashs = await query.limit(10).getMany();
-
- return await this.flashEntityService.packMany(flashs, me);
+ const result = await this.flashService.featured({
+ offset: ps.offset,
+ limit: ps.limit,
+ });
+ return await this.flashEntityService.packMany(result, me);
});
}
}
diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts
index 8994c3fff6..a504441df3 100644
--- a/packages/backend/src/server/api/endpoints/i/update.ts
+++ b/packages/backend/src/server/api/endpoints/i/update.ts
@@ -11,7 +11,7 @@ import { JSDOM } from 'jsdom';
import { extractCustomEmojisFromMfm } from '@/misc/extract-custom-emojis-from-mfm.js';
import { extractHashtags } from '@/misc/extract-hashtags.js';
import * as Acct from '@/misc/acct.js';
-import type { UsersRepository, DriveFilesRepository, UserProfilesRepository, PagesRepository } from '@/models/_.js';
+import type { UsersRepository, DriveFilesRepository, MiMeta, UserProfilesRepository, PagesRepository } from '@/models/_.js';
import type { MiLocalUser, MiUser } from '@/models/User.js';
import { birthdaySchema, listenbrainzSchema, descriptionSchema, followedMessageSchema, locationSchema, nameSchema } from '@/models/User.js';
import type { MiUserProfile } from '@/models/UserProfile.js';
@@ -22,6 +22,7 @@ import { UserEntityService } from '@/core/entities/UserEntityService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { UserFollowingService } from '@/core/UserFollowingService.js';
import { AccountUpdateService } from '@/core/AccountUpdateService.js';
+import { UtilityService } from '@/core/UtilityService.js';
import { HashtagService } from '@/core/HashtagService.js';
import { DI } from '@/di-symbols.js';
import { RolePolicies, RoleService } from '@/core/RoleService.js';
@@ -126,6 +127,13 @@ export const meta = {
code: 'RESTRICTED_BY_ROLE',
id: '8feff0ba-5ab5-585b-31f4-4df816663fad',
},
+
+ nameContainsProhibitedWords: {
+ message: 'Your new name contains prohibited words.',
+ code: 'YOUR_NAME_CONTAINS_PROHIBITED_WORDS',
+ id: '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191',
+ httpStatusCode: 422,
+ },
},
res: {
@@ -241,6 +249,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
@Inject(DI.config)
private config: Config,
+ @Inject(DI.meta)
+ private instanceMeta: MiMeta,
+
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -265,6 +276,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
private cacheService: CacheService,
private httpRequestService: HttpRequestService,
private avatarDecorationService: AvatarDecorationService,
+ private utilityService: UtilityService,
) {
super(meta, paramDef, async (ps, _user, token) => {
const user = await this.usersRepository.findOneByOrFail({ id: _user.id }) as MiLocalUser;
@@ -485,6 +497,14 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint-
const newFields = profileUpdates.fields === undefined ? profile.fields : profileUpdates.fields;
if (newName != null) {
+ let hasProhibitedWords = false;
+ if (!await this.roleService.isModerator(user)) {
+ hasProhibitedWords = this.utilityService.isKeyWordIncluded(newName, this.instanceMeta.prohibitedWordsForNameOfUser);
+ }
+ if (hasProhibitedWords) {
+ throw new ApiError(meta.errors.nameContainsProhibitedWords);
+ }
+
const tokens = mfm.parseSimple(newName);
emojis = emojis.concat(extractCustomEmojisFromMfm(tokens));
}
diff --git a/packages/backend/src/server/web/boot.js b/packages/backend/src/server/web/boot.js
index ad92480c1c..bf83340bde 100644
--- a/packages/backend/src/server/web/boot.js
+++ b/packages/backend/src/server/web/boot.js
@@ -108,7 +108,7 @@
}
for (const [k, v] of Object.entries(themeProps)) {
if (k.startsWith('font')) continue;
- document.documentElement.style.setProperty(`--${k}`, v.toString());
+ document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
// HTMLã® theme-color é©ç”¨
if (k === 'htmlThemeColor') {
diff --git a/packages/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css
index 1cd9cadecf..8094a0f6de 100644
--- a/packages/backend/src/server/web/style.css
+++ b/packages/backend/src/server/web/style.css
@@ -5,8 +5,8 @@
*/
html {
- background-color: var(--bg);
- color: var(--fg);
+ background-color: var(--MI_THEME-bg);
+ color: var(--MI_THEME-fg);
}
#splash {
@@ -17,7 +17,7 @@ html {
width: 100vw;
height: 100vh;
cursor: wait;
- background-color: var(--bg);
+ background-color: var(--MI_THEME-bg);
opacity: 1;
transition: opacity 0.5s ease;
}
@@ -45,7 +45,7 @@ html {
width: 28px;
height: 28px;
transform: translateY(80px);
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
#splashSpinner > .spinner {
diff --git a/packages/backend/src/server/web/style.embed.css b/packages/backend/src/server/web/style.embed.css
index a7b110d80a..5e8786cc4e 100644
--- a/packages/backend/src/server/web/style.embed.css
+++ b/packages/backend/src/server/web/style.embed.css
@@ -5,8 +5,8 @@
*/
html {
- background-color: var(--bg);
- color: var(--fg);
+ background-color: var(--MI_THEME-bg);
+ color: var(--MI_THEME-fg);
}
html.embed {
@@ -24,7 +24,7 @@ html.embed {
width: 100vw;
height: 100vh;
cursor: wait;
- background-color: var(--bg);
+ background-color: var(--MI_THEME-bg);
opacity: 1;
transition: opacity 0.5s ease;
}
@@ -33,7 +33,7 @@ html.embed #splash {
box-sizing: border-box;
min-height: 300px;
border-radius: var(--radius, 12px);
- border: 1px solid var(--divider, #e8e8e8);
+ border: 1px solid var(--MI_THEME-divider, #e8e8e8);
}
html.embed.norounded #splash {
@@ -67,7 +67,7 @@ html.embed.noborder #splash {
width: 28px;
height: 28px;
transform: translateY(70px);
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
#splashSpinner > .spinner {
diff --git a/packages/backend/src/types.ts b/packages/backend/src/types.ts
index 2aa4f279ea..9779f3dbf5 100644
--- a/packages/backend/src/types.ts
+++ b/packages/backend/src/types.ts
@@ -17,6 +17,7 @@
* roleAssigned - ロールãŒä»˜ä¸Žã•れãŸ
* achievementEarned - 実績をç²å¾—
* exportCompleted - エクスãƒãƒ¼ãƒˆãŒå®Œäº†
+ * login - ログイン
* app - アプリ通知
* test - テスト通知(サーãƒãƒ¼å´ï¼‰
*/
@@ -35,6 +36,7 @@ export const notificationTypes = [
'roleAssigned',
'achievementEarned',
'exportCompleted',
+ 'login',
'app',
'test',
] as const;
@@ -104,6 +106,8 @@ export const moderationLogTypes = [
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
'resolveAbuseReport',
+ 'forwardAbuseReport',
+ 'updateAbuseReportNote',
'createInvitation',
'createAd',
'updateAd',
@@ -298,7 +302,18 @@ export type ModerationLogPayloads = {
resolveAbuseReport: {
reportId: string;
report: any;
- forwarded: boolean;
+ forwarded?: boolean;
+ resolvedAs?: string | null;
+ };
+ forwardAbuseReport: {
+ reportId: string;
+ report: any;
+ };
+ updateAbuseReportNote: {
+ reportId: string;
+ report: any;
+ before: string;
+ after: string;
};
createInvitation: {
invitations: any[];
diff --git a/packages/backend/test-federation/.config/example.conf b/packages/backend/test-federation/.config/example.conf
new file mode 100644
index 0000000000..83d04eb39d
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.conf
@@ -0,0 +1,70 @@
+# based on https://github.com/misskey-dev/misskey-hub/blob/7071f63a1c80ee35c71f0fd8a6d8722c118c7574/src/docs/admin/nginx.md
+
+# For WebSocket
+map $http_upgrade $connection_upgrade {
+ default upgrade;
+ '' close;
+}
+
+proxy_cache_path /tmp/nginx_cache levels=1:2 keys_zone=cache1:16m max_size=1g inactive=720m use_temp_path=off;
+
+server {
+ listen 80;
+ listen [::]:80;
+ server_name ${HOST};
+
+ # For SSL domain validation
+ root /var/www/html;
+ location /.well-known/acme-challenge/ { allow all; }
+ location /.well-known/pki-validation/ { allow all; }
+ location / { return 301 https://$server_name$request_uri; }
+}
+
+server {
+ listen 443 ssl;
+ listen [::]:443 ssl;
+ http2 on;
+ server_name ${HOST};
+
+ ssl_session_timeout 1d;
+ ssl_session_cache shared:ssl_session_cache:10m;
+ ssl_session_tickets off;
+
+ ssl_trusted_certificate /etc/nginx/certificates/rootCA.crt;
+ ssl_certificate /etc/nginx/certificates/$server_name.crt;
+ ssl_certificate_key /etc/nginx/certificates/$server_name.key;
+
+ # SSL protocol settings
+ ssl_protocols TLSv1.2 TLSv1.3;
+ ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
+ ssl_prefer_server_ciphers off;
+ ssl_stapling on;
+ ssl_stapling_verify on;
+
+ # Change to your upload limit
+ client_max_body_size 80m;
+
+ # Proxy to Node
+ location / {
+ proxy_pass http://misskey.${HOST}:3000;
+ proxy_set_header Host $host;
+ proxy_http_version 1.1;
+ proxy_redirect off;
+
+ # If it's behind another reverse proxy or CDN, remove the following.
+ proxy_set_header X-Real-IP $remote_addr;
+ proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+ proxy_set_header X-Forwarded-Proto https;
+
+ # For WebSocket
+ proxy_set_header Upgrade $http_upgrade;
+ proxy_set_header Connection $connection_upgrade;
+
+ # Cache settings
+ proxy_cache cache1;
+ proxy_cache_lock on;
+ proxy_cache_use_stale updating;
+ proxy_force_ranges on;
+ add_header X-Cache $upstream_cache_status;
+ }
+}
diff --git a/packages/backend/test-federation/.config/example.default.yml b/packages/backend/test-federation/.config/example.default.yml
new file mode 100644
index 0000000000..ff1760a5a6
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.default.yml
@@ -0,0 +1,25 @@
+url: https://${HOST}/
+port: 3000
+db:
+ host: db.${HOST}
+ port: 5432
+ db: misskey
+ user: postgres
+ pass: postgres
+dbReplications: false
+redis:
+ host: redis.test
+ port: 6379
+id: 'aidx'
+proxyBypassHosts:
+ - api.deepl.com
+ - api-free.deepl.com
+ - www.recaptcha.net
+ - hcaptcha.com
+ - challenges.cloudflare.com
+proxyRemoteFiles: true
+signToActivityPubGet: true
+allowedPrivateNetworks: [
+ '127.0.0.1/32',
+ '172.20.0.0/16'
+]
diff --git a/packages/backend/test-federation/.config/example.docker.env b/packages/backend/test-federation/.config/example.docker.env
new file mode 100644
index 0000000000..a8af7cce49
--- /dev/null
+++ b/packages/backend/test-federation/.config/example.docker.env
@@ -0,0 +1,5 @@
+NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
+POSTGRES_DB=misskey
+POSTGRES_USER=postgres
+POSTGRES_PASSWORD=postgres
+MK_VERBOSE=true
diff --git a/packages/backend/test-federation/.gitignore b/packages/backend/test-federation/.gitignore
new file mode 100644
index 0000000000..e00f952cb5
--- /dev/null
+++ b/packages/backend/test-federation/.gitignore
@@ -0,0 +1,6 @@
+certificates
+volumes
+.env
+docker.env
+*.test.conf
+*.test.default.yml
diff --git a/packages/backend/test-federation/README.md b/packages/backend/test-federation/README.md
new file mode 100644
index 0000000000..967d51f085
--- /dev/null
+++ b/packages/backend/test-federation/README.md
@@ -0,0 +1,24 @@
+## test-federation
+Test federation between two Misskey servers: `a.test` and `b.test`.
+
+Before testing, you need to build the entire project, and change working directory to here:
+```sh
+pnpm build
+cd packages/backend/test-federation
+```
+
+First, you need to start servers by executing following commands:
+```sh
+bash ./setup.sh
+docker compose up --scale tester=0
+```
+
+Then you can run all tests by a following command:
+```sh
+docker compose run --no-deps --rm tester
+```
+
+For testing a specific file, run a following command:
+```sh
+docker compose run --no-deps --rm tester -- pnpm -F backend test:fed packages/backend/test-federation/test/user.test.ts
+```
diff --git a/packages/backend/test-federation/compose.a.yml b/packages/backend/test-federation/compose.a.yml
new file mode 100644
index 0000000000..6a305b404c
--- /dev/null
+++ b/packages/backend/test-federation/compose.a.yml
@@ -0,0 +1,64 @@
+services:
+ a.test:
+ extends:
+ file: ./compose.tpl.yml
+ service: nginx
+ depends_on:
+ misskey.a.test:
+ condition: service_healthy
+ networks:
+ - internal_network_a
+ volumes:
+ - type: bind
+ source: ./.config/a.test.conf
+ target: /etc/nginx/conf.d/a.test.conf
+ read_only: true
+ - type: bind
+ source: ./certificates/a.test.crt
+ target: /etc/nginx/certificates/a.test.crt
+ read_only: true
+ - type: bind
+ source: ./certificates/a.test.key
+ target: /etc/nginx/certificates/a.test.key
+ read_only: true
+
+ misskey.a.test:
+ extends:
+ file: ./compose.tpl.yml
+ service: misskey
+ depends_on:
+ db.a.test:
+ condition: service_healthy
+ redis.test:
+ condition: service_healthy
+ setup:
+ condition: service_completed_successfully
+ networks:
+ - internal_network_a
+ volumes:
+ - type: bind
+ source: ./.config/a.test.default.yml
+ target: /misskey/.config/default.yml
+ read_only: true
+
+ db.a.test:
+ extends:
+ file: ./compose.tpl.yml
+ service: db
+ networks:
+ - internal_network_a
+ volumes:
+ - type: bind
+ source: ./volumes/db.a
+ target: /var/lib/postgresql/data
+ bind:
+ create_host_path: true
+
+networks:
+ internal_network_a:
+ internal: true
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.21.0.0/16
+ ip_range: 172.21.0.0/24
diff --git a/packages/backend/test-federation/compose.b.yml b/packages/backend/test-federation/compose.b.yml
new file mode 100644
index 0000000000..1158b53bae
--- /dev/null
+++ b/packages/backend/test-federation/compose.b.yml
@@ -0,0 +1,64 @@
+services:
+ b.test:
+ extends:
+ file: ./compose.tpl.yml
+ service: nginx
+ depends_on:
+ misskey.b.test:
+ condition: service_healthy
+ networks:
+ - internal_network_b
+ volumes:
+ - type: bind
+ source: ./.config/b.test.conf
+ target: /etc/nginx/conf.d/b.test.conf
+ read_only: true
+ - type: bind
+ source: ./certificates/b.test.crt
+ target: /etc/nginx/certificates/b.test.crt
+ read_only: true
+ - type: bind
+ source: ./certificates/b.test.key
+ target: /etc/nginx/certificates/b.test.key
+ read_only: true
+
+ misskey.b.test:
+ extends:
+ file: ./compose.tpl.yml
+ service: misskey
+ depends_on:
+ db.b.test:
+ condition: service_healthy
+ redis.test:
+ condition: service_healthy
+ setup:
+ condition: service_completed_successfully
+ networks:
+ - internal_network_b
+ volumes:
+ - type: bind
+ source: ./.config/b.test.default.yml
+ target: /misskey/.config/default.yml
+ read_only: true
+
+ db.b.test:
+ extends:
+ file: ./compose.tpl.yml
+ service: db
+ networks:
+ - internal_network_b
+ volumes:
+ - type: bind
+ source: ./volumes/db.b
+ target: /var/lib/postgresql/data
+ bind:
+ create_host_path: true
+
+networks:
+ internal_network_b:
+ internal: true
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.22.0.0/16
+ ip_range: 172.22.0.0/24
diff --git a/packages/backend/test-federation/compose.override.yaml b/packages/backend/test-federation/compose.override.yaml
new file mode 100644
index 0000000000..60a7631ab5
--- /dev/null
+++ b/packages/backend/test-federation/compose.override.yaml
@@ -0,0 +1,117 @@
+services:
+ setup:
+ volumes:
+ - type: volume
+ source: node_modules
+ target: /misskey/node_modules
+ - type: volume
+ source: node_modules_backend
+ target: /misskey/packages/backend/node_modules
+ - type: volume
+ source: node_modules_misskey-js
+ target: /misskey/packages/misskey-js/node_modules
+ - type: volume
+ source: node_modules_misskey-reversi
+ target: /misskey/packages/misskey-reversi/node_modules
+
+ tester:
+ networks:
+ external_network:
+ internal_network:
+ ipv4_address: 172.20.1.1
+ volumes:
+ - type: volume
+ source: node_modules_dev
+ target: /misskey/node_modules
+ - type: volume
+ source: node_modules_backend_dev
+ target: /misskey/packages/backend/node_modules
+ - type: volume
+ source: node_modules_misskey-js_dev
+ target: /misskey/packages/misskey-js/node_modules
+
+ daemon:
+ networks:
+ - external_network
+ - internal_network_a
+ - internal_network_b
+ volumes:
+ - type: volume
+ source: node_modules_dev
+ target: /misskey/node_modules
+ - type: volume
+ source: node_modules_backend_dev
+ target: /misskey/packages/backend/node_modules
+
+ redis.test:
+ networks:
+ - internal_network_a
+ - internal_network_b
+
+ a.test:
+ networks:
+ - internal_network
+
+ misskey.a.test:
+ networks:
+ - external_network
+ - internal_network
+ volumes:
+ - type: volume
+ source: node_modules
+ target: /misskey/node_modules
+ - type: volume
+ source: node_modules_backend
+ target: /misskey/packages/backend/node_modules
+ - type: volume
+ source: node_modules_misskey-js
+ target: /misskey/packages/misskey-js/node_modules
+ - type: volume
+ source: node_modules_misskey-reversi
+ target: /misskey/packages/misskey-reversi/node_modules
+
+ b.test:
+ networks:
+ - internal_network
+
+ misskey.b.test:
+ networks:
+ - external_network
+ - internal_network
+ volumes:
+ - type: volume
+ source: node_modules
+ target: /misskey/node_modules
+ - type: volume
+ source: node_modules_backend
+ target: /misskey/packages/backend/node_modules
+ - type: volume
+ source: node_modules_misskey-js
+ target: /misskey/packages/misskey-js/node_modules
+ - type: volume
+ source: node_modules_misskey-reversi
+ target: /misskey/packages/misskey-reversi/node_modules
+
+networks:
+ external_network:
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.23.0.0/16
+ ip_range: 172.23.0.0/24
+ internal_network:
+ internal: true
+ driver: bridge
+ ipam:
+ config:
+ - subnet: 172.20.0.0/16
+ ip_range: 172.20.0.0/24
+
+volumes:
+ node_modules:
+ node_modules_dev:
+ node_modules_backend:
+ node_modules_backend_dev:
+ node_modules_misskey-js:
+ node_modules_misskey-js_dev:
+ node_modules_misskey-reversi:
diff --git a/packages/backend/test-federation/compose.tpl.yml b/packages/backend/test-federation/compose.tpl.yml
new file mode 100644
index 0000000000..8c38f16919
--- /dev/null
+++ b/packages/backend/test-federation/compose.tpl.yml
@@ -0,0 +1,101 @@
+services:
+ nginx:
+ image: nginx:1.27
+ volumes:
+ - type: bind
+ source: ./certificates/rootCA.crt
+ target: /etc/nginx/certificates/rootCA.crt
+ read_only: true
+ healthcheck:
+ test: service nginx status
+ interval: 5s
+ retries: 20
+
+ misskey:
+ image: node:20
+ env_file:
+ - ./.config/docker.env
+ environment:
+ - NODE_ENV=production
+ volumes:
+ - type: bind
+ source: ../../../built
+ target: /misskey/built
+ read_only: true
+ - type: bind
+ source: ../assets
+ target: /misskey/packages/backend/assets
+ read_only: true
+ - type: bind
+ source: ../built
+ target: /misskey/packages/backend/built
+ read_only: true
+ - type: bind
+ source: ../migration
+ target: /misskey/packages/backend/migration
+ read_only: true
+ - type: bind
+ source: ../ormconfig.js
+ target: /misskey/packages/backend/ormconfig.js
+ read_only: true
+ - type: bind
+ source: ../package.json
+ target: /misskey/packages/backend/package.json
+ read_only: true
+ - type: bind
+ source: ../../misskey-js/built
+ target: /misskey/packages/misskey-js/built
+ read_only: true
+ - type: bind
+ source: ../../misskey-js/package.json
+ target: /misskey/packages/misskey-js/package.json
+ read_only: true
+ - type: bind
+ source: ../../misskey-reversi/built
+ target: /misskey/packages/misskey-reversi/built
+ read_only: true
+ - type: bind
+ source: ../../misskey-reversi/package.json
+ target: /misskey/packages/misskey-reversi/package.json
+ read_only: true
+ - type: bind
+ source: ../../../healthcheck.sh
+ target: /misskey/healthcheck.sh
+ read_only: true
+ - type: bind
+ source: ../../../package.json
+ target: /misskey/package.json
+ read_only: true
+ - type: bind
+ source: ../../../pnpm-lock.yaml
+ target: /misskey/pnpm-lock.yaml
+ read_only: true
+ - type: bind
+ source: ../../../pnpm-workspace.yaml
+ target: /misskey/pnpm-workspace.yaml
+ read_only: true
+ - type: bind
+ source: ./certificates/rootCA.crt
+ target: /usr/local/share/ca-certificates/rootCA.crt
+ read_only: true
+ working_dir: /misskey
+ command: >
+ bash -c "
+ corepack enable && corepack prepare
+ pnpm -F backend migrate
+ pnpm -F backend start
+ "
+ healthcheck:
+ test: bash /misskey/healthcheck.sh
+ interval: 5s
+ retries: 20
+
+ db:
+ image: postgres:15-alpine
+ env_file:
+ - ./.config/docker.env
+ volumes:
+ healthcheck:
+ test: pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB
+ interval: 5s
+ retries: 20
diff --git a/packages/backend/test-federation/compose.yml b/packages/backend/test-federation/compose.yml
new file mode 100644
index 0000000000..62d7e977c0
--- /dev/null
+++ b/packages/backend/test-federation/compose.yml
@@ -0,0 +1,133 @@
+include:
+ - ./compose.a.yml
+ - ./compose.b.yml
+
+services:
+ setup:
+ extends:
+ file: ./compose.tpl.yml
+ service: misskey
+ command: >
+ bash -c "
+ corepack enable && corepack prepare
+ pnpm -F backend i
+ pnpm -F misskey-js i
+ pnpm -F misskey-reversi i
+ "
+
+ tester:
+ image: node:20
+ depends_on:
+ a.test:
+ condition: service_healthy
+ b.test:
+ condition: service_healthy
+ environment:
+ - NODE_ENV=development
+ - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
+ volumes:
+ - type: bind
+ source: ../package.json
+ target: /misskey/packages/backend/package.json
+ read_only: true
+ - type: bind
+ source: ../test/resources
+ target: /misskey/packages/backend/test/resources
+ read_only: true
+ - type: bind
+ source: ./test
+ target: /misskey/packages/backend/test-federation/test
+ read_only: true
+ - type: bind
+ source: ../jest.config.cjs
+ target: /misskey/packages/backend/jest.config.cjs
+ read_only: true
+ - type: bind
+ source: ../jest.config.fed.cjs
+ target: /misskey/packages/backend/jest.config.fed.cjs
+ read_only: true
+ - type: bind
+ source: ../../misskey-js/built
+ target: /misskey/packages/misskey-js/built
+ read_only: true
+ - type: bind
+ source: ../../misskey-js/package.json
+ target: /misskey/packages/misskey-js/package.json
+ read_only: true
+ - type: bind
+ source: ../../../package.json
+ target: /misskey/package.json
+ read_only: true
+ - type: bind
+ source: ../../../pnpm-lock.yaml
+ target: /misskey/pnpm-lock.yaml
+ read_only: true
+ - type: bind
+ source: ../../../pnpm-workspace.yaml
+ target: /misskey/pnpm-workspace.yaml
+ read_only: true
+ - type: bind
+ source: ./certificates/rootCA.crt
+ target: /usr/local/share/ca-certificates/rootCA.crt
+ read_only: true
+ working_dir: /misskey
+ entrypoint: >
+ bash -c '
+ corepack enable && corepack prepare
+ pnpm -F misskey-js i --frozen-lockfile
+ pnpm -F backend i --frozen-lockfile
+ exec "$0" "$@"
+ '
+ command: pnpm -F backend test:fed
+
+ daemon:
+ image: node:20
+ depends_on:
+ redis.test:
+ condition: service_healthy
+ volumes:
+ - type: bind
+ source: ../package.json
+ target: /misskey/packages/backend/package.json
+ read_only: true
+ - type: bind
+ source: ./daemon.ts
+ target: /misskey/packages/backend/test-federation/daemon.ts
+ read_only: true
+ - type: bind
+ source: ./tsconfig.json
+ target: /misskey/packages/backend/test-federation/tsconfig.json
+ read_only: true
+ - type: bind
+ source: ../../../package.json
+ target: /misskey/package.json
+ read_only: true
+ - type: bind
+ source: ../../../pnpm-lock.yaml
+ target: /misskey/pnpm-lock.yaml
+ read_only: true
+ - type: bind
+ source: ../../../pnpm-workspace.yaml
+ target: /misskey/pnpm-workspace.yaml
+ read_only: true
+ working_dir: /misskey
+ command: >
+ bash -c "
+ corepack enable && corepack prepare
+ pnpm -F backend i --frozen-lockfile
+ pnpm exec tsc -p ./packages/backend/test-federation
+ node ./packages/backend/test-federation/built/daemon.js
+ "
+
+ redis.test:
+ image: redis:7-alpine
+ volumes:
+ - type: bind
+ source: ./volumes/redis
+ target: /data
+ bind:
+ create_host_path: true
+ healthcheck:
+ test: redis-cli ping
+ interval: 5s
+ retries: 20
diff --git a/packages/backend/test-federation/daemon.ts b/packages/backend/test-federation/daemon.ts
new file mode 100644
index 0000000000..46b6963c79
--- /dev/null
+++ b/packages/backend/test-federation/daemon.ts
@@ -0,0 +1,38 @@
+import IPCIDR from 'ip-cidr';
+import { Redis } from 'ioredis';
+
+const TESTER_IP_ADDRESS = '172.20.1.1';
+
+/**
+ * This should be same as {@link file://./../src/misc/get-ip-hash.ts}.
+ */
+function getIpHash(ip: string) {
+ const prefix = IPCIDR.createAddress(ip).mask(64);
+ return `ip-${BigInt('0b' + prefix).toString(36)}`;
+}
+
+/**
+ * This prevents hitting rate limit when login.
+ */
+export async function purgeLimit(host: string, client: Redis) {
+ const ipHash = getIpHash(TESTER_IP_ADDRESS);
+ const key = `${host}:limit:${ipHash}:signin`;
+ const res = await client.zrange(key, 0, -1);
+ if (res.length !== 0) {
+ console.log(`${key} - ${JSON.stringify(res)}`);
+ await client.del(key);
+ }
+}
+
+console.log('Daemon started running');
+
+{
+ const redisClient = new Redis({
+ host: 'redis.test',
+ });
+
+ setInterval(() => {
+ purgeLimit('a.test', redisClient);
+ purgeLimit('b.test', redisClient);
+ }, 200);
+}
diff --git a/packages/backend/test-federation/eslint.config.js b/packages/backend/test-federation/eslint.config.js
new file mode 100644
index 0000000000..e3bcf4c0fe
--- /dev/null
+++ b/packages/backend/test-federation/eslint.config.js
@@ -0,0 +1,21 @@
+import globals from 'globals';
+import tsParser from '@typescript-eslint/parser';
+import sharedConfig from '../../shared/eslint.config.js';
+
+export default [
+ ...sharedConfig,
+ {
+ files: ['**/*.ts', '**/*.tsx'],
+ languageOptions: {
+ globals: {
+ ...globals.node,
+ },
+ parserOptions: {
+ parser: tsParser,
+ project: ['./tsconfig.json'],
+ sourceType: 'module',
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ },
+];
diff --git a/packages/backend/test-federation/setup.sh b/packages/backend/test-federation/setup.sh
new file mode 100644
index 0000000000..1bc3a2a87c
--- /dev/null
+++ b/packages/backend/test-federation/setup.sh
@@ -0,0 +1,35 @@
+#!/bin/bash
+mkdir certificates
+
+# rootCA
+openssl genrsa -des3 \
+ -passout pass:rootCA \
+ -out certificates/rootCA.key 4096
+openssl req -x509 -new -nodes -batch \
+ -key certificates/rootCA.key \
+ -sha256 \
+ -days 1024 \
+ -passin pass:rootCA \
+ -out certificates/rootCA.crt
+
+# domain
+function generate {
+ openssl req -new -newkey rsa:2048 -sha256 -nodes \
+ -keyout certificates/$1.key \
+ -subj "/CN=$1/emailAddress=admin@$1/C=JP/ST=/L=/O=Misskey Tester/OU=Some Unit" \
+ -out certificates/$1.csr
+ openssl x509 -req -sha256 \
+ -in certificates/$1.csr \
+ -CA certificates/rootCA.crt \
+ -CAkey certificates/rootCA.key \
+ -CAcreateserial \
+ -passin pass:rootCA \
+ -out certificates/$1.crt \
+ -days 500
+ if [ ! -f .config/docker.env ]; then cp .config/example.docker.env .config/docker.env; fi
+ if [ ! -f .config/$1.conf ]; then sed "s/\${HOST}/$1/g" .config/example.conf > .config/$1.conf; fi
+ if [ ! -f .config/$1.default.yml ]; then sed "s/\${HOST}/$1/g" .config/example.default.yml > .config/$1.default.yml; fi
+}
+
+generate a.test
+generate b.test
diff --git a/packages/backend/test-federation/test/abuse-report.test.ts b/packages/backend/test-federation/test/abuse-report.test.ts
new file mode 100644
index 0000000000..b54d6222b4
--- /dev/null
+++ b/packages/backend/test-federation/test/abuse-report.test.ts
@@ -0,0 +1,52 @@
+import { rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, createModerator, resolveRemoteUser, sleep, type LoginUser } from './utils.js';
+
+describe('Abuse report', () => {
+ describe('Forwarding report', () => {
+ let alice: LoginUser, bob: LoginUser, aModerator: LoginUser, bModerator: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [aModerator, bModerator] = await Promise.all([
+ createModerator('a.test'),
+ createModerator('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ test('Alice reports Bob, moderator in A forwards it, and B moderator receives it', async () => {
+ const comment = crypto.randomUUID();
+ await alice.client.request('users/report-abuse', { userId: bobInA.id, comment });
+ const reports = await aModerator.client.request('admin/abuse-user-reports', {});
+ const report = reports.filter(report => report.comment === comment)[0];
+ await aModerator.client.request('admin/forward-abuse-user-report', { reportId: report.id });
+ await sleep();
+
+ const reportsInB = await bModerator.client.request('admin/abuse-user-reports', {});
+ const reportInB = reportsInB.filter(report => report.comment.includes(comment))[0];
+ // NOTE: reporter is not Alice, and is not moderator in A
+ strictEqual(reportInB.reporter.url, 'https://a.test/@instance.actor');
+ strictEqual(reportInB.targetUserId, bob.id);
+
+ // NOTE: cannot forward multiple times
+ await rejects(
+ async () => await aModerator.client.request('admin/forward-abuse-user-report', { reportId: report.id }),
+ (err: any) => {
+ strictEqual(err.code, 'INTERNAL_ERROR');
+ strictEqual(err.info.e.message, 'The report has already been forwarded.');
+ return true;
+ },
+ );
+ });
+ });
+});
diff --git a/packages/backend/test-federation/test/block.test.ts b/packages/backend/test-federation/test/block.test.ts
new file mode 100644
index 0000000000..ef910eeaea
--- /dev/null
+++ b/packages/backend/test-federation/test/block.test.ts
@@ -0,0 +1,224 @@
+import { deepStrictEqual, rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
+
+describe('Block', () => {
+ describe('Check follow', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ test('Cannot follow if blocked', async () => {
+ await alice.client.request('blocking/create', { userId: bobInA.id });
+ await sleep();
+ await rejects(
+ async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+ (err: any) => {
+ strictEqual(err.code, 'BLOCKED');
+ return true;
+ },
+ );
+
+ const following = await bob.client.request('users/following', { userId: bob.id });
+ strictEqual(following.length, 0);
+ const followers = await alice.client.request('users/followers', { userId: alice.id });
+ strictEqual(followers.length, 0);
+ });
+
+ // FIXME: this is invalid case
+ test('Cannot follow even if unblocked', async () => {
+ // unblock here
+ await alice.client.request('blocking/delete', { userId: bobInA.id });
+ await sleep();
+
+ // TODO: why still being blocked?
+ await rejects(
+ async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+ (err: any) => {
+ strictEqual(err.code, 'BLOCKED');
+ return true;
+ },
+ );
+ });
+
+ test.skip('Can follow if unblocked', async () => {
+ await alice.client.request('blocking/delete', { userId: bobInA.id });
+ await sleep();
+
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+
+ const following = await bob.client.request('users/following', { userId: bob.id });
+ strictEqual(following.length, 1);
+ const followers = await alice.client.request('users/followers', { userId: alice.id });
+ strictEqual(followers.length, 1);
+ });
+
+ test.skip('Remove follower when block them', async () => {
+ test('before block', async () => {
+ const following = await bob.client.request('users/following', { userId: bob.id });
+ strictEqual(following.length, 1);
+ const followers = await alice.client.request('users/followers', { userId: alice.id });
+ strictEqual(followers.length, 1);
+ });
+
+ await alice.client.request('blocking/create', { userId: bobInA.id });
+ await sleep();
+
+ test('after block', async () => {
+ const following = await bob.client.request('users/following', { userId: bob.id });
+ strictEqual(following.length, 0);
+ const followers = await alice.client.request('users/followers', { userId: alice.id });
+ strictEqual(followers.length, 0);
+ });
+ });
+ });
+
+ describe('Check reply', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ test('Cannot reply if blocked', async () => {
+ await alice.client.request('blocking/create', { userId: bobInA.id });
+ await sleep();
+
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+ await rejects(
+ async () => await bob.client.request('notes/create', { text: 'b', replyId: resolvedNote.id }),
+ (err: any) => {
+ strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
+ return true;
+ },
+ );
+ });
+
+ test('Can reply if unblocked', async () => {
+ await alice.client.request('blocking/delete', { userId: bobInA.id });
+ await sleep();
+
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+ const reply = (await bob.client.request('notes/create', { text: 'b', replyId: resolvedNote.id })).createdNote;
+
+ await resolveRemoteNote('b.test', reply.id, alice);
+ });
+ });
+
+ describe('Check reaction', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ test('Cannot reaction if blocked', async () => {
+ await alice.client.request('blocking/create', { userId: bobInA.id });
+ await sleep();
+
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+ await rejects(
+ async () => await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }),
+ (err: any) => {
+ strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
+ return true;
+ },
+ );
+ });
+
+ // FIXME: this is invalid case
+ test('Cannot reaction even if unblocked', async () => {
+ // unblock here
+ await alice.client.request('blocking/delete', { userId: bobInA.id });
+ await sleep();
+
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+
+ // TODO: why still being blocked?
+ await rejects(
+ async () => await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' }),
+ (err: any) => {
+ strictEqual(err.code, 'YOU_HAVE_BEEN_BLOCKED');
+ return true;
+ },
+ );
+ });
+
+ test.skip('Can reaction if unblocked', async () => {
+ await alice.client.request('blocking/delete', { userId: bobInA.id });
+ await sleep();
+
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+ await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: '😅' });
+
+ const _note = await alice.client.request('notes/show', { noteId: note.id });
+ deepStrictEqual(_note.reactions, { '😅': 1 });
+ });
+ });
+
+ describe('Check mention', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ /** NOTE: You should mute the target to stop receiving notifications */
+ test('Can mention and notified even if blocked', async () => {
+ await alice.client.request('blocking/create', { userId: bobInA.id });
+ await sleep();
+
+ const text = `@${alice.username}@a.test plz unblock me!`;
+ await assertNotificationReceived(
+ 'a.test', alice,
+ async () => await bob.client.request('notes/create', { text }),
+ notification => notification.type === 'mention' && notification.userId === bobInA.id && notification.note.text === text,
+ true,
+ );
+ });
+ });
+});
diff --git a/packages/backend/test-federation/test/drive.test.ts b/packages/backend/test-federation/test/drive.test.ts
new file mode 100644
index 0000000000..f755183b4d
--- /dev/null
+++ b/packages/backend/test-federation/test/drive.test.ts
@@ -0,0 +1,175 @@
+import assert, { strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
+
+const bAdmin = await fetchAdmin('b.test');
+
+describe('Drive', () => {
+ describe('Upload image in a.test and resolve from b.test', () => {
+ let uploader: LoginUser;
+
+ beforeAll(async () => {
+ uploader = await createAccount('a.test');
+ });
+
+ let image: Misskey.entities.DriveFile, imageInB: Misskey.entities.DriveFile;
+
+ describe('Upload', () => {
+ beforeAll(async () => {
+ image = await uploadFile('a.test', uploader);
+ const noteWithImage = (await uploader.client.request('notes/create', { fileIds: [image.id] })).createdNote;
+ const noteInB = await resolveRemoteNote('a.test', noteWithImage.id, bAdmin);
+ assert(noteInB.files != null);
+ strictEqual(noteInB.files.length, 1);
+ imageInB = noteInB.files[0];
+ });
+
+ test('Check consistency of DriveFile', () => {
+ // console.log(`a.test: ${JSON.stringify(image, null, '\t')}`);
+ // console.log(`b.test: ${JSON.stringify(imageInB, null, '\t')}`);
+
+ deepStrictEqualWithExcludedFields(image, imageInB, [
+ 'id',
+ 'createdAt',
+ 'size',
+ 'url',
+ 'thumbnailUrl',
+ 'userId',
+ ]);
+ });
+ });
+
+ let updatedImage: Misskey.entities.DriveFile, updatedImageInB: Misskey.entities.DriveFile;
+
+ describe('Update', () => {
+ beforeAll(async () => {
+ updatedImage = await uploader.client.request('drive/files/update', {
+ fileId: image.id,
+ name: 'updated_192.jpg',
+ isSensitive: true,
+ });
+
+ updatedImageInB = await bAdmin.client.request('drive/files/show', {
+ fileId: imageInB.id,
+ });
+ });
+
+ test('Check consistency', () => {
+ // console.log(`a.test: ${JSON.stringify(updatedImage, null, '\t')}`);
+ // console.log(`b.test: ${JSON.stringify(updatedImageInB, null, '\t')}`);
+
+ // FIXME: not updated with `drive/files/update`
+ strictEqual(updatedImage.isSensitive, true);
+ strictEqual(updatedImage.name, 'updated_192.jpg');
+ strictEqual(updatedImageInB.isSensitive, false);
+ strictEqual(updatedImageInB.name, '192.jpg');
+ });
+ });
+
+ let reupdatedImageInB: Misskey.entities.DriveFile;
+
+ describe('Re-update with attaching to Note', () => {
+ beforeAll(async () => {
+ const noteWithUpdatedImage = (await uploader.client.request('notes/create', { fileIds: [updatedImage.id] })).createdNote;
+ const noteWithUpdatedImageInB = await resolveRemoteNote('a.test', noteWithUpdatedImage.id, bAdmin);
+ assert(noteWithUpdatedImageInB.files != null);
+ strictEqual(noteWithUpdatedImageInB.files.length, 1);
+ reupdatedImageInB = noteWithUpdatedImageInB.files[0];
+ });
+
+ test('Check consistency', () => {
+ // console.log(`b.test: ${JSON.stringify(reupdatedImageInB, null, '\t')}`);
+
+ // `isSensitive` is updated
+ strictEqual(reupdatedImageInB.isSensitive, true);
+ // FIXME: but `name` is not updated
+ strictEqual(reupdatedImageInB.name, '192.jpg');
+ });
+ });
+ });
+
+ describe('Sensitive flag', () => {
+ describe('isSensitive is federated in delivering to followers', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+ });
+
+ test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
+ const file = await uploadFile('a.test', alice);
+ await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
+ await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id] });
+ await sleep();
+
+ const notes = await bob.client.request('notes/timeline', {});
+ strictEqual(notes.length, 1);
+ const noteInB = notes[0];
+ assert(noteInB.files != null);
+ strictEqual(noteInB.files.length, 1);
+ strictEqual(noteInB.files[0].isSensitive, true);
+ });
+ });
+
+ describe('isSensitive is federated in resolving', () => {
+ let alice: LoginUser, bob: LoginUser;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+ });
+
+ test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
+ const file = await uploadFile('a.test', alice);
+ await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
+ const note = (await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id] })).createdNote;
+
+ const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+ assert(noteInB.files != null);
+ strictEqual(noteInB.files.length, 1);
+ strictEqual(noteInB.files[0].isSensitive, true);
+ });
+ });
+
+ /** @see https://github.com/misskey-dev/misskey/issues/12208 */
+ describe('isSensitive is federated in replying', () => {
+ let alice: LoginUser, bob: LoginUser;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+ });
+
+ test('Alice uploads sensitive image and it is shown as sensitive from Bob', async () => {
+ const bobNote = (await bob.client.request('notes/create', { text: 'I\'m Bob' })).createdNote;
+
+ const file = await uploadFile('a.test', alice);
+ await alice.client.request('drive/files/update', { fileId: file.id, isSensitive: true });
+ const bobNoteInA = await resolveRemoteNote('b.test', bobNote.id, alice);
+ const note = (await alice.client.request('notes/create', { text: 'sensitive', fileIds: [file.id], replyId: bobNoteInA.id })).createdNote;
+ await sleep();
+
+ const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+ assert(noteInB.files != null);
+ strictEqual(noteInB.files.length, 1);
+ strictEqual(noteInB.files[0].isSensitive, true);
+ });
+ });
+ });
+});
diff --git a/packages/backend/test-federation/test/emoji.test.ts b/packages/backend/test-federation/test/emoji.test.ts
new file mode 100644
index 0000000000..3119ca6e4d
--- /dev/null
+++ b/packages/backend/test-federation/test/emoji.test.ts
@@ -0,0 +1,97 @@
+import assert, { deepStrictEqual, strictEqual } from 'assert';
+import * as Misskey from 'misskey-js';
+import { addCustomEmoji, createAccount, type LoginUser, resolveRemoteUser, sleep } from './utils.js';
+
+describe('Emoji', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+ });
+
+ test('Custom emoji are delivered with Note delivery', async () => {
+ const emoji = await addCustomEmoji('a.test');
+ await alice.client.request('notes/create', { text: `I love :${emoji.name}:` });
+ await sleep();
+
+ const notes = await bob.client.request('notes/timeline', {});
+ const noteInB = notes[0];
+
+ strictEqual(noteInB.text, `I love \u200b:${emoji.name}:\u200b`);
+ assert(noteInB.emojis != null);
+ assert(emoji.name in noteInB.emojis);
+ strictEqual(noteInB.emojis[emoji.name], emoji.url);
+ });
+
+ test('Custom emoji are delivered with Reaction delivery', async () => {
+ const emoji = await addCustomEmoji('a.test');
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ await sleep();
+
+ await alice.client.request('notes/reactions/create', { noteId: note.id, reaction: `:${emoji.name}:` });
+ await sleep();
+
+ const noteInB = (await bob.client.request('notes/timeline', {}))[0];
+ deepStrictEqual(noteInB.reactions[`:${emoji.name}@a.test:`], 1);
+ deepStrictEqual(noteInB.reactionEmojis[`${emoji.name}@a.test`], emoji.url);
+ });
+
+ test('Custom emoji are delivered with Profile delivery', async () => {
+ const emoji = await addCustomEmoji('a.test');
+ const renewedAlice = await alice.client.request('i/update', { name: `:${emoji.name}:` });
+ await sleep();
+
+ const renewedaliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+ strictEqual(renewedaliceInB.name, renewedAlice.name);
+ assert(emoji.name in renewedaliceInB.emojis);
+ strictEqual(renewedaliceInB.emojis[emoji.name], emoji.url);
+ });
+
+ test('Local-only custom emoji aren\'t delivered with Note delivery', async () => {
+ const emoji = await addCustomEmoji('a.test', { localOnly: true });
+ await alice.client.request('notes/create', { text: `I love :${emoji.name}:` });
+ await sleep();
+
+ const notes = await bob.client.request('notes/timeline', {});
+ const noteInB = notes[0];
+
+ strictEqual(noteInB.text, `I love \u200b:${emoji.name}:\u200b`);
+ // deepStrictEqual(noteInB.emojis, {}); // TODO: this fails (why?)
+ deepStrictEqual({ ...noteInB.emojis }, {});
+ });
+
+ test('Local-only custom emoji aren\'t delivered with Reaction delivery', async () => {
+ const emoji = await addCustomEmoji('a.test', { localOnly: true });
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ await sleep();
+
+ await alice.client.request('notes/reactions/create', { noteId: note.id, reaction: `:${emoji.name}:` });
+ await sleep();
+
+ const noteInB = (await bob.client.request('notes/timeline', {}))[0];
+ deepStrictEqual({ ...noteInB.reactions }, { 'â¤': 1 });
+ deepStrictEqual({ ...noteInB.reactionEmojis }, {});
+ });
+
+ test('Local-only custom emoji aren\'t delivered with Profile delivery', async () => {
+ const emoji = await addCustomEmoji('a.test', { localOnly: true });
+ const renewedAlice = await alice.client.request('i/update', { name: `:${emoji.name}:` });
+ await sleep();
+
+ const renewedaliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+ strictEqual(renewedaliceInB.name, renewedAlice.name);
+ deepStrictEqual({ ...renewedaliceInB.emojis }, {});
+ });
+});
diff --git a/packages/backend/test-federation/test/move.test.ts b/packages/backend/test-federation/test/move.test.ts
new file mode 100644
index 0000000000..56a57de8a4
--- /dev/null
+++ b/packages/backend/test-federation/test/move.test.ts
@@ -0,0 +1,52 @@
+import assert, { strictEqual } from 'node:assert';
+import { createAccount, type LoginUser, sleep } from './utils.js';
+
+describe('Move', () => {
+ test('Minimum move', async () => {
+ const [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ await bob.client.request('i/update', { alsoKnownAs: [`@${alice.username}@a.test`] });
+ await alice.client.request('i/move', { moveToAccount: `@${bob.username}@b.test` });
+ });
+
+ /** @see https://github.com/misskey-dev/misskey/issues/11320 */
+ describe('Following relation is transferred after move', () => {
+ let alice: LoginUser, bob: LoginUser, carol: LoginUser;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+ carol = await createAccount('a.test');
+
+ // Follow @carol@a.test ==> @alice@a.test
+ await carol.client.request('following/create', { userId: alice.id });
+
+ // Move @alice@a.test ==> @bob@b.test
+ await bob.client.request('i/update', { alsoKnownAs: [`@${alice.username}@a.test`] });
+ await alice.client.request('i/move', { moveToAccount: `@${bob.username}@b.test` });
+ await sleep();
+ });
+
+ test('Check from follower', async () => {
+ const following = await carol.client.request('users/following', { userId: carol.id });
+ strictEqual(following.length, 2);
+ const followees = following.map(({ followee }) => followee);
+ assert(followees.every(followee => followee != null));
+ assert(followees.some(({ id, url }) => id === alice.id && url === null));
+ assert(followees.some(({ url }) => url === `https://b.test/@${bob.username}`));
+ });
+
+ test('Check from followee', async () => {
+ const followers = await bob.client.request('users/followers', { userId: bob.id });
+ strictEqual(followers.length, 1);
+ const follower = followers[0].follower;
+ assert(follower != null);
+ strictEqual(follower.url, `https://a.test/@${carol.username}`);
+ });
+ });
+});
diff --git a/packages/backend/test-federation/test/note.test.ts b/packages/backend/test-federation/test/note.test.ts
new file mode 100644
index 0000000000..bacc4cc54f
--- /dev/null
+++ b/packages/backend/test-federation/test/note.test.ts
@@ -0,0 +1,317 @@
+import assert, { rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { addCustomEmoji, createAccount, createModerator, deepStrictEqualWithExcludedFields, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep, uploadFile } from './utils.js';
+
+describe('Note', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ describe('Note content', () => {
+ test('Consistency of Public Note', async () => {
+ const image = await uploadFile('a.test', alice);
+ const note = (await alice.client.request('notes/create', {
+ text: 'I am Alice!',
+ fileIds: [image.id],
+ poll: {
+ choices: ['neko', 'inu'],
+ multiple: false,
+ expiredAfter: 60 * 60 * 1000,
+ },
+ })).createdNote;
+
+ const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+ deepStrictEqualWithExcludedFields(note, resolvedNote, [
+ 'id',
+ 'emojis',
+ /** Consistency of files is checked at {@link file://./drive.test.ts}, so let's skip. */
+ 'fileIds',
+ 'files',
+ /** @see https://github.com/misskey-dev/misskey/issues/12409 */
+ 'reactionAcceptance',
+ 'userId',
+ 'user',
+ 'uri',
+ ]);
+ strictEqual(aliceInB.id, resolvedNote.userId);
+ });
+
+ test('Consistency of reply', async () => {
+ const _replyedNote = (await alice.client.request('notes/create', {
+ text: 'a',
+ })).createdNote;
+ const note = (await alice.client.request('notes/create', {
+ text: 'b',
+ replyId: _replyedNote.id,
+ })).createdNote;
+ // NOTE: the repliedCount is incremented, so fetch again
+ const replyedNote = await alice.client.request('notes/show', { noteId: _replyedNote.id });
+ strictEqual(replyedNote.repliesCount, 1);
+
+ const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+ deepStrictEqualWithExcludedFields(note, resolvedNote, [
+ 'id',
+ 'emojis',
+ 'reactionAcceptance',
+ 'replyId',
+ 'reply',
+ 'userId',
+ 'user',
+ 'uri',
+ ]);
+ assert(resolvedNote.replyId != null);
+ assert(resolvedNote.reply != null);
+ deepStrictEqualWithExcludedFields(replyedNote, resolvedNote.reply, [
+ 'id',
+ // TODO: why clippedCount loses consistency?
+ 'clippedCount',
+ 'emojis',
+ 'userId',
+ 'user',
+ 'uri',
+ // flaky because this is parallelly incremented, so let's check it below
+ 'repliesCount',
+ ]);
+ strictEqual(aliceInB.id, resolvedNote.userId);
+
+ await sleep();
+
+ const resolvedReplyedNote = await bob.client.request('notes/show', { noteId: resolvedNote.replyId });
+ strictEqual(resolvedReplyedNote.repliesCount, 1);
+ });
+
+ test('Consistency of Renote', async () => {
+ // NOTE: the renoteCount is not incremented, so no need to fetch again
+ const renotedNote = (await alice.client.request('notes/create', {
+ text: 'a',
+ })).createdNote;
+ const note = (await alice.client.request('notes/create', {
+ text: 'b',
+ renoteId: renotedNote.id,
+ })).createdNote;
+
+ const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+ deepStrictEqualWithExcludedFields(note, resolvedNote, [
+ 'id',
+ 'emojis',
+ 'reactionAcceptance',
+ 'renoteId',
+ 'renote',
+ 'userId',
+ 'user',
+ 'uri',
+ ]);
+ assert(resolvedNote.renoteId != null);
+ assert(resolvedNote.renote != null);
+ deepStrictEqualWithExcludedFields(renotedNote, resolvedNote.renote, [
+ 'id',
+ 'emojis',
+ 'userId',
+ 'user',
+ 'uri',
+ ]);
+ strictEqual(aliceInB.id, resolvedNote.userId);
+ });
+ });
+
+ describe('Other props', () => {
+ test('localOnly', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote;
+ rejects(
+ async () => await bob.client.request('ap/show', { uri: `https://a.test/notes/${note.id}` }),
+ (err: any) => {
+ /**
+ * FIXME: this error is not handled
+ * @see https://github.com/misskey-dev/misskey/issues/12736
+ */
+ strictEqual(err.code, 'INTERNAL_ERROR');
+ return true;
+ },
+ );
+ });
+ });
+
+ describe('Deletion', () => {
+ describe('Check Delete consistency', () => {
+ let carol: LoginUser;
+
+ beforeAll(async () => {
+ carol = await createAccount('a.test');
+
+ await carol.client.request('following/create', { userId: bobInA.id });
+ await sleep();
+ });
+
+ test('Delete is derivered to followers', async () => {
+ const note = (await bob.client.request('notes/create', { text: 'I\'m Bob.' })).createdNote;
+ const noteInA = await resolveRemoteNote('b.test', note.id, carol);
+ await bob.client.request('notes/delete', { noteId: note.id });
+ await sleep();
+
+ await rejects(
+ async () => await carol.client.request('notes/show', { noteId: noteInA.id }),
+ (err: any) => {
+ strictEqual(err.code, 'NO_SUCH_NOTE');
+ return true;
+ },
+ );
+ });
+ });
+
+ describe('Deletion of remote user\'s note for moderation', () => {
+ let note: Misskey.entities.Note;
+
+ test('Alice post is deleted in B', async () => {
+ note = (await alice.client.request('notes/create', { text: 'Hello' })).createdNote;
+ const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+ const bMod = await createModerator('b.test');
+ await bMod.client.request('notes/delete', { noteId: noteInB.id });
+ await rejects(
+ async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
+ (err: any) => {
+ strictEqual(err.code, 'NO_SUCH_NOTE');
+ return true;
+ },
+ );
+ });
+
+ /**
+ * FIXME: implement soft deletion as well as user?
+ * @see https://github.com/misskey-dev/misskey/issues/11437
+ */
+ test.failing('Not found even if resolve again', async () => {
+ const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+ await rejects(
+ async () => await bob.client.request('notes/show', { noteId: noteInB.id }),
+ (err: any) => {
+ strictEqual(err.code, 'NO_SUCH_NOTE');
+ return true;
+ },
+ );
+ });
+ });
+ });
+
+ describe('Reaction', () => {
+ describe('Consistency', () => {
+ test('Unicode reaction', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+ const reaction = '😅';
+ await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction });
+ await sleep();
+
+ const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+ strictEqual(reactions.length, 1);
+ strictEqual(reactions[0].type, reaction);
+ strictEqual(reactions[0].user.id, bobInA.id);
+ });
+
+ test('Custom emoji reaction', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const resolvedNote = await resolveRemoteNote('a.test', note.id, bob);
+ const emoji = await addCustomEmoji('b.test');
+ await bob.client.request('notes/reactions/create', { noteId: resolvedNote.id, reaction: `:${emoji.name}:` });
+ await sleep();
+
+ const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+ strictEqual(reactions.length, 1);
+ strictEqual(reactions[0].type, `:${emoji.name}@b.test:`);
+ strictEqual(reactions[0].user.id, bobInA.id);
+ });
+ });
+
+ describe('Acceptance', () => {
+ test('Even if likeOnly, remote users can react with custom emoji, but it is converted to like', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'likeOnly' })).createdNote;
+ const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+ const emoji = await addCustomEmoji('b.test');
+ await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` });
+ await sleep();
+
+ const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+ strictEqual(reactions.length, 1);
+ strictEqual(reactions[0].type, 'â¤');
+ });
+
+ /**
+ * TODO: this may be unexpected behavior?
+ * @see https://github.com/misskey-dev/misskey/issues/12409
+ */
+ test('Even if nonSensitiveOnly, remote users can react with sensitive emoji, and it is not converted', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a', reactionAcceptance: 'nonSensitiveOnly' })).createdNote;
+ const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+ const emoji = await addCustomEmoji('b.test', { isSensitive: true });
+ await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction: `:${emoji.name}:` });
+ await sleep();
+
+ const reactions = await alice.client.request('notes/reactions', { noteId: note.id });
+ strictEqual(reactions.length, 1);
+ strictEqual(reactions[0].type, `:${emoji.name}@b.test:`);
+ });
+ });
+ });
+
+ describe('Poll', () => {
+ describe('Any remote user\'s vote is delivered to the author', () => {
+ let carol: LoginUser;
+
+ beforeAll(async () => {
+ carol = await createAccount('a.test');
+ });
+
+ test('Bob creates poll and receives a vote from Carol', async () => {
+ const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote;
+ const noteInA = await resolveRemoteNote('b.test', note.id, carol);
+ await carol.client.request('notes/polls/vote', { noteId: noteInA.id, choice: 0 });
+ await sleep();
+
+ const noteAfterVote = await bob.client.request('notes/show', { noteId: note.id });
+ assert(noteAfterVote.poll != null);
+ strictEqual(noteAfterVote.poll.choices[0].votes, 1);
+ strictEqual(noteAfterVote.poll.choices[1].votes, 0);
+ });
+ });
+
+ describe('Local user\'s vote is delivered to the author\'s remote followers', () => {
+ let bobRemoteFollower: LoginUser, localVoter: LoginUser;
+
+ beforeAll(async () => {
+ [
+ bobRemoteFollower,
+ localVoter,
+ ] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ await bobRemoteFollower.client.request('following/create', { userId: bobInA.id });
+ await sleep();
+ });
+
+ test('A vote in Bob\'s server is delivered to Bob\'s remote followers', async () => {
+ const note = (await bob.client.request('notes/create', { poll: { choices: ['inu', 'neko'] } })).createdNote;
+ // NOTE: resolve before voting
+ const noteInA = await resolveRemoteNote('b.test', note.id, bobRemoteFollower);
+ await localVoter.client.request('notes/polls/vote', { noteId: note.id, choice: 0 });
+ await sleep();
+
+ const noteAfterVote = await bobRemoteFollower.client.request('notes/show', { noteId: noteInA.id });
+ assert(noteAfterVote.poll != null);
+ strictEqual(noteAfterVote.poll.choices[0].votes, 1);
+ strictEqual(noteAfterVote.poll.choices[1].votes, 0);
+ });
+ });
+ });
+});
diff --git a/packages/backend/test-federation/test/notification.test.ts b/packages/backend/test-federation/test/notification.test.ts
new file mode 100644
index 0000000000..6d55353653
--- /dev/null
+++ b/packages/backend/test-federation/test/notification.test.ts
@@ -0,0 +1,107 @@
+import * as Misskey from 'misskey-js';
+import { assertNotificationReceived, createAccount, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
+
+describe('Notification', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ describe('Follow', () => {
+ test('Get notification when follow', async () => {
+ await assertNotificationReceived(
+ 'b.test', bob,
+ async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+ notification => notification.type === 'followRequestAccepted' && notification.userId === aliceInB.id,
+ true,
+ );
+
+ await bob.client.request('following/delete', { userId: aliceInB.id });
+ await sleep();
+ });
+
+ test('Get notification when get followed', async () => {
+ await assertNotificationReceived(
+ 'a.test', alice,
+ async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+ notification => notification.type === 'follow' && notification.userId === bobInA.id,
+ true,
+ );
+ });
+
+ afterAll(async () => await bob.client.request('following/delete', { userId: aliceInB.id }));
+ });
+
+ describe('Note', () => {
+ test('Get notification when get a reaction', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+ const reaction = '😅';
+ await assertNotificationReceived(
+ 'a.test', alice,
+ async () => await bob.client.request('notes/reactions/create', { noteId: noteInB.id, reaction }),
+ notification =>
+ notification.type === 'reaction' && notification.note.id === note.id && notification.userId === bobInA.id && notification.reaction === reaction,
+ true,
+ );
+ });
+
+ test('Get notification when replied', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+ const text = crypto.randomUUID();
+ await assertNotificationReceived(
+ 'a.test', alice,
+ async () => await bob.client.request('notes/create', { text, replyId: noteInB.id }),
+ notification =>
+ notification.type === 'reply' && notification.note.reply!.id === note.id && notification.userId === bobInA.id && notification.note.text === text,
+ true,
+ );
+ });
+
+ test('Get notification when renoted', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+ await assertNotificationReceived(
+ 'a.test', alice,
+ async () => await bob.client.request('notes/create', { renoteId: noteInB.id }),
+ notification =>
+ notification.type === 'renote' && notification.note.renote!.id === note.id && notification.userId === bobInA.id,
+ true,
+ );
+ });
+
+ test('Get notification when quoted', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ const noteInB = await resolveRemoteNote('a.test', note.id, bob);
+ const text = crypto.randomUUID();
+ await assertNotificationReceived(
+ 'a.test', alice,
+ async () => await bob.client.request('notes/create', { text, renoteId: noteInB.id }),
+ notification =>
+ notification.type === 'quote' && notification.note.renote!.id === note.id && notification.userId === bobInA.id && notification.note.text === text,
+ true,
+ );
+ });
+
+ test('Get notification when mentioned', async () => {
+ const text = `@${alice.username}@a.test`;
+ await assertNotificationReceived(
+ 'a.test', alice,
+ async () => await bob.client.request('notes/create', { text }),
+ notification => notification.type === 'mention' && notification.userId === bobInA.id && notification.note.text === text,
+ true,
+ );
+ });
+ });
+});
diff --git a/packages/backend/test-federation/test/timeline.test.ts b/packages/backend/test-federation/test/timeline.test.ts
new file mode 100644
index 0000000000..2250bf4a42
--- /dev/null
+++ b/packages/backend/test-federation/test/timeline.test.ts
@@ -0,0 +1,328 @@
+import { strictEqual } from 'assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, fetchAdmin, isNoteUpdatedEventFired, isFired, type LoginUser, type Request, resolveRemoteUser, sleep, createRole } from './utils.js';
+
+const bAdmin = await fetchAdmin('b.test');
+
+describe('Timeline', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+ });
+
+ type TimelineChannel = keyof Misskey.Channels & (`${string}Timeline` | 'antenna' | 'userList' | 'hashtag');
+ type TimelineEndpoint = keyof Misskey.Endpoints & (`${string}timeline` | 'antennas/notes' | 'roles/notes' | 'notes/search-by-tag');
+ const timelineMap = new Map<TimelineChannel, TimelineEndpoint>([
+ ['antenna', 'antennas/notes'],
+ ['globalTimeline', 'notes/global-timeline'],
+ ['homeTimeline', 'notes/timeline'],
+ ['hybridTimeline', 'notes/hybrid-timeline'],
+ ['localTimeline', 'notes/local-timeline'],
+ ['roleTimeline', 'roles/notes'],
+ ['hashtag', 'notes/search-by-tag'],
+ ['userList', 'notes/user-list-timeline'],
+ ]);
+
+ async function postAndCheckReception<C extends TimelineChannel>(
+ timelineChannel: C,
+ expect: boolean,
+ noteParams: Misskey.entities.NotesCreateRequest = {},
+ channelParams: Misskey.Channels[C]['params'] = {},
+ ) {
+ let note: Misskey.entities.Note | undefined;
+ const text = noteParams.text ?? crypto.randomUUID();
+ const streamingFired = await isFired(
+ 'b.test', bob, timelineChannel,
+ async () => {
+ note = (await alice.client.request('notes/create', { text, ...noteParams })).createdNote;
+ },
+ 'note', msg => msg.text === text,
+ channelParams,
+ );
+ strictEqual(streamingFired, expect);
+
+ const endpoint = timelineMap.get(timelineChannel)!;
+ const params: Misskey.Endpoints[typeof endpoint]['req'] =
+ endpoint === 'antennas/notes' ? { antennaId: (channelParams as Misskey.Channels['antenna']['params']).antennaId } :
+ endpoint === 'notes/user-list-timeline' ? { listId: (channelParams as Misskey.Channels['userList']['params']).listId } :
+ endpoint === 'notes/search-by-tag' ? { query: (channelParams as Misskey.Channels['hashtag']['params']).q } :
+ endpoint === 'roles/notes' ? { roleId: (channelParams as Misskey.Channels['roleTimeline']['params']).roleId } :
+ {};
+
+ await sleep();
+ const notes = await (bob.client.request as Request)(endpoint, params);
+ const noteInB = notes.filter(({ uri }) => uri === `https://a.test/notes/${note!.id}`).pop();
+ const endpointFired = noteInB != null;
+ strictEqual(endpointFired, expect);
+
+ // Let's check Delete reception
+ if (expect) {
+ const streamingFired = await isNoteUpdatedEventFired(
+ 'b.test', bob, noteInB!.id,
+ async () => await alice.client.request('notes/delete', { noteId: note!.id }),
+ msg => msg.type === 'deleted' && msg.id === noteInB!.id,
+ );
+ strictEqual(streamingFired, true);
+
+ await sleep();
+ const notes = await (bob.client.request as Request)(endpoint, params);
+ const endpointFired = notes.every(({ uri }) => uri !== `https://a.test/notes/${note!.id}`);
+ strictEqual(endpointFired, true);
+ }
+ }
+
+ describe('homeTimeline', () => {
+ // NOTE: narrowing scope intentionally to prevent mistakes by copy-and-paste
+ const homeTimeline = 'homeTimeline';
+
+ describe('Check reception of remote followee\'s Note', () => {
+ test('Receive remote followee\'s Note', async () => {
+ await postAndCheckReception(homeTimeline, true);
+ });
+
+ test('Receive remote followee\'s home-only Note', async () => {
+ await postAndCheckReception(homeTimeline, true, { visibility: 'home' });
+ });
+
+ test('Receive remote followee\'s followers-only Note', async () => {
+ await postAndCheckReception(homeTimeline, true, { visibility: 'followers' });
+ });
+
+ test('Receive remote followee\'s visible specified-only Note', async () => {
+ await postAndCheckReception(homeTimeline, true, { visibility: 'specified', visibleUserIds: [bobInA.id] });
+ });
+
+ test('Don\'t receive remote followee\'s localOnly Note', async () => {
+ await postAndCheckReception(homeTimeline, false, { localOnly: true });
+ });
+
+ test('Don\'t receive remote followee\'s invisible specified-only Note', async () => {
+ await postAndCheckReception(homeTimeline, false, { visibility: 'specified' });
+ });
+
+ /**
+ * FIXME: can receive this
+ * @see https://github.com/misskey-dev/misskey/issues/14083
+ */
+ test.failing('Don\'t receive remote followee\'s invisible and mentioned specified-only Note', async () => {
+ await postAndCheckReception(homeTimeline, false, { text: `@${bob.username}@b.test Hello`, visibility: 'specified' });
+ });
+
+ /**
+ * FIXME: cannot receive this
+ * @see https://github.com/misskey-dev/misskey/issues/14084
+ */
+ test.failing('Receive remote followee\'s visible specified-only reply to invisible specified-only Note', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'specified' })).createdNote;
+ await postAndCheckReception(homeTimeline, true, { replyId: note.id, visibility: 'specified', visibleUserIds: [bobInA.id] });
+ });
+ });
+ });
+
+ describe('localTimeline', () => {
+ const localTimeline = 'localTimeline';
+
+ describe('Check reception of remote followee\'s Note', () => {
+ test('Don\'t receive remote followee\'s Note', async () => {
+ await postAndCheckReception(localTimeline, false);
+ });
+ });
+ });
+
+ describe('hybridTimeline', () => {
+ const hybridTimeline = 'hybridTimeline';
+
+ describe('Check reception of remote followee\'s Note', () => {
+ test('Receive remote followee\'s Note', async () => {
+ await postAndCheckReception(hybridTimeline, true);
+ });
+
+ test('Receive remote followee\'s home-only Note', async () => {
+ await postAndCheckReception(hybridTimeline, true, { visibility: 'home' });
+ });
+
+ test('Receive remote followee\'s followers-only Note', async () => {
+ await postAndCheckReception(hybridTimeline, true, { visibility: 'followers' });
+ });
+
+ test('Receive remote followee\'s visible specified-only Note', async () => {
+ await postAndCheckReception(hybridTimeline, true, { visibility: 'specified', visibleUserIds: [bobInA.id] });
+ });
+ });
+ });
+
+ describe('globalTimeline', () => {
+ const globalTimeline = 'globalTimeline';
+
+ describe('Check reception of remote followee\'s Note', () => {
+ test('Receive remote followee\'s Note', async () => {
+ await postAndCheckReception(globalTimeline, true);
+ });
+
+ test('Don\'t receive remote followee\'s home-only Note', async () => {
+ await postAndCheckReception(globalTimeline, false, { visibility: 'home' });
+ });
+
+ test('Don\'t receive remote followee\'s followers-only Note', async () => {
+ await postAndCheckReception(globalTimeline, false, { visibility: 'followers' });
+ });
+
+ test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
+ await postAndCheckReception(globalTimeline, false, { visibility: 'specified', visibleUserIds: [bobInA.id] });
+ });
+ });
+ });
+
+ describe('userList', () => {
+ const userList = 'userList';
+
+ let list: Misskey.entities.UserList;
+
+ beforeAll(async () => {
+ list = await bob.client.request('users/lists/create', { name: 'Bob\'s List' });
+ await bob.client.request('users/lists/push', { listId: list.id, userId: aliceInB.id });
+ await sleep();
+ });
+
+ describe('Check reception of remote followee\'s Note', () => {
+ test('Receive remote followee\'s Note', async () => {
+ await postAndCheckReception(userList, true, {}, { listId: list.id });
+ });
+
+ test('Receive remote followee\'s home-only Note', async () => {
+ await postAndCheckReception(userList, true, { visibility: 'home' }, { listId: list.id });
+ });
+
+ test('Receive remote followee\'s followers-only Note', async () => {
+ await postAndCheckReception(userList, true, { visibility: 'followers' }, { listId: list.id });
+ });
+
+ test('Receive remote followee\'s visible specified-only Note', async () => {
+ await postAndCheckReception(userList, true, { visibility: 'specified', visibleUserIds: [bobInA.id] }, { listId: list.id });
+ });
+ });
+ });
+
+ describe('hashtag', () => {
+ const hashtag = 'hashtag';
+
+ describe('Check reception of remote followee\'s Note', () => {
+ test('Receive remote followee\'s Note', async () => {
+ const tag = crypto.randomUUID();
+ await postAndCheckReception(hashtag, true, { text: `#${tag}` }, { q: [[tag]] });
+ });
+
+ test('Receive remote followee\'s home-only Note', async () => {
+ const tag = crypto.randomUUID();
+ await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'home' }, { q: [[tag]] });
+ });
+
+ test('Receive remote followee\'s followers-only Note', async () => {
+ const tag = crypto.randomUUID();
+ await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'followers' }, { q: [[tag]] });
+ });
+
+ test('Receive remote followee\'s visible specified-only Note', async () => {
+ const tag = crypto.randomUUID();
+ await postAndCheckReception(hashtag, true, { text: `#${tag}`, visibility: 'specified', visibleUserIds: [bobInA.id] }, { q: [[tag]] });
+ });
+ });
+ });
+
+ describe('roleTimeline', () => {
+ const roleTimeline = 'roleTimeline';
+
+ let role: Misskey.entities.Role;
+
+ beforeAll(async () => {
+ role = await createRole('b.test', {
+ name: 'Remote Users',
+ description: 'Remote users are assigned to this role.',
+ condFormula: {
+ /** TODO: @see https://github.com/misskey-dev/misskey/issues/14169 */
+ type: 'isRemote' as never,
+ },
+ });
+ await sleep();
+ });
+
+ describe('Check reception of remote followee\'s Note', () => {
+ test('Receive remote followee\'s Note', async () => {
+ await postAndCheckReception(roleTimeline, true, {}, { roleId: role.id });
+ });
+
+ test('Don\'t receive remote followee\'s home-only Note', async () => {
+ await postAndCheckReception(roleTimeline, false, { visibility: 'home' }, { roleId: role.id });
+ });
+
+ test('Don\'t receive remote followee\'s followers-only Note', async () => {
+ await postAndCheckReception(roleTimeline, false, { visibility: 'followers' }, { roleId: role.id });
+ });
+
+ test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
+ await postAndCheckReception(roleTimeline, false, { visibility: 'specified', visibleUserIds: [bobInA.id] }, { roleId: role.id });
+ });
+ });
+
+ afterAll(async () => {
+ await bAdmin.client.request('admin/roles/delete', { roleId: role.id });
+ });
+ });
+
+ // TODO: Cannot test
+ describe.skip('antenna', () => {
+ const antenna = 'antenna';
+
+ let bobAntenna: Misskey.entities.Antenna;
+
+ beforeAll(async () => {
+ bobAntenna = await bob.client.request('antennas/create', {
+ name: 'Bob\'s Egosurfing Antenna',
+ src: 'all',
+ keywords: [['Bob']],
+ excludeKeywords: [],
+ users: [],
+ caseSensitive: false,
+ localOnly: false,
+ withReplies: true,
+ withFile: true,
+ });
+ await sleep();
+ });
+
+ describe('Check reception of remote followee\'s Note', () => {
+ test('Receive remote followee\'s Note', async () => {
+ await postAndCheckReception(antenna, true, { text: 'I love Bob (1)' }, { antennaId: bobAntenna.id });
+ });
+
+ test('Don\'t receive remote followee\'s home-only Note', async () => {
+ await postAndCheckReception(antenna, false, { text: 'I love Bob (2)', visibility: 'home' }, { antennaId: bobAntenna.id });
+ });
+
+ test('Don\'t receive remote followee\'s followers-only Note', async () => {
+ await postAndCheckReception(antenna, false, { text: 'I love Bob (3)', visibility: 'followers' }, { antennaId: bobAntenna.id });
+ });
+
+ test('Don\'t receive remote followee\'s visible specified-only Note', async () => {
+ await postAndCheckReception(antenna, false, { text: 'I love Bob (4)', visibility: 'specified', visibleUserIds: [bobInA.id] }, { antennaId: bobAntenna.id });
+ });
+ });
+
+ afterAll(async () => {
+ await bob.client.request('antennas/delete', { antennaId: bobAntenna.id });
+ });
+ });
+});
diff --git a/packages/backend/test-federation/test/user.test.ts b/packages/backend/test-federation/test/user.test.ts
new file mode 100644
index 0000000000..76605e61d4
--- /dev/null
+++ b/packages/backend/test-federation/test/user.test.ts
@@ -0,0 +1,560 @@
+import assert, { rejects, strictEqual } from 'node:assert';
+import * as Misskey from 'misskey-js';
+import { createAccount, deepStrictEqualWithExcludedFields, fetchAdmin, type LoginUser, resolveRemoteNote, resolveRemoteUser, sleep } from './utils.js';
+
+const [aAdmin, bAdmin] = await Promise.all([
+ fetchAdmin('a.test'),
+ fetchAdmin('b.test'),
+]);
+
+describe('User', () => {
+ describe('Profile', () => {
+ describe('Consistency of profile', () => {
+ let alice: LoginUser;
+ let aliceWatcher: LoginUser;
+ let aliceWatcherInB: LoginUser;
+
+ beforeAll(async () => {
+ alice = await createAccount('a.test');
+ [
+ aliceWatcher,
+ aliceWatcherInB,
+ ] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+ });
+
+ test('Check consistency', async () => {
+ const aliceInA = await aliceWatcher.client.request('users/show', { userId: alice.id });
+ const resolved = await resolveRemoteUser('a.test', aliceInA.id, aliceWatcherInB);
+ const aliceInB = await aliceWatcherInB.client.request('users/show', { userId: resolved.id });
+
+ // console.log(`a.test: ${JSON.stringify(aliceInA, null, '\t')}`);
+ // console.log(`b.test: ${JSON.stringify(aliceInB, null, '\t')}`);
+
+ deepStrictEqualWithExcludedFields(aliceInA, aliceInB, [
+ 'id',
+ 'host',
+ 'avatarUrl',
+ 'instance',
+ 'badgeRoles',
+ 'url',
+ 'uri',
+ 'createdAt',
+ 'lastFetchedAt',
+ 'publicReactions',
+ ]);
+ });
+ });
+
+ describe('ffVisibility is federated', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+
+ // NOTE: follow each other
+ await Promise.all([
+ alice.client.request('following/create', { userId: bobInA.id }),
+ bob.client.request('following/create', { userId: aliceInB.id }),
+ ]);
+ await sleep();
+ });
+
+ test('Visibility set public by default', async () => {
+ for (const user of await Promise.all([
+ alice.client.request('users/show', { userId: bobInA.id }),
+ bob.client.request('users/show', { userId: aliceInB.id }),
+ ])) {
+ strictEqual(user.followersVisibility, 'public');
+ strictEqual(user.followingVisibility, 'public');
+ }
+ });
+
+ /** FIXME: not working */
+ test.skip('Setting private for followersVisibility is federated', async () => {
+ await Promise.all([
+ alice.client.request('i/update', { followersVisibility: 'private' }),
+ bob.client.request('i/update', { followersVisibility: 'private' }),
+ ]);
+ await sleep();
+
+ for (const user of await Promise.all([
+ alice.client.request('users/show', { userId: bobInA.id }),
+ bob.client.request('users/show', { userId: aliceInB.id }),
+ ])) {
+ strictEqual(user.followersVisibility, 'private');
+ strictEqual(user.followingVisibility, 'public');
+ }
+ });
+
+ test.skip('Setting private for followingVisibility is federated', async () => {
+ await Promise.all([
+ alice.client.request('i/update', { followingVisibility: 'private' }),
+ bob.client.request('i/update', { followingVisibility: 'private' }),
+ ]);
+ await sleep();
+
+ for (const user of await Promise.all([
+ alice.client.request('users/show', { userId: bobInA.id }),
+ bob.client.request('users/show', { userId: aliceInB.id }),
+ ])) {
+ strictEqual(user.followersVisibility, 'private');
+ strictEqual(user.followingVisibility, 'private');
+ }
+ });
+ });
+
+ describe('isCat is federated', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ test('Not isCat for default', () => {
+ strictEqual(aliceInB.isCat, false);
+ });
+
+ test('Becoming a cat is sent to their followers', async () => {
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+
+ await alice.client.request('i/update', { isCat: true });
+ await sleep();
+
+ const res = await bob.client.request('users/show', { userId: aliceInB.id });
+ strictEqual(res.isCat, true);
+ });
+ });
+
+ describe('Pinning Notes', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+ aliceInB = await resolveRemoteUser('a.test', alice.id, bob);
+
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ });
+
+ test('Pinning localOnly Note is not delivered', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a', localOnly: true })).createdNote;
+ await alice.client.request('i/pin', { noteId: note.id });
+ await sleep();
+
+ const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+ strictEqual(_aliceInB.pinnedNoteIds.length, 0);
+ });
+
+ test('Pinning followers-only Note is not delivered', async () => {
+ const note = (await alice.client.request('notes/create', { text: 'a', visibility: 'followers' })).createdNote;
+ await alice.client.request('i/pin', { noteId: note.id });
+ await sleep();
+
+ const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+ strictEqual(_aliceInB.pinnedNoteIds.length, 0);
+ });
+
+ let pinnedNote: Misskey.entities.Note;
+
+ test('Pinning normal Note is delivered', async () => {
+ pinnedNote = (await alice.client.request('notes/create', { text: 'a' })).createdNote;
+ await alice.client.request('i/pin', { noteId: pinnedNote.id });
+ await sleep();
+
+ const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+ strictEqual(_aliceInB.pinnedNoteIds.length, 1);
+ const pinnedNoteInB = await resolveRemoteNote('a.test', pinnedNote.id, bob);
+ strictEqual(_aliceInB.pinnedNotes[0].id, pinnedNoteInB.id);
+ });
+
+ test('Unpinning normal Note is delivered', async () => {
+ await alice.client.request('i/unpin', { noteId: pinnedNote.id });
+ await sleep();
+
+ const _aliceInB = await bob.client.request('users/show', { userId: aliceInB.id });
+ strictEqual(_aliceInB.pinnedNoteIds.length, 0);
+ });
+ });
+ });
+
+ describe('Follow / Unfollow', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ describe('Follow a.test ==> b.test', () => {
+ beforeAll(async () => {
+ await alice.client.request('following/create', { userId: bobInA.id });
+
+ await sleep();
+ });
+
+ test('Check consistency with `users/following` and `users/followers` endpoints', async () => {
+ await Promise.all([
+ strictEqual(
+ (await alice.client.request('users/following', { userId: alice.id }))
+ .some(v => v.followeeId === bobInA.id),
+ true,
+ ),
+ strictEqual(
+ (await bob.client.request('users/followers', { userId: bob.id }))
+ .some(v => v.followerId === aliceInB.id),
+ true,
+ ),
+ ]);
+ });
+ });
+
+ describe('Unfollow a.test ==> b.test', () => {
+ beforeAll(async () => {
+ await alice.client.request('following/delete', { userId: bobInA.id });
+
+ await sleep();
+ });
+
+ test('Check consistency with `users/following` and `users/followers` endpoints', async () => {
+ await Promise.all([
+ strictEqual(
+ (await alice.client.request('users/following', { userId: alice.id }))
+ .some(v => v.followeeId === bobInA.id),
+ false,
+ ),
+ strictEqual(
+ (await bob.client.request('users/followers', { userId: bob.id }))
+ .some(v => v.followerId === aliceInB.id),
+ false,
+ ),
+ ]);
+ });
+ });
+ });
+
+ describe('Follow requests', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+
+ await alice.client.request('i/update', { isLocked: true });
+ });
+
+ describe('Send follow request from Bob to Alice and cancel', () => {
+ describe('Bob sends follow request to Alice', () => {
+ beforeAll(async () => {
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+ });
+
+ test('Alice should have a request', async () => {
+ const requests = await alice.client.request('following/requests/list', {});
+ strictEqual(requests.length, 1);
+ strictEqual(requests[0].followee.id, alice.id);
+ strictEqual(requests[0].follower.id, bobInA.id);
+ });
+ });
+
+ describe('Alice cancels it', () => {
+ beforeAll(async () => {
+ await bob.client.request('following/requests/cancel', { userId: aliceInB.id });
+ await sleep();
+ });
+
+ test('Alice should have no requests', async () => {
+ const requests = await alice.client.request('following/requests/list', {});
+ strictEqual(requests.length, 0);
+ });
+ });
+ });
+
+ describe('Send follow request from Bob to Alice and reject', () => {
+ beforeAll(async () => {
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+
+ await alice.client.request('following/requests/reject', { userId: bobInA.id });
+ await sleep();
+ });
+
+ test('Bob should have no requests', async () => {
+ await rejects(
+ async () => await bob.client.request('following/requests/cancel', { userId: aliceInB.id }),
+ (err: any) => {
+ strictEqual(err.code, 'FOLLOW_REQUEST_NOT_FOUND');
+ return true;
+ },
+ );
+ });
+
+ test('Bob doesn\'t follow Alice', async () => {
+ const following = await bob.client.request('users/following', { userId: bob.id });
+ strictEqual(following.length, 0);
+ });
+ });
+
+ describe('Send follow request from Bob to Alice and accept', () => {
+ beforeAll(async () => {
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+
+ await alice.client.request('following/requests/accept', { userId: bobInA.id });
+ await sleep();
+ });
+
+ test('Bob follows Alice', async () => {
+ const following = await bob.client.request('users/following', { userId: bob.id });
+ strictEqual(following.length, 1);
+ strictEqual(following[0].followeeId, aliceInB.id);
+ strictEqual(following[0].followerId, bob.id);
+ });
+ });
+ });
+
+ describe('Deletion', () => {
+ describe('Check Delete consistency', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ test('Bob follows Alice, and Alice deleted themself', async () => {
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+
+ const followers = await alice.client.request('users/followers', { userId: alice.id });
+ strictEqual(followers.length, 1); // followed by Bob
+
+ await alice.client.request('i/delete-account', { password: alice.password });
+ await sleep();
+
+ const following = await bob.client.request('users/following', { userId: bob.id });
+ strictEqual(following.length, 0); // no following relation
+
+ await rejects(
+ async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+ (err: any) => {
+ strictEqual(err.code, 'NO_SUCH_USER');
+ return true;
+ },
+ );
+ });
+ });
+
+ describe('Deletion of remote user for moderation', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ test('Bob follows Alice, then Alice gets deleted in B server', async () => {
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+
+ const followers = await alice.client.request('users/followers', { userId: alice.id });
+ strictEqual(followers.length, 1); // followed by Bob
+
+ await bAdmin.client.request('admin/delete-account', { userId: aliceInB.id });
+ await sleep();
+
+ /**
+ * FIXME: remote account is not deleted!
+ * @see https://github.com/misskey-dev/misskey/issues/14728
+ */
+ const deletedAlice = await bob.client.request('users/show', { userId: aliceInB.id });
+ assert(deletedAlice.id, aliceInB.id);
+
+ // TODO: why still following relation?
+ const following = await bob.client.request('users/following', { userId: bob.id });
+ strictEqual(following.length, 1);
+ await rejects(
+ async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+ (err: any) => {
+ strictEqual(err.code, 'ALREADY_FOLLOWING');
+ return true;
+ },
+ );
+ });
+
+ test('Alice tries to follow Bob, but it is not processed', async () => {
+ await alice.client.request('following/create', { userId: bobInA.id });
+ await sleep();
+
+ const following = await alice.client.request('users/following', { userId: alice.id });
+ strictEqual(following.length, 0); // Not following Bob because B server doesn't return Accept
+
+ const followers = await bob.client.request('users/followers', { userId: bob.id });
+ strictEqual(followers.length, 0); // Alice's Follow is not processed
+ });
+ });
+ });
+
+ describe('Suspension', () => {
+ describe('Check suspend/unsuspend consistency', () => {
+ let alice: LoginUser, bob: LoginUser;
+ let bobInA: Misskey.entities.UserDetailedNotMe, aliceInB: Misskey.entities.UserDetailedNotMe;
+
+ beforeAll(async () => {
+ [alice, bob] = await Promise.all([
+ createAccount('a.test'),
+ createAccount('b.test'),
+ ]);
+
+ [bobInA, aliceInB] = await Promise.all([
+ resolveRemoteUser('b.test', bob.id, alice),
+ resolveRemoteUser('a.test', alice.id, bob),
+ ]);
+ });
+
+ test('Bob follows Alice, and Alice gets suspended, there is no following relation, and Bob fails to follow again', async () => {
+ await bob.client.request('following/create', { userId: aliceInB.id });
+ await sleep();
+
+ const followers = await alice.client.request('users/followers', { userId: alice.id });
+ strictEqual(followers.length, 1); // followed by Bob
+
+ await aAdmin.client.request('admin/suspend-user', { userId: alice.id });
+ await sleep();
+
+ const following = await bob.client.request('users/following', { userId: bob.id });
+ strictEqual(following.length, 0); // no following relation
+
+ await rejects(
+ async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+ (err: any) => {
+ strictEqual(err.code, 'NO_SUCH_USER');
+ return true;
+ },
+ );
+ });
+
+ test('Alice gets unsuspended, Bob succeeds in following Alice', async () => {
+ await aAdmin.client.request('admin/unsuspend-user', { userId: alice.id });
+ await sleep();
+
+ const followers = await alice.client.request('users/followers', { userId: alice.id });
+ strictEqual(followers.length, 1); // FIXME: followers are not deleted??
+
+ /**
+ * FIXME: still rejected!
+ * seems to can't process Undo Delete activity because it is not implemented
+ * related @see https://github.com/misskey-dev/misskey/issues/13273
+ */
+ await rejects(
+ async () => await bob.client.request('following/create', { userId: aliceInB.id }),
+ (err: any) => {
+ strictEqual(err.code, 'NO_SUCH_USER');
+ return true;
+ },
+ );
+
+ // FIXME: resolving also fails
+ await rejects(
+ async () => await resolveRemoteUser('a.test', alice.id, bob),
+ (err: any) => {
+ strictEqual(err.code, 'INTERNAL_ERROR');
+ return true;
+ },
+ );
+ });
+
+ /**
+ * instead of simple unsuspension, let's tell existence by following from Alice
+ */
+ test('Alice can follow Bob', async () => {
+ await alice.client.request('following/create', { userId: bobInA.id });
+ await sleep();
+
+ const bobFollowers = await bob.client.request('users/followers', { userId: bob.id });
+ strictEqual(bobFollowers.length, 1); // followed by Alice
+ assert(bobFollowers[0].follower != null);
+ const renewedaliceInB = bobFollowers[0].follower;
+ assert(aliceInB.username === renewedaliceInB.username);
+ assert(aliceInB.host === renewedaliceInB.host);
+ assert(aliceInB.id !== renewedaliceInB.id); // TODO: Same username and host, but their ids are different! Is it OK?
+
+ const following = await bob.client.request('users/following', { userId: bob.id });
+ strictEqual(following.length, 0); // following are deleted
+
+ // Bob tries to follow Alice
+ await bob.client.request('following/create', { userId: renewedaliceInB.id });
+ await sleep();
+
+ const aliceFollowers = await alice.client.request('users/followers', { userId: alice.id });
+ strictEqual(aliceFollowers.length, 1);
+
+ // FIXME: but resolving still fails ...
+ await rejects(
+ async () => await resolveRemoteUser('a.test', alice.id, bob),
+ (err: any) => {
+ strictEqual(err.code, 'INTERNAL_ERROR');
+ return true;
+ },
+ );
+ });
+ });
+ });
+});
diff --git a/packages/backend/test-federation/test/utils.ts b/packages/backend/test-federation/test/utils.ts
new file mode 100644
index 0000000000..483bf4b254
--- /dev/null
+++ b/packages/backend/test-federation/test/utils.ts
@@ -0,0 +1,309 @@
+import { deepStrictEqual, strictEqual } from 'assert';
+import { readFile } from 'fs/promises';
+import { dirname, join } from 'path';
+import { fileURLToPath } from 'url';
+import * as Misskey from 'misskey-js';
+import { WebSocket } from 'ws';
+
+const __filename = fileURLToPath(import.meta.url);
+const __dirname = dirname(__filename);
+
+export const ADMIN_PARAMS = { username: 'admin', password: 'admin' };
+const ADMIN_CACHE = new Map<Host, SigninResponse>();
+
+await Promise.all([
+ fetchAdmin('a.test'),
+ fetchAdmin('b.test'),
+]);
+
+type SigninResponse = Omit<Misskey.entities.SigninFlowResponse & { finished: true }, 'finished'>;
+
+export type LoginUser = SigninResponse & {
+ client: Misskey.api.APIClient;
+ username: string;
+ password: string;
+}
+
+/** used for avoiding overload and some endpoints */
+export type Request = <
+ E extends keyof Misskey.Endpoints,
+ P extends Misskey.Endpoints[E]['req'],
+>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+) => Promise<Misskey.api.SwitchCaseResponseType<E, P>>;
+
+type Host = 'a.test' | 'b.test';
+
+export async function sleep(ms = 200): Promise<void> {
+ return new Promise(resolve => setTimeout(resolve, ms));
+}
+
+async function signin(
+ host: Host,
+ params: Misskey.entities.SigninFlowRequest,
+): Promise<SigninResponse> {
+ // wait for a second to prevent hit rate limit
+ await sleep(1000);
+
+ return await (new Misskey.api.APIClient({ origin: `https://${host}` }).request as Request)('signin-flow', params)
+ .then(res => {
+ strictEqual(res.finished, true);
+ if (params.username === ADMIN_PARAMS.username) ADMIN_CACHE.set(host, res);
+ return res;
+ })
+ .then(({ id, i }) => ({ id, i }))
+ .catch(async err => {
+ if (err.code === 'TOO_MANY_AUTHENTICATION_FAILURES') {
+ await sleep(Math.random() * 2000);
+ return await signin(host, params);
+ }
+ throw err;
+ });
+}
+
+async function createAdmin(host: Host): Promise<Misskey.entities.SignupResponse | undefined> {
+ const client = new Misskey.api.APIClient({ origin: `https://${host}` });
+ return await client.request('admin/accounts/create', ADMIN_PARAMS).then(res => {
+ ADMIN_CACHE.set(host, {
+ id: res.id,
+ // @ts-expect-error FIXME: openapi-typescript generates incorrect response type for this endpoint, so ignore this
+ i: res.token,
+ });
+ return res as Misskey.entities.SignupResponse;
+ }).then(async res => {
+ await client.request('admin/roles/update-default-policies', {
+ policies: {
+ /** TODO: @see https://github.com/misskey-dev/misskey/issues/14169 */
+ rateLimitFactor: 0 as never,
+ },
+ }, res.token);
+ return res;
+ }).catch(err => {
+ if (err.info.e.message === 'access denied') return undefined;
+ throw err;
+ });
+}
+
+export async function fetchAdmin(host: Host): Promise<LoginUser> {
+ const admin = ADMIN_CACHE.get(host) ?? await signin(host, ADMIN_PARAMS)
+ .catch(async err => {
+ if (err.id === '6cc579cc-885d-43d8-95c2-b8c7fc963280') {
+ await createAdmin(host);
+ return await signin(host, ADMIN_PARAMS);
+ }
+ throw err;
+ });
+
+ return {
+ ...admin,
+ client: new Misskey.api.APIClient({ origin: `https://${host}`, credential: admin.i }),
+ ...ADMIN_PARAMS,
+ };
+}
+
+export async function createAccount(host: Host): Promise<LoginUser> {
+ const username = crypto.randomUUID().replaceAll('-', '').substring(0, 20);
+ const password = crypto.randomUUID().replaceAll('-', '');
+ const admin = await fetchAdmin(host);
+ await admin.client.request('admin/accounts/create', { username, password });
+ const signinRes = await signin(host, { username, password });
+
+ return {
+ ...signinRes,
+ client: new Misskey.api.APIClient({ origin: `https://${host}`, credential: signinRes.i }),
+ username,
+ password,
+ };
+}
+
+export async function createModerator(host: Host): Promise<LoginUser> {
+ const user = await createAccount(host);
+ const role = await createRole(host, {
+ name: 'Moderator',
+ isModerator: true,
+ });
+ const admin = await fetchAdmin(host);
+ await admin.client.request('admin/roles/assign', { roleId: role.id, userId: user.id });
+ return user;
+}
+
+export async function createRole(
+ host: Host,
+ params: Partial<Misskey.entities.AdminRolesCreateRequest> = {},
+): Promise<Misskey.entities.Role> {
+ const admin = await fetchAdmin(host);
+ return await admin.client.request('admin/roles/create', {
+ name: 'Some role',
+ description: 'Role for testing',
+ color: null,
+ iconUrl: null,
+ target: 'conditional',
+ condFormula: {},
+ isPublic: true,
+ isModerator: false,
+ isAdministrator: false,
+ isExplorable: true,
+ asBadge: false,
+ canEditMembersByModerator: false,
+ displayOrder: 0,
+ policies: {},
+ ...params,
+ });
+}
+
+export async function resolveRemoteUser(
+ host: Host,
+ id: string,
+ from: LoginUser,
+): Promise<Misskey.entities.UserDetailedNotMe> {
+ const uri = `https://${host}/users/${id}`;
+ return await from.client.request('ap/show', { uri })
+ .then(res => {
+ strictEqual(res.type, 'User');
+ strictEqual(res.object.uri, uri);
+ return res.object;
+ });
+}
+
+export async function resolveRemoteNote(
+ host: Host,
+ id: string,
+ from: LoginUser,
+): Promise<Misskey.entities.Note> {
+ const uri = `https://${host}/notes/${id}`;
+ return await from.client.request('ap/show', { uri })
+ .then(res => {
+ strictEqual(res.type, 'Note');
+ strictEqual(res.object.uri, uri);
+ return res.object;
+ });
+}
+
+export async function uploadFile(
+ host: Host,
+ user: { i: string },
+ path = '../../test/resources/192.jpg',
+): Promise<Misskey.entities.DriveFile> {
+ const filename = path.split('/').pop() ?? 'untitled';
+ const blob = new Blob([await readFile(join(__dirname, path))]);
+
+ const body = new FormData();
+ body.append('i', user.i);
+ body.append('force', 'true');
+ body.append('file', blob);
+ body.append('name', filename);
+
+ return await fetch(`https://${host}/api/drive/files/create`, { method: 'POST', body })
+ .then(async res => await res.json());
+}
+
+export async function addCustomEmoji(
+ host: Host,
+ param?: Partial<Misskey.entities.AdminEmojiAddRequest>,
+ path?: string,
+): Promise<Misskey.entities.EmojiDetailed> {
+ const admin = await fetchAdmin(host);
+ const name = crypto.randomUUID().replaceAll('-', '');
+ const file = await uploadFile(host, admin, path);
+ return await admin.client.request('admin/emoji/add', { name, fileId: file.id, ...param });
+}
+
+export function deepStrictEqualWithExcludedFields<T>(actual: T, expected: T, excludedFields: (keyof T)[]) {
+ const _actual = structuredClone(actual);
+ const _expected = structuredClone(expected);
+ for (const obj of [_actual, _expected]) {
+ for (const field of excludedFields) {
+ delete obj[field];
+ }
+ }
+ deepStrictEqual(_actual, _expected);
+}
+
+export async function isFired<C extends keyof Misskey.Channels, T extends keyof Misskey.Channels[C]['events']>(
+ host: Host,
+ user: { i: string },
+ channel: C,
+ trigger: () => Promise<unknown>,
+ type: T,
+ // @ts-expect-error TODO: why getting error here?
+ cond: (msg: Parameters<Misskey.Channels[C]['events'][T]>[0]) => boolean,
+ params?: Misskey.Channels[C]['params'],
+): Promise<boolean> {
+ return new Promise<boolean>(async (resolve, reject) => {
+ // @ts-expect-error TODO: why?
+ const stream = new Misskey.Stream(`wss://${host}`, { token: user.i }, { WebSocket });
+ const connection = stream.useChannel(channel, params);
+ connection.on(type as any, ((msg: any) => {
+ if (cond(msg)) {
+ stream.close();
+ clearTimeout(timer);
+ resolve(true);
+ }
+ }) as any);
+
+ let timer: NodeJS.Timeout | undefined;
+
+ await trigger().then(() => {
+ timer = setTimeout(() => {
+ stream.close();
+ resolve(false);
+ }, 500);
+ }).catch(err => {
+ stream.close();
+ clearTimeout(timer);
+ reject(err);
+ });
+ });
+};
+
+export async function isNoteUpdatedEventFired(
+ host: Host,
+ user: { i: string },
+ noteId: string,
+ trigger: () => Promise<unknown>,
+ cond: (msg: Parameters<Misskey.StreamEvents['noteUpdated']>[0]) => boolean,
+): Promise<boolean> {
+ return new Promise<boolean>(async (resolve, reject) => {
+ // @ts-expect-error TODO: why?
+ const stream = new Misskey.Stream(`wss://${host}`, { token: user.i }, { WebSocket });
+ stream.send('s', { id: noteId });
+ stream.on('noteUpdated', msg => {
+ if (cond(msg)) {
+ stream.close();
+ clearTimeout(timer);
+ resolve(true);
+ }
+ });
+
+ let timer: NodeJS.Timeout | undefined;
+
+ await trigger().then(() => {
+ timer = setTimeout(() => {
+ stream.close();
+ resolve(false);
+ }, 500);
+ }).catch(err => {
+ stream.close();
+ clearTimeout(timer);
+ reject(err);
+ });
+ });
+};
+
+export async function assertNotificationReceived(
+ receiverHost: Host,
+ receiver: LoginUser,
+ trigger: () => Promise<unknown>,
+ cond: (notification: Misskey.entities.Notification) => boolean,
+ expect: boolean,
+) {
+ const streamingFired = await isFired(receiverHost, receiver, 'main', trigger, 'notification', cond);
+ strictEqual(streamingFired, expect);
+
+ const endpointFired = await receiver.client.request('i/notifications', {})
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ .then(([notification]) => notification != null ? cond(notification) : false);
+ strictEqual(endpointFired, expect);
+}
diff --git a/packages/backend/test-federation/tsconfig.json b/packages/backend/test-federation/tsconfig.json
new file mode 100644
index 0000000000..3a1cb3b9f3
--- /dev/null
+++ b/packages/backend/test-federation/tsconfig.json
@@ -0,0 +1,114 @@
+{
+ "compilerOptions": {
+ /* Visit https://aka.ms/tsconfig to read more about this file */
+
+ /* Projects */
+ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
+ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
+ // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
+ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
+ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
+ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
+
+ /* Language and Environment */
+ "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
+ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
+ // "jsx": "preserve", /* Specify what JSX code is generated. */
+ // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
+ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
+ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
+ // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
+ // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
+ // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
+ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
+ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
+ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
+
+ /* Modules */
+ "module": "NodeNext", /* Specify what module code is generated. */
+ // "rootDir": "./", /* Specify the root folder within your source files. */
+ // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
+ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
+ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
+ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
+ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
+ // "types": [], /* Specify type package names to be included without being referenced in a source file. */
+ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
+ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
+ // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
+ // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
+ // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
+ // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
+ // "noUncheckedSideEffectImports": true, /* Check side effect imports. */
+ // "resolveJsonModule": true, /* Enable importing .json files. */
+ // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
+ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
+
+ /* JavaScript Support */
+ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
+ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
+ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
+
+ /* Emit */
+ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
+ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
+ // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
+ // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
+ // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
+ // "noEmit": true, /* Disable emitting files from a compilation. */
+ // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
+ "outDir": "./built", /* Specify an output folder for all emitted files. */
+ // "removeComments": true, /* Disable emitting comments. */
+ // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
+ // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
+ // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
+ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
+ // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
+ // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
+ // "newLine": "crlf", /* Set the newline character for emitting files. */
+ // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
+ // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
+ // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
+ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
+ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
+
+ /* Interop Constraints */
+ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
+ // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
+ // "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
+ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
+ "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
+ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
+ "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
+
+ /* Type Checking */
+ "strict": true, /* Enable all strict type-checking options. */
+ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
+ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
+ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
+ // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
+ // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
+ // "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
+ // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
+ // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
+ // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
+ // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
+ // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
+ // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
+ // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
+ // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
+ // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
+ // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
+ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
+ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
+ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
+
+ /* Completeness */
+ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
+ "skipLibCheck": true /* Skip type checking all .d.ts files. */
+ },
+ "include": [
+ "daemon.ts",
+ "./test/**/*.ts"
+ ]
+}
diff --git a/packages/backend/test/e2e/2fa.ts b/packages/backend/test/e2e/2fa.ts
index 48da6ba27f..289359a2ce 100644
--- a/packages/backend/test/e2e/2fa.ts
+++ b/packages/backend/test/e2e/2fa.ts
@@ -138,13 +138,7 @@ describe('2è¦ç´ èªè¨¼', () => {
keyName: string,
credentialId: Buffer,
requestOptions: PublicKeyCredentialRequestOptionsJSON,
- }): {
- username: string,
- password: string,
- credential: AuthenticationResponseJSON,
- 'g-recaptcha-response'?: string | null,
- 'hcaptcha-response'?: string | null,
- } => {
+ }): misskey.entities.SigninFlowRequest => {
// AuthenticatorAssertionResponse.authenticatorData
// https://developer.mozilla.org/en-US/docs/Web/API/AuthenticatorAssertionResponse/authenticatorData
const authenticatorData = Buffer.concat([
@@ -204,17 +198,21 @@ describe('2è¦ç´ èªè¨¼', () => {
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const usersShowResponse = await api('users/show', {
- username,
- }, alice);
- assert.strictEqual(usersShowResponse.status, 200);
- assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
+ const signinWithoutTokenResponse = await api('signin-flow', {
+ ...signinParam(),
+ });
+ assert.strictEqual(signinWithoutTokenResponse.status, 200);
+ assert.deepStrictEqual(signinWithoutTokenResponse.body, {
+ finished: false,
+ next: 'totp',
+ });
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付ã‘
@@ -255,27 +253,23 @@ describe('2è¦ç´ èªè¨¼', () => {
assert.strictEqual(keyDoneResponse.body.id, credentialId.toString('base64url'));
assert.strictEqual(keyDoneResponse.body.name, keyName);
- const usersShowResponse = await api('users/show', {
- username,
- });
- assert.strictEqual(usersShowResponse.status, 200);
- assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, true);
-
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
- assert.strictEqual(signinResponse.body.i, undefined);
- assert.notEqual((signinResponse.body as unknown as { challenge: unknown | undefined }).challenge, undefined);
- assert.notEqual((signinResponse.body as unknown as { allowCredentials: unknown | undefined }).allowCredentials, undefined);
- assert.strictEqual((signinResponse.body as unknown as { allowCredentials: {id: string}[] }).allowCredentials[0].id, credentialId.toString('base64url'));
+ assert.strictEqual(signinResponse.body.finished, false);
+ assert.strictEqual(signinResponse.body.next, 'passkey');
+ assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
+ assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
+ assert.strictEqual(signinResponse.body.authRequest.allowCredentials && signinResponse.body.authRequest.allowCredentials[0]?.id, credentialId.toString('base64url'));
- const signinResponse2 = await api('signin', signinWithSecurityKeyParam({
+ const signinResponse2 = await api('signin-flow', signinWithSecurityKeyParam({
keyName,
credentialId,
- requestOptions: signinResponse.body,
- } as any));
+ requestOptions: signinResponse.body.authRequest,
+ }));
assert.strictEqual(signinResponse2.status, 200);
+ assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付ã‘
@@ -317,28 +311,30 @@ describe('2è¦ç´ èªè¨¼', () => {
}, alice);
assert.strictEqual(passwordLessResponse.status, 204);
- const usersShowResponse = await api('users/show', {
- username,
- });
- assert.strictEqual(usersShowResponse.status, 200);
- assert.strictEqual((usersShowResponse.body as unknown as { usePasswordLessLogin: boolean }).usePasswordLessLogin, true);
+ const iResponse = await api('i', {}, alice);
+ assert.strictEqual(iResponse.status, 200);
+ assert.strictEqual(iResponse.body.usePasswordLessLogin, true);
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
password: '',
});
assert.strictEqual(signinResponse.status, 200);
- assert.strictEqual(signinResponse.body.i, undefined);
+ assert.strictEqual(signinResponse.body.finished, false);
+ assert.strictEqual(signinResponse.body.next, 'passkey');
+ assert.notEqual(signinResponse.body.authRequest.challenge, undefined);
+ assert.notEqual(signinResponse.body.authRequest.allowCredentials, undefined);
- const signinResponse2 = await api('signin', {
+ const signinResponse2 = await api('signin-flow', {
...signinWithSecurityKeyParam({
keyName,
credentialId,
- requestOptions: signinResponse.body,
+ requestOptions: signinResponse.body.authRequest,
} as any),
password: '',
});
assert.strictEqual(signinResponse2.status, 200);
+ assert.strictEqual(signinResponse2.body.finished, true);
assert.notEqual(signinResponse2.body.i, undefined);
// 後片付ã‘
@@ -426,11 +422,11 @@ describe('2è¦ç´ èªè¨¼', () => {
assert.strictEqual(keyDoneResponse.status, 200);
// テストã®å®Ÿè¡Œé †ã«ã‚ˆã£ã¦ã¯è¤‡æ•°æ®‹ã£ã¦ã‚‹ã®ã§å…¨éƒ¨æ¶ˆã™
- const iResponse = await api('i', {
+ const beforeIResponse = await api('i', {
}, alice);
- assert.strictEqual(iResponse.status, 200);
- assert.ok(iResponse.body.securityKeysList);
- for (const key of iResponse.body.securityKeysList) {
+ assert.strictEqual(beforeIResponse.status, 200);
+ assert.ok(beforeIResponse.body.securityKeysList);
+ for (const key of beforeIResponse.body.securityKeysList) {
const removeKeyResponse = await api('i/2fa/remove-key', {
token: otpToken(registerResponse.body.secret),
password,
@@ -439,17 +435,16 @@ describe('2è¦ç´ èªè¨¼', () => {
assert.strictEqual(removeKeyResponse.status, 200);
}
- const usersShowResponse = await api('users/show', {
- username,
- });
- assert.strictEqual(usersShowResponse.status, 200);
- assert.strictEqual((usersShowResponse.body as unknown as { securityKeys: boolean }).securityKeys, false);
+ const afterIResponse = await api('i', {}, alice);
+ assert.strictEqual(afterIResponse.status, 200);
+ assert.strictEqual(afterIResponse.body.securityKeys, false);
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
token: otpToken(registerResponse.body.secret),
});
assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付ã‘
@@ -470,11 +465,9 @@ describe('2è¦ç´ èªè¨¼', () => {
}, alice);
assert.strictEqual(doneResponse.status, 200);
- const usersShowResponse = await api('users/show', {
- username,
- });
- assert.strictEqual(usersShowResponse.status, 200);
- assert.strictEqual((usersShowResponse.body as unknown as { twoFactorEnabled: boolean }).twoFactorEnabled, true);
+ const iResponse = await api('i', {}, alice);
+ assert.strictEqual(iResponse.status, 200);
+ assert.strictEqual(iResponse.body.twoFactorEnabled, true);
const unregisterResponse = await api('i/2fa/unregister', {
token: otpToken(registerResponse.body.secret),
@@ -482,10 +475,11 @@ describe('2è¦ç´ èªè¨¼', () => {
}, alice);
assert.strictEqual(unregisterResponse.status, 204);
- const signinResponse = await api('signin', {
+ const signinResponse = await api('signin-flow', {
...signinParam(),
});
assert.strictEqual(signinResponse.status, 200);
+ assert.strictEqual(signinResponse.body.finished, true);
assert.notEqual(signinResponse.body.i, undefined);
// 後片付ã‘
diff --git a/packages/backend/test/e2e/endpoints.ts b/packages/backend/test/e2e/endpoints.ts
index 5aaec7f6f9..b91d77c398 100644
--- a/packages/backend/test/e2e/endpoints.ts
+++ b/packages/backend/test/e2e/endpoints.ts
@@ -66,9 +66,9 @@ describe('Endpoints', () => {
});
});
- describe('signin', () => {
+ describe('signin-flow', () => {
test('é–“é•ã£ãŸãƒ‘スワードã§ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã§ããªã„', async () => {
- const res = await api('signin', {
+ const res = await api('signin-flow', {
username: 'test1',
password: 'bar',
});
@@ -77,7 +77,7 @@ describe('Endpoints', () => {
});
test('クエリをインジェクションã§ããªã„', async () => {
- const res = await api('signin', {
+ const res = await api('signin-flow', {
username: 'test1',
// @ts-expect-error password must be string
password: {
@@ -89,7 +89,7 @@ describe('Endpoints', () => {
});
test('æ­£ã—ã„æƒ…å ±ã§ã‚µã‚¤ãƒ³ã‚¤ãƒ³ã§ãã‚‹', async () => {
- const res = await api('signin', {
+ const res = await api('signin-flow', {
username: 'test1',
password: 'test1',
});
diff --git a/packages/backend/test/e2e/synalio/abuse-report.ts b/packages/backend/test/e2e/synalio/abuse-report.ts
index 6ce6e47781..c98d199f35 100644
--- a/packages/backend/test/e2e/synalio/abuse-report.ts
+++ b/packages/backend/test/e2e/synalio/abuse-report.ts
@@ -157,7 +157,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
- forward: false,
}, admin);
});
@@ -214,7 +213,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
});
@@ -257,7 +255,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: webhookBody1.body.id,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -288,7 +285,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -319,7 +315,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
@@ -350,7 +345,6 @@ describe('[シナリオ] ユーザ通報', () => {
const webhookBody2 = await captureWebhook(async () => {
await resolveAbuseReport({
reportId: abuseReportId,
- forward: false,
}, admin);
}).catch(e => e.message);
diff --git a/packages/backend/test/e2e/users.ts b/packages/backend/test/e2e/users.ts
index 7d2e14f85d..7b21834b57 100644
--- a/packages/backend/test/e2e/users.ts
+++ b/packages/backend/test/e2e/users.ts
@@ -86,9 +86,6 @@ describe('ユーザー', () => {
publicReactions: user.publicReactions,
followingVisibility: user.followingVisibility,
followersVisibility: user.followersVisibility,
- twoFactorEnabled: user.twoFactorEnabled,
- usePasswordLessLogin: user.usePasswordLessLogin,
- securityKeys: user.securityKeys,
roles: user.roles,
memo: user.memo,
});
@@ -153,6 +150,9 @@ describe('ユーザー', () => {
achievements: user.achievements,
loggedInDays: user.loggedInDays,
policies: user.policies,
+ twoFactorEnabled: user.twoFactorEnabled,
+ usePasswordLessLogin: user.usePasswordLessLogin,
+ securityKeys: user.securityKeys,
...(security ? {
email: user.email,
emailVerified: user.emailVerified,
@@ -350,9 +350,6 @@ describe('ユーザー', () => {
assert.strictEqual(response.publicReactions, true);
assert.strictEqual(response.followingVisibility, 'public');
assert.strictEqual(response.followersVisibility, 'public');
- assert.strictEqual(response.twoFactorEnabled, false);
- assert.strictEqual(response.usePasswordLessLogin, false);
- assert.strictEqual(response.securityKeys, false);
assert.deepStrictEqual(response.roles, []);
assert.strictEqual(response.memo, null);
@@ -393,6 +390,9 @@ describe('ユーザー', () => {
assert.deepStrictEqual(response.achievements, []);
assert.deepStrictEqual(response.loggedInDays, 0);
assert.deepStrictEqual(response.policies, DEFAULT_POLICIES);
+ assert.strictEqual(response.twoFactorEnabled, false);
+ assert.strictEqual(response.usePasswordLessLogin, false);
+ assert.strictEqual(response.securityKeys, false);
assert.notStrictEqual(response.email, undefined);
assert.strictEqual(response.emailVerified, false);
assert.deepStrictEqual(response.securityKeysList, []);
@@ -649,6 +649,9 @@ describe('ユーザー', () => {
{ label: '自分以外ã‹ã‚‰è¦‹ãŸã¨ãã¯Administratorã‹åˆ¤å®šã§ããªã„', user: () => userAdmin, selector: (user: misskey.entities.UserDetailedNotMe) => user.isAdmin, expected: () => undefined },
{ label: 'Moderatorã«ãªã£ã¦ã„ã‚‹', user: () => userModerator, me: () => userModerator, selector: (user: misskey.entities.MeDetailed) => user.isModerator },
{ label: '自分以外ã‹ã‚‰è¦‹ãŸã¨ãã¯Moderatorã‹åˆ¤å®šã§ããªã„', user: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.isModerator, expected: () => undefined },
+ { label: '自分ã‹ã‚‰è¦‹ãŸå ´åˆã«äºŒè¦ç´ èªè¨¼é–¢é€£ã®ãƒ—ロパティãŒã‚»ãƒƒãƒˆã•れã¦ã„ã‚‹', user: () => alice, me: () => alice, selector: (user: misskey.entities.MeDetailed) => user.twoFactorEnabled, expected: () => false },
+ { label: '自分以外ã‹ã‚‰è¦‹ãŸå ´åˆã«äºŒè¦ç´ èªè¨¼é–¢é€£ã®ãƒ—ロパティãŒã‚»ãƒƒãƒˆã•れã¦ã„ãªã„', user: () => alice, me: () => bob, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => undefined },
+ { label: 'モデレーターã‹ã‚‰è¦‹ãŸå ´åˆã«äºŒè¦ç´ èªè¨¼é–¢é€£ã®ãƒ—ロパティãŒã‚»ãƒƒãƒˆã•れã¦ã„ã‚‹', user: () => alice, me: () => userModerator, selector: (user: misskey.entities.UserDetailedNotMe) => user.twoFactorEnabled, expected: () => false },
{ label: 'サイレンスã«ãªã£ã¦ã„ã‚‹', user: () => userSilenced, selector: (user: misskey.entities.UserDetailed) => user.isSilenced },
// FIXME: è½ã¡ã‚‹
//{ label: 'サスペンドã«ãªã£ã¦ã„ã‚‹', user: () => userSuspended, selector: (user: misskey.entities.UserDetailed) => user.isSuspended },
diff --git a/packages/backend/test/unit/AbuseReportNotificationService.ts b/packages/backend/test/unit/AbuseReportNotificationService.ts
index e971659070..235af29f0d 100644
--- a/packages/backend/test/unit/AbuseReportNotificationService.ts
+++ b/packages/backend/test/unit/AbuseReportNotificationService.ts
@@ -5,6 +5,7 @@
import { jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
+import { randomString } from '../utils.js';
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import {
AbuseReportNotificationRecipientRepository,
@@ -25,7 +26,7 @@ import { ModerationLogService } from '@/core/ModerationLogService.js';
import { GlobalEventService } from '@/core/GlobalEventService.js';
import { RecipientMethod } from '@/models/AbuseReportNotificationRecipient.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
-import { randomString } from '../utils.js';
+import { UserEntityService } from '@/core/entities/UserEntityService.js';
process.env.NODE_ENV = 'test';
@@ -111,6 +112,9 @@ describe('AbuseReportNotificationService', () => {
provide: SystemWebhookService, useFactory: () => ({ enqueueSystemWebhook: jest.fn() }),
},
{
+ provide: UserEntityService, useFactory: () => ({ pack: (v: any) => v }),
+ },
+ {
provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
},
{
diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts
index bf8f3ab0e3..1e3605aafc 100644
--- a/packages/backend/test/unit/FetchInstanceMetadataService.ts
+++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts
@@ -8,6 +8,7 @@ process.env.NODE_ENV = 'test';
import { jest } from '@jest/globals';
import { Test } from '@nestjs/testing';
import { Redis } from 'ioredis';
+import type { TestingModule } from '@nestjs/testing';
import { GlobalModule } from '@/GlobalModule.js';
import { FetchInstanceMetadataService } from '@/core/FetchInstanceMetadataService.js';
import { FederatedInstanceService } from '@/core/FederatedInstanceService.js';
@@ -16,7 +17,6 @@ import { LoggerService } from '@/core/LoggerService.js';
import { UtilityService } from '@/core/UtilityService.js';
import { IdService } from '@/core/IdService.js';
import { DI } from '@/di-symbols.js';
-import type { TestingModule } from '@nestjs/testing';
function mockRedis() {
const hash = {} as any;
@@ -52,7 +52,7 @@ describe('FetchInstanceMetadataService', () => {
if (token === HttpRequestService) {
return { getJson: jest.fn(), getHtml: jest.fn(), send: jest.fn() };
} else if (token === FederatedInstanceService) {
- return { fetch: jest.fn() };
+ return { fetchOrRegister: jest.fn() };
} else if (token === DI.redis) {
return mockRedis;
}
@@ -75,7 +75,7 @@ describe('FetchInstanceMetadataService', () => {
test('Lock and update', async () => {
redisClient.set = mockRedis();
const now = Date.now();
- federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
+ federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => { return now - 10 * 1000 * 60 * 60 * 24; } } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
@@ -83,14 +83,14 @@ describe('FetchInstanceMetadataService', () => {
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1);
- expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
+ expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1);
expect(httpRequestService.getJson).toHaveBeenCalled();
});
test('Lock and don\'t update', async () => {
redisClient.set = mockRedis();
const now = Date.now();
- federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
+ federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
const unlockSpy = jest.spyOn(fetchInstanceMetadataService, 'unlock');
@@ -98,14 +98,14 @@ describe('FetchInstanceMetadataService', () => {
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(1);
- expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(1);
+ expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(1);
expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
});
test('Do nothing when lock not acquired', async () => {
redisClient.set = mockRedis();
const now = Date.now();
- federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
+ federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
@@ -114,14 +114,14 @@ describe('FetchInstanceMetadataService', () => {
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any);
expect(tryLockSpy).toHaveBeenCalledTimes(1);
expect(unlockSpy).toHaveBeenCalledTimes(0);
- expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
+ expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalledTimes(0);
});
test('Do when lock not acquired but forced', async () => {
redisClient.set = mockRedis();
const now = Date.now();
- federatedInstanceService.fetch.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
+ federatedInstanceService.fetchOrRegister.mockResolvedValue({ infoUpdatedAt: { getTime: () => now - 10 * 1000 * 60 * 60 * 24 } } as any);
httpRequestService.getJson.mockImplementation(() => { throw Error(); });
await fetchInstanceMetadataService.tryLock('example.com');
const tryLockSpy = jest.spyOn(fetchInstanceMetadataService, 'tryLock');
@@ -130,7 +130,7 @@ describe('FetchInstanceMetadataService', () => {
await fetchInstanceMetadataService.fetchInstanceMetadata({ host: 'example.com' } as any, true);
expect(tryLockSpy).toHaveBeenCalledTimes(0);
expect(unlockSpy).toHaveBeenCalledTimes(1);
- expect(federatedInstanceService.fetch).toHaveBeenCalledTimes(0);
+ expect(federatedInstanceService.fetchOrRegister).toHaveBeenCalledTimes(0);
expect(httpRequestService.getJson).toHaveBeenCalled();
});
});
diff --git a/packages/backend/test/unit/FlashService.ts b/packages/backend/test/unit/FlashService.ts
new file mode 100644
index 0000000000..12ffaf3421
--- /dev/null
+++ b/packages/backend/test/unit/FlashService.ts
@@ -0,0 +1,152 @@
+/* eslint-disable @typescript-eslint/no-unused-vars */
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { FlashService } from '@/core/FlashService.js';
+import { IdService } from '@/core/IdService.js';
+import { FlashsRepository, MiFlash, MiUser, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { DI } from '@/di-symbols.js';
+import { GlobalModule } from '@/GlobalModule.js';
+
+describe('FlashService', () => {
+ let app: TestingModule;
+ let service: FlashService;
+
+ // --------------------------------------------------------------------------------------
+
+ let flashsRepository: FlashsRepository;
+ let usersRepository: UsersRepository;
+ let userProfilesRepository: UserProfilesRepository;
+ let idService: IdService;
+
+ // --------------------------------------------------------------------------------------
+
+ let root: MiUser;
+ let alice: MiUser;
+ let bob: MiUser;
+
+ // --------------------------------------------------------------------------------------
+
+ async function createFlash(data: Partial<MiFlash>) {
+ return flashsRepository.insert({
+ id: idService.gen(),
+ updatedAt: new Date(),
+ userId: root.id,
+ title: 'title',
+ summary: 'summary',
+ script: 'script',
+ permissions: [],
+ likedCount: 0,
+ ...data,
+ }).then(x => flashsRepository.findOneByOrFail(x.identifiers[0]));
+ }
+
+ async function createUser(data: Partial<MiUser> = {}) {
+ const user = await usersRepository
+ .insert({
+ id: idService.gen(),
+ ...data,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+ await userProfilesRepository.insert({
+ userId: user.id,
+ });
+
+ return user;
+ }
+
+ // --------------------------------------------------------------------------------------
+
+ beforeEach(async () => {
+ app = await Test.createTestingModule({
+ imports: [
+ GlobalModule,
+ ],
+ providers: [
+ FlashService,
+ IdService,
+ ],
+ }).compile();
+
+ service = app.get(FlashService);
+
+ flashsRepository = app.get(DI.flashsRepository);
+ usersRepository = app.get(DI.usersRepository);
+ userProfilesRepository = app.get(DI.userProfilesRepository);
+ idService = app.get(IdService);
+
+ root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+ alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+ bob = await createUser({ username: 'bob', usernameLower: 'bob', isRoot: false });
+ });
+
+ afterEach(async () => {
+ await usersRepository.delete({});
+ await userProfilesRepository.delete({});
+ await flashsRepository.delete({});
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ // --------------------------------------------------------------------------------------
+
+ describe('featured', () => {
+ test('should return featured flashes', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash3, flash2, flash1]);
+ });
+
+ test('should return featured flashes public visibility only', async () => {
+ const flash1 = await createFlash({ likedCount: 1, visibility: 'public' });
+ const flash2 = await createFlash({ likedCount: 2, visibility: 'public' });
+ const flash3 = await createFlash({ likedCount: 3, visibility: 'private' });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash2, flash1]);
+ });
+
+ test('should return featured flashes with offset', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 1,
+ limit: 10,
+ });
+
+ expect(result).toEqual([flash2, flash1]);
+ });
+
+ test('should return featured flashes with limit', async () => {
+ const flash1 = await createFlash({ likedCount: 1 });
+ const flash2 = await createFlash({ likedCount: 2 });
+ const flash3 = await createFlash({ likedCount: 3 });
+
+ const result = await service.featured({
+ offset: 0,
+ limit: 2,
+ });
+
+ expect(result).toEqual([flash3, flash2]);
+ });
+ });
+});
diff --git a/packages/backend/test/unit/RoleService.ts b/packages/backend/test/unit/RoleService.ts
index ef80d25f81..9c1b1008d6 100644
--- a/packages/backend/test/unit/RoleService.ts
+++ b/packages/backend/test/unit/RoleService.ts
@@ -10,6 +10,8 @@ import { jest } from '@jest/globals';
import { ModuleMocker } from 'jest-mock';
import { Test } from '@nestjs/testing';
import * as lolex from '@sinonjs/fake-timers';
+import type { TestingModule } from '@nestjs/testing';
+import type { MockFunctionMetadata } from 'jest-mock';
import { GlobalModule } from '@/GlobalModule.js';
import { RoleService } from '@/core/RoleService.js';
import {
@@ -31,8 +33,6 @@ import { secureRndstr } from '@/misc/secure-rndstr.js';
import { NotificationService } from '@/core/NotificationService.js';
import { RoleCondFormulaValue } from '@/models/Role.js';
import { UserEntityService } from '@/core/entities/UserEntityService.js';
-import type { TestingModule } from '@nestjs/testing';
-import type { MockFunctionMetadata } from 'jest-mock';
const moduleMocker = new ModuleMocker(global);
@@ -277,9 +277,9 @@ describe('RoleService', () => {
});
describe('getModeratorIds', () => {
- test('includeAdmins = false, excludeExpire = false', async () => {
- const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
- createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+ test('includeAdmins = false, includeRoot = false, excludeExpire = false', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -295,13 +295,17 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
- const result = await roleService.getModeratorIds(false, false);
+ const result = await roleService.getModeratorIds({
+ includeAdmins: false,
+ includeRoot: false,
+ excludeExpire: false,
+ });
expect(result).toEqual([modeUser1.id, modeUser2.id]);
});
- test('includeAdmins = false, excludeExpire = true', async () => {
- const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
- createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+ test('includeAdmins = false, includeRoot = false, excludeExpire = true', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -317,13 +321,17 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
- const result = await roleService.getModeratorIds(false, true);
+ const result = await roleService.getModeratorIds({
+ includeAdmins: false,
+ includeRoot: false,
+ excludeExpire: true,
+ });
expect(result).toEqual([modeUser1.id]);
});
- test('includeAdmins = true, excludeExpire = false', async () => {
- const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
- createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+ test('includeAdmins = true, includeRoot = false, excludeExpire = false', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -339,13 +347,17 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
- const result = await roleService.getModeratorIds(true, false);
+ const result = await roleService.getModeratorIds({
+ includeAdmins: true,
+ includeRoot: false,
+ excludeExpire: false,
+ });
expect(result).toEqual([adminUser1.id, adminUser2.id, modeUser1.id, modeUser2.id]);
});
- test('includeAdmins = true, excludeExpire = true', async () => {
- const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2] = await Promise.all([
- createUser(), createUser(), createUser(), createUser(), createUser(), createUser(),
+ test('includeAdmins = true, includeRoot = false, excludeExpire = true', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
]);
const role1 = await createRole({ name: 'admin', isAdministrator: true });
@@ -361,9 +373,111 @@ describe('RoleService', () => {
assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
]);
- const result = await roleService.getModeratorIds(true, true);
+ const result = await roleService.getModeratorIds({
+ includeAdmins: true,
+ includeRoot: false,
+ excludeExpire: true,
+ });
expect(result).toEqual([adminUser1.id, modeUser1.id]);
});
+
+ test('includeAdmins = false, includeRoot = true, excludeExpire = false', async () => {
+ const [adminUser1, adminUser2, modeUser1, modeUser2, normalUser1, normalUser2, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ ]);
+
+ const role1 = await createRole({ name: 'admin', isAdministrator: true });
+ const role2 = await createRole({ name: 'moderator', isModerator: true });
+ const role3 = await createRole({ name: 'normal' });
+
+ await Promise.all([
+ assignRole({ userId: adminUser1.id, roleId: role1.id }),
+ assignRole({ userId: adminUser2.id, roleId: role1.id, expiresAt: new Date(Date.now() - 1000) }),
+ assignRole({ userId: modeUser1.id, roleId: role2.id }),
+ assignRole({ userId: modeUser2.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+ assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ assignRole({ userId: normalUser2.id, roleId: role3.id, expiresAt: new Date(Date.now() - 1000) }),
+ ]);
+
+ const result = await roleService.getModeratorIds({
+ includeAdmins: false,
+ includeRoot: true,
+ excludeExpire: false,
+ });
+ expect(result).toEqual([modeUser1.id, modeUser2.id, rootUser.id]);
+ });
+
+ test('root has moderator role', async () => {
+ const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ ]);
+
+ const role1 = await createRole({ name: 'admin', isAdministrator: true });
+ const role2 = await createRole({ name: 'moderator', isModerator: true });
+ const role3 = await createRole({ name: 'normal' });
+
+ await Promise.all([
+ assignRole({ userId: adminUser1.id, roleId: role1.id }),
+ assignRole({ userId: modeUser1.id, roleId: role2.id }),
+ assignRole({ userId: rootUser.id, roleId: role2.id }),
+ assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ ]);
+
+ const result = await roleService.getModeratorIds({
+ includeAdmins: false,
+ includeRoot: true,
+ excludeExpire: false,
+ });
+ expect(result).toEqual([modeUser1.id, rootUser.id]);
+ });
+
+ test('root has administrator role', async () => {
+ const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ ]);
+
+ const role1 = await createRole({ name: 'admin', isAdministrator: true });
+ const role2 = await createRole({ name: 'moderator', isModerator: true });
+ const role3 = await createRole({ name: 'normal' });
+
+ await Promise.all([
+ assignRole({ userId: adminUser1.id, roleId: role1.id }),
+ assignRole({ userId: rootUser.id, roleId: role1.id }),
+ assignRole({ userId: modeUser1.id, roleId: role2.id }),
+ assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ ]);
+
+ const result = await roleService.getModeratorIds({
+ includeAdmins: true,
+ includeRoot: true,
+ excludeExpire: false,
+ });
+ expect(result).toEqual([adminUser1.id, modeUser1.id, rootUser.id]);
+ });
+
+ test('root has moderator role(expire)', async () => {
+ const [adminUser1, modeUser1, normalUser1, rootUser] = await Promise.all([
+ createUser(), createUser(), createUser(), createUser({ isRoot: true }),
+ ]);
+
+ const role1 = await createRole({ name: 'admin', isAdministrator: true });
+ const role2 = await createRole({ name: 'moderator', isModerator: true });
+ const role3 = await createRole({ name: 'normal' });
+
+ await Promise.all([
+ assignRole({ userId: adminUser1.id, roleId: role1.id }),
+ assignRole({ userId: modeUser1.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+ assignRole({ userId: rootUser.id, roleId: role2.id, expiresAt: new Date(Date.now() - 1000) }),
+ assignRole({ userId: normalUser1.id, roleId: role3.id }),
+ ]);
+
+ const result = await roleService.getModeratorIds({
+ includeAdmins: false,
+ includeRoot: true,
+ excludeExpire: true,
+ });
+ expect(result).toEqual([rootUser.id]);
+ });
});
describe('conditional role', () => {
diff --git a/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
new file mode 100644
index 0000000000..1506283a3c
--- /dev/null
+++ b/packages/backend/test/unit/queue/processors/CheckModeratorsActivityProcessorService.ts
@@ -0,0 +1,379 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { jest } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import * as lolex from '@sinonjs/fake-timers';
+import { addHours, addSeconds, subDays, subHours, subSeconds } from 'date-fns';
+import { CheckModeratorsActivityProcessorService } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
+import { MiSystemWebhook, MiUser, MiUserProfile, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { RoleService } from '@/core/RoleService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { MetaService } from '@/core/MetaService.js';
+import { DI } from '@/di-symbols.js';
+import { QueueLoggerService } from '@/queue/QueueLoggerService.js';
+import { EmailService } from '@/core/EmailService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { AnnouncementService } from '@/core/AnnouncementService.js';
+
+const baseDate = new Date(Date.UTC(2000, 11, 15, 12, 0, 0));
+
+describe('CheckModeratorsActivityProcessorService', () => {
+ let app: TestingModule;
+ let clock: lolex.InstalledClock;
+ let service: CheckModeratorsActivityProcessorService;
+
+ // --------------------------------------------------------------------------------------
+
+ let usersRepository: UsersRepository;
+ let userProfilesRepository: UserProfilesRepository;
+ let idService: IdService;
+ let roleService: jest.Mocked<RoleService>;
+ let announcementService: jest.Mocked<AnnouncementService>;
+ let emailService: jest.Mocked<EmailService>;
+ let systemWebhookService: jest.Mocked<SystemWebhookService>;
+
+ let systemWebhook1: MiSystemWebhook;
+ let systemWebhook2: MiSystemWebhook;
+ let systemWebhook3: MiSystemWebhook;
+
+ // --------------------------------------------------------------------------------------
+
+ async function createUser(data: Partial<MiUser> = {}, profile: Partial<MiUserProfile> = {}): Promise<MiUser> {
+ const id = idService.gen();
+ const user = await usersRepository
+ .insert({
+ id: id,
+ username: `user_${id}`,
+ usernameLower: `user_${id}`.toLowerCase(),
+ ...data,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+ await userProfilesRepository.insert({
+ userId: user.id,
+ ...profile,
+ });
+
+ return user;
+ }
+
+ function crateSystemWebhook(data: Partial<MiSystemWebhook> = {}): MiSystemWebhook {
+ return {
+ id: idService.gen(),
+ isActive: true,
+ updatedAt: new Date(),
+ latestSentAt: null,
+ latestStatus: null,
+ name: 'test',
+ url: 'https://example.com',
+ secret: 'test',
+ on: [],
+ ...data,
+ };
+ }
+
+ function mockModeratorRole(users: MiUser[]) {
+ roleService.getModerators.mockReset();
+ roleService.getModerators.mockResolvedValue(users);
+ }
+
+ // --------------------------------------------------------------------------------------
+
+ beforeAll(async () => {
+ app = await Test
+ .createTestingModule({
+ imports: [
+ GlobalModule,
+ ],
+ providers: [
+ CheckModeratorsActivityProcessorService,
+ IdService,
+ {
+ provide: RoleService, useFactory: () => ({ getModerators: jest.fn() }),
+ },
+ {
+ provide: MetaService, useFactory: () => ({ fetch: jest.fn() }),
+ },
+ {
+ provide: AnnouncementService, useFactory: () => ({ create: jest.fn() }),
+ },
+ {
+ provide: EmailService, useFactory: () => ({ sendEmail: jest.fn() }),
+ },
+ {
+ provide: SystemWebhookService, useFactory: () => ({
+ fetchActiveSystemWebhooks: jest.fn(),
+ enqueueSystemWebhook: jest.fn(),
+ }),
+ },
+ {
+ provide: QueueLoggerService, useFactory: () => ({
+ logger: ({
+ createSubLogger: () => ({
+ info: jest.fn(),
+ warn: jest.fn(),
+ succ: jest.fn(),
+ }),
+ }),
+ }),
+ },
+ ],
+ })
+ .compile();
+
+ usersRepository = app.get(DI.usersRepository);
+ userProfilesRepository = app.get(DI.userProfilesRepository);
+
+ service = app.get(CheckModeratorsActivityProcessorService);
+ idService = app.get(IdService);
+ roleService = app.get(RoleService) as jest.Mocked<RoleService>;
+ announcementService = app.get(AnnouncementService) as jest.Mocked<AnnouncementService>;
+ emailService = app.get(EmailService) as jest.Mocked<EmailService>;
+ systemWebhookService = app.get(SystemWebhookService) as jest.Mocked<SystemWebhookService>;
+
+ app.enableShutdownHooks();
+ });
+
+ beforeEach(async () => {
+ clock = lolex.install({
+ now: new Date(baseDate),
+ shouldClearNativeTimers: true,
+ });
+
+ systemWebhook1 = crateSystemWebhook({ on: ['inactiveModeratorsWarning'] });
+ systemWebhook2 = crateSystemWebhook({ on: ['inactiveModeratorsWarning', 'inactiveModeratorsInvitationOnlyChanged'] });
+ systemWebhook3 = crateSystemWebhook({ on: ['abuseReport'] });
+
+ emailService.sendEmail.mockReturnValue(Promise.resolve());
+ announcementService.create.mockReturnValue(Promise.resolve({} as never));
+ systemWebhookService.fetchActiveSystemWebhooks.mockResolvedValue([systemWebhook1, systemWebhook2, systemWebhook3]);
+ systemWebhookService.enqueueSystemWebhook.mockReturnValue(Promise.resolve({} as never));
+ });
+
+ afterEach(async () => {
+ clock.uninstall();
+ await usersRepository.delete({});
+ await userProfilesRepository.delete({});
+ roleService.getModerators.mockReset();
+ announcementService.create.mockReset();
+ emailService.sendEmail.mockReset();
+ systemWebhookService.enqueueSystemWebhook.mockReset();
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ // --------------------------------------------------------------------------------------
+
+ describe('evaluateModeratorsInactiveDays', () => {
+ test('[isModeratorsInactive] inactiveãªãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ãŒã„ã¦ã‚‚ä»–ã®ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿ãƒ¼ãŒã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã‚‰"é‹å–¶ãŒéžã‚¢ã‚¯ãƒ†ã‚£ãƒ–"ã¨ã—ã¦ã¿ãªã•れãªã„', async () => {
+ const [user1, user2, user3, user4] = await Promise.all([
+ // 期é™ã‚ˆã‚Šã‚‚1ç§’æ–°ã—ã„タイミングã§ã‚¢ã‚¯ãƒ†ã‚£ãƒ–化(セーフ)
+ createUser({ lastActiveDate: subDays(addSeconds(baseDate, 1), 7) }),
+ // 期é™ã¡ã‚‡ã†ã©ã«ã‚¢ã‚¯ãƒ†ã‚£ãƒ–化(セーフ)
+ createUser({ lastActiveDate: subDays(baseDate, 7) }),
+ // 期é™ã‚ˆã‚Šã‚‚1ç§’å¤ã„タイミングã§ã‚¢ã‚¯ãƒ†ã‚£ãƒ–化(アウト)
+ createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
+ // 対象外
+ createUser({ lastActiveDate: null }),
+ ]);
+
+ mockModeratorRole([user1, user2, user3, user4]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(false);
+ expect(result.inactiveModerators).toEqual([user3]);
+ });
+
+ test('[isModeratorsInactive] 全員éžã‚¢ã‚¯ãƒ†ã‚£ãƒ–ãªã‚‰"é‹å–¶ãŒéžã‚¢ã‚¯ãƒ†ã‚£ãƒ–"ã¨ã—ã¦ã¿ãªã•れる', async () => {
+ const [user1, user2] = await Promise.all([
+ // 期é™ã‚ˆã‚Šã‚‚1ç§’å¤ã„タイミングã§ã‚¢ã‚¯ãƒ†ã‚£ãƒ–化(アウト)
+ createUser({ lastActiveDate: subDays(subSeconds(baseDate, 1), 7) }),
+ // 対象外
+ createUser({ lastActiveDate: null }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(true);
+ expect(result.inactiveModerators).toEqual([user1]);
+ });
+
+ test('[remainingTime] 猶予ã¾ã§24時間ã‚ã‚‹å ´åˆã€çŒ¶äºˆ1æ—¥ã¨ã—ã¦è¨ˆç®—ã•れる', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 8) }),
+ // 猶予ã¯ã“ã®ãƒ¦ãƒ¼ã‚¶åŸºæº–ã§è¨ˆç®—ã•れる想定。
+ // 期é™ã¾ã§æ®‹ã‚Š24時間->猶予1æ—¥ã¨ã—ã¦è¨ˆç®—ã•れるã¯ãšã§ã‚ã‚‹
+ createUser({ lastActiveDate: subDays(baseDate, 6) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(false);
+ expect(result.inactiveModerators).toEqual([user1]);
+ expect(result.remainingTime.asDays).toBe(1);
+ expect(result.remainingTime.asHours).toBe(24);
+ });
+
+ test('[remainingTime] 猶予ã¾ã§25時間ã‚ã‚‹å ´åˆã€çŒ¶äºˆ1æ—¥ã¨ã—ã¦è¨ˆç®—ã•れる', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 8) }),
+ // 猶予ã¯ã“ã®ãƒ¦ãƒ¼ã‚¶åŸºæº–ã§è¨ˆç®—ã•れる想定。
+ // 期é™ã¾ã§æ®‹ã‚Š25時間->猶予1æ—¥ã¨ã—ã¦è¨ˆç®—ã•れるã¯ãšã§ã‚ã‚‹
+ createUser({ lastActiveDate: subDays(addHours(baseDate, 1), 6) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(false);
+ expect(result.inactiveModerators).toEqual([user1]);
+ expect(result.remainingTime.asDays).toBe(1);
+ expect(result.remainingTime.asHours).toBe(25);
+ });
+
+ test('[remainingTime] 猶予ã¾ã§23時間ã‚ã‚‹å ´åˆã€çŒ¶äºˆ0æ—¥ã¨ã—ã¦è¨ˆç®—ã•れる', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 8) }),
+ // 猶予ã¯ã“ã®ãƒ¦ãƒ¼ã‚¶åŸºæº–ã§è¨ˆç®—ã•れる想定。
+ // 期é™ã¾ã§æ®‹ã‚Š23時間->猶予0æ—¥ã¨ã—ã¦è¨ˆç®—ã•れるã¯ãšã§ã‚ã‚‹
+ createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 6) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(false);
+ expect(result.inactiveModerators).toEqual([user1]);
+ expect(result.remainingTime.asDays).toBe(0);
+ expect(result.remainingTime.asHours).toBe(23);
+ });
+
+ test('[remainingTime] 期é™ã¡ã‚‡ã†ã©ã®å ´åˆã€çŒ¶äºˆ0æ—¥ã¨ã—ã¦è¨ˆç®—ã•れる', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 8) }),
+ // 猶予ã¯ã“ã®ãƒ¦ãƒ¼ã‚¶åŸºæº–ã§è¨ˆç®—ã•れる想定。
+ // 期é™ã¡ã‚‡ã†ã©->猶予0æ—¥ã¨ã—ã¦è¨ˆç®—ã•れるã¯ãšã§ã‚ã‚‹
+ createUser({ lastActiveDate: subDays(baseDate, 7) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(false);
+ expect(result.inactiveModerators).toEqual([user1]);
+ expect(result.remainingTime.asDays).toBe(0);
+ expect(result.remainingTime.asHours).toBe(0);
+ });
+
+ test('[remainingTime] 期é™ã‚ˆã‚Š1時間超éŽã—ã¦ã„ã‚‹å ´åˆã€çŒ¶äºˆ-1æ—¥ã¨ã—ã¦è¨ˆç®—ã•れる', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 8) }),
+ // 猶予ã¯ã“ã®ãƒ¦ãƒ¼ã‚¶åŸºæº–ã§è¨ˆç®—ã•れる想定。
+ // 期é™ã‚ˆã‚Š1時間超éŽ->猶予-1æ—¥ã¨ã—ã¦è¨ˆç®—ã•れるã¯ãšã§ã‚ã‚‹
+ createUser({ lastActiveDate: subDays(subHours(baseDate, 1), 7) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(true);
+ expect(result.inactiveModerators).toEqual([user1, user2]);
+ expect(result.remainingTime.asDays).toBe(-1);
+ expect(result.remainingTime.asHours).toBe(-1);
+ });
+
+ test('[remainingTime] 期é™ã‚ˆã‚Š25時間超éŽã—ã¦ã„ã‚‹å ´åˆã€çŒ¶äºˆ-2æ—¥ã¨ã—ã¦è¨ˆç®—ã•れる', async () => {
+ const [user1, user2] = await Promise.all([
+ createUser({ lastActiveDate: subDays(baseDate, 10) }),
+ // 猶予ã¯ã“ã®ãƒ¦ãƒ¼ã‚¶åŸºæº–ã§è¨ˆç®—ã•れる想定。
+ // 期é™ã‚ˆã‚Š1時間超éŽ->猶予-1æ—¥ã¨ã—ã¦è¨ˆç®—ã•れるã¯ãšã§ã‚ã‚‹
+ createUser({ lastActiveDate: subDays(subHours(baseDate, 25), 7) }),
+ ]);
+
+ mockModeratorRole([user1, user2]);
+
+ const result = await service.evaluateModeratorsInactiveDays();
+ expect(result.isModeratorsInactive).toBe(true);
+ expect(result.inactiveModerators).toEqual([user1, user2]);
+ expect(result.remainingTime.asDays).toBe(-2);
+ expect(result.remainingTime.asHours).toBe(-25);
+ });
+ });
+
+ describe('notifyInactiveModeratorsWarning', () => {
+ test('[notification + mail] 通知ã¯ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿å…¨å“¡ã«ç™ºä¿¡ã•れã€ãƒ¡ãƒ¼ãƒ«ã¯ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ãŒå­˜åœ¨ï¼‹èªè¨¼æ¸ˆã¿ã®å ´åˆã®ã¿', async () => {
+ const [user1, user2, user3, user4, root] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ createUser({}, { email: 'user2@example.com', emailVerified: false }),
+ createUser({}, { email: null, emailVerified: false }),
+ createUser({}, { email: 'user4@example.com', emailVerified: true }),
+ createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1, user2, user3, root]);
+ await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+ expect(emailService.sendEmail).toHaveBeenCalledTimes(2);
+ expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com');
+ expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com');
+ });
+
+ test('[systemWebhook] "inactiveModeratorsWarning"ãŒæœ‰åйãªSystemWebhookã«å¯¾ã—ã¦é€ä¿¡ã•れる', async () => {
+ const [user1] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1]);
+ await service.notifyInactiveModeratorsWarning({ time: 1, asDays: 0, asHours: 0 });
+
+ expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(2);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook1);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[1][0]).toEqual(systemWebhook2);
+ });
+ });
+
+ describe('notifyChangeToInvitationOnly', () => {
+ test('[notification + mail] 通知ã¯ãƒ¢ãƒ‡ãƒ¬ãƒ¼ã‚¿å…¨å“¡ã«ç™ºä¿¡ã•れã€ãƒ¡ãƒ¼ãƒ«ã¯ãƒ¡ãƒ¼ãƒ«ã‚¢ãƒ‰ãƒ¬ã‚¹ãŒå­˜åœ¨ï¼‹èªè¨¼æ¸ˆã¿ã®å ´åˆã®ã¿', async () => {
+ const [user1, user2, user3, user4, root] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ createUser({}, { email: 'user2@example.com', emailVerified: false }),
+ createUser({}, { email: null, emailVerified: false }),
+ createUser({}, { email: 'user4@example.com', emailVerified: true }),
+ createUser({ isRoot: true }, { email: 'root@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1, user2, user3, root]);
+ await service.notifyChangeToInvitationOnly();
+
+ expect(announcementService.create).toHaveBeenCalledTimes(4);
+ expect(announcementService.create.mock.calls[0][0].userId).toBe(user1.id);
+ expect(announcementService.create.mock.calls[1][0].userId).toBe(user2.id);
+ expect(announcementService.create.mock.calls[2][0].userId).toBe(user3.id);
+ expect(announcementService.create.mock.calls[3][0].userId).toBe(root.id);
+
+ expect(emailService.sendEmail).toHaveBeenCalledTimes(2);
+ expect(emailService.sendEmail.mock.calls[0][0]).toBe('user1@example.com');
+ expect(emailService.sendEmail.mock.calls[1][0]).toBe('root@example.com');
+ });
+
+ test('[systemWebhook] "inactiveModeratorsInvitationOnlyChanged"ãŒæœ‰åйãªSystemWebhookã«å¯¾ã—ã¦é€ä¿¡ã•れる', async () => {
+ const [user1] = await Promise.all([
+ createUser({}, { email: 'user1@example.com', emailVerified: true }),
+ ]);
+
+ mockModeratorRole([user1]);
+ await service.notifyChangeToInvitationOnly();
+
+ expect(systemWebhookService.enqueueSystemWebhook).toHaveBeenCalledTimes(1);
+ expect(systemWebhookService.enqueueSystemWebhook.mock.calls[0][0]).toEqual(systemWebhook2);
+ });
+ });
+});
diff --git a/packages/frontend-embed/package.json b/packages/frontend-embed/package.json
index 021a63068a..e91221eba7 100644
--- a/packages/frontend-embed/package.json
+++ b/packages/frontend-embed/package.json
@@ -19,7 +19,7 @@
"@transfem-org/sfm-js": "0.24.5",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.4",
- "@vue/compiler-sfc": "3.5.10",
+ "@vue/compiler-sfc": "3.5.11",
"astring": "1.9.0",
"buraha": "0.0.1",
"estree-walker": "3.0.3",
@@ -27,8 +27,8 @@
"frontend-shared": "workspace:*",
"punycode": "2.3.1",
"rollup": "4.22.5",
- "sass": "1.79.3",
- "shiki": "1.12.0",
+ "sass": "1.79.4",
+ "shiki": "1.21.0",
"tinycolor2": "1.6.0",
"tsc-alias": "1.8.10",
"tsconfig-paths": "4.2.0",
@@ -36,7 +36,7 @@
"uuid": "10.0.0",
"json5": "2.2.3",
"vite": "5.4.8",
- "vue": "3.5.10"
+ "vue": "3.5.11"
},
"devDependencies": {
"@misskey-dev/summaly": "5.1.0",
@@ -51,10 +51,10 @@
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0",
- "@vue/runtime-core": "3.5.10",
+ "@vue/runtime-core": "3.5.11",
"acorn": "8.12.1",
"cross-env": "7.0.3",
- "eslint-plugin-import": "2.30.0",
+ "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.28.0",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",
diff --git a/packages/frontend-embed/src/components/EmCustomEmoji.vue b/packages/frontend-embed/src/components/EmCustomEmoji.vue
index e4149cf363..59b670cdc6 100644
--- a/packages/frontend-embed/src/components/EmCustomEmoji.vue
+++ b/packages/frontend-embed/src/components/EmCustomEmoji.vue
@@ -38,8 +38,6 @@ const props = defineProps<{
host?: string | null;
url?: string;
useOriginalSize?: boolean;
- menu?: boolean;
- menuReaction?: boolean;
fallbackToImage?: boolean;
}>();
diff --git a/packages/frontend-embed/src/components/EmLoading.vue b/packages/frontend-embed/src/components/EmLoading.vue
index 49d8ace37b..47d797606b 100644
--- a/packages/frontend-embed/src/components/EmLoading.vue
+++ b/packages/frontend-embed/src/components/EmLoading.vue
@@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
--size: 38px;
&.colored {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
&.inline {
diff --git a/packages/frontend-embed/src/components/EmMediaBanner.vue b/packages/frontend-embed/src/components/EmMediaBanner.vue
index 435da238a4..cf4a4c53b5 100644
--- a/packages/frontend-embed/src/components/EmMediaBanner.vue
+++ b/packages/frontend-embed/src/components/EmMediaBanner.vue
@@ -31,17 +31,17 @@ defineProps<{
display: flex;
align-items: center;
width: 100%;
- padding: var(--margin);
+ padding: var(--MI-margin);
margin-top: 4px;
- border: 1px solid var(--inputBorder);
- border-radius: var(--radius);
- background-color: var(--panel);
+ border: 1px solid var(--MI_THEME-inputBorder);
+ border-radius: var(--MI-radius);
+ background-color: var(--MI_THEME-panel);
transition: background-color .1s, border-color .1s;
&:hover {
text-decoration: none;
- border-color: var(--inputBorderHover);
- background-color: var(--buttonHoverBg);
+ border-color: var(--MI_THEME-inputBorderHover);
+ background-color: var(--MI_THEME-buttonHoverBg);
}
}
diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue
index 076386e876..3bdf702b01 100644
--- a/packages/frontend-embed/src/components/EmMediaImage.vue
+++ b/packages/frontend-embed/src/components/EmMediaImage.vue
@@ -36,7 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.indicators">
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
<div v-if="image.comment" :class="$style.indicator">ALT</div>
- <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+ <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
</div>
<i v-if="!hide" class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
</div>
@@ -94,8 +94,8 @@ async function onclick(ev: MouseEvent) {
display: block;
position: absolute;
border-radius: 6px;
- background-color: var(--fg);
- color: var(--accentLighten);
+ background-color: var(--MI_THEME-fg);
+ color: var(--MI_THEME-accentLighten);
font-size: 12px;
opacity: .5;
padding: 5px 8px;
@@ -114,19 +114,19 @@ async function onclick(ev: MouseEvent) {
.visible {
position: relative;
- //box-shadow: 0 0 0 1px var(--divider) inset;
- background: var(--bg);
+ //box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset;
+ background: var(--MI_THEME-bg);
background-size: 16px 16px;
}
html[data-color-scheme=dark] .visible {
--c: rgb(255 255 255 / 2%);
- background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+ background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}
html[data-color-scheme=light] .visible {
--c: rgb(0 0 0 / 2%);
- background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+ background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}
.imageContainer {
@@ -150,10 +150,10 @@ html[data-color-scheme=light] .visible {
}
.indicator {
- /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
+ /* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
background-color: black;
border-radius: 6px;
- color: var(--accentLighten);
+ color: var(--MI_THEME-accentLighten);
display: inline-block;
font-weight: bold;
font-size: 0.8em;
diff --git a/packages/frontend-embed/src/components/EmMediaVideo.vue b/packages/frontend-embed/src/components/EmMediaVideo.vue
index ce751f9acd..e2779bdee4 100644
--- a/packages/frontend-embed/src/components/EmMediaVideo.vue
+++ b/packages/frontend-embed/src/components/EmMediaVideo.vue
@@ -29,9 +29,9 @@ defineProps<{
width: 100%;
height: auto;
aspect-ratio: 16 / 9;
- padding: var(--margin);
- border: 1px solid var(--divider);
- border-radius: var(--radius);
+ padding: var(--MI-margin);
+ border: 1px solid var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
background-color: #000;
&:hover {
@@ -49,7 +49,7 @@ defineProps<{
}
.videoOverlayPlayButton {
- background: var(--accent);
+ background: var(--MI_THEME-accent);
color: #fff;
padding: 1rem;
border-radius: 99rem;
diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue
index a631783507..a71364237d 100644
--- a/packages/frontend-embed/src/components/EmMention.vue
+++ b/packages/frontend-embed/src/components/EmMention.vue
@@ -27,7 +27,7 @@ const canonical = props.host === localHost ? `@${props.username}` : `@${props.us
const url = `/${canonical}`;
-const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--mention'));
+const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-mention'));
bg.setAlpha(0.1);
const bgCss = bg.toRgbString();
</script>
@@ -37,7 +37,7 @@ const bgCss = bg.toRgbString();
display: inline-block;
padding: 4px 8px 4px 4px;
border-radius: 999px;
- color: var(--mention);
+ color: var(--MI_THEME-mention);
}
.host {
diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts
index 40a09df939..715cbea952 100644
--- a/packages/frontend-embed/src/components/EmMfm.ts
+++ b/packages/frontend-embed/src/components/EmMfm.ts
@@ -6,6 +6,7 @@
import { VNode, h, SetupContext, provide } from 'vue';
import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
+import { host } from '@@/js/config.js';
import EmUrl from '@/components/EmUrl.vue';
import EmTime from '@/components/EmTime.vue';
import EmLink from '@/components/EmLink.vue';
@@ -13,7 +14,6 @@ import EmMention from '@/components/EmMention.vue';
import EmEmoji from '@/components/EmEmoji.vue';
import EmCustomEmoji from '@/components/EmCustomEmoji.vue';
import EmA from '@/components/EmA.vue';
-import { host } from '@@/js/config.js';
function safeParseFloat(str: unknown): number | null {
if (typeof str !== 'string' || str === '') return null;
@@ -26,8 +26,8 @@ const QUOTE_STYLE = `
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
-color: var(--fg);
-border-left: solid 3px var(--fg);
+color: var(--MI_THEME-fg);
+border-left: solid 3px var(--MI_THEME-fg);
opacity: 0.7;
`.split('\n').join(' ');
@@ -41,9 +41,6 @@ type MfmProps = {
rootScale?: number;
nyaize?: boolean | 'respect';
parsedNodes?: mfm.MfmNode[] | null;
- enableEmojiMenu?: boolean;
- enableEmojiMenuReaction?: boolean;
- linkNavigationBehavior?: string;
};
type MfmEvents = {
@@ -52,8 +49,6 @@ type MfmEvents = {
// eslint-disable-next-line import/no-default-export
export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
- provide('linkNavigationBehavior', props.linkNavigationBehavior);
-
const isNote = props.isNote ?? true;
const shouldNyaize = props.nyaize === 'respect' && props.author?.isCat && props.author?.speakAsCat;
@@ -287,7 +282,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
}
case 'border': {
let color = validColor(token.props.args.color);
- color = color ? `#${color}` : 'var(--accent)';
+ color = color ? `#${color}` : 'var(--MI_THEME-accent)';
let b_style = token.props.args.style;
if (
typeof b_style !== 'string' ||
@@ -320,7 +315,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
const child = token.children[0];
const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
return h('span', {
- style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
+ style: 'display: inline-block; font-size: 90%; border: solid 1px var(--MI_THEME-divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
}, [
h('i', {
class: 'ti ti-clock',
@@ -391,7 +386,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
return [h('bdi', h(EmA, {
key: Math.random(),
to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
- style: 'color:var(--hashtag);',
+ style: 'color:var(--MI_THEME-hashtag);',
}, `#${token.props.hashtag}`))];
}
@@ -428,8 +423,6 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
normal: props.plain,
host: null,
useOriginalSize: scale >= 2.5,
- menu: props.enableEmojiMenu,
- menuReaction: props.enableEmojiMenuReaction,
fallbackToImage: false,
})];
} else {
diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue
index 4677284747..a42cbbce8b 100644
--- a/packages/frontend-embed/src/components/EmNote.vue
+++ b/packages/frontend-embed/src/components/EmNote.vue
@@ -109,6 +109,8 @@ SPDX-License-Identifier: AGPL-3.0-only
import { computed, inject, ref, shallowRef } from 'vue';
import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { url } from '@@/js/config.js';
import I18n from '@/components/I18n.vue';
import EmNoteSub from '@/components/EmNoteSub.vue';
import EmNoteHeader from '@/components/EmNoteHeader.vue';
@@ -124,8 +126,6 @@ import EmUserName from '@/components/EmUserName.vue';
import EmTime from '@/components/EmTime.vue';
import { userPage } from '@/utils.js';
import { i18n } from '@/i18n.js';
-import { shouldCollapsed } from '@@/js/collapsed.js';
-import { url } from '@@/js/config.js';
function getAppearNote(note: Misskey.entities.Note) {
return Misskey.note.isPureRenote(note) ? note.renote : note;
@@ -165,14 +165,8 @@ const isDeleted = ref(false);
font-size: 1.05em;
overflow: clip;
contain: content;
-
- // ã“ã‚Œã‚‰ã®æŒ‡å®šã¯ãƒ‘フォーマンスå‘上ã«ã¯æœ‰åйã ãŒã€ãƒŽãƒ¼ãƒˆã®é«˜ã•ã¯ä¸€å®šã§ãªã„ãŸã‚ã€
- // ä¸‹ã®æ–¹ã¾ã§ã‚¹ã‚¯ãƒ­ãƒ¼ãƒ«ã™ã‚‹ã¨ä¸Šã®ãƒŽãƒ¼ãƒˆã®é«˜ã•ãŒã“ã“ã§æ±ºã‚打ã¡ã•れãŸã‚‚ã®ã«å¤‰åŒ–ã—ã€è¡¨ç¤ºã—ã¦ã„るノートã®ä½ç½®ãŒå¤‰ã‚ã£ã¦ã—ã¾ã†
- // ノートãŒãƒžã‚¦ãƒ³ãƒˆã•れãŸã¨ãã«è‡ªèº«ã®é«˜ã•ã‚’å–å¾—ã— contain-intrinsic-size を設定ã—ãªãŠã›ã°ã»ã¼è§£æ±ºã§ããã†ã ãŒã€
- // 今度ã¯ãã®å‡¦ç†è‡ªä½“ãŒãƒ‘フォーマンス低下ã®åŽŸå› ã«ãªã‚‰ãªã„ã‹æ‡¸å¿µã•れる。ã¾ãŸã€è¢«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ã‚‚高ã•ã¯å¤‰åŒ–ã™ã‚‹ãŸã‚ã€ã‚„ã¯ã‚Šå¤šå°‘ã®ã‚ºãƒ¬ã¯ç”Ÿã˜ã‚‹
- // 一度レンダリングã•れãŸè¦ç´ ã¯ãƒ–ラウザãŒã‚ˆã—ãªã«ã‚µã‚¤ã‚ºã‚’覚ãˆã¦ãŠã„ã¦ãれるよã†ãªå®Ÿè£…ã«ãªã‚‹ã¾ã§å¾…ã£ãŸæ–¹ãŒè‰¯ã•ãã†(ãªã‚‹ã®ã‹ï¼Ÿ)
- //content-visibility: auto;
- //contain-intrinsic-size: 0 128px;
+ content-visibility: auto;
+ contain-intrinsic-size: 0 150px;
&:focus-visible {
outline: none;
@@ -190,8 +184,8 @@ const isDeleted = ref(false);
margin: auto;
width: calc(100% - 8px);
height: calc(100% - 8px);
- border: dashed 2px var(--focus);
- border-radius: var(--radius);
+ border: dashed 2px var(--MI_THEME-focus);
+ border-radius: var(--MI-radius);
box-sizing: border-box;
}
}
@@ -213,9 +207,9 @@ const isDeleted = ref(false);
right: 12px;
padding: 0 4px;
margin-bottom: 0 !important;
- background: var(--popup);
+ background: var(--MI_THEME-popup);
border-radius: 8px;
- box-shadow: 0px 4px 32px var(--shadow);
+ box-shadow: 0px 4px 32px var(--MI_THEME-shadow);
}
.footerButton {
@@ -260,7 +254,7 @@ const isDeleted = ref(false);
padding: 16px 32px 8px 32px;
line-height: 28px;
white-space: pre;
- color: var(--renote);
+ color: var(--MI_THEME-renote);
& + .article {
padding-top: 8px;
@@ -357,7 +351,7 @@ const isDeleted = ref(false);
width: 58px;
height: 58px;
position: sticky !important;
- top: calc(22px + var(--stickyTop, 0px));
+ top: calc(22px + var(--MI-stickyTop, 0px));
left: 0;
}
@@ -378,12 +372,12 @@ const isDeleted = ref(false);
width: 100%;
margin-top: 14px;
position: sticky;
- bottom: calc(var(--stickyBottom, 0px) + 14px);
+ bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
}
.showLessLabel {
display: inline-block;
- background: var(--popup);
+ background: var(--MI_THEME-popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
@@ -404,16 +398,16 @@ const isDeleted = ref(false);
z-index: 2;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+ background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
&:hover > .collapsedLabel {
- background: var(--panelHighlight);
+ background: var(--MI_THEME-panelHighlight);
}
}
.collapsedLabel {
display: inline-block;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
@@ -425,13 +419,13 @@ const isDeleted = ref(false);
}
.replyIcon {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
margin-right: 0.5em;
}
.translation {
- border: solid 0.5px var(--divider);
- border-radius: var(--radius);
+ border: solid 0.5px var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
padding: 12px;
margin-top: 8px;
}
@@ -450,7 +444,7 @@ const isDeleted = ref(false);
.quoteNote {
padding: 16px;
- border: dashed 1px var(--renote);
+ border: dashed 1px var(--MI_THEME-renote);
border-radius: 8px;
overflow: clip;
}
@@ -474,7 +468,7 @@ const isDeleted = ref(false);
}
&:hover {
- color: var(--fgHighlighted);
+ color: var(--MI_THEME-fgHighlighted);
}
}
@@ -551,7 +545,7 @@ const isDeleted = ref(false);
margin: 0 10px 0 0;
width: 46px;
height: 46px;
- top: calc(14px + var(--stickyTop, 0px));
+ top: calc(14px + var(--MI-stickyTop, 0px));
}
}
diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue
index 0068e05bc4..c4ea9b4f2e 100644
--- a/packages/frontend-embed/src/components/EmNoteDetailed.vue
+++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue
@@ -196,7 +196,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
padding: 16px 32px 8px 32px;
line-height: 28px;
white-space: pre;
- color: var(--renote);
+ color: var(--MI_THEME-renote);
}
.renoteAvatar {
@@ -282,7 +282,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
padding: 4px 6px;
font-size: 80%;
line-height: 1;
- border: solid 0.5px var(--divider);
+ border: solid 0.5px var(--MI_THEME-divider);
border-radius: 4px;
}
@@ -324,14 +324,14 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
}
.noteReplyTarget {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
margin-right: 0.5em;
}
.rn {
margin-left: 4px;
font-style: oblique;
- color: var(--renote);
+ color: var(--MI_THEME-renote);
}
.reactionOmitted {
@@ -351,7 +351,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
.quoteNote {
padding: 16px;
- border: dashed 1px var(--renote);
+ border: dashed 1px var(--MI_THEME-renote);
border-radius: 8px;
overflow: clip;
}
@@ -365,12 +365,12 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
width: 100%;
margin-top: 14px;
position: sticky;
- bottom: calc(var(--stickyBottom, 0px) + 14px);
+ bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
}
.showLessLabel {
display: inline-block;
- background: var(--popup);
+ background: var(--MI_THEME-popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
@@ -391,16 +391,16 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
z-index: 2;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), var(--X15));
+ background: linear-gradient(0deg, var(--MI_THEME-panel), var(--MI_THEME-X15));
&:hover > .collapsedLabel {
- background: var(--panelHighlight);
+ background: var(--MI_THEME-panelHighlight);
}
}
.collapsedLabel {
display: inline-block;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
@@ -423,7 +423,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
}
&:hover {
- color: var(--fgHighlighted);
+ color: var(--MI_THEME-fgHighlighted);
}
}
@@ -439,7 +439,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong);
opacity: 0.7;
&.reacted {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
diff --git a/packages/frontend-embed/src/components/EmNoteHeader.vue b/packages/frontend-embed/src/components/EmNoteHeader.vue
index 7d0b9bacad..85b4aac071 100644
--- a/packages/frontend-embed/src/components/EmNoteHeader.vue
+++ b/packages/frontend-embed/src/components/EmNoteHeader.vue
@@ -72,7 +72,7 @@ defineProps<{
margin: 0 .5em 0 0;
padding: 1px 6px;
font-size: 80%;
- border: solid 0.5px var(--divider);
+ border: solid 0.5px var(--MI_THEME-divider);
border-radius: 3px;
}
diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue
index d4afc0b4d7..83e73f9870 100644
--- a/packages/frontend-embed/src/components/EmNoteSimple.vue
+++ b/packages/frontend-embed/src/components/EmNoteSimple.vue
@@ -53,7 +53,7 @@ const showContent = ref(false);
height: 34px;
border-radius: 8px;
position: sticky !important;
- top: calc(16px + var(--stickyTop, 0px));
+ top: calc(16px + var(--MI-stickyTop, 0px));
left: 0;
}
diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue
index 9fbd1eae35..cc379e8281 100644
--- a/packages/frontend-embed/src/components/EmNoteSub.vue
+++ b/packages/frontend-embed/src/components/EmNoteSub.vue
@@ -123,7 +123,7 @@ if (props.detail) {
}
.reply, .more {
- border-left: solid 0.5px var(--divider);
+ border-left: solid 0.5px var(--MI_THEME-divider);
margin-top: 10px;
}
@@ -144,7 +144,7 @@ if (props.detail) {
.muted {
text-align: center;
padding: 8px !important;
- border: 1px solid var(--divider);
+ border: 1px solid var(--MI_THEME-divider);
margin: 8px 8px 0 8px;
border-radius: 8px;
}
diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue
index 3418d97f77..4211261e19 100644
--- a/packages/frontend-embed/src/components/EmNotes.vue
+++ b/packages/frontend-embed/src/components/EmNotes.vue
@@ -43,10 +43,10 @@ defineExpose({
<style lang="scss" module>
.root {
- background: var(--panel);
+ background: var(--MI_THEME-panel);
}
.note {
- border-bottom: 0.5px solid var(--divider);
+ border-bottom: 0.5px solid var(--MI_THEME-divider);
}
</style>
diff --git a/packages/frontend-embed/src/components/EmPoll.vue b/packages/frontend-embed/src/components/EmPoll.vue
index a2b1203449..d197e094c6 100644
--- a/packages/frontend-embed/src/components/EmPoll.vue
+++ b/packages/frontend-embed/src/components/EmPoll.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<li v-for="(choice, i) in poll.choices" :key="i" :class="$style.choice">
<div :class="$style.bg" :style="{ 'width': `${choice.votes / total * 100}%` }"></div>
<span :class="$style.fg">
- <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
+ <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
<EmMfm :text="choice.text" :plain="true"/>
<span style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
</span>
@@ -52,8 +52,8 @@ const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
position: relative;
margin: 4px 0;
padding: 4px;
- //border: solid 0.5px var(--divider);
- background: var(--accentedBg);
+ //border: solid 0.5px var(--MI_THEME-divider);
+ background: var(--MI_THEME-accentedBg);
border-radius: 4px;
overflow: clip;
}
@@ -63,8 +63,8 @@ const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
top: 0;
left: 0;
height: 100%;
- background: var(--accent);
- background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
+ background: var(--MI_THEME-accent);
+ background: linear-gradient(90deg,var(--MI_THEME-buttonGradateA),var(--MI_THEME-buttonGradateB));
transition: width 1s ease;
}
@@ -72,11 +72,11 @@ const total = computed(() => sum(props.poll.choices.map(x => x.votes)));
position: relative;
display: inline-block;
padding: 3px 5px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: 3px;
}
.info {
- color: var(--fg);
+ color: var(--MI_THEME-fg);
}
</style>
diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue
index 2e43eb8d17..2ebff489fd 100644
--- a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue
+++ b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue
@@ -38,7 +38,7 @@ const props = defineProps<{
justify-content: center;
&.canToggle {
- background: var(--buttonBg);
+ background: var(--MI_THEME-buttonBg);
&:hover {
background: rgba(0, 0, 0, 0.1);
@@ -72,12 +72,12 @@ const props = defineProps<{
}
&.reacted, &.reacted:hover {
- background: var(--accentedBg);
- color: var(--accent);
- box-shadow: 0 0 0 1px var(--accent) inset;
+ background: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
+ box-shadow: 0 0 0 1px var(--MI_THEME-accent) inset;
> .count {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
> .icon {
diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue
index f433573df5..e9acbcb293 100644
--- a/packages/frontend-embed/src/components/EmSubNoteContent.vue
+++ b/packages/frontend-embed/src/components/EmSubNoteContent.vue
@@ -65,11 +65,11 @@ const collapsed = ref(isLong);
left: 0;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+ background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
@@ -78,7 +78,7 @@ const collapsed = ref(isLong);
&:hover {
> .fadeLabel {
- background: var(--panelHighlight);
+ background: var(--MI_THEME-panelHighlight);
}
}
}
@@ -87,25 +87,25 @@ const collapsed = ref(isLong);
.reply {
margin-right: 6px;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
.rp {
margin-left: 4px;
font-style: oblique;
- color: var(--renote);
+ color: var(--MI_THEME-renote);
}
.showLess {
width: 100%;
margin-top: 14px;
position: sticky;
- bottom: calc(var(--stickyBottom, 0px) + 14px);
+ bottom: calc(var(--MI-stickyBottom, 0px) + 14px);
}
.showLessLabel {
display: inline-block;
- background: var(--popup);
+ background: var(--MI_THEME-popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: 999px;
diff --git a/packages/frontend-embed/src/components/EmTime.vue b/packages/frontend-embed/src/components/EmTime.vue
index c3986f7d70..7902e18483 100644
--- a/packages/frontend-embed/src/components/EmTime.vue
+++ b/packages/frontend-embed/src/components/EmTime.vue
@@ -98,10 +98,10 @@ if (!invalid && props.origin === null && (props.mode === 'relative' || props.mod
<style lang="scss" module>
.old1 {
- color: var(--warn);
+ color: var(--MI_THEME-warn);
}
.old1.old2 {
- color: var(--error);
+ color: var(--MI_THEME-error);
}
</style>
diff --git a/packages/frontend-embed/src/components/EmTimelineContainer.vue b/packages/frontend-embed/src/components/EmTimelineContainer.vue
index 6c30b1102d..60fd67ced9 100644
--- a/packages/frontend-embed/src/components/EmTimelineContainer.vue
+++ b/packages/frontend-embed/src/components/EmTimelineContainer.vue
@@ -20,7 +20,7 @@ withDefaults(defineProps<{
<style module lang="scss">
.timelineRoot {
- background-color: var(--panel);
+ background-color: var(--MI_THEME-panel);
height: 100%;
max-height: var(--embedMaxHeight, none);
display: flex;
@@ -29,7 +29,7 @@ withDefaults(defineProps<{
.header {
flex-shrink: 0;
- border-bottom: 1px solid var(--divider);
+ border-bottom: 1px solid var(--MI_THEME-divider);
}
.body {
diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue
index 2528dc4b80..f4d4e8cf6f 100644
--- a/packages/frontend-embed/src/pages/clip.vue
+++ b/packages/frontend-embed/src/pages/clip.vue
@@ -100,7 +100,7 @@ function top(ev: MouseEvent) {
display: flex;
min-width: 0;
align-items: center;
- gap: var(--margin);
+ gap: var(--MI-margin);
overflow: hidden;
.headerClipIconRoot {
@@ -110,8 +110,8 @@ function top(ev: MouseEvent) {
line-height: 32px;
font-size: 14px;
text-align: center;
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
border-radius: 50%;
}
diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue
index 6f6c8c0f63..e879430286 100644
--- a/packages/frontend-embed/src/pages/note.vue
+++ b/packages/frontend-embed/src/pages/note.vue
@@ -47,6 +47,6 @@ if (note.value?.url != null || note.value?.uri != null) {
<style lang="scss" module>
.noteEmbedRoot {
- background-color: var(--panel);
+ background-color: var(--MI_THEME-panel);
}
</style>
diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue
index b481b3ebe5..4b00ae7c2d 100644
--- a/packages/frontend-embed/src/pages/tag.vue
+++ b/packages/frontend-embed/src/pages/tag.vue
@@ -83,7 +83,7 @@ function top(ev: MouseEvent) {
display: flex;
min-width: 0;
align-items: center;
- gap: var(--margin);
+ gap: var(--MI-margin);
overflow: hidden;
.headerClipIconRoot {
@@ -93,8 +93,8 @@ function top(ev: MouseEvent) {
line-height: 32px;
font-size: 14px;
text-align: center;
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
border-radius: 50%;
}
diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue
index 85e6f52d50..348b1a7622 100644
--- a/packages/frontend-embed/src/pages/user-timeline.vue
+++ b/packages/frontend-embed/src/pages/user-timeline.vue
@@ -117,7 +117,7 @@ function top(ev: MouseEvent) {
display: flex;
min-width: 0;
align-items: center;
- gap: var(--margin);
+ gap: var(--MI-margin);
overflow: hidden;
.avatarLink {
diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss
index 979e959f52..6097281c37 100644
--- a/packages/frontend-embed/src/style.scss
+++ b/packages/frontend-embed/src/style.scss
@@ -7,18 +7,18 @@
*/
:root {
- --radius: 12px;
- --marginFull: 14px;
- --marginHalf: 10px;
+ --MI-radius: 12px;
+ --MI-marginFull: 14px;
+ --MI-marginHalf: 10px;
- --margin: var(--marginFull);
+ --MI-margin: var(--MI-marginFull);
}
html {
background-color: transparent;
color-scheme: light dark;
- color: var(--fg);
- accent-color: var(--accent);
+ color: var(--MI_THEME-fg);
+ accent-color: var(--MI_THEME-accent);
overflow: clip;
overflow-wrap: break-word;
font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
@@ -29,7 +29,7 @@ html {
-webkit-text-size-adjust: 100%;
&, * {
- scrollbar-color: var(--scrollbarHandle) transparent;
+ scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
scrollbar-width: thin;
&::-webkit-scrollbar {
@@ -42,14 +42,14 @@ html {
}
&::-webkit-scrollbar-thumb {
- background: var(--scrollbarHandle);
+ background: var(--MI_THEME-scrollbarHandle);
&:hover {
- background: var(--scrollbarHandleHover);
+ background: var(--MI_THEME-scrollbarHandleHover);
}
&:active {
- background: var(--accent);
+ background: var(--MI_THEME-accent);
}
}
}
@@ -93,7 +93,7 @@ rt {
}
:focus-visible {
- outline: var(--focus) solid 2px;
+ outline: var(--MI_THEME-focus) solid 2px;
outline-offset: -2px;
&:hover {
@@ -151,38 +151,38 @@ rt {
._buttonGray {
@extend ._button;
- background: var(--buttonBg);
+ background: var(--MI_THEME-buttonBg);
&:not(:disabled):hover {
- background: var(--buttonHoverBg);
+ background: var(--MI_THEME-buttonHoverBg);
}
}
._buttonPrimary {
@extend ._button;
- color: var(--fgOnAccent);
- background: var(--accent);
+ color: var(--MI_THEME-fgOnAccent);
+ background: var(--MI_THEME-accent);
&:not(:disabled):hover {
- background: hsl(from var(--accent) h s calc(l + 5));
+ background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
}
&:not(:disabled):active {
- background: hsl(from var(--accent) h s calc(l - 5));
+ background: hsl(from var(--MI_THEME-accent) h s calc(l - 5));
}
}
._buttonGradate {
@extend ._buttonPrimary;
- color: var(--fgOnAccent);
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ color: var(--MI_THEME-fgOnAccent);
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
&:not(:disabled):hover {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
&:not(:disabled):active {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
@@ -199,13 +199,13 @@ rt {
}
._help {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
cursor: help;
}
._textButton {
@extend ._button;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
&:focus-visible {
outline-offset: 2px;
@@ -217,13 +217,13 @@ rt {
}
._panel {
- background: var(--panel);
- border-radius: var(--radius);
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
overflow: clip;
}
._margin {
- margin: var(--margin) 0;
+ margin: var(--MI-margin) 0;
}
._gaps_m {
@@ -241,7 +241,7 @@ rt {
._gaps {
display: flex;
flex-direction: column;
- gap: var(--margin);
+ gap: var(--MI-margin);
}
._buttons {
@@ -263,24 +263,24 @@ rt {
padding: 10px;
box-sizing: border-box;
text-align: center;
- border: solid 0.5px var(--divider);
- border-radius: var(--radius);
+ border: solid 0.5px var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
&:active {
- border-color: var(--accent);
+ border-color: var(--MI_THEME-accent);
}
}
._popup {
- background: var(--popup);
- border-radius: var(--radius);
+ background: var(--MI_THEME-popup);
+ border-radius: var(--MI-radius);
contain: content;
}
._acrylic {
- background: var(--acrylicPanel);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ background: var(--MI_THEME-acrylicPanel);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
._fullinfo {
@@ -296,7 +296,7 @@ rt {
}
._link {
- color: var(--link);
+ color: var(--MI_THEME-link);
}
._caption {
diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts
index 23e70cd0d3..4664ad4880 100644
--- a/packages/frontend-embed/src/theme.ts
+++ b/packages/frontend-embed/src/theme.ts
@@ -61,7 +61,7 @@ export function applyTheme(theme: Theme, persist = true) {
}
for (const [k, v] of Object.entries(props)) {
- document.documentElement.style.setProperty(`--${k}`, v.toString());
+ document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
}
// iframeを正常ã«é€éŽã•ã›ã‚‹ãŸã‚ã«ã€cssã®color-scheme㯠`light dark;` 固定ã«ã—ã¦ã‚る。style.scsså‚ç…§
diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue
index 8da5f46a96..4ba5968a91 100644
--- a/packages/frontend-embed/src/ui.vue
+++ b/packages/frontend-embed/src/ui.vue
@@ -88,14 +88,14 @@ onUnmounted(() => {
<style lang="scss" module>
.rootForEmbedPage {
box-sizing: border-box;
- border: 1px solid var(--divider);
- background-color: var(--bg);
+ border: 1px solid var(--MI_THEME-divider);
+ background-color: var(--MI_THEME-bg);
overflow: hidden;
position: relative;
height: auto;
&.rounded {
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
}
&.noBorder {
diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts
index b95533c2cd..bf6f558893 100644
--- a/packages/frontend-embed/vite.config.ts
+++ b/packages/frontend-embed/vite.config.ts
@@ -90,6 +90,11 @@ export function getConfig(): UserConfig {
return shortId + '-' + toBase62(hash(id)).substring(0, 4);
},
},
+ preprocessorOptions: {
+ scss: {
+ api: 'modern-compiler',
+ },
+ },
},
define: {
diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts
index 8236cc9a4d..0e100d574b 100644
--- a/packages/frontend-shared/js/const.ts
+++ b/packages/frontend-shared/js/const.ts
@@ -119,6 +119,7 @@ export const notificationTypes = [
'roleAssigned',
'achievementEarned',
'exportCompleted',
+ 'login',
'test',
'app',
'edited',
diff --git a/packages/frontend-shared/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5
index e4649311c3..b3f1ab824b 100644
--- a/packages/frontend-shared/themes/_dark.json5
+++ b/packages/frontend-shared/themes/_dark.json5
@@ -30,7 +30,7 @@
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
- panelBorder: '" solid 1px var(--divider)',
+ panelBorder: '" solid 1px var(--MI_THEME-divider)',
thread: ':lighten<12<@panel',
acrylicPanel: ':alpha<0.5<@panel',
windowHeader: ':alpha<0.85<@panel',
@@ -68,7 +68,6 @@
switchOnFg: '@accent',
inputBorder: 'rgba(255, 255, 255, 0.1)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
- listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
driveFolderBg: ':alpha<0.3<@accent',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
badge: '#31b1ce',
diff --git a/packages/frontend-shared/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5
index b6218a5f1d..89c632b057 100644
--- a/packages/frontend-shared/themes/_light.json5
+++ b/packages/frontend-shared/themes/_light.json5
@@ -30,7 +30,7 @@
panelHeaderBg: ':lighten<3<@panel',
panelHeaderFg: '@fg',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
- panelBorder: '" solid 1px var(--divider)',
+ panelBorder: '" solid 1px var(--MI_THEME-divider)',
thread: ':darken<12<@panel',
acrylicPanel: ':alpha<0.5<@panel',
windowHeader: ':alpha<0.85<@panel',
@@ -68,7 +68,6 @@
switchOnFg: '@fgOnAccent',
inputBorder: 'rgba(0, 0, 0, 0.1)',
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
- listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
driveFolderBg: ':alpha<0.3<@accent',
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
badge: '#31b1ce',
diff --git a/packages/frontend-shared/themes/d-astro.json5 b/packages/frontend-shared/themes/d-astro.json5
index a674a5c5c9..4422526a33 100644
--- a/packages/frontend-shared/themes/d-astro.json5
+++ b/packages/frontend-shared/themes/d-astro.json5
@@ -36,7 +36,7 @@
dateLabelFg: '@fg',
inputBorder: 'rgba(255, 255, 255, 0.1)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
- panelBorder: '" solid 1px var(--divider)',
+ panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
@@ -50,7 +50,6 @@
htmlThemeColor: '@bg',
fgOnWhite: '@accent',
panelHighlight: ':lighten<3<@panel',
- listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
panelHeaderDivider: 'rgba(0, 0, 0, 0)',
diff --git a/packages/frontend-shared/themes/d-u0.json5 b/packages/frontend-shared/themes/d-u0.json5
index 32ac9ec5cf..fb707c74c3 100644
--- a/packages/frontend-shared/themes/d-u0.json5
+++ b/packages/frontend-shared/themes/d-u0.json5
@@ -55,7 +55,7 @@
codeBoolean: '#c59eff',
dateLabelFg: '@fg',
inputBorder: 'rgba(255, 255, 255, 0.1)',
- panelBorder: '" solid 1px var(--divider)',
+ panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@indicator',
@@ -69,7 +69,6 @@
buttonGradateB: ':hue<20<@accent',
htmlThemeColor: '@bg',
panelHighlight: ':lighten<3<@panel',
- listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: 'rgba(255, 255, 255, 0.2)',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
diff --git a/packages/frontend-shared/themes/l-u0.json5 b/packages/frontend-shared/themes/l-u0.json5
index 0b952b003a..7062e7fe5b 100644
--- a/packages/frontend-shared/themes/l-u0.json5
+++ b/packages/frontend-shared/themes/l-u0.json5
@@ -56,7 +56,7 @@
codeBoolean: '#c59eff',
dateLabelFg: '@fg',
inputBorder: 'rgba(255, 255, 255, 0.1)',
- panelBorder: '" solid 1px var(--divider)',
+ panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@indicator',
@@ -71,7 +71,6 @@
buttonGradateB: ':hue<20<@accent',
htmlThemeColor: '@bg',
panelHighlight: ':lighten<3<@panel',
- listItemHoverBg: 'rgba(255, 255, 255, 0.03)',
scrollbarHandle: '#74747433',
inputBorderHover: 'rgba(255, 255, 255, 0.2)',
wallpaperOverlay: 'rgba(0, 0, 0, 0.5)',
diff --git a/packages/frontend-shared/themes/l-vivid.json5 b/packages/frontend-shared/themes/l-vivid.json5
index f1c63dde6e..39768d4ac6 100644
--- a/packages/frontend-shared/themes/l-vivid.json5
+++ b/packages/frontend-shared/themes/l-vivid.json5
@@ -39,7 +39,7 @@
dateLabelFg: '@fg',
inputBorder: 'rgba(0, 0, 0, 0.1)',
inputBorderHover: 'rgba(0, 0, 0, 0.2)',
- panelBorder: '" solid 1px var(--divider)',
+ panelBorder: '" solid 1px var(--MI_THEME-divider)',
accentDarken: ':darken<10<@accent',
acrylicPanel: ':alpha<0.5<@panel',
navIndicator: '@accent',
@@ -52,7 +52,6 @@
panelHeaderFg: '@fg',
htmlThemeColor: '@bg',
panelHighlight: ':darken<3<@panel',
- listItemHoverBg: 'rgba(0, 0, 0, 0.03)',
scrollbarHandle: 'rgba(0, 0, 0, 0.2)',
wallpaperOverlay: 'rgba(255, 255, 255, 0.5)',
fgTransparentWeak: ':alpha<0.75<@fg',
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index 42d1a10f0a..f2bdc631d2 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -397,7 +397,18 @@ function toStories(component: string): Promise<string> {
const globs = await Promise.all([
glob('src/components/global/Mk*.vue'),
glob('src/components/global/RouterView.vue'),
- glob('src/components/Mk[A-E]*.vue'),
+ glob('src/components/MkAbuseReportWindow.vue'),
+ glob('src/components/MkAccountMoved.vue'),
+ glob('src/components/MkAchievements.vue'),
+ glob('src/components/MkAnalogClock.vue'),
+ glob('src/components/MkAnimBg.vue'),
+ glob('src/components/MkAnnouncementDialog.vue'),
+ glob('src/components/MkAntennaEditor.vue'),
+ glob('src/components/MkAntennaEditorDialog.vue'),
+ glob('src/components/MkAsUi.vue'),
+ glob('src/components/MkAutocomplete.vue'),
+ glob('src/components/MkAvatars.vue'),
+ glob('src/components/Mk[B-E]*.vue'),
glob('src/components/MkFlashPreview.vue'),
glob('src/components/MkGalleryPostPreview.vue'),
glob('src/components/MkSignupServerRules.vue'),
diff --git a/packages/frontend/assets/testcaptcha.png b/packages/frontend/assets/testcaptcha.png
new file mode 100644
index 0000000000..9bfd252b51
--- /dev/null
+++ b/packages/frontend/assets/testcaptcha.png
Binary files differ
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 1b4b5d5bd1..c60ae82b7a 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -29,7 +29,7 @@
"@syuilo/aiscript": "0.19.0",
"@twemoji/parser": "15.1.1",
"@vitejs/plugin-vue": "5.1.4",
- "@vue/compiler-sfc": "3.5.10",
+ "@vue/compiler-sfc": "3.5.11",
"aiscript-vscode": "github:aiscript-dev/aiscript-vscode#v0.1.11",
"astring": "1.9.0",
"broadcast-channel": "7.0.0",
@@ -40,12 +40,13 @@
"chartjs-chart-matrix": "2.0.1",
"chartjs-plugin-gradient": "0.6.1",
"chartjs-plugin-zoom": "2.0.1",
- "chromatic": "11.10.4",
+ "chromatic": "11.11.0",
"compare-versions": "6.1.1",
"cropperjs": "2.0.0-rc.2",
"date-fns": "2.30.0",
"estree-walker": "3.0.3",
"eventemitter3": "5.0.1",
+ "frontend-shared": "workspace:*",
"idb-keyval": "6.2.1",
"insert-text-at-cursor": "0.3.0",
"is-file-animated": "1.0.2",
@@ -55,13 +56,12 @@
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
- "frontend-shared": "workspace:*",
"photoswipe": "5.4.4",
"punycode": "2.3.1",
"rollup": "4.22.5",
- "sanitize-html": "2.13.0",
+ "sanitize-html": "2.13.1",
"sass": "1.79.3",
- "shiki": "1.12.0",
+ "shiki": "1.21.0",
"strict-event-emitter-types": "2.0.0",
"textarea-caret": "3.1.0",
"three": "0.169.0",
@@ -73,30 +73,31 @@
"uuid": "10.0.0",
"v-code-diff": "1.13.1",
"vite": "5.4.8",
- "vue": "3.5.10",
+ "vue": "3.5.11",
"vuedraggable": "next"
},
"devDependencies": {
"@misskey-dev/summaly": "5.1.0",
- "@storybook/addon-actions": "8.3.3",
- "@storybook/addon-essentials": "8.3.3",
- "@storybook/addon-interactions": "8.3.3",
- "@storybook/addon-links": "8.3.3",
- "@storybook/addon-mdx-gfm": "8.3.3",
- "@storybook/addon-storysource": "8.3.3",
- "@storybook/blocks": "8.3.3",
- "@storybook/components": "8.3.3",
- "@storybook/core-events": "8.3.3",
- "@storybook/manager-api": "8.3.3",
- "@storybook/preview-api": "8.3.3",
- "@storybook/react": "8.3.3",
- "@storybook/react-vite": "8.3.3",
- "@storybook/test": "8.3.3",
- "@storybook/theming": "8.3.3",
- "@storybook/types": "8.3.3",
- "@storybook/vue3": "8.3.3",
- "@storybook/vue3-vite": "8.3.3",
+ "@storybook/addon-actions": "8.3.4",
+ "@storybook/addon-essentials": "8.3.4",
+ "@storybook/addon-interactions": "8.3.4",
+ "@storybook/addon-links": "8.3.4",
+ "@storybook/addon-mdx-gfm": "8.3.4",
+ "@storybook/addon-storysource": "8.3.4",
+ "@storybook/blocks": "8.3.4",
+ "@storybook/components": "8.3.4",
+ "@storybook/core-events": "8.3.4",
+ "@storybook/manager-api": "8.3.4",
+ "@storybook/preview-api": "8.3.4",
+ "@storybook/react": "8.3.4",
+ "@storybook/react-vite": "8.3.4",
+ "@storybook/test": "8.3.4",
+ "@storybook/theming": "8.3.4",
+ "@storybook/types": "8.3.4",
+ "@storybook/vue3": "8.3.4",
+ "@storybook/vue3-vite": "8.3.4",
"@testing-library/vue": "8.1.0",
+ "@types/canvas-confetti": "^1.6.4",
"@types/estree": "1.0.6",
"@types/katex": "^0.16.7",
"@types/matter-js": "0.19.7",
@@ -112,11 +113,11 @@
"@typescript-eslint/eslint-plugin": "7.17.0",
"@typescript-eslint/parser": "7.17.0",
"@vitest/coverage-v8": "1.6.0",
- "@vue/runtime-core": "3.5.10",
+ "@vue/runtime-core": "3.5.11",
"acorn": "8.12.1",
"cross-env": "7.0.3",
"cypress": "13.15.0",
- "eslint-plugin-import": "2.30.0",
+ "eslint-plugin-import": "2.31.0",
"eslint-plugin-vue": "9.28.0",
"fast-glob": "3.3.2",
"happy-dom": "10.0.3",
@@ -130,7 +131,7 @@
"react-dom": "18.3.1",
"seedrandom": "3.0.5",
"start-server-and-test": "2.0.8",
- "storybook": "8.3.3",
+ "storybook": "8.3.4",
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
"vite-plugin-turbosnap": "1.0.3",
"vitest": "1.6.0",
diff --git a/packages/frontend/src/_dev_boot_.ts b/packages/frontend/src/_dev_boot_.ts
index 1601f247d7..f312765dcf 100644
--- a/packages/frontend/src/_dev_boot_.ts
+++ b/packages/frontend/src/_dev_boot_.ts
@@ -43,7 +43,7 @@ async function main() {
const theme = localStorage.getItem('theme');
if (theme) {
for (const [k, v] of Object.entries(JSON.parse(theme))) {
- document.documentElement.style.setProperty(`--${k}`, v.toString());
+ document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
// HTMLã® theme-color é©ç”¨
if (k === 'htmlThemeColor') {
diff --git a/packages/frontend/src/account.ts b/packages/frontend/src/account.ts
index e3416f2c29..995578dece 100644
--- a/packages/frontend/src/account.ts
+++ b/packages/frontend/src/account.ts
@@ -227,7 +227,7 @@ export async function openAccountMenu(opts: {
function showSigninDialog() {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
- done: res => {
+ done: (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
addAccount(res.id, res.i);
success();
},
@@ -237,9 +237,9 @@ export async function openAccountMenu(opts: {
function createAccount() {
const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
- done: res => {
- addAccount(res.id, res.i);
- switchAccountWithToken(res.i);
+ done: (res: Misskey.entities.SignupResponse) => {
+ addAccount(res.id, res.token);
+ switchAccountWithToken(res.token);
},
closed: () => dispose(),
});
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index af8bbf57d2..6102f88ae5 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -183,24 +183,18 @@ export async function common(createVue: () => App<Element>) {
if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
defaultStore.set('themeInitial', false);
- } else {
- if (defaultStore.state.darkMode) {
- applyTheme(darkTheme.value);
- } else {
- applyTheme(lightTheme.value);
- }
}
});
watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
- document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none');
+ document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
}, { immediate: true });
watch(defaultStore.reactiveState.useBlurEffect, v => {
if (v) {
- document.documentElement.style.removeProperty('--blur');
+ document.documentElement.style.removeProperty('--MI-blur');
} else {
- document.documentElement.style.setProperty('--blur', 'none');
+ document.documentElement.style.setProperty('--MI-blur', 'none');
}
}, { immediate: true });
diff --git a/packages/frontend/src/components/MkAbuseReport.vue b/packages/frontend/src/components/MkAbuseReport.vue
index d13eedaade..4d6757a09f 100644
--- a/packages/frontend/src/components/MkAbuseReport.vue
+++ b/packages/frontend/src/components/MkAbuseReport.vue
@@ -4,141 +4,153 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
- <div class="bcekxzvu _margin _panel">
- <div class="target">
- <MkA v-user-preview="report.targetUserId" class="info" :to="`/admin/user/${report.targetUserId}`" :behavior="'window'">
- <MkAvatar class="avatar" :user="report.targetUser" indicator/>
- <div class="names">
- <MkUserName class="name" :user="report.targetUser"/>
- <MkAcct class="acct" :user="report.targetUser" style="display: block;"/>
- </div>
- </MkA>
- <div class="keyvalCtn">
- <MkKeyValue>
- <template #key>{{ i18n.ts.registeredDate }}</template>
- <template #value>{{ dateString(report.targetUser.createdAt) }} (<MkTime :time="report.targetUser.createdAt"/>)</template>
- </MkKeyValue>
- <MkKeyValue>
- <template #key>{{ i18n.ts.reporter }}</template>
- <template #value><MkA :to="`/admin/user/${report.reporter.id}`" class="_link" :behavior="'window'">@{{ report.reporter.username }}</MkA></template>
- </MkKeyValue>
- <MkKeyValue>
- <template #key>{{ i18n.ts.createdAt }}</template>
- <template #value><MkTime :time="report.createdAt" mode="absolute"/> (<MkTime :time="report.createdAt" mode="relative"/>)</template>
- </MkKeyValue>
- </div>
- <hr>
+<MkFolder>
+ <template #icon>
+ <i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--MI_THEME-success)"></i>
+ <i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--MI_THEME-error)"></i>
+ <i v-else-if="report.resolved" class="ti ti-slash"></i>
+ <i v-else class="ti ti-exclamation-circle" style="color: var(--MI_THEME-warn)"></i>
+ </template>
+ <template #label><MkAcct :user="report.targetUser"/> (by <MkAcct :user="report.reporter"/>)</template>
+ <template #caption>{{ report.comment }}</template>
+ <template #suffix><MkTime :time="report.createdAt"/></template>
+ <template #footer>
+ <div class="_buttons">
+ <template v-if="!report.resolved">
+ <MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--MI_THEME-success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton>
+ <MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--MI_THEME-error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton>
+ <MkButton @click="resolve(null)"><i class="ti ti-slash"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts.other }})</MkButton>
+ </template>
+ <template v-if="report.targetUser.host != null">
+ <MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton>
+ <div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div>
+ </template>
+ <button class="_button" style="margin-left: auto; width: 34px;" @click="showMenu"><i class="ti ti-dots"></i></button>
</div>
- <div class="detail">
- <div>
+ </template>
+
+ <div :class="$style.root" class="_gaps_s">
+ <MkFolder :withSpacer="false">
+ <template #icon><MkAvatar :user="report.targetUser" style="width: 18px; height: 18px;"/></template>
+ <template #label>{{ i18n.ts.target }}: <MkAcct :user="report.targetUser"/></template>
+ <template #suffix>#{{ report.targetUserId.toUpperCase() }}</template>
+
+ <div style="container-type: inline-size;">
+ <RouterView :router="targetRouter"/>
+ </div>
+ </MkFolder>
+
+ <MkFolder :defaultOpen="true">
+ <template #icon><i class="ti ti-message-2"></i></template>
+ <template #label>{{ i18n.ts.details }}</template>
+ <div class="_gaps_s">
<Mfm :text="report.comment" :isBlock="true" :linkNavigationBehavior="'window'"/>
</div>
- <hr/>
- <div v-if="report.assignee" class="assignee">
- {{ i18n.ts.moderator }}:
- <MkA :to="`/admin/user/${report.assignee.id}`" class="_link" :behavior="'window'">@{{ report.assignee.username }}</MkA>
+ </MkFolder>
+
+ <MkFolder :withSpacer="false">
+ <template #icon><MkAvatar :user="report.reporter" style="width: 18px; height: 18px;"/></template>
+ <template #label>{{ i18n.ts.reporter }}: <MkAcct :user="report.reporter"/></template>
+ <template #suffix>#{{ report.reporterId.toUpperCase() }}</template>
+
+ <div style="container-type: inline-size;">
+ <RouterView :router="reporterRouter"/>
</div>
- <div class="action">
- <MkSwitch v-model="forward" c:disabled="report.targetUser.host == null || report.resolved">
- {{ i18n.ts.forwardReport }}
- <template #caption>{{ i18n.ts.forwardReportIsAnonymous }}</template>
- </MkSwitch>
- <MkButton v-if="!report.resolved" primary @click="resolve">{{ i18n.ts.abuseMarkAsResolved }}</MkButton>
+ </MkFolder>
+
+ <MkFolder :defaultOpen="false">
+ <template #icon><i class="ti ti-message-2"></i></template>
+ <template #label>{{ i18n.ts.moderationNote }}</template>
+ <template #suffix>{{ moderationNote.length > 0 ? '...' : i18n.ts.none }}</template>
+ <div class="_gaps_s">
+ <MkTextarea v-model="moderationNote" manualSave>
+ <template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
+ </MkTextarea>
</div>
+ </MkFolder>
+
+ <div v-if="report.assignee">
+ {{ i18n.ts.moderator }}:
+ <MkAcct :user="report.assignee"/>
</div>
</div>
+</MkFolder>
</template>
<script lang="ts" setup>
-import { ref } from 'vue';
+import { provide, ref, watch } from 'vue';
+import * as Misskey from 'misskey-js';
import MkButton from '@/components/MkButton.vue';
import MkSwitch from '@/components/MkSwitch.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { dateString } from '@/filters/date.js';
+import MkFolder from '@/components/MkFolder.vue';
+import RouterView from '@/components/global/RouterView.vue';
+import { useRouterFactory } from '@/router/supplier';
+import MkTextarea from '@/components/MkTextarea.vue';
+import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
const props = defineProps<{
- report: any;
+ report: Misskey.entities.AdminAbuseUserReportsResponse[number];
}>();
const emit = defineEmits<{
(ev: 'resolved', reportId: string): void;
}>();
-const forward = ref(props.report.forwarded);
+const routerFactory = useRouterFactory();
+const targetRouter = routerFactory(`/admin/user/${props.report.targetUserId}`);
+targetRouter.init();
+const reporterRouter = routerFactory(`/admin/user/${props.report.reporterId}`);
+reporterRouter.init();
+
+const moderationNote = ref(props.report.moderationNote ?? '');
+
+watch(moderationNote, async () => {
+ os.apiWithDialog('admin/update-abuse-user-report', {
+ reportId: props.report.id,
+ moderationNote: moderationNote.value,
+ }).then(() => {
+ });
+});
-function resolve() {
+function resolve(resolvedAs) {
os.apiWithDialog('admin/resolve-abuse-user-report', {
- forward: forward.value,
reportId: props.report.id,
+ resolvedAs,
}).then(() => {
emit('resolved', props.report.id);
});
}
-</script>
-
-<style lang="scss" scoped>
-.bcekxzvu {
- display: flex;
- flex-direction: column;
- transition: .1s;
-
- > .target {
- box-sizing: border-box;
- text-align: left;
- padding: 24px 24px 0px 24px;
-
- > .info {
- display: flex;
- box-sizing: border-box;
- align-items: center;
- padding: 14px;
- border-radius: var(--radius-sm);
- --c: rgb(255 196 0 / 15%);
- background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
- background-size: 16px 16px;
-
- > .avatar {
- width: 42px;
- height: 42px;
- }
-
- > .names {
- margin-left: 0.3em;
- padding: 0 8px;
- flex: 1;
-
- white-space: pre;
- overflow: hidden;
- > .name {
- font-weight: bold;
- }
- }
- }
-
- > .keyvalCtn {
- display: inline-flex;
- gap: 15px;
- margin-top: 15px;
- }
- }
+function forward() {
+ os.apiWithDialog('admin/forward-abuse-user-report', {
+ reportId: props.report.id,
+ }).then(() => {
- > .detail {
- display: flex;
- flex-direction: column;
- padding: 0px 24px 24px 24px;
+ });
+}
- .assignee {
- margin-bottom: 15px;
- }
+function showMenu(ev: MouseEvent) {
+ os.popupMenu([{
+ icon: 'ti ti-id',
+ text: 'Copy ID',
+ action: () => {
+ copyToClipboard(props.report.id);
+ },
+ }, {
+ icon: 'ti ti-json',
+ text: 'Copy JSON',
+ action: () => {
+ copyToClipboard(JSON.stringify(props.report, null, '\t'));
+ },
+ }], ev.currentTarget ?? ev.target);
+}
+</script>
- .action {
- display: flex;
- flex-direction: column;
- gap: 15px;
- }
- }
+<style lang="scss" module>
+.root {
}
</style>
diff --git a/packages/frontend/src/components/MkAccountMoved.vue b/packages/frontend/src/components/MkAccountMoved.vue
index 796524fce9..0839955d9d 100644
--- a/packages/frontend/src/components/MkAccountMoved.vue
+++ b/packages/frontend/src/components/MkAccountMoved.vue
@@ -32,9 +32,9 @@ misskeyApi('users/show', { userId: props.movedTo }).then(u => user.value = u);
.root {
padding: 16px;
font-size: 90%;
- background: var(--infoWarnBg);
- color: var(--error);
- border-radius: var(--radius);
+ background: var(--MI_THEME-infoWarnBg);
+ color: var(--MI_THEME-error);
+ border-radius: var(--MI-radius);
}
.link {
diff --git a/packages/frontend/src/components/MkAnalogClock.vue b/packages/frontend/src/components/MkAnalogClock.vue
index 835efbd6cd..c8fa6246e0 100644
--- a/packages/frontend/src/components/MkAnalogClock.vue
+++ b/packages/frontend/src/components/MkAnalogClock.vue
@@ -193,12 +193,12 @@ tick();
function calcColors() {
const computedStyle = getComputedStyle(document.documentElement);
- const dark = tinycolor(computedStyle.getPropertyValue('--bg')).isDark();
- const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
+ const dark = tinycolor(computedStyle.getPropertyValue('--MI_THEME-bg')).isDark();
+ const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
majorGraduationColor.value = dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
//minorGraduationColor = dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
sHandColor.value = dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
- mHandColor.value = tinycolor(computedStyle.getPropertyValue('--fg')).toHexString();
+ mHandColor.value = tinycolor(computedStyle.getPropertyValue('--MI_THEME-fg')).toHexString();
hHandColor.value = accent;
nowColor.value = accent;
}
diff --git a/packages/frontend/src/components/MkAnnouncementDialog.vue b/packages/frontend/src/components/MkAnnouncementDialog.vue
index c81fea175c..0e85b27ad8 100644
--- a/packages/frontend/src/components/MkAnnouncementDialog.vue
+++ b/packages/frontend/src/components/MkAnnouncementDialog.vue
@@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.header">
<span :class="$style.icon">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
- <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
- <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
- <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+ <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+ <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+ <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
</span>
<span :class="$style.title">{{ announcement.title }}</span>
</div>
@@ -83,8 +83,8 @@ onMounted(() => {
min-width: 320px;
max-width: 480px;
box-sizing: border-box;
- background: var(--panel);
- border-radius: var(--radius);
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
}
.header {
diff --git a/packages/frontend/src/components/MkAntennaEditor.vue b/packages/frontend/src/components/MkAntennaEditor.vue
index cb7ee3d6ca..2386ba6fa7 100644
--- a/packages/frontend/src/components/MkAntennaEditor.vue
+++ b/packages/frontend/src/components/MkAntennaEditor.vue
@@ -170,6 +170,6 @@ function addUser() {
.actions {
margin-top: 16px;
padding: 24px 0;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
</style>
diff --git a/packages/frontend/src/components/MkAsUi.vue b/packages/frontend/src/components/MkAsUi.vue
index e2af4f034e..13680e7d9c 100644
--- a/packages/frontend/src/components/MkAsUi.vue
+++ b/packages/frontend/src/components/MkAsUi.vue
@@ -106,7 +106,7 @@ const containerStyle = computed(() => {
const border = isBordered ? {
borderWidth: c.borderWidth ?? '1px',
- borderColor: c.borderColor ?? 'var(--divider)',
+ borderColor: c.borderColor ?? 'var(--MI_THEME-divider)',
borderStyle: c.borderStyle ?? 'solid',
} : undefined;
@@ -165,7 +165,7 @@ function openPostForm() {
}
.postForm {
- background: var(--bg);
+ background: var(--MI_THEME-bg);
border-radius: var(--radius-sm);
}
</style>
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index de5207f350..ef6b8c69e5 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -407,16 +407,16 @@ onBeforeUnmount(() => {
text-overflow: ellipsis;
&:hover {
- background: var(--X3);
+ background: var(--MI_THEME-X3);
}
&[data-selected='true'] {
- background: var(--accent);
+ background: var(--MI_THEME-accent);
color: #fff !important;
}
&:active {
- background: var(--accentDarken);
+ background: var(--MI_THEME-accentDarken);
color: #fff !important;
}
}
diff --git a/packages/frontend/src/components/MkButton.vue b/packages/frontend/src/components/MkButton.vue
index e30f74460d..6427f9b58a 100644
--- a/packages/frontend/src/components/MkButton.vue
+++ b/packages/frontend/src/components/MkButton.vue
@@ -129,7 +129,7 @@ function onMousedown(evt: MouseEvent): void {
font-size: 95%;
box-shadow: none;
text-decoration: none;
- background: var(--buttonBg);
+ background: var(--MI_THEME-buttonBg);
border-radius: var(--radius-xs);
overflow: clip;
box-sizing: border-box;
@@ -140,11 +140,11 @@ function onMousedown(evt: MouseEvent): void {
}
&:not(:disabled):hover {
- background: var(--buttonHoverBg);
+ background: var(--MI_THEME-buttonHoverBg);
}
&:not(:disabled):active {
- background: var(--buttonHoverBg);
+ background: var(--MI_THEME-buttonHoverBg);
}
&.small {
@@ -167,15 +167,15 @@ function onMousedown(evt: MouseEvent): void {
&.primary {
font-weight: bold;
- color: var(--fgOnAccent) !important;
- background: var(--accent);
+ color: var(--MI_THEME-fgOnAccent) !important;
+ background: var(--MI_THEME-accent);
&:not(:disabled):hover {
- background: hsl(from var(--accent) h s calc(l + 5));
+ background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
}
&:not(:disabled):active {
- background: hsl(from var(--accent) h s calc(l + 5));
+ background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
}
}
@@ -216,15 +216,15 @@ function onMousedown(evt: MouseEvent): void {
&.gradate {
font-weight: bold;
- color: var(--fgOnAccent) !important;
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ color: var(--MI_THEME-fgOnAccent) !important;
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
&:not(:disabled):hover {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
&:not(:disabled):active {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
diff --git a/packages/frontend/src/components/MkCaptcha.vue b/packages/frontend/src/components/MkCaptcha.vue
index ab00ea9930..aebf3128b0 100644
--- a/packages/frontend/src/components/MkCaptcha.vue
+++ b/packages/frontend/src/components/MkCaptcha.vue
@@ -10,6 +10,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<div id="mcaptcha__widget-container" class="m-captcha-style"></div>
<div ref="captchaEl"></div>
</div>
+ <div v-if="props.provider == 'testcaptcha'" style="background: #eee; border: solid 1px #888; padding: 8px; color: #000; max-width: 320px; display: flex; gap: 10px; align-items: center; box-shadow: 2px 2px 6px #0004; border-radius: 4px;">
+ <img src="/client-assets/testcaptcha.png" style="width: 60px; height: 60px; "/>
+ <div v-if="testcaptchaPassed">
+ <div style="color: green;">Test captcha passed!</div>
+ </div>
+ <div v-else>
+ <div style="font-size: 13px; margin-bottom: 4px;">Type "ai-chan-kawaii" to pass captcha</div>
+ <input v-model="testcaptchaInput" data-cy-testcaptcha-input/>
+ <button type="button" data-cy-testcaptcha-submit @click="testcaptchaSubmit">Submit</button>
+ </div>
+ </div>
<div v-else ref="captchaEl"></div>
</div>
</template>
@@ -32,7 +43,7 @@ export type Captcha = {
}): void;
};
-export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'fc';
+export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'fc' | 'testcaptcha';
type CaptchaContainer = {
readonly [_ in CaptchaProvider]?: Captcha;
@@ -57,6 +68,9 @@ const available = ref(false);
const captchaEl = shallowRef<HTMLDivElement | undefined>();
+const testcaptchaInput = ref('');
+const testcaptchaPassed = ref(false);
+
const variable = computed(() => {
switch (props.provider) {
case 'hcaptcha': return 'hcaptcha';
@@ -64,6 +78,7 @@ const variable = computed(() => {
case 'turnstile': return 'turnstile';
case 'mcaptcha': return 'mcaptcha';
case 'fc': return 'friendlyChallenge';
+ case 'testcaptcha': return 'testcaptcha';
}
});
@@ -76,6 +91,7 @@ const src = computed(() => {
case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit';
case 'fc': return 'https://cdn.jsdelivr.net/npm/friendly-challenge@0.9.18/widget.min.js';
case 'mcaptcha': return null;
+ case 'testcaptcha': return null;
}
});
@@ -83,7 +99,7 @@ const scriptId = computed(() => `script-${props.provider}`);
const captcha = computed<Captcha>(() => window[variable.value] || {} as unknown as Captcha);
-if (loaded || props.provider === 'mcaptcha') {
+if (loaded || props.provider === 'mcaptcha' || props.provider === 'testcaptcha') {
available.value = true;
} else if (src.value !== null) {
(document.getElementById(scriptId.value) ?? document.head.appendChild(Object.assign(document.createElement('script'), {
@@ -96,6 +112,8 @@ if (loaded || props.provider === 'mcaptcha') {
function reset() {
if (captcha.value.reset) captcha.value.reset();
+ testcaptchaPassed.value = false;
+ testcaptchaInput.value = '';
}
async function requestRender() {
@@ -140,6 +158,12 @@ function onReceivedMessage(message: MessageEvent) {
}
}
+function testcaptchaSubmit() {
+ testcaptchaPassed.value = testcaptchaInput.value === 'ai-chan-kawaii';
+ callback(testcaptchaPassed.value ? 'testcaptcha-passed' : undefined);
+ if (!testcaptchaPassed.value) testcaptchaInput.value = '';
+}
+
onMounted(() => {
if (available.value) {
window.addEventListener('message', onReceivedMessage);
diff --git a/packages/frontend/src/components/MkChannelFollowButton.vue b/packages/frontend/src/components/MkChannelFollowButton.vue
index 6dace43fde..99922ffbc8 100644
--- a/packages/frontend/src/components/MkChannelFollowButton.vue
+++ b/packages/frontend/src/components/MkChannelFollowButton.vue
@@ -68,9 +68,9 @@ async function onClick() {
position: relative;
display: inline-block;
font-weight: bold;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
background: transparent;
- border: solid 1px var(--accent);
+ border: solid 1px var(--MI_THEME-accent);
padding: 0;
height: 31px;
font-size: 16px;
@@ -99,17 +99,17 @@ async function onClick() {
}
&.active {
- color: var(--fgOnAccent);
- background: var(--accent);
+ color: var(--MI_THEME-fgOnAccent);
+ background: var(--MI_THEME-accent);
&:hover {
- background: var(--accentLighten);
- border-color: var(--accentLighten);
+ background: var(--MI_THEME-accentLighten);
+ border-color: var(--MI_THEME-accentLighten);
}
&:active {
- background: var(--accentDarken);
- border-color: var(--accentDarken);
+ background: var(--MI_THEME-accentDarken);
+ border-color: var(--MI_THEME-accentDarken);
}
}
diff --git a/packages/frontend/src/components/MkChannelPreview.vue b/packages/frontend/src/components/MkChannelPreview.vue
index 2f7ec34d44..a25c596975 100644
--- a/packages/frontend/src/components/MkChannelPreview.vue
+++ b/packages/frontend/src/components/MkChannelPreview.vue
@@ -100,7 +100,7 @@ const bannerStyle = computed(() => {
height: 100%;
border-radius: inherit;
pointer-events: none;
- box-shadow: inset 0 0 0 2px var(--focus);
+ box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
}
}
@@ -117,7 +117,7 @@ const bannerStyle = computed(() => {
left: 0;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+ background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
}
> .name {
@@ -148,7 +148,7 @@ const bannerStyle = computed(() => {
bottom: 16px;
left: 16px;
background: rgba(0, 0, 0, 0.7);
- color: var(--warn);
+ color: var(--MI_THEME-warn);
border-radius: var(--radius-sm);
font-weight: bold;
font-size: 1em;
@@ -167,7 +167,7 @@ const bannerStyle = computed(() => {
> footer {
padding: 12px 16px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
> span {
opacity: 0.7;
@@ -213,8 +213,8 @@ const bannerStyle = computed(() => {
top: 0;
right: 0;
transform: translate(25%, -25%);
- background-color: var(--accent);
- border: solid var(--bg) 4px;
+ background-color: var(--MI_THEME-accent);
+ border: solid var(--MI_THEME-bg) 4px;
border-radius: var(--radius-full);
width: 1.5rem;
height: 1.5rem;
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index 57d325b11a..d05f4921f6 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -863,8 +863,8 @@ onMounted(() => {
left: 0;
width: 100%;
height: 100%;
- -webkit-backdrop-filter: var(--blur, blur(12px));
- backdrop-filter: var(--blur, blur(12px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(12px));
+ backdrop-filter: var(--MI-blur, blur(12px));
display: flex;
justify-content: center;
align-items: center;
diff --git a/packages/frontend/src/components/MkChartLegend.vue b/packages/frontend/src/components/MkChartLegend.vue
index 240c9c919e..1e402a617c 100644
--- a/packages/frontend/src/components/MkChartLegend.vue
+++ b/packages/frontend/src/components/MkChartLegend.vue
@@ -53,11 +53,11 @@ defineExpose({
> .item {
font-size: 85%;
padding: 4px 12px 4px 8px;
- border: solid 1px var(--divider);
+ border: solid 1px var(--MI_THEME-divider);
border-radius: var(--radius-ellipse);
&:hover {
- border-color: var(--inputBorderHover);
+ border-color: var(--MI_THEME-inputBorderHover);
}
&.disabled {
diff --git a/packages/frontend/src/components/MkClipPreview.vue b/packages/frontend/src/components/MkClipPreview.vue
index dd550733cb..5b09ec90dd 100644
--- a/packages/frontend/src/components/MkClipPreview.vue
+++ b/packages/frontend/src/components/MkClipPreview.vue
@@ -49,13 +49,13 @@ const remaining = computed(() => {
outline: none;
.root {
- box-shadow: inset 0 0 0 2px var(--focus);
+ box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
}
}
&:hover {
text-decoration: none;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
@@ -65,7 +65,7 @@ const remaining = computed(() => {
.divider {
height: 1px;
- background: var(--divider);
+ background: var(--MI_THEME-divider);
}
.description {
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index 9e54420034..ae80bf33ba 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -96,7 +96,7 @@ watch(() => props.lang, (to) => {
margin: .5em 0;
overflow: auto;
border-radius: var(--radius-sm);
- border: 1px solid var(--divider);
+ border: 1px solid var(--MI_THEME-divider);
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
color: var(--shiki-fallback);
diff --git a/packages/frontend/src/components/MkCode.vue b/packages/frontend/src/components/MkCode.vue
index 05cde89dd9..b4a04d4cc3 100644
--- a/packages/frontend/src/components/MkCode.vue
+++ b/packages/frontend/src/components/MkCode.vue
@@ -71,7 +71,7 @@ function copy() {
.codeBlockFallbackRoot {
display: block;
overflow-wrap: anywhere;
- background: var(--bg);
+ background: var(--MI_THEME-bg);
padding: 1em;
margin: .5em 0;
overflow: auto;
@@ -94,8 +94,8 @@ function copy() {
border-radius: var(--radius-sm);
padding: 24px;
margin-top: 4px;
- color: var(--fg);
- background: var(--bg);
+ color: var(--MI_THEME-fg);
+ background: var(--MI_THEME-bg);
}
.codePlaceholderContainer {
diff --git a/packages/frontend/src/components/MkCodeEditor.vue b/packages/frontend/src/components/MkCodeEditor.vue
index b233189ab0..1f9bd3e186 100644
--- a/packages/frontend/src/components/MkCodeEditor.vue
+++ b/packages/frontend/src/components/MkCodeEditor.vue
@@ -140,7 +140,7 @@ watch(v, newValue => {
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@@ -160,17 +160,17 @@ watch(v, newValue => {
margin: 0;
border-radius: var(--radius-sm);
padding: 0;
- color: var(--fg);
- border: solid 1px var(--panel);
+ color: var(--MI_THEME-fg);
+ border: solid 1px var(--MI_THEME-panel);
transition: border-color 0.1s ease-out;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
&:hover {
- border-color: var(--inputBorderHover) !important;
+ border-color: var(--MI_THEME-inputBorderHover) !important;
}
}
.focused.codeEditorRoot {
- border-color: var(--accent) !important;
+ border-color: var(--MI_THEME-accent) !important;
border-radius: var(--radius-sm);
}
@@ -196,7 +196,7 @@ watch(v, newValue => {
resize: none;
text-align: left;
color: transparent;
- caret-color: var(--fg);
+ caret-color: var(--MI_THEME-fg);
background-color: transparent;
border: 0;
border-radius: var(--radius-sm);
@@ -213,6 +213,6 @@ watch(v, newValue => {
}
.textarea::selection {
- color: var(--bg);
+ color: var(--MI_THEME-bg);
}
</style>
diff --git a/packages/frontend/src/components/MkCodeInline.vue b/packages/frontend/src/components/MkCodeInline.vue
index 6add80d1bc..04b6e54108 100644
--- a/packages/frontend/src/components/MkCodeInline.vue
+++ b/packages/frontend/src/components/MkCodeInline.vue
@@ -18,7 +18,7 @@ const props = defineProps<{
display: inline-block;
font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
overflow-wrap: anywhere;
- background: var(--bg);
+ background: var(--MI_THEME-bg);
padding: .1em;
border-radius: .3em;
}
diff --git a/packages/frontend/src/components/MkColorInput.vue b/packages/frontend/src/components/MkColorInput.vue
index 99aa46d561..e83c0bccd4 100644
--- a/packages/frontend/src/components/MkColorInput.vue
+++ b/packages/frontend/src/components/MkColorInput.vue
@@ -60,7 +60,7 @@ const onInput = () => {
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@@ -72,8 +72,8 @@ const onInput = () => {
&.focused {
> .inputCore {
- border-color: var(--accent) !important;
- //box-shadow: 0 0 0 4px var(--focus);
+ border-color: var(--MI_THEME-accent) !important;
+ //box-shadow: 0 0 0 4px var(--MI_THEME-focus);
}
}
@@ -98,9 +98,9 @@ const onInput = () => {
font: inherit;
font-weight: normal;
font-size: 1em;
- color: var(--fg);
- background: var(--panel);
- border: solid 1px var(--panel);
+ color: var(--MI_THEME-fg);
+ background: var(--MI_THEME-panel);
+ border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
outline: none;
box-shadow: none;
@@ -108,7 +108,7 @@ const onInput = () => {
transition: border-color 0.1s ease-out;
&:hover {
- border-color: var(--inputBorderHover) !important;
+ border-color: var(--MI_THEME-inputBorderHover) !important;
}
}
</style>
diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue
index 5f71e289b8..15d41baa7e 100644
--- a/packages/frontend/src/components/MkContainer.vue
+++ b/packages/frontend/src/components/MkContainer.vue
@@ -165,10 +165,11 @@ onUnmounted(() => {
.header {
position: sticky;
- top: var(--stickyTop, 0px);
+ top: var(--MI-stickyTop, 0px);
left: 0;
- color: var(--panelHeaderFg);
- border-bottom: solid 0.5px var(--panelHeaderDivider);
+ color: var(--MI_THEME-panelHeaderFg);
+ background: var(--MI_THEME-panelHeaderBg);
+ border-bottom: solid 0.5px var(--MI_THEME-panelHeaderDivider);
z-index: 2;
line-height: 1.4em;
background: color-mix(in srgb, var(--panelHeaderBg) 35%, transparent);
@@ -201,7 +202,7 @@ onUnmounted(() => {
}
.content {
- --stickyTop: 0px;
+ --MI-stickyTop: 0px;
&.omitted {
position: relative;
@@ -216,11 +217,11 @@ onUnmounted(() => {
left: 0;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+ background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: var(--radius-ellipse);
@@ -229,7 +230,7 @@ onUnmounted(() => {
&:hover {
> .fadeLabel {
- background: var(--panelHighlight);
+ background: var(--MI_THEME-panelHighlight);
}
}
}
diff --git a/packages/frontend/src/components/MkCropperDialog.vue b/packages/frontend/src/components/MkCropperDialog.vue
index 2e1e92cbdf..c2a1aaf29a 100644
--- a/packages/frontend/src/components/MkCropperDialog.vue
+++ b/packages/frontend/src/components/MkCropperDialog.vue
@@ -125,7 +125,7 @@ onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement);
const selection = cropper.getCropperSelection()!;
- selection.themeColor = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
+ selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
selection.aspectRatio = props.aspectRatio;
selection.initialAspectRatio = props.aspectRatio;
selection.outlined = true;
@@ -170,8 +170,8 @@ onMounted(() => {
display: flex;
align-items: center;
justify-content: center;
- -webkit-backdrop-filter: var(--blur, blur(10px));
- backdrop-filter: var(--blur, blur(10px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(10px));
+ backdrop-filter: var(--MI-blur, blur(10px));
background: rgba(0, 0, 0, 0.5);
}
diff --git a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
index c7f1288729..949adc6a8e 100644
--- a/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
+++ b/packages/frontend/src/components/MkCustomEmojiDetailedDialog.vue
@@ -85,8 +85,8 @@ function cancel() {
.emojiImgWrapper {
max-width: 100%;
height: 40cqh;
- background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--X5) 8px, var(--X5) 14px);
- border-radius: var(--radius);
+ background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-X5) 8px, var(--MI_THEME-X5) 14px);
+ border-radius: var(--MI-radius);
margin: auto;
overflow-y: hidden;
}
@@ -101,8 +101,8 @@ function cancel() {
display: inline-block;
word-break: break-all;
padding: 3px 10px;
- background-color: var(--X5);
- border: solid 1px var(--divider);
- border-radius: var(--radius);
+ background-color: var(--MI_THEME-X5);
+ border: solid 1px var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
}
</style>
diff --git a/packages/frontend/src/components/MkDateSeparatedList.vue b/packages/frontend/src/components/MkDateSeparatedList.vue
index 98bf5191f7..8fda097df6 100644
--- a/packages/frontend/src/components/MkDateSeparatedList.vue
+++ b/packages/frontend/src/components/MkDateSeparatedList.vue
@@ -9,6 +9,7 @@ import MkAd from '@/components/global/MkAd.vue';
import { isDebuggerEnabled, stackTraceInstances } from '@/debug.js';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
+import { instance } from '@/instance.js';
import { defaultStore } from '@/store.js';
import { MisskeyEntity } from '@/types/date-separated-list.js';
@@ -99,11 +100,13 @@ export default defineComponent({
return [el, separator];
} else {
- if (props.ad && item._shouldInsertAd_) {
- return [h(MkAd, {
+ if (props.ad && instance.ads.length > 0 && item._shouldInsertAd_) {
+ return [h('div', {
key: item.id + ':ad',
+ class: $style['ad-wrapper'],
+ }, [h(MkAd, {
prefer: ['horizontal', 'horizontal-big'],
- }), el];
+ })]), el];
} else {
return el;
}
@@ -182,7 +185,7 @@ export default defineComponent({
}
&:not(.date-separated-list-nogap) > *:not(:last-child) {
- margin-bottom: var(--margin);
+ margin-bottom: var(--MI-margin);
}
}
@@ -196,7 +199,7 @@ export default defineComponent({
box-shadow: none;
&:not(:last-child) {
- border-bottom: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
}
}
}
@@ -237,7 +240,7 @@ export default defineComponent({
line-height: 32px;
text-align: center;
font-size: 12px;
- color: var(--dateLabelFg);
+ color: var(--MI_THEME-dateLabelFg);
}
.date-1 {
@@ -255,5 +258,11 @@ export default defineComponent({
.date-2-icon {
margin-left: 8px;
}
+
+.ad-wrapper {
+ padding: 8px;
+ background-size: auto auto;
+ background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--MI_THEME-bg) 8px, var(--MI_THEME-bg) 14px);
+}
</style>
diff --git a/packages/frontend/src/components/MkDialog.vue b/packages/frontend/src/components/MkDialog.vue
index 7dc381b662..5af2816acf 100644
--- a/packages/frontend/src/components/MkDialog.vue
+++ b/packages/frontend/src/components/MkDialog.vue
@@ -185,7 +185,7 @@ function onInputKeydown(evt: KeyboardEvent) {
max-width: 480px;
box-sizing: border-box;
text-align: center;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: var(--radius-md);
}
@@ -207,15 +207,15 @@ function onInputKeydown(evt: KeyboardEvent) {
}
.type_success {
- color: var(--success);
+ color: var(--MI_THEME-success);
}
.type_error {
- color: var(--error);
+ color: var(--MI_THEME-error);
}
.type_warning {
- color: var(--warn);
+ color: var(--MI_THEME-warn);
}
.title {
diff --git a/packages/frontend/src/components/MkDivider.vue b/packages/frontend/src/components/MkDivider.vue
index e4e3af99e4..f72f091383 100644
--- a/packages/frontend/src/components/MkDivider.vue
+++ b/packages/frontend/src/components/MkDivider.vue
@@ -27,6 +27,6 @@ defineProps<{
<style scoped lang="scss">
.default {
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
</style>
diff --git a/packages/frontend/src/components/MkDonation.vue b/packages/frontend/src/components/MkDonation.vue
index 1dfdebf0d4..9f413fc078 100644
--- a/packages/frontend/src/components/MkDonation.vue
+++ b/packages/frontend/src/components/MkDonation.vue
@@ -75,12 +75,12 @@ function neverShow() {
.root {
position: fixed;
z-index: v-bind(zIndex);
- bottom: var(--margin);
+ bottom: var(--MI-margin);
left: 0;
right: 0;
margin: auto;
box-sizing: border-box;
- width: calc(100% - (var(--margin) * 2));
+ width: calc(100% - (var(--MI-margin) * 2));
max-width: 500px;
display: flex;
backdrop-filter: var(--blur, blur(15px));
@@ -90,7 +90,7 @@ function neverShow() {
text-align: center;
padding-top: 25px;
width: 100px;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
@media (max-width: 500px) {
.icon {
diff --git a/packages/frontend/src/components/MkDrive.file.vue b/packages/frontend/src/components/MkDrive.file.vue
index 20ad2984d8..f7249f19fb 100644
--- a/packages/frontend/src/components/MkDrive.file.vue
+++ b/packages/frontend/src/components/MkDrive.file.vue
@@ -152,14 +152,14 @@ function onDragend() {
}
&.isSelected {
- background: var(--accent);
+ background: var(--MI_THEME-accent);
&:hover {
- background: var(--accentLighten);
+ background: var(--MI_THEME-accentLighten);
}
&:active {
- background: var(--accentDarken);
+ background: var(--MI_THEME-accentDarken);
}
> .label {
@@ -248,7 +248,7 @@ function onDragend() {
font-size: 0.8em;
text-align: center;
word-break: break-all;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
overflow: hidden;
}
</style>
diff --git a/packages/frontend/src/components/MkDrive.folder.vue b/packages/frontend/src/components/MkDrive.folder.vue
index 44788a6ffb..a0693c3827 100644
--- a/packages/frontend/src/components/MkDrive.folder.vue
+++ b/packages/frontend/src/components/MkDrive.folder.vue
@@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, defineAsyncComponent, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import type { MenuItem } from '@/types/menu.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
-import type { MenuItem } from '@/types/menu.js';
const props = withDefaults(defineProps<{
folder: Misskey.entities.DriveFolder;
@@ -313,7 +313,7 @@ function onContextmenu(ev: MouseEvent) {
position: relative;
padding: 8px;
height: 64px;
- background: var(--driveFolderBg);
+ background: var(--MI_THEME-driveFolderBg);
border-radius: var(--radius-xs);
cursor: pointer;
@@ -326,7 +326,7 @@ function onContextmenu(ev: MouseEvent) {
right: -4px;
bottom: -4px;
left: -4px;
- border: 2px dashed var(--focus);
+ border: 2px dashed var(--MI_THEME-focus);
border-radius: var(--radius-xs);
}
}
@@ -345,13 +345,13 @@ function onContextmenu(ev: MouseEvent) {
width: 18px;
height: 18px;
background: #fff;
- border: solid 2px var(--divider);
+ border: solid 2px var(--MI_THEME-divider);
border-radius: 4px;
box-sizing: border-box;
&.checked {
- border-color: var(--accent);
- background: var(--accent);
+ border-color: var(--MI_THEME-accent);
+ background: var(--MI_THEME-accent);
&::after {
content: "\ea5e";
@@ -368,14 +368,13 @@ function onContextmenu(ev: MouseEvent) {
}
&:hover {
- background: var(--accentedBg);
+ background: var(--MI_THEME-accentedBg);
}
}
.name {
margin: 0;
font-size: 0.9em;
- color: var(--desktopDriveFolderFg);
}
.icon {
@@ -388,6 +387,5 @@ function onContextmenu(ev: MouseEvent) {
margin: 4px 4px;
font-size: 0.8em;
text-align: right;
- color: var(--desktopDriveFolderFg);
}
</style>
diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue
index a471457b44..4b8e5f27c2 100644
--- a/packages/frontend/src/components/MkDrive.vue
+++ b/packages/frontend/src/components/MkDrive.vue
@@ -735,7 +735,7 @@ onBeforeUnmount(() => {
box-sizing: border-box;
overflow: auto;
font-size: 0.9em;
- box-shadow: 0 1px 0 var(--divider);
+ box-shadow: 0 1px 0 var(--MI_THEME-divider);
user-select: none;
}
@@ -787,7 +787,7 @@ onBeforeUnmount(() => {
.main {
flex: 1;
overflow: auto;
- padding: var(--margin);
+ padding: var(--MI-margin);
user-select: none;
&.fetching {
@@ -834,7 +834,7 @@ onBeforeUnmount(() => {
top: 38px;
width: 100%;
height: calc(100% - 38px);
- border: dashed 2px var(--focus);
+ border: dashed 2px var(--MI_THEME-focus);
pointer-events: none;
}
</style>
diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue
index 543cf24022..623c4895eb 100644
--- a/packages/frontend/src/components/MkDriveFileThumbnail.vue
+++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue
@@ -69,7 +69,7 @@ const isThumbnailAvailable = computed(() => {
.root {
position: relative;
display: flex;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: var(--radius-sm);
overflow: clip;
}
@@ -83,7 +83,7 @@ const isThumbnailAvailable = computed(() => {
height: 100%;
pointer-events: none;
border-radius: inherit;
- box-shadow: inset 0 0 0 4px var(--warn);
+ box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
}
.iconSub {
diff --git a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
index c060c3a659..c2bb516c7c 100644
--- a/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
+++ b/packages/frontend/src/components/MkEmbedCodeGenDialog.vue
@@ -306,9 +306,9 @@ onUnmounted(() => {
.embedCodeGenPreviewRoot {
position: relative;
- background-color: var(--bg);
+ background-color: var(--MI_THEME-bg);
background-size: auto auto;
- background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--panel) 6px, var(--panel) 12px);
+ background-image: repeating-linear-gradient(135deg, transparent, transparent 6px, var(--MI_THEME-panel) 6px, var(--MI_THEME-panel) 12px);
cursor: not-allowed;
}
@@ -381,8 +381,8 @@ onUnmounted(() => {
.embedCodeGenResultHeadingIcon {
margin: 0 auto;
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
text-align: center;
height: 64px;
width: 64px;
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index 151843b18c..e2762eb3cb 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<!-- ã“ã®ã‚³ãƒ³ãƒãƒ¼ãƒãƒ³ãƒˆã®è¦ç´ ã®classã¯è¦ªã‹ã‚‰åˆ©ç”¨ã•れるã®ã§ã‚€ã‚„ã¿ã«å¼„らãªã„ã“㨠-->
<!-- フォルダã®ä¸­ã«ã¯ã‚«ã‚¹ã‚¿ãƒ çµµæ–‡å­—ã ã‘(Unicode絵文字もã“ã£ã¡ï¼‰ -->
-<section v-if="!hasChildSection" v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);">
+<section v-if="!hasChildSection" v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--MI_THEME-divider);">
<header class="_acrylic" @click="shown = !shown">
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ph-smiley-sticker ph-bold ph-lg"></i>:{{ emojis.length }})
</header>
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</section>
<!-- フォルダã®ä¸­ã«ã¯ã‚«ã‚¹ã‚¿ãƒ çµµæ–‡å­—やフォルダãŒã‚ã‚‹ -->
-<section v-else v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--divider);">
+<section v-else v-panel style="border-radius: var(--radius-sm); border-bottom: 0.5px solid var(--MI_THEME-divider);">
<header class="_acrylic" @click="shown = !shown">
<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder ti-fw"></i>:{{ customEmojiTree?.length }} <i class="ph-smiley-sticker ph-bold ph-lg ti-fw"></i>:{{ emojis.length }})
</header>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 949ed4db91..667bb832a2 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -580,7 +580,7 @@ defineExpose({
&:disabled {
cursor: not-allowed;
- background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
+ background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
opacity: 1;
> .emoji {
@@ -615,7 +615,7 @@ defineExpose({
&:disabled {
cursor: not-allowed;
- background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
+ background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
opacity: 1;
> .emoji {
@@ -638,7 +638,7 @@ defineExpose({
outline: none;
border: none;
background: transparent;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
&:not(:focus):not(.filled) {
margin-bottom: env(safe-area-inset-bottom, 0px);
@@ -647,7 +647,7 @@ defineExpose({
&:not(.filled) {
order: 1;
z-index: 2;
- box-shadow: 0px -1px 0 0px var(--divider);
+ box-shadow: 0px -1px 0 0px var(--MI_THEME-divider);
}
}
@@ -658,11 +658,11 @@ defineExpose({
> .tab {
flex: 1;
height: 38px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
&.active {
- border-top: solid 1px var(--accent);
- color: var(--accent);
+ border-top: solid 1px var(--MI_THEME-accent);
+ color: var(--MI_THEME-accent);
}
}
}
@@ -681,7 +681,7 @@ defineExpose({
> .group {
&:not(.index) {
padding: 4px 0 8px 0;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
> header {
@@ -708,7 +708,7 @@ defineExpose({
cursor: pointer;
&:hover {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
@@ -730,13 +730,13 @@ defineExpose({
}
&:active {
- background: var(--accent);
+ background: var(--MI_THEME-accent);
box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
}
&:disabled {
cursor: not-allowed;
- background: linear-gradient(-45deg, transparent 0% 48%, var(--X6) 48% 52%, transparent 52% 100%);
+ background: linear-gradient(-45deg, transparent 0% 48%, var(--MI_THEME-X6) 48% 52%, transparent 52% 100%);
opacity: 1;
> .emoji {
@@ -757,7 +757,7 @@ defineExpose({
}
&.result {
- border-bottom: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
&:empty {
display: none;
diff --git a/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts
new file mode 100644
index 0000000000..6763f7c546
--- /dev/null
+++ b/packages/frontend/src/components/MkExtensionInstaller.stories.impl.ts
@@ -0,0 +1,83 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { StoryObj } from '@storybook/vue3';
+import MkExtensionInstaller from './MkExtensionInstaller.vue';
+import lightTheme from '@@/themes/_light.json5';
+
+export const Plugin = {
+ render(args) {
+ return {
+ components: {
+ MkExtensionInstaller,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkExtensionInstaller v-bind="props" />',
+ };
+ },
+ args: {
+ extension: {
+ type: 'plugin',
+ raw: '"do nothing"',
+ meta: {
+ name: 'do nothing plugin',
+ version: '1.0',
+ author: 'syuilo and misskey-project',
+ description: 'a plugin that does nothing',
+ permissions: ['read:account'],
+ config: {
+ 'doNothing': true,
+ },
+ },
+ },
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkExtensionInstaller>;
+
+export const Theme = {
+ render(args) {
+ return {
+ components: {
+ MkExtensionInstaller,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkExtensionInstaller v-bind="props" />',
+ };
+ },
+ args: {
+ extension: {
+ type: 'theme',
+ raw: JSON.stringify(lightTheme),
+ meta: lightTheme,
+ },
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkExtensionInstaller>;
diff --git a/packages/frontend/src/components/MkExtensionInstaller.vue b/packages/frontend/src/components/MkExtensionInstaller.vue
new file mode 100644
index 0000000000..b41604b2c3
--- /dev/null
+++ b/packages/frontend/src/components/MkExtensionInstaller.vue
@@ -0,0 +1,146 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div class="_gaps_m" :class="$style.extInstallerRoot">
+ <div :class="$style.extInstallerIconWrapper">
+ <i v-if="isPlugin" class="ti ti-plug"></i>
+ <i v-else-if="isTheme" class="ti ti-palette"></i>
+ <!-- 拡張用? -->
+ <i v-else class="ti ti-download"></i>
+ </div>
+ <h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].title }}</h2>
+ <div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
+ <MkInfo v-if="isPlugin" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
+ <FormSection>
+ <template #label>{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].metaTitle }}</template>
+ <div class="_gaps_s">
+ <FormSplit>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.name }}</template>
+ <template #value>{{ extension.meta.name }}</template>
+ </MkKeyValue>
+ <MkKeyValue>
+ <template #key>{{ i18n.ts.author }}</template>
+ <template #value>{{ extension.meta.author }}</template>
+ </MkKeyValue>
+ </FormSplit>
+ <MkKeyValue v-if="isPlugin">
+ <template #key>{{ i18n.ts.description }}</template>
+ <template #value>{{ extension.meta.description ?? i18n.ts.none }}</template>
+ </MkKeyValue>
+ <MkKeyValue v-if="isPlugin">
+ <template #key>{{ i18n.ts.version }}</template>
+ <template #value>{{ extension.meta.version }}</template>
+ </MkKeyValue>
+ <MkKeyValue v-if="isPlugin">
+ <template #key>{{ i18n.ts.permission }}</template>
+ <template #value>
+ <ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
+ <li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
+ </ul>
+ <template v-else>{{ i18n.ts.none }}</template>
+ </template>
+ </MkKeyValue>
+ <MkKeyValue v-if="isTheme">
+ <template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
+ <template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
+ </MkKeyValue>
+ <MkFolder>
+ <template #icon><i class="ti ti-code"></i></template>
+ <template #label>{{ i18n.ts._plugin.viewSource }}</template>
+
+ <MkCode :code="extension.raw"/>
+ </MkFolder>
+ </div>
+ </FormSection>
+ <slot name="additionalInfo"/>
+ <div class="_buttonsCenter">
+ <MkButton primary @click="emits('confirm')"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+export type Extension = {
+ type: 'plugin';
+ raw: string;
+ meta: {
+ name: string;
+ version: string;
+ author: string;
+ description?: string;
+ permissions?: string[];
+ config?: Record<string, any>;
+ };
+} | {
+ type: 'theme';
+ raw: string;
+ meta: {
+ name: string;
+ author: string;
+ base?: 'light' | 'dark';
+ };
+};
+</script>
+<script lang="ts" setup>
+import { computed } from 'vue';
+import MkButton from '@/components/MkButton.vue';
+import FormSection from '@/components/form/section.vue';
+import FormSplit from '@/components/form/split.vue';
+import MkCode from '@/components/MkCode.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkKeyValue from '@/components/MkKeyValue.vue';
+import { i18n } from '@/i18n.js';
+
+const isPlugin = computed(() => props.extension.type === 'plugin');
+const isTheme = computed(() => props.extension.type === 'theme');
+
+const props = defineProps<{
+ extension: Extension;
+}>();
+
+const emits = defineEmits<{
+ (ev: 'confirm'): void;
+}>();
+</script>
+
+<style lang="scss" module>
+.extInstallerRoot {
+ border-radius: var(--MI-radius);
+ background: var(--MI_THEME-panel);
+ padding: 1.5rem;
+}
+
+.extInstallerIconWrapper {
+ width: 48px;
+ height: 48px;
+ font-size: 24px;
+ line-height: 48px;
+ text-align: center;
+ border-radius: 50%;
+ margin-left: auto;
+ margin-right: auto;
+
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
+}
+
+.extInstallerTitle {
+ font-size: 1.2rem;
+ text-align: center;
+ margin: 0;
+}
+
+.extInstallerNormDesc {
+ text-align: center;
+}
+
+.extInstallerKVList {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+</style>
diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue
index 7af68a32ba..5bc85a3a83 100644
--- a/packages/frontend/src/components/MkFileListForAdmin.vue
+++ b/packages/frontend/src/components/MkFileListForAdmin.vue
@@ -66,7 +66,7 @@ const props = defineProps<{
align-items: center;
&:hover {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
> .thumbnail {
diff --git a/packages/frontend/src/components/MkFlashPreview.vue b/packages/frontend/src/components/MkFlashPreview.vue
index 8a2a438624..b7278ac742 100644
--- a/packages/frontend/src/components/MkFlashPreview.vue
+++ b/packages/frontend/src/components/MkFlashPreview.vue
@@ -36,7 +36,7 @@ const props = defineProps<{
&:hover {
text-decoration: none;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
&:focus-visible {
@@ -83,7 +83,6 @@ const props = defineProps<{
> p {
display: inline-block;
margin: 0;
- color: var(--urlPreviewInfo);
font-size: 0.8em;
line-height: 16px;
vertical-align: top;
@@ -92,7 +91,7 @@ const props = defineProps<{
}
&:global(.gray) {
- --c: var(--bg);
+ --c: var(--MI_THEME-bg);
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
}
diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index f10d58b38a..1717f8fc98 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -83,7 +83,7 @@ function afterLeave(element: Element) {
onMounted(() => {
function getParentBg(el?: HTMLElement | null): string {
- if (el == null || el.tagName === 'BODY') return 'var(--bg)';
+ if (el == null || el.tagName === 'BODY') return 'var(--MI_THEME-bg)';
const background = el.style.background || el.style.backgroundColor;
if (background) {
return background;
@@ -118,9 +118,9 @@ onMounted(() => {
position: relative;
z-index: 10;
position: sticky;
- top: var(--stickyTop, 0px);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(20px));
+ top: var(--MI-stickyTop, 0px);
+ -webkit-backdrop-filter: var(--MI-blur, blur(8px));
+ backdrop-filter: var(--MI-blur, blur(20px));
}
.title {
@@ -134,7 +134,7 @@ onMounted(() => {
flex: 1;
margin: auto;
height: 1px;
- background: var(--divider);
+ background: var(--MI_THEME-divider);
}
.button {
diff --git a/packages/frontend/src/components/MkFolder.vue b/packages/frontend/src/components/MkFolder.vue
index 392963fdb9..3715654b03 100644
--- a/packages/frontend/src/components/MkFolder.vue
+++ b/packages/frontend/src/components/MkFolder.vue
@@ -38,9 +38,12 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<KeepAlive>
<div v-show="opened">
- <MkSpacer :marginMin="14" :marginMax="22">
+ <MkSpacer v-if="withSpacer" :marginMin="14" :marginMax="22">
<slot></slot>
</MkSpacer>
+ <div v-else>
+ <slot></slot>
+ </div>
<div v-if="$slots.footer" :class="$style.footer">
<slot name="footer"></slot>
</div>
@@ -59,9 +62,11 @@ import { defaultStore } from '@/store.js';
const props = withDefaults(defineProps<{
defaultOpen?: boolean;
maxHeight?: number | null;
+ withSpacer?: boolean;
}>(), {
defaultOpen: false,
maxHeight: null,
+ withSpacer: true,
});
const getBgColor = (el: HTMLElement) => {
@@ -113,7 +118,7 @@ function toggle() {
onMounted(() => {
const computedStyle = getComputedStyle(document.documentElement);
const parentBg = getBgColor(rootEl.value!.parentElement!);
- const myBg = computedStyle.getPropertyValue('--panel');
+ const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
bgSame.value = parentBg === myBg;
});
</script>
@@ -139,15 +144,15 @@ onMounted(() => {
width: 100%;
box-sizing: border-box;
padding: 9px 12px 9px 12px;
- background: var(--folderHeaderBg);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ background: var(--MI_THEME-folderHeaderBg);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
border-radius: var(--radius-sm);
transition: border-radius 0.3s;
&:hover {
text-decoration: none;
- background: var(--folderHeaderHoverBg);
+ background: var(--MI_THEME-folderHeaderHoverBg);
}
&:focus-within {
@@ -155,8 +160,8 @@ onMounted(() => {
}
&.active {
- color: var(--accent);
- background: var(--folderHeaderHoverBg);
+ color: var(--MI_THEME-accent);
+ background: var(--MI_THEME-folderHeaderHoverBg);
}
&.opened {
@@ -170,7 +175,7 @@ onMounted(() => {
}
.headerLower {
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
font-size: .85em;
padding-left: 4px;
}
@@ -204,13 +209,13 @@ onMounted(() => {
}
.headerTextSub {
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
font-size: .85em;
}
.headerRight {
margin-left: auto;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
white-space: nowrap;
}
@@ -219,26 +224,26 @@ onMounted(() => {
}
.body {
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: 0 0 var(--radius-sm) var(--radius-sm);
container-type: inline-size;
&.bgSame {
- background: var(--bg);
+ background: var(--MI_THEME-bg);
}
}
.footer {
position: sticky !important;
z-index: 1;
- bottom: var(--stickyBottom, 0px);
+ bottom: var(--MI-stickyBottom, 0px);
left: 0;
padding: 12px;
- background: var(--acrylicBg);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ background: var(--MI_THEME-acrylicBg);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
background-size: auto auto;
- background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--panel) 5px, var(--panel) 10px);
+ background-image: repeating-linear-gradient(135deg, transparent, transparent 5px, var(--MI_THEME-panel) 5px, var(--MI_THEME-panel) 10px);
border-radius: 0 0 6px 6px;
}
</style>
diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue
index 52497a2994..3733583192 100644
--- a/packages/frontend/src/components/MkFollowButton.vue
+++ b/packages/frontend/src/components/MkFollowButton.vue
@@ -165,8 +165,8 @@ onBeforeUnmount(() => {
position: relative;
display: inline-block;
font-weight: bold;
- color: var(--fgOnWhite);
- border: solid 1px var(--accent);
+ color: var(--MI_THEME-fgOnWhite);
+ border: solid 1px var(--MI_THEME-accent);
padding: 0;
height: 31px;
font-size: 16px;
@@ -201,17 +201,17 @@ onBeforeUnmount(() => {
}
&.active {
- color: var(--fgOnAccent);
- background: var(--accent);
+ color: var(--MI_THEME-fgOnAccent);
+ background: var(--MI_THEME-accent);
&:hover {
- background: var(--accentLighten);
- border-color: var(--accentLighten);
+ background: var(--MI_THEME-accentLighten);
+ border-color: var(--MI_THEME-accentLighten);
}
&:active {
- background: var(--accentDarken);
- border-color: var(--accentDarken);
+ background: var(--MI_THEME-accentDarken);
+ border-color: var(--MI_THEME-accentDarken);
}
}
diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue
index 9360594236..ecb6cf882b 100644
--- a/packages/frontend/src/components/MkFormDialog.file.vue
+++ b/packages/frontend/src/components/MkFormDialog.file.vue
@@ -66,6 +66,6 @@ function selectButton(ev: MouseEvent) {
<style module>
.fileNotSelected {
font-weight: 700;
- color: var(--infoWarnFg);
+ color: var(--MI_THEME-infoWarnFg);
}
</style>
diff --git a/packages/frontend/src/components/MkFormFooter.vue b/packages/frontend/src/components/MkFormFooter.vue
index 1e88d59d8e..f409f6ce50 100644
--- a/packages/frontend/src/components/MkFormFooter.vue
+++ b/packages/frontend/src/components/MkFormFooter.vue
@@ -36,7 +36,7 @@ const props = defineProps<{
}
.text {
- color: var(--warn);
+ color: var(--MI_THEME-warn);
font-size: 90%;
animation: modified-blink 2s infinite;
}
diff --git a/packages/frontend/src/components/MkFukidashi.vue b/packages/frontend/src/components/MkFukidashi.vue
new file mode 100644
index 0000000000..8b1c56fca4
--- /dev/null
+++ b/packages/frontend/src/components/MkFukidashi.vue
@@ -0,0 +1,100 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div
+ :class="[
+ $style.root,
+ tail === 'left' ? $style.left : $style.right,
+ negativeMargin === true && $style.negativeMargin,
+ shadow === true && $style.shadow,
+ ]"
+>
+ <div :class="$style.bg">
+ <svg v-if="tail !== 'none'" :class="$style.tail" version="1.1" viewBox="0 0 14.597 14.58" xmlns="http://www.w3.org/2000/svg">
+ <g transform="translate(-173.71 -87.184)">
+ <path d="m188.19 87.657c-1.469 2.3218-3.9315 3.8312-6.667 4.0865-2.2309-1.7379-4.9781-2.6816-7.8061-2.6815h-5.1e-4v12.702h12.702v-5.1e-4c2e-5 -1.9998-0.47213-3.9713-1.378-5.754 2.0709-1.6834 3.2732-4.2102 3.273-6.8791-6e-5 -0.49375-0.0413-0.98662-0.1235-1.4735z" fill-rule="evenodd" stroke-linecap="round" stroke-linejoin="round" stroke-width=".33225" style="paint-order:stroke fill markers"/>
+ </g>
+ </svg>
+ <div :class="$style.content">
+ <slot></slot>
+ </div>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+withDefaults(defineProps<{
+ tail?: 'left' | 'right' | 'none';
+ negativeMargin?: boolean;
+ shadow?: boolean;
+}>(), {
+ tail: 'right',
+ negativeMargin: false,
+ shadow: false,
+});
+</script>
+
+<style module lang="scss">
+.root {
+ --fukidashi-radius: var(--MI-radius);
+ --fukidashi-bg: var(--MI_THEME-panel);
+
+ position: relative;
+ display: inline-block;
+ min-height: calc(var(--fukidashi-radius) * 2);
+ padding-top: calc(var(--fukidashi-radius) * .13);
+
+ &.shadow {
+ filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow));
+ }
+
+ &.left {
+ padding-left: calc(var(--fukidashi-radius) * .13);
+
+ &.negativeMargin {
+ margin-left: calc(calc(var(--fukidashi-radius) * .13) * -1);
+ }
+ }
+
+ &.right {
+ padding-right: calc(var(--fukidashi-radius) * .13);
+
+ &.negativeMargin {
+ margin-right: calc(calc(var(--fukidashi-radius) * .13) * -1);
+ }
+ }
+}
+
+.bg {
+ width: 100%;
+ height: 100%;
+ background: var(--fukidashi-bg);
+ border-radius: var(--fukidashi-radius);
+}
+
+.content {
+ position: relative;
+ padding: 8px 12px;
+}
+
+.tail {
+ position: absolute;
+ top: 0;
+ display: block;
+ width: calc(var(--fukidashi-radius) * 1.13);
+ height: auto;
+ fill: var(--fukidashi-bg);
+}
+
+.left .tail {
+ left: 0;
+ transform: rotateY(180deg);
+}
+
+.right .tail {
+ right: 0;
+}
+</style>
diff --git a/packages/frontend/src/components/MkGalleryPostPreview.vue b/packages/frontend/src/components/MkGalleryPostPreview.vue
index 2bb5b8762a..22f8355acf 100644
--- a/packages/frontend/src/components/MkGalleryPostPreview.vue
+++ b/packages/frontend/src/components/MkGalleryPostPreview.vue
@@ -75,7 +75,7 @@ function leaveHover(): void {
&:hover {
text-decoration: none;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
> .thumbnail {
transform: scale(1.1);
diff --git a/packages/frontend/src/components/MkGoogle.vue b/packages/frontend/src/components/MkGoogle.vue
index 54c7585f18..ad26030846 100644
--- a/packages/frontend/src/components/MkGoogle.vue
+++ b/packages/frontend/src/components/MkGoogle.vue
@@ -41,7 +41,7 @@ const search = () => {
width: 100%;
height: 40px;
font-size: 16px;
- border: solid 1px var(--divider);
+ border: solid 1px var(--MI_THEME-divider);
border-radius: var(--radius-xs) 0 0 var(--radius-xs);
-webkit-appearance: textfield;
}
@@ -50,7 +50,7 @@ const search = () => {
flex-shrink: 0;
margin: 0;
padding: 0 16px;
- border: solid 1px var(--divider);
+ border: solid 1px var(--MI_THEME-divider);
border-left: none;
border-radius: 0 var(--radius-xs) var(--radius-xs) 0;
diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue
index 46c2db4b1f..bdc38f5142 100644
--- a/packages/frontend/src/components/MkInfo.vue
+++ b/packages/frontend/src/components/MkInfo.vue
@@ -36,15 +36,15 @@ function close() {
align-items: center;
padding: 12px 14px;
font-size: 90%;
- background: color-mix(in srgb, var(--infoBg) 65%, transparent);
- color: var(--infoFg);
- border-radius: var(--radius);
+ background: color-mix(in srgb, var(--MI_THEME-infoBg) 65%, transparent);
+ color: var(--MI_THEME-infoFg);
+ border-radius: var(--MI-radius);
white-space: pre-wrap;
z-index: 1;
&.warn {
- background: color-mix(in srgb, var(--infoWarnBg) 65%, transparent);
- color: var(--infoWarnFg);
+ background: color-mix(in srgb, var(--MI_THEME-infoWarnBg) 65%, transparent);
+ color: var(--MI_THEME-infoWarnFg);
}
}
diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue
index 42e1146e27..edaa605590 100644
--- a/packages/frontend/src/components/MkInput.vue
+++ b/packages/frontend/src/components/MkInput.vue
@@ -199,7 +199,7 @@ defineExpose({
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@@ -216,8 +216,8 @@ defineExpose({
&.focused {
> .inputCore {
- border-color: var(--accent) !important;
- //box-shadow: 0 0 0 4px var(--focus);
+ border-color: var(--MI_THEME-accent) !important;
+ //box-shadow: 0 0 0 4px var(--MI_THEME-focus);
}
}
@@ -242,9 +242,9 @@ defineExpose({
font: inherit;
font-weight: normal;
font-size: 1em;
- color: var(--fg);
- background: var(--panel);
- border: solid 1px var(--panel);
+ color: var(--MI_THEME-fg);
+ background: var(--MI_THEME-panel);
+ border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
outline: none;
box-shadow: none;
@@ -252,7 +252,7 @@ defineExpose({
transition: border-color 0.1s ease-out;
&:hover {
- border-color: var(--inputBorderHover) !important;
+ border-color: var(--MI_THEME-inputBorderHover) !important;
}
}
diff --git a/packages/frontend/src/components/MkInstanceCardMini.vue b/packages/frontend/src/components/MkInstanceCardMini.vue
index 10b390e7f9..4ba49be941 100644
--- a/packages/frontend/src/components/MkInstanceCardMini.vue
+++ b/packages/frontend/src/components/MkInstanceCardMini.vue
@@ -46,7 +46,7 @@ function getInstanceIcon(instance): string {
display: flex;
align-items: center;
padding: 16px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: var(--radius-sm);
> :global(.icon) {
@@ -62,7 +62,7 @@ function getInstanceIcon(instance): string {
flex: 1;
overflow: hidden;
font-size: 0.9em;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
padding-right: 8px;
> :global(.host) {
@@ -109,7 +109,7 @@ function getInstanceIcon(instance): string {
}
&:global(.gray) {
- --c: var(--bg);
+ --c: var(--MI_THEME-bg);
background-image: linear-gradient(45deg, var(--c) 16.67%, transparent 16.67%, transparent 50%, var(--c) 50%, var(--c) 66.67%, transparent 66.67%, transparent 100%);
background-size: 16px 16px;
}
diff --git a/packages/frontend/src/components/MkInstanceStats.vue b/packages/frontend/src/components/MkInstanceStats.vue
index d74c885041..8ccbf61e48 100644
--- a/packages/frontend/src/components/MkInstanceStats.vue
+++ b/packages/frontend/src/components/MkInstanceStats.vue
@@ -121,7 +121,7 @@ function createDoughnut(chartEl, tooltip, data) {
labels: data.map(x => x.name),
datasets: [{
backgroundColor: data.map(x => x.color),
- borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
+ borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
borderWidth: 2,
hoverOffset: 0,
data: data.map(x => x.value),
@@ -256,8 +256,8 @@ onMounted(() => {
flex: 1;
min-width: 0;
position: relative;
- background: var(--panel);
- border-radius: var(--radius);
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
padding: 24px;
max-height: 300px;
diff --git a/packages/frontend/src/components/MkInviteCode.vue b/packages/frontend/src/components/MkInviteCode.vue
index 4aee64f78e..1a71f6574f 100644
--- a/packages/frontend/src/components/MkInviteCode.vue
+++ b/packages/frontend/src/components/MkInviteCode.vue
@@ -8,8 +8,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ invite.code }}</template>
<template #suffix>
<span v-if="invite.used">{{ i18n.ts.used }}</span>
- <span v-else-if="isExpired" style="color: var(--error)">{{ i18n.ts.expired }}</span>
- <span v-else style="color: var(--success)">{{ i18n.ts.unused }}</span>
+ <span v-else-if="isExpired" style="color: var(--MI_THEME-error)">{{ i18n.ts.expired }}</span>
+ <span v-else style="color: var(--MI_THEME-success)">{{ i18n.ts.unused }}</span>
</template>
<template #footer>
<div class="_buttons">
diff --git a/packages/frontend/src/components/MkLaunchPad.vue b/packages/frontend/src/components/MkLaunchPad.vue
index e0880ec3e7..0382dbe926 100644
--- a/packages/frontend/src/components/MkLaunchPad.vue
+++ b/packages/frontend/src/components/MkLaunchPad.vue
@@ -12,13 +12,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
- <span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
+ <span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span>
</button>
<MkA v-else v-click-anime :to="item.to" class="item" @click.passive="close()">
<i class="icon" :class="item.icon"></i>
<div class="text">{{ item.text }}</div>
<span v-if="item.indicate && item.indicateValue" class="_indicateCounter indicatorWithValue">{{ item.indicateValue }}</span>
- <span v-else-if="item.indicate" class="indicator"><i class="_indicatorCircle"></i></span>
+ <span v-else-if="item.indicate" class="indicator _blink"><i class="_indicatorCircle"></i></span>
</MkA>
</template>
</div>
@@ -105,8 +105,8 @@ function close() {
box-sizing: border-box;
&:hover {
- color: var(--accent);
- background: var(--accentedBg);
+ color: var(--MI_THEME-accent);
+ background: var(--MI_THEME-accentedBg);
text-decoration: none;
}
@@ -137,9 +137,8 @@ function close() {
position: absolute;
top: 32px;
left: 32px;
- color: var(--indicator);
+ color: var(--MI_THEME-indicator);
font-size: 8px;
- animation: global-blink 1s infinite;
@media (max-width: 500px) {
top: 16px;
diff --git a/packages/frontend/src/components/MkMediaAudio.vue b/packages/frontend/src/components/MkMediaAudio.vue
index 2d7cde1af2..10450fb621 100644
--- a/packages/frontend/src/components/MkMediaAudio.vue
+++ b/packages/frontend/src/components/MkMediaAudio.vue
@@ -394,8 +394,8 @@ onDeactivated(() => {
.audioContainer {
container-type: inline-size;
position: relative;
- border: .5px solid var(--divider);
- border-radius: var(--radius);
+ border: .5px solid var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
overflow: clip;
&:focus-visible {
@@ -415,7 +415,7 @@ onDeactivated(() => {
height: 100%;
pointer-events: none;
border-radius: inherit;
- box-shadow: inset 0 0 0 4px var(--warn);
+ box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
}
}
@@ -457,12 +457,12 @@ onDeactivated(() => {
.controlButton {
padding: 6px;
- border-radius: calc(var(--radius) / 2);
+ border-radius: calc(var(--MI-radius) / 2);
font-size: 1.05rem;
&:hover {
- color: var(--accent);
- background-color: var(--accentedBg);
+ color: var(--MI_THEME-accent);
+ background-color: var(--MI_THEME-accentedBg);
}
&:focus-visible {
diff --git a/packages/frontend/src/components/MkMediaBanner.vue b/packages/frontend/src/components/MkMediaBanner.vue
index 77a86ff2fb..e1714fb54d 100644
--- a/packages/frontend/src/components/MkMediaBanner.vue
+++ b/packages/frontend/src/components/MkMediaBanner.vue
@@ -68,7 +68,6 @@ async function show() {
}
.download {
- background: var(--noteAttachedFile);
}
.sensitive {
diff --git a/packages/frontend/src/components/MkMediaImage.vue b/packages/frontend/src/components/MkMediaImage.vue
index 02c054956c..2aedaa4cd6 100644
--- a/packages/frontend/src/components/MkMediaImage.vue
+++ b/packages/frontend/src/components/MkMediaImage.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.indicators">
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
<div v-if="image.comment" :class="$style.indicator">ALT</div>
- <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+ <div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
<div v-if="!image.comment" :class="$style.indicator" title="Image lacks descriptive text"><i class="ph-pencil-simple ph-bold ph-lg-off"></i></div>
</div>
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
@@ -166,7 +166,7 @@ function showMenu(ev: MouseEvent) {
height: 100%;
pointer-events: none;
border-radius: inherit;
- box-shadow: inset 0 0 0 4px var(--warn);
+ box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
}
}
@@ -188,7 +188,7 @@ function showMenu(ev: MouseEvent) {
position: absolute;
border-radius: var(--radius-sm);
background-color: black;
- color: var(--accentLighten);
+ color: var(--MI_THEME-accentLighten);
font-size: 12px;
opacity: .5;
padding: 5px 8px;
@@ -207,19 +207,19 @@ function showMenu(ev: MouseEvent) {
.visible {
position: relative;
- //box-shadow: 0 0 0 1px var(--divider) inset;
- background: var(--bg);
+ //box-shadow: 0 0 0 1px var(--MI_THEME-divider) inset;
+ background: var(--MI_THEME-bg);
background-size: 16px 16px;
}
html[data-color-scheme=dark] .visible {
--c: rgb(255 255 255 / 2%);
- background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+ background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}
html[data-color-scheme=light] .visible {
--c: rgb(0 0 0 / 2%);
- background-image: linear-gradient(45deg, var(--c) 16.67%, var(--bg) 16.67%, var(--bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--bg) 66.67%, var(--bg) 100%);
+ background-image: linear-gradient(45deg, var(--c) 16.67%, var(--MI_THEME-bg) 16.67%, var(--MI_THEME-bg) 50%, var(--c) 50%, var(--c) 66.67%, var(--MI_THEME-bg) 66.67%, var(--MI_THEME-bg) 100%);
}
.menu {
@@ -227,8 +227,8 @@ html[data-color-scheme=light] .visible {
position: absolute;
border-radius: var(--radius-ellipse);
background-color: rgba(0, 0, 0, 0.3);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
color: #fff;
font-size: 0.8em;
width: 28px;
@@ -259,10 +259,10 @@ html[data-color-scheme=light] .visible {
}
.indicator {
- /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
+ /* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
background-color: black;
border-radius: var(--radius-sm);
- color: var(--accentLighten);
+ color: var(--MI_THEME-accentLighten);
display: inline-block;
font-weight: bold;
font-size: 0.8em;
diff --git a/packages/frontend/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue
index 39fa6ff012..7f8033bcb6 100644
--- a/packages/frontend/src/components/MkMediaList.vue
+++ b/packages/frontend/src/components/MkMediaList.vue
@@ -329,14 +329,14 @@ defineExpose({
:global(.pswp) {
--pswp-root-z-index: var(--mk-pswp-root-z-index, 2000700) !important;
- --pswp-bg: var(--modalBg) !important;
+ --pswp-bg: var(--MI_THEME-modalBg) !important;
}
</style>
<style lang="scss">
.pswp__bg {
- background: var(--modalBg);
- backdrop-filter: var(--modalBgFilter);
+ background: var(--MI_THEME-modalBg);
+ backdrop-filter: var(--MI-modalBgFilter);
}
.pswp__alt-text-container {
@@ -354,14 +354,14 @@ defineExpose({
}
.pswp__alt-text {
- color: var(--fg);
+ color: var(--MI_THEME-fg);
margin: 0 auto;
text-align: center;
- padding: var(--margin);
- border-radius: var(--radius);
+ padding: var(--MI-margin);
+ border-radius: var(--MI-radius);
max-height: 8em;
overflow-y: auto;
- text-shadow: var(--bg) 0 0 10px, var(--bg) 0 0 3px, var(--bg) 0 0 3px;
+ text-shadow: var(--MI_THEME-bg) 0 0 10px, var(--MI_THEME-bg) 0 0 3px, var(--MI_THEME-bg) 0 0 3px;
white-space: pre-line;
}
</style>
diff --git a/packages/frontend/src/components/MkMediaRange.vue b/packages/frontend/src/components/MkMediaRange.vue
index 86ed8ba2cf..df7505b0c3 100644
--- a/packages/frontend/src/components/MkMediaRange.vue
+++ b/packages/frontend/src/components/MkMediaRange.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<!-- Media系専用ã®input range -->
<template>
-<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--scrollbarHandle);'">
+<div :style="sliderBgWhite ? '--sliderBg: rgba(255,255,255,.25);' : '--sliderBg: var(--MI_THEME-scrollbarHandle);'">
<div :class="$style.controlsSeekbar">
<progress v-if="buffer !== undefined" :class="$style.buffer" :value="isNaN(buffer) ? 0 : buffer" min="0" max="1">{{ Math.round(buffer * 100) }}% buffered</progress>
<input v-model="model" :class="$style.seek" :style="`--value: ${modelValue * 100}%;`" type="range" min="0" max="1" step="any" @change="emit('dragEnded', modelValue)"/>
@@ -48,7 +48,7 @@ const modelValue = computed({
background: transparent;
border: 0;
border-radius: 26px;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
display: block;
height: 19px;
margin: 0;
diff --git a/packages/frontend/src/components/MkMediaVideo.vue b/packages/frontend/src/components/MkMediaVideo.vue
index 0502bdd401..c3cecba7b7 100644
--- a/packages/frontend/src/components/MkMediaVideo.vue
+++ b/packages/frontend/src/components/MkMediaVideo.vue
@@ -42,7 +42,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
<div :class="$style.indicators">
<div v-if="video.comment" :class="$style.indicator">ALT</div>
- <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+ <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
</div>
</div>
@@ -67,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ti ti-eye-off" :class="$style.hide" @click="hide = true"></i>
<div :class="$style.indicators">
<div v-if="video.comment" :class="$style.indicator">ALT</div>
- <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
+ <div v-if="video.isSensitive" :class="$style.indicator" style="color: var(--MI_THEME-warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
</div>
<div :class="$style.videoControls" @click.self="togglePlayPause">
<div :class="[$style.controlsChild, $style.controlsLeft]">
@@ -511,7 +511,7 @@ onDeactivated(() => {
height: 100%;
pointer-events: none;
border-radius: inherit;
- box-shadow: inset 0 0 0 4px var(--warn);
+ box-shadow: inset 0 0 0 4px var(--MI_THEME-warn);
}
}
@@ -526,10 +526,10 @@ onDeactivated(() => {
}
.indicator {
- /* Hardcode to black because either --bg or --fg makes it hard to read in dark/light mode */
+ /* Hardcode to black because either --MI_THEME-bg or --MI_THEME-fg makes it hard to read in dark/light mode */
background-color: black;
border-radius: 6px;
- color: var(--accentLighten);
+ color: var(--MI_THEME-accentLighten);
display: inline-block;
font-weight: bold;
font-size: 0.8em;
@@ -541,7 +541,7 @@ onDeactivated(() => {
position: absolute;
border-radius: var(--radius-sm);
background-color: black;
- color: var(--accentLighten);
+ color: var(--MI_THEME-accentLighten);
font-size: 12px;
opacity: .5;
padding: 5px 8px;
@@ -595,7 +595,7 @@ onDeactivated(() => {
opacity: 0;
transition: opacity .4s ease-in-out;
- background: var(--accent);
+ background: var(--MI_THEME-accent);
color: #fff;
padding: 1rem;
border-radius: 99rem;
@@ -661,12 +661,12 @@ onDeactivated(() => {
.controlButton {
padding: 6px;
- border-radius: calc(var(--radius) / 2);
+ border-radius: calc(var(--MI-radius) / 2);
transition: background-color .2s ease-in-out;
font-size: 1.05rem;
&:hover {
- background-color: var(--accent);
+ background-color: var(--MI_THEME-accent);
}
&:focus-visible {
diff --git a/packages/frontend/src/components/MkMention.vue b/packages/frontend/src/components/MkMention.vue
index de2048b6f2..0391c6bc39 100644
--- a/packages/frontend/src/components/MkMention.vue
+++ b/packages/frontend/src/components/MkMention.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :style="{ background: bgCss }" :behavior="navigationBehavior">
+<MkA v-user-preview="canonical" :class="[$style.root, { [$style.isMe]: isMe }]" :to="url" :behavior="navigationBehavior">
<img :class="$style.icon" :src="avatarUrl" alt="">
<span>
<span>@{{ username }}</span>
@@ -16,7 +16,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { toUnicode } from 'punycode';
import { computed } from 'vue';
-import tinycolor from 'tinycolor2';
import { host as localHost } from '@@/js/config.js';
import { $i } from '@/account.js';
import { defaultStore } from '@/store.js';
@@ -37,11 +36,7 @@ const isMe = $i && (
`@${props.username}@${toUnicode(props.host)}` === `@${$i.username}@${toUnicode(localHost)}`.toLowerCase()
);
-const bg = tinycolor(getComputedStyle(document.documentElement).getPropertyValue(isMe ? '--mentionMe' : '--mention'));
-bg.setAlpha(0.1);
-const bgCss = bg.toRgbString();
-
-const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
+const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages || defaultStore.state.dataSaver.avatar
? getStaticImageUrl(`/avatar/@${props.username}@${props.host}`)
: `/avatar/@${props.username}@${props.host}`,
);
@@ -52,11 +47,13 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages
display: inline-block;
padding: 4px 8px 4px 4px;
border-radius: var(--radius-ellipse);
- color: var(--mention);
+ color: var(--MI_THEME-mention);
+ background: color(from var(--MI_THEME-mention) srgb r g b / 0.1);
white-space: nowrap;
&.isMe {
- color: var(--mentionMe);
+ color: var(--MI_THEME-mentionMe);
+ background: color(from var(--MI_THEME-mentionMe) srgb r g b / 0.1);
}
}
diff --git a/packages/frontend/src/components/MkMenu.vue b/packages/frontend/src/components/MkMenu.vue
index fe6df7090c..ff5f9b9a5d 100644
--- a/packages/frontend/src/components/MkMenu.vue
+++ b/packages/frontend/src/components/MkMenu.vue
@@ -49,7 +49,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<div :class="$style.item_content">
<span :class="$style.item_content_text">{{ item.text }}</span>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</div>
</MkA>
<a
@@ -68,7 +68,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-if="item.icon" class="ti-fw" :class="[$style.icon, item.icon]"></i>
<div :class="$style.item_content">
<span :class="$style.item_content_text">{{ item.text }}</span>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</div>
</a>
<button
@@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<MkAvatar :user="item.user" :class="$style.avatar"/><MkUserName :user="item.user"/>
<div v-if="item.indicate" :class="$style.item_content">
- <span :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <span :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</div>
</button>
<button
@@ -161,7 +161,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAvatar v-if="item.avatar" :user="item.avatar" :class="$style.avatar"/>
<div :class="$style.item_content">
<span :class="$style.item_content_text">{{ item.text }}</span>
- <span v-if="item.indicate" :class="$style.indicator"><i class="_indicatorCircle"></i></span>
+ <span v-if="item.indicate" :class="$style.indicator" class="_blink"><i class="_indicatorCircle"></i></span>
</div>
</button>
</template>
@@ -437,9 +437,11 @@ onBeforeUnmount(() => {
&.big:not(.asDrawer) {
> .menu {
+ min-width: 230px;
+
> .item {
padding: 6px 20px;
- font-size: 1em;
+ font-size: 0.95em;
line-height: 24px;
}
}
@@ -505,7 +507,7 @@ onBeforeUnmount(() => {
overflow: hidden;
text-overflow: ellipsis;
text-decoration: none !important;
- color: var(--menuFg, var(--fg));
+ color: var(--menuFg, var(--MI_THEME-fg));
&::before {
content: "";
@@ -525,7 +527,7 @@ onBeforeUnmount(() => {
outline: none;
&:not(:hover):not(:active)::before {
- outline: var(--focus) solid 2px;
+ outline: var(--MI_THEME-focus) solid 2px;
outline-offset: -2px;
}
}
@@ -534,19 +536,19 @@ onBeforeUnmount(() => {
&:hover,
&:focus-visible:active,
&:focus-visible.active {
- color: var(--menuHoverFg, var(--accent));
+ color: var(--menuHoverFg, var(--MI_THEME-accent));
&::before {
- background-color: var(--menuHoverBg, var(--accentedBg));
+ background-color: var(--menuHoverBg, var(--MI_THEME-accentedBg));
}
}
&:not(:focus-visible):active,
&:not(:focus-visible).active {
- color: var(--menuActiveFg, var(--fgOnAccent));
+ color: var(--menuActiveFg, var(--MI_THEME-fgOnAccent));
&::before {
- background-color: var(--menuActiveBg, var(--accent));
+ background-color: var(--menuActiveBg, var(--MI_THEME-accent));
}
}
}
@@ -564,13 +566,13 @@ onBeforeUnmount(() => {
}
&.radio {
- --menuActiveFg: var(--accent);
- --menuActiveBg: var(--accentedBg);
+ --menuActiveFg: var(--MI_THEME-accent);
+ --menuActiveBg: var(--MI_THEME-accentedBg);
}
&.parent {
- --menuActiveFg: var(--accent);
- --menuActiveBg: var(--accentedBg);
+ --menuActiveFg: var(--MI_THEME-accent);
+ --menuActiveBg: var(--MI_THEME-accentedBg);
}
&.label {
@@ -635,14 +637,13 @@ onBeforeUnmount(() => {
.indicator {
display: flex;
align-items: center;
- color: var(--indicator);
+ color: var(--MI_THEME-indicator);
font-size: 12px;
- animation: global-blink 1s infinite;
}
.divider {
margin: 8px 0;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.radioIcon {
@@ -652,11 +653,11 @@ onBeforeUnmount(() => {
height: 1em;
vertical-align: -0.125em;
border-radius: 50%;
- border: solid 2px var(--divider);
- background-color: var(--panel);
+ border: solid 2px var(--MI_THEME-divider);
+ background-color: var(--MI_THEME-panel);
&.radioChecked {
- border-color: var(--accent);
+ border-color: var(--MI_THEME-accent);
&::after {
content: "";
@@ -668,7 +669,7 @@ onBeforeUnmount(() => {
width: 50%;
height: 50%;
border-radius: 50%;
- background-color: var(--accent);
+ background-color: var(--MI_THEME-accent);
}
}
}
diff --git a/packages/frontend/src/components/MkMiniChart.vue b/packages/frontend/src/components/MkMiniChart.vue
index 1b6f6cef31..7ea585ecc2 100644
--- a/packages/frontend/src/components/MkMiniChart.vue
+++ b/packages/frontend/src/components/MkMiniChart.vue
@@ -48,7 +48,7 @@ const polygonPoints = ref('');
const headX = ref<number | null>(null);
const headY = ref<number | null>(null);
const clock = ref<number | null>(null);
-const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
+const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
const color = accent.toRgbString();
function draw(): void {
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index f26959888b..fe9e1ce088 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -90,12 +90,12 @@ defineExpose({
display: flex;
flex-direction: column;
contain: content;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
--root-margin: 24px;
- --headerHeight: 46px;
- --headerHeightNarrow: 42px;
+ --MI_THEME-headerHeight: 46px;
+ --MI_THEME-headerHeightNarrow: 42px;
@media (max-width: 500px) {
--root-margin: 16px;
@@ -105,24 +105,24 @@ defineExpose({
.header {
display: flex;
flex-shrink: 0;
- background: var(--windowHeader);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ background: var(--MI_THEME-windowHeader);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
.headerButton {
- height: var(--headerHeight);
- width: var(--headerHeight);
+ height: var(--MI_THEME-headerHeight);
+ width: var(--MI_THEME-headerHeight);
@media (max-width: 500px) {
- height: var(--headerHeightNarrow);
- width: var(--headerHeightNarrow);
+ height: var(--MI_THEME-headerHeightNarrow);
+ width: var(--MI_THEME-headerHeightNarrow);
}
}
.title {
flex: 1;
- line-height: var(--headerHeight);
+ line-height: var(--MI_THEME-headerHeight);
padding-left: 32px;
font-weight: bold;
white-space: nowrap;
@@ -131,7 +131,7 @@ defineExpose({
pointer-events: none;
@media (max-width: 500px) {
- line-height: var(--headerHeightNarrow);
+ line-height: var(--MI_THEME-headerHeightNarrow);
padding-left: 16px;
}
}
@@ -143,7 +143,7 @@ defineExpose({
.body {
flex: 1;
overflow: auto;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
container-type: size;
}
</style>
diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue
index 7ba4f0b9d9..55259406f8 100644
--- a/packages/frontend/src/components/MkNote.vue
+++ b/packages/frontend/src/components/MkNote.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-show="!isDeleted"
ref="rootEl"
v-hotkey="keymap"
- :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover }]"
+ :class="[$style.root, { [$style.showActionsOnlyHover]: defaultStore.state.showNoteActionsOnlyHover, [$style.skipRender]: defaultStore.state.skipNoteRender }]"
:tabindex="isDeleted ? '-1' : '0'"
>
<div v-if="appearNote.reply && inReplyToCollapsed" :class="$style.collapsedInReplyTo">
@@ -156,8 +156,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button ref="reactButton" :class="$style.footerButton" class="_button" @click="toggleReact()" @click.stop>
- <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
- <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
+ <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
+ <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p>
@@ -201,6 +201,9 @@ import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } fro
import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
import { isLink } from '@@/js/is-link.js';
+import { shouldCollapsed } from '@@/js/collapsed.js';
+import { host } from '@@/js/config.js';
+import type { MenuItem } from '@/types/menu.js';
import MkNoteSub from '@/components/MkNoteSub.vue';
import MkNoteHeader from '@/components/MkNoteHeader.vue';
import MkNoteSimple from '@/components/MkNoteSimple.vue';
@@ -233,13 +236,10 @@ import { deepClone } from '@/scripts/clone.js';
import { useTooltip } from '@/scripts/use-tooltip.js';
import { claimAchievement } from '@/scripts/achievements.js';
import { getNoteSummary } from '@/scripts/get-note-summary.js';
-import type { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
import { useRouter } from '@/router/supplier.js';
import { boostMenuItems, type Visibility } from '@/scripts/boost-quote.js';
-import { shouldCollapsed } from '@@/js/collapsed.js';
-import { host } from '@@/js/config.js';
import { isEnabledUrlPreview } from '@/instance.js';
import { type Keymap } from '@/scripts/hotkey.js';
import { focusPrev, focusNext } from '@/scripts/focus.js';
@@ -877,14 +877,6 @@ function emitUpdReaction(emoji: string, delta: number) {
overflow: clip;
contain: content;
- // ã“ã‚Œã‚‰ã®æŒ‡å®šã¯ãƒ‘フォーマンスå‘上ã«ã¯æœ‰åйã ãŒã€ãƒŽãƒ¼ãƒˆã®é«˜ã•ã¯ä¸€å®šã§ãªã„ãŸã‚ã€
- // ä¸‹ã®æ–¹ã¾ã§ã‚¹ã‚¯ãƒ­ãƒ¼ãƒ«ã™ã‚‹ã¨ä¸Šã®ãƒŽãƒ¼ãƒˆã®é«˜ã•ãŒã“ã“ã§æ±ºã‚打ã¡ã•れãŸã‚‚ã®ã«å¤‰åŒ–ã—ã€è¡¨ç¤ºã—ã¦ã„るノートã®ä½ç½®ãŒå¤‰ã‚ã£ã¦ã—ã¾ã†
- // ノートãŒãƒžã‚¦ãƒ³ãƒˆã•れãŸã¨ãã«è‡ªèº«ã®é«˜ã•ã‚’å–å¾—ã— contain-intrinsic-size を設定ã—ãªãŠã›ã°ã»ã¼è§£æ±ºã§ããã†ã ãŒã€
- // 今度ã¯ãã®å‡¦ç†è‡ªä½“ãŒãƒ‘フォーマンス低下ã®åŽŸå› ã«ãªã‚‰ãªã„ã‹æ‡¸å¿µã•れる。ã¾ãŸã€è¢«ãƒªã‚¢ã‚¯ã‚·ãƒ§ãƒ³ã§ã‚‚高ã•ã¯å¤‰åŒ–ã™ã‚‹ãŸã‚ã€ã‚„ã¯ã‚Šå¤šå°‘ã®ã‚ºãƒ¬ã¯ç”Ÿã˜ã‚‹
- // 一度レンダリングã•れãŸè¦ç´ ã¯ãƒ–ラウザãŒã‚ˆã—ãªã«ã‚µã‚¤ã‚ºã‚’覚ãˆã¦ãŠã„ã¦ãれるよã†ãªå®Ÿè£…ã«ãªã‚‹ã¾ã§å¾…ã£ãŸæ–¹ãŒè‰¯ã•ãã†(ãªã‚‹ã®ã‹ï¼Ÿ)
- //content-visibility: auto;
- //contain-intrinsic-size: 0 128px;
-
&:focus-visible {
outline: none;
@@ -901,8 +893,8 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: auto;
width: calc(100% - 8px);
height: calc(100% - 8px);
- border: dashed 2px var(--focus);
- border-radius: var(--radius);
+ border: dashed 2px var(--MI_THEME-focus);
+ border-radius: var(--MI-radius);
box-sizing: border-box;
}
}
@@ -929,9 +921,9 @@ function emitUpdReaction(emoji: string, delta: number) {
right: 12px;
padding: 0 4px;
margin-bottom: 0 !important;
- background: var(--popup);
+ background: var(--MI_THEME-popup);
border-radius: var(--radius-sm);
- box-shadow: 0px 4px 32px var(--shadow);
+ box-shadow: 0px 4px 32px var(--MI_THEME-shadow);
}
.footerButton {
@@ -950,6 +942,11 @@ function emitUpdReaction(emoji: string, delta: number) {
}
}
+.skipRender {
+ content-visibility: auto;
+ contain-intrinsic-size: 0 150px;
+}
+
.tip {
display: flex;
align-items: center;
@@ -976,7 +973,7 @@ function emitUpdReaction(emoji: string, delta: number) {
padding: 16px 32px 8px 32px;
line-height: 28px;
white-space: pre;
- color: var(--renote);
+ color: var(--MI_THEME-renote);
& + .article {
padding-top: 8px;
@@ -1081,7 +1078,7 @@ function emitUpdReaction(emoji: string, delta: number) {
width: var(--avatar);
height: var(--avatar);
position: sticky !important;
- top: calc(22px + var(--stickyTop, 0px));
+ top: calc(22px + var(--MI-stickyTop, 0px));
left: 0;
}
@@ -1101,12 +1098,12 @@ function emitUpdReaction(emoji: string, delta: number) {
width: 100%;
margin-top: 14px;
position: sticky;
- bottom: calc(var(--stickyBottom, 0px) - 100px);
+ bottom: calc(var(--MI-stickyBottom, 0px) - 100px);
}
.showLessLabel {
display: inline-block;
- background: var(--popup);
+ background: var(--MI_THEME-popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: var(--radius-ellipse);
@@ -1127,16 +1124,16 @@ function emitUpdReaction(emoji: string, delta: number) {
z-index: 2;
width: 100%;
height: 64px;
- //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+ //background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
&:hover > .collapsedLabel {
- background: var(--panelHighlight);
+ background: var(--MI_THEME-panelHighlight);
}
}
.collapsedLabel {
display: inline-block;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: var(--radius-ellipse);
@@ -1149,13 +1146,13 @@ function emitUpdReaction(emoji: string, delta: number) {
}
.replyIcon {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
margin-right: 0.5em;
}
.translation {
- border: solid 0.5px var(--divider);
- border-radius: var(--radius);
+ border: solid 0.5px var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
padding: 12px;
margin-top: 8px;
}
@@ -1178,7 +1175,7 @@ function emitUpdReaction(emoji: string, delta: number) {
.quoteNote {
padding: 16px;
- border: dashed 1px var(--renote);
+ border: dashed 1px var(--MI_THEME-renote);
border-radius: var(--radius-sm);
overflow: clip;
}
@@ -1202,7 +1199,7 @@ function emitUpdReaction(emoji: string, delta: number) {
}
&:hover {
- color: var(--fgHighlighted);
+ color: var(--MI_THEME-fgHighlighted);
}
}
@@ -1277,7 +1274,7 @@ function emitUpdReaction(emoji: string, delta: number) {
margin: 0 10px 0 0;
width: 46px;
height: 46px;
- top: calc(14px + var(--stickyTop, 0px));
+ top: calc(14px + var(--MI-stickyTop, 0px));
}
}
diff --git a/packages/frontend/src/components/MkNoteDetailed.vue b/packages/frontend/src/components/MkNoteDetailed.vue
index 5acb18c871..1e0c78e82e 100644
--- a/packages/frontend/src/components/MkNoteDetailed.vue
+++ b/packages/frontend/src/components/MkNoteDetailed.vue
@@ -157,8 +157,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<i class="ph-heart ph-bold ph-lg"></i>
</button>
<button ref="reactButton" :class="$style.noteFooterButton" class="_button" @click="toggleReact()">
- <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--love);"></i>
- <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--accent);"></i>
+ <i v-if="appearNote.reactionAcceptance === 'likeOnly' && appearNote.myReaction != null" class="ti ti-heart-filled" style="color: var(--MI_THEME-love);"></i>
+ <i v-else-if="appearNote.myReaction != null" class="ti ti-minus" style="color: var(--MI_THEME-accent);"></i>
<i v-else-if="appearNote.reactionAcceptance === 'likeOnly'" class="ti ti-heart"></i>
<i v-else class="ph-smiley ph-bold ph-lg"></i>
<p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p>
@@ -843,8 +843,8 @@ function animatedMFM() {
margin: auto;
width: calc(100% - 8px);
height: calc(100% - 8px);
- border: dashed 2px var(--focus);
- border-radius: var(--radius);
+ border: dashed 2px var(--MI_THEME-focus);
+ border-radius: var(--MI-radius);
box-sizing: border-box;
}
}
@@ -874,7 +874,7 @@ function animatedMFM() {
padding: 16px 32px 8px 32px;
line-height: 28px;
white-space: pre;
- color: var(--renote);
+ color: var(--MI_THEME-renote);
}
.renoteAvatar {
@@ -956,7 +956,7 @@ function animatedMFM() {
padding: 4px 6px;
font-size: 80%;
line-height: 1;
- border: solid 0.5px var(--divider);
+ border: solid 0.5px var(--MI_THEME-divider);
border-radius: var(--radius-xs);
}
@@ -989,19 +989,19 @@ function animatedMFM() {
}
.noteReplyTarget {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
margin-right: 0.5em;
}
.rn {
margin-left: 4px;
font-style: oblique;
- color: var(--renote);
+ color: var(--MI_THEME-renote);
}
.translation {
- border: solid 0.5px var(--divider);
- border-radius: var(--radius);
+ border: solid 0.5px var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
padding: 12px;
margin-top: 8px;
}
@@ -1016,7 +1016,7 @@ function animatedMFM() {
.quoteNote {
padding: 16px;
- border: dashed 1px var(--renote);
+ border: dashed 1px var(--MI_THEME-renote);
border-radius: var(--radius-sm);
overflow: clip;
}
@@ -1042,7 +1042,7 @@ function animatedMFM() {
}
&:hover {
- color: var(--fgHighlighted);
+ color: var(--MI_THEME-fgHighlighted);
}
}
@@ -1052,17 +1052,17 @@ function animatedMFM() {
opacity: 0.7;
&.reacted {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
.reply:not(:first-child) {
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.tabs {
- border-top: solid 0.5px var(--divider);
- border-bottom: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
display: flex;
}
@@ -1074,7 +1074,7 @@ function animatedMFM() {
}
.tabActive {
- border-bottom: solid 2px var(--accent);
+ border-bottom: solid 2px var(--MI_THEME-accent);
}
.tab_renotes {
@@ -1094,12 +1094,12 @@ function animatedMFM() {
.reactionTab {
padding: 4px 6px;
- border: solid 1px var(--divider);
+ border: solid 1px var(--MI_THEME-divider);
border-radius: var(--radius-sm);
}
.reactionTabActive {
- border-color: var(--accent);
+ border-color: var(--MI_THEME-accent);
}
@container (max-width: 500px) {
diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue
index cd6fdf576c..10107ba0b1 100644
--- a/packages/frontend/src/components/MkNoteHeader.vue
+++ b/packages/frontend/src/components/MkNoteHeader.vue
@@ -5,18 +5,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<header :class="$style.root">
- <component :is="defaultStore.state.enableCondensedLine ? 'MkCondensedLine' : 'div'" :minScale="0.5" style="min-width: 0;">
- <div style="display: flex; white-space: nowrap; align-items: baseline;">
- <div v-if="mock" :class="$style.name">
- <MkUserName :user="note.user"/>
- </div>
- <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
- <MkUserName :user="note.user"/>
- </MkA>
- <div v-if="note.user.isBot" :class="$style.isBot">bot</div>
- <div :class="$style.username"><MkAcct :user="note.user"/></div>
- </div>
- </component>
+ <div v-if="mock" :class="$style.name">
+ <MkUserName :user="note.user"/>
+ </div>
+ <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)">
+ <MkUserName :user="note.user"/>
+ </MkA>
+ <div v-if="note.user.isBot" :class="$style.isBot">bot</div>
+ <div :class="$style.username"><MkAcct :user="note.user"/></div>
<div v-if="note.user.badgeRoles" :class="$style.badgeRoles">
<img v-for="(role, i) in note.user.badgeRoles" :key="i" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl!"/>
</div>
@@ -95,7 +91,7 @@ const mock = inject<boolean>('mock', false);
margin: 0 .5em 0 0;
padding: 1px 6px;
font-size: 80%;
- border: solid 0.5px var(--divider);
+ border: solid 0.5px var(--MI_THEME-divider);
border-radius: var(--radius-xs);
}
diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue
index 542e3e79ea..4a4cdef679 100644
--- a/packages/frontend/src/components/MkNoteSimple.vue
+++ b/packages/frontend/src/components/MkNoteSimple.vue
@@ -58,7 +58,7 @@ watch(() => props.expandAllCws, (expandAllCws) => {
height: 34px;
border-radius: var(--radius-sm);
position: sticky !important;
- top: calc(16px + var(--stickyTop, 0px));
+ top: calc(16px + var(--MI-stickyTop, 0px));
left: 0;
}
diff --git a/packages/frontend/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue
index 45276839ad..c0be406893 100644
--- a/packages/frontend/src/components/MkNoteSub.vue
+++ b/packages/frontend/src/components/MkNoteSub.vue
@@ -510,7 +510,7 @@ if (props.detail) {
}
.reply, .more {
- border-left: solid 0.5px var(--divider);
+ border-left: solid 0.5px var(--MI_THEME-divider);
margin-top: 10px;
}
@@ -531,7 +531,7 @@ if (props.detail) {
.muted {
text-align: center;
padding: 8px !important;
- border: 1px solid var(--divider);
+ border: 1px solid var(--MI_THEME-divider);
margin: 8px 8px 0 8px;
border-radius: var(--radius-sm);
}
diff --git a/packages/frontend/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue
index 15173fbd99..4144e69d1e 100644
--- a/packages/frontend/src/components/MkNotes.vue
+++ b/packages/frontend/src/components/MkNotes.vue
@@ -64,17 +64,17 @@ defineExpose({
border-radius: var(--radius);
> .notes {
- background: color-mix(in srgb, var(--panel) 65%, transparent);
+ background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent);
}
}
&:not(.noGap) {
> .notes {
- background: var(--bg);
+ background: var(--MI_THEME-bg);
.note {
- background: color-mix(in srgb, var(--panel) 65%, transparent);
- border-radius: var(--radius);
+ background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent);
+ border-radius: var(--MI-radius);
}
}
}
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index 7bec9bdc65..ed66360d0e 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -7,13 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<div :class="$style.head">
<MkAvatar v-if="['pollEnded', 'note', 'edited'].includes(notification.type) && 'note' in notification" :class="$style.icon" :user="notification.note.user" link preview/>
- <MkAvatar v-else-if="['roleAssigned', 'achievementEarned'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
- <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
+ <MkAvatar v-else-if="['roleAssigned', 'achievementEarned', 'exportCompleted', 'login'].includes(notification.type)" :class="$style.icon" :user="$i" link preview/>
+ <div v-else-if="notification.type === 'reaction:grouped' && notification.note.reactionAcceptance === 'likeOnly'" :class="[$style.icon, $style.icon_reactionGroupHeart]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'reaction:grouped'" :class="[$style.icon, $style.icon_reactionGroup]"><i class="ph-smiley ph-bold ph-lg" style="line-height: 1;"></i></div>
<div v-else-if="notification.type === 'renote:grouped'" :class="[$style.icon, $style.icon_renoteGroup]"><i class="ti ti-repeat" style="line-height: 1;"></i></div>
<img v-else-if="notification.type === 'test'" :class="$style.icon" :src="infoImageUrl"/>
<MkAvatar v-else-if="'user' in notification" :class="$style.icon" :user="notification.user" link preview/>
- <MkAvatar v-else-if="notification.type === 'exportCompleted'" :class="$style.icon" :user="$i" link preview/>
<img v-else-if="'icon' in notification && notification.icon != null" :class="[$style.icon, $style.icon_app]" :src="notification.icon" alt=""/>
<div
:class="[$style.subIcon, {
@@ -27,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
[$style.t_pollEnded]: notification.type === 'pollEnded',
[$style.t_achievementEarned]: notification.type === 'achievementEarned',
[$style.t_exportCompleted]: notification.type === 'exportCompleted',
+ [$style.t_login]: notification.type === 'login',
[$style.t_roleAssigned]: notification.type === 'roleAssigned' && notification.role.iconUrl == null,
[$style.t_pollEnded]: notification.type === 'edited',
}]"
@@ -41,6 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<i v-else-if="notification.type === 'pollEnded'" class="ti ti-chart-arrows"></i>
<i v-else-if="notification.type === 'achievementEarned'" class="ti ti-medal"></i>
<i v-else-if="notification.type === 'exportCompleted'" class="ti ti-archive"></i>
+ <i v-else-if="notification.type === 'login'" class="ti ti-login-2"></i>
<template v-else-if="notification.type === 'roleAssigned'">
<img v-if="notification.role.iconUrl" style="height: 1.3em; vertical-align: -22%;" :src="notification.role.iconUrl" alt=""/>
<i v-else class="ti ti-badges"></i>
@@ -62,6 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-else-if="notification.type === 'note'">{{ i18n.ts._notification.newNote }}: <MkUserName :user="notification.note.user"/></span>
<span v-else-if="notification.type === 'roleAssigned'">{{ i18n.ts._notification.roleAssigned }}</span>
<span v-else-if="notification.type === 'achievementEarned'">{{ i18n.ts._notification.achievementEarned }}</span>
+ <span v-else-if="notification.type === 'login'">{{ i18n.ts._notification.login }}</span>
<span v-else-if="notification.type === 'test'">{{ i18n.ts._notification.testNotification }}</span>
<span v-else-if="notification.type === 'exportCompleted'">{{ i18n.tsx._notification.exportOfXCompleted({ x: exportEntityName[notification.exportedEntity] }) }}</span>
<MkA v-else-if="notification.type === 'follow' || notification.type === 'mention' || notification.type === 'reply' || notification.type === 'renote' || notification.type === 'quote' || notification.type === 'reaction' || notification.type === 'receiveFollowRequest' || notification.type === 'followRequestAccepted'" v-user-preview="notification.user.id" :class="$style.headerName" :to="userPage(notification.user)"><MkUserName :user="notification.user"/></MkA>
@@ -228,13 +230,16 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
overflow-wrap: break-word;
display: flex;
contain: content;
+ content-visibility: auto;
+ contain-intrinsic-size: 0 100px;
--eventFollow: #36aed2;
--eventRenote: #36d298;
--eventReply: #007aff;
- --eventReactionHeart: var(--love);
+ --eventReactionHeart: var(--MI_THEME-love);
--eventReaction: #e99a0b;
--eventAchievement: #cb9a11;
+ --eventLogin: #007aff;
--eventOther: #88a6b7;
}
@@ -291,8 +296,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
height: 20px;
box-sizing: border-box;
border-radius: var(--radius-full);
- background: var(--panel);
- box-shadow: 0 0 0 3px var(--panel);
+ background: var(--MI_THEME-panel);
+ box-shadow: 0 0 0 3px var(--MI_THEME-panel);
font-size: 11px;
text-align: center;
color: #fff;
@@ -356,6 +361,12 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
pointer-events: none;
}
+.t_login {
+ padding: 3px;
+ background: var(--eventLogin);
+ pointer-events: none;
+}
+
.tail {
flex: 1;
min-width: 0;
@@ -438,8 +449,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification)
height: 20px;
box-sizing: border-box;
border-radius: var(--radius-full);
- background: var(--panel);
- box-shadow: 0 0 0 3px var(--panel);
+ background: var(--MI_THEME-panel);
+ box-shadow: 0 0 0 3px var(--MI_THEME-panel);
font-size: 11px;
text-align: center;
color: #fff;
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index a395734add..51c4ea7ce4 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -111,6 +111,6 @@ defineExpose({
<style lang="scss" module>
.list {
- background: var(--panel);
+ background: var(--MI_THEME-panel);
}
</style>
diff --git a/packages/frontend/src/components/MkNumberDiff.vue b/packages/frontend/src/components/MkNumberDiff.vue
index 1825cc5405..80c634fdce 100644
--- a/packages/frontend/src/components/MkNumberDiff.vue
+++ b/packages/frontend/src/components/MkNumberDiff.vue
@@ -24,11 +24,11 @@ const isZero = computed(() => props.value === 0);
<style lang="scss" module>
.isPlus {
- color: var(--success);
+ color: var(--MI_THEME-success);
}
.isMinus {
- color: var(--error);
+ color: var(--MI_THEME-error);
}
.isZero {
diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue
index 870599aa94..dabdd324fd 100644
--- a/packages/frontend/src/components/MkObjectView.value.vue
+++ b/packages/frontend/src/components/MkObjectView.value.vue
@@ -78,7 +78,7 @@ function collapsable(v): boolean {
> .boolean {
display: inline;
- color: var(--codeBoolean);
+ color: var(--MI_THEME-codeBoolean);
&.true {
font-weight: bold;
@@ -91,12 +91,12 @@ function collapsable(v): boolean {
> .string {
display: inline;
- color: var(--codeString);
+ color: var(--MI_THEME-codeString);
}
> .number {
display: inline;
- color: var(--codeNumber);
+ color: var(--MI_THEME-codeNumber);
}
> .array.empty {
@@ -127,7 +127,7 @@ function collapsable(v): boolean {
> .toggle {
width: 16px;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
visibility: hidden;
&.visible {
diff --git a/packages/frontend/src/components/MkOmit.vue b/packages/frontend/src/components/MkOmit.vue
index 94cbaf5c91..e19c34ba87 100644
--- a/packages/frontend/src/components/MkOmit.vue
+++ b/packages/frontend/src/components/MkOmit.vue
@@ -47,7 +47,7 @@ onUnmounted(() => {
<style lang="scss" module>
.content {
- --stickyTop: 0px;
+ --MI-stickyTop: 0px;
&.omitted {
position: relative;
@@ -62,11 +62,11 @@ onUnmounted(() => {
left: 0;
width: 100%;
height: 64px;
- //background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+ //background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: var(--radius-ellipse);
@@ -75,7 +75,7 @@ onUnmounted(() => {
&:hover {
> .fadeLabel {
- background: var(--panelHighlight);
+ background: var(--MI_THEME-panelHighlight);
}
}
}
diff --git a/packages/frontend/src/components/MkPagePreview.vue b/packages/frontend/src/components/MkPagePreview.vue
index 8559d4b96e..35a37a1f7d 100644
--- a/packages/frontend/src/components/MkPagePreview.vue
+++ b/packages/frontend/src/components/MkPagePreview.vue
@@ -42,7 +42,7 @@ const props = defineProps<{
.eyeCatchingImageRoot {
width: 100%;
height: 200px;
- border-radius: var(--radius) var(--radius) 0 0;
+ border-radius: var(--MI-radius) var(--MI-radius) 0 0;
overflow: hidden;
}
</style>
@@ -54,7 +54,7 @@ const props = defineProps<{
&:hover {
text-decoration: none;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
&:focus-within {
@@ -67,22 +67,22 @@ const props = defineProps<{
left: 0;
width: 100%;
height: 100%;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
pointer-events: none;
- box-shadow: inset 0 0 0 2px var(--focus);
+ box-shadow: inset 0 0 0 2px var(--MI_THEME-focus);
}
}
> .thumbnail {
& + article {
- border-radius: 0 0 var(--radius) var(--radius);
+ border-radius: 0 0 var(--MI-radius) var(--MI-radius);
}
}
> article {
- background-color: var(--panel);
+ background-color: var(--MI_THEME-panel);
padding: 16px;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
> header {
margin-bottom: 8px;
@@ -115,7 +115,6 @@ const props = defineProps<{
> p {
display: inline-block;
margin: 0;
- color: var(--urlPreviewInfo);
font-size: 0.8em;
line-height: 16px;
vertical-align: top;
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index f67a1e5b63..4aac283ecd 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -181,8 +181,8 @@ defineExpose({
overscroll-behavior: contain;
min-height: 100%;
- background: var(--bg);
+ background: var(--MI_THEME-bg);
- --margin: var(--marginHalf);
+ --MI-margin: var(--MI-marginHalf);
}
</style>
diff --git a/packages/frontend/src/components/MkPoll.vue b/packages/frontend/src/components/MkPoll.vue
index 592a511fb0..e11fb4fc99 100644
--- a/packages/frontend/src/components/MkPoll.vue
+++ b/packages/frontend/src/components/MkPoll.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<li v-for="(choice, i) in props.poll.choices" :key="i" :class="$style.choice" @click="vote(i)">
<div :class="$style.bg" :style="{ 'width': `${showResult ? (choice.votes / total * 100) : 0}%` }"></div>
<span :class="$style.fg">
- <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--accent);"></i></template>
+ <template v-if="choice.isVoted"><i class="ti ti-check" style="margin-right: 4px; color: var(--MI_THEME-accent);"></i></template>
<Mfm :text="choice.text" :plain="true"/>
<span v-if="showResult" style="margin-left: 4px; opacity: 0.7;">({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})</span>
</span>
@@ -139,8 +139,8 @@ const refreshVotes = async () => {
position: relative;
margin: 4px 0;
padding: 4px;
- //border: solid 0.5px var(--divider);
- background: var(--accentedBg);
+ //border: solid 0.5px var(--MI_THEME-divider);
+ background: var(--MI_THEME-accentedBg);
border-radius: var(--radius-xs);
overflow: clip;
cursor: pointer;
@@ -151,8 +151,8 @@ const refreshVotes = async () => {
top: 0;
left: 0;
height: 100%;
- background: var(--accent);
- background: linear-gradient(90deg,var(--buttonGradateA),var(--buttonGradateB));
+ background: var(--MI_THEME-accent);
+ background: linear-gradient(90deg,var(--MI_THEME-buttonGradateA),var(--MI_THEME-buttonGradateB));
transition: width 1s ease;
}
@@ -160,12 +160,12 @@ const refreshVotes = async () => {
position: relative;
display: inline-block;
padding: 3px 5px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: var(--radius-xs);
}
.info {
- color: var(--fg);
+ color: var(--MI_THEME-fg);
}
.done {
diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue
index 4a29b27ac4..b7d67f19ad 100644
--- a/packages/frontend/src/components/MkPostForm.vue
+++ b/packages/frontend/src/components/MkPostForm.vue
@@ -1175,7 +1175,7 @@ defineExpose({
outline: none;
.submitInner {
- outline: 2px solid var(--fgOnAccent);
+ outline: 2px solid var(--MI_THEME-fgOnAccent);
outline-offset: -4px;
}
}
@@ -1190,13 +1190,13 @@ defineExpose({
&:not(:disabled):hover {
> .inner {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
&:not(:disabled):active {
> .inner {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
}
@@ -1218,8 +1218,8 @@ defineExpose({
border-radius: var(--radius-sm);
min-width: 90px;
box-sizing: border-box;
- color: var(--fgOnAccent);
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ color: var(--MI_THEME-fgOnAccent);
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
}
.headerRightItem {
@@ -1228,7 +1228,7 @@ defineExpose({
border-radius: var(--radius-sm);
&:hover {
- background: var(--X5);
+ background: var(--MI_THEME-X5);
}
&:disabled {
@@ -1272,7 +1272,7 @@ defineExpose({
.withQuote {
margin: 0 0 8px 0;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
.toSpecified {
@@ -1292,7 +1292,7 @@ defineExpose({
margin-right: 14px;
padding: 8px 0 8px 8px;
border-radius: var(--radius-sm);
- background: var(--X4);
+ background: var(--MI_THEME-X4);
}
.hasNotSpecifiedMentions {
@@ -1311,7 +1311,7 @@ defineExpose({
border: none;
border-radius: 0;
background: transparent;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
font-family: inherit;
&:focus {
@@ -1326,7 +1326,7 @@ defineExpose({
.cwFrame {
z-index: 1;
padding-bottom: 8px;
- border-bottom: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
width: 100%;
position: relative;
@@ -1336,7 +1336,7 @@ defineExpose({
z-index: 1;
padding-top: 8px;
padding-bottom: 8px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.textOuter {
@@ -1362,7 +1362,7 @@ defineExpose({
right: 2px;
padding: 4px 6px;
font-size: .9em;
- color: var(--warn);
+ color: var(--MI_THEME-warn);
border-radius: var(--radius-sm);
min-width: 1.6em;
text-align: center;
@@ -1406,16 +1406,16 @@ defineExpose({
border-radius: var(--radius-sm);
&:hover {
- background: var(--X5);
+ background: var(--MI_THEME-X5);
}
&.footerButtonActive {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
.previewButtonActive {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
@container (max-width: 500px) {
diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue
index f90fcfef33..a601a110fa 100644
--- a/packages/frontend/src/components/MkPostFormAttaches.vue
+++ b/packages/frontend/src/components/MkPostFormAttaches.vue
@@ -216,7 +216,7 @@ function showFileMenu(file: Misskey.entities.DriveFile, ev: MouseEvent | Keyboar
width: 100%;
height: 100%;
z-index: 1;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
}
.sensitive {
diff --git a/packages/frontend/src/components/MkRadio.vue b/packages/frontend/src/components/MkRadio.vue
index e02f76a58f..838eaee292 100644
--- a/packages/frontend/src/components/MkRadio.vue
+++ b/packages/frontend/src/components/MkRadio.vue
@@ -53,9 +53,9 @@ function toggle(): void {
cursor: pointer;
padding: 7px 10px;
min-width: 60px;
- background-color: var(--panel);
+ background-color: var(--MI_THEME-panel);
background-clip: padding-box !important;
- border: solid 1px var(--panel);
+ border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
font-size: 90%;
transition: all 0.2s;
@@ -67,25 +67,25 @@ function toggle(): void {
}
&:hover {
- border-color: var(--inputBorderHover) !important;
+ border-color: var(--MI_THEME-inputBorderHover) !important;
}
&:focus-within {
outline: none;
- box-shadow: 0 0 0 2px var(--focus);
+ box-shadow: 0 0 0 2px var(--MI_THEME-focus);
}
&.checked {
- background-color: var(--accentedBg) !important;
- border-color: var(--accentedBg) !important;
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg) !important;
+ border-color: var(--MI_THEME-accentedBg) !important;
+ color: var(--MI_THEME-accent);
cursor: default !important;
> .button {
- border-color: var(--accent);
+ border-color: var(--MI_THEME-accent);
&::after {
- background-color: var(--accent);
+ background-color: var(--MI_THEME-accent);
transform: scale(1);
opacity: 1;
}
@@ -106,7 +106,7 @@ function toggle(): void {
width: 14px;
height: 14px;
background: none;
- border: solid 2px var(--inputBorder);
+ border: solid 2px var(--MI_THEME-inputBorder);
border-radius: var(--radius-full);
transition: inherit;
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index 705c93f770..af81eb814d 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -77,7 +77,7 @@ export default defineComponent({
> .caption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
diff --git a/packages/frontend/src/components/MkRange.vue b/packages/frontend/src/components/MkRange.vue
index 22c187c357..72e9aa6c0b 100644
--- a/packages/frontend/src/components/MkRange.vue
+++ b/packages/frontend/src/components/MkRange.vue
@@ -212,7 +212,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
> .caption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@@ -224,8 +224,8 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
> .body {
padding: 7px 12px;
- background: var(--panel);
- border: solid 1px var(--panel);
+ background: var(--MI_THEME-panel);
+ border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
> .container {
@@ -250,7 +250,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
top: 0;
left: 0;
height: 100%;
- background: var(--accent);
+ background: var(--MI_THEME-accent);
opacity: 0.5;
}
}
@@ -272,7 +272,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
width: $tickWidth;
height: 3px;
margin-left: - math.div($tickWidth, 2);
- background: var(--divider);
+ background: var(--MI_THEME-divider);
border-radius: var(--radius-ellipse);
}
}
@@ -282,11 +282,11 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
width: $thumbWidth;
height: $thumbHeight;
cursor: grab;
- background: var(--accent);
+ background: var(--MI_THEME-accent);
border-radius: var(--radius-ellipse);
&:hover {
- background: var(--accentLighten);
+ background: var(--MI_THEME-accentLighten);
}
}
}
diff --git a/packages/frontend/src/components/MkReactionEffect.vue b/packages/frontend/src/components/MkReactionEffect.vue
index 361e246e9f..5a59a5e055 100644
--- a/packages/frontend/src/components/MkReactionEffect.vue
+++ b/packages/frontend/src/components/MkReactionEffect.vue
@@ -60,7 +60,7 @@ onMounted(() => {
right: 0;
bottom: 0;
margin: auto;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
font-size: 18px;
font-weight: bold;
transform: translateY(-30px);
diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index 6fdeb3a3ab..a12bb55fa3 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -57,7 +57,7 @@ function getReactionName(reaction: string): string {
max-width: 100px;
padding-right: 10px;
text-align: center;
- border-right: solid 0.5px var(--divider);
+ border-right: solid 0.5px var(--MI_THEME-divider);
}
.reactionIcon {
diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
index 957ee0e76b..32ab8ac3c3 100644
--- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue
@@ -180,7 +180,7 @@ if (!mock) {
justify-content: center;
&.canToggle {
- background: var(--buttonBg);
+ background: var(--MI_THEME-buttonBg);
&:hover {
background: rgba(0, 0, 0, 0.1);
@@ -214,12 +214,12 @@ if (!mock) {
}
&.reacted, &.reacted:hover {
- background: var(--accentedBg);
- color: var(--accent);
- box-shadow: 0 0 0 1px var(--accent) inset;
+ background: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
+ box-shadow: 0 0 0 1px var(--MI_THEME-accent) inset;
> .count {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
> .icon {
diff --git a/packages/frontend/src/components/MkRemoteCaution.vue b/packages/frontend/src/components/MkRemoteCaution.vue
index 2b59eab9d9..6391468204 100644
--- a/packages/frontend/src/components/MkRemoteCaution.vue
+++ b/packages/frontend/src/components/MkRemoteCaution.vue
@@ -19,15 +19,15 @@ defineProps<{
.root {
font-size: 0.8em;
padding: 16px;
- background: color-mix(in srgb, var(--infoWarnBg) 65%, transparent);
- color: var(--infoWarnFg);
- border-radius: var(--radius);
+ background: color-mix(in srgb, var(--MI_THEME-infoWarnBg) 65%, transparent);
+ color: var(--MI_THEME-infoWarnFg);
+ border-radius: var(--MI-radius);
overflow: clip;
z-index: 1;
}
.link {
margin-left: 4px;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
</style>
diff --git a/packages/frontend/src/components/MkRetentionLineChart.vue b/packages/frontend/src/components/MkRetentionLineChart.vue
index c3daa9c9a4..d41793b0fa 100644
--- a/packages/frontend/src/components/MkRetentionLineChart.vue
+++ b/packages/frontend/src/components/MkRetentionLineChart.vue
@@ -44,7 +44,7 @@ onMounted(async () => {
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
- const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--accent'));
+ const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
const color = accent.toHex();
if (chartEl.value == null) return;
diff --git a/packages/frontend/src/components/MkRippleEffect.vue b/packages/frontend/src/components/MkRippleEffect.vue
index ee5bb73ebf..2949cf156d 100644
--- a/packages/frontend/src/components/MkRippleEffect.vue
+++ b/packages/frontend/src/components/MkRippleEffect.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="$style.root" :style="{ zIndex, top: `${y - 64}px`, left: `${x - 64}px` }">
<svg width="128" height="128" viewBox="0 0 128 128" xmlns="http://www.w3.org/2000/svg">
- <circle fill="none" cx="64" cy="64" style="stroke: var(--accent);">
+ <circle fill="none" cx="64" cy="64" style="stroke: var(--MI_THEME-accent);">
<animate
attributeName="r"
begin="0s" dur="0.5s"
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
/>
</circle>
<g fill="none" fill-rule="evenodd">
- <circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--accent);">
+ <circle v-for="(particle, i) in particles" :key="i" :fill="particle.color" style="stroke: var(--MI_THEME-accent);">
<animate
attributeName="r"
begin="0s" dur="0.8s"
diff --git a/packages/frontend/src/components/MkRolePreview.vue b/packages/frontend/src/components/MkRolePreview.vue
index ce17ae08e0..3f14c5b5e0 100644
--- a/packages/frontend/src/components/MkRolePreview.vue
+++ b/packages/frontend/src/components/MkRolePreview.vue
@@ -6,8 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkA :to="forModeration ? `/admin/roles/${role.id}` : `/roles/${role.id}`" :class="$style.root" tabindex="-1" :style="{ '--color': role.color }">
<template v-if="forModeration">
- <i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--success)"></i>
- <i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--warn)"></i>
+ <i v-if="role.isPublic" class="ti ti-world" :class="$style.icon" style="color: var(--MI_THEME-success)"></i>
+ <i v-else class="ti ti-lock" :class="$style.icon" style="color: var(--MI_THEME-warn)"></i>
</template>
<div v-adaptive-bg class="_panel" :class="$style.body">
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<img :class="$style.bodyBadge" :src="role.iconUrl"/>
</template>
<template v-else>
- <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--accent);"></i>
- <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--accent);"></i>
+ <i v-if="role.isAdministrator" class="ti ti-crown" style="color: var(--MI_THEME-accent);"></i>
+ <i v-else-if="role.isModerator" class="ti ti-shield" style="color: var(--MI_THEME-accent);"></i>
<i v-else class="ti ti-user" style="opacity: 0.7;"></i>
</template>
</span>
diff --git a/packages/frontend/src/components/MkSelect.vue b/packages/frontend/src/components/MkSelect.vue
index 150a5c6d54..154fff6d2f 100644
--- a/packages/frontend/src/components/MkSelect.vue
+++ b/packages/frontend/src/components/MkSelect.vue
@@ -202,7 +202,7 @@ function show() {
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@@ -220,8 +220,8 @@ function show() {
&.focused {
> .inputCore {
- border-color: var(--accent) !important;
- //box-shadow: 0 0 0 4px var(--focus);
+ border-color: var(--MI_THEME-accent) !important;
+ //box-shadow: 0 0 0 4px var(--MI_THEME-focus);
}
}
@@ -240,7 +240,7 @@ function show() {
&:hover {
> .inputCore {
- border-color: var(--inputBorderHover) !important;
+ border-color: var(--MI_THEME-inputBorderHover) !important;
}
}
}
@@ -256,9 +256,9 @@ function show() {
font: inherit;
font-weight: normal;
font-size: 1em;
- color: var(--fg);
- background: var(--panel);
- border: solid 1px var(--panel);
+ color: var(--MI_THEME-fg);
+ background: var(--MI_THEME-panel);
+ border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
outline: none;
box-shadow: none;
diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue
new file mode 100644
index 0000000000..34c22abc31
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.input.vue
@@ -0,0 +1,206 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper" data-cy-signin-page-input>
+ <div :class="$style.root">
+ <div :class="$style.avatar">
+ <i class="ti ti-user"></i>
+ </div>
+
+ <!-- ログイン画é¢ãƒ¡ãƒƒã‚»ãƒ¼ã‚¸ -->
+ <MkInfo v-if="message">
+ {{ message }}
+ </MkInfo>
+
+ <!-- 外部サーãƒãƒ¼ã¸ã®è»¢é€ -->
+ <div v-if="openOnRemote" class="_gaps_m">
+ <div class="_gaps_s">
+ <MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
+ {{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
+ </MkButton>
+ <button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
+ {{ i18n.ts.specifyServerHost }}
+ </button>
+ </div>
+ <div :class="$style.orHr">
+ <p :class="$style.orMsg">{{ i18n.ts.or }}</p>
+ </div>
+ </div>
+
+ <!-- username入力 -->
+ <form class="_gaps_s" @submit.prevent="emit('usernameSubmitted', username)">
+ <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username>
+ <template #prefix>@</template>
+ <template #suffix>@{{ host }}</template>
+ </MkInput>
+ <MkButton type="submit" large primary rounded style="margin: 0 auto;" data-cy-signin-page-input-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </form>
+
+ <!-- パスワードレスログイン -->
+ <div :class="$style.orHr">
+ <p :class="$style.orMsg">{{ i18n.ts.or }}</p>
+ </div>
+ <div>
+ <MkButton type="submit" style="margin: auto auto;" large rounded primary gradate @click="emit('passkeyClick', $event)">
+ <i class="ti ti-device-usb" style="font-size: medium;"></i>{{ i18n.ts.signinWithPasskey }}
+ </MkButton>
+ </div>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+import { toUnicode } from 'punycode/';
+
+import { query, extractDomain } from '@@/js/url.js';
+import { host as configHost } from '@@/js/config.js';
+import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkInfo from '@/components/MkInfo.vue';
+
+const props = withDefaults(defineProps<{
+ message?: string,
+ openOnRemote?: OpenOnRemoteOptions,
+}>(), {
+ message: '',
+ openOnRemote: undefined,
+});
+
+const emit = defineEmits<{
+ (ev: 'usernameSubmitted', v: string): void;
+ (ev: 'passkeyClick', v: MouseEvent): void;
+}>();
+
+const host = toUnicode(configHost);
+
+const username = ref('');
+
+//#region Open on remote
+function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
+ switch (options.type) {
+ case 'web':
+ case 'lookup': {
+ let _path: string;
+
+ if (options.type === 'lookup') {
+ // TODO: v2024.7.0以é™ãŒæµ¸é€ã—ã¦ããŸã‚‰æ­£å¼ãªURLã«å¤‰æ›´ã™ã‚‹â–¼
+ // _path = `/lookup?uri=${encodeURIComponent(_path)}`;
+ _path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
+ } else {
+ _path = options.path;
+ }
+
+ if (targetHost) {
+ window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
+ } else {
+ window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
+ }
+ break;
+ }
+ case 'share': {
+ const params = query(options.params);
+ if (targetHost) {
+ window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
+ } else {
+ window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
+ }
+ break;
+ }
+ }
+}
+
+async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
+ const { canceled, result: hostTemp } = await os.inputText({
+ title: i18n.ts.inputHostName,
+ placeholder: 'misskey.example.com',
+ });
+
+ if (canceled) return;
+
+ let targetHost: string | null = hostTemp;
+
+ // ドメイン部分ã ã‘ã‚’å–り出ã™
+ targetHost = extractDomain(targetHost ?? '');
+ if (targetHost == null) {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.invalidValue,
+ text: i18n.ts.tryAgain,
+ });
+ return;
+ }
+ openRemote(options, targetHost);
+}
+//#endregion
+</script>
+
+<style lang="scss" module>
+.root {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.wrapper {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ min-height: 336px;
+
+ > .root {
+ width: 100%;
+ }
+}
+
+.avatar {
+ margin: 0 auto;
+ background-color: color-mix(in srgb, var(--MI_THEME-fg), transparent 85%);
+ color: color-mix(in srgb, var(--MI_THEME-fg), transparent 25%);
+ text-align: center;
+ height: 64px;
+ width: 64px;
+ font-size: 24px;
+ line-height: 64px;
+ border-radius: 50%;
+}
+
+.instanceManualSelectButton {
+ display: block;
+ text-align: center;
+ opacity: .7;
+ font-size: .8em;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.orHr {
+ position: relative;
+ margin: .4em auto;
+ width: 100%;
+ height: 1px;
+ background: var(--MI_THEME-divider);
+}
+
+.orMsg {
+ position: absolute;
+ top: -.6em;
+ display: inline-block;
+ padding: 0 1em;
+ background: var(--MI_THEME-panel);
+ font-size: 0.8em;
+ color: var(--MI_THEME-fgOnPanel);
+ margin: 0;
+ left: 50%;
+ transform: translateX(-50%);
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.passkey.vue b/packages/frontend/src/components/MkSignin.passkey.vue
new file mode 100644
index 0000000000..e5a56ab66d
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.passkey.vue
@@ -0,0 +1,92 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper">
+ <div class="_gaps" :class="$style.root">
+ <div class="_gaps_s">
+ <div :class="$style.passkeyIcon">
+ <i class="ti ti-fingerprint"></i>
+ </div>
+ <div :class="$style.passkeyDescription">{{ i18n.ts.useSecurityKey }}</div>
+ </div>
+
+ <MkButton large primary rounded :disabled="queryingKey" style="margin: 0 auto;" @click="queryKey">{{ i18n.ts.retry }}</MkButton>
+
+ <MkButton v-if="isPerformingPasswordlessLogin !== true" transparent rounded :disabled="queryingKey" style="margin: 0 auto;" @click="emit('useTotp')">{{ i18n.ts.useTotp }}</MkButton>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref, onMounted } from 'vue';
+import { get as webAuthnRequest } from '@github/webauthn-json/browser-ponyfill';
+
+import { i18n } from '@/i18n.js';
+
+import MkButton from '@/components/MkButton.vue';
+
+import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
+
+const props = defineProps<{
+ credentialRequest: CredentialRequestOptions;
+ isPerformingPasswordlessLogin?: boolean;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'done', credential: AuthenticationPublicKeyCredential): void;
+ (ev: 'useTotp'): void;
+}>();
+
+const queryingKey = ref(true);
+
+async function queryKey() {
+ queryingKey.value = true;
+ await webAuthnRequest(props.credentialRequest)
+ .catch(() => {
+ return Promise.reject(null);
+ })
+ .then((credential) => {
+ emit('done', credential);
+ })
+ .finally(() => {
+ queryingKey.value = false;
+ });
+}
+
+onMounted(() => {
+ queryKey();
+});
+</script>
+
+<style lang="scss" module>
+.wrapper {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ min-height: 336px;
+
+ > .root {
+ width: 100%;
+ }
+}
+
+.passkeyIcon {
+ margin: 0 auto;
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
+ text-align: center;
+ height: 64px;
+ width: 64px;
+ font-size: 24px;
+ line-height: 64px;
+ border-radius: 50%;
+}
+
+.passkeyDescription {
+ text-align: center;
+ font-size: 1.1em;
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue
new file mode 100644
index 0000000000..5608122a39
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.password.vue
@@ -0,0 +1,188 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper" data-cy-signin-page-password>
+ <div class="_gaps" :class="$style.root">
+ <div :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined }"></div>
+ <div :class="$style.welcomeBackMessage">
+ <I18n :src="i18n.ts.welcomeBackWithName" tag="span">
+ <template #name><Mfm :text="user.name ?? user.username" :plain="true"/></template>
+ </I18n>
+ </div>
+
+ <!-- password入力 -->
+ <form class="_gaps_s" @submit.prevent="onSubmit">
+ <!-- ブラウザ オートコンプリート用 -->
+ <input type="hidden" name="username" autocomplete="username" :value="user.username">
+
+ <MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required autofocus data-cy-signin-password>
+ <template #prefix><i class="ti ti-lock"></i></template>
+ <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
+ </MkInput>
+
+ <div v-if="needCaptcha">
+ <MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
+ <MkCaptcha v-if="instance.enableMcaptcha" ref="mcaptcha" v-model="mCaptchaResponse" :class="$style.captcha" provider="mcaptcha" :sitekey="instance.mcaptchaSiteKey" :instanceUrl="instance.mcaptchaInstanceUrl"/>
+ <MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
+ <MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
+ <MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
+ </div>
+
+ <MkButton type="submit" :disabled="needCaptcha && captchaFailed" large primary rounded style="margin: 0 auto;" data-cy-signin-page-password-continue>{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </form>
+ </div>
+</div>
+</template>
+
+<script lang="ts">
+export type PwResponse = {
+ password: string;
+ captcha: {
+ hCaptchaResponse: string | null;
+ mCaptchaResponse: string | null;
+ reCaptchaResponse: string | null;
+ turnstileResponse: string | null;
+ testcaptchaResponse: string | null;
+ };
+};
+</script>
+
+<script setup lang="ts">
+import { ref, computed, useTemplateRef, defineAsyncComponent } from 'vue';
+import * as Misskey from 'misskey-js';
+
+import { instance } from '@/instance.js';
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkCaptcha from '@/components/MkCaptcha.vue';
+
+const props = defineProps<{
+ user: Misskey.entities.UserDetailed;
+ needCaptcha: boolean;
+}>();
+
+const emit = defineEmits<{
+ (ev: 'passwordSubmitted', v: PwResponse): void;
+}>();
+
+const password = ref('');
+
+const hCaptcha = useTemplateRef('hcaptcha');
+const mCaptcha = useTemplateRef('mcaptcha');
+const reCaptcha = useTemplateRef('recaptcha');
+const turnstile = useTemplateRef('turnstile');
+const testcaptcha = useTemplateRef('testcaptcha');
+
+const hCaptchaResponse = ref<string | null>(null);
+const mCaptchaResponse = ref<string | null>(null);
+const reCaptchaResponse = ref<string | null>(null);
+const turnstileResponse = ref<string | null>(null);
+const testcaptchaResponse = ref<string | null>(null);
+
+const captchaFailed = computed((): boolean => {
+ return (
+ (instance.enableHcaptcha && !hCaptchaResponse.value) ||
+ (instance.enableMcaptcha && !mCaptchaResponse.value) ||
+ (instance.enableRecaptcha && !reCaptchaResponse.value) ||
+ (instance.enableTurnstile && !turnstileResponse.value) ||
+ (instance.enableTestcaptcha && !testcaptchaResponse.value)
+ );
+});
+
+function resetPassword(): void {
+ const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
+ closed: () => dispose(),
+ });
+}
+
+function onSubmit() {
+ emit('passwordSubmitted', {
+ password: password.value,
+ captcha: {
+ hCaptchaResponse: hCaptchaResponse.value,
+ mCaptchaResponse: mCaptchaResponse.value,
+ reCaptchaResponse: reCaptchaResponse.value,
+ turnstileResponse: turnstileResponse.value,
+ testcaptchaResponse: testcaptchaResponse.value,
+ },
+ });
+}
+
+function resetCaptcha() {
+ hCaptcha.value?.reset();
+ mCaptcha.value?.reset();
+ reCaptcha.value?.reset();
+ turnstile.value?.reset();
+ testcaptcha.value?.reset();
+}
+
+defineExpose({
+ resetCaptcha,
+});
+</script>
+
+<style lang="scss" module>
+.wrapper {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ min-height: 336px;
+
+ > .root {
+ width: 100%;
+ }
+}
+
+.avatar {
+ margin: 0 auto 0 auto;
+ width: 64px;
+ height: 64px;
+ background: #ddd;
+ background-position: center;
+ background-size: cover;
+ border-radius: 100%;
+}
+
+.welcomeBackMessage {
+ text-align: center;
+ font-size: 1.1em;
+}
+
+.instanceManualSelectButton {
+ display: block;
+ text-align: center;
+ opacity: .7;
+ font-size: .8em;
+
+ &:hover {
+ text-decoration: underline;
+ }
+}
+
+.orHr {
+ position: relative;
+ margin: .4em auto;
+ width: 100%;
+ height: 1px;
+ background: var(--MI_THEME-divider);
+}
+
+.orMsg {
+ position: absolute;
+ top: -.6em;
+ display: inline-block;
+ padding: 0 1em;
+ background: var(--MI_THEME-panel);
+ font-size: 0.8em;
+ color: var(--MI_THEME-fgOnPanel);
+ margin: 0;
+ left: 50%;
+ transform: translateX(-50%);
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.totp.vue b/packages/frontend/src/components/MkSignin.totp.vue
new file mode 100644
index 0000000000..670b8057c2
--- /dev/null
+++ b/packages/frontend/src/components/MkSignin.totp.vue
@@ -0,0 +1,74 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrapper">
+ <div class="_gaps" :class="$style.root">
+ <div class="_gaps_s">
+ <div :class="$style.totpIcon">
+ <i class="ti ti-key"></i>
+ </div>
+ <div :class="$style.totpDescription">{{ i18n.ts['2fa'] }}</div>
+ </div>
+
+ <!-- totp入力 -->
+ <form class="_gaps_s" @submit.prevent="emit('totpSubmitted', token)">
+ <MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required autofocus :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
+ <template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
+ <template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
+ <template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
+ </MkInput>
+
+ <MkButton type="submit" large primary rounded style="margin: 0 auto;">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </form>
+ </div>
+</div>
+</template>
+
+<script setup lang="ts">
+import { ref } from 'vue';
+
+import { i18n } from '@/i18n.js';
+
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+
+const emit = defineEmits<{
+ (ev: 'totpSubmitted', token: string): void;
+}>();
+
+const token = ref('');
+const isBackupCode = ref(false);
+</script>
+
+<style lang="scss" module>
+.wrapper {
+ display: flex;
+ align-items: center;
+ width: 100%;
+ min-height: 336px;
+
+ > .root {
+ width: 100%;
+ }
+}
+
+.totpIcon {
+ margin: 0 auto;
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
+ text-align: center;
+ height: 64px;
+ width: 64px;
+ font-size: 24px;
+ line-height: 64px;
+ border-radius: 50%;
+}
+
+.totpDescription {
+ text-align: center;
+ font-size: 1.1em;
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue
index 82e0df8a01..4a6219071b 100644
--- a/packages/frontend/src/components/MkSignin.vue
+++ b/packages/frontend/src/components/MkSignin.vue
@@ -4,245 +4,290 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<form :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
- <div class="_gaps_m">
- <div v-show="withAvatar" :class="$style.avatar" :style="{ backgroundImage: user ? `url('${user.avatarUrl}')` : undefined, marginBottom: message ? '1.5em' : undefined }"></div>
- <MkInfo v-if="message">
- {{ message }}
- </MkInfo>
- <div v-if="openOnRemote" class="_gaps_m">
- <div class="_gaps_s">
- <MkButton type="button" rounded primary style="margin: 0 auto;" @click="openRemote(openOnRemote)">
- {{ i18n.ts.continueOnRemote }} <i class="ti ti-external-link"></i>
- </MkButton>
- <button type="button" class="_button" :class="$style.instanceManualSelectButton" @click="specifyHostAndOpenRemote(openOnRemote)">
- {{ i18n.ts.specifyServerHost }}
- </button>
- </div>
- <div :class="$style.orHr">
- <p :class="$style.orMsg">{{ i18n.ts.or }}</p>
- </div>
- </div>
- <div v-if="!totpLogin" class="normal-signin _gaps_m">
- <MkInput v-model="username" :placeholder="i18n.ts.username" type="text" pattern="^[a-zA-Z0-9_]+$" :spellcheck="false" autocomplete="username webauthn" autofocus required data-cy-signin-username @update:modelValue="onUsernameChange">
- <template #prefix>@</template>
- <template #suffix>@{{ host }}</template>
- </MkInput>
- <MkInput v-model="password" :placeholder="i18n.ts.password" type="password" autocomplete="current-password webauthn" :withPasswordToggle="true" required data-cy-signin-password>
- <template #prefix><i class="ti ti-lock"></i></template>
- <template #caption><button class="_textButton" type="button" @click="resetPassword">{{ i18n.ts.forgotPassword }}</button></template>
- </MkInput>
- <MkButton type="submit" large primary rounded :disabled="signing" style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
- </div>
- <div v-if="totpLogin" class="2fa-signin" :class="{ securityKeys: user && user.securityKeys }">
- <div v-if="user && user.securityKeys" class="twofa-group tap-group">
- <p>{{ i18n.ts.useSecurityKey }}</p>
- <MkButton v-if="!queryingKey" @click="query2FaKey">
- {{ i18n.ts.retry }}
- </MkButton>
- </div>
- <div v-if="user && user.securityKeys" :class="$style.orHr">
- <p :class="$style.orMsg">{{ i18n.ts.or }}</p>
- </div>
- <div class="twofa-group totp-group _gaps">
- <MkInput v-model="token" type="text" :pattern="isBackupCode ? '^[A-Z0-9]{32}$' :'^[0-9]{6}$'" autocomplete="one-time-code" required :spellcheck="false" :inputmode="isBackupCode ? undefined : 'numeric'">
- <template #label>{{ i18n.ts.token }} ({{ i18n.ts['2fa'] }})</template>
- <template #prefix><i v-if="isBackupCode" class="ti ti-key"></i><i v-else class="ti ti-123"></i></template>
- <template #caption><button class="_textButton" type="button" @click="isBackupCode = !isBackupCode">{{ isBackupCode ? i18n.ts.useTotp : i18n.ts.useBackupCode }}</button></template>
- </MkInput>
- <MkButton type="submit" :disabled="signing" large primary rounded style="margin: 0 auto;">{{ signing ? i18n.ts.loggingIn : i18n.ts.login }}</MkButton>
- </div>
- </div>
- <div v-if="!totpLogin && usePasswordLessLogin" :class="$style.orHr">
- <p :class="$style.orMsg">{{ i18n.ts.or }}</p>
- </div>
- <div v-if="!totpLogin && usePasswordLessLogin" class="twofa-group tap-group">
- <MkButton v-if="!queryingKey" type="submit" :disabled="signing" style="margin: auto auto;" rounded large primary @click="onPasskeyLogin">
- <i class="ti ti-device-usb" style="font-size: medium;"></i>
- {{ signing ? i18n.ts.loggingIn : i18n.ts.signinWithPasskey }}
- </MkButton>
- <p v-if="queryingKey">{{ i18n.ts.useSecurityKey }}</p>
- </div>
+<div :class="$style.signinRoot">
+ <Transition
+ mode="out-in"
+ :enterActiveClass="$style.transition_enterActive"
+ :leaveActiveClass="$style.transition_leaveActive"
+ :enterFromClass="$style.transition_enterFrom"
+ :leaveToClass="$style.transition_leaveTo"
+
+ :inert="waiting"
+ >
+ <!-- 1. 外部サーãƒãƒ¼ã¸ã®è»¢é€ãƒ»username入力・パスキー -->
+ <XInput
+ v-if="page === 'input'"
+ key="input"
+ :message="message"
+ :openOnRemote="openOnRemote"
+
+ @usernameSubmitted="onUsernameSubmitted"
+ @passkeyClick="onPasskeyLogin"
+ />
+
+ <!-- 2. パスワード入力 -->
+ <XPassword
+ v-else-if="page === 'password'"
+ key="password"
+ ref="passwordPageEl"
+
+ :user="userInfo!"
+ :needCaptcha="needCaptcha"
+
+ @passwordSubmitted="onPasswordSubmitted"
+ />
+
+ <!-- 3. ワンタイムパスワード -->
+ <XTotp
+ v-else-if="page === 'totp'"
+ key="totp"
+
+ @totpSubmitted="onTotpSubmitted"
+ />
+
+ <!-- 4. パスキー -->
+ <XPasskey
+ v-else-if="page === 'passkey'"
+ key="passkey"
+
+ :credentialRequest="credentialRequest!"
+ :isPerformingPasswordlessLogin="doingPasskeyFromInputPage"
+
+ @done="onPasskeyDone"
+ @useTotp="onUseTotp"
+ />
+ </Transition>
+ <div v-if="waiting" :class="$style.waitingRoot">
+ <MkLoading/>
</div>
-</form>
+</div>
</template>
-<script lang="ts" setup>
-import { defineAsyncComponent, ref } from 'vue';
-import { toUnicode } from 'punycode/';
+<script setup lang="ts">
+import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue';
import * as Misskey from 'misskey-js';
-import { supported as webAuthnSupported, get as webAuthnRequest, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
-import { SigninWithPasskeyResponse } from 'misskey-js/entities.js';
-import { query, extractDomain } from '@@/js/url.js';
-import { host as configHost } from '@@/js/config.js';
-import MkDivider from './MkDivider.vue';
+import { supported as webAuthnSupported, parseRequestOptionsFromJSON } from '@github/webauthn-json/browser-ponyfill';
+
+import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
-import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
-import MkButton from '@/components/MkButton.vue';
-import MkInput from '@/components/MkInput.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
+import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
import { login } from '@/account.js';
import { i18n } from '@/i18n.js';
import { showSystemAccountDialog } from '@/scripts/show-system-account-dialog.js';
+import * as os from '@/os.js';
-const signing = ref(false);
-const user = ref<Misskey.entities.UserDetailed | null>(null);
-const usePasswordLessLogin = ref<Misskey.entities.UserDetailed['usePasswordLessLogin']>(true);
-const username = ref('');
-const password = ref('');
-const token = ref('');
-const host = ref(toUnicode(configHost));
-const totpLogin = ref(false);
-const isBackupCode = ref(false);
-const queryingKey = ref(false);
-let credentialRequest: CredentialRequestOptions | null = null;
-const passkey_context = ref('');
+import XInput from '@/components/MkSignin.input.vue';
+import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue';
+import XTotp from '@/components/MkSignin.totp.vue';
+import XPasskey from '@/components/MkSignin.passkey.vue';
const emit = defineEmits<{
- (ev: 'login', v: any): void;
+ (ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
}>();
const props = withDefaults(defineProps<{
- withAvatar?: boolean;
autoSet?: boolean;
message?: string,
openOnRemote?: OpenOnRemoteOptions,
}>(), {
- withAvatar: true,
autoSet: false,
message: '',
openOnRemote: undefined,
});
-function onUsernameChange(): void {
- const usernameRequested = username.value;
- misskeyApi('users/show', {
- username: usernameRequested,
- }).then(userResponse => {
- if (userResponse.username === username.value) {
- user.value = userResponse;
- usePasswordLessLogin.value = userResponse.usePasswordLessLogin;
- }
- }, () => {
- if (usernameRequested === username.value) {
- user.value = null;
- usePasswordLessLogin.value = true;
- }
- });
-}
+const page = ref<'input' | 'password' | 'totp' | 'passkey'>('input');
+const waiting = ref(false);
-function onLogin(res: any): Promise<void> | void {
- if (props.autoSet) {
- return login(res.i);
- }
-}
+const passwordPageEl = useTemplateRef('passwordPageEl');
+const needCaptcha = ref(false);
-async function query2FaKey(): Promise<void> {
- if (credentialRequest == null) return;
- queryingKey.value = true;
- await webAuthnRequest(credentialRequest)
- .catch(() => {
- queryingKey.value = false;
- return Promise.reject(null);
- }).then(credential => {
- credentialRequest = null;
- queryingKey.value = false;
- signing.value = true;
- return misskeyApi('signin', {
- username: username.value,
- password: password.value,
- credential: credential.toJSON(),
- });
- }).then(res => {
- emit('login', res);
- return onLogin(res);
- }).catch(err => {
- if (err === null) return;
- os.alert({
- type: 'error',
- text: i18n.ts.signinFailed,
- });
- signing.value = false;
- });
-}
+const userInfo = ref<null | Misskey.entities.UserDetailed>(null);
+const password = ref('');
+
+//#region Passkey Passwordless
+const credentialRequest = shallowRef<CredentialRequestOptions | null>(null);
+const passkeyContext = ref('');
+const doingPasskeyFromInputPage = ref(false);
function onPasskeyLogin(): void {
- signing.value = true;
if (webAuthnSupported()) {
+ doingPasskeyFromInputPage.value = true;
+ waiting.value = true;
misskeyApi('signin-with-passkey', {})
- .then((res: SigninWithPasskeyResponse) => {
- totpLogin.value = false;
- signing.value = false;
- queryingKey.value = true;
- passkey_context.value = res.context ?? '';
- credentialRequest = parseRequestOptionsFromJSON({
+ .then((res) => {
+ passkeyContext.value = res.context ?? '';
+ credentialRequest.value = parseRequestOptionsFromJSON({
publicKey: res.option,
});
+
+ page.value = 'passkey';
+ waiting.value = false;
})
- .then(() => queryPasskey())
- .catch(loginFailed);
+ .catch(onSigninApiError);
}
}
-async function queryPasskey(): Promise<void> {
- if (credentialRequest == null) return;
- queryingKey.value = true;
- console.log('Waiting passkey auth...');
- await webAuthnRequest(credentialRequest)
- .catch((err) => {
- console.warn('Passkey Auth fail!: ', err);
- queryingKey.value = false;
- return Promise.reject(null);
- }).then(credential => {
- credentialRequest = null;
- queryingKey.value = false;
- signing.value = true;
- return misskeyApi('signin-with-passkey', {
- credential: credential.toJSON(),
- context: passkey_context.value,
- });
- }).then((res: SigninWithPasskeyResponse) => {
+function onPasskeyDone(credential: AuthenticationPublicKeyCredential): void {
+ waiting.value = true;
+
+ if (doingPasskeyFromInputPage.value) {
+ misskeyApi('signin-with-passkey', {
+ credential: credential.toJSON(),
+ context: passkeyContext.value,
+ }).then((res) => {
+ if (res.signinResponse == null) {
+ onSigninApiError();
+ return;
+ }
emit('login', res.signinResponse);
- return onLogin(res.signinResponse);
+ }).catch(onSigninApiError);
+ } else if (userInfo.value != null) {
+ tryLogin({
+ username: userInfo.value.username,
+ password: password.value,
+ credential: credential.toJSON(),
});
+ }
}
-function onSubmit(): void {
- signing.value = true;
- if (!totpLogin.value && user.value && user.value.twoFactorEnabled) {
- if (webAuthnSupported() && user.value.securityKeys) {
- misskeyApi('signin', {
- username: username.value,
- password: password.value,
- }).then(res => {
- totpLogin.value = true;
- signing.value = false;
- credentialRequest = parseRequestOptionsFromJSON({
- publicKey: res,
- });
- })
- .then(() => query2FaKey())
- .catch(loginFailed);
- } else {
- totpLogin.value = true;
- signing.value = false;
- }
+function onUseTotp(): void {
+ page.value = 'totp';
+}
+//#endregion
+
+async function onUsernameSubmitted(username: string) {
+ waiting.value = true;
+
+ userInfo.value = await misskeyApi('users/show', {
+ username,
+ }).catch(() => null);
+
+ await tryLogin({
+ username,
+ });
+}
+
+async function onPasswordSubmitted(pw: PwResponse) {
+ waiting.value = true;
+ password.value = pw.password;
+
+ if (userInfo.value == null) {
+ await os.alert({
+ type: 'error',
+ title: i18n.ts.noSuchUser,
+ text: i18n.ts.signinFailed,
+ });
+ waiting.value = false;
+ return;
+ } else {
+ await tryLogin({
+ username: userInfo.value.username,
+ password: pw.password,
+ 'hcaptcha-response': pw.captcha.hCaptchaResponse,
+ 'm-captcha-response': pw.captcha.mCaptchaResponse,
+ 'g-recaptcha-response': pw.captcha.reCaptchaResponse,
+ 'frc-captcha-solution': pw.captcha.fcResponse,
+ 'turnstile-response': pw.captcha.turnstileResponse,
+ 'testcaptcha-response': pw.captcha.testcaptchaResponse,
+ });
+ }
+}
+
+async function onTotpSubmitted(token: string) {
+ waiting.value = true;
+
+ if (userInfo.value == null) {
+ await os.alert({
+ type: 'error',
+ title: i18n.ts.noSuchUser,
+ text: i18n.ts.signinFailed,
+ });
+ waiting.value = false;
+ return;
} else {
- misskeyApi('signin', {
- username: username.value,
+ await tryLogin({
+ username: userInfo.value.username,
password: password.value,
- token: user.value?.twoFactorEnabled ? token.value : undefined,
- }).then(res => {
+ token,
+ });
+ }
+}
+
+async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promise<Misskey.entities.SigninFlowResponse> {
+ const _req = {
+ username: req.username ?? userInfo.value?.username,
+ ...req,
+ };
+
+ function assertIsSigninFlowRequest(x: Partial<Misskey.entities.SigninFlowRequest>): x is Misskey.entities.SigninFlowRequest {
+ return x.username != null;
+ }
+
+ if (!assertIsSigninFlowRequest(_req)) {
+ throw new Error('Invalid request');
+ }
+
+ return await misskeyApi('signin-flow', _req).then(async (res) => {
+ if (res.finished) {
emit('login', res);
- onLogin(res);
- }).catch(loginFailed);
+ await onLoginSucceeded(res);
+ } else {
+ switch (res.next) {
+ case 'captcha': {
+ needCaptcha.value = true;
+ page.value = 'password';
+ break;
+ }
+ case 'password': {
+ needCaptcha.value = false;
+ page.value = 'password';
+ break;
+ }
+ case 'totp': {
+ page.value = 'totp';
+ break;
+ }
+ case 'passkey': {
+ if (webAuthnSupported()) {
+ credentialRequest.value = parseRequestOptionsFromJSON({
+ publicKey: res.authRequest,
+ });
+ page.value = 'passkey';
+ } else {
+ page.value = 'totp';
+ }
+ break;
+ }
+ }
+
+ if (doingPasskeyFromInputPage.value === true) {
+ doingPasskeyFromInputPage.value = false;
+ page.value = 'input';
+ password.value = '';
+ }
+ passwordPageEl.value?.resetCaptcha();
+ nextTick(() => {
+ waiting.value = false;
+ });
+ }
+ return res;
+ }).catch((err) => {
+ onSigninApiError(err);
+ return Promise.reject(err);
+ });
+}
+
+async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true }) {
+ if (props.autoSet) {
+ await login(res.i);
}
}
-function loginFailed(err: any): void {
- switch (err.id) {
+function onSigninApiError(err?: any): void {
+ const id = err?.id ?? null;
+
+ switch (id) {
case '6cc579cc-885d-43d8-95c2-b8c7fc963280': {
os.alert({
type: 'error',
@@ -275,6 +320,14 @@ function loginFailed(err: any): void {
});
break;
}
+ case 'cdf1235b-ac71-46d4-a3a6-84ccce48df6f': {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.loginFailed,
+ text: i18n.ts.incorrectTotp,
+ });
+ break;
+ }
case '36b96a7d-b547-412d-aeed-2d611cdc8cdc': {
os.alert({
type: 'error',
@@ -283,6 +336,14 @@ function loginFailed(err: any): void {
});
break;
}
+ case '93b86c4b-72f9-40eb-9815-798928603d1e': {
+ os.alert({
+ type: 'error',
+ title: i18n.ts.loginFailed,
+ text: i18n.ts.passkeyVerificationFailed,
+ });
+ break;
+ }
case 'b18c89a7-5b5e-4cec-bb5b-0419f332d430': {
os.alert({
type: 'error',
@@ -309,113 +370,55 @@ function loginFailed(err: any): void {
}
}
- totpLogin.value = false;
- signing.value = false;
-}
-
-function resetPassword(): void {
- const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkForgotPassword.vue')), {}, {
- closed: () => dispose(),
- });
-}
-
-function openRemote(options: OpenOnRemoteOptions, targetHost?: string): void {
- switch (options.type) {
- case 'web':
- case 'lookup': {
- let _path: string;
-
- if (options.type === 'lookup') {
- // TODO: v2024.7.0以é™ãŒæµ¸é€ã—ã¦ããŸã‚‰æ­£å¼ãªURLã«å¤‰æ›´ã™ã‚‹â–¼
- // _path = `/lookup?uri=${encodeURIComponent(_path)}`;
- _path = `/authorize-follow?acct=${encodeURIComponent(options.url)}`;
- } else {
- _path = options.path;
- }
-
- if (targetHost) {
- window.open(`https://${targetHost}${_path}`, '_blank', 'noopener');
- } else {
- window.open(`https://misskey-hub.net/mi-web/?path=${encodeURIComponent(_path)}`, '_blank', 'noopener');
- }
- break;
- }
- case 'share': {
- const params = query(options.params);
- if (targetHost) {
- window.open(`https://${targetHost}/share?${params}`, '_blank', 'noopener');
- } else {
- window.open(`https://misskey-hub.net/share/?${params}`, '_blank', 'noopener');
- }
- break;
- }
+ if (doingPasskeyFromInputPage.value === true) {
+ doingPasskeyFromInputPage.value = false;
+ page.value = 'input';
+ password.value = '';
}
-}
-
-async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<void> {
- const { canceled, result: hostTemp } = await os.inputText({
- title: i18n.ts.inputHostName,
- placeholder: 'misskey.example.com',
+ passwordPageEl.value?.resetCaptcha();
+ nextTick(() => {
+ waiting.value = false;
});
-
- if (canceled) return;
-
- let targetHost: string | null = hostTemp;
-
- // ドメイン部分ã ã‘ã‚’å–り出ã™
- targetHost = extractDomain(targetHost);
- if (targetHost == null) {
- os.alert({
- type: 'error',
- title: i18n.ts.invalidValue,
- text: i18n.ts.tryAgain,
- });
- return;
- }
- openRemote(options, targetHost);
}
+
+onBeforeUnmount(() => {
+ password.value = '';
+ needCaptcha.value = false;
+ userInfo.value = null;
+});
</script>
<style lang="scss" module>
-.avatar {
- margin: 0 auto 0 auto;
- width: 64px;
- height: 64px;
- background: #ddd;
- background-position: center;
- background-size: cover;
- border-radius: var(--radius-full);
+.transition_enterActive,
+.transition_leaveActive {
+ transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
}
-
-.instanceManualSelectButton {
- display: block;
- text-align: center;
- opacity: .7;
- font-size: .8em;
-
- &:hover {
- text-decoration: underline;
- }
+.transition_enterFrom {
+ opacity: 0;
+ transform: translateX(50px);
+}
+.transition_leaveTo {
+ opacity: 0;
+ transform: translateX(-50px);
}
-.orHr {
+.signinRoot {
+ overflow-x: hidden;
+ overflow-x: clip;
+
position: relative;
- margin: .4em auto;
- width: 100%;
- height: 1px;
- background: var(--divider);
}
-.orMsg {
+.waitingRoot {
position: absolute;
- top: -.6em;
- display: inline-block;
- padding: 0 1em;
- background: var(--panel);
- font-size: 0.8em;
- color: var(--fgOnPanel);
- margin: 0;
- left: 50%;
- transform: translateX(-50%);
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%);
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ z-index: 1;
}
</style>
diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue
index d48780e9de..676a336ec7 100644
--- a/packages/frontend/src/components/MkSigninDialog.vue
+++ b/packages/frontend/src/components/MkSigninDialog.vue
@@ -4,26 +4,30 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
-<MkModalWindow
- ref="dialog"
- :width="400"
- :height="450"
- @close="onClose"
+<MkModal
+ ref="modal"
+ :preferType="'dialog'"
+ @click="onClose"
@closed="emit('closed')"
>
- <template #header>{{ i18n.ts.login }}</template>
-
- <MkSpacer :marginMin="20" :marginMax="28">
- <MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
- </MkSpacer>
-</MkModalWindow>
+ <div :class="$style.root">
+ <div :class="$style.header">
+ <div :class="$style.headerText"><i class="ti ti-login-2"></i> {{ i18n.ts.login }}</div>
+ <button :class="$style.closeButton" class="_button" @click="onClose"><i class="ti ti-x"></i></button>
+ </div>
+ <div :class="$style.content">
+ <MkSignin :autoSet="autoSet" :message="message" :openOnRemote="openOnRemote" @login="onLogin"/>
+ </div>
+ </div>
+</MkModal>
</template>
<script lang="ts" setup>
+import * as Misskey from 'misskey-js';
import { shallowRef } from 'vue';
import type { OpenOnRemoteOptions } from '@/scripts/please-login.js';
import MkSignin from '@/components/MkSignin.vue';
-import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkModal from '@/components/MkModal.vue';
import { i18n } from '@/i18n.js';
withDefaults(defineProps<{
@@ -37,20 +41,67 @@ withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'done', v: any): void;
+ (ev: 'done', v: Misskey.entities.SigninFlowResponse & { finished: true }): void;
(ev: 'closed'): void;
(ev: 'cancelled'): void;
}>();
-const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const modal = shallowRef<InstanceType<typeof MkModal>>();
function onClose() {
emit('cancelled');
- if (dialog.value) dialog.value.close();
+ if (modal.value) modal.value.close();
}
-function onLogin(res) {
+function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true }) {
emit('done', res);
- if (dialog.value) dialog.value.close();
+ if (modal.value) modal.value.close();
}
</script>
+
+<style lang="scss" module>
+.root {
+ overflow: auto;
+ margin: auto;
+ position: relative;
+ width: 100%;
+ max-width: 400px;
+ height: 100%;
+ max-height: 450px;
+ box-sizing: border-box;
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
+}
+
+.header {
+ position: sticky;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 50px;
+ box-sizing: border-box;
+ display: flex;
+ align-items: center;
+ font-weight: bold;
+ backdrop-filter: var(--MI-blur, blur(15px));
+ background: var(--MI_THEME-acrylicBg);
+ z-index: 1;
+}
+
+.headerText {
+ padding: 0 20px;
+ box-sizing: border-box;
+}
+
+.closeButton {
+ margin-left: auto;
+ padding: 16px;
+ font-size: 16px;
+ line-height: 16px;
+}
+
+.content {
+ padding: 32px;
+ box-sizing: border-box;
+}
+</style>
diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue
index 4c55831a3a..f0b440d2ef 100644
--- a/packages/frontend/src/components/MkSignupDialog.form.vue
+++ b/packages/frontend/src/components/MkSignupDialog.form.vue
@@ -21,12 +21,12 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #caption>
<div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div>
<span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
- <span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
- <span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
- <span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
- <span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
- <span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
- <span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
+ <span v-else-if="usernameState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
+ <span v-else-if="usernameState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
+ <span v-else-if="usernameState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
+ <span v-else-if="usernameState === 'invalid-format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
+ <span v-else-if="usernameState === 'min-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
+ <span v-else-if="usernameState === 'max-range'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
</template>
</MkInput>
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:modelValue="onChangeEmail">
@@ -34,32 +34,32 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix><i class="ti ti-mail"></i></template>
<template #caption>
<span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
- <span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
- <span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
- <span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
- <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
- <span v-else-if="emailState === 'unavailable:banned'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
- <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
- <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
- <span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
- <span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
+ <span v-else-if="emailState === 'ok'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
+ <span v-else-if="emailState === 'unavailable:used'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
+ <span v-else-if="emailState === 'unavailable:format'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
+ <span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
+ <span v-else-if="emailState === 'unavailable:banned'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.banned }}</span>
+ <span v-else-if="emailState === 'unavailable:mx'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
+ <span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
+ <span v-else-if="emailState === 'unavailable'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
+ <span v-else-if="emailState === 'error'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
</template>
</MkInput>
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:modelValue="onChangePassword">
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption>
- <span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
- <span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
- <span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
+ <span v-if="passwordStrength == 'low'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
+ <span v-if="passwordStrength == 'medium'" style="color: var(--MI_THEME-warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
+ <span v-if="passwordStrength == 'high'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
</template>
</MkInput>
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:modelValue="onChangePasswordRetype">
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
<template #prefix><i class="ti ti-lock"></i></template>
<template #caption>
- <span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
- <span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
+ <span v-if="passwordRetypeState == 'match'" style="color: var(--MI_THEME-success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
+ <span v-if="passwordRetypeState == 'not-match'" style="color: var(--MI_THEME-error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
</template>
</MkInput>
<MkInput v-if="instance.approvalRequiredForSignup" v-model="reason" type="text" :spellcheck="false" required data-cy-signup-reason>
@@ -71,6 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
<MkCaptcha v-if="instance.enableFC" ref="fc" v-model="fcResponse" :class="$style.captcha" provider="fc" :sitekey="instance.fcSiteKey"/>
+ <MkCaptcha v-if="instance.enableTestcaptcha" ref="testcaptcha" v-model="testcaptchaResponse" :class="$style.captcha" provider="testcaptcha"/>
<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
<template v-if="submitting">
<MkLoading :em="true" :colored="false"/>
@@ -86,10 +87,10 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref, computed } from 'vue';
import { toUnicode } from 'punycode/';
import * as Misskey from 'misskey-js';
+import * as config from '@@/js/config.js';
import MkButton from './MkButton.vue';
import MkInput from './MkInput.vue';
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
-import * as config from '@@/js/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { login } from '@/account.js';
@@ -103,7 +104,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'signup', user: Misskey.entities.SigninResponse): void;
+ (ev: 'signup', user: Misskey.entities.SignupResponse): void;
(ev: 'signupEmailPending'): void;
(ev: 'approvalPending'): void;
}>();
@@ -111,9 +112,11 @@ const emit = defineEmits<{
const host = toUnicode(config.host);
const hcaptcha = ref<Captcha | undefined>();
+const mcaptcha = ref<Captcha | undefined>();
const recaptcha = ref<Captcha | undefined>();
const turnstile = ref<Captcha | undefined>();
const fc = ref<Captcha | undefined>();
+const testcaptcha = ref<Captcha | undefined>();
const username = ref<string>('');
const password = ref<string>('');
@@ -131,6 +134,7 @@ const mCaptchaResponse = ref<string | null>(null);
const reCaptchaResponse = ref<string | null>(null);
const turnstileResponse = ref<string | null>(null);
const fcResponse = ref<string | null>(null);
+const testcaptchaResponse = ref<string | null>(null);
const usernameAbortController = ref<null | AbortController>(null);
const emailAbortController = ref<null | AbortController>(null);
@@ -141,6 +145,7 @@ const shouldDisableSubmitting = computed((): boolean => {
instance.enableRecaptcha && !reCaptchaResponse.value ||
instance.enableTurnstile && !turnstileResponse.value ||
instance.enableFC && !fcResponse.value ||
+ instance.enableTestcaptcha && !testcaptchaResponse.value ||
instance.emailRequiredForSignup && emailState.value !== 'ok' ||
usernameState.value !== 'ok' ||
passwordRetypeState.value !== 'match';
@@ -259,20 +264,33 @@ async function onSubmit(): Promise<void> {
if (submitting.value) return;
submitting.value = true;
- try {
- await misskeyApi('signup', {
- username: username.value,
- password: password.value,
- emailAddress: email.value,
- invitationCode: invitationCode.value,
- reason: reason.value,
- 'hcaptcha-response': hCaptchaResponse.value,
- 'm-captcha-response': mCaptchaResponse.value,
- 'g-recaptcha-response': reCaptchaResponse.value,
- 'turnstile-response': turnstileResponse.value,
- 'frc-captcha-solution': fcResponse.value,
- });
- if (instance.emailRequiredForSignup) {
+ const signupPayload: Misskey.entities.SignupRequest = {
+ username: username.value,
+ password: password.value,
+ emailAddress: email.value,
+ invitationCode: invitationCode.value,
+ reason: reason.value,
+ 'hcaptcha-response': hCaptchaResponse.value,
+ 'm-captcha-response': mCaptchaResponse.value,
+ 'g-recaptcha-response': reCaptchaResponse.value,
+ 'turnstile-response': turnstileResponse.value,
+ 'frc-captcha-solution': fcResponse.value,
+ 'testcaptcha-response': testcaptchaResponse.value,
+ };
+
+ const res = await fetch(`${config.apiUrl}/signup`, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify(signupPayload),
+ }).catch(() => {
+ onSignupApiError();
+ return null;
+ });
+
+ if (res) {
+ if (res.status === 204 || instance.emailRequiredForSignup) {
os.alert({
type: 'success',
title: i18n.ts._signup.almostThere,
@@ -287,28 +305,33 @@ async function onSubmit(): Promise<void> {
});
emit('approvalPending');
} else {
- const res = await misskeyApi('signin', {
- username: username.value,
- password: password.value,
- });
- emit('signup', res);
+ const resJson = (await res.json()) as Misskey.entities.SignupResponse;
+ if (_DEV_) console.log(resJson);
+
+ emit('signup', resJson);
if (props.autoSet) {
- return login(res.i);
+ await login(resJson.token);
}
}
- } catch {
- submitting.value = false;
- hcaptcha.value?.reset?.();
- recaptcha.value?.reset?.();
- turnstile.value?.reset?.();
- fc.value?.reset?.();
-
- os.alert({
- type: 'error',
- text: i18n.ts.somethingHappened,
- });
}
+
+ submitting.value = false;
+}
+
+function onSignupApiError() {
+ submitting.value = false;
+ hcaptcha.value?.reset?.();
+ mcaptcha.value?.reset?.();
+ recaptcha.value?.reset?.();
+ turnstile.value?.reset?.();
+ fc.value?.reset?.();
+ testcaptcha.value?.reset?.();
+
+ os.alert({
+ type: 'error',
+ text: i18n.ts.somethingHappened,
+ });
}
</script>
@@ -317,8 +340,8 @@ async function onSubmit(): Promise<void> {
padding: 16px;
text-align: center;
font-size: 26px;
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
}
.captcha {
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.vue b/packages/frontend/src/components/MkSignupDialog.rules.vue
index 251c805401..12f9621fda 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.vue
+++ b/packages/frontend/src/components/MkSignupDialog.rules.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-if="availableServerRules" :defaultOpen="true">
<template #label>{{ i18n.ts.serverRules }}</template>
- <template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template>
+ <template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<ol class="_gaps_s" :class="$style.rules">
<li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="sanitizeHtml(item)"></div></li>
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder v-if="availableTos || availablePrivacyPolicy" :defaultOpen="true">
<template #label>{{ tosPrivacyPolicyLabel }}</template>
- <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--success)"></i></template>
+ <template #suffix><i v-if="agreeTosAndPrivacyPolicy" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<div class="_gaps_s">
<div v-if="availableTos"><a :href="instance.tosUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a></div>
<div v-if="availablePrivacyPolicy"><a :href="instance.privacyPolicyUrl ?? undefined" class="_link" target="_blank">{{ i18n.ts.privacyPolicy }} <i class="ti ti-external-link"></i></a></div>
@@ -43,7 +43,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
- <template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template>
+ <template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<a href="https://activitypub.software/TransFem-org/Sharkey/-/blob/stable/IMPORTANT_NOTES.md" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
@@ -151,8 +151,8 @@ async function updateAgreeNote(v: boolean) {
padding: 16px;
text-align: center;
font-size: 26px;
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
}
.rules {
@@ -171,14 +171,14 @@ async function updateAgreeNote(v: boolean) {
flex-shrink: 0;
display: flex;
position: sticky;
- top: calc(var(--stickyTop, 0px) + 8px);
+ top: calc(var(--MI-stickyTop, 0px) + 8px);
counter-increment: item;
content: counter(item);
width: 32px;
height: 32px;
line-height: 32px;
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
font-size: 13px;
font-weight: bold;
align-items: center;
diff --git a/packages/frontend/src/components/MkSignupDialog.vue b/packages/frontend/src/components/MkSignupDialog.vue
index 91e7d5dd53..b8e6318d17 100644
--- a/packages/frontend/src/components/MkSignupDialog.vue
+++ b/packages/frontend/src/components/MkSignupDialog.vue
@@ -47,7 +47,7 @@ const props = withDefaults(defineProps<{
});
const emit = defineEmits<{
- (ev: 'done', res: Misskey.entities.SigninResponse): void;
+ (ev: 'done', res: Misskey.entities.SignupResponse): void;
(ev: 'closed'): void;
}>();
@@ -55,7 +55,7 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
const isAcceptedServerRule = ref(false);
-function onSignup(res: Misskey.entities.SigninResponse) {
+function onSignup(res: Misskey.entities.SignupResponse) {
emit('done', res);
dialog.value?.close();
}
diff --git a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
index 7743a89242..9bfa2789af 100644
--- a/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
+++ b/packages/frontend/src/components/MkSourceCodeAvailablePopup.vue
@@ -63,12 +63,12 @@ function close() {
.root {
position: fixed;
z-index: v-bind(zIndex);
- bottom: var(--margin);
+ bottom: var(--MI-margin);
left: 0;
right: 0;
margin: auto;
box-sizing: border-box;
- width: calc(100% - (var(--margin) * 2));
+ width: calc(100% - (var(--MI-margin) * 2));
max-width: 500px;
display: flex;
backdrop-filter: var(--blur, blur(15px));
@@ -78,7 +78,7 @@ function close() {
text-align: center;
padding-top: 25px;
width: 100px;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
@media (max-width: 500px) {
.icon {
diff --git a/packages/frontend/src/components/MkSubNoteContent.vue b/packages/frontend/src/components/MkSubNoteContent.vue
index 6bd00fcc2a..46ef575c23 100644
--- a/packages/frontend/src/components/MkSubNoteContent.vue
+++ b/packages/frontend/src/components/MkSubNoteContent.vue
@@ -110,11 +110,11 @@ watch(() => props.expandAllCws, (expandAllCws) => {
left: 0;
width: 100%;
height: 64px;
- // background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+ // background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
> .fadeLabel {
display: inline-block;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
padding: 6px 10px;
font-size: 0.8em;
border-radius: var(--radius-ellipse);
@@ -123,7 +123,7 @@ watch(() => props.expandAllCws, (expandAllCws) => {
&:hover {
> .fadeLabel {
- background: var(--panelHighlight);
+ background: var(--MI_THEME-panelHighlight);
}
}
}
@@ -132,13 +132,13 @@ watch(() => props.expandAllCws, (expandAllCws) => {
.reply {
margin-right: 6px;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
.rp {
margin-left: 4px;
font-style: oblique;
- color: var(--renote);
+ color: var(--MI_THEME-renote);
}
.translation {
@@ -152,7 +152,7 @@ watch(() => props.expandAllCws, (expandAllCws) => {
width: 100%;
margin-top: 14px;
position: sticky;
- bottom: calc(var(--stickyBottom, 0px) - 100px);
+ bottom: calc(var(--MI-stickyBottom, 0px) - 100px);
}
.playMFMButton {
@@ -161,7 +161,7 @@ watch(() => props.expandAllCws, (expandAllCws) => {
.showLessLabel {
display: inline-block;
- background: var(--popup);
+ background: var(--MI_THEME-popup);
padding: 6px 10px;
font-size: 0.8em;
border-radius: var(--radius-ellipse);
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 430e3c7958..e938da8e57 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -43,7 +43,7 @@ defineProps<{
& + .group {
margin-top: 16px;
padding-top: 16px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
> .title {
@@ -64,7 +64,7 @@ defineProps<{
&:hover {
text-decoration: none;
- background: var(--panelHighlight);
+ background: var(--MI_THEME-panelHighlight);
}
&:focus-visible {
@@ -72,12 +72,12 @@ defineProps<{
}
&.active {
- color: var(--accent);
- background: var(--accentedBg);
+ color: var(--MI_THEME-accent);
+ background: var(--MI_THEME-accentedBg);
}
&.danger {
- color: var(--error);
+ color: var(--MI_THEME-error);
}
> .icon {
@@ -128,10 +128,10 @@ defineProps<{
&:hover {
text-decoration: none;
background: none;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
> .icon {
- background: var(--accentedBg);
+ background: var(--MI_THEME-accentedBg);
}
}
@@ -144,7 +144,7 @@ defineProps<{
width: 60px;
height: 60px;
aspect-ratio: 1;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: var(--radius-full);
}
diff --git a/packages/frontend/src/components/MkSwitch.button.vue b/packages/frontend/src/components/MkSwitch.button.vue
index f7c413e1d3..a06a407de1 100644
--- a/packages/frontend/src/components/MkSwitch.button.vue
+++ b/packages/frontend/src/components/MkSwitch.button.vue
@@ -51,9 +51,9 @@ const toggle = () => {
width: calc(var(--height) * 1.6);
height: calc(var(--height) + 2px); // æž ç·š
outline: none;
- background: var(--switchOffBg);
+ background: var(--MI_THEME-switchOffBg);
background-clip: content-box;
- border: solid 1px var(--switchOffBg);
+ border: solid 1px var(--MI_THEME-switchOffBg);
border-radius: var(--radius-ellipse);
cursor: pointer;
transition: inherit;
@@ -61,8 +61,8 @@ const toggle = () => {
}
.buttonChecked {
- background-color: var(--switchOnBg) !important;
- border-color: var(--switchOnBg) !important;
+ background-color: var(--MI_THEME-switchOnBg) !important;
+ border-color: var(--MI_THEME-switchOnBg) !important;
}
.buttonDisabled {
@@ -80,12 +80,12 @@ const toggle = () => {
&:not(.knobChecked) {
left: 3px;
- background: var(--switchOffFg);
+ background: var(--MI_THEME-switchOffFg);
}
}
.knobChecked {
left: calc(calc(100% - var(--height)) + 3px);
- background: var(--switchOnFg);
+ background: var(--MI_THEME-switchOnFg);
}
</style>
diff --git a/packages/frontend/src/components/MkSwitch.vue b/packages/frontend/src/components/MkSwitch.vue
index a0994d9cc9..5e6029ee40 100644
--- a/packages/frontend/src/components/MkSwitch.vue
+++ b/packages/frontend/src/components/MkSwitch.vue
@@ -59,7 +59,7 @@ const toggle = () => {
&:hover {
> .button {
- border-color: var(--inputBorderHover) !important;
+ border-color: var(--MI_THEME-inputBorderHover) !important;
}
}
@@ -77,7 +77,7 @@ const toggle = () => {
margin: 0;
&:focus-visible ~ .toggle {
- outline: 2px solid var(--focus);
+ outline: 2px solid var(--MI_THEME-focus);
outline-offset: 2px;
}
}
@@ -87,7 +87,7 @@ const toggle = () => {
margin-top: 2px;
display: block;
transition: inherit;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
}
.label {
@@ -99,7 +99,7 @@ const toggle = () => {
.caption {
margin: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
font-size: 0.85em;
&:empty {
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index ec3b1c90ca..485d003f93 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -55,6 +55,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSwitch>
<MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.userCreated)" @click="test('userCreated')"><i class="ti ti-send"></i></MkButton>
</div>
+ <div :class="$style.switchBox">
+ <MkSwitch v-model="events.inactiveModeratorsWarning" :disabled="disabledEvents.inactiveModeratorsWarning">
+ <template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsWarning }}</template>
+ </MkSwitch>
+ <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsWarning)" @click="test('inactiveModeratorsWarning')"><i class="ti ti-send"></i></MkButton>
+ </div>
+ <div :class="$style.switchBox">
+ <MkSwitch v-model="events.inactiveModeratorsInvitationOnlyChanged" :disabled="disabledEvents.inactiveModeratorsInvitationOnlyChanged">
+ <template #label>{{ i18n.ts._webhookSettings._systemEvents.inactiveModeratorsInvitationOnlyChanged }}</template>
+ </MkSwitch>
+ <MkButton v-show="mode === 'edit'" transparent :class="$style.testButton" :disabled="!(isActive && events.inactiveModeratorsInvitationOnlyChanged)" @click="test('inactiveModeratorsInvitationOnlyChanged')"><i class="ti ti-send"></i></MkButton>
+ </div>
</div>
<div v-show="mode === 'edit'" :class="$style.description">
@@ -100,6 +112,8 @@ type EventType = {
abuseReport: boolean;
abuseReportResolved: boolean;
userCreated: boolean;
+ inactiveModeratorsWarning: boolean;
+ inactiveModeratorsInvitationOnlyChanged: boolean;
}
const emit = defineEmits<{
@@ -123,6 +137,8 @@ const events = ref<EventType>({
abuseReport: true,
abuseReportResolved: true,
userCreated: true,
+ inactiveModeratorsWarning: true,
+ inactiveModeratorsInvitationOnlyChanged: true,
});
const isActive = ref<boolean>(true);
@@ -130,6 +146,8 @@ const disabledEvents = ref<EventType>({
abuseReport: false,
abuseReportResolved: false,
userCreated: false,
+ inactiveModeratorsWarning: false,
+ inactiveModeratorsInvitationOnlyChanged: false,
});
const disableSubmitButton = computed(() => {
@@ -261,10 +279,10 @@ onMounted(async () => {
bottom: 0;
left: 0;
padding: 12px;
- border-top: solid 0.5px var(--divider);
- background: var(--acrylicBg);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ border-top: solid 0.5px var(--MI_THEME-divider);
+ background: var(--MI_THEME-acrylicBg);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
.switchBox {
@@ -289,6 +307,6 @@ onMounted(async () => {
.description {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
}
</style>
diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue
index 54ab8fc663..07ab007482 100644
--- a/packages/frontend/src/components/MkTab.vue
+++ b/packages/frontend/src/components/MkTab.vue
@@ -47,13 +47,13 @@ export default defineComponent({
}
&.active {
- color: var(--accent);
- background: var(--accentedBg);
+ color: var(--MI_THEME-accent);
+ background: var(--MI_THEME-accentedBg);
}
&:not(.active):hover {
- color: var(--fgHighlighted);
- background: var(--panelHighlight);
+ color: var(--MI_THEME-fgHighlighted);
+ background: var(--MI_THEME-panelHighlight);
}
&:not(:first-child) {
diff --git a/packages/frontend/src/components/MkTagCloud.vue b/packages/frontend/src/components/MkTagCloud.vue
index 6b9c181597..87aa046963 100644
--- a/packages/frontend/src/components/MkTagCloud.vue
+++ b/packages/frontend/src/components/MkTagCloud.vue
@@ -33,7 +33,7 @@ watch(available, () => {
try {
window.TagCanvas.Start(idForCanvas, idForTags, {
textColour: '#ffffff',
- outlineColour: tinycolor(computedStyle.getPropertyValue('--accent')).toHexString(),
+ outlineColour: tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString(),
outlineRadius: 10,
initial: [-0.030, -0.010],
frontSelect: true,
diff --git a/packages/frontend/src/components/MkTextarea.vue b/packages/frontend/src/components/MkTextarea.vue
index 72d6e12656..9a003d9db8 100644
--- a/packages/frontend/src/components/MkTextarea.vue
+++ b/packages/frontend/src/components/MkTextarea.vue
@@ -159,7 +159,7 @@ onUnmounted(() => {
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
@@ -179,9 +179,9 @@ onUnmounted(() => {
font: inherit;
font-weight: normal;
font-size: 1em;
- color: var(--fg);
- background: var(--panel);
- border: solid 1px var(--panel);
+ color: var(--MI_THEME-fg);
+ background: var(--MI_THEME-panel);
+ border: solid 1px var(--MI_THEME-panel);
border-radius: var(--radius-sm);
outline: none;
box-shadow: none;
@@ -189,13 +189,13 @@ onUnmounted(() => {
transition: border-color 0.1s ease-out;
&:hover {
- border-color: var(--inputBorderHover) !important;
+ border-color: var(--MI_THEME-inputBorderHover) !important;
}
}
.focused {
> .textarea {
- border-color: var(--accent) !important;
+ border-color: var(--MI_THEME-accent) !important;
}
}
@@ -226,7 +226,7 @@ onUnmounted(() => {
.mfmPreview {
padding: 12px;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
box-sizing: border-box;
min-height: 130px;
pointer-events: none;
diff --git a/packages/frontend/src/components/MkTokenGenerateWindow.vue b/packages/frontend/src/components/MkTokenGenerateWindow.vue
index b32066c950..a7bc3f37f1 100644
--- a/packages/frontend/src/components/MkTokenGenerateWindow.vue
+++ b/packages/frontend/src/components/MkTokenGenerateWindow.vue
@@ -136,15 +136,15 @@ function enableAll(): void {
.adminPermissions {
margin: 8px -6px 0;
padding: 24px 6px 6px;
- border: 2px solid var(--error);
- border-radius: calc(var(--radius) / 2);
+ border: 2px solid var(--MI_THEME-error);
+ border-radius: calc(var(--MI-radius) / 2);
}
.adminPermissionsHeader {
margin: -34px 0 6px 12px;
padding: 0 4px;
width: fit-content;
- color: var(--error);
- background: var(--panel);
+ color: var(--MI_THEME-error);
+ background: var(--MI_THEME-panel);
}
</style>
diff --git a/packages/frontend/src/components/MkTooltip.vue b/packages/frontend/src/components/MkTooltip.vue
index aac07008a4..25350a8a40 100644
--- a/packages/frontend/src/components/MkTooltip.vue
+++ b/packages/frontend/src/components/MkTooltip.vue
@@ -110,7 +110,7 @@ onUnmounted(() => {
box-sizing: border-box;
text-align: center;
border-radius: var(--radius-xs);
- border: solid 0.5px var(--divider);
+ border: solid 0.5px var(--MI_THEME-divider);
pointer-events: none;
transform-origin: center center;
}
diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue
index cec7d69943..53b8db38b2 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Note.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue
@@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</I18n>
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
<div v-if="onceReacted">
- <b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>
+ <b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>
<I18n :src="i18n.ts._initialTutorial._reaction.reactDone">
<template #undo>
<i class="ph-minus ph-bold ph-lg"></i>
@@ -116,13 +116,13 @@ function removeReaction(emoji) {
<style lang="scss" module>
.exampleNoteRoot {
- border-radius: var(--radius);
- border: var(--panelBorder);
- background: var(--panel);
+ border-radius: var(--MI-radius);
+ border: var(--MI_THEME-panelBorder);
+ background: var(--MI_THEME-panel);
}
.divider {
height: 1px;
- background: var(--divider);
+ background: var(--MI_THEME-divider);
}
</style>
diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
index a9014d4202..0d210acbae 100644
--- a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue
@@ -81,14 +81,14 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
<style lang="scss" module>
.exampleRoot {
max-width: none!important;
- border-radius: var(--radius);
- border: var(--panelBorder);
- background: var(--panel);
+ border-radius: var(--MI-radius);
+ border: var(--MI_THEME-panelBorder);
+ background: var(--MI_THEME-panel);
}
.divider {
height: 1px;
- background: var(--divider);
+ background: var(--MI_THEME-divider);
}
.image {
@@ -101,7 +101,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
display: block;
width: 100%;
height: 40px;
- color: var(--fgOnAccent);
+ color: var(--MI_THEME-fgOnAccent);
font-weight: bold;
text-align: left;
@@ -117,7 +117,7 @@ const exampleCWNote = reactive<Misskey.entities.Note>({
right: 0;
bottom: 0;
border-radius: var(--radius-ellipse);
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
}
}
diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
index 322082f5a0..3ac58163c5 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:initialNote="exampleNote"
@fileChangeSensitive="doSucceeded"
></MkPostForm>
- <div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
+ <div v-if="onceSucceeded"><b style="color: var(--MI_THEME-accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div>
<MkFolder>
<template #label>{{ i18n.ts.previewNoteText }}</template>
<MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote>
@@ -91,14 +91,14 @@ const exampleNote = reactive<Misskey.entities.Note>({
<style lang="scss" module>
.exampleRoot {
- border-radius: var(--radius);
- border: var(--panelBorder);
- background: var(--panel);
+ border-radius: var(--MI-radius);
+ border: var(--MI_THEME-panelBorder);
+ background: var(--MI_THEME-panel);
}
.divider {
height: 1px;
- background: var(--divider);
+ background: var(--MI_THEME-divider);
}
.image {
@@ -111,7 +111,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
display: block;
width: 100%;
height: 40px;
- color: var(--fgOnAccent);
+ color: var(--MI_THEME-fgOnAccent);
font-weight: bold;
text-align: left;
@@ -127,7 +127,7 @@ const exampleNote = reactive<Misskey.entities.Note>({
right: 0;
bottom: 0;
border-radius: var(--radius-ellipse);
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
}
}
diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
index b900a30c85..328f7e95d1 100644
--- a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue
@@ -31,14 +31,14 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
<style lang="scss" module>
.exampleNoteRoot {
- border-radius: var(--radius);
- border: var(--panelBorder);
- background: var(--panel);
+ border-radius: var(--MI-radius);
+ border: var(--MI_THEME-panelBorder);
+ background: var(--MI_THEME-panel);
}
.divider {
height: 1px;
- background: var(--divider);
+ background: var(--MI_THEME-divider);
}
.image {
@@ -51,7 +51,7 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
display: block;
width: 100%;
height: 40px;
- color: var(--fgOnAccent);
+ color: var(--MI_THEME-fgOnAccent);
font-weight: bold;
text-align: left;
@@ -67,7 +67,7 @@ import { basicTimelineIconClass, basicTimelineTypes } from '@/timelines.js';
right: 0;
bottom: 0;
border-radius: var(--radius-ellipse);
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
}
}
diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue
index 1f5a2b9381..11d7c8dc4d 100644
--- a/packages/frontend/src/components/MkTutorialDialog.vue
+++ b/packages/frontend/src/components/MkTutorialDialog.vue
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
- <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+ <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._landing.title }}</div>
<div>{{ i18n.ts._initialTutorial._landing.description }}</div>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
@@ -126,7 +126,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
- <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+ <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;">
<template #link>
@@ -223,7 +223,7 @@ async function close(skip: boolean) {
.progressBarValue {
height: 100%;
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
transition: all 0.5s cubic-bezier(0,.5,.5,1);
}
@@ -253,7 +253,7 @@ async function close(skip: boolean) {
left: 0;
flex-shrink: 0;
padding: 12px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
-webkit-backdrop-filter: blur(15px);
backdrop-filter: blur(15px);
}
diff --git a/packages/frontend/src/components/MkUpdated.vue b/packages/frontend/src/components/MkUpdated.vue
index 91f5b86c2d..7cafb1b0af 100644
--- a/packages/frontend/src/components/MkUpdated.vue
+++ b/packages/frontend/src/components/MkUpdated.vue
@@ -46,8 +46,8 @@ onMounted(() => {
max-width: 480px;
box-sizing: border-box;
text-align: center;
- background: var(--panel);
- border-radius: var(--radius);
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
}
.title {
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 04f5314463..be12304ae6 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -84,13 +84,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, onDeactivated, onUnmounted, ref } from 'vue';
-import type { summaly } from '@misskey-dev/summaly';
import { url as local } from '@@/js/config.js';
+import { versatileLang } from '@@/js/intl-const.js';
+import type { summaly } from '@misskey-dev/summaly';
import { i18n } from '@/i18n.js';
import * as os from '@/os.js';
import { deviceKind } from '@/scripts/device-kind.js';
import MkButton from '@/components/MkButton.vue';
-import { versatileLang } from '@@/js/intl-const.js';
import { transformPlayerUrl } from '@/scripts/player-url-transform.js';
import { defaultStore } from '@/store.js';
@@ -219,7 +219,7 @@ onUnmounted(() => {
height: 1.5em;
padding: 0;
margin: 0;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
background: rgba(128, 128, 128, 0.2);
opacity: 0.7;
@@ -240,7 +240,7 @@ onUnmounted(() => {
position: relative;
display: block;
font-size: 14px;
- box-shadow: 0 0 0 1px var(--divider);
+ box-shadow: 0 0 0 1px var(--MI_THEME-divider);
border-radius: var(--radius-sm);
overflow: clip;
@@ -270,7 +270,7 @@ onUnmounted(() => {
height: 100%;
background-position: center;
background-size: cover;
- background-color: var(--bg);
+ background-color: var(--MI_THEME-bg);
display: flex;
justify-content: center;
align-items: center;
@@ -317,7 +317,6 @@ onUnmounted(() => {
.siteName {
display: inline-block;
margin: 0;
- color: var(--urlPreviewInfo);
font-size: 0.8em;
line-height: 16px;
vertical-align: top;
diff --git a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
index 3c5f563aa0..7a2b5f5ddc 100644
--- a/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
+++ b/packages/frontend/src/components/MkUserAnnouncementEditDialog.vue
@@ -25,9 +25,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkRadios v-model="icon">
<template #label>{{ i18n.ts.icon }}</template>
<option value="info"><i class="ti ti-info-circle"></i></option>
- <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option>
- <option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
- <option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
+ <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
+ <option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
+ <option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
</MkRadios>
<MkRadios v-model="display">
<template #label>{{ i18n.ts.display }}</template>
@@ -141,8 +141,8 @@ async function del() {
bottom: 0;
left: 0;
padding: 12px;
- border-top: solid 0.5px var(--divider);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ border-top: solid 0.5px var(--MI_THEME-divider);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
</style>
diff --git a/packages/frontend/src/components/MkUserCardMini.vue b/packages/frontend/src/components/MkUserCardMini.vue
index 603f9f2435..f3c3625c3a 100644
--- a/packages/frontend/src/components/MkUserCardMini.vue
+++ b/packages/frontend/src/components/MkUserCardMini.vue
@@ -23,7 +23,7 @@ import { acct } from '@/filters/user.js';
const props = withDefaults(defineProps<{
user: Misskey.entities.User;
- withChart: boolean;
+ withChart?: boolean;
}>(), {
withChart: true,
});
@@ -49,7 +49,7 @@ $bodyInfoHieght: 16px;
display: flex;
align-items: center;
padding: 16px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: var(--radius-sm);
}
@@ -64,7 +64,7 @@ $bodyInfoHieght: 16px;
flex: 1;
overflow: hidden;
font-size: 0.9em;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
padding-right: 8px;
}
diff --git a/packages/frontend/src/components/MkUserInfo.vue b/packages/frontend/src/components/MkUserInfo.vue
index 73cdd9ce00..64a3867d33 100644
--- a/packages/frontend/src/components/MkUserInfo.vue
+++ b/packages/frontend/src/components/MkUserInfo.vue
@@ -77,7 +77,7 @@ defineProps<{
z-index: 2;
width: var(--avatar);
height: var(--avatar);
- border: solid 4px var(--panel);
+ border: solid 4px var(--MI_THEME-panel);
}
.title {
@@ -98,7 +98,7 @@ defineProps<{
margin: 0;
line-height: 16px;
font-size: 0.8em;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
opacity: 0.7;
}
@@ -116,7 +116,7 @@ defineProps<{
.description {
padding: 16px;
font-size: 0.8em;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.mfm {
@@ -128,7 +128,7 @@ defineProps<{
.status {
padding: 10px 16px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.statusItem {
@@ -139,12 +139,12 @@ defineProps<{
.statusItemLabel {
margin: 0;
font-size: 0.7em;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
}
.statusItemValue {
font-size: 1em;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
.follow {
diff --git a/packages/frontend/src/components/MkUserList.vue b/packages/frontend/src/components/MkUserList.vue
index ac82ecc3d6..8dc01a08ab 100644
--- a/packages/frontend/src/components/MkUserList.vue
+++ b/packages/frontend/src/components/MkUserList.vue
@@ -39,6 +39,6 @@ const props = withDefaults(defineProps<{
.root {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
- grid-gap: var(--margin);
+ grid-gap: var(--MI-margin);
}
</style>
diff --git a/packages/frontend/src/components/MkUserOnlineIndicator.vue b/packages/frontend/src/components/MkUserOnlineIndicator.vue
index 9f04353f62..a4fee52367 100644
--- a/packages/frontend/src/components/MkUserOnlineIndicator.vue
+++ b/packages/frontend/src/components/MkUserOnlineIndicator.vue
@@ -36,7 +36,7 @@ const text = computed(() => {
<style lang="scss" module>
.root {
- box-shadow: 0 0 0 3px var(--panel);
+ box-shadow: 0 0 0 3px var(--MI_THEME-panel);
// sharkey: the comment mentions something about 100% radius not behaving correctly on blink.
// couldn't reproduce, assuming the 120% here was just an old workaround
diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue
index c6f4699b3e..9de8639fe4 100644
--- a/packages/frontend/src/components/MkUserPopup.vue
+++ b/packages/frontend/src/components/MkUserPopup.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<svg viewBox="0 0 128 128" :class="$style.avatarBack">
<g transform="matrix(1.6,0,0,1.6,-38.4,-51.2)">
- <path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--popup);"/>
+ <path d="M64,32C81.661,32 96,46.339 96,64C95.891,72.184 104,72 104,72C104,72 74.096,80 64,80C52.755,80 24,72 24,72C24,72 31.854,72.018 32,64C32,46.339 46.339,32 64,32Z" style="fill: var(--MI_THEME-popup);"/>
</g>
</svg>
<MkAvatar :class="$style.avatar" :user="user" indicator/>
@@ -231,8 +231,8 @@ onMounted(() => {
padding: 16px 26px;
font-size: 0.8em;
text-align: center;
- border-top: solid 1px var(--divider);
- border-bottom: solid 1px var(--divider);
+ border-top: solid 1px var(--MI_THEME-divider);
+ border-bottom: solid 1px var(--MI_THEME-divider);
}
.fields {
@@ -296,7 +296,7 @@ onMounted(() => {
.statusItemLabel {
font-size: 0.7em;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
}
.menu {
@@ -304,7 +304,7 @@ onMounted(() => {
top: 8px;
right: 44px;
padding: 6px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: var(--radius-ellipse);
}
diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue
index a5b48c8ce2..7c11744368 100644
--- a/packages/frontend/src/components/MkUserSelectDialog.vue
+++ b/packages/frontend/src/components/MkUserSelectDialog.vue
@@ -196,11 +196,11 @@ onMounted(() => {
font-size: 14px;
&:hover {
- background: var(--X7);
+ background: var(--MI_THEME-X7);
}
&.selected {
- background: var(--accent);
+ background: var(--MI_THEME-accent);
color: #fff;
}
}
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
index 1524ea0ec9..5153c06139 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -62,7 +62,7 @@ const popularUsers: Paging = {
.users {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
- grid-gap: var(--margin);
+ grid-gap: var(--MI-margin);
justify-content: center;
}
</style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
index 3194641cdb..7cb48f6afb 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -51,6 +51,11 @@ watch(name, () => {
// 空文字列をnullã«ã—ãŸã„ã®ã§??ã¯ä½¿ã†ãª
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
name: name.value || null,
+ }, undefined, {
+ '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
+ title: i18n.ts.yourNameContainsProhibitedWords,
+ text: i18n.ts.yourNameContainsProhibitedWordsDescription,
+ },
});
});
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue
index c80349d034..c7732af808 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.User.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue
@@ -61,7 +61,7 @@ async function follow() {
z-index: 2;
width: var(--avatar);
height: var(--avatar);
- border: solid 4px var(--panel);
+ border: solid 4px var(--MI_THEME-panel);
}
.title {
@@ -82,7 +82,7 @@ async function follow() {
margin: 0;
line-height: 16px;
font-size: 0.8em;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
opacity: 0.7;
}
@@ -99,7 +99,7 @@ async function follow() {
}
.footer {
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
padding: 16px;
}
</style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
index 1fb1eda039..b7261129ef 100644
--- a/packages/frontend/src/components/MkUserSetupDialog.vue
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -35,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
- <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+ <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div>
<div>{{ i18n.ts._initialAccountSetting.letsStartAccountSetup }}</div>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton>
@@ -91,7 +91,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.centerPage">
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
- <i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+ <i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
<div style="padding: 0 16px;">{{ i18n.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div>
<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
@@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="text-align: center;">
- <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+ <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--MI_THEME-accent);"></i>
<div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
<div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div>
<div class="_buttonsCenter" style="margin-top: 16px;">
@@ -223,7 +223,7 @@ async function later(later: boolean) {
.progressBarValue {
height: 100%;
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
transition: all 0.5s cubic-bezier(0,.5,.5,1);
}
@@ -252,7 +252,7 @@ async function later(later: boolean) {
left: 0;
flex-shrink: 0;
padding: 12px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
-webkit-backdrop-filter: blur(15px);
backdrop-filter: blur(15px);
}
diff --git a/packages/frontend/src/components/MkVisibilityPicker.vue b/packages/frontend/src/components/MkVisibilityPicker.vue
index 3c3f9e94b6..465204d7a2 100644
--- a/packages/frontend/src/components/MkVisibilityPicker.vue
+++ b/packages/frontend/src/components/MkVisibilityPicker.vue
@@ -124,7 +124,7 @@ function choose(visibility: typeof Misskey.noteVisibilities[number]): void {
}
&.active {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
diff --git a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
index cab42cd59d..d098dad9a1 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.ActiveUsersChart.vue
@@ -62,7 +62,7 @@ async function renderChart() {
const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
const computedStyle = getComputedStyle(document.documentElement);
- const accent = tinycolor(computedStyle.getPropertyValue('--accent')).toHexString();
+ const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
const colorRead = accent;
const colorWrite = '#2ecc71';
diff --git a/packages/frontend/src/components/MkVisitorDashboard.vue b/packages/frontend/src/components/MkVisitorDashboard.vue
index 874eff6c79..688340c6b1 100644
--- a/packages/frontend/src/components/MkVisitorDashboard.vue
+++ b/packages/frontend/src/components/MkVisitorDashboard.vue
@@ -110,8 +110,8 @@ function showMenu(ev: MouseEvent) {
.panel {
position: relative;
- background: var(--panel);
- border-radius: var(--radius);
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
box-shadow: 0 12px 32px rgb(0 0 0 / 25%);
}
@@ -191,14 +191,14 @@ function showMenu(ev: MouseEvent) {
}
.statsItemLabel {
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
font-size: 0.9em;
}
.statsItemCount {
font-weight: bold;
font-size: 1.2em;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
.tl {
@@ -207,7 +207,7 @@ function showMenu(ev: MouseEvent) {
.tlHeader {
padding: 12px 16px;
- border-bottom: solid 1px var(--divider);
+ border-bottom: solid 1px var(--MI_THEME-divider);
}
.tlBody {
diff --git a/packages/frontend/src/components/MkWaitingDialog.vue b/packages/frontend/src/components/MkWaitingDialog.vue
index 60b75b6d30..34fa6b0723 100644
--- a/packages/frontend/src/components/MkWaitingDialog.vue
+++ b/packages/frontend/src/components/MkWaitingDialog.vue
@@ -47,8 +47,8 @@ watch(() => props.showing, () => {
padding: 32px;
box-sizing: border-box;
text-align: center;
- background: var(--panel);
- border-radius: var(--radius);
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
width: 250px;
&.iconOnly {
@@ -65,7 +65,7 @@ watch(() => props.showing, () => {
font-size: 32px;
&.success {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
&.waiting {
diff --git a/packages/frontend/src/components/MkWidgets.vue b/packages/frontend/src/components/MkWidgets.vue
index 99840bf8d7..f3d0bcd58f 100644
--- a/packages/frontend/src/components/MkWidgets.vue
+++ b/packages/frontend/src/components/MkWidgets.vue
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.root">
<template v-if="edit">
<header :class="$style.editHeader">
- <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--margin)" data-cy-widget-select>
+ <MkSelect v-model="widgetAdderSelected" style="margin-bottom: var(--MI-margin)" data-cy-widget-select>
<template #label>{{ i18n.ts.selectWidget }}</template>
<option v-for="widget in widgetDefs" :key="widget" :value="widget">{{ i18n.ts._widgets[widget] }}</option>
</MkSelect>
@@ -123,7 +123,7 @@ function onContextmenu(widget: Widget, ev: MouseEvent) {
.widget {
contain: content;
- margin: var(--margin) 0;
+ margin: var(--MI-margin) 0;
&:first-of-type {
margin-top: 0;
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 08906a1205..056b6a37ed 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -54,9 +54,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { onBeforeUnmount, onMounted, provide, shallowRef, ref } from 'vue';
+import type { MenuItem } from '@/types/menu.js';
import contains from '@/scripts/contains.js';
import * as os from '@/os.js';
-import type { MenuItem } from '@/types/menu.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
@@ -484,6 +484,10 @@ defineExpose({
}
.root {
+ // universal.vueã¨ã‹ã§ç›´æŽ¥--MI-stickyBottomãŒå®šç¾©ã•れã¦ã„ãŸã‚Šã™ã‚‹ã®ã§ãƒªã‚»ãƒƒãƒˆ
+ --MI-stickyTop: 0;
+ --MI-stickyBottom: 0;
+
position: fixed;
top: 0;
left: 0;
@@ -502,7 +506,7 @@ defineExpose({
contain: content;
width: 100%;
height: 100%;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
}
.header {
@@ -514,10 +518,10 @@ defineExpose({
flex-shrink: 0;
user-select: none;
height: var(--height);
- background: var(--windowHeader);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
- //border-bottom: solid 1px var(--divider);
+ background: var(--MI_THEME-windowHeader);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
+ //border-bottom: solid 1px var(--MI_THEME-divider);
font-size: 90%;
font-weight: bold;
@@ -531,11 +535,11 @@ defineExpose({
width: var(--height);
&:hover {
- color: var(--fgHighlighted);
+ color: var(--MI_THEME-fgHighlighted);
}
&.highlighted {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
@@ -560,7 +564,7 @@ defineExpose({
.content {
flex: 1;
overflow: auto;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
container-type: size;
}
diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue
index f5546edf1e..1cfde0c903 100644
--- a/packages/frontend/src/components/form/link.vue
+++ b/packages/frontend/src/components/form/link.vue
@@ -60,18 +60,18 @@ const props = defineProps<{
width: 100%;
box-sizing: border-box;
padding: 10px 14px;
- background: var(--folderHeaderBg);
+ background: var(--MI_THEME-folderHeaderBg);
border-radius: var(--radius-sm);
font-size: 0.9em;
&:hover {
text-decoration: none;
- background: var(--folderHeaderHoverBg);
+ background: var(--MI_THEME-folderHeaderHoverBg);
}
&.active {
- color: var(--accent);
- background: var(--folderHeaderHoverBg);
+ color: var(--MI_THEME-accent);
+ background: var(--MI_THEME-folderHeaderHoverBg);
}
}
@@ -79,7 +79,7 @@ const props = defineProps<{
margin-right: 0.75em;
flex-shrink: 0;
text-align: center;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
diff --git a/packages/frontend/src/components/form/section.vue b/packages/frontend/src/components/form/section.vue
index ad37daa265..5fca3acc31 100644
--- a/packages/frontend/src/components/form/section.vue
+++ b/packages/frontend/src/components/form/section.vue
@@ -21,8 +21,8 @@ defineProps<{
<style lang="scss" module>
.root {
- border-top: solid 0.5px var(--divider);
- //border-bottom: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
+ //border-bottom: solid 0.5px var(--MI_THEME-divider);
}
.rootFirst {
@@ -49,7 +49,7 @@ defineProps<{
.description {
font-size: 0.85em;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
margin: 0 0 8px 0;
}
</style>
diff --git a/packages/frontend/src/components/form/slot.vue b/packages/frontend/src/components/form/slot.vue
index f54db0ca82..da94b7abbb 100644
--- a/packages/frontend/src/components/form/slot.vue
+++ b/packages/frontend/src/components/form/slot.vue
@@ -35,7 +35,7 @@ function focus() {
.caption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
&:empty {
display: none;
diff --git a/packages/frontend/src/components/global/MkAd.vue b/packages/frontend/src/components/global/MkAd.vue
index 1238e6ef23..57af034354 100644
--- a/packages/frontend/src/components/global/MkAd.vue
+++ b/packages/frontend/src/components/global/MkAd.vue
@@ -30,12 +30,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</component>
</div>
<div v-else :class="$style.menu">
- <div :class="$style.menuContainer">
- <div>Ads by {{ host }}</div>
- <!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>-->
- <MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton>
- <button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button>
- </div>
+ <div>Ads by {{ host }}</div>
+ <!--<MkButton class="button" primary>{{ i18n.ts._ad.like }}</MkButton>-->
+ <MkButton v-if="chosen.ratio !== 0" :class="$style.menuButton" @click="reduceFrequency">{{ i18n.ts._ad.reduceFrequencyOfThisAd }}</MkButton>
+ <button class="_textButton" @click="toggleMenu">{{ i18n.ts._ad.back }}</button>
</div>
</div>
<div v-else></div>
@@ -43,9 +41,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed } from 'vue';
+import { url as local, host } from '@@/js/config.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
-import { url as local, host } from '@@/js/config.js';
import MkButton from '@/components/MkButton.vue';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
@@ -123,8 +121,7 @@ function reduceFrequency(): void {
<style lang="scss" module>
.root {
- background-size: auto auto;
- background-image: repeating-linear-gradient(45deg, transparent, transparent 8px, var(--ad) 8px, var(--ad) 14px );
+
}
.main {
@@ -139,8 +136,6 @@ function reduceFrequency(): void {
}
&.form_horizontal {
- padding: 8px;
-
> .link,
> .link > .img {
max-width: min(600px, 100%);
@@ -149,8 +144,6 @@ function reduceFrequency(): void {
}
&.form_horizontalBig {
- padding: 8px;
-
> .link,
> .link > .img {
max-width: min(600px, 100%);
@@ -191,7 +184,7 @@ function reduceFrequency(): void {
right: 1px;
display: grid;
place-content: center;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: var(--radius-full);
padding: 2px;
}
@@ -202,15 +195,12 @@ function reduceFrequency(): void {
}
.menu {
- padding: 8px;
text-align: center;
-}
-
-.menuContainer {
padding: 8px;
margin: 0 auto;
max-width: 400px;
- border: solid 1px var(--divider);
+ background: var(--MI_THEME-panel);
+ border: solid 1px var(--MI_THEME-divider);
}
.menuButton {
diff --git a/packages/frontend/src/components/global/MkLoading.vue b/packages/frontend/src/components/global/MkLoading.vue
index 49d8ace37b..47d797606b 100644
--- a/packages/frontend/src/components/global/MkLoading.vue
+++ b/packages/frontend/src/components/global/MkLoading.vue
@@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
--size: 38px;
&.colored {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
&.inline {
diff --git a/packages/frontend/src/components/global/MkMfm.ts b/packages/frontend/src/components/global/MkMfm.ts
index 9bf9f4a872..90b3999092 100644
--- a/packages/frontend/src/components/global/MkMfm.ts
+++ b/packages/frontend/src/components/global/MkMfm.ts
@@ -7,6 +7,7 @@ import { VNode, h, defineAsyncComponent, SetupContext, provide } from 'vue';
import * as mfm from '@transfem-org/sfm-js';
import * as Misskey from 'misskey-js';
import CkFollowMouse from '../CkFollowMouse.vue';
+import { host } from '@@/js/config.js';
import MkUrl from '@/components/global/MkUrl.vue';
import MkTime from '@/components/global/MkTime.vue';
import MkLink from '@/components/MkLink.vue';
@@ -18,7 +19,6 @@ import MkCodeInline from '@/components/MkCodeInline.vue';
import MkGoogle from '@/components/MkGoogle.vue';
import MkSparkle from '@/components/MkSparkle.vue';
import MkA, { MkABehavior } from '@/components/global/MkA.vue';
-import { host } from '@@/js/config.js';
import { defaultStore } from '@/store.js';
function safeParseFloat(str: unknown): number | null {
@@ -32,8 +32,8 @@ const QUOTE_STYLE = `
display: block;
margin: 8px;
padding: 6px 0 6px 12px;
-color: var(--fg);
-border-left: solid 3px var(--fg);
+color: var(--MI_THEME-fg);
+border-left: solid 3px var(--MI_THEME-fg);
opacity: 0.7;
`.split('\n').join(' ');
@@ -60,7 +60,8 @@ type MfmEvents = {
// eslint-disable-next-line import/no-default-export
export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEvents>['emit'] }) {
- provide('linkNavigationBehavior', props.linkNavigationBehavior);
+ // ã“ã†ã—ãŸã„ã¨ã“ã‚ã ã‘ã© functional component 内ã§ã¯ provide ã¯ä½¿ãˆãªã„
+ //provide('linkNavigationBehavior', props.linkNavigationBehavior);
const isNote = props.isNote ?? true;
const shouldNyaize = props.nyaize === 'respect' && props.author?.isCat && props.author?.speakAsCat && !defaultStore.state.disableCatSpeak;
@@ -328,7 +329,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
}
case 'border': {
let color = validColor(token.props.args.color);
- color = color ? `#${color}` : 'var(--accent)';
+ color = color ? `#${color}` : 'var(--MI_THEME-accent)';
let b_style = token.props.args.style;
if (
typeof b_style !== 'string' ||
@@ -361,7 +362,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
const child = token.children[0];
const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
return h('span', {
- style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: var(--radius-ellipse); padding: 4px 10px 4px 6px;',
+ style: 'display: inline-block; font-size: 90%; border: solid 1px var(--MI_THEME-divider); border-radius: var(--radius-ellipse); padding: 4px 10px 4px 6px;',
}, [
h('i', {
class: 'ti ti-clock',
@@ -409,6 +410,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
+ navigationBehavior: props.linkNavigationBehavior,
}))];
}
@@ -417,6 +419,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
key: Math.random(),
url: token.props.url,
rel: 'nofollow noopener',
+ navigationBehavior: props.linkNavigationBehavior,
}, genEl(token.children, scale, true)))];
}
@@ -425,6 +428,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
key: Math.random(),
host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
username: token.props.username,
+ navigationBehavior: props.linkNavigationBehavior,
}))];
}
@@ -432,7 +436,8 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven
return [h('bdi', h(MkA, {
key: Math.random(),
to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
- style: 'color:var(--hashtag);',
+ style: 'color:var(--MI_THEME-hashtag);',
+ behavior: props.linkNavigationBehavior,
}, `#${token.props.hashtag}`))];
}
diff --git a/packages/frontend/src/components/global/MkPageHeader.tabs.vue b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
index 7d13fb9279..20be1b8725 100644
--- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue
@@ -248,7 +248,7 @@ onUnmounted(() => {
position: absolute;
bottom: 0;
height: 3px;
- background: var(--accent);
+ background: var(--MI_THEME-accent);
border-radius: var(--radius-ellipse);
transition: none;
pointer-events: none;
diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue
index bb20f54fa4..cc7633dcff 100644
--- a/packages/frontend/src/components/global/MkPageHeader.vue
+++ b/packages/frontend/src/components/global/MkPageHeader.vue
@@ -115,7 +115,7 @@ function goBack(): void {
}
const calcBg = () => {
- const rawBg = 'var(--bg)';
+ const rawBg = 'var(--MI_THEME-bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
@@ -146,9 +146,9 @@ onUnmounted(() => {
<style lang="scss" module>
.root {
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
- border-bottom: solid 0.5px var(--divider);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
width: 100%;
}
@@ -161,7 +161,7 @@ onUnmounted(() => {
.upper {
--height: 50px;
display: flex;
- gap: var(--margin);
+ gap: var(--MI-margin);
height: var(--height);
.tabs:first-child {
@@ -246,7 +246,7 @@ onUnmounted(() => {
}
&.highlighted {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
diff --git a/packages/frontend/src/components/global/MkStickyContainer.vue b/packages/frontend/src/components/global/MkStickyContainer.vue
index 72993991ce..1aebf487bb 100644
--- a/packages/frontend/src/components/global/MkStickyContainer.vue
+++ b/packages/frontend/src/components/global/MkStickyContainer.vue
@@ -5,32 +5,30 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div ref="rootEl">
- <div ref="headerEl">
+ <div ref="headerEl" :class="$style.header">
<slot name="header"></slot>
</div>
<div
- ref="bodyEl"
+ :class="$style.body"
:data-sticky-container-header-height="headerHeight"
:data-sticky-container-footer-height="footerHeight"
- style="position: relative; z-index: 0;"
>
<slot></slot>
</div>
- <div ref="footerEl">
+ <div ref="footerEl" :class="$style.footer">
<slot name="footer"></slot>
</div>
</div>
</template>
<script lang="ts" setup>
-import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, shallowRef } from 'vue';
+import { onMounted, onUnmounted, provide, inject, Ref, ref, watch, useTemplateRef } from 'vue';
import { CURRENT_STICKY_BOTTOM, CURRENT_STICKY_TOP } from '@@/js/const.js';
-const rootEl = shallowRef<HTMLElement>();
-const headerEl = shallowRef<HTMLElement>();
-const footerEl = shallowRef<HTMLElement>();
-const bodyEl = shallowRef<HTMLElement>();
+const rootEl = useTemplateRef('rootEl');
+const headerEl = useTemplateRef('headerEl');
+const footerEl = useTemplateRef('footerEl');
const headerHeight = ref<string | undefined>();
const childStickyTop = ref(0);
@@ -67,31 +65,11 @@ onMounted(() => {
watch([parentStickyTop, parentStickyBottom], calc);
- watch(childStickyTop, () => {
- if (bodyEl.value == null) return;
- bodyEl.value.style.setProperty('--stickyTop', `${childStickyTop.value}px`);
- }, {
- immediate: true,
- });
-
- watch(childStickyBottom, () => {
- if (bodyEl.value == null) return;
- bodyEl.value.style.setProperty('--stickyBottom', `${childStickyBottom.value}px`);
- }, {
- immediate: true,
- });
-
if (headerEl.value != null) {
- headerEl.value.style.position = 'sticky';
- headerEl.value.style.top = 'var(--stickyTop, 0)';
- headerEl.value.style.zIndex = '1';
observer.observe(headerEl.value);
}
if (footerEl.value != null) {
- footerEl.value.style.position = 'sticky';
- footerEl.value.style.bottom = 'var(--stickyBottom, 0)';
- footerEl.value.style.zIndex = '1';
observer.observe(footerEl.value);
}
});
@@ -101,6 +79,27 @@ onUnmounted(() => {
});
defineExpose({
- rootEl: rootEl,
+ rootEl,
});
</script>
+
+<style lang='scss' module>
+.body {
+ position: relative;
+ z-index: 0;
+ --MI-stickyTop: v-bind("childStickyTop + 'px'");
+ --MI-stickyBottom: v-bind("childStickyBottom + 'px'");
+}
+
+.header {
+ position: sticky;
+ top: var(--MI-stickyTop, 0);
+ z-index: 1;
+}
+
+.footer {
+ position: sticky;
+ bottom: var(--MI-stickyBottom, 0);
+ z-index: 1;
+}
+</style>
diff --git a/packages/frontend/src/components/global/MkTime.vue b/packages/frontend/src/components/global/MkTime.vue
index 50bec990a1..f600f7eed2 100644
--- a/packages/frontend/src/components/global/MkTime.vue
+++ b/packages/frontend/src/components/global/MkTime.vue
@@ -99,10 +99,10 @@ if (!invalid && props.origin === null && (props.mode === 'relative' || props.mod
<style lang="scss" module>
.old1 {
- color: var(--warn);
+ color: var(--MI_THEME-warn);
}
.old1.old2 {
- color: var(--error);
+ color: var(--MI_THEME-error);
}
</style>
diff --git a/packages/frontend/src/components/global/RouterView.vue b/packages/frontend/src/components/global/RouterView.vue
index 19bd794a5d..38bdfc52d4 100644
--- a/packages/frontend/src/components/global/RouterView.vue
+++ b/packages/frontend/src/components/global/RouterView.vue
@@ -27,6 +27,7 @@ import MkLoadingPage from '@/pages/_loading_.vue';
const props = defineProps<{
router?: IRouter;
+ nested?: boolean;
}>();
const router = props.router ?? inject('router');
@@ -39,6 +40,8 @@ const currentDepth = inject('routerCurrentDepth', 0);
provide('routerCurrentDepth', currentDepth + 1);
function resolveNested(current: Resolved, d = 0): Resolved | null {
+ if (!props.nested) return current;
+
if (d === currentDepth) {
return current;
} else {
diff --git a/packages/frontend/src/components/page/page.dynamic.vue b/packages/frontend/src/components/page/page.dynamic.vue
index 8c511a690d..c2449931c1 100644
--- a/packages/frontend/src/components/page/page.dynamic.vue
+++ b/packages/frontend/src/components/page/page.dynamic.vue
@@ -27,9 +27,9 @@ const props = defineProps<{
<style lang="scss" module>
.root {
- border: 1px solid var(--divider);
- border-radius: var(--radius);
- padding: var(--margin);
+ border: 1px solid var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
+ padding: var(--MI-margin);
text-align: center;
}
diff --git a/packages/frontend/src/components/page/page.image.vue b/packages/frontend/src/components/page/page.image.vue
index fc1ce9fc7b..69443ce7dd 100644
--- a/packages/frontend/src/components/page/page.image.vue
+++ b/packages/frontend/src/components/page/page.image.vue
@@ -28,8 +28,8 @@ onMounted(() => {
<style lang="scss" module>
.root {
- border: 1px solid var(--divider);
- border-radius: var(--radius);
+ border: 1px solid var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
overflow: hidden;
}
.mediaList {
diff --git a/packages/frontend/src/components/page/page.note.vue b/packages/frontend/src/components/page/page.note.vue
index b5ba407806..84436e7adb 100644
--- a/packages/frontend/src/components/page/page.note.vue
+++ b/packages/frontend/src/components/page/page.note.vue
@@ -35,7 +35,7 @@ onMounted(() => {
<style lang="scss" module>
.root {
- border: 1px solid var(--divider);
- border-radius: var(--radius);
+ border: 1px solid var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
}
</style>
diff --git a/packages/frontend/src/directives/adaptive-bg.ts b/packages/frontend/src/directives/adaptive-bg.ts
index 23fd1bddf4..45891de889 100644
--- a/packages/frontend/src/directives/adaptive-bg.ts
+++ b/packages/frontend/src/directives/adaptive-bg.ts
@@ -21,7 +21,7 @@ export default {
const myBg = window.getComputedStyle(src).backgroundColor;
if (parentBg === myBg) {
- src.style.backgroundColor = 'var(--bg)';
+ src.style.backgroundColor = 'var(--MI_THEME-bg)';
} else {
src.style.backgroundColor = myBg;
}
diff --git a/packages/frontend/src/directives/adaptive-border.ts b/packages/frontend/src/directives/adaptive-border.ts
index b436075fcd..685ca38e96 100644
--- a/packages/frontend/src/directives/adaptive-border.ts
+++ b/packages/frontend/src/directives/adaptive-border.ts
@@ -21,7 +21,7 @@ export default {
const myBg = window.getComputedStyle(src).backgroundColor;
if (parentBg === myBg) {
- src.style.borderColor = 'var(--divider)';
+ src.style.borderColor = 'var(--MI_THEME-divider)';
} else {
src.style.borderColor = myBg;
}
diff --git a/packages/frontend/src/directives/panel.ts b/packages/frontend/src/directives/panel.ts
index bbcc220e09..7b5969c679 100644
--- a/packages/frontend/src/directives/panel.ts
+++ b/packages/frontend/src/directives/panel.ts
@@ -18,12 +18,12 @@ export default {
const parentBg = getBgColor(src.parentElement);
- const myBg = getComputedStyle(document.documentElement).getPropertyValue('--panel');
+ const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel');
if (parentBg === myBg) {
- src.style.backgroundColor = 'var(--bg)';
+ src.style.backgroundColor = 'var(--MI_THEME-bg)';
} else {
- src.style.backgroundColor = 'var(--panel)';
+ src.style.backgroundColor = 'var(--MI_THEME-panel)';
}
},
} as Directive;
diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts
index e73aa77a3c..5d3d4c0a55 100644
--- a/packages/frontend/src/os.ts
+++ b/packages/frontend/src/os.ts
@@ -10,6 +10,7 @@ import { EventEmitter } from 'eventemitter3';
import * as Misskey from 'misskey-js';
import type { ComponentProps as CP } from 'vue-component-type-helpers';
import type { Form, GetFormResultType } from '@/scripts/form.js';
+import type { MenuItem } from '@/types/menu.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { defaultStore } from '@/store.js';
import { i18n } from '@/i18n.js';
@@ -22,7 +23,6 @@ import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
import MkPopupMenu from '@/components/MkPopupMenu.vue';
import MkContextMenu from '@/components/MkContextMenu.vue';
-import type { MenuItem } from '@/types/menu.js';
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
import { pleaseLogin } from '@/scripts/please-login.js';
import { showMovedDialog } from '@/scripts/show-moved-dialog.js';
@@ -35,6 +35,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
endpoint: E,
data: P = {} as any,
token?: string | null | undefined,
+ customErrors?: Record<string, { title?: string; text: string; }>,
) => {
const promise = misskeyApi(endpoint, data, token);
promiseDialog(promise, null, async (err) => {
@@ -77,6 +78,9 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
} else if (err.message.startsWith('Unexpected token')) {
title = i18n.ts.gotInvalidResponseError;
text = i18n.ts.gotInvalidResponseErrorDescription;
+ } else if (customErrors && customErrors[err.id] != null) {
+ title = customErrors[err.id].title;
+ text = customErrors[err.id].text;
}
alert({
type: 'error',
@@ -86,7 +90,7 @@ export const apiWithDialog = (<E extends keyof Misskey.Endpoints = keyof Misskey
});
return promise;
-}) as typeof misskeyApi;
+});
export function promiseDialog<T extends Promise<any>>(
promise: T,
diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue
index 71353c7dfa..2190be8bec 100644
--- a/packages/frontend/src/pages/about.federation.vue
+++ b/packages/frontend/src/pages/about.federation.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix><i class="ti ti-search"></i></template>
<template #label>{{ i18n.ts.host }}</template>
</MkInput>
- <FormSplit style="margin-top: var(--margin);">
+ <FormSplit style="margin-top: var(--MI-margin);">
<MkSelect v-model="state">
<template #label>{{ i18n.ts.state }}</template>
<option value="all">{{ i18n.ts.all }}</option>
diff --git a/packages/frontend/src/pages/about.overview.vue b/packages/frontend/src/pages/about.overview.vue
index ca070c65a4..42898b3447 100644
--- a/packages/frontend/src/pages/about.overview.vue
+++ b/packages/frontend/src/pages/about.overview.vue
@@ -172,7 +172,7 @@ await misskeyApi('sponsors', { instance: true }).then((res) => sponsors.value.pu
text-align: center;
border-radius: var(--radius);
overflow: clip;
- background-color: var(--panel);
+ background-color: var(--MI_THEME-panel);
background-size: cover;
background-position: center center;
}
@@ -208,14 +208,14 @@ await misskeyApi('sponsors', { instance: true }).then((res) => sponsors.value.pu
flex-shrink: 0;
display: flex;
position: sticky;
- top: calc(var(--stickyTop, 0px) + 8px);
+ top: calc(var(--MI-stickyTop, 0px) + 8px);
counter-increment: item;
content: counter(item);
width: 32px;
height: 32px;
line-height: 32px;
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
font-size: 13px;
font-weight: bold;
align-items: center;
diff --git a/packages/frontend/src/pages/admin-user.vue b/packages/frontend/src/pages/admin-user.vue
index 22e16effe0..f63b81c393 100644
--- a/packages/frontend/src/pages/admin-user.vue
+++ b/packages/frontend/src/pages/admin-user.vue
@@ -55,6 +55,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
+ <template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
<FormSection v-if="user.host">
@@ -140,15 +141,21 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-else-if="tab === 'announcements'" class="_gaps">
<MkButton primary rounded @click="createAnnouncement"><i class="ti ti-plus"></i> {{ i18n.ts._announcement.new }}</MkButton>
+ <MkSelect v-model="announcementsStatus">
+ <template #label>{{ i18n.ts.filter }}</template>
+ <option value="active">{{ i18n.ts.active }}</option>
+ <option value="archived">{{ i18n.ts.archived }}</option>
+ </MkSelect>
+
<MkPagination :pagination="announcementsPagination">
<template #default="{ items }">
<div class="_gaps_s">
<div v-for="announcement in items" :key="announcement.id" v-panel :class="$style.announcementItem" @click="editAnnouncement(announcement)">
<span style="margin-right: 0.5em;">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
- <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
- <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
- <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+ <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+ <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+ <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
</span>
<span>{{ announcement.title }}</span>
<span v-if="announcement.reads > 0" style="margin-left: auto; opacity: 0.7;">{{ i18n.ts.messageRead }}</span>
@@ -193,6 +200,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, defineAsyncComponent, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { url } from '@@/js/config.js';
import MkChart from '@/components/MkChart.vue';
import MkObjectView from '@/components/MkObjectView.vue';
import MkTextarea from '@/components/MkTextarea.vue';
@@ -208,7 +216,6 @@ import MkFileListForAdmin from '@/components/MkFileListForAdmin.vue';
import MkInfo from '@/components/MkInfo.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
-import { url } from '@@/js/config.js';
import { acct } from '@/filters/user.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { i18n } from '@/i18n.js';
@@ -244,11 +251,15 @@ const filesPagination = {
userId: props.userId,
})),
};
+
+const announcementsStatus = ref<'active' | 'archived'>('active');
+
const announcementsPagination = {
endpoint: 'admin/announcements/list' as const,
limit: 10,
params: computed(() => ({
userId: props.userId,
+ status: announcementsStatus.value,
})),
};
const expandedRoles = ref([]);
@@ -600,18 +611,18 @@ definePageMetadata(() => ({
}
> .suspended {
- color: var(--error);
- border-color: var(--error);
+ color: var(--MI_THEME-error);
+ border-color: var(--MI_THEME-error);
}
> .silenced {
- color: var(--warn);
- border-color: var(--warn);
+ color: var(--MI_THEME-warn);
+ border-color: var(--MI_THEME-warn);
}
> .moderator {
- color: var(--success);
- border-color: var(--success);
+ color: var(--MI_THEME-success);
+ border-color: var(--MI_THEME-success);
}
}
}
@@ -670,7 +681,7 @@ definePageMetadata(() => ({
.roleItemSub {
padding: 6px 12px;
font-size: 85%;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
}
.roleUnassign {
diff --git a/packages/frontend/src/pages/admin/RolesEditorFormula.vue b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
index f001a4ac20..4762ef3f97 100644
--- a/packages/frontend/src/pages/admin/RolesEditorFormula.vue
+++ b/packages/frontend/src/pages/admin/RolesEditorFormula.vue
@@ -155,12 +155,12 @@ function removeSelf() {
}
.item {
- border: solid 2px var(--divider);
- border-radius: var(--radius);
+ border: solid 2px var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
padding: 12px;
&:hover {
- border-color: var(--accent);
+ border-color: var(--MI_THEME-accent);
}
}
</style>
diff --git a/packages/frontend/src/pages/admin/_header_.vue b/packages/frontend/src/pages/admin/_header_.vue
index 2a71e3efab..b061b2fa0c 100644
--- a/packages/frontend/src/pages/admin/_header_.vue
+++ b/packages/frontend/src/pages/admin/_header_.vue
@@ -119,7 +119,7 @@ function onTabClick(tab: Tab, ev: MouseEvent): void {
}
const calcBg = () => {
- const rawBg = pageMetadata.value?.bg ?? 'var(--bg)';
+ const rawBg = pageMetadata.value?.bg ?? 'var(--MI_THEME-bg)';
const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
tinyBg.setAlpha(0.85);
bg.value = tinyBg.toRgbString();
@@ -156,8 +156,8 @@ onUnmounted(() => {
--height: 60px;
display: flex;
width: 100%;
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
> .buttons {
--margin: 8px;
@@ -189,7 +189,7 @@ onUnmounted(() => {
}
&.highlighted {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
@@ -286,7 +286,7 @@ onUnmounted(() => {
position: absolute;
bottom: 0;
height: 3px;
- background: var(--accent);
+ background: var(--MI_THEME-accent);
border-radius: var(--radius-ellipse);
transition: all 0.2s ease;
pointer-events: none;
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
index 827e22e8ae..eef24afd32 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.editor.vue
@@ -294,10 +294,10 @@ onMounted(async () => {
bottom: 0;
left: 0;
padding: 12px;
- border-top: solid 0.5px var(--divider);
- background: var(--acrylicBg);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ border-top: solid 0.5px var(--MI_THEME-divider);
+ background: var(--MI_THEME-acrylicBg);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
.systemWebhook {
diff --git a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
index 0b86808faf..36d586bd23 100644
--- a/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
+++ b/packages/frontend/src/pages/admin/abuse-report/notification-recipient.item.vue
@@ -87,7 +87,7 @@ function onDeleteButtonClicked() {
}
.rightDivider {
- border-right: 0.5px solid var(--divider);
+ border-right: 0.5px solid var(--MI_THEME-divider);
}
.recipientButtons {
@@ -108,7 +108,7 @@ function onDeleteButtonClicked() {
padding: 8px;
&:hover {
- background-color: var(--buttonBg);
+ background-color: var(--MI_THEME-buttonBg);
}
}
</style>
diff --git a/packages/frontend/src/pages/admin/abuses.vue b/packages/frontend/src/pages/admin/abuses.vue
index c8a9ca7112..a164ecb1fe 100644
--- a/packages/frontend/src/pages/admin/abuses.vue
+++ b/packages/frontend/src/pages/admin/abuses.vue
@@ -12,6 +12,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton link to="/admin/abuse-report-notification-recipient" primary>{{ i18n.ts.notificationSetting }}</MkButton>
</div>
+ <MkInfo v-if="!defaultStore.reactiveState.abusesTutorial.value" closable @close="closeTutorial()">
+ {{ i18n.ts._abuseUserReport.resolveTutorial }}
+ </MkInfo>
+
<div :class="$style.inputs" class="_gaps">
<MkSelect v-model="state" style="margin: 0; flex: 1;">
<template #label>{{ i18n.ts.state }}</template>
@@ -44,8 +48,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
-->
- <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);">
- <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
+ <MkPagination v-slot="{items}" ref="reports" :pagination="pagination" :displayLimit="50">
+ <div class="_gaps">
+ <XAbuseReport v-for="report in items" :key="report.id" :report="report" @resolved="resolved"/>
+ </div>
</MkPagination>
</div>
</MkSpacer>
@@ -54,7 +60,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { computed, shallowRef, ref } from 'vue';
-
import XHeader from './_header_.vue';
import MkSelect from '@/components/MkSelect.vue';
import MkPagination from '@/components/MkPagination.vue';
@@ -62,6 +67,8 @@ import XAbuseReport from '@/components/MkAbuseReport.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkButton from '@/components/MkButton.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import { defaultStore } from '@/store.js';
const reports = shallowRef<InstanceType<typeof MkPagination>>();
@@ -85,6 +92,10 @@ function resolved(reportId) {
reports.value?.removeItem(reportId);
}
+function closeTutorial() {
+ defaultStore.set('abusesTutorial', false);
+}
+
const headerActions = computed(() => []);
const headerTabs = computed(() => []);
diff --git a/packages/frontend/src/pages/admin/ads.vue b/packages/frontend/src/pages/admin/ads.vue
index 6c8901b10b..0d67359e47 100644
--- a/packages/frontend/src/pages/admin/ads.vue
+++ b/packages/frontend/src/pages/admin/ads.vue
@@ -266,7 +266,7 @@ definePageMetadata(() => ({
padding: 32px;
&:not(:last-child) {
- margin-bottom: var(--margin);
+ margin-bottom: var(--MI-margin);
}
}
.input {
diff --git a/packages/frontend/src/pages/admin/announcements.vue b/packages/frontend/src/pages/admin/announcements.vue
index fd37311b21..e420586017 100644
--- a/packages/frontend/src/pages/admin/announcements.vue
+++ b/packages/frontend/src/pages/admin/announcements.vue
@@ -24,9 +24,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ announcement.title }}</template>
<template #icon>
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
- <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
- <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
- <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+ <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+ <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+ <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
</template>
<template #caption>{{ announcement.text }}</template>
<template #footer>
@@ -51,9 +51,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkRadios v-model="announcement.icon">
<template #label>{{ i18n.ts.icon }}</template>
<option value="info"><i class="ti ti-info-circle"></i></option>
- <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--warn);"></i></option>
- <option value="error"><i class="ti ti-circle-x" style="color: var(--error);"></i></option>
- <option value="success"><i class="ti ti-check" style="color: var(--success);"></i></option>
+ <option value="warning"><i class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i></option>
+ <option value="error"><i class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i></option>
+ <option value="success"><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i></option>
</MkRadios>
<MkRadios v-model="announcement.display">
<template #label>{{ i18n.ts.display }}</template>
diff --git a/packages/frontend/src/pages/admin/bot-protection.vue b/packages/frontend/src/pages/admin/bot-protection.vue
index 644436cde6..2f6dac8097 100644
--- a/packages/frontend/src/pages/admin/bot-protection.vue
+++ b/packages/frontend/src/pages/admin/bot-protection.vue
@@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-else-if="botProtectionForm.savedState.provider === 'recaptcha'" #suffix>reCAPTCHA</template>
<template v-else-if="botProtectionForm.savedState.provider === 'turnstile'" #suffix>Turnstile</template>
<template v-else-if="botProtectionForm.savedState.provider === 'fc'" #suffix>FriendlyCaptcha</template>
+ <template v-else-if="botProtectionForm.savedState.provider === 'testcaptcha'" #suffix>testCaptcha</template>
<template v-else #suffix>{{ i18n.ts.none }} ({{ i18n.ts.notRecommended }})</template>
<template v-if="botProtectionForm.modified.value" #footer>
<MkFormFooter :form="botProtectionForm"/>
@@ -25,6 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<option value="recaptcha">reCAPTCHA</option>
<option value="turnstile">Turnstile</option>
<option value="fc">FriendlyCaptcha</option>
+ <option value="testcaptcha">testCaptcha</option>
</MkRadios>
<template v-if="botProtectionForm.state.provider === 'hcaptcha'">
@@ -101,6 +103,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkCaptcha provider="fc" :sitekey="botProtectionForm.state.fcSiteKey"/>
</FormSlot>
</template>
+ <template v-else-if="botProtectionForm.state.provider === 'testcaptcha'">
+ <MkInfo warn><span v-html="i18n.ts.testCaptchaWarning"></span></MkInfo>
+ <FormSlot>
+ <template #label>{{ i18n.ts.preview }}</template>
+ <MkCaptcha provider="testcaptcha"/>
+ </FormSlot>
+ </template>
</div>
</MkFolder>
</template>
@@ -117,6 +126,7 @@ import { i18n } from '@/i18n.js';
import { useForm } from '@/scripts/use-form.js';
import MkFormFooter from '@/components/MkFormFooter.vue';
import MkFolder from '@/components/MkFolder.vue';
+import MkInfo from '@/components/MkInfo.vue';
const MkCaptcha = defineAsyncComponent(() => import('@/components/MkCaptcha.vue'));
@@ -133,6 +143,8 @@ const botProtectionForm = useForm({
? 'mcaptcha'
: meta.enableFC
? 'fc'
+ : meta.enableTestcaptcha
+ ? 'testcaptcha'
: null,
hcaptchaSiteKey: meta.hcaptchaSiteKey,
hcaptchaSecretKey: meta.hcaptchaSecretKey,
@@ -163,6 +175,7 @@ const botProtectionForm = useForm({
enableFC: state.provider === 'fc',
fcSiteKey: state.fcSiteKey,
fcSecretKey: state.fcSecretKey,
+ enableTestcaptcha: state.provider === 'testcaptcha',
});
fetchInstance(true);
});
diff --git a/packages/frontend/src/pages/admin/branding.vue b/packages/frontend/src/pages/admin/branding.vue
index d3d52002fe..cc05466832 100644
--- a/packages/frontend/src/pages/admin/branding.vue
+++ b/packages/frontend/src/pages/admin/branding.vue
@@ -218,7 +218,7 @@ definePageMetadata(() => ({
<style lang="scss" module>
.footer {
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
</style>
diff --git a/packages/frontend/src/pages/admin/email-settings.vue b/packages/frontend/src/pages/admin/email-settings.vue
index ddfe5ae81f..2f1d12ebb5 100644
--- a/packages/frontend/src/pages/admin/email-settings.vue
+++ b/packages/frontend/src/pages/admin/email-settings.vue
@@ -138,7 +138,7 @@ definePageMetadata(() => ({
<style lang="scss" module>
.footer {
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
</style>
diff --git a/packages/frontend/src/pages/admin/federation.vue b/packages/frontend/src/pages/admin/federation.vue
index 1902c97724..ef6bbb865b 100644
--- a/packages/frontend/src/pages/admin/federation.vue
+++ b/packages/frontend/src/pages/admin/federation.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #prefix><i class="ti ti-search"></i></template>
<template #label>{{ i18n.ts.host }}</template>
</MkInput>
- <FormSplit style="margin-top: var(--margin);">
+ <FormSplit style="margin-top: var(--MI-margin);">
<MkSelect v-model="state">
<template #label>{{ i18n.ts.state }}</template>
<option value="all">{{ i18n.ts.all }}</option>
diff --git a/packages/frontend/src/pages/admin/files.vue b/packages/frontend/src/pages/admin/files.vue
index 5132b85c64..4cc859227f 100644
--- a/packages/frontend/src/pages/admin/files.vue
+++ b/packages/frontend/src/pages/admin/files.vue
@@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><XHeader :actions="headerActions"/></template>
<MkSpacer :contentMax="900">
<div class="_gaps">
- <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+ <div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
<MkSelect v-model="origin" style="margin: 0; flex: 1;">
<template #label>{{ i18n.ts.instance }}</template>
<option value="combined">{{ i18n.ts.all }}</option>
@@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #label>{{ i18n.ts.host }}</template>
</MkInput>
</div>
- <div class="inputs" style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+ <div class="inputs" style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
<MkInput v-model="userId" :debounce="true" type="search" style="margin: 0; flex: 1;">
<template #label>User ID</template>
</MkInput>
diff --git a/packages/frontend/src/pages/admin/index.vue b/packages/frontend/src/pages/admin/index.vue
index e8d123060a..1fb49d51d9 100644
--- a/packages/frontend/src/pages/admin/index.vue
+++ b/packages/frontend/src/pages/admin/index.vue
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSpacer>
</div>
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
- <RouterView/>
+ <RouterView nested/>
</div>
</div>
</template>
@@ -346,7 +346,7 @@ defineExpose({
width: 32%;
max-width: 280px;
box-sizing: border-box;
- border-right: solid 0.5px var(--divider);
+ border-right: solid 0.5px var(--MI_THEME-divider);
overflow: auto;
height: 100%;
}
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index bbcf2a6f77..03d1731a74 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -12,6 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m">
<MkSwitch v-model="enableRegistration" @change="onChange_enableRegistration">
<template #label>{{ i18n.ts.enableRegistration }}</template>
+ <template #caption>{{ i18n.ts._serverSettings.thisSettingWillAutomaticallyOffWhenModeratorsInactive }}</template>
</MkSwitch>
<MkSwitch v-model="emailRequiredForSignup" @change="onChange_emailRequiredForSignup">
@@ -85,6 +86,18 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkFolder>
<MkFolder>
+ <template #icon><i class="ti ti-user-x"></i></template>
+ <template #label>{{ i18n.ts.prohibitedWordsForNameOfUser }}</template>
+
+ <div class="_gaps">
+ <MkTextarea v-model="prohibitedWordsForNameOfUser">
+ <template #caption>{{ i18n.ts.prohibitedWordsForNameOfUserDescription }}<br>{{ i18n.ts.prohibitedWordsDescription2 }}</template>
+ </MkTextarea>
+ <MkButton primary @click="save_prohibitedWordsForNameOfUser">{{ i18n.ts.save }}</MkButton>
+ </div>
+ </MkFolder>
+
+ <MkFolder>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #label>{{ i18n.ts.hiddenTags }}</template>
@@ -160,6 +173,7 @@ const approvalRequiredForSignup = ref<boolean>(false);
const bubbleTimelineEnabled = ref<boolean>(false);
const sensitiveWords = ref<string>('');
const prohibitedWords = ref<string>('');
+const prohibitedWordsForNameOfUser = ref<string>('');
const hiddenTags = ref<string>('');
const preservedUsernames = ref<string>('');
const bubbleTimeline = ref<string>('');
@@ -175,13 +189,14 @@ async function init() {
approvalRequiredForSignup.value = meta.approvalRequiredForSignup;
sensitiveWords.value = meta.sensitiveWords.join('\n');
prohibitedWords.value = meta.prohibitedWords.join('\n');
+ prohibitedWordsForNameOfUser.value = meta.prohibitedWordsForNameOfUser.join('\n');
hiddenTags.value = meta.hiddenTags.join('\n');
preservedUsernames.value = meta.preservedUsernames.join('\n');
bubbleTimeline.value = meta.bubbleInstances.join('\n');
bubbleTimelineEnabled.value = meta.policies.btlAvailable;
trustedLinkUrlPatterns.value = meta.trustedLinkUrlPatterns.join('\n');
blockedHosts.value = meta.blockedHosts.join('\n');
- silencedHosts.value = meta.silencedHosts.join('\n');
+ silencedHosts.value = meta.silencedHosts?.join('\n') ?? '';
mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n');
}
@@ -249,6 +264,14 @@ function save_prohibitedWords() {
});
}
+function save_prohibitedWordsForNameOfUser() {
+ os.apiWithDialog('admin/update-meta', {
+ prohibitedWordsForNameOfUser: prohibitedWordsForNameOfUser.value.split('\n'),
+ }).then(() => {
+ fetchInstance(true);
+ });
+}
+
function save_hiddenTags() {
os.apiWithDialog('admin/update-meta', {
hiddenTags: hiddenTags.value.split('\n'),
diff --git a/packages/frontend/src/pages/admin/modlog.ModLog.vue b/packages/frontend/src/pages/admin/modlog.ModLog.vue
index 6c81155c51..8af8d29e1a 100644
--- a/packages/frontend/src/pages/admin/modlog.ModLog.vue
+++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue
@@ -101,7 +101,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<div>
- <div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+ <div style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
<div style="flex: 1;">{{ i18n.ts.moderator }}: <MkA :to="`/admin/user/${log.userId}`" class="_link">@{{ log.user?.username }}</MkA></div>
<div style="flex: 1;">{{ i18n.ts.dateAndTime }}: <MkTime :time="log.createdAt" mode="detail"/></div>
</div>
@@ -180,6 +180,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<CodeDiff :context="5" :hideHeader="true" :oldString="JSON5.stringify(log.info.before, null, '\t')" :newString="JSON5.stringify(log.info.after, null, '\t')" language="javascript" maxHeight="300px"/>
</div>
</template>
+ <template v-else-if="log.type === 'updateAbuseReportNote'">
+ <div :class="$style.diff">
+ <CodeDiff :context="5" :hideHeader="true" :oldString="log.info.before ?? ''" :newString="log.info.after ?? ''" maxHeight="300px"/>
+ </div>
+ </template>
<details>
<summary>raw</summary>
@@ -215,14 +220,14 @@ const props = defineProps<{
}
.logYellow {
- color: var(--warn);
+ color: var(--MI_THEME-warn);
}
.logRed {
- color: var(--error);
+ color: var(--MI_THEME-error);
}
.logGreen {
- color: var(--success);
+ color: var(--MI_THEME-success);
}
</style>
diff --git a/packages/frontend/src/pages/admin/modlog.vue b/packages/frontend/src/pages/admin/modlog.vue
index 4e0d0f941e..35f939f1be 100644
--- a/packages/frontend/src/pages/admin/modlog.vue
+++ b/packages/frontend/src/pages/admin/modlog.vue
@@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><XHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="900">
<div>
- <div style="display: flex; gap: var(--margin); flex-wrap: wrap;">
+ <div style="display: flex; gap: var(--MI-margin); flex-wrap: wrap;">
<MkSelect v-model="type" style="margin: 0; flex: 1;">
<template #label>{{ i18n.ts.type }}</template>
<option :value="null">{{ i18n.ts.all }}</option>
@@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkInput>
</div>
- <MkPagination v-slot="{items}" ref="logs" :pagination="pagination" :displayLimit="50" style="margin-top: var(--margin);">
- <div class="_gaps_s">
- <XModLog v-for="item in items" :key="item.id" :log="item"/>
- </div>
+ <MkPagination v-slot="{items}" ref="logs" :pagination="pagination" :displayLimit="50" style="margin-top: var(--MI-margin);">
+ <MkDateSeparatedList v-slot="{ item }" :items="items" :noGap="false" style="--MI-margin: 8px;">
+ <XModLog :key="item.id" :log="item"/>
+ </MkDateSeparatedList>
</MkPagination>
</div>
</MkSpacer>
@@ -39,6 +39,7 @@ import MkInput from '@/components/MkInput.vue';
import MkPagination from '@/components/MkPagination.vue';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
+import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
const logs = shallowRef<InstanceType<typeof MkPagination>>();
diff --git a/packages/frontend/src/pages/admin/object-storage.vue b/packages/frontend/src/pages/admin/object-storage.vue
index 5fddb715cd..d5a664934c 100644
--- a/packages/frontend/src/pages/admin/object-storage.vue
+++ b/packages/frontend/src/pages/admin/object-storage.vue
@@ -157,7 +157,7 @@ definePageMetadata(() => ({
<style lang="scss" module>
.footer {
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
</style>
diff --git a/packages/frontend/src/pages/admin/overview.ap-requests.vue b/packages/frontend/src/pages/admin/overview.ap-requests.vue
index 4bbb9210af..570fcddc07 100644
--- a/packages/frontend/src/pages/admin/overview.ap-requests.vue
+++ b/packages/frontend/src/pages/admin/overview.ap-requests.vue
@@ -278,7 +278,7 @@ onMounted(async () => {
padding: 16px;
&:first-child {
- border-bottom: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
}
}
}
diff --git a/packages/frontend/src/pages/admin/overview.federation.vue b/packages/frontend/src/pages/admin/overview.federation.vue
index ea01d073ea..1ada4a5251 100644
--- a/packages/frontend/src/pages/admin/overview.federation.vue
+++ b/packages/frontend/src/pages/admin/overview.federation.vue
@@ -151,8 +151,8 @@ onMounted(async () => {
height: 100%;
aspect-ratio: 1;
margin-right: 12px;
- background: var(--accentedBg);
- color: var(--accent);
+ background: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
border-radius: var(--radius);
}
diff --git a/packages/frontend/src/pages/admin/overview.pie.vue b/packages/frontend/src/pages/admin/overview.pie.vue
index c7a9f2a702..a21ec6c464 100644
--- a/packages/frontend/src/pages/admin/overview.pie.vue
+++ b/packages/frontend/src/pages/admin/overview.pie.vue
@@ -41,7 +41,7 @@ onMounted(() => {
labels: props.data.map(x => x.name),
datasets: [{
backgroundColor: props.data.map(x => x.color),
- borderColor: getComputedStyle(document.documentElement).getPropertyValue('--panel'),
+ borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
borderWidth: 2,
hoverOffset: 0,
data: props.data.map(x => x.value),
diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue
index fb190f5325..de6b254412 100644
--- a/packages/frontend/src/pages/admin/overview.queue.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.vue
@@ -119,8 +119,8 @@ onUnmounted(() => {
> .chart {
min-width: 0;
padding: 16px;
- background: var(--panel);
- border-radius: var(--radius);
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
> .title {
font-size: 0.85em;
diff --git a/packages/frontend/src/pages/admin/overview.stats.vue b/packages/frontend/src/pages/admin/overview.stats.vue
index 37399a5724..12f2c82ff7 100644
--- a/packages/frontend/src/pages/admin/overview.stats.vue
+++ b/packages/frontend/src/pages/admin/overview.stats.vue
@@ -114,8 +114,8 @@ onMounted(async () => {
height: 100%;
aspect-ratio: 1;
margin-right: 12px;
- background: var(--accentedBg);
- color: var(--accent);
+ background: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
border-radius: var(--radius);
}
diff --git a/packages/frontend/src/pages/admin/performance.vue b/packages/frontend/src/pages/admin/performance.vue
index 7e0a932f82..12338f0bf9 100644
--- a/packages/frontend/src/pages/admin/performance.vue
+++ b/packages/frontend/src/pages/admin/performance.vue
@@ -30,6 +30,13 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="_panel" style="padding: 16px;">
+ <MkSwitch v-model="enableStatsForFederatedInstances" @change="onChange_enableStatsForFederatedInstances">
+ <template #label>{{ i18n.ts.enableStatsForFederatedInstances }}</template>
+ <template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
+ </MkSwitch>
+ </div>
+
+ <div class="_panel" style="padding: 16px;">
<MkSwitch v-model="enableChartsForFederatedInstances" @change="onChange_enableChartsForFederatedInstances">
<template #label>{{ i18n.ts.enableChartsForFederatedInstances }}</template>
<template #caption>{{ i18n.ts.turnOffToImprovePerformance }}</template>
@@ -120,6 +127,7 @@ const meta = await misskeyApi('admin/meta');
const enableServerMachineStats = ref(meta.enableServerMachineStats);
const enableIdenticonGeneration = ref(meta.enableIdenticonGeneration);
const enableChartsForRemoteUser = ref(meta.enableChartsForRemoteUser);
+const enableStatsForFederatedInstances = ref(meta.enableStatsForFederatedInstances);
const enableChartsForFederatedInstances = ref(meta.enableChartsForFederatedInstances);
function onChange_enableServerMachineStats(value: boolean) {
@@ -146,6 +154,14 @@ function onChange_enableChartsForRemoteUser(value: boolean) {
});
}
+function onChange_enableStatsForFederatedInstances(value: boolean) {
+ os.apiWithDialog('admin/update-meta', {
+ enableStatsForFederatedInstances: value,
+ }).then(() => {
+ fetchInstance(true);
+ });
+}
+
function onChange_enableChartsForFederatedInstances(value: boolean) {
os.apiWithDialog('admin/update-meta', {
enableChartsForFederatedInstances: value,
diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue
index 960a263a86..7c171ba0e1 100644
--- a/packages/frontend/src/pages/admin/queue.chart.vue
+++ b/packages/frontend/src/pages/admin/queue.chart.vue
@@ -135,8 +135,8 @@ onUnmounted(() => {
.chart {
min-width: 0;
padding: 16px;
- background: var(--panel);
- border-radius: var(--radius);
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
}
.chartTitle {
diff --git a/packages/frontend/src/pages/admin/relays.vue b/packages/frontend/src/pages/admin/relays.vue
index 04982eea1f..17e99e6593 100644
--- a/packages/frontend/src/pages/admin/relays.vue
+++ b/packages/frontend/src/pages/admin/relays.vue
@@ -11,8 +11,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-for="relay in relays" :key="relay.inbox" class="relaycxt _panel" style="padding: 16px;">
<div>{{ relay.inbox }}</div>
<div style="margin: 8px 0;">
- <i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--success);"></i>
- <i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--error);"></i>
+ <i v-if="relay.status === 'accepted'" class="ti ti-check" :class="$style.icon" style="color: var(--MI_THEME-success);"></i>
+ <i v-else-if="relay.status === 'rejected'" class="ti ti-ban" :class="$style.icon" style="color: var(--MI_THEME-error);"></i>
<i v-else class="ti ti-clock" :class="$style.icon"></i>
<span>{{ i18n.ts._relayStatus[relay.status] }}</span>
</div>
diff --git a/packages/frontend/src/pages/admin/roles.edit.vue b/packages/frontend/src/pages/admin/roles.edit.vue
index 60f06d50ba..2b4006c3f7 100644
--- a/packages/frontend/src/pages/admin/roles.edit.vue
+++ b/packages/frontend/src/pages/admin/roles.edit.vue
@@ -95,7 +95,7 @@ definePageMetadata(() => ({
<style lang="scss" module>
.footer {
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
</style>
diff --git a/packages/frontend/src/pages/admin/roles.role.vue b/packages/frontend/src/pages/admin/roles.role.vue
index 6bce60cfb9..d1c7be39d6 100644
--- a/packages/frontend/src/pages/admin/roles.role.vue
+++ b/packages/frontend/src/pages/admin/roles.role.vue
@@ -184,7 +184,7 @@ definePageMetadata(() => ({
.userItemSub {
padding: 6px 12px;
font-size: 85%;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
}
.userItemMainBody {
diff --git a/packages/frontend/src/pages/admin/server-rules.vue b/packages/frontend/src/pages/admin/server-rules.vue
index 4788c16830..1ce7190d9c 100644
--- a/packages/frontend/src/pages/admin/server-rules.vue
+++ b/packages/frontend/src/pages/admin/server-rules.vue
@@ -76,7 +76,7 @@ definePageMetadata(() => ({
<style lang="scss" module>
.item {
display: block;
- color: var(--navFg);
+ color: var(--MI_THEME-navFg);
}
.itemHeader {
@@ -96,8 +96,8 @@ definePageMetadata(() => ({
.itemNumber {
display: flex;
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
font-size: 14px;
font-weight: bold;
width: 28px;
@@ -117,12 +117,12 @@ definePageMetadata(() => ({
.itemRemove {
width: 40px;
height: 40px;
- color: var(--error);
+ color: var(--MI_THEME-error);
margin-left: auto;
border-radius: var(--radius-sm);
&:hover {
- background: var(--X5);
+ background: var(--MI_THEME-X5);
}
}
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 04975dcbf3..68f211de5c 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -438,6 +438,6 @@ definePageMetadata(() => ({
<style lang="scss" module>
.subCaption {
font-size: 0.85em;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
}
</style>
diff --git a/packages/frontend/src/pages/admin/system-webhook.item.vue b/packages/frontend/src/pages/admin/system-webhook.item.vue
index 4e767fba16..45f0fff107 100644
--- a/packages/frontend/src/pages/admin/system-webhook.item.vue
+++ b/packages/frontend/src/pages/admin/system-webhook.item.vue
@@ -6,15 +6,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkFolder>
<template #label>{{ entity.name || entity.url }}</template>
+ <template v-if="entity.name != null && entity.name != ''" #caption>{{ entity.url }}</template>
<template #icon>
<i v-if="!entity.isActive" class="ti ti-player-pause"/>
<i v-else-if="entity.latestStatus === null" class="ti ti-circle"/>
<i
v-else-if="[200, 201, 204].includes(entity.latestStatus)"
class="ti ti-check"
- :style="{ color: 'var(--success)' }"
+ :style="{ color: 'var(--MI_THEME-success)' }"
/>
- <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"/>
+ <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"/>
</template>
<template #suffix>
<MkTime v-if="entity.latestSentAt" :time="entity.latestSentAt" style="margin-right: 8px"/>
@@ -74,6 +75,6 @@ function onDeleteClick() {
margin-right: 0.75em;
flex-shrink: 0;
text-align: center;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
}
</style>
diff --git a/packages/frontend/src/pages/announcement.vue b/packages/frontend/src/pages/announcement.vue
index 802a6bf399..3840e6a494 100644
--- a/packages/frontend/src/pages/announcement.vue
+++ b/packages/frontend/src/pages/announcement.vue
@@ -20,9 +20,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
<span style="margin-right: 0.5em;">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
- <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
- <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
- <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+ <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+ <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+ <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
</span>
<Mfm :text="announcement.title"/>
</div>
diff --git a/packages/frontend/src/pages/announcements.vue b/packages/frontend/src/pages/announcements.vue
index a9bcfee803..4eee275f07 100644
--- a/packages/frontend/src/pages/announcements.vue
+++ b/packages/frontend/src/pages/announcements.vue
@@ -17,9 +17,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<span v-if="$i && !announcement.silence && !announcement.isRead" style="margin-right: 0.5em;">🆕</span>
<span style="margin-right: 0.5em;">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
- <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
- <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
- <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+ <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+ <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+ <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
</span>
<MkA :to="`/announcements/${announcement.id}`"><span>{{ announcement.title }}</span></MkA>
</div>
diff --git a/packages/frontend/src/pages/antenna-timeline.vue b/packages/frontend/src/pages/antenna-timeline.vue
index e57e212b60..fbfbb6edf7 100644
--- a/packages/frontend/src/pages/antenna-timeline.vue
+++ b/packages/frontend/src/pages/antenna-timeline.vue
@@ -97,26 +97,26 @@ definePageMetadata(() => ({
<style lang="scss" module>
.new {
position: sticky;
- top: calc(var(--stickyTop, 0px) + 16px);
+ top: calc(var(--MI-stickyTop, 0px) + 16px);
z-index: 1000;
width: 100%;
margin: calc(-0.675em - 8px) 0;
&:first-child {
- margin-top: calc(-0.675em - 8px - var(--margin));
+ margin-top: calc(-0.675em - 8px - var(--MI-margin));
}
}
.newButton {
display: block;
- margin: var(--margin) auto 0 auto;
+ margin: var(--MI-margin) auto 0 auto;
padding: 8px 16px;
border-radius: var(--radius-xl);
}
.tl {
- background: var(--bg);
- border-radius: var(--radius);
+ background: var(--MI_THEME-bg);
+ border-radius: var(--MI-radius);
overflow: clip;
}
</style>
diff --git a/packages/frontend/src/pages/avatar-decorations.vue b/packages/frontend/src/pages/avatar-decorations.vue
index b377314856..b97e7c0eea 100644
--- a/packages/frontend/src/pages/avatar-decorations.vue
+++ b/packages/frontend/src/pages/avatar-decorations.vue
@@ -124,7 +124,7 @@ definePageMetadata(() => ({
display: grid;
grid-template-columns: 1fr;
grid-template-rows: auto auto;
- gap: var(--margin);
+ gap: var(--MI-margin);
}
.preview {
@@ -132,7 +132,7 @@ definePageMetadata(() => ({
place-items: center;
grid-template-columns: 1fr 1fr;
grid-template-rows: 1fr;
- gap: var(--margin);
+ gap: var(--MI-margin);
}
.previewItem {
@@ -142,7 +142,7 @@ definePageMetadata(() => ({
display: flex;
align-items: center;
justify-content: center;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
&.light {
background: #eee;
@@ -157,7 +157,7 @@ definePageMetadata(() => ({
.editorWrapper {
grid-template-columns: 200px 1fr;
grid-template-rows: 1fr;
- gap: calc(var(--margin) * 2);
+ gap: calc(var(--MI-margin) * 2);
}
.preview {
diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue
index d3f4a65b89..6d8274a55c 100644
--- a/packages/frontend/src/pages/channel-editor.vue
+++ b/packages/frontend/src/pages/channel-editor.vue
@@ -216,7 +216,7 @@ definePageMetadata(() => ({
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
- color: var(--navFg);
+ color: var(--MI_THEME-navFg);
}
.pinnedNoteRemove {
diff --git a/packages/frontend/src/pages/channel.vue b/packages/frontend/src/pages/channel.vue
index 790e16e471..6193e8857e 100644
--- a/packages/frontend/src/pages/channel.vue
+++ b/packages/frontend/src/pages/channel.vue
@@ -300,14 +300,14 @@ definePageMetadata(() => ({
<style lang="scss" module>
.main {
- min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
+ min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
}
.footer {
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
- background: var(--acrylicBg);
- border-top: solid 0.5px var(--divider);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
+ background: var(--MI_THEME-acrylicBg);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.bannerContainer {
@@ -341,7 +341,7 @@ definePageMetadata(() => ({
left: 0;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+ background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
}
.bannerStatus {
@@ -366,7 +366,7 @@ definePageMetadata(() => ({
bottom: 16px;
left: 16px;
background: rgba(0, 0, 0, 0.7);
- color: var(--warn);
+ color: var(--MI_THEME-warn);
border-radius: var(--radius-sm);
font-weight: bold;
font-size: 1em;
diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue
index 52b852ad17..9d8ba71a2d 100644
--- a/packages/frontend/src/pages/clip.vue
+++ b/packages/frontend/src/pages/clip.vue
@@ -198,7 +198,7 @@ definePageMetadata(() => ({
.user {
--height: 32px;
padding: 16px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
line-height: var(--height);
}
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index 8904096875..48a64e5a02 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -330,28 +330,28 @@ definePageMetadata(() => ({
.ogwlenmc {
> .local {
.empty {
- margin: var(--margin);
+ margin: var(--MI-margin);
}
.ldhfsamy {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
grid-gap: 12px;
- margin: var(--margin) 0;
+ margin: var(--MI-margin) 0;
> .emoji {
display: flex;
align-items: center;
padding: 11px;
text-align: left;
- border: solid 1px var(--panel);
+ border: solid 1px var(--MI_THEME-panel);
&:hover {
- border-color: var(--inputBorderHover);
+ border-color: var(--MI_THEME-inputBorderHover);
}
&.selected {
- border-color: var(--accent);
+ border-color: var(--MI_THEME-accent);
}
> .img {
@@ -382,14 +382,14 @@ definePageMetadata(() => ({
> .remote {
.empty {
- margin: var(--margin);
+ margin: var(--MI-margin);
}
.ldhfsamy {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
grid-gap: 12px;
- margin: var(--margin) 0;
+ margin: var(--MI-margin) 0;
> .emoji {
display: flex;
@@ -398,7 +398,7 @@ definePageMetadata(() => ({
text-align: left;
&:hover {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
> .img {
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index 36e95cec2d..604abba562 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -231,8 +231,8 @@ onMounted(async () => {
<style lang="scss" module>
.filePreviewRoot {
- background: var(--panel);
- border-radius: var(--radius);
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
// MkMediaList 内ã®ä¸Šéƒ¨ãƒžãƒ¼ã‚¸ãƒ³ 4px
padding: calc(1rem - 4px) 1rem 1rem;
}
@@ -262,8 +262,8 @@ onMounted(async () => {
&:hover,
&:focus-visible {
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
text-decoration: none;
outline: none;
}
@@ -285,7 +285,7 @@ onMounted(async () => {
align-items: center;
min-width: 0;
font-weight: 700;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
font-size: .8rem;
>.fileNameEditIcon {
@@ -299,12 +299,12 @@ onMounted(async () => {
}
&:hover {
- background-color: var(--accentedBg);
+ background-color: var(--MI_THEME-accentedBg);
>.fileName,
>.fileNameEditIcon {
visibility: visible;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
}
@@ -322,7 +322,7 @@ onMounted(async () => {
display: block;
width: 100%;
padding: .5rem 1rem;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
.kvEditIcon {
display: inline-block;
@@ -332,11 +332,11 @@ onMounted(async () => {
}
&:hover {
- color: var(--accent);
- background-color: var(--accentedBg);
+ color: var(--MI_THEME-accent);
+ background-color: var(--MI_THEME-accentedBg);
.kvEditIcon {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
visibility: visible;
}
}
diff --git a/packages/frontend/src/pages/drop-and-fusion.game.vue b/packages/frontend/src/pages/drop-and-fusion.game.vue
index 4db952eac2..fb4d599c28 100644
--- a/packages/frontend/src/pages/drop-and-fusion.game.vue
+++ b/packages/frontend/src/pages/drop-and-fusion.game.vue
@@ -111,7 +111,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="replaying" class="_woodenFrame">
<div class="_woodenFrameInner">
<div style="background: #0004;">
- <div style="height: 10px; background: var(--accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
+ <div style="height: 10px; background: var(--MI_THEME-accent); will-change: width;" :style="{ width: `${(currentFrame / endedAtFrame) * 100}%` }"></div>
</div>
</div>
<div class="_woodenFrameInner">
diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue
index c5f0dde878..7bcd5addef 100644
--- a/packages/frontend/src/pages/emoji-edit-dialog.vue
+++ b/packages/frontend/src/pages/emoji-edit-dialog.vue
@@ -243,9 +243,9 @@ async function del() {
bottom: 0;
left: 0;
padding: 12px;
- border-top: solid 0.5px var(--divider);
- background: var(--acrylicBg);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ border-top: solid 0.5px var(--MI_THEME-divider);
+ background: var(--MI_THEME-acrylicBg);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
</style>
diff --git a/packages/frontend/src/pages/emojis.emoji.vue b/packages/frontend/src/pages/emojis.emoji.vue
index 03a3b8f1c0..458642699e 100644
--- a/packages/frontend/src/pages/emojis.emoji.vue
+++ b/packages/frontend/src/pages/emojis.emoji.vue
@@ -58,11 +58,11 @@ function menu(ev) {
align-items: center;
padding: 12px;
text-align: left;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: var(--radius-sm);
&:hover {
- border-color: var(--accent);
+ border-color: var(--MI_THEME-accent);
}
}
diff --git a/packages/frontend/src/pages/explore.featured.vue b/packages/frontend/src/pages/explore.featured.vue
index cfdb235d3a..8b16a88ff3 100644
--- a/packages/frontend/src/pages/explore.featured.vue
+++ b/packages/frontend/src/pages/explore.featured.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkSpacer :contentMax="800">
- <MkTab v-model="tab" style="margin-bottom: var(--margin);">
+ <MkTab v-model="tab" style="margin-bottom: var(--MI-margin);">
<option value="notes">{{ i18n.ts.notes }}</option>
<option value="polls">{{ i18n.ts.poll }}</option>
</MkTab>
diff --git a/packages/frontend/src/pages/explore.users.vue b/packages/frontend/src/pages/explore.users.vue
index 805d826946..2550100a42 100644
--- a/packages/frontend/src/pages/explore.users.vue
+++ b/packages/frontend/src/pages/explore.users.vue
@@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<MkSpacer :contentMax="1200">
- <MkTab v-model="origin" style="margin-bottom: var(--margin);">
+ <MkTab v-model="origin" style="margin-bottom: var(--MI-margin);">
<option value="local">{{ i18n.ts.local }}</option>
<option value="remote">{{ i18n.ts.remote }}</option>
</MkTab>
diff --git a/packages/frontend/src/pages/favorites.vue b/packages/frontend/src/pages/favorites.vue
index cb1feef016..ab26573b19 100644
--- a/packages/frontend/src/pages/favorites.vue
+++ b/packages/frontend/src/pages/favorites.vue
@@ -53,7 +53,7 @@ definePageMetadata(() => ({
<style lang="scss" module>
.note {
- background: var(--panel);
- border-radius: var(--radius);
+ background: var(--MI_THEME-panel);
+ border-radius: var(--MI-radius);
}
</style>
diff --git a/packages/frontend/src/pages/flash/flash-edit.vue b/packages/frontend/src/pages/flash/flash-edit.vue
index fd6fadd0b3..d84ec4873b 100644
--- a/packages/frontend/src/pages/flash/flash-edit.vue
+++ b/packages/frontend/src/pages/flash/flash-edit.vue
@@ -467,8 +467,8 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
.footer {
- backdrop-filter: var(--blur, blur(15px));
- background: var(--acrylicBg);
- border-top: solid .5px var(--divider);
+ backdrop-filter: var(--MI-blur, blur(15px));
+ background: var(--MI_THEME-acrylicBg);
+ border-top: solid .5px var(--MI_THEME-divider);
}
</style>
diff --git a/packages/frontend/src/pages/flash/flash-index.vue b/packages/frontend/src/pages/flash/flash-index.vue
index f63a799365..2b85489706 100644
--- a/packages/frontend/src/pages/flash/flash-index.vue
+++ b/packages/frontend/src/pages/flash/flash-index.vue
@@ -55,7 +55,8 @@ const tab = ref('featured');
const featuredFlashsPagination = {
endpoint: 'flash/featured' as const,
- noPaging: true,
+ limit: 5,
+ offsetMode: true,
};
const myFlashsPagination = {
endpoint: 'flash/my' as const,
diff --git a/packages/frontend/src/pages/flash/flash.vue b/packages/frontend/src/pages/flash/flash.vue
index 1229fcfd4e..7a7d52691c 100644
--- a/packages/frontend/src/pages/flash/flash.vue
+++ b/packages/frontend/src/pages/flash/flash.vue
@@ -51,7 +51,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div><i class="ti ti-clock"></i> {{ i18n.ts.createdAt }}: <MkTime :time="flash.createdAt" mode="detail"/></div>
</div>
</div>
- <MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--accent);">{{ i18n.ts._play.editThisPage }}</MkA>
+ <MkA v-if="$i && $i.id === flash.userId" :to="`/play/${flash.id}/edit`" style="color: var(--MI_THEME-accent);">{{ i18n.ts._play.editThisPage }}</MkA>
<MkAd :prefer="['horizontal', 'horizontal-big']"/>
</div>
<MkError v-else-if="error" @retry="fetchFlash()"/>
@@ -367,7 +367,7 @@ definePageMetadata(() => ({
justify-content: center;
gap: 12px;
padding: 16px;
- border-bottom: 1px solid var(--divider);
+ border-bottom: 1px solid var(--MI_THEME-divider);
&:last-child {
border-bottom: none;
diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue
index a68a7e5c41..70f8b2c31d 100644
--- a/packages/frontend/src/pages/gallery/edit.vue
+++ b/packages/frontend/src/pages/gallery/edit.vue
@@ -141,7 +141,7 @@ definePageMetadata(() => ({
top: 8px;
left: 9px;
padding: 8px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
}
> .remove {
@@ -149,7 +149,7 @@ definePageMetadata(() => ({
top: 8px;
right: 9px;
padding: 8px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
}
}
</style>
diff --git a/packages/frontend/src/pages/gallery/index.vue b/packages/frontend/src/pages/gallery/index.vue
index e0b137ed28..2162e269bf 100644
--- a/packages/frontend/src/pages/gallery/index.vue
+++ b/packages/frontend/src/pages/gallery/index.vue
@@ -130,6 +130,6 @@ definePageMetadata(() => ({
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
- margin: 0 var(--margin);
+ margin: 0 var(--MI-margin);
}
</style>
diff --git a/packages/frontend/src/pages/gallery/post.vue b/packages/frontend/src/pages/gallery/post.vue
index 6ed119c0c4..539a6a9a7b 100644
--- a/packages/frontend/src/pages/gallery/post.vue
+++ b/packages/frontend/src/pages/gallery/post.vue
@@ -262,14 +262,14 @@ definePageMetadata(() => ({
align-items: center;
margin-top: 16px;
padding: 16px 0 0 0;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
> .like {
> .button {
- --accent: rgb(241 97 132);
- --X8: rgb(241 92 128);
- --buttonBg: rgb(216 71 106 / 5%);
- --buttonHoverBg: rgb(216 71 106 / 10%);
+ --MI_THEME-accent: rgb(241 97 132);
+ --MI_THEME-X8: rgb(241 92 128);
+ --MI_THEME-buttonBg: rgb(216 71 106 / 5%);
+ --MI_THEME-buttonHoverBg: rgb(216 71 106 / 10%);
color: #ff002f;
::v-deep(.count) {
@@ -286,7 +286,7 @@ definePageMetadata(() => ({
margin: 0 8px;
&:hover {
- color: var(--fgHighlighted);
+ color: var(--MI_THEME-fgHighlighted);
}
}
}
@@ -295,7 +295,7 @@ definePageMetadata(() => ({
> .user {
margin-top: 16px;
padding: 16px 0 0 0;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
display: flex;
align-items: center;
flex-wrap: wrap;
@@ -321,7 +321,7 @@ definePageMetadata(() => ({
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
- margin: var(--margin);
+ margin: var(--MI-margin);
> .post {
diff --git a/packages/frontend/src/pages/games.vue b/packages/frontend/src/pages/games.vue
index b52f4decaa..998b8be0f3 100644
--- a/packages/frontend/src/pages/games.vue
+++ b/packages/frontend/src/pages/games.vue
@@ -35,7 +35,7 @@ definePageMetadata(() => ({
<style module>
.link:focus-within {
- outline: 2px solid var(--focus);
+ outline: 2px solid var(--MI_THEME-focus);
outline-offset: -2px;
}
</style>
diff --git a/packages/frontend/src/pages/install-extensions.vue b/packages/frontend/src/pages/install-extensions.vue
index 4bee437f65..6d68ed83b4 100644
--- a/packages/frontend/src/pages/install-extensions.vue
+++ b/packages/frontend/src/pages/install-extensions.vue
@@ -8,76 +8,26 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #header><MkPageHeader :actions="headerActions" :tabs="headerTabs"/></template>
<MkSpacer :contentMax="500">
<MkLoading v-if="uiPhase === 'fetching'"/>
- <div v-else-if="uiPhase === 'confirm' && data" class="_gaps_m" :class="$style.extInstallerRoot">
- <div :class="$style.extInstallerIconWrapper">
- <i v-if="data.type === 'plugin'" class="ti ti-plug"></i>
- <i v-else-if="data.type === 'theme'" class="ti ti-palette"></i>
- <i v-else class="ti ti-download"></i>
- </div>
- <h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${data.type}`].title }}</h2>
- <div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
- <MkInfo v-if="data.type === 'plugin'" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
- <FormSection>
- <template #label>{{ i18n.ts._externalResourceInstaller[`_${data.type}`].metaTitle }}</template>
- <div class="_gaps_s">
- <FormSplit>
+ <MkExtensionInstaller v-else-if="uiPhase === 'confirm' && data" :extension="data" @confirm="install()">
+ <template #additionalInfo>
+ <FormSection>
+ <template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template>
+ <div class="_gaps_s">
<MkKeyValue>
- <template #key>{{ i18n.ts.name }}</template>
- <template #value>{{ data.meta?.name }}</template>
+ <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
+ <template #value><MkUrl :url="url" :showUrlPreview="false"></MkUrl></template>
</MkKeyValue>
<MkKeyValue>
- <template #key>{{ i18n.ts.author }}</template>
- <template #value>{{ data.meta?.author }}</template>
+ <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
+ <template #value>
+ <!-- ã“ã®ç”»é¢ãŒå‡ºã¦ã„る時点ã§ãƒãƒƒã‚·ãƒ¥ã®æ¤œè¨¼ã«ã¯æˆåŠŸã—ã¦ã„ã‚‹ -->
+ <i class="ti ti-check" style="color: var(--MI_THEME-accent)"></i>
+ </template>
</MkKeyValue>
- </FormSplit>
- <MkKeyValue v-if="data.type === 'plugin'">
- <template #key>{{ i18n.ts.description }}</template>
- <template #value>{{ data.meta?.description }}</template>
- </MkKeyValue>
- <MkKeyValue v-if="data.type === 'plugin'">
- <template #key>{{ i18n.ts.version }}</template>
- <template #value>{{ data.meta?.version }}</template>
- </MkKeyValue>
- <MkKeyValue v-if="data.type === 'plugin'">
- <template #key>{{ i18n.ts.permission }}</template>
- <template #value>
- <ul :class="$style.extInstallerKVList">
- <li v-for="permission in data.meta?.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
- </ul>
- </template>
- </MkKeyValue>
- <MkKeyValue v-if="data.type === 'theme' && data.meta?.base">
- <template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
- <template #value>{{ i18n.ts[data.meta.base] }}</template>
- </MkKeyValue>
- <MkFolder>
- <template #icon><i class="ti ti-code"></i></template>
- <template #label>{{ i18n.ts._plugin.viewSource }}</template>
-
- <MkCode :code="data.raw ?? ''"/>
- </MkFolder>
- </div>
- </FormSection>
- <FormSection>
- <template #label>{{ i18n.ts._externalResourceInstaller._vendorInfo.title }}</template>
- <div class="_gaps_s">
- <MkKeyValue>
- <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.endpoint }}</template>
- <template #value><MkUrl :url="url ?? ''" :showUrlPreview="false"></MkUrl></template>
- </MkKeyValue>
- <MkKeyValue>
- <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template>
- <template #value>
- <!--ã“ã®ç”»é¢ãŒå‡ºã¦ã„る時点ã§ãƒãƒƒã‚·ãƒ¥ã®æ¤œè¨¼ã«ã¯æˆåŠŸã—ã¦ã„ã‚‹-->
- <i class="ti ti-check" style="color: var(--accent)"></i>
- </template>
- </MkKeyValue>
- </div>
- </FormSection>
- <div class="_buttonsCenter">
- <MkButton primary @click="install()"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
- </div>
- </div>
+ </div>
+ </FormSection>
+ </template>
+ </MkExtensionInstaller>
<div v-else-if="uiPhase === 'error'" class="_gaps_m" :class="[$style.extInstallerRoot, $style.error]">
<div :class="$style.extInstallerIconWrapper">
<i class="ti ti-circle-x"></i>
@@ -96,14 +46,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref, computed, onActivated, onDeactivated, nextTick } from 'vue';
import MkLoading from '@/components/global/MkLoading.vue';
+import MkExtensionInstaller, { type Extension } from '@/components/MkExtensionInstaller.vue';
import MkButton from '@/components/MkButton.vue';
-import FormSection from '@/components/form/section.vue';
-import FormSplit from '@/components/form/split.vue';
-import MkCode from '@/components/MkCode.vue';
-import MkUrl from '@/components/global/MkUrl.vue';
-import MkInfo from '@/components/MkInfo.vue';
-import MkFolder from '@/components/MkFolder.vue';
import MkKeyValue from '@/components/MkKeyValue.vue';
+import MkUrl from '@/components/global/MkUrl.vue';
+import FormSection from '@/components/form/section.vue';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { AiScriptPluginMeta, parsePluginMeta, installPlugin } from '@/scripts/install-plugin.js';
@@ -124,24 +71,7 @@ const errorKV = ref<{
const url = ref<string | null>(null);
const hash = ref<string | null>(null);
-const data = ref<{
- type: 'plugin' | 'theme';
- raw: string;
- meta?: {
- // Plugin & Theme Common
- name: string;
- author: string;
-
- // Plugin
- description?: string;
- version?: string;
- permissions?: string[];
- config?: Record<string, any>;
-
- // Theme
- base?: 'light' | 'dark';
- };
-} | null>(null);
+const data = ref<Extension | null>(null);
function goBack(): void {
history.back();
@@ -227,7 +157,7 @@ async function fetch() {
data.value = {
type: 'theme',
meta: {
- description,
+ // description, // 使用ã•れã¦ã„ãªã„
...meta,
},
raw: res.data,
@@ -320,8 +250,8 @@ definePageMetadata(() => ({
<style lang="scss" module>
.extInstallerRoot {
- border-radius: var(--radius);
- background: var(--panel);
+ border-radius: var(--MI-radius);
+ background: var(--MI_THEME-panel);
padding: 1.5rem;
}
@@ -335,8 +265,8 @@ definePageMetadata(() => ({
margin-left: auto;
margin-right: auto;
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
}
.error .extInstallerIconWrapper {
@@ -353,9 +283,4 @@ definePageMetadata(() => ({
.extInstallerNormDesc {
text-align: center;
}
-
-.extInstallerKVList {
- margin-top: 0;
- margin-bottom: 0;
-}
</style>
diff --git a/packages/frontend/src/pages/instance-info.vue b/packages/frontend/src/pages/instance-info.vue
index a5e6e5ac33..78d765df80 100644
--- a/packages/frontend/src/pages/instance-info.vue
+++ b/packages/frontend/src/pages/instance-info.vue
@@ -59,6 +59,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkButton @click="refreshMetadata"><i class="ti ti-refresh"></i> Refresh metadata</MkButton>
<MkTextarea v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
+ <template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
</div>
</FormSection>
diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue
index 03ab804d05..cab807d610 100644
--- a/packages/frontend/src/pages/list.vue
+++ b/packages/frontend/src/pages/list.vue
@@ -108,7 +108,7 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
.main {
- min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
+ min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
}
.userItem {
diff --git a/packages/frontend/src/pages/my-antennas/index.vue b/packages/frontend/src/pages/my-antennas/index.vue
index 9233695900..607843892e 100644
--- a/packages/frontend/src/pages/my-antennas/index.vue
+++ b/packages/frontend/src/pages/my-antennas/index.vue
@@ -73,11 +73,11 @@ onActivated(() => {
.antenna {
display: block;
padding: 16px;
- border: solid 1px var(--divider);
+ border: solid 1px var(--MI_THEME-divider);
border-radius: var(--radius-sm);
&:hover {
- border: solid 1px var(--accent);
+ border: solid 1px var(--MI_THEME-accent);
text-decoration: none;
}
}
diff --git a/packages/frontend/src/pages/my-lists/index.vue b/packages/frontend/src/pages/my-lists/index.vue
index cd68be4ac3..a6e1f12d8a 100644
--- a/packages/frontend/src/pages/my-lists/index.vue
+++ b/packages/frontend/src/pages/my-lists/index.vue
@@ -85,12 +85,12 @@ onActivated(() => {
.list {
display: block;
padding: 16px;
- border: solid 1px var(--divider);
+ border: solid 1px var(--MI_THEME-divider);
border-radius: var(--radius-sm);
margin-bottom: 8px;
&:hover {
- border: solid 1px var(--accent);
+ border: solid 1px var(--MI_THEME-accent);
text-decoration: none;
}
}
diff --git a/packages/frontend/src/pages/my-lists/list.vue b/packages/frontend/src/pages/my-lists/list.vue
index 5f195693cc..804a5ae8f8 100644
--- a/packages/frontend/src/pages/my-lists/list.vue
+++ b/packages/frontend/src/pages/my-lists/list.vue
@@ -134,7 +134,7 @@ async function removeUser(item, ev) {
async function showMembershipMenu(item, ev) {
const withRepliesRef = ref(item.withReplies);
-
+
os.popupMenu([{
type: 'switch',
text: i18n.ts.showRepliesToOthersInTimeline,
@@ -199,7 +199,7 @@ definePageMetadata(() => ({
<style lang="scss" module>
.main {
- min-height: calc(100cqh - (var(--stickyTop, 0px) + var(--stickyBottom, 0px)));
+ min-height: calc(100cqh - (var(--MI-stickyTop, 0px) + var(--MI-stickyBottom, 0px)));
}
.userItem {
@@ -234,8 +234,8 @@ definePageMetadata(() => ({
}
.footer {
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
- border-top: solid 0.5px var(--divider);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
</style>
diff --git a/packages/frontend/src/pages/note.vue b/packages/frontend/src/pages/note.vue
index d0bbaeddd9..f6eef52153 100644
--- a/packages/frontend/src/pages/note.vue
+++ b/packages/frontend/src/pages/note.vue
@@ -182,11 +182,11 @@ definePageMetadata(() => ({
}
.loadNext {
- margin-bottom: var(--margin);
+ margin-bottom: var(--MI-margin);
}
.loadPrev {
- margin-top: var(--margin);
+ margin-top: var(--MI-margin);
}
.loadButton {
@@ -194,7 +194,7 @@ definePageMetadata(() => ({
}
.note {
- border-radius: var(--radius);
- background: var(--panel);
+ border-radius: var(--MI-radius);
+ background: var(--MI_THEME-panel);
}
</style>
diff --git a/packages/frontend/src/pages/notifications.vue b/packages/frontend/src/pages/notifications.vue
index bd93fc8369..46ee501c76 100644
--- a/packages/frontend/src/pages/notifications.vue
+++ b/packages/frontend/src/pages/notifications.vue
@@ -102,7 +102,7 @@ definePageMetadata(() => ({
<style module lang="scss">
.notifications {
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
overflow: clip;
}
</style>
diff --git a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
index 14c3e6845e..f09f7e1acd 100644
--- a/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
+++ b/packages/frontend/src/pages/page-editor/els/page-editor.el.text.vue
@@ -63,7 +63,7 @@ onUnmounted(() => {
box-shadow: none;
padding: 16px;
background: transparent;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
font-size: 14px;
box-sizing: border-box;
}
diff --git a/packages/frontend/src/pages/page-editor/page-editor.container.vue b/packages/frontend/src/pages/page-editor/page-editor.container.vue
index 2531de0e62..28b224f752 100644
--- a/packages/frontend/src/pages/page-editor/page-editor.container.vue
+++ b/packages/frontend/src/pages/page-editor/page-editor.container.vue
@@ -60,12 +60,12 @@ function remove() {
.cpjygsrt {
position: relative;
overflow: hidden;
- background: var(--panel);
- border: solid 2px var(--X12);
+ background: var(--MI_THEME-panel);
+ border: solid 2px var(--MI_THEME-X12);
border-radius: var(--radius-sm);
&:hover {
- border: solid 2px var(--X13);
+ border: solid 2px var(--MI_THEME-X13);
}
&.warn {
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index 12b70fa64f..544e112111 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -357,24 +357,24 @@ definePageMetadata(() => ({
&:hover,
&:focus-visible {
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
text-decoration: none;
outline: none;
}
}
.pageMain {
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
padding: 2rem;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
box-sizing: border-box;
}
.pageBanner {
width: calc(100% + 4rem);
margin: -2rem -2rem 1.5rem;
- border-radius: var(--radius) var(--radius) 0 0;
+ border-radius: var(--MI-radius) var(--MI-radius) 0 0;
overflow: hidden;
position: relative;
@@ -399,7 +399,7 @@ definePageMetadata(() => ({
}
.pageBannerBgFallback2 {
- background-color: var(--accentedBg);
+ background-color: var(--MI_THEME-accentedBg);
}
&::after {
@@ -409,7 +409,7 @@ definePageMetadata(() => ({
bottom: 0;
width: 100%;
height: 100px;
- background: linear-gradient(0deg, var(--panel), transparent);
+ background: linear-gradient(0deg, var(--MI_THEME-panel), transparent);
}
}
@@ -433,7 +433,7 @@ definePageMetadata(() => ({
h1 {
font-size: 2rem;
font-weight: 700;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
margin: 0;
}
@@ -458,7 +458,7 @@ definePageMetadata(() => ({
flex-shrink: 0;
display: flex;
align-items: center;
- gap: var(--marginHalf);
+ gap: var(--MI-marginHalf);
margin-left: auto;
}
}
@@ -472,14 +472,14 @@ definePageMetadata(() => ({
display: flex;
align-items: center;
- border-top: 1px solid var(--divider);
+ border-top: 1px solid var(--MI_THEME-divider);
padding-top: 1.5rem;
margin-bottom: 1.5rem;
> .other {
margin-left: auto;
display: flex;
- gap: var(--marginHalf);
+ gap: var(--MI-marginHalf);
}
}
@@ -487,7 +487,7 @@ definePageMetadata(() => ({
display: flex;
align-items: center;
- border-top: 1px solid var(--divider);
+ border-top: 1px solid var(--MI_THEME-divider);
padding-top: 1.5rem;
margin-bottom: 1.5rem;
@@ -526,14 +526,14 @@ definePageMetadata(() => ({
display: flex;
align-items: center;
flex-wrap: wrap;
- gap: var(--marginHalf);
+ gap: var(--MI-marginHalf);
}
.relatedPagesRoot {
- padding: var(--margin);
+ padding: var(--MI-margin);
}
.relatedPagesItem > article {
- background-color: var(--panelHighlight) !important;
+ background-color: var(--MI_THEME-panelHighlight) !important;
}
</style>
diff --git a/packages/frontend/src/pages/reversi/game.board.vue b/packages/frontend/src/pages/reversi/game.board.vue
index 54e66f6e16..429f502133 100644
--- a/packages/frontend/src/pages/reversi/game.board.vue
+++ b/packages/frontend/src/pages/reversi/game.board.vue
@@ -504,7 +504,7 @@ $gap: 4px;
.boardInner {
padding: 32px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
border-radius: 8px;
}
@@ -574,34 +574,34 @@ $gap: 4px;
transition: border 0.25s ease, opacity 0.25s ease;
&.boardCell_empty {
- border: solid 2px var(--divider);
+ border: solid 2px var(--MI_THEME-divider);
}
&.boardCell_empty.boardCell_can {
- border-color: var(--accent);
+ border-color: var(--MI_THEME-accent);
opacity: 0.5;
}
&.boardCell_empty.boardCell_myTurn {
- border-color: var(--divider);
+ border-color: var(--MI_THEME-divider);
opacity: 1;
&.boardCell_can {
- border-color: var(--accent);
+ border-color: var(--MI_THEME-accent);
cursor: pointer;
&:hover {
- background: var(--accent);
+ background: var(--MI_THEME-accent);
}
}
}
&.boardCell_prev {
- box-shadow: 0 0 0 4px var(--accent);
+ box-shadow: 0 0 0 4px var(--MI_THEME-accent);
}
&.boardCell_isEnded {
- border-color: var(--divider);
+ border-color: var(--MI_THEME-divider);
}
&.boardCell_none {
diff --git a/packages/frontend/src/pages/reversi/game.setting.vue b/packages/frontend/src/pages/reversi/game.setting.vue
index 08bb3cb76c..dfb6e3f53e 100644
--- a/packages/frontend/src/pages/reversi/game.setting.vue
+++ b/packages/frontend/src/pages/reversi/game.setting.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template v-else>
<div class="_panel">
- <div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--divider);">
+ <div style="display: flex; align-items: center; padding: 16px; border-bottom: solid 1px var(--MI_THEME-divider);">
<div>{{ mapName }}</div>
<MkButton style="margin-left: auto;" @click="chooseMap">{{ i18n.ts._reversi.chooseBoard }}</MkButton>
</div>
@@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.footer">
<MkSpacer :contentMax="700" :marginMin="16" :marginMax="16">
<div style="text-align: center;" class="_gaps_s">
- <div v-if="opponentHasSettingsChanged" style="color: var(--warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div>
+ <div v-if="opponentHasSettingsChanged" style="color: var(--MI_THEME-warn);">{{ i18n.ts._reversi.opponentHasSettingsChanged }}</div>
<div>
<template v-if="isReady && isOpReady">{{ i18n.ts._reversi.thisGameIsStartedSoon }}<MkEllipsis/></template>
<template v-if="isReady && !isOpReady">{{ i18n.ts._reversi.waitingForOther }}<MkEllipsis/></template>
@@ -273,14 +273,14 @@ onUnmounted(() => {
width: 300px;
height: 300px;
margin: 0 auto;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
}
.boardCell {
display: grid;
place-items: center;
background: transparent;
- border: solid 2px var(--divider);
+ border: solid 2px var(--MI_THEME-divider);
border-radius: 6px;
overflow: clip;
cursor: pointer;
@@ -290,9 +290,9 @@ onUnmounted(() => {
}
.footer {
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
- background: var(--acrylicBg);
- border-top: solid 0.5px var(--divider);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
+ background: var(--MI_THEME-acrylicBg);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
</style>
diff --git a/packages/frontend/src/pages/reversi/index.vue b/packages/frontend/src/pages/reversi/index.vue
index d823861b4a..d608a2411c 100644
--- a/packages/frontend/src/pages/reversi/index.vue
+++ b/packages/frontend/src/pages/reversi/index.vue
@@ -36,13 +36,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.gamePreviews">
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
<div :class="$style.gamePreviewPlayers">
- <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+ <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
<span style="margin: 0 1em;">vs</span>
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
<span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
- <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+ <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
</div>
<div :class="$style.gamePreviewFooter">
<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
@@ -63,13 +63,13 @@ SPDX-License-Identifier: AGPL-3.0-only
<div :class="$style.gamePreviews">
<MkA v-for="g in items" :key="g.id" v-panel :class="[$style.gamePreview, !g.isStarted && !g.isEnded && $style.gamePreviewWaiting, g.isStarted && !g.isEnded && $style.gamePreviewActive]" tabindex="-1" :to="`/reversi/g/${g.id}`">
<div :class="$style.gamePreviewPlayers">
- <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+ <span v-if="g.winnerId === g.user1Id" style="margin-right: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
<span v-if="g.winnerId === g.user2Id" style="margin-right: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user1"/>
<span style="margin: 0 1em;">vs</span>
<MkAvatar :class="$style.gamePreviewPlayersAvatar" :user="g.user2"/>
<span v-if="g.winnerId === g.user1Id" style="margin-left: 0.75em; visibility: hidden;"><i class="ti ti-x"></i></span>
- <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
+ <span v-if="g.winnerId === g.user2Id" style="margin-left: 0.75em; color: var(--MI_THEME-accent); font-weight: bold;"><i class="ti ti-trophy"></i></span>
</div>
<div :class="$style.gamePreviewFooter">
<span v-if="g.isStarted && !g.isEnded" :class="$style.gamePreviewStatusActive">{{ i18n.ts._reversi.playing }}</span>
@@ -285,7 +285,7 @@ definePageMetadata(() => ({
.gamePreviews {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
- grid-gap: var(--margin);
+ grid-gap: var(--MI-margin);
}
.gamePreview {
@@ -295,11 +295,11 @@ definePageMetadata(() => ({
}
.gamePreviewActive {
- box-shadow: inset 0 0 8px 0px var(--accent);
+ box-shadow: inset 0 0 8px 0px var(--MI_THEME-accent);
}
.gamePreviewWaiting {
- box-shadow: inset 0 0 8px 0px var(--warn);
+ box-shadow: inset 0 0 8px 0px var(--MI_THEME-warn);
}
.gamePreviewPlayers {
@@ -324,19 +324,19 @@ definePageMetadata(() => ({
.gamePreviewFooter {
display: flex;
align-items: baseline;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
padding: 6px 10px;
font-size: 0.9em;
}
.gamePreviewStatusActive {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
font-weight: bold;
animation: blink 2s infinite;
}
.gamePreviewStatusWaiting {
- color: var(--warn);
+ color: var(--MI_THEME-warn);
font-weight: bold;
animation: blink 2s infinite;
}
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 155d8b82d7..2250e1ce60 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -204,7 +204,7 @@ definePageMetadata(() => ({
.root {
display: flex;
flex-direction: column;
- gap: var(--margin);
+ gap: var(--MI-margin);
}
.editor {
@@ -242,14 +242,14 @@ definePageMetadata(() => ({
}
.uiInspectorUnShown {
- color: var(--fgTransparent);
+ color: var(--MI_THEME-fgTransparent);
}
.uiInspectorType {
display: inline-block;
border: hidden;
border-radius: 10px;
- background-color: var(--panelHighlight);
+ background-color: var(--MI_THEME-panelHighlight);
padding: 2px 8px;
font-size: 12px;
}
diff --git a/packages/frontend/src/pages/search.note.vue b/packages/frontend/src/pages/search.note.vue
index 66c5c92480..d64537d289 100644
--- a/packages/frontend/src/pages/search.note.vue
+++ b/packages/frontend/src/pages/search.note.vue
@@ -225,12 +225,12 @@ async function search() {
justify-content: center;
}
.addMeButton {
- border: 2px dashed var(--fgTransparent);
+ border: 2px dashed var(--MI_THEME-fgTransparent);
padding: 12px;
margin-right: 16px;
}
.addUserButton {
- border: 2px dashed var(--fgTransparent);
+ border: 2px dashed var(--MI_THEME-fgTransparent);
padding: 12px;
flex-grow: 1;
}
diff --git a/packages/frontend/src/pages/settings/2fa.vue b/packages/frontend/src/pages/settings/2fa.vue
index 6a9a1e16e2..a76b748ac1 100644
--- a/packages/frontend/src/pages/settings/2fa.vue
+++ b/packages/frontend/src/pages/settings/2fa.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon><i class="ti ti-shield-lock"></i></template>
<template #label>{{ i18n.ts.totp }}</template>
<template #caption>{{ i18n.ts.totpDescription }}</template>
- <template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--success)"></i></template>
+ <template #suffix><i v-if="$i.twoFactorEnabled" class="ti ti-check" style="color: var(--MI_THEME-success)"></i></template>
<div v-if="$i.twoFactorEnabled" class="_gaps_s">
<div v-text="i18n.ts._2fa.alreadyRegistered"/>
diff --git a/packages/frontend/src/pages/settings/accounts.vue b/packages/frontend/src/pages/settings/accounts.vue
index 08c9261dcf..1bbedb817e 100644
--- a/packages/frontend/src/pages/settings/accounts.vue
+++ b/packages/frontend/src/pages/settings/accounts.vue
@@ -45,7 +45,7 @@ const init = async () => {
});
};
-function menu(account, ev) {
+function menu(account: Misskey.entities.UserDetailed, ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts.switch,
icon: 'ti ti-switch-horizontal',
@@ -58,7 +58,7 @@ function menu(account, ev) {
}], ev.currentTarget ?? ev.target);
}
-function addAccount(ev) {
+function addAccount(ev: MouseEvent) {
os.popupMenu([{
text: i18n.ts.existingAccount,
action: () => { addExistingAccount(); },
@@ -68,14 +68,14 @@ function addAccount(ev) {
}], ev.currentTarget ?? ev.target);
}
-async function removeAccount(account) {
+async function removeAccount(account: Misskey.entities.UserDetailed) {
await _removeAccount(account.id);
accounts.value = accounts.value.filter(x => x.id !== account.id);
}
function addExistingAccount() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
- done: async res => {
+ done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
await addAccounts(res.id, res.i);
os.success();
init();
@@ -86,17 +86,17 @@ function addExistingAccount() {
function createAccount() {
const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
- done: async res => {
- await addAccounts(res.id, res.i);
- switchAccountWithToken(res.i);
+ done: async (res: Misskey.entities.SignupResponse) => {
+ await addAccounts(res.id, res.token);
+ switchAccountWithToken(res.token);
},
closed: () => dispose(),
});
}
async function switchAccount(account: any) {
- const fetchedAccounts: any[] = await getAccounts();
- const token = fetchedAccounts.find(x => x.id === account.id).token;
+ const fetchedAccounts = await getAccounts();
+ const token = fetchedAccounts.find(x => x.id === account.id)!.token;
switchAccountWithToken(token);
}
diff --git a/packages/frontend/src/pages/settings/apps.vue b/packages/frontend/src/pages/settings/apps.vue
index c58ea5d378..68e36ef1bb 100644
--- a/packages/frontend/src/pages/settings/apps.vue
+++ b/packages/frontend/src/pages/settings/apps.vue
@@ -14,30 +14,39 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template #default="{items}">
<div class="_gaps">
- <div v-for="token in items" :key="token.id" class="_panel" :class="$style.app">
- <img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/>
- <div :class="$style.appBody">
- <div :class="$style.appName">{{ token.name }}</div>
- <div>{{ token.description }}</div>
- <MkKeyValue oneline>
- <template #key>{{ i18n.ts.installedDate }}</template>
- <template #value><MkTime :time="token.createdAt"/></template>
- </MkKeyValue>
- <MkKeyValue oneline>
- <template #key>{{ i18n.ts.lastUsedDate }}</template>
- <template #value><MkTime :time="token.lastUsedAt"/></template>
- </MkKeyValue>
- <details>
- <summary>{{ i18n.ts.details }}</summary>
+ <MkFolder v-for="token in items" :key="token.id" :defaultOpen="true">
+ <template #icon>
+ <img v-if="token.iconUrl" :class="$style.appIcon" :src="token.iconUrl" alt=""/>
+ <i v-else class="ti ti-plug"/>
+ </template>
+ <template #label>{{ token.name }}</template>
+ <template #caption>{{ token.description }}</template>
+ <template #suffix><MkTime :time="token.lastUsedAt"/></template>
+ <template #footer>
+ <MkButton danger @click="revoke(token)"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+ </template>
+
+ <div class="_gaps_s">
+ <div v-if="token.description">{{ token.description }}</div>
+ <div>
+ <MkKeyValue oneline>
+ <template #key>{{ i18n.ts.installedDate }}</template>
+ <template #value><MkTime :time="token.createdAt" :mode="'detail'"/></template>
+ </MkKeyValue>
+ <MkKeyValue oneline>
+ <template #key>{{ i18n.ts.lastUsedDate }}</template>
+ <template #value><MkTime :time="token.lastUsedAt" :mode="'detail'"/></template>
+ </MkKeyValue>
+ </div>
+ <MkFolder>
+ <template #label>{{ i18n.ts.permission }}</template>
+ <template #suffix>{{ Object.keys(token.permission).length === 0 ? i18n.ts.none : Object.keys(token.permission).length }}</template>
<ul>
<li v-for="p in token.permission" :key="p">{{ i18n.ts._permissions[p] }}</li>
</ul>
- </details>
- <div>
- <MkButton inline danger @click="revoke(token)"><i class="ti ti-trash"></i></MkButton>
- </div>
+ </MkFolder>
</div>
- </div>
+ </MkFolder>
</div>
</template>
</FormPagination>
@@ -52,6 +61,7 @@ import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkKeyValue from '@/components/MkKeyValue.vue';
import MkButton from '@/components/MkButton.vue';
+import MkFolder from '@/components/MkFolder.vue';
import { infoImageUrl } from '@/instance.js';
const list = ref<InstanceType<typeof FormPagination>>();
@@ -82,26 +92,9 @@ definePageMetadata(() => ({
</script>
<style lang="scss" module>
-.app {
- display: flex;
- padding: 16px;
-}
-
.appIcon {
- display: block;
- flex-shrink: 0;
- margin: 0 12px 0 0;
- width: 50px;
- height: 50px;
- border-radius: var(--radius-sm);
-}
-
-.appBody {
- width: calc(100% - 62px);
- position: relative;
-}
-
-.appName {
- font-weight: bold;
+ width: 20px;
+ height: 20px;
+ border-radius: 4px;
}
</style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
index 9f7852a71d..5c59f4cbe4 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue
@@ -44,7 +44,7 @@ const emit = defineEmits<{
.root {
cursor: pointer;
padding: 16px 16px 28px 16px;
- border: solid 2px var(--divider);
+ border: solid 2px var(--MI_THEME-divider);
border-radius: var(--radius-sm);
text-align: center;
font-size: 90%;
@@ -53,8 +53,8 @@ const emit = defineEmits<{
}
.active {
- background-color: var(--accentedBg);
- border-color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ border-color: var(--MI_THEME-accent);
}
.name {
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
index 4ec4610279..b84665b111 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue
@@ -159,8 +159,8 @@ async function detach() {
bottom: 0;
left: 0;
padding: 12px;
- border-top: solid 0.5px var(--divider);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ border-top: solid 0.5px var(--MI_THEME-divider);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
</style>
diff --git a/packages/frontend/src/pages/settings/avatar-decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.vue
index 5324a6b7f7..efcd2a5f3f 100644
--- a/packages/frontend/src/pages/settings/avatar-decoration.vue
+++ b/packages/frontend/src/pages/settings/avatar-decoration.vue
@@ -148,7 +148,7 @@ definePageMetadata(() => ({
.current {
padding: 16px;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
}
.decorations {
diff --git a/packages/frontend/src/pages/settings/drive-cleaner.vue b/packages/frontend/src/pages/settings/drive-cleaner.vue
index 432295aa7f..4054cd810a 100644
--- a/packages/frontend/src/pages/settings/drive-cleaner.vue
+++ b/packages/frontend/src/pages/settings/drive-cleaner.vue
@@ -177,7 +177,7 @@ definePageMetadata(() => ({
align-items: center;
&:hover {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
}
diff --git a/packages/frontend/src/pages/settings/email.vue b/packages/frontend/src/pages/settings/email.vue
index f226647569..d452f249b6 100644
--- a/packages/frontend/src/pages/settings/email.vue
+++ b/packages/frontend/src/pages/settings/email.vue
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="emailAddress" type="email" manualSave>
<template #prefix><i class="ti ti-mail"></i></template>
<template v-if="$i.email && !$i.emailVerified" #caption>{{ i18n.ts.verificationEmailSent }}</template>
- <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--success);"></i> {{ i18n.ts.emailVerified }}</template>
+ <template v-else-if="emailAddress === $i.email && $i.emailVerified" #caption><i class="ti ti-check" style="color: var(--MI_THEME-success);"></i> {{ i18n.ts.emailVerified }}</template>
</MkInput>
</FormSection>
diff --git a/packages/frontend/src/pages/settings/emoji-picker.vue b/packages/frontend/src/pages/settings/emoji-picker.vue
index bb919c6c53..eb5424ea6c 100644
--- a/packages/frontend/src/pages/settings/emoji-picker.vue
+++ b/packages/frontend/src/pages/settings/emoji-picker.vue
@@ -287,9 +287,9 @@ definePageMetadata(() => ({
<style lang="scss" module>
.tab {
- margin: calc(var(--margin) / 2) 0;
- padding: calc(var(--margin) / 2) 0;
- background: var(--bg);
+ margin: calc(var(--MI-margin) / 2) 0;
+ padding: calc(var(--MI-margin) / 2) 0;
+ background: var(--MI_THEME-bg);
}
.emojis {
@@ -311,6 +311,6 @@ definePageMetadata(() => ({
.editorCaption {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
}
</style>
diff --git a/packages/frontend/src/pages/settings/index.vue b/packages/frontend/src/pages/settings/index.vue
index f6f2ffc553..552b4ee028 100644
--- a/packages/frontend/src/pages/settings/index.vue
+++ b/packages/frontend/src/pages/settings/index.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-if="!(narrow && currentPage?.route.name == null)" class="main">
<div class="bkzroven" style="container-type: inline-size;">
- <RouterView/>
+ <RouterView nested/>
</div>
</div>
</div>
diff --git a/packages/frontend/src/pages/settings/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue
index a5c381200f..82aeb6063f 100644
--- a/packages/frontend/src/pages/settings/mute-block.vue
+++ b/packages/frontend/src/pages/settings/mute-block.vue
@@ -243,7 +243,7 @@ definePageMetadata(() => ({
.userItemSub {
padding: 6px 12px;
font-size: 85%;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
}
.userItemMainBody {
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index a0e6cad9c8..b189db0f8f 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -122,7 +122,7 @@ definePageMetadata(() => ({
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
- color: var(--navFg);
+ color: var(--MI_THEME-navFg);
}
.itemIcon {
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index eb9d158c1e..abe4524126 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -65,6 +65,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSwitch v-model="enableCondensedLine">
<template #label>Enable condensed line</template>
</MkSwitch>
+ <MkSwitch v-model="skipNoteRender">
+ <template #label>Enable note render skipping</template>
+ </MkSwitch>
</div>
</MkFolder>
@@ -116,9 +119,14 @@ const $i = signinRequired();
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
const enableCondensedLine = computed(defaultStore.makeGetterSetter('enableCondensedLine'));
+const skipNoteRender = computed(defaultStore.makeGetterSetter('skipNoteRender'));
const devMode = computed(defaultStore.makeGetterSetter('devMode'));
const defaultWithReplies = computed(defaultStore.makeGetterSetter('defaultWithReplies'));
+watch(skipNoteRender, async () => {
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
+});
+
async function deleteAccount() {
{
const { canceled } = await os.confirm({
diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue
index 8036ef5555..f7dcc7b139 100644
--- a/packages/frontend/src/pages/settings/preferences-backups.vue
+++ b/packages/frontend/src/pages/settings/preferences-backups.vue
@@ -469,7 +469,7 @@ definePageMetadata(() => ({
<style lang="scss" module>
.buttons {
display: flex;
- gap: var(--margin);
+ gap: var(--MI-margin);
flex-wrap: wrap;
}
diff --git a/packages/frontend/src/pages/settings/profile.vue b/packages/frontend/src/pages/settings/profile.vue
index c94cd512f3..9ff4a63e09 100644
--- a/packages/frontend/src/pages/settings/profile.vue
+++ b/packages/frontend/src/pages/settings/profile.vue
@@ -52,14 +52,17 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkFolder>
<template #icon><i class="ti ti-list"></i></template>
<template #label>{{ i18n.ts._profile.metadataEdit }}</template>
-
- <div :class="$style.metadataRoot">
- <div :class="$style.metadataMargin">
- <MkButton :disabled="fields.length >= 16" inline style="margin-right: 8px;" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
- <MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" inline danger style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
- <MkButton v-else inline style="margin-right: 8px;" @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
- <MkButton inline primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+ <template #footer>
+ <div class="_buttons">
+ <MkButton primary @click="saveFields"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
+ <MkButton :disabled="fields.length >= 16" @click="addField"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
+ <MkButton v-if="!fieldEditMode" :disabled="fields.length <= 1" danger @click="fieldEditMode = !fieldEditMode"><i class="ti ti-trash"></i> {{ i18n.ts.delete }}</MkButton>
+ <MkButton v-else @click="fieldEditMode = !fieldEditMode"><i class="ti ti-arrows-sort"></i> {{ i18n.ts.rearrange }}</MkButton>
</div>
+ </template>
+
+ <div :class="$style.metadataRoot" class="_gaps_s">
+ <MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
<Sortable
v-model="fields"
@@ -71,24 +74,20 @@ SPDX-License-Identifier: AGPL-3.0-only
@end="e => e.item.classList.remove('active')"
>
<template #item="{element, index}">
- <div :class="$style.fieldDragItem">
+ <div v-panel :class="$style.fieldDragItem">
<button v-if="!fieldEditMode" class="_button" :class="$style.dragItemHandle" tabindex="-1"><i class="ti ti-menu"></i></button>
<button v-if="fieldEditMode" :disabled="fields.length <= 1" class="_button" :class="$style.dragItemRemove" @click="deleteField(index)"><i class="ti ti-x"></i></button>
<div :class="$style.dragItemForm">
<FormSplit :minWidth="200">
- <MkInput v-model="element.name" small>
- <template #label>{{ i18n.ts._profile.metadataLabel }}</template>
+ <MkInput v-model="element.name" small :placeholder="i18n.ts._profile.metadataLabel">
</MkInput>
- <MkInput v-model="element.value" small>
- <template #label>{{ i18n.ts._profile.metadataContent }}</template>
+ <MkInput v-model="element.value" small :placeholder="i18n.ts._profile.metadataContent">
</MkInput>
</FormSplit>
</div>
</div>
</template>
</Sortable>
-
- <MkInfo>{{ i18n.ts._profile.verifiedLinkDescription }}</MkInfo>
</div>
</MkFolder>
<template #caption>{{ i18n.ts._profile.metadataDescription }}</template>
@@ -158,6 +157,10 @@ const setMaxBirthDate = () => {
return `${y}-12-31`;
};
+function assertVaildLang(lang: string | null): lang is keyof typeof langmap {
+ return lang != null && lang in langmap;
+}
+
const profile = reactive({
name: $i.name,
description: $i.description,
@@ -165,7 +168,7 @@ const profile = reactive({
location: $i.location,
birthday: $i.birthday,
listenbrainz: $i.listenbrainz,
- lang: $i.lang,
+ lang: assertVaildLang($i.lang) ? $i.lang : null,
isBot: $i.isBot ?? false,
isCat: $i.isCat ?? false,
speakAsCat: $i.speakAsCat ?? false,
@@ -229,6 +232,11 @@ function save() {
isBot: !!profile.isBot,
isCat: !!profile.isCat,
speakAsCat: !!profile.speakAsCat,
+ }, undefined, {
+ '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': {
+ title: i18n.ts.yourNameContainsProhibitedWords,
+ text: i18n.ts.yourNameContainsProhibitedWordsDescription,
+ },
});
globalEvents.emit('requestClearPageCache');
claimAchievement('profileFilled');
@@ -417,7 +425,7 @@ definePageMetadata(() => ({
height: 130px;
background-size: cover;
background-position: center;
- border-bottom: solid 1px var(--divider);
+ border-bottom: solid 1px var(--MI_THEME-divider);
overflow: clip;
}
@@ -449,19 +457,11 @@ definePageMetadata(() => ({
container-type: inline-size;
}
-.metadataMargin {
- margin-bottom: 1.5em;
-}
-
.fieldDragItem {
display: flex;
- padding-bottom: .75em;
+ padding: 10px;
align-items: flex-end;
- border-bottom: solid 0.5px var(--divider);
-
- &:last-child {
- border-bottom: 0;
- }
+ border-radius: 6px;
/* (drag button) 32px + (drag button margin) 8px + (input width) 200px * 2 + (input gap) 12px = 452px */
@container (max-width: 452px) {
diff --git a/packages/frontend/src/pages/settings/security.vue b/packages/frontend/src/pages/settings/security.vue
index de0f63630e..8f9d4f858b 100644
--- a/packages/frontend/src/pages/settings/security.vue
+++ b/packages/frontend/src/pages/settings/security.vue
@@ -124,7 +124,7 @@ definePageMetadata(() => ({
}
&:not(:last-child) {
- border-bottom: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
}
> header {
@@ -136,11 +136,11 @@ definePageMetadata(() => ({
margin-right: 0.75em;
&.succ {
- color: var(--success);
+ color: var(--MI_THEME-success);
}
&.fail {
- color: var(--error);
+ color: var(--MI_THEME-error);
}
}
diff --git a/packages/frontend/src/pages/settings/sounds.sound.vue b/packages/frontend/src/pages/settings/sounds.sound.vue
index 81478fede5..56f65e2309 100644
--- a/packages/frontend/src/pages/settings/sounds.sound.vue
+++ b/packages/frontend/src/pages/settings/sounds.sound.vue
@@ -194,7 +194,7 @@ function save() {
flex-grow: 1;
min-width: 0;
font-weight: 700;
- color: var(--error);
+ color: var(--MI_THEME-error);
}
.fileSelectorButton {
@@ -203,6 +203,6 @@ function save() {
.fileNotSelected {
font-weight: 700;
- color: var(--infoWarnFg);
+ color: var(--MI_THEME-infoWarnFg);
}
</style>
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index e7aef55a53..34a0245406 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -204,7 +204,7 @@ definePageMetadata(() => ({
}
.dn:focus-visible ~ .toggle {
- outline: 2px solid var(--focus);
+ outline: 2px solid var(--MI_THEME-focus);
outline-offset: 2px;
}
@@ -227,12 +227,12 @@ definePageMetadata(() => ({
> .before {
left: -70px;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
> .after {
right: -68px;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
}
}
@@ -350,11 +350,11 @@ definePageMetadata(() => ({
background-color: #749DD6;
> .before {
- color: var(--fg);
+ color: var(--MI_THEME-fg);
}
> .after {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
.toggle__handler {
@@ -405,14 +405,14 @@ definePageMetadata(() => ({
> .sync {
padding: 14px 16px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
}
.rsljpzjq {
> .selects {
display: flex;
- gap: 1.5em var(--margin);
+ gap: 1.5em var(--MI-margin);
flex-wrap: wrap;
> .select {
diff --git a/packages/frontend/src/pages/settings/webhook.edit.vue b/packages/frontend/src/pages/settings/webhook.edit.vue
index adeaf8550c..40d23e36c5 100644
--- a/packages/frontend/src/pages/settings/webhook.edit.vue
+++ b/packages/frontend/src/pages/settings/webhook.edit.vue
@@ -184,6 +184,6 @@ definePageMetadata(() => ({
.description {
font-size: 0.85em;
padding: 8px 0 0 0;
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
}
</style>
diff --git a/packages/frontend/src/pages/settings/webhook.vue b/packages/frontend/src/pages/settings/webhook.vue
index 0d11b00c97..af8b7ca945 100644
--- a/packages/frontend/src/pages/settings/webhook.vue
+++ b/packages/frontend/src/pages/settings/webhook.vue
@@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template #icon>
<i v-if="webhook.active === false" class="ti ti-player-pause"></i>
<i v-else-if="webhook.latestStatus === null" class="ti ti-circle"></i>
- <i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--success)' }"></i>
- <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--error)' }"></i>
+ <i v-else-if="[200, 201, 204].includes(webhook.latestStatus)" class="ti ti-check" :style="{ color: 'var(--MI_THEME-success)' }"></i>
+ <i v-else class="ti ti-alert-triangle" :style="{ color: 'var(--MI_THEME-error)' }"></i>
</template>
{{ webhook.name || webhook.url }}
<template #suffix>
diff --git a/packages/frontend/src/pages/signup-complete.vue b/packages/frontend/src/pages/signup-complete.vue
index 226f08bf8e..503e6e0f73 100644
--- a/packages/frontend/src/pages/signup-complete.vue
+++ b/packages/frontend/src/pages/signup-complete.vue
@@ -78,7 +78,7 @@ place-content: center;
.form {
position: relative;
z-index: 10;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
overflow: clip;
max-width: 500px;
@@ -88,7 +88,7 @@ place-content: center;
padding: 16px;
text-align: center;
font-size: 26px;
- background-color: var(--accentedBg);
- color: var(--accent);
+ background-color: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
}
</style>
diff --git a/packages/frontend/src/pages/tag.vue b/packages/frontend/src/pages/tag.vue
index 0d261b1af3..a45b61fb10 100644
--- a/packages/frontend/src/pages/tag.vue
+++ b/packages/frontend/src/pages/tag.vue
@@ -76,10 +76,10 @@ definePageMetadata(() => ({
<style lang="scss" module>
.footer {
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
- background: var(--acrylicBg);
- border-top: solid 0.5px var(--divider);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
+ background: var(--MI_THEME-acrylicBg);
+ border-top: solid 0.5px var(--MI_THEME-divider);
display: flex;
}
diff --git a/packages/frontend/src/pages/theme-editor.vue b/packages/frontend/src/pages/theme-editor.vue
index 2fa6eb81ba..21f4548d2e 100644
--- a/packages/frontend/src/pages/theme-editor.vue
+++ b/packages/frontend/src/pages/theme-editor.vue
@@ -259,7 +259,7 @@ definePageMetadata(() => ({
}
&.active {
- box-shadow: 0 0 0 2px var(--divider) inset;
+ box-shadow: 0 0 0 2px var(--MI_THEME-divider) inset;
}
&.rounded {
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 60974da971..92bfd9cdf1 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -9,10 +9,10 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkSpacer :contentMax="800">
<MkHorizontalSwipe v-model:tab="src" :tabs="$i ? headerTabs : headerTabsWhenNotLogin">
<div :key="src" ref="rootEl">
- <MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
+ <MkInfo v-if="isBasicTimeline(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--MI-margin);" closable @close="closeTutorial()">
{{ i18n.ts._timelineDescription[src] }}
</MkInfo>
- <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
+ <MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--MI-margin);"/>
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
<div :class="$style.tl">
<MkTimeline
@@ -363,30 +363,30 @@ definePageMetadata(() => ({
<style lang="scss" module>
.new {
position: sticky;
- top: calc(var(--stickyTop, 0px) + 16px);
+ top: calc(var(--MI-stickyTop, 0px) + 16px);
z-index: 1000;
width: 100%;
margin: calc(-0.675em - 8px) 0;
&:first-child {
- margin-top: calc(-0.675em - 8px - var(--margin));
+ margin-top: calc(-0.675em - 8px - var(--MI-margin));
}
}
.newButton {
display: block;
- margin: var(--margin) auto 0 auto;
+ margin: var(--MI-margin) auto 0 auto;
padding: 8px 16px;
border-radius: var(--radius-xl);
}
.postForm {
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
}
.tl {
- background: var(--bg);
- border-radius: var(--radius);
+ background: var(--MI_THEME-bg);
+ border-radius: var(--MI-radius);
overflow: clip;
}
</style>
diff --git a/packages/frontend/src/pages/user-list-timeline.vue b/packages/frontend/src/pages/user-list-timeline.vue
index 396e6eb14a..d7326f0195 100644
--- a/packages/frontend/src/pages/user-list-timeline.vue
+++ b/packages/frontend/src/pages/user-list-timeline.vue
@@ -114,26 +114,26 @@ definePageMetadata(() => ({
<style lang="scss" module>
.new {
position: sticky;
- top: calc(var(--stickyTop, 0px) + 16px);
+ top: calc(var(--MI-stickyTop, 0px) + 16px);
z-index: 1000;
width: 100%;
margin: calc(-0.675em - 8px) 0;
&:first-child {
- margin-top: calc(-0.675em - 8px - var(--margin));
+ margin-top: calc(-0.675em - 8px - var(--MI-margin));
}
}
.newButton {
display: block;
- margin: var(--margin) auto 0 auto;
+ margin: var(--MI-margin) auto 0 auto;
padding: 8px 16px;
border-radius: var(--radius-xl);
}
.tl {
- background: var(--bg);
- border-radius: var(--radius);
+ background: var(--MI_THEME-bg);
+ border-radius: var(--MI-radius);
overflow: clip;
}
</style>
diff --git a/packages/frontend/src/pages/user/clips.vue b/packages/frontend/src/pages/user/clips.vue
index ac01cff8cd..38ce78e8d5 100644
--- a/packages/frontend/src/pages/user/clips.vue
+++ b/packages/frontend/src/pages/user/clips.vue
@@ -43,6 +43,6 @@ const pagination = {
.description {
margin-top: 8px;
padding-top: 8px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
</style>
diff --git a/packages/frontend/src/pages/user/follow-list.vue b/packages/frontend/src/pages/user/follow-list.vue
index e60dccec17..868767e8f4 100644
--- a/packages/frontend/src/pages/user/follow-list.vue
+++ b/packages/frontend/src/pages/user/follow-list.vue
@@ -45,6 +45,6 @@ const followersPagination = {
.users {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
- grid-gap: var(--margin);
+ grid-gap: var(--MI-margin);
}
</style>
diff --git a/packages/frontend/src/pages/user/gallery.vue b/packages/frontend/src/pages/user/gallery.vue
index 9ba81322ba..0bc5628528 100644
--- a/packages/frontend/src/pages/user/gallery.vue
+++ b/packages/frontend/src/pages/user/gallery.vue
@@ -38,6 +38,6 @@ const pagination = {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
grid-gap: 12px;
- margin: var(--margin);
+ margin: var(--MI-margin);
}
</style>
diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue
index 279f301d78..30ecbd5100 100644
--- a/packages/frontend/src/pages/user/home.vue
+++ b/packages/frontend/src/pages/user/home.vue
@@ -22,7 +22,7 @@ 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" :title="i18n.ts.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
+ <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-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>
<button v-if="$i && !isEditingMemo && !memoDraft" class="_button add-note-button" @click="showMemoTextarea">
@@ -49,15 +49,16 @@ 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.isAdmin" style="color: var(--badge);"><i class="ti ti-shield"></i></span>
+ <span v-if="user.isAdmin" :title="i18n.ts.isAdmin" style="color: var(--MI_THEME-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>
</div>
<div v-if="user.followedMessage != null" class="followedMessage">
- <div style="border: solid 1px var(--love); border-radius: 6px; background: color-mix(in srgb, var(--love), transparent 90%); padding: 6px 8px;">
- <Mfm :text="user.followedMessage" :author="user"/>
- </div>
+ <MkFukidashi class="fukidashi" :tail="narrow ? 'none' : 'left'" negativeMargin shadow>
+ <div class="messageHeader">{{ i18n.ts.messageToFollower }}</div>
+ <div><MkSparkle><Mfm :plain="true" :text="user.followedMessage" :author="user"/></MkSparkle></div>
+ </MkFukidashi>
</div>
<div v-if="user.roles.length > 0" class="roles">
<span v-for="role in user.roles" :key="role.id" v-tooltip="role.description" class="role" :style="{ '--color': role.color }">
@@ -70,6 +71,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="iAmModerator" class="moderationNote">
<MkTextarea v-if="editModerationNote || (moderationNote != null && moderationNote !== '')" v-model="moderationNote" manualSave>
<template #label>{{ i18n.ts.moderationNote }}</template>
+ <template #caption>{{ i18n.ts.moderationNoteDescription }}</template>
</MkTextarea>
<div v-else>
<MkButton small @click="editModerationNote = true">{{ i18n.ts.addModerationNote }}</MkButton>
@@ -190,15 +192,16 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, computed, onMounted, onUnmounted, nextTick, watch, ref } from 'vue';
import * as Misskey from 'misskey-js';
+import { getScrollPosition } from '@@/js/scroll.js';
import MkTab from '@/components/MkTab.vue';
import MkNotes from '@/components/MkNotes.vue';
import MkFollowButton from '@/components/MkFollowButton.vue';
import MkAccountMoved from '@/components/MkAccountMoved.vue';
+import MkFukidashi from '@/components/MkFukidashi.vue';
import MkRemoteCaution from '@/components/MkRemoteCaution.vue';
import MkTextarea from '@/components/MkTextarea.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkButton from '@/components/MkButton.vue';
-import { getScrollPosition } from '@@/js/scroll.js';
import { getUserMenu } from '@/scripts/get-user-menu.js';
import number from '@/filters/number.js';
import { userPage } from '@/filters/user.js';
@@ -213,11 +216,12 @@ import { isFollowingVisibleForMe, isFollowersVisibleForMe } from '@/scripts/isFf
import { useRouter } from '@/router/supplier.js';
import { getStaticImageUrl } from '@/scripts/media-proxy.js';
import { infoImageUrl } from '@/instance.js';
+import MkSparkle from '@/components/MkSparkle.vue';
const MkNote = defineAsyncComponent(() =>
defaultStore.state.noteDesign === 'sharkey'
- ? import('@/components/SkNote.vue')
- : import('@/components/MkNote.vue'),
+ ? import('@/components/SkNote.vue')
+ : import('@/components/MkNote.vue'),
);
function calcAge(birthdate: string): number {
@@ -473,8 +477,8 @@ onUnmounted(() => {
position: absolute;
top: 12px;
right: 12px;
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(8px));
+ backdrop-filter: var(--MI-blur, blur(8px));
background: rgba(0, 0, 0, 0.2);
padding: 8px;
border-radius: var(--radius-lg);
@@ -528,8 +532,8 @@ onUnmounted(() => {
> .add-note-button {
background: rgba(0, 0, 0, 0.2);
color: #fff;
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(8px));
+ backdrop-filter: var(--MI-blur, blur(8px));
border-radius: var(--radius-lg);
padding: 4px 8px;
font-size: 80%;
@@ -543,7 +547,7 @@ onUnmounted(() => {
text-align: center;
padding: 50px 8px 16px 8px;
font-weight: bold;
- border-bottom: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
> .bottom {
> * {
@@ -567,7 +571,18 @@ onUnmounted(() => {
> .followedMessage {
padding: 24px 24px 0 154px;
- font-size: 0.9em;
+
+ > .fukidashi {
+ display: block;
+ --fukidashi-bg: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-panel) 85%);
+ --fukidashi-radius: 16px;
+ font-size: 0.9em;
+
+ .messageHeader {
+ opacity: 0.7;
+ font-size: 0.85em;
+ }
+ }
}
> .roles {
@@ -578,7 +593,7 @@ onUnmounted(() => {
gap: 8px;
> .role {
- border: solid 1px var(--color, var(--divider));
+ border: solid 1px var(--color, var(--MI_THEME-divider));
border-radius: var(--radius-ellipse);
margin-right: 4px;
padding: 3px 8px;
@@ -592,15 +607,15 @@ onUnmounted(() => {
> .memo {
margin: 12px 24px 0 154px;
background: transparent;
- color: var(--fg);
- border: 1px solid var(--divider);
+ color: var(--MI_THEME-fg);
+ border: 1px solid var(--MI_THEME-divider);
border-radius: var(--radius-sm);
padding: 8px;
line-height: 0;
> .heading {
text-align: left;
- color: var(--fgTransparent);
+ color: var(--MI_THEME-fgTransparent);
line-height: 1.5;
font-size: 85%;
}
@@ -615,7 +630,7 @@ onUnmounted(() => {
height: auto;
min-height: 0;
line-height: 1.5;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
overflow: hidden;
background: transparent;
font-family: inherit;
@@ -635,7 +650,7 @@ onUnmounted(() => {
> .fields {
padding: 24px;
font-size: 0.9em;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
> .field {
display: flex;
@@ -672,14 +687,14 @@ onUnmounted(() => {
> .status {
display: flex;
padding: 24px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
> a {
flex: 1;
text-align: center;
&.active {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
&:hover {
@@ -701,7 +716,7 @@ onUnmounted(() => {
> .contents {
> .content {
- margin-bottom: var(--margin);
+ margin-bottom: var(--MI-margin);
}
}
}
@@ -718,7 +733,7 @@ onUnmounted(() => {
> .sub {
max-width: 350px;
min-width: 350px;
- margin-left: var(--margin);
+ margin-left: var(--MI-margin);
}
}
}
@@ -796,7 +811,7 @@ onUnmounted(() => {
<style lang="scss" module>
.tl {
background-color: rgba(0, 0, 0, 0);
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
overflow: clip;
z-index: 0;
}
@@ -817,7 +832,7 @@ onUnmounted(() => {
.verifiedLink {
margin-left: 4px;
- color: var(--success);
+ color: var(--MI_THEME-success);
}
.pinnedNote:not(:last-child) {
diff --git a/packages/frontend/src/pages/user/index.timeline.vue b/packages/frontend/src/pages/user/index.timeline.vue
index 8dbf90f344..49d015a530 100644
--- a/packages/frontend/src/pages/user/index.timeline.vue
+++ b/packages/frontend/src/pages/user/index.timeline.vue
@@ -51,13 +51,13 @@ const pagination = computed(() => tab.value === 'featured' ? {
<style lang="scss" module>
.tab {
- padding: calc(var(--margin) / 2) 0;
- background: var(--bg);
+ padding: calc(var(--MI-margin) / 2) 0;
+ background: var(--MI_THEME-bg);
}
.tl {
- background: var(--bg);
- border-radius: var(--radius);
+ background: var(--MI_THEME-bg);
+ border-radius: var(--MI-radius);
overflow: clip;
}
</style>
diff --git a/packages/frontend/src/pages/user/lists.vue b/packages/frontend/src/pages/user/lists.vue
index 8f95ce2dc9..46af3f7f37 100644
--- a/packages/frontend/src/pages/user/lists.vue
+++ b/packages/frontend/src/pages/user/lists.vue
@@ -44,12 +44,12 @@ const pagination = {
.list {
display: block;
padding: 16px;
- border: solid 1px var(--divider);
+ border: solid 1px var(--MI_THEME-divider);
border-radius: var(--radius-sm);
margin-bottom: 8px;
&:hover {
- border: solid 1px var(--accent);
+ border: solid 1px var(--MI_THEME-accent);
text-decoration: none;
}
}
diff --git a/packages/frontend/src/pages/user/raw.vue b/packages/frontend/src/pages/user/raw.vue
index ac18ad9392..4499eec10a 100644
--- a/packages/frontend/src/pages/user/raw.vue
+++ b/packages/frontend/src/pages/user/raw.vue
@@ -113,18 +113,18 @@ const suspended = computed(() => props.user.isSuspended ?? false);
}
> .suspended {
- color: var(--error);
- border-color: var(--error);
+ color: var(--MI_THEME-error);
+ border-color: var(--MI_THEME-error);
}
> .silenced {
- color: var(--warn);
- border-color: var(--warn);
+ color: var(--MI_THEME-warn);
+ border-color: var(--MI_THEME-warn);
}
> .moderator {
- color: var(--success);
- border-color: var(--success);
+ color: var(--MI_THEME-success);
+ border-color: var(--MI_THEME-success);
}
}
</style>
diff --git a/packages/frontend/src/pages/user/reactions.vue b/packages/frontend/src/pages/user/reactions.vue
index 3671decc18..7168778e12 100644
--- a/packages/frontend/src/pages/user/reactions.vue
+++ b/packages/frontend/src/pages/user/reactions.vue
@@ -44,7 +44,7 @@ const pagination = {
align-items: center;
padding: 8px 16px;
margin-bottom: 8px;
- border-bottom: solid 2px var(--divider);
+ border-bottom: solid 2px var(--MI_THEME-divider);
}
.avatar {
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index 0d132e6a86..f6b7557e6d 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -98,7 +98,7 @@ misskeyApiGet('federation/instances', {
left: 0;
width: 100vw;
height: 100vh;
- background: var(--accent);
+ background: var(--MI_THEME-accent);
clip-path: polygon(0% 0%, 45% 0%, 20% 100%, 0% 100%);
}
> .shape2 {
@@ -107,7 +107,7 @@ misskeyApiGet('federation/instances', {
left: 0;
width: 100vw;
height: 100vh;
- background: var(--accent);
+ background: var(--MI_THEME-accent);
clip-path: polygon(0% 0%, 25% 0%, 35% 100%, 0% 100%);
opacity: 0.5;
}
@@ -164,9 +164,9 @@ misskeyApiGet('federation/instances', {
left: 0;
right: 0;
margin: auto;
- background: var(--acrylicPanel);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ background: var(--MI_THEME-acrylicPanel);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
border-radius: var(--radius-ellipse);
overflow: clip;
width: 800px;
@@ -186,7 +186,7 @@ misskeyApiGet('federation/instances', {
vertical-align: bottom;
padding: 6px 12px 6px 6px;
margin: 0 10px 0 0;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
border-radius: var(--radius-ellipse);
> :global(.icon) {
diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue
index 5a41100bf1..58e815d89c 100644
--- a/packages/frontend/src/pages/welcome.setup.vue
+++ b/packages/frontend/src/pages/welcome.setup.vue
@@ -14,6 +14,10 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div class="_gaps_m" style="padding: 32px;">
<div>{{ i18n.ts.intro }}</div>
+ <MkInput v-model="setupPassword" type="password" data-cy-admin-initial-password>
+ <template #label>{{ i18n.ts.initialPasswordForSetup }} <div v-tooltip:dialog="i18n.ts.initialPasswordForSetupDescription" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
+ <template #prefix><i class="ti ti-lock"></i></template>
+ </MkInput>
<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username>
<template #label>{{ i18n.ts.username }}</template>
<template #prefix>@</template>
@@ -36,9 +40,9 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { ref } from 'vue';
+import { host, version } from '@@/js/config.js';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
-import { host, version } from '@@/js/config.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
import { login } from '@/account.js';
@@ -47,6 +51,7 @@ import MkAnimBg from '@/components/MkAnimBg.vue';
const username = ref('');
const password = ref('');
+const setupPassword = ref('');
const submitting = ref(false);
function submit() {
@@ -56,14 +61,27 @@ function submit() {
misskeyApi('admin/accounts/create', {
username: username.value,
password: password.value,
+ setupPassword: setupPassword.value === '' ? null : setupPassword.value,
}).then(res => {
return login(res.token);
- }).catch(() => {
+ }).catch((err) => {
submitting.value = false;
+ let title = i18n.ts.somethingHappened;
+ let text = err.message + '\n' + err.id;
+
+ if (err.code === 'ACCESS_DENIED') {
+ title = i18n.ts.permissionDeniedError;
+ text = i18n.ts.operationForbidden;
+ } else if (err.code === 'INCORRECT_INITIAL_PASSWORD') {
+ title = i18n.ts.permissionDeniedError;
+ text = i18n.ts.incorrectPassword;
+ }
+
os.alert({
type: 'error',
- text: i18n.ts.somethingHappened,
+ title,
+ text,
});
});
}
@@ -74,14 +92,14 @@ function submit() {
min-height: 100svh;
padding: 32px 32px 64px 32px;
box-sizing: border-box;
-display: grid;
-place-content: center;
+ display: grid;
+ place-content: center;
}
.form {
position: relative;
z-index: 10;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
overflow: clip;
max-width: 500px;
@@ -92,8 +110,8 @@ place-content: center;
font-size: 1.5em;
text-align: center;
padding: 32px;
- background: var(--accentedBg);
- color: var(--accent);
+ background: var(--MI_THEME-accentedBg);
+ color: var(--MI_THEME-accent);
font-weight: bold;
}
diff --git a/packages/frontend/src/pages/welcome.timeline.note.vue b/packages/frontend/src/pages/welcome.timeline.note.vue
index ee8d4e1d62..5a03483d0c 100644
--- a/packages/frontend/src/pages/welcome.timeline.note.vue
+++ b/packages/frontend/src/pages/welcome.timeline.note.vue
@@ -84,7 +84,7 @@ onUpdated(() => {
left: 0;
width: 100%;
height: 64px;
- background: linear-gradient(0deg, var(--panel), color(from var(--panel) srgb r g b / 0));
+ background: linear-gradient(0deg, var(--MI_THEME-panel), color(from var(--MI_THEME-panel) srgb r g b / 0));
}
}
@@ -100,7 +100,7 @@ onUpdated(() => {
margin: 8px -16px -8px;
padding: 8px 16px 0;
width: calc(100% + 32px);
- border-top: 1px solid var(--divider);
+ border-top: 1px solid var(--MI_THEME-divider);
}
.richcontent {
diff --git a/packages/frontend/src/pages/welcome.timeline.vue b/packages/frontend/src/pages/welcome.timeline.vue
index 16d558cc91..2964b15164 100644
--- a/packages/frontend/src/pages/welcome.timeline.vue
+++ b/packages/frontend/src/pages/welcome.timeline.vue
@@ -60,7 +60,7 @@ onUpdated(() => {
transform: translate3d(0, 0, 0);
}
100% {
- transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0);
+ transform: translate3d(0, calc(calc(-100% - 128px) - var(--MI-margin)), 0);
}
}
@@ -69,7 +69,7 @@ onUpdated(() => {
transform: translate3d(0, -128px, 0);
}
100% {
- transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0);
+ transform: translate3d(0, calc(calc(-100% - 128px) - var(--MI-margin)), 0);
}
}
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index 4f3fb65665..c56fd185b6 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -267,13 +267,10 @@ export function getNoteMenu(props: {
function togglePin(pin: boolean): void {
os.apiWithDialog(pin ? 'i/pin' : 'i/unpin', {
noteId: appearNote.id,
- }, undefined, null, res => {
- if (res.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
- os.alert({
- type: 'error',
- text: i18n.ts.pinLimitExceeded,
- });
- }
+ }, undefined, {
+ '72dab508-c64d-498f-8740-a8eec1ba385a': {
+ text: i18n.ts.pinLimitExceeded,
+ },
});
}
diff --git a/packages/frontend/src/scripts/init-chart.ts b/packages/frontend/src/scripts/init-chart.ts
index 2465a14703..41e1636aa7 100644
--- a/packages/frontend/src/scripts/init-chart.ts
+++ b/packages/frontend/src/scripts/init-chart.ts
@@ -50,7 +50,7 @@ export function initChart() {
);
// フォントカラー
- Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--fg');
+ Chart.defaults.color = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-fg');
Chart.defaults.borderColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.1)' : 'rgba(0, 0, 0, 0.1)';
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index bd3cddde67..8242e7d2e4 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -123,7 +123,7 @@ export function applyTheme(theme: Theme, persist = true) {
for (const [k, v] of Object.entries(props)) {
if (k.startsWith('font')) continue;
- document.documentElement.style.setProperty(`--${k}`, v.toString());
+ document.documentElement.style.setProperty(`--MI_THEME-${k}`, v.toString());
}
document.documentElement.style.setProperty('color-scheme', colorScheme);
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index fd84ad2e4d..986ee9bf94 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -80,6 +80,10 @@ export const defaultStore = markRaw(new Storage('base', {
global: false,
},
},
+ abusesTutorial: {
+ where: 'account',
+ default: false,
+ },
keepCw: {
where: 'account',
default: true,
@@ -277,7 +281,7 @@ export const defaultStore = markRaw(new Storage('base', {
},
animatedMfm: {
where: 'device',
- default: false,
+ default: !window.matchMedia('(prefers-reduced-motion)').matches,
},
advancedMfm: {
where: 'device',
@@ -559,6 +563,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: 'app' as 'app' | 'appWithShift' | 'native',
},
+ skipNoteRender: {
+ where: 'device',
+ default: true,
+ },
sound_masterVolume: {
where: 'device',
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index d990a706b3..116e650666 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -24,19 +24,18 @@
--radius-ellipse: 5px;
--radius-full: 5px;
- --marginFull: 16px;
- --marginHalf: 10px;
+ --MI-radius: 12px;
+ --MI-marginFull: 16px;
+ --MI-marginHalf: 10px;
- --margin: var(--marginFull);
+ --MI-margin: var(--MI-marginFull);
// switch dynamically
- --minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px)));
- --minBottomSpacing: var(--minBottomSpacingMobile);
-
- //--ad: rgb(255 169 0 / 10%);
+ --MI-minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px)));
+ --MI-minBottomSpacing: var(--MI-minBottomSpacingMobile);
@media (max-width: 500px) {
- --margin: var(--marginHalf);
+ --MI-margin: var(--MI-marginHalf);
}
--avatar: 48px;
@@ -55,14 +54,14 @@ html.radius-misskey {
}
::selection {
- color: var(--fgOnAccent);
- background-color: var(--accent);
+ color: var(--MI_THEME-fgOnAccent);
+ background-color: var(--MI_THEME-accent);
}
html {
- background-color: var(--bg);
- color: var(--fg);
- accent-color: var(--accent);
+ background-color: var(--MI_THEME-bg);
+ color: var(--MI_THEME-fg);
+ accent-color: var(--MI_THEME-accent);
overflow: auto;
overflow-wrap: break-word;
font-family: 'sharkey-theme-font-face', 'Lexend', 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
@@ -73,7 +72,7 @@ html {
-webkit-text-size-adjust: 100%;
&, * {
- scrollbar-color: var(--scrollbarHandle) transparent;
+ scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
scrollbar-width: thin;
&::-webkit-scrollbar {
@@ -86,14 +85,14 @@ html {
}
&::-webkit-scrollbar-thumb {
- background: var(--scrollbarHandle);
+ background: var(--MI_THEME-scrollbarHandle);
&:hover {
- background: var(--scrollbarHandleHover);
+ background: var(--MI_THEME-scrollbarHandleHover);
}
&:active {
- background: var(--accent);
+ background: var(--MI_THEME-accent);
}
}
}
@@ -155,15 +154,15 @@ textarea, input {
}
optgroup, option {
- background: var(--panel);
- color: var(--fg);
+ background: var(--MI_THEME-panel);
+ color: var(--MI_THEME-fg);
}
hr {
- margin: var(--margin) 0 var(--margin) 0;
+ margin: var(--MI-margin) 0 var(--MI-margin) 0;
border: none;
height: 1px;
- background: var(--divider);
+ background: var(--MI_THEME-divider);
}
rt {
@@ -171,7 +170,7 @@ rt {
}
:focus-visible {
- outline: var(--focus) solid 2px;
+ outline: var(--MI_THEME-focus) solid 2px;
outline-offset: -2px;
&:hover {
@@ -204,9 +203,9 @@ rt {
._indicateCounter {
display: inline-flex;
- color: var(--fgOnAccent);
+ color: var(--MI_THEME-fgOnAccent);
font-weight: 700;
- background: var(--indicator);
+ background: var(--MI_THEME-indicator);
height: 1.5em;
min-width: 1em;
align-items: center;
@@ -239,13 +238,13 @@ rt {
left: 0;
width: 100%;
height: 100%;
- background: var(--modalBg);
- -webkit-backdrop-filter: var(--modalBgFilter);
- backdrop-filter: var(--modalBgFilter);
+ background: var(--MI_THEME-modalBg);
+ -webkit-backdrop-filter: var(--MI-modalBgFilter);
+ backdrop-filter: var(--MI-modalBgFilter);
}
._shadow {
- box-shadow: 0px 4px 32px var(--shadow) !important;
+ box-shadow: 0px 4px 32px var(--MI_THEME-shadow) !important;
}
._button {
@@ -278,40 +277,40 @@ rt {
._buttonPrimary {
@extend ._button;
- color: var(--fgOnAccent);
- background: var(--accent);
+ color: var(--MI_THEME-fgOnAccent);
+ background: var(--MI_THEME-accent);
&:not(:disabled):hover {
- background: hsl(from var(--accent) h s calc(l + 5));
+ background: hsl(from var(--MI_THEME-accent) h s calc(l + 5));
}
&:not(:disabled):active {
- background: hsl(from var(--accent) h s calc(l - 5));
+ background: hsl(from var(--MI_THEME-accent) h s calc(l - 5));
}
}
._buttonGradate {
@extend ._buttonPrimary;
- color: var(--fgOnAccent);
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ color: var(--MI_THEME-fgOnAccent);
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
&:not(:disabled):hover {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
&:not(:disabled):active {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
._help {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
cursor: help;
}
._textButton {
@extend ._button;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
&:focus-visible {
outline-offset: 2px;
@@ -323,13 +322,13 @@ rt {
}
._panel {
- background: color-mix(in srgb, var(--panel) 65%, transparent);
- border-radius: var(--radius);
+ background: color-mix(in srgb, var(--MI_THEME-panel) 65%, transparent);
+ border-radius: var(--MI-radius);
overflow: clip;
}
._margin {
- margin: var(--margin) 0;
+ margin: var(--MI-margin) 0;
}
._gaps_m {
@@ -347,7 +346,7 @@ rt {
._gaps {
display: flex;
flex-direction: column;
- gap: var(--margin);
+ gap: var(--MI-margin);
}
._buttons {
@@ -369,24 +368,24 @@ rt {
padding: 10px;
box-sizing: border-box;
text-align: center;
- border: solid 0.5px var(--divider);
- border-radius: var(--radius);
+ border: solid 0.5px var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
&:active {
- border-color: var(--accent);
+ border-color: var(--MI_THEME-accent);
}
}
._popup {
- background: var(--popup);
- border-radius: var(--radius);
+ background: var(--MI_THEME-popup);
+ border-radius: var(--MI-radius);
contain: content;
}
._acrylic {
- background: var(--acrylicPanel);
- -webkit-backdrop-filter: var(--blur, blur(15px));
- backdrop-filter: var(--blur, blur(15px));
+ background: var(--MI_THEME-acrylicPanel);
+ -webkit-backdrop-filter: var(--MI-blur, blur(15px));
+ backdrop-filter: var(--MI-blur, blur(15px));
}
._formLinksGrid {
@@ -399,8 +398,8 @@ rt {
margin-left: 0.7em;
font-size: 65%;
padding: 2px 3px;
- color: var(--accent);
- border: solid 1px var(--accent);
+ color: var(--MI_THEME-accent);
+ border: solid 1px var(--MI_THEME-accent);
border-radius: var(--radius-xs);
vertical-align: top;
}
@@ -409,8 +408,8 @@ rt {
margin-left: 0.7em;
font-size: 65%;
padding: 2px 3px;
- color: var(--warn);
- border: solid 1px var(--warn);
+ color: var(--MI_THEME-warn);
+ border: solid 1px var(--MI_THEME-warn);
border-radius: 4px;
vertical-align: top;
}
@@ -456,7 +455,7 @@ rt {
}
._link {
- color: var(--link);
+ color: var(--MI_THEME-link);
}
._caption {
@@ -480,14 +479,14 @@ rt {
box-shadow: 0 6px 16px #0007, 0 0 1px 1px #693410, inset 0 0 2px 1px #ce8a5c;
border-radius: 10px;
- --bg: #F1E8DC;
- --fg: #693410;
+ --MI_THEME-bg: #F1E8DC;
+ --MI_THEME-fg: #693410;
}
html[data-color-scheme=dark] ._woodenFrame {
- --bg: #1d0c02;
- --fg: #F1E8DC;
- --panel: #192320;
+ --MI_THEME-bg: #1d0c02;
+ --MI_THEME-fg: #F1E8DC;
+ --MI_THEME-panel: #192320;
}
._woodenFrameH {
@@ -498,10 +497,10 @@ html[data-color-scheme=dark] ._woodenFrame {
._woodenFrameInner {
padding: 8px;
margin-top: 8px;
- background: var(--bg);
+ background: var(--MI_THEME-bg);
box-shadow: 0 0 2px 1px #ce8a5c, inset 0 0 1px 1px #693410;
border-radius: 6px;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
&:first-child {
margin-top: 0;
@@ -516,7 +515,11 @@ html[data-color-scheme=dark] ._woodenFrame {
transform: scale(0.9);
}
-@keyframes global-blink {
+._blink {
+ animation: blink 1s infinite;
+}
+
+@keyframes blink {
0% { opacity: 1; transform: scale(1); }
30% { opacity: 1; transform: scale(1); }
90% { opacity: 0; transform: scale(0.5); }
diff --git a/packages/frontend/src/ui/_common_/announcements.vue b/packages/frontend/src/ui/_common_/announcements.vue
index 374bc20b54..d153dc8726 100644
--- a/packages/frontend/src/ui/_common_/announcements.vue
+++ b/packages/frontend/src/ui/_common_/announcements.vue
@@ -13,9 +13,9 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<span :class="$style.icon">
<i v-if="announcement.icon === 'info'" class="ti ti-info-circle"></i>
- <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--warn);"></i>
- <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--error);"></i>
- <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--success);"></i>
+ <i v-else-if="announcement.icon === 'warning'" class="ti ti-alert-triangle" style="color: var(--MI_THEME-warn);"></i>
+ <i v-else-if="announcement.icon === 'error'" class="ti ti-circle-x" style="color: var(--MI_THEME-error);"></i>
+ <i v-else-if="announcement.icon === 'success'" class="ti ti-check" style="color: var(--MI_THEME-success);"></i>
</span>
<span :class="$style.title">{{ announcement.title }}</span>
<span :class="$style.body">{{ announcement.text }}</span>
@@ -30,7 +30,7 @@ import { $i } from '@/account.js';
<style lang="scss" module>
.root {
font-size: 15px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
}
.item {
@@ -44,8 +44,8 @@ import { $i } from '@/account.js';
height: var(--height);
overflow: clip;
contain: strict;
- background: var(--accent);
- color: var(--fgOnAccent);
+ background: var(--MI_THEME-accent);
+ color: var(--MI_THEME-fgOnAccent);
@container (max-width: 1000px) {
display: block;
diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue
index a8ff2a4c8d..7e29a5eeff 100644
--- a/packages/frontend/src/ui/_common_/common.vue
+++ b/packages/frontend/src/ui/_common_/common.vue
@@ -129,26 +129,26 @@ function getPointerEvents() {
.notifications {
position: fixed;
z-index: 3900000;
- padding: 0 var(--margin);
+ padding: 0 var(--MI-margin);
display: flex;
&.notificationsPosition_leftTop {
- top: var(--margin);
+ top: var(--MI-margin);
left: 0;
}
&.notificationsPosition_rightTop {
- top: var(--margin);
+ top: var(--MI-margin);
right: 0;
}
&.notificationsPosition_leftBottom {
- bottom: calc(var(--minBottomSpacing) + var(--margin));
+ bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin));
left: 0;
}
&.notificationsPosition_rightBottom {
- bottom: calc(var(--minBottomSpacing) + var(--margin));
+ bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin));
right: 0;
}
@@ -246,8 +246,8 @@ function getPointerEvents() {
height: 18px;
box-sizing: border-box;
border: solid 2px transparent;
- border-top-color: var(--accent);
- border-left-color: var(--accent);
+ border-top-color: var(--MI_THEME-accent);
+ border-left-color: var(--MI_THEME-accent);
border-radius: 50%;
animation: progress-spinner 400ms linear infinite;
}
diff --git a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
index f3244b5697..c8c0915dc9 100644
--- a/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
+++ b/packages/frontend/src/ui/_common_/navbar-for-mobile.vue
@@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="item === '-'" :class="$style.divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" class="_button" :class="[$style.item, { [$style.active]: navbarItemDef[item].active }]" :activeClass="$style.active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
- <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator">
+ <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink">
<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
<i v-else class="_indicatorCircle"></i>
</span>
@@ -31,7 +31,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
<button :class="$style.item" class="_button" @click="more">
<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
- <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
+ <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
</button>
<MkA :class="$style.item" :activeClass="$style.active" to="/settings">
<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
@@ -82,7 +82,7 @@ function more() {
<style lang="scss" module>
.root {
- --nav-bg-transparent: color(from var(--navBg) srgb r g b / 0.5);
+ --nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5);
display: flex;
flex-direction: column;
@@ -94,8 +94,8 @@ function more() {
z-index: 1;
padding: 20px 0;
background: var(--nav-bg-transparent);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(8px));
+ backdrop-filter: var(--MI-blur, blur(8px));
}
.banner {
@@ -135,8 +135,8 @@ function more() {
bottom: 0;
padding: 20px 0;
background: var(--nav-bg-transparent);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(8px));
+ backdrop-filter: var(--MI-blur, blur(8px));
}
.post {
@@ -144,7 +144,7 @@ function more() {
display: block;
width: 100%;
height: 40px;
- color: var(--fgOnAccent);
+ color: var(--MI_THEME-fgOnAccent);
font-weight: bold;
text-align: left;
@@ -160,12 +160,12 @@ function more() {
right: 0;
bottom: 0;
border-radius: var(--radius-ellipse);
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
}
&:hover, &.active {
&::before {
- background: var(--accentLighten);
+ background: var(--MI_THEME-accentLighten);
}
}
}
@@ -209,7 +209,7 @@ function more() {
.divider {
margin: 16px 16px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.item {
@@ -223,15 +223,15 @@ function more() {
width: 100%;
text-align: left;
box-sizing: border-box;
- color: var(--navFg);
+ color: var(--MI_THEME-navFg);
&:hover {
text-decoration: none;
- color: var(--navHoverFg);
+ color: var(--MI_THEME-navHoverFg);
}
&.active {
- color: var(--navActive);
+ color: var(--MI_THEME-navActive);
}
&:hover, &.active {
@@ -247,7 +247,7 @@ function more() {
right: 0;
bottom: 0;
border-radius: var(--radius-ellipse);
- background: var(--accentedBg);
+ background: var(--MI_THEME-accentedBg);
}
}
}
@@ -262,9 +262,8 @@ function more() {
position: absolute;
top: 0;
left: 20px;
- color: var(--navIndicator);
+ color: var(--MI_THEME-navIndicator);
font-size: 8px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/_common_/navbar.vue b/packages/frontend/src/ui/_common_/navbar.vue
index 17690df412..9481a99231 100644
--- a/packages/frontend/src/ui/_common_/navbar.vue
+++ b/packages/frontend/src/ui/_common_/navbar.vue
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}"
>
<i class="ti-fw" :class="[$style.itemIcon, navbarItemDef[item].icon]"></i><span :class="$style.itemText">{{ navbarItemDef[item].title }}</span>
- <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator">
+ <span v-if="navbarItemDef[item].indicated" :class="$style.itemIndicator" class="_blink">
<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
<i v-else class="_indicatorCircle"></i>
</span>
@@ -41,7 +41,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
<button class="_button" :class="$style.item" @click="more">
<i :class="$style.itemIcon" class="ti ti-grid-dots ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.more }}</span>
- <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator"><i class="_indicatorCircle"></i></span>
+ <span v-if="otherMenuItemIndicated" :class="$style.itemIndicator" class="_blink"><i class="_indicatorCircle"></i></span>
</button>
<MkA v-tooltip.noDelay.right="i18n.ts.settings" :class="$style.item" :activeClass="$style.active" to="/settings">
<i :class="$style.itemIcon" class="ti ti-settings ti-fw"></i><span :class="$style.itemText">{{ i18n.ts.settings }}</span>
@@ -111,7 +111,7 @@ function more(ev: MouseEvent) {
.root {
--nav-width: 250px;
--nav-icon-only-width: 80px;
- --nav-bg-transparent: color(from var(--navBg) srgb r g b / 0.5);
+ --nav-bg-transparent: color(from var(--MI_THEME-navBg) srgb r g b / 0.5);
flex: 0 0 var(--nav-width);
width: var(--nav-width);
@@ -129,7 +129,7 @@ function more(ev: MouseEvent) {
overflow: auto;
overflow-x: clip;
overscroll-behavior: contain;
- background: var(--navBg);
+ background: var(--MI_THEME-navBg);
contain: strict;
display: flex;
flex-direction: column;
@@ -146,8 +146,8 @@ function more(ev: MouseEvent) {
z-index: 1;
padding: 20px 0;
background: var(--nav-bg-transparent);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(8px));
+ backdrop-filter: var(--MI-blur, blur(8px));
}
.banner {
@@ -172,7 +172,7 @@ function more(ev: MouseEvent) {
outline: none;
> .instanceIcon {
- outline: 2px solid var(--focus);
+ outline: 2px solid var(--MI_THEME-focus);
outline-offset: 2px;
}
}
@@ -196,8 +196,8 @@ function more(ev: MouseEvent) {
bottom: 0;
padding-top: 20px;
background: var(--nav-bg-transparent);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(8px));
+ backdrop-filter: var(--MI-blur, blur(8px));
}
.post {
@@ -205,7 +205,7 @@ function more(ev: MouseEvent) {
display: block;
width: 100%;
height: 40px;
- color: var(--fgOnAccent);
+ color: var(--MI_THEME-fgOnAccent);
font-weight: bold;
text-align: left;
@@ -221,21 +221,21 @@ function more(ev: MouseEvent) {
right: 0;
bottom: 0;
border-radius: var(--radius-ellipse);
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
}
&:focus-visible {
outline: none;
&::before {
- outline: 2px solid var(--fgOnAccent);
+ outline: 2px solid var(--MI_THEME-fgOnAccent);
outline-offset: -4px;
}
}
&:hover, &.active {
&::before {
- background: var(--accentLighten);
+ background: var(--MI_THEME-accentLighten);
}
}
}
@@ -265,7 +265,7 @@ function more(ev: MouseEvent) {
outline: none;
> .avatar {
- box-shadow: 0 0 0 4px var(--focus);
+ box-shadow: 0 0 0 4px var(--MI_THEME-focus);
}
}
}
@@ -291,7 +291,7 @@ function more(ev: MouseEvent) {
.divider {
margin: 16px 16px;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.item {
@@ -305,28 +305,28 @@ function more(ev: MouseEvent) {
width: 100%;
text-align: left;
box-sizing: border-box;
- color: var(--navFg);
+ color: var(--MI_THEME-navFg);
&:hover {
text-decoration: none;
- color: var(--navHoverFg);
+ color: var(--MI_THEME-navHoverFg);
}
&.active {
- color: var(--navActive);
+ color: var(--MI_THEME-navActive);
}
&:focus-visible {
outline: none;
&::before {
- outline: 2px solid var(--focus);
+ outline: 2px solid var(--MI_THEME-focus);
outline-offset: -2px;
}
}
&:hover, &.active, &:focus {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
&::before {
content: "";
@@ -340,7 +340,7 @@ function more(ev: MouseEvent) {
right: 0;
bottom: 0;
border-radius: var(--radius-ellipse);
- background: var(--accentedBg);
+ background: var(--MI_THEME-accentedBg);
}
}
}
@@ -355,9 +355,8 @@ function more(ev: MouseEvent) {
position: absolute;
top: 0;
left: 20px;
- color: var(--navIndicator);
+ color: var(--MI_THEME-navIndicator);
font-size: 8px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
@@ -387,8 +386,8 @@ function more(ev: MouseEvent) {
z-index: 1;
padding: 20px 0;
background: var(--nav-bg-transparent);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(8px));
+ backdrop-filter: var(--MI-blur, blur(8px));
}
.instance {
@@ -400,7 +399,7 @@ function more(ev: MouseEvent) {
outline: none;
> .instanceIcon {
- outline: 2px solid var(--focus);
+ outline: 2px solid var(--MI_THEME-focus);
outline-offset: 2px;
}
}
@@ -417,8 +416,8 @@ function more(ev: MouseEvent) {
bottom: 0;
padding-top: 20px;
background: var(--nav-bg-transparent);
- -webkit-backdrop-filter: var(--blur, blur(8px));
- backdrop-filter: var(--blur, blur(8px));
+ -webkit-backdrop-filter: var(--MI-blur, blur(8px));
+ backdrop-filter: var(--MI-blur, blur(8px));
}
.post {
@@ -440,28 +439,28 @@ function more(ev: MouseEvent) {
width: 52px;
aspect-ratio: 1/1;
border-radius: var(--radius-full);
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
}
&:focus-visible {
outline: none;
&::before {
- outline: 2px solid var(--fgOnAccent);
+ outline: 2px solid var(--MI_THEME-fgOnAccent);
outline-offset: -4px;
}
}
&:hover, &.active {
&::before {
- background: var(--accentLighten);
+ background: var(--MI_THEME-accentLighten);
}
}
}
.postIcon {
position: relative;
- color: var(--fgOnAccent);
+ color: var(--MI_THEME-fgOnAccent);
}
.postText {
@@ -479,7 +478,7 @@ function more(ev: MouseEvent) {
outline: none;
> .avatar {
- box-shadow: 0 0 0 4px var(--focus);
+ box-shadow: 0 0 0 4px var(--MI_THEME-focus);
}
}
}
@@ -501,7 +500,7 @@ function more(ev: MouseEvent) {
.divider {
margin: 8px auto;
width: calc(100% - 32px);
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.item {
@@ -515,14 +514,14 @@ function more(ev: MouseEvent) {
outline: none;
&::before {
- outline: 2px solid var(--focus);
+ outline: 2px solid var(--MI_THEME-focus);
outline-offset: -2px;
}
}
&:hover, &.active, &:focus {
text-decoration: none;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
&::before {
content: "";
@@ -536,7 +535,7 @@ function more(ev: MouseEvent) {
right: 0;
bottom: 0;
border-radius: var(--radius-ellipse);
- background: var(--accentedBg);
+ background: var(--MI_THEME-accentedBg);
}
> .icon,
@@ -560,9 +559,8 @@ function more(ev: MouseEvent) {
position: absolute;
top: 6px;
left: 24px;
- color: var(--navIndicator);
+ color: var(--MI_THEME-navIndicator);
font-size: 8px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue
index 690366307b..5f9a938017 100644
--- a/packages/frontend/src/ui/_common_/statusbars.vue
+++ b/packages/frontend/src/ui/_common_/statusbars.vue
@@ -32,7 +32,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
<style lang="scss" module>
.root {
font-size: 15px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
}
.item {
@@ -81,7 +81,7 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
.name {
padding: 0 var(--nameMargin);
font-weight: bold;
- color: var(--accent);
+ color: var(--MI_THEME-accent);
&:empty {
display: none;
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index ad93b7e61c..cc62a28b14 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -48,8 +48,8 @@ onUnmounted(() => {
.root {
position: fixed;
z-index: v-bind(zIndex);
- bottom: calc(var(--minBottomSpacing) + var(--margin));
- right: var(--margin);
+ bottom: calc(var(--MI-minBottomSpacing) + var(--MI-margin));
+ right: var(--MI-margin);
margin: 0;
padding: 12px;
font-size: 0.9em;
diff --git a/packages/frontend/src/ui/_common_/upload.vue b/packages/frontend/src/ui/_common_/upload.vue
index 96030c7897..d15c497645 100644
--- a/packages/frontend/src/ui/_common_/upload.vue
+++ b/packages/frontend/src/ui/_common_/upload.vue
@@ -125,10 +125,10 @@ const zIndex = os.claimZIndex('high');
height: 8px;
}
.mk-uploader > ol > li > progress::-webkit-progress-value {
- background: var(--accent);
+ background: var(--MI_THEME-accent);
}
.mk-uploader > ol > li > progress::-webkit-progress-bar {
- //background: var(--accentAlpha01);
+ //background: var(--MI_THEME-accentAlpha01);
background: transparent;
}
</style>
diff --git a/packages/frontend/src/ui/classic.header.vue b/packages/frontend/src/ui/classic.header.vue
index c03afd6cd6..f4633314ae 100644
--- a/packages/frontend/src/ui/classic.header.vue
+++ b/packages/frontend/src/ui/classic.header.vue
@@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="item === '-'" class="divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime v-tooltip="navbarItemDef[item].title" class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="ti-fw" :class="navbarItemDef[item].icon"></i>
- <span v-if="navbarItemDef[item].indicated" class="indicator"><i class="_indicatorCircle"></i></span>
+ <span v-if="navbarItemDef[item].indicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
</component>
</template>
<div class="divider"></div>
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="ti ti-dots ti-fw"></i>
- <span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span>
+ <span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
</button>
</div>
<div class="right">
@@ -104,7 +104,7 @@ onMounted(() => {
z-index: 1000;
width: 100%;
height: $height;
- background-color: var(--bg);
+ background-color: var(--MI_THEME-bg);
> .body {
max-width: 1380px;
@@ -140,18 +140,17 @@ onMounted(() => {
position: absolute;
top: 0;
left: 0;
- color: var(--navIndicator);
+ color: var(--MI_THEME-navIndicator);
font-size: 8px;
- animation: global-blink 1s infinite;
}
&:hover {
text-decoration: none;
- color: var(--navHoverFg);
+ color: var(--MI_THEME-navHoverFg);
}
&.active {
- color: var(--navActive);
+ color: var(--MI_THEME-navActive);
}
}
@@ -159,7 +158,7 @@ onMounted(() => {
display: inline-block;
height: 16px;
margin: 0 10px;
- border-right: solid 0.5px var(--divider);
+ border-right: solid 0.5px var(--MI_THEME-divider);
}
> .instance {
diff --git a/packages/frontend/src/ui/classic.sidebar.vue b/packages/frontend/src/ui/classic.sidebar.vue
index 96cc24c9b9..f17027bcde 100644
--- a/packages/frontend/src/ui/classic.sidebar.vue
+++ b/packages/frontend/src/ui/classic.sidebar.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div v-if="item === '-'" class="divider"></div>
<component :is="navbarItemDef[item].to ? 'MkA' : 'button'" v-else-if="navbarItemDef[item] && (navbarItemDef[item].show !== false)" v-click-anime class="item _button" :class="item" activeClass="active" :to="navbarItemDef[item].to" v-on="navbarItemDef[item].action ? { click: navbarItemDef[item].action } : {}">
<i class="ti-fw" :class="navbarItemDef[item].icon"></i><span class="text">{{ navbarItemDef[item].title }}</span>
- <span v-if="navbarItemDef[item].indicated" class="indicator">
+ <span v-if="navbarItemDef[item].indicated" class="indicator _blink">
<span v-if="navbarItemDef[item].indicateValue" class="_indicateCounter itemIndicateValueIcon">{{ navbarItemDef[item].indicateValue }}</span>
<i v-else class="_indicatorCircle"></i>
</span>
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkA>
<button v-click-anime class="item _button" @click="more">
<i class="ti ti-dots ti-fw"></i><span class="text">{{ i18n.ts.more }}</span>
- <span v-if="otherNavItemIndicated" class="indicator"><i class="_indicatorCircle"></i></span>
+ <span v-if="otherNavItemIndicated" class="indicator _blink"><i class="_indicatorCircle"></i></span>
</button>
<MkA v-click-anime class="item" activeClass="active" to="/settings" :behavior="settingsWindowed ? 'window' : null">
<i class="ti ti-settings ti-fw"></i><span class="text">{{ i18n.ts.settings }}</span>
@@ -159,7 +159,7 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
> .divider {
margin: 10px 0;
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
> .post {
@@ -224,9 +224,8 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
position: absolute;
top: 0;
left: 0;
- color: var(--navIndicator);
+ color: var(--MI_THEME-navIndicator);
font-size: 8px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
@@ -237,11 +236,11 @@ watch(defaultStore.reactiveState.menuDisplay, () => {
&:hover {
text-decoration: none;
- color: var(--navHoverFg);
+ color: var(--MI_THEME-navHoverFg);
}
&.active {
- color: var(--navActive);
+ color: var(--MI_THEME-navActive);
}
}
}
diff --git a/packages/frontend/src/ui/classic.vue b/packages/frontend/src/ui/classic.vue
index 31bb1ddc14..ded945dda1 100644
--- a/packages/frontend/src/ui/classic.vue
+++ b/packages/frontend/src/ui/classic.vue
@@ -12,7 +12,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<XSidebar/>
</div>
<div v-else-if="!pageMetadata?.needWideArea" ref="widgetsLeft" class="widgets left">
- <XWidgets place="left" :marginTop="'var(--margin)'" @mounted="attachSticky(widgetsLeft)"/>
+ <XWidgets place="left" :marginTop="'var(--MI-margin)'" @mounted="attachSticky(widgetsLeft)"/>
</div>
<main class="main" @contextmenu.stop="onContextmenu">
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</main>
<div v-if="isDesktop && !pageMetadata?.needWideArea" ref="widgetsRight" class="widgets right">
- <XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--margin)'" @mounted="attachSticky(widgetsRight)"/>
+ <XWidgets :place="showMenuOnTop ? 'right' : null" :marginTop="showMenuOnTop ? '0' : 'var(--MI-margin)'" @mounted="attachSticky(widgetsRight)"/>
</div>
</div>
@@ -216,8 +216,8 @@ onMounted(() => {
box-sizing: border-box;
&.wallpaper {
- background: var(--wallpaperOverlay);
- //backdrop-filter: var(--blur, blur(4px));
+ background: var(--MI_THEME-wallpaperOverlay);
+ //backdrop-filter: var(--MI-blur, blur(4px));
}
> .columns {
@@ -249,17 +249,16 @@ onMounted(() => {
min-width: 0;
width: 750px;
margin: 0 16px 0 0;
- border-left: solid 1px var(--divider);
- border-right: solid 1px var(--divider);
+ border-left: solid 1px var(--MI_THEME-divider);
+ border-right: solid 1px var(--MI_THEME-divider);
border-radius: 0;
overflow: clip;
- --margin: 12px;
+ --MI-margin: 12px;
}
> .widgets {
position: sticky;
top: 0;
- width: 300px;
height: 100%;
padding-top: 16px;
box-sizing: border-box;
@@ -281,13 +280,13 @@ onMounted(() => {
&.withGlobalHeader {
> .main {
margin-top: 0;
- border: solid 1px var(--divider);
- border-radius: var(--radius);
- --stickyTop: var(--globalHeaderHeight);
+ border: solid 1px var(--MI_THEME-divider);
+ border-radius: var(--MI-radius);
+ --MI-stickyTop: var(--globalHeaderHeight);
}
> .widgets {
- --stickyTop: var(--globalHeaderHeight);
+ --MI-stickyTop: var(--globalHeaderHeight);
margin-top: 0;
}
}
@@ -296,7 +295,7 @@ onMounted(() => {
margin: 0;
> .sidebar {
- border-right: solid 0.5px var(--divider);
+ border-right: solid 0.5px var(--MI_THEME-divider);
}
> .main {
@@ -318,10 +317,10 @@ onMounted(() => {
right: 0;
z-index: 1001;
height: 100dvh;
- padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
+ padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
box-sizing: border-box;
overflow: auto;
- background: var(--bg);
+ background: var(--MI_THEME-bg);
}
> .ivnzpscs {
diff --git a/packages/frontend/src/ui/deck.vue b/packages/frontend/src/ui/deck.vue
index cd4e256056..dfd6c7bf6f 100644
--- a/packages/frontend/src/ui/deck.vue
+++ b/packages/frontend/src/ui/deck.vue
@@ -50,11 +50,11 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
<div v-if="isMobile" :class="$style.nav">
- <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
+ <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')">
<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
- <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
+ <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink">
<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
</span>
</button>
@@ -97,6 +97,7 @@ import { v4 as uuid } from 'uuid';
import XCommon from './_common_/common.vue';
import { deckStore, columnTypes, addColumn as addColumnToStore, loadDeck, getProfiles, deleteProfile as deleteProfile_ } from './deck/deck-store.js';
import type { ColumnType } from './deck/deck-store.js';
+import type { MenuItem } from '@/types/menu.js';
import XSidebar from '@/ui/_common_/navbar.vue';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import MkButton from '@/components/MkButton.vue';
@@ -118,7 +119,6 @@ import XMentionsColumn from '@/ui/deck/mentions-column.vue';
import XDirectColumn from '@/ui/deck/direct-column.vue';
import XRoleTimelineColumn from '@/ui/deck/role-timeline-column.vue';
import { mainRouter } from '@/router/main.js';
-import type { MenuItem } from '@/types/menu.js';
const XStatusBars = defineAsyncComponent(() => import('@/ui/_common_/statusbars.vue'));
const XAnnouncements = defineAsyncComponent(() => import('@/ui/_common_/announcements.vue'));
@@ -305,7 +305,7 @@ body {
.root {
$nav-hide-threshold: 650px; // TODO: ã©ã“ã‹ã«é›†ç´„ã—ãŸã„
- --margin: var(--marginHalf);
+ --MI-margin: var(--MI-marginHalf);
--columnGap: 6px;
@@ -332,7 +332,7 @@ body {
overflow-x: auto;
overflow-y: clip;
overscroll-behavior: contain;
- background: var(--deckBg);
+ background: var(--MI_THEME-deckBg);
&.center {
> .section:first-of-type {
@@ -414,7 +414,7 @@ body {
contain: strict;
overflow: auto;
overscroll-behavior: contain;
- background: var(--navBg);
+ background: var(--MI_THEME-navBg);
}
.nav {
@@ -428,10 +428,10 @@ body {
grid-gap: 8px;
width: 100%;
box-sizing: border-box;
- -webkit-backdrop-filter: var(--blur, blur(32px));
- backdrop-filter: var(--blur, blur(32px));
- background-color: var(--header);
- border-top: solid 0.5px var(--divider);
+ -webkit-backdrop-filter: var(--MI-blur, blur(32px));
+ backdrop-filter: var(--MI-blur, blur(32px));
+ background-color: var(--MI_THEME-header);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.navButton {
@@ -443,30 +443,30 @@ body {
margin: auto;
border-radius: var(--radius-lg);
background: transparent;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
&:hover {
- color: var(--accent);
+ color: var(--MI_THEME-accent);
}
&:active {
- color: var(--accent);
- background: hsl(from var(--panel) h s calc(l - 2));
+ color: var(--MI_THEME-accent);
+ background: hsl(from var(--MI_THEME-panel) h s calc(l - 2));
}
}
.postButton {
composes: navButton;
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
- color: var(--fgOnAccent);
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
+ color: var(--MI_THEME-fgOnAccent);
&:hover {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
- color: var(--fgOnAccent);
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
+ color: var(--MI_THEME-fgOnAccent);
}
&:active {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
color: var(--fgOnAccent);
}
}
@@ -480,9 +480,8 @@ body {
position: absolute;
top: 0;
left: 0;
- color: var(--indicator);
+ color: var(--MI_THEME-indicator);
font-size: 16px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index 5ed3aa754f..d89c322c72 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<svg viewBox="0 0 256 128" :class="$style.tabShape">
<g transform="matrix(6.2431,0,0,6.2431,-677.417,-29.3839)">
- <path d="M149.512,4.707L108.507,4.707C116.252,4.719 118.758,14.958 118.758,14.958C118.758,14.958 121.381,25.283 129.009,25.209L149.512,25.209L149.512,4.707Z" style="fill:var(--deckBg);"/>
+ <path d="M149.512,4.707L108.507,4.707C116.252,4.719 118.758,14.958 118.758,14.958C118.758,14.958 121.381,25.283 129.009,25.209L149.512,25.209L149.512,4.707Z" style="fill:var(--MI_THEME-deckBg);"/>
</g>
</svg>
<div :class="$style.color"></div>
@@ -299,7 +299,7 @@ function onDrop(ev) {
left: 0;
width: 100%;
height: 100%;
- background: var(--focus);
+ background: var(--MI_THEME-focus);
}
}
@@ -313,7 +313,7 @@ function onDrop(ev) {
left: 0;
width: 100%;
height: 100%;
- background: var(--focus);
+ background: var(--MI_THEME-focus);
opacity: 0.5;
}
}
@@ -331,19 +331,19 @@ function onDrop(ev) {
}
&.naked {
- background: var(--acrylicBg) !important;
- -webkit-backdrop-filter: var(--blur, blur(10px));
- backdrop-filter: var(--blur, blur(10px));
+ background: var(--MI_THEME-acrylicBg) !important;
+ -webkit-backdrop-filter: var(--MI-blur, blur(10px));
+ backdrop-filter: var(--MI-blur, blur(10px));
> .header {
background: transparent;
box-shadow: none;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
}
> .body {
background: transparent !important;
- scrollbar-color: var(--scrollbarHandle) transparent;
+ scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
&::-webkit-scrollbar-track {
background: transparent;
@@ -352,12 +352,12 @@ function onDrop(ev) {
}
&.paged {
- background: var(--bg) !important;
+ background: var(--MI_THEME-bg) !important;
> .body {
- background: var(--bg) !important;
+ background: var(--MI_THEME-bg) !important;
overflow-y: scroll !important;
- scrollbar-color: var(--scrollbarHandle) transparent;
+ scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent;
&::-webkit-scrollbar-track {
background: inherit;
@@ -374,9 +374,9 @@ function onDrop(ev) {
height: var(--deckColumnHeaderHeight);
padding: 0 16px 0 30px;
font-size: 0.9em;
- color: var(--panelHeaderFg);
- background: var(--panelHeaderBg);
- box-shadow: 0 1px 0 0 var(--panelHeaderDivider);
+ color: var(--MI_THEME-panelHeaderFg);
+ background: var(--MI_THEME-panelHeaderBg);
+ box-shadow: 0 1px 0 0 var(--MI_THEME-panelHeaderDivider);
cursor: pointer;
user-select: none;
}
@@ -387,7 +387,7 @@ function onDrop(ev) {
left: 12px;
width: 3px;
height: calc(100% - 24px);
- background: var(--accent);
+ background: var(--MI_THEME-accent);
border-radius: var(--radius-ellipse);
}
@@ -441,11 +441,11 @@ function onDrop(ev) {
overscroll-behavior-y: contain;
box-sizing: border-box;
container-type: size;
- background-color: var(--bg);
- scrollbar-color: var(--scrollbarHandle) var(--panel);
+ background-color: var(--MI_THEME-bg);
+ scrollbar-color: var(--MI_THEME-scrollbarHandle) var(--MI_THEME-panel);
&::-webkit-scrollbar-track {
- background: var(--panel);
+ background: var(--MI_THEME-panel);
}
}
</style>
diff --git a/packages/frontend/src/ui/deck/widgets-column.vue b/packages/frontend/src/ui/deck/widgets-column.vue
index 9995996771..a0e62c8264 100644
--- a/packages/frontend/src/ui/deck/widgets-column.vue
+++ b/packages/frontend/src/ui/deck/widgets-column.vue
@@ -57,10 +57,10 @@ const menu = [{
<style lang="scss" module>
.root {
- --margin: 8px;
- --panelBorder: none;
+ --MI-margin: 8px;
+ --MI_THEME-panelBorder: none;
- padding: 0 var(--margin);
+ padding: 0 var(--MI-margin);
}
.intro {
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 2fdaca775b..0b3f5875f2 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<button v-if="(!isDesktop || pageMetadata?.needWideArea) && !isMobile" :class="$style.widgetButton" class="_button" @click="widgetsShowing = true"><i class="ti ti-apps"></i></button>
<div v-if="isMobile" ref="navFooter" :class="$style.nav">
- <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator"><i class="_indicatorCircle"></i></span></button>
+ <button :class="$style.navButton" class="_button" @click="drawerMenuShowing = true"><i :class="$style.navButtonIcon" class="ti ti-menu-2"></i><span v-if="menuIndicated" :class="$style.navButtonIndicator" class="_blink"><i class="_indicatorCircle"></i></span></button>
<button :class="$style.navButton" class="_button" @click="isRoot ? top() : mainRouter.push('/')"><i :class="$style.navButtonIcon" class="ti ti-home"></i></button>
<button :class="$style.navButton" class="_button" @click="mainRouter.push('/my/notifications')">
<i :class="$style.navButtonIcon" class="ti ti-bell"></i>
- <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator">
+ <span v-if="$i?.hasUnreadNotification" :class="$style.navButtonIndicator" class="_blink">
<span class="_indicateCounter" :class="$style.itemIndicateValueIcon">{{ $i.unreadNotificationsCount > 99 ? '99+' : $i.unreadNotificationsCount }}</span>
</span>
</button>
@@ -96,9 +96,11 @@ SPDX-License-Identifier: AGPL-3.0-only
<script lang="ts" setup>
import { defineAsyncComponent, provide, onMounted, computed, ref, watch, shallowRef, Ref } from 'vue';
+import { instanceName } from '@@/js/config.js';
+import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js';
+import { isLink } from '@@/js/is-link.js';
import XCommon from './_common_/common.vue';
import type MkStickyContainer from '@/components/global/MkStickyContainer.vue';
-import { instanceName } from '@@/js/config.js';
import XDrawerMenu from '@/ui/_common_/navbar-for-mobile.vue';
import * as os from '@/os.js';
import { defaultStore } from '@/store.js';
@@ -108,10 +110,8 @@ import { $i } from '@/account.js';
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { deviceKind } from '@/scripts/device-kind.js';
import { miLocalStorage } from '@/local-storage.js';
-import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js';
import { useScrollPositionManager } from '@/nirax.js';
import { mainRouter } from '@/router/main.js';
-import { isLink } from '@@/js/is-link.js';
const XWidgets = defineAsyncComponent(() => import('./universal.widgets.vue'));
const XSidebar = defineAsyncComponent(() => import('@/ui/_common_/navbar.vue'));
@@ -225,12 +225,12 @@ provide<Ref<number>>(CURRENT_STICKY_BOTTOM, navFooterHeight);
watch(navFooter, () => {
if (navFooter.value) {
navFooterHeight.value = navFooter.value.offsetHeight;
- document.body.style.setProperty('--stickyBottom', `${navFooterHeight.value}px`);
- document.body.style.setProperty('--minBottomSpacing', 'var(--minBottomSpacingMobile)');
+ document.body.style.setProperty('--MI-stickyBottom', `${navFooterHeight.value}px`);
+ document.body.style.setProperty('--MI-minBottomSpacing', 'var(--MI-minBottomSpacingMobile)');
} else {
navFooterHeight.value = 0;
- document.body.style.setProperty('--stickyBottom', '0px');
- document.body.style.setProperty('--minBottomSpacing', '0px');
+ document.body.style.setProperty('--MI-stickyBottom', '0px');
+ document.body.style.setProperty('--MI-minBottomSpacing', '0px');
}
}, {
immediate: true,
@@ -318,7 +318,7 @@ $widgets-hide-threshold: 1090px;
}
.sidebar {
- border-right: solid 0.5px var(--divider);
+ border-right: solid 0.5px var(--MI_THEME-divider);
}
.contents {
@@ -328,7 +328,7 @@ $widgets-hide-threshold: 1090px;
overflow: auto;
overflow-y: scroll;
overscroll-behavior: unset;
- background: var(--bg);
+ background: var(--MI_THEME-bg);
}
.widgets {
@@ -336,9 +336,9 @@ $widgets-hide-threshold: 1090px;
height: 100%;
box-sizing: border-box;
overflow: auto;
- padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px));
- border-left: solid 0.5px var(--divider);
- background: var(--bg);
+ padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
+ border-left: solid 0.5px var(--MI_THEME-divider);
+ background: var(--MI_THEME-bg);
@media (max-width: $widgets-hide-threshold) {
display: none;
@@ -356,7 +356,7 @@ $widgets-hide-threshold: 1090px;
border-radius: var(--radius-full);
box-shadow: 0 3px 5px -1px rgba(0, 0, 0, 0.2), 0 6px 10px 0 rgba(0, 0, 0, 0.14), 0 1px 18px 0 rgba(0, 0, 0, 0.12);
font-size: 22px;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
}
.widgetsDrawerBg {
@@ -370,11 +370,11 @@ $widgets-hide-threshold: 1090px;
z-index: 1001;
width: 310px;
height: 100dvh;
- padding: var(--margin) var(--margin) calc(var(--margin) + env(safe-area-inset-bottom, 0px)) !important;
+ padding: var(--MI-margin) var(--MI-margin) calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)) !important;
box-sizing: border-box;
overflow: auto;
overscroll-behavior: contain;
- background: var(--bg);
+ background: var(--MI_THEME-bg);
}
.widgetsCloseButton {
@@ -400,10 +400,10 @@ $widgets-hide-threshold: 1090px;
grid-gap: 8px;
width: 100%;
box-sizing: border-box;
- -webkit-backdrop-filter: var(--blur, blur(24px));
- backdrop-filter: var(--blur, blur(24px));
- background-color: var(--header);
- border-top: solid 0.5px var(--divider);
+ -webkit-backdrop-filter: var(--MI-blur, blur(24px));
+ backdrop-filter: var(--MI-blur, blur(24px));
+ background-color: var(--MI_THEME-header);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
.navButton {
@@ -415,32 +415,32 @@ $widgets-hide-threshold: 1090px;
margin: auto;
border-radius: var(--radius-lg);
background: transparent;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
&:hover {
- background: var(--panelHighlight);
- color: var(--accent);
+ background: var(--MI_THEME-panelHighlight);
+ color: var(--MI_THEME-accent);
}
&:active {
- background: hsl(from var(--panel) h s calc(l - 2));
- color: var(--accent);
+ background: hsl(from var(--MI_THEME-panel) h s calc(l - 2));
+ color: var(--MI_THEME-accent);
}
}
.postButton {
composes: navButton;
- background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
- color: var(--fgOnAccent);
+ background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
+ color: var(--MI_THEME-fgOnAccent);
&:hover {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
- color: var(--fgOnAccent);
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
+ color: var(--MI_THEME-fgOnAccent);
}
&:active {
- background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
- color: var(--fgOnAccent);
+ color: var(--MI_THEME-fgOnAccent);
+ background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
}
}
@@ -453,9 +453,8 @@ $widgets-hide-threshold: 1090px;
position: absolute;
top: 0;
left: 0;
- color: var(--indicator);
+ color: var(--MI_THEME-indicator);
font-size: 16px;
- animation: global-blink 1s infinite;
&:has(.itemIndicateValueIcon) {
animation: none;
@@ -478,7 +477,7 @@ $widgets-hide-threshold: 1090px;
contain: strict;
overflow: auto;
overscroll-behavior: contain;
- background: var(--navBg);
+ background: var(--MI_THEME-navBg);
}
.statusbars {
@@ -488,6 +487,6 @@ $widgets-hide-threshold: 1090px;
}
.spacer {
- height: calc(var(--minBottomSpacing));
+ height: calc(var(--MI-minBottomSpacing));
}
</style>
diff --git a/packages/frontend/src/ui/visitor.vue b/packages/frontend/src/ui/visitor.vue
index 510b2a4342..371eba4540 100644
--- a/packages/frontend/src/ui/visitor.vue
+++ b/packages/frontend/src/ui/visitor.vue
@@ -189,7 +189,7 @@ defineExpose({
left: 0;
width: 500px;
height: 100vh;
- background: var(--accent);
+ background: var(--MI_THEME-accent);
z-index: 1;
> .banner {
@@ -218,7 +218,7 @@ defineExpose({
min-width: 0;
> .header {
- background: var(--panel);
+ background: var(--MI_THEME-panel);
position: relative;
z-index: 1;
@@ -255,7 +255,7 @@ defineExpose({
left: 0;
width: 240px;
height: 100vh;
- background: var(--panel);
+ background: var(--MI_THEME-panel);
> .link {
display: block;
@@ -269,7 +269,7 @@ defineExpose({
> .divider {
margin: 8px auto;
width: calc(100% - 32px);
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
> .action {
@@ -284,7 +284,7 @@ defineExpose({
border-radius: var(--radius-ellipse);
&._button {
- background: var(--panel);
+ background: var(--MI_THEME-panel);
}
&:first-child {
diff --git a/packages/frontend/src/ui/zen.vue b/packages/frontend/src/ui/zen.vue
index ac13d7822f..7c2a6cc729 100644
--- a/packages/frontend/src/ui/zen.vue
+++ b/packages/frontend/src/ui/zen.vue
@@ -63,12 +63,12 @@ document.documentElement.style.overflowY = 'scroll';
}
.rootWithBottom {
- min-height: calc(100dvh - (60px + (var(--margin) * 2) + env(safe-area-inset-bottom, 0px)));
+ min-height: calc(100dvh - (60px + (var(--MI-margin) * 2) + env(safe-area-inset-bottom, 0px)));
box-sizing: border-box;
}
.bottom {
- height: calc(60px + (var(--margin) * 2) + env(safe-area-inset-bottom, 0px));
+ height: calc(60px + (var(--MI-margin) * 2) + env(safe-area-inset-bottom, 0px));
width: 100%;
margin-top: auto;
}
@@ -81,9 +81,9 @@ document.documentElement.style.overflowY = 'scroll';
max-width: 60px;
margin: auto;
border-radius: var(--radius-full);
- background: var(--panel);
- color: var(--fg);
- right: var(--margin);
- bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px));
+ background: var(--MI_THEME-panel);
+ color: var(--MI_THEME-fg);
+ right: var(--MI-margin);
+ bottom: calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px));
}
</style>
diff --git a/packages/frontend/src/widgets/WidgetAiscript.vue b/packages/frontend/src/widgets/WidgetAiscript.vue
index 8e29254819..6a1fcfa1a6 100644
--- a/packages/frontend/src/widgets/WidgetAiscript.vue
+++ b/packages/frontend/src/widgets/WidgetAiscript.vue
@@ -126,10 +126,10 @@ defineExpose<WidgetComponentExpose>({
max-width: 100%;
min-width: 100%;
padding: 16px;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
background: transparent;
border: none;
- border-bottom: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
border-radius: 0;
box-sizing: border-box;
font: inherit;
@@ -154,7 +154,7 @@ defineExpose<WidgetComponentExpose>({
}
> .logs {
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
text-align: left;
padding: 16px;
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index bcfaaf00ab..c2bda85ac7 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -115,7 +115,7 @@ defineExpose<WidgetComponentExpose>({
<style lang="scss" module>
.bdayFRoot {
overflow: hidden;
- min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--margin) * 2));
+ min-height: calc(calc(calc(50px * 3) - 8px) + calc(var(--MI-margin) * 2));
}
.bdayFGrid {
display: grid;
@@ -123,7 +123,7 @@ defineExpose<WidgetComponentExpose>({
grid-template-rows: repeat(3, 42px);
place-content: center;
gap: 8px;
- margin: var(--margin) auto;
+ margin: var(--MI-margin) auto;
}
.bdayFFallback {
@@ -139,6 +139,6 @@ defineExpose<WidgetComponentExpose>({
width: auto;
max-width: 90%;
margin-bottom: 8px;
- border-radius: var(--radius);
+ border-radius: var(--MI-radius);
}
</style>
diff --git a/packages/frontend/src/widgets/WidgetCalendar.vue b/packages/frontend/src/widgets/WidgetCalendar.vue
index e4ac1acfc7..2387cb1ab4 100644
--- a/packages/frontend/src/widgets/WidgetCalendar.vue
+++ b/packages/frontend/src/widgets/WidgetCalendar.vue
@@ -207,7 +207,7 @@ defineExpose<WidgetComponentExpose>({
.meter {
width: 100%;
overflow: hidden;
- background: var(--X11);
+ background: var(--MI_THEME-X11);
border-radius: var(--radius-sm);
}
diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue
index e91a77beab..3632975048 100644
--- a/packages/frontend/src/widgets/WidgetFederation.vue
+++ b/packages/frontend/src/widgets/WidgetFederation.vue
@@ -105,7 +105,7 @@ defineExpose<WidgetComponentExpose>({
display: flex;
align-items: center;
padding: 14px 16px;
- border-bottom: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
> img {
display: block;
@@ -120,7 +120,7 @@ defineExpose<WidgetComponentExpose>({
flex: 1;
overflow: hidden;
font-size: 0.9em;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
padding-right: 8px;
> .a {
diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue
index edf6622a13..0ee6b863dc 100644
--- a/packages/frontend/src/widgets/WidgetJobQueue.vue
+++ b/packages/frontend/src/widgets/WidgetJobQueue.vue
@@ -173,14 +173,14 @@ defineExpose<WidgetComponentExpose>({
padding: 16px;
&:not(:first-child) {
- border-top: solid 0.5px var(--divider);
+ border-top: solid 0.5px var(--MI_THEME-divider);
}
> .label {
display: flex;
> .icon {
- color: var(--warn);
+ color: var(--MI_THEME-warn);
margin-left: auto;
animation: warnBlink 1s infinite;
}
@@ -198,11 +198,11 @@ defineExpose<WidgetComponentExpose>({
> div:last-child {
&.inc {
- color: var(--warn);
+ color: var(--MI_THEME-warn);
}
&.dec {
- color: var(--success);
+ color: var(--MI_THEME-success);
}
}
}
diff --git a/packages/frontend/src/widgets/WidgetMemo.vue b/packages/frontend/src/widgets/WidgetMemo.vue
index ee89beb944..82e03c1c65 100644
--- a/packages/frontend/src/widgets/WidgetMemo.vue
+++ b/packages/frontend/src/widgets/WidgetMemo.vue
@@ -84,10 +84,10 @@ defineExpose<WidgetComponentExpose>({
max-width: 100%;
min-width: 100%;
padding: 16px;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
background: transparent;
border: none;
- border-bottom: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
border-radius: 0;
box-sizing: border-box;
font: inherit;
diff --git a/packages/frontend/src/widgets/WidgetOnlineUsers.vue b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
index d56ee96ac1..d8c4e259c8 100644
--- a/packages/frontend/src/widgets/WidgetOnlineUsers.vue
+++ b/packages/frontend/src/widgets/WidgetOnlineUsers.vue
@@ -72,6 +72,6 @@ defineExpose<WidgetComponentExpose>({
}
.text {
- color: var(--fgTransparentWeak);
+ color: var(--MI_THEME-fgTransparentWeak);
}
</style>
diff --git a/packages/frontend/src/widgets/WidgetRss.vue b/packages/frontend/src/widgets/WidgetRss.vue
index 511777a570..3e43687709 100644
--- a/packages/frontend/src/widgets/WidgetRss.vue
+++ b/packages/frontend/src/widgets/WidgetRss.vue
@@ -113,7 +113,7 @@ defineExpose<WidgetComponentExpose>({
.item {
display: block;
padding: 8px 16px;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index b393ecd74b..4f594b720f 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -171,7 +171,7 @@ defineExpose<WidgetComponentExpose>({
display: inline-flex;
align-items: center;
vertical-align: bottom;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
}
.divider {
@@ -179,6 +179,6 @@ defineExpose<WidgetComponentExpose>({
width: 0.5px;
height: 16px;
margin: 0 1em;
- background: var(--divider);
+ background: var(--MI_THEME-divider);
}
</style>
diff --git a/packages/frontend/src/widgets/WidgetTrends.vue b/packages/frontend/src/widgets/WidgetTrends.vue
index a41db513e8..47a4efc106 100644
--- a/packages/frontend/src/widgets/WidgetTrends.vue
+++ b/packages/frontend/src/widgets/WidgetTrends.vue
@@ -91,13 +91,13 @@ defineExpose<WidgetComponentExpose>({
display: flex;
align-items: center;
padding: 14px 16px;
- border-bottom: solid 0.5px var(--divider);
+ border-bottom: solid 0.5px var(--MI_THEME-divider);
> .tag {
flex: 1;
overflow: hidden;
font-size: 0.9em;
- color: var(--fg);
+ color: var(--MI_THEME-fg);
> .a {
display: block;
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index f8bd433335..b9cb30999f 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -109,6 +109,11 @@ export function getConfig(): UserConfig {
return shortId + '-' + toBase62(hash(id)).substring(0, 4);
},
},
+ preprocessorOptions: {
+ scss: {
+ api: 'modern-compiler',
+ },
+ },
},
define: {
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 5af1a4112f..4109be3766 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -4,7 +4,9 @@
```ts
+import type { AuthenticationResponseJSON } from '@simplewebauthn/types';
import { EventEmitter } from 'eventemitter3';
+import type { PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
// Warning: (ae-forgotten-export) The symbol "components" needs to be exported by the entry point index.d.ts
//
@@ -218,6 +220,9 @@ type AdminFederationRemoveAllFollowingRequest = operations['admin___federation__
type AdminFederationUpdateInstanceRequest = operations['admin___federation___update-instance']['requestBody']['content']['application/json'];
// @public (undocumented)
+type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
type AdminGetIndexStatsResponse = operations['admin___get-index-stats']['responses']['200']['content']['application/json'];
// @public (undocumented)
@@ -395,6 +400,9 @@ type AdminUnsilenceUserRequest = operations['admin___unsilence-user']['requestBo
type AdminUnsuspendUserRequest = operations['admin___unsuspend-user']['requestBody']['content']['application/json'];
// @public (undocumented)
+type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
type AdminUpdateMetaRequest = operations['admin___update-meta']['requestBody']['content']['application/json'];
// @public (undocumented)
@@ -1190,13 +1198,25 @@ export type Endpoints = Overwrite<Endpoints_2, {
req: SignupPendingRequest;
res: SignupPendingResponse;
};
- 'signin': {
- req: SigninRequest;
- res: SigninResponse;
+ 'signin-flow': {
+ req: SigninFlowRequest;
+ res: SigninFlowResponse;
};
'signin-with-passkey': {
req: SigninWithPasskeyRequest;
- res: SigninWithPasskeyResponse;
+ res: {
+ $switch: {
+ $cases: [
+ [
+ {
+ context: string;
+ },
+ SigninWithPasskeyResponse
+ ]
+ ];
+ $default: SigninWithPasskeyInitResponse;
+ };
+ };
};
'admin/roles/create': {
req: Overwrite<AdminRolesCreateRequest, {
@@ -1228,10 +1248,11 @@ declare namespace entities {
SignupResponse,
SignupPendingRequest,
SignupPendingResponse,
- SigninRequest,
+ SigninFlowRequest,
+ SigninFlowResponse,
SigninWithPasskeyRequest,
+ SigninWithPasskeyInitResponse,
SigninWithPasskeyResponse,
- SigninResponse,
PartialRolePolicyOverride,
EmptyRequest,
EmptyResponse,
@@ -1317,6 +1338,8 @@ declare namespace entities {
AdminResetPasswordRequest,
AdminResetPasswordResponse,
AdminResolveAbuseUserReportRequest,
+ AdminForwardAbuseUserReportRequest,
+ AdminUpdateAbuseUserReportRequest,
AdminSendEmailRequest,
AdminServerInfoResponse,
AdminShowModerationLogsRequest,
@@ -1719,6 +1742,7 @@ declare namespace entities {
FlashCreateRequest,
FlashCreateResponse,
FlashDeleteRequest,
+ FlashFeaturedRequest,
FlashFeaturedResponse,
FlashLikeRequest,
FlashShowRequest,
@@ -1970,6 +1994,9 @@ type FlashCreateResponse = operations['flash___create']['responses']['200']['con
type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
// @public (undocumented)
+type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
+
+// @public (undocumented)
type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
// @public (undocumented)
@@ -2601,6 +2628,12 @@ type ModerationLog = {
type: 'resolveAbuseReport';
info: ModerationLogPayloads['resolveAbuseReport'];
} | {
+ type: 'forwardAbuseReport';
+ info: ModerationLogPayloads['forwardAbuseReport'];
+} | {
+ type: 'updateAbuseReportNote';
+ info: ModerationLogPayloads['updateAbuseReportNote'];
+} | {
type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar'];
} | {
@@ -3126,29 +3159,48 @@ type ServerStatsLog = ServerStats[];
type Signin = components['schemas']['Signin'];
// @public (undocumented)
-type SigninRequest = {
+type SigninFlowRequest = {
username: string;
- password: string;
+ password?: string;
token?: string;
+ credential?: AuthenticationResponseJSON;
+ 'hcaptcha-response'?: string | null;
+ 'g-recaptcha-response'?: string | null;
+ 'turnstile-response'?: string | null;
+ 'm-captcha-response'?: string | null;
};
// @public (undocumented)
-type SigninResponse = {
+type SigninFlowResponse = {
+ finished: true;
id: User['id'];
i: string;
+} | {
+ finished: false;
+ next: 'captcha' | 'password' | 'totp';
+} | {
+ finished: false;
+ next: 'passkey';
+ authRequest: PublicKeyCredentialRequestOptionsJSON;
+};
+
+// @public (undocumented)
+type SigninWithPasskeyInitResponse = {
+ option: PublicKeyCredentialRequestOptionsJSON;
+ context: string;
};
// @public (undocumented)
type SigninWithPasskeyRequest = {
- credential?: object;
+ credential?: AuthenticationResponseJSON;
context?: string;
};
// @public (undocumented)
type SigninWithPasskeyResponse = {
- option?: object;
- context?: string;
- signinResponse?: SigninResponse;
+ signinResponse: SigninFlowResponse & {
+ finished: true;
+ };
};
// @public (undocumented)
@@ -3172,6 +3224,7 @@ type SignupRequest = {
'hcaptcha-response'?: string | null;
'g-recaptcha-response'?: string | null;
'turnstile-response'?: string | null;
+ 'm-captcha-response'?: string | null;
};
// @public (undocumented)
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 05cda46574..04deccffd6 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2024.9.1",
+ "version": "2024.10.3",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
@@ -57,6 +57,7 @@
"built"
],
"dependencies": {
+ "@simplewebauthn/types": "10.0.0",
"eventemitter3": "5.0.1",
"reconnecting-websocket": "4.4.0"
}
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index 4c3f2e1578..838949f8e1 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -3,8 +3,9 @@ import { UserDetailed } from './autogen/models.js';
import { AdminRolesCreateRequest, AdminRolesCreateResponse, UsersShowRequest } from './autogen/entities.js';
import {
PartialRolePolicyOverride,
- SigninRequest,
- SigninResponse,
+ SigninFlowRequest,
+ SigninFlowResponse,
+ SigninWithPasskeyInitResponse,
SigninWithPasskeyRequest,
SigninWithPasskeyResponse,
SignupPendingRequest,
@@ -80,14 +81,26 @@ export type Endpoints = Overwrite<
res: SignupPendingResponse;
},
// api.jsonã«ã¯è¼‰ã›ãªã„ã‚‚ã®ãªã®ã§ã“ã“ã§å®šç¾©
- 'signin': {
- req: SigninRequest;
- res: SigninResponse;
+ 'signin-flow': {
+ req: SigninFlowRequest;
+ res: SigninFlowResponse;
},
'signin-with-passkey': {
req: SigninWithPasskeyRequest;
- res: SigninWithPasskeyResponse;
- }
+ res: {
+ $switch: {
+ $cases: [
+ [
+ {
+ context: string;
+ },
+ SigninWithPasskeyResponse,
+ ],
+ ];
+ $default: SigninWithPasskeyInitResponse;
+ },
+ },
+ },
'admin/roles/create': {
req: Overwrite<AdminRolesCreateRequest, { policies: PartialRolePolicyOverride }>;
res: AdminRolesCreateResponse;
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index 3fa67b7990..b825476cd5 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -694,6 +694,28 @@ declare module '../api.js' {
/**
* No description provided.
*
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ */
+ request<E extends 'admin/forward-abuse-user-report', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ */
+ request<E extends 'admin/update-abuse-user-report', P extends Endpoints[E]['req']>(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise<SwitchCaseResponseType<E, P>>;
+
+ /**
+ * No description provided.
+ *
* **Credential required**: *Yes* / **Permission**: *write:admin:send-email*
*/
request<E extends 'admin/send-email', P extends Endpoints[E]['req']>(
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 609c9f99d8..3dcdaadfe8 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -83,6 +83,8 @@ import type {
AdminResetPasswordRequest,
AdminResetPasswordResponse,
AdminResolveAbuseUserReportRequest,
+ AdminForwardAbuseUserReportRequest,
+ AdminUpdateAbuseUserReportRequest,
AdminSendEmailRequest,
AdminServerInfoResponse,
AdminShowModerationLogsRequest,
@@ -485,6 +487,7 @@ import type {
FlashCreateRequest,
FlashCreateResponse,
FlashDeleteRequest,
+ FlashFeaturedRequest,
FlashFeaturedResponse,
FlashLikeRequest,
FlashShowRequest,
@@ -659,6 +662,8 @@ export type Endpoints = {
'admin/relays/remove': { req: AdminRelaysRemoveRequest; res: EmptyResponse };
'admin/reset-password': { req: AdminResetPasswordRequest; res: AdminResetPasswordResponse };
'admin/resolve-abuse-user-report': { req: AdminResolveAbuseUserReportRequest; res: EmptyResponse };
+ 'admin/forward-abuse-user-report': { req: AdminForwardAbuseUserReportRequest; res: EmptyResponse };
+ 'admin/update-abuse-user-report': { req: AdminUpdateAbuseUserReportRequest; res: EmptyResponse };
'admin/send-email': { req: AdminSendEmailRequest; res: EmptyResponse };
'admin/server-info': { req: EmptyRequest; res: AdminServerInfoResponse };
'admin/show-moderation-logs': { req: AdminShowModerationLogsRequest; res: AdminShowModerationLogsResponse };
@@ -926,7 +931,7 @@ export type Endpoints = {
'pages/update': { req: PagesUpdateRequest; res: EmptyResponse };
'flash/create': { req: FlashCreateRequest; res: FlashCreateResponse };
'flash/delete': { req: FlashDeleteRequest; res: EmptyResponse };
- 'flash/featured': { req: EmptyRequest; res: FlashFeaturedResponse };
+ 'flash/featured': { req: FlashFeaturedRequest; res: FlashFeaturedResponse };
'flash/like': { req: FlashLikeRequest; res: EmptyResponse };
'flash/show': { req: FlashShowRequest; res: FlashShowResponse };
'flash/unlike': { req: FlashUnlikeRequest; res: EmptyResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 999dd4dd54..e4ae018338 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -86,6 +86,8 @@ export type AdminRelaysRemoveRequest = operations['admin___relays___remove']['re
export type AdminResetPasswordRequest = operations['admin___reset-password']['requestBody']['content']['application/json'];
export type AdminResetPasswordResponse = operations['admin___reset-password']['responses']['200']['content']['application/json'];
export type AdminResolveAbuseUserReportRequest = operations['admin___resolve-abuse-user-report']['requestBody']['content']['application/json'];
+export type AdminForwardAbuseUserReportRequest = operations['admin___forward-abuse-user-report']['requestBody']['content']['application/json'];
+export type AdminUpdateAbuseUserReportRequest = operations['admin___update-abuse-user-report']['requestBody']['content']['application/json'];
export type AdminSendEmailRequest = operations['admin___send-email']['requestBody']['content']['application/json'];
export type AdminServerInfoResponse = operations['admin___server-info']['responses']['200']['content']['application/json'];
export type AdminShowModerationLogsRequest = operations['admin___show-moderation-logs']['requestBody']['content']['application/json'];
@@ -488,6 +490,7 @@ export type PagesUpdateRequest = operations['pages___update']['requestBody']['co
export type FlashCreateRequest = operations['flash___create']['requestBody']['content']['application/json'];
export type FlashCreateResponse = operations['flash___create']['responses']['200']['content']['application/json'];
export type FlashDeleteRequest = operations['flash___delete']['requestBody']['content']['application/json'];
+export type FlashFeaturedRequest = operations['flash___featured']['requestBody']['content']['application/json'];
export type FlashFeaturedResponse = operations['flash___featured']['responses']['200']['content']['application/json'];
export type FlashLikeRequest = operations['flash___like']['requestBody']['content']['application/json'];
export type FlashShowRequest = operations['flash___show']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index e275e4b475..eb727edb25 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -576,6 +576,24 @@ export type paths = {
*/
post: operations['admin___resolve-abuse-user-report'];
};
+ '/admin/forward-abuse-user-report': {
+ /**
+ * admin/forward-abuse-user-report
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ */
+ post: operations['admin___forward-abuse-user-report'];
+ };
+ '/admin/update-abuse-user-report': {
+ /**
+ * admin/update-abuse-user-report
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ */
+ post: operations['admin___update-abuse-user-report'];
+ };
'/admin/send-email': {
/**
* admin/send-email
@@ -3959,16 +3977,13 @@ export type components = {
followingVisibility: 'public' | 'followers' | 'private';
/** @enum {string} */
followersVisibility: 'public' | 'followers' | 'private';
- /** @default false */
- twoFactorEnabled: boolean;
- /** @default false */
- usePasswordLessLogin: boolean;
- /** @default false */
- securityKeys: boolean;
roles: components['schemas']['RoleLite'][];
followedMessage?: string | null;
memo: string | null;
moderationNote?: string;
+ twoFactorEnabled?: boolean;
+ usePasswordLessLogin?: boolean;
+ securityKeys?: boolean;
isFollowing?: boolean;
isFollowed?: boolean;
hasPendingFollowRequestFromYou?: boolean;
@@ -4153,6 +4168,12 @@ export type components = {
}[];
loggedInDays: number;
policies: components['schemas']['RolePolicies'];
+ /** @default false */
+ twoFactorEnabled: boolean;
+ /** @default false */
+ usePasswordLessLogin: boolean;
+ /** @default false */
+ securityKeys: boolean;
email?: string | null;
emailVerified?: boolean | null;
securityKeysList?: {
@@ -4469,7 +4490,14 @@ export type components = {
exportedEntity: 'antenna' | 'blocking' | 'clip' | 'customEmoji' | 'favorite' | 'following' | 'muting' | 'note' | 'userList';
/** Format: id */
fileId: string;
- }) | ({
+ }) | {
+ /** Format: id */
+ id: string;
+ /** Format: date-time */
+ createdAt: string;
+ /** @enum {string} */
+ type: 'login';
+ } | ({
/** Format: id */
id: string;
/** Format: date-time */
@@ -5145,6 +5173,7 @@ export type components = {
enableFC: boolean;
fcSiteKey: string | null;
enableAchievements: boolean | null;
+ enableTestcaptcha: boolean;
swPublickey: string | null;
/** @default /assets/ai.png */
mascotImageUrl: string;
@@ -5227,7 +5256,7 @@ export type components = {
latestSentAt: string | null;
latestStatus: number | null;
name: string;
- on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+ on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
url: string;
secret: string;
};
@@ -5285,6 +5314,7 @@ export type operations = {
turnstileSiteKey: string | null;
enableFC: boolean;
fcSiteKey: string | null;
+ enableTestcaptcha: boolean;
swPublickey: string | null;
/** @default /assets/ai.png */
mascotImageUrl: string | null;
@@ -5306,6 +5336,7 @@ export type operations = {
blockedHosts: string[];
sensitiveWords: string[];
prohibitedWords: string[];
+ prohibitedWordsForNameOfUser: string[];
bannedEmailDomains?: string[];
preservedUsernames: string[];
bubbleInstances: string[];
@@ -5349,6 +5380,7 @@ export type operations = {
truemailAuthKey: string | null;
enableChartsForRemoteUser: boolean;
enableChartsForFederatedInstances: boolean;
+ enableStatsForFederatedInstances: boolean;
enableServerMachineStats: boolean;
enableAchievements: boolean;
enableIdenticonGeneration: boolean;
@@ -5462,8 +5494,6 @@ export type operations = {
* @enum {string}
*/
targetUserOrigin?: 'combined' | 'local' | 'remote';
- /** @default false */
- forwarded?: boolean;
};
};
};
@@ -5490,7 +5520,11 @@ export type operations = {
assigneeId: string | null;
reporter: components['schemas']['UserDetailedNotMe'];
targetUser: components['schemas']['UserDetailedNotMe'];
- assignee?: components['schemas']['UserDetailedNotMe'] | null;
+ assignee: components['schemas']['UserDetailedNotMe'] | null;
+ forwarded: boolean;
+ /** @enum {string|null} */
+ resolvedAs: 'accept' | 'reject' | null;
+ moderationNote: string;
})[];
};
};
@@ -5824,6 +5858,7 @@ export type operations = {
'application/json': {
username: string;
password: string;
+ setupPassword?: string | null;
};
};
};
@@ -8906,8 +8941,113 @@ export type operations = {
'application/json': {
/** Format: misskey:id */
reportId: string;
- /** @default false */
- forward?: boolean;
+ /** @enum {string|null} */
+ resolvedAs?: 'accept' | 'reject' | null;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/forward-abuse-user-report
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ */
+ 'admin___forward-abuse-user-report': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ reportId: string;
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
+ /**
+ * admin/update-abuse-user-report
+ * @description No description provided.
+ *
+ * **Credential required**: *Yes* / **Permission**: *write:admin:resolve-abuse-user-report*
+ */
+ 'admin___update-abuse-user-report': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ reportId: string;
+ moderationNote?: string;
};
};
};
@@ -9861,6 +10001,7 @@ export type operations = {
blockedHosts?: string[] | null;
sensitiveWords?: string[] | null;
prohibitedWords?: string[] | null;
+ prohibitedWordsForNameOfUser?: string[] | null;
themeColor?: string | null;
mascotImageUrl?: string | null;
bannerUrl?: string | null;
@@ -9899,6 +10040,7 @@ export type operations = {
enableFC?: boolean;
fcSiteKey?: string | null;
fcSecretKey?: string | null;
+ enableTestcaptcha?: boolean;
/** @enum {string} */
sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote';
/** @enum {string} */
@@ -9954,6 +10096,7 @@ export type operations = {
truemailAuthKey?: string | null;
enableChartsForRemoteUser?: boolean;
enableChartsForFederatedInstances?: boolean;
+ enableStatsForFederatedInstances?: boolean;
enableServerMachineStats?: boolean;
enableAchievements?: boolean;
enableIdenticonGeneration?: boolean;
@@ -10657,7 +10800,7 @@ export type operations = {
'application/json': {
isActive: boolean;
name: string;
- on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+ on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
url: string;
secret: string;
};
@@ -10767,7 +10910,7 @@ export type operations = {
content: {
'application/json': {
isActive?: boolean;
- on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+ on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
};
};
};
@@ -10880,7 +11023,7 @@ export type operations = {
id: string;
isActive: boolean;
name: string;
- on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[];
+ on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[];
url: string;
secret: string;
};
@@ -10939,7 +11082,7 @@ export type operations = {
/** Format: misskey:id */
webhookId: string;
/** @enum {string} */
- type: 'abuseReport' | 'abuseReportResolved' | 'userCreated';
+ type: 'abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged';
override?: {
url?: string;
secret?: string;
@@ -25057,6 +25200,16 @@ export type operations = {
* **Credential required**: *No*
*/
flash___featured: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** @default 0 */
+ offset?: number;
+ /** @default 10 */
+ limit?: number;
+ };
+ };
+ };
responses: {
/** @description OK (with results) */
200: {
@@ -29056,4 +29209,3 @@ export type operations = {
};
};
};
-
diff --git a/packages/misskey-js/src/consts.ts b/packages/misskey-js/src/consts.ts
index c99b8f5570..61fe6ba12a 100644
--- a/packages/misskey-js/src/consts.ts
+++ b/packages/misskey-js/src/consts.ts
@@ -149,6 +149,8 @@ export const moderationLogTypes = [
'markSensitiveDriveFile',
'unmarkSensitiveDriveFile',
'resolveAbuseReport',
+ 'forwardAbuseReport',
+ 'updateAbuseReportNote',
'createInvitation',
'createAd',
'updateAd',
@@ -347,7 +349,18 @@ export type ModerationLogPayloads = {
resolveAbuseReport: {
reportId: string;
report: ReceivedAbuseReport;
- forwarded: boolean;
+ forwarded?: boolean;
+ resolvedAs?: string | null;
+ };
+ forwardAbuseReport: {
+ reportId: string;
+ report: ReceivedAbuseReport;
+ };
+ updateAbuseReportNote: {
+ reportId: string;
+ report: ReceivedAbuseReport;
+ before: string;
+ after: string;
};
createInvitation: {
invitations: InviteCode[];
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index f85574d9d5..efe5ba19fb 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -10,6 +10,7 @@ import {
User,
UserDetailedNotMe,
} from './autogen/models.js';
+import type { AuthenticationResponseJSON, PublicKeyCredentialRequestOptionsJSON } from '@simplewebauthn/types';
export * from './autogen/entities.js';
export * from './autogen/models.js';
@@ -159,6 +160,12 @@ export type ModerationLog = {
type: 'resolveAbuseReport';
info: ModerationLogPayloads['resolveAbuseReport'];
} | {
+ type: 'forwardAbuseReport';
+ info: ModerationLogPayloads['forwardAbuseReport'];
+} | {
+ type: 'updateAbuseReportNote';
+ info: ModerationLogPayloads['updateAbuseReportNote'];
+} | {
type: 'unsetUserAvatar';
info: ModerationLogPayloads['unsetUserAvatar'];
} | {
@@ -256,6 +263,7 @@ export type SignupRequest = {
'hcaptcha-response'?: string | null;
'g-recaptcha-response'?: string | null;
'turnstile-response'?: string | null;
+ 'm-captcha-response'?: string | null;
}
export type SignupResponse = MeDetailed & {
@@ -271,26 +279,42 @@ export type SignupPendingResponse = {
i: string,
};
-export type SigninRequest = {
+export type SigninFlowRequest = {
username: string;
- password: string;
+ password?: string;
token?: string;
+ credential?: AuthenticationResponseJSON;
+ 'hcaptcha-response'?: string | null;
+ 'g-recaptcha-response'?: string | null;
+ 'turnstile-response'?: string | null;
+ 'm-captcha-response'?: string | null;
+};
+
+export type SigninFlowResponse = {
+ finished: true;
+ id: User['id'];
+ i: string;
+} | {
+ finished: false;
+ next: 'captcha' | 'password' | 'totp';
+} | {
+ finished: false;
+ next: 'passkey';
+ authRequest: PublicKeyCredentialRequestOptionsJSON;
};
export type SigninWithPasskeyRequest = {
- credential?: object;
+ credential?: AuthenticationResponseJSON;
context?: string;
};
-export type SigninWithPasskeyResponse = {
- option?: object;
- context?: string;
- signinResponse?: SigninResponse;
+export type SigninWithPasskeyInitResponse = {
+ option: PublicKeyCredentialRequestOptionsJSON;
+ context: string;
};
-export type SigninResponse = {
- id: User['id'],
- i: string,
+export type SigninWithPasskeyResponse = {
+ signinResponse: SigninFlowResponse & { finished: true };
};
type Values<T extends Record<PropertyKey, unknown>> = T[keyof T];
diff --git a/packages/shared/eslint.config.js b/packages/shared/eslint.config.js
index e9d27c4a72..0368d008c0 100644
--- a/packages/shared/eslint.config.js
+++ b/packages/shared/eslint.config.js
@@ -6,6 +6,7 @@ export default [
{
files: ['**/*.cjs'],
languageOptions: {
+ sourceType: 'commonjs',
parserOptions: {
sourceType: 'commonjs',
},
@@ -25,4 +26,10 @@ export default [
globals: globals.node,
},
},
+ {
+ files: ['**/*.js', '**/*.cjs'],
+ rules: {
+ '@typescript-eslint/no-var-requires': 'off',
+ },
+ },
];
diff --git a/packages/sw/src/scripts/create-notification.ts b/packages/sw/src/scripts/create-notification.ts
index 9c56e338c7..7ec575721d 100644
--- a/packages/sw/src/scripts/create-notification.ts
+++ b/packages/sw/src/scripts/create-notification.ts
@@ -210,6 +210,12 @@ async function composeNotification(data: PushNotificationDataMap[keyof PushNotif
tag: `achievement:${data.body.achievement}`,
}];
+ case 'login':
+ return [i18n.ts._notification.login, {
+ badge: iconUrl('login-2'),
+ data,
+ }];
+
case 'exportCompleted': {
const entityName = {
antenna: i18n.ts.antennas,
diff --git a/packages/sw/src/types.ts b/packages/sw/src/types.ts
index fac3e707d8..4f82779808 100644
--- a/packages/sw/src/types.ts
+++ b/packages/sw/src/types.ts
@@ -50,4 +50,5 @@ export type BadgeNames =
| 'quote'
| 'repeat'
| 'user-plus'
- | 'users';
+ | 'users'
+ | 'login-2';
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index bda23dfd32..47416fd0ee 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -48,7 +48,7 @@ importers:
devDependencies:
'@misskey-dev/eslint-plugin':
specifier: 2.0.3
- version: 2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)
+ version: 2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)
'@types/node':
specifier: 20.14.12
version: 20.14.12
@@ -98,29 +98,29 @@ importers:
specifier: 15.1.0
version: 15.1.0
'@fastify/accepts':
- specifier: 5.0.0
- version: 5.0.0
+ specifier: 5.0.1
+ version: 5.0.1
'@fastify/cookie':
- specifier: 10.0.0
- version: 10.0.0
+ specifier: 10.0.1
+ version: 10.0.1
'@fastify/cors':
- specifier: 10.0.0
- version: 10.0.0
+ specifier: 10.0.1
+ version: 10.0.1
'@fastify/express':
- specifier: 4.0.0
- version: 4.0.0
+ specifier: 4.0.1
+ version: 4.0.1
'@fastify/http-proxy':
specifier: 10.0.0
version: 10.0.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
'@fastify/multipart':
- specifier: 9.0.0
- version: 9.0.0
+ specifier: 9.0.1
+ version: 9.0.1
'@fastify/static':
- specifier: 8.0.0
- version: 8.0.0
+ specifier: 8.0.1
+ version: 8.0.1
'@fastify/view':
- specifier: 10.0.0
- version: 10.0.0
+ specifier: 10.0.1
+ version: 10.0.1
'@misskey-dev/sharp-read-bmp':
specifier: 1.2.0
version: 1.2.0
@@ -131,14 +131,14 @@ importers:
specifier: 0.1.56
version: 0.1.56
'@nestjs/common':
- specifier: 10.4.3
- version: 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ specifier: 10.4.4
+ version: 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/core':
- specifier: 10.4.3
- version: 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ specifier: 10.4.4
+ version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nestjs/testing':
- specifier: 10.4.3
- version: 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3))
+ specifier: 10.4.4
+ version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4))
'@peertube/http-signature':
specifier: 1.7.0
version: 1.7.0
@@ -194,8 +194,8 @@ importers:
specifier: 1.20.3
version: 1.20.3
bullmq:
- specifier: 5.13.2
- version: 5.13.2
+ specifier: 5.15.0
+ version: 5.15.0
cacheable-lookup:
specifier: 7.0.0
version: 7.0.0
@@ -344,8 +344,8 @@ importers:
specifier: 0.0.14
version: 0.0.14
otpauth:
- specifier: 9.3.2
- version: 9.3.2
+ specifier: 9.3.4
+ version: 9.3.4
parse5:
specifier: 7.1.2
version: 7.1.2
@@ -398,8 +398,8 @@ importers:
specifier: 7.8.1
version: 7.8.1
sanitize-html:
- specifier: 2.13.0
- version: 2.13.0
+ specifier: 2.13.1
+ version: 2.13.1
secure-json-parse:
specifier: 2.7.0
version: 2.7.0
@@ -541,8 +541,8 @@ importers:
specifier: 29.7.0
version: 29.7.0
'@nestjs/platform-express':
- specifier: 10.4.3
- version: 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3)
+ specifier: 10.4.4
+ version: 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)
'@simplewebauthn/types':
specifier: 10.0.0
version: 10.0.0
@@ -562,8 +562,8 @@ importers:
specifier: 1.19.5
version: 1.19.5
'@types/color-convert':
- specifier: 2.0.3
- version: 2.0.3
+ specifier: 2.0.4
+ version: 2.0.4
'@types/content-disposition':
specifier: 0.5.8
version: 0.5.8
@@ -740,10 +740,10 @@ importers:
version: 15.1.1
'@vitejs/plugin-vue':
specifier: 5.1.4
- version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))
+ version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))
'@vue/compiler-sfc':
- specifier: 3.5.10
- version: 3.5.10
+ specifier: 3.5.11
+ version: 3.5.11
aiscript-vscode:
specifier: github:aiscript-dev/aiscript-vscode#v0.1.11
version: https://codeload.github.com/aiscript-dev/aiscript-vscode/tar.gz/e1e1b27f2f72cd28a473e004b6da0d8fc0bd40d9
@@ -775,8 +775,8 @@ importers:
specifier: 2.0.1
version: 2.0.1(chart.js@4.4.4)
chromatic:
- specifier: 11.10.4
- version: 11.10.4
+ specifier: 11.11.0
+ version: 11.11.0
compare-versions:
specifier: 6.1.1
version: 6.1.1
@@ -832,14 +832,14 @@ importers:
specifier: 4.22.5
version: 4.22.5
sanitize-html:
- specifier: 2.13.0
- version: 2.13.0
+ specifier: 2.13.1
+ version: 2.13.1
sass:
specifier: 1.79.3
version: 1.79.3
shiki:
- specifier: 1.12.0
- version: 1.12.0
+ specifier: 1.21.0
+ version: 1.21.0
strict-event-emitter-types:
specifier: 2.0.0
version: 2.0.0
@@ -869,77 +869,80 @@ importers:
version: 10.0.0
v-code-diff:
specifier: 1.13.1
- version: 1.13.1(vue@3.5.10(typescript@5.6.2))
+ version: 1.13.1(vue@3.5.11(typescript@5.6.2))
vite:
specifier: 5.4.8
version: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
vue:
- specifier: 3.5.10
- version: 3.5.10(typescript@5.6.2)
+ specifier: 3.5.11
+ version: 3.5.11(typescript@5.6.2)
vuedraggable:
specifier: next
- version: 4.1.0(vue@3.5.10(typescript@5.6.2))
+ version: 4.1.0(vue@3.5.11(typescript@5.6.2))
devDependencies:
'@misskey-dev/summaly':
specifier: 5.1.0
version: 5.1.0
'@storybook/addon-actions':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/addon-essentials':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/addon-interactions':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/addon-links':
- specifier: 8.3.3
- version: 8.3.3(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/addon-mdx-gfm':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/addon-storysource':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/blocks':
- specifier: 8.3.3
- version: 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/components':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/core-events':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/manager-api':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/preview-api':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/react':
- specifier: 8.3.3
- version: 8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)
+ specifier: 8.3.4
+ version: 8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)
'@storybook/react-vite':
- specifier: 8.3.3
- version: 8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
+ specifier: 8.3.4
+ version: 8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
'@storybook/test':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/theming':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/types':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/vue3':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.10(typescript@5.6.2))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.11(typescript@5.6.2))
'@storybook/vue3-vite':
- specifier: 8.3.3
- version: 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))
+ specifier: 8.3.4
+ version: 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))
'@testing-library/vue':
specifier: 8.1.0
- version: 8.1.0(@vue/compiler-sfc@3.5.10)(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))
+ version: 8.1.0(@vue/compiler-sfc@3.5.11)(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))
+ '@types/canvas-confetti':
+ specifier: ^1.6.4
+ version: 1.6.4
'@types/estree':
specifier: 1.0.6
version: 1.0.6
@@ -986,8 +989,8 @@ importers:
specifier: 1.6.0
version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0))
'@vue/runtime-core':
- specifier: 3.5.10
- version: 3.5.10
+ specifier: 3.5.11
+ version: 3.5.11
acorn:
specifier: 8.12.1
version: 8.12.1
@@ -998,8 +1001,8 @@ importers:
specifier: 13.15.0
version: 13.15.0
eslint-plugin-import:
- specifier: 2.30.0
- version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)
+ specifier: 2.31.0
+ version: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)
eslint-plugin-vue:
specifier: 9.28.0
version: 9.28.0(eslint@9.8.0)
@@ -1040,11 +1043,11 @@ importers:
specifier: 2.0.8
version: 2.0.8
storybook:
- specifier: 8.3.3
- version: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ specifier: 8.3.4
+ version: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
storybook-addon-misskey-theme:
specifier: github:misskey-dev/storybook-addon-misskey-theme
- version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ version: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
vite-plugin-turbosnap:
specifier: 1.0.3
version: 1.0.3
@@ -1089,10 +1092,10 @@ importers:
version: 15.1.1
'@vitejs/plugin-vue':
specifier: 5.1.4
- version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))
+ version: 5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))
'@vue/compiler-sfc':
- specifier: 3.5.10
- version: 3.5.10
+ specifier: 3.5.11
+ version: 3.5.11
astring:
specifier: 1.9.0
version: 1.9.0
@@ -1118,11 +1121,11 @@ importers:
specifier: 4.22.5
version: 4.22.5
sass:
- specifier: 1.79.3
- version: 1.79.3
+ specifier: 1.79.4
+ version: 1.79.4
shiki:
- specifier: 1.12.0
- version: 1.12.0
+ specifier: 1.21.0
+ version: 1.21.0
tinycolor2:
specifier: 1.6.0
version: 1.6.0
@@ -1140,17 +1143,17 @@ importers:
version: 10.0.0
vite:
specifier: 5.4.8
- version: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
+ version: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
vue:
- specifier: 3.5.10
- version: 3.5.10(typescript@5.6.2)
+ specifier: 3.5.11
+ version: 3.5.11(typescript@5.6.2)
devDependencies:
'@misskey-dev/summaly':
specifier: 5.1.0
version: 5.1.0
'@testing-library/vue':
specifier: 8.1.0
- version: 8.1.0(@vue/compiler-sfc@3.5.10)(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))
+ version: 8.1.0(@vue/compiler-sfc@3.5.11)(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))
'@types/estree':
specifier: 1.0.6
version: 1.0.6
@@ -1180,10 +1183,10 @@ importers:
version: 7.17.0(eslint@9.8.0)(typescript@5.6.2)
'@vitest/coverage-v8':
specifier: 1.6.0
- version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0))
+ version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0))
'@vue/runtime-core':
- specifier: 3.5.10
- version: 3.5.10
+ specifier: 3.5.11
+ version: 3.5.11
acorn:
specifier: 8.12.1
version: 8.12.1
@@ -1191,8 +1194,8 @@ importers:
specifier: 7.0.3
version: 7.0.3
eslint-plugin-import:
- specifier: 2.30.0
- version: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)
+ specifier: 2.31.0
+ version: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)
eslint-plugin-vue:
specifier: 9.28.0
version: 9.28.0(eslint@9.8.0)
@@ -1400,6 +1403,9 @@ importers:
packages/misskey-js:
dependencies:
+ '@simplewebauthn/types':
+ specifier: 10.0.0
+ version: 10.0.0
eventemitter3:
specifier: 5.0.1
version: 5.0.1
@@ -2797,8 +2803,8 @@ packages:
'@fastify/accept-negotiator@2.0.0':
resolution: {integrity: sha512-/Sce/kBzuTxIq5tJh85nVNOq9wKD8s+viIgX0fFMDBdw95gnpf53qmF1oBgJym3cPFliWUuSloVg/1w/rH0FcQ==}
- '@fastify/accepts@5.0.0':
- resolution: {integrity: sha512-5wpgycrn+DXPkATGqUbXY9tyqLNgxo9S8f0EHUyIWvUacor2cXa3liYZggsqoyMXgpIqUbGLPBl+dN2hRcU9jQ==}
+ '@fastify/accepts@5.0.1':
+ resolution: {integrity: sha512-8ji2MGTbceSnAXKYx/U9iWt6Fmf0zJovh0meO5rpwYS/vy0Z3QIR2J/hKmbcTpYfMu5NUliNpsAtMavmzBQhmA==}
'@fastify/ajv-compiler@4.0.1':
resolution: {integrity: sha512-DxrBdgsjNLP0YM6W5Hd6/Fmj43S8zMKiFJYgi+Ri3htTGAowPVG/tG1wpnWLMjufEnehRivUCKZ1pLDIoZdTuw==}
@@ -2814,11 +2820,11 @@ packages:
'@fastify/busboy@3.0.0':
resolution: {integrity: sha512-83rnH2nCvclWaPQQKvkJ2pdOjG4TZyEVuFDnlOF6KP08lDaaceVyw/W63mDuafQT+MKHCvXIPpE5uYWeM0rT4w==}
- '@fastify/cookie@10.0.0':
- resolution: {integrity: sha512-S43spazwAfzm5nKlqq/spAGW+O6r+WQzg5vXXI1ArCXXFa8KBA/tiU3XRVQUehSNtbN5PA6+g183hzh5/dZ6Iw==}
+ '@fastify/cookie@10.0.1':
+ resolution: {integrity: sha512-NV/wbCUv4ETJ5KM1KMu0fLx0nSCm9idIxwg66NZnNbfPQH3rdbx6k0qRs5uy0y+MhBgvDudYRA30KlK659chyw==}
- '@fastify/cors@10.0.0':
- resolution: {integrity: sha512-kb9fkc/LVbLTQ3lhA+ZZjC/Styzysodo/MTCdVCvTtgHa/gBwxrEEkcp3fuoKIfAQt85wksrpXjUGbw5NQffEQ==}
+ '@fastify/cors@10.0.1':
+ resolution: {integrity: sha512-O8JIf6448uQbOgzSkCqhClw6gFTAqrdfeA6R3fc/3gwTJGUp7gl8/3tbNB+6INuu4RmgVOq99BmvdGbtu5pgOA==}
'@fastify/deepmerge@2.0.0':
resolution: {integrity: sha512-fsaybTGDyQ5KpPsplQqb9yKdCf2x/pbNpMNk8Tvp3rRz7lVcupKysH4b2ELMN2P4Hak1+UqTYdTj/u4FNV2p0g==}
@@ -2826,8 +2832,8 @@ packages:
'@fastify/error@4.0.0':
resolution: {integrity: sha512-OO/SA8As24JtT1usTUTKgGH7uLvhfwZPwlptRi2Dp5P4KKmJI3gvsZ8MIHnNwDs4sLf/aai5LzTyl66xr7qMxA==}
- '@fastify/express@4.0.0':
- resolution: {integrity: sha512-e+IMKKV9+HRCVm7LVW8PaMrpEerHfqNLpRkbiVHYfVm0xeOphiwyNEoge4VA3Sh8gubtDfo9yKkpRzx6gx63kg==}
+ '@fastify/express@4.0.1':
+ resolution: {integrity: sha512-mEQ6pawaENeZ3swqVtkxdLi8NQC5eKBkclE+7ma1qQMuB+yI6WxDyEp55pdbqPIqBQTN/cGgHv84qxVS7NKC2Q==}
'@fastify/fast-json-stringify-compiler@5.0.1':
resolution: {integrity: sha512-f2d3JExJgFE3UbdFcpPwqNUEoHWmt8pAKf8f+9YuLESdefA0WgqxeT6DrGL4Yrf/9ihXNSKOqpjEmurV405meA==}
@@ -2838,8 +2844,8 @@ packages:
'@fastify/merge-json-schemas@0.1.1':
resolution: {integrity: sha512-fERDVz7topgNjtXsJTTW1JKLy0rhuLRcquYqNR9rF7OcVpCa2OVW49ZPDIhaRRCaUuvVxI+N416xUoF76HNSXA==}
- '@fastify/multipart@9.0.0':
- resolution: {integrity: sha512-B/rzOl1wmkj4LddH2i+zR8Gke8ZX1J8D7n4uJeis5VdIa7OR9Ys/TzUxI0/h1SF9ubHlNhBP+eO/FwnftarP9w==}
+ '@fastify/multipart@9.0.1':
+ resolution: {integrity: sha512-vt2gOCw/O4EwpN4KlLVJxth4iQlDf7T5ggw2Db2C+UbO2WJBG7y0jEBvu/HT6JIW/lBYaqrrUy9MmTpCKgXEpw==}
'@fastify/reply-from@11.0.1':
resolution: {integrity: sha512-F2Qk88gcqIIiug9V+4I6WeeV1faj1Wu798JyOnwbJcjQhm4LYrHdkpFSVwJE0g1cVjYCFFmH3OVh1HHaninttQ==}
@@ -2847,15 +2853,9 @@ packages:
'@fastify/send@3.1.1':
resolution: {integrity: sha512-LdiV2mle/2tH8vh6GwGl0ubfUAgvY+9yF9oGI1iiwVyNUVOQamvw5n+OFu6iCNNoyuCY80FFURBn4TZCbTe8LA==}
- '@fastify/static@8.0.0':
- resolution: {integrity: sha512-VKGn1PQslB2VqzspyMKPu9xasF9vj+YuyGhVLb1ih6V60VVcRvcf0fFRcl3opt6c6YWwhKKdTUTfVE6COnpw6A==}
-
'@fastify/static@8.0.1':
resolution: {integrity: sha512-7idyhbcgf14v4bjWzUeHEFvnVxvNJ1n5cyGPgFtwTZjnjUQ1wgC7a2FQai7OGKqCKywDEjzbPhAZRW+uEK1LMg==}
- '@fastify/view@10.0.0':
- resolution: {integrity: sha512-2KnfgpSbAImKV5kKdNAkSyjV+9kYUYLvgDLx/wlzgqel92bN9Z520cwG3g3bAkr0yVnEJu62dIm2qAL9FASS1w==}
-
'@fastify/view@10.0.1':
resolution: {integrity: sha512-rXtBN0oVDmoRZAS7lelrCIahf+qFtlMOOas8VPdA7JvrJ9ChcF7e36pIUPU0Vbs3KmHxESUb7XatavUZEe/k5Q==}
@@ -3311,8 +3311,8 @@ packages:
resolution: {integrity: sha512-SujSchzG6lLc/wT+Mwxam/w30Kk2sFTiU6bLFcidecKSmlhenAhGMQhZh2iGFfKoh2+8iit0jrt99n6TqReICQ==}
engines: {node: '>= 10'}
- '@nestjs/common@10.4.3':
- resolution: {integrity: sha512-4hbLd3XIJubHSylYd/1WSi4VQvG68KM/ECYpMDqA3k3J1/T17SAg40sDoq3ZoO5OZgU0xuNyjuISdOTjs11qVg==}
+ '@nestjs/common@10.4.4':
+ resolution: {integrity: sha512-0j2/zqRw9nvHV1GKTktER8B/hIC/Z8CYFjN/ZqUuvwayCH+jZZBhCR2oRyuvLTXdnlSmtCAg2xvQ0ULqQvzqhA==}
peerDependencies:
class-transformer: '*'
class-validator: '*'
@@ -3324,8 +3324,8 @@ packages:
class-validator:
optional: true
- '@nestjs/core@10.4.3':
- resolution: {integrity: sha512-6OQz+5C8mT8yRtfvE5pPCq+p6w5jDot+oQku1KzQ24ABn+lay1KGuJwcKZhdVNuselx+8xhdMxknZTA8wrGLIg==}
+ '@nestjs/core@10.4.4':
+ resolution: {integrity: sha512-y9tjmAzU6LTh1cC/lWrRsCcOd80khSR0qAHAqwY2svbW+AhsR/XCzgpZrAAKJrm/dDfjLCZKyxJSayeirGcW5Q==}
peerDependencies:
'@nestjs/common': ^10.0.0
'@nestjs/microservices': ^10.0.0
@@ -3341,14 +3341,14 @@ packages:
'@nestjs/websockets':
optional: true
- '@nestjs/platform-express@10.4.3':
- resolution: {integrity: sha512-ss7gkofVm3eO+1P9iRhmGq6Xcjg+mIN3dWisKJZYelSV+msb0QpJmqChLvWjLkWtlqDnx915FKUk0IzCa0TVzw==}
+ '@nestjs/platform-express@10.4.4':
+ resolution: {integrity: sha512-y52q1MxhbHaT3vAgWd08RgiYon0lJgtTa8U6g6gV0KI0IygwZhDQFJVxnrRDUdxQGIP5CKHmfQu3sk9gTNFoEA==}
peerDependencies:
'@nestjs/common': ^10.0.0
'@nestjs/core': ^10.0.0
- '@nestjs/testing@10.4.3':
- resolution: {integrity: sha512-SBNWrMU51YAlYmW86wyjlGZ2uLnASNiOPD0lBcNIlxxei0b05/aI3nh7OPuxbXQUdedUJfPq2d2jZj4TRG4S0w==}
+ '@nestjs/testing@10.4.4':
+ resolution: {integrity: sha512-qRGFj51A5RM7JqA8pcyEwSLA3Y0dle/PAZ8oxP0suimoCusRY3Tk7wYqutZdCNj1ATb678SDaUZDHk2pwSv9/g==}
peerDependencies:
'@nestjs/common': ^10.0.0
'@nestjs/core': ^10.0.0
@@ -3360,9 +3360,9 @@ packages:
'@nestjs/platform-express':
optional: true
- '@noble/hashes@1.4.0':
- resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==}
- engines: {node: '>= 16'}
+ '@noble/hashes@1.5.0':
+ resolution: {integrity: sha512-1j6kQFb7QRru7eKN3ZDvRcP13rugwdxZqCjbiAVZfIJwgj2A65UmT4TgARXGlXgnRkORLTDTrO19ZErt7+QXgA==}
+ engines: {node: ^14.21.3 || >=16}
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
@@ -3789,8 +3789,20 @@ packages:
resolution: {integrity: sha512-+1I5H8dojURiEUGPliDwheQk8dhjp8uV1sMccR/W/zjFrt4wZyPs+Ttp/V7gzm9LDJoNek9tmELert/jQqWTgg==}
engines: {node: '>=14.18'}
- '@shikijs/core@1.12.0':
- resolution: {integrity: sha512-mc1cLbm6UQ8RxLc0dZES7v5rkH+99LxQp/ZvTqV3NLyYsO/fD6JhEflP1H5b2SDq9gI0+0G36AVZWxvounfR9w==}
+ '@shikijs/core@1.21.0':
+ resolution: {integrity: sha512-zAPMJdiGuqXpZQ+pWNezQAk5xhzRXBNiECFPcJLtUdsFM3f//G95Z15EHTnHchYycU8kIIysqGgxp8OVSj1SPQ==}
+
+ '@shikijs/engine-javascript@1.21.0':
+ resolution: {integrity: sha512-jxQHNtVP17edFW4/0vICqAVLDAxmyV31MQJL4U/Kg+heQALeKYVOWo0sMmEZ18FqBt+9UCdyqGKYE7bLRtk9mg==}
+
+ '@shikijs/engine-oniguruma@1.21.0':
+ resolution: {integrity: sha512-AIZ76XocENCrtYzVU7S4GY/HL+tgHGbVU+qhiDyNw1qgCA5OSi4B4+HY4BtAoJSMGuD/L5hfTzoRVbzEm2WTvg==}
+
+ '@shikijs/types@1.21.0':
+ resolution: {integrity: sha512-tzndANDhi5DUndBtpojEq/42+dpUF2wS7wdCDQaFtIXm3Rd1QkrcVgSSRLOvEwexekihOXfbYJINW37g96tJRw==}
+
+ '@shikijs/vscode-textmate@9.3.0':
+ resolution: {integrity: sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==}
'@sideway/address@4.1.4':
resolution: {integrity: sha512-7vwq+rOHVWjyXxVlR76Agnvhy8I9rpzjosTESvmhNeXOXdZZB15Fl+TI9x1SiHZH5Jv2wTGduSxFDIaq0m3DUw==}
@@ -4078,97 +4090,97 @@ packages:
'@sqltools/formatter@1.2.5':
resolution: {integrity: sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==}
- '@storybook/addon-actions@8.3.3':
- resolution: {integrity: sha512-cbpksmld7iADwDGXgojZ4r8LGI3YA3NP68duAHg2n1dtnx1oUaFK5wd6dbNuz7GdjyhIOIy3OKU1dAuylYNGOQ==}
+ '@storybook/addon-actions@8.3.4':
+ resolution: {integrity: sha512-1y0yD3upKcyzNwwA6loAGW2cRDqExwl4oAT7GJQA4tmabI+fNwmANSgU/ezLvvSUf4Qo0eJHg2Zcn8y+Apq2eA==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-backgrounds@8.3.3':
- resolution: {integrity: sha512-aX0OIrtjIB7UgSaiv20SFkfC1iWwJIGMPsPSJ5ZPhXIIOWIEBtSujh8YXwjDEXSC4DOHalmeT4bitRRe5KrVKA==}
+ '@storybook/addon-backgrounds@8.3.4':
+ resolution: {integrity: sha512-o3nl7cN3x8erJNxLEv8YptanEQAnbqnaseOAsvSC6/nnSAcRYBSs3BvekKvo4CcpS2mxn7F5NJTBFYnCXzy8EA==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-controls@8.3.3':
- resolution: {integrity: sha512-78xRtVpY7eX/Lti00JLgwYCBRB6ZcvzY3SWk0uQjEqcTnQGoQkVg2L7oWFDlDoA1LBY18P5ei2vu8MYT9GXU4g==}
+ '@storybook/addon-controls@8.3.4':
+ resolution: {integrity: sha512-qQcaK6dczsb6wXkzGZKOjUYNA7FfKBewRv6NvoVKYY6LfhllGOkmUAtYpdtQG8adsZWTSoZaAOJS2vP2uM67lw==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-docs@8.3.3':
- resolution: {integrity: sha512-REUandqq1RnMNOhsocRwx5q2fdlBAYPTDFlKASYfEn4Ln5NgbQRGxOAWl7yXAAFzbDmUDU7K20hkauecF0tyMw==}
+ '@storybook/addon-docs@8.3.4':
+ resolution: {integrity: sha512-TWauhqF/gJgfwPuWeM6KM3LwC+ErCOM+K2z16w3vgao9s67sij8lnrdAoQ0hjA+kw2/KAdCakFS6FyciG81qog==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-essentials@8.3.3':
- resolution: {integrity: sha512-E/uXoUYcg8ulG3lVbsEKb4v5hnMeGkq9YJqiZYKgVK7iRFa6p4HeVB1wU1adnm7RgjWvh+p0vQRo4KL2CTNXqw==}
+ '@storybook/addon-essentials@8.3.4':
+ resolution: {integrity: sha512-C3+3hpmSn/8zdx5sXEP0eE6zMzxgRosHVZYfe9nBcMiEDp6UKVUyHVetWxEULOEgN46ysjcpllZ0bUkRYxi2IQ==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-highlight@8.3.3':
- resolution: {integrity: sha512-MB084xJM66rLU+iFFk34kjLUiAWzDiy6Kz4uZRa1CnNqEK0sdI8HaoQGgOxTIa2xgJor05/8/mlYlMkP/0INsQ==}
+ '@storybook/addon-highlight@8.3.4':
+ resolution: {integrity: sha512-rxZTeuZyZ7RnU+xmRhS01COFLbGnVEmlUNxBw8ArsrTEZKW5PbKpIxNLTj9F0zdH8H0MfryJGP+Aadcm0oHWlw==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-interactions@8.3.3':
- resolution: {integrity: sha512-3w5tpCGYdF33wF44xEhTS3Zmcwd6nITtwy5q+PJvHCJAm3fpjzL3xrjtlHKDvXNwYacJPRCbWKn2QwtxZIdN0g==}
+ '@storybook/addon-interactions@8.3.4':
+ resolution: {integrity: sha512-ORxqe35wUmF7EDHo45mdDHiju3Ryk2pZ1vO9PyvW6ZItNlHt/IxAr7T/TysGejZ/eTBg6tMZR3ExGky3lTg/CQ==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-links@8.3.3':
- resolution: {integrity: sha512-rz4KEbzr1ca4zZEZwbOnhKiaEsokCl1KkngxT/C1YIkpW908j/kg2nnIb5MrtlAW1nirXguAR74t6CGntvdU9w==}
+ '@storybook/addon-links@8.3.4':
+ resolution: {integrity: sha512-R1DjARmxRIKJDGIG6uxmQ1yFNyoQbb+QIPUFjgWCak8+AdLJbC7W+Esvo9F5hQfh6czyy0piiM3qj5hpQJVh3A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
- storybook: ^8.3.3
+ storybook: ^8.3.4
peerDependenciesMeta:
react:
optional: true
- '@storybook/addon-mdx-gfm@8.3.3':
- resolution: {integrity: sha512-jdwVXoBSEdmuw8L4MxUeJ/qIInADfCwdtShnfTQIJBBRucOl8ykgfTKKNjllT79TFiK0gsWoiZmE05P4wuBofw==}
+ '@storybook/addon-mdx-gfm@8.3.4':
+ resolution: {integrity: sha512-O0sMP7VFo1fKsdViY+W6OMNYEXvB5FzEEsqgsydMcsJ0qOKR1li2l3cLCMLXdUKVZ+2uRbEhnm2RnB9RWF5O7g==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-measure@8.3.3':
- resolution: {integrity: sha512-R20Z83gnxDRrocES344dw1Of/zDhe3XHSM6TLq80UQTJ9PhnMI+wYHQlK9DsdP3KiRkI+pQA6GCOp0s2ZRy5dg==}
+ '@storybook/addon-measure@8.3.4':
+ resolution: {integrity: sha512-IJ6WKEbqmG+r7sukFjo+bVmPB2Zry04sylGx/OGyOh7zIhhqAqpwOwMHP0uQrc3tLNnUM6qB/o83UyYX79ql+A==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-outline@8.3.3':
- resolution: {integrity: sha512-OwqYfieNuqSqWNtUZLu3UmsfQNnwA2UaSMBZyeC2Dte9Jd59PPYggcWmH+b0S6OTbYXWNAUK5U6WdK+X9Ypzdw==}
+ '@storybook/addon-outline@8.3.4':
+ resolution: {integrity: sha512-kRRJTTLKM8gMfeh/e83djN5XLlc0hFtr9zKWxuZxaXt9Hmr+9tH/PRFtVK/S4SgqnBDoXk49Wgv6raiwj5/e3A==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-storysource@8.3.3':
- resolution: {integrity: sha512-yPYQH9NepSNxoSsV9E7OV3/EVFrbU/r2B3E5WP/mCfqTXPg/5noce7iRi+rWqcVM1tsN1qPnSjfQQc7noF0h0Q==}
+ '@storybook/addon-storysource@8.3.4':
+ resolution: {integrity: sha512-uHTUiK7dzWRZAKpPafBH3U5PWAP7+J97lg66HDKAHpmmQdy7v3HfXaYNX1FoI+PeC5piUxFETXM0z+BNvJCknA==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-toolbars@8.3.3':
- resolution: {integrity: sha512-4WyiVqDm4hlJdENIVQg9pLNLdfhnNKa+haerYYSzTVjzYrUx0X6Bxafshq+sud6aRtSYU14abwP56lfW8hgTlA==}
+ '@storybook/addon-toolbars@8.3.4':
+ resolution: {integrity: sha512-Km1YciVIxqluDbd1xmHjANNFyMonEOtnA6e4MrnBnC9XkPXSigeFlj0JvxyI/zjBsLBoFRmQiwq55W6l3hQ9sA==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/addon-viewport@8.3.3':
- resolution: {integrity: sha512-2S+UpbKAL+z1ppzUCkixjaem2UDMkfmm/kyJ1wm3A/ofGLYi4fjMSKNRckk+7NdolXGQJjBo0RcaotUTxFIFwQ==}
+ '@storybook/addon-viewport@8.3.4':
+ resolution: {integrity: sha512-fU4LdXSSqIOLbCEh2leq/tZUYlFliXZBWr/+igQHdUoU7HY8RIImXqVUaR9wlCaTb48WezAWT60vJtwNijyIiQ==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/blocks@8.3.3':
- resolution: {integrity: sha512-8Vsvxqstop3xfbsx3Dn1nEjyxvQUcOYd8vpxyp2YumxYO8FlXIRuYL6HAkYbcX8JexsKvCZYxor52D2vUGIKZg==}
+ '@storybook/blocks@8.3.4':
+ resolution: {integrity: sha512-1g4aCrd5CcN+pVhF2ATu9ZRVvAIgBMb2yF9KkCuTpdvqKDuDNK3sGb0CxjS7jp3LOvyjJr9laTOQsz8v8MQc5A==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
- storybook: ^8.3.3
+ storybook: ^8.3.4
peerDependenciesMeta:
react:
optional: true
react-dom:
optional: true
- '@storybook/builder-vite@8.3.3':
- resolution: {integrity: sha512-3yTXCLaB6bzhoPH3PqtacKkcaC1uV4L+IHTf1Zypx1NO1pLZHyhYf0T7dIOxTh2JZfqu1Pm9hTvOmWfR12m+9w==}
+ '@storybook/builder-vite@8.3.4':
+ resolution: {integrity: sha512-Sa6SZ7LeHpkrnuvua8P8MR8e8a+MPKbyMmr9TqCCy8Ud/t4AM4kHY3JpJGtrgeK9l43fBnBwfdZYoRl5J6oWeA==}
peerDependencies:
'@preact/preset-vite': '*'
- storybook: ^8.3.3
+ storybook: ^8.3.4
typescript: '>= 4.3.x'
vite: ^4.0.0 || ^5.0.0
vite-plugin-glimmerx: '*'
@@ -4180,23 +4192,23 @@ packages:
vite-plugin-glimmerx:
optional: true
- '@storybook/components@8.3.3':
- resolution: {integrity: sha512-i2JYtesFGkdu+Hwuj+o9fLuO3yo+LPT1/8o5xBVYtEqsgDtEAyuRUWjSz8d8NPtzloGPOv5kvR6MokWDfbeMfw==}
+ '@storybook/components@8.3.4':
+ resolution: {integrity: sha512-iQzLJd87uGbFBbYNqlrN/ABrnx3dUrL0tjPCarzglzshZoPCNOsllJeJx5TJwB9kCxSZ8zB9TTOgr7NXl+oyVA==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/core-events@8.3.3':
- resolution: {integrity: sha512-YL+gBuCS81qktzTkvw0MXUJW0bYAXfRzMoiLfDBTrEKZfcJOB4JAlMGmvRRar0+jygK3icD42Rl5BwWoZY6KFQ==}
+ '@storybook/core-events@8.3.4':
+ resolution: {integrity: sha512-3/5oJN2UnlmUILXCh7SXMTa2MYZOvrjeZCm3wFomoQASU2FFzS5AxBYYnwNdtrZmn4w32uw4T7qvA0+96Utwsg==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/core@8.3.3':
- resolution: {integrity: sha512-pmf2bP3fzh45e56gqOuBT8sDX05hGdUKIZ/hcI84d5xmd6MeHiPW8th2v946wCHcxHzxib2/UU9vQUh+mB4VNw==}
+ '@storybook/core@8.3.4':
+ resolution: {integrity: sha512-4PZB91JJpuKfcjeOR2LXj3ABaPLLSd2P/SfYOKNCygrDstsQa/yay3/yN5Z9yi1cIG84KRr6/sUW+0x8HsGLPg==}
- '@storybook/csf-plugin@8.3.3':
- resolution: {integrity: sha512-7AD7ojpXr3THqpTcEI4K7oKUfSwt1hummgL/cASuQvEPOwAZCVZl2gpGtKxcXhtJXTkn3GMCAvlYMoe7O/1YWw==}
+ '@storybook/csf-plugin@8.3.4':
+ resolution: {integrity: sha512-ZMFWYxeTN4GxCn8dyIH4roECyLDy29yv/QKM+pHM3AC5Ny2HWI35SohWao4fGBAFxPQFbR5hPN8xa6ofHPSSTg==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
'@storybook/csf@0.1.11':
resolution: {integrity: sha512-dHYFQH3mA+EtnCkHXzicbLgsvzYjcDJ1JWsogbItZogkPHgSJM/Wr71uMkcvw8v9mmCyP4NpXJuu6bPoVsOnzg==}
@@ -4211,45 +4223,45 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
- '@storybook/instrumenter@8.3.3':
- resolution: {integrity: sha512-ZiODB9EwCQkl4PBxGJjBHXRTLxcNs68ZZvR+xeMr0eMFzzlJG+trXoX5kK95oA4BFhGN+3uM0Zl3MoRjBtJTNA==}
+ '@storybook/instrumenter@8.3.4':
+ resolution: {integrity: sha512-jVhfNOPekOyJmta0BTkQl9Z6rgRbFHlc0eV4z1oSrzaawSlc9TFzAeDCtCP57vg3FuBX8ydDYAvyZ7s4xPpLyg==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/manager-api@8.3.3':
- resolution: {integrity: sha512-Na4U+McOeVUJAR6qzJfQ6y2Qt0kUgEDUriNoAn+curpoKPTmIaZ79RAXBzIqBl31VyQKknKpZbozoRGf861YaQ==}
+ '@storybook/manager-api@8.3.4':
+ resolution: {integrity: sha512-tBx7MBfPUrKSlD666zmVjtIvoNArwCciZiW/UJ8IWmomrTJRfFBnVvPVM2gp1lkDIzRHYmz5x9BHbYaEDNcZWQ==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/preview-api@8.3.3':
- resolution: {integrity: sha512-GP2QlaF3BBQGAyo248N7549YkTQjCentsc1hUvqPnFWU4xfjkejbnFk8yLaIw0VbYbL7jfd7npBtjZ+6AnphMQ==}
+ '@storybook/preview-api@8.3.4':
+ resolution: {integrity: sha512-/YKQ3QDVSHmtFXXCShf5w0XMlg8wkfTpdYxdGv1CKFV8DU24f3N7KWulAgeWWCWQwBzZClDa9kzxmroKlQqx3A==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/react-dom-shim@8.3.3':
- resolution: {integrity: sha512-0dPC9K7+K5+X/bt3GwYmh+pCpisUyKVjWsI+PkzqGnWqaXFakzFakjswowIAIO1rf7wYZR591x3ehUAyL2bJiQ==}
+ '@storybook/react-dom-shim@8.3.4':
+ resolution: {integrity: sha512-L4llDvjaAzqPx6h4ddZMh36wPr75PrI2S8bXy+flLqAeVRYnRt4WNKGuxqH0t0U6MwId9+vlCZ13JBfFuY7eQQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/react-vite@8.3.3':
- resolution: {integrity: sha512-vzOqVaA/rv+X5J17eWKxdZztMKEKfsCSP8pNNmrqXWxK3pSlW0fAPxtn1kw3UNxGtAv71pcqvaCUtTJKqI1PYA==}
+ '@storybook/react-vite@8.3.4':
+ resolution: {integrity: sha512-0Xm8eTH+jQ7SV4moLkPN4G6U2IDrqXPXUqsZdXaccepIMcD4G75foQFm2LOrFJuY+IMySPspKeTqf8OLskPppw==}
engines: {node: '>=18.0.0'}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
- storybook: ^8.3.3
+ storybook: ^8.3.4
vite: ^4.0.0 || ^5.0.0
- '@storybook/react@8.3.3':
- resolution: {integrity: sha512-fHOW/mNqI+sZWttGOE32Q+rAIbN7/Oib091cmE8usOM0z0vPNpywUBtqC2cCQH39vp19bhTsQaSsTcoBSweAHw==}
+ '@storybook/react@8.3.4':
+ resolution: {integrity: sha512-PA7iQL4/9X2/iLrv+AUPNtlhTHJWhDao9gQIT1Hef39FtFk+TU9lZGbv+g29R1H9V3cHP5162nG2aTu395kmbA==}
engines: {node: '>=18.0.0'}
peerDependencies:
- '@storybook/test': 8.3.3
+ '@storybook/test': 8.3.4
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0-beta
- storybook: ^8.3.3
+ storybook: ^8.3.4
typescript: '>= 4.2.x'
peerDependenciesMeta:
'@storybook/test':
@@ -4257,38 +4269,38 @@ packages:
typescript:
optional: true
- '@storybook/source-loader@8.3.3':
- resolution: {integrity: sha512-NeP7l53mvnnfwi+91vtRaibZer+UJi6gkoaGRCpphL3L+3qVIXN3p41uXhAy+TahdFI2dbrWvLSNgtsvdXVaFg==}
+ '@storybook/source-loader@8.3.4':
+ resolution: {integrity: sha512-wH//LuWfa2iOmjykSqsub8M8e0EdhEUZoHUFhwBeizfYQQHaMaSEBhhAQCaWWKmdGB9lnCe1cioQ32c2IWtBIw==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/test@8.3.3':
- resolution: {integrity: sha512-uZ8nMIovfI2ry989K2+cYAeEVD/3dpjj2+Rbmy7DiZWWVhFALfmqaTRkzZfShLmlH0TFv+rfcBPihGccBtw0FQ==}
+ '@storybook/test@8.3.4':
+ resolution: {integrity: sha512-HRiUenitln8QPHu6DEWUg9s9cEoiGN79lMykzXzw9shaUvdEIhWCsh82YKtmB3GJPj6qcc6dZL/Aio8srxyGAg==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/theming@8.3.3':
- resolution: {integrity: sha512-gWJKetI6XJQgkrvvry4ez10+jLaGNCQKi5ygRPM9N+qrjA3BB8F2LCuFUTBuisa4l64TILDNjfwP/YTWV5+u5A==}
+ '@storybook/theming@8.3.4':
+ resolution: {integrity: sha512-D4XVsQgTtpHEHLhwkx59aGy1GBwOedVr/mNns7hFrH8FjEpxrrWCuZQASq1ZpCl8LXlh7uvmT5sM2rOdQbGuGg==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/types@8.3.3':
- resolution: {integrity: sha512-wV1kupG1tfTMOXaBrtVHXuqp19vURVDqWTQX6nqkoUFD7Xb1lz/YNVeGP1uT/zJdJy42/HIyoib9JPx9h0Vx9w==}
+ '@storybook/types@8.3.4':
+ resolution: {integrity: sha512-kIyb0g8C6EizI0Mv+l6L6yjCJe9/vW3UvgsZL5BXqs8THTAfs3/+A9Q9jDEMovSIVI3EgesO79+OCEazDUHmOA==}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
- '@storybook/vue3-vite@8.3.3':
- resolution: {integrity: sha512-IFcoOGlUGuUkL3rpm9UFs8FK9JX1ZdfGpLXRObVOVRhW3t+MsNLpx4Fqp3a/re6WcCC3yvHzbLXgvGcjpapkbw==}
+ '@storybook/vue3-vite@8.3.4':
+ resolution: {integrity: sha512-0H1tLbRd8i6L3EW8QC9bDlgPIUM5i6b7onvyyQhyIxODWRfigHi6UP9sjHfrljdvnlOtYlZT2A5QbpkugzwLjg==}
engines: {node: '>=18.0.0'}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
vite: ^4.0.0 || ^5.0.0
- '@storybook/vue3@8.3.3':
- resolution: {integrity: sha512-peu8MFGwmhpXoD3n42qG6TxeVHRhfHZ0/HW4+A6FXSB1c9w0CC4AzHs5f1w3yUvshtexNN5bkw9Q4nSVKtfU7A==}
+ '@storybook/vue3@8.3.4':
+ resolution: {integrity: sha512-NNQXwidr+QjLndORWtPjXv/obsNNfJhP5Xj6vUZslrDpdIyTL3NEM+ktLK2EMw/a3zUbJMnMkyMgoWvioCNHxQ==}
engines: {node: '>=18.0.0'}
peerDependencies:
- storybook: ^8.3.3
+ storybook: ^8.3.4
vue: ^3.0.0
'@swc/cli@0.3.12':
@@ -4625,8 +4637,11 @@ packages:
'@types/cacheable-request@6.0.3':
resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
- '@types/color-convert@2.0.3':
- resolution: {integrity: sha512-2Q6wzrNiuEvYxVQqhh7sXM2mhIhvZR/Paq4FdsQkOMgWsCIkKvSGj8Le1/XalulrmgOzPMqNa0ix+ePY4hTrfg==}
+ '@types/canvas-confetti@1.6.4':
+ resolution: {integrity: sha512-fNyZ/Fdw/Y92X0vv7B+BD6ysHL4xVU5dJcgzgxLdGbn8O3PezZNIJpml44lKM0nsGur+o/6+NZbZeNTt00U1uA==}
+
+ '@types/color-convert@2.0.4':
+ resolution: {integrity: sha512-Ub1MmDdyZ7mX//g25uBAoH/mWGd9swVbt8BseymnaE18SU4po/PjmCrHxqIIRjBo3hV/vh1KGr0eMxUhp+t+dQ==}
'@types/color-name@1.1.1':
resolution: {integrity: sha512-rr+OQyAjxze7GgWrSaJwydHStIhHq2lvY3BOC2Mj7KnzI7XK0Uw1TOOdI9lDoajEbSWLiYgoo4f1R51erQfhPQ==}
@@ -5205,23 +5220,29 @@ packages:
'@vue/compiler-core@3.5.10':
resolution: {integrity: sha512-iXWlk+Cg/ag7gLvY0SfVucU8Kh2CjysYZjhhP70w9qI4MvSox4frrP+vDGvtQuzIcgD8+sxM6lZvCtdxGunTAA==}
+ '@vue/compiler-core@3.5.11':
+ resolution: {integrity: sha512-PwAdxs7/9Hc3ieBO12tXzmTD+Ln4qhT/56S+8DvrrZ4kLDn4Z/AMUr8tXJD0axiJBS0RKIoNaR0yMuQB9v9Udg==}
+
'@vue/compiler-dom@3.4.37':
resolution: {integrity: sha512-rIiSmL3YrntvgYV84rekAtU/xfogMUJIclUMeIKEtVBFngOL3IeZHhsH3UaFEgB5iFGpj6IW+8YuM/2Up+vVag==}
'@vue/compiler-dom@3.5.10':
resolution: {integrity: sha512-DyxHC6qPcktwYGKOIy3XqnHRrrXyWR2u91AjP+nLkADko380srsC2DC3s7Y1Rk6YfOlxOlvEQKa9XXmLI+W4ZA==}
+ '@vue/compiler-dom@3.5.11':
+ resolution: {integrity: sha512-pyGf8zdbDDRkBrEzf8p7BQlMKNNF5Fk/Cf/fQ6PiUz9at4OaUfyXW0dGJTo2Vl1f5U9jSLCNf0EZJEogLXoeew==}
+
'@vue/compiler-sfc@3.4.37':
resolution: {integrity: sha512-vCfetdas40Wk9aK/WWf8XcVESffsbNkBQwS5t13Y/PcfqKfIwJX2gF+82th6dOpnpbptNMlMjAny80li7TaCIg==}
- '@vue/compiler-sfc@3.5.10':
- resolution: {integrity: sha512-to8E1BgpakV7224ZCm8gz1ZRSyjNCAWEplwFMWKlzCdP9DkMKhRRwt0WkCjY7jkzi/Vz3xgbpeig5Pnbly4Tow==}
+ '@vue/compiler-sfc@3.5.11':
+ resolution: {integrity: sha512-gsbBtT4N9ANXXepprle+X9YLg2htQk1sqH/qGJ/EApl+dgpUBdTv3yP7YlR535uHZY3n6XaR0/bKo0BgwwDniw==}
'@vue/compiler-ssr@3.4.37':
resolution: {integrity: sha512-TyAgYBWrHlFrt4qpdACh8e9Ms6C/AZQ6A6xLJaWrCL8GCX5DxMzxyeFAEMfU/VFr4tylHm+a2NpfJpcd7+20XA==}
- '@vue/compiler-ssr@3.5.10':
- resolution: {integrity: sha512-hxP4Y3KImqdtyUKXDRSxKSRkSm1H9fCvhojEYrnaoWhE4w/y8vwWhnosJoPPe2AXm5sU7CSbYYAgkt2ZPhDz+A==}
+ '@vue/compiler-ssr@3.5.11':
+ resolution: {integrity: sha512-P4+GPjOuC2aFTk1Z4WANvEhyOykcvEd5bIj2KVNGKGfM745LaXGr++5njpdBTzVz5pZifdlR1kpYSJJpIlSePA==}
'@vue/compiler-vue2@2.7.16':
resolution: {integrity: sha512-qYC3Psj9S/mfu9uVi5WvNZIzq+xnXMhOwbTFKKDD7b1lhpnn71jXSFdTQ+WsIEk0ONCd7VV2IMm7ONl6tbQ86A==}
@@ -5245,30 +5266,30 @@ packages:
'@vue/reactivity@3.4.37':
resolution: {integrity: sha512-UmdKXGx0BZ5kkxPqQr3PK3tElz6adTey4307NzZ3whZu19i5VavYal7u2FfOmAzlcDVgE8+X0HZ2LxLb/jgbYw==}
- '@vue/reactivity@3.5.10':
- resolution: {integrity: sha512-kW08v06F6xPSHhid9DJ9YjOGmwNDOsJJQk0ax21wKaUYzzuJGEuoKNU2Ujux8FLMrP7CFJJKsHhXN9l2WOVi2g==}
+ '@vue/reactivity@3.5.11':
+ resolution: {integrity: sha512-Nqo5VZEn8MJWlCce8XoyVqHZbd5P2NH+yuAaFzuNSR96I+y1cnuUiq7xfSG+kyvLSiWmaHTKP1r3OZY4mMD50w==}
'@vue/runtime-core@3.4.37':
resolution: {integrity: sha512-MNjrVoLV/sirHZoD7QAilU1Ifs7m/KJv4/84QVbE6nyAZGQNVOa1HGxaOzp9YqCG+GpLt1hNDC4RbH+KtanV7w==}
- '@vue/runtime-core@3.5.10':
- resolution: {integrity: sha512-9Q86I5Qq3swSkFfzrZ+iqEy7Vla325M7S7xc1NwKnRm/qoi1Dauz0rT6mTMmscqx4qz0EDJ1wjB+A36k7rl8mA==}
+ '@vue/runtime-core@3.5.11':
+ resolution: {integrity: sha512-7PsxFGqwfDhfhh0OcDWBG1DaIQIVOLgkwA5q6MtkPiDFjp5gohVnJEahSktwSFLq7R5PtxDKy6WKURVN1UDbzA==}
'@vue/runtime-dom@3.4.37':
resolution: {integrity: sha512-Mg2EwgGZqtwKrqdL/FKMF2NEaOHuH+Ks9TQn3DHKyX//hQTYOun+7Tqp1eo0P4Ds+SjltZshOSRq6VsU0baaNg==}
- '@vue/runtime-dom@3.5.10':
- resolution: {integrity: sha512-t3x7ht5qF8ZRi1H4fZqFzyY2j+GTMTDxRheT+i8M9Ph0oepUxoadmbwlFwMoW7RYCpNQLpP2Yx3feKs+fyBdpA==}
+ '@vue/runtime-dom@3.5.11':
+ resolution: {integrity: sha512-GNghjecT6IrGf0UhuYmpgaOlN7kxzQBhxWEn08c/SQDxv1yy4IXI1bn81JgEpQ4IXjRxWtPyI8x0/7TF5rPfYQ==}
'@vue/server-renderer@3.4.37':
resolution: {integrity: sha512-jZ5FAHDR2KBq2FsRUJW6GKDOAG9lUTX8aBEGq4Vf6B/35I9fPce66BornuwmqmKgfiSlecwuOb6oeoamYMohkg==}
peerDependencies:
vue: 3.4.37
- '@vue/server-renderer@3.5.10':
- resolution: {integrity: sha512-IVE97tt2kGKwHNq9yVO0xdh1IvYfZCShvDSy46JIh5OQxP1/EXSpoDqetVmyIzL7CYOWnnmMkVqd7YK2QSWkdw==}
+ '@vue/server-renderer@3.5.11':
+ resolution: {integrity: sha512-cVOwYBxR7Wb1B1FoxYvtjJD8X/9E5nlH4VSkJy2uMA1MzYNdzAAB//l8nrmN9py/4aP+3NjWukf9PZ3TeWULaA==}
peerDependencies:
- vue: 3.5.10
+ vue: 3.5.11
'@vue/shared@3.4.37':
resolution: {integrity: sha512-nIh8P2fc3DflG8+5Uw8PT/1i17ccFn0xxN/5oE9RfV5SVnd7G0XEFRwakrnNFE/jlS95fpGXDVG5zDETS26nmg==}
@@ -5276,6 +5297,9 @@ packages:
'@vue/shared@3.5.10':
resolution: {integrity: sha512-VkkBhU97Ki+XJ0xvl4C9YJsIZ2uIlQ7HqPpZOS3m9VCvmROPaChZU6DexdMJqvz9tbgG+4EtFVrSuailUq5KGQ==}
+ '@vue/shared@3.5.11':
+ resolution: {integrity: sha512-W8GgysJVnFo81FthhzurdRAWP/byq3q2qIw70e0JWblzVhjgOMiC2GyovXrZTFQJnFVryYaKGP3Tc9vYzYm6PQ==}
+
'@vue/test-utils@2.4.1':
resolution: {integrity: sha512-VO8nragneNzUZUah6kOjiFmD/gwRjUauG9DROh6oaOeFwX1cZRUNHhdeogE8635cISigXFTtGLUQWx5KCb0xeg==}
peerDependencies:
@@ -5669,10 +5693,6 @@ packages:
bn.js@4.12.0:
resolution: {integrity: sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==}
- body-parser@1.20.2:
- resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
- engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
-
body-parser@1.20.3:
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
@@ -5750,8 +5770,8 @@ packages:
resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
engines: {node: '>=6.14.2'}
- bullmq@5.13.2:
- resolution: {integrity: sha512-McGE8k3mrCvdUHdU0sHkTKDS1xr4pff+hbEKBY51wk5S6Za0gkuejYA620VQTo3Zz37E/NVWMgumwiXPQ3yZcA==}
+ bullmq@5.15.0:
+ resolution: {integrity: sha512-h53shVjx8s6wxYGtUfzAfENpSP7N5T0D4PMTvbZncozLjb8yUKhopfpa7PmcpQfq7SSO9dm/OZ9XQuGOCSGNug==}
buraha@0.0.1:
resolution: {integrity: sha512-G563A0mTbzknm2jDaNxfZuNKIdeArs8T+XQN6t+KbmgnOoevXSXhKDkyf8Md/36Jrx99ikwbCag37VGe3myExQ==}
@@ -5879,6 +5899,12 @@ packages:
resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==}
engines: {node: '>=10'}
+ character-entities-html4@2.1.0:
+ resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==}
+
+ character-entities-legacy@3.0.0:
+ resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==}
+
character-entities@2.0.2:
resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==}
@@ -5940,8 +5966,8 @@ packages:
resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==}
engines: {node: '>=10'}
- chromatic@11.10.4:
- resolution: {integrity: sha512-nfgDpW5gQ4FtgV1lZXXfqLjONKDCh2K4vwI3dbZrtU1ObOL9THyAzpIdnK9LRcNSeisDLX+XFCryfMg1Ql2U2g==}
+ chromatic@11.11.0:
+ resolution: {integrity: sha512-mwmYsNMsZlRLtlfFUEtac5zhoVRhc+O/lsuMdOpwkiDQiKX6WdSNIhic+dkLenfuzao2r18s50nphcOgFoatBg==}
hasBin: true
peerDependencies:
'@chromatic-com/cypress': ^0.*.* || ^1.0.0
@@ -6050,6 +6076,9 @@ packages:
resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==}
engines: {node: '>= 0.8'}
+ comma-separated-tokens@2.0.3:
+ resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==}
+
commander@10.0.1:
resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==}
engines: {node: '>=14'}
@@ -6756,6 +6785,16 @@ packages:
'@typescript-eslint/parser':
optional: true
+ eslint-plugin-import@2.31.0:
+ resolution: {integrity: sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==}
+ engines: {node: '>=4'}
+ peerDependencies:
+ '@typescript-eslint/parser': '*'
+ eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9
+ peerDependenciesMeta:
+ '@typescript-eslint/parser':
+ optional: true
+
eslint-plugin-vue@9.27.0:
resolution: {integrity: sha512-5Dw3yxEyuBSXTzT5/Ge1X5kIkRTQ3nvBn/VwPwInNiZBSJOO/timWMUaflONnFBzU6NhB68lxnCda7ULV5N7LA==}
engines: {node: ^14.17.0 || >=16.0.0}
@@ -6900,10 +6939,6 @@ packages:
exponential-backoff@3.1.1:
resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
- express@4.19.2:
- resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
- engines: {node: '>= 0.10.0'}
-
express@4.21.0:
resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==}
engines: {node: '>= 0.10.0'}
@@ -7057,10 +7092,6 @@ packages:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
- finalhandler@1.2.0:
- resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
- engines: {node: '>= 0.8'}
-
finalhandler@1.3.1:
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
engines: {node: '>= 0.8'}
@@ -7432,9 +7463,15 @@ packages:
hast-util-is-element@3.0.0:
resolution: {integrity: sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==}
+ hast-util-to-html@9.0.3:
+ resolution: {integrity: sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==}
+
hast-util-to-string@3.0.0:
resolution: {integrity: sha512-OGkAxX1Ua3cbcW6EJ5pT/tslVb90uViVkcJ4ZZIMW/R33DX/AkcJcRrPebPwJkHYwlDHXz4aIwvAAaAdtrACFA==}
+ hast-util-whitespace@3.0.0:
+ resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==}
+
he@1.2.0:
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
hasBin: true
@@ -7474,6 +7511,9 @@ packages:
resolution: {integrity: sha512-vy7ClnArOZwCnqZgvv+ddgHgJiAFXe3Ge9ML5/mBctVJoUoYPCdxVucOywjDARn6CVoh3dRSFdPHy2sX80L0Wg==}
engines: {node: '>=8'}
+ html-void-elements@3.0.0:
+ resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==}
+
htmlescape@1.1.1:
resolution: {integrity: sha512-eVcrzgbR4tim7c7soKQKtxa/kQM4TzjnlU83rcZ9bHU6t31ehfV7SktN6McWgwPWg+JYMA/O3qpGxBvFq1z2Jg==}
engines: {node: '>=0.10'}
@@ -8480,6 +8520,9 @@ packages:
mdast-util-phrasing@4.1.0:
resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==}
+ mdast-util-to-hast@13.2.0:
+ resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==}
+
mdast-util-to-markdown@2.1.0:
resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==}
@@ -8509,9 +8552,6 @@ packages:
resolution: {integrity: sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==}
engines: {node: '>=10'}
- merge-descriptors@1.0.1:
- resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
-
merge-descriptors@1.0.3:
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
@@ -9086,6 +9126,9 @@ packages:
resolution: {integrity: sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==}
engines: {node: '>=12'}
+ oniguruma-to-js@0.4.3:
+ resolution: {integrity: sha512-X0jWUcAlxORhOqqBREgPMgnshB7ZGYszBNspP+tS9hPD3l13CdaXcHbgImoHUHlrvGx/7AvFEkTRhAGYh+jzjQ==}
+
open@8.4.2:
resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
engines: {node: '>=12'}
@@ -9117,8 +9160,8 @@ packages:
ospath@1.2.2:
resolution: {integrity: sha512-o6E5qJV5zkAbIDNhGSIlyOhScKXgQrSRMilfph0clDfM0nEnBOlKlH4sWDmG95BW/CvwNz0vmm7dJVtU2KlMiA==}
- otpauth@9.3.2:
- resolution: {integrity: sha512-KixtXWN9RGdS8WHPfDo7qsOYiivCbl+VeLBT+7HBTtJebBO6aXr/bpZXr+TwY2COecdY82VeBghm31mLYQVZlQ==}
+ otpauth@9.3.4:
+ resolution: {integrity: sha512-qXv+lpsCUO9ewitLYfeDKbLYt7UUCivnU/fwGK2OqhgrCBsRkTUNKWsgKAhkXG3aistOY+jEeuL90JEBu6W3mQ==}
outvariant@1.4.2:
resolution: {integrity: sha512-Ou3dJ6bA/UJ5GVHxah4LnqDwZRwAmWxrG3wtrHrbGnP4RnLCtA64A4F+ae7Y8ww660JaddSoArUR5HjipWSHAQ==}
@@ -9258,9 +9301,6 @@ packages:
path-to-regexp@0.1.10:
resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==}
- path-to-regexp@0.1.7:
- resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
-
path-to-regexp@1.8.0:
resolution: {integrity: sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==}
@@ -9594,10 +9634,6 @@ packages:
postcss-value-parser@4.2.0:
resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==}
- postcss@8.4.38:
- resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
- engines: {node: ^10 || ^12 || >=14}
-
postcss@8.4.47:
resolution: {integrity: sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==}
engines: {node: ^10 || ^12 || >=14}
@@ -9709,6 +9745,9 @@ packages:
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
+ property-information@6.5.0:
+ resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
+
proto-list@1.2.4:
resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==}
@@ -9801,10 +9840,6 @@ packages:
engines: {node: '>=10.13.0'}
hasBin: true
- qs@6.11.0:
- resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
- engines: {node: '>=0.6'}
-
qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
@@ -9966,6 +10001,9 @@ packages:
regenerator-runtime@0.14.0:
resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
+ regex@4.4.0:
+ resolution: {integrity: sha512-uCUSuobNVeqUupowbdZub6ggI5/JZkYyJdDogddJr60L764oxC2pMZov1fQ3wM9bdyzUILDG+Sqx6NAKAz9rKQ==}
+
regexp.prototype.flags@1.5.0:
resolution: {integrity: sha512-0SutC3pNudRKgquxGoRGIz946MZVHqbNfPjBdxeOhBrdgDKlRoXmYLQN9xRbrR09ZXWeGAdPuif7egofn6v5LA==}
engines: {node: '>= 0.4'}
@@ -10124,14 +10162,19 @@ packages:
safer-buffer@2.1.2:
resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==}
- sanitize-html@2.13.0:
- resolution: {integrity: sha512-Xff91Z+4Mz5QiNSLdLWwjgBDm5b1RU6xBT0+12rapjiaR7SwfRdjw8f+6Rir2MXKLrDicRFHdb51hGOAxmsUIA==}
+ sanitize-html@2.13.1:
+ resolution: {integrity: sha512-ZXtKq89oue4RP7abL9wp/9URJcqQNABB5GGJ2acW1sdO8JTVl92f4ygD7Yc9Ze09VAZhnt2zegeU0tbNsdcLYg==}
sass@1.79.3:
resolution: {integrity: sha512-m7dZxh0W9EZ3cw50Me5GOuYm/tVAJAn91SUnohLRo9cXBixGUOdvmryN+dXpwR831bhoY3Zv7rEFt85PUwTmzA==}
engines: {node: '>=14.0.0'}
hasBin: true
+ sass@1.79.4:
+ resolution: {integrity: sha512-K0QDSNPXgyqO4GZq2HO5Q70TLxTH6cIT59RdoCHMivrC8rqzaTw5ab9prjz9KUN1El4FLXrBXJhik61JR4HcGg==}
+ engines: {node: '>=14.0.0'}
+ hasBin: true
+
sax@1.2.4:
resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==}
@@ -10182,18 +10225,10 @@ packages:
engines: {node: '>=10'}
hasBin: true
- send@0.18.0:
- resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
- engines: {node: '>= 0.8.0'}
-
send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
- serve-static@1.15.0:
- resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
- engines: {node: '>= 0.8.0'}
-
serve-static@1.16.2:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
engines: {node: '>= 0.8.0'}
@@ -10245,8 +10280,8 @@ packages:
shiki@0.14.7:
resolution: {integrity: sha512-dNPAPrxSc87ua2sKJ3H5dQ/6ZaY8RNnaAqK+t0eG7p0Soi2ydiqbGOTaZCqaYvA/uZYfS1LJnemt3Q+mSfcPCg==}
- shiki@1.12.0:
- resolution: {integrity: sha512-BuAxWOm5JhRcbSOl7XCei8wGjgJJonnV0oipUupPY58iULxUGyHhW5CF+9FRMuM1pcJ5cGEJGll1LusX6FwpPA==}
+ shiki@1.21.0:
+ resolution: {integrity: sha512-apCH5BoWTrmHDPGgg3RF8+HAAbEL/CdbYr8rMw7eIrdhCkZHdVGat5mMNlRtd1erNG01VPMIKHNQ0Pj2HMAiog==}
shimmer@1.2.1:
resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==}
@@ -10515,8 +10550,8 @@ packages:
react-dom:
optional: true
- storybook@8.3.3:
- resolution: {integrity: sha512-FG2KAVQN54T9R6voudiEftehtkXtLO+YVGP2gBPfacEdDQjY++ld7kTbHzpTT/bpCDx7Yq3dqOegLm9arVJfYw==}
+ storybook@8.3.4:
+ resolution: {integrity: sha512-nzvuK5TsEgJwcWGLGgafabBOxKn37lfJVv7ZoUVPgJIjk2mNRyJDFwYRJzUZaD37eiR/c/lQ6MoaeqlGwiXoxw==}
hasBin: true
stream-browserify@3.0.0:
@@ -10584,6 +10619,9 @@ packages:
string_decoder@1.3.0:
resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==}
+ stringify-entities@4.0.4:
+ resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==}
+
stringz@2.1.0:
resolution: {integrity: sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A==}
@@ -10828,6 +10866,9 @@ packages:
trace-redirect@1.0.6:
resolution: {integrity: sha512-UUfa1DjjU5flcjMdaFIiIEGDTyu2y/IiMjOX4uGXa7meKBS4vD4f2Uy/tken9Qkd4Jsm4sRsfZcIIPqrRVF3Mg==}
+ trim-lines@3.0.1:
+ resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==}
+
trim-newlines@3.0.1:
resolution: {integrity: sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==}
engines: {node: '>=8'}
@@ -11132,6 +11173,9 @@ packages:
unist-util-is@6.0.0:
resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==}
+ unist-util-position@5.0.0:
+ resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==}
+
unist-util-stringify-position@4.0.0:
resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==}
@@ -11361,6 +11405,9 @@ packages:
vue-component-type-helpers@2.0.16:
resolution: {integrity: sha512-qisL/iAfdO++7w+SsfYQJVPj6QKvxp4i1MMxvsNO41z/8zu3KuAw9LkhKUfP/kcOWGDxESp+pQObWppXusejCA==}
+ vue-component-type-helpers@2.1.10:
+ resolution: {integrity: sha512-lfgdSLQKrUmADiSV6PbBvYgQ33KF3Ztv6gP85MfGaGaSGMTXORVaHT1EHfsqCgzRNBstPKYDmvAV9Do5CmJ07A==}
+
vue-component-type-helpers@2.1.6:
resolution: {integrity: sha512-ng11B8B/ZADUMMOsRbqv0arc442q7lifSubD0v8oDXIFoMg/mXwAPUunrroIDkY+mcD0dHKccdaznSVp8EoX3w==}
@@ -11408,8 +11455,8 @@ packages:
typescript:
optional: true
- vue@3.5.10:
- resolution: {integrity: sha512-Vy2kmJwHPlouC/tSnIgXVg03SG+9wSqT1xu1Vehc+ChsXsRd7jLkKgMltVEFOzUdBr3uFwBCG+41LJtfAcBRng==}
+ vue@3.5.11:
+ resolution: {integrity: sha512-/8Wurrd9J3lb72FTQS7gRMNQD4nztTtKPmuDuPuhqXmmpD6+skVjAeahNpVzsuky6Sy9gy7wn8UadqPtt9SQIg==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@@ -11689,13 +11736,13 @@ snapshots:
dependencies:
'@aws-crypto/util': 5.2.0
'@aws-sdk/types': 3.609.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-crypto/crc32c@5.2.0':
dependencies:
'@aws-crypto/util': 5.2.0
'@aws-sdk/types': 3.609.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-crypto/sha1-browser@5.2.0':
dependencies:
@@ -11724,13 +11771,13 @@ snapshots:
'@aws-crypto/supports-web-crypto@5.2.0':
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-crypto/util@5.2.0':
dependencies:
'@aws-sdk/types': 3.609.0
'@smithy/util-utf8': 2.0.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-sdk/client-s3@3.620.0':
dependencies:
@@ -11879,7 +11926,7 @@ snapshots:
'@smithy/util-middleware': 3.0.3
'@smithy/util-retry': 3.0.3
'@smithy/util-utf8': 3.0.0
- tslib: 2.6.3
+ tslib: 2.7.0
transitivePeerDependencies:
- aws-crt
@@ -11943,7 +11990,7 @@ snapshots:
'@aws-sdk/types': 3.609.0
'@smithy/property-provider': 3.1.3
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-sdk/credential-provider-http@3.620.0':
dependencies:
@@ -11955,7 +12002,7 @@ snapshots:
'@smithy/smithy-client': 3.1.11
'@smithy/types': 3.3.0
'@smithy/util-stream': 3.1.3
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-sdk/credential-provider-ini@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))(@aws-sdk/client-sts@3.620.0)':
dependencies:
@@ -11970,7 +12017,7 @@ snapshots:
'@smithy/property-provider': 3.1.3
'@smithy/shared-ini-file-loader': 3.1.4
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
transitivePeerDependencies:
- '@aws-sdk/client-sso-oidc'
- aws-crt
@@ -12000,7 +12047,7 @@ snapshots:
'@smithy/property-provider': 3.1.3
'@smithy/shared-ini-file-loader': 3.1.4
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-sdk/credential-provider-sso@3.620.0(@aws-sdk/client-sso-oidc@3.620.0(@aws-sdk/client-sts@3.620.0))':
dependencies:
@@ -12010,7 +12057,7 @@ snapshots:
'@smithy/property-provider': 3.1.3
'@smithy/shared-ini-file-loader': 3.1.4
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
transitivePeerDependencies:
- '@aws-sdk/client-sso-oidc'
- aws-crt
@@ -12021,7 +12068,7 @@ snapshots:
'@aws-sdk/types': 3.609.0
'@smithy/property-provider': 3.1.3
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-sdk/lib-storage@3.620.0(@aws-sdk/client-s3@3.620.0)':
dependencies:
@@ -12151,7 +12198,7 @@ snapshots:
'@smithy/property-provider': 3.1.3
'@smithy/shared-ini-file-loader': 3.1.4
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-sdk/types@3.609.0':
dependencies:
@@ -12160,7 +12207,7 @@ snapshots:
'@aws-sdk/util-arn-parser@3.568.0':
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-sdk/util-endpoints@3.614.0':
dependencies:
@@ -12171,7 +12218,7 @@ snapshots:
'@aws-sdk/util-locate-window@3.208.0':
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
'@aws-sdk/util-user-agent-browser@3.609.0':
dependencies:
@@ -12525,8 +12572,8 @@ snapshots:
'@babel/template@7.24.0':
dependencies:
'@babel/code-frame': 7.24.7
- '@babel/parser': 7.24.7
- '@babel/types': 7.24.7
+ '@babel/parser': 7.25.7
+ '@babel/types': 7.25.7
'@babel/template@7.24.7':
dependencies:
@@ -12726,7 +12773,7 @@ snapshots:
'@emnapi/runtime@1.3.0':
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
optional: true
'@esbuild/aix-ppc64@0.19.11':
@@ -13137,7 +13184,7 @@ snapshots:
'@fastify/accept-negotiator@2.0.0': {}
- '@fastify/accepts@5.0.0':
+ '@fastify/accepts@5.0.1':
dependencies:
accepts: 1.3.8
fastify-plugin: 5.0.1
@@ -13156,12 +13203,12 @@ snapshots:
'@fastify/busboy@3.0.0': {}
- '@fastify/cookie@10.0.0':
+ '@fastify/cookie@10.0.1':
dependencies:
cookie-signature: 1.2.1
fastify-plugin: 5.0.1
- '@fastify/cors@10.0.0':
+ '@fastify/cors@10.0.1':
dependencies:
fastify-plugin: 5.0.1
mnemonist: 0.39.8
@@ -13170,9 +13217,9 @@ snapshots:
'@fastify/error@4.0.0': {}
- '@fastify/express@4.0.0':
+ '@fastify/express@4.0.1':
dependencies:
- express: 4.19.2
+ express: 4.21.0
fastify-plugin: 5.0.1
transitivePeerDependencies:
- supports-color
@@ -13195,7 +13242,7 @@ snapshots:
dependencies:
fast-deep-equal: 3.1.3
- '@fastify/multipart@9.0.0':
+ '@fastify/multipart@9.0.1':
dependencies:
'@fastify/busboy': 3.0.0
'@fastify/deepmerge': 2.0.0
@@ -13221,15 +13268,6 @@ snapshots:
http-errors: 2.0.0
mime: 3.0.0
- '@fastify/static@8.0.0':
- dependencies:
- '@fastify/accept-negotiator': 2.0.0
- '@fastify/send': 3.1.1
- content-disposition: 0.5.4
- fastify-plugin: 5.0.1
- fastq: 1.17.1
- glob: 11.0.0
-
'@fastify/static@8.0.1':
dependencies:
'@fastify/accept-negotiator': 2.0.0
@@ -13239,11 +13277,6 @@ snapshots:
fastq: 1.17.1
glob: 11.0.0
- '@fastify/view@10.0.0':
- dependencies:
- fastify-plugin: 5.0.1
- toad-cache: 3.7.0
-
'@fastify/view@10.0.1':
dependencies:
fastify-plugin: 5.0.1
@@ -13676,13 +13709,13 @@ snapshots:
'@misskey-dev/browser-image-resizer@2024.1.0': {}
- '@misskey-dev/eslint-plugin@2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)':
+ '@misskey-dev/eslint-plugin@2.0.3(@eslint/compat@1.1.1)(@typescript-eslint/eslint-plugin@7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2))(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0))(eslint@9.8.0)(globals@15.9.0)':
dependencies:
'@eslint/compat': 1.1.1
'@typescript-eslint/eslint-plugin': 7.17.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)(typescript@5.6.2)
'@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
eslint: 9.8.0
- eslint-plugin-import: 2.30.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)
+ eslint-plugin-import: 2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0)
globals: 15.9.0
'@misskey-dev/sharp-read-bmp@1.2.0':
@@ -13788,7 +13821,7 @@ snapshots:
'@napi-rs/canvas-linux-x64-musl': 0.1.56
'@napi-rs/canvas-win32-x64-msvc': 0.1.56
- '@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+ '@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)':
dependencies:
iterare: 1.2.1
reflect-metadata: 0.2.2
@@ -13796,9 +13829,9 @@ snapshots:
tslib: 2.7.0
uid: 2.0.2
- '@nestjs/core@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
+ '@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)':
dependencies:
- '@nestjs/common': 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
'@nuxtjs/opencollective': 0.3.2(encoding@0.1.13)
fast-safe-stringify: 2.1.1
iterare: 1.2.1
@@ -13808,14 +13841,14 @@ snapshots:
tslib: 2.7.0
uid: 2.0.2
optionalDependencies:
- '@nestjs/platform-express': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3)
+ '@nestjs/platform-express': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)
transitivePeerDependencies:
- encoding
- '@nestjs/platform-express@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3)':
+ '@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)':
dependencies:
- '@nestjs/common': 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
body-parser: 1.20.3
cors: 2.8.5
express: 4.21.0
@@ -13824,15 +13857,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@nestjs/testing@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3))':
+ '@nestjs/testing@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4))':
dependencies:
- '@nestjs/common': 10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1)
- '@nestjs/core': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.3)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/common': 10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1)
+ '@nestjs/core': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/platform-express@10.4.4)(encoding@0.1.13)(reflect-metadata@0.2.2)(rxjs@7.8.1)
tslib: 2.7.0
optionalDependencies:
- '@nestjs/platform-express': 10.4.3(@nestjs/common@10.4.3(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.3)
+ '@nestjs/platform-express': 10.4.4(@nestjs/common@10.4.4(reflect-metadata@0.2.2)(rxjs@7.8.1))(@nestjs/core@10.4.4)
- '@noble/hashes@1.4.0': {}
+ '@noble/hashes@1.5.0': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
@@ -13858,7 +13891,7 @@ snapshots:
'@npmcli/fs@3.1.0':
dependencies:
- semver: 7.6.0
+ semver: 7.6.3
'@nuxtjs/opencollective@0.3.2(encoding@0.1.13)':
dependencies:
@@ -14358,9 +14391,32 @@ snapshots:
dependencies:
'@sentry/types': 8.20.0
- '@shikijs/core@1.12.0':
+ '@shikijs/core@1.21.0':
dependencies:
+ '@shikijs/engine-javascript': 1.21.0
+ '@shikijs/engine-oniguruma': 1.21.0
+ '@shikijs/types': 1.21.0
+ '@shikijs/vscode-textmate': 9.3.0
'@types/hast': 3.0.4
+ hast-util-to-html: 9.0.3
+
+ '@shikijs/engine-javascript@1.21.0':
+ dependencies:
+ '@shikijs/types': 1.21.0
+ '@shikijs/vscode-textmate': 9.3.0
+ oniguruma-to-js: 0.4.3
+
+ '@shikijs/engine-oniguruma@1.21.0':
+ dependencies:
+ '@shikijs/types': 1.21.0
+ '@shikijs/vscode-textmate': 9.3.0
+
+ '@shikijs/types@1.21.0':
+ dependencies:
+ '@shikijs/vscode-textmate': 9.3.0
+ '@types/hast': 3.0.4
+
+ '@shikijs/vscode-textmate@9.3.0': {}
'@sideway/address@4.1.4':
dependencies:
@@ -14437,11 +14493,11 @@ snapshots:
'@smithy/chunked-blob-reader-native@3.0.0':
dependencies:
'@smithy/util-base64': 3.0.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/chunked-blob-reader@3.0.0':
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/config-resolver@3.0.5':
dependencies:
@@ -14468,14 +14524,14 @@ snapshots:
'@smithy/property-provider': 3.1.3
'@smithy/types': 3.3.0
'@smithy/url-parser': 3.0.3
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/eventstream-codec@3.1.2':
dependencies:
'@aws-crypto/crc32': 5.2.0
'@smithy/types': 3.3.0
'@smithy/util-hex-encoding': 3.0.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/eventstream-serde-browser@3.0.5':
dependencies:
@@ -14498,7 +14554,7 @@ snapshots:
dependencies:
'@smithy/eventstream-codec': 3.1.2
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/fetch-http-handler@3.2.4':
dependencies:
@@ -14535,11 +14591,11 @@ snapshots:
'@smithy/is-array-buffer@2.0.0':
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/is-array-buffer@3.0.0':
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/md5-js@3.0.3':
dependencies:
@@ -14611,7 +14667,7 @@ snapshots:
'@smithy/property-provider@3.1.3':
dependencies:
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/protocol-http@3.3.0':
dependencies:
@@ -14633,12 +14689,12 @@ snapshots:
dependencies:
'@smithy/types': 3.3.0
'@smithy/util-uri-escape': 3.0.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/querystring-parser@3.0.3':
dependencies:
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/service-error-classification@3.0.3':
dependencies:
@@ -14647,7 +14703,7 @@ snapshots:
'@smithy/shared-ini-file-loader@3.1.4':
dependencies:
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/signature-v4@4.1.0':
dependencies:
@@ -14658,7 +14714,7 @@ snapshots:
'@smithy/util-middleware': 3.0.3
'@smithy/util-uri-escape': 3.0.0
'@smithy/util-utf8': 3.0.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/smithy-client@3.1.11':
dependencies:
@@ -14700,16 +14756,16 @@ snapshots:
'@smithy/util-buffer-from@2.0.0':
dependencies:
'@smithy/is-array-buffer': 2.0.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/util-buffer-from@3.0.0':
dependencies:
'@smithy/is-array-buffer': 3.0.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/util-config-provider@3.0.0':
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/util-defaults-mode-browser@3.0.13':
dependencies:
@@ -14737,12 +14793,12 @@ snapshots:
'@smithy/util-hex-encoding@3.0.0':
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/util-middleware@3.0.3':
dependencies:
'@smithy/types': 3.3.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/util-retry@3.0.3':
dependencies:
@@ -14763,16 +14819,16 @@ snapshots:
'@smithy/util-uri-escape@2.2.0':
dependencies:
- tslib: 2.6.2
+ tslib: 2.7.0
'@smithy/util-uri-escape@3.0.0':
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/util-utf8@2.0.0':
dependencies:
'@smithy/util-buffer-from': 2.0.0
- tslib: 2.6.3
+ tslib: 2.7.0
'@smithy/util-utf8@3.0.0':
dependencies:
@@ -14787,120 +14843,120 @@ snapshots:
'@sqltools/formatter@1.2.5': {}
- '@storybook/addon-actions@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-actions@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/global': 5.0.0
'@types/uuid': 9.0.8
dequal: 2.0.3
polished: 4.2.2
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
uuid: 9.0.1
- '@storybook/addon-backgrounds@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-backgrounds@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/global': 5.0.0
memoizerific: 1.11.3
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
- '@storybook/addon-controls@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-controls@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/global': 5.0.0
dequal: 2.0.3
lodash: 4.17.21
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
- '@storybook/addon-docs@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-docs@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@mdx-js/react': 3.0.1(@types/react@18.0.28)(react@18.3.1)
- '@storybook/blocks': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/csf-plugin': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/blocks': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/csf-plugin': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/global': 5.0.0
- '@storybook/react-dom-shim': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/react-dom-shim': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@types/react': 18.0.28
fs-extra: 11.1.1
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
rehype-external-links: 3.0.0
rehype-slug: 6.0.0
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
- '@storybook/addon-essentials@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-essentials@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
- '@storybook/addon-actions': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/addon-backgrounds': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/addon-controls': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/addon-docs': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/addon-highlight': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/addon-measure': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/addon-outline': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/addon-toolbars': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/addon-viewport': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ '@storybook/addon-actions': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/addon-backgrounds': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/addon-controls': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/addon-docs': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/addon-highlight': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/addon-measure': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/addon-outline': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/addon-toolbars': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/addon-viewport': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
- '@storybook/addon-highlight@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-highlight@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/global': 5.0.0
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/addon-interactions@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-interactions@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/global': 5.0.0
- '@storybook/instrumenter': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/test': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/instrumenter': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/test': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
polished: 4.2.2
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
- '@storybook/addon-links@8.3.3(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-links@8.3.4(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/csf': 0.1.11
'@storybook/global': 5.0.0
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
optionalDependencies:
react: 18.3.1
- '@storybook/addon-mdx-gfm@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-mdx-gfm@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
remark-gfm: 4.0.0
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
transitivePeerDependencies:
- supports-color
- '@storybook/addon-measure@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-measure@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/global': 5.0.0
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
tiny-invariant: 1.3.3
- '@storybook/addon-outline@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-outline@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/global': 5.0.0
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
- '@storybook/addon-storysource@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-storysource@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
- '@storybook/source-loader': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/source-loader': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
estraverse: 5.3.0
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
tiny-invariant: 1.3.3
- '@storybook/addon-toolbars@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-toolbars@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/addon-viewport@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/addon-viewport@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
memoizerific: 1.11.3
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/blocks@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/blocks@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/csf': 0.1.11
'@storybook/global': 5.0.0
@@ -14913,7 +14969,7 @@ snapshots:
memoizerific: 1.11.3
polished: 4.2.2
react-colorful: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
telejson: 7.2.0
ts-dedent: 2.2.0
util-deprecate: 1.0.2
@@ -14921,17 +14977,17 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
- '@storybook/builder-vite@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
+ '@storybook/builder-vite@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
dependencies:
- '@storybook/csf-plugin': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/csf-plugin': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@types/find-cache-dir': 3.2.1
browser-assert: 1.2.1
es-module-lexer: 1.5.4
- express: 4.19.2
+ express: 4.21.0
find-cache-dir: 3.3.2
fs-extra: 11.1.1
- magic-string: 0.30.10
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ magic-string: 0.30.11
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
optionalDependencies:
@@ -14939,15 +14995,15 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@storybook/components@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/components@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/core-events@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/core-events@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/core@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
+ '@storybook/core@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)':
dependencies:
'@storybook/csf': 0.1.11
'@types/express': 4.17.21
@@ -14955,7 +15011,7 @@ snapshots:
browser-assert: 1.2.1
esbuild: 0.23.1
esbuild-register: 3.5.0(esbuild@0.23.1)
- express: 4.19.2
+ express: 4.21.0
jsdoc-type-pratt-parser: 4.1.0
process: 0.11.10
recast: 0.23.6
@@ -14967,9 +15023,9 @@ snapshots:
- supports-color
- utf-8-validate
- '@storybook/csf-plugin@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/csf-plugin@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
unplugin: 1.4.0
'@storybook/csf@0.1.11':
@@ -14983,40 +15039,40 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
- '@storybook/instrumenter@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/instrumenter@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/global': 5.0.0
'@vitest/utils': 2.1.2
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
util: 0.12.5
- '@storybook/manager-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/manager-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/preview-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/preview-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/react-dom-shim@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/react-dom-shim@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/react-vite@8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
+ '@storybook/react-vite@8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(rollup@4.22.5)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))':
dependencies:
'@joshwooding/vite-plugin-react-docgen-typescript': 0.3.0(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
'@rollup/pluginutils': 5.1.2(rollup@4.22.5)
- '@storybook/builder-vite': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
- '@storybook/react': 8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)
+ '@storybook/builder-vite': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
+ '@storybook/react': 8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)
find-up: 5.0.0
- magic-string: 0.30.10
+ magic-string: 0.30.11
react: 18.3.1
react-docgen: 7.0.1
react-dom: 18.3.1(react@18.3.1)
resolve: 1.22.8
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
tsconfig-paths: 4.2.0
vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
transitivePeerDependencies:
@@ -15027,14 +15083,14 @@ snapshots:
- typescript
- vite-plugin-glimmerx
- '@storybook/react@8.3.3(@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)':
+ '@storybook/react@8.3.4(@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)':
dependencies:
- '@storybook/components': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/components': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/global': 5.0.0
- '@storybook/manager-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/preview-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/react-dom-shim': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/theming': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/manager-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/preview-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/react-dom-shim': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/theming': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@types/escodegen': 0.0.6
'@types/estree': 0.0.51
'@types/node': 22.7.5
@@ -15047,74 +15103,74 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-element-to-jsx-string: 15.0.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
- semver: 7.6.0
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ semver: 7.6.3
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
type-fest: 2.19.0
util-deprecate: 1.0.2
optionalDependencies:
- '@storybook/test': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/test': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
typescript: 5.6.2
- '@storybook/source-loader@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/source-loader@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/csf': 0.1.11
estraverse: 5.3.0
lodash: 4.17.21
prettier: 3.3.3
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/test@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/test@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
'@storybook/csf': 0.1.11
'@storybook/global': 5.0.0
- '@storybook/instrumenter': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/instrumenter': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@testing-library/dom': 10.4.0
'@testing-library/jest-dom': 6.5.0
'@testing-library/user-event': 14.5.2(@testing-library/dom@10.4.0)
'@vitest/expect': 2.0.5
'@vitest/spy': 2.0.5
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
util: 0.12.5
- '@storybook/theming@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/theming@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/types@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
+ '@storybook/types@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))':
dependencies:
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- '@storybook/vue3-vite@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))':
+ '@storybook/vue3-vite@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))':
dependencies:
- '@storybook/builder-vite': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
- '@storybook/vue3': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.10(typescript@5.6.2))
+ '@storybook/builder-vite': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(typescript@5.6.2)(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))
+ '@storybook/vue3': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.11(typescript@5.6.2))
find-package-json: 1.2.0
- magic-string: 0.30.10
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ magic-string: 0.30.11
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
typescript: 5.6.2
vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
vue-component-meta: 2.0.16(typescript@5.6.2)
- vue-docgen-api: 4.75.1(vue@3.5.10(typescript@5.6.2))
+ vue-docgen-api: 4.75.1(vue@3.5.11(typescript@5.6.2))
transitivePeerDependencies:
- '@preact/preset-vite'
- supports-color
- vite-plugin-glimmerx
- vue
- '@storybook/vue3@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.10(typescript@5.6.2))':
+ '@storybook/vue3@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))(vue@3.5.11(typescript@5.6.2))':
dependencies:
- '@storybook/components': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/components': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
'@storybook/global': 5.0.0
- '@storybook/manager-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/preview-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/theming': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@vue/compiler-core': 3.4.37
- storybook: 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ '@storybook/manager-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/preview-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/theming': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@vue/compiler-core': 3.5.10
+ storybook: 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
ts-dedent: 2.2.0
type-fest: 2.19.0
- vue: 3.5.10(typescript@5.6.2)
- vue-component-type-helpers: 2.1.6
+ vue: 3.5.11(typescript@5.6.2)
+ vue-component-type-helpers: 2.1.10
'@swc/cli@0.3.12(@swc/core@1.6.6)(chokidar@3.5.3)':
dependencies:
@@ -15336,14 +15392,14 @@ snapshots:
dependencies:
'@testing-library/dom': 10.4.0
- '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.10)(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))':
+ '@testing-library/vue@8.1.0(@vue/compiler-sfc@3.5.11)(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))':
dependencies:
'@babel/runtime': 7.23.4
'@testing-library/dom': 9.3.4
- '@vue/test-utils': 2.4.1(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))
- vue: 3.5.10(typescript@5.6.2)
+ '@vue/test-utils': 2.4.1(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))
+ vue: 3.5.11(typescript@5.6.2)
optionalDependencies:
- '@vue/compiler-sfc': 3.5.10
+ '@vue/compiler-sfc': 3.5.11
transitivePeerDependencies:
- '@vue/server-renderer'
@@ -15377,24 +15433,24 @@ snapshots:
'@types/babel__core@7.20.0':
dependencies:
- '@babel/parser': 7.24.7
- '@babel/types': 7.24.7
+ '@babel/parser': 7.25.7
+ '@babel/types': 7.25.7
'@types/babel__generator': 7.6.4
'@types/babel__template': 7.4.1
'@types/babel__traverse': 7.20.0
'@types/babel__generator@7.6.4':
dependencies:
- '@babel/types': 7.24.7
+ '@babel/types': 7.25.7
'@types/babel__template@7.4.1':
dependencies:
- '@babel/parser': 7.24.7
- '@babel/types': 7.24.7
+ '@babel/parser': 7.25.7
+ '@babel/types': 7.25.7
'@types/babel__traverse@7.20.0':
dependencies:
- '@babel/types': 7.24.7
+ '@babel/types': 7.25.7
'@types/bcryptjs@2.4.6': {}
@@ -15412,7 +15468,9 @@ snapshots:
'@types/node': 20.14.12
'@types/responselike': 1.0.0
- '@types/color-convert@2.0.3':
+ '@types/canvas-confetti@1.6.4': {}
+
+ '@types/color-convert@2.0.4':
dependencies:
'@types/color-name': 1.1.1
@@ -16079,10 +16137,15 @@ snapshots:
'@ungap/structured-clone@1.2.0': {}
- '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.10(typescript@5.6.2))':
+ '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))':
dependencies:
vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
- vue: 3.5.10(typescript@5.6.2)
+ vue: 3.5.11(typescript@5.6.2)
+
+ '@vitejs/plugin-vue@5.1.4(vite@5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0))(vue@3.5.11(typescript@5.6.2))':
+ dependencies:
+ vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+ vue: 3.5.11(typescript@5.6.2)
'@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0))':
dependencies:
@@ -16103,7 +16166,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0))':
+ '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0))':
dependencies:
'@ampproject/remapping': 2.2.1
'@bcoe/v8-coverage': 0.2.3
@@ -16118,7 +16181,7 @@ snapshots:
std-env: 3.7.0
strip-literal: 2.1.0
test-exclude: 6.0.0
- vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0)
+ vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0)
transitivePeerDependencies:
- supports-color
@@ -16210,11 +16273,11 @@ snapshots:
'@vue/compiler-core@3.4.37':
dependencies:
- '@babel/parser': 7.24.7
+ '@babel/parser': 7.25.7
'@vue/shared': 3.4.37
entities: 5.0.0
estree-walker: 2.0.2
- source-map-js: 1.2.0
+ source-map-js: 1.2.1
'@vue/compiler-core@3.5.10':
dependencies:
@@ -16222,7 +16285,15 @@ snapshots:
'@vue/shared': 3.5.10
entities: 4.5.0
estree-walker: 2.0.2
- source-map-js: 1.2.0
+ source-map-js: 1.2.1
+
+ '@vue/compiler-core@3.5.11':
+ dependencies:
+ '@babel/parser': 7.25.7
+ '@vue/shared': 3.5.11
+ entities: 4.5.0
+ estree-walker: 2.0.2
+ source-map-js: 1.2.1
'@vue/compiler-dom@3.4.37':
dependencies:
@@ -16234,39 +16305,44 @@ snapshots:
'@vue/compiler-core': 3.5.10
'@vue/shared': 3.5.10
+ '@vue/compiler-dom@3.5.11':
+ dependencies:
+ '@vue/compiler-core': 3.5.11
+ '@vue/shared': 3.5.11
+
'@vue/compiler-sfc@3.4.37':
dependencies:
- '@babel/parser': 7.24.7
+ '@babel/parser': 7.25.7
'@vue/compiler-core': 3.4.37
'@vue/compiler-dom': 3.4.37
'@vue/compiler-ssr': 3.4.37
'@vue/shared': 3.4.37
estree-walker: 2.0.2
- magic-string: 0.30.10
+ magic-string: 0.30.11
postcss: 8.4.47
- source-map-js: 1.2.0
+ source-map-js: 1.2.1
- '@vue/compiler-sfc@3.5.10':
+ '@vue/compiler-sfc@3.5.11':
dependencies:
'@babel/parser': 7.25.7
- '@vue/compiler-core': 3.5.10
- '@vue/compiler-dom': 3.5.10
- '@vue/compiler-ssr': 3.5.10
- '@vue/shared': 3.5.10
+ '@vue/compiler-core': 3.5.11
+ '@vue/compiler-dom': 3.5.11
+ '@vue/compiler-ssr': 3.5.11
+ '@vue/shared': 3.5.11
estree-walker: 2.0.2
magic-string: 0.30.11
postcss: 8.4.47
- source-map-js: 1.2.0
+ source-map-js: 1.2.1
'@vue/compiler-ssr@3.4.37':
dependencies:
'@vue/compiler-dom': 3.4.37
'@vue/shared': 3.4.37
- '@vue/compiler-ssr@3.5.10':
+ '@vue/compiler-ssr@3.5.11':
dependencies:
- '@vue/compiler-dom': 3.5.10
- '@vue/shared': 3.5.10
+ '@vue/compiler-dom': 3.5.11
+ '@vue/shared': 3.5.11
'@vue/compiler-vue2@2.7.16':
dependencies:
@@ -16276,8 +16352,8 @@ snapshots:
'@vue/language-core@2.0.16(typescript@5.6.2)':
dependencies:
'@volar/language-core': 2.2.0
- '@vue/compiler-dom': 3.4.37
- '@vue/shared': 3.4.37
+ '@vue/compiler-dom': 3.5.10
+ '@vue/shared': 3.5.10
computeds: 0.0.1
minimatch: 9.0.4
path-browserify: 1.0.1
@@ -16302,19 +16378,19 @@ snapshots:
dependencies:
'@vue/shared': 3.4.37
- '@vue/reactivity@3.5.10':
+ '@vue/reactivity@3.5.11':
dependencies:
- '@vue/shared': 3.5.10
+ '@vue/shared': 3.5.11
'@vue/runtime-core@3.4.37':
dependencies:
'@vue/reactivity': 3.4.37
'@vue/shared': 3.4.37
- '@vue/runtime-core@3.5.10':
+ '@vue/runtime-core@3.5.11':
dependencies:
- '@vue/reactivity': 3.5.10
- '@vue/shared': 3.5.10
+ '@vue/reactivity': 3.5.11
+ '@vue/shared': 3.5.11
'@vue/runtime-dom@3.4.37':
dependencies:
@@ -16323,11 +16399,11 @@ snapshots:
'@vue/shared': 3.4.37
csstype: 3.1.3
- '@vue/runtime-dom@3.5.10':
+ '@vue/runtime-dom@3.5.11':
dependencies:
- '@vue/reactivity': 3.5.10
- '@vue/runtime-core': 3.5.10
- '@vue/shared': 3.5.10
+ '@vue/reactivity': 3.5.11
+ '@vue/runtime-core': 3.5.11
+ '@vue/shared': 3.5.11
csstype: 3.1.3
'@vue/server-renderer@3.4.37(vue@3.4.37(typescript@5.5.4))':
@@ -16336,23 +16412,25 @@ snapshots:
'@vue/shared': 3.4.37
vue: 3.4.37(typescript@5.5.4)
- '@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2))':
+ '@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2))':
dependencies:
- '@vue/compiler-ssr': 3.5.10
- '@vue/shared': 3.5.10
- vue: 3.5.10(typescript@5.6.2)
+ '@vue/compiler-ssr': 3.5.11
+ '@vue/shared': 3.5.11
+ vue: 3.5.11(typescript@5.6.2)
'@vue/shared@3.4.37': {}
'@vue/shared@3.5.10': {}
- '@vue/test-utils@2.4.1(@vue/server-renderer@3.5.10(vue@3.5.10(typescript@5.6.2)))(vue@3.5.10(typescript@5.6.2))':
+ '@vue/shared@3.5.11': {}
+
+ '@vue/test-utils@2.4.1(@vue/server-renderer@3.5.11(vue@3.5.11(typescript@5.6.2)))(vue@3.5.11(typescript@5.6.2))':
dependencies:
js-beautify: 1.14.9
- vue: 3.5.10(typescript@5.6.2)
+ vue: 3.5.11(typescript@5.6.2)
vue-component-type-helpers: 1.8.4
optionalDependencies:
- '@vue/server-renderer': 3.5.10(vue@3.5.10(typescript@5.6.2))
+ '@vue/server-renderer': 3.5.11(vue@3.5.11(typescript@5.6.2))
abbrev@1.1.1: {}
@@ -16623,7 +16701,7 @@ snapshots:
dependencies:
pvtsutils: 1.3.5
pvutils: 1.1.3
- tslib: 2.6.3
+ tslib: 2.7.0
assert-never@1.2.1: {}
@@ -16635,7 +16713,7 @@ snapshots:
ast-types@0.16.1:
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
astral-regex@2.0.0: {}
@@ -16740,7 +16818,7 @@ snapshots:
babel-plugin-jest-hoist@29.6.3:
dependencies:
'@babel/template': 7.24.0
- '@babel/types': 7.24.7
+ '@babel/types': 7.25.7
'@types/babel__core': 7.20.0
'@types/babel__traverse': 7.20.0
@@ -16836,23 +16914,6 @@ snapshots:
bn.js@4.12.0: {}
- body-parser@1.20.2:
- dependencies:
- bytes: 3.1.2
- content-type: 1.0.5
- debug: 2.6.9
- depd: 2.0.0
- destroy: 1.2.0
- http-errors: 2.0.0
- iconv-lite: 0.4.24
- on-finished: 2.4.1
- qs: 6.11.0
- raw-body: 2.5.2
- type-is: 1.6.18
- unpipe: 1.0.0
- transitivePeerDependencies:
- - supports-color
-
body-parser@1.20.3:
dependencies:
bytes: 3.1.2
@@ -16955,14 +17016,14 @@ snapshots:
node-gyp-build: 4.8.1
optional: true
- bullmq@5.13.2:
+ bullmq@5.15.0:
dependencies:
cron-parser: 4.8.1
ioredis: 5.4.1
msgpackr: 1.10.1
node-abort-controller: 3.1.1
- semver: 7.6.0
- tslib: 2.6.3
+ semver: 7.6.3
+ tslib: 2.7.0
uuid: 9.0.1
transitivePeerDependencies:
- supports-color
@@ -17120,6 +17181,10 @@ snapshots:
char-regex@1.0.2: {}
+ character-entities-html4@2.1.0: {}
+
+ character-entities-legacy@3.0.0: {}
+
character-entities@2.0.2: {}
character-parser@2.2.0:
@@ -17203,7 +17268,7 @@ snapshots:
chownr@2.0.0: {}
- chromatic@11.10.4: {}
+ chromatic@11.11.0: {}
ci-info@3.7.1: {}
@@ -17303,6 +17368,8 @@ snapshots:
dependencies:
delayed-stream: 1.0.0
+ comma-separated-tokens@2.0.3: {}
+
commander@10.0.1: {}
commander@12.1.0: {}
@@ -17890,7 +17957,7 @@ snapshots:
'@one-ini/wasm': 0.1.1
commander: 10.0.1
minimatch: 9.0.1
- semver: 7.6.0
+ semver: 7.6.3
ee-first@1.1.1: {}
@@ -18087,7 +18154,7 @@ snapshots:
esbuild-register@3.5.0(esbuild@0.23.1):
dependencies:
- debug: 4.3.5(supports-color@8.1.1)
+ debug: 4.3.7
esbuild: 0.23.1
transitivePeerDependencies:
- supports-color
@@ -18308,6 +18375,35 @@ snapshots:
- eslint-import-resolver-webpack
- supports-color
+ eslint-plugin-import@2.31.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint@9.8.0):
+ dependencies:
+ '@rtsao/scc': 1.1.0
+ array-includes: 3.1.8
+ array.prototype.findlastindex: 1.2.5
+ array.prototype.flat: 1.3.2
+ array.prototype.flatmap: 1.3.2
+ debug: 3.2.7(supports-color@8.1.1)
+ doctrine: 2.1.0
+ eslint: 9.8.0
+ eslint-import-resolver-node: 0.3.9
+ eslint-module-utils: 2.12.0(@typescript-eslint/parser@7.17.0(eslint@9.8.0)(typescript@5.6.2))(eslint-import-resolver-node@0.3.9)(eslint@9.8.0)
+ hasown: 2.0.2
+ is-core-module: 2.15.1
+ is-glob: 4.0.3
+ minimatch: 3.1.2
+ object.fromentries: 2.0.8
+ object.groupby: 1.0.3
+ object.values: 1.2.0
+ semver: 6.3.1
+ string.prototype.trimend: 1.0.8
+ tsconfig-paths: 3.15.0
+ optionalDependencies:
+ '@typescript-eslint/parser': 7.17.0(eslint@9.8.0)(typescript@5.6.2)
+ transitivePeerDependencies:
+ - eslint-import-resolver-typescript
+ - eslint-import-resolver-webpack
+ - supports-color
+
eslint-plugin-vue@9.27.0(eslint@9.8.0):
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@9.8.0)
@@ -18581,42 +18677,6 @@ snapshots:
exponential-backoff@3.1.1: {}
- express@4.19.2:
- dependencies:
- accepts: 1.3.8
- array-flatten: 1.1.1
- body-parser: 1.20.2
- content-disposition: 0.5.4
- content-type: 1.0.5
- cookie: 0.6.0
- cookie-signature: 1.0.6
- debug: 2.6.9
- depd: 2.0.0
- encodeurl: 1.0.2
- escape-html: 1.0.3
- etag: 1.8.1
- finalhandler: 1.2.0
- fresh: 0.5.2
- http-errors: 2.0.0
- merge-descriptors: 1.0.1
- methods: 1.1.2
- on-finished: 2.4.1
- parseurl: 1.3.3
- path-to-regexp: 0.1.7
- proxy-addr: 2.0.7
- qs: 6.11.0
- range-parser: 1.2.1
- safe-buffer: 5.2.1
- send: 0.18.0
- serve-static: 1.15.0
- setprototypeof: 1.2.0
- statuses: 2.0.1
- type-is: 1.6.18
- utils-merge: 1.0.1
- vary: 1.1.2
- transitivePeerDependencies:
- - supports-color
-
express@4.21.0:
dependencies:
accepts: 1.3.8
@@ -18839,18 +18899,6 @@ snapshots:
dependencies:
to-regex-range: 5.0.1
- finalhandler@1.2.0:
- dependencies:
- debug: 2.6.9
- encodeurl: 1.0.2
- escape-html: 1.0.3
- on-finished: 2.4.1
- parseurl: 1.3.3
- statuses: 2.0.1
- unpipe: 1.0.0
- transitivePeerDependencies:
- - supports-color
-
finalhandler@1.3.1:
dependencies:
debug: 2.6.9
@@ -19271,10 +19319,28 @@ snapshots:
dependencies:
'@types/hast': 3.0.4
+ hast-util-to-html@9.0.3:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/unist': 3.0.2
+ ccount: 2.0.1
+ comma-separated-tokens: 2.0.3
+ hast-util-whitespace: 3.0.0
+ html-void-elements: 3.0.0
+ mdast-util-to-hast: 13.2.0
+ property-information: 6.5.0
+ space-separated-tokens: 2.0.2
+ stringify-entities: 4.0.4
+ zwitch: 2.0.4
+
hast-util-to-string@3.0.0:
dependencies:
'@types/hast': 3.0.4
+ hast-util-whitespace@3.0.0:
+ dependencies:
+ '@types/hast': 3.0.4
+
he@1.2.0: {}
headers-polyfill@4.0.2: {}
@@ -19301,6 +19367,8 @@ snapshots:
html-tags@3.2.0: {}
+ html-void-elements@3.0.0: {}
+
htmlescape@1.1.1: {}
htmlparser2@5.0.1:
@@ -19576,7 +19644,7 @@ snapshots:
is-generator-function@1.0.10:
dependencies:
- has-tostringtag: 1.0.0
+ has-tostringtag: 1.0.2
is-glob@4.0.3:
dependencies:
@@ -19714,10 +19782,10 @@ snapshots:
istanbul-lib-instrument@6.0.0:
dependencies:
'@babel/core': 7.24.7
- '@babel/parser': 7.24.7
+ '@babel/parser': 7.25.7
'@istanbuljs/schema': 0.1.3
istanbul-lib-coverage: 3.2.2
- semver: 7.6.0
+ semver: 7.6.3
transitivePeerDependencies:
- supports-color
@@ -19729,7 +19797,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
- debug: 4.3.5(supports-color@8.1.1)
+ debug: 4.3.7
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@@ -20467,7 +20535,7 @@ snapshots:
magic-string@0.27.0:
dependencies:
- '@jridgewell/sourcemap-codec': 1.4.15
+ '@jridgewell/sourcemap-codec': 1.5.0
magic-string@0.30.10:
dependencies:
@@ -20619,6 +20687,18 @@ snapshots:
'@types/mdast': 4.0.3
unist-util-is: 6.0.0
+ mdast-util-to-hast@13.2.0:
+ dependencies:
+ '@types/hast': 3.0.4
+ '@types/mdast': 4.0.3
+ '@ungap/structured-clone': 1.2.0
+ devlop: 1.1.0
+ micromark-util-sanitize-uri: 2.0.0
+ trim-lines: 3.0.1
+ unist-util-position: 5.0.0
+ unist-util-visit: 5.0.0
+ vfile: 6.0.1
+
mdast-util-to-markdown@2.1.0:
dependencies:
'@types/mdast': 4.0.3
@@ -20667,8 +20747,6 @@ snapshots:
type-fest: 0.18.1
yargs-parser: 20.2.9
- merge-descriptors@1.0.1: {}
-
merge-descriptors@1.0.3: {}
merge-stream@2.0.0: {}
@@ -20853,7 +20931,7 @@ snapshots:
micromark@4.0.0:
dependencies:
'@types/debug': 4.1.12
- debug: 4.3.5(supports-color@8.1.1)
+ debug: 4.3.7
decode-named-character-reference: 1.0.2
devlop: 1.1.0
micromark-core-commonmark: 2.0.0
@@ -21359,6 +21437,10 @@ snapshots:
dependencies:
mimic-fn: 4.0.0
+ oniguruma-to-js@0.4.3:
+ dependencies:
+ regex: 4.4.0
+
open@8.4.2:
dependencies:
define-lazy-prop: 2.0.0
@@ -21402,9 +21484,9 @@ snapshots:
ospath@1.2.2: {}
- otpauth@9.3.2:
+ otpauth@9.3.4:
dependencies:
- '@noble/hashes': 1.4.0
+ '@noble/hashes': 1.5.0
outvariant@1.4.2: {}
@@ -21523,8 +21605,6 @@ snapshots:
path-to-regexp@0.1.10: {}
- path-to-regexp@0.1.7: {}
-
path-to-regexp@1.8.0:
dependencies:
isarray: 0.0.1
@@ -21834,12 +21914,6 @@ snapshots:
postcss-value-parser@4.2.0: {}
- postcss@8.4.38:
- dependencies:
- nanoid: 3.3.7
- picocolors: 1.0.0
- source-map-js: 1.2.0
-
postcss@8.4.47:
dependencies:
nanoid: 3.3.7
@@ -21943,6 +22017,8 @@ snapshots:
object-assign: 4.1.1
react-is: 16.13.1
+ property-information@6.5.0: {}
+
proto-list@1.2.4: {}
proxy-addr@2.0.7:
@@ -22046,7 +22122,7 @@ snapshots:
pvtsutils@1.3.5:
dependencies:
- tslib: 2.6.3
+ tslib: 2.7.0
pvutils@1.1.3: {}
@@ -22056,10 +22132,6 @@ snapshots:
pngjs: 5.0.0
yargs: 15.4.1
- qs@6.11.0:
- dependencies:
- side-channel: 1.0.4
-
qs@6.13.0:
dependencies:
side-channel: 1.0.6
@@ -22125,7 +22197,7 @@ snapshots:
dependencies:
'@babel/core': 7.24.7
'@babel/traverse': 7.24.7
- '@babel/types': 7.24.7
+ '@babel/types': 7.25.7
'@types/babel__core': 7.20.0
'@types/babel__traverse': 7.20.0
'@types/doctrine': 0.0.9
@@ -22218,7 +22290,7 @@ snapshots:
esprima: 4.0.1
source-map: 0.6.1
tiny-invariant: 1.3.3
- tslib: 2.6.3
+ tslib: 2.7.0
reconnecting-websocket@4.4.0: {}
@@ -22243,6 +22315,8 @@ snapshots:
regenerator-runtime@0.14.0: {}
+ regex@4.4.0: {}
+
regexp.prototype.flags@1.5.0:
dependencies:
call-bind: 1.0.2
@@ -22449,14 +22523,14 @@ snapshots:
safer-buffer@2.1.2: {}
- sanitize-html@2.13.0:
+ sanitize-html@2.13.1:
dependencies:
deepmerge: 4.2.2
escape-string-regexp: 4.0.0
htmlparser2: 8.0.1
is-plain-object: 5.0.0
parse-srcset: 1.0.2
- postcss: 8.4.38
+ postcss: 8.4.47
sass@1.79.3:
dependencies:
@@ -22464,6 +22538,12 @@ snapshots:
immutable: 4.2.2
source-map-js: 1.2.0
+ sass@1.79.4:
+ dependencies:
+ chokidar: 3.5.3
+ immutable: 4.2.2
+ source-map-js: 1.2.1
+
sax@1.2.4: {}
saxes@6.0.0:
@@ -22500,24 +22580,6 @@ snapshots:
semver@7.6.3: {}
- send@0.18.0:
- dependencies:
- debug: 2.6.9
- depd: 2.0.0
- destroy: 1.2.0
- encodeurl: 1.0.2
- escape-html: 1.0.3
- etag: 1.8.1
- fresh: 0.5.2
- http-errors: 2.0.0
- mime: 1.6.0
- ms: 2.1.3
- on-finished: 2.4.1
- range-parser: 1.2.1
- statuses: 2.0.1
- transitivePeerDependencies:
- - supports-color
-
send@0.19.0:
dependencies:
debug: 2.6.9
@@ -22536,15 +22598,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- serve-static@1.15.0:
- dependencies:
- encodeurl: 1.0.2
- escape-html: 1.0.3
- parseurl: 1.3.3
- send: 0.18.0
- transitivePeerDependencies:
- - supports-color
-
serve-static@1.16.2:
dependencies:
encodeurl: 2.0.0
@@ -22628,9 +22681,13 @@ snapshots:
vscode-oniguruma: 1.7.0
vscode-textmate: 8.0.0
- shiki@1.12.0:
+ shiki@1.21.0:
dependencies:
- '@shikijs/core': 1.12.0
+ '@shikijs/core': 1.21.0
+ '@shikijs/engine-javascript': 1.21.0
+ '@shikijs/engine-oniguruma': 1.21.0
+ '@shikijs/types': 1.21.0
+ '@shikijs/vscode-textmate': 9.3.0
'@types/hast': 3.0.4
shimmer@1.2.1: {}
@@ -22883,22 +22940,22 @@ snapshots:
dependencies:
internal-slot: 1.0.5
- storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
+ storybook-addon-misskey-theme@https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/components@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/core-events@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/manager-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/preview-api@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/theming@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(@storybook/types@8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)))(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
- '@storybook/blocks': 8.3.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/components': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/core-events': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/manager-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/preview-api': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/theming': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
- '@storybook/types': 8.3.3(storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/blocks': 8.3.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/components': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/core-events': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/manager-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/preview-api': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/theming': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
+ '@storybook/types': 8.3.4(storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4))
optionalDependencies:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
- storybook@8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4):
+ storybook@8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4):
dependencies:
- '@storybook/core': 8.3.3(bufferutil@4.0.8)(utf-8-validate@6.0.4)
+ '@storybook/core': 8.3.4(bufferutil@4.0.8)(utf-8-validate@6.0.4)
transitivePeerDependencies:
- bufferutil
- supports-color
@@ -22994,6 +23051,11 @@ snapshots:
dependencies:
safe-buffer: 5.2.1
+ stringify-entities@4.0.4:
+ dependencies:
+ character-entities-html4: 2.1.0
+ character-entities-legacy: 3.0.0
+
stringz@2.1.0:
dependencies:
char-regex: 1.0.2
@@ -23210,6 +23272,8 @@ snapshots:
trace-redirect@1.0.6: {}
+ trim-lines@3.0.1: {}
+
trim-newlines@3.0.1: {}
trim-repeated@2.0.0:
@@ -23491,6 +23555,10 @@ snapshots:
dependencies:
'@types/unist': 3.0.2
+ unist-util-position@5.0.0:
+ dependencies:
+ '@types/unist': 3.0.2
+
unist-util-stringify-position@4.0.0:
dependencies:
'@types/unist': 3.0.2
@@ -23563,8 +23631,8 @@ snapshots:
inherits: 2.0.4
is-arguments: 1.1.1
is-generator-function: 1.0.10
- is-typed-array: 1.1.10
- which-typed-array: 1.1.11
+ is-typed-array: 1.1.13
+ which-typed-array: 1.1.15
utils-merge@1.0.1: {}
@@ -23574,13 +23642,13 @@ snapshots:
uuid@9.0.1: {}
- v-code-diff@1.13.1(vue@3.5.10(typescript@5.6.2)):
+ v-code-diff@1.13.1(vue@3.5.11(typescript@5.6.2)):
dependencies:
diff: 5.2.0
diff-match-patch: 1.0.5
highlight.js: 11.10.0
- vue: 3.5.10(typescript@5.6.2)
- vue-demi: 0.14.7(vue@3.5.10(typescript@5.6.2))
+ vue: 3.5.11(typescript@5.6.2)
+ vue-demi: 0.14.7(vue@3.5.11(typescript@5.6.2))
v8-to-istanbul@9.2.0:
dependencies:
@@ -23632,6 +23700,24 @@ snapshots:
- supports-color
- terser
+ vite-node@1.6.0(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0):
+ dependencies:
+ cac: 6.7.14
+ debug: 4.3.5(supports-color@8.1.1)
+ pathe: 1.1.2
+ picocolors: 1.0.1
+ vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+ transitivePeerDependencies:
+ - '@types/node'
+ - less
+ - lightningcss
+ - sass
+ - sass-embedded
+ - stylus
+ - sugarss
+ - supports-color
+ - terser
+
vite-plugin-turbosnap@1.0.3: {}
vite@5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0):
@@ -23645,6 +23731,17 @@ snapshots:
sass: 1.79.3
terser: 5.33.0
+ vite@5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0):
+ dependencies:
+ esbuild: 0.21.5
+ postcss: 8.4.47
+ rollup: 4.22.5
+ optionalDependencies:
+ '@types/node': 20.14.12
+ fsevents: 2.3.3
+ sass: 1.79.4
+ terser: 5.33.0
+
vitest-fetch-mock@0.2.2(encoding@0.1.13)(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.79.3)(terser@5.33.0)):
dependencies:
cross-fetch: 3.1.6(encoding@0.1.13)
@@ -23688,7 +23785,7 @@ snapshots:
- supports-color
- terser
- vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.3)(terser@5.33.0):
+ vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.79.4)(terser@5.33.0):
dependencies:
'@vitest/expect': 1.6.0
'@vitest/runner': 1.6.0
@@ -23707,8 +23804,8 @@ snapshots:
strip-literal: 2.1.0
tinybench: 2.6.0
tinypool: 0.8.4
- vite: 5.4.8(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
- vite-node: 1.6.0(@types/node@20.14.12)(sass@1.79.3)(terser@5.33.0)
+ vite: 5.4.8(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
+ vite-node: 1.6.0(@types/node@20.14.12)(sass@1.79.4)(terser@5.33.0)
why-is-node-running: 2.2.2
optionalDependencies:
'@types/node': 20.14.12
@@ -23731,7 +23828,7 @@ snapshots:
vscode-languageclient@9.0.1:
dependencies:
minimatch: 5.1.2
- semver: 7.6.0
+ semver: 7.6.3
vscode-languageserver-protocol: 3.17.5
vscode-languageserver-protocol@3.17.5:
@@ -23766,26 +23863,28 @@ snapshots:
vue-component-type-helpers@2.0.16: {}
+ vue-component-type-helpers@2.1.10: {}
+
vue-component-type-helpers@2.1.6: {}
- vue-demi@0.14.7(vue@3.5.10(typescript@5.6.2)):
+ vue-demi@0.14.7(vue@3.5.11(typescript@5.6.2)):
dependencies:
- vue: 3.5.10(typescript@5.6.2)
+ vue: 3.5.11(typescript@5.6.2)
- vue-docgen-api@4.75.1(vue@3.5.10(typescript@5.6.2)):
+ vue-docgen-api@4.75.1(vue@3.5.11(typescript@5.6.2)):
dependencies:
- '@babel/parser': 7.24.7
- '@babel/types': 7.24.7
- '@vue/compiler-dom': 3.4.37
- '@vue/compiler-sfc': 3.5.10
+ '@babel/parser': 7.25.7
+ '@babel/types': 7.25.7
+ '@vue/compiler-dom': 3.5.10
+ '@vue/compiler-sfc': 3.5.11
ast-types: 0.16.1
hash-sum: 2.0.0
lru-cache: 8.0.4
pug: 3.0.3
recast: 0.23.6
ts-map: 1.0.3
- vue: 3.5.10(typescript@5.6.2)
- vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.10(typescript@5.6.2))
+ vue: 3.5.11(typescript@5.6.2)
+ vue-inbrowser-compiler-independent-utils: 4.71.1(vue@3.5.11(typescript@5.6.2))
vue-eslint-parser@9.4.3(eslint@9.8.0):
dependencies:
@@ -23800,9 +23899,9 @@ snapshots:
transitivePeerDependencies:
- supports-color
- vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.10(typescript@5.6.2)):
+ vue-inbrowser-compiler-independent-utils@4.71.1(vue@3.5.11(typescript@5.6.2)):
dependencies:
- vue: 3.5.10(typescript@5.6.2)
+ vue: 3.5.11(typescript@5.6.2)
vue-template-compiler@2.7.14:
dependencies:
@@ -23826,20 +23925,20 @@ snapshots:
optionalDependencies:
typescript: 5.5.4
- vue@3.5.10(typescript@5.6.2):
+ vue@3.5.11(typescript@5.6.2):
dependencies:
- '@vue/compiler-dom': 3.5.10
- '@vue/compiler-sfc': 3.5.10
- '@vue/runtime-dom': 3.5.10
- '@vue/server-renderer': 3.5.10(vue@3.5.10(typescript@5.6.2))
- '@vue/shared': 3.5.10
+ '@vue/compiler-dom': 3.5.11
+ '@vue/compiler-sfc': 3.5.11
+ '@vue/runtime-dom': 3.5.11
+ '@vue/server-renderer': 3.5.11(vue@3.5.11(typescript@5.6.2))
+ '@vue/shared': 3.5.11
optionalDependencies:
typescript: 5.6.2
- vuedraggable@4.1.0(vue@3.5.10(typescript@5.6.2)):
+ vuedraggable@4.1.0(vue@3.5.11(typescript@5.6.2)):
dependencies:
sortablejs: 1.14.0
- vue: 3.5.10(typescript@5.6.2)
+ vue: 3.5.11(typescript@5.6.2)
w3c-xmlserializer@5.0.0:
dependencies: