diff options
| author | misskey-release-bot[bot] <157398866+misskey-release-bot[bot]@users.noreply.github.com> | 2024-10-15 04:53:46 +0000 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2024-10-15 04:53:46 +0000 |
| commit | b99e13e66730209ffa7a87785257b9a785efc7ea (patch) | |
| tree | 73f32398d2afa502dcef62ff37fe99bc832eb792 | |
| parent | Merge pull request #14675 from misskey-dev/develop (diff) | |
| parent | Release: 2024.10.1 (diff) | |
| download | misskey-b99e13e66730209ffa7a87785257b9a785efc7ea.tar.gz misskey-b99e13e66730209ffa7a87785257b9a785efc7ea.tar.bz2 misskey-b99e13e66730209ffa7a87785257b9a785efc7ea.zip | |
Merge pull request #14741 from misskey-dev/develop
Release: 2024.10.1
424 files changed, 6341 insertions, 1859 deletions
diff --git a/.github/labeler.yml b/.github/labeler.yml index a77f73706b..b64d726d65 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -6,7 +6,7 @@ 'packages/backend:test': - any: - changed-files: - - any-glob-to-any-file: ['packages/backend/test/**/*'] + - any-glob-to-any-file: ['packages/backend/test/**/*', 'packages/backend/test-federation/**/*'] 'packages/frontend': - any: diff --git a/.github/workflows/test-federation.yml b/.github/workflows/test-federation.yml new file mode 100644 index 0000000000..183ddb6f34 --- /dev/null +++ b/.github/workflows/test-federation.yml @@ -0,0 +1,59 @@ +name: Test (federation) + +on: + push: + branches: + - master + - develop + paths: + - packages/backend/** + - packages/misskey-js/** + - .github/workflows/test-federation.yml + pull_request: + paths: + - packages/backend/** + - packages/misskey-js/** + - .github/workflows/test-federation.yml + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + node-version: [20.16.0] + steps: + - uses: actions/checkout@v4 + with: + submodules: true + - name: Install pnpm + uses: pnpm/action-setup@v4 + - name: Install FFmpeg + uses: FedericoCarboni/setup-ffmpeg@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4.0.3 + with: + node-version: ${{ matrix.node-version }} + cache: 'pnpm' + - name: Build Misskey + run: | + corepack enable && corepack prepare + pnpm i --frozen-lockfile + pnpm build + - name: Setup + run: | + cd packages/backend/test-federation + bash ./setup.sh + sudo chmod 644 ./certificates/*.test.key + - name: Start servers + # https://github.com/docker/compose/issues/1294#issuecomment-374847206 + run: | + cd packages/backend/test-federation + docker compose up -d --scale tester=0 + - name: Test + run: | + cd packages/backend/test-federation + docker compose run --no-deps tester + - name: Stop servers + run: | + cd packages/backend/test-federation + docker compose down diff --git a/.gitignore b/.gitignore index b270d5cb3a..5b8a798ba6 100644 --- a/.gitignore +++ b/.gitignore @@ -37,7 +37,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 b32f63a3c2..504c1bbef6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,26 @@ +## 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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f61311f1e5..fc72cf42ea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -181,31 +181,45 @@ 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 @@ -578,18 +592,18 @@ 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); ``` 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/locales/ar-SA.yml b/locales/ar-SA.yml index d95600cb1f..de24ad4bb9 100644 --- a/locales/ar-SA.yml +++ b/locales/ar-SA.yml @@ -1252,7 +1252,6 @@ _theme: buttonBg: "خلفية الأزرار" buttonHoverBg: "خلفية الأزرار (عند التمرير فوقها)" inputBorder: "حواف حقل الإدخال" - listItemHoverBg: "خلفية عناصر القائمة (عند التمرير فوقها)" driveFolderBg: "خلفية مجلد قرص التخزين" messageBg: "خلفية المحادثة" _sfx: diff --git a/locales/bn-BD.yml b/locales/bn-BD.yml index ab0ee74bb4..0e761b0743 100644 --- a/locales/bn-BD.yml +++ b/locales/bn-BD.yml @@ -1017,7 +1017,6 @@ _theme: buttonBg: "বাটনের পটভূমি" buttonHoverBg: "বাটনের পটভূমি (হভার)" inputBorder: "ইনপুট ফিল্ডের বর্ডার" - listItemHoverBg: "লিস্ট আইটেমের পটভূমি (হোভার)" driveFolderBg: "ড্রাইভ ফোল্ডারের পটভূমি" wallpaperOverlay: "ওয়ালপেপার ওভারলে" badge: "ব্যাজ" diff --git a/locales/ca-ES.yml b/locales/ca-ES.yml index ad5fde37bc..b9f3fecc76 100644 --- a/locales/ca-ES.yml +++ b/locales/ca-ES.yml @@ -453,6 +453,7 @@ totpDescription: "Escriu una contrasenya d'un sol us fent servir l'aplicació d' moderator: "Moderador/a" moderation: "Moderació" moderationNote: "Nota de moderació " +moderationNoteDescription: "Pots escriure notes que es compartiran entre els moderadors." addModerationNote: "Afegir una nota de moderació " moderationLogs: "Registre de moderació " nUsersMentioned: "{n} usuaris mencionats" @@ -1284,6 +1285,15 @@ unknownWebAuthnKey: "Passkey desconeguda" passkeyVerificationFailed: "La verificació a fallat" passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verificació de la passkey a estat correcta, però s'ha deshabilitat l'inici de sessió sense contrasenya." messageToFollower: "Missatge als meus seguidors" +target: "Assumpte " +testCaptchaWarning: "És una característica dissenyada per a la prova de CAPTCHA. <strong>No l'utilitzes en l'entorn real.</strong>" +_abuseUserReport: + forward: "Reenviar " + forwardDescription: "Reenvia l'informe a una altra instància com un compte del sistema anònima." + resolve: "Solució " + accept: "Acceptar " + reject: "Rebutjar" + resolveTutorial: "Si l'informe és legítim selecciona \"Acceptar\" per resoldre'l positivament. Però si l'informe no és legítim selecciona \"Rebutjar\" per resoldre'l negativament." _delivery: status: "Estat d'entrega " stop: "Suspés" @@ -1421,6 +1431,7 @@ _serverSettings: reactionsBufferingDescription: "Quan s'activa aquesta opció millora bastant el rendiment en recuperar les línies de temps reduint la càrrega de la base. Com a contrapunt, augmentarà l'ús de memòria de Redís. Desactiva aquesta opció en cas de tenir un servidor amb poca memòria o si tens problemes d'inestabilitat." inquiryUrl: "URL de consulta " inquiryUrlDescription: "Escriu adreça URL per al formulari de consulta per al mantenidor del servidor o una pàgina web amb el contacte d'informació." + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Si no es detecta activitat per part del moderador durant un període de temps, aquesta opció es desactiva automàticament per evitar el correu brossa." _accountMigration: moveFrom: "Migrar un altre compte a aquest" moveFromSub: "Crear un àlies per un altre compte" @@ -1974,7 +1985,6 @@ _theme: buttonBg: "Fons botó " buttonHoverBg: "Fons botó (en passar-hi per sobre)" inputBorder: "Contorn del cap d'introducció " - listItemHoverBg: "Fons dels elements d'una llista" driveFolderBg: "Fons de la carpeta Disc" wallpaperOverlay: "Superposició del fons de pantalla " badge: "Insígnia " @@ -2520,6 +2530,8 @@ _moderationLogTypes: markSensitiveDriveFile: "Fitxer marcat com a sensible" unmarkSensitiveDriveFile: "S'ha tret la marca de sensible del fitxer" resolveAbuseReport: "Informe resolt" + forwardAbuseReport: "Informe reenviat" + updateAbuseReportNote: "Nota de moderació d'un informe actualitzat" createInvitation: "Crear codi d'invitació " createAd: "Anunci creat" deleteAd: "Anunci esborrat" diff --git a/locales/cs-CZ.yml b/locales/cs-CZ.yml index 4233a68f17..caf6d6e163 100644 --- a/locales/cs-CZ.yml +++ b/locales/cs-CZ.yml @@ -1629,7 +1629,6 @@ _theme: buttonBg: "Pozadí tlačítka" buttonHoverBg: "Pozadí tlačítka (Hover)" inputBorder: "Ohraničení vstupního pole" - listItemHoverBg: "Pozadí položky seznamu (Hover)" driveFolderBg: "Pozadí složky disku" wallpaperOverlay: "Překrytí tapety" badge: "Odznak" diff --git a/locales/de-DE.yml b/locales/de-DE.yml index 35a04b453c..4e2bd06934 100644 --- a/locales/de-DE.yml +++ b/locales/de-DE.yml @@ -1784,7 +1784,6 @@ _theme: buttonBg: "Hintergrund von Schaltflächen" buttonHoverBg: "Hintergrund von Schaltflächen (Mouseover)" inputBorder: "Rahmen von Eingabefeldern" - listItemHoverBg: "Hintergrund von Listeneinträgen (Mouseover)" driveFolderBg: "Hintergrund von Drive-Ordnern" wallpaperOverlay: "Hintergrundbild-Overlay" badge: "Wappen" diff --git a/locales/en-US.yml b/locales/en-US.yml index 126f769644..6ea7fb4f8d 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -1984,7 +1984,6 @@ _theme: buttonBg: "Button background" buttonHoverBg: "Button background (Hover)" inputBorder: "Input field border" - listItemHoverBg: "List item background (Hover)" driveFolderBg: "Drive folder background" wallpaperOverlay: "Wallpaper overlay" badge: "Badge" diff --git a/locales/es-ES.yml b/locales/es-ES.yml index de9ea0c32a..d574999e40 100644 --- a/locales/es-ES.yml +++ b/locales/es-ES.yml @@ -1915,7 +1915,6 @@ _theme: buttonBg: "Fondo de botón" buttonHoverBg: "Fondo de botón (hover)" inputBorder: "Borde de los campos de entrada" - listItemHoverBg: "Fondo de elemento de listas (hover)" driveFolderBg: "Fondo de capeta del drive" wallpaperOverlay: "Transparencia del fondo de pantalla" badge: "Medalla" diff --git a/locales/fr-FR.yml b/locales/fr-FR.yml index 7dfc64d63f..a7060c06fc 100644 --- a/locales/fr-FR.yml +++ b/locales/fr-FR.yml @@ -1701,7 +1701,6 @@ _theme: buttonBg: "Arrière-plan du bouton" buttonHoverBg: "Arrière-plan du bouton (survolé)" inputBorder: "Cadre de la zone de texte" - listItemHoverBg: "Arrière-plan d'item de liste (survolé)" driveFolderBg: "Arrière-plan du dossier de disque" wallpaperOverlay: "Superposition de fond d'écran" badge: "Badge" diff --git a/locales/id-ID.yml b/locales/id-ID.yml index fbfedb89e3..ce3958b167 100644 --- a/locales/id-ID.yml +++ b/locales/id-ID.yml @@ -1924,7 +1924,6 @@ _theme: buttonBg: "Latar belakang tombol" buttonHoverBg: "Latar belakang tombol (Mengambang)" inputBorder: "Batas bidang masukan" - listItemHoverBg: "Latar belakang daftar item (Mengambang)" driveFolderBg: "Latar belakang folder drive" wallpaperOverlay: "Lapisan wallpaper" badge: "Lencana" diff --git a/locales/index.d.ts b/locales/index.d.ts index d502c5b432..b5af5909a3 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -4367,6 +4367,10 @@ export interface Locale extends ILocale { */ "enableChartsForFederatedInstances": string; /** + * リモートサーバーの情報を取得 + */ + "enableStatsForFederatedInstances": string; + /** * ノートのアクションにクリップを追加 */ "showClipButtonInNoteFooter": string; @@ -5166,6 +5170,26 @@ export interface Locale extends ILocale { * 対象 */ "target": string; + /** + * CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong> + */ + "testCaptchaWarning": string; + /** + * 禁止ワード(ユーザーの名前) + */ + "prohibitedWordsForNameOfUser": string; + /** + * このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。 + */ + "prohibitedWordsForNameOfUserDescription": string; + /** + * 変更しようとした名前に禁止された文字列が含まれています + */ + "yourNameContainsProhibitedWords": string; + /** + * 名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。 + */ + "yourNameContainsProhibitedWordsDescription": string; "_abuseUserReport": { /** * 転送 @@ -5696,6 +5720,10 @@ export interface Locale extends ILocale { * サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。 */ "inquiryUrlDescription": string; + /** + * 一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。 + */ + "thisSettingWillAutomaticallyOffWhenModeratorsInactive": string; }; "_accountMigration": { /** @@ -7706,10 +7734,6 @@ export interface Locale extends ILocale { */ "inputBorder": string; /** - * リスト項目の背景 (ホバー) - */ - "listItemHoverBg": string; - /** * ドライブフォルダーの背景 */ "driveFolderBg": string; @@ -9637,6 +9661,14 @@ export interface Locale extends ILocale { * ユーザーが作成されたとき */ "userCreated": string; + /** + * モデレーターが一定期間非アクティブになったとき + */ + "inactiveModeratorsWarning": string; + /** + * モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき + */ + "inactiveModeratorsInvitationOnlyChanged": string; }; /** * Webhookを削除しますか? diff --git a/locales/it-IT.yml b/locales/it-IT.yml index 004bb6e9fd..bcabf1bdb6 100644 --- a/locales/it-IT.yml +++ b/locales/it-IT.yml @@ -454,6 +454,7 @@ totpDescription: "Puoi autenticarti inserendo un codice OTP tramite la tua App d moderator: "Moderatore" moderation: "moderazione" moderationNote: "Promemoria di moderazione" +moderationNoteDescription: "Puoi scrivere promemoria condivisi solo tra moderatori." addModerationNote: "Aggiungi promemoria di moderazione" moderationLogs: "Cronologia di moderazione" nUsersMentioned: "{n} profili ne parlano" @@ -841,7 +842,7 @@ onlineStatus: "Stato di connessione" hideOnlineStatus: "Modalità invisibile" hideOnlineStatusDescription: "Attivando questa opzione potresti ridurre l'usabilità di alcune funzioni, come la ricerca." online: "Online" -active: "Attività" +active: "Attivo" offline: "Offline" notRecommended: "Sconsigliato" botProtection: "Protezione contro i bot" @@ -1086,6 +1087,7 @@ retryAllQueuesConfirmTitle: "Vuoi ritentare adesso?" retryAllQueuesConfirmText: "Potrebbe sovraccaricare il server temporaneamente." enableChartsForRemoteUser: "Abilita i grafici per i profili remoti" enableChartsForFederatedInstances: "Abilita i grafici per le istanze federate" +enableStatsForFederatedInstances: "Informazioni statistiche sui server federati" showClipButtonInNoteFooter: "Aggiungi il bottone Clip tra le azioni delle Note" reactionsDisplaySize: "Grandezza delle reazioni" limitWidthOfReaction: "Limita la larghezza delle reazioni e ridimensionale" @@ -1285,6 +1287,19 @@ unknownWebAuthnKey: "Questa è una passkey sconosciuta." passkeyVerificationFailed: "La verifica della passkey non è riuscita." passkeyVerificationSucceededButPasswordlessLoginDisabled: "La verifica della passkey è riuscita, ma l'accesso senza password è disabilitato." messageToFollower: "Messaggio ai follower" +target: "Riferimento" +testCaptchaWarning: "Questa funzione è destinata al test CAPTCHA. <strong>Da non utilizzare in ambiente di produzione.</strong>" +prohibitedWordsForNameOfUser: "Parole proibite (nome utente)" +prohibitedWordsForNameOfUserDescription: "Il sistema rifiuta di rinominare un utente, se il nome contiene qualsiasi parola nell'elenco. Sono esenti i profili con privilegi di moderazione." +yourNameContainsProhibitedWords: "Il nome che hai scelto contiene una o più parole vietate" +yourNameContainsProhibitedWordsDescription: "Se desideri comunque utilizzare questo nome, contatta l''amministrazione." +_abuseUserReport: + forward: "Inoltra" + forwardDescription: "Inoltra il report al server remoto, per mezzo di account di sistema, anonimo." + resolve: "Risolvi" + accept: "Approva" + reject: "Rifiuta" + resolveTutorial: "Se moderi una segnalazione legittima, scegli \"Approva\" per risolvere positivamente.\nSe la segnalazione non è legittima, seleziona \"Rifiuta\" per risolvere negativamente." _delivery: status: "Stato della consegna" stop: "Sospensione" @@ -1312,16 +1327,16 @@ _bubbleGame: _announcement: forExistingUsers: "Solo ai profili attuali" forExistingUsersDescription: "L'annuncio sarà visibile solo ai profili esistenti in questo momento. Se disabilitato, sarà visibile anche ai profili che verranno creati dopo la pubblicazione di questo annuncio." - needConfirmationToRead: "Richiede la conferma di lettura" - needConfirmationToReadDescription: "Sarà visualizzata una finestra di dialogo che richiede la conferma di lettura. Inoltre, non è soggetto a conferme di lettura massicce." + needConfirmationToRead: "Conferma di lettura obbligatoria" + needConfirmationToReadDescription: "I profili riceveranno una finestra di dialogo che richiede di accettare obbligatoriamente per procedere. Tale richiesta è esente da \"conferma tutte\"." end: "Archivia l'annuncio" tooManyActiveAnnouncementDescription: "L'esperienza delle persone può peggiorare se ci sono troppi annunci attivi. Considera anche l'archiviazione degli annunci conclusi." readConfirmTitle: "Segnare come già letto?" readConfirmText: "Hai già letto \"{title}˝?" shouldNotBeUsedToPresentPermanentInfo: "Ti consigliamo di utilizzare gli annunci per pubblicare informazioni tempestive e limitate nel tempo, anziché informazioni importanti a lungo andare nel tempo, poiché potrebbero risultare difficili da ritrovare e peggiorare la fruibilità del servizio, specialmente alle nuove persone iscritte." dialogAnnouncementUxWarn: "Ti consigliamo di usarli con cautela, poiché è molto probabile che avere più di un annuncio in stile \"finestra di dialogo\" peggiori sensibilmente la fruibilità del servizio, specialmente alle nuove persone iscritte." - silence: "Silenziare gli annunci" - silenceDescription: "Se attivi questa opzione, non riceverai notifiche sugli annunci, evitando di contrassegnarle come già lette." + silence: "Annuncio silenzioso" + silenceDescription: "Attivando questa opzione, non invierai la notifica, evitando che debba essere contrassegnata come già letta." _initialAccountSetting: accountCreated: "Il tuo profilo è stato creato!" letsStartAccountSetup: "Per iniziare, impostiamo il tuo profilo." @@ -1422,6 +1437,7 @@ _serverSettings: reactionsBufferingDescription: "Attivando questa opzione, puoi migliorare significativamente le prestazioni durante la creazione delle reazioni e ridurre il carico sul database. Tuttavia, aumenterà l'impiego di memoria Redis." inquiryUrl: "URL di contatto" inquiryUrlDescription: "Specificare l'URL al modulo di contatto, oppure le informazioni con i dati di contatto dell'amministrazione." + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "Per prevenire SPAM, questa impostazione verrà disattivata automaticamente, se non si rileva alcuna attività di moderazione durante un certo periodo di tempo." _accountMigration: moveFrom: "Migra un altro profilo dentro a questo" moveFromSub: "Crea un alias verso un altro profilo remoto" @@ -1975,7 +1991,6 @@ _theme: buttonBg: "Sfondo del pulsante" buttonHoverBg: "Sfondo del pulsante (sorvolato)" inputBorder: "Inquadra casella di testo" - listItemHoverBg: "Sfondo della voce di elenco (sorvolato)" driveFolderBg: "Sfondo della cartella di disco" wallpaperOverlay: "Sovrapposizione dello sfondo" badge: "Distintivo" @@ -2188,7 +2203,7 @@ _widgets: _userList: chooseList: "Seleziona una lista" clicker: "Cliccaggio" - birthdayFollowings: "Chi nacque oggi" + birthdayFollowings: "Compleanni del giorno" _cw: hide: "Nascondere" show: "Continua la lettura..." @@ -2477,6 +2492,8 @@ _webhookSettings: abuseReport: "Quando arriva una segnalazione" abuseReportResolved: "Quando una segnalazione è risolta" userCreated: "Quando viene creato un profilo" + inactiveModeratorsWarning: "Quando un profilo moderatore rimane inattivo per un determinato periodo" + inactiveModeratorsInvitationOnlyChanged: "Quando la moderazione è rimasta inattiva per un determinato periodo e il sistema è cambiato in modalità \"solo inviti\"" deleteConfirm: "Vuoi davvero eliminare il Webhook?" testRemarks: "Clicca il bottone a destra dell'interruttore, per provare l'invio di un webhook con dati fittizi." _abuseReport: @@ -2522,6 +2539,8 @@ _moderationLogTypes: markSensitiveDriveFile: "File nel Drive segnato come esplicito" unmarkSensitiveDriveFile: "File nel Drive segnato come non esplicito" resolveAbuseReport: "Segnalazione risolta" + forwardAbuseReport: "Segnalazione inoltrata" + updateAbuseReportNote: "Ha aggiornato la segnalazione" createInvitation: "Genera codice di invito" createAd: "Banner creato" deleteAd: "Banner eliminato" diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 678bc7e66b..c448d4d50a 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1087,6 +1087,7 @@ retryAllQueuesConfirmTitle: "今すぐ再試行しますか?" retryAllQueuesConfirmText: "一時的にサーバーの負荷が増大することがあります。" enableChartsForRemoteUser: "リモートユーザーのチャートを生成" enableChartsForFederatedInstances: "リモートサーバーのチャートを生成" +enableStatsForFederatedInstances: "リモートサーバーの情報を取得" showClipButtonInNoteFooter: "ノートのアクションにクリップを追加" reactionsDisplaySize: "リアクションの表示サイズ" limitWidthOfReaction: "リアクションの最大横幅を制限し、縮小して表示する" @@ -1287,6 +1288,11 @@ passkeyVerificationFailed: "パスキーの検証に失敗しました。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。" messageToFollower: "フォロワーへのメッセージ" target: "対象" +testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>" +prohibitedWordsForNameOfUser: "禁止ワード(ユーザーの名前)" +prohibitedWordsForNameOfUserDescription: "このリストに含まれる文字列がユーザーの名前に含まれる場合、ユーザーの名前の変更を拒否します。モデレーター権限を持つユーザーはこの制限の影響を受けません。" +yourNameContainsProhibitedWords: "変更しようとした名前に禁止された文字列が含まれています" +yourNameContainsProhibitedWordsDescription: "名前に禁止されている文字列が含まれています。この名前を使用したい場合は、サーバー管理者にお問い合わせください。" _abuseUserReport: forward: "転送" @@ -1440,6 +1446,7 @@ _serverSettings: reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。" inquiryUrl: "問い合わせ先URL" inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。" + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。" _accountMigration: moveFrom: "別のアカウントからこのアカウントに移行" @@ -2018,7 +2025,6 @@ _theme: buttonBg: "ボタンの背景" buttonHoverBg: "ボタンの背景 (ホバー)" inputBorder: "入力ボックスの縁取り" - listItemHoverBg: "リスト項目の背景 (ホバー)" driveFolderBg: "ドライブフォルダーの背景" wallpaperOverlay: "壁紙のオーバーレイ" badge: "バッジ" @@ -2553,6 +2559,8 @@ _webhookSettings: abuseReport: "ユーザーから通報があったとき" abuseReportResolved: "ユーザーからの通報を処理したとき" userCreated: "ユーザーが作成されたとき" + inactiveModeratorsWarning: "モデレーターが一定期間非アクティブになったとき" + inactiveModeratorsInvitationOnlyChanged: "モデレーターが一定期間非アクティブだったため、システムにより招待制へと変更されたとき" deleteConfirm: "Webhookを削除しますか?" testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。" diff --git a/locales/ja-KS.yml b/locales/ja-KS.yml index 52a8f41380..0a8b3828f2 100644 --- a/locales/ja-KS.yml +++ b/locales/ja-KS.yml @@ -1943,7 +1943,6 @@ _theme: buttonBg: "ボタンの背景" buttonHoverBg: "ボタンの背景 (ホバー)" inputBorder: "入力ボックスの縁取り" - listItemHoverBg: "リスト項目の背景 (ホバー)" driveFolderBg: "ドライブフォルダーの背景" wallpaperOverlay: "壁紙のオーバーレイ" badge: "バッジ" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index 757afe53f9..414202adab 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -1087,6 +1087,7 @@ retryAllQueuesConfirmTitle: "지금 다시 시도하시겠습니까?" retryAllQueuesConfirmText: "일시적으로 서버의 부하가 증가할 수 있습니다." enableChartsForRemoteUser: "리모트 유저의 차트를 생성" enableChartsForFederatedInstances: "리모트 서버의 차트를 생성" +enableStatsForFederatedInstances: "리모트 서버 정보 받아오기" showClipButtonInNoteFooter: "노트 동작에 클립을 추가" reactionsDisplaySize: "리액션 표시 크기" limitWidthOfReaction: "리액션의 최대 폭을 제한하고 작게 표시하기" @@ -1287,6 +1288,11 @@ passkeyVerificationFailed: "패스키 검증을 실패했습니다." passkeyVerificationSucceededButPasswordlessLoginDisabled: "패스키를 검증했으나, 비밀번호 없이 로그인하기가 꺼져 있습니다." messageToFollower: "팔로워에 보낼 메시지" target: "대상" +testCaptchaWarning: "CAPTCHA를 테스트하기 위한 기능입니다. <strong>실제 환경에서는 사용하지 마세요.</strong>" +prohibitedWordsForNameOfUser: "금지 단어 (사용자 이름)" +prohibitedWordsForNameOfUserDescription: "이 목록에 포함되는 키워드가 사용자 이름에 있는 경우, 일반 사용자는 이름을 바꿀 수 없습니다. 모더레이터 권한을 가진 사용자는 제한 대상에서 제외됩니다." +yourNameContainsProhibitedWords: "바꾸려는 이름에 금지된 키워드가 포함되어 있습니다." +yourNameContainsProhibitedWordsDescription: "이름에 금지된 키워드가 있습니다. 이름을 사용해야 하는 경우, 서버 관리자에 문의하세요." _abuseUserReport: forward: "전달" forwardDescription: "익명 시스템 계정을 사용하여 리모트 서버에 신고 내용을 전달할 수 있습니다." @@ -1431,6 +1437,7 @@ _serverSettings: reactionsBufferingDescription: "활성화 한 경우, 리액션 작성 퍼포먼스가 대폭 향상되어 DB의 부하를 줄일 수 있으나, Redis의 메모리 사용량이 많아집니다." inquiryUrl: "문의처 URL" inquiryUrlDescription: "서버 운영자에게 보내는 문의 양식의 URL이나 운영자의 연락처 등이 적힌 웹 페이지의 URL을 설정합니다." + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "일정 기간동안 모더레이터의 활동이 감지되지 않는 경우, 스팸 방지를 위해 이 설정은 자동으로 꺼집니다." _accountMigration: moveFrom: "다른 계정에서 이 계정으로 이사" moveFromSub: "다른 계정에 대한 별칭을 생성" @@ -1984,7 +1991,6 @@ _theme: buttonBg: "버튼 배경" buttonHoverBg: "버튼 배경 (호버)" inputBorder: "입력 필드 테두리" - listItemHoverBg: "리스트 항목 배경 (호버)" driveFolderBg: "드라이브 폴더 배경" wallpaperOverlay: "배경화면 오버레이" badge: "배지" @@ -2486,6 +2492,8 @@ _webhookSettings: abuseReport: "유저롭" abuseReportResolved: "받은 신고를 처리했을 때" userCreated: "유저가 생성되었을 때" + inactiveModeratorsWarning: "모더레이터가 일정 기간동안 활동하지 않은 경우" + inactiveModeratorsInvitationOnlyChanged: "모더레이터가 일정 기간 활동하지 않아 시스템에 의해 초대제로 바뀐 경우" deleteConfirm: "Webhook을 삭제할까요?" testRemarks: "스위치 오른쪽에 있는 버튼을 클릭하여 더미 데이터를 사용한 테스트용 웹 훅을 보낼 수 있습니다." _abuseReport: diff --git a/locales/pl-PL.yml b/locales/pl-PL.yml index 117434ad32..d7afd57760 100644 --- a/locales/pl-PL.yml +++ b/locales/pl-PL.yml @@ -1205,7 +1205,6 @@ _theme: buttonBg: "Tło przycisku" buttonHoverBg: "Tło przycisku (po najechaniu)" inputBorder: "Obramowanie pola wejścia" - listItemHoverBg: "Tło elementu listy (po najechaniu)" driveFolderBg: "Tło folderu na dysku" wallpaperOverlay: "Nakładka tapety" badge: "Odznaka" diff --git a/locales/pt-PT.yml b/locales/pt-PT.yml index 98d42eb44a..9039fd2141 100644 --- a/locales/pt-PT.yml +++ b/locales/pt-PT.yml @@ -25,7 +25,7 @@ basicSettings: "Configurações básicas" otherSettings: "Outras configurações" openInWindow: "Abrir em um janela" profile: "Perfil" -timeline: "Cronologia" +timeline: "Linha do tempo" noAccountDescription: "Este usuário não tem uma descrição." login: "Iniciar sessão" loggingIn: "Iniciando sessão…" @@ -1944,7 +1944,6 @@ _theme: buttonBg: "Plano de fundo de botão" buttonHoverBg: "Plano de fundo de botão (Selecionado)" inputBorder: "Borda de campo digitável" - listItemHoverBg: "Plano de fundo do item de uma lista (Selecionado)" driveFolderBg: "Plano de fundo da pasta no Drive" wallpaperOverlay: "Sobreposição do papel de parede." badge: "Emblema" diff --git a/locales/ru-RU.yml b/locales/ru-RU.yml index befb537105..70178ec2fd 100644 --- a/locales/ru-RU.yml +++ b/locales/ru-RU.yml @@ -1694,7 +1694,6 @@ _theme: buttonBg: "Фон кнопки" buttonHoverBg: "Текст кнопки" inputBorder: "Рамка поля ввода" - listItemHoverBg: "Фон пункта списка (под указателем)" driveFolderBg: "Фон папки «Диска»" wallpaperOverlay: "Слой обоев" badge: "Значок" diff --git a/locales/sk-SK.yml b/locales/sk-SK.yml index 8cb73e1303..60ce45a6b9 100644 --- a/locales/sk-SK.yml +++ b/locales/sk-SK.yml @@ -1108,7 +1108,6 @@ _theme: buttonBg: "Pozadie tlačidla" buttonHoverBg: "Pozadie tlačidla (pod kurzorom)" inputBorder: "Okraj vstupného poľa" - listItemHoverBg: "Pozadie položky zoznamu (pod kurzorom)" driveFolderBg: "Pozadie priečinu disku" wallpaperOverlay: "Vrstvenie pozadia" badge: "Odznak" diff --git a/locales/th-TH.yml b/locales/th-TH.yml index 31eee2bccc..c70d448e2b 100644 --- a/locales/th-TH.yml +++ b/locales/th-TH.yml @@ -1943,7 +1943,6 @@ _theme: buttonBg: "ปุ่มพื้นหลัง" buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวอร์)" inputBorder: "เส้นขอบของช่องป้อนข้อมูล" - listItemHoverBg: "รายการไอเทมพื้นหลัง (โฮเวอร์)" driveFolderBg: "พื้นหลังโฟลเดอร์ไดรฟ์" wallpaperOverlay: "วอลล์เปเปอร์ซ้อนทับ" badge: "ตรา" diff --git a/locales/uk-UA.yml b/locales/uk-UA.yml index 974508b3a7..f2262cd71f 100644 --- a/locales/uk-UA.yml +++ b/locales/uk-UA.yml @@ -1302,7 +1302,6 @@ _theme: buttonBg: "Фон кнопки" buttonHoverBg: "Фон кнопки (при наведенні)" inputBorder: "Край поля вводу" - listItemHoverBg: "Фон елементу в списку (при наведенні)" driveFolderBg: "Фон папки на диску" wallpaperOverlay: "Накладання шпалер" badge: "Значок" diff --git a/locales/vi-VN.yml b/locales/vi-VN.yml index 6cf9b3f278..235497d844 100644 --- a/locales/vi-VN.yml +++ b/locales/vi-VN.yml @@ -1546,7 +1546,6 @@ _theme: buttonBg: "Nền nút" buttonHoverBg: "Nền nút (Chạm)" inputBorder: "Đường viền khung soạn thảo" - listItemHoverBg: "Nền mục liệt kê (Chạm)" driveFolderBg: "Nền thư mục Ổ đĩa" wallpaperOverlay: "Lớp phủ hình nền" badge: "Huy hiệu" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 09feca5b4e..b81018cc1f 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -1087,6 +1087,7 @@ retryAllQueuesConfirmTitle: "要再尝试一次吗?" retryAllQueuesConfirmText: "可能会使服务器负荷在一定时间内增加" enableChartsForRemoteUser: "生成远程用户的图表" enableChartsForFederatedInstances: "生成远程服务器的图表" +enableStatsForFederatedInstances: "获取远程服务器的信息" showClipButtonInNoteFooter: "在贴文下方显示便签按钮" reactionsDisplaySize: "回应显示大小" limitWidthOfReaction: "限制回应的最大宽度,并将其缩小显示" @@ -1287,6 +1288,11 @@ passkeyVerificationFailed: "验证通行密钥失败。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "通行密钥验证成功,但账户未开启无密码登录。" messageToFollower: "给关注者的消息" target: "对象" +testCaptchaWarning: "此功能为测试 CAPTCHA 用。<strong>请勿在正式环境中使用。</strong>" +prohibitedWordsForNameOfUser: "用户名中禁止的词" +prohibitedWordsForNameOfUserDescription: "更改用户名时,如果用户名中包含此列表里的词汇,用户的改名请求将被拒绝。持有管理员权限的用户不受此限制。" +yourNameContainsProhibitedWords: "目标用户名包含违禁词" +yourNameContainsProhibitedWordsDescription: "用户名内含有违禁词。若想使用此用户名,请联系服务器管理员。" _abuseUserReport: forward: "转发" forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。" @@ -1431,6 +1437,7 @@ _serverSettings: reactionsBufferingDescription: "开启时可显著提高发送回应时的性能,及减轻数据库负荷。但 Redis 的内存用量会相应增加。" inquiryUrl: "联络地址" inquiryUrlDescription: "用来指定诸如向服务运营商咨询的论坛地址,或记载了运营商联系方式之类的网页地址。" + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "若在一段时间内没有检测到管理活动,为防止垃圾信息,此设定将自动关闭。" _accountMigration: moveFrom: "从别的账号迁移到此账户" moveFromSub: "为另一个账户建立别名" @@ -1984,7 +1991,6 @@ _theme: buttonBg: "按钮背景" buttonHoverBg: "按钮背景(悬停)" inputBorder: "输入框边框" - listItemHoverBg: "下拉列表项目背景(悬停)" driveFolderBg: "网盘的文件夹背景" wallpaperOverlay: "壁纸叠加层" badge: "徽章" @@ -2263,7 +2269,7 @@ _profile: avatarDecorationMax: "最多可添加 {max} 个挂件" followedMessage: "被关注时显示的消息" followedMessageDescription: "可以设置被关注时向对方显示的短消息。" - followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在被请求被批准后显示。" + followedMessageDescriptionForLockedAccount: "需要批准才能关注的情况下,消息是在请求被批准后显示。" _exportOrImport: allNotes: "所有帖子" favoritedNotes: "收藏的帖子" @@ -2486,6 +2492,8 @@ _webhookSettings: abuseReport: "当收到举报时" abuseReportResolved: "当举报被处理时" userCreated: "当用户被创建时" + inactiveModeratorsWarning: "当管理员在一段时间内不活跃时" + inactiveModeratorsInvitationOnlyChanged: "当因为管理员在一段时间内不活跃,导致服务器变为邀请制时" deleteConfirm: "要删除 webhook 吗?" testRemarks: "点击开关右侧的按钮,可以发送使用假数据的测试 Webhook。" _abuseReport: diff --git a/locales/zh-TW.yml b/locales/zh-TW.yml index 5e8a5d8f8d..de18342bbf 100644 --- a/locales/zh-TW.yml +++ b/locales/zh-TW.yml @@ -454,6 +454,7 @@ totpDescription: "以驗證應用程式輸入一次性密碼" moderator: "審查員" moderation: "審查" moderationNote: "管理筆記" +moderationNoteDescription: "您可以編寫僅在審查員之間共用的註解。" addModerationNote: "新增管理筆記" moderationLogs: "管理日誌" nUsersMentioned: "被 {n} 個人提及" @@ -519,7 +520,7 @@ menuStyle: "選單風格" style: "風格" drawer: "側邊欄" popup: "彈出式視窗" -showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的操作選項" +showNoteActionsOnlyHover: "僅在游標停留時顯示貼文的" showReactionsCount: "顯示貼文的反應數目" noHistory: "沒有歷史紀錄" signinHistory: "登入歷史" @@ -1018,7 +1019,7 @@ show: "檢視" neverShow: "不再顯示" remindMeLater: "以後再說" didYouLikeMisskey: "您喜歡 Misskey 嗎?" -pleaseDonate: "Misskey 是由 {host} 使用的免費軟體。請贊助我們,讓開發得以持續!" +pleaseDonate: "Misskey是由{host}使用的免費軟體。請贊助我們,讓開發的工作能夠持續!" correspondingSourceIsAvailable: "對應的原始碼可以在 {anchor} 處找到。" roles: "角色" role: "角色" @@ -1086,6 +1087,7 @@ retryAllQueuesConfirmTitle: "要現在重試嗎?" retryAllQueuesConfirmText: "伺服器的負荷可能會暫時增加。" enableChartsForRemoteUser: "生成遠端使用者的圖表" enableChartsForFederatedInstances: "生成遠端伺服器的圖表" +enableStatsForFederatedInstances: "取得遠端伺服器資訊" showClipButtonInNoteFooter: "新增摘錄按鈕至貼文" reactionsDisplaySize: "反應的顯示尺寸" limitWidthOfReaction: "限制反應的最大寬度,並縮小顯示尺寸。" @@ -1194,7 +1196,7 @@ showRenotes: "顯示其他人的轉發貼文" edited: "已編輯" notificationRecieveConfig: "接受通知的設定" mutualFollow: "互相追隨" -followingOrFollower: "追隨中或追隨者" +followingOrFollower: "追隨中或者追隨者" fileAttachedOnly: "只顯示包含附件的貼文" showRepliesToOthersInTimeline: "顯示給其他人的回覆" hideRepliesToOthersInTimeline: "在時間軸上隱藏給其他人的回覆" @@ -1265,7 +1267,7 @@ useNativeUIForVideoAudioPlayer: "使用瀏覽器的 UI 播放影片與音訊" keepOriginalFilename: "保留原始檔名" keepOriginalFilenameDescription: "如果關閉此設置,上傳時檔案名稱會自動替換為隨機字串。" noDescription: "沒有說明文字" -alwaysConfirmFollow: "點擊追隨時總是顯示確認訊息" +alwaysConfirmFollow: "跟隨時總是確認" inquiry: "聯絡我們" tryAgain: "請再試一次。" confirmWhenRevealingSensitiveMedia: "要顯示敏感媒體時需確認" @@ -1285,6 +1287,19 @@ unknownWebAuthnKey: "未註冊的金鑰。" passkeyVerificationFailed: "驗證金鑰失敗。" passkeyVerificationSucceededButPasswordlessLoginDisabled: "雖然驗證金鑰成功,但是無密碼登入的方式是停用的。" messageToFollower: "給追隨者的訊息" +target: "目標 " +testCaptchaWarning: "此功能用於 CAPTCHA 的測試。<strong>請勿在正式環境中使用。</strong>" +prohibitedWordsForNameOfUser: "禁止使用的字詞(使用者名稱)" +prohibitedWordsForNameOfUserDescription: "如果使用者名稱包含此清單中的任何字串,則拒絕重新命名使用者。 具有審查員權限的使用者不受此限制的影響。" +yourNameContainsProhibitedWords: "您嘗試更改的名稱包含禁止的字串" +yourNameContainsProhibitedWordsDescription: "名稱中包含禁止使用的字串。 如果您想使用此名稱,請聯絡您的伺服器管理員。" +_abuseUserReport: + forward: "轉發" + forwardDescription: "以匿名系統帳戶將檢舉轉發至遠端伺服器。" + resolve: "解決" + accept: "接受" + reject: "拒絕" + resolveTutorial: "如果您已回覆正當的檢舉,請選擇「接受」以將案件標記為已解決。\n 如果檢舉的內容不正當,請選擇「拒絕」將案件標記為已解決。" _delivery: status: "傳送狀態" stop: "停止發送" @@ -1422,6 +1437,7 @@ _serverSettings: reactionsBufferingDescription: "啟用時,可以顯著提高建立反應時的效能並減少資料庫的負載。 但是,Redis 記憶體使用量會增加。" inquiryUrl: "聯絡表單網址" inquiryUrlDescription: "指定伺服器運營者的聯絡表單網址,或包含運營者聯絡資訊網頁的網址。" + thisSettingWillAutomaticallyOffWhenModeratorsInactive: "為了防止 spam,如果一段期間內沒有偵測到審查員的活動,此設定將自動關閉。" _accountMigration: moveFrom: "從其他帳戶遷移到這個帳戶" moveFromSub: "為另一個帳戶建立別名" @@ -1435,7 +1451,7 @@ _accountMigration: startMigration: "遷移" migrationConfirm: "確定要將這個帳戶遷移至 {account} 嗎?一旦遷移就無法撤銷,也就無法以原來的狀態使用這個帳戶。\n另外,請確認在要遷移到的帳戶已經建立了一個別名。" movedAndCannotBeUndone: "帳戶已遷移。\n遷移無法撤消。" - postMigrationNote: "在完成遷移的 24 小時後解除此帳戶的追隨。此帳戶的追隨中、追隨者數量變為 0。由於不會解除追隨者,你的追隨者仍然可以繼續檢視這個帳戶發布給追隨者的貼文。" + postMigrationNote: "取消追蹤此帳戶將在遷移操作後 24 小時執行。\n 此帳戶有 0 個關注者/關注者。 您的關注者仍然可以看到此帳戶的關注者帖子,因為您不會被取消關注。" movedTo: "要遷移到的帳戶:" _achievements: earnedAt: "獲得日期" @@ -1555,7 +1571,7 @@ _achievements: _markedAsCat: title: "我是貓" description: "已將帳戶設定為貓" - flavor: "還沒有名字。" + flavor: "沒有名字。" _following1: title: "首次追隨" description: "首次追隨了" @@ -1569,7 +1585,7 @@ _achievements: title: "一百位朋友" description: "追隨超過100人了" _following300: - title: "朋友過多" + title: "朋友太多" description: "追隨超過300人了" _followers1: title: "第一個追隨者" @@ -1895,7 +1911,7 @@ _channel: following: "追隨中" usersCount: "有 {n} 人參與" notesCount: "有 {n} 篇貼文" - nameAndDescription: "名稱與說明" + nameAndDescription: "名稱" nameOnly: "僅名稱" allowRenoteToExternal: "允許在頻道外轉發和引用" _menuDisplay: @@ -1975,7 +1991,6 @@ _theme: buttonBg: "按鈕背景" buttonHoverBg: "按鈕背景 (漂浮)" inputBorder: "輸入框邊框" - listItemHoverBg: "列表物品背景 (漂浮)" driveFolderBg: "雲端硬碟文件夾背景" wallpaperOverlay: "壁紙覆蓋層" badge: "徽章" @@ -2477,6 +2492,8 @@ _webhookSettings: abuseReport: "當使用者檢舉時" abuseReportResolved: "當處理了使用者的檢舉時" userCreated: "使用者被新增時" + inactiveModeratorsWarning: "當審查員在一段時間內沒有活動時" + inactiveModeratorsInvitationOnlyChanged: "當審查員在一段時間內不活動時,系統會將模式變更為邀請制" deleteConfirm: "請問是否要刪除 Webhook?" testRemarks: "按下切換開關右側的按鈕,就會將假資料發送至 Webhook。" _abuseReport: @@ -2491,7 +2508,7 @@ _abuseReport: mail: "寄送到擁有監察員權限的使用者電子郵件地址(僅在收到檢舉時)" webhook: "向指定的 SystemWebhook 發送通知(在收到檢舉和解決檢舉時發送)" keywords: "關鍵字" - notifiedUser: "被通知的使用者" + notifiedUser: "通知的使用者" notifiedWebhook: "使用的 Webhook" deleteConfirm: "確定要刪除通知對象嗎?" _moderationLogTypes: @@ -2522,6 +2539,8 @@ _moderationLogTypes: markSensitiveDriveFile: "標記為敏感檔案" unmarkSensitiveDriveFile: "撤銷標記為敏感檔案" resolveAbuseReport: "解決檢舉" + forwardAbuseReport: "轉發檢舉" + updateAbuseReportNote: "更新檢舉的審查備註" createInvitation: "建立邀請碼" createAd: "建立廣告" deleteAd: "刪除廣告" diff --git a/package.json b/package.json index 3afd84253a..8bf96d916d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "misskey", - "version": "2024.10.0", + "version": "2024.10.1", "codename": "nasubi", "repository": { "type": "git", diff --git a/packages/backend/eslint.config.js b/packages/backend/eslint.config.js index 4fd9f0cd51..ae7b2baf49 100644 --- a/packages/backend/eslint.config.js +++ b/packages/backend/eslint.config.js @@ -11,7 +11,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/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 c6e31797f8..0dd738a1e6 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": "tsc --noEmit && tsc -p test --noEmit", - "eslint": "eslint --quiet \"src/**/*.ts\"", + "typecheck": "tsc --noEmit && tsc -p test --noEmit && tsc -p test-federation --noEmit", + "eslint": "eslint --quiet \"{src,test-federation}/**/*.ts\"", "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" diff --git a/packages/backend/src/core/AbuseReportNotificationService.ts b/packages/backend/src/core/AbuseReportNotificationService.ts index fb7c7bd2c3..25e265f2b1 100644 --- a/packages/backend/src/core/AbuseReportNotificationService.ts +++ b/packages/backend/src/core/AbuseReportNotificationService.ts @@ -61,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) { @@ -285,8 +288,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { .log(updater, 'createAbuseReportNotificationRecipient', { recipientId: id, recipient: created, - }) - .then(); + }); return created; } @@ -324,8 +326,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { recipientId: params.id, before: beforeEntity, after: afterEntity, - }) - .then(); + }); return afterEntity; } @@ -346,8 +347,7 @@ export class AbuseReportNotificationService implements OnApplicationShutdown { .log(updater, 'deleteAbuseReportNotificationRecipient', { recipientId: id, recipient: entity, - }) - .then(); + }); } /** @@ -370,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 73baad5499..0b022d3b08 100644 --- a/packages/backend/src/core/AbuseReportService.ts +++ b/packages/backend/src/core/AbuseReportService.ts @@ -110,8 +110,7 @@ export class AbuseReportService { reportId: report.id, report: report, resolvedAs: ps.resolvedAs, - }) - .then(); + }); } return this.abuseUserReportsRepository.findBy({ id: In(reports.map(it => it.id)) }) @@ -148,8 +147,7 @@ export class AbuseReportService { .log(moderator, 'forwardAbuseReport', { reportId: report.id, report: report, - }) - .then(); + }); } @bindThis 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 f6b7955cd2..206d0dbe0a 100644 --- a/packages/backend/src/core/CaptchaService.ts +++ b/packages/backend/src/core/CaptchaService.ts @@ -119,5 +119,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/CustomEmojiService.ts b/packages/backend/src/core/CustomEmojiService.ts index 5db3c5b980..4566113449 100644 --- a/packages/backend/src/core/CustomEmojiService.ts +++ b/packages/backend/src/core/CustomEmojiService.ts @@ -103,19 +103,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(), @@ -135,7 +149,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], }); @@ -157,6 +171,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 7aeeb78178..73bbf03b26 100644 --- a/packages/backend/src/core/FederatedInstanceService.ts +++ b/packages/backend/src/core/FederatedInstanceService.ts @@ -47,7 +47,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); @@ -71,6 +71,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/NoteCreateService.ts b/packages/backend/src/core/NoteCreateService.ts index 0ce57f16e6..3647fa7231 100644 --- a/packages/backend/src/core/NoteCreateService.ts +++ b/packages/backend/src/core/NoteCreateService.ts @@ -511,13 +511,15 @@ export class NoteCreateService implements OnApplicationShutdown { } // Register host - if (this.userEntityService.isRemoteUser(user)) { - this.federatedInstanceService.fetch(user.host).then(async i => { - 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 => { + 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 f9f8ace386..4ecd2592b2 100644 --- a/packages/backend/src/core/NoteDeleteService.ts +++ b/packages/backend/src/core/NoteDeleteService.ts @@ -106,13 +106,15 @@ export class NoteDeleteService { this.perUserNotesChart.update(user, note, false); } - if (this.userEntityService.isRemoteUser(user)) { - this.federatedInstanceService.fetch(user.host).then(async i => { - 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 => { + 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 f35e456556..37028026cc 100644 --- a/packages/backend/src/core/QueueService.ts +++ b/packages/backend/src/core/QueueService.ts @@ -93,6 +93,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 583eea1a34..5af6b05942 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -101,6 +101,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; @@ -136,6 +137,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 @@ -416,49 +418,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 cc8a3d6461..3865392b7f 100644 --- a/packages/backend/src/core/SignupService.ts +++ b/packages/backend/src/core/SignupService.ts @@ -150,8 +150,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 4c45b95a64..55c8a52705 100644 --- a/packages/backend/src/core/WebhookTestService.ts +++ b/packages/backend/src/core/WebhookTestService.ts @@ -12,6 +12,7 @@ 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; @@ -446,6 +447,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 e042a85782..73281078e5 100644 --- a/packages/backend/src/core/activitypub/models/ApPersonService.ts +++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts @@ -408,13 +408,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/MetaEntityService.ts b/packages/backend/src/core/entities/MetaEntityService.ts index fbd982eb34..409dca3426 100644 --- a/packages/backend/src/core/entities/MetaEntityService.ts +++ b/packages/backend/src/core/entities/MetaEntityService.ts @@ -96,6 +96,7 @@ export class MetaEntityService { recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, + 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 c64e9151a7..3e1f094fce 100644 --- a/packages/backend/src/core/entities/NoteEntityService.ts +++ b/packages/backend/src/core/entities/NoteEntityService.ts @@ -22,6 +22,30 @@ import type { ReactionService } from '../ReactionService.js'; import type { UserEntityService } from './UserEntityService.js'; import type { DriveFileEntityService } from './DriveFileEntityService.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; @@ -90,7 +114,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; @@ -227,7 +251,7 @@ export class NoteEntityService implements OnModuleInit { return true; } else { // 指定されているかどうか - return note.visibleUserIds.some((id: any) => meId === id); + return note.visibleUserIds.some(id => meId === id); } } @@ -421,7 +445,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>(); @@ -432,7 +456,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/misc/is-renote.ts b/packages/backend/src/misc/is-renote.ts index 48f821806c..f4bb329d80 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/Meta.ts b/packages/backend/src/models/Meta.ts index d29689f907..ad5e31ad6f 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', { @@ -258,6 +263,11 @@ export class MiMeta { }) public turnstileSecretKey: string | null; + @Column('boolean', { + default: false, + }) + public enableTestcaptcha: boolean; + // chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること @Column('enum', { @@ -520,6 +530,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/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 99feeaa7d7..e3fd63464a 100644 --- a/packages/backend/src/models/json-schema/meta.ts +++ b/packages/backend/src/models/json-schema/meta.ts @@ -115,6 +115,10 @@ export const packedMetaLiteSchema = { type: 'string', optional: false, nullable: true, }, + enableTestcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, swPublickey: { type: 'string', optional: false, nullable: true, diff --git a/packages/backend/src/queue/QueueProcessorModule.ts b/packages/backend/src/queue/QueueProcessorModule.ts index 0027b5ef3d..9044285bf6 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'; @@ -80,6 +81,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 e9e1c45224..6940e1c188 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'; @@ -66,7 +67,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}`; } @@ -120,24 +121,35 @@ 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; - function renderError(e: Error): any { - if (e) { // 何故かeがundefinedで来ることがある - return { - stack: e.stack, - message: e.message, - name: e.name, - }; - } else { - return { - stack: '?', - message: '?', - name: '?', - }; + function renderError(e?: Error) { + // 何故かeがundefinedで来ることがある + if (!e) return '?'; + + if (e instanceof Bull.UnrecoverableError || e.name === 'AbortError') { + return `${e.name}: ${e.message}`; } + + return { + stack: e.stack, + message: e.message, + name: e.name, + }; + } + + function renderJob(job?: Bull.Job) { + if (!job) return '?'; + + return { + name: job.name || undefined, + info: getJobInfo(job), + failedReason: job.failedReason || undefined, + data: job.data, + }; } //#region system @@ -150,6 +162,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`); } @@ -172,15 +185,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err: Error) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); + logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.message}`, { + Sentry.captureMessage(`Queue: System: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -229,15 +242,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); + logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.message}`, { + Sentry.captureMessage(`Queue: DB: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -269,15 +282,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); + logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Deliver: ${err.message}`, { + Sentry.captureMessage(`Queue: Deliver: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -309,15 +322,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job, e: renderError(err) }); + logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} activity=${job ? (job.data.activity ? job.data.activity.id : 'none') : '-'}`, { job: renderJob(job), e: renderError(err) }); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Inbox: ${err.message}`, { + Sentry.captureMessage(`Queue: Inbox: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -349,15 +362,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); + logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.message}`, { + Sentry.captureMessage(`Queue: UserWebhookDeliver: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -389,15 +402,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active ${getJobInfo(job, true)} to=${job.data.to}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) ${getJobInfo(job, true)} to=${job.data.to}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); + logger.error(`failed(${err.name}: ${err.message}) ${getJobInfo(job)} to=${job ? job.data.to : '-'}`); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.message}`, { + Sentry.captureMessage(`Queue: SystemWebhookDeliver: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -436,15 +449,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); + logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.message}`, { + Sentry.captureMessage(`Queue: Relationship: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion @@ -477,15 +490,15 @@ export class QueueProcessorService implements OnApplicationShutdown { .on('active', (job) => logger.debug(`active id=${job.id}`)) .on('completed', (job, result) => logger.debug(`completed(${result}) id=${job.id}`)) .on('failed', (job, err) => { - logger.error(`failed(${err.stack}) id=${job ? job.id : '-'}`, { job, e: renderError(err) }); + logger.error(`failed(${err.name}: ${err.message}) id=${job?.id ?? '?'}`, { job: renderJob(job), e: renderError(err) }); if (config.sentryForBackend) { - Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.message}`, { + Sentry.captureMessage(`Queue: ObjectStorage: ${job?.name ?? '?'}: ${err.name}: ${err.message}`, { level: 'error', extra: { job, err }, }); } }) - .on('error', (err: Error) => logger.error(`error ${err.stack}`, { e: renderError(err) })) + .on('error', (err: Error) => logger.error(`error ${err.name}: ${err.message}`, { e: renderError(err) })) .on('stalled', (jobId) => logger.warn(`stalled id=${jobId}`)); } //#endregion 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 a77c968395..95d764e4d8 100644 --- a/packages/backend/src/queue/processors/InboxProcessorService.ts +++ b/packages/backend/src/queue/processors/InboxProcessorService.ts @@ -192,21 +192,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 be63635efe..3a8cb19f01 100644 --- a/packages/backend/src/server/api/ApiServerService.ts +++ b/packages/backend/src/server/api/ApiServerService.ts @@ -119,6 +119,7 @@ export class ApiServerService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; } }>('/signup', (request, reply) => this.signupApiService.signup(request, reply)); @@ -132,6 +133,7 @@ export class ApiServerService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; }; }>('/signin-flow', (request, reply) => this.signinApiService.signin(request, reply)); diff --git a/packages/backend/src/server/api/SigninApiService.ts b/packages/backend/src/server/api/SigninApiService.ts index 0d24ffa56a..1d983ca4bc 100644 --- a/packages/backend/src/server/api/SigninApiService.ts +++ b/packages/backend/src/server/api/SigninApiService.ts @@ -71,6 +71,7 @@ export class SigninApiService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; }; }>, reply: FastifyReply, @@ -194,6 +195,12 @@ export class SigninApiService { 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) { diff --git a/packages/backend/src/server/api/SignupApiService.ts b/packages/backend/src/server/api/SignupApiService.ts index c499638018..3ec5e5d3e6 100644 --- a/packages/backend/src/server/api/SignupApiService.ts +++ b/packages/backend/src/server/api/SignupApiService.ts @@ -67,6 +67,7 @@ export class SignupApiService { 'g-recaptcha-response'?: string; 'turnstile-response'?: string; 'm-captcha-response'?: string; + 'testcaptcha-response'?: string; } }>, reply: FastifyReply, @@ -99,6 +100,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/admin/emoji/update.ts b/packages/backend/src/server/api/endpoints/admin/emoji/update.ts index 22609a16a3..212cba5c5d 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'; @@ -78,25 +78,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 (ps.name && (ps.name !== emoji.name)) { - const isDuplicate = await this.customEmojiService.checkDuplicate(ps.name); - if (isDuplicate) throw new ApiError(meta.errors.sameNameEmojiExists); - } - } else { - if (!ps.name) throw new Error('Invalid Params unexpectedly passed. This is a BUG. Please report it to the development team.'); - const emoji = await this.customEmojiService.getEmojiByName(ps.name); - if (!emoji) throw new ApiError(meta.errors.noSuchEmoji); - emojiId = emoji.id; - } + // JSON schemeのanyOfの型変換がうまくいっていないらしい + const required = { id: ps.id, name: ps.name } 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: ps.name, category: ps.category, aliases: ps.aliases, license: ps.license, @@ -104,6 +93,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/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts index b76ed5c524..64e3cc33bd 100644 --- a/packages/backend/src/server/api/endpoints/admin/meta.ts +++ b/packages/backend/src/server/api/endpoints/admin/meta.ts @@ -69,6 +69,10 @@ export const meta = { type: 'string', optional: false, nullable: true, }, + enableTestcaptcha: { + type: 'boolean', + optional: false, nullable: false, + }, swPublickey: { type: 'string', optional: false, nullable: true, @@ -173,6 +177,13 @@ export const meta = { type: 'string', }, }, + prohibitedWordsForNameOfUser: { + type: 'array', + optional: false, nullable: false, + items: { + type: 'string', + }, + }, bannedEmailDomains: { type: 'array', optional: true, nullable: false, @@ -337,6 +348,10 @@ export const meta = { type: 'boolean', optional: false, nullable: false, }, + enableStatsForFederatedInstances: { + type: 'boolean', + optional: false, nullable: false, + }, enableServerMachineStats: { type: 'boolean', optional: false, nullable: false, @@ -555,6 +570,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- recaptchaSiteKey: instance.recaptchaSiteKey, enableTurnstile: instance.enableTurnstile, turnstileSiteKey: instance.turnstileSiteKey, + enableTestcaptcha: instance.enableTestcaptcha, swPublickey: instance.swPublicKey, themeColor: instance.themeColor, mascotImageUrl: instance.mascotImageUrl, @@ -581,6 +597,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, hcaptchaSecretKey: instance.hcaptchaSecretKey, mcaptchaSecretKey: instance.mcaptchaSecretKey, @@ -622,6 +639,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, enableIdenticonGeneration: instance.enableIdenticonGeneration, bannedEmailDomains: instance.bannedEmailDomains, 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 2fef9abbf9..2b2c8c60ab 100644 --- a/packages/backend/src/server/api/endpoints/admin/show-users.ts +++ b/packages/backend/src/server/api/endpoints/admin/show-users.ts @@ -71,13 +71,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-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts index 9ffae840b6..38ef0d1de8 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 }, @@ -78,6 +83,7 @@ export const paramDef = { enableTurnstile: { type: 'boolean' }, turnstileSiteKey: { type: 'string', nullable: true }, turnstileSecretKey: { 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' }, @@ -130,6 +136,7 @@ export const paramDef = { truemailAuthKey: { type: 'string', nullable: true }, enableChartsForRemoteUser: { type: 'boolean' }, enableChartsForFederatedInstances: { type: 'boolean' }, + enableStatsForFederatedInstances: { type: 'boolean' }, enableServerMachineStats: { type: 'boolean' }, enableIdenticonGeneration: { type: 'boolean' }, serverRules: { type: 'array', items: { type: 'string' } }, @@ -213,6 +220,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) => { @@ -357,6 +367,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> { // eslint- set.turnstileSecretKey = ps.turnstileSecretKey; } + if (ps.enableTestcaptcha !== undefined) { + set.enableTestcaptcha = ps.enableTestcaptcha; + } + if (ps.sensitiveMediaDetection !== undefined) { set.sensitiveMediaDetection = ps.sensitiveMediaDetection; } @@ -565,6 +579,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; } diff --git a/packages/backend/src/server/api/endpoints/i/update.ts b/packages/backend/src/server/api/endpoints/i/update.ts index 798bd98cf1..0b35005a87 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, 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'; @@ -114,6 +115,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: { @@ -223,6 +231,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, @@ -247,6 +258,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; @@ -449,6 +461,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 7c6a533429..a04640d993 100644 --- a/packages/backend/src/server/web/boot.js +++ b/packages/backend/src/server/web/boot.js @@ -98,7 +98,7 @@ 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/backend/src/server/web/style.css b/packages/backend/src/server/web/style.css index dbcc8f537c..5d81f2bed0 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(70px); - 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/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/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/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/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 470352469b..d711020a74 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 59f0d495e6..cae2feb8fb 100644 --- a/packages/frontend-embed/src/components/EmMfm.ts +++ b/packages/frontend-embed/src/components/EmMfm.ts @@ -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(' '); @@ -251,7 +251,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' || @@ -284,7 +284,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', @@ -355,7 +355,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven return [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}`)]; } diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue index f7899bfb03..f5b064c293 100644 --- a/packages/frontend-embed/src/components/EmNote.vue +++ b/packages/frontend-embed/src/components/EmNote.vue @@ -108,6 +108,8 @@ SPDX-License-Identifier: AGPL-3.0-only import { computed, inject, ref, shallowRef } from 'vue'; import * as mfm from 'mfm-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'; @@ -123,8 +125,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; @@ -164,14 +164,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; @@ -189,8 +183,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; } } @@ -212,9 +206,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 { @@ -259,7 +253,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; @@ -356,7 +350,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; } @@ -377,12 +371,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; @@ -403,16 +397,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; @@ -424,13 +418,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; } @@ -449,7 +443,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; } @@ -473,7 +467,7 @@ const isDeleted = ref(false); } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -550,7 +544,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 360de31864..b39b47c065 100644 --- a/packages/frontend-embed/src/components/EmNoteDetailed.vue +++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue @@ -195,7 +195,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 { @@ -281,7 +281,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; } @@ -323,14 +323,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 { @@ -350,7 +350,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; } @@ -364,12 +364,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; @@ -390,16 +390,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; @@ -422,7 +422,7 @@ const collapsed = ref(appearNote.value.cw == null && isLong); } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -438,7 +438,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 704a876e59..b9aaf3fa4a 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 f60aea3e7e..59be8608e0 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 db2666a45f..61815ddfd8 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 02008ddbd0..2e43cfd20a 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-shared/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5 index eb5fda3dc0..5271785e62 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)', acrylicPanel: ':alpha<0.5<@panel', windowHeader: ':alpha<0.85<@panel', popup: ':lighten<3<@panel', @@ -67,7 +67,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 e0196dcbf3..be331ce58f 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)', acrylicPanel: ':alpha<0.5<@panel', windowHeader: ':alpha<0.85<@panel', popup: ':lighten<3<@panel', @@ -67,7 +67,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/assets/testcaptcha.png b/packages/frontend/assets/testcaptcha.png Binary files differnew file mode 100644 index 0000000000..9bfd252b51 --- /dev/null +++ b/packages/frontend/assets/testcaptcha.png 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 84d89b1b3f..b91834b94f 100644 --- a/packages/frontend/src/account.ts +++ b/packages/frontend/src/account.ts @@ -226,7 +226,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(); }, @@ -236,9 +236,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 af05f657c8..1145891b71 100644 --- a/packages/frontend/src/boot/common.ts +++ b/packages/frontend/src/boot/common.ts @@ -182,24 +182,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 0278cb30f0..b9413270ae 100644 --- a/packages/frontend/src/components/MkAbuseReport.vue +++ b/packages/frontend/src/components/MkAbuseReport.vue @@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <MkFolder> <template #icon> - <i v-if="report.resolved && report.resolvedAs === 'accept'" class="ti ti-check" style="color: var(--success)"></i> - <i v-else-if="report.resolved && report.resolvedAs === 'reject'" class="ti ti-x" style="color: var(--error)"></i> + <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(--warn)"></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> @@ -17,8 +17,8 @@ SPDX-License-Identifier: AGPL-3.0-only <template #footer> <div class="_buttons"> <template v-if="!report.resolved"> - <MkButton @click="resolve('accept')"><i class="ti ti-check" style="color: var(--success)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.accept }})</MkButton> - <MkButton @click="resolve('reject')"><i class="ti ti-x" style="color: var(--error)"></i> {{ i18n.ts._abuseUserReport.resolve }} ({{ i18n.ts._abuseUserReport.reject }})</MkButton> + <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"> 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 f27694658e..1adb244c9e 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 b50a7fea5c..e52ab5ccad 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: 8px; } </style> diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue index f547991369..0ea4566d4e 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 1156b3f2b8..311facb4aa 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: 5px; 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 c5b6e0caed..82fc89e51c 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> @@ -29,7 +40,7 @@ export type Captcha = { getResponse(id: string): string; }; -export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha'; +export type CaptchaProvider = 'hcaptcha' | 'recaptcha' | 'turnstile' | 'mcaptcha' | 'testcaptcha'; type CaptchaContainer = { readonly [_ in CaptchaProvider]?: Captcha; @@ -54,12 +65,16 @@ 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'; case 'recaptcha': return 'grecaptcha'; case 'turnstile': return 'turnstile'; case 'mcaptcha': return 'mcaptcha'; + case 'testcaptcha': return 'testcaptcha'; } }); @@ -71,6 +86,7 @@ const src = computed(() => { case 'recaptcha': return 'https://www.recaptcha.net/recaptcha/api.js?render=explicit'; case 'turnstile': return 'https://challenges.cloudflare.com/turnstile/v0/api.js?render=explicit'; case 'mcaptcha': return null; + case 'testcaptcha': return null; } }); @@ -78,7 +94,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'), { @@ -91,6 +107,8 @@ if (loaded || props.provider === 'mcaptcha') { function reset() { if (captcha.value.reset) captcha.value.reset(); + testcaptchaPassed.value = false; + testcaptchaInput.value = ''; } async function requestRender() { @@ -127,6 +145,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 35dc3ad4bf..d4e4f6179a 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 3c0874a1eb..99580df5e2 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: 6px; 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: 100%; 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 6eb2009784..574cde9da4 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: 999px; &: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 c0e7df5dac..0d7a67eaec 100644 --- a/packages/frontend/src/components/MkCode.core.vue +++ b/packages/frontend/src/components/MkCode.core.vue @@ -77,7 +77,7 @@ watch(() => props.lang, (to) => { margin: .5em 0; overflow: auto; border-radius: 8px; - 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 716dd92678..cb82bfd98b 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: 8px; 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 afd9132a12..5bf2301e72 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: 6px; 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: 6px; } @@ -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: 6px; @@ -211,6 +211,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 f5c580789b..55a32664de 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: 6px; 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 8ad653a0bf..8ab01d7db8 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -165,11 +165,11 @@ onUnmounted(() => { .header { position: sticky; - top: var(--stickyTop, 0px); + top: var(--MI-stickyTop, 0px); left: 0; - color: var(--panelHeaderFg); - background: var(--panelHeaderBg); - 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; } @@ -201,7 +201,7 @@ onUnmounted(() => { } .content { - --stickyTop: 0px; + --MI-stickyTop: 0px; &.omitted { position: relative; @@ -216,11 +216,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: 999px; @@ -229,7 +229,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 4b94bef4b6..f04e5cf7c6 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); } } @@ -194,7 +197,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); } } } @@ -235,7 +238,7 @@ export default defineComponent({ line-height: 32px; text-align: center; font-size: 12px; - color: var(--dateLabelFg); + color: var(--MI_THEME-dateLabelFg); } .date-1 { @@ -253,5 +256,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 16cf5b1b75..22130d4fab 100644 --- a/packages/frontend/src/components/MkDialog.vue +++ b/packages/frontend/src/components/MkDialog.vue @@ -184,7 +184,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: 16px; } @@ -206,15 +206,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 098be07a8c..0e0da64750 100644 --- a/packages/frontend/src/components/MkDonation.vue +++ b/packages/frontend/src/components/MkDonation.vue @@ -65,12 +65,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; } @@ -79,7 +79,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 90284890a5..e45c3bd9ce 100644 --- a/packages/frontend/src/components/MkDrive.file.vue +++ b/packages/frontend/src/components/MkDrive.file.vue @@ -148,14 +148,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 { @@ -244,7 +244,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 92b3a23662..44e3b59ade 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: 4px; 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: 4px; } } @@ -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 d9ca0a72a0..23883a44e9 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -721,7 +721,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; } @@ -768,7 +768,7 @@ onBeforeUnmount(() => { .main { flex: 1; overflow: auto; - padding: var(--margin); + padding: var(--MI-margin); user-select: none; &.fetching { @@ -815,7 +815,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 eb93aaab6e..3410a915c3 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: 8px; 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 fca7aa2f4e..f4caa730bf 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: 6px; border-bottom: 0.5px solid var(--divider);"> +<section v-if="!hasChildSection" v-panel style="border-radius: 6px; 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-icons"></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: 6px; border-bottom: 0.5px solid var(--divider);"> +<section v-else v-panel style="border-radius: 6px; 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="ti ti-icons ti-fw"></i>:{{ emojis.length }}) </header> diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue index 3bad8da06f..219950f135 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.vue b/packages/frontend/src/components/MkExtensionInstaller.vue index 0f7acd69e7..b41604b2c3 100644 --- a/packages/frontend/src/components/MkExtensionInstaller.vue +++ b/packages/frontend/src/components/MkExtensionInstaller.vue @@ -110,8 +110,8 @@ const emits = defineEmits<{ <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; } @@ -125,8 +125,8 @@ const emits = defineEmits<{ margin-left: auto; margin-right: auto; - background-color: var(--accentedBg); - color: var(--accent); + background-color: var(--MI_THEME-accentedBg); + color: var(--MI_THEME-accent); } .extInstallerTitle { diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue index 13295c455b..d5d32ebb28 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 8262ae5d0c..5f9500d923 100644 --- a/packages/frontend/src/components/MkFolder.vue +++ b/packages/frontend/src/components/MkFolder.vue @@ -118,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> @@ -144,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: 6px; transition: border-radius 0.3s; &:hover { text-decoration: none; - background: var(--folderHeaderHoverBg); + background: var(--MI_THEME-folderHeaderHoverBg); } &:focus-within { @@ -160,8 +160,8 @@ onMounted(() => { } &.active { - color: var(--accent); - background: var(--folderHeaderHoverBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-folderHeaderHoverBg); } &.opened { @@ -175,7 +175,7 @@ onMounted(() => { } .headerLower { - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); font-size: .85em; padding-left: 4px; } @@ -209,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; } @@ -224,26 +224,26 @@ onMounted(() => { } .body { - background: var(--panel); + background: var(--MI_THEME-panel); border-radius: 0 0 6px 6px; 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 0de52906ed..ccea7cd453 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 index 09825487bf..8b1c56fca4 100644 --- a/packages/frontend/src/components/MkFukidashi.vue +++ b/packages/frontend/src/components/MkFukidashi.vue @@ -39,8 +39,8 @@ withDefaults(defineProps<{ <style module lang="scss"> .root { - --fukidashi-radius: var(--radius); - --fukidashi-bg: var(--panel); + --fukidashi-radius: var(--MI-radius); + --fukidashi-bg: var(--MI_THEME-panel); position: relative; display: inline-block; @@ -48,7 +48,7 @@ withDefaults(defineProps<{ padding-top: calc(var(--fukidashi-radius) * .13); &.shadow { - filter: drop-shadow(0 4px 32px var(--shadow)); + filter: drop-shadow(0 4px 32px var(--MI_THEME-shadow)); } &.left { 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 2988d77fe3..2eaee5b115 100644 --- a/packages/frontend/src/components/MkGoogle.vue +++ b/packages/frontend/src/components/MkGoogle.vue @@ -39,7 +39,7 @@ const search = () => { width: 100%; height: 40px; font-size: 16px; - border: solid 1px var(--divider); + border: solid 1px var(--MI_THEME-divider); border-radius: 4px 0 0 4px; -webkit-appearance: textfield; } @@ -48,7 +48,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 4px 4px 0; diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue index 33e65ccc4e..90ca1b5a9d 100644 --- a/packages/frontend/src/components/MkInfo.vue +++ b/packages/frontend/src/components/MkInfo.vue @@ -36,14 +36,14 @@ function close() { align-items: center; padding: 12px 14px; font-size: 90%; - background: var(--infoBg); - color: var(--infoFg); - border-radius: var(--radius); + background: var(--MI_THEME-infoBg); + color: var(--MI_THEME-infoFg); + border-radius: var(--MI-radius); white-space: pre-wrap; &.warn { - background: var(--infoWarnBg); - color: var(--infoWarnFg); + background: var(--MI_THEME-infoWarnBg); + color: var(--MI_THEME-infoWarnFg); } } diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index 4c2fc1ba00..e01ff86c5a 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: 6px; 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 17c974dd04..b0601cf7f9 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: 8px; > :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 8e3c19bd12..32c1a2d172 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 b41705d5e6..8b713b2734 100644 --- a/packages/frontend/src/components/MkMediaAudio.vue +++ b/packages/frontend/src/components/MkMediaAudio.vue @@ -391,8 +391,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 { @@ -412,7 +412,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); } } @@ -454,12 +454,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 11995e1f3b..3e521e0a03 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 0c5c8fd9de..ec85569df5 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> <button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button> <i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i> @@ -165,7 +165,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); } } @@ -186,8 +186,8 @@ function showMenu(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; @@ -206,19 +206,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 { @@ -226,8 +226,8 @@ html[data-color-scheme=light] .visible { position: absolute; border-radius: 999px; 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; @@ -258,10 +258,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/src/components/MkMediaList.vue b/packages/frontend/src/components/MkMediaList.vue index 4a4a99be25..32766f2029 100644 --- a/packages/frontend/src/components/MkMediaList.vue +++ b/packages/frontend/src/components/MkMediaList.vue @@ -310,14 +310,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 { @@ -335,14 +335,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 1b1915e6c8..d3a12ca734 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]"> @@ -508,7 +508,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); } } @@ -523,10 +523,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; @@ -537,8 +537,8 @@ onDeactivated(() => { 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; @@ -592,7 +592,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; @@ -658,12 +658,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 71bd5addfb..ac2d3f4398 100644 --- a/packages/frontend/src/components/MkMention.vue +++ b/packages/frontend/src/components/MkMention.vue @@ -47,12 +47,12 @@ const avatarUrl = computed(() => defaultStore.state.disableShowingAnimatedImages display: inline-block; padding: 4px 8px 4px 4px; border-radius: 999px; - color: var(--mention); - background: color(from var(--mention) srgb r g b / 0.1); + color: var(--MI_THEME-mention); + background: color(from var(--MI_THEME-mention) srgb r g b / 0.1); &.isMe { - color: var(--mentionMe); - background: color(from var(--mentionMe) srgb r g b / 0.1); + 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 14f6bdcc34..13a65e411f 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> @@ -507,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: ""; @@ -527,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; } } @@ -536,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)); } } } @@ -566,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 { @@ -637,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 { @@ -654,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: ""; @@ -670,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 e8ff743bf2..828ad2e872 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'" > <MkNoteSub v-if="appearNote.reply && !renoteCollapsed" :note="appearNote.reply" :class="$style.replyTo"/> @@ -126,8 +126,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-ban"></i> </button> <button ref="reactButton" :class="$style.footerButton" 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="ti ti-plus"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.footerButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -171,6 +171,9 @@ import { computed, inject, onMounted, ref, shallowRef, Ref, watch, provide } fro import * as mfm from 'mfm-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'; @@ -200,11 +203,8 @@ 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 { 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'; @@ -619,14 +619,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; @@ -643,8 +635,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; } } @@ -666,9 +658,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: 8px; - box-shadow: 0px 4px 32px var(--shadow); + box-shadow: 0px 4px 32px var(--MI_THEME-shadow); } .footerButton { @@ -687,6 +679,11 @@ function emitUpdReaction(emoji: string, delta: number) { } } +.skipRender { + content-visibility: auto; + contain-intrinsic-size: 0 150px; +} + .tip { display: flex; align-items: center; @@ -713,7 +710,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; @@ -810,7 +807,7 @@ function emitUpdReaction(emoji: string, delta: number) { width: 58px; height: 58px; position: sticky !important; - top: calc(22px + var(--stickyTop, 0px)); + top: calc(22px + var(--MI-stickyTop, 0px)); left: 0; } @@ -831,12 +828,12 @@ function emitUpdReaction(emoji: string, delta: number) { 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; @@ -857,16 +854,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: 999px; @@ -878,13 +875,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; } @@ -903,7 +900,7 @@ function emitUpdReaction(emoji: string, delta: number) { .quoteNote { padding: 16px; - border: dashed 1px var(--renote); + border: dashed 1px var(--MI_THEME-renote); border-radius: 8px; overflow: clip; } @@ -927,7 +924,7 @@ function emitUpdReaction(emoji: string, delta: number) { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -998,7 +995,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 bdb800b32a..6d53685651 100644 --- a/packages/frontend/src/components/MkNoteDetailed.vue +++ b/packages/frontend/src/components/MkNoteDetailed.vue @@ -135,8 +135,8 @@ SPDX-License-Identifier: AGPL-3.0-only <i class="ti ti-ban"></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="ti ti-plus"></i> <p v-if="(appearNote.reactionAcceptance === 'likeOnly' || defaultStore.state.showReactionsCount) && appearNote.reactionCount > 0" :class="$style.noteFooterButtonCount">{{ number(appearNote.reactionCount) }}</p> @@ -569,8 +569,8 @@ function loadConversation() { 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; } } @@ -591,7 +591,7 @@ function loadConversation() { padding: 16px 32px 8px 32px; line-height: 28px; white-space: pre; - color: var(--renote); + color: var(--MI_THEME-renote); } .renoteAvatar { @@ -671,7 +671,7 @@ function loadConversation() { 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; } @@ -699,19 +699,19 @@ function loadConversation() { } .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; } @@ -726,7 +726,7 @@ function loadConversation() { .quoteNote { padding: 16px; - border: dashed 1px var(--renote); + border: dashed 1px var(--MI_THEME-renote); border-radius: 8px; overflow: clip; } @@ -752,7 +752,7 @@ function loadConversation() { } &:hover { - color: var(--fgHighlighted); + color: var(--MI_THEME-fgHighlighted); } } @@ -762,17 +762,17 @@ function loadConversation() { 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; } @@ -784,7 +784,7 @@ function loadConversation() { } .tabActive { - border-bottom: solid 2px var(--accent); + border-bottom: solid 2px var(--MI_THEME-accent); } .tab_renotes { @@ -804,12 +804,12 @@ function loadConversation() { .reactionTab { padding: 4px 6px; - border: solid 1px var(--divider); + border: solid 1px var(--MI_THEME-divider); border-radius: 6px; } .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 a75b9ddd10..750e32a9ff 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -78,7 +78,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: 3px; } diff --git a/packages/frontend/src/components/MkNoteSimple.vue b/packages/frontend/src/components/MkNoteSimple.vue index c3f3c42b42..e684cf2a30 100644 --- a/packages/frontend/src/components/MkNoteSimple.vue +++ b/packages/frontend/src/components/MkNoteSimple.vue @@ -51,7 +51,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/src/components/MkNoteSub.vue b/packages/frontend/src/components/MkNoteSub.vue index 829b37e7a7..e4bade309b 100644 --- a/packages/frontend/src/components/MkNoteSub.vue +++ b/packages/frontend/src/components/MkNoteSub.vue @@ -135,7 +135,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; } @@ -156,7 +156,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/src/components/MkNotes.vue b/packages/frontend/src/components/MkNotes.vue index 0856c146ba..1c17c6b691 100644 --- a/packages/frontend/src/components/MkNotes.vue +++ b/packages/frontend/src/components/MkNotes.vue @@ -56,17 +56,17 @@ defineExpose({ .root { &.noGap { > .notes { - background: var(--panel); + background: var(--MI_THEME-panel); } } &:not(.noGap) { > .notes { - background: var(--bg); + background: var(--MI_THEME-bg); .note { - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } } } diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue index b27d883b85..093bdb8b4c 100644 --- a/packages/frontend/src/components/MkNotification.vue +++ b/packages/frontend/src/components/MkNotification.vue @@ -220,11 +220,13 @@ 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; @@ -284,8 +286,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) height: 20px; box-sizing: border-box; border-radius: 100%; - 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; @@ -437,8 +439,8 @@ function getActualReactedUsersCount(notification: Misskey.entities.Notification) height: 20px; box-sizing: border-box; border-radius: 100%; - 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 d67616e6b2..5a6ada474a 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -106,6 +106,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 ee1f15c189..a05176e2f4 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: 999px; @@ -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 2b993ab12f..4777da2848 100644 --- a/packages/frontend/src/components/MkPageWindow.vue +++ b/packages/frontend/src/components/MkPageWindow.vue @@ -179,8 +179,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 e1d5db2730..48913004e0 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 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> @@ -114,8 +114,8 @@ const vote = async (id) => { 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; cursor: pointer; @@ -126,8 +126,8 @@ const vote = async (id) => { 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; } @@ -135,12 +135,12 @@ const vote = async (id) => { 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); } .done { diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 471ffdd896..76a6e4212a 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -1113,7 +1113,7 @@ defineExpose({ outline: none; .submitInner { - outline: 2px solid var(--fgOnAccent); + outline: 2px solid var(--MI_THEME-fgOnAccent); outline-offset: -4px; } } @@ -1128,13 +1128,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))); } } } @@ -1156,8 +1156,8 @@ defineExpose({ border-radius: 6px; 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 { @@ -1166,7 +1166,7 @@ defineExpose({ border-radius: 6px; &:hover { - background: var(--X5); + background: var(--MI_THEME-X5); } &:disabled { @@ -1218,7 +1218,7 @@ html[data-color-scheme=light] .preview { .withQuote { margin: 0 0 8px 0; - color: var(--accent); + color: var(--MI_THEME-accent); } .toSpecified { @@ -1238,7 +1238,7 @@ html[data-color-scheme=light] .preview { margin-right: 14px; padding: 8px 0 8px 8px; border-radius: 8px; - background: var(--X4); + background: var(--MI_THEME-X4); } .hasNotSpecifiedMentions { @@ -1257,7 +1257,7 @@ html[data-color-scheme=light] .preview { border: none; border-radius: 0; background: transparent; - color: var(--fg); + color: var(--MI_THEME-fg); font-family: inherit; &:focus { @@ -1272,14 +1272,14 @@ html[data-color-scheme=light] .preview { .cw { z-index: 1; padding-bottom: 8px; - border-bottom: solid 0.5px var(--divider); + border-bottom: solid 0.5px var(--MI_THEME-divider); } .hashtags { 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 { @@ -1305,7 +1305,7 @@ html[data-color-scheme=light] .preview { right: 2px; padding: 4px 6px; font-size: .9em; - color: var(--warn); + color: var(--MI_THEME-warn); border-radius: 6px; min-width: 1.6em; text-align: center; @@ -1349,16 +1349,16 @@ html[data-color-scheme=light] .preview { border-radius: 6px; &: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 42322fec3d..ee7038df64 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 22fc86723e..e735d9fff8 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: 6px; 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: 100%; 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 cfaaa67d58..264b559222 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: 6px; > .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: 999px; } } @@ -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: 999px; &: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 8038ec7429..f4c3643ba8 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 f42a0b3227..b65038aadc 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 f1050d26e6..a56a4b1671 100644 --- a/packages/frontend/src/components/MkRemoteCaution.vue +++ b/packages/frontend/src/components/MkRemoteCaution.vue @@ -19,14 +19,14 @@ defineProps<{ .root { font-size: 0.8em; padding: 16px; - background: var(--infoWarnBg); - color: var(--infoWarnFg); - border-radius: var(--radius); + background: var(--MI_THEME-infoWarnBg); + color: var(--MI_THEME-infoWarnFg); + border-radius: var(--MI-radius); overflow: clip; } .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 343524fc82..a2ec384ac5 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: 6px; outline: none; box-shadow: none; diff --git a/packages/frontend/src/components/MkSignin.input.vue b/packages/frontend/src/components/MkSignin.input.vue index 6336b78c80..34c22abc31 100644 --- a/packages/frontend/src/components/MkSignin.input.vue +++ b/packages/frontend/src/components/MkSignin.input.vue @@ -162,8 +162,8 @@ async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<v .avatar { margin: 0 auto; - background-color: color-mix(in srgb, var(--fg), transparent 85%); - color: color-mix(in srgb, var(--fg), transparent 25%); + 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; @@ -188,7 +188,7 @@ async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<v margin: .4em auto; width: 100%; height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .orMsg { @@ -196,9 +196,9 @@ async function specifyHostAndOpenRemote(options: OpenOnRemoteOptions): Promise<v top: -.6em; display: inline-block; padding: 0 1em; - background: var(--panel); + background: var(--MI_THEME-panel); font-size: 0.8em; - color: var(--fgOnPanel); + color: var(--MI_THEME-fgOnPanel); margin: 0; left: 50%; transform: translateX(-50%); diff --git a/packages/frontend/src/components/MkSignin.passkey.vue b/packages/frontend/src/components/MkSignin.passkey.vue index 0d68955fab..e5a56ab66d 100644 --- a/packages/frontend/src/components/MkSignin.passkey.vue +++ b/packages/frontend/src/components/MkSignin.passkey.vue @@ -75,8 +75,8 @@ onMounted(() => { .passkeyIcon { 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/MkSignin.password.vue b/packages/frontend/src/components/MkSignin.password.vue index 2d79e2aeb1..5608122a39 100644 --- a/packages/frontend/src/components/MkSignin.password.vue +++ b/packages/frontend/src/components/MkSignin.password.vue @@ -28,6 +28,7 @@ SPDX-License-Identifier: AGPL-3.0-only <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> @@ -44,6 +45,7 @@ export type PwResponse = { mCaptchaResponse: string | null; reCaptchaResponse: string | null; turnstileResponse: string | null; + testcaptchaResponse: string | null; }; }; </script> @@ -75,18 +77,21 @@ 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.enableTurnstile && !turnstileResponse.value) || + (instance.enableTestcaptcha && !testcaptchaResponse.value) ); }); @@ -104,6 +109,7 @@ function onSubmit() { mCaptchaResponse: mCaptchaResponse.value, reCaptchaResponse: reCaptchaResponse.value, turnstileResponse: turnstileResponse.value, + testcaptchaResponse: testcaptchaResponse.value, }, }); } @@ -113,6 +119,7 @@ function resetCaptcha() { mCaptcha.value?.reset(); reCaptcha.value?.reset(); turnstile.value?.reset(); + testcaptcha.value?.reset(); } defineExpose({ @@ -163,7 +170,7 @@ defineExpose({ margin: .4em auto; width: 100%; height: 1px; - background: var(--divider); + background: var(--MI_THEME-divider); } .orMsg { @@ -171,9 +178,9 @@ defineExpose({ top: -.6em; display: inline-block; padding: 0 1em; - background: var(--panel); + background: var(--MI_THEME-panel); font-size: 0.8em; - color: var(--fgOnPanel); + color: var(--MI_THEME-fgOnPanel); margin: 0; left: 50%; transform: translateX(-50%); diff --git a/packages/frontend/src/components/MkSignin.totp.vue b/packages/frontend/src/components/MkSignin.totp.vue index 880c08315e..670b8057c2 100644 --- a/packages/frontend/src/components/MkSignin.totp.vue +++ b/packages/frontend/src/components/MkSignin.totp.vue @@ -57,8 +57,8 @@ const isBackupCode = ref(false); .totpIcon { 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/MkSignin.vue b/packages/frontend/src/components/MkSignin.vue index 26e1ac516c..776ee20e36 100644 --- a/packages/frontend/src/components/MkSignin.vue +++ b/packages/frontend/src/components/MkSignin.vue @@ -68,6 +68,8 @@ import { nextTick, onBeforeUnmount, ref, shallowRef, useTemplateRef } from 'vue' import * as Misskey from 'misskey-js'; 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 { misskeyApi } from '@/scripts/misskey-api.js'; import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js'; import { login } from '@/account.js'; @@ -79,11 +81,8 @@ import XPassword, { type PwResponse } from '@/components/MkSignin.password.vue'; import XTotp from '@/components/MkSignin.totp.vue'; import XPasskey from '@/components/MkSignin.passkey.vue'; -import type { AuthenticationPublicKeyCredential } from '@github/webauthn-json/browser-ponyfill'; -import type { OpenOnRemoteOptions } from '@/scripts/please-login.js'; - const emit = defineEmits<{ - (ev: 'login', v: Misskey.entities.SigninFlowResponse): void; + (ev: 'login', v: Misskey.entities.SigninFlowResponse & { finished: true }): void; }>(); const props = withDefaults(defineProps<{ @@ -188,6 +187,7 @@ async function onPasswordSubmitted(pw: PwResponse) { 'm-captcha-response': pw.captcha.mCaptchaResponse, 'g-recaptcha-response': pw.captcha.reCaptchaResponse, 'turnstile-response': pw.captcha.turnstileResponse, + 'testcaptcha-response': pw.captcha.testcaptchaResponse, }); } } @@ -276,7 +276,7 @@ async function tryLogin(req: Partial<Misskey.entities.SigninFlowRequest>): Promi }); } -async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true; }) { +async function onLoginSucceeded(res: Misskey.entities.SigninFlowResponse & { finished: true }) { if (props.autoSet) { await login(res.i); } @@ -409,7 +409,7 @@ onBeforeUnmount(() => { left: 0; width: 100%; height: 100%; - background-color: color-mix(in srgb, var(--panel), transparent 50%); + background-color: color-mix(in srgb, var(--MI_THEME-panel), transparent 50%); display: flex; justify-content: center; align-items: center; diff --git a/packages/frontend/src/components/MkSigninDialog.vue b/packages/frontend/src/components/MkSigninDialog.vue index 8351d7d5e0..676a336ec7 100644 --- a/packages/frontend/src/components/MkSigninDialog.vue +++ b/packages/frontend/src/components/MkSigninDialog.vue @@ -23,6 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only </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'; @@ -40,7 +41,7 @@ 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; }>(); @@ -52,7 +53,7 @@ function onClose() { if (modal.value) modal.value.close(); } -function onLogin(res) { +function onLogin(res: Misskey.entities.SigninFlowResponse & { finished: true }) { emit('done', res); if (modal.value) modal.value.close(); } @@ -68,8 +69,8 @@ function onLogin(res) { height: 100%; max-height: 450px; box-sizing: border-box; - background: var(--panel); - border-radius: var(--radius); + background: var(--MI_THEME-panel); + border-radius: var(--MI-radius); } .header { @@ -82,8 +83,8 @@ function onLogin(res) { display: flex; align-items: center; font-weight: bold; - backdrop-filter: var(--blur, blur(15px)); - background: var(--acrylicBg); + backdrop-filter: var(--MI-blur, blur(15px)); + background: var(--MI_THEME-acrylicBg); z-index: 1; } diff --git a/packages/frontend/src/components/MkSignupDialog.form.vue b/packages/frontend/src/components/MkSignupDialog.form.vue index ff096dc729..3d1c44fc90 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,38 +34,39 @@ 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> <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"/> <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"/> @@ -98,7 +99,7 @@ const props = withDefaults(defineProps<{ }); const emit = defineEmits<{ - (ev: 'signup', user: Misskey.entities.SigninFlowResponse): void; + (ev: 'signup', user: Misskey.entities.SignupResponse): void; (ev: 'signupEmailPending'): void; }>(); @@ -108,6 +109,7 @@ const hcaptcha = ref<Captcha | undefined>(); const mcaptcha = ref<Captcha | undefined>(); const recaptcha = ref<Captcha | undefined>(); const turnstile = ref<Captcha | undefined>(); +const testcaptcha = ref<Captcha | undefined>(); const username = ref<string>(''); const password = ref<string>(''); @@ -123,6 +125,7 @@ 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 usernameAbortController = ref<null | AbortController>(null); const emailAbortController = ref<null | AbortController>(null); @@ -132,6 +135,7 @@ const shouldDisableSubmitting = computed((): boolean => { instance.enableMcaptcha && !mCaptchaResponse.value || instance.enableRecaptcha && !reCaptchaResponse.value || instance.enableTurnstile && !turnstileResponse.value || + instance.enableTestcaptcha && !testcaptchaResponse.value || instance.emailRequiredForSignup && emailState.value !== 'ok' || usernameState.value !== 'ok' || passwordRetypeState.value !== 'match'; @@ -250,18 +254,31 @@ 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, - 'hcaptcha-response': hCaptchaResponse.value, - 'm-captcha-response': mCaptchaResponse.value, - 'g-recaptcha-response': reCaptchaResponse.value, - 'turnstile-response': turnstileResponse.value, - }); - if (instance.emailRequiredForSignup) { + const signupPayload: Misskey.entities.SignupRequest = { + username: username.value, + password: password.value, + emailAddress: email.value, + invitationCode: invitationCode.value, + 'hcaptcha-response': hCaptchaResponse.value, + 'm-captcha-response': mCaptchaResponse.value, + 'g-recaptcha-response': reCaptchaResponse.value, + 'turnstile-response': turnstileResponse.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, @@ -269,33 +286,32 @@ async function onSubmit(): Promise<void> { }); emit('signupEmailPending'); } else { - const res = await misskeyApi('signin-flow', { - 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 && res.finished) { - return login(res.i); - } else { - os.alert({ - type: 'error', - text: i18n.ts.somethingHappened, - }); + if (props.autoSet) { + await login(resJson.token); } } - } catch { - submitting.value = false; - hcaptcha.value?.reset?.(); - mcaptcha.value?.reset?.(); - recaptcha.value?.reset?.(); - turnstile.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?.(); + testcaptcha.value?.reset?.(); + + os.alert({ + type: 'error', + text: i18n.ts.somethingHappened, + }); } </script> @@ -304,8 +320,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 59a3651cd4..e2a06dd91f 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="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://misskey-hub.net/docs/for-users/onboarding/warning/" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a> @@ -150,8 +150,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 { @@ -170,14 +170,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 4cccd99492..f240e6dc46 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.SigninFlowResponse): 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.SigninFlowResponse) { +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 1845b01b69..4c197ed43e 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; } @@ -77,7 +77,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 3bbb163f0f..9e02884b8c 100644 --- a/packages/frontend/src/components/MkSubNoteContent.vue +++ b/packages/frontend/src/components/MkSubNoteContent.vue @@ -62,11 +62,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; @@ -75,7 +75,7 @@ const collapsed = ref(isLong); &:hover { > .fadeLabel { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } } } @@ -84,25 +84,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/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue index 3746ffd8f3..6e7a875dec 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: 100%; } diff --git a/packages/frontend/src/components/MkSwitch.button.vue b/packages/frontend/src/components/MkSwitch.button.vue index 226908e221..fd8ed0992e 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: 999px; 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 f2d0c95013..f557ffa5dc 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 59490c552a..d1a6e1ebbf 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: 6px; 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 a3620aab68..10365d29b1 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: 4px; - 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 2a26d22dc2..b26a01737e 100644 --- a/packages/frontend/src/components/MkTutorialDialog.Note.vue +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -18,7 +18,7 @@ SPDX-License-Identifier: AGPL-3.0-only <div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div> <div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div> <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>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div> + <div v-if="onceReacted"><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.ts._initialTutorial._reaction.reactDone }}</div> </div> </template> @@ -105,13 +105,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 27483cc7c2..6559307a94 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: 999px; - 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 d8d4b5aab7..f7b60fbc45 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: 999px; - 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 2d4da3fbd4..b203110ef0 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: 999px; - 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 f8af276836..c937b4ce59 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 f5f9b43197..c287effadc 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: 8px; 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 d3e77b2818..7a2e878931 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: 8px; } @@ -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 f0b9606590..0164515a8a 100644 --- a/packages/frontend/src/components/MkUserInfo.vue +++ b/packages/frontend/src/components/MkUserInfo.vue @@ -69,7 +69,7 @@ defineProps<{ z-index: 2; width: 58px; height: 58px; - border: solid 4px var(--panel); + border: solid 4px var(--MI_THEME-panel); } .title { @@ -90,7 +90,7 @@ defineProps<{ margin: 0; line-height: 16px; font-size: 0.8em; - color: var(--fg); + color: var(--MI_THEME-fg); opacity: 0.7; } @@ -108,7 +108,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 { @@ -120,7 +120,7 @@ defineProps<{ .status { padding: 10px 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .statusItem { @@ -131,12 +131,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 17a9254d01..8b4afd7994 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 c39a900bcf..5cebeea2f4 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); border-radius: 120%; // Blinkのバグか知らんけど、100%ぴったりにすると何故か若干楕円でレンダリングされる &.status_online { diff --git a/packages/frontend/src/components/MkUserPopup.vue b/packages/frontend/src/components/MkUserPopup.vue index ea1241002e..740202f28b 100644 --- a/packages/frontend/src/components/MkUserPopup.vue +++ b/packages/frontend/src/components/MkUserPopup.vue @@ -18,7 +18,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/> @@ -197,8 +197,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); } .mfm { @@ -220,7 +220,7 @@ onMounted(() => { .statusItemLabel { font-size: 0.7em; - color: var(--fgTransparentWeak); + color: var(--MI_THEME-fgTransparentWeak); } .menu { @@ -228,7 +228,7 @@ onMounted(() => { top: 8px; right: 44px; padding: 6px; - background: var(--panel); + background: var(--MI_THEME-panel); border-radius: 999px; } diff --git a/packages/frontend/src/components/MkUserSelectDialog.vue b/packages/frontend/src/components/MkUserSelectDialog.vue index 1374817c72..8e58a6c5a2 100644 --- a/packages/frontend/src/components/MkUserSelectDialog.vue +++ b/packages/frontend/src/components/MkUserSelectDialog.vue @@ -195,11 +195,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 bb9af676e2..004edab630 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: 58px; height: 58px; - 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 75066bbc32..650e639c4f 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 a6c8baeaaa..97c765d81c 100644 --- a/packages/frontend/src/components/MkVisitorDashboard.vue +++ b/packages/frontend/src/components/MkVisitorDashboard.vue @@ -106,8 +106,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%); } @@ -178,14 +178,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 { @@ -194,7 +194,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 0c51cfa9ce..492dd4cdc0 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 d6585bf4a5..8fa9e4affb 100644 --- a/packages/frontend/src/components/form/link.vue +++ b/packages/frontend/src/components/form/link.vue @@ -51,18 +51,18 @@ const props = defineProps<{ width: 100%; box-sizing: border-box; padding: 10px 14px; - background: var(--folderHeaderBg); + background: var(--MI_THEME-folderHeaderBg); border-radius: 6px; 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); } } @@ -70,7 +70,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 f0e943960d..0d68d02e35 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: 100%; 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 1beb8874e0..0d4ae8cacb 100644 --- a/packages/frontend/src/components/global/MkMfm.ts +++ b/packages/frontend/src/components/global/MkMfm.ts @@ -31,8 +31,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(' '); @@ -270,7 +270,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' || @@ -303,7 +303,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', @@ -377,7 +377,7 @@ export default function (props: MfmProps, { emit }: { emit: SetupContext<MfmEven return [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 fcc46cc345..adf8638dae 100644 --- a/packages/frontend/src/components/global/MkPageHeader.tabs.vue +++ b/packages/frontend/src/components/global/MkPageHeader.tabs.vue @@ -247,7 +247,7 @@ onUnmounted(() => { position: absolute; bottom: 0; height: 3px; - background: var(--accent); + background: var(--MI_THEME-accent); border-radius: 999px; transition: none; pointer-events: none; diff --git a/packages/frontend/src/components/global/MkPageHeader.vue b/packages/frontend/src/components/global/MkPageHeader.vue index f1a451808f..aa4be69b2c 100644 --- a/packages/frontend/src/components/global/MkPageHeader.vue +++ b/packages/frontend/src/components/global/MkPageHeader.vue @@ -99,7 +99,7 @@ function onTabClick(): 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(); @@ -130,9 +130,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%; } @@ -145,7 +145,7 @@ onUnmounted(() => { .upper { --height: 50px; display: flex; - gap: var(--margin); + gap: var(--MI-margin); height: var(--height); .tabs:first-child { @@ -230,7 +230,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/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 60e4218a48..4d41cf5bc0 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-misskey.vue b/packages/frontend/src/pages/about-misskey.vue index b481fd590c..68b98c2ab7 100644 --- a/packages/frontend/src/pages/about-misskey.vue +++ b/packages/frontend/src/pages/about-misskey.vue @@ -266,6 +266,9 @@ const patronsWithIcon = [{ }, { name: 'なっかあ', icon: 'https://assets.misskey-hub.net/patrons/c2f5f3e394e74a64912284a2f4ca710e.jpg', +}, { + name: '如月ユカ', + icon: 'https://assets.misskey-hub.net/patrons/f24a042076a041b6811a2f124eb620ca.jpg', }]; const patrons = [ @@ -441,7 +444,7 @@ definePageMetadata(() => ({ .znqjceqz { > .about { position: relative; - border-radius: var(--radius); + border-radius: var(--MI-radius); > .treasure { position: absolute; @@ -529,17 +532,17 @@ definePageMetadata(() => ({ display: flex; align-items: center; padding: 12px; - background: var(--buttonBg); + background: var(--MI_THEME-buttonBg); border-radius: 6px; &:hover { text-decoration: none; - background: var(--buttonHoverBg); + background: var(--MI_THEME-buttonHoverBg); } &.active { - color: var(--accent); - background: var(--buttonHoverBg); + color: var(--MI_THEME-accent); + background: var(--MI_THEME-buttonHoverBg); } } @@ -562,7 +565,7 @@ definePageMetadata(() => ({ display: flex; align-items: center; padding: 12px; - background: var(--buttonBg); + background: var(--MI_THEME-buttonBg); border-radius: 6px; } diff --git a/packages/frontend/src/pages/about.federation.vue b/packages/frontend/src/pages/about.federation.vue index b3776c67e6..0a7cb8a50b 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 b645506eff..e5e57c05c4 100644 --- a/packages/frontend/src/pages/about.overview.vue +++ b/packages/frontend/src/pages/about.overview.vue @@ -147,7 +147,7 @@ const initStats = () => misskeyApi('stats', {}); text-align: center; border-radius: 10px; overflow: clip; - background-color: var(--panel); + background-color: var(--MI_THEME-panel); background-size: cover; background-position: center center; } @@ -183,14 +183,14 @@ const initStats = () => misskeyApi('stats', {}); 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 033634396e..948e7a3cce 100644 --- a/packages/frontend/src/pages/admin-user.vue +++ b/packages/frontend/src/pages/admin-user.vue @@ -153,15 +153,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.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> @@ -254,11 +260,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([]); @@ -582,18 +592,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); } } } @@ -640,7 +650,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 d22e078c2a..9b1bf51f58 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: 999px; 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/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 b34592cd6a..d07add4408 100644 --- a/packages/frontend/src/pages/admin/bot-protection.vue +++ b/packages/frontend/src/pages/admin/bot-protection.vue @@ -11,6 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template v-else-if="botProtectionForm.savedState.provider === 'mcaptcha'" #suffix>mCaptcha</template> <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 === '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"/> @@ -23,6 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only <option value="mcaptcha">mCaptcha</option> <option value="recaptcha">reCAPTCHA</option> <option value="turnstile">Turnstile</option> + <option value="testcaptcha">testCaptcha</option> </MkRadios> <template v-if="botProtectionForm.state.provider === 'hcaptcha'"> @@ -85,6 +87,13 @@ SPDX-License-Identifier: AGPL-3.0-only <MkCaptcha provider="turnstile" :sitekey="botProtectionForm.state.turnstileSiteKey || '1x00000000000000000000AA'"/> </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> @@ -101,6 +110,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')); @@ -115,7 +125,9 @@ const botProtectionForm = useForm({ ? 'turnstile' : meta.enableMcaptcha ? 'mcaptcha' - : null, + : meta.enableTestcaptcha + ? 'testcaptcha' + : null, hcaptchaSiteKey: meta.hcaptchaSiteKey, hcaptchaSecretKey: meta.hcaptchaSecretKey, mcaptchaSiteKey: meta.mcaptchaSiteKey, @@ -140,6 +152,7 @@ const botProtectionForm = useForm({ enableTurnstile: state.provider === 'turnstile', turnstileSiteKey: state.turnstileSiteKey, turnstileSecretKey: state.turnstileSecretKey, + 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 947dde767e..95f82c1f24 100644 --- a/packages/frontend/src/pages/admin/branding.vue +++ b/packages/frontend/src/pages/admin/branding.vue @@ -183,7 +183,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 4a858887f3..5b60e67dac 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 debf684c9b..e7b9fd8621 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 61745e0ff3..8a206a2f79 100644 --- a/packages/frontend/src/pages/admin/index.vue +++ b/packages/frontend/src/pages/admin/index.vue @@ -331,7 +331,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 54eb95cd51..5d8a581b2e 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"> @@ -57,6 +58,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> @@ -130,6 +143,7 @@ const enableRegistration = ref<boolean>(false); const emailRequiredForSignup = ref<boolean>(false); const sensitiveWords = ref<string>(''); const prohibitedWords = ref<string>(''); +const prohibitedWordsForNameOfUser = ref<string>(''); const hiddenTags = ref<string>(''); const preservedUsernames = ref<string>(''); const blockedHosts = ref<string>(''); @@ -142,10 +156,11 @@ async function init() { emailRequiredForSignup.value = meta.emailRequiredForSignup; 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'); blockedHosts.value = meta.blockedHosts.join('\n'); - silencedHosts.value = meta.silencedHosts.join('\n'); + silencedHosts.value = meta.silencedHosts?.join('\n') ?? ''; mediaSilencedHosts.value = meta.mediaSilencedHosts.join('\n'); } @@ -189,6 +204,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 6cf95e936e..1e144394fb 100644 --- a/packages/frontend/src/pages/admin/modlog.ModLog.vue +++ b/packages/frontend/src/pages/admin/modlog.ModLog.vue @@ -89,7 +89,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> @@ -205,14 +205,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 38610e7e92..c9eaf07531 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,8 +19,8 @@ SPDX-License-Identifier: AGPL-3.0-only </MkInput> </div> - <MkPagination v-slot="{items}" ref="logs" :pagination="pagination" style="margin-top: var(--margin);"> - <MkDateSeparatedList v-slot="{ item }" :items="items" :noGap="false" style="--margin: 8px;"> + <MkPagination v-slot="{items}" ref="logs" :pagination="pagination" 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> 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 022b392d2d..0896859f3c 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: 10px; } 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 0f4707f08d..222e9f4673 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: 10px; } 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 8b3c906d8a..1c237a69b4 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 ff9b8d6299..552958af6f 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: 6px; &: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 5a7cdee576..ea7603a45a 100644 --- a/packages/frontend/src/pages/admin/settings.vue +++ b/packages/frontend/src/pages/admin/settings.vue @@ -400,6 +400,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 124790338c..45f0fff107 100644 --- a/packages/frontend/src/pages/admin/system-webhook.item.vue +++ b/packages/frontend/src/pages/admin/system-webhook.item.vue @@ -13,9 +13,9 @@ SPDX-License-Identifier: AGPL-3.0-only <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"/> @@ -75,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 e50b208775..688a542988 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 22c5231dd9..a01bafd996 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: 32px; } .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 8b014c7a4e..b61054118d 100644 --- a/packages/frontend/src/pages/channel.vue +++ b/packages/frontend/src/pages/channel.vue @@ -269,14 +269,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 { @@ -310,7 +310,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 { @@ -335,7 +335,7 @@ definePageMetadata(() => ({ bottom: 16px; left: 16px; background: rgba(0, 0, 0, 0.7); - color: var(--warn); + color: var(--MI_THEME-warn); border-radius: 6px; font-weight: bold; font-size: 1em; diff --git a/packages/frontend/src/pages/clip.vue b/packages/frontend/src/pages/clip.vue index 7e5f0423f6..7b1737fece 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 4747aa5205..1e416e22d3 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -317,28 +317,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 { @@ -369,14 +369,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; @@ -385,7 +385,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 12ebbbe3ff..dfcc82c77b 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 853c1d6b0b..969aa6bbf7 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 97429c29a4..fcd22155b7 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: 8px; &: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 e9608ae94e..c9acfec04f 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 c3d4cae4aa..6716566101 100644 --- a/packages/frontend/src/pages/favorites.vue +++ b/packages/frontend/src/pages/favorites.vue @@ -46,7 +46,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.vue b/packages/frontend/src/pages/flash/flash.vue index cf10bee0f5..3b982a405e 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 e0e187f2ce..f396fd2c0c 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 8c4dfc3b83..feb4c60611 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 83f16fce68..6d68ed83b4 100644 --- a/packages/frontend/src/pages/install-extensions.vue +++ b/packages/frontend/src/pages/install-extensions.vue @@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template #key>{{ i18n.ts._externalResourceInstaller._vendorInfo.hashVerify }}</template> <template #value> <!-- この画面が出ている時点でハッシュの検証には成功している --> - <i class="ti ti-check" style="color: var(--accent)"></i> + <i class="ti ti-check" style="color: var(--MI_THEME-accent)"></i> </template> </MkKeyValue> </div> @@ -250,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; } @@ -265,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 { diff --git a/packages/frontend/src/pages/list.vue b/packages/frontend/src/pages/list.vue index 954246ff93..48bc568ac4 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 21c96348f0..f387740728 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: 6px; &: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 82fde284c1..6cbcca73c2 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: 6px; 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 97f32d35cd..448244204d 100644 --- a/packages/frontend/src/pages/note.vue +++ b/packages/frontend/src/pages/note.vue @@ -170,11 +170,11 @@ definePageMetadata(() => ({ } .loadNext { - margin-bottom: var(--margin); + margin-bottom: var(--MI-margin); } .loadPrev { - margin-top: var(--margin); + margin-top: var(--MI-margin); } .loadButton { @@ -182,7 +182,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 f2081c452c..a96c2c2a77 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: 8px; &: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 7926dab88b..a1bec52f18 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 9cf7fbe8d8..105c947d25 100644 --- a/packages/frontend/src/pages/search.note.vue +++ b/packages/frontend/src/pages/search.note.vue @@ -211,12 +211,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/avatar-decoration.decoration.vue b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue index f37b53aebb..f72a0b9383 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.decoration.vue @@ -43,7 +43,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: 8px; text-align: center; font-size: 90%; @@ -52,8 +52,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 1938b8d48d..853e536ea3 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.dialog.vue @@ -150,8 +150,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 77229d3349..9fca306f9f 100644 --- a/packages/frontend/src/pages/settings/avatar-decoration.vue +++ b/packages/frontend/src/pages/settings/avatar-decoration.vue @@ -145,7 +145,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 8d2946db63..6a13984dd7 100644 --- a/packages/frontend/src/pages/settings/drive-cleaner.vue +++ b/packages/frontend/src/pages/settings/drive-cleaner.vue @@ -132,7 +132,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 999a73df4c..fd3581d114 100644 --- a/packages/frontend/src/pages/settings/emoji-picker.vue +++ b/packages/frontend/src/pages/settings/emoji-picker.vue @@ -248,9 +248,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 { @@ -272,6 +272,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/mute-block.vue b/packages/frontend/src/pages/settings/mute-block.vue index f4ee7dffbf..4d413d53ab 100644 --- a/packages/frontend/src/pages/settings/mute-block.vue +++ b/packages/frontend/src/pages/settings/mute-block.vue @@ -244,7 +244,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 410a3f53c7..4a52e59d02 100644 --- a/packages/frontend/src/pages/settings/other.vue +++ b/packages/frontend/src/pages/settings/other.vue @@ -54,6 +54,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> @@ -105,9 +108,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 80d04ec686..7388e014ed 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -445,7 +445,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 19c5d892de..561894d2b7 100644 --- a/packages/frontend/src/pages/settings/profile.vue +++ b/packages/frontend/src/pages/settings/profile.vue @@ -142,13 +142,17 @@ const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.d const reactionAcceptance = computed(defaultStore.makeGetterSetter('reactionAcceptance')); +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, followedMessage: $i.followedMessage, location: $i.location, birthday: $i.birthday, - lang: $i.lang, + lang: assertVaildLang($i.lang) ? $i.lang : null, isBot: $i.isBot ?? false, isCat: $i.isCat ?? false, }); @@ -202,6 +206,11 @@ function save() { lang: profile.lang || null, isBot: !!profile.isBot, isCat: !!profile.isCat, + }, undefined, { + '0b3f9f6a-2f4d-4b1f-9fb4-49d3a2fd7191': { + title: i18n.ts.yourNameContainsProhibitedWords, + text: i18n.ts.yourNameContainsProhibitedWordsDescription, + }, }); globalEvents.emit('requestClearPageCache'); claimAchievement('profileFilled'); @@ -282,7 +291,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; } 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 ce8ec68692..f1ec231588 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 8c2f7042cd..14fb96d4f1 100644 --- a/packages/frontend/src/pages/signup-complete.vue +++ b/packages/frontend/src/pages/signup-complete.vue @@ -71,7 +71,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; @@ -81,7 +81,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 1b3e1ecaee..b669e25179 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 a62fe5d581..b2e084f53f 100644 --- a/packages/frontend/src/pages/theme-editor.vue +++ b/packages/frontend/src/pages/theme-editor.vue @@ -268,7 +268,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 12e2db2293..4feba54104 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 @@ -345,30 +345,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: 32px; } .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 31a3f1b060..3efeb46c0a 100644 --- a/packages/frontend/src/pages/user-list-timeline.vue +++ b/packages/frontend/src/pages/user-list-timeline.vue @@ -79,26 +79,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: 32px; } .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 111df41127..00b5740639 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -23,7 +23,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"> @@ -42,7 +42,7 @@ 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> @@ -377,8 +377,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: 24px; @@ -432,8 +432,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: 24px; padding: 4px 8px; font-size: 80%; @@ -447,7 +447,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 { > * { @@ -474,7 +474,7 @@ onUnmounted(() => { > .fukidashi { display: block; - --fukidashi-bg: color-mix(in srgb, var(--accent), var(--panel) 85%); + --fukidashi-bg: color-mix(in srgb, var(--MI_THEME-accent), var(--MI_THEME-panel) 85%); --fukidashi-radius: 16px; font-size: 0.9em; @@ -493,7 +493,7 @@ onUnmounted(() => { gap: 8px; > .role { - border: solid 1px var(--color, var(--divider)); + border: solid 1px var(--color, var(--MI_THEME-divider)); border-radius: 999px; margin-right: 4px; padding: 3px 8px; @@ -507,15 +507,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: 8px; padding: 8px; line-height: 0; > .heading { text-align: left; - color: var(--fgTransparent); + color: var(--MI_THEME-fgTransparent); line-height: 1.5; font-size: 85%; } @@ -530,7 +530,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; @@ -550,7 +550,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; @@ -587,14 +587,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 { @@ -616,7 +616,7 @@ onUnmounted(() => { > .contents { > .content { - margin-bottom: var(--margin); + margin-bottom: var(--MI-margin); } } } @@ -633,7 +633,7 @@ onUnmounted(() => { > .sub { max-width: 350px; min-width: 350px; - margin-left: var(--margin); + margin-left: var(--MI-margin); } } } @@ -710,13 +710,13 @@ onUnmounted(() => { <style lang="scss" module> .tl { - background: var(--bg); - border-radius: var(--radius); + background: var(--MI_THEME-bg); + border-radius: var(--MI-radius); overflow: clip; } .verifiedLink { margin-left: 4px; - color: var(--success); + color: var(--MI_THEME-success); } </style> 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 5e9b95eb74..00de3e9132 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: 6px; 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 dd57048409..e6e66bd6af 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 d6ba397f1b..f0e4a852c9 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: 999px; 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: 999px; > :global(.icon) { diff --git a/packages/frontend/src/pages/welcome.setup.vue b/packages/frontend/src/pages/welcome.setup.vue index dd258aad98..33cc139a45 100644 --- a/packages/frontend/src/pages/welcome.setup.vue +++ b/packages/frontend/src/pages/welcome.setup.vue @@ -99,7 +99,7 @@ function submit() { .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; @@ -110,8 +110,8 @@ function submit() { 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 6a9ecd9a62..8fb84fd58f 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 732d483615..9be3a80a9e 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 4ffa0ab94d..c1846b0589 100644 --- a/packages/frontend/src/scripts/get-note-menu.ts +++ b/packages/frontend/src/scripts/get-note-menu.ts @@ -245,13 +245,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 b7e7a5a3f8..1a3909c132 100644 --- a/packages/frontend/src/scripts/theme.ts +++ b/packages/frontend/src/scripts/theme.ts @@ -94,7 +94,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()); } document.documentElement.style.setProperty('color-scheme', colorScheme); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index cb52938980..aab67e0b5c 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -468,6 +468,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 b835096b15..4204c5af59 100644 --- a/packages/frontend/src/style.scss +++ b/packages/frontend/src/style.scss @@ -7,32 +7,30 @@ */ :root { - --radius: 12px; - --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); } } ::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: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif; @@ -43,7 +41,7 @@ html { -webkit-text-size-adjust: 100%; &, * { - scrollbar-color: var(--scrollbarHandle) transparent; + scrollbar-color: var(--MI_THEME-scrollbarHandle) transparent; scrollbar-width: thin; &::-webkit-scrollbar { @@ -56,14 +54,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); } } } @@ -125,15 +123,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 { @@ -141,7 +139,7 @@ rt { } :focus-visible { - outline: var(--focus) solid 2px; + outline: var(--MI_THEME-focus) solid 2px; outline-offset: -2px; &:hover { @@ -174,9 +172,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: 1.5em; align-items: center; @@ -209,13 +207,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 { @@ -244,40 +242,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; @@ -289,13 +287,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 { @@ -313,7 +311,7 @@ rt { ._gaps { display: flex; flex-direction: column; - gap: var(--margin); + gap: var(--MI-margin); } ._buttons { @@ -335,24 +333,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 { @@ -365,8 +363,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: 4px; vertical-align: top; } @@ -375,8 +373,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; } @@ -422,7 +420,7 @@ rt { } ._link { - color: var(--link); + color: var(--MI_THEME-link); } ._caption { @@ -446,14 +444,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 { @@ -464,10 +462,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; @@ -482,7 +480,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 61881c02e1..d145b9b6c6 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -116,27 +116,27 @@ if ($i) { .notifications { position: fixed; z-index: 3900000; - padding: 0 var(--margin); + padding: 0 var(--MI-margin); pointer-events: none; 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; } @@ -234,8 +234,8 @@ if ($i) { 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 5115d21d56..44253e93bd 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 { @@ -128,8 +128,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 { @@ -137,7 +137,7 @@ function more() { display: block; width: 100%; height: 40px; - color: var(--fgOnAccent); + color: var(--MI_THEME-fgOnAccent); font-weight: bold; text-align: left; @@ -153,12 +153,12 @@ function more() { right: 0; bottom: 0; border-radius: 999px; - 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); } } } @@ -202,7 +202,7 @@ function more() { .divider { margin: 16px 16px; - border-top: solid 0.5px var(--divider); + border-top: solid 0.5px var(--MI_THEME-divider); } .item { @@ -216,15 +216,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 { @@ -240,7 +240,7 @@ function more() { right: 0; bottom: 0; border-radius: 999px; - background: var(--accentedBg); + background: var(--MI_THEME-accentedBg); } } } @@ -255,9 +255,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 05f5ff2565..8ae11efa2c 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; } } @@ -189,8 +189,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 { @@ -198,7 +198,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; @@ -214,21 +214,21 @@ function more(ev: MouseEvent) { right: 0; bottom: 0; border-radius: 999px; - 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); } } } @@ -258,7 +258,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); } } } @@ -284,7 +284,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 { @@ -298,28 +298,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: ""; @@ -333,7 +333,7 @@ function more(ev: MouseEvent) { right: 0; bottom: 0; border-radius: 999px; - background: var(--accentedBg); + background: var(--MI_THEME-accentedBg); } } } @@ -348,9 +348,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; @@ -380,8 +379,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 { @@ -393,7 +392,7 @@ function more(ev: MouseEvent) { outline: none; > .instanceIcon { - outline: 2px solid var(--focus); + outline: 2px solid var(--MI_THEME-focus); outline-offset: 2px; } } @@ -410,8 +409,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 { @@ -433,28 +432,28 @@ function more(ev: MouseEvent) { width: 52px; aspect-ratio: 1/1; border-radius: 100%; - 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 { @@ -472,7 +471,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); } } } @@ -494,7 +493,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 { @@ -508,14 +507,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: ""; @@ -529,7 +528,7 @@ function more(ev: MouseEvent) { right: 0; bottom: 0; border-radius: 999px; - background: var(--accentedBg); + background: var(--MI_THEME-accentedBg); } > .icon, @@ -553,9 +552,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 6db7f9cae7..c7d1387eae 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 87b4515d46..5acef0bef8 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> @@ -157,7 +157,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 { @@ -165,7 +165,7 @@ watch(defaultStore.reactiveState.menuDisplay, () => { top: 0; z-index: 1; padding: 16px 0; - background: var(--bg); + background: var(--MI_THEME-bg); > .button { min-width: 0; @@ -220,9 +220,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; @@ -233,11 +232,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 fa04409d2d..5ea9bf7068 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,17 @@ 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 { - //--panelBorder: none; + //--MI_THEME-panelBorder: none; width: 300px; - padding-bottom: calc(var(--margin) + env(safe-area-inset-bottom, 0px)); + padding-bottom: calc(var(--MI-margin) + env(safe-area-inset-bottom, 0px)); @media (max-width: $widgets-hide-threshold) { display: none; @@ -277,13 +277,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; } } @@ -292,7 +292,7 @@ onMounted(() => { margin: 0; > .sidebar { - border-right: solid 0.5px var(--divider); + border-right: solid 0.5px var(--MI_THEME-divider); } > .main { @@ -314,10 +314,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 750cdca90e..a1a76a7e7d 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 { @@ -442,29 +442,29 @@ body { max-width: 60px; margin: auto; border-radius: 100%; - background: var(--panel); - color: var(--fg); + background: var(--MI_THEME-panel); + color: var(--MI_THEME-fg); &:hover { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } &:active { - background: hsl(from var(--panel) h s calc(l - 2)); + 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))); + 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))); } &: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))); } } @@ -477,9 +477,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 b97d86f4a3..da0bf24a56 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: 999px; } @@ -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 a2a79c74a1..d739c2e1cd 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: contain; - 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: 100%; 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 { @@ -414,29 +414,29 @@ $widgets-hide-threshold: 1090px; max-width: 60px; margin: auto; border-radius: 100%; - background: var(--panel); - color: var(--fg); + background: var(--MI_THEME-panel); + color: var(--MI_THEME-fg); &:hover { - background: var(--panelHighlight); + background: var(--MI_THEME-panelHighlight); } &:active { - background: hsl(from var(--panel) h s calc(l - 2)); + 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))); + 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))); } &: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))); } } @@ -449,9 +449,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; @@ -474,7 +473,7 @@ $widgets-hide-threshold: 1090px; contain: strict; overflow: auto; overscroll-behavior: contain; - background: var(--navBg); + background: var(--MI_THEME-navBg); } .statusbars { @@ -484,6 +483,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 01d0737123..7d8677e3be 100644 --- a/packages/frontend/src/ui/visitor.vue +++ b/packages/frontend/src/ui/visitor.vue @@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div class="mk-app"> - <a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--panel); color:var(--fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a> + <a v-if="isRoot" href="https://github.com/misskey-dev/misskey" target="_blank" class="github-corner" aria-label="View source on GitHub"><svg width="80" height="80" viewBox="0 0 250 250" style="fill:var(--MI_THEME-panel); color:var(--MI_THEME-fg); position: fixed; z-index: 10; top: 0; border: 0; right: 0;" aria-hidden="true"><path d="M0,0 L115,115 L130,115 L142,142 L250,250 L250,0 Z"></path><path d="M128.3,109.0 C113.8,99.7 119.0,89.6 119.0,89.6 C122.0,82.7 120.5,78.6 120.5,78.6 C119.2,72.0 123.4,76.3 123.4,76.3 C127.3,80.9 125.5,87.3 125.5,87.3 C122.9,97.6 130.6,101.9 134.4,103.2" fill="currentColor" style="transform-origin: 130px 106px;" class="octo-arm"></path><path d="M115.0,115.0 C114.9,115.1 118.7,116.5 119.8,115.4 L133.7,101.6 C136.9,99.2 139.9,98.4 142.2,98.6 C133.8,88.0 127.5,74.4 143.8,58.0 C148.5,53.4 154.0,51.2 159.7,51.0 C160.3,49.4 163.2,43.6 171.4,40.1 C171.4,40.1 176.1,42.5 178.8,56.2 C183.1,58.6 187.2,61.8 190.9,65.4 C194.5,69.0 197.7,73.2 200.1,77.6 C213.8,80.2 216.3,84.9 216.3,84.9 C212.7,93.1 206.9,96.0 205.4,96.6 C205.1,102.4 203.0,107.8 198.3,112.5 C181.9,128.9 168.3,122.5 157.7,114.1 C157.9,116.9 156.7,120.9 152.7,124.9 L141.0,136.5 C139.8,137.7 141.6,141.9 141.8,141.8 Z" fill="currentColor" class="octo-body"></path></svg></a> <div v-if="!narrow && !isRoot" class="side"> <div class="banner" :style="{ backgroundImage: instance.backgroundImageUrl ? `url(${ instance.backgroundImageUrl })` : 'none' }"></div> @@ -191,7 +191,7 @@ defineExpose({ left: 0; width: 500px; height: 100vh; - background: var(--accent); + background: var(--MI_THEME-accent); > .banner { position: absolute; @@ -219,7 +219,7 @@ defineExpose({ min-width: 0; > .header { - background: var(--panel); + background: var(--MI_THEME-panel); > .wide { line-height: 50px; @@ -254,7 +254,7 @@ defineExpose({ left: 0; width: 240px; height: 100vh; - background: var(--panel); + background: var(--MI_THEME-panel); > .link { display: block; @@ -268,7 +268,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 { @@ -283,7 +283,7 @@ defineExpose({ border-radius: 999px; &._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 f22bf41fd7..1f73b5fcaf 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: 100%; - 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 a74483e85e..cf7e9c5a3e 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 412d527819..2443e40789 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: 8px; } diff --git a/packages/frontend/src/widgets/WidgetFederation.vue b/packages/frontend/src/widgets/WidgetFederation.vue index c10416e4fb..4a10a612e2 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 7ee83157c6..7b179ce703 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/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md index 1da8e4e613..72c236373d 100644 --- a/packages/misskey-js/etc/misskey-js.api.md +++ b/packages/misskey-js/etc/misskey-js.api.md @@ -3095,7 +3095,9 @@ type SigninWithPasskeyRequest = { // @public (undocumented) type SigninWithPasskeyResponse = { - signinResponse: SigninFlowResponse; + signinResponse: SigninFlowResponse & { + finished: true; + }; }; // @public (undocumented) diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json index a7e04d6dac..a0a46a1162 100644 --- a/packages/misskey-js/package.json +++ b/packages/misskey-js/package.json @@ -1,7 +1,7 @@ { "type": "module", "name": "misskey-js", - "version": "2024.10.0", + "version": "2024.10.1", "description": "Misskey SDK for JavaScript", "license": "MIT", "main": "./built/index.js", diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts index 76ef7ea1fb..698c08826a 100644 --- a/packages/misskey-js/src/autogen/types.ts +++ b/packages/misskey-js/src/autogen/types.ts @@ -4972,6 +4972,7 @@ export type components = { recaptchaSiteKey: string | null; enableTurnstile: boolean; turnstileSiteKey: string | null; + enableTestcaptcha: boolean; swPublickey: string | null; /** @default /assets/ai.png */ mascotImageUrl: string; @@ -5047,7 +5048,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; }; @@ -5102,6 +5103,7 @@ export type operations = { recaptchaSiteKey: string | null; enableTurnstile: boolean; turnstileSiteKey: string | null; + enableTestcaptcha: boolean; swPublickey: string | null; /** @default /assets/ai.png */ mascotImageUrl: string | null; @@ -5122,6 +5124,7 @@ export type operations = { blockedHosts: string[]; sensitiveWords: string[]; prohibitedWords: string[]; + prohibitedWordsForNameOfUser: string[]; bannedEmailDomains?: string[]; preservedUsernames: string[]; hcaptchaSecretKey: string | null; @@ -5162,6 +5165,7 @@ export type operations = { truemailAuthKey: string | null; enableChartsForRemoteUser: boolean; enableChartsForFederatedInstances: boolean; + enableStatsForFederatedInstances: boolean; enableServerMachineStats: boolean; enableIdenticonGeneration: boolean; manifestJsonOverride: string; @@ -9459,6 +9463,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; @@ -9491,6 +9496,7 @@ export type operations = { enableTurnstile?: boolean; turnstileSiteKey?: string | null; turnstileSecretKey?: string | null; + enableTestcaptcha?: boolean; /** @enum {string} */ sensitiveMediaDetection?: 'none' | 'all' | 'local' | 'remote'; /** @enum {string} */ @@ -9542,6 +9548,7 @@ export type operations = { truemailAuthKey?: string | null; enableChartsForRemoteUser?: boolean; enableChartsForFederatedInstances?: boolean; + enableStatsForFederatedInstances?: boolean; enableServerMachineStats?: boolean; enableIdenticonGeneration?: boolean; serverRules?: string[]; @@ -10242,7 +10249,7 @@ export type operations = { 'application/json': { isActive: boolean; name: string; - on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; + on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[]; url: string; secret: string; }; @@ -10352,7 +10359,7 @@ export type operations = { content: { 'application/json': { isActive?: boolean; - on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; + on?: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[]; }; }; }; @@ -10465,7 +10472,7 @@ export type operations = { id: string; isActive: boolean; name: string; - on: ('abuseReport' | 'abuseReportResolved' | 'userCreated')[]; + on: ('abuseReport' | 'abuseReportResolved' | 'userCreated' | 'inactiveModeratorsWarning' | 'inactiveModeratorsInvitationOnlyChanged')[]; url: string; secret: string; }; @@ -10524,7 +10531,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; diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts index 2ffee40fba..dd88791ed0 100644 --- a/packages/misskey-js/src/entities.ts +++ b/packages/misskey-js/src/entities.ts @@ -308,7 +308,7 @@ export type SigninWithPasskeyInitResponse = { }; export type SigninWithPasskeyResponse = { - signinResponse: SigninFlowResponse; + 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', + }, + }, ]; |