Merge branch 'misskey-dev:develop' into dev
This commit is contained in:
commit
7b88e742b0
|
@ -75,7 +75,7 @@ jobs:
|
|||
- run: corepack enable
|
||||
- run: pnpm i --frozen-lockfile
|
||||
- name: Restore eslint cache
|
||||
uses: actions/cache@v4.0.2
|
||||
uses: actions/cache@v4.1.0
|
||||
with:
|
||||
path: ${{ env.eslint-cache-path }}
|
||||
key: eslint-${{ env.eslint-cache-version }}-${{ matrix.workspace }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ github.ref_name }}-${{ github.sha }}
|
||||
|
|
20
CHANGELOG.md
20
CHANGELOG.md
|
@ -1,7 +1,25 @@
|
|||
## 2024.10.1
|
||||
### Note
|
||||
- 悪質なユーザからサーバを守る措置の一環として、モデレータ権限を持つユーザの最終アクティブ日時を確認し、
|
||||
7日間活動していない場合は自動的に招待制へと移行(コントロールパネル -> モデレーション -> "誰でも新規登録できるようにする"をオフに変更)するようになりました。
|
||||
詳細な経緯は https://github.com/misskey-dev/misskey/issues/13437 をご確認ください。
|
||||
|
||||
### Client
|
||||
- Enhance: l10nの更新
|
||||
- Fix: メールアドレス不要でCaptchaが有効な場合にアカウント登録完了後自動でのログインに失敗する問題を修正
|
||||
|
||||
### Server
|
||||
- Feat: モデレータ権限を持つユーザが全員7日間活動しなかった場合は自動的に招待制へと移行するように ( #13437 )
|
||||
- Fix: `admin/emoji/update`エンドポイントのidのみ指定した時不正なエラーが発生するバグを修正
|
||||
|
||||
### Server
|
||||
- Fix: キューのエラーログを簡略化するように
|
||||
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/649)
|
||||
|
||||
## 2024.10.0
|
||||
|
||||
### Note
|
||||
- サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)
|
||||
- セキュリティ向上のため、サーバー初期設定時に使用する初期パスワードを設定できるようになりました。今後Misskeyサーバーを新たに設置する際には、初回の起動前にコンフィグファイルの`setupPassword`をコメントアウトし、初期パスワードを設定することをおすすめします。(すでに初期設定を完了しているサーバーについては、この変更に伴い対応する必要はありません)
|
||||
- ホスティングサービスを運営している場合は、コンフィグファイルを構築する際に`setupPassword`をランダムな値に設定し、ユーザーに通知するようにシステムを更新することをおすすめします。
|
||||
- なお、初期パスワードが設定されていない場合でも初期設定を行うことが可能です(UI上で初期パスワードの入力欄を空欄にすると続行できます)。
|
||||
- ユーザーデータを読み込む際の型が一部変更されました。
|
||||
|
|
|
@ -578,18 +578,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);
|
||||
```
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -1252,7 +1252,6 @@ _theme:
|
|||
buttonBg: "خلفية الأزرار"
|
||||
buttonHoverBg: "خلفية الأزرار (عند التمرير فوقها)"
|
||||
inputBorder: "حواف حقل الإدخال"
|
||||
listItemHoverBg: "خلفية عناصر القائمة (عند التمرير فوقها)"
|
||||
driveFolderBg: "خلفية مجلد قرص التخزين"
|
||||
messageBg: "خلفية المحادثة"
|
||||
_sfx:
|
||||
|
|
|
@ -1017,7 +1017,6 @@ _theme:
|
|||
buttonBg: "বাটনের পটভূমি"
|
||||
buttonHoverBg: "বাটনের পটভূমি (হভার)"
|
||||
inputBorder: "ইনপুট ফিল্ডের বর্ডার"
|
||||
listItemHoverBg: "লিস্ট আইটেমের পটভূমি (হোভার)"
|
||||
driveFolderBg: "ড্রাইভ ফোল্ডারের পটভূমি"
|
||||
wallpaperOverlay: "ওয়ালপেপার ওভারলে"
|
||||
badge: "ব্যাজ"
|
||||
|
|
|
@ -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,14 @@ 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 "
|
||||
_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"
|
||||
|
@ -1974,7 +1983,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 +2528,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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -112,7 +112,7 @@ enterEmoji: "Enter an emoji"
|
|||
renote: "Renote"
|
||||
unrenote: "Remove renote"
|
||||
renoted: "Renoted."
|
||||
renotedToX: "Renote to {name}."
|
||||
renotedToX: "Renoted to {name}."
|
||||
cantRenote: "This post can't be renoted."
|
||||
cantReRenote: "A renote can't be renoted."
|
||||
quote: "Quote"
|
||||
|
@ -454,6 +454,7 @@ totpDescription: "Use an authenticator app to enter one-time passwords"
|
|||
moderator: "Moderator"
|
||||
moderation: "Moderation"
|
||||
moderationNote: "Moderation note"
|
||||
moderationNoteDescription: "You can fill in notes that will be shared only among moderators."
|
||||
addModerationNote: "Add moderation note"
|
||||
moderationLogs: "Moderation logs"
|
||||
nUsersMentioned: "Mentioned by {n} users"
|
||||
|
@ -921,6 +922,7 @@ followersVisibility: "Visibility of followers"
|
|||
continueThread: "View thread continuation"
|
||||
deleteAccountConfirm: "This will irreversibly delete your account. Proceed?"
|
||||
incorrectPassword: "Incorrect password."
|
||||
incorrectTotp: "The one-time password is incorrect or has expired."
|
||||
voteConfirm: "Confirm your vote for \"{choice}\"?"
|
||||
hide: "Hide"
|
||||
useDrawerReactionPickerForMobile: "Display reaction picker as drawer on mobile"
|
||||
|
@ -1284,6 +1286,14 @@ unknownWebAuthnKey: "Unknown Passkey"
|
|||
passkeyVerificationFailed: "Passkey verification has failed."
|
||||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "Passkey verification has succeeded but password-less login is disabled."
|
||||
messageToFollower: "Message to followers"
|
||||
target: "Target"
|
||||
_abuseUserReport:
|
||||
forward: "Forward"
|
||||
forwardDescription: "Forward the report to a remote server as an anonymous system account."
|
||||
resolve: "Resolve"
|
||||
accept: "Accept"
|
||||
reject: "Reject"
|
||||
resolveTutorial: "If the report is legitimate in content, select \"Accept\" to mark the case as resolved in the affirmative.\nIf the content of the report is not legitimate, select \"Reject\" to mark the case as resolved in the negative."
|
||||
_delivery:
|
||||
status: "Delivery status"
|
||||
stop: "Suspended"
|
||||
|
@ -1737,7 +1747,7 @@ _role:
|
|||
canManageAvatarDecorations: "Manage avatar decorations"
|
||||
driveCapacity: "Drive capacity"
|
||||
alwaysMarkNsfw: "Always mark files as NSFW"
|
||||
canUpdateBioMedia: "Allow to edit an icon or a banner image"
|
||||
canUpdateBioMedia: "Can edit an icon or a banner image"
|
||||
pinMax: "Maximum number of pinned notes"
|
||||
antennaMax: "Maximum number of antennas"
|
||||
wordMuteMax: "Maximum number of characters allowed in word mutes"
|
||||
|
@ -1974,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"
|
||||
|
@ -2473,22 +2482,22 @@ _webhookSettings:
|
|||
reaction: "When receiving a reaction"
|
||||
mention: "When being mentioned"
|
||||
_systemEvents:
|
||||
abuseReport: "When received a new abuse report"
|
||||
abuseReportResolved: "When resolved abuse report"
|
||||
abuseReport: "When received a new report"
|
||||
abuseReportResolved: "When resolved report"
|
||||
userCreated: "When user is created"
|
||||
deleteConfirm: "Are you sure you want to delete the Webhook?"
|
||||
testRemarks: "Click the button to the right of the switch to send a test Webhook with dummy data."
|
||||
_abuseReport:
|
||||
_notificationRecipient:
|
||||
createRecipient: "Add a recipient for abuse reports"
|
||||
modifyRecipient: "Edit a recipient for abuse reports"
|
||||
createRecipient: "Add a recipient for reports"
|
||||
modifyRecipient: "Edit a recipient for reports"
|
||||
recipientType: "Notification type"
|
||||
_recipientType:
|
||||
mail: "Email"
|
||||
webhook: "Webhook"
|
||||
_captions:
|
||||
mail: "Send the email to moderators' email addresses when you receive abuse."
|
||||
webhook: "Send a notification to SystemWebhook when you receive or resolve abuse."
|
||||
mail: "Send the email to moderators' email addresses when you receive reports."
|
||||
webhook: "Send a notification to System Webhook when you receive or resolve reports."
|
||||
keywords: "Keywords"
|
||||
notifiedUser: "Users to notify"
|
||||
notifiedWebhook: "Webhook to use"
|
||||
|
@ -2521,6 +2530,8 @@ _moderationLogTypes:
|
|||
markSensitiveDriveFile: "File marked as sensitive"
|
||||
unmarkSensitiveDriveFile: "File unmarked as sensitive"
|
||||
resolveAbuseReport: "Report resolved"
|
||||
forwardAbuseReport: "Report forwarded"
|
||||
updateAbuseReportNote: "Moderation note of a report updated"
|
||||
createInvitation: "Invite generated"
|
||||
createAd: "Ad created"
|
||||
deleteAd: "Ad deleted"
|
||||
|
@ -2528,18 +2539,18 @@ _moderationLogTypes:
|
|||
createAvatarDecoration: "Avatar decoration created"
|
||||
updateAvatarDecoration: "Avatar decoration updated"
|
||||
deleteAvatarDecoration: "Avatar decoration deleted"
|
||||
unsetUserAvatar: "Unset this user's avatar"
|
||||
unsetUserBanner: "Unset this user's banner"
|
||||
createSystemWebhook: "Create SystemWebhook"
|
||||
updateSystemWebhook: "Update SystemWebhook"
|
||||
deleteSystemWebhook: "Delete SystemWebhook"
|
||||
createAbuseReportNotificationRecipient: "Create a recipient for abuse reports"
|
||||
updateAbuseReportNotificationRecipient: "Update recipients for abuse reports"
|
||||
deleteAbuseReportNotificationRecipient: "Delete a recipient for abuse reports"
|
||||
deleteAccount: "Delete the account"
|
||||
deletePage: "Delete the page"
|
||||
deleteFlash: "Delete Play"
|
||||
deleteGalleryPost: "Delete the gallery post"
|
||||
unsetUserAvatar: "User avatar unset"
|
||||
unsetUserBanner: "User banner unset"
|
||||
createSystemWebhook: "System Webhook created"
|
||||
updateSystemWebhook: "System Webhook updated"
|
||||
deleteSystemWebhook: "System Webhook deleted"
|
||||
createAbuseReportNotificationRecipient: "Recipient for reports created"
|
||||
updateAbuseReportNotificationRecipient: "Recipient for reports updated"
|
||||
deleteAbuseReportNotificationRecipient: "Recipient for reports deleted"
|
||||
deleteAccount: "Account deleted"
|
||||
deletePage: "Page deleted"
|
||||
deleteFlash: "Play deleted"
|
||||
deleteGalleryPost: "Gallery post deleted"
|
||||
_fileViewer:
|
||||
title: "File details"
|
||||
type: "File type"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -5174,6 +5174,10 @@ export interface Locale extends ILocale {
|
|||
* 対象
|
||||
*/
|
||||
"target": string;
|
||||
/**
|
||||
* CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>
|
||||
*/
|
||||
"testCaptchaWarning": string;
|
||||
"_abuseUserReport": {
|
||||
/**
|
||||
* 転送
|
||||
|
@ -5704,6 +5708,10 @@ export interface Locale extends ILocale {
|
|||
* サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。
|
||||
*/
|
||||
"inquiryUrlDescription": string;
|
||||
/**
|
||||
* 一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。
|
||||
*/
|
||||
"thisSettingWillAutomaticallyOffWhenModeratorsInactive": string;
|
||||
};
|
||||
"_accountMigration": {
|
||||
/**
|
||||
|
@ -7725,10 +7733,6 @@ export interface Locale extends ILocale {
|
|||
* 入力ボックスの縁取り
|
||||
*/
|
||||
"inputBorder": string;
|
||||
/**
|
||||
* リスト項目の背景 (ホバー)
|
||||
*/
|
||||
"listItemHoverBg": string;
|
||||
/**
|
||||
* ドライブフォルダーの背景
|
||||
*/
|
||||
|
|
|
@ -1975,7 +1975,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"
|
||||
|
|
|
@ -1289,6 +1289,7 @@ passkeyVerificationFailed: "パスキーの検証に失敗しました。"
|
|||
passkeyVerificationSucceededButPasswordlessLoginDisabled: "パスキーの検証に成功しましたが、パスワードレスログインが無効になっています。"
|
||||
messageToFollower: "フォロワーへのメッセージ"
|
||||
target: "対象"
|
||||
testCaptchaWarning: "CAPTCHAのテストを目的とした機能です。<strong>本番環境で使用しないでください。</strong>"
|
||||
|
||||
_abuseUserReport:
|
||||
forward: "転送"
|
||||
|
@ -1442,6 +1443,7 @@ _serverSettings:
|
|||
reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
|
||||
inquiryUrl: "問い合わせ先URL"
|
||||
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
|
||||
thisSettingWillAutomaticallyOffWhenModeratorsInactive: "一定期間モデレーターのアクティビティが検出されなかった場合、スパム防止のためこの設定は自動でオフになります。"
|
||||
|
||||
_accountMigration:
|
||||
moveFrom: "別のアカウントからこのアカウントに移行"
|
||||
|
@ -2023,7 +2025,6 @@ _theme:
|
|||
buttonBg: "ボタンの背景"
|
||||
buttonHoverBg: "ボタンの背景 (ホバー)"
|
||||
inputBorder: "入力ボックスの縁取り"
|
||||
listItemHoverBg: "リスト項目の背景 (ホバー)"
|
||||
driveFolderBg: "ドライブフォルダーの背景"
|
||||
wallpaperOverlay: "壁紙のオーバーレイ"
|
||||
badge: "バッジ"
|
||||
|
|
|
@ -1943,7 +1943,6 @@ _theme:
|
|||
buttonBg: "ボタンの背景"
|
||||
buttonHoverBg: "ボタンの背景 (ホバー)"
|
||||
inputBorder: "入力ボックスの縁取り"
|
||||
listItemHoverBg: "リスト項目の背景 (ホバー)"
|
||||
driveFolderBg: "ドライブフォルダーの背景"
|
||||
wallpaperOverlay: "壁紙のオーバーレイ"
|
||||
badge: "バッジ"
|
||||
|
|
|
@ -1984,7 +1984,6 @@ _theme:
|
|||
buttonBg: "버튼 배경"
|
||||
buttonHoverBg: "버튼 배경 (호버)"
|
||||
inputBorder: "입력 필드 테두리"
|
||||
listItemHoverBg: "리스트 항목 배경 (호버)"
|
||||
driveFolderBg: "드라이브 폴더 배경"
|
||||
wallpaperOverlay: "배경화면 오버레이"
|
||||
badge: "배지"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
---
|
||||
_lang_: "日本語"
|
||||
_lang_: "Português"
|
||||
headlineMisskey: "Uma rede ligada por notas"
|
||||
introMisskey: "Bem-vindo! O Misskey é um serviço de microblog descentralizado de código aberto.\nCrie \"notas\" para compartilhar o que está acontecendo agora ou para se expressar com todos à sua volta 📡\nVocê também pode adicionar rapidamente reações às notas de outras pessoas usando a função \"Reações\" 👍\nVamos explorar um novo mundo 🚀"
|
||||
poweredByMisskeyDescription: "{name} é uma instância da plataforma de código aberto <b>Misskey</b>."
|
||||
|
@ -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…"
|
||||
|
@ -1058,7 +1058,7 @@ resetPasswordConfirm: "Deseja realmente mudar a sua senha?"
|
|||
sensitiveWords: "Palavras sensíveis"
|
||||
sensitiveWordsDescription: "A visibilidade de todas as notas contendo as palavras configuradas será colocadas como \"Início\" automaticamente. Você pode listar várias delas separando-as por linha."
|
||||
sensitiveWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)"
|
||||
prohibitedWords: "Palavras proibídas"
|
||||
prohibitedWords: "Palavras proibidas"
|
||||
prohibitedWordsDescription: "Habilita um erro ao tentar publicar uma nota contendo as palavras escolhidas. Várias palavras podem ser escolhidas, separando-as por linha."
|
||||
prohibitedWordsDescription2: "Utilizar espaços irá criar expressões aditivas (AND) e cercar palavras-chave com barras irá transformá-las em expressões regulares (RegEx)"
|
||||
hiddenTags: "Hashtags escondidas"
|
||||
|
@ -1416,7 +1416,7 @@ _achievements:
|
|||
_types:
|
||||
_notes1:
|
||||
title: "Configurando o meu misskey"
|
||||
description: "Post uma nota pela primeira vez"
|
||||
description: "Poste uma nota pela primeira vez"
|
||||
flavor: "Divirta-se com o Misskey!"
|
||||
_notes10:
|
||||
title: "Algumas notas"
|
||||
|
@ -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"
|
||||
|
|
|
@ -1694,7 +1694,6 @@ _theme:
|
|||
buttonBg: "Фон кнопки"
|
||||
buttonHoverBg: "Текст кнопки"
|
||||
inputBorder: "Рамка поля ввода"
|
||||
listItemHoverBg: "Фон пункта списка (под указателем)"
|
||||
driveFolderBg: "Фон папки «Диска»"
|
||||
wallpaperOverlay: "Слой обоев"
|
||||
badge: "Значок"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1943,7 +1943,6 @@ _theme:
|
|||
buttonBg: "ปุ่มพื้นหลัง"
|
||||
buttonHoverBg: "ปุ่มพื้นหลัง (โฮเวอร์)"
|
||||
inputBorder: "เส้นขอบของช่องป้อนข้อมูล"
|
||||
listItemHoverBg: "รายการไอเทมพื้นหลัง (โฮเวอร์)"
|
||||
driveFolderBg: "พื้นหลังโฟลเดอร์ไดรฟ์"
|
||||
wallpaperOverlay: "วอลล์เปเปอร์ซ้อนทับ"
|
||||
badge: "ตรา"
|
||||
|
|
|
@ -1302,7 +1302,6 @@ _theme:
|
|||
buttonBg: "Фон кнопки"
|
||||
buttonHoverBg: "Фон кнопки (при наведенні)"
|
||||
inputBorder: "Край поля вводу"
|
||||
listItemHoverBg: "Фон елементу в списку (при наведенні)"
|
||||
driveFolderBg: "Фон папки на диску"
|
||||
wallpaperOverlay: "Накладання шпалер"
|
||||
badge: "Значок"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -1199,10 +1199,10 @@ followingOrFollower: "关注中或关注者"
|
|||
fileAttachedOnly: "仅限媒体"
|
||||
showRepliesToOthersInTimeline: "在时间线中包含给别人的回复"
|
||||
hideRepliesToOthersInTimeline: "在时间线中隐藏给别人的回复"
|
||||
showRepliesToOthersInTimelineAll: "在时间线中包含现在关注的所有人的回复"
|
||||
hideRepliesToOthersInTimelineAll: "在时间线中隐藏现在关注的所有人的回复"
|
||||
confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中包含现在关注的所有人的回复吗?"
|
||||
confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏现在关注的所有人的回复吗?"
|
||||
showRepliesToOthersInTimelineAll: "在时间线中显示所有现在关注的人的回复"
|
||||
hideRepliesToOthersInTimelineAll: "在时间线中隐藏所有现在关注的人的回复"
|
||||
confirmShowRepliesAll: "此操作不可撤销。确认要在时间线中显示所有现在关注的人的回复吗?"
|
||||
confirmHideRepliesAll: "此操作不可撤销。确认要在时间线中隐藏所有现在关注的人的回复吗?"
|
||||
externalServices: "外部服务"
|
||||
sourceCode: "源代码"
|
||||
sourceCodeIsNotYetProvided: "还未提供源代码。要解决此问题请联系管理员。"
|
||||
|
@ -1290,6 +1290,10 @@ target: "对象"
|
|||
_abuseUserReport:
|
||||
forward: "转发"
|
||||
forwardDescription: "目标是匿名系统账户,将把举报转发给远程服务器。"
|
||||
resolve: "解决"
|
||||
accept: "确认"
|
||||
reject: "拒绝"
|
||||
resolveTutorial: "如果举报内容有理且已解决,选择「确认」将案件以肯定的态度标记为已解决。\n如果举报内容站不住脚,选择「拒绝」将案件以否定的态度标记为已解决。"
|
||||
_delivery:
|
||||
status: "投递状态"
|
||||
stop: "停止投递"
|
||||
|
@ -1626,7 +1630,7 @@ _achievements:
|
|||
_postedAt0min0sec:
|
||||
title: "报时"
|
||||
description: "在 0 点发布一篇帖子"
|
||||
flavor: "报时信号最后一响,零点整"
|
||||
flavor: "嘟 · 嘟 · 嘟 · 哔——"
|
||||
_selfQuote:
|
||||
title: "自我引用"
|
||||
description: "引用了自己的帖子"
|
||||
|
@ -1980,7 +1984,6 @@ _theme:
|
|||
buttonBg: "按钮背景"
|
||||
buttonHoverBg: "按钮背景(悬停)"
|
||||
inputBorder: "输入框边框"
|
||||
listItemHoverBg: "下拉列表项目背景(悬停)"
|
||||
driveFolderBg: "网盘的文件夹背景"
|
||||
wallpaperOverlay: "壁纸叠加层"
|
||||
badge: "徽章"
|
||||
|
|
|
@ -1975,7 +1975,6 @@ _theme:
|
|||
buttonBg: "按鈕背景"
|
||||
buttonHoverBg: "按鈕背景 (漂浮)"
|
||||
inputBorder: "輸入框邊框"
|
||||
listItemHoverBg: "列表物品背景 (漂浮)"
|
||||
driveFolderBg: "雲端硬碟文件夾背景"
|
||||
wallpaperOverlay: "壁紙覆蓋層"
|
||||
badge: "徽章"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "2024.10.0-beta.6",
|
||||
"version": "2024.10.1-beta.3",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -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"`);
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
@ -370,7 +373,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) {
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -93,6 +93,13 @@ export class QueueService {
|
|||
repeat: { pattern: '0 0 * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
|
||||
this.systemQueue.add('checkModeratorsActivity', {
|
||||
}, {
|
||||
// 毎時30分に起動
|
||||
repeat: { pattern: '30 * * * *' },
|
||||
removeOnComplete: true,
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -103,6 +103,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;
|
||||
|
@ -138,6 +139,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
|
||||
|
||||
|
@ -423,29 +425,42 @@ 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)) })
|
||||
: [];
|
||||
|
||||
const now = Date.now();
|
||||
const result = [
|
||||
// Setを経由して重複を除去(ユーザIDは重複する可能性があるので)
|
||||
...new Set(
|
||||
const now = Date.now();
|
||||
const resultSet = new Set(
|
||||
assigns
|
||||
.filter(it =>
|
||||
(excludeExpire)
|
||||
|
@ -453,19 +468,35 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
|
|||
: true,
|
||||
)
|
||||
.map(a => a.userId),
|
||||
),
|
||||
];
|
||||
);
|
||||
|
||||
return result.sort((x, y) => x.localeCompare(y));
|
||||
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 [...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({
|
||||
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),
|
||||
}) : [];
|
||||
return users;
|
||||
})
|
||||
: [];
|
||||
}
|
||||
|
||||
@bindThis
|
||||
|
|
|
@ -40,7 +40,7 @@ export class FlashEntityService {
|
|||
// { schema: 'UserDetailed' } すると無限ループするので注意
|
||||
const user = hint?.packedUser ?? await this.userEntityService.pack(flash.user ?? flash.userId, me);
|
||||
|
||||
let isLiked = false;
|
||||
let isLiked = undefined;
|
||||
if (meId) {
|
||||
isLiked = hint?.likedFlashIds
|
||||
? hint.likedFlashIds.includes(flash.id)
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -258,6 +258,11 @@ export class MiMeta {
|
|||
})
|
||||
public turnstileSecretKey: string | null;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public enableTestcaptcha: boolean;
|
||||
|
||||
// chaptcha系を追加した際にはnodeinfoのレスポンスに追加するのを忘れないようにすること
|
||||
|
||||
@Column('enum', {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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で来ることがある
|
||||
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,
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
stack: '?',
|
||||
message: '?',
|
||||
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
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
/*
|
||||
* SPDX-FileCopyrightText: syuilo and misskey-project
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import type Logger from '@/logger.js';
|
||||
import { bindThis } from '@/decorators.js';
|
||||
import { MetaService } from '@/core/MetaService.js';
|
||||
import { RoleService } from '@/core/RoleService.js';
|
||||
import { QueueLoggerService } from '../QueueLoggerService.js';
|
||||
|
||||
// モデレーターが不在と判断する日付の閾値
|
||||
const MODERATOR_INACTIVITY_LIMIT_DAYS = 7;
|
||||
const ONE_DAY_MILLI_SEC = 1000 * 60 * 60 * 24;
|
||||
|
||||
@Injectable()
|
||||
export class CheckModeratorsActivityProcessorService {
|
||||
private logger: Logger;
|
||||
|
||||
constructor(
|
||||
private metaService: MetaService,
|
||||
private roleService: RoleService,
|
||||
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 { isModeratorsInactive, inactivityLimitCountdown } = await this.evaluateModeratorsInactiveDays();
|
||||
if (isModeratorsInactive) {
|
||||
this.logger.warn(`The moderator has been inactive for ${MODERATOR_INACTIVITY_LIMIT_DAYS} days. We will move to invitation only.`);
|
||||
await this.changeToInvitationOnly();
|
||||
|
||||
// TODO: モデレータに通知メール+Misskey通知
|
||||
// TODO: SystemWebhook通知
|
||||
} else {
|
||||
if (inactivityLimitCountdown <= 2) {
|
||||
this.logger.warn(`A moderator has been inactive for a period of time. If you are inactive for an additional ${inactivityLimitCountdown} days, it will switch to invitation only.`);
|
||||
|
||||
// TODO: 警告メール
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* モデレーターが不在であるかどうかを確認する。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() {
|
||||
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 inactivityLimitCountdown = Math.floor((newestLastActiveDate.getTime() - inactivePeriod.getTime()) / ONE_DAY_MILLI_SEC);
|
||||
|
||||
return {
|
||||
isModeratorsInactive: inactiveModerators.length === moderators.length,
|
||||
inactiveModerators,
|
||||
inactivityLimitCountdown,
|
||||
};
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async changeToInvitationOnly() {
|
||||
await this.metaService.update({ disableRegistration: true });
|
||||
}
|
||||
|
||||
@bindThis
|
||||
private async fetchModerators() {
|
||||
// TODO: モデレーター以外にも特別な権限を持つユーザーがいる場合は考慮する
|
||||
return this.roleService.getModerators({
|
||||
includeAdmins: true,
|
||||
includeRoot: true,
|
||||
excludeExpire: true,
|
||||
});
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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'];
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
@ -559,6 +563,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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -78,6 +78,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' },
|
||||
|
@ -370,6 +371,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;
|
||||
}
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -0,0 +1,235 @@
|
|||
/*
|
||||
* 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 { MiUser, 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';
|
||||
|
||||
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>;
|
||||
|
||||
// --------------------------------------------------------------------------------------
|
||||
|
||||
async function createUser(data: Partial<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,
|
||||
});
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
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: 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>;
|
||||
|
||||
app.enableShutdownHooks();
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
clock = lolex.install({
|
||||
now: new Date(baseDate),
|
||||
shouldClearNativeTimers: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
clock.uninstall();
|
||||
await usersRepository.delete({});
|
||||
await userProfilesRepository.delete({});
|
||||
roleService.getModerators.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('[countdown] 猶予まで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.inactivityLimitCountdown).toBe(1);
|
||||
});
|
||||
|
||||
test('[countdown] 猶予まで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.inactivityLimitCountdown).toBe(1);
|
||||
});
|
||||
|
||||
test('[countdown] 猶予まで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.inactivityLimitCountdown).toBe(0);
|
||||
});
|
||||
|
||||
test('[countdown] 期限ちょうどの場合、猶予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.inactivityLimitCountdown).toBe(0);
|
||||
});
|
||||
|
||||
test('[countdown] 期限より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.inactivityLimitCountdown).toBe(-1);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -56,7 +56,7 @@ const props = withDefaults(defineProps<{
|
|||
--size: 38px;
|
||||
|
||||
&.colored {
|
||||
color: var(--accent);
|
||||
color: var(--MI_THEME-accent);
|
||||
}
|
||||
|
||||
&.inline {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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}`)];
|
||||
}
|
||||
|
||||
|
|
|
@ -189,8 +189,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 +212,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 +259,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 +356,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 +377,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 +403,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 +424,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 +449,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 +473,7 @@ const isDeleted = ref(false);
|
|||
}
|
||||
|
||||
&:hover {
|
||||
color: var(--fgHighlighted);
|
||||
color: var(--MI_THEME-fgHighlighted);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -550,7 +550,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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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参照
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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)',
|
||||
|
|
|
@ -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)',
|
||||
|
|
|
@ -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)',
|
||||
|
|
|
@ -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',
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 2.6 KiB |
|
@ -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') {
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
|
|
|
@ -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 });
|
||||
|
||||
|
|
|
@ -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,11 +17,11 @@ 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">
|
||||
<template v-if="report.targetUser.host != null">
|
||||
<MkButton :disabled="report.forwarded" primary @click="forward"><i class="ti ti-corner-up-right"></i> {{ i18n.ts._abuseUserReport.forward }}</MkButton>
|
||||
<div v-tooltip:dialog="i18n.ts._abuseUserReport.forwardDescription" class="_button _help"><i class="ti ti-help-circle"></i></div>
|
||||
</template>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue