Compare commits
	
		
			110 Commits
		
	
	
		
			2025.3.0-b
			...
			2025.3.2-a
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					bcb891e4fd | ||
| 
						 | 
					152660fcf2 | ||
| 
						 | 
					a1204f5e3e | ||
| 
						 | 
					7acd3d1a88 | ||
| 
						 | 
					8c9ec5827f | ||
| 
						 | 
					44073736de | ||
| 
						 | 
					0126dba475 | ||
| 
						 | 
					3280a3d661 | ||
| 
						 | 
					bdf80c49d8 | ||
| 
						 | 
					59169a6450 | ||
| 
						 | 
					5d228fb0f3 | ||
| 
						 | 
					10b67e1b3a | ||
| 
						 | 
					3ced310f77 | ||
| 
						 | 
					010ec113c2 | ||
| 
						 | 
					30005ba959 | ||
| 
						 | 
					6b69588c03 | ||
| 
						 | 
					8593aa1418 | ||
| 
						 | 
					9876ff9a7a | ||
| 
						 | 
					ce6eba77d9 | ||
| 
						 | 
					9b2af53025 | ||
| 
						 | 
					7b6ff19ea3 | ||
| 
						 | 
					c9fa95429a | ||
| 
						 | 
					e5d117dc98 | ||
| 
						 | 
					4a73feb041 | ||
| 
						 | 
					a06b9eefaa | ||
| 
						 | 
					3129fcf164 | ||
| 
						 | 
					35a4544477 | ||
| 
						 | 
					aa1cc2f817 | ||
| 
						 | 
					15685be4cc | ||
| 
						 | 
					8508c4dadc | ||
| 
						 | 
					e594fb0037 | ||
| 
						 | 
					a369721791 | ||
| 
						 | 
					f8e244f48d | ||
| 
						 | 
					8410611512 | ||
| 
						 | 
					caab1ec7c3 | ||
| 
						 | 
					ffade9740e | ||
| 
						 | 
					b03bcf26cd | ||
| 
						 | 
					ddbc83b2e4 | ||
| 
						 | 
					d185785f20 | ||
| 
						 | 
					02d7fbefc4 | ||
| 
						 | 
					f7ea92c68c | ||
| 
						 | 
					e891d5c5d3 | ||
| 
						 | 
					57a6b630b7 | ||
| 
						 | 
					eda768a08c | ||
| 
						 | 
					1f345eb839 | ||
| 
						 | 
					1f2801af02 | ||
| 
						 | 
					a4ba096e2a | ||
| 
						 | 
					6841cdfa76 | ||
| 
						 | 
					794f360bc2 | ||
| 
						 | 
					f797765b1d | ||
| 
						 | 
					9dce512fbb | ||
| 
						 | 
					9e91f85370 | ||
| 
						 | 
					9998cb84e8 | ||
| 
						 | 
					5ed1101bbd | ||
| 
						 | 
					6c9153300d | ||
| 
						 | 
					7957ee5191 | ||
| 
						 | 
					b200743845 | ||
| 
						 | 
					08f7e7d9b3 | ||
| 
						 | 
					16ad6b3f6c | ||
| 
						 | 
					4df9083bf0 | ||
| 
						 | 
					6419af2179 | ||
| 
						 | 
					d9858b03c9 | ||
| 
						 | 
					88efc0a3be | ||
| 
						 | 
					ac21fa7194 | ||
| 
						 | 
					c76afce9a7 | ||
| 
						 | 
					8e3304344f | ||
| 
						 | 
					db5c127cdd | ||
| 
						 | 
					0402866b43 | ||
| 
						 | 
					6cefabc6b6 | ||
| 
						 | 
					c9c04d8391 | ||
| 
						 | 
					27e8805dcb | ||
| 
						 | 
					933abedc90 | ||
| 
						 | 
					69eee9f050 | ||
| 
						 | 
					2918fb2609 | ||
| 
						 | 
					fcd7fa62ba | ||
| 
						 | 
					be7e3b9a0c | ||
| 
						 | 
					06e7272ca1 | ||
| 
						 | 
					f35eb0f6d9 | ||
| 
						 | 
					bdb74539d4 | ||
| 
						 | 
					abc1e9168d | ||
| 
						 | 
					d30ddd4c2e | ||
| 
						 | 
					05cdc095c0 | ||
| 
						 | 
					7c1dc3d632 | ||
| 
						 | 
					c53349c3b4 | ||
| 
						 | 
					a710af54ed | ||
| 
						 | 
					ac07bb8d92 | ||
| 
						 | 
					698505030e | ||
| 
						 | 
					e16a14dcef | ||
| 
						 | 
					6d93725084 | ||
| 
						 | 
					cb9981d4eb | ||
| 
						 | 
					bee4db82bb | ||
| 
						 | 
					d7706ef1b5 | ||
| 
						 | 
					baf3f4a1d1 | ||
| 
						 | 
					c7a56c2c2b | ||
| 
						 | 
					8dfff79ca2 | ||
| 
						 | 
					83c3bb839f | ||
| 
						 | 
					a9fe7eff0a | ||
| 
						 | 
					d49ecab792 | ||
| 
						 | 
					56459bbe68 | ||
| 
						 | 
					6c150ef1fb | ||
| 
						 | 
					c78f45ea20 | ||
| 
						 | 
					82481c01e0 | ||
| 
						 | 
					741cbc34e6 | ||
| 
						 | 
					5e86550de3 | ||
| 
						 | 
					92aef300ee | ||
| 
						 | 
					9ce1b68fd7 | ||
| 
						 | 
					5be5c8bec4 | ||
| 
						 | 
					0214a0001f | ||
| 
						 | 
					46067f6e17 | ||
| 
						 | 
					2b71bdf114 | 
@@ -7,8 +7,8 @@
 | 
			
		||||
		"ghcr.io/devcontainers/features/node:1": {
 | 
			
		||||
			"version": "22.11.0"
 | 
			
		||||
		},
 | 
			
		||||
		"ghcr.io/devcontainers-extra/features/corepack:1": {
 | 
			
		||||
			"version": "0.31.0"
 | 
			
		||||
		"ghcr.io/devcontainers-extra/features/pnpm:2": {
 | 
			
		||||
			"version": "10.6.1"
 | 
			
		||||
		}
 | 
			
		||||
	},
 | 
			
		||||
	"forwardPorts": [3000],
 | 
			
		||||
 
 | 
			
		||||
@@ -7,8 +7,6 @@ sudo apt-get update
 | 
			
		||||
sudo apt-get -y install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libnss3 libxss1 libasound2 libxtst6 xauth xvfb
 | 
			
		||||
git config --global --add safe.directory /workspace
 | 
			
		||||
git submodule update --init
 | 
			
		||||
corepack install
 | 
			
		||||
corepack enable
 | 
			
		||||
pnpm config set store-dir /home/node/.local/share/pnpm/store
 | 
			
		||||
pnpm install --frozen-lockfile
 | 
			
		||||
