Compare commits
	
		
			34 Commits
		
	
	
		
			2025.3.2-b
			...
			feat/bundl
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					f066247988 | ||
| 
						 | 
					6015254e59 | ||
| 
						 | 
					c02f0b3b33 | ||
| 
						 | 
					abddd40c09 | ||
| 
						 | 
					a865a949b5 | ||
| 
						 | 
					0007723405 | ||
| 
						 | 
					71188b3463 | ||
| 
						 | 
					7f534a41a6 | ||
| 
						 | 
					f25963e2c2 | ||
| 
						 | 
					dfab6b1b8d | ||
| 
						 | 
					fac59d75e2 | ||
| 
						 | 
					fd3e28812e | ||
| 
						 | 
					6a90b7e04b | ||
| 
						 | 
					8d8414687a | ||
| 
						 | 
					0c682dd027 | ||
| 
						 | 
					3399c786a8 | ||
| 
						 | 
					64cf101fe7 | ||
| 
						 | 
					8b6d90b7a4 | ||
| 
						 | 
					070749bdc8 | ||
| 
						 | 
					161706c5e2 | ||
| 
						 | 
					596e517f77 | ||
| 
						 | 
					91670caca0 | ||
| 
						 | 
					fccaadacf0 | ||
| 
						 | 
					282caa0b7e | ||
| 
						 | 
					9529867630 | ||
| 
						 | 
					d06fadd095 | ||
| 
						 | 
					9dd13f364b | ||
| 
						 | 
					b067d4dcd6 | ||
| 
						 | 
					acac759d87 | ||
| 
						 | 
					b37622fa64 | ||
| 
						 | 
					c17a104de6 | ||
| 
						 | 
					4ab9f66356 | ||
| 
						 | 
					aed95a765d | ||
| 
						 | 
					71841e365e | 
							
								
								
									
										4
									
								
								.github/workflows/storybook.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/storybook.yml
									
									
									
									
										vendored
									
									
								
							@@ -50,8 +50,8 @@ jobs:
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - name: Check pnpm-lock.yaml
 | 
			
		||||
      run: git diff --exit-code pnpm-lock.yaml
 | 
			
		||||
    - name: Build misskey-js
 | 
			
		||||
      run: pnpm --filter misskey-js build
 | 
			
		||||
    - name: Build dependent packages
 | 
			
		||||
      run: pnpm -F misskey-js -F misskey-bubble-game -F misskey-reversi build
 | 
			
		||||
    - name: Build storybook
 | 
			
		||||
      run: pnpm --filter frontend build-storybook
 | 
			
		||||
    - name: Publish to Chromatic
 | 
			
		||||
 
 | 
			
		||||
@@ -18,12 +18,14 @@
 | 
			
		||||
- Enhance: 投稿フォームの設定メニューを改良
 | 
			
		||||
  - 投稿フォームをリセットできるように
 | 
			
		||||
  - 文字数カウントを復活
 | 
			
		||||
- Enhance: 2段階認証時のリカバリーコードのファイル名にサーバーURLを含めるように
 | 
			
		||||
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
 | 
			
		||||
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
 | 
			
		||||
- Fix: 連合無しモードでも外部から照会可能だった問題を修正
 | 
			
		||||
- Fix: テスト用WebHookのペイロードの`emojis`パラメータが実際のものと異なる問題を修正
 | 
			
		||||
 | 
			
		||||
## 2025.3.1
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1013,6 +1013,7 @@ flip: "اقلب"
 | 
			
		||||
lastNDays: "آخر {n} أيام"
 | 
			
		||||
surrender: "ألغِ"
 | 
			
		||||
postForm: "أنشئ ملاحظة"
 | 
			
		||||
information: "عن"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "مُعلّق"
 | 
			
		||||
_initialAccountSetting:
 | 
			
		||||
 
 | 
			
		||||
@@ -853,6 +853,7 @@ renotes: "রিনোট"
 | 
			
		||||
sourceCode: "সোর্স কোড"
 | 
			
		||||
flip: "উল্টান"
 | 
			
		||||
postForm: "নোট লিখুন"
 | 
			
		||||
information: "আপনার সম্পর্কে"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "স্থগিত করা হয়েছে"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -1334,6 +1334,7 @@ paste: "Pegar"
 | 
			
		||||
emojiPalette: "Calaix d'emojis"
 | 
			
		||||
postForm: "Formulari de publicació"
 | 
			
		||||
textCount: "Nombre de caràcters "
 | 
			
		||||
information: "Informació"
 | 
			
		||||
_emojiPalette:
 | 
			
		||||
  palettes: "Calaixos d'emojis"
 | 
			
		||||
  enableSyncBetweenDevicesForPalettes: "Activa la sincronització dels calaixos d'emojis entre dispositius"
 | 
			
		||||
@@ -2673,10 +2674,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "Assegura't que qui distribueix aquest recurs és fiable abans d'instal·lar-ho."
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "Vols instal·lar aquest afegit?"
 | 
			
		||||
    metaTitle: "Informació de l'afegit "
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "Vols instal·lar aquest tema?"
 | 
			
		||||
    metaTitle: "Informació del tema"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "Paleta de colors base"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -169,6 +169,9 @@ addAccount: "Přidat účet"
 | 
			
		||||
reloadAccountsList: "Obnovit list účtů"
 | 
			
		||||
loginFailed: "Přihlášení se nezdařilo."
 | 
			
		||||
showOnRemote: "Více na původním profilu"
 | 
			
		||||
continueOnRemote: "Pokračujte na původní profil"
 | 
			
		||||
chooseServerOnMisskeyHub: "Vyberete si server z Misskey Hubu"
 | 
			
		||||
inputHostName: "Zadejte doménu"
 | 
			
		||||
general: "Obecně"
 | 
			
		||||
wallpaper: "Obrázek na pozadí"
 | 
			
		||||
setWallpaper: "Nastavení obrázku na pozadí"
 | 
			
		||||
@@ -193,6 +196,7 @@ perHour: "za hodinu"
 | 
			
		||||
perDay: "za den"
 | 
			
		||||
stopActivityDelivery: "Přestat zasílat aktivitu"
 | 
			
		||||
blockThisInstance: "Blokovat tuto instanci"
 | 
			
		||||
silenceThisInstance: "Utišit tuto instanci"
 | 
			
		||||
operations: "Operace"
 | 
			
		||||
software: "Software"
 | 
			
		||||
version: "Verze"
 | 
			
		||||
@@ -1099,6 +1103,7 @@ flip: "Otočit"
 | 
			
		||||
lastNDays: "Posledních {n} dnů"
 | 
			
		||||
surrender: "Zrušit"
 | 
			
		||||
postForm: "Formulář pro odeslání"
 | 
			
		||||
information: "Informace"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "Suspendováno"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -1309,6 +1309,7 @@ availableRoles: "Verfügbare Rollen"
 | 
			
		||||
federationSpecified: "Dieser Server arbeitet mit Whitelist-Föderation. Er kann nicht mit anderen als den vom Administrator angegebenen Servern interagieren."
 | 
			
		||||
federationDisabled: "Föderation ist auf diesem Server deaktiviert. Es ist nicht möglich, mit Benutzern auf anderen Servern zu interagieren."
 | 
			
		||||
postForm: "Notizfenster"
 | 
			
		||||
information: "Über"
 | 
			
		||||
_settings:
 | 
			
		||||
  webhook: "Webhook"
 | 
			
		||||
_accountSettings:
 | 
			
		||||
@@ -2524,10 +2525,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "Überprüfe vor Installation die Vertrauenswürdigkeit des Vertreibers."
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "Möchtest du dieses Plugin installieren?"
 | 
			
		||||
    metaTitle: "Plugininformation"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "Möchten du dieses Farbschema installieren?"
 | 
			
		||||
    metaTitle: "Farbschemainfo"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "Farbschemavorlage"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -289,6 +289,7 @@ icon: "Εικονίδιο"
 | 
			
		||||
replies: "Απάντηση"
 | 
			
		||||
renotes: "Κοινοποίηση σημειώματος"
 | 
			
		||||
postForm: "Φόρμα δημοσίευσης"
 | 
			
		||||
information: "Πληροφορίες"
 | 
			
		||||
_email:
 | 
			
		||||
  _follow:
 | 
			
		||||
    title: "Έχετε ένα νέο ακόλουθο"
 | 
			
		||||
 
 | 
			
		||||
@@ -1317,7 +1317,7 @@ unmarkAsSensitiveConfirm: "Do you want to remove the sensitive designation for t
 | 
			
		||||
preferences: "Preferences"
 | 
			
		||||
accessibility: "Accessibility"
 | 
			
		||||
preferencesProfile: "Preferences profile"
 | 
			
		||||
copyPreferenceId: "Copy the proference ID"
 | 
			
		||||
copyPreferenceId: "Copy the preference ID"
 | 
			
		||||
resetToDefaultValue: "Revert to default"
 | 
			
		||||
overrideByAccount: "Override by the account"
 | 
			
		||||
untitled: "Untitled"
 | 
			
		||||
@@ -1334,6 +1334,7 @@ paste: "Paste"
 | 
			
		||||
emojiPalette: "Emoji palette"
 | 
			
		||||
postForm: "Posting form"
 | 
			
		||||
textCount: "Character count"
 | 
			
		||||
information: "About"
 | 
			
		||||
_emojiPalette:
 | 
			
		||||
  palettes: "Palette"
 | 
			
		||||
  enableSyncBetweenDevicesForPalettes: "Enable palette sync between devices"
 | 
			
		||||
@@ -2673,10 +2674,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "Make sure the distributor of this resource is trustworthy before installation."
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "Do you want to install this plugin?"
 | 
			
		||||
    metaTitle: "Plugin information"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "Do you want to install this theme?"
 | 
			
		||||
    metaTitle: "Theme information"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "Base color scheme"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
@@ -2877,8 +2876,8 @@ _selfXssPrevention:
 | 
			
		||||
  description2: "If you do not understand exactly what you are trying to paste, %cstop working right now and close this window."
 | 
			
		||||
  description3: "For more information, please refer to this. {link}"
 | 
			
		||||
_followRequest:
 | 
			
		||||
  recieved: "Received application"
 | 
			
		||||
  sent: "Sent application"
 | 
			
		||||
  recieved: "Received request"
 | 
			
		||||
  sent: "Sent request"
 | 
			
		||||
_remoteLookupErrors:
 | 
			
		||||
  _federationNotAllowed:
 | 
			
		||||
    title: "Unable to communicate with this server"
 | 
			
		||||
 
 | 
			
		||||
@@ -1300,6 +1300,7 @@ target: "Para"
 | 
			
		||||
federationSpecified: "Este servidor opera en una federación de listas blancas. No puede interactuar con otros servidores que no sean los especificados por el administrador."
 | 
			
		||||
federationDisabled: "La federación está desactivada en este servidor. No puede interactuar con usuarios de otros servidores"
 | 
			
		||||
postForm: "Formulario"
 | 
			
		||||
information: "Información"
 | 
			
		||||
_settings:
 | 
			
		||||
  webhook: "Webhook"
 | 
			
		||||
_accountSettings:
 | 
			
		||||
@@ -2521,10 +2522,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "Asegúrate de que el distribuidor de este recurso es de confianza antes de proceder a la instalación."
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "¿Quieres instalar este plugin?"
 | 
			
		||||
    metaTitle: "Información del plugin"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "¿Quieres instalar este tema?"
 | 
			
		||||
    metaTitle: "Información del tema"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "Esquema de color base"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -1278,6 +1278,7 @@ lockdown: "Verrouiller"
 | 
			
		||||
pleaseSelectAccount: "Sélectionner un compte"
 | 
			
		||||
availableRoles: "Rôles disponibles"
 | 
			
		||||
postForm: "Formulaire de publication"
 | 
			
		||||
information: "Informations"
 | 
			
		||||
_abuseUserReport:
 | 
			
		||||
  forward: "Transférer"
 | 
			
		||||
  forwardDescription: "Transférer le signalement vers une instance distante en tant qu'anonyme."
 | 
			
		||||
@@ -2295,10 +2296,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "Veuillez confirmer que le distributeur est fiable avant l'installation."
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "Voulez-vous installer cette extension ?"
 | 
			
		||||
    metaTitle: "Informations sur l'extension"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "Voulez-vous installer ce thème ?"
 | 
			
		||||
    metaTitle: "Informations sur le thème"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "Palette de couleurs de base"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -1262,6 +1262,7 @@ modified: "Diubah"
 | 
			
		||||
thereAreNChanges: "Ada {n} perubahan"
 | 
			
		||||
prohibitedWordsForNameOfUser: "Kata yang dilarang untuk nama pengguna"
 | 
			
		||||
postForm: "Buat catatan"
 | 
			
		||||
information: "Informasi"
 | 
			
		||||
_settings:
 | 
			
		||||
  webhook: "Webhook"
 | 
			
		||||
_abuseUserReport:
 | 
			
		||||