cp .devcontainer/devcontainer.yml .config/default.yml
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								.github/workflows/api-misskey-js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -9,10 +9,6 @@ on:
 | 
			
		||||
    paths:
 | 
			
		||||
      - packages/misskey-js/**
 | 
			
		||||
      - .github/workflows/api-misskey-js.yml
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  report:
 | 
			
		||||
 | 
			
		||||
@@ -22,7 +18,8 @@ jobs:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
 | 
			
		||||
      - run: corepack enable
 | 
			
		||||
      - name: Setup pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node.js
 | 
			
		||||
        uses: actions/setup-node@v4.2.0
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								.github/workflows/get-api-diff.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -9,10 +9,6 @@ on:
 | 
			
		||||
    paths:
 | 
			
		||||
      - packages/backend/**
 | 
			
		||||
      - .github/workflows/get-api-diff.yml
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  get-from-misskey:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
@@ -34,14 +30,13 @@ jobs:
 | 
			
		||||
      with:
 | 
			
		||||
        ref: ${{ matrix.ref }}
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - name: Check pnpm-lock.yaml
 | 
			
		||||
      run: git diff --exit-code pnpm-lock.yaml
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										16
									
								
								.github/workflows/lint.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -28,10 +28,6 @@ on:
 | 
			
		||||
      - packages/misskey-reversi/**
 | 
			
		||||
      - packages/shared/eslint.config.js
 | 
			
		||||
      - .github/workflows/lint.yml
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  pnpm_install:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
@@ -40,12 +36,12 @@ jobs:
 | 
			
		||||
      with:
 | 
			
		||||
        fetch-depth: 0
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - uses: actions/setup-node@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.node-version'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
 | 
			
		||||
  lint:
 | 
			
		||||
@@ -71,12 +67,12 @@ jobs:
 | 
			
		||||
      with:
 | 
			
		||||
        fetch-depth: 0
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - uses: actions/setup-node@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.node-version'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - name: Restore eslint cache
 | 
			
		||||
      uses: actions/cache@v4.2.2
 | 
			
		||||
@@ -101,12 +97,12 @@ jobs:
 | 
			
		||||
      with:
 | 
			
		||||
        fetch-depth: 0
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - uses: actions/setup-node@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.node-version'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - run: pnpm --filter misskey-js run build
 | 
			
		||||
      if: ${{ matrix.workspace == 'backend' || matrix.workspace == 'sw' }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/workflows/locale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -9,10 +9,6 @@ on:
 | 
			
		||||
    paths:
 | 
			
		||||
      - locales/**
 | 
			
		||||
      - .github/workflows/locale.yml
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  locale_verify:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
@@ -22,11 +18,11 @@ jobs:
 | 
			
		||||
      with:
 | 
			
		||||
        fetch-depth: 0
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - uses: actions/setup-node@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.node-version'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - run: cd locales && node verify.js
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/workflows/on-release-created.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -6,9 +6,6 @@ on:
 | 
			
		||||
 | 
			
		||||
  workflow_dispatch:
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  publish-misskey-js:
 | 
			
		||||
    name: Publish misskey-js
 | 
			
		||||
@@ -26,8 +23,8 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
        with:
 | 
			
		||||
          submodules: true
 | 
			
		||||
      - name: Install pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Setup pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
      - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
        uses: actions/setup-node@v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
@@ -36,7 +33,6 @@ jobs:
 | 
			
		||||
          registry-url: 'https://registry.npmjs.org'
 | 
			
		||||
      - name: Publish package
 | 
			
		||||
        run: |
 | 
			
		||||
          corepack enable
 | 
			
		||||
          pnpm i --frozen-lockfile
 | 
			
		||||
          pnpm build
 | 
			
		||||
          pnpm --filter misskey-js publish --access public --no-git-checks --provenance
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/workflows/storybook.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -13,9 +13,6 @@ on:
 | 
			
		||||
      # This is a waste of chromatic build quota, so we don't run storybook CI on pull requests targets master.
 | 
			
		||||
      - master
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  build:
 | 
			
		||||
    # chromatic is not likely to be available for fork repositories, so we disable for fork repositories.
 | 
			
		||||
@@ -43,14 +40,13 @@ jobs:
 | 
			
		||||
      run: |
 | 
			
		||||
        echo "base=$(git rev-list --parents -n1 HEAD | cut -d" " -f2)" >> $GITHUB_OUTPUT
 | 
			
		||||
        git checkout $(git rev-list --parents -n1 HEAD | cut -d" " -f3)
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - name: Use Node.js 20.x
 | 
			
		||||
      uses: actions/setup-node@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
        node-version-file: '.node-version'
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - name: Check pnpm-lock.yaml
 | 
			
		||||
      run: git diff --exit-code pnpm-lock.yaml
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/workflows/test-backend.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -18,10 +18,6 @@ on:
 | 
			
		||||
      - packages/misskey-js/**
 | 
			
		||||
      - .github/workflows/test-backend.yml
 | 
			
		||||
      - .github/misskey/test.yml
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  unit:
 | 
			
		||||
    name: Unit tests (backend)
 | 
			
		||||
@@ -48,8 +44,8 @@ jobs:
 | 
			
		||||
    - uses: actions/checkout@v4.2.2
 | 
			
		||||
      with:
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - name: Install FFmpeg
 | 
			
		||||
      run: |
 | 
			
		||||
        for i in {1..3}; do
 | 
			
		||||
@@ -70,7 +66,6 @@ jobs:
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - name: Check pnpm-lock.yaml
 | 
			
		||||
      run: git diff --exit-code pnpm-lock.yaml
 | 
			
		||||
@@ -111,14 +106,13 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
        with:
 | 
			
		||||
          submodules: true
 | 
			
		||||
      - name: Install pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Setup pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
      - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
        uses: actions/setup-node@v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          node-version: ${{ matrix.node-version }}
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
      - run: corepack enable
 | 
			
		||||
      - run: pnpm i --frozen-lockfile
 | 
			
		||||
      - name: Check pnpm-lock.yaml
 | 
			
		||||
        run: git diff --exit-code pnpm-lock.yaml
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/workflows/test-federation.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -15,9 +15,6 @@ on:
 | 
			
		||||
      - packages/misskey-js/**
 | 
			
		||||
      - .github/workflows/test-federation.yml
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    name: Federation test
 | 
			
		||||
@@ -29,8 +26,8 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@v4
 | 
			
		||||
        with:
 | 
			
		||||
          submodules: true
 | 
			
		||||
      - name: Install pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4
 | 
			
		||||
      - name: Setup pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
      - name: Install FFmpeg
 | 
			
		||||
        run: |
 | 
			
		||||
          for i in {1..3}; do
 | 
			
		||||
@@ -53,7 +50,6 @@ jobs:
 | 
			
		||||
          cache: 'pnpm'
 | 
			
		||||
      - name: Build Misskey
 | 
			
		||||
        run: |
 | 
			
		||||
          corepack enable && corepack prepare
 | 
			
		||||
          pnpm i --frozen-lockfile
 | 
			
		||||
          pnpm build
 | 
			
		||||
      - name: Setup
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										14
									
								
								.github/workflows/test-frontend.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -22,10 +22,6 @@ on:
 | 
			
		||||
      - packages/backend/**
 | 
			
		||||
      - .github/workflows/test-frontend.yml
 | 
			
		||||
      - .github/misskey/test.yml
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  vitest:
 | 
			
		||||
    name: Unit tests (frontend)
 | 
			
		||||
@@ -39,14 +35,13 @@ jobs:
 | 
			
		||||
    - uses: actions/checkout@v4.2.2
 | 
			
		||||
      with:
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - name: Check pnpm-lock.yaml
 | 
			
		||||
      run: git diff --exit-code pnpm-lock.yaml
 | 
			
		||||
@@ -95,14 +90,13 @@ jobs:
 | 
			
		||||
    #  if: ${{ matrix.browser == 'firefox' }}
 | 
			
		||||
    #- uses: browser-actions/setup-firefox@latest
 | 
			
		||||
    #  if: ${{ matrix.browser == 'firefox' }}
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - name: Copy Configure
 | 
			
		||||
      run: cp .github/misskey/test.yml .config
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										7
									
								
								.github/workflows/test-misskey-js.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -14,10 +14,6 @@ on:
 | 
			
		||||
    paths:
 | 
			
		||||
      - packages/misskey-js/**
 | 
			
		||||
      - .github/workflows/test-misskey-js.yml
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  test:
 | 
			
		||||
    name: Unit tests (misskey.js)
 | 
			
		||||
@@ -33,7 +29,8 @@ jobs:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
 | 
			
		||||
      - run: corepack enable
 | 
			
		||||
      - name: Setup pnpm
 | 
			
		||||
        uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node.js ${{ matrix.node-version }}
 | 
			
		||||
        uses: actions/setup-node@v4.2.0
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										6
									
								
								.github/workflows/test-production.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -9,7 +9,6 @@ on:
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  NODE_ENV: production
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  production:
 | 
			
		||||
@@ -24,14 +23,13 @@ jobs:
 | 
			
		||||
    - uses: actions/checkout@v4.2.2
 | 
			
		||||
      with:
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
        node-version: ${{ matrix.node-version }}
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - name: Check pnpm-lock.yaml
 | 
			
		||||
      run: git diff --exit-code pnpm-lock.yaml
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								.github/workflows/validate-api-json.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -12,10 +12,6 @@ on:
 | 
			
		||||
    paths:
 | 
			
		||||
      - packages/backend/**
 | 
			
		||||
      - .github/workflows/validate-api-json.yml
 | 
			
		||||
 | 
			
		||||
env:
 | 
			
		||||
  COREPACK_DEFAULT_TO_LATEST: 0
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  validate-api-json:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
@@ -28,8 +24,8 @@ jobs:
 | 
			
		||||
    - uses: actions/checkout@v4.2.2
 | 
			
		||||
      with:
 | 
			
		||||
        submodules: true
 | 
			
		||||
    - name: Install pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4
 | 
			
		||||
    - name: Setup pnpm
 | 
			
		||||
      uses: pnpm/action-setup@v4.1.0
 | 
			
		||||
    - name: Use Node.js ${{ matrix.node-version }}
 | 
			
		||||
      uses: actions/setup-node@v4.2.0
 | 
			
		||||
      with:
 | 
			
		||||
@@ -37,7 +33,6 @@ jobs:
 | 
			
		||||
        cache: 'pnpm'
 | 
			
		||||
    - name: Install Redocly CLI
 | 
			
		||||
      run: npm i -g @redocly/cli
 | 
			
		||||
    - run: corepack enable
 | 
			
		||||
    - run: pnpm i --frozen-lockfile
 | 
			
		||||
    - name: Check pnpm-lock.yaml
 | 
			
		||||
      run: git diff --exit-code pnpm-lock.yaml
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										35
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						@@ -1,3 +1,38 @@
 | 
			
		||||
## 2025.3.2
 | 
			
		||||
 | 
			
		||||
### General
 | 
			
		||||
-
 | 
			
		||||
 | 
			
		||||
### Client
 | 
			
		||||
- Feat: 設定の管理が強化されました
 | 
			
		||||
  - 自動でバックアップされるように
 | 
			
		||||
	- 任意の設定項目をデバイス間で同期できるように(実験的)
 | 
			
		||||
- Enhance: プラグインの管理が強化されました
 | 
			
		||||
  - インストール/アンインストール/設定の変更時にリロード不要になりました
 | 
			
		||||
- Enhance: CWの注釈テキストが入力されていない場合, Postボタンを非アクティブに
 | 
			
		||||
- Enhance: CWを無効にした場合, 注釈テキストが最大入力文字数を超えていても投稿できるように
 | 
			
		||||
- Enhance: テーマ設定画面のデザインを改善
 | 
			
		||||
- Fix: テーマ切り替え時に一部の色が変わらない問題を修正
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
- Fix: プロフィール追加情報で無効なURLに入力された場合に照会エラーを出るのを修正
 | 
			
		||||
- Fix: ActivityPubリクエストURLチェック実装は仕様に従っていないのを修正
 | 
			
		||||
 | 
			
		||||
## 2025.3.1
 | 
			
		||||
 | 
			
		||||
### General
 | 
			
		||||
- pnpmをv10に更新
 | 
			
		||||
- Corepackを削除
 | 
			
		||||
 | 
			
		||||
### Client
 | 
			
		||||
- Feat: 設定の検索を追加(実験的)
 | 
			
		||||
- Enhance: 設定項目の再配置
 | 
			
		||||
 | 
			
		||||
### Server
 | 
			
		||||
- Fix: DBマイグレーション際にシステムアカウントのユーザーID判定が正しくない問題を修正
 | 
			
		||||
- Fix: user.featured列が状況によってJSON文字列になっていたのを修正
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
## 2025.3.0
 | 
			
		||||
 | 
			
		||||
### General
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						@@ -6,8 +6,6 @@ ARG NODE_VERSION=22.11.0-bookworm
 | 
			
		||||
 | 
			
		||||
FROM --platform=$BUILDPLATFORM node:${NODE_VERSION} AS native-builder
 | 
			
		||||
 | 
			
		||||
ENV COREPACK_DEFAULT_TO_LATEST=0
 | 
			
		||||
 | 
			
		||||
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
 | 
			
		||||
	--mount=type=cache,target=/var/lib/apt,sharing=locked \
 | 
			
		||||
	rm -f /etc/apt/apt.conf.d/docker-clean \
 | 
			
		||||
@@ -16,8 +14,6 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
 | 
			
		||||
	&& apt-get install -yqq --no-install-recommends \
 | 
			
		||||
	build-essential
 | 
			
		||||
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
WORKDIR /misskey
 | 
			
		||||
 | 
			
		||||
COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
 | 
			
		||||
@@ -33,6 +29,8 @@ COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bu
 | 
			
		||||
 | 
			
		||||
ARG NODE_ENV=production
 | 
			
		||||
 | 
			
		||||
RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g
 | 
			
		||||
 | 
			
		||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
 | 
			
		||||
	pnpm i --frozen-lockfile --aggregate-output
 | 
			
		||||
 | 
			
		||||
@@ -46,14 +44,10 @@ RUN rm -rf .git/
 | 
			
		||||
 | 
			
		||||
FROM --platform=$TARGETPLATFORM node:${NODE_VERSION} AS target-builder
 | 
			
		||||
 | 
			
		||||
ENV COREPACK_DEFAULT_TO_LATEST=0
 | 
			
		||||
 | 
			
		||||
RUN apt-get update \
 | 
			
		||||
	&& apt-get install -yqq --no-install-recommends \
 | 
			
		||||
	build-essential
 | 
			
		||||
 | 
			
		||||
RUN corepack enable
 | 
			
		||||
 | 
			
		||||
WORKDIR /misskey
 | 
			
		||||
 | 
			
		||||
COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
 | 
			
		||||
@@ -65,6 +59,8 @@ COPY --link ["packages/misskey-bubble-game/package.json", "./packages/misskey-bu
 | 
			
		||||
 | 
			
		||||
ARG NODE_ENV=production
 | 
			
		||||
 | 
			
		||||
RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g
 | 
			
		||||
 | 
			
		||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
 | 
			
		||||
	pnpm i --frozen-lockfile --aggregate-output
 | 
			
		||||
 | 
			
		||||
@@ -72,13 +68,11 @@ FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner
 | 
			
		||||
 | 
			
		||||
ARG UID="991"
 | 
			
		||||
ARG GID="991"
 | 
			
		||||
ENV COREPACK_DEFAULT_TO_LATEST=0
 | 
			
		||||
 | 
			
		||||
RUN apt-get update \
 | 
			
		||||
	&& apt-get install -y --no-install-recommends \
 | 
			
		||||
	ffmpeg tini curl libjemalloc-dev libjemalloc2 \
 | 
			
		||||
	&& ln -s /usr/lib/$(uname -m)-linux-gnu/libjemalloc.so.2 /usr/local/lib/libjemalloc.so \
 | 
			
		||||
	&& corepack enable \
 | 
			
		||||
	&& groupadd -g "${GID}" misskey \
 | 
			
		||||
	&& useradd -l -u "${UID}" -g "${GID}" -m -d /misskey misskey \
 | 
			
		||||
	&& find / -type d -path /sys -prune -o -type d -path /proc -prune -o -type f -perm /u+s -ignore_readdir_race -exec chmod u-s {} \; \
 | 
			
		||||
@@ -86,13 +80,13 @@ RUN apt-get update \
 | 
			
		||||
	&& apt-get clean \
 | 
			
		||||
	&& rm -rf /var/lib/apt/lists
 | 
			
		||||
 | 
			
		||||
# add package.json to add pnpm
 | 
			
		||||
COPY ./package.json ./package.json
 | 
			
		||||
RUN node -e "console.log(JSON.parse(require('node:fs').readFileSync('./package.json')).packageManager)" | xargs npm install -g
 | 
			
		||||
 | 
			
		||||
USER misskey
 | 
			
		||||
WORKDIR /misskey
 | 
			
		||||
 | 
			
		||||
# add package.json to add pnpm
 | 
			
		||||
COPY --chown=misskey:misskey ./package.json ./package.json
 | 
			
		||||
RUN corepack install
 | 
			
		||||
 | 
			
		||||
COPY --chown=misskey:misskey --from=target-builder /misskey/node_modules ./node_modules
 | 
			
		||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/backend/node_modules ./packages/backend/node_modules
 | 
			
		||||
COPY --chown=misskey:misskey --from=target-builder /misskey/packages/misskey-js/node_modules ./packages/misskey-js/node_modules
 | 
			
		||||
 
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 94 KiB  | 
| 
		 Before Width: | Height: | Size: 317 KiB  | 
| 
		 Before Width: | Height: | Size: 24 KiB  | 
| 
		 Before Width: | Height: | Size: 95 KiB  | 
| 
		 Before Width: | Height: | Size: 238 KiB  | 
| 
		 Before Width: | Height: | Size: 148 KiB  | 
@@ -111,7 +111,7 @@ followRequests: "Peticions de seguiment"
 | 
			
		||||
unfollow: "Deixar de seguir"
 | 
			
		||||
followRequestPending: "Sol·licituds de seguiment pendents"
 | 
			
		||||
enterEmoji: "Introduir un emoji"
 | 
			
		||||
renote: "Impulsos"
 | 
			
		||||
renote: "Impulsar"
 | 
			
		||||
unrenote: "Anul·la l'impuls"
 | 
			
		||||
renoted: "S'ha impulsat"
 | 
			
		||||
renotedToX: "Impulsat per {name}."
 | 
			
		||||
@@ -1114,7 +1114,7 @@ forceShowAds: "Mostra els anuncis sempre "
 | 
			
		||||
addMemo: "Afegir recordatori"
 | 
			
		||||
editMemo: "Editar recordatori"
 | 
			
		||||
reactionsList: "Reaccions"
 | 
			
		||||
renotesList: "Impulsos"
 | 
			
		||||
renotesList: "Llistat d'impulsos "
 | 
			
		||||
notificationDisplay: "Notificacions"
 | 
			
		||||
leftTop: "Dalt a l'esquerra "
 | 
			
		||||
rightTop: "Dalt a la dreta "
 | 
			
		||||
@@ -1190,7 +1190,7 @@ pastAnnouncements: "Informes passats"
 | 
			
		||||
youHaveUnreadAnnouncements: "Tens informes per llegir."
 | 
			
		||||
useSecurityKey: "Segueix les instruccions del teu navegador O dispositiu per fer servir el teu passkey."
 | 
			
		||||
replies: "Respostes"
 | 
			
		||||
renotes: "Impulsos"
 | 
			
		||||
renotes: "Impulsar"
 | 
			
		||||
loadReplies: "Mostrar les respostes"
 | 
			
		||||
loadConversation: "Mostrar la conversació "
 | 
			
		||||
pinnedList: "Llista fixada"
 | 
			
		||||
@@ -2452,7 +2452,7 @@ _notification:
 | 
			
		||||
    follow: "Segueix-me"
 | 
			
		||||
    mention: "Menció"
 | 
			
		||||
    reply: "Respostes"
 | 
			
		||||
    renote: "Renotar"
 | 
			
		||||
    renote: "Impulsar"
 | 
			
		||||
    quote: "Citar"
 | 
			
		||||
    reaction: "Reaccions"
 | 
			
		||||
    pollEnded: "Enquesta terminada"
 | 
			
		||||
@@ -2467,7 +2467,7 @@ _notification:
 | 
			
		||||
  _actions:
 | 
			
		||||
    followBack: "També et segueix"
 | 
			
		||||
    reply: "Respondre"
 | 
			
		||||
    renote: "Renotar"
 | 
			
		||||
    renote: "Impulsos"
 | 
			
		||||
_deck:
 | 
			
		||||
  alwaysShowMainColumn: "Mostrar sempre la columna principal"
 | 
			
		||||
  columnAlign: "Alinea les columnes"
 | 
			
		||||
 
 | 
			
		||||
@@ -1311,6 +1311,8 @@ federationSpecified: "This server is operated in a whitelist federation. Interac
 | 
			
		||||
federationDisabled: "Federation is disabled on this server. You cannot interact with users on other servers."
 | 
			
		||||
confirmOnReact: "Confirm when reacting"
 | 
			
		||||
reactAreYouSure: "Would you like to add a \"{emoji}\" reaction?"
 | 
			
		||||
markAsSensitiveConfirm: "Do you want to set this media as sensitive?"
 | 
			
		||||
unmarkAsSensitiveConfirm: "Do you want to remove the sensitive designation for this media?"
 | 
			
		||||
_accountSettings:
 | 
			
		||||
  requireSigninToViewContents: "Require sign-in to view contents"
 | 
			
		||||
  requireSigninToViewContentsDescription1: "Require login to view all notes and other content you have created. This will have the effect of preventing crawlers from collecting your information."
 | 
			
		||||
@@ -2594,6 +2596,7 @@ _moderationLogTypes:
 | 
			
		||||
  deletePage: "Page deleted"
 | 
			
		||||
  deleteFlash: "Play deleted"
 | 
			
		||||
  deleteGalleryPost: "Gallery post deleted"
 | 
			
		||||
  updateProxyAccountDescription: "Update the description of the proxy account"
 | 
			
		||||
_fileViewer:
 | 
			
		||||
  title: "File details"
 | 
			
		||||
  type: "File type"
 | 
			
		||||
@@ -2857,4 +2860,8 @@ _bootErrors:
 | 
			
		||||
_search:
 | 
			
		||||
  searchScopeAll: "All"
 | 
			
		||||
  searchScopeLocal: "Local"
 | 
			
		||||
  searchScopeServer: "Specific server"
 | 
			
		||||
  searchScopeUser: "Specific user"
 | 
			
		||||
  pleaseEnterServerHost: "Enter the server host"
 | 
			
		||||
  pleaseSelectUser: "Select user"
 | 
			
		||||
  serverHostPlaceholder: "Example: misskey.example.com"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										226
									
								
								locales/index.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -4971,7 +4971,7 @@ export interface Locale extends ILocale {
 | 
			
		||||
     */
 | 
			
		||||
    "disableStreamingTimeline": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 通知をグルーピングして表示する
 | 
			
		||||
     * 通知をグルーピング
 | 
			
		||||
     */
 | 
			
		||||
    "useGroupedNotifications": string;
 | 
			
		||||
    /**
 | 
			
		||||
@@ -5270,6 +5270,218 @@ export interface Locale extends ILocale {
 | 
			
		||||
     * このメディアのセンシティブ指定を解除しますか?
 | 
			
		||||
     */
 | 
			
		||||
    "unmarkAsSensitiveConfirm": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 環境設定
 | 
			
		||||
     */
 | 
			
		||||
    "preferences": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * アクセシビリティ
 | 
			
		||||
     */
 | 
			
		||||
    "accessibility": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 設定のプロファイル
 | 
			
		||||
     */
 | 
			
		||||
    "preferencesProfile": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 設定IDをコピー
 | 
			
		||||
     */
 | 
			
		||||
    "copyPreferenceId": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 初期値に戻す
 | 
			
		||||
     */
 | 
			
		||||
    "resetToDefaultValue": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * アカウントで上書き
 | 
			
		||||
     */
 | 
			
		||||
    "overrideByAccount": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 無題
 | 
			
		||||
     */
 | 
			
		||||
    "untitled": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 名前はありません
 | 
			
		||||
     */
 | 
			
		||||
    "noName": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * スキップ
 | 
			
		||||
     */
 | 
			
		||||
    "skip": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 復元
 | 
			
		||||
     */
 | 
			
		||||
    "restore": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * デバイス間で同期
 | 
			
		||||
     */
 | 
			
		||||
    "syncBetweenDevices": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * サーバーに設定値が存在します
 | 
			
		||||
     */
 | 
			
		||||
    "preferenceSyncConflictTitle": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?
 | 
			
		||||
     */
 | 
			
		||||
    "preferenceSyncConflictText": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * サーバーの設定値
 | 
			
		||||
     */
 | 
			
		||||
    "preferenceSyncConflictChoiceServer": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * デバイスの設定値
 | 
			
		||||
     */
 | 
			
		||||
    "preferenceSyncConflictChoiceDevice": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 同期の有効化をキャンセル
 | 
			
		||||
     */
 | 
			
		||||
    "preferenceSyncConflictChoiceCancel": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * ペースト
 | 
			
		||||
     */
 | 
			
		||||
    "paste": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 絵文字パレット
 | 
			
		||||
     */
 | 
			
		||||
    "emojiPalette": string;
 | 
			
		||||
    /**
 | 
			
		||||
     * 投稿フォーム
 | 
			
		||||
     */
 | 
			
		||||
    "postForm": string;
 | 
			
		||||
    "_emojiPalette": {
 | 
			
		||||
        /**
 | 
			
		||||
         * パレット
 | 
			
		||||
         */
 | 
			
		||||
        "palettes": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * パレットのデバイス間同期を有効にする
 | 
			
		||||
         */
 | 
			
		||||
        "enableSyncBetweenDevicesForPalettes": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * メインで使用するパレット
 | 
			
		||||
         */
 | 
			
		||||
        "paletteForMain": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * リアクションで使用するパレット
 | 
			
		||||
         */
 | 
			
		||||
        "paletteForReaction": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_settings": {
 | 
			
		||||
        /**
 | 
			
		||||
         * ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "driveBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "pluginBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "notificationsBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * API
 | 
			
		||||
         */
 | 
			
		||||
        "api": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * Webhook
 | 
			
		||||
         */
 | 
			
		||||
        "webhook": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * サービス連携
 | 
			
		||||
         */
 | 
			
		||||
        "serviceConnection": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "serviceConnectionBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * アカウントのデータ
 | 
			
		||||
         */
 | 
			
		||||
        "accountData": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * アカウントデータのアーカイブをエクスポート/インポートして管理できます。
 | 
			
		||||
         */
 | 
			
		||||
        "accountDataBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "muteAndBlockBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。
 | 
			
		||||
         */
 | 
			
		||||
        "accessibilityBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "privacyBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "securityBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 好みに応じた、クライアントの全体的な動作の設定が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "preferencesBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "appearanceBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * クライアントで再生するサウンドの設定が行えます。
 | 
			
		||||
         */
 | 
			
		||||
        "soundsBanner": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * タイムラインとノート
 | 
			
		||||
         */
 | 
			
		||||
        "timelineAndNote": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_preferencesProfile": {
 | 
			
		||||
        /**
 | 
			
		||||
         * プロファイル名
 | 
			
		||||
         */
 | 
			
		||||
        "profileName": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * このデバイスを識別する名前を設定してください。
 | 
			
		||||
         */
 | 
			
		||||
        "profileNameDescription": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 例: 「メインPC」、「スマホ」など
 | 
			
		||||
         */
 | 
			
		||||
        "profileNameDescription2": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_preferencesBackup": {
 | 
			
		||||
        /**
 | 
			
		||||
         * 自動バックアップ
 | 
			
		||||
         */
 | 
			
		||||
        "autoBackup": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * バックアップから復元
 | 
			
		||||
         */
 | 
			
		||||
        "restoreFromBackup": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * バックアップが見つかりませんでした
 | 
			
		||||
         */
 | 
			
		||||
        "noBackupsFoundTitle": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。
 | 
			
		||||
         */
 | 
			
		||||
        "noBackupsFoundDescription": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 復元するバックアップを選択してください
 | 
			
		||||
         */
 | 
			
		||||
        "selectBackupToRestore": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 自動バックアップを有効にするにはプロファイル名の設定が必要です。
 | 
			
		||||
         */
 | 
			
		||||
        "youNeedToNameYourProfileToEnableAutoBackup": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * このデバイスで設定の自動バックアップは有効になっていません。
 | 
			
		||||
         */
 | 
			
		||||
        "autoPreferencesBackupIsNotEnabledForThisDevice": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 設定のバックアップが見つかりました
 | 
			
		||||
         */
 | 
			
		||||
        "backupFound": string;
 | 
			
		||||
    };
 | 
			
		||||
    "_accountSettings": {
 | 
			
		||||
        /**
 | 
			
		||||
         * コンテンツの表示にログインを必須にする
 | 
			
		||||
@@ -5307,6 +5519,10 @@ export interface Locale extends ILocale {
 | 
			
		||||
         * リモートサーバーに連合されたノートには効果が及ばない場合があります。
 | 
			
		||||
         */
 | 
			
		||||
        "mayNotEffectForFederatedNotes": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。
 | 
			
		||||
         */
 | 
			
		||||
        "mayNotEffectSomeSituations": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * 指定した時間を経過しているノート
 | 
			
		||||
         */
 | 
			
		||||
@@ -7654,6 +7870,10 @@ export interface Locale extends ILocale {
 | 
			
		||||
         * 標準のテーマ
 | 
			
		||||
         */
 | 
			
		||||
        "builtinThemes": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * サーバーのテーマ
 | 
			
		||||
         */
 | 
			
		||||
        "instanceTheme": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * そのテーマは既にインストールされています
 | 
			
		||||
         */
 | 
			
		||||
@@ -9662,6 +9882,10 @@ export interface Locale extends ILocale {
 | 
			
		||||
         * 幅を自動調整
 | 
			
		||||
         */
 | 
			
		||||
        "flexible": string;
 | 
			
		||||
        /**
 | 
			
		||||
         * プロファイル情報のデバイス間同期を有効にする
 | 
			
		||||
         */
 | 
			
		||||
        "enableSyncBetweenDevicesForProfiles": string;
 | 
			
		||||
        "_columns": {
 | 
			
		||||
            /**
 | 
			
		||||
             * メイン
 | 
			
		||||
 
 | 
			
		||||
@@ -1313,6 +1313,8 @@ confirmOnReact: "Confermare le reazioni"
 | 
			
		||||
reactAreYouSure: "Vuoi davvero reagire con {emoji} ?"
 | 
			
		||||
markAsSensitiveConfirm: "Vuoi davvero indicare questo contenuto multimediale come esplicito?"
 | 
			
		||||
unmarkAsSensitiveConfirm: "Vuoi davvero indicare come non esplicito il contenuto multimediale?"
 | 
			
		||||
preferences: "Preferenze"
 | 
			
		||||
accessibility: "Accessibilità"
 | 
			
		||||
_accountSettings:
 | 
			
		||||
  requireSigninToViewContents: "Per vedere il contenuto, è necessaria l'iscrizione"
 | 
			
		||||
  requireSigninToViewContentsDescription1: "Richiedere l'iscrizione per visualizzare tutte le Note e gli altri contenuti che hai creato. Probabilmente l'effetto è impedire la raccolta di informazioni da parte dei bot crawler."
 | 
			
		||||
@@ -1475,7 +1477,7 @@ _serverSettings:
 | 
			
		||||
_accountMigration:
 | 
			
		||||
  moveFrom: "Migra un altro profilo dentro a questo"
 | 
			
		||||
  moveFromSub: "Crea un alias verso un altro profilo remoto"
 | 
			
		||||
  moveFromLabel: "Profilo da cui migrare #{n}"
 | 
			
		||||
  moveFromLabel: "Profilo da cui migrare n. {n}"
 | 
			
		||||
  moveFromDescription: "Se desideri spostare i Follower da un altro profilo a questo, devi prima creare un alias qui. Assicurati averlo creato PRIMA di eseguire l'attività! Inserisci l'indirizzo del profilo mittente in questo modo: @persona@vecchia.istanza.it"
 | 
			
		||||
  moveTo: "Migrare questo profilo verso un un altro"
 | 
			
		||||
  moveToLabel: "Profilo verso cui migrare"
 | 
			
		||||
 
 | 
			
		||||
@@ -1238,7 +1238,7 @@ releaseToRefresh: "離してリロード"
 | 
			
		||||
refreshing: "リロード中"
 | 
			
		||||
pullDownToRefresh: "引っ張ってリロード"
 | 
			
		||||
disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
 | 
			
		||||
useGroupedNotifications: "通知をグルーピングして表示する"
 | 
			
		||||
useGroupedNotifications: "通知をグルーピング"
 | 
			
		||||
signupPendingError: "メールアドレスの確認中に問題が発生しました。リンクの有効期限が切れている可能性があります。"
 | 
			
		||||
cwNotationRequired: "「内容を隠す」がオンの場合は注釈の記述が必要です。"
 | 
			
		||||
doReaction: "リアクションする"
 | 
			
		||||
@@ -1313,6 +1313,65 @@ confirmOnReact: "リアクションする際に確認する"
 | 
			
		||||
reactAreYouSure: "\" {emoji} \" をリアクションしますか?"
 | 
			
		||||
markAsSensitiveConfirm: "このメディアをセンシティブとして設定しますか?"
 | 
			
		||||
unmarkAsSensitiveConfirm: "このメディアのセンシティブ指定を解除しますか?"
 | 
			
		||||
preferences: "環境設定"
 | 
			
		||||
accessibility: "アクセシビリティ"
 | 
			
		||||
preferencesProfile: "設定のプロファイル"
 | 
			
		||||
copyPreferenceId: "設定IDをコピー"
 | 
			
		||||
resetToDefaultValue: "初期値に戻す"
 | 
			
		||||
overrideByAccount: "アカウントで上書き"
 | 
			
		||||
untitled: "無題"
 | 
			
		||||
noName: "名前はありません"
 | 
			
		||||
skip: "スキップ"
 | 
			
		||||
restore: "復元"
 | 
			
		||||
syncBetweenDevices: "デバイス間で同期"
 | 
			
		||||
preferenceSyncConflictTitle: "サーバーに設定値が存在します"
 | 
			
		||||
preferenceSyncConflictText: "同期が有効にされた設定項目は設定値をサーバーに保存しますが、この設定項目のサーバーに保存された設定値が見つかりました。どちらの設定値で上書きしますか?"
 | 
			
		||||
preferenceSyncConflictChoiceServer: "サーバーの設定値"
 | 
			
		||||
preferenceSyncConflictChoiceDevice: "デバイスの設定値"
 | 
			
		||||
preferenceSyncConflictChoiceCancel: "同期の有効化をキャンセル"
 | 
			
		||||
paste: "ペースト"
 | 
			
		||||
emojiPalette: "絵文字パレット"
 | 
			
		||||
postForm: "投稿フォーム"
 | 
			
		||||
 | 
			
		||||
_emojiPalette:
 | 
			
		||||
  palettes: "パレット"
 | 
			
		||||
  enableSyncBetweenDevicesForPalettes: "パレットのデバイス間同期を有効にする"
 | 
			
		||||
  paletteForMain: "メインで使用するパレット"
 | 
			
		||||
  paletteForReaction: "リアクションで使用するパレット"
 | 
			
		||||
 | 
			
		||||
_settings:
 | 
			
		||||
  driveBanner: "ドライブの管理と設定、使用量の確認、ファイルをアップロードする際の設定を行えます。"
 | 
			
		||||
  pluginBanner: "プラグインを利用するとクライアントの機能を拡張することができます。プラグインのインストール、個別の設定と管理が行えます。"
 | 
			
		||||
  notificationsBanner: "サーバーからの受信する通知の種類と範囲や、プッシュ通知の設定が行えます。"
 | 
			
		||||
  api: "API"
 | 
			
		||||
  webhook: "Webhook"
 | 
			
		||||
  serviceConnection: "サービス連携"
 | 
			
		||||
  serviceConnectionBanner: "外部のアプリ・サービスと連携するためのアクセストークンやWebhookの管理と設定が行えます。"
 | 
			
		||||
  accountData: "アカウントのデータ"
 | 
			
		||||
  accountDataBanner: "アカウントデータのアーカイブをエクスポート/インポートして管理できます。"
 | 
			
		||||
  muteAndBlockBanner: "非表示にするコンテンツの設定や、特定のユーザーからのアクションを制限する設定と管理を行えます。"
 | 
			
		||||
  accessibilityBanner: "クライアントの視覚や動作に関するパーソナライズを行い、より最適に使用できるように設定できます。"
 | 
			
		||||
  privacyBanner: "コンテンツの公開範囲、見つけやすさ、フォローの承認制などアカウントのプライバシーに関する設定を行えます。"
 | 
			
		||||
  securityBanner: "パスワード、ログイン方法、認証アプリ、パスキーなどアカウントのセキュリティに関する設定を行えます。"
 | 
			
		||||
  preferencesBanner: "好みに応じた、クライアントの全体的な動作の設定が行えます。"
 | 
			
		||||
  appearanceBanner: "好みに応じた、クライアントの見た目・表示方法に関する設定が行えます。"
 | 
			
		||||
  soundsBanner: "クライアントで再生するサウンドの設定が行えます。"
 | 
			
		||||
  timelineAndNote: "タイムラインとノート"
 | 
			
		||||
 | 
			
		||||
_preferencesProfile:
 | 
			
		||||
  profileName: "プロファイル名"
 | 
			
		||||
  profileNameDescription: "このデバイスを識別する名前を設定してください。"
 | 
			
		||||
  profileNameDescription2: "例: 「メインPC」、「スマホ」など"
 | 
			
		||||
 | 
			
		||||
_preferencesBackup:
 | 
			
		||||
  autoBackup: "自動バックアップ"
 | 
			
		||||
  restoreFromBackup: "バックアップから復元"
 | 
			
		||||
  noBackupsFoundTitle: "バックアップが見つかりませんでした"
 | 
			
		||||
  noBackupsFoundDescription: "自動で作成されたバックアップは見つかりませんでしたが、バックアップファイルを手動で保存している場合、それをインポートして復元することはできます。"
 | 
			
		||||
  selectBackupToRestore: "復元するバックアップを選択してください"
 | 
			
		||||
  youNeedToNameYourProfileToEnableAutoBackup: "自動バックアップを有効にするにはプロファイル名の設定が必要です。"
 | 
			
		||||
  autoPreferencesBackupIsNotEnabledForThisDevice: "このデバイスで設定の自動バックアップは有効になっていません。"
 | 
			
		||||
  backupFound: "設定のバックアップが見つかりました"
 | 
			
		||||
 | 
			
		||||
_accountSettings:
 | 
			
		||||
  requireSigninToViewContents: "コンテンツの表示にログインを必須にする"
 | 
			
		||||
@@ -1324,6 +1383,7 @@ _accountSettings:
 | 
			
		||||
  makeNotesHiddenBefore: "過去のノートを非公開化する"
 | 
			
		||||
  makeNotesHiddenBeforeDescription: "この機能が有効になっている間、設定された日時より過去、または設定された時間を経過しているノートが自分のみ表示可能(非公開化)になります。無効に戻すと、ノートの公開状態も元に戻ります。"
 | 
			
		||||
  mayNotEffectForFederatedNotes: "リモートサーバーに連合されたノートには効果が及ばない場合があります。"
 | 
			
		||||
  mayNotEffectSomeSituations: "これらの制限は簡易的なものです。リモートサーバーでの閲覧やモデレーション時など、一部のシチュエーションでは適用されない場合があります。"
 | 
			
		||||
  notesHavePassedSpecifiedPeriod: "指定した時間を経過しているノート"
 | 
			
		||||
  notesOlderThanSpecifiedDateAndTime: "指定した日時より前のノート"
 | 
			
		||||
 | 
			
		||||
@@ -2005,6 +2065,7 @@ _theme:
 | 
			
		||||
  installed: "{name}をインストールしました"
 | 
			
		||||
  installedThemes: "インストールされたテーマ"
 | 
			
		||||
  builtinThemes: "標準のテーマ"
 | 
			
		||||
  instanceTheme: "サーバーのテーマ"
 | 
			
		||||
  alreadyInstalled: "そのテーマは既にインストールされています"
 | 
			
		||||
  invalid: "テーマの形式が間違っています"
 | 
			
		||||
  make: "テーマを作る"
 | 
			
		||||
@@ -2552,6 +2613,7 @@ _deck:
 | 
			
		||||
  useSimpleUiForNonRootPages: "非ルートページは簡易UIで表示"
 | 
			
		||||
  usedAsMinWidthWhenFlexible: "「幅を自動調整」が有効の場合、これが幅の最小値となります"
 | 
			
		||||
  flexible: "幅を自動調整"
 | 
			
		||||
  enableSyncBetweenDevicesForProfiles: "プロファイル情報のデバイス間同期を有効にする"
 | 
			
		||||
 | 
			
		||||
  _columns:
 | 
			
		||||
    main: "メイン"
 | 
			
		||||
 
 | 
			
		||||
@@ -1313,6 +1313,8 @@ confirmOnReact: "发送回应前需要确认"
 | 
			
		||||
reactAreYouSure: "要用「{emoji}」进行回应吗?"
 | 
			
		||||
markAsSensitiveConfirm: "要将此媒体标记为敏感吗?"
 | 
			
		||||
unmarkAsSensitiveConfirm: "要将此媒体解除敏感标记吗?"
 | 
			
		||||
preferences: "设置"
 | 
			
		||||
accessibility: "辅助功能"
 | 
			
		||||
_accountSettings:
 | 
			
		||||
  requireSigninToViewContents: "需要登录才能显示内容"
 | 
			
		||||
  requireSigninToViewContentsDescription1: "您发布的所有帖子将变成需要登入后才会显示。有望防止爬虫收集各种信息。"
 | 
			
		||||
 
 | 
			
		||||
@@ -1313,6 +1313,8 @@ confirmOnReact: "反應時確認"
 | 
			
		||||
reactAreYouSure: "用「 {emoji} 」反應嗎?"
 | 
			
		||||
markAsSensitiveConfirm: "要將這個媒體設定為敏感嗎?"
 | 
			
		||||
unmarkAsSensitiveConfirm: "要解除這個媒體的敏感設定嗎?"
 | 
			
		||||
preferences: "環境設定"
 | 
			
		||||
accessibility: "輔助工具"
 | 
			
		||||
_accountSettings:
 | 
			
		||||
  requireSigninToViewContents: "須登入以顯示內容"
 | 
			
		||||
  requireSigninToViewContentsDescription1: "必須登入才會顯示您建立的貼文等內容。可望有效防止資訊被爬蟲蒐集。"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -1,12 +1,12 @@
 | 
			
		||||
{
 | 
			
		||||
	"name": "misskey",
 | 
			
		||||
	"version": "2025.3.0-beta.2",
 | 
			
		||||
	"version": "2025.3.2-alpha.11",
 | 
			
		||||
	"codename": "nasubi",
 | 
			
		||||
	"repository": {
 | 
			
		||||
		"type": "git",
 | 
			
		||||
		"url": "https://github.com/misskey-dev/misskey.git"
 | 
			
		||||
	},
 | 
			
		||||
	"packageManager": "pnpm@9.15.4",
 | 
			
		||||
	"packageManager": "pnpm@10.6.1",
 | 
			
		||||
	"workspaces": [
 | 
			
		||||
		"packages/frontend-shared",
 | 
			
		||||
		"packages/frontend",
 | 
			
		||||
@@ -24,6 +24,7 @@
 | 
			
		||||
		"build": "pnpm build-pre && pnpm -r build && pnpm build-assets",
 | 
			
		||||
		"build-storybook": "pnpm --filter frontend build-storybook",
 | 
			
		||||
		"build-misskey-js-with-types": "pnpm build-pre && pnpm --filter backend... --filter=!misskey-js build && pnpm --filter backend generate-api-json --no-build && ncp packages/backend/built/api.json packages/misskey-js/generator/api.json && pnpm --filter misskey-js update-autogen-code && pnpm --filter misskey-js build && pnpm --filter misskey-js api",
 | 
			
		||||
		"build-frontend-search-index": "pnpm --filter frontend build-search-index",
 | 
			
		||||
		"start": "pnpm check:connect && cd packages/backend && node ./built/boot/entry.js",
 | 
			
		||||
		"start:test": "ncp ./.github/misskey/test.yml ./.config/test.yml && cd packages/backend && cross-env NODE_ENV=test node ./built/boot/entry.js",
 | 
			
		||||
		"init": "pnpm migrate",
 | 
			
		||||
@@ -47,35 +48,44 @@
 | 
			
		||||
		"cleanall": "pnpm clean-all"
 | 
			
		||||
	},
 | 
			
		||||
	"resolutions": {
 | 
			
		||||
		"chokidar": "3.6.0",
 | 
			
		||||
		"chokidar": "4.0.3",
 | 
			
		||||
		"lodash": "4.17.21"
 | 
			
		||||
	},
 | 
			
		||||
	"dependencies": {
 | 
			
		||||
		"cssnano": "7.0.6",
 | 
			
		||||
		"execa": "8.0.1",
 | 
			
		||||
		"execa": "9.5.2",
 | 
			
		||||
		"fast-glob": "3.3.3",
 | 
			
		||||
		"ignore-walk": "6.0.5",
 | 
			
		||||
		"ignore-walk": "7.0.0",
 | 
			
		||||
		"js-yaml": "4.1.0",
 | 
			
		||||
		"postcss": "8.5.2",
 | 
			
		||||
		"tar": "6.2.1",
 | 
			
		||||
		"postcss": "8.5.3",
 | 
			
		||||
		"tar": "7.4.3",
 | 
			
		||||
		"terser": "5.39.0",
 | 
			
		||||
		"typescript": "5.7.3",
 | 
			
		||||
		"typescript": "5.8.2",
 | 
			
		||||
		"esbuild": "0.25.0",
 | 
			
		||||
		"glob": "11.0.1"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@misskey-dev/eslint-plugin": "2.1.0",
 | 
			
		||||
		"@types/node": "22.13.4",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "8.24.0",
 | 
			
		||||
		"@typescript-eslint/parser": "8.24.0",
 | 
			
		||||
		"@types/node": "22.13.10",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "8.26.0",
 | 
			
		||||
		"@typescript-eslint/parser": "8.26.0",
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"cypress": "14.0.3",
 | 
			
		||||
		"eslint": "9.20.1",
 | 
			
		||||
		"globals": "15.15.0",
 | 
			
		||||
		"cypress": "14.1.0",
 | 
			
		||||
		"eslint": "9.22.0",
 | 
			
		||||
		"globals": "16.0.0",
 | 
			
		||||
		"ncp": "2.0.0",
 | 
			
		||||
		"pnpm": "10.6.1",
 | 
			
		||||
		"start-server-and-test": "2.0.10"
 | 
			
		||||
	},
 | 
			
		||||
	"optionalDependencies": {
 | 
			
		||||
		"@tensorflow/tfjs-core": "4.22.0"
 | 
			
		||||
	},
 | 
			
		||||
	"pnpm": {
 | 
			
		||||
		"overrides": {
 | 
			
		||||
			"@aiscript-dev/aiscript-languageserver": "-"
 | 
			
		||||
		},
 | 
			
		||||
		"patchedDependencies": {
 | 
			
		||||
			"re2": "scripts/dependency-patches/re2.patch"
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class SystemAccounts1741279404074 {
 | 
			
		||||
    name = 'SystemAccounts1741279404074'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        const instanceActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'instance.actor' AND "host" IS NULL AND "id" NOT IN (SELECT "userId" FROM "system_account" WHERE "type" = 'actor')`);
 | 
			
		||||
        if (instanceActor.length > 0) {
 | 
			
		||||
            console.warn('instance.actor was incorrect, updating...');
 | 
			
		||||
            await queryRunner.query(`UPDATE "system_account" SET "id" = '${instanceActor[0].id}', "userId" = '${instanceActor[0].id}' WHERE "type" = 'actor'`);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        const relayActor = await queryRunner.query(`SELECT "id" FROM "user" WHERE "username" = 'relay.actor' AND "host" IS NULL AND "id" NOT IN (SELECT "userId" FROM "system_account" WHERE "type" = 'relay')`);
 | 
			
		||||
        if (relayActor.length > 0) {
 | 
			
		||||
            console.warn('relay.actor was incorrect, updating...');
 | 
			
		||||
            await queryRunner.query(`UPDATE "system_account" SET "id" = '${relayActor[0].id}', "userId" = '${relayActor[0].id}' WHERE "type" = 'relay'`);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        // fixup migration, no down migration
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
export class UserFeaturedFixup1741424411879 {
 | 
			
		||||
    name = 'UserFeaturedFixup1741424411879'
 | 
			
		||||
 | 
			
		||||
    async up(queryRunner) {
 | 
			
		||||
        await queryRunner.query(`CREATE OR REPLACE FUNCTION pg_temp.extract_ap_id(text) RETURNS text AS $$
 | 
			
		||||
            SELECT
 | 
			
		||||
                CASE
 | 
			
		||||
                    WHEN $1 ~ '^https?://' THEN $1
 | 
			
		||||
                    WHEN $1 LIKE '{%' THEN COALESCE(jsonb_extract_path_text($1::jsonb, 'id'), null)
 | 
			
		||||
                    ELSE null
 | 
			
		||||
                END;
 | 
			
		||||
        $$ LANGUAGE sql IMMUTABLE;`);
 | 
			
		||||
 | 
			
		||||
        // "host" is NOT NULL is not needed but just in case add it to prevent overwriting irreplaceable data
 | 
			
		||||
        await queryRunner.query(`UPDATE "user" SET "featured" = pg_temp.extract_ap_id("featured") WHERE "host" IS NOT NULL`);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    async down(queryRunner) {
 | 
			
		||||
        // fixup migration, no down migration
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -268,7 +268,6 @@ export class FileInfoService {
 | 
			
		||||
	private async *asyncIterateFrames(cwd: string, command: FFmpeg.FfmpegCommand): AsyncGenerator<string, void> {
 | 
			
		||||
		const watcher = new FSWatcher({
 | 
			
		||||
			cwd,
 | 
			
		||||
			disableGlobbing: true,
 | 
			
		||||
		});
 | 
			
		||||
		let finished = false;
 | 
			
		||||
		command.once('end', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,7 @@ import type { Config } from '@/config.js';
 | 
			
		||||
import { StatusError } from '@/misc/status-error.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
 | 
			
		||||
import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import type { IObject } from '@/core/activitypub/type.js';
 | 
			
		||||
import type { Response } from 'node-fetch';
 | 
			
		||||
import type { URL } from 'node:url';
 | 
			
		||||
@@ -265,7 +265,7 @@ export class HttpRequestService {
 | 
			
		||||
		const finalUrl = res.url; // redirects may have been involved
 | 
			
		||||
		const activity = await res.json() as IObject;
 | 
			
		||||
 | 
			
		||||
		assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail);
 | 
			
		||||
		assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail);
 | 
			
		||||
 | 
			
		||||
		return activity;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -499,11 +499,28 @@ export class ApRendererService {
 | 
			
		||||
			this.userProfilesRepository.findOneByOrFail({ userId: user.id }),
 | 
			
		||||
		]);
 | 
			
		||||
 | 
			
		||||
		const tryRewriteUrl = (maybeUrl: string) => {
 | 
			
		||||
			const urlSafeRegex = /^(?:http[s]?:\/\/.)?(?:www\.)?[-a-zA-Z0-9@%._\+~#=]{2,256}\.[a-z]{2,6}\b(?:[-a-zA-Z0-9@:%_\+.~#?&\/\/=]*)/;
 | 
			
		||||
			try {
 | 
			
		||||
				const match = maybeUrl.match(urlSafeRegex);
 | 
			
		||||
				if (!match) {
 | 
			
		||||
					return maybeUrl;
 | 
			
		||||
				}
 | 
			
		||||
				const urlPart = match[0];
 | 
			
		||||
				const urlPartParsed = new URL(urlPart);
 | 
			
		||||
				const restPart = maybeUrl.slice(match[0].length);
 | 
			
		||||
				
 | 
			
		||||
				return `<a href="${urlPartParsed.href}" rel="me nofollow noopener" target="_blank">${urlPart}</a>${restPart}`;
 | 
			
		||||
			} catch (e) {
 | 
			
		||||
				return maybeUrl;
 | 
			
		||||
			}
 | 
			
		||||
		};
 | 
			
		||||
 | 
			
		||||
		const attachment = profile.fields.map(field => ({
 | 
			
		||||
			type: 'PropertyValue',
 | 
			
		||||
			name: field.name,
 | 
			
		||||
			value: (field.value.startsWith('http://') || field.value.startsWith('https://'))
 | 
			
		||||
				? `<a href="${new URL(field.value).href}" rel="me nofollow noopener" target="_blank">${new URL(field.value).href}</a>`
 | 
			
		||||
				? tryRewriteUrl(field.value)
 | 
			
		||||
				: field.value,
 | 
			
		||||
		}));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import { LoggerService } from '@/core/LoggerService.js';
 | 
			
		||||
import { bindThis } from '@/decorators.js';
 | 
			
		||||
import type Logger from '@/logger.js';
 | 
			
		||||
import { validateContentTypeSetAsActivityPub } from '@/core/activitypub/misc/validator.js';
 | 
			
		||||
import { assertActivityMatchesUrls, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import { assertActivityMatchesUrl, FetchAllowSoftFailMask as FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import type { IObject } from './type.js';
 | 
			
		||||
 | 
			
		||||
type Request = {
 | 
			
		||||
@@ -258,7 +258,7 @@ export class ApRequestService {
 | 
			
		||||
		const finalUrl = res.url; // redirects may have been involved
 | 
			
		||||
		const activity = await res.json() as IObject;
 | 
			
		||||
 | 
			
		||||
		assertActivityMatchesUrls(url, activity, [finalUrl], allowSoftfail);
 | 
			
		||||
		assertActivityMatchesUrl(url, activity, finalUrl, allowSoftfail);
 | 
			
		||||
 | 
			
		||||
		return activity;
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -75,7 +75,7 @@ function normalizeSynonymousSubdomain(url: URL | string): URL {
 | 
			
		||||
	return new URL(urlParsed.toString().replace(host, normalizedHost));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function assertActivityMatchesUrls(requestUrl: string | URL, activity: IObject, candidateUrls: (string | URL)[], allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask {
 | 
			
		||||
export function assertActivityMatchesUrl(requestUrl: string | URL, activity: IObject, finalUrl: string | URL, allowSoftfail: FetchAllowSoftFailMask): FetchAllowSoftFailMask {
 | 
			
		||||
	// must have a unique identifier to verify authority
 | 
			
		||||
	if (!activity.id) {
 | 
			
		||||
		throw new Error('bad Activity: missing id field');
 | 
			
		||||
@@ -95,26 +95,32 @@ export function assertActivityMatchesUrls(requestUrl: string | URL, activity: IO
 | 
			
		||||
	const requestUrlParsed = normalizeSynonymousSubdomain(requestUrl);
 | 
			
		||||
	const idParsed = normalizeSynonymousSubdomain(activity.id);
 | 
			
		||||
 | 
			
		||||
	const candidateUrlsParsed = candidateUrls.map(it => normalizeSynonymousSubdomain(it));
 | 
			
		||||
	const finalUrlParsed = normalizeSynonymousSubdomain(finalUrl);
 | 
			
		||||
 | 
			
		||||
	// mastodon sends activities with hash in the URL
 | 
			
		||||
	// currently it only happens with likes, deletes etc.
 | 
			
		||||
	// but object ID never has hash
 | 
			
		||||
	requestUrlParsed.hash = '';
 | 
			
		||||
	finalUrlParsed.hash = '';
 | 
			
		||||
 | 
			
		||||
	const requestUrlSecure = requestUrlParsed.protocol === 'https:';
 | 
			
		||||
	const finalUrlSecure = candidateUrlsParsed.every(it => it.protocol === 'https:');
 | 
			
		||||
	const finalUrlSecure = finalUrlParsed.protocol === 'https:';
 | 
			
		||||
	if (requestUrlSecure && !finalUrlSecure) {
 | 
			
		||||
		throw new Error(`bad Activity: id(${activity.id}) is not allowed to have http:// in the url`);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Compare final URL to the ID
 | 
			
		||||
	if (!candidateUrlsParsed.some(it => it.href === idParsed.href)) {
 | 
			
		||||
		requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${candidateUrlsParsed.map(it => it.toString())})`);
 | 
			
		||||
	if (finalUrlParsed.href !== idParsed.href) {
 | 
			
		||||
		requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match response url(${finalUrlParsed.toString()})`);
 | 
			
		||||
 | 
			
		||||
		// at lease host need to match exactly (ActivityPub requirement)
 | 
			
		||||
		if (!candidateUrlsParsed.some(it => idParsed.host === it.host)) {
 | 
			
		||||
			throw new Error(`bad Activity: id(${activity.id}) does not match response host(${candidateUrlsParsed.map(it => it.host)})`);
 | 
			
		||||
		if (idParsed.host !== finalUrlParsed.host) {
 | 
			
		||||
			throw new Error(`bad Activity: id(${activity.id}) does not match response host(${finalUrlParsed.host})`);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Compare request URL to the ID
 | 
			
		||||
	if (!requestUrlParsed.href.includes(idParsed.href)) {
 | 
			
		||||
	if (requestUrlParsed.href !== idParsed.href) {
 | 
			
		||||
		requireSoftfail(FetchAllowSoftFailMask.NonCanonicalId, `bad Activity: id(${activity.id}) does not match request url(${requestUrlParsed.toString()})`);
 | 
			
		||||
 | 
			
		||||
		// if cross-origin lookup is allowed, we can accept some variation between the original request URL to the final object ID (but not between the final URL and the object ID)
 | 
			
		||||
 
 | 
			
		||||
@@ -560,7 +560,7 @@ export class ApPersonService implements OnModuleInit {
 | 
			
		||||
			inbox: person.inbox,
 | 
			
		||||
			sharedInbox: person.sharedInbox ?? person.endpoints?.sharedInbox ?? null,
 | 
			
		||||
			followersUri: person.followers ? getApId(person.followers) : undefined,
 | 
			
		||||
			featured: person.featured,
 | 
			
		||||
			featured: person.featured ? getApId(person.featured) : undefined,
 | 
			
		||||
			emojis: emojiNames,
 | 
			
		||||
			name: truncate(person.name, nameLength),
 | 
			
		||||
			tags,
 | 
			
		||||
 
 | 
			
		||||
@@ -751,7 +751,7 @@ export class ActivityPubServerService {
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// follow
 | 
			
		||||
		fastify.get<{ Params: { followRequestId: string ; } }>('/follows/:followRequestId', async (request, reply) => {
 | 
			
		||||
		fastify.get<{ Params: { followRequestId: string; } }>('/follows/:followRequestId', async (request, reply) => {
 | 
			
		||||
			// This may be used before the follow is completed, so we do not
 | 
			
		||||
			// check if the following exists and only check if the follow request exists.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -497,7 +497,7 @@ export class FileServerService {
 | 
			
		||||
 | 
			
		||||
	@bindThis
 | 
			
		||||
	private async downloadAndDetectTypeFromUrl(url: string): Promise<
 | 
			
		||||
		{ state: 'remote' ; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
 | 
			
		||||
		{ state: 'remote'; mime: string; ext: string | null; path: string; cleanup: () => void; filename: string; }
 | 
			
		||||
	> {
 | 
			
		||||
		const [path, cleanup] = await createTemp();
 | 
			
		||||
		try {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,6 @@ services:
 | 
			
		||||
      - ./.config/docker.env
 | 
			
		||||
    environment:
 | 
			
		||||
      - NODE_ENV=production
 | 
			
		||||
      - COREPACK_DEFAULT_TO_LATEST=0
 | 
			
		||||
    volumes:
 | 
			
		||||
      - type: bind
 | 
			
		||||
        source: ../../../built
 | 
			
		||||
@@ -75,6 +74,10 @@ services:
 | 
			
		||||
        source: ../../../pnpm-workspace.yaml
 | 
			
		||||
        target: /misskey/pnpm-workspace.yaml
 | 
			
		||||
        read_only: true
 | 
			
		||||
      - type: bind
 | 
			
		||||
        source: ../../../scripts/dependency-patches
 | 
			
		||||
        target: /misskey/scripts/dependency-patches
 | 
			
		||||
        read_only: true
 | 
			
		||||
      - type: bind
 | 
			
		||||
        source: ./certificates/rootCA.crt
 | 
			
		||||
        target: /usr/local/share/ca-certificates/rootCA.crt
 | 
			
		||||
@@ -82,7 +85,7 @@ services:
 | 
			
		||||
    working_dir: /misskey
 | 
			
		||||
    command: >
 | 
			
		||||
      bash -c "
 | 
			
		||||
        corepack enable && corepack prepare
 | 
			
		||||
        npm install -g pnpm
 | 
			
		||||
        pnpm -F backend migrate
 | 
			
		||||
        pnpm -F backend start
 | 
			
		||||
      "
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ services:
 | 
			
		||||
      service: misskey
 | 
			
		||||
    command: >
 | 
			
		||||
      bash -c "
 | 
			
		||||
        corepack enable && corepack prepare
 | 
			
		||||
        npm install -g pnpm
 | 
			
		||||
        pnpm -F backend i
 | 
			
		||||
        pnpm -F misskey-js i
 | 
			
		||||
        pnpm -F misskey-reversi i
 | 
			
		||||
@@ -29,7 +29,6 @@ services:
 | 
			
		||||
    environment:
 | 
			
		||||
      - NODE_ENV=development
 | 
			
		||||
      - NODE_EXTRA_CA_CERTS=/usr/local/share/ca-certificates/rootCA.crt
 | 
			
		||||
      - COREPACK_DEFAULT_TO_LATEST=0
 | 
			
		||||
    volumes:
 | 
			
		||||
      - type: bind
 | 
			
		||||
        source: ../package.json
 | 
			
		||||
@@ -71,6 +70,10 @@ services:
 | 
			
		||||
        source: ../../../pnpm-workspace.yaml
 | 
			
		||||
        target: /misskey/pnpm-workspace.yaml
 | 
			
		||||
        read_only: true
 | 
			
		||||
      - type: bind
 | 
			
		||||
        source: ../../../scripts/dependency-patches
 | 
			
		||||
        target: /misskey/scripts/dependency-patches
 | 
			
		||||
        read_only: true
 | 
			
		||||
      - type: bind
 | 
			
		||||
        source: ./certificates/rootCA.crt
 | 
			
		||||
        target: /usr/local/share/ca-certificates/rootCA.crt
 | 
			
		||||
@@ -78,7 +81,7 @@ services:
 | 
			
		||||
    working_dir: /misskey
 | 
			
		||||
    entrypoint: >
 | 
			
		||||
      bash -c '
 | 
			
		||||
        corepack enable && corepack prepare
 | 
			
		||||
        npm install -g pnpm
 | 
			
		||||
        pnpm -F misskey-js i --frozen-lockfile
 | 
			
		||||
        pnpm -F backend i --frozen-lockfile
 | 
			
		||||
        exec "$0" "$@"
 | 
			
		||||
@@ -90,8 +93,6 @@ services:
 | 
			
		||||
    depends_on:
 | 
			
		||||
      redis.test:
 | 
			
		||||
        condition: service_healthy
 | 
			
		||||
    environment:
 | 
			
		||||
      - COREPACK_DEFAULT_TO_LATEST=0
 | 
			
		||||
    volumes:
 | 
			
		||||
      - type: bind
 | 
			
		||||
        source: ../package.json
 | 
			
		||||
@@ -117,10 +118,14 @@ services:
 | 
			
		||||
        source: ../../../pnpm-workspace.yaml
 | 
			
		||||
        target: /misskey/pnpm-workspace.yaml
 | 
			
		||||
        read_only: true
 | 
			
		||||
      - type: bind
 | 
			
		||||
        source: ../../../scripts/dependency-patches
 | 
			
		||||
        target: /misskey/scripts/dependency-patches
 | 
			
		||||
        read_only: true
 | 
			
		||||
    working_dir: /misskey
 | 
			
		||||
    command: >
 | 
			
		||||
      bash -c "
 | 
			
		||||
        corepack enable && corepack prepare
 | 
			
		||||
        npm install -g pnpm
 | 
			
		||||
        pnpm -F backend i --frozen-lockfile
 | 
			
		||||
        pnpm exec tsc -p ./packages/backend/test-federation
 | 
			
		||||
        node ./packages/backend/test-federation/built/daemon.js
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ import httpSignature from '@peertube/http-signature';
 | 
			
		||||
 | 
			
		||||
import { genRsaKeyPair } from '@/misc/gen-key-pair.js';
 | 
			
		||||
import { ApRequestCreator } from '@/core/activitypub/ApRequestService.js';
 | 
			
		||||
import { assertActivityMatchesUrls, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import { assertActivityMatchesUrl, FetchAllowSoftFailMask } from '@/core/activitypub/misc/check-against-url.js';
 | 
			
		||||
import { IObject } from '@/core/activitypub/type.js';
 | 
			
		||||
 | 
			
		||||
export const buildParsedSignature = (signingString: string, signature: string, algorithm: string) => {
 | 
			
		||||
@@ -66,23 +66,26 @@ describe('ap-request', () => {
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('rejects non matching domain', () => {
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrls(
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://alice.example.com/abc' } as IObject,
 | 
			
		||||
			[
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'validation should pass base case');
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://bob.example.com/abc' } as IObject,
 | 
			
		||||
			[
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Any,
 | 
			
		||||
		), 'validation should fail no matter what if the response URL is inconsistent with the object ID');
 | 
			
		||||
		
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/abc#test',
 | 
			
		||||
			{ id: 'https://alice.example.com/abc' } as IObject,
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'validation should pass with hash in request URL');
 | 
			
		||||
 | 
			
		||||
		// fix issues like threads
 | 
			
		||||
		// https://github.com/misskey-dev/misskey/issues/15039
 | 
			
		||||
		const withOrWithoutWWW = [
 | 
			
		||||
@@ -97,89 +100,71 @@ describe('ap-request', () => {
 | 
			
		||||
			),
 | 
			
		||||
			withOrWithoutWWW,
 | 
			
		||||
		).forEach(([[a, b], c]) => {
 | 
			
		||||
			assert.doesNotThrow(() => assertActivityMatchesUrls(
 | 
			
		||||
			assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
				a,
 | 
			
		||||
				{ id: b } as IObject,
 | 
			
		||||
				[
 | 
			
		||||
				c,
 | 
			
		||||
				],
 | 
			
		||||
				FetchAllowSoftFailMask.Strict,
 | 
			
		||||
			), 'validation should pass with or without www. subdomain');
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('cross origin lookup', () => {
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrls(
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://bob.example.com/abc' } as IObject,
 | 
			
		||||
			[
 | 
			
		||||
			'https://bob.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.CrossOrigin | FetchAllowSoftFailMask.NonCanonicalId,
 | 
			
		||||
		), 'validation should pass if the response is otherwise consistent and cross-origin is allowed');
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://bob.example.com/abc' } as IObject,
 | 
			
		||||
			[
 | 
			
		||||
			'https://bob.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'validation should fail if the response is otherwise consistent and cross-origin is not allowed');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('rejects non-canonical ID', () => {
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/@alice',
 | 
			
		||||
			{ id: 'https://alice.example.com/users/alice' } as IObject,
 | 
			
		||||
			[
 | 
			
		||||
				'https://alice.example.com/users/alice'
 | 
			
		||||
			],
 | 
			
		||||
			'https://alice.example.com/users/alice',
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'throws if the response ID did not exactly match the expected ID');
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrls(
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/@alice',
 | 
			
		||||
			{ id: 'https://alice.example.com/users/alice' } as IObject,
 | 
			
		||||
			[
 | 
			
		||||
			'https://alice.example.com/users/alice',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.NonCanonicalId,
 | 
			
		||||
		), 'does not throw if non-canonical ID is allowed');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('origin relaxed alignment', () => {
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrls(
 | 
			
		||||
		assert.doesNotThrow(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://ap.alice.example.com/abc' } as IObject,
 | 
			
		||||
			[
 | 
			
		||||
			'https://ap.alice.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
 | 
			
		||||
		), 'validation should pass if response is a subdomain of the expected origin');
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.multi-tenant.example.com/abc',
 | 
			
		||||
			{ id: 'https://alice.multi-tenant.example.com/abc' } as IObject,
 | 
			
		||||
			[
 | 
			
		||||
			'https://bob.multi-tenant.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.MisalignedOrigin | FetchAllowSoftFailMask.NonCanonicalId,
 | 
			
		||||
		), 'validation should fail if response is a disjoint domain of the expected origin');
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://ap.alice.example.com/abc' } as IObject,
 | 
			
		||||
			[
 | 
			
		||||
			'https://ap.alice.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'throws if relaxed origin is forbidden');
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	test('resist HTTP downgrade', () => {
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrls(
 | 
			
		||||
		assert.throws(() => assertActivityMatchesUrl(
 | 
			
		||||
			'https://alice.example.com/abc',
 | 
			
		||||
			{ id: 'https://alice.example.com/abc' } as IObject,
 | 
			
		||||
			[
 | 
			
		||||
			'http://alice.example.com/abc',
 | 
			
		||||
			],
 | 
			
		||||
			FetchAllowSoftFailMask.Strict,
 | 
			
		||||
		), 'throws if HTTP downgrade is detected');
 | 
			
		||||
	});
 | 
			
		||||
 
 | 
			
		||||
@@ -34,7 +34,7 @@
 | 
			
		||||
		"typescript": "5.8.2",
 | 
			
		||||
		"uuid": "11.1.0",
 | 
			
		||||
		"json5": "2.2.3",
 | 
			
		||||
		"vite": "6.2.0",
 | 
			
		||||
		"vite": "6.2.1",
 | 
			
		||||
		"vue": "3.5.13"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
@@ -48,14 +48,14 @@
 | 
			
		||||
		"@types/ws": "8.18.0",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "8.26.0",
 | 
			
		||||
		"@typescript-eslint/parser": "8.26.0",
 | 
			
		||||
		"@vitest/coverage-v8": "3.0.7",
 | 
			
		||||
		"@vitest/coverage-v8": "3.0.8",
 | 
			
		||||
		"@vue/runtime-core": "3.5.13",
 | 
			
		||||
		"acorn": "8.14.0",
 | 
			
		||||
		"acorn": "8.14.1",
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"eslint-plugin-import": "2.31.0",
 | 
			
		||||
		"eslint-plugin-vue": "9.33.0",
 | 
			
		||||
		"eslint-plugin-vue": "10.0.0",
 | 
			
		||||
		"fast-glob": "3.3.3",
 | 
			
		||||
		"happy-dom": "17.2.2",
 | 
			
		||||
		"happy-dom": "17.3.0",
 | 
			
		||||
		"intersection-observer": "0.12.2",
 | 
			
		||||
		"micromatch": "4.0.8",
 | 
			
		||||
		"msw": "2.7.3",
 | 
			
		||||
@@ -64,7 +64,7 @@
 | 
			
		||||
		"start-server-and-test": "2.0.10",
 | 
			
		||||
		"vite-plugin-turbosnap": "1.0.3",
 | 
			
		||||
		"vue-component-type-helpers": "2.2.8",
 | 
			
		||||
		"vue-eslint-parser": "9.4.3",
 | 
			
		||||
		"vue-eslint-parser": "10.1.1",
 | 
			
		||||
		"vue-tsc": "2.2.8"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -25,10 +25,10 @@
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "8.26.0",
 | 
			
		||||
		"@typescript-eslint/parser": "8.26.0",
 | 
			
		||||
		"esbuild": "0.25.0",
 | 
			
		||||
		"eslint-plugin-vue": "9.33.0",
 | 
			
		||||
		"eslint-plugin-vue": "10.0.0",
 | 
			
		||||
		"nodemon": "3.1.9",
 | 
			
		||||
		"typescript": "5.8.2",
 | 
			
		||||
		"vue-eslint-parser": "9.4.3"
 | 
			
		||||
		"vue-eslint-parser": "10.1.1"
 | 
			
		||||
	},
 | 
			
		||||
	"files": [
 | 
			
		||||
		"js-built"
 | 
			
		||||
 
 | 
			
		||||
@@ -17,8 +17,52 @@ interface SatisfiesExpression extends estree.BaseExpression {
 | 
			
		||||
	reference: estree.Identifier;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface ImportDeclaration extends estree.ImportDeclaration {
 | 
			
		||||
	kind?: 'type';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const generator = {
 | 
			
		||||
	...GENERATOR,
 | 
			
		||||
	ImportDeclaration(node: ImportDeclaration, state: State) {
 | 
			
		||||
		state.write('import ');
 | 
			
		||||
		if (node.kind === 'type') state.write('type ');
 | 
			
		||||
		const { specifiers } = node;
 | 
			
		||||
		if (specifiers.length > 0) {
 | 
			
		||||
			let i = 0;
 | 
			
		||||
			for (; i < specifiers.length; i++) {
 | 
			
		||||
				if (i > 0) {
 | 
			
		||||
					state.write(', ');
 | 
			
		||||
				}
 | 
			
		||||
				const specifier = specifiers[i]!;
 | 
			
		||||
				if (specifier.type === 'ImportDefaultSpecifier') {
 | 
			
		||||
					state.write(specifier.local.name, specifier);
 | 
			
		||||
				} else if (specifier.type === 'ImportNamespaceSpecifier') {
 | 
			
		||||
					state.write(`* as ${specifier.local.name}`, specifier);
 | 
			
		||||
				} else {
 | 
			
		||||
					break;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
			if (i < specifiers.length) {
 | 
			
		||||
				state.write('{');
 | 
			
		||||
				for (; i < specifiers.length; i++) {
 | 
			
		||||
					const specifier = specifiers[i]! as estree.ImportSpecifier;
 | 
			
		||||
					const { name } = specifier.imported as estree.Identifier;
 | 
			
		||||
					state.write(name, specifier);
 | 
			
		||||
					if (name !== specifier.local.name) {
 | 
			
		||||
						state.write(` as ${specifier.local.name}`);
 | 
			
		||||
					}
 | 
			
		||||
					if (i < specifiers.length - 1) {
 | 
			
		||||
						state.write(', ');
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
				state.write('}');
 | 
			
		||||
			}
 | 
			
		||||
			state.write(' from ');
 | 
			
		||||
		}
 | 
			
		||||
		this.Literal(node.source, state);
 | 
			
		||||
 | 
			
		||||
		state.write(';');
 | 
			
		||||
	},
 | 
			
		||||
	SatisfiesExpression(node: SatisfiesExpression, state: State) {
 | 
			
		||||
		switch (node.expression.type) {
 | 
			
		||||
			case 'ArrowFunctionExpression': {
 | 
			
		||||
@@ -155,7 +199,8 @@ function toStories(component: string): Promise<string> {
 | 
			
		||||
									/> as estree.ImportSpecifier,
 | 
			
		||||
								]),
 | 
			
		||||
					]}
 | 
			
		||||
				/> as estree.ImportDeclaration,
 | 
			
		||||
					kind={'type'}
 | 
			
		||||
				/> as ImportDeclaration,
 | 
			
		||||
				...(hasMsw
 | 
			
		||||
					? [
 | 
			
		||||
							<import-declaration
 | 
			
		||||
@@ -165,7 +210,7 @@ function toStories(component: string): Promise<string> {
 | 
			
		||||
										local={<identifier name='msw' /> as estree.Identifier}
 | 
			
		||||
									/> as estree.ImportNamespaceSpecifier,
 | 
			
		||||
								]}
 | 
			
		||||
							/> as estree.ImportDeclaration,
 | 
			
		||||
							/> as ImportDeclaration,
 | 
			
		||||
						]
 | 
			
		||||
					: []),
 | 
			
		||||
				...(hasImplStories
 | 
			
		||||
@@ -176,7 +221,7 @@ function toStories(component: string): Promise<string> {
 | 
			
		||||
								specifiers={[
 | 
			
		||||
									<import-default-specifier local={identifier} /> as estree.ImportDefaultSpecifier,
 | 
			
		||||
								]}
 | 
			
		||||
							/> as estree.ImportDeclaration,
 | 
			
		||||
							/> as ImportDeclaration,
 | 
			
		||||
						]),
 | 
			
		||||
				...(hasMetaStories
 | 
			
		||||
					? [
 | 
			
		||||
@@ -187,7 +232,7 @@ function toStories(component: string): Promise<string> {
 | 
			
		||||
										local={<identifier name='storiesMeta' /> as estree.Identifier}
 | 
			
		||||
									/> as estree.ImportNamespaceSpecifier,
 | 
			
		||||
								]}
 | 
			
		||||
							/> as estree.ImportDeclaration,
 | 
			
		||||
							/> as ImportDeclaration,
 | 
			
		||||
						]
 | 
			
		||||
					: []),
 | 
			
		||||
				<variable-declaration
 | 
			
		||||
 
 | 
			
		||||
@@ -39,6 +39,10 @@ const config = {
 | 
			
		||||
		if (~replacePluginForIsChromatic) {
 | 
			
		||||
			config.plugins?.splice(replacePluginForIsChromatic, 1);
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		//pluginsからcreateSearchIndexを削除、複数あるかもしれないので全て削除
 | 
			
		||||
		config.plugins = config.plugins?.filter((plugin: Plugin) => plugin && plugin.name !== 'createSearchIndex') ?? [];
 | 
			
		||||
 | 
			
		||||
		return mergeConfig(config, {
 | 
			
		||||
			plugins: [
 | 
			
		||||
				{
 | 
			
		||||
 
 | 
			
		||||
@@ -21,7 +21,7 @@ let moduleInitialized = false;
 | 
			
		||||
let unobserve = () => {};
 | 
			
		||||
let misskeyOS = null;
 | 
			
		||||
 | 
			
		||||
function loadTheme(applyTheme: typeof import('../src/scripts/theme')['applyTheme']) {
 | 
			
		||||
function loadTheme(applyTheme: typeof import('../src/theme')['applyTheme']) {
 | 
			
		||||
	unobserve();
 | 
			
		||||
	const theme = themes[document.documentElement.dataset.misskeyTheme];
 | 
			
		||||
	if (theme) {
 | 
			
		||||
@@ -67,10 +67,10 @@ queueMicrotask(() => {
 | 
			
		||||
		import('../src/components'),
 | 
			
		||||
		import('../src/directives'),
 | 
			
		||||
		import('../src/widgets'),
 | 
			
		||||
		import('../src/scripts/theme'),
 | 
			
		||||
		import('../src/store'),
 | 
			
		||||
		import('../src/theme'),
 | 
			
		||||
		import('../src/preferences'),
 | 
			
		||||
		import('../src/os'),
 | 
			
		||||
	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { defaultStore }, os]) => {
 | 
			
		||||
	]).then(([{ default: components }, { default: directives }, { default: widgets }, { applyTheme }, { prefer }, os]) => {
 | 
			
		||||
		setup((app) => {
 | 
			
		||||
			moduleInitialized = true;
 | 
			
		||||
			if (app[appInitialized]) {
 | 
			
		||||
@@ -83,7 +83,7 @@ queueMicrotask(() => {
 | 
			
		||||
			widgets(app);
 | 
			
		||||
			misskeyOS = os;
 | 
			
		||||
			if (isChromatic()) {
 | 
			
		||||
				defaultStore.set('animation', false);
 | 
			
		||||
				prefer.set('animation', false);
 | 
			
		||||
			}
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
@@ -104,9 +104,9 @@ const preview = {
 | 
			
		||||
							}
 | 
			
		||||
						}).catch(() => {})
 | 
			
		||||
					: Promise.resolve();
 | 
			
		||||
				const resetDefaultStorePromise = import('../src/store').then(({ defaultStore }) => {
 | 
			
		||||
				const resetDefaultStorePromise = import('../src/store').then(({ store }) => {
 | 
			
		||||
					// @ts-expect-error
 | 
			
		||||
					defaultStore.init();
 | 
			
		||||
					store.init();
 | 
			
		||||
				}).catch(() => {});
 | 
			
		||||
				Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
 | 
			
		||||
					initLocalStorage();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								packages/frontend/@types/theme.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -4,7 +4,7 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
declare module '@@/themes/*.json5' {
 | 
			
		||||
	import { Theme } from '@/scripts/theme.js';
 | 
			
		||||
	import { Theme } from '@/theme.js';
 | 
			
		||||
 | 
			
		||||
	const theme: Theme;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/bell_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 25 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/cloud_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 18 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/desktop_computer_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 34 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/electric_plug_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 16 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/gear_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 39 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/link_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 30 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/locked_with_key_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 43 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/mens_room_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 31 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/musical_note_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 31 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/package_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 30 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/prohibited_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 40 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/speaker_high_volume_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 27 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								packages/frontend/assets/unlocked_3d.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 30 KiB  | 
@@ -58,7 +58,7 @@ describe(normalizeClass.name, () => {
 | 
			
		||||
 | 
			
		||||
it('Composition API (standard)', () => {
 | 
			
		||||
	const ast = parse(`
 | 
			
		||||
import { c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
 | 
			
		||||
import { c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc } from './app-!~{001}~.js';
 | 
			
		||||
import { M as MkContainer } from './MkContainer-!~{03M}~.js';
 | 
			
		||||
import { b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode } from './vue-!~{002}~.js';
 | 
			
		||||
import './photoswipe-!~{003}~.js';
 | 
			
		||||
@@ -74,7 +74,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
 | 
			
		||||
    let fetching = ref(true);
 | 
			
		||||
    let images = ref([]);
 | 
			
		||||
    function thumbnail(image) {
 | 
			
		||||
      return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
 | 
			
		||||
      return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
 | 
			
		||||
    }
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
      const image = [
 | 
			
		||||
@@ -173,7 +173,7 @@ export { index_photos as default };
 | 
			
		||||
`.slice(1), { ecmaVersion: 'latest', sourceType: 'module' });
 | 
			
		||||
	unwindCssModuleClassName(ast);
 | 
			
		||||
	expect(generate(ast)).toBe(`
 | 
			
		||||
import {c as api, d as defaultStore, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
 | 
			
		||||
import {c as api, d as store, i as i18n, aD as notePage, bN as ImgWithBlurhash, bY as getStaticImageUrl, _ as _export_sfc} from './app-!~{001}~.js';
 | 
			
		||||
import {M as MkContainer} from './MkContainer-!~{03M}~.js';
 | 
			
		||||
import {b as defineComponent, a as ref, e as onMounted, z as resolveComponent, g as openBlock, h as createBlock, i as withCtx, K as createTextVNode, E as toDisplayString, u as unref, l as createBaseVNode, q as normalizeClass, B as createCommentVNode, k as createElementBlock, F as Fragment, C as renderList, A as createVNode} from './vue-!~{002}~.js';
 | 
			
		||||
import './photoswipe-!~{003}~.js';
 | 
			
		||||
@@ -190,7 +190,7 @@ const index_photos = defineComponent({
 | 
			
		||||
    let fetching = ref(true);
 | 
			
		||||
    let images = ref([]);
 | 
			
		||||
    function thumbnail(image) {
 | 
			
		||||
      return defaultStore.state.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
 | 
			
		||||
      return store.s.disableShowingAnimatedImages ? getStaticImageUrl(image.url) : image.thumbnailUrl;
 | 
			
		||||
    }
 | 
			
		||||
    onMounted(() => {
 | 
			
		||||
      const image = ["image/jpeg", "image/webp", "image/avif", "image/png", "image/gif", "image/apng", "image/vnd.mozilla.apng"];
 | 
			
		||||
@@ -268,7 +268,7 @@ export {index_photos as default};
 | 
			
		||||
it('Composition API (with `useCssModule()`)', () => {
 | 
			
		||||
	const ast = parse(`
 | 
			
		||||
import { a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup } from './!~{002}~.js';
 | 
			
		||||
import { d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
 | 
			
		||||
import { d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc } from './app-!~{001}~.js';
 | 
			
		||||
 | 
			
		||||
function isDebuggerEnabled(id) {
 | 
			
		||||
  try {
 | 
			
		||||
@@ -393,7 +393,7 @@ const _sfc_main = defineComponent({
 | 
			
		||||
      el.style.left = "";
 | 
			
		||||
    }
 | 
			
		||||
    return () => h(
 | 
			
		||||
      defaultStore.state.animation ? TransitionGroup : "div",
 | 
			
		||||
      prefer.s.animation ? TransitionGroup : "div",
 | 
			
		||||
      {
 | 
			
		||||
        class: {
 | 
			
		||||
          [$style["date-separated-list"]]: true,
 | 
			
		||||
@@ -402,7 +402,7 @@ const _sfc_main = defineComponent({
 | 
			
		||||
          [$style["direction-down"]]: props.direction === "down",
 | 
			
		||||
          [$style["direction-up"]]: props.direction === "up"
 | 
			
		||||
        },
 | 
			
		||||
        ...defaultStore.state.animation ? {
 | 
			
		||||
        ...prefer.s.animation ? {
 | 
			
		||||
          name: "list",
 | 
			
		||||
          tag: "div",
 | 
			
		||||
          onBeforeLeave,
 | 
			
		||||
@@ -441,7 +441,7 @@ export { MkDateSeparatedList as M };
 | 
			
		||||
	unwindCssModuleClassName(ast);
 | 
			
		||||
	expect(generate(ast)).toBe(`
 | 
			
		||||
import {a7 as getCurrentInstance, b as defineComponent, G as useCssModule, a1 as h, H as TransitionGroup} from './!~{002}~.js';
 | 
			
		||||
import {d as defaultStore, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
 | 
			
		||||
import {d as store, aK as toast, b5 as MkAd, i as i18n, _ as _export_sfc} from './app-!~{001}~.js';
 | 
			
		||||
function isDebuggerEnabled(id) {
 | 
			
		||||
  try {
 | 
			
		||||
    return localStorage.getItem(\`DEBUG_\${id}\`) !== null;
 | 
			
		||||
@@ -555,7 +555,7 @@ const _sfc_main = defineComponent({
 | 
			
		||||
      el.style.top = "";
 | 
			
		||||
      el.style.left = "";
 | 
			
		||||
    }
 | 
			
		||||
    return () => h(defaultStore.state.animation ? TransitionGroup : "div", {
 | 
			
		||||
    return () => h(prefer.s.animation ? TransitionGroup : "div", {
 | 
			
		||||
      class: {
 | 
			
		||||
        [$style["date-separated-list"]]: true,
 | 
			
		||||
        [$style["date-separated-list-nogap"]]: props.noGap,
 | 
			
		||||
@@ -563,7 +563,7 @@ const _sfc_main = defineComponent({
 | 
			
		||||
        [$style["direction-down"]]: props.direction === "down",
 | 
			
		||||
        [$style["direction-up"]]: props.direction === "up"
 | 
			
		||||
      },
 | 
			
		||||
      ...defaultStore.state.animation ? {
 | 
			
		||||
      ...prefer.s.animation ? {
 | 
			
		||||
        name: "list",
 | 
			
		||||
        tag: "div",
 | 
			
		||||
        onBeforeLeave,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1521
									
								
								packages/frontend/lib/vite-plugin-create-search-index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -5,6 +5,7 @@
 | 
			
		||||
	"scripts": {
 | 
			
		||||
		"watch": "vite",
 | 
			
		||||
		"build": "vite build",
 | 
			
		||||
		"build-search-index": "vite-node --config \"./vite-node.config.ts\" \"./scripts/generate-search-index.ts\"",
 | 
			
		||||
		"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
 | 
			
		||||
		"build-storybook-pre": "(tsc -p .storybook || echo done.) && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
 | 
			
		||||
		"build-storybook": "pnpm build-storybook-pre && storybook build --webpack-stats-json storybook-static",
 | 
			
		||||
@@ -51,6 +52,7 @@
 | 
			
		||||
		"insert-text-at-cursor": "0.3.0",
 | 
			
		||||
		"is-file-animated": "1.0.2",
 | 
			
		||||
		"json5": "2.2.3",
 | 
			
		||||
		"magic-string": "0.30.17",
 | 
			
		||||
		"matter-js": "0.20.0",
 | 
			
		||||
		"mfm-js": "0.24.0",
 | 
			
		||||
		"misskey-bubble-game": "workspace:*",
 | 
			
		||||
@@ -72,30 +74,31 @@
 | 
			
		||||
		"typescript": "5.8.2",
 | 
			
		||||
		"uuid": "11.1.0",
 | 
			
		||||
		"v-code-diff": "1.13.1",
 | 
			
		||||
		"vite": "6.2.0",
 | 
			
		||||
		"vite": "6.2.1",
 | 
			
		||||
		"vue": "3.5.13",
 | 
			
		||||
		"vuedraggable": "next"
 | 
			
		||||
		"vuedraggable": "next",
 | 
			
		||||
		"wanakana": "5.3.1"
 | 
			
		||||
	},
 | 
			
		||||
	"devDependencies": {
 | 
			
		||||
		"@misskey-dev/summaly": "5.2.0",
 | 
			
		||||
		"@storybook/addon-actions": "8.6.3",
 | 
			
		||||
		"@storybook/addon-essentials": "8.6.3",
 | 
			
		||||
		"@storybook/addon-interactions": "8.6.3",
 | 
			
		||||
		"@storybook/addon-links": "8.6.3",
 | 
			
		||||
		"@storybook/addon-mdx-gfm": "8.6.3",
 | 
			
		||||
		"@storybook/addon-storysource": "8.6.3",
 | 
			
		||||
		"@storybook/blocks": "8.6.3",
 | 
			
		||||
		"@storybook/components": "8.6.3",
 | 
			
		||||
		"@storybook/core-events": "8.6.3",
 | 
			
		||||
		"@storybook/manager-api": "8.6.3",
 | 
			
		||||
		"@storybook/preview-api": "8.6.3",
 | 
			
		||||
		"@storybook/react": "8.6.3",
 | 
			
		||||
		"@storybook/react-vite": "8.6.3",
 | 
			
		||||
		"@storybook/test": "8.6.3",
 | 
			
		||||
		"@storybook/theming": "8.6.3",
 | 
			
		||||
		"@storybook/types": "8.6.3",
 | 
			
		||||
		"@storybook/vue3": "8.6.3",
 | 
			
		||||
		"@storybook/vue3-vite": "8.6.3",
 | 
			
		||||
		"@storybook/addon-actions": "8.6.4",
 | 
			
		||||
		"@storybook/addon-essentials": "8.6.4",
 | 
			
		||||
		"@storybook/addon-interactions": "8.6.4",
 | 
			
		||||
		"@storybook/addon-links": "8.6.4",
 | 
			
		||||
		"@storybook/addon-mdx-gfm": "8.6.4",
 | 
			
		||||
		"@storybook/addon-storysource": "8.6.4",
 | 
			
		||||
		"@storybook/blocks": "8.6.4",
 | 
			
		||||
		"@storybook/components": "8.6.4",
 | 
			
		||||
		"@storybook/core-events": "8.6.4",
 | 
			
		||||
		"@storybook/manager-api": "8.6.4",
 | 
			
		||||
		"@storybook/preview-api": "8.6.4",
 | 
			
		||||
		"@storybook/react": "8.6.4",
 | 
			
		||||
		"@storybook/react-vite": "8.6.4",
 | 
			
		||||
		"@storybook/test": "8.6.4",
 | 
			
		||||
		"@storybook/theming": "8.6.4",
 | 
			
		||||
		"@storybook/types": "8.6.4",
 | 
			
		||||
		"@storybook/vue3": "8.6.4",
 | 
			
		||||
		"@storybook/vue3-vite": "8.6.4",
 | 
			
		||||
		"@testing-library/vue": "8.1.0",
 | 
			
		||||
		"@types/canvas-confetti": "1.9.0",
 | 
			
		||||
		"@types/estree": "1.0.6",
 | 
			
		||||
@@ -110,15 +113,15 @@
 | 
			
		||||
		"@types/ws": "8.18.0",
 | 
			
		||||
		"@typescript-eslint/eslint-plugin": "8.26.0",
 | 
			
		||||
		"@typescript-eslint/parser": "8.26.0",
 | 
			
		||||
		"@vitest/coverage-v8": "3.0.7",
 | 
			
		||||
		"@vitest/coverage-v8": "3.0.8",
 | 
			
		||||
		"@vue/runtime-core": "3.5.13",
 | 
			
		||||
		"acorn": "8.14.0",
 | 
			
		||||
		"acorn": "8.14.1",
 | 
			
		||||
		"cross-env": "7.0.3",
 | 
			
		||||
		"cypress": "14.1.0",
 | 
			
		||||
		"eslint-plugin-import": "2.31.0",
 | 
			
		||||
		"eslint-plugin-vue": "9.33.0",
 | 
			
		||||
		"eslint-plugin-vue": "10.0.0",
 | 
			
		||||
		"fast-glob": "3.3.3",
 | 
			
		||||
		"happy-dom": "17.2.2",
 | 
			
		||||
		"happy-dom": "17.3.0",
 | 
			
		||||
		"intersection-observer": "0.12.2",
 | 
			
		||||
		"micromatch": "4.0.8",
 | 
			
		||||
		"msw": "2.7.3",
 | 
			
		||||
@@ -129,13 +132,14 @@
 | 
			
		||||
		"react-dom": "19.0.0",
 | 
			
		||||
		"seedrandom": "3.0.5",
 | 
			
		||||
		"start-server-and-test": "2.0.10",
 | 
			
		||||
		"storybook": "8.6.3",
 | 
			
		||||
		"storybook": "8.6.4",
 | 
			
		||||
		"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
 | 
			
		||||
		"vite-node": "3.0.8",
 | 
			
		||||
		"vite-plugin-turbosnap": "1.0.3",
 | 
			
		||||
		"vitest": "3.0.7",
 | 
			
		||||
		"vitest": "3.0.8",
 | 
			
		||||
		"vitest-fetch-mock": "0.4.5",
 | 
			
		||||
		"vue-component-type-helpers": "2.2.8",
 | 
			
		||||
		"vue-eslint-parser": "9.4.3",
 | 
			
		||||
		"vue-eslint-parser": "10.1.1",
 | 
			
		||||
		"vue-tsc": "2.2.8"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								packages/frontend/scripts/generate-search-index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,15 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { searchIndexes } from '../vite.config.js';
 | 
			
		||||
import { generateSearchIndex } from '../lib/vite-plugin-create-search-index.js';
 | 
			
		||||
 | 
			
		||||
async function main() {
 | 
			
		||||
	for (const searchIndex of searchIndexes) {
 | 
			
		||||
		await generateSearchIndex(searchIndex);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
main();
 | 
			
		||||
@@ -1,389 +0,0 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { defineAsyncComponent, reactive, ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { apiUrl } from '@@/js/config.js';
 | 
			
		||||
import type { MenuItem, MenuButton } from '@/types/menu.js';
 | 
			
		||||
import { defaultMemoryStorage } from '@/memory-storage';
 | 
			
		||||
import { showSuspendedDialog } from '@/scripts/show-suspended-dialog.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
import { del, get, set } from '@/scripts/idb-proxy.js';
 | 
			
		||||
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { unisonReload, reloadChannel } from '@/scripts/unison-reload.js';
 | 
			
		||||
 | 
			
		||||
// TODO: 他のタブと永続化されたstateを同期
 | 
			
		||||
 | 
			
		||||
type Account = Misskey.entities.MeDetailed & { token: string };
 | 
			
		||||
 | 
			
		||||
const accountData = miLocalStorage.getItem('account');
 | 
			
		||||
 | 
			
		||||
// TODO: 外部からはreadonlyに
 | 
			
		||||
export const $i = accountData ? reactive(JSON.parse(accountData) as Account) : null;
 | 
			
		||||
 | 
			
		||||
export const iAmModerator = $i != null && ($i.isAdmin === true || $i.isModerator === true);
 | 
			
		||||
export const iAmAdmin = $i != null && $i.isAdmin;
 | 
			
		||||
 | 
			
		||||
export function signinRequired() {
 | 
			
		||||
	if ($i == null) throw new Error('signin required');
 | 
			
		||||
	return $i;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export let notesCount = $i == null ? 0 : $i.notesCount;
 | 
			
		||||
export function incNotesCount() {
 | 
			
		||||
	notesCount++;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function signout() {
 | 
			
		||||
	if (!$i) return;
 | 
			
		||||
 | 
			
		||||
	defaultMemoryStorage.clear();
 | 
			
		||||
 | 
			
		||||
	waiting();
 | 
			
		||||
	document.cookie.split(';').forEach((cookie) => {
 | 
			
		||||
		const cookieName = cookie.split('=')[0].trim();
 | 
			
		||||
		if (cookieName === 'token') {
 | 
			
		||||
			document.cookie = `${cookieName}=; max-age=0; path=/`;
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	miLocalStorage.removeItem('account');
 | 
			
		||||
	await removeAccount($i.id);
 | 
			
		||||
	const accounts = await getAccounts();
 | 
			
		||||
 | 
			
		||||
	//#region Remove service worker registration
 | 
			
		||||
	try {
 | 
			
		||||
		if (navigator.serviceWorker.controller) {
 | 
			
		||||
			const registration = await navigator.serviceWorker.ready;
 | 
			
		||||
			const push = await registration.pushManager.getSubscription();
 | 
			
		||||
			if (push) {
 | 
			
		||||
				await window.fetch(`${apiUrl}/sw/unregister`, {
 | 
			
		||||
					method: 'POST',
 | 
			
		||||
					body: JSON.stringify({
 | 
			
		||||
						i: $i.token,
 | 
			
		||||
						endpoint: push.endpoint,
 | 
			
		||||
					}),
 | 
			
		||||
					headers: {
 | 
			
		||||
						'Content-Type': 'application/json',
 | 
			
		||||
					},
 | 
			
		||||
				});
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if (accounts.length === 0) {
 | 
			
		||||
			await navigator.serviceWorker.getRegistrations()
 | 
			
		||||
				.then(registrations => {
 | 
			
		||||
					return Promise.all(registrations.map(registration => registration.unregister()));
 | 
			
		||||
				});
 | 
			
		||||
		}
 | 
			
		||||
	} catch (err) {}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	if (accounts.length > 0) login(accounts[0].token);
 | 
			
		||||
	else unisonReload('/');
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function getAccounts(): Promise<{ id: Account['id'], token: Account['token'] }[]> {
 | 
			
		||||
	return (await get('accounts')) || [];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function addAccount(id: Account['id'], token: Account['token']) {
 | 
			
		||||
	const accounts = await getAccounts();
 | 
			
		||||
	if (!accounts.some(x => x.id === id)) {
 | 
			
		||||
		await set('accounts', accounts.concat([{ id, token }]));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function removeAccount(idOrToken: Account['id']) {
 | 
			
		||||
	const accounts = await getAccounts();
 | 
			
		||||
	const i = accounts.findIndex(x => x.id === idOrToken || x.token === idOrToken);
 | 
			
		||||
	if (i !== -1) accounts.splice(i, 1);
 | 
			
		||||
 | 
			
		||||
	if (accounts.length > 0) {
 | 
			
		||||
		await set('accounts', accounts);
 | 
			
		||||
	} else {
 | 
			
		||||
		await del('accounts');
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Account> {
 | 
			
		||||
	document.cookie = 'token=; path=/; max-age=0';
 | 
			
		||||
	document.cookie = `token=${token}; path=/queue; max-age=86400; SameSite=Strict; Secure`; // bull dashboardの認証とかで使う
 | 
			
		||||
 | 
			
		||||
	return new Promise((done, fail) => {
 | 
			
		||||
		window.fetch(`${apiUrl}/i`, {
 | 
			
		||||
			method: 'POST',
 | 
			
		||||
			body: JSON.stringify({
 | 
			
		||||
				i: token,
 | 
			
		||||
			}),
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Content-Type': 'application/json',
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
			.then(res => new Promise<Account | { error: Record<string, any> }>((done2, fail2) => {
 | 
			
		||||
				if (res.status >= 500 && res.status < 600) {
 | 
			
		||||
					// サーバーエラー(5xx)の場合をrejectとする
 | 
			
		||||
					// (認証エラーなど4xxはresolve)
 | 
			
		||||
					return fail2(res);
 | 
			
		||||
				}
 | 
			
		||||
				res.json().then(done2, fail2);
 | 
			
		||||
			}))
 | 
			
		||||
			.then(async res => {
 | 
			
		||||
				if ('error' in res) {
 | 
			
		||||
					if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
 | 
			
		||||
						// SUSPENDED
 | 
			
		||||
						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
 | 
			
		||||
							await showSuspendedDialog();
 | 
			
		||||
						}
 | 
			
		||||
					} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
 | 
			
		||||
						// USER_IS_DELETED
 | 
			
		||||
						// アカウントが削除されている
 | 
			
		||||
						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
 | 
			
		||||
							await alert({
 | 
			
		||||
								type: 'error',
 | 
			
		||||
								title: i18n.ts.accountDeleted,
 | 
			
		||||
								text: i18n.ts.accountDeletedDescription,
 | 
			
		||||
							});
 | 
			
		||||
						}
 | 
			
		||||
					} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
 | 
			
		||||
						// AUTHENTICATION_FAILED
 | 
			
		||||
						// トークンが無効化されていたりアカウントが削除されたりしている
 | 
			
		||||
						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
 | 
			
		||||
							await alert({
 | 
			
		||||
								type: 'error',
 | 
			
		||||
								title: i18n.ts.tokenRevoked,
 | 
			
		||||
								text: i18n.ts.tokenRevokedDescription,
 | 
			
		||||
							});
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						await alert({
 | 
			
		||||
							type: 'error',
 | 
			
		||||
							title: i18n.ts.failedToFetchAccountInformation,
 | 
			
		||||
							text: JSON.stringify(res.error),
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// rejectかつ理由がtrueの場合、削除対象であることを示す
 | 
			
		||||
					fail(true);
 | 
			
		||||
				} else {
 | 
			
		||||
					(res as Account).token = token;
 | 
			
		||||
					done(res as Account);
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.catch(fail);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateAccount(accountData: Account) {
 | 
			
		||||
	if (!$i) return;
 | 
			
		||||
	for (const key of Object.keys($i)) {
 | 
			
		||||
		delete $i[key];
 | 
			
		||||
	}
 | 
			
		||||
	for (const [key, value] of Object.entries(accountData)) {
 | 
			
		||||
		$i[key] = value;
 | 
			
		||||
	}
 | 
			
		||||
	miLocalStorage.setItem('account', JSON.stringify($i));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateAccountPartial(accountData: Partial<Account>) {
 | 
			
		||||
	if (!$i) return;
 | 
			
		||||
	for (const [key, value] of Object.entries(accountData)) {
 | 
			
		||||
		$i[key] = value;
 | 
			
		||||
	}
 | 
			
		||||
	miLocalStorage.setItem('account', JSON.stringify($i));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function refreshAccount() {
 | 
			
		||||
	if (!$i) return;
 | 
			
		||||
	return fetchAccount($i.token, $i.id)
 | 
			
		||||
		.then(updateAccount, reason => {
 | 
			
		||||
			if (reason === true) return signout();
 | 
			
		||||
			return;
 | 
			
		||||
		});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function login(token: Account['token'], redirect?: string) {
 | 
			
		||||
	const showing = ref(true);
 | 
			
		||||
	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
 | 
			
		||||
		success: false,
 | 
			
		||||
		showing: showing,
 | 
			
		||||
	}, {
 | 
			
		||||
		closed: () => dispose(),
 | 
			
		||||
	});
 | 
			
		||||
	if (_DEV_) console.log('logging as token ', token);
 | 
			
		||||
	const me = await fetchAccount(token, undefined, true)
 | 
			
		||||
		.catch(reason => {
 | 
			
		||||
			if (reason === true) {
 | 
			
		||||
				// 削除対象の場合
 | 
			
		||||
				removeAccount(token);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			showing.value = false;
 | 
			
		||||
			throw reason;
 | 
			
		||||
		});
 | 
			
		||||
	miLocalStorage.setItem('account', JSON.stringify(me));
 | 
			
		||||
	await addAccount(me.id, token);
 | 
			
		||||
 | 
			
		||||
	if (redirect) {
 | 
			
		||||
		// 他のタブは再読み込みするだけ
 | 
			
		||||
		reloadChannel.postMessage(null);
 | 
			
		||||
		// このページはredirectで指定された先に移動
 | 
			
		||||
		location.href = redirect;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unisonReload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function openAccountMenu(opts: {
 | 
			
		||||
	includeCurrentAccount?: boolean;
 | 
			
		||||
	withExtraOperation: boolean;
 | 
			
		||||
	active?: Misskey.entities.UserDetailed['id'];
 | 
			
		||||
	onChoose?: (account: Misskey.entities.UserDetailed) => void;
 | 
			
		||||
}, ev: MouseEvent) {
 | 
			
		||||
	if (!$i) return;
 | 
			
		||||
 | 
			
		||||
	async function switchAccount(account: Misskey.entities.UserDetailed) {
 | 
			
		||||
		const storedAccounts = await getAccounts();
 | 
			
		||||
		const found = storedAccounts.find(x => x.id === account.id);
 | 
			
		||||
		if (found == null) return;
 | 
			
		||||
		switchAccountWithToken(found.token);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	function switchAccountWithToken(token: string) {
 | 
			
		||||
		login(token);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const storedAccounts = await getAccounts().then(accounts => accounts.filter(x => x.id !== $i.id));
 | 
			
		||||
	const accountsPromise = misskeyApi('users/show', { userIds: storedAccounts.map(x => x.id) });
 | 
			
		||||
 | 
			
		||||
	function createItem(account: Misskey.entities.UserDetailed) {
 | 
			
		||||
		return {
 | 
			
		||||
			type: 'user' as const,
 | 
			
		||||
			user: account,
 | 
			
		||||
			active: opts.active != null ? opts.active === account.id : false,
 | 
			
		||||
			action: () => {
 | 
			
		||||
				if (opts.onChoose) {
 | 
			
		||||
					opts.onChoose(account);
 | 
			
		||||
				} else {
 | 
			
		||||
					switchAccount(account);
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const accountItemPromises = storedAccounts.map(a => new Promise<ReturnType<typeof createItem> | MenuButton>(res => {
 | 
			
		||||
		accountsPromise.then(accounts => {
 | 
			
		||||
			const account = accounts.find(x => x.id === a.id);
 | 
			
		||||
			if (account == null) return res({
 | 
			
		||||
				type: 'button' as const,
 | 
			
		||||
				text: a.id,
 | 
			
		||||
				action: () => {
 | 
			
		||||
					switchAccountWithToken(a.token);
 | 
			
		||||
				},
 | 
			
		||||
			});
 | 
			
		||||
 | 
			
		||||
			res(createItem(account));
 | 
			
		||||
		});
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	const menuItems: MenuItem[] = [];
 | 
			
		||||
 | 
			
		||||
	if (opts.withExtraOperation) {
 | 
			
		||||
		menuItems.push({
 | 
			
		||||
			type: 'link',
 | 
			
		||||
			text: i18n.ts.profile,
 | 
			
		||||
			to: `/@${$i.username}`,
 | 
			
		||||
			avatar: $i,
 | 
			
		||||
		}, {
 | 
			
		||||
			type: 'divider',
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (opts.includeCurrentAccount) {
 | 
			
		||||
			menuItems.push(createItem($i));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		menuItems.push(...accountItemPromises);
 | 
			
		||||
 | 
			
		||||
		menuItems.push({
 | 
			
		||||
			type: 'parent',
 | 
			
		||||
			icon: 'ti ti-plus',
 | 
			
		||||
			text: i18n.ts.addAccount,
 | 
			
		||||
			children: [{
 | 
			
		||||
				text: i18n.ts.existingAccount,
 | 
			
		||||
				action: () => {
 | 
			
		||||
					getAccountWithSigninDialog().then(res => {
 | 
			
		||||
						if (res != null) {
 | 
			
		||||
							success();
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				},
 | 
			
		||||
			}, {
 | 
			
		||||
				text: i18n.ts.createAccount,
 | 
			
		||||
				action: () => {
 | 
			
		||||
					getAccountWithSignupDialog().then(res => {
 | 
			
		||||
						if (res != null) {
 | 
			
		||||
							switchAccountWithToken(res.token);
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				},
 | 
			
		||||
			}],
 | 
			
		||||
		}, {
 | 
			
		||||
			type: 'link',
 | 
			
		||||
			icon: 'ti ti-users',
 | 
			
		||||
			text: i18n.ts.manageAccounts,
 | 
			
		||||
			to: '/settings/accounts',
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		if (opts.includeCurrentAccount) {
 | 
			
		||||
			menuItems.push(createItem($i));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		menuItems.push(...accountItemPromises);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	popupMenu(menuItems, ev.currentTarget ?? ev.target, {
 | 
			
		||||
		align: 'left',
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
 | 
			
		||||
	return new Promise((resolve) => {
 | 
			
		||||
		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
 | 
			
		||||
			done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
 | 
			
		||||
				await addAccount(res.id, res.i);
 | 
			
		||||
				resolve({ id: res.id, token: res.i });
 | 
			
		||||
			},
 | 
			
		||||
			cancelled: () => {
 | 
			
		||||
				resolve(null);
 | 
			
		||||
			},
 | 
			
		||||
			closed: () => {
 | 
			
		||||
				dispose();
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
 | 
			
		||||
	return new Promise((resolve) => {
 | 
			
		||||
		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
 | 
			
		||||
			done: async (res: Misskey.entities.SignupResponse) => {
 | 
			
		||||
				await addAccount(res.id, res.token);
 | 
			
		||||
				resolve({ id: res.id, token: res.token });
 | 
			
		||||
			},
 | 
			
		||||
			cancelled: () => {
 | 
			
		||||
				resolve(null);
 | 
			
		||||
			},
 | 
			
		||||
			closed: () => {
 | 
			
		||||
				dispose();
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
if (_DEV_) {
 | 
			
		||||
	(window as any).$i = $i;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										341
									
								
								packages/frontend/src/accounts.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,341 @@
 | 
			
		||||
/*
 | 
			
		||||
 * SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
 * SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import { defineAsyncComponent, ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { apiUrl, host } from '@@/js/config.js';
 | 
			
		||||
import type { MenuItem } from '@/types/menu.js';
 | 
			
		||||
import { showSuspendedDialog } from '@/utility/show-suspended-dialog.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
import { waiting, popup, popupMenu, success, alert } from '@/os.js';
 | 
			
		||||
import { unisonReload, reloadChannel } from '@/utility/unison-reload.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
import { $i } from '@/i.js';
 | 
			
		||||
import { signout } from '@/signout.js';
 | 
			
		||||
 | 
			
		||||
// TODO: 他のタブと永続化されたstateを同期
 | 
			
		||||
 | 
			
		||||
type AccountWithToken = Misskey.entities.MeDetailed & { token: string };
 | 
			
		||||
 | 
			
		||||
export async function getAccounts(): Promise<{
 | 
			
		||||
	host: string;
 | 
			
		||||
	user: Misskey.entities.User;
 | 
			
		||||
	token: string | null;
 | 
			
		||||
}[]> {
 | 
			
		||||
	const tokens = store.s.accountTokens;
 | 
			
		||||
	const accounts = prefer.s.accounts;
 | 
			
		||||
	return accounts.map(([host, user]) => ({
 | 
			
		||||
		host,
 | 
			
		||||
		user,
 | 
			
		||||
		token: tokens[host + '/' + user.id] ?? null,
 | 
			
		||||
	}));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function addAccount(host: string, user: Misskey.entities.User, token: AccountWithToken['token']) {
 | 
			
		||||
	if (!prefer.s.accounts.some(x => x[0] === host && x[1].id === user.id)) {
 | 
			
		||||
		store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + user.id]: token });
 | 
			
		||||
		prefer.commit('accounts', [...prefer.s.accounts, [host, user]]);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function removeAccount(host: string, id: AccountWithToken['id']) {
 | 
			
		||||
	const tokens = JSON.parse(JSON.stringify(store.s.accountTokens));
 | 
			
		||||
	delete tokens[host + '/' + id];
 | 
			
		||||
	store.set('accountTokens', tokens);
 | 
			
		||||
	prefer.commit('accounts', prefer.s.accounts.filter(x => x[0] !== host || x[1].id !== id));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const isAccountDeleted = Symbol('isAccountDeleted');
 | 
			
		||||
 | 
			
		||||
function fetchAccount(token: string, id?: string, forceShowDialog?: boolean): Promise<Misskey.entities.MeDetailed> {
 | 
			
		||||
	return new Promise((done, fail) => {
 | 
			
		||||
		window.fetch(`${apiUrl}/i`, {
 | 
			
		||||
			method: 'POST',
 | 
			
		||||
			body: JSON.stringify({
 | 
			
		||||
				i: token,
 | 
			
		||||
			}),
 | 
			
		||||
			headers: {
 | 
			
		||||
				'Content-Type': 'application/json',
 | 
			
		||||
			},
 | 
			
		||||
		})
 | 
			
		||||
			.then(res => new Promise<Misskey.entities.MeDetailed | { error: Record<string, any> }>((done2, fail2) => {
 | 
			
		||||
				if (res.status >= 500 && res.status < 600) {
 | 
			
		||||
					// サーバーエラー(5xx)の場合をrejectとする
 | 
			
		||||
					// (認証エラーなど4xxはresolve)
 | 
			
		||||
					return fail2(res);
 | 
			
		||||
				}
 | 
			
		||||
				res.json().then(done2, fail2);
 | 
			
		||||
			}))
 | 
			
		||||
			.then(async res => {
 | 
			
		||||
				if ('error' in res) {
 | 
			
		||||
					if (res.error.id === 'a8c724b3-6e9c-4b46-b1a8-bc3ed6258370') {
 | 
			
		||||
						// SUSPENDED
 | 
			
		||||
						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
 | 
			
		||||
							await showSuspendedDialog();
 | 
			
		||||
						}
 | 
			
		||||
					} else if (res.error.id === 'e5b3b9f0-2b8f-4b9f-9c1f-8c5c1b2e1b1a') {
 | 
			
		||||
						// USER_IS_DELETED
 | 
			
		||||
						// アカウントが削除されている
 | 
			
		||||
						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
 | 
			
		||||
							await alert({
 | 
			
		||||
								type: 'error',
 | 
			
		||||
								title: i18n.ts.accountDeleted,
 | 
			
		||||
								text: i18n.ts.accountDeletedDescription,
 | 
			
		||||
							});
 | 
			
		||||
						}
 | 
			
		||||
					} else if (res.error.id === 'b0a7f5f8-dc2f-4171-b91f-de88ad238e14') {
 | 
			
		||||
						// AUTHENTICATION_FAILED
 | 
			
		||||
						// トークンが無効化されていたりアカウントが削除されたりしている
 | 
			
		||||
						if (forceShowDialog || $i && (token === $i.token || id === $i.id)) {
 | 
			
		||||
							await alert({
 | 
			
		||||
								type: 'error',
 | 
			
		||||
								title: i18n.ts.tokenRevoked,
 | 
			
		||||
								text: i18n.ts.tokenRevokedDescription,
 | 
			
		||||
							});
 | 
			
		||||
						}
 | 
			
		||||
					} else {
 | 
			
		||||
						await alert({
 | 
			
		||||
							type: 'error',
 | 
			
		||||
							title: i18n.ts.failedToFetchAccountInformation,
 | 
			
		||||
							text: JSON.stringify(res.error),
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					fail(isAccountDeleted);
 | 
			
		||||
				} else {
 | 
			
		||||
					done(res);
 | 
			
		||||
				}
 | 
			
		||||
			})
 | 
			
		||||
			.catch(fail);
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateCurrentAccount(accountData: Misskey.entities.MeDetailed) {
 | 
			
		||||
	if (!$i) return;
 | 
			
		||||
	const token = $i.token;
 | 
			
		||||
	for (const key of Object.keys($i)) {
 | 
			
		||||
		delete $i[key];
 | 
			
		||||
	}
 | 
			
		||||
	for (const [key, value] of Object.entries(accountData)) {
 | 
			
		||||
		$i[key] = value;
 | 
			
		||||
	}
 | 
			
		||||
	prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
 | 
			
		||||
		// TODO: $iのホストも比較したいけど通常null
 | 
			
		||||
		if (user.id === $i.id) {
 | 
			
		||||
			return [host, $i];
 | 
			
		||||
		} else {
 | 
			
		||||
			return [host, user];
 | 
			
		||||
		}
 | 
			
		||||
	}));
 | 
			
		||||
	$i.token = token;
 | 
			
		||||
	miLocalStorage.setItem('account', JSON.stringify($i));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function updateCurrentAccountPartial(accountData: Partial<Misskey.entities.MeDetailed>) {
 | 
			
		||||
	if (!$i) return;
 | 
			
		||||
	for (const [key, value] of Object.entries(accountData)) {
 | 
			
		||||
		$i[key] = value;
 | 
			
		||||
	}
 | 
			
		||||
	prefer.commit('accounts', prefer.s.accounts.map(([host, user]) => {
 | 
			
		||||
		// TODO: $iのホストも比較したいけど通常null
 | 
			
		||||
		if (user.id === $i.id) {
 | 
			
		||||
			const newUser = JSON.parse(JSON.stringify($i));
 | 
			
		||||
			for (const [key, value] of Object.entries(accountData)) {
 | 
			
		||||
				newUser[key] = value;
 | 
			
		||||
			}
 | 
			
		||||
			return [host, newUser];
 | 
			
		||||
		}
 | 
			
		||||
		return [host, user];
 | 
			
		||||
	}));
 | 
			
		||||
	miLocalStorage.setItem('account', JSON.stringify($i));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function refreshCurrentAccount() {
 | 
			
		||||
	if (!$i) return;
 | 
			
		||||
	return fetchAccount($i.token, $i.id).then(updateCurrentAccount).catch(reason => {
 | 
			
		||||
		if (reason === isAccountDeleted) {
 | 
			
		||||
			removeAccount(host, $i.id);
 | 
			
		||||
			if (Object.keys(store.s.accountTokens).length > 0) {
 | 
			
		||||
				login(Object.values(store.s.accountTokens)[0]);
 | 
			
		||||
			} else {
 | 
			
		||||
				signout();
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function login(token: AccountWithToken['token'], redirect?: string) {
 | 
			
		||||
	const showing = ref(true);
 | 
			
		||||
	const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkWaitingDialog.vue')), {
 | 
			
		||||
		success: false,
 | 
			
		||||
		showing: showing,
 | 
			
		||||
	}, {
 | 
			
		||||
		closed: () => dispose(),
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	const me = await fetchAccount(token, undefined, true).catch(reason => {
 | 
			
		||||
		showing.value = false;
 | 
			
		||||
		throw reason;
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	miLocalStorage.setItem('account', JSON.stringify({
 | 
			
		||||
		...me,
 | 
			
		||||
		token,
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	await addAccount(host, me, token);
 | 
			
		||||
 | 
			
		||||
	if (redirect) {
 | 
			
		||||
		// 他のタブは再読み込みするだけ
 | 
			
		||||
		reloadChannel.postMessage(null);
 | 
			
		||||
		// このページはredirectで指定された先に移動
 | 
			
		||||
		location.href = redirect;
 | 
			
		||||
		return;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	unisonReload();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function switchAccount(host: string, id: string) {
 | 
			
		||||
	const token = store.s.accountTokens[host + '/' + id];
 | 
			
		||||
	if (token) {
 | 
			
		||||
		login(token);
 | 
			
		||||
	} else {
 | 
			
		||||
		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
 | 
			
		||||
			done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
 | 
			
		||||
				store.set('accountTokens', { ...store.s.accountTokens, [host + '/' + res.id]: res.i });
 | 
			
		||||
				login(res.i);
 | 
			
		||||
			},
 | 
			
		||||
			closed: () => {
 | 
			
		||||
				dispose();
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function openAccountMenu(opts: {
 | 
			
		||||
	includeCurrentAccount?: boolean;
 | 
			
		||||
	withExtraOperation: boolean;
 | 
			
		||||
	active?: Misskey.entities.User['id'];
 | 
			
		||||
	onChoose?: (account: Misskey.entities.User) => void;
 | 
			
		||||
}, ev: MouseEvent) {
 | 
			
		||||
	if (!$i) return;
 | 
			
		||||
 | 
			
		||||
	function createItem(host: string, account: Misskey.entities.User): MenuItem {
 | 
			
		||||
		return {
 | 
			
		||||
			type: 'user' as const,
 | 
			
		||||
			user: account,
 | 
			
		||||
			active: opts.active != null ? opts.active === account.id : false,
 | 
			
		||||
			action: async () => {
 | 
			
		||||
				if (opts.onChoose) {
 | 
			
		||||
					opts.onChoose(account);
 | 
			
		||||
				} else {
 | 
			
		||||
					switchAccount(host, account.id);
 | 
			
		||||
				}
 | 
			
		||||
			},
 | 
			
		||||
		};
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const menuItems: MenuItem[] = [];
 | 
			
		||||
 | 
			
		||||
	// TODO: $iのホストも比較したいけど通常null
 | 
			
		||||
	const accountItems = (await getAccounts().then(accounts => accounts.filter(x => x.user.id !== $i.id))).map(a => createItem(a.host, a.user));
 | 
			
		||||
 | 
			
		||||
	if (opts.withExtraOperation) {
 | 
			
		||||
		menuItems.push({
 | 
			
		||||
			type: 'link',
 | 
			
		||||
			text: i18n.ts.profile,
 | 
			
		||||
			to: `/@${$i.username}`,
 | 
			
		||||
			avatar: $i,
 | 
			
		||||
		}, {
 | 
			
		||||
			type: 'divider',
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		if (opts.includeCurrentAccount) {
 | 
			
		||||
			menuItems.push(createItem(host, $i));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		menuItems.push(...accountItems);
 | 
			
		||||
 | 
			
		||||
		menuItems.push({
 | 
			
		||||
			type: 'parent',
 | 
			
		||||
			icon: 'ti ti-plus',
 | 
			
		||||
			text: i18n.ts.addAccount,
 | 
			
		||||
			children: [{
 | 
			
		||||
				text: i18n.ts.existingAccount,
 | 
			
		||||
				action: () => {
 | 
			
		||||
					getAccountWithSigninDialog().then(res => {
 | 
			
		||||
						if (res != null) {
 | 
			
		||||
							success();
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				},
 | 
			
		||||
			}, {
 | 
			
		||||
				text: i18n.ts.createAccount,
 | 
			
		||||
				action: () => {
 | 
			
		||||
					getAccountWithSignupDialog().then(res => {
 | 
			
		||||
						if (res != null) {
 | 
			
		||||
							switchAccount(host, res.id);
 | 
			
		||||
						}
 | 
			
		||||
					});
 | 
			
		||||
				},
 | 
			
		||||
			}],
 | 
			
		||||
		}, {
 | 
			
		||||
			type: 'link',
 | 
			
		||||
			icon: 'ti ti-users',
 | 
			
		||||
			text: i18n.ts.manageAccounts,
 | 
			
		||||
			to: '/settings/accounts',
 | 
			
		||||
		});
 | 
			
		||||
	} else {
 | 
			
		||||
		if (opts.includeCurrentAccount) {
 | 
			
		||||
			menuItems.push(createItem(host, $i));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		menuItems.push(...accountItems);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	popupMenu(menuItems, ev.currentTarget ?? ev.target, {
 | 
			
		||||
		align: 'left',
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAccountWithSigninDialog(): Promise<{ id: string, token: string } | null> {
 | 
			
		||||
	return new Promise((resolve) => {
 | 
			
		||||
		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSigninDialog.vue')), {}, {
 | 
			
		||||
			done: async (res: Misskey.entities.SigninFlowResponse & { finished: true }) => {
 | 
			
		||||
				const user = await fetchAccount(res.i, res.id, true);
 | 
			
		||||
				await addAccount(host, user, res.i);
 | 
			
		||||
				resolve({ id: res.id, token: res.i });
 | 
			
		||||
			},
 | 
			
		||||
			cancelled: () => {
 | 
			
		||||
				resolve(null);
 | 
			
		||||
			},
 | 
			
		||||
			closed: () => {
 | 
			
		||||
				dispose();
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function getAccountWithSignupDialog(): Promise<{ id: string, token: string } | null> {
 | 
			
		||||
	return new Promise((resolve) => {
 | 
			
		||||
		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkSignupDialog.vue')), {}, {
 | 
			
		||||
			done: async (res: Misskey.entities.SignupResponse) => {
 | 
			
		||||
				const user = JSON.parse(JSON.stringify(res));
 | 
			
		||||
				delete user.token;
 | 
			
		||||
				await addAccount(host, user, res.token);
 | 
			
		||||
				resolve({ id: res.id, token: res.token });
 | 
			
		||||
			},
 | 
			
		||||
			cancelled: () => {
 | 
			
		||||
				resolve(null);
 | 
			
		||||
			},
 | 
			
		||||
			closed: () => {
 | 
			
		||||
				dispose();
 | 
			
		||||
			},
 | 
			
		||||
		});
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
@@ -8,8 +8,8 @@ import * as Misskey from 'misskey-js';
 | 
			
		||||
import { url, lang } from '@@/js/config.js';
 | 
			
		||||
import { assertStringAndIsIn } from './common.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { $i } from '@/i.js';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
import { customEmojis } from '@/custom-emojis.js';
 | 
			
		||||
 | 
			
		||||
@@ -6,26 +6,30 @@
 | 
			
		||||
import { computed, watch, version as vueVersion } from 'vue';
 | 
			
		||||
import { compareVersions } from 'compare-versions';
 | 
			
		||||
import { version, lang, updateLocale, locale } from '@@/js/config.js';
 | 
			
		||||
import defaultLightTheme from '@@/themes/l-light.json5';
 | 
			
		||||
import defaultDarkTheme from '@@/themes/d-green-lime.json5';
 | 
			
		||||
import type { App } from 'vue';
 | 
			
		||||
import widgets from '@/widgets/index.js';
 | 
			
		||||
import directives from '@/directives/index.js';
 | 
			
		||||
import components from '@/components/index.js';
 | 
			
		||||
import { applyTheme } from '@/scripts/theme.js';
 | 
			
		||||
import { isDeviceDarkmode } from '@/scripts/is-device-darkmode.js';
 | 
			
		||||
import { applyTheme } from '@/theme.js';
 | 
			
		||||
import { isDeviceDarkmode } from '@/utility/is-device-darkmode.js';
 | 
			
		||||
import { updateI18n, i18n } from '@/i18n.js';
 | 
			
		||||
import { $i, refreshAccount, login } from '@/account.js';
 | 
			
		||||
import { defaultStore, ColdDeviceStorage } from '@/store.js';
 | 
			
		||||
import { refreshCurrentAccount, login } from '@/accounts.js';
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
import { fetchInstance, instance } from '@/instance.js';
 | 
			
		||||
import { deviceKind, updateDeviceKind } from '@/scripts/device-kind.js';
 | 
			
		||||
import { reloadChannel } from '@/scripts/unison-reload.js';
 | 
			
		||||
import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
 | 
			
		||||
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
 | 
			
		||||
import { deviceKind, updateDeviceKind } from '@/utility/device-kind.js';
 | 
			
		||||
import { reloadChannel } from '@/utility/unison-reload.js';
 | 
			
		||||
import { getUrlWithoutLoginId } from '@/utility/login-id.js';
 | 
			
		||||
import { getAccountFromId } from '@/utility/get-account-from-id.js';
 | 
			
		||||
import { deckStore } from '@/ui/deck/deck-store.js';
 | 
			
		||||
import { analytics, initAnalytics } from '@/analytics.js';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
import { fetchCustomEmojis } from '@/custom-emojis.js';
 | 
			
		||||
import { setupRouter } from '@/router/main.js';
 | 
			
		||||
import { createMainRouter } from '@/router/definition.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import { $i } from '@/i.js';
 | 
			
		||||
 | 
			
		||||
export async function common(createVue: () => App<Element>) {
 | 
			
		||||
	console.info(`Misskey v${version}`);
 | 
			
		||||
@@ -35,11 +39,6 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
 | 
			
		||||
		console.info(`vue ${vueVersion}`);
 | 
			
		||||
 | 
			
		||||
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
		(window as any).$i = $i;
 | 
			
		||||
		// eslint-disable-next-line @typescript-eslint/no-explicit-any
 | 
			
		||||
		(window as any).$store = defaultStore;
 | 
			
		||||
 | 
			
		||||
		window.addEventListener('error', event => {
 | 
			
		||||
			console.error(event);
 | 
			
		||||
			/*
 | 
			
		||||
@@ -123,7 +122,7 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
	html.setAttribute('lang', lang);
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	await defaultStore.ready;
 | 
			
		||||
	await store.ready;
 | 
			
		||||
	await deckStore.ready;
 | 
			
		||||
 | 
			
		||||
	const fetchInstanceMetaPromise = fetchInstance();
 | 
			
		||||
@@ -151,56 +150,63 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
 | 
			
		||||
	watch(defaultStore.reactiveState.darkMode, (darkMode) => {
 | 
			
		||||
		applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
 | 
			
		||||
	watch(store.r.darkMode, (darkMode) => {
 | 
			
		||||
		applyTheme(darkMode
 | 
			
		||||
			? (prefer.s.darkTheme ?? defaultDarkTheme)
 | 
			
		||||
			: (prefer.s.lightTheme ?? defaultLightTheme),
 | 
			
		||||
		);
 | 
			
		||||
	}, { immediate: miLocalStorage.getItem('theme') == null });
 | 
			
		||||
 | 
			
		||||
	document.documentElement.dataset.colorScheme = defaultStore.state.darkMode ? 'dark' : 'light';
 | 
			
		||||
	document.documentElement.dataset.colorScheme = store.s.darkMode ? 'dark' : 'light';
 | 
			
		||||
 | 
			
		||||
	const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
 | 
			
		||||
	const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
 | 
			
		||||
	const darkTheme = prefer.model('darkTheme');
 | 
			
		||||
	const lightTheme = prefer.model('lightTheme');
 | 
			
		||||
 | 
			
		||||
	watch(darkTheme, (theme) => {
 | 
			
		||||
		if (defaultStore.state.darkMode) {
 | 
			
		||||
			applyTheme(theme);
 | 
			
		||||
		if (store.s.darkMode) {
 | 
			
		||||
			applyTheme(theme ?? defaultDarkTheme);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	watch(lightTheme, (theme) => {
 | 
			
		||||
		if (!defaultStore.state.darkMode) {
 | 
			
		||||
			applyTheme(theme);
 | 
			
		||||
		if (!store.s.darkMode) {
 | 
			
		||||
			applyTheme(theme ?? defaultLightTheme);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	//#region Sync dark mode
 | 
			
		||||
	if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
 | 
			
		||||
		defaultStore.set('darkMode', isDeviceDarkmode());
 | 
			
		||||
	if (prefer.s.syncDeviceDarkMode) {
 | 
			
		||||
		store.set('darkMode', isDeviceDarkmode());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', (mql) => {
 | 
			
		||||
		if (ColdDeviceStorage.get('syncDeviceDarkMode')) {
 | 
			
		||||
			defaultStore.set('darkMode', mql.matches);
 | 
			
		||||
		if (prefer.s.syncDeviceDarkMode) {
 | 
			
		||||
			store.set('darkMode', mql.matches);
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
	fetchInstanceMetaPromise.then(() => {
 | 
			
		||||
		if (defaultStore.state.themeInitial) {
 | 
			
		||||
			if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON.parse(instance.defaultLightTheme));
 | 
			
		||||
			if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON.parse(instance.defaultDarkTheme));
 | 
			
		||||
			defaultStore.set('themeInitial', false);
 | 
			
		||||
	if (prefer.s.darkTheme && store.s.darkMode) {
 | 
			
		||||
		if (miLocalStorage.getItem('themeId') !== prefer.s.darkTheme.id) applyTheme(prefer.s.darkTheme);
 | 
			
		||||
	} else if (prefer.s.lightTheme && !store.s.darkMode) {
 | 
			
		||||
		if (miLocalStorage.getItem('themeId') !== prefer.s.lightTheme.id) applyTheme(prefer.s.lightTheme);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fetchInstanceMetaPromise.then(() => {
 | 
			
		||||
		// TODO: instance.defaultLightTheme/instance.defaultDarkThemeが不正な形式だった場合のケア
 | 
			
		||||
		if (prefer.s.lightTheme == null && instance.defaultLightTheme != null) prefer.commit('lightTheme', JSON.parse(instance.defaultLightTheme));
 | 
			
		||||
		if (prefer.s.darkTheme == null && instance.defaultDarkTheme != null) prefer.commit('darkTheme', JSON.parse(instance.defaultDarkTheme));
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	watch(defaultStore.reactiveState.overridedDeviceKind, (kind) => {
 | 
			
		||||
	watch(prefer.r.overridedDeviceKind, (kind) => {
 | 
			
		||||
		updateDeviceKind(kind);
 | 
			
		||||
	}, { immediate: true });
 | 
			
		||||
 | 
			
		||||
	watch(defaultStore.reactiveState.useBlurEffectForModal, v => {
 | 
			
		||||
	watch(prefer.r.useBlurEffectForModal, v => {
 | 
			
		||||
		document.documentElement.style.setProperty('--MI-modalBgFilter', v ? 'blur(4px)' : 'none');
 | 
			
		||||
	}, { immediate: true });
 | 
			
		||||
 | 
			
		||||
	watch(defaultStore.reactiveState.useBlurEffect, v => {
 | 
			
		||||
	watch(prefer.r.useBlurEffect, v => {
 | 
			
		||||
		if (v) {
 | 
			
		||||
			document.documentElement.style.removeProperty('--MI-blur');
 | 
			
		||||
		} else {
 | 
			
		||||
@@ -214,7 +220,7 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
			navigator.wakeLock.request('screen');
 | 
			
		||||
		}
 | 
			
		||||
	});
 | 
			
		||||
	if (defaultStore.state.keepScreenOn && 'wakeLock' in navigator) {
 | 
			
		||||
	if (prefer.s.keepScreenOn && 'wakeLock' in navigator) {
 | 
			
		||||
		navigator.wakeLock.request('screen')
 | 
			
		||||
			.then(onVisibilityChange)
 | 
			
		||||
			.catch(() => {
 | 
			
		||||
@@ -234,7 +240,7 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
			console.log('account cache found. refreshing...');
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		refreshAccount();
 | 
			
		||||
		refreshCurrentAccount();
 | 
			
		||||
	}
 | 
			
		||||
	//#endregion
 | 
			
		||||
 | 
			
		||||
@@ -316,6 +322,7 @@ export async function common(createVue: () => App<Element>) {
 | 
			
		||||
 | 
			
		||||
	return {
 | 
			
		||||
		isClientUpdated,
 | 
			
		||||
		lastVersion,
 | 
			
		||||
		app,
 | 
			
		||||
	};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,36 +5,45 @@
 | 
			
		||||
 | 
			
		||||
import { createApp, defineAsyncComponent, markRaw } from 'vue';
 | 
			
		||||
import { ui } from '@@/js/config.js';
 | 
			
		||||
import { common } from './common.js';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { v4 as uuid } from 'uuid';
 | 
			
		||||
import { compareVersions } from 'compare-versions';
 | 
			
		||||
import { common } from './common.js';
 | 
			
		||||
import type { Component } from 'vue';
 | 
			
		||||
import type { Keymap } from '@/utility/hotkey.js';
 | 
			
		||||
import type { DeckProfile } from '@/deck.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { alert, confirm, popup, post, toast } from '@/os.js';
 | 
			
		||||
import { useStream } from '@/stream.js';
 | 
			
		||||
import * as sound from '@/scripts/sound.js';
 | 
			
		||||
import { $i, signout, updateAccountPartial } from '@/account.js';
 | 
			
		||||
import * as sound from '@/utility/sound.js';
 | 
			
		||||
import { $i } from '@/i.js';
 | 
			
		||||
import { instance } from '@/instance.js';
 | 
			
		||||
import { ColdDeviceStorage, defaultStore } from '@/store.js';
 | 
			
		||||
import { reactionPicker } from '@/scripts/reaction-picker.js';
 | 
			
		||||
import { ColdDeviceStorage, store } from '@/store.js';
 | 
			
		||||
import { reactionPicker } from '@/utility/reaction-picker.js';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
import { claimAchievement, claimedAchievements } from '@/scripts/achievements.js';
 | 
			
		||||
import { initializeSw } from '@/scripts/initialize-sw.js';
 | 
			
		||||
import { deckStore } from '@/ui/deck/deck-store.js';
 | 
			
		||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
 | 
			
		||||
import { claimAchievement, claimedAchievements } from '@/utility/achievements.js';
 | 
			
		||||
import { initializeSw } from '@/utility/initialize-sw.js';
 | 
			
		||||
import { emojiPicker } from '@/utility/emoji-picker.js';
 | 
			
		||||
import { mainRouter } from '@/router/main.js';
 | 
			
		||||
import { makeHotkey } from '@/scripts/hotkey.js';
 | 
			
		||||
import type { Keymap } from '@/scripts/hotkey.js';
 | 
			
		||||
import { makeHotkey } from '@/utility/hotkey.js';
 | 
			
		||||
import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { deckStore } from '@/ui/deck/deck-store.js';
 | 
			
		||||
import { launchPlugins } from '@/plugin.js';
 | 
			
		||||
import { unisonReload } from '@/utility/unison-reload.js';
 | 
			
		||||
import { updateCurrentAccountPartial } from '@/accounts.js';
 | 
			
		||||
import { signout } from '@/signout.js';
 | 
			
		||||
 | 
			
		||||
export async function mainBoot() {
 | 
			
		||||
	const { isClientUpdated } = await common(() => {
 | 
			
		||||
	const { isClientUpdated, lastVersion } = await common(() => {
 | 
			
		||||
		let uiStyle = ui;
 | 
			
		||||
		const searchParams = new URLSearchParams(window.location.search);
 | 
			
		||||
 | 
			
		||||
		if (!$i) uiStyle = 'visitor';
 | 
			
		||||
 | 
			
		||||
		if (searchParams.has('zen')) uiStyle = 'zen';
 | 
			
		||||
		if (uiStyle === 'deck' && deckStore.state.useSimpleUiForNonRootPages && location.pathname !== '/') uiStyle = 'zen';
 | 
			
		||||
		if (uiStyle === 'deck' && prefer.s['deck.useSimpleUiForNonRootPages'] && location.pathname !== '/') uiStyle = 'zen';
 | 
			
		||||
 | 
			
		||||
		if (searchParams.has('ui')) uiStyle = searchParams.get('ui');
 | 
			
		||||
 | 
			
		||||
@@ -67,15 +76,146 @@ export async function mainBoot() {
 | 
			
		||||
		const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {
 | 
			
		||||
			closed: () => dispose(),
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// prefereces migration
 | 
			
		||||
		// TODO: そのうち消す
 | 
			
		||||
		if (lastVersion && (compareVersions('2025.3.2-alpha.0', lastVersion) === 1)) {
 | 
			
		||||
			console.log('Preferences migration');
 | 
			
		||||
 | 
			
		||||
			store.loaded.then(async () => {
 | 
			
		||||
				const themes = await misskeyApi('i/registry/get', { scope: ['client'], key: 'themes' }).catch(() => []);
 | 
			
		||||
				if (themes.length > 0) {
 | 
			
		||||
					prefer.commit('themes', themes);
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				const plugins = ColdDeviceStorage.get('plugins');
 | 
			
		||||
				prefer.commit('plugins', plugins.map(p => ({
 | 
			
		||||
					...p,
 | 
			
		||||
					installId: (p as any).id,
 | 
			
		||||
					id: undefined,
 | 
			
		||||
				})));
 | 
			
		||||
 | 
			
		||||
				prefer.commit('deck.profile', deckStore.s.profile);
 | 
			
		||||
				misskeyApi('i/registry/keys', {
 | 
			
		||||
					scope: ['client', 'deck', 'profiles'],
 | 
			
		||||
				}).then(async keys => {
 | 
			
		||||
					const profiles: DeckProfile[] = [];
 | 
			
		||||
					for (const key of keys) {
 | 
			
		||||
						const deck = await misskeyApi('i/registry/get', {
 | 
			
		||||
							scope: ['client', 'deck', 'profiles'],
 | 
			
		||||
							key: key,
 | 
			
		||||
						});
 | 
			
		||||
						profiles.push({
 | 
			
		||||
							id: uuid(),
 | 
			
		||||
							name: key,
 | 
			
		||||
							columns: deck.columns,
 | 
			
		||||
							layout: deck.layout,
 | 
			
		||||
						});
 | 
			
		||||
					}
 | 
			
		||||
					prefer.commit('deck.profiles', profiles);
 | 
			
		||||
				});
 | 
			
		||||
 | 
			
		||||
				prefer.commit('lightTheme', ColdDeviceStorage.get('lightTheme'));
 | 
			
		||||
				prefer.commit('darkTheme', ColdDeviceStorage.get('darkTheme'));
 | 
			
		||||
				prefer.commit('syncDeviceDarkMode', ColdDeviceStorage.get('syncDeviceDarkMode'));
 | 
			
		||||
				prefer.commit('emojiPalettes', [{
 | 
			
		||||
					id: 'reactions',
 | 
			
		||||
					name: '',
 | 
			
		||||
					emojis: store.s.reactions,
 | 
			
		||||
				}, {
 | 
			
		||||
					id: 'pinnedEmojis',
 | 
			
		||||
					name: '',
 | 
			
		||||
					emojis: store.s.pinnedEmojis,
 | 
			
		||||
				}]);
 | 
			
		||||
				prefer.commit('emojiPaletteForMain', 'pinnedEmojis');
 | 
			
		||||
				prefer.commit('emojiPaletteForReaction', 'reactions');
 | 
			
		||||
				prefer.commit('overridedDeviceKind', store.s.overridedDeviceKind);
 | 
			
		||||
				prefer.commit('widgets', store.s.widgets);
 | 
			
		||||
				prefer.commit('keepCw', store.s.keepCw);
 | 
			
		||||
				prefer.commit('collapseRenotes', store.s.collapseRenotes);
 | 
			
		||||
				prefer.commit('rememberNoteVisibility', store.s.rememberNoteVisibility);
 | 
			
		||||
				prefer.commit('uploadFolder', store.s.uploadFolder);
 | 
			
		||||
				prefer.commit('keepOriginalUploading', store.s.keepOriginalUploading);
 | 
			
		||||
				prefer.commit('menu', store.s.menu);
 | 
			
		||||
				prefer.commit('statusbars', store.s.statusbars);
 | 
			
		||||
				prefer.commit('pinnedUserLists', store.s.pinnedUserLists);
 | 
			
		||||
				prefer.commit('serverDisconnectedBehavior', store.s.serverDisconnectedBehavior);
 | 
			
		||||
				prefer.commit('nsfw', store.s.nsfw);
 | 
			
		||||
				prefer.commit('highlightSensitiveMedia', store.s.highlightSensitiveMedia);
 | 
			
		||||
				prefer.commit('animation', store.s.animation);
 | 
			
		||||
				prefer.commit('animatedMfm', store.s.animatedMfm);
 | 
			
		||||
				prefer.commit('advancedMfm', store.s.advancedMfm);
 | 
			
		||||
				prefer.commit('showReactionsCount', store.s.showReactionsCount);
 | 
			
		||||
				prefer.commit('enableQuickAddMfmFunction', store.s.enableQuickAddMfmFunction);
 | 
			
		||||
				prefer.commit('loadRawImages', store.s.loadRawImages);
 | 
			
		||||
				prefer.commit('imageNewTab', store.s.imageNewTab);
 | 
			
		||||
				prefer.commit('disableShowingAnimatedImages', store.s.disableShowingAnimatedImages);
 | 
			
		||||
				prefer.commit('emojiStyle', store.s.emojiStyle);
 | 
			
		||||
				prefer.commit('menuStyle', store.s.menuStyle);
 | 
			
		||||
				prefer.commit('useBlurEffectForModal', store.s.useBlurEffectForModal);
 | 
			
		||||
				prefer.commit('useBlurEffect', store.s.useBlurEffect);
 | 
			
		||||
				prefer.commit('showFixedPostForm', store.s.showFixedPostForm);
 | 
			
		||||
				prefer.commit('showFixedPostFormInChannel', store.s.showFixedPostFormInChannel);
 | 
			
		||||
				prefer.commit('enableInfiniteScroll', store.s.enableInfiniteScroll);
 | 
			
		||||
				prefer.commit('useReactionPickerForContextMenu', store.s.useReactionPickerForContextMenu);
 | 
			
		||||
				prefer.commit('showGapBetweenNotesInTimeline', store.s.showGapBetweenNotesInTimeline);
 | 
			
		||||
				prefer.commit('instanceTicker', store.s.instanceTicker);
 | 
			
		||||
				prefer.commit('emojiPickerScale', store.s.emojiPickerScale);
 | 
			
		||||
				prefer.commit('emojiPickerWidth', store.s.emojiPickerWidth);
 | 
			
		||||
				prefer.commit('emojiPickerHeight', store.s.emojiPickerHeight);
 | 
			
		||||
				prefer.commit('emojiPickerStyle', store.s.emojiPickerStyle);
 | 
			
		||||
				prefer.commit('reportError', store.s.reportError);
 | 
			
		||||
				prefer.commit('squareAvatars', store.s.squareAvatars);
 | 
			
		||||
				prefer.commit('showAvatarDecorations', store.s.showAvatarDecorations);
 | 
			
		||||
				prefer.commit('numberOfPageCache', store.s.numberOfPageCache);
 | 
			
		||||
				prefer.commit('showNoteActionsOnlyHover', store.s.showNoteActionsOnlyHover);
 | 
			
		||||
				prefer.commit('showClipButtonInNoteFooter', store.s.showClipButtonInNoteFooter);
 | 
			
		||||
				prefer.commit('reactionsDisplaySize', store.s.reactionsDisplaySize);
 | 
			
		||||
				prefer.commit('limitWidthOfReaction', store.s.limitWidthOfReaction);
 | 
			
		||||
				prefer.commit('forceShowAds', store.s.forceShowAds);
 | 
			
		||||
				prefer.commit('aiChanMode', store.s.aiChanMode);
 | 
			
		||||
				prefer.commit('devMode', store.s.devMode);
 | 
			
		||||
				prefer.commit('mediaListWithOneImageAppearance', store.s.mediaListWithOneImageAppearance);
 | 
			
		||||
				prefer.commit('notificationPosition', store.s.notificationPosition);
 | 
			
		||||
				prefer.commit('notificationStackAxis', store.s.notificationStackAxis);
 | 
			
		||||
				prefer.commit('enableCondensedLine', store.s.enableCondensedLine);
 | 
			
		||||
				prefer.commit('keepScreenOn', store.s.keepScreenOn);
 | 
			
		||||
				prefer.commit('disableStreamingTimeline', store.s.disableStreamingTimeline);
 | 
			
		||||
				prefer.commit('useGroupedNotifications', store.s.useGroupedNotifications);
 | 
			
		||||
				prefer.commit('dataSaver', store.s.dataSaver);
 | 
			
		||||
				prefer.commit('enableSeasonalScreenEffect', store.s.enableSeasonalScreenEffect);
 | 
			
		||||
				prefer.commit('enableHorizontalSwipe', store.s.enableHorizontalSwipe);
 | 
			
		||||
				prefer.commit('useNativeUiForVideoAudioPlayer', store.s.useNativeUIForVideoAudioPlayer);
 | 
			
		||||
				prefer.commit('keepOriginalFilename', store.s.keepOriginalFilename);
 | 
			
		||||
				prefer.commit('alwaysConfirmFollow', store.s.alwaysConfirmFollow);
 | 
			
		||||
				prefer.commit('confirmWhenRevealingSensitiveMedia', store.s.confirmWhenRevealingSensitiveMedia);
 | 
			
		||||
				prefer.commit('contextMenu', store.s.contextMenu);
 | 
			
		||||
				prefer.commit('skipNoteRender', store.s.skipNoteRender);
 | 
			
		||||
				prefer.commit('showSoftWordMutedWord', store.s.showSoftWordMutedWord);
 | 
			
		||||
				prefer.commit('confirmOnReact', store.s.confirmOnReact);
 | 
			
		||||
				prefer.commit('defaultFollowWithReplies', store.s.defaultWithReplies);
 | 
			
		||||
				prefer.commit('sound.masterVolume', store.s.sound_masterVolume);
 | 
			
		||||
				prefer.commit('sound.notUseSound', store.s.sound_notUseSound);
 | 
			
		||||
				prefer.commit('sound.useSoundOnlyWhenActive', store.s.sound_useSoundOnlyWhenActive);
 | 
			
		||||
				prefer.commit('sound.on.note', store.s.sound_note as any);
 | 
			
		||||
				prefer.commit('sound.on.noteMy', store.s.sound_noteMy as any);
 | 
			
		||||
				prefer.commit('sound.on.notification', store.s.sound_notification as any);
 | 
			
		||||
				prefer.commit('sound.on.reaction', store.s.sound_reaction as any);
 | 
			
		||||
 | 
			
		||||
				window.setTimeout(() => {
 | 
			
		||||
					unisonReload();
 | 
			
		||||
				}, 5000);
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const stream = useStream();
 | 
			
		||||
 | 
			
		||||
	let reloadDialogShowing = false;
 | 
			
		||||
	stream.on('_disconnected_', async () => {
 | 
			
		||||
		if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
 | 
			
		||||
		if (prefer.s.serverDisconnectedBehavior === 'reload') {
 | 
			
		||||
			location.reload();
 | 
			
		||||
		} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
 | 
			
		||||
		} else if (prefer.s.serverDisconnectedBehavior === 'dialog') {
 | 
			
		||||
			if (reloadDialogShowing) return;
 | 
			
		||||
			reloadDialogShowing = true;
 | 
			
		||||
			const { canceled } = await confirm({
 | 
			
		||||
@@ -102,30 +242,24 @@ export async function mainBoot() {
 | 
			
		||||
		removeCustomEmojis(emojiData.emojis);
 | 
			
		||||
	});
 | 
			
		||||
 | 
			
		||||
	for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
 | 
			
		||||
		import('@/plugin.js').then(async ({ install }) => {
 | 
			
		||||
			// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
 | 
			
		||||
			await new Promise(r => setTimeout(r, 0));
 | 
			
		||||
			install(plugin);
 | 
			
		||||
		});
 | 
			
		||||
	}
 | 
			
		||||
	launchPlugins();
 | 
			
		||||
 | 
			
		||||
	try {
 | 
			
		||||
		if (defaultStore.state.enableSeasonalScreenEffect) {
 | 
			
		||||
		if (prefer.s.enableSeasonalScreenEffect) {
 | 
			
		||||
			const month = new Date().getMonth() + 1;
 | 
			
		||||
			if (defaultStore.state.hemisphere === 'S') {
 | 
			
		||||
			if (prefer.s.hemisphere === 'S') {
 | 
			
		||||
				// ▼南半球
 | 
			
		||||
				if (month === 7 || month === 8) {
 | 
			
		||||
					const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
 | 
			
		||||
					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
 | 
			
		||||
					new SnowfallEffect({}).render();
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				// ▼北半球
 | 
			
		||||
				if (month === 12 || month === 1) {
 | 
			
		||||
					const SnowfallEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
 | 
			
		||||
					const SnowfallEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
 | 
			
		||||
					new SnowfallEffect({}).render();
 | 
			
		||||
				} else if (month === 3 || month === 4) {
 | 
			
		||||
					const SakuraEffect = (await import('@/scripts/snowfall-effect.js')).SnowfallEffect;
 | 
			
		||||
					const SakuraEffect = (await import('@/utility/snowfall-effect.js')).SnowfallEffect;
 | 
			
		||||
					new SakuraEffect({
 | 
			
		||||
						sakura: true,
 | 
			
		||||
					}).render();
 | 
			
		||||
@@ -138,8 +272,8 @@ export async function mainBoot() {
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if ($i) {
 | 
			
		||||
		defaultStore.loaded.then(() => {
 | 
			
		||||
			if (defaultStore.state.accountSetupWizard !== -1) {
 | 
			
		||||
		store.loaded.then(async () => {
 | 
			
		||||
			if (store.s.accountSetupWizard !== -1) {
 | 
			
		||||
				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {
 | 
			
		||||
					closed: () => dispose(),
 | 
			
		||||
				});
 | 
			
		||||
@@ -154,7 +288,7 @@ export async function mainBoot() {
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		function onAnnouncementCreated (ev: { announcement: Misskey.entities.Announcement }) {
 | 
			
		||||
		function onAnnouncementCreated(ev: { announcement: Misskey.entities.Announcement }) {
 | 
			
		||||
			const announcement = ev.announcement;
 | 
			
		||||
			if (announcement.display === 'dialog') {
 | 
			
		||||
				const { dispose } = popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
 | 
			
		||||
@@ -348,11 +482,11 @@ export async function mainBoot() {
 | 
			
		||||
 | 
			
		||||
		// 自分の情報が更新されたとき
 | 
			
		||||
		main.on('meUpdated', i => {
 | 
			
		||||
			updateAccountPartial(i);
 | 
			
		||||
			updateCurrentAccountPartial(i);
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		main.on('readAllNotifications', () => {
 | 
			
		||||
			updateAccountPartial({
 | 
			
		||||
			updateCurrentAccountPartial({
 | 
			
		||||
				hasUnreadNotification: false,
 | 
			
		||||
				unreadNotificationsCount: 0,
 | 
			
		||||
			});
 | 
			
		||||
@@ -360,39 +494,39 @@ export async function mainBoot() {
 | 
			
		||||
 | 
			
		||||
		main.on('unreadNotification', () => {
 | 
			
		||||
			const unreadNotificationsCount = ($i?.unreadNotificationsCount ?? 0) + 1;
 | 
			
		||||
			updateAccountPartial({
 | 
			
		||||
			updateCurrentAccountPartial({
 | 
			
		||||
				hasUnreadNotification: true,
 | 
			
		||||
				unreadNotificationsCount,
 | 
			
		||||
			});
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		main.on('unreadMention', () => {
 | 
			
		||||
			updateAccountPartial({ hasUnreadMentions: true });
 | 
			
		||||
			updateCurrentAccountPartial({ hasUnreadMentions: true });
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		main.on('readAllUnreadMentions', () => {
 | 
			
		||||
			updateAccountPartial({ hasUnreadMentions: false });
 | 
			
		||||
			updateCurrentAccountPartial({ hasUnreadMentions: false });
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		main.on('unreadSpecifiedNote', () => {
 | 
			
		||||
			updateAccountPartial({ hasUnreadSpecifiedNotes: true });
 | 
			
		||||
			updateCurrentAccountPartial({ hasUnreadSpecifiedNotes: true });
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		main.on('readAllUnreadSpecifiedNotes', () => {
 | 
			
		||||
			updateAccountPartial({ hasUnreadSpecifiedNotes: false });
 | 
			
		||||
			updateCurrentAccountPartial({ hasUnreadSpecifiedNotes: false });
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		main.on('readAllAntennas', () => {
 | 
			
		||||
			updateAccountPartial({ hasUnreadAntenna: false });
 | 
			
		||||
			updateCurrentAccountPartial({ hasUnreadAntenna: false });
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		main.on('unreadAntenna', () => {
 | 
			
		||||
			updateAccountPartial({ hasUnreadAntenna: true });
 | 
			
		||||
			updateCurrentAccountPartial({ hasUnreadAntenna: true });
 | 
			
		||||
			sound.playMisskeySfx('antenna');
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		main.on('readAllAnnouncements', () => {
 | 
			
		||||
			updateAccountPartial({ hasUnreadAnnouncement: false });
 | 
			
		||||
			updateCurrentAccountPartial({ hasUnreadAnnouncement: false });
 | 
			
		||||
		});
 | 
			
		||||
 | 
			
		||||
		// 個人宛てお知らせが発行されたとき
 | 
			
		||||
@@ -412,7 +546,7 @@ export async function mainBoot() {
 | 
			
		||||
			post();
 | 
			
		||||
		},
 | 
			
		||||
		'd': () => {
 | 
			
		||||
			defaultStore.set('darkMode', !defaultStore.state.darkMode);
 | 
			
		||||
			store.set('darkMode', !store.s.darkMode);
 | 
			
		||||
		},
 | 
			
		||||
		's': () => {
 | 
			
		||||
			mainRouter.push('/search');
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
 | 
			
		||||
import { createApp, defineAsyncComponent } from 'vue';
 | 
			
		||||
import { common } from './common.js';
 | 
			
		||||
import { emojiPicker } from '@/scripts/emoji-picker.js';
 | 
			
		||||
import { emojiPicker } from '@/utility/emoji-picker.js';
 | 
			
		||||
 | 
			
		||||
export async function subBoot() {
 | 
			
		||||
	const { isClientUpdated } = await common(() => createApp(
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,8 @@
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { Cache } from '@/scripts/cache.js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { Cache } from '@/utility/cache.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
 | 
			
		||||
export const clipsCache = new Cache<Misskey.entities.Clip[]>(1000 * 60 * 30, () => misskeyApi('clips/list'));
 | 
			
		||||
export const rolesCache = new Cache(1000 * 60 * 30, () => misskeyApi('admin/roles/list'));
 | 
			
		||||
 
 | 
			
		||||
@@ -90,7 +90,7 @@ import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import RouterView from '@/components/global/RouterView.vue';
 | 
			
		||||
import { useRouterFactory } from '@/router/supplier';
 | 
			
		||||
import MkTextarea from '@/components/MkTextarea.vue';
 | 
			
		||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 | 
			
		||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	report: Misskey.entities.AdminAbuseUserReportsResponse[number];
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@ import * as Misskey from 'misskey-js';
 | 
			
		||||
import MkMention from './MkMention.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { host as localHost } from '@@/js/config.js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
 | 
			
		||||
const user = ref<Misskey.entities.UserLite>();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ import { HttpResponse, http } from 'msw';
 | 
			
		||||
import { userDetailed } from '../../.storybook/fakes.js';
 | 
			
		||||
import { commonHandlers } from '../../.storybook/mocks.js';
 | 
			
		||||
import MkAchievements from './MkAchievements.vue';
 | 
			
		||||
import { ACHIEVEMENT_TYPES } from '@/scripts/achievements.js';
 | 
			
		||||
import { ACHIEVEMENT_TYPES } from '@/utility/achievements.js';
 | 
			
		||||
export const Empty = {
 | 
			
		||||
	render(args) {
 | 
			
		||||
		return {
 | 
			
		||||
 
 | 
			
		||||
@@ -55,9 +55,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { onMounted, ref, computed } from 'vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/scripts/achievements.js';
 | 
			
		||||
import { ACHIEVEMENT_TYPES, ACHIEVEMENT_BADGES, claimAchievement } from '@/utility/achievements.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	user: Misskey.entities.User;
 | 
			
		||||
 
 | 
			
		||||
@@ -82,7 +82,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import { computed, onMounted, onBeforeUnmount, ref } from 'vue';
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import { globalEvents } from '@/events.js';
 | 
			
		||||
import { defaultIdlingRenderScheduler } from '@/scripts/idle-render.js';
 | 
			
		||||
import { defaultIdlingRenderScheduler } from '@/utility/idle-render.js';
 | 
			
		||||
 | 
			
		||||
// https://stackoverflow.com/questions/1878907/how-can-i-find-the-difference-between-two-angles
 | 
			
		||||
const angleDiff = (a: number, b: number) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,11 +25,12 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import { onMounted, shallowRef } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import MkModal from '@/components/MkModal.vue';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { $i, updateAccountPartial } from '@/account.js';
 | 
			
		||||
import { $i } from '@/i.js';
 | 
			
		||||
import { updateCurrentAccountPartial } from '@/accounts.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	announcement: Misskey.entities.Announcement;
 | 
			
		||||
@@ -51,7 +52,7 @@ async function ok() {
 | 
			
		||||
 | 
			
		||||
	modal.value?.close();
 | 
			
		||||
	misskeyApi('i/read-announcement', { announcementId: props.announcement.id });
 | 
			
		||||
	updateAccountPartial({
 | 
			
		||||
	updateCurrentAccountPartial({
 | 
			
		||||
		unreadAnnouncements: $i!.unreadAnnouncements.filter(a => a.id !== props.announcement.id),
 | 
			
		||||
	});
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -59,10 +59,10 @@ import MkTextarea from '@/components/MkTextarea.vue';
 | 
			
		||||
import MkSelect from '@/components/MkSelect.vue';
 | 
			
		||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { deepMerge } from '@/scripts/merge.js';
 | 
			
		||||
import type { DeepPartial } from '@/scripts/merge.js';
 | 
			
		||||
import { deepMerge } from '@/utility/merge.js';
 | 
			
		||||
import type { DeepPartial } from '@/utility/merge.js';
 | 
			
		||||
 | 
			
		||||
type PartialAllowedAntenna = Omit<Misskey.entities.Antenna, 'id' | 'createdAt' | 'updatedAt'> & {
 | 
			
		||||
	id?: string;
 | 
			
		||||
 
 | 
			
		||||
@@ -71,7 +71,7 @@ import MkInput from '@/components/MkInput.vue';
 | 
			
		||||
import MkSwitch from '@/components/MkSwitch.vue';
 | 
			
		||||
import MkTextarea from '@/components/MkTextarea.vue';
 | 
			
		||||
import MkSelect from '@/components/MkSelect.vue';
 | 
			
		||||
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/scripts/aiscript/ui.js';
 | 
			
		||||
import type { AsUiComponent, AsUiRoot, AsUiPostFormButton } from '@/aiscript/ui.js';
 | 
			
		||||
import MkFolder from '@/components/MkFolder.vue';
 | 
			
		||||
import MkPostForm from '@/components/MkPostForm.vue';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -117,14 +117,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
<script setup lang="ts">
 | 
			
		||||
import { ref, computed } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
 | 
			
		||||
import { $i, getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/account.js';
 | 
			
		||||
import { $i } from '@/i.js';
 | 
			
		||||
import { getAccounts, getAccountWithSigninDialog, getAccountWithSignupDialog } from '@/accounts.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	name?: string;
 | 
			
		||||
@@ -158,7 +157,7 @@ async function init() {
 | 
			
		||||
 | 
			
		||||
	const accounts = await getAccounts();
 | 
			
		||||
 | 
			
		||||
	const accountIdsToFetch = accounts.map(a => a.id).filter(id => !users.value.has(id));
 | 
			
		||||
	const accountIdsToFetch = accounts.map(a => a.user.id).filter(id => !users.value.has(id));
 | 
			
		||||
 | 
			
		||||
	if (accountIdsToFetch.length > 0) {
 | 
			
		||||
		const usersRes = await misskeyApi('users/show', {
 | 
			
		||||
@@ -170,7 +169,7 @@ async function init() {
 | 
			
		||||
 | 
			
		||||
			users.value.set(user.id, {
 | 
			
		||||
				...user,
 | 
			
		||||
				token: accounts.find(a => a.id === user.id)!.token,
 | 
			
		||||
				token: accounts.find(a => a.user.id === user.id)!.token,
 | 
			
		||||
			});
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ import { userDetailed } from '../../.storybook/fakes.js';
 | 
			
		||||
import { commonHandlers } from '../../.storybook/mocks.js';
 | 
			
		||||
import MkAutocomplete from './MkAutocomplete.vue';
 | 
			
		||||
import MkInput from './MkInput.vue';
 | 
			
		||||
import { tick } from '@/scripts/test-utils.js';
 | 
			
		||||
import { tick } from '@/utility/test-utils.js';
 | 
			
		||||
const common = {
 | 
			
		||||
	render(args) {
 | 
			
		||||
		return {
 | 
			
		||||
 
 | 
			
		||||
@@ -49,22 +49,23 @@ import sanitizeHtml from 'sanitize-html';
 | 
			
		||||
import { emojilist, getEmojiName } from '@@/js/emojilist.js';
 | 
			
		||||
import { char2twemojiFilePath, char2fluentEmojiFilePath } from '@@/js/emoji-base.js';
 | 
			
		||||
import { MFM_TAGS, MFM_PARAMS } from '@@/js/const.js';
 | 
			
		||||
import type { EmojiDef } from '@/scripts/search-emoji.js';
 | 
			
		||||
import contains from '@/scripts/contains.js';
 | 
			
		||||
import type { EmojiDef } from '@/utility/search-emoji.js';
 | 
			
		||||
import contains from '@/utility/contains.js';
 | 
			
		||||
import { acct } from '@/filters/user.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { miLocalStorage } from '@/local-storage.js';
 | 
			
		||||
import { customEmojis } from '@/custom-emojis.js';
 | 
			
		||||
import { searchEmoji } from '@/scripts/search-emoji.js';
 | 
			
		||||
import { searchEmoji } from '@/utility/search-emoji.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
 | 
			
		||||
const lib = emojilist.filter(x => x.category !== 'flags');
 | 
			
		||||
 | 
			
		||||
const emojiDb = computed(() => {
 | 
			
		||||
	//#region Unicode Emoji
 | 
			
		||||
	const char2path = defaultStore.reactiveState.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 | 
			
		||||
	const char2path = prefer.r.emojiStyle.value === 'twemoji' ? char2twemojiFilePath : char2fluentEmojiFilePath;
 | 
			
		||||
 | 
			
		||||
	const unicodeEmojiDB: EmojiDef[] = lib.map(x => ({
 | 
			
		||||
		emoji: x.char,
 | 
			
		||||
@@ -72,7 +73,7 @@ const emojiDb = computed(() => {
 | 
			
		||||
		url: char2path(x.char),
 | 
			
		||||
	}));
 | 
			
		||||
 | 
			
		||||
	for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
 | 
			
		||||
	for (const index of Object.values(store.s.additionalUnicodeEmojiIndexes)) {
 | 
			
		||||
		for (const [emoji, keywords] of Object.entries(index)) {
 | 
			
		||||
			for (const k of keywords) {
 | 
			
		||||
				unicodeEmojiDB.push({
 | 
			
		||||
@@ -154,10 +155,10 @@ function complete(type: string, value: any) {
 | 
			
		||||
	emit('done', { type, value });
 | 
			
		||||
	emit('closed');
 | 
			
		||||
	if (type === 'emoji') {
 | 
			
		||||
		let recents = defaultStore.state.recentlyUsedEmojis;
 | 
			
		||||
		let recents = store.s.recentlyUsedEmojis;
 | 
			
		||||
		recents = recents.filter((emoji: any) => emoji !== value);
 | 
			
		||||
		recents.unshift(value);
 | 
			
		||||
		defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
		store.set('recentlyUsedEmojis', recents.splice(0, 32));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -237,7 +238,7 @@ function exec() {
 | 
			
		||||
	} else if (props.type === 'emoji') {
 | 
			
		||||
		if (!props.q || props.q === '') {
 | 
			
		||||
			// 最近使った絵文字をサジェスト
 | 
			
		||||
			emojis.value = defaultStore.state.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
 | 
			
		||||
			emojis.value = store.s.recentlyUsedEmojis.map(emoji => emojiDb.value.find(dbEmoji => dbEmoji.emoji === emoji)).filter(x => x) as EmojiDef[];
 | 
			
		||||
			return;
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	userIds: string[];
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
<button
 | 
			
		||||
	v-if="!link"
 | 
			
		||||
	ref="el" class="_button"
 | 
			
		||||
	:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
 | 
			
		||||
	:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly }]"
 | 
			
		||||
	:type="type"
 | 
			
		||||
	:name="name"
 | 
			
		||||
	:value="value"
 | 
			
		||||
@@ -22,7 +22,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
</button>
 | 
			
		||||
<MkA
 | 
			
		||||
	v-else class="_button"
 | 
			
		||||
	:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike }]"
 | 
			
		||||
	:class="[$style.root, { [$style.inline]: inline, [$style.primary]: primary, [$style.gradate]: gradate, [$style.danger]: danger, [$style.rounded]: rounded, [$style.full]: full, [$style.small]: small, [$style.large]: large, [$style.transparent]: transparent, [$style.asLike]: asLike, [$style.iconOnly]: iconOnly }]"
 | 
			
		||||
	:to="to ?? '#'"
 | 
			
		||||
	:behavior="linkBehavior"
 | 
			
		||||
	@mousedown="onMousedown"
 | 
			
		||||
@@ -57,6 +57,7 @@ const props = defineProps<{
 | 
			
		||||
	name?: string;
 | 
			
		||||
	value?: string;
 | 
			
		||||
	disabled?: boolean;
 | 
			
		||||
	iconOnly?: boolean;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
@@ -147,6 +148,11 @@ function onMousedown(evt: MouseEvent): void {
 | 
			
		||||
		background: var(--MI_THEME-buttonHoverBg);
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.iconOnly {
 | 
			
		||||
		padding: 7px;
 | 
			
		||||
		min-width: auto;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.small {
 | 
			
		||||
		font-size: 90%;
 | 
			
		||||
		padding: 6px 12px;
 | 
			
		||||
@@ -220,28 +226,28 @@ function onMousedown(evt: MouseEvent): void {
 | 
			
		||||
		background: linear-gradient(90deg, var(--MI_THEME-buttonGradateA), var(--MI_THEME-buttonGradateB));
 | 
			
		||||
 | 
			
		||||
		&:not(:disabled):hover {
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		&:not(:disabled):active {
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-accent) h s calc(l + 5)), hsl(from var(--MI_THEME-accent) h s calc(l + 5)));
 | 
			
		||||
			background: linear-gradient(90deg, hsl(from var(--MI_THEME-buttonGradateA) h s calc(l + 5)), hsl(from var(--MI_THEME-buttonGradateB) h s calc(l + 5)));
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	&.danger {
 | 
			
		||||
		font-weight: bold;
 | 
			
		||||
		color: #ff2a2a;
 | 
			
		||||
		color: var(--MI_THEME-error);
 | 
			
		||||
 | 
			
		||||
		&.primary {
 | 
			
		||||
			color: #fff;
 | 
			
		||||
			background: #ff2a2a;
 | 
			
		||||
			background: var(--MI_THEME-error);
 | 
			
		||||
 | 
			
		||||
			&:not(:disabled):hover {
 | 
			
		||||
				background: #ff4242;
 | 
			
		||||
				background: hsl(from var(--MI_THEME-error) h s calc(l + 10));
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			&:not(:disabled):active {
 | 
			
		||||
				background: #d42e2e;
 | 
			
		||||
				background: hsl(from var(--MI_THEME-error) h s calc(l - 10));
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, shallowRef, computed, onMounted, onBeforeUnmount, watch, onUnmounted } from 'vue';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
 | 
			
		||||
// APIs provided by Captcha services
 | 
			
		||||
// see: https://docs.hcaptcha.com/configuration/#javascript-api
 | 
			
		||||
@@ -154,7 +154,7 @@ async function requestRender() {
 | 
			
		||||
 | 
			
		||||
		captchaWidgetId.value = captcha.value.render(elem, {
 | 
			
		||||
			sitekey: props.sitekey,
 | 
			
		||||
			theme: defaultStore.state.darkMode ? 'dark' : 'light',
 | 
			
		||||
			theme: store.s.darkMode ? 'dark' : 'light',
 | 
			
		||||
			callback: callback,
 | 
			
		||||
			'expired-callback': () => callback(undefined),
 | 
			
		||||
			'error-callback': () => callback(undefined),
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { misskeyApi } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { misskeyApi } from '@/utility/misskey-api.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
 
 | 
			
		||||
@@ -53,15 +53,15 @@ export type ChartSrc =
 | 
			
		||||
import { onMounted, ref, shallowRef, watch } from 'vue';
 | 
			
		||||
import { Chart } from 'chart.js';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { useChartTooltip } from '@/scripts/use-chart-tooltip.js';
 | 
			
		||||
import { chartVLine } from '@/scripts/chart-vline.js';
 | 
			
		||||
import { alpha } from '@/scripts/color.js';
 | 
			
		||||
import { misskeyApiGet } from '@/utility/misskey-api.js';
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
import { useChartTooltip } from '@/use/use-chart-tooltip.js';
 | 
			
		||||
import { chartVLine } from '@/utility/chart-vline.js';
 | 
			
		||||
import { alpha } from '@/utility/color.js';
 | 
			
		||||
import date from '@/filters/date.js';
 | 
			
		||||
import bytes from '@/filters/bytes.js';
 | 
			
		||||
import { initChart } from '@/scripts/init-chart.js';
 | 
			
		||||
import { chartLegend } from '@/scripts/chart-legend.js';
 | 
			
		||||
import { initChart } from '@/utility/init-chart.js';
 | 
			
		||||
import { chartLegend } from '@/utility/chart-legend.js';
 | 
			
		||||
import MkChartLegend from '@/components/MkChartLegend.vue';
 | 
			
		||||
 | 
			
		||||
initChart();
 | 
			
		||||
@@ -161,7 +161,7 @@ const render = () => {
 | 
			
		||||
		chartInstance.destroy();
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	const vLineColor = defaultStore.state.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
	const vLineColor = store.s.darkMode ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
 | 
			
		||||
 | 
			
		||||
	const maxes = chartData.series.map((x, i) => Math.max(...x.data.map(d => d.y)));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -23,9 +23,9 @@ import { computed, onMounted, onUnmounted, ref } from 'vue';
 | 
			
		||||
import MkPlusOneEffect from '@/components/MkPlusOneEffect.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { useInterval } from '@@/js/use-interval.js';
 | 
			
		||||
import * as game from '@/scripts/clicker-game.js';
 | 
			
		||||
import * as game from '@/utility/clicker-game.js';
 | 
			
		||||
import number from '@/filters/number.js';
 | 
			
		||||
import { claimAchievement } from '@/scripts/achievements.js';
 | 
			
		||||
import { claimAchievement } from '@/utility/achievements.js';
 | 
			
		||||
 | 
			
		||||
const saveData = game.saveData;
 | 
			
		||||
const cookies = computed(() => saveData.value?.cookies);
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { $i } from '@/i.js';
 | 
			
		||||
import number from '@/filters/number.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import { computed, ref, watch } from 'vue';
 | 
			
		||||
import { bundledLanguagesInfo } from 'shiki/langs';
 | 
			
		||||
import type { BundledLanguage } from 'shiki/langs';
 | 
			
		||||
import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { getHighlighter, getTheme } from '@/utility/code-highlighter.js';
 | 
			
		||||
import { store } from '@/store.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
	code: string;
 | 
			
		||||
@@ -22,7 +22,7 @@ const props = defineProps<{
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const highlighter = await getHighlighter();
 | 
			
		||||
const darkMode = defaultStore.reactiveState.darkMode;
 | 
			
		||||
const darkMode = store.r.darkMode;
 | 
			
		||||
const codeLang = ref<BundledLanguage | 'aiscript'>('js');
 | 
			
		||||
 | 
			
		||||
const [lightThemeName, darkThemeName] = await Promise.all([
 | 
			
		||||
@@ -74,10 +74,8 @@ watch(() => props.lang, (to) => {
 | 
			
		||||
<style module lang="scss">
 | 
			
		||||
.codeBlockRoot :global(.shiki) {
 | 
			
		||||
	padding: 1em;
 | 
			
		||||
	margin: .5em 0;
 | 
			
		||||
	margin: 0;
 | 
			
		||||
	overflow: auto;
 | 
			
		||||
	border-radius: 8px;
 | 
			
		||||
	border: 1px solid var(--MI_THEME-divider);
 | 
			
		||||
	font-family: Consolas, Monaco, Andale Mono, Ubuntu Mono, monospace;
 | 
			
		||||
 | 
			
		||||
	color: var(--shiki-fallback);
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
	</button>
 | 
			
		||||
	<Suspense>
 | 
			
		||||
		<template #fallback>
 | 
			
		||||
			<MkLoading />
 | 
			
		||||
			<MkLoading/>
 | 
			
		||||
		</template>
 | 
			
		||||
		<XCode v-if="show && lang" :code="code" :lang="lang"/>
 | 
			
		||||
		<pre v-else-if="show" :class="$style.codeBlockFallbackRoot"><code :class="$style.codeBlockFallbackCode">{{ code }}</code></pre>
 | 
			
		||||
@@ -28,9 +28,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import { defineAsyncComponent, ref } from 'vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import MkLoading from '@/components/global/MkLoading.vue';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { copyToClipboard } from '@/scripts/copy-to-clipboard.js';
 | 
			
		||||
import { copyToClipboard } from '@/utility/copy-to-clipboard.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
	code: string;
 | 
			
		||||
@@ -42,7 +42,7 @@ const props = withDefaults(defineProps<{
 | 
			
		||||
	forceShow: false,
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
const show = ref(props.forceShow === true ? true : !defaultStore.state.dataSaver.code);
 | 
			
		||||
const show = ref(props.forceShow === true ? true : !prefer.s.dataSaver.code);
 | 
			
		||||
 | 
			
		||||
const XCode = defineAsyncComponent(() => import('@/components/MkCode.core.vue'));
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,10 +19,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
		</div>
 | 
			
		||||
	</header>
 | 
			
		||||
	<Transition
 | 
			
		||||
		:enterActiveClass="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
 | 
			
		||||
		:leaveActiveClass="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
 | 
			
		||||
		:enterFromClass="defaultStore.state.animation ? $style.transition_toggle_enterFrom : ''"
 | 
			
		||||
		:leaveToClass="defaultStore.state.animation ? $style.transition_toggle_leaveTo : ''"
 | 
			
		||||
		:enterActiveClass="prefer.s.animation ? $style.transition_toggle_enterActive : ''"
 | 
			
		||||
		:leaveActiveClass="prefer.s.animation ? $style.transition_toggle_leaveActive : ''"
 | 
			
		||||
		:enterFromClass="prefer.s.animation ? $style.transition_toggle_enterFrom : ''"
 | 
			
		||||
		:leaveToClass="prefer.s.animation ? $style.transition_toggle_leaveTo : ''"
 | 
			
		||||
		@enter="enter"
 | 
			
		||||
		@afterEnter="afterEnter"
 | 
			
		||||
		@leave="leave"
 | 
			
		||||
@@ -40,7 +40,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { onMounted, onUnmounted, ref, shallowRef, watch } from 'vue';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
 | 
			
		||||
const props = withDefaults(defineProps<{
 | 
			
		||||
 
 | 
			
		||||
@@ -6,10 +6,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
<template>
 | 
			
		||||
<Transition
 | 
			
		||||
	appear
 | 
			
		||||
	:enterActiveClass="defaultStore.state.animation ? $style.transition_fade_enterActive : ''"
 | 
			
		||||
	:leaveActiveClass="defaultStore.state.animation ? $style.transition_fade_leaveActive : ''"
 | 
			
		||||
	:enterFromClass="defaultStore.state.animation ? $style.transition_fade_enterFrom : ''"
 | 
			
		||||
	:leaveToClass="defaultStore.state.animation ? $style.transition_fade_leaveTo : ''"
 | 
			
		||||
	:enterActiveClass="prefer.s.animation ? $style.transition_fade_enterActive : ''"
 | 
			
		||||
	:leaveActiveClass="prefer.s.animation ? $style.transition_fade_leaveActive : ''"
 | 
			
		||||
	:enterFromClass="prefer.s.animation ? $style.transition_fade_enterFrom : ''"
 | 
			
		||||
	:leaveToClass="prefer.s.animation ? $style.transition_fade_leaveTo : ''"
 | 
			
		||||
>
 | 
			
		||||
	<div ref="rootEl" :class="$style.root" :style="{ zIndex }" @contextmenu.prevent.stop="() => {}">
 | 
			
		||||
		<MkMenu :items="items" :align="'left'" @close="emit('closed')"/>
 | 
			
		||||
@@ -21,8 +21,8 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import { onMounted, onBeforeUnmount, shallowRef, ref } from 'vue';
 | 
			
		||||
import MkMenu from './MkMenu.vue';
 | 
			
		||||
import type { MenuItem } from '@/types/menu.js';
 | 
			
		||||
import contains from '@/scripts/contains.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import contains from '@/utility/contains.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
 | 
			
		||||
const props = defineProps<{
 | 
			
		||||
 
 | 
			
		||||
@@ -35,13 +35,13 @@ import { onMounted, shallowRef, ref } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import Cropper from 'cropperjs';
 | 
			
		||||
import tinycolor from 'tinycolor2';
 | 
			
		||||
import { apiUrl } from '@@/js/config.js';
 | 
			
		||||
import MkModalWindow from '@/components/MkModalWindow.vue';
 | 
			
		||||
import * as os from '@/os.js';
 | 
			
		||||
import { $i } from '@/account.js';
 | 
			
		||||
import { defaultStore } from '@/store.js';
 | 
			
		||||
import { apiUrl } from '@@/js/config.js';
 | 
			
		||||
import { $i } from '@/i.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import { getProxiedImageUrl } from '@/scripts/media-proxy.js';
 | 
			
		||||
import { getProxiedImageUrl } from '@/utility/media-proxy.js';
 | 
			
		||||
import { prefer } from '@/preferences.js';
 | 
			
		||||
 | 
			
		||||
const emit = defineEmits<{
 | 
			
		||||
	(ev: 'ok', cropped: Misskey.entities.DriveFile): void;
 | 
			
		||||
@@ -81,8 +81,8 @@ const ok = async () => {
 | 
			
		||||
			formData.append('i', $i!.token);
 | 
			
		||||
			if (props.uploadFolder) {
 | 
			
		||||
				formData.append('folderId', props.uploadFolder);
 | 
			
		||||
			} else if (props.uploadFolder !== null && defaultStore.state.uploadFolder) {
 | 
			
		||||
				formData.append('folderId', defaultStore.state.uploadFolder);
 | 
			
		||||
			} else if (props.uploadFolder !== null && prefer.s.uploadFolder) {
 | 
			
		||||
				formData.append('folderId', prefer.s.uploadFolder);
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			window.fetch(apiUrl + '/drive/files/create', {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
import { computed } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import type { PollEditorModelValue } from '@/components/MkPollEditor.vue';
 | 
			
		||||
import { concat } from '@/scripts/array.js';
 | 
			
		||||
import { concat } from '@/utility/array.js';
 | 
			
		||||
import { i18n } from '@/i18n.js';
 | 
			
		||||
import MkButton from '@/components/MkButton.vue';
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||