@@ -2492,10 +2493,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "Pastikan sumber dari sumber daya ini terpercaya sebelum melakukan pemasangan."
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "Apakah kamu ingin memasang plugin ini?"
 | 
			
		||||
    metaTitle: "Informasi plugin"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "Apakah kamu ingin memasang tema ini?"
 | 
			
		||||
    metaTitle: "Informasi tema"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "Skema warna dasar"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										12
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							@@ -5354,6 +5354,10 @@ export interface Locale extends ILocale {
 | 
			
		||||
     * 文字数
 | 
			
		||||
     */
 | 
			
		||||
    "textCount": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 情報
 | 
			
		||||
     */
 | 
			
		||||
    "information": string;
 | 
			
		||||
    "_emojiPalette": {
 | 
			
		||||
        /**
 | 
			
		||||
         * パレット
 | 
			
		||||
@@ -10359,20 +10363,12 @@ export interface Locale extends ILocale {
 | 
			
		||||
             * このプラグインをインストールしますか?
 | 
			
		||||
             */
 | 
			
		||||
            "title": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * プラグイン情報
 | 
			
		||||
             */
 | 
			
		||||
            "metaTitle": string;
 | 
			
		||||
        };
 | 
			
		||||
        "_theme": {
 | 
			
		||||
            /**
 | 
			
		||||
             * このテーマをインストールしますか?
 | 
			
		||||
             */
 | 
			
		||||
            "title": string;
 | 
			
		||||
            /**
 | 
			
		||||
             * テーマ情報
 | 
			
		||||
             */
 | 
			
		||||
            "metaTitle": string;
 | 
			
		||||
        };
 | 
			
		||||
        "_meta": {
 | 
			
		||||
            /**
 | 
			
		||||
 
 | 
			
		||||
@@ -1334,6 +1334,7 @@ paste: "Incolla"
 | 
			
		||||
emojiPalette: "Tavolozza emoji"
 | 
			
		||||
postForm: "Finestra di pubblicazione"
 | 
			
		||||
textCount: "Il numero di caratteri"
 | 
			
		||||
information: "Informazioni"
 | 
			
		||||
_emojiPalette:
 | 
			
		||||
  palettes: "Tavolozza"
 | 
			
		||||
  enableSyncBetweenDevicesForPalettes: "Attiva la sincronizzazione tra dispositivi"
 | 
			
		||||
@@ -2673,10 +2674,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "Prima di installare, assicurati che la fonte sia affidabile."
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "Vuoi davvero installare questo componente aggiuntivo?"
 | 
			
		||||
    metaTitle: "Informazioni sul componente aggiuntivo"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "Vuoi davvero installare questa variazione grafica?"
 | 
			
		||||
    metaTitle: "Informazioni sulla variazione grafica"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "Combinazione base di colori"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -1334,6 +1334,7 @@ paste: "ペースト"
 | 
			
		||||
emojiPalette: "絵文字パレット"
 | 
			
		||||
postForm: "投稿フォーム"
 | 
			
		||||
textCount: "文字数"
 | 
			
		||||
information: "情報"
 | 
			
		||||
 | 
			
		||||
_emojiPalette:
 | 
			
		||||
  palettes: "パレット"
 | 
			
		||||
@@ -2749,10 +2750,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "配布元が信頼できるかを確認した上でインストールしてください。"
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "このプラグインをインストールしますか?"
 | 
			
		||||
    metaTitle: "プラグイン情報"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "このテーマをインストールしますか?"
 | 
			
		||||
    metaTitle: "テーマ情報"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "基本のカラースキーム"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -1312,6 +1312,7 @@ federationDisabled: "このサーバーは連合が無効化されてるで。
 | 
			
		||||
confirmOnReact: "ツッコむときに確認とる"
 | 
			
		||||
reactAreYouSure: "\" {emoji} \" でツッコむ?"
 | 
			
		||||
postForm: "投稿フォーム"
 | 
			
		||||
information: "情報"
 | 
			
		||||
_settings:
 | 
			
		||||
  webhook: "Webhook"
 | 
			
		||||
_accountSettings:
 | 
			
		||||
@@ -2610,10 +2611,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "配ってるとこが信頼できるか確認した上でインストールしてな。"
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "このプラグイン、インストールする?"
 | 
			
		||||
    metaTitle: "プラグイン情報"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "このテーマインストールする?"
 | 
			
		||||
    metaTitle: "テーマ情報"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: ""
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -655,6 +655,7 @@ replies: "답하기"
 | 
			
		||||
renotes: "리노트"
 | 
			
		||||
attach: "옇기"
 | 
			
		||||
surrender: "아이예"
 | 
			
		||||
information: "정보"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "고만 보내예"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -1330,6 +1330,7 @@ preferenceSyncConflictChoiceDevice: "장치 설정값"
 | 
			
		||||
paste: "붙여넣기"
 | 
			
		||||
emojiPalette: "이모지 팔레트"
 | 
			
		||||
postForm: "글 입력란"
 | 
			
		||||
information: "정보"
 | 
			
		||||
_emojiPalette:
 | 
			
		||||
  palettes: "팔레트"
 | 
			
		||||
  paletteForMain: "메인으로 사용할 팔레트"
 | 
			
		||||
@@ -2650,10 +2651,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "제공자를 신뢰할 수 있는 경우에만 설치하십시오."
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "이 플러그인을 설치하시겠습니까?"
 | 
			
		||||
    metaTitle: "플러그인 정보"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "이 테마를 설치하시겠습니까?"
 | 
			
		||||
    metaTitle: "테마 정보"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "기본 컬러 스키마"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -394,6 +394,7 @@ searchByGoogle: "ຄົ້ນຫາ"
 | 
			
		||||
file: "ໄຟລ໌"
 | 
			
		||||
replies: "ຕອບກັບ"
 | 
			
		||||
renotes: "Renote"
 | 
			
		||||
information: "ກ່ຽວກັບ"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "ໂຈະ"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -462,6 +462,7 @@ loggedInAsBot: "Momenteel als bot ingelogd"
 | 
			
		||||
icon: "Avatar"
 | 
			
		||||
replies: "Antwoord"
 | 
			
		||||
renotes: "Herdelen"
 | 
			
		||||
information: "Over"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "Opgeschort"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -463,6 +463,7 @@ icon: "Avatar"
 | 
			
		||||
replies: "Svar"
 | 
			
		||||
renotes: "Renote"
 | 
			
		||||
surrender: "Avbryt"
 | 
			
		||||
information: "Informasjon"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "Suspendert"
 | 
			
		||||
_initialAccountSetting:
 | 
			
		||||
 
 | 
			
		||||
@@ -1045,6 +1045,7 @@ lastNDays: "W ciągu ostatnich {n} dni"
 | 
			
		||||
surrender: "Odrzuć"
 | 
			
		||||
gameRetry: "Spróbuj ponownie"
 | 
			
		||||
postForm: "Formularz tworzenia wpisu"
 | 
			
		||||
information: "Informacje"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "Zawieszono"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -1302,6 +1302,7 @@ pleaseSelectAccount: "Selecione uma conta"
 | 
			
		||||
availableRoles: "Cargos disponíveis"
 | 
			
		||||
acknowledgeNotesAndEnable: "Ative após compreender as precauções."
 | 
			
		||||
postForm: "Campo de postagem"
 | 
			
		||||
information: "Informações"
 | 
			
		||||
_settings:
 | 
			
		||||
  webhook: "Webhook"
 | 
			
		||||
_accountSettings:
 | 
			
		||||
@@ -2598,10 +2599,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "Tenha certeza de que o distribuidor desse recurso é confiável antes da instalação."
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "Deseja instalar esse plugin?"
 | 
			
		||||
    metaTitle: "Informações do plugin"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "Deseja instalar esse tema?"
 | 
			
		||||
    metaTitle: "Informações do tema"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "Paleta de cores base"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -646,6 +646,7 @@ show: "Arată"
 | 
			
		||||
icon: "Avatar"
 | 
			
		||||
replies: "Răspunde"
 | 
			
		||||
renotes: "Re-notează"
 | 
			
		||||
information: "Despre"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "Suspendat"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -1182,6 +1182,7 @@ alwaysConfirmFollow: "Всегда подтверждать подписку"
 | 
			
		||||
inquiry: "Связаться"
 | 
			
		||||
messageToFollower: "Сообщение подписчикам"
 | 
			
		||||
postForm: "Форма отправки"
 | 
			
		||||
information: "Описание"
 | 
			
		||||
_settings:
 | 
			
		||||
  webhook: "Вебхук"
 | 
			
		||||
_delivery:
 | 
			
		||||
 
 | 
			
		||||
@@ -918,6 +918,7 @@ sourceCode: "Zdrojový kód"
 | 
			
		||||
flip: "Preklopiť"
 | 
			
		||||
lastNDays: "Posledných {n} dní"
 | 
			
		||||
postForm: "Napísať poznámku"
 | 
			
		||||
information: "Informácie"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "Zmrazené"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -562,6 +562,7 @@ inquiry: "Kontakt"
 | 
			
		||||
tryAgain: "Försök igen senare"
 | 
			
		||||
signinWithPasskey: "Logga in med nyckel"
 | 
			
		||||
unknownWebAuthnKey: "Okänd nyckel"
 | 
			
		||||
information: "Om"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "Suspenderad"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -1293,6 +1293,7 @@ prohibitedWordsForNameOfUserDescription: "หากมีสตริงใด
 | 
			
		||||
yourNameContainsProhibitedWords: "ชื่อของคุณนั้นมีคำที่ต้องห้าม"
 | 
			
		||||
yourNameContainsProhibitedWordsDescription: "ถ้าหากคุณต้องการใช้ชื่อนี้ กรุณาติดต่อผู้ดูแลระบบของเซิร์ฟเวอร์นะค่ะ"
 | 
			
		||||
postForm: "แบบฟอร์มการโพสต์"
 | 
			
		||||
information: "เกี่ยวกับ"
 | 
			
		||||
_settings:
 | 
			
		||||
  webhook: "Webhook"
 | 
			
		||||
_abuseUserReport:
 | 
			
		||||
@@ -2572,10 +2573,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "โปรดตรวจสอบให้แน่ใจว่าแหล่งแจกหน่ายมีความน่าเชื่อถือก่อนทำการติดตั้ง"
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "ต้องการติดตั้งปลั๊กอินนี้ใช่ไหม?"
 | 
			
		||||
    metaTitle: "ข้อมูลส่วนเสริม"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "ต้องการติดตั้งธีมนี้ใช่ไหม?"
 | 
			
		||||
    metaTitle: "ข้อมูลธีม"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "โทนสีพื้นฐาน"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -910,6 +910,7 @@ sourceCode: "Вихідний код"
 | 
			
		||||
flip: "Перевернути"
 | 
			
		||||
lastNDays: "Останні {n} днів"
 | 
			
		||||
postForm: "Створення нотатки"
 | 
			
		||||
information: "Інформація"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "Призупинено"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -841,6 +841,7 @@ icon: "Avatar"
 | 
			
		||||
replies: "Javob berish"
 | 
			
		||||
renotes: "Qayta qayd etish"
 | 
			
		||||
flip: "Teskari"
 | 
			
		||||
information: "Haqida"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "To'xtatilgan"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -1120,6 +1120,7 @@ cwNotationRequired: "Nếu \"Ẩn nội dung\" được bật thì cần phải
 | 
			
		||||
lastNDays: "{n} ngày trước"
 | 
			
		||||
surrender: "Từ chối"
 | 
			
		||||
postForm: "Mẫu đăng"
 | 
			
		||||
information: "Giới thiệu"
 | 
			
		||||
_delivery:
 | 
			
		||||
  stop: "Đã vô hiệu hóa"
 | 
			
		||||
  _type:
 | 
			
		||||
 
 | 
			
		||||
@@ -1334,6 +1334,7 @@ paste: "粘贴"
 | 
			
		||||
emojiPalette: "表情符号调色板"
 | 
			
		||||
postForm: "投稿窗口"
 | 
			
		||||
textCount: "字数"
 | 
			
		||||
information: "关于"
 | 
			
		||||
_emojiPalette:
 | 
			
		||||
  palettes: "调色板"
 | 
			
		||||
  enableSyncBetweenDevicesForPalettes: "启用调色板的设备间同步"
 | 
			
		||||
@@ -2673,10 +2674,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "请在安装前确保来源可靠"
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "要安装此插件吗?"
 | 
			
		||||
    metaTitle: "插件信息"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "要安装此主题吗?"
 | 
			
		||||
    metaTitle: "主题信息"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "基本配色方案"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -1334,6 +1334,7 @@ paste: "貼上"
 | 
			
		||||
emojiPalette: "表情符號調色盤"
 | 
			
		||||
postForm: "發文視窗"
 | 
			
		||||
textCount: "字數"
 | 
			
		||||
information: "關於"
 | 
			
		||||
_emojiPalette:
 | 
			
		||||
  palettes: "調色盤"
 | 
			
		||||
  enableSyncBetweenDevicesForPalettes: "啟用裝置與裝置之間的調色盤同步化"
 | 
			
		||||
@@ -2673,10 +2674,8 @@ _externalResourceInstaller:
 | 
			
		||||
  checkVendorBeforeInstall: "安裝前請確認提供者是可信賴的。"
 | 
			
		||||
  _plugin:
 | 
			
		||||
    title: "要安裝此外掛嘛?"
 | 
			
		||||
    metaTitle: "外掛資訊"
 | 
			
		||||
  _theme:
 | 
			
		||||
    title: "要安裝此佈景主題嗎?"
 | 
			
		||||
    metaTitle: "佈景主題資訊"
 | 
			
		||||
  _meta:
 | 
			
		||||
    base: "基本配色方案"
 | 
			
		||||
  _vendorInfo:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"version": "2025.3.2-beta.4",
 | 
			
		||||
	"version": "2025.3.2-beta.6",
 | 
			
		||||
	"codename": "nasubi",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
 
 | 
			
		||||
@@ -235,6 +235,7 @@
 | 
			
		||||
		"jest-mock": "29.7.0",
 | 
			
		||||
		"nodemon": "3.1.9",
 | 
			
		||||
		"pid-port": "1.0.2",
 | 
			
		||||
		"simple-oauth2": "5.1.0"
 | 
			
		||||
		"simple-oauth2": "5.1.0",
 | 
			
		||||
		"vite": "6.2.1"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,10 +5,13 @@
 | 
			
		||||
 | 
			
		||||
import * as fs from 'node:fs';
 | 
			
		||||
import { fileURLToPath } from 'node:url';
 | 
			
		||||
import { dirname, resolve } from 'node:path';
 | 
			
		||||
import { dirname, join, resolve } from 'node:path';
 | 
			
		||||
import * as yaml from 'js-yaml';
 | 
			
		||||
import * as Sentry from '@sentry/node';
 | 
			
		||||
import locale from '../../../locales/index.js';
 | 
			
		||||
import type { RedisOptions } from 'ioredis';
 | 
			
		||||
import type { Manifest, ManifestChunk } from 'vite';
 | 
			
		||||
import type { ILocale } from '../../../locales/index.js';
 | 
			
		||||
 | 
			
		||||
type RedisOptionsSource = Partial<RedisOptions> & {
 | 
			
		||||
	host: string;
 | 
			
		||||
@@ -185,9 +188,12 @@ export type Config = {
 | 
			
		||||
	authUrl: string;
 | 
			
		||||
	driveUrl: string;
 | 
			
		||||
	userAgent: string;
 | 
			
		||||
	frontendEntry: string;
 | 
			
		||||
	localeEntries: Record<string, string>;
 | 
			
		||||
	errorLocaleMessages: Record<string, ILocale>;
 | 
			
		||||
	configEntry: ManifestChunk;
 | 
			
		||||
	frontendEntry: ManifestChunk;
 | 
			
		||||
	frontendManifestExists: boolean;
 | 
			
		||||
	frontendEmbedEntry: string;
 | 
			
		||||
	frontendEmbedEntry: ManifestChunk;
 | 
			
		||||
	frontendEmbedManifestExists: boolean;
 | 
			
		||||
	mediaProxy: string;
 | 
			
		||||
	externalMediaProxyEnabled: boolean;
 | 
			
		||||
@@ -229,12 +235,23 @@ export function loadConfig(): Config {
 | 
			
		||||
 | 
			
		||||
	const frontendManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_vite_/manifest.json');
 | 
			
		||||
	const frontendEmbedManifestExists = fs.existsSync(_dirname + '/../../../built/_frontend_embed_vite_/manifest.json');
 | 
			
		||||
	const frontendManifest = frontendManifestExists ?
 | 
			
		||||
		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
		: { 'src/_boot_.ts': { file: 'src/_boot_.ts' } };
 | 
			
		||||
	const frontendEmbedManifest = frontendEmbedManifestExists ?
 | 
			
		||||
		JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
		: { 'src/boot.ts': { file: 'src/boot.ts' } };
 | 
			
		||||
	const frontendManifest: Manifest = frontendManifestExists
 | 
			
		||||
		? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
		: Object.entries(locale).reduce<Record<string, ManifestChunk>>((a, [k]) => {
 | 
			
		||||
			a[`locale:${k}`] = { file: `locale:${k}` };
 | 
			
		||||
			return a;
 | 
			
		||||
		}, {
 | 
			
		||||
			'src/_boot_.ts': { file: 'src/_boot_.ts' },
 | 
			
		||||
			'../frontend-shared/js/config.ts': { file: join('@fs', _dirname.slice(1), '../../frontend-shared/js/config.ts') },
 | 
			
		||||
		});
 | 
			
		||||
	const frontendEmbedManifest: Manifest = frontendEmbedManifestExists
 | 
			
		||||
		? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_frontend_embed_vite_/manifest.json`, 'utf-8'))
 | 
			
		||||
		: Object.entries(locale).reduce<Record<string, ManifestChunk>>((a, [k]) => {
 | 
			
		||||
			a[`locale:${k}`] = { file: `locale:${k}` };
 | 
			
		||||
			return a;
 | 
			
		||||
		}, {
 | 
			
		||||
			'src/boot.ts': { file: 'src/boot.ts' },
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
	const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source;
 | 
			
		||||
 | 
			
		||||
@@ -310,6 +327,20 @@ export function loadConfig(): Config {
 | 
			
		||||
			config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
 | 
			
		||||
			: null,
 | 
			
		||||
		userAgent: `Misskey/${version} (${config.url})`,
 | 
			
		||||
		localeEntries: Object.entries<ManifestChunk>(frontendManifest).reduce<Record<string, string>>((a, [k, v]) => {
 | 
			
		||||
			if (k.startsWith('locale:')) {
 | 
			
		||||
				a[k.slice('locale:'.length)] = v.file;
 | 
			
		||||
			}
 | 
			
		||||
			return a;
 | 
			
		||||
		}, {}),
 | 
			
		||||
		errorLocaleMessages: Object.entries(locale).reduce<Record<string, ILocale>>((a, [k, v]) => {
 | 
			
		||||
			a[k] = {
 | 
			
		||||
				_bootErrors: v._bootErrors,
 | 
			
		||||
				reload: v.reload,
 | 
			
		||||
			};
 | 
			
		||||
			return a;
 | 
			
		||||
		}, {}),
 | 
			
		||||
		configEntry: frontendManifest['../frontend-shared/js/config.ts'],
 | 
			
		||||
		frontendEntry: frontendManifest['src/_boot_.ts'],
 | 
			
		||||
		frontendManifestExists: frontendManifestExists,
 | 
			
		||||
		frontendEmbedEntry: frontendEmbedManifest['src/boot.ts'],
 | 
			
		||||
 
 | 
			
		||||
@@ -7,42 +7,16 @@ import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
 | 
			
		||||
import { AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js';
 | 
			
		||||
import { Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import { type AbuseReportPayload, SystemWebhookPayload, SystemWebhookService } from '@/core/SystemWebhookService.js';
 | 
			
		||||
import { type Packed } from '@/misc/json-schema.js';
 | 
			
		||||
import { type WebhookEventTypes } from '@/models/Webhook.js';
 | 
			
		||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
 | 
			
		||||
import { type UserWebhookPayload, UserWebhookService } from '@/core/UserWebhookService.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
import { ModeratorInactivityRemainingTime } from '@/queue/processors/CheckModeratorsActivityProcessorService.js';
 | 
			
		||||
 | 
			
		||||
const oneDayMillis = 24 * 60 * 60 * 1000;
 | 
			
		||||
 | 
			
		||||
function generateAbuseReport(override?: Partial<MiAbuseUserReport>): AbuseReportPayload {
 | 
			
		||||
	const result: MiAbuseUserReport = {
 | 
			
		||||
		id: 'dummy-abuse-report1',
 | 
			
		||||
		targetUserId: 'dummy-target-user',
 | 
			
		||||
		targetUser: null,
 | 
			
		||||
		reporterId: 'dummy-reporter-user',
 | 
			
		||||
		reporter: null,
 | 
			
		||||
		assigneeId: null,
 | 
			
		||||
		assignee: null,
 | 
			
		||||
		resolved: false,
 | 
			
		||||
		forwarded: false,
 | 
			
		||||
		comment: 'This is a dummy report for testing purposes.',
 | 
			
		||||
		targetUserHost: null,
 | 
			
		||||
		reporterHost: null,
 | 
			
		||||
		resolvedAs: null,
 | 
			
		||||
		moderationNote: 'foo',
 | 
			
		||||
		...override,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		...result,
 | 
			
		||||
		targetUser: result.targetUser ? toPackedUserLite(result.targetUser) : null,
 | 
			
		||||
		reporter: result.reporter ? toPackedUserLite(result.reporter) : null,
 | 
			
		||||
		assignee: result.assignee ? toPackedUserLite(result.assignee) : null,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function generateDummyUser(override?: Partial<MiUser>): MiUser {
 | 
			
		||||
	return {
 | 
			
		||||
		id: 'dummy-user-1',
 | 
			
		||||
@@ -134,124 +108,6 @@ function generateDummyNote(override?: Partial<MiNote>): MiNote {
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
 | 
			
		||||
	return {
 | 
			
		||||
		id: note.id,
 | 
			
		||||
		createdAt: new Date().toISOString(),
 | 
			
		||||
		deletedAt: null,
 | 
			
		||||
		text: note.text,
 | 
			
		||||
		cw: note.cw,
 | 
			
		||||
		userId: note.userId,
 | 
			
		||||
		user: toPackedUserLite(note.user ?? generateDummyUser()),
 | 
			
		||||
		replyId: note.replyId,
 | 
			
		||||
		renoteId: note.renoteId,
 | 
			
		||||
		isHidden: false,
 | 
			
		||||
		visibility: note.visibility,
 | 
			
		||||
		mentions: note.mentions,
 | 
			
		||||
		visibleUserIds: note.visibleUserIds,
 | 
			
		||||
		fileIds: note.fileIds,
 | 
			
		||||
		files: [],
 | 
			
		||||
		tags: note.tags,
 | 
			
		||||
		poll: null,
 | 
			
		||||
		emojis: note.emojis,
 | 
			
		||||
		channelId: note.channelId,
 | 
			
		||||
		channel: note.channel,
 | 
			
		||||
		localOnly: note.localOnly,
 | 
			
		||||
		reactionAcceptance: note.reactionAcceptance,
 | 
			
		||||
		reactionEmojis: {},
 | 
			
		||||
		reactions: {},
 | 
			
		||||
		reactionCount: 0,
 | 
			
		||||
		renoteCount: note.renoteCount,
 | 
			
		||||
		repliesCount: note.repliesCount,
 | 
			
		||||
		uri: note.uri ?? undefined,
 | 
			
		||||
		url: note.url ?? undefined,
 | 
			
		||||
		reactionAndUserPairCache: note.reactionAndUserPairCache,
 | 
			
		||||
		...(detail ? {
 | 
			
		||||
			clippedCount: note.clippedCount,
 | 
			
		||||
			reply: note.reply ? toPackedNote(note.reply, false) : null,
 | 
			
		||||
			renote: note.renote ? toPackedNote(note.renote, true) : null,
 | 
			
		||||
			myReaction: null,
 | 
			
		||||
		} : {}),
 | 
			
		||||
		...override,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
 | 
			
		||||
	return {
 | 
			
		||||
		id: user.id,
 | 
			
		||||
		name: user.name,
 | 
			
		||||
		username: user.username,
 | 
			
		||||
		host: user.host,
 | 
			
		||||
		avatarUrl: user.avatarUrl,
 | 
			
		||||
		avatarBlurhash: user.avatarBlurhash,
 | 
			
		||||
		avatarDecorations: user.avatarDecorations.map(it => ({
 | 
			
		||||
			id: it.id,
 | 
			
		||||
			angle: it.angle,
 | 
			
		||||
			flipH: it.flipH,
 | 
			
		||||
			url: 'https://example.com/dummy-image001.png',
 | 
			
		||||
			offsetX: it.offsetX,
 | 
			
		||||
			offsetY: it.offsetY,
 | 
			
		||||
		})),
 | 
			
		||||
		isBot: user.isBot,
 | 
			
		||||
		isCat: user.isCat,
 | 
			
		||||
		emojis: user.emojis,
 | 
			
		||||
		onlineStatus: 'active',
 | 
			
		||||
		badgeRoles: [],
 | 
			
		||||
		...override,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
 | 
			
		||||
	return {
 | 
			
		||||
		...toPackedUserLite(user),
 | 
			
		||||
		url: null,
 | 
			
		||||
		uri: null,
 | 
			
		||||
		movedTo: null,
 | 
			
		||||
		alsoKnownAs: [],
 | 
			
		||||
		createdAt: new Date().toISOString(),
 | 
			
		||||
		updatedAt: user.updatedAt?.toISOString() ?? null,
 | 
			
		||||
		lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
 | 
			
		||||
		bannerUrl: user.bannerUrl,
 | 
			
		||||
		bannerBlurhash: user.bannerBlurhash,
 | 
			
		||||
		isLocked: user.isLocked,
 | 
			
		||||
		isSilenced: false,
 | 
			
		||||
		isSuspended: user.isSuspended,
 | 
			
		||||
		description: null,
 | 
			
		||||
		location: null,
 | 
			
		||||
		birthday: null,
 | 
			
		||||
		lang: null,
 | 
			
		||||
		fields: [],
 | 
			
		||||
		verifiedLinks: [],
 | 
			
		||||
		followersCount: user.followersCount,
 | 
			
		||||
		followingCount: user.followingCount,
 | 
			
		||||
		notesCount: user.notesCount,
 | 
			
		||||
		pinnedNoteIds: [],
 | 
			
		||||
		pinnedNotes: [],
 | 
			
		||||
		pinnedPageId: null,
 | 
			
		||||
		pinnedPage: null,
 | 
			
		||||
		publicReactions: true,
 | 
			
		||||
		followersVisibility: 'public',
 | 
			
		||||
		followingVisibility: 'public',
 | 
			
		||||
		twoFactorEnabled: false,
 | 
			
		||||
		usePasswordLessLogin: false,
 | 
			
		||||
		securityKeys: false,
 | 
			
		||||
		roles: [],
 | 
			
		||||
		memo: null,
 | 
			
		||||
		moderationNote: undefined,
 | 
			
		||||
		isFollowing: false,
 | 
			
		||||
		isFollowed: false,
 | 
			
		||||
		hasPendingFollowRequestFromYou: false,
 | 
			
		||||
		hasPendingFollowRequestToYou: false,
 | 
			
		||||
		isBlocking: false,
 | 
			
		||||
		isBlocked: false,
 | 
			
		||||
		isMuted: false,
 | 
			
		||||
		isRenoteMuted: false,
 | 
			
		||||
		notify: 'none',
 | 
			
		||||
		withReplies: true,
 | 
			
		||||
		...override,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const dummyUser1 = generateDummyUser();
 | 
			
		||||
const dummyUser2 = generateDummyUser({
 | 
			
		||||
	id: 'dummy-user-2',
 | 
			
		||||
@@ -284,6 +140,7 @@ export class WebhookTestService {
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	constructor(
 | 
			
		||||
		private customEmojiService: CustomEmojiService,
 | 
			
		||||
		private userWebhookService: UserWebhookService,
 | 
			
		||||
		private systemWebhookService: SystemWebhookService,
 | 
			
		||||
		private queueService: QueueService,
 | 
			
		||||
@@ -354,31 +211,31 @@ export class WebhookTestService {
 | 
			
		||||
 | 
			
		||||
		switch (params.type) {
 | 
			
		||||
			case 'note': {
 | 
			
		||||
				send('note', { note: toPackedNote(dummyNote1) });
 | 
			
		||||
				send('note', { note: await this.toPackedNote(dummyNote1) });
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'reply': {
 | 
			
		||||
				send('reply', { note: toPackedNote(dummyReply1) });
 | 
			
		||||
				send('reply', { note: await this.toPackedNote(dummyReply1) });
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'renote': {
 | 
			
		||||
				send('renote', { note: toPackedNote(dummyRenote1) });
 | 
			
		||||
				send('renote', { note: await this.toPackedNote(dummyRenote1) });
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'mention': {
 | 
			
		||||
				send('mention', { note: toPackedNote(dummyMention1) });
 | 
			
		||||
				send('mention', { note: await this.toPackedNote(dummyMention1) });
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'follow': {
 | 
			
		||||
				send('follow', { user: toPackedUserDetailedNotMe(dummyUser1) });
 | 
			
		||||
				send('follow', { user: await this.toPackedUserDetailedNotMe(dummyUser1) });
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'followed': {
 | 
			
		||||
				send('followed', { user: toPackedUserLite(dummyUser2) });
 | 
			
		||||
				send('followed', { user: await this.toPackedUserLite(dummyUser2) });
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'unfollow': {
 | 
			
		||||
				send('unfollow', { user: toPackedUserDetailedNotMe(dummyUser3) });
 | 
			
		||||
				send('unfollow', { user: await this.toPackedUserDetailedNotMe(dummyUser3) });
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			// まだ実装されていない (#9485)
 | 
			
		||||
@@ -427,7 +284,7 @@ export class WebhookTestService {
 | 
			
		||||
 | 
			
		||||
		switch (params.type) {
 | 
			
		||||
			case 'abuseReport': {
 | 
			
		||||
				send('abuseReport', generateAbuseReport({
 | 
			
		||||
				send('abuseReport', await this.generateAbuseReport({
 | 
			
		||||
					targetUserId: dummyUser1.id,
 | 
			
		||||
					targetUser: dummyUser1,
 | 
			
		||||
					reporterId: dummyUser2.id,
 | 
			
		||||
@@ -436,7 +293,7 @@ export class WebhookTestService {
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'abuseReportResolved': {
 | 
			
		||||
				send('abuseReportResolved', generateAbuseReport({
 | 
			
		||||
				send('abuseReportResolved', await this.generateAbuseReport({
 | 
			
		||||
					targetUserId: dummyUser1.id,
 | 
			
		||||
					targetUser: dummyUser1,
 | 
			
		||||
					reporterId: dummyUser2.id,
 | 
			
		||||
@@ -448,7 +305,7 @@ export class WebhookTestService {
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'userCreated': {
 | 
			
		||||
				send('userCreated', toPackedUserLite(dummyUser1));
 | 
			
		||||
				send('userCreated', await this.toPackedUserLite(dummyUser1));
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
			case 'inactiveModeratorsWarning': {
 | 
			
		||||
@@ -474,4 +331,153 @@ export class WebhookTestService {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async generateAbuseReport(override?: Partial<MiAbuseUserReport>): Promise<AbuseReportPayload> {
 | 
			
		||||
		const result: MiAbuseUserReport = {
 | 
			
		||||
			id: 'dummy-abuse-report1',
 | 
			
		||||
			targetUserId: 'dummy-target-user',
 | 
			
		||||
			targetUser: null,
 | 
			
		||||
			reporterId: 'dummy-reporter-user',
 | 
			
		||||
			reporter: null,
 | 
			
		||||
			assigneeId: null,
 | 
			
		||||
			assignee: null,
 | 
			
		||||
			resolved: false,
 | 
			
		||||
			forwarded: false,
 | 
			
		||||
			comment: 'This is a dummy report for testing purposes.',
 | 
			
		||||
			targetUserHost: null,
 | 
			
		||||
			reporterHost: null,
 | 
			
		||||
			resolvedAs: null,
 | 
			
		||||
			moderationNote: 'foo',
 | 
			
		||||
			...override,
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		return {
 | 
			
		||||
			...result,
 | 
			
		||||
			targetUser: result.targetUser ? await this.toPackedUserLite(result.targetUser) : null,
 | 
			
		||||
			reporter: result.reporter ? await this.toPackedUserLite(result.reporter) : null,
 | 
			
		||||
			assignee: result.assignee ? await this.toPackedUserLite(result.assignee) : null,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Promise<Packed<'Note'>> {
 | 
			
		||||
		return {
 | 
			
		||||
			id: note.id,
 | 
			
		||||
			createdAt: new Date().toISOString(),
 | 
			
		||||
			deletedAt: null,
 | 
			
		||||
			text: note.text,
 | 
			
		||||
			cw: note.cw,
 | 
			
		||||
			userId: note.userId,
 | 
			
		||||
			user: await this.toPackedUserLite(note.user ?? generateDummyUser()),
 | 
			
		||||
			replyId: note.replyId,
 | 
			
		||||
			renoteId: note.renoteId,
 | 
			
		||||
			isHidden: false,
 | 
			
		||||
			visibility: note.visibility,
 | 
			
		||||
			mentions: note.mentions,
 | 
			
		||||
			visibleUserIds: note.visibleUserIds,
 | 
			
		||||
			fileIds: note.fileIds,
 | 
			
		||||
			files: [],
 | 
			
		||||
			tags: note.tags,
 | 
			
		||||
			poll: null,
 | 
			
		||||
			emojis: await this.customEmojiService.populateEmojis(note.emojis, note.userHost),
 | 
			
		||||
			channelId: note.channelId,
 | 
			
		||||
			channel: note.channel,
 | 
			
		||||
			localOnly: note.localOnly,
 | 
			
		||||
			reactionAcceptance: note.reactionAcceptance,
 | 
			
		||||
			reactionEmojis: {},
 | 
			
		||||
			reactions: {},
 | 
			
		||||
			reactionCount: 0,
 | 
			
		||||
			renoteCount: note.renoteCount,
 | 
			
		||||
			repliesCount: note.repliesCount,
 | 
			
		||||
			uri: note.uri ?? undefined,
 | 
			
		||||
			url: note.url ?? undefined,
 | 
			
		||||
			reactionAndUserPairCache: note.reactionAndUserPairCache,
 | 
			
		||||
			...(detail ? {
 | 
			
		||||
				clippedCount: note.clippedCount,
 | 
			
		||||
				reply: note.reply ? await this.toPackedNote(note.reply, false) : null,
 | 
			
		||||
				renote: note.renote ? await this.toPackedNote(note.renote, true) : null,
 | 
			
		||||
				myReaction: null,
 | 
			
		||||
			} : {}),
 | 
			
		||||
			...override,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Promise<Packed<'UserLite'>> {
 | 
			
		||||
		return {
 | 
			
		||||
			id: user.id,
 | 
			
		||||
			name: user.name,
 | 
			
		||||
			username: user.username,
 | 
			
		||||
			host: user.host,
 | 
			
		||||
			avatarUrl: user.avatarUrl,
 | 
			
		||||
			avatarBlurhash: user.avatarBlurhash,
 | 
			
		||||
			avatarDecorations: user.avatarDecorations.map(it => ({
 | 
			
		||||
				id: it.id,
 | 
			
		||||
				angle: it.angle,
 | 
			
		||||
				flipH: it.flipH,
 | 
			
		||||
				url: 'https://example.com/dummy-image001.png',
 | 
			
		||||
				offsetX: it.offsetX,
 | 
			
		||||
				offsetY: it.offsetY,
 | 
			
		||||
			})),
 | 
			
		||||
			isBot: user.isBot,
 | 
			
		||||
			isCat: user.isCat,
 | 
			
		||||
			emojis: await this.customEmojiService.populateEmojis(user.emojis, user.host),
 | 
			
		||||
			onlineStatus: 'active',
 | 
			
		||||
			badgeRoles: [],
 | 
			
		||||
			...override,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Promise<Packed<'UserDetailedNotMe'>> {
 | 
			
		||||
		return {
 | 
			
		||||
			...await this.toPackedUserLite(user),
 | 
			
		||||
			url: null,
 | 
			
		||||
			uri: null,
 | 
			
		||||
			movedTo: null,
 | 
			
		||||
			alsoKnownAs: [],
 | 
			
		||||
			createdAt: new Date().toISOString(),
 | 
			
		||||
			updatedAt: user.updatedAt?.toISOString() ?? null,
 | 
			
		||||
			lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
 | 
			
		||||
			bannerUrl: user.bannerUrl,
 | 
			
		||||
			bannerBlurhash: user.bannerBlurhash,
 | 
			
		||||
			isLocked: user.isLocked,
 | 
			
		||||
			isSilenced: false,
 | 
			
		||||
			isSuspended: user.isSuspended,
 | 
			
		||||
			description: null,
 | 
			
		||||
			location: null,
 | 
			
		||||
			birthday: null,
 | 
			
		||||
			lang: null,
 | 
			
		||||
			fields: [],
 | 
			
		||||
			verifiedLinks: [],
 | 
			
		||||
			followersCount: user.followersCount,
 | 
			
		||||
			followingCount: user.followingCount,
 | 
			
		||||
			notesCount: user.notesCount,
 | 
			
		||||
			pinnedNoteIds: [],
 | 
			
		||||
			pinnedNotes: [],
 | 
			
		||||
			pinnedPageId: null,
 | 
			
		||||
			pinnedPage: null,
 | 
			
		||||
			publicReactions: true,
 | 
			
		||||
			followersVisibility: 'public',
 | 
			
		||||
			followingVisibility: 'public',
 | 
			
		||||
			twoFactorEnabled: false,
 | 
			
		||||
			usePasswordLessLogin: false,
 | 
			
		||||
			securityKeys: false,
 | 
			
		||||
			roles: [],
 | 
			
		||||
			memo: null,
 | 
			
		||||
			moderationNote: undefined,
 | 
			
		||||
			isFollowing: false,
 | 
			
		||||
			isFollowed: false,
 | 
			
		||||
			hasPendingFollowRequestFromYou: false,
 | 
			
		||||
			hasPendingFollowRequestToYou: false,
 | 
			
		||||
			isBlocking: false,
 | 
			
		||||
			isBlocked: false,
 | 
			
		||||
			isMuted: false,
 | 
			
		||||
			isRenoteMuted: false,
 | 
			
		||||
			notify: 'none',
 | 
			
		||||
			withReplies: true,
 | 
			
		||||
			...override,
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -166,6 +166,7 @@ export interface Schema extends OfSchema {
 | 
			
		||||
	readonly maximum?: number;
 | 
			
		||||
	readonly minimum?: number;
 | 
			
		||||
	readonly pattern?: string;
 | 
			
		||||
	readonly additionalProperties?: Schema | boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type RequiredPropertyNames<s extends Obj> = {
 | 
			
		||||
@@ -217,6 +218,13 @@ type ObjectSchemaTypeDef<p extends Schema> =
 | 
			
		||||
	:
 | 
			
		||||
	p['anyOf'] extends ReadonlyArray<Schema> ? never : // see CONTRIBUTING.md
 | 
			
		||||
	p['allOf'] extends ReadonlyArray<Schema> ? UnionToIntersection<UnionSchemaType<p['allOf']>> :
 | 
			
		||||
	p['additionalProperties'] extends true ? Record<string, any> :
 | 
			
		||||
	p['additionalProperties'] extends Schema ?
 | 
			
		||||
		p['additionalProperties'] extends infer AdditionalProperties ?
 | 
			
		||||
			AdditionalProperties extends Schema ?
 | 
			
		||||
				Record<string, SchemaType<AdditionalProperties>> :
 | 
			
		||||
				never :
 | 
			
		||||
			never :
 | 
			
		||||
	any;
 | 
			
		||||
 | 
			
		||||
type ObjectSchemaType<p extends Schema> = NullOrUndefined<p, ObjectSchemaTypeDef<p>>;
 | 
			
		||||
 
 | 
			
		||||
@@ -32,56 +32,24 @@
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region Detect language & fetch translations
 | 
			
		||||
	if (!localStorage.hasOwnProperty('locale')) {
 | 
			
		||||
		const supportedLangs = LANGS;
 | 
			
		||||
		let lang = localStorage.getItem('lang');
 | 
			
		||||
		if (lang == null || !supportedLangs.includes(lang)) {
 | 
			
		||||
			if (supportedLangs.includes(navigator.language)) {
 | 
			
		||||
				lang = navigator.language;
 | 
			
		||||
			} else {
 | 
			
		||||
				lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
 | 
			
		||||
 | 
			
		||||
				// Fallback
 | 
			
		||||
				if (lang == null) lang = 'en-US';
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const metaRes = await window.fetch('/api/meta', {
 | 
			
		||||
			method: 'POST',
 | 
			
		||||
			body: JSON.stringify({}),
 | 
			
		||||
			credentials: 'omit',
 | 
			
		||||
			cache: 'no-cache',
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Content-Type': 'application/json',
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
		if (metaRes.status !== 200) {
 | 
			
		||||
			renderError('META_FETCH');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		const meta = await metaRes.json();
 | 
			
		||||
		const v = meta.version;
 | 
			
		||||
		if (v == null) {
 | 
			
		||||
			renderError('META_FETCH_V');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// for https://github.com/misskey-dev/misskey/issues/10202
 | 
			
		||||
		if (lang == null || lang.toString == null || lang.toString() === 'null') {
 | 
			
		||||
			console.error('invalid lang value detected!!!', typeof lang, lang);
 | 
			
		||||
			lang = 'en-US';
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
 | 
			
		||||
		if (localRes.status === 200) {
 | 
			
		||||
			localStorage.setItem('lang', lang);
 | 
			
		||||
			localStorage.setItem('locale', await localRes.text());
 | 
			
		||||
			localStorage.setItem('localeVersion', v);
 | 
			
		||||
	const supportedLangs = LANGS;
 | 
			
		||||
	let lang = localStorage.getItem('lang');
 | 
			
		||||
	if (!supportedLangs.includes(lang)) {
 | 
			
		||||
		if (supportedLangs.includes(navigator.language)) {
 | 
			
		||||
			lang = navigator.language;
 | 
			
		||||
		} else {
 | 
			
		||||
			renderError('LOCALE_FETCH');
 | 
			
		||||
			return;
 | 
			
		||||
			lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
 | 
			
		||||
 | 
			
		||||
			// Fallback
 | 
			
		||||
			if (lang == null) lang = 'en-US';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	await import(`/vite/${LOCALES[lang]}`)
 | 
			
		||||
		.catch(async e => {
 | 
			
		||||
			console.error(e);
 | 
			
		||||
			renderError('LOCALE_FETCH', e);
 | 
			
		||||
		});
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region Script
 | 
			
		||||
@@ -115,10 +83,21 @@
 | 
			
		||||
			await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const locale = JSON.parse(localStorage.getItem('locale') || '{}');
 | 
			
		||||
		const supportedLangs = LANGS;
 | 
			
		||||
		let lang = localStorage.getItem('lang');
 | 
			
		||||
		if (!supportedLangs.includes(lang)) {
 | 
			
		||||
			if (supportedLangs.includes(navigator.language)) {
 | 
			
		||||
				lang = navigator.language;
 | 
			
		||||
			} else {
 | 
			
		||||
				lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
 | 
			
		||||
 | 
			
		||||
		const title = locale?._bootErrors?.title || 'Failed to initialize Misskey';
 | 
			
		||||
		const reload = locale?.reload || 'Reload';
 | 
			
		||||
				// Fallback
 | 
			
		||||
				if (lang == null) lang = 'en-US';
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		const { locale } = await import(`/vite/${CONFIG_ENTRY}`).catch(() => ({ locale: {} }));
 | 
			
		||||
		const title = locale._bootErrors.title;
 | 
			
		||||
		const reload = locale.reload;
 | 
			
		||||
 | 
			
		||||
		document.body.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M12 12m-9 0a9 9 0 1 0 18 0a9 9 0 1 0 -18 0" /><path d="M12 9v4" /><path d="M12 16v.01" /></svg>
 | 
			
		||||
		<div class="message">${title}</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -23,56 +23,24 @@
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region Detect language & fetch translations
 | 
			
		||||
	if (!localStorage.hasOwnProperty('locale')) {
 | 
			
		||||
		const supportedLangs = LANGS;
 | 
			
		||||
		let lang = localStorage.getItem('lang');
 | 
			
		||||
		if (lang == null || !supportedLangs.includes(lang)) {
 | 
			
		||||
			if (supportedLangs.includes(navigator.language)) {
 | 
			
		||||
				lang = navigator.language;
 | 
			
		||||
			} else {
 | 
			
		||||
				lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
 | 
			
		||||
 | 
			
		||||
				// Fallback
 | 
			
		||||
				if (lang == null) lang = 'en-US';
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const metaRes = await window.fetch('/api/meta', {
 | 
			
		||||
			method: 'POST',
 | 
			
		||||
			body: JSON.stringify({}),
 | 
			
		||||
			credentials: 'omit',
 | 
			
		||||
			cache: 'no-cache',
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Content-Type': 'application/json',
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
		if (metaRes.status !== 200) {
 | 
			
		||||
			renderError('META_FETCH');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
		const meta = await metaRes.json();
 | 
			
		||||
		const v = meta.version;
 | 
			
		||||
		if (v == null) {
 | 
			
		||||
			renderError('META_FETCH_V');
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// for https://github.com/misskey-dev/misskey/issues/10202
 | 
			
		||||
		if (lang == null || lang.toString == null || lang.toString() === 'null') {
 | 
			
		||||
			console.error('invalid lang value detected!!!', typeof lang, lang);
 | 
			
		||||
			lang = 'en-US';
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const localRes = await window.fetch(`/assets/locales/${lang}.${v}.json`);
 | 
			
		||||
		if (localRes.status === 200) {
 | 
			
		||||
			localStorage.setItem('lang', lang);
 | 
			
		||||
			localStorage.setItem('locale', await localRes.text());
 | 
			
		||||
			localStorage.setItem('localeVersion', v);
 | 
			
		||||
	const supportedLangs = LANGS;
 | 
			
		||||
	let lang = localStorage.getItem('lang');
 | 
			
		||||
	if (!supportedLangs.includes(lang)) {
 | 
			
		||||
		if (supportedLangs.includes(navigator.language)) {
 | 
			
		||||
			lang = navigator.language;
 | 
			
		||||
		} else {
 | 
			
		||||
			renderError('LOCALE_FETCH');
 | 
			
		||||
			return;
 | 
			
		||||
			lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
 | 
			
		||||
 | 
			
		||||
			// Fallback
 | 
			
		||||
			if (lang == null) lang = 'en-US';
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	await import(`/vite/${LOCALES[lang]}`)
 | 
			
		||||
		.catch(async e => {
 | 
			
		||||
			console.error(e);
 | 
			
		||||
			renderError('LOCALE_FETCH', e);
 | 
			
		||||
		});
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region Script
 | 
			
		||||
@@ -151,21 +119,25 @@
 | 
			
		||||
			await new Promise(resolve => window.addEventListener('DOMContentLoaded', resolve));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const locale = JSON.parse(localStorage.getItem('locale') || '{}');
 | 
			
		||||
		const supportedLangs = LANGS;
 | 
			
		||||
		let lang = localStorage.getItem('lang');
 | 
			
		||||
		if (!supportedLangs.includes(lang)) {
 | 
			
		||||
			if (supportedLangs.includes(navigator.language)) {
 | 
			
		||||
				lang = navigator.language;
 | 
			
		||||
			} else {
 | 
			
		||||
				lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
 | 
			
		||||
 | 
			
		||||
		const messages = Object.assign({
 | 
			
		||||
			title: 'Failed to initialize Misskey',
 | 
			
		||||
			solution: 'The following actions may solve the problem.',
 | 
			
		||||
			solution1: 'Update your os and browser',
 | 
			
		||||
			solution2: 'Disable an adblocker',
 | 
			
		||||
			solution3: 'Clear the browser cache',
 | 
			
		||||
			solution4: '(Tor Browser) Set dom.webaudio.enabled to true',
 | 
			
		||||
			otherOption: 'Other options',
 | 
			
		||||
			otherOption1: 'Clear preferences and cache',
 | 
			
		||||
			otherOption2: 'Start the simple client',
 | 
			
		||||
			otherOption3: 'Start the repair tool',
 | 
			
		||||
		}, locale?._bootErrors || {});
 | 
			
		||||
		const reload = locale?.reload || 'Reload';
 | 
			
		||||
				// Fallback
 | 
			
		||||
				if (lang == null) lang = 'en-US';
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		const { locale } = await import(`/vite/${CONFIG_ENTRY}`).catch(() => ({
 | 
			
		||||
			locale: {
 | 
			
		||||
				_bootErrors: {},
 | 
			
		||||
			},
 | 
			
		||||
		}));
 | 
			
		||||
		const messages = locale._bootErrors;
 | 
			
		||||
		const reload = locale.reload;
 | 
			
		||||
 | 
			
		||||
		let errorsElement = document.getElementById('errors');
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,23 +6,22 @@
 | 
			
		||||
'use strict';
 | 
			
		||||
 | 
			
		||||
(() => {
 | 
			
		||||
	document.addEventListener('DOMContentLoaded', () => {
 | 
			
		||||
		const locale = JSON.parse(localStorage.getItem('locale') || '{}');
 | 
			
		||||
	document.addEventListener('DOMContentLoaded', async () => {
 | 
			
		||||
		const supportedLangs = LANGS;
 | 
			
		||||
		let lang = localStorage.getItem('lang');
 | 
			
		||||
		if (!supportedLangs.includes(lang)) {
 | 
			
		||||
			if (supportedLangs.includes(navigator.language)) {
 | 
			
		||||
				lang = navigator.language;
 | 
			
		||||
			} else {
 | 
			
		||||
				lang = supportedLangs.find(x => x.split('-')[0] === navigator.language);
 | 
			
		||||
 | 
			
		||||
		const messages = Object.assign({
 | 
			
		||||
			title: 'Failed to initialize Misskey',
 | 
			
		||||
			serverError: 'If reloading after a period of time does not resolve the problem, contact the server administrator with the following ERROR ID.',
 | 
			
		||||
			solution: 'The following actions may solve the problem.',
 | 
			
		||||
			solution1: 'Update your os and browser',
 | 
			
		||||
			solution2: 'Disable an adblocker',
 | 
			
		||||
			solution3: 'Clear the browser cache',
 | 
			
		||||
			solution4: '(Tor Browser) Set dom.webaudio.enabled to true',
 | 
			
		||||
			otherOption: 'Other options',
 | 
			
		||||
			otherOption1: 'Clear preferences and cache',
 | 
			
		||||
			otherOption2: 'Start the simple client',
 | 
			
		||||
			otherOption3: 'Start the repair tool',
 | 
			
		||||
		}, locale?._bootErrors || {});
 | 
			
		||||
		const reload = locale?.reload || 'Reload';
 | 
			
		||||
				// Fallback
 | 
			
		||||
				if (lang == null) lang = 'en-US';
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		const locale = ERROR_MESSAGES[lang];
 | 
			
		||||
		const messages = locale._bootErrors;
 | 
			
		||||
		const reload = locale.reload;
 | 
			
		||||
 | 
			
		||||
		const reloadEls = document.querySelectorAll('[data-i18n-reload]');
 | 
			
		||||
		for (const el of reloadEls) {
 | 
			
		||||
 
 | 
			
		||||
@@ -41,6 +41,8 @@ html(class='embed')
 | 
			
		||||
		script.
 | 
			
		||||
			var VERSION = "#{version}";
 | 
			
		||||
			var CLIENT_ENTRY = "#{entry.file}";
 | 
			
		||||
			var CONFIG_ENTRY = "#{config.configEntry.file}";
 | 
			
		||||
			var LOCALES = JSON.parse(`!{JSON.stringify(config.localeEntries)}`);
 | 
			
		||||
 | 
			
		||||
		script(type='application/json' id='misskey_meta' data-generated-at=now)
 | 
			
		||||
			!= metaJson
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,8 @@ html
 | 
			
		||||
		script.
 | 
			
		||||
			var VERSION = "#{version}";
 | 
			
		||||
			var CLIENT_ENTRY = "#{entry.file}";
 | 
			
		||||
			var CONFIG_ENTRY = "#{config.configEntry.file}";
 | 
			
		||||
			var LOCALES = JSON.parse(`!{JSON.stringify(config.localeEntries)}`);
 | 
			
		||||
 | 
			
		||||
		script(type='application/json' id='misskey_meta' data-generated-at=now)
 | 
			
		||||
			!= metaJson
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,9 @@ html
 | 
			
		||||
		style
 | 
			
		||||
			include ../error.css
 | 
			
		||||
 | 
			
		||||
		script.
 | 
			
		||||
			var ERROR_MESSAGES = JSON.parse(`!{JSON.stringify(config.errorLocaleMessages)}`);
 | 
			
		||||
 | 
			
		||||
		script
 | 
			
		||||
			include ../error.js
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersReposi
 | 
			
		||||
import { IdService } from '@/core/IdService.js';
 | 
			
		||||
import { DI } from '@/di-symbols.js';
 | 
			
		||||
import { QueueService } from '@/core/QueueService.js';
 | 
			
		||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
 | 
			
		||||
 | 
			
		||||
describe('WebhookTestService', () => {
 | 
			
		||||
	let app: TestingModule;
 | 
			
		||||
@@ -56,6 +57,11 @@ describe('WebhookTestService', () => {
 | 
			
		||||
			providers: [
 | 
			
		||||
				WebhookTestService,
 | 
			
		||||
				IdService,
 | 
			
		||||
				{
 | 
			
		||||
					provide: CustomEmojiService, useFactory: () => ({
 | 
			
		||||
						populateEmojis: jest.fn(),
 | 
			
		||||
					}),
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					provide: QueueService, useFactory: () => ({
 | 
			
		||||
						systemWebhookDeliver: jest.fn(),
 | 
			
		||||
 
 | 
			
		||||
@@ -13,18 +13,17 @@ import { createApp, defineAsyncComponent } from 'vue';
 | 
			
		||||
import defaultLightTheme from '@@/themes/l-light.json5';
 | 
			
		||||
import defaultDarkTheme from '@@/themes/d-dark.json5';
 | 
			
		||||
import { MediaProxy } from '@@/js/media-proxy.js';
 | 
			
		||||
import { url, version, locale, lang, updateLocale } from '@@/js/config.js';
 | 
			
		||||
import { parseEmbedParams } from '@@/js/embed-page.js';
 | 
			
		||||
import type { Theme } from '@/theme.js';
 | 
			
		||||
import { applyTheme, assertIsTheme } from '@/theme.js';
 | 
			
		||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
 | 
			
		||||
import { DI } from '@/di.js';
 | 
			
		||||
import { serverMetadata } from '@/server-metadata.js';
 | 
			
		||||
import { url, version, locale, lang, updateLocale } from '@@/js/config.js';
 | 
			
		||||
import { parseEmbedParams } from '@@/js/embed-page.js';
 | 
			
		||||
import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
 | 
			
		||||
import { serverContext } from '@/server-context.js';
 | 
			
		||||
import { i18n, updateI18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
import type { Theme } from '@/theme.js';
 | 
			
		||||
 | 
			
		||||
console.log('Misskey Embed');
 | 
			
		||||
 | 
			
		||||
//#region Embedパラメータの取得・パース
 | 
			
		||||
@@ -71,22 +70,6 @@ if (embedParams.colorMode === 'dark') {
 | 
			
		||||
}
 | 
			
		||||
//#endregion
 | 
			
		||||
 | 
			
		||||
//#region Detect language & fetch translations
 | 
			
		||||
const localeVersion = localStorage.getItem('localeVersion');
 | 
			
		||||
const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null);
 | 
			
		||||
if (localeOutdated) {
 | 
			
		||||
	const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
 | 
			
		||||
	if (res.status === 200) {
 | 
			
		||||
		const newLocale = await res.text();
 | 
			
		||||
		const parsedNewLocale = JSON.parse(newLocale);
 | 
			
		||||
		localStorage.setItem('locale', newLocale);
 | 
			
		||||
		localStorage.setItem('localeVersion', version);
 | 
			
		||||
		updateLocale(parsedNewLocale);
 | 
			
		||||
		updateI18n(parsedNewLocale);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
//#endregion
 | 
			
		||||
 | 
			
		||||
// サイズの制限
 | 
			
		||||
document.documentElement.style.maxWidth = '500px';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,13 +33,11 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
 | 
			
		||||
				Math.min(navigator.hardwareConcurrency - 1, 4),
 | 
			
		||||
			);
 | 
			
		||||
			resolve(workers);
 | 
			
		||||
			if (_DEV_) console.log('WebGL2 in worker is supported!');
 | 
			
		||||
		} else {
 | 
			
		||||
			const canvas = document.createElement('canvas');
 | 
			
		||||
			canvas.width = 64;
 | 
			
		||||
			canvas.height = 64;
 | 
			
		||||
			resolve(canvas);
 | 
			
		||||
			if (_DEV_) console.log('WebGL2 in worker is not supported...');
 | 
			
		||||
		}
 | 
			
		||||
		testWorker.terminate();
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -12,12 +12,12 @@ const siteName = document.querySelector<HTMLMetaElement>('meta[property="og:site
 | 
			
		||||
export const host = address.host;
 | 
			
		||||
export const hostname = address.hostname;
 | 
			
		||||
export const url = address.origin;
 | 
			
		||||
export const port = address.port;
 | 
			
		||||
export const apiUrl = location.origin + '/api';
 | 
			
		||||
export const wsOrigin = location.origin;
 | 
			
		||||
export const lang = localStorage.getItem('lang') ?? 'en-US';
 | 
			
		||||
export const langs = _LANGS_;
 | 
			
		||||
const preParseLocale = localStorage.getItem('locale');
 | 
			
		||||
export let locale: Locale = preParseLocale ? JSON.parse(preParseLocale) : null;
 | 
			
		||||
export let locale: Locale;
 | 
			
		||||
export const version = _VERSION_;
 | 
			
		||||
export const instanceName = (siteName === 'Misskey' || siteName == null) ? host : siteName;
 | 
			
		||||
export const ui = localStorage.getItem('ui');
 | 
			
		||||
 
 | 
			
		||||
@@ -131,7 +131,7 @@ export function imageDataUrl(options?: {
 | 
			
		||||
		alpha?: number,
 | 
			
		||||
	}
 | 
			
		||||
}, seed?: string): string {
 | 
			
		||||
	const canvas = document.createElement('canvas');
 | 
			
		||||
	const canvas = window.document.createElement('canvas');
 | 
			
		||||
	canvas.width = options?.size?.width ?? 100;
 | 
			
		||||
	canvas.height = options?.size?.height ?? 100;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,9 +23,9 @@ let misskeyOS = null;
 | 
			
		||||
 | 
			
		||||
function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
 | 
			
		||||
	unobserve();
 | 
			
		||||
	const theme = themes[document.documentElement.dataset.misskeyTheme];
 | 
			
		||||
	const theme = themes[window.document.documentElement.dataset.misskeyTheme];
 | 
			
		||||
	if (theme) {
 | 
			
		||||
		applyTheme(themes[document.documentElement.dataset.misskeyTheme]);
 | 
			
		||||
		applyTheme(themes[window.document.documentElement.dataset.misskeyTheme]);
 | 
			
		||||
	} else {
 | 
			
		||||
		applyTheme(themes['l-light']);
 | 
			
		||||
	}
 | 
			
		||||
@@ -42,7 +42,7 @@ function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	observer.observe(document.documentElement, {
 | 
			
		||||
	observer.observe(window.document.documentElement, {
 | 
			
		||||
		attributes: true,
 | 
			
		||||
		attributeFilter: ['data-misskey-theme'],
 | 
			
		||||
	});
 | 
			
		||||
@@ -55,7 +55,6 @@ function initLocalStorage() {
 | 
			
		||||
		...userDetailed(),
 | 
			
		||||
		policies: {},
 | 
			
		||||
	}));
 | 
			
		||||
	localStorage.setItem('locale', JSON.stringify(locale));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
initialize({
 | 
			
		||||
@@ -70,13 +69,17 @@ queueMicrotask(() => {
 | 
			
		||||
		import('../src/theme.js'),
 | 
			
		||||
		import('../src/preferences.js'),
 | 
			
		||||
		import('../src/os.js'),
 | 
			
		||||
	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { prefer }, os]) => {
 | 
			
		||||
		import('../src/i18n.js'),
 | 
			
		||||
		import('../../frontend-shared/js/config.js'),
 | 
			
		||||
	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { prefer }, os, { updateI18n }, { updateLocale }]) => {
 | 
			
		||||
		setup((app) => {
 | 
			
		||||
			moduleInitialized = true;
 | 
			
		||||
			if (app[appInitialized]) {
 | 
			
		||||
				return;
 | 
			
		||||
			}
 | 
			
		||||
			app[appInitialized] = true;
 | 
			
		||||
			updateLocale(locale);
 | 
			
		||||
			updateI18n(locale);
 | 
			
		||||
			loadTheme(applyTheme);
 | 
			
		||||
			components(app);
 | 
			
		||||
			directives(app);
 | 
			
		||||
 
 | 
			
		||||
@@ -50,9 +50,46 @@ export default [
 | 
			
		||||
			// defineExposeが誤検知されてしまう
 | 
			
		||||
			'@typescript-eslint/no-unused-expressions': 'off',
 | 
			
		||||
			'import/consistent-type-specifier-style': ['error', 'prefer-top-level'],
 | 
			
		||||
			// window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
 | 
			
		||||
			// e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
 | 
			
		||||
			'id-denylist': ['error', 'window', 'e'],
 | 
			
		||||
			// window ... グローバルスコープと衝突し、予期せぬ結果を招くため
 | 
			
		||||
			// e ... error や event など、複数のキーワードの頭文字であり分かりにくいため
 | 
			
		||||
			// close ... window.closeと衝突 or 紛らわしい
 | 
			
		||||
			// open ... window.openと衝突 or 紛らわしい
 | 
			
		||||
			// fetch ... window.fetchと衝突 or 紛らわしい
 | 
			
		||||
			// location ... window.locationと衝突 or 紛らわしい
 | 
			
		||||
			// document ... window.documentと衝突 or 紛らわしい
 | 
			
		||||
			// history ... window.historyと衝突 or 紛らわしい
 | 
			
		||||
			'id-denylist': ['warn', 'window', 'e', 'close', 'open', 'fetch', 'location', 'document', 'history'],
 | 
			
		||||
			'no-restricted-globals': [
 | 
			
		||||
				'error',
 | 
			
		||||
				{
 | 
			
		||||
					'name': 'open',
 | 
			
		||||
					'message': 'Use `window.open`.',
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					'name': 'close',
 | 
			
		||||
					'message': 'Use `window.close`.',
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					'name': 'fetch',
 | 
			
		||||
					'message': 'Use `window.fetch`.',
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					'name': 'location',
 | 
			
		||||
					'message': 'Use `window.location`.',
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					'name': 'document',
 | 
			
		||||
					'message': 'Use `window.document`.',
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					'name': 'history',
 | 
			
		||||
					'message': 'Use `window.history`.',
 | 
			
		||||
				},
 | 
			
		||||
				{
 | 
			
		||||
					'name': 'name',
 | 
			
		||||
					'message': 'Use `window.name`. もしくは name という変数名を定義し忘れている',
 | 
			
		||||
				},
 | 
			
		||||
			],
 | 
			
		||||
			'no-shadow': ['warn'],
 | 
			
		||||
			'vue/attributes-order': ['error', {
 | 
			
		||||
				alphabetical: false,
 | 
			
		||||
 
 | 
			
		||||
@@ -12,9 +12,9 @@ import '@/style.scss';
 | 
			
		||||
import { mainBoot } from '@/boot/main-boot.js';
 | 
			
		||||
import { subBoot } from '@/boot/sub-boot.js';
 | 
			
		||||
 | 
			
		||||
const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete'];
 | 
			
		||||
const subBootPaths = ['/share', '/auth', '/miauth', '/oauth', '/signup-complete', '/install-extensions'];
 | 
			
		||||
 | 
			
		||||
if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
 | 
			
		||||
if (subBootPaths.some(i => window.location.pathname === i || window.location.pathname.startsWith(i + '/'))) {
 | 
			
		||||
	subBoot();
 | 
			
		||||
} else {
 | 
			
		||||
	mainBoot();
 | 
			
		||||
 
 | 
			
		||||
@@ -191,7 +191,7 @@ export async function login(token: AccountWithToken['token'], redirect?: string)
 | 
			
		||||
		// 他のタブは再読み込みするだけ
 | 
			
		||||
		reloadChannel.postMessage(null);
 | 
			
		||||
		// このページはredirectで指定された先に移動
 | 
			
		||||
		location.href = redirect;
 | 
			
		||||
		window.location.href = redirect;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -78,45 +78,29 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	//#region Detect language & fetch translations
 | 
			
		||||
	const localeVersion = miLocalStorage.getItem('localeVersion');
 | 
			
		||||
	const localeOutdated = (localeVersion == null || localeVersion !== version || locale == null);
 | 
			
		||||
	if (localeOutdated) {
 | 
			
		||||
		const res = await window.fetch(`/assets/locales/${lang}.${version}.json`);
 | 
			
		||||
		if (res.status === 200) {
 | 
			
		||||
			const newLocale = await res.text();
 | 
			
		||||
			const parsedNewLocale = JSON.parse(newLocale);
 | 
			
		||||
			miLocalStorage.setItem('locale', newLocale);
 | 
			
		||||
			miLocalStorage.setItem('localeVersion', version);
 | 
			
		||||
			updateLocale(parsedNewLocale);
 | 
			
		||||
			updateI18n(parsedNewLocale);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	// タッチデバイスでCSSの:hoverを機能させる
 | 
			
		||||
	document.addEventListener('touchend', () => {}, { passive: true });
 | 
			
		||||
	window.document.addEventListener('touchend', () => {}, { passive: true });
 | 
			
		||||
 | 
			
		||||
	// URLに#pswpを含む場合は取り除く
 | 
			
		||||
	if (location.hash === '#pswp') {
 | 
			
		||||
		history.replaceState(null, '', location.href.replace('#pswp', ''));
 | 
			
		||||
	if (window.location.hash === '#pswp') {
 | 
			
		||||
		window.history.replaceState(null, '', window.location.href.replace('#pswp', ''));
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// 一斉リロード
 | 
			
		||||
	reloadChannel.addEventListener('message', path => {
 | 
			
		||||
		if (path !== null) location.href = path;
 | 
			
		||||
		else location.reload();
 | 
			
		||||
		if (path !== null) window.location.href = path;
 | 
			
		||||
		else window.location.reload();
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	// If mobile, insert the viewport meta tag
 | 
			
		||||
	if (['smartphone', 'tablet'].includes(deviceKind)) {
 | 
			
		||||
		const viewport = document.getElementsByName('viewport').item(0);
 | 
			
		||||
		const viewport = window.document.getElementsByName('viewport').item(0);
 | 
			
		||||
		viewport.setAttribute('content',
 | 
			
		||||
			`${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region Set lang attr
 | 
			
		||||
	const html = document.documentElement;
 | 
			
		||||
	const html = window.document.documentElement;
 | 
			
		||||
	html.setAttribute('lang', lang);
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
@@ -130,11 +114,11 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#region loginId
 | 
			
		||||
	const params = new URLSearchParams(location.search);
 | 
			
		||||
	const params = new URLSearchParams(window.location.search);
 | 
			
		||||
	const loginId = params.get('loginId');
 | 
			
		||||
 | 
			
		||||
	if (loginId) {
 | 
			
		||||
		const target = getUrlWithoutLoginId(location.href);
 | 
			
		||||
		const target = getUrlWithoutLoginId(window.location.href);
 | 
			
		||||
 | 
			
		||||
		if (!$i || $i.id !== loginId) {
 | 
			
		||||
			const account = await getAccountFromId(loginId);
 | 
			
		||||
@@ -143,7 +127,7 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		history.replaceState({ misskey: 'loginId' }, '', target);
 | 
			
		||||
		window.history.replaceState({ misskey: 'loginId' }, '', target);
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
@@ -155,7 +139,7 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
		);
 | 
			
		||||
	}, { immediate: miLocalStorage.getItem('theme') == null });
 | 
			
		||||
 | 
			
		||||
	document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
 | 
			
		||||
	window.document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
 | 
			
		||||
 | 
			
		||||
	const darkTheme = prefer.model('darkTheme');
 | 
			
		||||
	const lightTheme = prefer.model('lightTheme');
 | 
			
		||||
@@ -201,20 +185,20 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
	}, { immediate: true });
 | 
			
		||||
 | 
			
		||||
	watch(prefer.r.useBlurEffectForModal, v => {
 | 
			
		||||
		document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
 | 
			
		||||
		window.document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
 | 
			
		||||
	}, { immediate: true });
 | 
			
		||||
 | 
			
		||||
	watch(prefer.r.useBlurEffect, v => {
 | 
			
		||||
		if (v) {
 | 
			
		||||
			document.documentElement.style.removeProperty('--MI-blur');
 | 
			
		||||
			window.document.documentElement.style.removeProperty('--MI-blur');
 | 
			
		||||
		} else {
 | 
			
		||||
			document.documentElement.style.setProperty('--MI-blur', 'none');
 | 
			
		||||
			window.document.documentElement.style.setProperty('--MI-blur', 'none');
 | 
			
		||||
		}
 | 
			
		||||
	}, { immediate: true });
 | 
			
		||||
 | 
			
		||||
	// Keep screen on
 | 
			
		||||
	const onVisibilityChange = () => document.addEventListener('visibilitychange', () => {
 | 
			
		||||
		if (document.visibilityState === 'visible') {
 | 
			
		||||
	const onVisibilityChange = () => window.document.addEventListener('visibilitychange', () => {
 | 
			
		||||
		if (window.document.visibilityState === 'visible') {
 | 
			
		||||
			navigator.wakeLock.request('screen');
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
@@ -224,7 +208,7 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
			.catch(() => {
 | 
			
		||||
				// On WebKit-based browsers, user activation is required to send wake lock request
 | 
			
		||||
				// https://webkit.org/blog/13862/the-user-activation-api/
 | 
			
		||||
				document.addEventListener(
 | 
			
		||||
				window.document.addEventListener(
 | 
			
		||||
					'click',
 | 
			
		||||
					() => navigator.wakeLock.request('screen').then(onVisibilityChange),
 | 
			
		||||
					{ once: true },
 | 
			
		||||
@@ -233,7 +217,7 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if (prefer.s.makeEveryTextElementsSelectable) {
 | 
			
		||||
		document.documentElement.classList.add('forceSelectableAll');
 | 
			
		||||
		window.document.documentElement.classList.add('forceSelectableAll');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	//#region Fetch user
 | 
			
		||||
@@ -278,16 +262,16 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
	const rootEl = ((): HTMLElement => {
 | 
			
		||||
		const MISSKEY_MOUNT_DIV_ID = 'misskey_app';
 | 
			
		||||
 | 
			
		||||
		const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID);
 | 
			
		||||
		const currentRoot = window.document.getElementById(MISSKEY_MOUNT_DIV_ID);
 | 
			
		||||
 | 
			
		||||
		if (currentRoot) {
 | 
			
		||||
			console.warn('multiple import detected');
 | 
			
		||||
			return currentRoot;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		const root = document.createElement('div');
 | 
			
		||||
		const root = window.document.createElement('div');
 | 
			
		||||
		root.id = MISSKEY_MOUNT_DIV_ID;
 | 
			
		||||
		document.body.appendChild(root);
 | 
			
		||||
		window.document.body.appendChild(root);
 | 
			
		||||
		return root;
 | 
			
		||||
	})();
 | 
			
		||||
 | 
			
		||||
@@ -300,24 +284,26 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
	removeSplash();
 | 
			
		||||
 | 
			
		||||
	//#region Self-XSS 対策メッセージ
 | 
			
		||||
	console.log(
 | 
			
		||||
		`%c${i18n.ts._selfXssPrevention.warning}`,
 | 
			
		||||
		'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;',
 | 
			
		||||
	);
 | 
			
		||||
	console.log(
 | 
			
		||||
		`%c${i18n.ts._selfXssPrevention.title}`,
 | 
			
		||||
		'color: #f00; font-weight: 900; font-family: "Hiragino Sans W9", "Hiragino Kaku Gothic ProN", sans-serif; font-size: 24px;',
 | 
			
		||||
	);
 | 
			
		||||
	console.log(
 | 
			
		||||
		`%c${i18n.ts._selfXssPrevention.description1}`,
 | 
			
		||||
		'font-size: 16px; font-weight: 700;',
 | 
			
		||||
	);
 | 
			
		||||
	console.log(
 | 
			
		||||
		`%c${i18n.ts._selfXssPrevention.description2}`,
 | 
			
		||||
		'font-size: 16px;',
 | 
			
		||||
		'font-size: 20px; font-weight: 700; color: #f00;',
 | 
			
		||||
	);
 | 
			
		||||
	console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' }));
 | 
			
		||||
	if (!_DEV_) {
 | 
			
		||||
		console.log(
 | 
			
		||||
			`%c${i18n.ts._selfXssPrevention.warning}`,
 | 
			
		||||
			'color: #f00; background-color: #ff0; font-size: 36px; padding: 4px;',
 | 
			
		||||
		);
 | 
			
		||||
		console.log(
 | 
			
		||||
			`%c${i18n.ts._selfXssPrevention.title}`,
 | 
			
		||||
			'color: #f00; font-weight: 900; font-family: "Hiragino Sans W9", "Hiragino Kaku Gothic ProN", sans-serif; font-size: 24px;',
 | 
			
		||||
		);
 | 
			
		||||
		console.log(
 | 
			
		||||
			`%c${i18n.ts._selfXssPrevention.description1}`,
 | 
			
		||||
			'font-size: 16px; font-weight: 700;',
 | 
			
		||||
		);
 | 
			
		||||
		console.log(
 | 
			
		||||
			`%c${i18n.ts._selfXssPrevention.description2}`,
 | 
			
		||||
			'font-size: 16px;',
 | 
			
		||||
			'font-size: 20px; font-weight: 700; color: #f00;',
 | 
			
		||||
		);
 | 
			
		||||
		console.log(i18n.tsx._selfXssPrevention.description3({ link: 'https://misskey-hub.net/docs/for-users/resources/self-xss/' }));
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
@@ -328,7 +314,7 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeSplash() {
 | 
			
		||||
	const splash = document.getElementById('splash');
 | 
			
		||||
	const splash = window.document.getElementById('splash');
 | 
			
		||||
	if (splash) {
 | 
			
		||||
		splash.style.opacity = '0';
 | 
			
		||||
		splash.style.pointerEvents = 'none';
 | 
			
		||||
 
 | 
			
		||||
@@ -43,7 +43,7 @@ export async function mainBoot() {
 | 
			
		||||
		if (!$i) uiStyle = 'visitor';
 | 
			
		||||
 | 
			
		||||
		if (searchParams.has('zen')) uiStyle = 'zen';
 | 
			
		||||
		if (uiStyle === 'deck' && prefer.s['deck.useSimpleUiForNonRootPages'] && location.pathname !== '/') uiStyle = 'zen';
 | 
			
		||||
		if (uiStyle === 'deck' && prefer.s['deck.useSimpleUiForNonRootPages'] && window.location.pathname !== '/') uiStyle = 'zen';
 | 
			
		||||
 | 
			
		||||
		if (searchParams.has('ui')) uiStyle = searchParams.get('ui');
 | 
			
		||||
 | 
			
		||||
@@ -216,7 +216,7 @@ export async function mainBoot() {
 | 
			
		||||
	let reloadDialogShowing = false;
 | 
			
		||||
	stream.on('_disconnected_', async () => {
 | 
			
		||||
		if (prefer.s.serverDisconnectedBehavior === 'reload') {
 | 
			
		||||
			location.reload();
 | 
			
		||||
			window.location.reload();
 | 
			
		||||
		} else if (prefer.s.serverDisconnectedBehavior === 'dialog') {
 | 
			
		||||
			if (reloadDialogShowing) return;
 | 
			
		||||
			reloadDialogShowing = true;
 | 
			
		||||
@@ -227,7 +227,7 @@ export async function mainBoot() {
 | 
			
		||||
			});
 | 
			
		||||
			reloadDialogShowing = false;
 | 
			
		||||
			if (!canceled) {
 | 
			
		||||
				location.reload();
 | 
			
		||||
				window.location.reload();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
@@ -398,7 +398,7 @@ export async function mainBoot() {
 | 
			
		||||
			let lastVisibilityChangedAt = Date.now();
 | 
			
		||||
 | 
			
		||||
			function claimPlainLucky() {
 | 
			
		||||
				if (document.visibilityState !== 'visible') {
 | 
			
		||||
				if (window.document.visibilityState !== 'visible') {
 | 
			
		||||
					if (justPlainLuckyTimer != null) window.clearTimeout(justPlainLuckyTimer);
 | 
			
		||||
					return;
 | 
			
		||||
				}
 | 
			
		||||
@@ -413,7 +413,7 @@ export async function mainBoot() {
 | 
			
		||||
			window.addEventListener('visibilitychange', () => {
 | 
			
		||||
				const now = Date.now();
 | 
			
		||||
 | 
			
		||||
				if (document.visibilityState === 'visible') {
 | 
			
		||||
				if (window.document.visibilityState === 'visible') {
 | 
			
		||||
					// タブを高速で切り替えたら取得処理が何度も走るのを防ぐ
 | 
			
		||||
					if ((now - lastVisibilityChangedAt) < 1000 * 10) {
 | 
			
		||||
						justPlainLuckyTimer = window.setTimeout(claimPlainLucky, 1000 * 10);
 | 
			
		||||
@@ -458,7 +458,7 @@ export async function mainBoot() {
 | 
			
		||||
 | 
			
		||||
		const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
 | 
			
		||||
		const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
 | 
			
		||||
		if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
 | 
			
		||||
		if (neverShowDonationInfo !== 'true' && (createdAt.getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !window.location.pathname.startsWith('/miauth')) {
 | 
			
		||||
			if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
 | 
			
		||||
				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {
 | 
			
		||||
					closed: () => dispose(),
 | 
			
		||||
@@ -554,7 +554,7 @@ export async function mainBoot() {
 | 
			
		||||
			mainRouter.push('/search');
 | 
			
		||||
		},
 | 
			
		||||
	} as const satisfies Keymap;
 | 
			
		||||
	document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
 | 
			
		||||
	window.document.addEventListener('keydown', makeHotkey(keymap), { passive: false });
 | 
			
		||||
 | 
			
		||||
	initializeSw();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -192,7 +192,7 @@ function tick() {
 | 
			
		||||
tick();
 | 
			
		||||
 | 
			
		||||
function calcColors() {
 | 
			
		||||
	const computedStyle = getComputedStyle(document.documentElement);
 | 
			
		||||
	const computedStyle = getComputedStyle(window.document.documentElement);
 | 
			
		||||
	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)';
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<canvas ref="canvasEl" style="width: 100%; height: 100%; pointer-events: none;"></canvas>
 | 
			
		||||
<canvas ref="canvasEl" style="display: block; width: 100%; height: 100%; pointer-events: none;"></canvas>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
 
 | 
			
		||||
@@ -359,7 +359,7 @@ onMounted(() => {
 | 
			
		||||
 | 
			
		||||
	props.textarea.addEventListener('keydown', onKeydown);
 | 
			
		||||
 | 
			
		||||
	document.body.addEventListener('mousedown', onMousedown);
 | 
			
		||||
	window.document.body.addEventListener('mousedown', onMousedown);
 | 
			
		||||
 | 
			
		||||
	nextTick(() => {
 | 
			
		||||
		exec();
 | 
			
		||||
@@ -375,7 +375,7 @@ onMounted(() => {
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
	props.textarea.removeEventListener('keydown', onKeydown);
 | 
			
		||||
 | 
			
		||||
	document.body.removeEventListener('mousedown', onMousedown);
 | 
			
		||||
	window.document.body.removeEventListener('mousedown', onMousedown);
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -92,7 +92,7 @@ function onMousedown(evt: MouseEvent): void {
 | 
			
		||||
	const target = evt.target! as HTMLElement;
 | 
			
		||||
	const rect = target.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
	const ripple = document.createElement('div');
 | 
			
		||||
	const ripple = window.document.createElement('div');
 | 
			
		||||
	ripple.classList.add(ripples.value!.dataset.childrenClass!);
 | 
			
		||||
	ripple.style.top = (evt.clientY - rect.top - 1).toString() + 'px';
 | 
			
		||||
	ripple.style.left = (evt.clientX - rect.left - 1).toString() + 'px';
 | 
			
		||||
 
 | 
			
		||||
@@ -112,7 +112,7 @@ watch(() => [props.instanceUrl, props.sitekey, props.secretKey], async () => {
 | 
			
		||||
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'), {
 | 
			
		||||
	(window.document.getElementById(scriptId.value) ?? window.document.head.appendChild(Object.assign(window.document.createElement('script'), {
 | 
			
		||||
		async: true,
 | 
			
		||||
		id: scriptId.value,
 | 
			
		||||
		src: src.value,
 | 
			
		||||
@@ -149,7 +149,7 @@ async function requestRender() {
 | 
			
		||||
	if (captcha.value.render && captchaEl.value instanceof Element && props.sitekey) {
 | 
			
		||||
		// reCAPTCHAのレンダリング重複判定を回避するため、captchaEl配下に仮のdivを用意する.
 | 
			
		||||
		// (同じdivに対して複数回renderを呼び出すとreCAPTCHAはエラーを返すので)
 | 
			
		||||
		const elem = document.createElement('div');
 | 
			
		||||
		const elem = window.document.createElement('div');
 | 
			
		||||
		captchaEl.value.appendChild(elem);
 | 
			
		||||
 | 
			
		||||
		captchaWidgetId.value = captcha.value.render(elem, {
 | 
			
		||||
@@ -174,7 +174,7 @@ async function requestRender() {
 | 
			
		||||
 | 
			
		||||
function clearWidget() {
 | 
			
		||||
	if (props.provider === 'mcaptcha') {
 | 
			
		||||
		const container = document.getElementById('mcaptcha__widget-container');
 | 
			
		||||
		const container = window.document.getElementById('mcaptcha__widget-container');
 | 
			
		||||
		if (container) {
 | 
			
		||||
			container.innerHTML = '';
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -68,11 +68,11 @@ onMounted(() => {
 | 
			
		||||
		rootEl.value.style.left = `${left}px`;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	document.body.addEventListener('mousedown', onMousedown);
 | 
			
		||||
	window.document.body.addEventListener('mousedown', onMousedown);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onBeforeUnmount(() => {
 | 
			
		||||
	document.body.removeEventListener('mousedown', onMousedown);
 | 
			
		||||
	window.document.body.removeEventListener('mousedown', onMousedown);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function onMousedown(evt: Event) {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,14 +3,12 @@
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
			
		||||
/* eslint-disable import/no-default-export */
 | 
			
		||||
import type { StoryObj } from '@storybook/vue3';
 | 
			
		||||
import { HttpResponse, http } from 'msw';
 | 
			
		||||
import { action } from '@storybook/addon-actions';
 | 
			
		||||
import { file } from '../../.storybook/fakes.js';
 | 
			
		||||
import { commonHandlers } from '../../.storybook/mocks.js';
 | 
			
		||||
import MkCropperDialog from './MkCropperDialog.vue';
 | 
			
		||||
import type { StoryObj } from '@storybook/vue3';
 | 
			
		||||
export const Default = {
 | 
			
		||||
	render(args) {
 | 
			
		||||
		return {
 | 
			
		||||
@@ -55,7 +53,7 @@ export const Default = {
 | 
			
		||||
				http.get('/proxy/image.webp', async ({ request }) => {
 | 
			
		||||
					const url = new URL(request.url).searchParams.get('url');
 | 
			
		||||
					if (url === 'https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/fedi.jpg?raw=true') {
 | 
			
		||||
						const image = await (await fetch('client-assets/fedi.jpg')).blob();
 | 
			
		||||
						const image = await (await window.fetch('client-assets/fedi.jpg')).blob();
 | 
			
		||||
						return new HttpResponse(image, {
 | 
			
		||||
							headers: {
 | 
			
		||||
								'Content-Type': 'image/jpeg',
 | 
			
		||||
 
 | 
			
		||||
@@ -122,7 +122,7 @@ onMounted(() => {
 | 
			
		||||
	cropper = new Cropper(imgEl.value!, {
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const computedStyle = getComputedStyle(document.documentElement);
 | 
			
		||||
	const computedStyle = getComputedStyle(window.document.documentElement);
 | 
			
		||||
 | 
			
		||||
	const selection = cropper.getCropperSelection()!;
 | 
			
		||||
	selection.themeColor = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 | 
			
		||||
 
 | 
			
		||||
@@ -180,7 +180,7 @@ function applyToPreview() {
 | 
			
		||||
	nextTick(() => {
 | 
			
		||||
		if (currentPreviewUrl === embedPreviewUrl.value) {
 | 
			
		||||
			// URLが変わらなくてもリロード
 | 
			
		||||
			iframeEl.value?.contentWindow?.location.reload();
 | 
			
		||||
			iframeEl.value?.contentWindow?.window.location.reload();
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,54 +11,91 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
		<!-- 拡張用? -->
 | 
			
		||||
		<i v-else class="ti ti-download"></i>
 | 
			
		||||
	</div>
 | 
			
		||||
	<h2 :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].title }}</h2>
 | 
			
		||||
	<div :class="$style.extInstallerNormDesc">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</div>
 | 
			
		||||
	<MkInfo v-if="isPlugin" :warn="true">{{ i18n.ts._plugin.installWarn }}</MkInfo>
 | 
			
		||||
	<FormSection>
 | 
			
		||||
		<template #label>{{ i18n.ts._externalResourceInstaller[`_${extension.type}`].metaTitle }}</template>
 | 
			
		||||
		<div class="_gaps_s">
 | 
			
		||||
			<FormSplit>
 | 
			
		||||
				<MkKeyValue>
 | 
			
		||||
					<template #key>{{ i18n.ts.name }}</template>
 | 
			
		||||
					<template #value>{{ extension.meta.name }}</template>
 | 
			
		||||
				</MkKeyValue>
 | 
			
		||||
				<MkKeyValue>
 | 
			
		||||
					<template #key>{{ i18n.ts.author }}</template>
 | 
			
		||||
					<template #value>{{ extension.meta.author }}</template>
 | 
			
		||||
				</MkKeyValue>
 | 
			
		||||
			</FormSplit>
 | 
			
		||||
			<MkKeyValue v-if="isPlugin">
 | 
			
		||||
				<template #key>{{ i18n.ts.description }}</template>
 | 
			
		||||
				<template #value>{{ extension.meta.description ?? i18n.ts.none }}</template>
 | 
			
		||||
			</MkKeyValue>
 | 
			
		||||
			<MkKeyValue v-if="isPlugin">
 | 
			
		||||
				<template #key>{{ i18n.ts.version }}</template>
 | 
			
		||||
				<template #value>{{ extension.meta.version }}</template>
 | 
			
		||||
			</MkKeyValue>
 | 
			
		||||
			<MkKeyValue v-if="isPlugin">
 | 
			
		||||
				<template #key>{{ i18n.ts.permission }}</template>
 | 
			
		||||
				<template #value>
 | 
			
		||||
					<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
 | 
			
		||||
						<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
 | 
			
		||||
					</ul>
 | 
			
		||||
					<template v-else>{{ i18n.ts.none }}</template>
 | 
			
		||||
				</template>
 | 
			
		||||
			</MkKeyValue>
 | 
			
		||||
			<MkKeyValue v-if="isTheme">
 | 
			
		||||
				<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
 | 
			
		||||
				<template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
 | 
			
		||||
			</MkKeyValue>
 | 
			
		||||
			<MkFolder>
 | 
			
		||||
				<template #icon><i class="ti ti-code"></i></template>
 | 
			
		||||
				<template #label>{{ i18n.ts._plugin.viewSource }}</template>
 | 
			
		||||
 | 
			
		||||
				<MkCode :code="extension.raw"/>
 | 
			
		||||
			</MkFolder>
 | 
			
		||||
		</div>
 | 
			
		||||
	</FormSection>
 | 
			
		||||
	<h2 v-if="isPlugin" :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller._plugin.title }}</h2>
 | 
			
		||||
	<h2 v-else-if="isTheme" :class="$style.extInstallerTitle">{{ i18n.ts._externalResourceInstaller._theme.title }}</h2>
 | 
			
		||||
 | 
			
		||||
	<MkInfo :warn="true">{{ i18n.ts._externalResourceInstaller.checkVendorBeforeInstall }}</MkInfo>
 | 
			
		||||
 | 
			
		||||
	<div v-if="isPlugin" class="_gaps_s">
 | 
			
		||||
		<MkFolder :defaultOpen="true">
 | 
			
		||||
			<template #icon><i class="ti ti-info-circle"></i></template>
 | 
			
		||||
			<template #label>{{ i18n.ts.metadata }}</template>
 | 
			
		||||
 | 
			
		||||
			<div class="_gaps_s">
 | 
			
		||||
				<FormSplit>
 | 
			
		||||
					<MkKeyValue>
 | 
			
		||||
						<template #key>{{ i18n.ts.name }}</template>
 | 
			
		||||
						<template #value>{{ extension.meta.name }}</template>
 | 
			
		||||
					</MkKeyValue>
 | 
			
		||||
					<MkKeyValue>
 | 
			
		||||
						<template #key>{{ i18n.ts.author }}</template>
 | 
			
		||||
						<template #value>{{ extension.meta.author }}</template>
 | 
			
		||||
					</MkKeyValue>
 | 
			
		||||
				</FormSplit>
 | 
			
		||||
				<MkKeyValue>
 | 
			
		||||
					<template #key>{{ i18n.ts.description }}</template>
 | 
			
		||||
					<template #value>{{ extension.meta.description ?? i18n.ts.none }}</template>
 | 
			
		||||
				</MkKeyValue>
 | 
			
		||||
				<MkKeyValue>
 | 
			
		||||
					<template #key>{{ i18n.ts.version }}</template>
 | 
			
		||||
					<template #value>{{ extension.meta.version }}</template>
 | 
			
		||||
				</MkKeyValue>
 | 
			
		||||
				<MkKeyValue>
 | 
			
		||||
					<template #key>{{ i18n.ts.permission }}</template>
 | 
			
		||||
					<template #value>
 | 
			
		||||
						<ul v-if="extension.meta.permissions && extension.meta.permissions.length > 0" :class="$style.extInstallerKVList">
 | 
			
		||||
							<li v-for="permission in extension.meta.permissions" :key="permission">{{ i18n.ts._permissions[permission] }}</li>
 | 
			
		||||
						</ul>
 | 
			
		||||
						<template v-else>{{ i18n.ts.none }}</template>
 | 
			
		||||
					</template>
 | 
			
		||||
				</MkKeyValue>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
 | 
			
		||||
		<MkFolder :withSpacer="false">
 | 
			
		||||
			<template #icon><i class="ti ti-code"></i></template>
 | 
			
		||||
			<template #label>{{ i18n.ts._plugin.viewSource }}</template>
 | 
			
		||||
 | 
			
		||||
			<MkCode :code="extension.raw"/>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div v-else-if="isTheme" class="_gaps_s">
 | 
			
		||||
		<MkFolder :defaultOpen="true">
 | 
			
		||||
			<template #icon><i class="ti ti-info-circle"></i></template>
 | 
			
		||||
			<template #label>{{ i18n.ts.metadata }}</template>
 | 
			
		||||
 | 
			
		||||
			<div class="_gaps_s">
 | 
			
		||||
				<FormSplit>
 | 
			
		||||
					<MkKeyValue>
 | 
			
		||||
						<template #key>{{ i18n.ts.name }}</template>
 | 
			
		||||
						<template #value>{{ extension.meta.name }}</template>
 | 
			
		||||
					</MkKeyValue>
 | 
			
		||||
					<MkKeyValue>
 | 
			
		||||
						<template #key>{{ i18n.ts.author }}</template>
 | 
			
		||||
						<template #value>{{ extension.meta.author }}</template>
 | 
			
		||||
					</MkKeyValue>
 | 
			
		||||
				</FormSplit>
 | 
			
		||||
				<MkKeyValue>
 | 
			
		||||
					<template #key>{{ i18n.ts._externalResourceInstaller._meta.base }}</template>
 | 
			
		||||
					<template #value>{{ i18n.ts[extension.meta.base ?? 'none'] }}</template>
 | 
			
		||||
				</MkKeyValue>
 | 
			
		||||
			</div>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
 | 
			
		||||
		<MkFolder :withSpacer="false">
 | 
			
		||||
			<template #icon><i class="ti ti-code"></i></template>
 | 
			
		||||
			<template #label>{{ i18n.ts._theme.code }}</template>
 | 
			
		||||
 | 
			
		||||
			<MkCode :code="extension.raw"/>
 | 
			
		||||
		</MkFolder>
 | 
			
		||||
	</div>
 | 
			
		||||
 | 
			
		||||
	<slot name="additionalInfo"/>
 | 
			
		||||
 | 
			
		||||
	<div class="_buttonsCenter">
 | 
			
		||||
		<MkButton primary @click="emits('confirm')"><i class="ti ti-check"></i> {{ i18n.ts.install }}</MkButton>
 | 
			
		||||
		<MkButton danger rounded large @click="emits('cancel')"><i class="ti ti-x"></i> {{ i18n.ts.cancel }}</MkButton>
 | 
			
		||||
		<MkButton gradate rounded large @click="emits('confirm')"><i class="ti ti-download"></i> {{ i18n.ts.install }}</MkButton>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
@@ -105,6 +142,7 @@ const props = defineProps<{
 | 
			
		||||
 | 
			
		||||
const emits = defineEmits<{
 | 
			
		||||
	(ev: 'confirm'): void;
 | 
			
		||||
	(ev: 'cancel'): void;
 | 
			
		||||
}>();
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
@@ -112,13 +150,13 @@ const emits = defineEmits<{
 | 
			
		||||
.extInstallerRoot {
 | 
			
		||||
	border-radius: var(--MI-radius);
 | 
			
		||||
	background: var(--MI_THEME-panel);
 | 
			
		||||
	padding: 1.5rem;
 | 
			
		||||
	padding: 20px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.extInstallerIconWrapper {
 | 
			
		||||
	width: 48px;
 | 
			
		||||
	height: 48px;
 | 
			
		||||
	font-size: 24px;
 | 
			
		||||
	font-size: 20px;
 | 
			
		||||
	line-height: 48px;
 | 
			
		||||
	text-align: center;
 | 
			
		||||
	border-radius: 50%;
 | 
			
		||||
@@ -135,10 +173,6 @@ const emits = defineEmits<{
 | 
			
		||||
	margin: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.extInstallerNormDesc {
 | 
			
		||||
	text-align: center;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.extInstallerKVList {
 | 
			
		||||
	margin-top: 0;
 | 
			
		||||
	margin-bottom: 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -116,7 +116,7 @@ function toggle() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	const computedStyle = getComputedStyle(document.documentElement);
 | 
			
		||||
	const computedStyle = getComputedStyle(window.document.documentElement);
 | 
			
		||||
	const parentBg = getBgColor(rootEl.value?.parentElement) ?? 'transparent';
 | 
			
		||||
	const myBg = computedStyle.getPropertyValue('--MI_THEME-panel');
 | 
			
		||||
	bgSame.value = parentBg === myBg;
 | 
			
		||||
 
 | 
			
		||||
@@ -55,7 +55,7 @@ import { extractAvgColorFromBlurhash } from '@@/js/extract-avg-color-from-blurha
 | 
			
		||||
const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resolve => {
 | 
			
		||||
	// テスト環境で Web Worker インスタンスは作成できない
 | 
			
		||||
	if (import.meta.env.MODE === 'test') {
 | 
			
		||||
		const canvas = document.createElement('canvas');
 | 
			
		||||
		const canvas = window.document.createElement('canvas');
 | 
			
		||||
		canvas.width = 64;
 | 
			
		||||
		canvas.height = 64;
 | 
			
		||||
		resolve(canvas);
 | 
			
		||||
@@ -69,13 +69,11 @@ const canvasPromise = new Promise<WorkerMultiDispatch | HTMLCanvasElement>(resol
 | 
			
		||||
				Math.min(navigator.hardwareConcurrency - 1, 4),
 | 
			
		||||
			);
 | 
			
		||||
			resolve(workers);
 | 
			
		||||
			if (_DEV_) console.log('WebGL2 in worker is supported!');
 | 
			
		||||
		} else {
 | 
			
		||||
			const canvas = document.createElement('canvas');
 | 
			
		||||
			const canvas = window.document.createElement('canvas');
 | 
			
		||||
			canvas.width = 64;
 | 
			
		||||
			canvas.height = 64;
 | 
			
		||||
			resolve(canvas);
 | 
			
		||||
			if (_DEV_) console.log('WebGL2 in worker is not supported...');
 | 
			
		||||
		}
 | 
			
		||||
		testWorker.terminate();
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -3,13 +3,12 @@
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
 | 
			
		||||
import type { StoryObj } from '@storybook/vue3';
 | 
			
		||||
import { HttpResponse, http } from 'msw';
 | 
			
		||||
import { federationInstance } from '../../.storybook/fakes.js';
 | 
			
		||||
import { commonHandlers } from '../../.storybook/mocks.js';
 | 
			
		||||
import { getChartResolver } from '../../.storybook/charts.js';
 | 
			
		||||
import MkInstanceCardMini from './MkInstanceCardMini.vue';
 | 
			
		||||
import type { StoryObj } from '@storybook/vue3';
 | 
			
		||||
 | 
			
		||||
export const Default = {
 | 
			
		||||
	render(args) {
 | 
			
		||||
@@ -48,7 +47,7 @@ export const Default = {
 | 
			
		||||
					const url = new URL(urlStr);
 | 
			
		||||
 | 
			
		||||
					if (url.href.startsWith('https://github.com/misskey-dev/misskey/blob/master/packages/frontend/assets/')) {
 | 
			
		||||
						const image = await (await fetch(`client-assets/${url.pathname.split('/').pop()}`)).blob();
 | 
			
		||||
						const image = await (await window.fetch(`client-assets/${url.pathname.split('/').pop()}`)).blob();
 | 
			
		||||
						return new HttpResponse(image, {
 | 
			
		||||
							headers: {
 | 
			
		||||
								'Content-Type': 'image/jpeg',
 | 
			
		||||
 
 | 
			
		||||
@@ -126,7 +126,7 @@ function createDoughnut(chartEl, tooltip, data) {
 | 
			
		||||
			labels: data.map(x => x.name),
 | 
			
		||||
			datasets: [{
 | 
			
		||||
				backgroundColor: data.map(x => x.color),
 | 
			
		||||
				borderColor: getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel'),
 | 
			
		||||
				borderColor: getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel'),
 | 
			
		||||
				borderWidth: 2,
 | 
			
		||||
				hoverOffset: 0,
 | 
			
		||||
				data: data.map(x => x.value),
 | 
			
		||||
 
 | 
			
		||||
@@ -148,7 +148,7 @@ const keymap = {
 | 
			
		||||
// PlayerElもしくはその子要素にフォーカスがあるかどうか
 | 
			
		||||
function hasFocus() {
 | 
			
		||||
	if (!playerEl.value) return false;
 | 
			
		||||
	return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
 | 
			
		||||
	return playerEl.value === window.document.activeElement || playerEl.value.contains(window.document.activeElement);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const playerEl = useTemplateRef('playerEl');
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ const props = defineProps<{
 | 
			
		||||
 | 
			
		||||
const gallery = useTemplateRef('gallery');
 | 
			
		||||
const pswpZIndex = os.claimZIndex('middle');
 | 
			
		||||
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
 | 
			
		||||
window.document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
 | 
			
		||||
const count = computed(() => props.mediaList.filter(media => previewable(media)).length);
 | 
			
		||||
let lightbox: PhotoSwipeLightbox | null = null;
 | 
			
		||||
 | 
			
		||||
@@ -166,7 +166,7 @@ onMounted(() => {
 | 
			
		||||
			className: 'pswp__alt-text-container',
 | 
			
		||||
			appendTo: 'wrapper',
 | 
			
		||||
			onInit: (el, pswp) => {
 | 
			
		||||
				const textBox = document.createElement('p');
 | 
			
		||||
				const textBox = window.document.createElement('p');
 | 
			
		||||
				textBox.className = 'pswp__alt-text _acrylic';
 | 
			
		||||
				el.appendChild(textBox);
 | 
			
		||||
 | 
			
		||||
@@ -178,19 +178,19 @@ onMounted(() => {
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	lightbox.on('afterInit', () => {
 | 
			
		||||
		activeEl = document.activeElement instanceof HTMLElement ? document.activeElement : null;
 | 
			
		||||
		activeEl = window.document.activeElement instanceof HTMLElement ? window.document.activeElement : null;
 | 
			
		||||
		focusParent(activeEl, true, true);
 | 
			
		||||
		lightbox?.pswp?.element?.focus({
 | 
			
		||||
			preventScroll: true,
 | 
			
		||||
		});
 | 
			
		||||
		history.pushState(null, '', '#pswp');
 | 
			
		||||
		window.history.pushState(null, '', '#pswp');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	lightbox.on('destroy', () => {
 | 
			
		||||
		focusParent(activeEl, true, false);
 | 
			
		||||
		activeEl = null;
 | 
			
		||||
		if (window.location.hash === '#pswp') {
 | 
			
		||||
			history.back();
 | 
			
		||||
			window.history.back();
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -171,7 +171,7 @@ const keymap = {
 | 
			
		||||
// PlayerElもしくはその子要素にフォーカスがあるかどうか
 | 
			
		||||
function hasFocus() {
 | 
			
		||||
	if (!playerEl.value) return false;
 | 
			
		||||
	return playerEl.value === document.activeElement || playerEl.value.contains(document.activeElement);
 | 
			
		||||
	return playerEl.value === window.document.activeElement || playerEl.value.contains(window.document.activeElement);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// eslint-disable-next-line vue/no-setup-props-reactivity-loss
 | 
			
		||||
@@ -216,7 +216,7 @@ function showMenu(ev: MouseEvent) {
 | 
			
		||||
				'2.0x': 2,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		...(document.pictureInPictureEnabled ? [{
 | 
			
		||||
		...(window.document.pictureInPictureEnabled ? [{
 | 
			
		||||
			text: i18n.ts._mediaControls.pip,
 | 
			
		||||
			icon: 'ti ti-picture-in-picture',
 | 
			
		||||
			action: togglePictureInPicture,
 | 
			
		||||
@@ -384,8 +384,8 @@ function toggleFullscreen() {
 | 
			
		||||
 | 
			
		||||
function togglePictureInPicture() {
 | 
			
		||||
	if (videoEl.value) {
 | 
			
		||||
		if (document.pictureInPictureElement) {
 | 
			
		||||
			document.exitPictureInPicture();
 | 
			
		||||
		if (window.document.pictureInPictureElement) {
 | 
			
		||||
			window.document.exitPictureInPicture();
 | 
			
		||||
		} else {
 | 
			
		||||
			videoEl.value.requestPictureInPicture();
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts">
 | 
			
		||||
import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, unref, watch } from 'vue';
 | 
			
		||||
import { computed, defineAsyncComponent, inject, nextTick, onBeforeUnmount, onMounted, ref, useTemplateRef, unref, watch, shallowRef } from 'vue';
 | 
			
		||||
import type { MenuItem, InnerMenuItem, MenuPending, MenuAction, MenuSwitch, MenuRadio, MenuRadioOption, MenuParent } from '@/types/menu.js';
 | 
			
		||||
import type { Keymap } from '@/utility/hotkey.js';
 | 
			
		||||
import MkSwitchButton from '@/components/MkSwitch.button.vue';
 | 
			
		||||
@@ -257,7 +257,7 @@ watch(() => props.items, () => {
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const childMenu = ref<MenuItem[] | null>();
 | 
			
		||||
const childTarget = useTemplateRef('childTarget');
 | 
			
		||||
const childTarget = shallowRef<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
function closeChild() {
 | 
			
		||||
	childMenu.value = null;
 | 
			
		||||
@@ -358,10 +358,10 @@ function switchItem(item: MenuSwitch & { ref: any }) {
 | 
			
		||||
 | 
			
		||||
function focusUp() {
 | 
			
		||||
	if (disposed) return;
 | 
			
		||||
	if (!itemsEl.value?.contains(document.activeElement)) return;
 | 
			
		||||
	if (!itemsEl.value?.contains(window.document.activeElement)) return;
 | 
			
		||||
 | 
			
		||||
	const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
 | 
			
		||||
	const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
 | 
			
		||||
	const activeIndex = focusableElements.findIndex(el => el === window.document.activeElement);
 | 
			
		||||
	const targetIndex = (activeIndex !== -1 && activeIndex !== 0) ? (activeIndex - 1) : (focusableElements.length - 1);
 | 
			
		||||
	const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
 | 
			
		||||
 | 
			
		||||
@@ -370,10 +370,10 @@ function focusUp() {
 | 
			
		||||
 | 
			
		||||
function focusDown() {
 | 
			
		||||
	if (disposed) return;
 | 
			
		||||
	if (!itemsEl.value?.contains(document.activeElement)) return;
 | 
			
		||||
	if (!itemsEl.value?.contains(window.document.activeElement)) return;
 | 
			
		||||
 | 
			
		||||
	const focusableElements = Array.from(itemsEl.value.children).filter(isFocusable);
 | 
			
		||||
	const activeIndex = focusableElements.findIndex(el => el === document.activeElement);
 | 
			
		||||
	const activeIndex = focusableElements.findIndex(el => el === window.document.activeElement);
 | 
			
		||||
	const targetIndex = (activeIndex !== -1 && activeIndex !== (focusableElements.length - 1)) ? (activeIndex + 1) : 0;
 | 
			
		||||
	const targetElement = focusableElements.at(targetIndex) ?? itemsEl.value;
 | 
			
		||||
 | 
			
		||||
@@ -400,9 +400,9 @@ const onGlobalMousedown = (ev: MouseEvent) => {
 | 
			
		||||
 | 
			
		||||
const setupHandlers = () => {
 | 
			
		||||
	if (!isNestingMenu) {
 | 
			
		||||
		document.addEventListener('focusin', onGlobalFocusin, { passive: true });
 | 
			
		||||
		window.document.addEventListener('focusin', onGlobalFocusin, { passive: true });
 | 
			
		||||
	}
 | 
			
		||||
	document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
 | 
			
		||||
	window.document.addEventListener('mousedown', onGlobalMousedown, { passive: true });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
let disposed = false;
 | 
			
		||||
@@ -410,9 +410,9 @@ let disposed = false;
 | 
			
		||||
const disposeHandlers = () => {
 | 
			
		||||
	disposed = true;
 | 
			
		||||
	if (!isNestingMenu) {
 | 
			
		||||
		document.removeEventListener('focusin', onGlobalFocusin);
 | 
			
		||||
		window.document.removeEventListener('focusin', onGlobalFocusin);
 | 
			
		||||
	}
 | 
			
		||||
	document.removeEventListener('mousedown', onGlobalMousedown);
 | 
			
		||||
	window.document.removeEventListener('mousedown', onGlobalMousedown);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ const polygonPoints = ref('');
 | 
			
		||||
const headX = ref<number | null>(null);
 | 
			
		||||
const headY = ref<number | null>(null);
 | 
			
		||||
const clock = ref<number | null>(null);
 | 
			
		||||
const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
 | 
			
		||||
const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
 | 
			
		||||
const color = accent.toRgbString();
 | 
			
		||||
 | 
			
		||||
function draw(): void {
 | 
			
		||||
 
 | 
			
		||||
@@ -59,7 +59,7 @@ const pagination = computed(() => prefer.r.useGroupedNotifications.value ? {
 | 
			
		||||
 | 
			
		||||
function onNotification(notification) {
 | 
			
		||||
	const isMuted = props.excludeTypes ? props.excludeTypes.includes(notification.type) : false;
 | 
			
		||||
	if (isMuted || document.visibilityState === 'visible') {
 | 
			
		||||
	if (isMuted || window.document.visibilityState === 'visible') {
 | 
			
		||||
		useStream().send('readNotification');
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								packages/frontend/src/components/MkPageWithAnimBg.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								packages/frontend/src/components/MkPageWithAnimBg.vue
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
			
		||||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div>
 | 
			
		||||
	<MkAnimBg style="position: absolute;"/>
 | 
			
		||||
	<div class="_pageScrollable" style="position: absolute; top: 0; width: 100%; height: 100%;">
 | 
			
		||||
		<slot></slot>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import MkAnimBg from '@/components/MkAnimBg.vue';
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
 | 
			
		||||
</style>
 | 
			
		||||
@@ -142,7 +142,7 @@ const {
 | 
			
		||||
} = prefer.r;
 | 
			
		||||
 | 
			
		||||
const contentEl = computed(() => props.pagination.pageEl ?? rootEl.value);
 | 
			
		||||
const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : document.body);
 | 
			
		||||
const scrollableElement = computed(() => contentEl.value ? getScrollContainer(contentEl.value) : window.document.body);
 | 
			
		||||
 | 
			
		||||
const visibility = useDocumentVisibility();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -151,9 +151,9 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
 | 
			
		||||
		closed: () => dispose(),
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const style = document.createElement('style');
 | 
			
		||||
	style.appendChild(document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
 | 
			
		||||
	document.head.appendChild(style);
 | 
			
		||||
	const style = window.document.createElement('style');
 | 
			
		||||
	style.appendChild(window.document.createTextNode('* { cursor: grabbing !important; } body * { pointer-events: none !important; }'));
 | 
			
		||||
	window.document.head.appendChild(style);
 | 
			
		||||
 | 
			
		||||
	const thumbWidth = getThumbWidth();
 | 
			
		||||
 | 
			
		||||
@@ -172,7 +172,7 @@ function onMousedown(ev: MouseEvent | TouchEvent) {
 | 
			
		||||
	let beforeValue = finalValue.value;
 | 
			
		||||
 | 
			
		||||
	const onMouseup = () => {
 | 
			
		||||
		document.head.removeChild(style);
 | 
			
		||||
		window.document.head.removeChild(style);
 | 
			
		||||
		tooltipForDragShowing.value = false;
 | 
			
		||||
		window.removeEventListener('mousemove', onDrag);
 | 
			
		||||
		window.removeEventListener('touchmove', onDrag);
 | 
			
		||||
 
 | 
			
		||||
@@ -136,7 +136,7 @@ async function menu(ev) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function anime() {
 | 
			
		||||
	if (document.hidden || !prefer.s.animation || buttonEl.value == null) return;
 | 
			
		||||
	if (window.document.hidden || !prefer.s.animation || buttonEl.value == null) return;
 | 
			
		||||
 | 
			
		||||
	const rect = buttonEl.value.getBoundingClientRect();
 | 
			
		||||
	const x = rect.left + 16;
 | 
			
		||||
 
 | 
			
		||||
@@ -44,7 +44,7 @@ onMounted(async () => {
 | 
			
		||||
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	const accent = tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-accent'));
 | 
			
		||||
	const accent = tinycolor(getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-accent'));
 | 
			
		||||
	const color = accent.toHex();
 | 
			
		||||
 | 
			
		||||
	if (chartEl.value == null) return;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
	:width="400"
 | 
			
		||||
	:height="500"
 | 
			
		||||
	@close="onCloseModalWindow"
 | 
			
		||||
	@closed="console.log('MkRoleSelectDialog: closed') ; $emit('dispose')"
 | 
			
		||||
	@closed="emit('closed')"
 | 
			
		||||
>
 | 
			
		||||
	<template #header>{{ title }}</template>
 | 
			
		||||
	<MkSpacer :marginMin="20" :marginMax="28">
 | 
			
		||||
@@ -58,7 +58,7 @@ import MkLoading from '@/components/global/MkLoading.vue';
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'done', value: Misskey.entities.Role[]),
 | 
			
		||||
	(ev: 'close'),
 | 
			
		||||
	(ev: 'dispose'),
 | 
			
		||||
	(ev: 'closed'),
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
 
 | 
			
		||||
@@ -267,7 +267,7 @@ async function onSubmit(): Promise<void> {
 | 
			
		||||
		'testcaptcha-response': testcaptchaResponse.value,
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	const res = await fetch(`${config.apiUrl}/signup`, {
 | 
			
		||||
	const res = await window.fetch(`${config.apiUrl}/signup`, {
 | 
			
		||||
		method: 'POST',
 | 
			
		||||
		headers: {
 | 
			
		||||
			'Content-Type': 'application/json',
 | 
			
		||||
 
 | 
			
		||||
@@ -20,7 +20,7 @@ import tinycolor from 'tinycolor2';
 | 
			
		||||
 | 
			
		||||
const loaded = !!window.TagCanvas;
 | 
			
		||||
const SAFE_FOR_HTML_ID = 'abcdefghijklmnopqrstuvwxyz';
 | 
			
		||||
const computedStyle = getComputedStyle(document.documentElement);
 | 
			
		||||
const computedStyle = getComputedStyle(window.document.documentElement);
 | 
			
		||||
const idForCanvas = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
 | 
			
		||||
const idForTags = Array.from({ length: 16 }, () => SAFE_FOR_HTML_ID[Math.floor(Math.random() * SAFE_FOR_HTML_ID.length)]).join('');
 | 
			
		||||
const available = ref(false);
 | 
			
		||||
@@ -57,7 +57,7 @@ onMounted(() => {
 | 
			
		||||
	if (loaded) {
 | 
			
		||||
		available.value = true;
 | 
			
		||||
	} else {
 | 
			
		||||
		document.head.appendChild(Object.assign(document.createElement('script'), {
 | 
			
		||||
		window.document.head.appendChild(Object.assign(window.document.createElement('script'), {
 | 
			
		||||
			async: true,
 | 
			
		||||
			src: '/client-assets/tagcanvas.min.js',
 | 
			
		||||
		})).addEventListener('load', () => available.value = true);
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,7 @@ async function renderChart() {
 | 
			
		||||
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	const computedStyle = getComputedStyle(document.documentElement);
 | 
			
		||||
	const computedStyle = getComputedStyle(window.document.documentElement);
 | 
			
		||||
	const accent = tinycolor(computedStyle.getPropertyValue('--MI_THEME-accent')).toHexString();
 | 
			
		||||
 | 
			
		||||
	const colorRead = accent;
 | 
			
		||||
 
 | 
			
		||||
@@ -240,7 +240,7 @@ function onHeaderMousedown(evt: MouseEvent | TouchEvent) {
 | 
			
		||||
	const main = rootEl.value;
 | 
			
		||||
	if (main == null) return;
 | 
			
		||||
 | 
			
		||||
	if (!contains(main, document.activeElement)) main.focus();
 | 
			
		||||
	if (!contains(main, window.document.activeElement)) main.focus();
 | 
			
		||||
 | 
			
		||||
	const position = main.getBoundingClientRect();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -87,7 +87,7 @@ function openWindow() {
 | 
			
		||||
 | 
			
		||||
function nav(ev: MouseEvent) {
 | 
			
		||||
	if (behavior === 'browser') {
 | 
			
		||||
		location.href = props.to;
 | 
			
		||||
		window.location.href = props.to;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -170,7 +170,7 @@ onMounted(() => {
 | 
			
		||||
 | 
			
		||||
	if (props.rootEl) {
 | 
			
		||||
		ro2 = new ResizeObserver((entries, observer) => {
 | 
			
		||||
			if (document.body.contains(el.value as HTMLElement)) {
 | 
			
		||||
			if (window.document.body.contains(el.value as HTMLElement)) {
 | 
			
		||||
				nextTick(() => renderTab());
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
 
 | 
			
		||||
@@ -69,6 +69,8 @@ const emit = defineEmits<{
 | 
			
		||||
	(ev: 'update:tab', key: string);
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const viewId = inject(DI.viewId);
 | 
			
		||||
const viewTransitionName = computed(() => `${viewId}---pageHeader`);
 | 
			
		||||
const injectedPageMetadata = inject(DI.pageMetadata);
 | 
			
		||||
const pageMetadata = computed(() => props.overridePageMetadata ?? injectedPageMetadata.value);
 | 
			
		||||
 | 
			
		||||
@@ -106,7 +108,7 @@ function onTabClick(): void {
 | 
			
		||||
 | 
			
		||||
const calcBg = () => {
 | 
			
		||||
	const rawBg = 'var(--MI_THEME-bg)';
 | 
			
		||||
	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
 | 
			
		||||
	const tinyBg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(window.document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
 | 
			
		||||
	tinyBg.setAlpha(0.85);
 | 
			
		||||
	bg.value = tinyBg.toRgbString();
 | 
			
		||||
};
 | 
			
		||||
@@ -120,7 +122,7 @@ onMounted(() => {
 | 
			
		||||
	if (el.value && el.value.parentElement) {
 | 
			
		||||
		narrow.value = el.value.parentElement.offsetWidth < 500;
 | 
			
		||||
		ro = new ResizeObserver((entries, observer) => {
 | 
			
		||||
			if (el.value && el.value.parentElement && document.body.contains(el.value as HTMLElement)) {
 | 
			
		||||
			if (el.value && el.value.parentElement && window.document.body.contains(el.value as HTMLElement)) {
 | 
			
		||||
				narrow.value = el.value.parentElement.offsetWidth < 500;
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
@@ -140,6 +142,7 @@ onUnmounted(() => {
 | 
			
		||||
	backdrop-filter: var(--MI-blur, blur(15px));
 | 
			
		||||
	border-bottom: solid 0.5px var(--MI_THEME-divider);
 | 
			
		||||
	width: 100%;
 | 
			
		||||
	view-transition-name: v-bind(viewTransitionName);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.upper,
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div class="_pageContainer" style="height: 100%;">
 | 
			
		||||
<div ref="rootEl" class="_pageContainer" :class="$style.root">
 | 
			
		||||
	<KeepAlive :max="prefer.s.numberOfPageCache">
 | 
			
		||||
		<Suspense :timeout="0">
 | 
			
		||||
			<component :is="currentPageComponent" :key="key" v-bind="Object.fromEntries(currentPageProps)"/>
 | 
			
		||||
@@ -18,11 +18,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { inject, provide, ref, shallowRef } from 'vue';
 | 
			
		||||
import { inject, nextTick, onMounted, provide, ref, shallowRef, useTemplateRef } from 'vue';
 | 
			
		||||
import type { Router } from '@/router.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import MkLoadingPage from '@/pages/_loading_.vue';
 | 
			
		||||
import { DI } from '@/di.js';
 | 
			
		||||
import { randomId } from '@/utility/random-id.js';
 | 
			
		||||
import { deepEqual } from '@/utility/deep-equal.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	router?: Router;
 | 
			
		||||
@@ -34,18 +36,76 @@ if (router == null) {
 | 
			
		||||
	throw new Error('no router provided');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const viewId = randomId();
 | 
			
		||||
provide(DI.viewId, viewId);
 | 
			
		||||
 | 
			
		||||
const currentDepth = inject(DI.routerCurrentDepth, 0);
 | 
			
		||||
provide(DI.routerCurrentDepth, currentDepth + 1);
 | 
			
		||||
 | 
			
		||||
const rootEl = useTemplateRef('rootEl');
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	rootEl.value.style.viewTransitionName = viewId; // view-transition-nameにcss varが使えないっぽいため直接代入
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
// view-transition-newなどの<pt-name-selector>にはcss varが使えず、v-bindできないため直接スタイルを生成
 | 
			
		||||
const viewTransitionStylesTag = window.document.createElement('style');
 | 
			
		||||
viewTransitionStylesTag.textContent = `
 | 
			
		||||
@keyframes ${viewId}-old {
 | 
			
		||||
	to { transform: scale(0.95); opacity: 0; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes ${viewId}-new {
 | 
			
		||||
	from { transform: scale(0.95); opacity: 0; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::view-transition-old(${viewId}) {
 | 
			
		||||
	animation-duration: 0.2s;
 | 
			
		||||
  animation-name: ${viewId}-old;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
::view-transition-new(${viewId}) {
 | 
			
		||||
	animation-duration: 0.2s;
 | 
			
		||||
  animation-name: ${viewId}-new;
 | 
			
		||||
}
 | 
			
		||||
`;
 | 
			
		||||
 | 
			
		||||
window.document.head.appendChild(viewTransitionStylesTag);
 | 
			
		||||
 | 
			
		||||
const current = router.current!;
 | 
			
		||||
const currentPageComponent = shallowRef('component' in current.route ? current.route.component : MkLoadingPage);
 | 
			
		||||
const currentPageProps = ref(current.props);
 | 
			
		||||
let currentRoutePath = current.route.path;
 | 
			
		||||
const key = ref(router.getCurrentFullPath());
 | 
			
		||||
 | 
			
		||||
router.useListener('change', ({ resolved }) => {
 | 
			
		||||
	if (resolved == null || 'redirect' in resolved.route) return;
 | 
			
		||||
	currentPageComponent.value = resolved.route.component;
 | 
			
		||||
	currentPageProps.value = resolved.props;
 | 
			
		||||
	key.value = router.getCurrentFullPath();
 | 
			
		||||
	if (resolved.route.path === currentRoutePath && deepEqual(resolved.props, currentPageProps.value)) return;
 | 
			
		||||
 | 
			
		||||
	function _() {
 | 
			
		||||
		currentPageComponent.value = resolved.route.component;
 | 
			
		||||
		currentPageProps.value = resolved.props;
 | 
			
		||||
		key.value = router.getCurrentFullPath();
 | 
			
		||||
		currentRoutePath = resolved.route.path;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 | 
			
		||||
	if (prefer.s.animation && window.document.startViewTransition) {
 | 
			
		||||
		window.document.startViewTransition(() => new Promise((res) => {
 | 
			
		||||
			_();
 | 
			
		||||
			nextTick(() => {
 | 
			
		||||
				res();
 | 
			
		||||
				//setTimeout(res, 100);
 | 
			
		||||
			});
 | 
			
		||||
		}));
 | 
			
		||||
	} else {
 | 
			
		||||
		_();
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.root {
 | 
			
		||||
	height: 100%;
 | 
			
		||||
	background-color: var(--MI_THEME-bg);
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ const highlighted = ref(props.markerId === searchMarkerId.value);
 | 
			
		||||
 | 
			
		||||
function checkChildren() {
 | 
			
		||||
	if (props.children?.includes(searchMarkerId.value)) {
 | 
			
		||||
		const el = document.querySelector(`[data-in-app-search-marker-id="${searchMarkerId.value}"]`);
 | 
			
		||||
		const el = window.document.querySelector(`[data-in-app-search-marker-id="${searchMarkerId.value}"]`);
 | 
			
		||||
		highlighted.value = el == null;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,4 +11,5 @@ export const DI = {
 | 
			
		||||
	router: Symbol() as InjectionKey<Router>,
 | 
			
		||||
	mock: Symbol() as InjectionKey<boolean>,
 | 
			
		||||
	pageMetadata: Symbol() as InjectionKey<Ref<Record<string, any>>>,
 | 
			
		||||
	viewId: Symbol() as InjectionKey<string>,
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ export default {
 | 
			
		||||
		el._keyHandler = makeHotkey(binding.value);
 | 
			
		||||
 | 
			
		||||
		if (el._hotkey_global) {
 | 
			
		||||
			document.addEventListener('keydown', el._keyHandler, { passive: false });
 | 
			
		||||
			window.document.addEventListener('keydown', el._keyHandler, { passive: false });
 | 
			
		||||
		} else {
 | 
			
		||||
			el.addEventListener('keydown', el._keyHandler, { passive: false });
 | 
			
		||||
		}
 | 
			
		||||
@@ -21,7 +21,7 @@ export default {
 | 
			
		||||
 | 
			
		||||
	unmounted(el) {
 | 
			
		||||
		if (el._hotkey_global) {
 | 
			
		||||
			document.removeEventListener('keydown', el._keyHandler);
 | 
			
		||||
			window.document.removeEventListener('keydown', el._keyHandler);
 | 
			
		||||
		} else {
 | 
			
		||||
			el.removeEventListener('keydown', el._keyHandler);
 | 
			
		||||
		}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ export default {
 | 
			
		||||
	mounted(src, binding, vn) {
 | 
			
		||||
		const parentBg = getBgColor(src.parentElement) ?? 'transparent';
 | 
			
		||||
 | 
			
		||||
		const myBg = getComputedStyle(document.documentElement).getPropertyValue('--MI_THEME-panel');
 | 
			
		||||
		const myBg = getComputedStyle(window.document.documentElement).getPropertyValue('--MI_THEME-panel');
 | 
			
		||||
 | 
			
		||||
		if (parentBg === myBg) {
 | 
			
		||||
			src.style.backgroundColor = 'var(--MI_THEME-bg)';
 | 
			
		||||
 
 | 
			
		||||
@@ -47,7 +47,7 @@ export default {
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		self.show = () => {
 | 
			
		||||
			if (!document.body.contains(el)) return;
 | 
			
		||||
			if (!window.document.body.contains(el)) return;
 | 
			
		||||
			if (self._close) return;
 | 
			
		||||
			if (self.text == null) return;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ export class UserPreview {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	private show() {
 | 
			
		||||
		if (!document.body.contains(this.el)) return;
 | 
			
		||||
		if (!window.document.body.contains(this.el)) return;
 | 
			
		||||
		if (this.promise) return;
 | 
			
		||||
 | 
			
		||||
		const showing = ref(true);
 | 
			
		||||
@@ -58,7 +58,7 @@ export class UserPreview {
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		this.checkTimer = window.setInterval(() => {
 | 
			
		||||
			if (!document.body.contains(this.el)) {
 | 
			
		||||
			if (!window.document.body.contains(this.el)) {
 | 
			
		||||
				window.clearTimeout(this.showTimer);
 | 
			
		||||
				window.clearTimeout(this.hideTimer);
 | 
			
		||||
				this.close();
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import { DEFAULT_INFO_IMAGE_URL, DEFAULT_NOT_FOUND_IMAGE_URL, DEFAULT_SERVER_ERR
 | 
			
		||||
// TODO: 他のタブと永続化されたstateを同期
 | 
			
		||||
 | 
			
		||||
//#region loader
 | 
			
		||||
const providedMetaEl = document.getElementById('misskey_meta');
 | 
			
		||||
const providedMetaEl = window.document.getElementById('misskey_meta');
 | 
			
		||||
 | 
			
		||||
let cachedMeta = miLocalStorage.getItem('instance') ? JSON.parse(miLocalStorage.getItem('instance')!) : null;
 | 
			
		||||
let cachedAt = miLocalStorage.getItem('instanceCachedAt') ? parseInt(miLocalStorage.getItem('instanceCachedAt')!) : 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -320,7 +320,7 @@ export class Nirax<DEF extends RouteDef[]> extends EventEmitter<RouterEvents> {
 | 
			
		||||
		}
 | 
			
		||||
		const res = this.navigate(fullPath);
 | 
			
		||||
		if (res.route.path === '/:(*)') {
 | 
			
		||||
			location.href = fullPath;
 | 
			
		||||
			window.location.href = fullPath;
 | 
			
		||||
		} else {
 | 
			
		||||
			this.emit('push', {
 | 
			
		||||
				beforeFullPath,
 | 
			
		||||
 
 | 
			
		||||
@@ -23,8 +23,8 @@ export type Keys = (
 | 
			
		||||
	'fontSize' |
 | 
			
		||||
	'ui' |
 | 
			
		||||
	'ui_temp' |
 | 
			
		||||
	'locale' |
 | 
			
		||||
	'localeVersion' |
 | 
			
		||||
	'locale' | // DEPRECATED
 | 
			
		||||
	'localeVersion' | // DEPRECATED
 | 
			
		||||
	'theme' |
 | 
			
		||||
	'themeId' |
 | 
			
		||||
	'customCss' |
 | 
			
		||||
 
 | 
			
		||||
@@ -167,7 +167,7 @@ export const navbarItemDef = reactive({
 | 
			
		||||
		title: i18n.ts.reload,
 | 
			
		||||
		icon: 'ti ti-refresh',
 | 
			
		||||
		action: (ev) => {
 | 
			
		||||
			location.reload();
 | 
			
		||||
			window.location.reload();
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
	profile: {
 | 
			
		||||
 
 | 
			
		||||
@@ -21,10 +21,10 @@ import MkWaitingDialog from '@/components/MkWaitingDialog.vue';
 | 
			
		||||
import MkPageWindow from '@/components/MkPageWindow.vue';
 | 
			
		||||
import MkToast from '@/components/MkToast.vue';
 | 
			
		||||
import MkDialog from '@/components/MkDialog.vue';
 | 
			
		||||
import MkPasswordDialog from '@/components/MkPasswordDialog.vue';
 | 
			
		||||
import MkEmojiPickerDialog from '@/components/MkEmojiPickerDialog.vue';
 | 
			
		||||
import MkPopupMenu from '@/components/MkPopupMenu.vue';
 | 
			
		||||
import MkContextMenu from '@/components/MkContextMenu.vue';
 | 
			
		||||
import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue';
 | 
			
		||||
import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue';
 | 
			
		||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 | 
			
		||||
import { pleaseLogin } from '@/utility/please-login.js';
 | 
			
		||||
import { showMovedDialog } from '@/utility/show-moved-dialog.js';
 | 
			
		||||
@@ -181,7 +181,7 @@ type EmitsExtractor<T> = {
 | 
			
		||||
export function popup<T extends Component>(
 | 
			
		||||
	component: T,
 | 
			
		||||
	props: ComponentProps<T>,
 | 
			
		||||
	events: ComponentEmit<T> = {} as ComponentEmit<T>,
 | 
			
		||||
	events: Partial<ComponentEmit<T>> = {},
 | 
			
		||||
): { dispose: () => void } {
 | 
			
		||||
	markRaw(component);
 | 
			
		||||
 | 
			
		||||
@@ -460,7 +460,7 @@ export function authenticateDialog(): Promise<{
 | 
			
		||||
	canceled: false; result: { password: string; token: string | null; };
 | 
			
		||||
}> {
 | 
			
		||||
	return new Promise(resolve => {
 | 
			
		||||
		const { dispose } = popup(MkPasswordDialog, {}, {
 | 
			
		||||
		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkPasswordDialog.vue')), {}, {
 | 
			
		||||
			done: result => {
 | 
			
		||||
				resolve(result ? { canceled: false, result } : { canceled: true, result: undefined });
 | 
			
		||||
			},
 | 
			
		||||
@@ -617,30 +617,26 @@ export async function selectDriveFolder(multiple: boolean): Promise<Misskey.enti
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function selectRole(params: {
 | 
			
		||||
	initialRoleIds?: string[],
 | 
			
		||||
	title?: string,
 | 
			
		||||
	infoMessage?: string,
 | 
			
		||||
	publicOnly?: boolean,
 | 
			
		||||
}): Promise<
 | 
			
		||||
export async function selectRole(params: ComponentProps<typeof MkRoleSelectDialog_TypeReferenceOnly>): Promise<
 | 
			
		||||
	{ canceled: true; result: undefined; } |
 | 
			
		||||
	{ canceled: false; result: Misskey.entities.Role[] }
 | 
			
		||||
	> {
 | 
			
		||||
> {
 | 
			
		||||
	return new Promise((resolve) => {
 | 
			
		||||
		popup(defineAsyncComponent(() => import('@/components/MkRoleSelectDialog.vue')), params, {
 | 
			
		||||
		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkRoleSelectDialog.vue')), params, {
 | 
			
		||||
			done: roles => {
 | 
			
		||||
				resolve({ canceled: false, result: roles });
 | 
			
		||||
			},
 | 
			
		||||
			close: () => {
 | 
			
		||||
				resolve({ canceled: true, result: undefined });
 | 
			
		||||
			},
 | 
			
		||||
		}, 'dispose');
 | 
			
		||||
			closed: () => dispose(),
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog>): Promise<string> {
 | 
			
		||||
export async function pickEmoji(src: HTMLElement, opts: ComponentProps<typeof MkEmojiPickerDialog_TypeReferenceOnly>): Promise<string> {
 | 
			
		||||
	return new Promise(resolve => {
 | 
			
		||||
		const { dispose } = popup(MkEmojiPickerDialog, {
 | 
			
		||||
		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
 | 
			
		||||
			src,
 | 
			
		||||
			...opts,
 | 
			
		||||
		}, {
 | 
			
		||||
@@ -675,7 +671,11 @@ export function popupMenu(items: MenuItem[], src?: HTMLElement | EventTarget | n
 | 
			
		||||
	width?: number;
 | 
			
		||||
	onClosing?: () => void;
 | 
			
		||||
}): Promise<void> {
 | 
			
		||||
	let returnFocusTo = getHTMLElementOrNull(src) ?? getHTMLElementOrNull(document.activeElement);
 | 
			
		||||
	if (!(src instanceof HTMLElement)) {
 | 
			
		||||
		src = null;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let returnFocusTo = getHTMLElementOrNull(src) ?? getHTMLElementOrNull(window.document.activeElement);
 | 
			
		||||
	return new Promise(resolve => nextTick(() => {
 | 
			
		||||
		const { dispose } = popup(MkPopupMenu, {
 | 
			
		||||
			items,
 | 
			
		||||
@@ -704,7 +704,7 @@ export function contextMenu(items: MenuItem[], ev: MouseEvent): Promise<void> {
 | 
			
		||||
		return Promise.resolve();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(document.activeElement);
 | 
			
		||||
	let returnFocusTo = getHTMLElementOrNull(ev.currentTarget ?? ev.target) ?? getHTMLElementOrNull(window.document.activeElement);
 | 
			
		||||
	ev.preventDefault();
 | 
			
		||||
	return new Promise(resolve => nextTick(() => {
 | 
			
		||||
		const { dispose } = popup(MkContextMenu, {
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user