From 42bc2817be1d8e50a4d3d12fc74767f7920bacd4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Sun, 29 Oct 2023 09:40:48 +0900
Subject: [PATCH 01/36] =?UTF-8?q?=E9=A0=BB=E7=B9=81=E3=81=ABAP=E3=83=AA?=
 =?UTF-8?q?=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88=E3=81=8C=E7=99=BA=E7=94=9F?=
 =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=B5=E3=83=BC=E3=83=90=E3=83=BC=E3=81=AB?=
 =?UTF-8?q?=E5=AF=BE=E3=81=97=E3=81=A6fetchInstanceMetadata=E6=89=8B?=
 =?UTF-8?q?=E5=8B=95=E6=9B=B4=E6=96=B0=E3=81=8C=E7=84=A1=E8=A6=96=E3=81=95?=
 =?UTF-8?q?=E3=82=8C=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3?=
 =?UTF-8?q?=20(MisskeyIO#194)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/backend/src/core/FetchInstanceMetadataService.ts | 8 ++++----
 .../backend/test/unit/FetchInstanceMetadataService.ts     | 5 +++--
 2 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/packages/backend/src/core/FetchInstanceMetadataService.ts b/packages/backend/src/core/FetchInstanceMetadataService.ts
index 24efd91c70..3f55a12f6e 100644
--- a/packages/backend/src/core/FetchInstanceMetadataService.ts
+++ b/packages/backend/src/core/FetchInstanceMetadataService.ts
@@ -52,20 +52,20 @@ export class FetchInstanceMetadataService {
 
 	@bindThis
 	public async tryLock(host: string): Promise<boolean> {
-		const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, '1', 'EX', 60 * 5, 'NX', 'GET');
-		return mutex !== '1';
+		const mutex = await this.redisClient.set(`fetchInstanceMetadata:mutex:${host}`, Date.now(), 'EX', 60 * 5, 'NX');
+		return mutex !== null;
 	}
 
 	@bindThis
 	public unlock(host: string): Promise<number> {
-		return this.redisClient.del(`fetchInstanceMetadata:mutex:${host}`);
+		return this.redisClient.unlink(`fetchInstanceMetadata:mutex:${host}`);
 	}
 
 	@bindThis
 	public async fetchInstanceMetadata(instance: MiInstance, force = false): Promise<void> {
 		const host = instance.host;
 		// Acquire mutex to ensure no parallel runs
-		if (!await this.tryLock(host)) return;
+		if (!await this.tryLock(host) && !force) return;
 		try {
 			if (!force) {
 				const _instance = await this.federatedInstanceService.fetch(host);
diff --git a/packages/backend/test/unit/FetchInstanceMetadataService.ts b/packages/backend/test/unit/FetchInstanceMetadataService.ts
index 22ce023216..9850aa181d 100644
--- a/packages/backend/test/unit/FetchInstanceMetadataService.ts
+++ b/packages/backend/test/unit/FetchInstanceMetadataService.ts
@@ -23,9 +23,10 @@ import type { MockFunctionMetadata } from 'jest-mock';
 function mockRedis() {
 	const hash = {};
 	const set = jest.fn((key, value) => {
-		const ret = hash[key];
+		// このテストで呼び出すSETにはNXオプションが付いてる
+		if (hash[key]) return null;
 		hash[key] = value;
-		return ret;
+		return 'OK';
 	});
 	return set;
 }

From db7a17d39631721913cf99e3af0d749c0a5b229d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 30 Oct 2023 00:27:20 +0900
Subject: [PATCH 02/36] =?UTF-8?q?=E3=83=86=E3=82=B9=E3=83=88=E3=81=AA?=
 =?UTF-8?q?=E3=81=A9=E3=81=ABKeyDB=E3=82=92=E4=BD=BF=E3=81=86=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=20(MisskeyIO#195)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .config/docker_example.yml                   |  6 +++---
 .devcontainer/devcontainer.yml               |  6 +++---
 .devcontainer/docker-compose.yml             | 10 +++++-----
 .dockerignore                                |  1 +
 .github/workflows/test-backend.yml           |  6 +++---
 .github/workflows/test-frontend.yml          |  6 +++---
 .gitignore                                   |  2 ++
 chart/templates/Deployment.yml               |  6 +++---
 docker-compose.yml.example                   | 12 ++++++------
 packages/backend/src/core/FeaturedService.ts | 10 +++++-----
 packages/backend/test/docker-compose.yml     |  6 +++---
 11 files changed, 37 insertions(+), 34 deletions(-)

diff --git a/.config/docker_example.yml b/.config/docker_example.yml
index d457569167..b3d26ca5a6 100644
--- a/.config/docker_example.yml
+++ b/.config/docker_example.yml
@@ -72,7 +72,7 @@ dbReplications: false
 #───┘ Redis configuration └─────────────────────────────────────
 
 redis:
-  host: redis
+  host: keydb
   port: 6379
   #family: 0  # 0=Both, 4=IPv4, 6=IPv6
   #pass: example-pass
@@ -80,7 +80,7 @@ redis:
   #db: 1
 
 #redisForPubsub:
-#  host: redis
+#  host: keydb
 #  port: 6379
 #  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 #  #pass: example-pass
@@ -88,7 +88,7 @@ redis:
 #  #db: 1
 
 #redisForJobQueue:
-#  host: redis
+#  host: keydb
 #  port: 6379
 #  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 #  #pass: example-pass
diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml
index 824a046dc0..9e95ca4b41 100644
--- a/.devcontainer/devcontainer.yml
+++ b/.devcontainer/devcontainer.yml
@@ -72,7 +72,7 @@ dbReplications: false
 #───┘ Redis configuration └─────────────────────────────────────
 
 redis:
-  host: redis
+  host: keydb
   port: 6379
   #family: 0  # 0=Both, 4=IPv4, 6=IPv6
   #pass: example-pass
@@ -80,7 +80,7 @@ redis:
   #db: 1
 
 #redisForPubsub:
-#  host: redis
+#  host: keydb
 #  port: 6379
 #  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 #  #pass: example-pass
@@ -88,7 +88,7 @@ redis:
 #  #db: 1
 
 #redisForJobQueue:
-#  host: redis
+#  host: keydb
 #  port: 6379
 #  #family: 0  # 0=Both, 4=IPv4, 6=IPv6
 #  #pass: example-pass
diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml
index 2809cd2ca4..ece9e8f113 100644
--- a/.devcontainer/docker-compose.yml
+++ b/.devcontainer/docker-compose.yml
@@ -15,15 +15,15 @@ services:
       - internal_network
       - external_network
 
-  redis:
+  keydb:
     restart: unless-stopped
-    image: redis:7-alpine
+    image: eqalpha/keydb:latest
     networks:
       - internal_network
     volumes:
-      - redis-data:/data
+      - keydb-data:/data
     healthcheck:
-      test: "redis-cli ping"
+      test: "keydb-cli ping"
       interval: 5s
       retries: 20
 
@@ -45,7 +45,7 @@ services:
 
 volumes:
   postgres-data:
-  redis-data:
+  keydb-data:
 
 networks:
   internal_network:
diff --git a/.dockerignore b/.dockerignore
index 1de0c7982b..8ecffe1c7f 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -11,6 +11,7 @@ docker-compose.yml
 node_modules/
 packages/*/node_modules
 redis/
+keydb/
 files/
 misskey-assets/
 fluent-emojis/
diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml
index d7be15bd4f..06961ace19 100644
--- a/.github/workflows/test-backend.yml
+++ b/.github/workflows/test-backend.yml
@@ -17,14 +17,14 @@ jobs:
 
     services:
       postgres:
-        image: postgres:13
+        image: postgres:15
         ports:
           - 54312:5432
         env:
           POSTGRES_DB: test-misskey
           POSTGRES_HOST_AUTH_METHOD: trust
-      redis:
-        image: redis:7
+      keydb:
+        image: eqalpha/keydb:latest
         ports:
           - 56312:6379
 
diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml
index 4ea4ba4628..d7de19c659 100644
--- a/.github/workflows/test-frontend.yml
+++ b/.github/workflows/test-frontend.yml
@@ -56,14 +56,14 @@ jobs:
 
     services:
       postgres:
-        image: postgres:13
+        image: postgres:15
         ports:
           - 54312:5432
         env:
           POSTGRES_DB: test-misskey
           POSTGRES_HOST_AUTH_METHOD: trust
-      redis:
-        image: redis:7
+      keydb:
+        image: eqalpha/keydb:latest
         ports:
           - 56312:6379
 
diff --git a/.gitignore b/.gitignore
index a66e527db0..f66d6e1344 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,7 @@ coverage
 !/.config/docker_example.env
 docker-compose.yml
 !/.devcontainer/docker-compose.yml
+!/packages/backend/test/docker-compose.yml
 
 # misskey
 /build
@@ -51,6 +52,7 @@ run.bat
 api-docs.json
 *.log
 /redis
+/keydb
 *.code-workspace
 .DS_Store
 /files
diff --git a/chart/templates/Deployment.yml b/chart/templates/Deployment.yml
index d5dd14f59e..a6ea8b2160 100644
--- a/chart/templates/Deployment.yml
+++ b/chart/templates/Deployment.yml
@@ -27,7 +27,7 @@ spec:
           ports:
             - containerPort: 3000
         - name: postgres
-          image: postgres:14-alpine
+          image: postgres:15-alpine
           env:
             - name: POSTGRES_USER
               value: "example-misskey-user"
@@ -37,8 +37,8 @@ spec:
               value: "misskey"
           ports:
             - containerPort: 5432
-        - name: redis
-          image: redis:alpine
+        - name: keydb
+          image: eqalpha/keydb:latest
           ports:
             - containerPort: 6379
       volumes:
diff --git a/docker-compose.yml.example b/docker-compose.yml.example
index a0061c5c20..e4f8a7f167 100644
--- a/docker-compose.yml.example
+++ b/docker-compose.yml.example
@@ -6,12 +6,12 @@ services:
     restart: always
     links:
       - db
-      - redis
+      - keydb
 #     - meilisearch
     depends_on:
       db:
         condition: service_healthy
-      redis:
+      keydb:
         condition: service_healthy
     ports:
       - "3000:3000"
@@ -22,15 +22,15 @@ services:
       - ./files:/misskey/files
       - ./.config:/misskey/.config:ro
 
-  redis:
+  keydb:
     restart: always
-    image: redis:7-alpine
+    image: eqalpha/keydb:latest
     networks:
       - internal_network
     volumes:
-      - ./redis:/data
+      - ./keydb:/data
     healthcheck:
-      test: "redis-cli ping"
+      test: "keydb-cli ping"
       interval: 5s
       retries: 20
 
diff --git a/packages/backend/src/core/FeaturedService.ts b/packages/backend/src/core/FeaturedService.ts
index 555a646cdc..0772dc689f 100644
--- a/packages/backend/src/core/FeaturedService.ts
+++ b/packages/backend/src/core/FeaturedService.ts
@@ -38,7 +38,7 @@ export class FeaturedService {
 		redisTransaction.expire(
 			`${name}:${currentWindow}`,
 			(windowRange * 3) / 1000,
-			'NX'); // "NX -- Set expiry only when the key has no expiry" = 有効期限がないときだけ設定
+		);
 		await redisTransaction.exec();
 	}
 
@@ -48,10 +48,10 @@ export class FeaturedService {
 		const previousWindow = currentWindow - 1;
 
 		const redisPipeline = this.redisClient.pipeline();
-		redisPipeline.zrange(
-			`${name}:${currentWindow}`, 0, threshold, 'REV', 'WITHSCORES');
-		redisPipeline.zrange(
-			`${name}:${previousWindow}`, 0, threshold, 'REV', 'WITHSCORES');
+		redisPipeline.zrevrange(
+			`${name}:${currentWindow}`, 0, threshold, 'WITHSCORES');
+		redisPipeline.zrevrange(
+			`${name}:${previousWindow}`, 0, threshold, 'WITHSCORES');
 		const [currentRankingResult, previousRankingResult] = await redisPipeline.exec().then(result => result ? result.map(r => r[1] as string[]) : [[], []]);
 
 		const ranking = new Map<string, number>();
diff --git a/packages/backend/test/docker-compose.yml b/packages/backend/test/docker-compose.yml
index da6c01dda1..286a6607a5 100644
--- a/packages/backend/test/docker-compose.yml
+++ b/packages/backend/test/docker-compose.yml
@@ -1,13 +1,13 @@
 version: "3"
 
 services:
-  redistest:
-    image: redis:7
+  keydbtest:
+    image: eqalpha/keydb:latest
     ports:
       - "127.0.0.1:56312:6379"
 
   dbtest:
-    image: postgres:13
+    image: postgres:15
     ports:
       - "127.0.0.1:54312:5432"
     environment:

From cced3de473417fa1deca4fff8a22e47de88fc945 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 30 Oct 2023 00:37:55 +0900
Subject: [PATCH 03/36] =?UTF-8?q?fix:=20=E5=80=8B=E4=BA=BA=E3=82=AB?=
 =?UTF-8?q?=E3=83=BC=E3=83=89=E3=81=AEemoji=E3=81=8C=E3=83=90=E3=83=83?=
 =?UTF-8?q?=E3=83=86=E3=83=AA=E3=83=BC=E3=81=AB=E3=81=AA=E3=81=A3=E3=81=A6?=
 =?UTF-8?q?=E3=81=84=E3=82=8B=20(misskey-dev#12190)=20(MisskeyIO#201)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: yupix <yupi0982@outlook.jp>
---
 packages/frontend/src/emojilist.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/packages/frontend/src/emojilist.json b/packages/frontend/src/emojilist.json
index fde06a4aa0..eae822e652 100644
--- a/packages/frontend/src/emojilist.json
+++ b/packages/frontend/src/emojilist.json
@@ -1061,7 +1061,7 @@
 	["💰", "moneybag", 6],
 	["🪙", "coin", 6],
 	["💳", "credit_card", 6],
-	["🪫", "identification_card", 6],
+	["🪪", "identification_card", 6],
 	["💎", "gem", 6],
 	["⚖", "balance_scale", 6],
 	["🧰", "toolbox", 6],

From 16f213ef0c6b79f0811df4420b725e0032d9fc5f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 30 Oct 2023 00:41:08 +0900
Subject: [PATCH 04/36] spec(deps): use node 20 (MisskeyIO#197)

---
 .devcontainer/devcontainer.json         | 2 +-
 .github/ISSUE_TEMPLATE/01_bug-report.md | 2 +-
 .github/workflows/test-backend.yml      | 2 +-
 .github/workflows/test-frontend.yml     | 4 ++--
 .github/workflows/test-misskey-js.yml   | 2 +-
 .github/workflows/test-production.yml   | 2 +-
 .node-version                           | 2 +-
 Dockerfile                              | 5 +++--
 8 files changed, 11 insertions(+), 10 deletions(-)

diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index 89df1b5d04..d0689667ac 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -6,7 +6,7 @@
 	"features": {
 		"ghcr.io/devcontainers-contrib/features/pnpm:2": {},
 		"ghcr.io/devcontainers/features/node:1": {
-			"version": "18.17.0"
+			"version": "20"
 		}
 	},
 	"forwardPorts": [3000],
diff --git a/.github/ISSUE_TEMPLATE/01_bug-report.md b/.github/ISSUE_TEMPLATE/01_bug-report.md
index 25e7fc8b20..b889d96eb3 100644
--- a/.github/ISSUE_TEMPLATE/01_bug-report.md
+++ b/.github/ISSUE_TEMPLATE/01_bug-report.md
@@ -54,7 +54,7 @@ Please include errors from the developer console and/or server log files if you
 
 * Installation Method or Hosting Service: <!-- Example: docker compose, k8s/docker, systemd, "Misskey install shell script", development environment -->
 * Misskey: 13.x.x
-* Node: 18.x.x
+* Node: 20.x.x
 * PostgreSQL: 15.x.x
 * Redis: 7.x.x
 * OS and Architecture: <!-- Example: Ubuntu 22.04.2 LTS aarch64 -->
diff --git a/.github/workflows/test-backend.yml b/.github/workflows/test-backend.yml
index 06961ace19..d133cf4d99 100644
--- a/.github/workflows/test-backend.yml
+++ b/.github/workflows/test-backend.yml
@@ -13,7 +13,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
     services:
       postgres:
diff --git a/.github/workflows/test-frontend.yml b/.github/workflows/test-frontend.yml
index d7de19c659..87b9a26f9b 100644
--- a/.github/workflows/test-frontend.yml
+++ b/.github/workflows/test-frontend.yml
@@ -13,7 +13,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
     steps:
     - uses: actions/checkout@v3.3.0
@@ -51,7 +51,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
         browser: [chrome]
 
     services:
diff --git a/.github/workflows/test-misskey-js.yml b/.github/workflows/test-misskey-js.yml
index b15e704c7f..213657ce1f 100644
--- a/.github/workflows/test-misskey-js.yml
+++ b/.github/workflows/test-misskey-js.yml
@@ -16,7 +16,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
         # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
 
     steps:
diff --git a/.github/workflows/test-production.yml b/.github/workflows/test-production.yml
index 5243a83777..8429465b5b 100644
--- a/.github/workflows/test-production.yml
+++ b/.github/workflows/test-production.yml
@@ -16,7 +16,7 @@ jobs:
 
     strategy:
       matrix:
-        node-version: [18.x]
+        node-version: [20.x]
 
     steps:
     - uses: actions/checkout@v3.3.0
diff --git a/.node-version b/.node-version
index 603606bc91..209e3ef4b6 100644
--- a/.node-version
+++ b/.node-version
@@ -1 +1 @@
-18.17.0
+20
diff --git a/Dockerfile b/Dockerfile
index 787e248cb1..64ebf8654f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,6 +1,6 @@
 # syntax = docker/dockerfile:1.4
 
-ARG NODE_VERSION=18.17.0-bullseye
+ARG NODE_VERSION=20
 
 # build assets & compile TypeScript
 
@@ -63,6 +63,7 @@ ARG GID="991"
 RUN apt-get update \
 	&& apt-get install -y --no-install-recommends \
 	ffmpeg tini curl 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 \
@@ -81,7 +82,7 @@ COPY --chown=misskey:misskey --from=native-builder /misskey/packages/backend/bui
 COPY --chown=misskey:misskey --from=native-builder /misskey/fluent-emojis /misskey/fluent-emojis
 COPY --chown=misskey:misskey . ./
 
-ENV LD_PRELOAD=/usr/lib/x86_64-linux-gnu/libjemalloc.so.2
+ENV LD_PRELOAD=/usr/local/lib/libjemalloc.so
 ENV MALLOC_CONF=background_thread:true,metadata_thp:auto,dirty_decay_ms:30000,muzzy_decay_ms:30000
 ENV NODE_ENV=production
 HEALTHCHECK --interval=5s --retries=20 CMD ["/bin/bash", "/misskey/healthcheck.sh"]

From 462b5470b764ca98e3814591e42e7cf41975ffff Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 30 Oct 2023 00:41:31 +0900
Subject: [PATCH 05/36] spec(deps): update @simplewebauthn/server to 8.3.5
 (MisskeyIO#198)

---
 packages/backend/package.json                |    4 +-
 packages/backend/src/core/WebAuthnService.ts |    7 +-
 pnpm-lock.yaml                               | 1409 +++++++++++++++---
 3 files changed, 1219 insertions(+), 201 deletions(-)

diff --git a/packages/backend/package.json b/packages/backend/package.json
index b5748d909e..c816ccbfdf 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -74,7 +74,7 @@
 		"@nestjs/core": "10.1.0",
 		"@nestjs/testing": "10.1.0",
 		"@peertube/http-signature": "1.7.0",
-		"@simplewebauthn/server": "^7.4.0",
+		"@simplewebauthn/server": "^8.3.5",
 		"@sinonjs/fake-timers": "10.3.0",
 		"@swc/cli": "0.1.62",
 		"@swc/core": "1.3.70",
@@ -164,7 +164,7 @@
 	},
 	"devDependencies": {
 		"@jest/globals": "29.6.1",
-		"@simplewebauthn/typescript-types": "^7.4.0",
+		"@simplewebauthn/typescript-types": "^8.3.4",
 		"@swc/jest": "0.2.26",
 		"@types/accepts": "1.3.5",
 		"@types/archiver": "5.3.2",
diff --git a/packages/backend/src/core/WebAuthnService.ts b/packages/backend/src/core/WebAuthnService.ts
index d244f4511a..9f548dd07f 100644
--- a/packages/backend/src/core/WebAuthnService.ts
+++ b/packages/backend/src/core/WebAuthnService.ts
@@ -7,7 +7,8 @@ import { Inject, Injectable } from '@nestjs/common';
 import * as Redis from 'ioredis';
 import {
 	generateAuthenticationOptions,
-	generateRegistrationOptions, verifyAuthenticationResponse,
+	generateRegistrationOptions,
+	verifyAuthenticationResponse,
 	verifyRegistrationResponse,
 } from '@simplewebauthn/server';
 import { AttestationFormat, isoCBOR } from '@simplewebauthn/server/helpers';
@@ -60,7 +61,7 @@ export class WebAuthnService {
 			userId: userId,
 		});
 
-		const registrationOptions = generateRegistrationOptions({
+		const registrationOptions = await generateRegistrationOptions({
 			rpName: relyingParty.rpName,
 			rpID: relyingParty.rpId,
 			userID: userId,
@@ -150,7 +151,7 @@ export class WebAuthnService {
 			throw new IdentifiableError('f27fd449-9af4-4841-9249-1f989b9fa4a4', 'no keys found');
 		}
 
-		const authenticationOptions = generateAuthenticationOptions({
+		const authenticationOptions = await generateAuthenticationOptions({
 			allowCredentials: keys.map(key => (<PublicKeyCredentialDescriptorFuture>{
 				id: Buffer.from(key.id, 'base64url'),
 				type: 'public-key',
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 926633e6d8..75a7e4242e 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -100,7 +100,7 @@ importers:
         version: 8.3.0
       '@fastify/http-proxy':
         specifier: 9.2.1
-        version: 9.2.1(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+        version: 9.2.1(bufferutil@4.0.8)(utf-8-validate@6.0.3)
       '@fastify/multipart':
         specifier: 7.7.1
         version: 7.7.1
@@ -123,8 +123,8 @@ importers:
         specifier: 1.7.0
         version: 1.7.0
       '@simplewebauthn/server':
-        specifier: ^7.4.0
-        version: 7.4.0
+        specifier: ^8.3.5
+        version: 8.3.5
       '@sinonjs/fake-timers':
         specifier: 10.3.0
         version: 10.3.0
@@ -226,7 +226,7 @@ importers:
         version: 4.1.0
       jsdom:
         specifier: 22.1.0
-        version: 22.1.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+        version: 22.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.3)
       json5:
         specifier: 2.2.3
         version: 2.2.3
@@ -379,7 +379,7 @@ importers:
         version: 3.6.3
       ws:
         specifier: 8.13.0
-        version: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+        version: 8.13.0(bufferutil@4.0.8)(utf-8-validate@6.0.3)
       xev:
         specifier: 3.0.2
         version: 3.0.2
@@ -428,7 +428,7 @@ importers:
         version: 4.4.0(seedrandom@3.0.5)
       bufferutil:
         specifier: ^4.0.7
-        version: 4.0.7
+        version: 4.0.8
       slacc-android-arm-eabi:
         specifier: 0.0.10
         version: 0.0.10
@@ -476,8 +476,8 @@ importers:
         specifier: 29.6.1
         version: 29.6.1
       '@simplewebauthn/typescript-types':
-        specifier: ^7.4.0
-        version: 7.4.0
+        specifier: ^8.3.4
+        version: 8.3.4
       '@swc/jest':
         specifier: 0.2.26
         version: 0.2.26(@swc/core@1.3.70)
@@ -970,7 +970,7 @@ importers:
         version: 7.0.27
       storybook-addon-misskey-theme:
         specifier: github:misskey-dev/storybook-addon-misskey-theme
-        version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.27)(@storybook/components@7.1.0)(@storybook/core-events@7.0.27)(@storybook/manager-api@7.0.27)(@storybook/preview-api@7.0.27)(@storybook/theming@7.0.27)(@storybook/types@7.0.27)(react-dom@18.2.0)(react@18.2.0)
+        version: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.27)(@storybook/components@7.5.1)(@storybook/core-events@7.0.27)(@storybook/manager-api@7.0.27)(@storybook/preview-api@7.0.27)(@storybook/theming@7.0.27)(@storybook/types@7.0.27)(react-dom@18.2.0)(react@18.2.0)
       summaly:
         specifier: github:misskey-dev/summaly
         version: github.com/misskey-dev/summaly/d2d8db49943ccb201c1b1b283e9d0a630519fac7
@@ -1822,10 +1822,10 @@ packages:
       '@babel/helper-compilation-targets': 7.22.9(@babel/core@7.21.8)
       '@babel/helper-module-transforms': 7.22.9(@babel/core@7.21.8)
       '@babel/helpers': 7.22.3
-      '@babel/parser': 7.22.7
+      '@babel/parser': 7.23.0
       '@babel/template': 7.22.5
       '@babel/traverse': 7.22.4
-      '@babel/types': 7.22.5
+      '@babel/types': 7.21.5
       convert-source-map: 1.9.0
       debug: 4.3.4(supports-color@8.1.1)
       gensync: 1.0.0-beta.2
@@ -1850,7 +1850,7 @@ packages:
       '@babel/traverse': 7.22.4
       '@babel/types': 7.22.5
       convert-source-map: 1.9.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       gensync: 1.0.0-beta.2
       json5: 2.2.3
       semver: 6.3.1
@@ -1862,7 +1862,7 @@ packages:
     resolution: {integrity: sha512-F3fZga2uv09wFdEjEQIJxXALXfz0+JaOb7SabvVMmjHxeVTuGW8wgE8Vp1Hd7O+zMTYtcfEISGRzPkeiaPPsvg==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/types': 7.22.5
+      '@babel/types': 7.21.5
       '@jridgewell/gen-mapping': 0.3.2
       '@jridgewell/trace-mapping': 0.3.18
       jsesc: 2.5.2
@@ -2029,7 +2029,7 @@ packages:
     engines: {node: '>=6.9.0'}
     dependencies:
       '@babel/template': 7.22.5
-      '@babel/types': 7.22.5
+      '@babel/types': 7.23.0
     dev: true
 
   /@babel/helper-hoist-variables@7.22.5:
@@ -2166,6 +2166,11 @@ packages:
     resolution: {integrity: sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==}
     engines: {node: '>=6.9.0'}
 
+  /@babel/helper-validator-identifier@7.22.20:
+    resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==}
+    engines: {node: '>=6.9.0'}
+    dev: true
+
   /@babel/helper-validator-identifier@7.22.5:
     resolution: {integrity: sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==}
     engines: {node: '>=6.9.0'}
@@ -2199,7 +2204,7 @@ packages:
     resolution: {integrity: sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==}
     engines: {node: '>=6.9.0'}
     dependencies:
-      '@babel/helper-validator-identifier': 7.22.5
+      '@babel/helper-validator-identifier': 7.22.20
       chalk: 2.4.2
       js-tokens: 4.0.0
     dev: true
@@ -2218,6 +2223,14 @@ packages:
     dependencies:
       '@babel/types': 7.22.5
 
+  /@babel/parser@7.23.0:
+    resolution: {integrity: sha512-vvPKKdMemU85V9WE/l5wZEmImpCtLqbnTvqDS2U1fJ96KrxoW7KrXhNsNCblQlg8Ck4b85yxdTyelsMUgFUXiw==}
+    engines: {node: '>=6.0.0'}
+    hasBin: true
+    dependencies:
+      '@babel/types': 7.23.0
+    dev: true
+
   /@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@7.22.5(@babel/core@7.21.8):
     resolution: {integrity: sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==}
     engines: {node: '>=6.9.0'}
@@ -3303,7 +3316,7 @@ packages:
       '@babel/helper-hoist-variables': 7.22.5
       '@babel/helper-module-transforms': 7.22.9(@babel/core@7.21.8)
       '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-validator-identifier': 7.22.5
+      '@babel/helper-validator-identifier': 7.22.20
     dev: true
 
   /@babel/plugin-transform-modules-systemjs@7.22.5(@babel/core@7.22.1):
@@ -3316,7 +3329,7 @@ packages:
       '@babel/helper-hoist-variables': 7.22.5
       '@babel/helper-module-transforms': 7.22.9(@babel/core@7.22.1)
       '@babel/helper-plugin-utils': 7.22.5
-      '@babel/helper-validator-identifier': 7.22.5
+      '@babel/helper-validator-identifier': 7.22.20
     dev: true
 
   /@babel/plugin-transform-modules-umd@7.22.5(@babel/core@7.21.8):
@@ -3872,7 +3885,7 @@ packages:
       '@babel/plugin-transform-unicode-escapes': 7.22.5(@babel/core@7.21.8)
       '@babel/plugin-transform-unicode-regex': 7.22.5(@babel/core@7.21.8)
       '@babel/preset-modules': 0.1.5(@babel/core@7.21.8)
-      '@babel/types': 7.22.5
+      '@babel/types': 7.21.5
       babel-plugin-polyfill-corejs2: 0.3.3(@babel/core@7.21.8)
       babel-plugin-polyfill-corejs3: 0.6.0(@babel/core@7.21.8)
       babel-plugin-polyfill-regenerator: 0.4.1(@babel/core@7.21.8)
@@ -4054,6 +4067,13 @@ packages:
     dependencies:
       regenerator-runtime: 0.13.11
 
+  /@babel/runtime@7.23.2:
+    resolution: {integrity: sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      regenerator-runtime: 0.14.0
+    dev: true
+
   /@babel/template@7.22.5:
     resolution: {integrity: sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==}
     engines: {node: '>=6.9.0'}
@@ -4073,8 +4093,8 @@ packages:
       '@babel/helper-function-name': 7.22.5
       '@babel/helper-hoist-variables': 7.22.5
       '@babel/helper-split-export-declaration': 7.22.6
-      '@babel/parser': 7.22.7
-      '@babel/types': 7.22.5
+      '@babel/parser': 7.21.8
+      '@babel/types': 7.21.5
       debug: 4.3.4(supports-color@8.1.1)
       globals: 11.12.0
     transitivePeerDependencies:
@@ -4093,7 +4113,7 @@ packages:
       '@babel/helper-split-export-declaration': 7.22.6
       '@babel/parser': 7.22.7
       '@babel/types': 7.22.5
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       globals: 11.12.0
     transitivePeerDependencies:
       - supports-color
@@ -4116,6 +4136,15 @@ packages:
       '@babel/helper-validator-identifier': 7.22.5
       to-fast-properties: 2.0.0
 
+  /@babel/types@7.23.0:
+    resolution: {integrity: sha512-0oIyUfKoI3mSqMvsxBdclDwxXKXAUA8v/apZbc+iSyARYou1o8ZGDxbUYyLFoW2arqS2jDGqJuZvv1d/io1axg==}
+    engines: {node: '>=6.9.0'}
+    dependencies:
+      '@babel/helper-string-parser': 7.22.5
+      '@babel/helper-validator-identifier': 7.22.20
+      to-fast-properties: 2.0.0
+    dev: true
+
   /@base2/pretty-print-object@1.0.1:
     resolution: {integrity: sha512-4iri8i1AqYHJE2DstZYkyEprg6Pq6sKx3xn5FpySk9sNhH7qN2LLlHJCfDTZRILNwQNPD7mATWM0TBui7uC1pA==}
     dev: true
@@ -4362,6 +4391,14 @@ packages:
       react: 18.2.0
     dev: true
 
+  /@emotion/use-insertion-effect-with-fallbacks@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==}
+    peerDependencies:
+      react: '>=16.8.0'
+    dependencies:
+      react: 18.2.0
+    dev: true
+
   /@esbuild/android-arm64@0.17.19:
     resolution: {integrity: sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==}
     engines: {node: '>=12'}
@@ -4756,7 +4793,7 @@ packages:
     engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
     dependencies:
       ajv: 6.12.6
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       espree: 9.6.0
       globals: 13.19.0
       ignore: 5.2.4
@@ -4832,12 +4869,12 @@ packages:
       fast-json-stringify: 5.7.0
     dev: false
 
-  /@fastify/http-proxy@9.2.1(bufferutil@4.0.7)(utf-8-validate@6.0.3):
+  /@fastify/http-proxy@9.2.1(bufferutil@4.0.8)(utf-8-validate@6.0.3):
     resolution: {integrity: sha512-SSxcdrDQQA2PYYBCK+2I+w83QEbMt1s5bsKEERiMG1jcraQulTW3t/Wkje+RWJNNblDhABnhdKXUTbDuA/EIXA==}
     dependencies:
       '@fastify/reply-from': 9.0.1
       fastify-plugin: 4.5.0
-      ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+      ws: 8.13.0(bufferutil@4.0.8)(utf-8-validate@6.0.3)
     transitivePeerDependencies:
       - bufferutil
       - utf-8-validate
@@ -4929,6 +4966,34 @@ packages:
       hashlru: 2.3.0
     dev: false
 
+  /@floating-ui/core@1.5.0:
+    resolution: {integrity: sha512-kK1h4m36DQ0UHGj5Ah4db7R0rHemTqqO0QLvUqi1/mUUp3LuAWbWxdxSIf/XsnH9VS6rRVPLJCncjRzUvyCLXg==}
+    dependencies:
+      '@floating-ui/utils': 0.1.6
+    dev: true
+
+  /@floating-ui/dom@1.5.3:
+    resolution: {integrity: sha512-ClAbQnEqJAKCJOEbbLo5IUlZHkNszqhuxS4fHAVxRPXPya6Ysf2G8KypnYcOTpx6I8xcgF9bbHb6g/2KpbV8qA==}
+    dependencies:
+      '@floating-ui/core': 1.5.0
+      '@floating-ui/utils': 0.1.6
+    dev: true
+
+  /@floating-ui/react-dom@2.0.2(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-5qhlDvjaLmAst/rKb3VdlCinwTF4EYMiVxuuc/HVUjs46W0zgtbMmAZ1UTsDrRTxRmUEzl92mOtWbeeXL26lSQ==}
+    peerDependencies:
+      react: '>=16.8.0'
+      react-dom: '>=16.8.0'
+    dependencies:
+      '@floating-ui/dom': 1.5.3
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@floating-ui/utils@0.1.6:
+    resolution: {integrity: sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==}
+    dev: true
+
   /@github/webauthn-json@2.1.1:
     resolution: {integrity: sha512-XrftRn4z75SnaJOmZQbt7Mk+IIjqVHw+glDGOxuHwXkZBZh/MBoRS7MHjSZMDaLhT4RjN2VqiEU7EOYleuJWSQ==}
     hasBin: true
@@ -4944,8 +5009,8 @@ packages:
       '@hapi/hoek': 9.3.0
     dev: true
 
-  /@hexagon/base64@1.1.27:
-    resolution: {integrity: sha512-PdUmzpvcUM3Rh39kvz9RdbPVYhMjBjdV7Suw7ZduP7urRLsZR8l5tzgSWKm7TExwBYDFwTnYrZbnE0rQ3N5NLQ==}
+  /@hexagon/base64@1.1.28:
+    resolution: {integrity: sha512-lhqDEAvWixy3bZ+UOYbPwUbBkwBq5C1LAJ/xPC8Oi+lL54oyakv/npbA0aU2hgCsx/1NUd4IBvV03+aUBWxerw==}
     dev: false
 
   /@humanwhocodes/config-array@0.11.10:
@@ -4953,7 +5018,7 @@ packages:
     engines: {node: '>=10.10.0'}
     dependencies:
       '@humanwhocodes/object-schema': 1.2.1
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       minimatch: 3.1.2
     transitivePeerDependencies:
       - supports-color
@@ -5075,7 +5140,7 @@ packages:
     resolution: {integrity: sha512-o319vIf5pEMx0LmzSxxkYYxo4wrRLKHq9dP1yJU7FoPTB0LfAKSz8SWD6D/6U3v/O52t9cF5t+MeJiRsfk7zMw==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
-      jest-get-type: 29.4.3
+      jest-get-type: 29.6.3
     dev: true
 
   /@jest/expect@29.6.1:
@@ -5156,8 +5221,8 @@ packages:
       '@sinclair/typebox': 0.24.51
     dev: true
 
-  /@jest/schemas@29.6.0:
-    resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==}
+  /@jest/schemas@29.6.3:
+    resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
       '@sinclair/typebox': 0.27.8
@@ -5230,7 +5295,7 @@ packages:
     resolution: {integrity: sha512-tPKQNMPuXgvdOn2/Lg9HNfUvjYVGolt04Hp03f5hAk878uwOLikN+JzeLY0HcVgKgFl9Hs3EIqpu3WX27XNhnw==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
-      '@jest/schemas': 29.6.0
+      '@jest/schemas': 29.6.3
       '@types/istanbul-lib-coverage': 2.0.4
       '@types/istanbul-reports': 3.0.1
       '@types/node': 20.4.2
@@ -5320,7 +5385,7 @@ packages:
     hasBin: true
     requiresBuild: true
     dependencies:
-      detect-libc: 2.0.1
+      detect-libc: 2.0.2
       https-proxy-agent: 5.0.1
       make-dir: 3.1.0
       node-fetch: 2.6.11
@@ -5398,7 +5463,7 @@ packages:
       ext-name: 5.0.0
       file-type: 17.1.6
       filenamify: 5.1.1
-      got: 11.8.5
+      got: 11.8.6
       os-filter-obj: 2.0.0
     dev: false
 
@@ -5615,48 +5680,48 @@ packages:
     resolution: {integrity: sha512-Aq58f5HiWdyDlFffbbSjAlv596h/cOnt2DO1w3DOC7OJ5EHs0hd/nycJfiu9RJbT6Yk6F1knnRRXNSpxoIVZ9Q==}
     dev: true
 
-  /@peculiar/asn1-android@2.3.6:
-    resolution: {integrity: sha512-zkYh4DsiRhiNfg6tWaUuRc+huwlb9XJbmeZLrjTz9v76UK1Ehq3EnfJFED6P3sdznW/nqWe46LoM9JrqxcD58g==}
+  /@peculiar/asn1-android@2.3.10:
+    resolution: {integrity: sha512-z9Rx9cFJv7UUablZISe7uksNbFJCq13hO0yEAOoIpAymALTLlvUOSLnGiQS7okPaM5dP42oTLhezH6XDXRXjGw==}
     dependencies:
-      '@peculiar/asn1-schema': 2.3.6
+      '@peculiar/asn1-schema': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.6.0
+      tslib: 2.6.2
     dev: false
 
-  /@peculiar/asn1-ecc@2.3.6:
-    resolution: {integrity: sha512-Hu1xzMJQWv8/GvzOiinaE6XiD1/kEhq2C/V89UEoWeZ2fLUcGNIvMxOr/pMyL0OmpRWj/mhCTXOZp4PP+a0aTg==}
+  /@peculiar/asn1-ecc@2.3.8:
+    resolution: {integrity: sha512-Ah/Q15y3A/CtxbPibiLM/LKcMbnLTdUdLHUgdpB5f60sSvGkXzxJCu5ezGTFHogZXWNX3KSmYqilCrfdmBc6pQ==}
     dependencies:
-      '@peculiar/asn1-schema': 2.3.6
-      '@peculiar/asn1-x509': 2.3.6
+      '@peculiar/asn1-schema': 2.3.8
+      '@peculiar/asn1-x509': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.6.0
+      tslib: 2.6.2
     dev: false
 
-  /@peculiar/asn1-rsa@2.3.6:
-    resolution: {integrity: sha512-DswjJyAXZnvESuImGNTvbNKvh1XApBVqU+r3UmrFFTAI23gv62byl0f5OFKWTNhCf66WQrd3sklpsCZc/4+jwA==}
+  /@peculiar/asn1-rsa@2.3.8:
+    resolution: {integrity: sha512-ES/RVEHu8VMYXgrg3gjb1m/XG0KJWnV4qyZZ7mAg7rrF3VTmRbLxO8mk+uy0Hme7geSMebp+Wvi2U6RLLEs12Q==}
     dependencies:
-      '@peculiar/asn1-schema': 2.3.6
-      '@peculiar/asn1-x509': 2.3.6
+      '@peculiar/asn1-schema': 2.3.8
+      '@peculiar/asn1-x509': 2.3.8
       asn1js: 3.0.5
-      tslib: 2.6.0
+      tslib: 2.6.2
     dev: false
 
-  /@peculiar/asn1-schema@2.3.6:
-    resolution: {integrity: sha512-izNRxPoaeJeg/AyH8hER6s+H7p4itk+03QCa4sbxI3lNdseQYCuxzgsuNK8bTXChtLTjpJz6NmXKA73qLa3rCA==}
+  /@peculiar/asn1-schema@2.3.8:
+    resolution: {integrity: sha512-ULB1XqHKx1WBU/tTFIA+uARuRoBVZ4pNdOA878RDrRbBfBGcSzi5HBkdScC6ZbHn8z7L8gmKCgPC1LHRrP46tA==}
     dependencies:
       asn1js: 3.0.5
-      pvtsutils: 1.3.3
-      tslib: 2.6.0
+      pvtsutils: 1.3.5
+      tslib: 2.6.2
     dev: false
 
-  /@peculiar/asn1-x509@2.3.6:
-    resolution: {integrity: sha512-dRwX31R1lcbIdzbztiMvLNTDoGptxdV7HocNx87LfKU0fEWh7fTWJjx4oV+glETSy6heF/hJHB2J4RGB3vVSYg==}
+  /@peculiar/asn1-x509@2.3.8:
+    resolution: {integrity: sha512-voKxGfDU1c6r9mKiN5ZUsZWh3Dy1BABvTM3cimf0tztNwyMJPhiXY94eRTgsMQe6ViLfT6EoXxkWVzcm3mFAFw==}
     dependencies:
-      '@peculiar/asn1-schema': 2.3.6
+      '@peculiar/asn1-schema': 2.3.8
       asn1js: 3.0.5
       ipaddr.js: 2.1.0
-      pvtsutils: 1.3.3
-      tslib: 2.6.0
+      pvtsutils: 1.3.5
+      tslib: 2.6.2
     dev: false
 
   /@peertube/http-signature@1.7.0:
@@ -5675,6 +5740,525 @@ packages:
     dev: false
     optional: true
 
+  /@radix-ui/number@1.0.1:
+    resolution: {integrity: sha512-T5gIdVO2mmPW3NNhjNgEP3cqMXjXL9UbO0BzWcXfvdBs+BohbQxvd/K5hSVKmn9/lbTdsQVKbUcP5WLCwvUbBg==}
+    dependencies:
+      '@babel/runtime': 7.23.2
+    dev: true
+
+  /@radix-ui/primitive@1.0.1:
+    resolution: {integrity: sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==}
+    dependencies:
+      '@babel/runtime': 7.23.2
+    dev: true
+
+  /@radix-ui/react-arrow@1.0.3(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-collection@1.0.3(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-3SzW+0PW7yBBoQlT8wNcGtaxaD0XSu0uLUFgrtHY08Acx05TaHaOmVLR73c0j/cqpDy53KBMO7s0dx2wmOIDIA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0)
+      '@radix-ui/react-context': 1.0.1(react@18.2.0)
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-slot': 1.0.2(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-compose-refs@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-context@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-direction@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-dismissable-layer@1.0.4(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-7UpBa/RKMoHJYjie1gkF1DlK8l1fdU/VKDpoS3rCCo8YBJR294GwcEHyxHw72yvphJ7ld0AXEcSLAzY2F/WyCg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/primitive': 1.0.1
+      '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0)
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0)
+      '@radix-ui/react-use-escape-keydown': 1.0.3(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-focus-guards@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-focus-scope@1.0.3(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-upXdPfqI4islj2CslyfUBNlaJCPybbqRHAi1KER7Isel9Q2AtSJ0zRBZv8mWQiFXD2nyAJ4BhC3yXgZ6kMBSrQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0)
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-id@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-use-layout-effect': 1.0.1(react@18.2.0)
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-popper@1.1.2(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-1CnGGfFi/bbqtJZZ0P/NQY20xdG3E0LALJaLUEoKwPLwl6PPPfbeiCqMVQnhoFRAxjJj4RpBRJzDmUgsex2tSg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@floating-ui/react-dom': 2.0.2(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-arrow': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0)
+      '@radix-ui/react-context': 1.0.1(react@18.2.0)
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0)
+      '@radix-ui/react-use-layout-effect': 1.0.1(react@18.2.0)
+      '@radix-ui/react-use-rect': 1.0.1(react@18.2.0)
+      '@radix-ui/react-use-size': 1.0.1(react@18.2.0)
+      '@radix-ui/rect': 1.0.1
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-portal@1.0.3(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-xLYZeHrWoPmA5mEKEfZZevoVRK/Q43GfzRXkWV6qawIWWK8t6ifIiLQdd7rmQ4Vk1bmI21XhqF9BN3jWf+phpA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-primitive@1.0.3(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-slot': 1.0.2(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-roving-focus@1.0.4(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-2mUg5Mgcu001VkGy+FfzZyzbmuUWzgWkj3rvv4yu+mLw03+mTzbxZHvfcGyFp2b8EkQeMkpRQ5FiA2Vr2O6TeQ==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/primitive': 1.0.1
+      '@radix-ui/react-collection': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0)
+      '@radix-ui/react-context': 1.0.1(react@18.2.0)
+      '@radix-ui/react-direction': 1.0.1(react@18.2.0)
+      '@radix-ui/react-id': 1.0.1(react@18.2.0)
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0)
+      '@radix-ui/react-use-controllable-state': 1.0.1(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-select@1.2.2(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-zI7McXr8fNaSrUY9mZe4x/HC0jTLY9fWNhO1oLWYMQGDXuV4UCivIGTxwioSzO0ZCYX9iSLyWmAh/1TOmX3Cnw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/number': 1.0.1
+      '@radix-ui/primitive': 1.0.1
+      '@radix-ui/react-collection': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0)
+      '@radix-ui/react-context': 1.0.1(react@18.2.0)
+      '@radix-ui/react-direction': 1.0.1(react@18.2.0)
+      '@radix-ui/react-dismissable-layer': 1.0.4(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-focus-guards': 1.0.1(react@18.2.0)
+      '@radix-ui/react-focus-scope': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-id': 1.0.1(react@18.2.0)
+      '@radix-ui/react-popper': 1.1.2(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-portal': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-slot': 1.0.2(react@18.2.0)
+      '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0)
+      '@radix-ui/react-use-controllable-state': 1.0.1(react@18.2.0)
+      '@radix-ui/react-use-layout-effect': 1.0.1(react@18.2.0)
+      '@radix-ui/react-use-previous': 1.0.1(react@18.2.0)
+      '@radix-ui/react-visually-hidden': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      aria-hidden: 1.2.3
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+      react-remove-scroll: 2.5.5(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-separator@1.0.3(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-itYmTy/kokS21aiV5+Z56MZB54KrhPgn6eHDKkFeOLR34HMN2s8PaN47qZZAGnvupcjxHaFZnW4pQEh0BvvVuw==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-slot@1.0.2(react@18.2.0):
+    resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-compose-refs': 1.0.1(react@18.2.0)
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-toggle-group@1.0.4(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-Uaj/M/cMyiyT9Bx6fOZO0SAG4Cls0GptBWiBmBxofmDbNVnYYoyRWj/2M/6VCi/7qcXFWnHhRUfdfZFvvkuu8A==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/primitive': 1.0.1
+      '@radix-ui/react-context': 1.0.1(react@18.2.0)
+      '@radix-ui/react-direction': 1.0.1(react@18.2.0)
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-roving-focus': 1.0.4(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-toggle': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-use-controllable-state': 1.0.1(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-toggle@1.0.3(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-Pkqg3+Bc98ftZGsl60CLANXQBBQ4W3mTFS9EJvNxKMZ7magklKV69/id1mlAlOFDDfHvlCms0fx8fA4CMKDJHg==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/primitive': 1.0.1
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-use-controllable-state': 1.0.1(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-toolbar@1.0.4(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-tBgmM/O7a07xbaEkYJWYTXkIdU/1pW4/KZORR43toC/4XWyBCURK0ei9kMUdp+gTPPKBgYLxXmRSH1EVcIDp8Q==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/primitive': 1.0.1
+      '@radix-ui/react-context': 1.0.1(react@18.2.0)
+      '@radix-ui/react-direction': 1.0.1(react@18.2.0)
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-roving-focus': 1.0.4(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-separator': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-toggle-group': 1.0.4(react-dom@18.2.0)(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/react-use-callback-ref@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-use-controllable-state@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0)
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-use-escape-keydown@1.0.3(react@18.2.0):
+    resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-use-callback-ref': 1.0.1(react@18.2.0)
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-use-layout-effect@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-use-previous@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-use-rect@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/rect': 1.0.1
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-use-size@1.0.1(react@18.2.0):
+    resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==}
+    peerDependencies:
+      '@types/react': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-use-layout-effect': 1.0.1(react@18.2.0)
+      react: 18.2.0
+    dev: true
+
+  /@radix-ui/react-visually-hidden@1.0.3(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==}
+    peerDependencies:
+      '@types/react': '*'
+      '@types/react-dom': '*'
+      react: ^16.8 || ^17.0 || ^18.0
+      react-dom: ^16.8 || ^17.0 || ^18.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+      '@types/react-dom':
+        optional: true
+    dependencies:
+      '@babel/runtime': 7.23.2
+      '@radix-ui/react-primitive': 1.0.3(react-dom@18.2.0)(react@18.2.0)
+      react: 18.2.0
+      react-dom: 18.2.0(react@18.2.0)
+    dev: true
+
+  /@radix-ui/rect@1.0.1:
+    resolution: {integrity: sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ==}
+    dependencies:
+      '@babel/runtime': 7.23.2
+    dev: true
+
   /@rollup/plugin-alias@5.0.0(rollup@3.26.3):
     resolution: {integrity: sha512-l9hY5chSCjuFRPsnRm16twWBiSApl2uYFLsepQYwtBuAxNMQ/1dJqADld40P0Jkqm65GRTLy/AC6hnpVebtLsA==}
     engines: {node: '>=14.0.0'}
@@ -5805,37 +6389,25 @@ packages:
     resolution: {integrity: sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==}
     dev: true
 
-  /@simplewebauthn/iso-webcrypto@7.4.0:
-    resolution: {integrity: sha512-LSx8zghjH+z9IFOhBdDv2AyhqnzDUCYFxFiwJbToowOigCgf4Y8fyZle9Y+0NS232bIoU6j/lgv5iT32m3eGyA==}
-    dependencies:
-      '@simplewebauthn/typescript-types': 7.4.0
-      '@types/node': 18.11.18
-    dev: false
-
-  /@simplewebauthn/server@7.4.0:
-    resolution: {integrity: sha512-Y6jj2WsE3zBDagSdOg3b7+SMw7zHku0Od45Q1ZpA19Wd5aUbV2mH281SbdhFN4UuKcGQSeeAgUObAWHvgxNOVA==}
+  /@simplewebauthn/server@8.3.5:
+    resolution: {integrity: sha512-Y6FkggTkzUdPk3cG3LLCiv7rqPQ3QI7g//RU9937G1pxogChvx12Y7/AZdWeMoeP+LFl0fPpdc1bIE0etJOxGA==}
     engines: {node: '>=16.0.0'}
     dependencies:
-      '@hexagon/base64': 1.1.27
-      '@peculiar/asn1-android': 2.3.6
-      '@peculiar/asn1-ecc': 2.3.6
-      '@peculiar/asn1-rsa': 2.3.6
-      '@peculiar/asn1-schema': 2.3.6
-      '@peculiar/asn1-x509': 2.3.6
-      '@simplewebauthn/iso-webcrypto': 7.4.0
-      '@simplewebauthn/typescript-types': 7.4.0
-      '@types/debug': 4.1.7
-      '@types/node': 18.11.18
-      cbor-x: 1.5.3
-      cross-fetch: 3.1.6
-      debug: 4.3.4(supports-color@8.1.1)
+      '@hexagon/base64': 1.1.28
+      '@peculiar/asn1-android': 2.3.10
+      '@peculiar/asn1-ecc': 2.3.8
+      '@peculiar/asn1-rsa': 2.3.8
+      '@peculiar/asn1-schema': 2.3.8
+      '@peculiar/asn1-x509': 2.3.8
+      '@simplewebauthn/typescript-types': 8.3.4
+      cbor-x: 1.5.4
+      cross-fetch: 4.0.0
     transitivePeerDependencies:
       - encoding
-      - supports-color
     dev: false
 
-  /@simplewebauthn/typescript-types@7.4.0:
-    resolution: {integrity: sha512-8/ZjHeUPe210Bt5oyaOIGx4h8lHdsQs19BiOT44gi/jBEgK7uBGA0Fy7NRsyh777al3m6WM0mBf0UR7xd4R7WQ==}
+  /@simplewebauthn/typescript-types@8.3.4:
+    resolution: {integrity: sha512-38xtca0OqfRVNloKBrFB5LEM6PN5vzFbJG6rAutPVrtGHFYxPdiV3btYWq0eAZAZmP+dqFPYJxJWeJrGfmYHng==}
 
   /@sinclair/typebox@0.24.51:
     resolution: {integrity: sha512-1P1OROm/rdubP5aFDSZQILU0vrLCJ4fvHt6EoqHEM+2D/G5MK3bIaymUKLit8Js9gbns5UyJnkP/TZROLw4tUA==}
@@ -5853,6 +6425,11 @@ packages:
   /@sindresorhus/is@5.3.0:
     resolution: {integrity: sha512-CX6t4SYQ37lzxicAqsBtxA3OseeoVrh9cSJ5PFYam0GksYlupRfy1A+Q4aYD3zvcfECLc0zO2u+ZnR2UYKvCrw==}
     engines: {node: '>=14.16'}
+    dev: false
+
+  /@sindresorhus/is@5.6.0:
+    resolution: {integrity: sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==}
+    engines: {node: '>=14.16'}
 
   /@sinonjs/commons@1.8.6:
     resolution: {integrity: sha512-Ky+XkAkqPZSm3NLBeUng77EBQl3cmeJhITaGHdYH8kjVB+aun3S4XBRti2zt17mtt0mIUDiNxYeoJm6drVvBJQ==}
@@ -6754,14 +7331,14 @@ packages:
     resolution: {integrity: sha512-YppvPa1qMyC+oCQJ3tf7Quzpf2NnBlvIRLPJiGAMssUwX5qE0iKe9lTtkNwMaNxEvzz6rDxewSlz+f/MWr4gPw==}
     dev: true
 
-  /@storybook/channels@7.1.0:
-    resolution: {integrity: sha512-8uzjWdVG2IK18P8n6H+olAs+jnZr+HeYs1t2xiRy4NVSLhBffB71ut5F+pcWZfdDe3gyX8Tfvy68NloTNt9POg==}
+  /@storybook/channels@7.5.1:
+    resolution: {integrity: sha512-7hTGHqvtdFTqRx8LuCznOpqPBYfUeMUt/0IIp7SFuZT585yMPxrYoaK//QmLEWnPb80B8HVTSQi7caUkJb32LA==}
     dependencies:
-      '@storybook/client-logger': 7.1.0
-      '@storybook/core-events': 7.1.0
+      '@storybook/client-logger': 7.5.1
+      '@storybook/core-events': 7.5.1
       '@storybook/global': 5.0.0
-      qs: 6.11.1
-      telejson: 7.0.4
+      qs: 6.11.2
+      telejson: 7.2.0
       tiny-invariant: 1.3.1
     dev: true
 
@@ -6826,8 +7403,8 @@ packages:
       '@storybook/global': 5.0.0
     dev: true
 
-  /@storybook/client-logger@7.1.0:
-    resolution: {integrity: sha512-br5GNTxNFmDZA4ESaCMn2VJ9ZW3ejbILEGoadOJjP2ZD40luSRNtTtWjeNiA+7762OvHMYVGwG0tnqk98f5nfg==}
+  /@storybook/client-logger@7.5.1:
+    resolution: {integrity: sha512-XxbLvg0aQRoBrzxYLcVYCbjDkGbkU8Rfb74XbV2CLiO2bIbFPmA1l1Nwbp+wkCGA+O6Z1zwzSl6wcKKqZ6XZCg==}
     dependencies:
       '@storybook/global': 5.0.0
     dev: true
@@ -6870,22 +7447,27 @@ packages:
       util-deprecate: 1.0.2
     dev: true
 
-  /@storybook/components@7.1.0(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-o8Z5L7cxxNCUhbEA+vGwoVrZ0vWhuZJb/AUc+347RIlH1QZF4Cu6fmgA49pKBsrJWPbtOmlLCbN/9LshszH0Zw==}
+  /@storybook/components@7.5.1(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-fdzzxGBV/Fj9pYwfYL3RZsVUHeBqlfLMBP/L6mPmjaZSwHFqkaRZZUajZc57lCtI+TOy2gY6WH3cPavEtqtgLw==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
-      '@storybook/client-logger': 7.1.0
-      '@storybook/csf': 0.1.0
+      '@radix-ui/react-select': 1.2.2(react-dom@18.2.0)(react@18.2.0)
+      '@radix-ui/react-toolbar': 1.0.4(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/client-logger': 7.5.1
+      '@storybook/csf': 0.1.1
       '@storybook/global': 5.0.0
-      '@storybook/theming': 7.1.0(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/types': 7.1.0
+      '@storybook/theming': 7.5.1(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/types': 7.5.1
       memoizerific: 1.11.3
       react: 18.2.0
       react-dom: 18.2.0(react@18.2.0)
       use-resize-observer: 9.1.0(react-dom@18.2.0)(react@18.2.0)
       util-deprecate: 1.0.2
+    transitivePeerDependencies:
+      - '@types/react'
+      - '@types/react-dom'
     dev: true
 
   /@storybook/core-client@7.0.27:
@@ -6932,8 +7514,10 @@ packages:
     resolution: {integrity: sha512-sNnqgO5i5DUIqeQfNbr987KWvAciMN9FmMBuYdKjVFMqWFyr44HTgnhfKwZZKl+VMDYkHA9Do7UGSYZIKy0P4g==}
     dev: true
 
-  /@storybook/core-events@7.1.0:
-    resolution: {integrity: sha512-b0kZ5ElPZj3NPqWhGsHHuLn0riA4wJXJ5mNBOe2scd8Cw52ELQr5rVHOMROhONOgpOaZBZ+QZd/MDvJDRyxTQw==}
+  /@storybook/core-events@7.5.1:
+    resolution: {integrity: sha512-2eyaUhTfmEEqOEZVoCXVITCBn6N7QuZCG2UNxv0l//ED+7MuMiFhVw7kS7H3WOVk65R7gb8qbKFTNX8HFTgBHg==}
+    dependencies:
+      ts-dedent: 2.2.0
     dev: true
 
   /@storybook/core-server@7.0.27:
@@ -6979,7 +7563,7 @@ packages:
       ts-dedent: 2.2.0
       util-deprecate: 1.0.2
       watchpack: 2.4.0
-      ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+      ws: 8.13.0
     transitivePeerDependencies:
       - bufferutil
       - encoding
@@ -7018,6 +7602,12 @@ packages:
       type-fest: 2.19.0
     dev: true
 
+  /@storybook/csf@0.1.1:
+    resolution: {integrity: sha512-4hE3AlNVxR60Wc5KSC68ASYzUobjPqtSKyhV6G+ge0FIXU55N5nTY7dXGRZHQGDBPq+XqchMkIdlkHPRs8nTHg==}
+    dependencies:
+      type-fest: 2.19.0
+    dev: true
+
   /@storybook/docs-mdx@0.1.0:
     resolution: {integrity: sha512-JDaBR9lwVY4eSH5W8EGHrhODjygPd6QImRbwjAuJNEnY0Vw4ie3bPkeGfnacB3OBW6u/agqPv2aRlR46JcAQLg==}
     dev: true
@@ -7310,14 +7900,14 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
-  /@storybook/theming@7.1.0(react-dom@18.2.0)(react@18.2.0):
-    resolution: {integrity: sha512-bO56c7NFlK7sfjsCbV56VLU59HHvQTW/HVu8RxUuoY+0WutyGAq6uZCmtQnMMGORzxh0p/uU2dSBVYEfW8QoTQ==}
+  /@storybook/theming@7.5.1(react-dom@18.2.0)(react@18.2.0):
+    resolution: {integrity: sha512-ETLAOn10hI4Mkmjsr0HGcM6HbzaURrrPBYmfXOrdbrzEVN+AHW4FlvP9d8fYyP1gdjPE1F39XvF0jYgt1zXiHQ==}
     peerDependencies:
       react: ^16.8.0 || ^17.0.0 || ^18.0.0
       react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
     dependencies:
-      '@emotion/use-insertion-effect-with-fallbacks': 1.0.0(react@18.2.0)
-      '@storybook/client-logger': 7.1.0
+      '@emotion/use-insertion-effect-with-fallbacks': 1.0.1(react@18.2.0)
+      '@storybook/client-logger': 7.5.1
       '@storybook/global': 5.0.0
       memoizerific: 1.11.3
       react: 18.2.0
@@ -7342,12 +7932,12 @@ packages:
       file-system-cache: 2.3.0
     dev: true
 
-  /@storybook/types@7.1.0:
-    resolution: {integrity: sha512-ify1+BypgEFefkKCqBfh9fTWnkZcEqeDvLlOxbEV82C2ozg0yPlDP9VLe1eN5XM5Biigs6ZQ6WuQysl0VlCaEw==}
+  /@storybook/types@7.5.1:
+    resolution: {integrity: sha512-ZcMSaqFNx1E+G00nRDUi8kKL7gxJVlnCvbKLNj3V85guy4DkIYAZr31yDqze07gDWbjvKoHIp3tKpgE+2i8upQ==}
     dependencies:
-      '@storybook/channels': 7.1.0
-      '@types/babel__core': 7.20.0
-      '@types/express': 4.17.17
+      '@storybook/channels': 7.5.1
+      '@types/babel__core': 7.20.3
+      '@types/express': 4.17.20
       file-system-cache: 2.3.0
     dev: true
 
@@ -7393,7 +7983,7 @@ packages:
       ts-dedent: 2.2.0
       type-fest: 2.19.0
       vue: 3.3.4
-      vue-component-type-helpers: 1.8.11
+      vue-component-type-helpers: 1.8.22
     transitivePeerDependencies:
       - encoding
       - supports-color
@@ -7861,7 +8451,7 @@ packages:
       '@types/webgl-ext': 0.0.30
       '@webgpu/types': 0.1.30
       long: 4.0.0
-      node-fetch: 2.6.7
+      node-fetch: 2.6.13
       seedrandom: 3.0.5
     transitivePeerDependencies:
       - encoding
@@ -7876,7 +8466,7 @@ packages:
     dependencies:
       '@tensorflow/tfjs-core': 4.4.0
       '@types/node-fetch': 2.6.4
-      node-fetch: 2.6.11
+      node-fetch: 2.6.13
       seedrandom: 3.0.5
       string_decoder: 1.3.0
     transitivePeerDependencies:
@@ -8028,12 +8618,28 @@ packages:
       '@types/babel__traverse': 7.20.0
     dev: true
 
+  /@types/babel__core@7.20.3:
+    resolution: {integrity: sha512-54fjTSeSHwfan8AyHWrKbfBWiEUrNTZsUwPTDSNaaP1QDQIZbeNUg3a59E9D+375MzUw/x1vx2/0F5LBz+AeYA==}
+    dependencies:
+      '@babel/parser': 7.23.0
+      '@babel/types': 7.23.0
+      '@types/babel__generator': 7.6.6
+      '@types/babel__template': 7.4.3
+      '@types/babel__traverse': 7.20.3
+    dev: true
+
   /@types/babel__generator@7.6.4:
     resolution: {integrity: sha512-tFkciB9j2K755yrTALxD44McOrk+gfpIpvC3sxHjRawj6PfnQxrse4Clq5y/Rq+G3mrBurMax/lG8Qn2t9mSsg==}
     dependencies:
       '@babel/types': 7.22.5
     dev: true
 
+  /@types/babel__generator@7.6.6:
+    resolution: {integrity: sha512-66BXMKb/sUWbMdBNdMvajU7i/44RkrA3z/Yt1c7R5xejt8qh84iU54yUWCtm0QwGJlDcf/gg4zd/x4mpLAlb/w==}
+    dependencies:
+      '@babel/types': 7.23.0
+    dev: true
+
   /@types/babel__template@7.4.1:
     resolution: {integrity: sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==}
     dependencies:
@@ -8041,12 +8647,25 @@ packages:
       '@babel/types': 7.22.5
     dev: true
 
+  /@types/babel__template@7.4.3:
+    resolution: {integrity: sha512-ciwyCLeuRfxboZ4isgdNZi/tkt06m8Tw6uGbBSBgWrnnZGNXiEyM27xc/PjXGQLqlZ6ylbgHMnm7ccF9tCkOeQ==}
+    dependencies:
+      '@babel/parser': 7.23.0
+      '@babel/types': 7.23.0
+    dev: true
+
   /@types/babel__traverse@7.20.0:
     resolution: {integrity: sha512-TBOjqAGf0hmaqRwpii5LLkJLg7c6OMm4nHLmpsUxwk9bBHtoTC6dAHdVWdGv4TBxj2CZOZY8Xfq8WmfoVi7n4Q==}
     dependencies:
       '@babel/types': 7.22.5
     dev: true
 
+  /@types/babel__traverse@7.20.3:
+    resolution: {integrity: sha512-Lsh766rGEFbaxMIDH7Qa+Yha8cMVI3qAK6CHt3OR0YfxOIn5Z54iHiyDRycHrBqeIiqGa20Kpsv1cavfBKkRSw==}
+    dependencies:
+      '@babel/types': 7.23.0
+    dev: true
+
   /@types/bcryptjs@2.4.2:
     resolution: {integrity: sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==}
     dev: true
@@ -8058,6 +8677,13 @@ packages:
       '@types/node': 20.4.2
     dev: true
 
+  /@types/body-parser@1.19.4:
+    resolution: {integrity: sha512-N7UDG0/xiPQa2D/XrVJXjkWbpqHCd2sBaB32ggRF2l83RhPfamgKGF8gwwqyksS95qUS5ZYF9aF+lLPRlwI2UA==}
+    dependencies:
+      '@types/connect': 3.4.37
+      '@types/node': 20.4.2
+    dev: true
+
   /@types/braces@3.0.1:
     resolution: {integrity: sha512-+euflG6ygo4bn0JHtn4pYqcXwRtLvElQ7/nnjDu7iYG56H0+OhCd7d6Ug0IE3WcFpZozBKW2+80FUbv5QGk5AQ==}
     dev: true
@@ -8065,7 +8691,7 @@ packages:
   /@types/cacheable-request@6.0.3:
     resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==}
     dependencies:
-      '@types/http-cache-semantics': 4.0.1
+      '@types/http-cache-semantics': 4.0.3
       '@types/keyv': 3.1.4
       '@types/node': 20.4.2
       '@types/responselike': 1.0.0
@@ -8103,6 +8729,12 @@ packages:
       '@types/node': 20.4.2
     dev: true
 
+  /@types/connect@3.4.37:
+    resolution: {integrity: sha512-zBUSRqkfZ59OcwXon4HVxhx5oWCJmc0OtBTK05M+p0dYjgN6iTwIL2T/WbsQZrEsdnwaF9cWQ+azOnpPvIqY3Q==}
+    dependencies:
+      '@types/node': 20.4.2
+    dev: true
+
   /@types/content-disposition@0.5.5:
     resolution: {integrity: sha512-v6LCdKfK6BwcqMo+wYW05rLS12S0ZO0Fl4w1h4aaZMD7bqT3gVUns6FvLJKGZHQmYn3SX55JWGpziwJRwVgutA==}
     dev: true
@@ -8115,6 +8747,7 @@ packages:
     resolution: {integrity: sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==}
     dependencies:
       '@types/ms': 0.7.31
+    dev: true
 
   /@types/detect-port@1.3.2:
     resolution: {integrity: sha512-xxgAGA2SAU4111QefXPSp5eGbDm/hW6zhvYl9IeEPZEry9F4d66QAHm5qpUXjb6IsevZV/7emAEx5MhP6O192g==}
@@ -8165,6 +8798,15 @@ packages:
       '@types/range-parser': 1.2.4
     dev: true
 
+  /@types/express-serve-static-core@4.17.39:
+    resolution: {integrity: sha512-BiEUfAiGCOllomsRAZOiMFP7LAnrifHpt56pc4Z7l9K6ACyN06Ns1JLMBxwkfLOjJRlSf06NwWsT7yzfpaVpyQ==}
+    dependencies:
+      '@types/node': 20.4.2
+      '@types/qs': 6.9.9
+      '@types/range-parser': 1.2.6
+      '@types/send': 0.17.3
+    dev: true
+
   /@types/express@4.17.17:
     resolution: {integrity: sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==}
     dependencies:
@@ -8174,6 +8816,15 @@ packages:
       '@types/serve-static': 1.15.1
     dev: true
 
+  /@types/express@4.17.20:
+    resolution: {integrity: sha512-rOaqlkgEvOW495xErXMsmyX3WKBInbhG5eqojXYi3cGUaLoRDlXa5d52fkfWZT963AZ3v2eZ4MbKE6WpDAGVsw==}
+    dependencies:
+      '@types/body-parser': 1.19.4
+      '@types/express-serve-static-core': 4.17.39
+      '@types/qs': 6.9.9
+      '@types/serve-static': 1.15.4
+    dev: true
+
   /@types/find-cache-dir@3.2.1:
     resolution: {integrity: sha512-frsJrz2t/CeGifcu/6uRo4b+SzAwT4NYCVPu1GN8IB9XTzrpPkGuV0tmh9mN+/L0PklAlsC3u5Fxt0ju00LXIw==}
     dev: true
@@ -8249,6 +8900,14 @@ packages:
 
   /@types/http-cache-semantics@4.0.1:
     resolution: {integrity: sha512-SZs7ekbP8CN0txVG2xVRH6EgKmEm31BOxA07vkFaETzZz1xh+cbt8BcI0slpymvwhx5dlFnQG2rTlPVQn+iRPQ==}
+    dev: false
+
+  /@types/http-cache-semantics@4.0.3:
+    resolution: {integrity: sha512-V46MYLFp08Wf2mmaBhvgjStM3tPa+2GAdy/iqoX+noX1//zje2x4XmrIU0cAwyClATsTmahbtoQ2EwP7I5WSiA==}
+
+  /@types/http-errors@2.0.3:
+    resolution: {integrity: sha512-pP0P/9BnCj1OVvQR2lF41EkDG/lWWnDyA203b/4Fmi2eTyORnBtcDoKDwjWQthELrBvWkMOrvSOnZ8OVlW6tXA==}
+    dev: true
 
   /@types/istanbul-lib-coverage@2.0.4:
     resolution: {integrity: sha512-z/QT1XN4K4KYuslS23k62yDIDLwLFkzxOuMplDtObz0+y7VqJCaO2o+SPwHCvLFZh7xazvvoor2tA/hPz9ee7g==}
@@ -8345,10 +9004,18 @@ packages:
     resolution: {integrity: sha512-vXOTGVSLR2jMw440moWTC7H19iUyLtP3Z1YTj7cSsubOICinjMxFeb/V57v9QdyyPGbbWolUFSSmSiRSn94tFw==}
     dev: true
 
+  /@types/mime@1.3.4:
+    resolution: {integrity: sha512-1Gjee59G25MrQGk8bsNvC6fxNiRgUlGn2wlhGf95a59DrprnnHk80FIMMFG9XHMdrfsuA119ht06QPDXA1Z7tw==}
+    dev: true
+
   /@types/mime@3.0.1:
     resolution: {integrity: sha512-Y4XFY5VJAuw0FgAqPNd6NNoV44jbq9Bz2L7Rh/J6jLTiHBSBJa9fxqQIvkIld4GsoDOcCbvzOUAbLPsSKKg+uA==}
     dev: true
 
+  /@types/mime@3.0.3:
+    resolution: {integrity: sha512-i8MBln35l856k5iOhKk2XJ4SeAWg75mLIpZB4v6imOagKL6twsukBZGDMNhdOVk7yRFTMPpfILocMos59Q1otQ==}
+    dev: true
+
   /@types/minimatch@5.1.2:
     resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==}
     dev: true
@@ -8359,9 +9026,11 @@ packages:
 
   /@types/ms@0.7.31:
     resolution: {integrity: sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==}
+    dev: true
 
   /@types/node-fetch@2.6.4:
     resolution: {integrity: sha512-1ZX9fcN4Rvkvgv4E6PAY5WXUFWFcRWxZa3EW83UjycOB9ljJCedb2CupIP4RZMEwF/M3eTcCihbBRgwtGbg5Rg==}
+    requiresBuild: true
     dependencies:
       '@types/node': 20.4.2
       form-data: 3.0.1
@@ -8454,6 +9123,10 @@ packages:
     resolution: {integrity: sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==}
     dev: true
 
+  /@types/qs@6.9.9:
+    resolution: {integrity: sha512-wYLxw35euwqGvTDx6zfY1vokBFnsK0HNrzc6xNHchxfO2hpuRg74GbkEW7e3sSmPvj0TjCDT1VCa6OtHXnubsg==}
+    dev: true
+
   /@types/random-seed@0.3.3:
     resolution: {integrity: sha512-kHsCbIRHNXJo6EN5W8EA5b4i1hdT6jaZke5crBPLUcLqaLdZ0QBq8QVMbafHzhjFF83Cl9qlee2dChD18d/kPg==}
     dev: true
@@ -8462,6 +9135,10 @@ packages:
     resolution: {integrity: sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==}
     dev: true
 
+  /@types/range-parser@1.2.6:
+    resolution: {integrity: sha512-+0autS93xyXizIYiyL02FCY8N+KkKPhILhcUSA276HxzreZ16kl+cmwvV2qAM/PuCCwPXzOXOWhiPcw20uSFcA==}
+    dev: true
+
   /@types/ratelimiter@3.4.4:
     resolution: {integrity: sha512-GSMb93iSA8KKFDgVL2Wzs/kqrHMJcU8xhLdwI5omoACcj7K18SacklLtY1C4G02HC5drd6GygtsIaGbfxJSe0g==}
     dev: true
@@ -8509,6 +9186,13 @@ packages:
     resolution: {integrity: sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==}
     dev: true
 
+  /@types/send@0.17.3:
+    resolution: {integrity: sha512-/7fKxvKUoETxjFUsuFlPB9YndePpxxRAOfGC/yJdc9kTjTeP5kRCTzfnE8kPUKCeyiyIZu0YQ76s50hCedI1ug==}
+    dependencies:
+      '@types/mime': 1.3.4
+      '@types/node': 20.4.2
+    dev: true
+
   /@types/serve-static@1.15.1:
     resolution: {integrity: sha512-NUo5XNiAdULrJENtJXZZ3fHtfMolzZwczzBbnAeBbqBwG+LaG6YaJtuwzwGSQZ2wsCrxjEhNNjAkKigy3n8teQ==}
     dependencies:
@@ -8516,6 +9200,14 @@ packages:
       '@types/node': 20.4.2
     dev: true
 
+  /@types/serve-static@1.15.4:
+    resolution: {integrity: sha512-aqqNfs1XTF0HDrFdlY//+SGUxmdSUbjeRXb5iaZc3x0/vMbYmdw9qvOgHWOyyLFxSSRnUuP5+724zBgfw8/WAw==}
+    dependencies:
+      '@types/http-errors': 2.0.3
+      '@types/mime': 3.0.3
+      '@types/node': 20.4.2
+    dev: true
+
   /@types/serviceworker@0.0.67:
     resolution: {integrity: sha512-7TCH7iNsCSNb+aUD9M/36TekrWFSLCjNK8zw/3n5kOtRjbLtDfGYMXTrDnGhSfqXNwpqmt9Vd90w5C/ad1tX6Q==}
     dev: true
@@ -8656,8 +9348,8 @@ packages:
       '@types/yargs-parser': 21.0.0
     dev: true
 
-  /@types/yauzl@2.10.0:
-    resolution: {integrity: sha512-Cn6WYCm0tXv8p6k+A8PvbDG763EDpBoTzHdA+Q/MF6H3sapGjCm9NzoaJncJS9tUKSuCoDs9XHxYYsQDgxR6kw==}
+  /@types/yauzl@2.10.2:
+    resolution: {integrity: sha512-Km7XAtUIduROw7QPgvcft0lIupeG8a8rdKL8RiSyKvlE7dYY31fEn41HVuQsRFDuROA8tA4K2UVL+WdfFmErBA==}
     requiresBuild: true
     dependencies:
       '@types/node': 20.4.2
@@ -8680,7 +9372,7 @@ packages:
       '@typescript-eslint/scope-manager': 5.61.0
       '@typescript-eslint/type-utils': 5.61.0(eslint@8.45.0)(typescript@5.1.6)
       '@typescript-eslint/utils': 5.61.0(eslint@8.45.0)(typescript@5.1.6)
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       eslint: 8.45.0
       graphemer: 1.4.0
       ignore: 5.2.4
@@ -8705,7 +9397,7 @@ packages:
       '@typescript-eslint/scope-manager': 5.61.0
       '@typescript-eslint/types': 5.61.0
       '@typescript-eslint/typescript-estree': 5.61.0(typescript@5.1.6)
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       eslint: 8.45.0
       typescript: 5.1.6
     transitivePeerDependencies:
@@ -8732,7 +9424,7 @@ packages:
     dependencies:
       '@typescript-eslint/typescript-estree': 5.61.0(typescript@5.1.6)
       '@typescript-eslint/utils': 5.61.0(eslint@8.45.0)(typescript@5.1.6)
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       eslint: 8.45.0
       tsutils: 3.21.0(typescript@5.1.6)
       typescript: 5.1.6
@@ -8756,7 +9448,7 @@ packages:
     dependencies:
       '@typescript-eslint/types': 5.61.0
       '@typescript-eslint/visitor-keys': 5.61.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       globby: 11.1.0
       is-glob: 4.0.3
       semver: 7.5.4
@@ -8942,12 +9634,31 @@ packages:
       estree-walker: 2.0.2
       source-map-js: 1.0.2
 
+  /@vue/compiler-core@3.3.7:
+    resolution: {integrity: sha512-pACdY6YnTNVLXsB86YD8OF9ihwpolzhhtdLVHhBL6do/ykr6kKXNYABRtNMGrsQXpEXXyAdwvWWkuTbs4MFtPQ==}
+    requiresBuild: true
+    dependencies:
+      '@babel/parser': 7.23.0
+      '@vue/shared': 3.3.7
+      estree-walker: 2.0.2
+      source-map-js: 1.0.2
+    dev: true
+    optional: true
+
   /@vue/compiler-dom@3.3.4:
     resolution: {integrity: sha512-wyM+OjOVpuUukIq6p5+nwHYtj9cFroz9cwkfmP9O1nzH68BenTTv0u7/ndggT8cIQlnBeOo6sUT/gvHcIkLA5w==}
     dependencies:
       '@vue/compiler-core': 3.3.4
       '@vue/shared': 3.3.4
 
+  /@vue/compiler-dom@3.3.7:
+    resolution: {integrity: sha512-0LwkyJjnUPssXv/d1vNJ0PKfBlDoQs7n81CbO6Q0zdL7H1EzqYRrTVXDqdBVqro0aJjo/FOa1qBAPVI4PGSHBw==}
+    dependencies:
+      '@vue/compiler-core': 3.3.7
+      '@vue/shared': 3.3.7
+    dev: true
+    optional: true
+
   /@vue/compiler-sfc@3.3.4:
     resolution: {integrity: sha512-6y/d8uw+5TkCuzBkgLS0v3lSM3hJDntFEiUORM11pQ/hKvkhSKZrXW6i69UyXlJQisJxuUEJKAWEqWbWsLeNKQ==}
     dependencies:
@@ -8968,6 +9679,15 @@ packages:
       '@vue/compiler-dom': 3.3.4
       '@vue/shared': 3.3.4
 
+  /@vue/compiler-ssr@3.3.7:
+    resolution: {integrity: sha512-TxOfNVVeH3zgBc82kcUv+emNHo+vKnlRrkv8YvQU5+Y5LJGJwSNzcmLUoxD/dNzv0bhQ/F0s+InlgV0NrApJZg==}
+    requiresBuild: true
+    dependencies:
+      '@vue/compiler-dom': 3.3.7
+      '@vue/shared': 3.3.7
+    dev: true
+    optional: true
+
   /@vue/language-core@1.8.5(typescript@5.1.6):
     resolution: {integrity: sha512-DKQNiNQzNV7nrkZQujvjfX73zqKdj2+KoM4YeKl+ft3f+crO3JB4ycPnmgaRMNX/ULJootdQPGHKFRl5cXxwaw==}
     peerDependencies:
@@ -9023,9 +9743,26 @@ packages:
       '@vue/shared': 3.3.4
       vue: 3.3.4
 
+  /@vue/server-renderer@3.3.7(vue@3.3.4):
+    resolution: {integrity: sha512-UlpKDInd1hIZiNuVVVvLgxpfnSouxKQOSE2bOfQpBuGwxRV/JqqTCyyjXUWiwtVMyeRaZhOYYqntxElk8FhBhw==}
+    peerDependencies:
+      vue: 3.3.7
+    dependencies:
+      '@vue/compiler-ssr': 3.3.7
+      '@vue/shared': 3.3.7
+      vue: 3.3.4
+    dev: true
+    optional: true
+
   /@vue/shared@3.3.4:
     resolution: {integrity: sha512-7OjdcV8vQ74eiz1TZLzZP4JwqM5fA94K6yntPS5Z25r9HDuGNzaGdgvwKYq6S+MxwF0TFRwe50fIR/MYnakdkQ==}
 
+  /@vue/shared@3.3.7:
+    resolution: {integrity: sha512-N/tbkINRUDExgcPTBvxNkvHGu504k8lzlNQRITVnm6YjOjwa4r0nnbd4Jb01sNpur5hAllyRJzSK5PvB9PPwRg==}
+    requiresBuild: true
+    dev: true
+    optional: true
+
   /@vue/test-utils@2.3.2(vue@3.3.4):
     resolution: {integrity: sha512-hJnVaYhbrIm0yBS0+e1Y0Sj85cMyAi+PAbK4JHqMRUZ6S622Goa+G7QzkRSyvCteG8wop7tipuEbHoZo26wsSA==}
     peerDependencies:
@@ -9034,8 +9771,8 @@ packages:
       js-beautify: 1.14.6
       vue: 3.3.4
     optionalDependencies:
-      '@vue/compiler-dom': 3.3.4
-      '@vue/server-renderer': 3.3.4(vue@3.3.4)
+      '@vue/compiler-dom': 3.3.7
+      '@vue/server-renderer': 3.3.7(vue@3.3.4)
     dev: true
 
   /@vue/typescript@1.8.5(typescript@5.1.6):
@@ -9164,7 +9901,7 @@ packages:
     resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==}
     engines: {node: '>= 6.0.0'}
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
     transitivePeerDependencies:
       - supports-color
 
@@ -9172,7 +9909,7 @@ packages:
     resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==}
     engines: {node: '>= 14'}
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
     transitivePeerDependencies:
       - supports-color
     dev: false
@@ -9181,7 +9918,7 @@ packages:
     resolution: {integrity: sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==}
     engines: {node: '>= 8.0.0'}
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       depd: 1.1.2
       humanize-ms: 1.2.1
     transitivePeerDependencies:
@@ -9378,6 +10115,7 @@ packages:
   /are-we-there-yet@2.0.0:
     resolution: {integrity: sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==}
     engines: {node: '>=10'}
+    requiresBuild: true
     dependencies:
       delegates: 1.0.0
       readable-stream: 3.6.0
@@ -9402,6 +10140,13 @@ packages:
   /argparse@2.0.1:
     resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
 
+  /aria-hidden@1.2.3:
+    resolution: {integrity: sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ==}
+    engines: {node: '>=10'}
+    dependencies:
+      tslib: 2.6.2
+    dev: true
+
   /aria-query@5.1.3:
     resolution: {integrity: sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==}
     dependencies:
@@ -9541,9 +10286,9 @@ packages:
     resolution: {integrity: sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ==}
     engines: {node: '>=12.0.0'}
     dependencies:
-      pvtsutils: 1.3.3
+      pvtsutils: 1.3.5
       pvutils: 1.1.3
-      tslib: 2.6.0
+      tslib: 2.6.2
     dev: false
 
   /assert-never@1.2.1:
@@ -9690,7 +10435,7 @@ packages:
     resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==}
     dependencies:
       archy: 1.0.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       fastq: 1.15.0
     transitivePeerDependencies:
       - supports-color
@@ -9713,7 +10458,7 @@ packages:
   /axios@0.24.0:
     resolution: {integrity: sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==}
     dependencies:
-      follow-redirects: 1.15.2(debug@4.3.4)
+      follow-redirects: 1.15.2
     transitivePeerDependencies:
       - debug
     dev: false
@@ -10159,12 +10904,13 @@ packages:
       ieee754: 1.2.1
     dev: false
 
-  /bufferutil@4.0.7:
-    resolution: {integrity: sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw==}
+  /bufferutil@4.0.8:
+    resolution: {integrity: sha512-4T53u4PdgsXqKaIctwF8ifXlRTTmEPJ8iEPWFdGZvcf7sbwYo6FKFEX9eNNAnzFZ7EzJAQ3CJeOtCRA4rDp7Pw==}
     engines: {node: '>=6.14.2'}
     requiresBuild: true
     dependencies:
       node-gyp-build: 4.6.0
+    dev: false
 
   /bullmq@4.4.0:
     resolution: {integrity: sha512-2fpKxT9wQUTaOfAxaVXztYM3krvngi91lG340jz0rHcKADbkl1sSqzELystXFbormW98lhfWo6V72Cqs84biPA==}
@@ -10268,6 +11014,18 @@ packages:
     resolution: {integrity: sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==}
     engines: {node: '>=14.16'}
 
+  /cacheable-request@10.2.14:
+    resolution: {integrity: sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==}
+    engines: {node: '>=14.16'}
+    dependencies:
+      '@types/http-cache-semantics': 4.0.3
+      get-stream: 6.0.1
+      http-cache-semantics: 4.1.1
+      keyv: 4.5.4
+      mimic-response: 4.0.0
+      normalize-url: 8.0.0
+      responselike: 3.0.0
+
   /cacheable-request@10.2.8:
     resolution: {integrity: sha512-IDVO5MJ4LItE6HKFQTqT2ocAQsisOoCTUDu1ddCmnhyiwFQjXNPp4081Xj23N4tO+AFEFNzGuNEf/c8Gwwt15A==}
     engines: {node: '>=14.16'}
@@ -10279,15 +11037,16 @@ packages:
       mimic-response: 4.0.0
       normalize-url: 8.0.0
       responselike: 3.0.0
+    dev: false
 
-  /cacheable-request@7.0.2:
-    resolution: {integrity: sha512-pouW8/FmiPQbuGpkXQ9BAPv/Mo5xDGANgSNXzTzJ8DrKGuXOssM4wIQRjfanNRh3Yu5cfYPvcorqbhg2KIJtew==}
+  /cacheable-request@7.0.4:
+    resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==}
     engines: {node: '>=8'}
     dependencies:
       clone-response: 1.0.3
       get-stream: 5.2.0
       http-cache-semantics: 4.1.1
-      keyv: 4.5.2
+      keyv: 4.5.4
       lowercase-keys: 2.0.0
       normalize-url: 6.1.0
       responselike: 2.0.1
@@ -10376,8 +11135,8 @@ packages:
     dev: false
     optional: true
 
-  /cbor-x@1.5.3:
-    resolution: {integrity: sha512-adrN0S67C7jY2hgqeGcw+Uj6iEGLQa5D/p6/9YNl5AaVIYJaJz/bARfWsP8UikBZWbhS27LN0DJK4531vo9ODw==}
+  /cbor-x@1.5.4:
+    resolution: {integrity: sha512-PVKILDn+Rf6MRhhcyzGXi5eizn1i0i3F8Fe6UMMxXBnWkalq9+C5+VTmlIjAYM4iF2IYF2N+zToqAfYOp+3rfw==}
     optionalDependencies:
       cbor-extract: 2.1.1
     dev: false
@@ -10540,7 +11299,7 @@ packages:
       css-what: 6.1.0
       domelementtype: 2.3.0
       domhandler: 5.0.3
-      domutils: 3.0.1
+      domutils: 3.1.0
 
   /cheerio@1.0.0-rc.12:
     resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==}
@@ -10549,8 +11308,8 @@ packages:
       cheerio-select: 2.1.0
       dom-serializer: 2.0.0
       domhandler: 5.0.3
-      domutils: 3.0.1
-      htmlparser2: 8.0.1
+      domutils: 3.1.0
+      htmlparser2: 8.0.2
       parse5: 7.1.2
       parse5-htmlparser2-tree-adapter: 7.0.0
 
@@ -10566,7 +11325,7 @@ packages:
       normalize-path: 3.0.0
       readdirp: 3.6.0
     optionalDependencies:
-      fsevents: 2.3.2
+      fsevents: 2.3.3
 
   /chownr@1.1.4:
     resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==}
@@ -11059,6 +11818,14 @@ packages:
       - encoding
     dev: false
 
+  /cross-fetch@4.0.0:
+    resolution: {integrity: sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==}
+    dependencies:
+      node-fetch: 2.7.0
+    transitivePeerDependencies:
+      - encoding
+    dev: false
+
   /cross-spawn@5.1.0:
     resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==}
     dependencies:
@@ -11090,7 +11857,7 @@ packages:
       boolbase: 1.0.0
       css-what: 6.1.0
       domhandler: 5.0.3
-      domutils: 3.0.1
+      domutils: 3.1.0
       nth-check: 2.1.1
 
   /css-what@6.1.0:
@@ -11272,6 +12039,16 @@ packages:
     dependencies:
       ms: 2.0.0
 
+  /debug@3.2.7:
+    resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+    dependencies:
+      ms: 2.1.3
+
   /debug@3.2.7(supports-color@5.5.0):
     resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
     peerDependencies:
@@ -11282,6 +12059,7 @@ packages:
     dependencies:
       ms: 2.1.3
       supports-color: 5.5.0
+    dev: true
 
   /debug@3.2.7(supports-color@8.1.1):
     resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==}
@@ -11295,6 +12073,17 @@ packages:
       supports-color: 8.1.1
     dev: true
 
+  /debug@4.3.4:
+    resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
+    engines: {node: '>=6.0'}
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+    dependencies:
+      ms: 2.1.2
+
   /debug@4.3.4(supports-color@8.1.1):
     resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
     engines: {node: '>=6.0'}
@@ -11306,6 +12095,7 @@ packages:
     dependencies:
       ms: 2.1.2
       supports-color: 8.1.1
+    dev: true
 
   /decamelize-keys@1.1.1:
     resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
@@ -11534,11 +12324,6 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
-  /detect-libc@2.0.1:
-    resolution: {integrity: sha512-463v3ZeIrcWtdgIg6vI6XUncguvr2TnGl4SzDXinkt9mSLpBJKXT3mW6xT3VQdDN11+WVs29pgvivTc4Lp8v+w==}
-    engines: {node: '>=8'}
-    dev: false
-
   /detect-libc@2.0.2:
     resolution: {integrity: sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==}
     engines: {node: '>=8'}
@@ -11548,6 +12333,10 @@ packages:
     engines: {node: '>=8'}
     dev: true
 
+  /detect-node-es@1.1.0:
+    resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==}
+    dev: true
+
   /detect-package-manager@2.0.1:
     resolution: {integrity: sha512-j/lJHyoLlWi6G1LDdLgvUtz60Zo5GEj+sVYtTVXnYLDPuzgC3llMxonXym9zIwhhUII8vjdw0LXxavpLqTbl1A==}
     engines: {node: '>=12'}
@@ -11575,6 +12364,11 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dev: true
 
+  /diff-sequences@29.6.3:
+    resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+    dev: true
+
   /diff@5.1.0:
     resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
     engines: {node: '>=0.3.1'}
@@ -11645,6 +12439,13 @@ packages:
       domelementtype: 2.3.0
       domhandler: 5.0.3
 
+  /domutils@3.1.0:
+    resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
+    dependencies:
+      dom-serializer: 2.0.0
+      domelementtype: 2.3.0
+      domhandler: 5.0.3
+
   /dotenv-expand@10.0.0:
     resolution: {integrity: sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==}
     engines: {node: '>=12'}
@@ -12029,7 +12830,7 @@ packages:
   /eslint-import-resolver-node@0.3.7:
     resolution: {integrity: sha512-gozW2blMLJCeFpBwugLTGyvVjNoeo1knonXAcatC6bjPBZitotxdWf7Gimr25N4c0AAOo4eOUfaG82IJPDpqCA==}
     dependencies:
-      debug: 3.2.7(supports-color@5.5.0)
+      debug: 3.2.7
       is-core-module: 2.11.0
       resolve: 1.22.1
     transitivePeerDependencies:
@@ -12058,7 +12859,7 @@ packages:
         optional: true
     dependencies:
       '@typescript-eslint/parser': 5.61.0(eslint@8.45.0)(typescript@5.1.6)
-      debug: 3.2.7(supports-color@5.5.0)
+      debug: 3.2.7
       eslint: 8.45.0
       eslint-import-resolver-node: 0.3.7
     transitivePeerDependencies:
@@ -12079,7 +12880,7 @@ packages:
       array-includes: 3.1.6
       array.prototype.flat: 1.3.1
       array.prototype.flatmap: 1.3.1
-      debug: 3.2.7(supports-color@5.5.0)
+      debug: 3.2.7
       doctrine: 2.1.0
       eslint: 8.45.0
       eslint-import-resolver-node: 0.3.7
@@ -12156,7 +12957,7 @@ packages:
       ajv: 6.12.6
       chalk: 4.1.2
       cross-spawn: 7.0.3
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       doctrine: 3.0.0
       escape-string-regexp: 4.0.0
       eslint-scope: 7.2.0
@@ -12403,8 +13204,8 @@ packages:
     dependencies:
       '@jest/expect-utils': 29.6.1
       '@types/node': 20.4.2
-      jest-get-type: 29.4.3
-      jest-matcher-utils: 29.6.1
+      jest-get-type: 29.6.3
+      jest-matcher-utils: 29.7.0
       jest-message-util: 29.6.1
       jest-util: 29.6.1
     dev: true
@@ -12537,7 +13338,7 @@ packages:
       get-stream: 5.2.0
       yauzl: 2.10.0
     optionalDependencies:
-      '@types/yauzl': 2.10.0
+      '@types/yauzl': 2.10.2
     transitivePeerDependencies:
       - supports-color
     dev: true
@@ -12936,6 +13737,16 @@ packages:
       readable-stream: 2.3.7
     dev: false
 
+  /follow-redirects@1.15.2:
+    resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
+    engines: {node: '>=4.0'}
+    peerDependencies:
+      debug: '*'
+    peerDependenciesMeta:
+      debug:
+        optional: true
+    dev: false
+
   /follow-redirects@1.15.2(debug@4.3.4):
     resolution: {integrity: sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==}
     engines: {node: '>=4.0'}
@@ -12946,6 +13757,7 @@ packages:
         optional: true
     dependencies:
       debug: 4.3.4(supports-color@8.1.1)
+    dev: true
 
   /for-each@0.3.3:
     resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@@ -12999,6 +13811,7 @@ packages:
   /form-data@3.0.1:
     resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==}
     engines: {node: '>= 6'}
+    requiresBuild: true
     dependencies:
       asynckit: 0.4.0
       combined-stream: 1.0.8
@@ -13115,8 +13928,8 @@ packages:
   /fs.realpath@1.0.0:
     resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
 
-  /fsevents@2.3.2:
-    resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
+  /fsevents@2.3.3:
+    resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
     os: [darwin]
     requiresBuild: true
@@ -13142,6 +13955,7 @@ packages:
   /gauge@3.0.2:
     resolution: {integrity: sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==}
     engines: {node: '>=10'}
+    requiresBuild: true
     dependencies:
       aproba: 2.0.0
       color-support: 1.1.3
@@ -13191,6 +14005,11 @@ packages:
       has: 1.0.3
       has-symbols: 1.0.3
 
+  /get-nonce@1.0.1:
+    resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==}
+    engines: {node: '>=6'}
+    dev: true
+
   /get-npm-tarball-url@2.0.3:
     resolution: {integrity: sha512-R/PW6RqyaBQNWYaSyfrh54/qtcnOp22FHCCiRhSSZj0FP3KQWCsxxt0DzIdVTbwTqe9CtQfvl/FPD4UIPt4pqw==}
     engines: {node: '>=12.17'}
@@ -13466,8 +14285,8 @@ packages:
       get-intrinsic: 1.2.0
     dev: true
 
-  /got@11.8.5:
-    resolution: {integrity: sha512-o0Je4NvQObAuZPHLFoRSkdG2lTgtcynqymzg2Vupdx6PorhaT5MCbIyXG6d4D94kk8ZG57QeosgdiqfJWhEhlQ==}
+  /got@11.8.6:
+    resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==}
     engines: {node: '>=10.19.0'}
     dependencies:
       '@sindresorhus/is': 4.6.0
@@ -13475,7 +14294,7 @@ packages:
       '@types/cacheable-request': 6.0.3
       '@types/responselike': 1.0.0
       cacheable-lookup: 5.0.4
-      cacheable-request: 7.0.2
+      cacheable-request: 7.0.4
       decompress-response: 6.0.0
       http2-wrapper: 1.0.3
       lowercase-keys: 2.0.0
@@ -13483,14 +14302,14 @@ packages:
       responselike: 2.0.1
     dev: false
 
-  /got@12.6.0:
-    resolution: {integrity: sha512-WTcaQ963xV97MN3x0/CbAriXFZcXCfgxVp91I+Ze6pawQOa7SgzwSx2zIJJsX+kTajMnVs0xcFD1TxZKFqhdnQ==}
+  /got@12.6.1:
+    resolution: {integrity: sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==}
     engines: {node: '>=14.16'}
     dependencies:
-      '@sindresorhus/is': 5.3.0
+      '@sindresorhus/is': 5.6.0
       '@szmarczak/http-timer': 5.0.1
       cacheable-lookup: 7.0.0
-      cacheable-request: 10.2.8
+      cacheable-request: 10.2.14
       decompress-response: 6.0.0
       form-data-encoder: 2.1.4
       get-stream: 6.0.1
@@ -13690,6 +14509,7 @@ packages:
   /has-flag@3.0.0:
     resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
     engines: {node: '>=4'}
+    dev: true
 
   /has-flag@4.0.0:
     resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
@@ -13837,6 +14657,14 @@ packages:
       domutils: 3.0.1
       entities: 4.5.0
 
+  /htmlparser2@8.0.2:
+    resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==}
+    dependencies:
+      domelementtype: 2.3.0
+      domhandler: 5.0.3
+      domutils: 3.1.0
+      entities: 4.5.0
+
   /http-cache-semantics@4.1.1:
     resolution: {integrity: sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==}
 
@@ -13856,7 +14684,7 @@ packages:
     dependencies:
       '@tootallnate/once': 2.0.0
       agent-base: 6.0.2
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
     transitivePeerDependencies:
       - supports-color
     dev: false
@@ -13907,7 +14735,7 @@ packages:
     requiresBuild: true
     dependencies:
       agent-base: 4.3.0
-      debug: 3.2.7(supports-color@5.5.0)
+      debug: 3.2.7
     transitivePeerDependencies:
       - supports-color
     dev: false
@@ -13928,7 +14756,7 @@ packages:
     engines: {node: '>= 6'}
     dependencies:
       agent-base: 6.0.2
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
     transitivePeerDependencies:
       - supports-color
 
@@ -13937,7 +14765,7 @@ packages:
     engines: {node: '>= 14'}
     dependencies:
       agent-base: 7.1.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
     transitivePeerDependencies:
       - supports-color
     dev: false
@@ -14085,6 +14913,12 @@ packages:
     resolution: {integrity: sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==}
     engines: {node: '>= 0.10'}
 
+  /invariant@2.2.4:
+    resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==}
+    dependencies:
+      loose-envify: 1.4.0
+    dev: true
+
   /invert-kv@1.0.0:
     resolution: {integrity: sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==}
     engines: {node: '>=0.10.0'}
@@ -14096,7 +14930,7 @@ packages:
     dependencies:
       '@ioredis/commands': 1.2.0
       cluster-key-slot: 1.1.2
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       denque: 2.1.0
       lodash.defaults: 4.2.0
       lodash.isarguments: 3.1.0
@@ -14659,7 +15493,7 @@ packages:
     engines: {node: '>=8'}
     dependencies:
       '@babel/core': 7.22.1
-      '@babel/parser': 7.22.7
+      '@babel/parser': 7.23.0
       '@istanbuljs/schema': 0.1.3
       istanbul-lib-coverage: 3.2.0
       semver: 6.3.1
@@ -14680,7 +15514,7 @@ packages:
     resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
     engines: {node: '>=10'}
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       istanbul-lib-coverage: 3.2.0
       source-map: 0.6.1
     transitivePeerDependencies:
@@ -14860,6 +15694,16 @@ packages:
       pretty-format: 29.6.1
     dev: true
 
+  /jest-diff@29.7.0:
+    resolution: {integrity: sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+    dependencies:
+      chalk: 4.1.2
+      diff-sequences: 29.6.3
+      jest-get-type: 29.6.3
+      pretty-format: 29.7.0
+    dev: true
+
   /jest-docblock@29.4.3:
     resolution: {integrity: sha512-fzdTftThczeSD9nZ3fzA/4KkHtnmllawWrXO69vtI+L9WjEIuXWs4AmyME7lN5hU7dB0sHhuPfcKofRsUb/2Fg==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -14909,6 +15753,11 @@ packages:
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dev: true
 
+  /jest-get-type@29.6.3:
+    resolution: {integrity: sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+    dev: true
+
   /jest-haste-map@29.6.1:
     resolution: {integrity: sha512-0m7f9PZXxOCk1gRACiVgX85knUKPKLPg4oRCjLoqIm9brTHXaorMA0JpmtmVkQiT8nmXyIVoZd/nnH1cfC33ig==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -14925,7 +15774,7 @@ packages:
       micromatch: 4.0.5
       walker: 1.0.8
     optionalDependencies:
-      fsevents: 2.3.2
+      fsevents: 2.3.3
     dev: true
 
   /jest-leak-detector@29.6.1:
@@ -14956,6 +15805,16 @@ packages:
       pretty-format: 29.6.1
     dev: true
 
+  /jest-matcher-utils@29.7.0:
+    resolution: {integrity: sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+    dependencies:
+      chalk: 4.1.2
+      jest-diff: 29.7.0
+      jest-get-type: 29.6.3
+      pretty-format: 29.7.0
+    dev: true
+
   /jest-message-util@29.6.1:
     resolution: {integrity: sha512-KoAW2zAmNSd3Gk88uJ56qXUWbFk787QKmjjJVOjtGFmmGSZgDBrlIL4AfQw1xyMYPNVD7dNInfIbur9B2rd/wQ==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
@@ -15334,7 +16193,7 @@ packages:
       - supports-color
     dev: true
 
-  /jsdom@22.1.0(bufferutil@4.0.7)(utf-8-validate@6.0.3):
+  /jsdom@22.1.0(bufferutil@4.0.8)(utf-8-validate@6.0.3):
     resolution: {integrity: sha512-/9AVW7xNbsBv6GfWho4TTNjEo9fe6Zhf9O7s0Fhhr3u+awPwAJMKwAMXnkk5vBxflqLW9hTHX/0cs+P3gW+cQw==}
     engines: {node: '>=16'}
     peerDependencies:
@@ -15364,7 +16223,7 @@ packages:
       whatwg-encoding: 2.0.0
       whatwg-mimetype: 3.0.0
       whatwg-url: 12.0.1
-      ws: 8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3)
+      ws: 8.13.0(bufferutil@4.0.8)(utf-8-validate@6.0.3)
       xml-name-validator: 4.0.0
     transitivePeerDependencies:
       - bufferutil
@@ -15394,7 +16253,7 @@ packages:
     resolution: {integrity: sha512-pJ4XLQP4Q9HTxl6RVDLJ8Cyh1uitSs0CzDBAz1uoJ4sRD/Bk7cFSXL1FUXDW3zJ7YnfliJx6eu8Jn283bpZ4Yg==}
     engines: {node: '>=10'}
     dependencies:
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       rfdc: 1.3.0
       uri-js: 4.4.1
     transitivePeerDependencies:
@@ -15526,6 +16385,12 @@ packages:
     resolution: {integrity: sha512-5MHbFaKn8cNSmVW7BYnijeAVlE4cYA/SVkifVgrh7yotnfhKmjuXpDKjrABLnT0SfHWV21P8ow07OGfRrNDg8g==}
     dependencies:
       json-buffer: 3.0.1
+    dev: false
+
+  /keyv@4.5.4:
+    resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==}
+    dependencies:
+      json-buffer: 3.0.1
 
   /kind-of@3.2.2:
     resolution: {integrity: sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==}
@@ -16290,6 +17155,7 @@ packages:
   /mkdirp@0.5.6:
     resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==}
     hasBin: true
+    requiresBuild: true
     dependencies:
       minimist: 1.2.8
 
@@ -16514,7 +17380,7 @@ packages:
     resolution: {integrity: sha512-6R9fqJ5Zcmf+uYaFgdIHmLwNldn5HbK8L5ybn7Uz+ylX/rnOsSp1AHcvQSrCaFN+qNM1wpymHqD7mVasEOlHGQ==}
     engines: {node: '>= 4.4.x'}
     dependencies:
-      debug: 3.2.7(supports-color@5.5.0)
+      debug: 3.2.7
       iconv-lite: 0.4.24
       sax: 1.2.4
     transitivePeerDependencies:
@@ -16551,14 +17417,14 @@ packages:
       path-to-regexp: 1.8.0
     dev: true
 
-  /node-abi@3.31.0:
-    resolution: {integrity: sha512-eSKV6s+APenqVh8ubJyiu/YhZgxQpGP66ntzUb3lY1xB9ukSRaGnx0AIxI+IM+1+IVYC1oWobgG5L3Lt9ARykQ==}
+  /node-abi@3.51.0:
+    resolution: {integrity: sha512-SQkEP4hmNWjlniS5zdnfIXTk1x7Ome85RDzHlTbBtzE97Gfwz/Ipw4v/Ryk20DWIy3yCNVLVlGKApCnmvYoJbA==}
     engines: {node: '>=10'}
     dependencies:
       semver: 7.5.4
 
-  /node-addon-api@5.0.0:
-    resolution: {integrity: sha512-CvkDw2OEnme7ybCykJpVcKH+uAOLV2qLqiyla128dN9TkEWfrYmxG6C2boDe5KcNQqZF3orkqzGgOMvZ/JNekA==}
+  /node-addon-api@5.1.0:
+    resolution: {integrity: sha512-eh0GgfEkpnoWDq+VY8OyvYhFEzBk6jIYbRKdIlyTiAXIVJ8PyBaKb0rp7oDtoddbdoHWhq8wwr+XZ81F1rpNdA==}
     dev: false
 
   /node-addon-api@6.1.0:
@@ -16595,6 +17461,19 @@ packages:
     dependencies:
       whatwg-url: 5.0.0
 
+  /node-fetch@2.6.13:
+    resolution: {integrity: sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==}
+    engines: {node: 4.x || >=6.0.0}
+    requiresBuild: true
+    peerDependencies:
+      encoding: ^0.1.0
+    peerDependenciesMeta:
+      encoding:
+        optional: true
+    dependencies:
+      whatwg-url: 5.0.0
+    dev: false
+
   /node-fetch@2.6.7:
     resolution: {integrity: sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==}
     engines: {node: 4.x || >=6.0.0}
@@ -16605,6 +17484,19 @@ packages:
         optional: true
     dependencies:
       whatwg-url: 5.0.0
+    dev: true
+
+  /node-fetch@2.7.0:
+    resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
+    engines: {node: 4.x || >=6.0.0}
+    peerDependencies:
+      encoding: ^0.1.0
+    peerDependenciesMeta:
+      encoding:
+        optional: true
+    dependencies:
+      whatwg-url: 5.0.0
+    dev: false
 
   /node-fetch@3.3.1:
     resolution: {integrity: sha512-cRVc/kyto/7E5shrWca1Wsea4y6tL9iYJE5FBCius3JQfb/4P4I295PfhgbJQBLTx6lATE4z+wK0rPM4VS2uow==}
@@ -16632,6 +17524,7 @@ packages:
     resolution: {integrity: sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==}
     hasBin: true
     requiresBuild: true
+    dev: false
 
   /node-gyp@9.4.0:
     resolution: {integrity: sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg==}
@@ -16792,6 +17685,7 @@ packages:
 
   /npmlog@5.0.1:
     resolution: {integrity: sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==}
+    requiresBuild: true
     dependencies:
       are-we-there-yet: 2.0.0
       console-control-strings: 1.1.0
@@ -17914,7 +18808,7 @@ packages:
       minimist: 1.2.8
       mkdirp-classic: 0.5.3
       napi-build-utils: 1.0.2
-      node-abi: 3.31.0
+      node-abi: 3.51.0
       pump: 3.0.0
       rc: 1.2.8
       simple-get: 4.0.1
@@ -17976,7 +18870,16 @@ packages:
     resolution: {integrity: sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==}
     engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
     dependencies:
-      '@jest/schemas': 29.6.0
+      '@jest/schemas': 29.6.3
+      ansi-styles: 5.2.0
+      react-is: 18.2.0
+    dev: true
+
+  /pretty-format@29.7.0:
+    resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==}
+    engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
+    dependencies:
+      '@jest/schemas': 29.6.3
       ansi-styles: 5.2.0
       react-is: 18.2.0
     dev: true
@@ -18026,6 +18929,7 @@ packages:
   /progress@2.0.3:
     resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==}
     engines: {node: '>=0.4.0'}
+    requiresBuild: true
 
   /promise-limit@2.7.0:
     resolution: {integrity: sha512-7nJ6v5lnJsXwGprnGXga4wx6d1POjvi5Qmf1ivTRxTjH4Z/9Czja/UCMLVmB9N93GeWOU93XaFaEt6jbuoagNw==}
@@ -18238,10 +19142,10 @@ packages:
       pngjs: 3.4.0
     dev: false
 
-  /pvtsutils@1.3.3:
-    resolution: {integrity: sha512-6sAOMlXyrJ+8tRN5IAaYfuYZRp1C2uJ0SyDynEFxL+VY8kCRib9Lpj/+KPaNFpaQWr/iRik5nrzz6iaNlxgEGA==}
+  /pvtsutils@1.3.5:
+    resolution: {integrity: sha512-ARvb14YB9Nm2Xi6nBq1ZX6dAM0FsJnuk+31aUp4TrcZEdKUlSqOqsxJHUPJDNE3qiIp+iUPEIeR6Je/tgV7zsA==}
     dependencies:
-      tslib: 2.6.1
+      tslib: 2.6.2
     dev: false
 
   /pvutils@1.1.3:
@@ -18286,6 +19190,13 @@ packages:
       side-channel: 1.0.4
     dev: true
 
+  /qs@6.11.2:
+    resolution: {integrity: sha512-tDNIz22aBzCDxLtVH++VnTfzxlfeK5CbqohpSqpJgj1Wg/cQbStNAz3NuqCs5vV+pjBsK4x4pN9HlVh7rcYRiA==}
+    engines: {node: '>=0.6'}
+    dependencies:
+      side-channel: 1.0.4
+    dev: true
+
   /qs@6.5.3:
     resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
     engines: {node: '>=0.6'}
@@ -18477,6 +19388,55 @@ packages:
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /react-remove-scroll-bar@2.3.4(react@18.2.0):
+    resolution: {integrity: sha512-63C4YQBUt0m6ALadE9XV56hV8BgJWDmmTPY758iIJjfQKt2nYwoUrPk0LXRXcB/yIj82T1/Ixfdpdk68LwIB0A==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      react: 18.2.0
+      react-style-singleton: 2.2.1(react@18.2.0)
+      tslib: 2.6.2
+    dev: true
+
+  /react-remove-scroll@2.5.5(react@18.2.0):
+    resolution: {integrity: sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      react: 18.2.0
+      react-remove-scroll-bar: 2.3.4(react@18.2.0)
+      react-style-singleton: 2.2.1(react@18.2.0)
+      tslib: 2.6.2
+      use-callback-ref: 1.3.0(react@18.2.0)
+      use-sidecar: 1.1.2(react@18.2.0)
+    dev: true
+
+  /react-style-singleton@2.2.1(react@18.2.0):
+    resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      get-nonce: 1.0.1
+      invariant: 2.2.4
+      react: 18.2.0
+      tslib: 2.6.2
+    dev: true
+
   /react-syntax-highlighter@15.5.0(react@18.2.0):
     resolution: {integrity: sha512-+zq2myprEnQmH5yw6Gqc8lD55QHnpKaU8TOcFeC/Lg/MQSs8UknEA0JC4nTZGFAXC2J2Hyj/ijJ7NlabyPi2gg==}
     peerDependencies:
@@ -18708,6 +19668,10 @@ packages:
   /regenerator-runtime@0.13.11:
     resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
 
+  /regenerator-runtime@0.14.0:
+    resolution: {integrity: sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==}
+    dev: true
+
   /regenerator-transform@0.15.1:
     resolution: {integrity: sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==}
     dependencies:
@@ -18994,6 +19958,7 @@ packages:
   /rimraf@2.7.1:
     resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==}
     hasBin: true
+    requiresBuild: true
     dependencies:
       glob: 7.2.3
 
@@ -19008,7 +19973,7 @@ packages:
     engines: {node: '>=14.18.0', npm: '>=8.0.0'}
     hasBin: true
     optionalDependencies:
-      fsevents: 2.3.2
+      fsevents: 2.3.3
 
   /rrweb-cssom@0.6.0:
     resolution: {integrity: sha512-APM0Gt1KoXBz0iIkkdB/kfvGOwC4UuJFeG/c+yV7wSc7q96cG/kJ0HiYCnzivD9SB53cLV1MlHFNfOuPaadYSw==}
@@ -19271,8 +20236,8 @@ packages:
     requiresBuild: true
     dependencies:
       color: 4.2.3
-      detect-libc: 2.0.1
-      node-addon-api: 5.0.0
+      detect-libc: 2.0.2
+      node-addon-api: 5.1.0
       prebuild-install: 7.1.1
       semver: 7.5.4
       simple-get: 4.0.1
@@ -19598,7 +20563,7 @@ packages:
     engines: {node: '>= 10'}
     dependencies:
       agent-base: 6.0.2
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       socks: 2.7.1
     transitivePeerDependencies:
       - supports-color
@@ -19730,6 +20695,7 @@ packages:
 
   /sprintf-js@1.0.3:
     resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==}
+    requiresBuild: true
 
   /sprintf-js@1.1.2:
     resolution: {integrity: sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==}
@@ -20073,6 +21039,7 @@ packages:
     engines: {node: '>=4'}
     dependencies:
       has-flag: 3.0.0
+    dev: true
 
   /supports-color@7.2.0:
     resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
@@ -20085,6 +21052,7 @@ packages:
     engines: {node: '>=10'}
     dependencies:
       has-flag: 4.0.0
+    dev: true
 
   /supports-hyperlinks@2.3.0:
     resolution: {integrity: sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==}
@@ -20201,6 +21169,12 @@ packages:
       memoizerific: 1.11.3
     dev: true
 
+  /telejson@7.2.0:
+    resolution: {integrity: sha512-1QTEcJkJEhc8OnStBx/ILRu5J2p0GjvWsBx56bmZRqnrkdBMUe+nX92jxV+p3dB4CP6PZCdJMQJwCggkNBMzkQ==}
+    dependencies:
+      memoizerific: 1.11.3
+    dev: true
+
   /temp-dir@2.0.0:
     resolution: {integrity: sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==}
     engines: {node: '>=8'}
@@ -20555,9 +21529,8 @@ packages:
   /tslib@2.6.0:
     resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==}
 
-  /tslib@2.6.1:
-    resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==}
-    dev: false
+  /tslib@2.6.2:
+    resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==}
 
   /tsutils@3.21.0(typescript@5.1.6):
     resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==}
@@ -20717,7 +21690,7 @@ packages:
       chalk: 4.1.2
       cli-highlight: 2.1.11
       date-fns: 2.30.0
-      debug: 4.3.4(supports-color@8.1.1)
+      debug: 4.3.4
       dotenv: 16.0.3
       glob: 8.1.0
       ioredis: 5.3.2
@@ -20990,6 +21963,20 @@ packages:
     resolution: {integrity: sha512-RtuPeMy7c1UrHwproMZN9gN6kiZ0SvJwRaEzwZY0j9MypEkFqyBaKv176jvlPtg58Zh36bOkS0NFABXMHvvGCA==}
     dev: false
 
+  /use-callback-ref@1.3.0(react@18.2.0):
+    resolution: {integrity: sha512-3FT9PRuRdbB9HfXhEq35u4oZkvpJ5kuYbpqhCfmiZyReuRgpnhDlbr2ZEnnuS0RrJAPn6l23xjFg9kpDM+Ms7w==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      react: 18.2.0
+      tslib: 2.6.2
+    dev: true
+
   /use-resize-observer@9.1.0(react-dom@18.2.0)(react@18.2.0):
     resolution: {integrity: sha512-R25VqO9Wb3asSD4eqtcxk8sJalvIOYBqS8MNZlpDSQ4l4xMQxC/J7Id9HoTqPq8FwULIn0PVW+OAqF2dyYbjow==}
     peerDependencies:
@@ -21001,6 +21988,21 @@ packages:
       react-dom: 18.2.0(react@18.2.0)
     dev: true
 
+  /use-sidecar@1.1.2(react@18.2.0):
+    resolution: {integrity: sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==}
+    engines: {node: '>=10'}
+    peerDependencies:
+      '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0
+      react: ^16.8.0 || ^17.0.0 || ^18.0.0
+    peerDependenciesMeta:
+      '@types/react':
+        optional: true
+    dependencies:
+      detect-node-es: 1.1.0
+      react: 18.2.0
+      tslib: 2.6.2
+    dev: true
+
   /use@3.1.1:
     resolution: {integrity: sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==}
     engines: {node: '>=0.10.0'}
@@ -21012,6 +22014,7 @@ packages:
     requiresBuild: true
     dependencies:
       node-gyp-build: 4.6.0
+    dev: false
 
   /util-deprecate@1.0.2:
     resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
@@ -21210,7 +22213,7 @@ packages:
       rollup: 3.26.3
       sass: 1.63.6
     optionalDependencies:
-      fsevents: 2.3.2
+      fsevents: 2.3.3
 
   /vitest-fetch-mock@0.2.2(vitest@0.33.0):
     resolution: {integrity: sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==}
@@ -21294,8 +22297,8 @@ packages:
     resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
     engines: {node: '>=0.10.0'}
 
-  /vue-component-type-helpers@1.8.11:
-    resolution: {integrity: sha512-CWItFzuEWjkSXDeMGwQEc5cFH4FaueyPQHfi1mBDe+wA2JABqNjFxFUtmZJ9WHkb0HpEwqgBg1umiXrWYXkXHw==}
+  /vue-component-type-helpers@1.8.22:
+    resolution: {integrity: sha512-LK3wJHs3vJxHG292C8cnsRusgyC5SEZDCzDCD01mdE/AoREFMl2tzLRuzwyuEsOIz13tqgBcnvysN3Lxsa14Fw==}
     dev: true
 
   /vue-docgen-api@4.64.1(vue@3.3.4):
@@ -21654,7 +22657,20 @@ packages:
       async-limiter: 1.0.1
     dev: true
 
-  /ws@8.13.0(bufferutil@4.0.7)(utf-8-validate@6.0.3):
+  /ws@8.13.0:
+    resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
+    engines: {node: '>=10.0.0'}
+    peerDependencies:
+      bufferutil: ^4.0.1
+      utf-8-validate: '>=5.0.2'
+    peerDependenciesMeta:
+      bufferutil:
+        optional: true
+      utf-8-validate:
+        optional: true
+    dev: true
+
+  /ws@8.13.0(bufferutil@4.0.8)(utf-8-validate@6.0.3):
     resolution: {integrity: sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==}
     engines: {node: '>=10.0.0'}
     peerDependencies:
@@ -21666,8 +22682,9 @@ packages:
       utf-8-validate:
         optional: true
     dependencies:
-      bufferutil: 4.0.7
+      bufferutil: 4.0.8
       utf-8-validate: 6.0.3
+    dev: false
 
   /xev@3.0.2:
     resolution: {integrity: sha512-8kxuH95iMXzHZj+fwqfA4UrPcYOy6bGIgfWzo9Ji23JoEc30ge/Z++Ubkiuy8c0+M64nXmmxrmJ7C8wnuBhluw==}
@@ -21865,7 +22882,7 @@ packages:
       sharp: 0.31.3
     dev: false
 
-  github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.27)(@storybook/components@7.1.0)(@storybook/core-events@7.0.27)(@storybook/manager-api@7.0.27)(@storybook/preview-api@7.0.27)(@storybook/theming@7.0.27)(@storybook/types@7.0.27)(react-dom@18.2.0)(react@18.2.0):
+  github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640(@storybook/blocks@7.0.27)(@storybook/components@7.5.1)(@storybook/core-events@7.0.27)(@storybook/manager-api@7.0.27)(@storybook/preview-api@7.0.27)(@storybook/theming@7.0.27)(@storybook/types@7.0.27)(react-dom@18.2.0)(react@18.2.0):
     resolution: {tarball: https://codeload.github.com/misskey-dev/storybook-addon-misskey-theme/tar.gz/cf583db098365b2ccc81a82f63ca9c93bc32b640}
     id: github.com/misskey-dev/storybook-addon-misskey-theme/cf583db098365b2ccc81a82f63ca9c93bc32b640
     name: storybook-addon-misskey-theme
@@ -21887,7 +22904,7 @@ packages:
         optional: true
     dependencies:
       '@storybook/blocks': 7.0.27(react-dom@18.2.0)(react@18.2.0)
-      '@storybook/components': 7.1.0(react-dom@18.2.0)(react@18.2.0)
+      '@storybook/components': 7.5.1(react-dom@18.2.0)(react@18.2.0)
       '@storybook/core-events': 7.0.27
       '@storybook/manager-api': 7.0.27(react-dom@18.2.0)(react@18.2.0)
       '@storybook/preview-api': 7.0.27
@@ -21904,7 +22921,7 @@ packages:
     dependencies:
       cheerio: 1.0.0-rc.12
       escape-regexp: 0.0.1
-      got: 12.6.0
+      got: 12.6.1
       html-entities: 2.3.2
       iconv-lite: 0.6.3
       jschardet: 3.0.0

From 5bcf8e9b9d63d035388696e418c2531a52ce6b10 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 30 Oct 2023 00:42:11 +0900
Subject: [PATCH 06/36] =?UTF-8?q?fix(backend):=20=E3=83=97=E3=83=AD?=
 =?UTF-8?q?=E3=83=95=E3=82=A3=E3=83=BC=E3=83=AB=E3=81=AE=E8=87=AA=E5=B7=B1?=
 =?UTF-8?q?=E7=B4=B9=E4=BB=8B=E6=AC=84=E3=81=AEMFM=E3=82=92=E9=80=A3?=
 =?UTF-8?q?=E5=90=88=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(missk?=
 =?UTF-8?q?ey-dev#12184)=20(MisskeyIO#199)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: kakkokari-gtyih <daisho7308+f@gmail.com>
---
 .../src/core/activitypub/ApRendererService.ts |  2 ++
 .../activitypub/models/ApPersonService.ts     | 20 +++++++++++++++++--
 packages/backend/src/core/activitypub/type.ts |  1 +
 3 files changed, 21 insertions(+), 2 deletions(-)

diff --git a/packages/backend/src/core/activitypub/ApRendererService.ts b/packages/backend/src/core/activitypub/ApRendererService.ts
index 607ab293b3..928144a12b 100644
--- a/packages/backend/src/core/activitypub/ApRendererService.ts
+++ b/packages/backend/src/core/activitypub/ApRendererService.ts
@@ -496,6 +496,7 @@ export class ApRendererService {
 			preferredUsername: user.username,
 			name: user.name,
 			summary: profile.description ? this.mfmService.toHtml(mfm.parse(profile.description)) : null,
+			_misskey_summary: profile.description,
 			icon: avatar ? this.renderImage(avatar) : null,
 			image: banner ? this.renderImage(banner) : null,
 			tag,
@@ -645,6 +646,7 @@ export class ApRendererService {
 					'_misskey_quote': 'misskey:_misskey_quote',
 					'_misskey_reaction': 'misskey:_misskey_reaction',
 					'_misskey_votes': 'misskey:_misskey_votes',
+					'_misskey_summary': 'misskey:_misskey_summary',
 					'isCat': 'misskey:isCat',
 					// vcard
 					vcard: 'http://www.w3.org/2006/vcard/ns#',
diff --git a/packages/backend/src/core/activitypub/models/ApPersonService.ts b/packages/backend/src/core/activitypub/models/ApPersonService.ts
index e133ce5d70..bc8c0a85ba 100644
--- a/packages/backend/src/core/activitypub/models/ApPersonService.ts
+++ b/packages/backend/src/core/activitypub/models/ApPersonService.ts
@@ -335,9 +335,17 @@ export class ApPersonService implements OnModuleInit {
 					emojis,
 				})) as MiRemoteUser;
 
+				let _description: string | null = null;
+
+				if (person._misskey_summary) {
+					_description = truncate(person._misskey_summary, summaryLength);
+				} else if (person.summary) {
+					_description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag);
+				}
+
 				await transactionalEntityManager.save(new MiUserProfile({
 					userId: user.id,
-					description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
+					description: _description,
 					url,
 					fields,
 					birthday: bday?.[0] ?? null,
@@ -503,10 +511,18 @@ export class ApPersonService implements OnModuleInit {
 			});
 		}
 
+		let _description: string | null = null;
+
+		if (person._misskey_summary) {
+			_description = truncate(person._misskey_summary, summaryLength);
+		} else if (person.summary) {
+			_description = this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag);
+		}
+
 		await this.userProfilesRepository.update({ userId: exist.id }, {
 			url,
 			fields,
-			description: person.summary ? this.apMfmService.htmlToMfm(truncate(person.summary, summaryLength), person.tag) : null,
+			description: _description,
 			birthday: bday?.[0] ?? null,
 			location: person['vcard:Address'] ?? null,
 		});
diff --git a/packages/backend/src/core/activitypub/type.ts b/packages/backend/src/core/activitypub/type.ts
index 16ff86e894..d9fcc99066 100644
--- a/packages/backend/src/core/activitypub/type.ts
+++ b/packages/backend/src/core/activitypub/type.ts
@@ -12,6 +12,7 @@ export interface IObject {
 	id?: string;
 	name?: string | null;
 	summary?: string;
+	_misskey_summary?: string;
 	published?: string;
 	cc?: ApObject;
 	to?: ApObject;

From 08c5e1616e3e25b21d50db7ec5b4d75306f0fafd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 30 Oct 2023 00:54:38 +0900
Subject: [PATCH 07/36] =?UTF-8?q?feat(frontend):=20=E7=B5=B5=E6=96=87?=
 =?UTF-8?q?=E5=AD=97=E3=83=94=E3=83=83=E3=82=AB=E3=83=BC=E3=81=AE=E3=82=AB?=
 =?UTF-8?q?=E3=83=86=E3=82=B4=E3=83=AA=E3=82=92=E5=A4=9A=E9=9A=8E=E5=B1=A4?=
 =?UTF-8?q?=E3=83=95=E3=82=A9=E3=83=AB=E3=83=80=E3=81=A7=E5=88=86=E9=A1=9E?=
 =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(missk?=
 =?UTF-8?q?ey-dev#12132)=20(MisskeyIO#196)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: meronmks <meronmks.8914@gmail.com>
Co-authored-by: mattyatea <mattyacocacora0@gmail.com>
---
 locales/en-US.yml                             |  1 +
 locales/index.d.ts                            |  1 +
 locales/ja-JP.yml                             |  1 +
 .../src/components/MkEmojiPicker.section.vue  | 47 ++++++++++++++++--
 .../frontend/src/components/MkEmojiPicker.vue | 49 ++++++++++++++++---
 packages/frontend/src/scripts/emojilist.ts    |  6 +++
 6 files changed, 96 insertions(+), 9 deletions(-)

diff --git a/locales/en-US.yml b/locales/en-US.yml
index b693f791bb..d448686187 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -307,6 +307,7 @@ folderName: "Folder name"
 createFolder: "Create a folder"
 renameFolder: "Rename this folder"
 deleteFolder: "Delete this folder"
+folder: "Folder"
 addFile: "Add a file"
 emptyDrive: "Your Drive is empty"
 emptyFolder: "This folder is empty"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index c41f40bbcf..b0b8a256f2 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -310,6 +310,7 @@ export interface Locale {
     "createFolder": string;
     "renameFolder": string;
     "deleteFolder": string;
+    "folder": string;
     "addFile": string;
     "emptyDrive": string;
     "emptyFolder": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 3464b1be40..979022e33a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -307,6 +307,7 @@ folderName: "フォルダー名"
 createFolder: "フォルダーを作成"
 renameFolder: "フォルダー名を変更"
 deleteFolder: "フォルダーを削除"
+folder: "フォルダー"
 addFile: "ファイルを追加"
 emptyDrive: "ドライブは空です"
 emptyFolder: "フォルダーは空です"
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index 4c8c0c7902..fda847bef9 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -5,9 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <template>
 <!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
-<section>
+<!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
+<section v-if="!hasChildSection">
 	<header class="_acrylic" @click="shown = !shown">
-		<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> ({{ emojis.length }})
+		<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-icons"></i>:{{ emojis.length }})
 	</header>
 	<div v-if="shown" class="body">
 		<button
@@ -23,15 +24,52 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</button>
 	</div>
 </section>
+<!-- フォルダの中にはカスタム絵文字やフォルダがある -->
+<section v-else>
+  <header class="_acrylic" @click="shown = !shown">
+    <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons"></i>:{{ emojis.length }})
+  </header>
+  <div v-if="shown" class="body">
+    <button
+        v-for="emoji in emojis"
+        :key="emoji"
+        :data-emoji="emoji"
+        class="_button item"
+        @pointerenter="computeButtonTitle"
+        @click="emit('chosen', emoji, $event)"
+    >
+      <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
+      <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
+    </button>
+  </div>
+  <div v-if="shown" style="padding-left: 9px;">
+    <MkEmojiPickerSection
+        v-for="child in customEmojiTree"
+        :key="`custom:${child.value}`"
+        :initialShown="initialShown"
+        :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
+        :hasChildSection="child.children.length !== 0"
+        :customEmojiTree="child.children"
+        @chosen="nestedChosen"
+    >
+			{{ child.value || i18n.ts.other }}
+    </MkEmojiPickerSection>
+  </div>
+</section>
 </template>
 
 <script lang="ts" setup>
 import { ref, computed, Ref } from 'vue';
-import { getEmojiName } from '@/scripts/emojilist';
+import { CustomEmojiFolderTree, getEmojiName } from '@/scripts/emojilist.js';
+import { i18n } from '../i18n.js';
+import { customEmojis } from '@/custom-emojis.js';
+import MkEmojiPickerSection from '@/components/MkEmojiPicker.section.vue';
 
 const props = defineProps<{
 	emojis: string[] | Ref<string[]>;
 	initialShown?: boolean;
+	hasChildSection?: boolean;
+	customEmojiTree?: CustomEmojiFolderTree[];
 }>();
 
 const emit = defineEmits<{
@@ -49,4 +87,7 @@ function computeButtonTitle(ev: MouseEvent): void {
 	elm.title = getEmojiName(emoji) ?? emoji;
 }
 
+function nestedChosen(emoji: any, ev?: MouseEvent) {
+	emit('chosen', emoji, ev);
+}
 </script>
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 169d0cb226..9d96ed3de6 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -73,18 +73,20 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<div v-once class="group">
 			<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
 			<XSection
-				v-for="category in customEmojiCategories"
-				:key="`custom:${category}`"
+				v-for="child in customEmojiFolderRoot.children"
+				:key="`custom:${child.value}`"
 				:initialShown="false"
-				:emojis="computed(() => customEmojis.filter(e => category === null ? (e.category === 'null' || !e.category) : e.category === category).filter(filterAvailable).map(e => `:${e.name}:`))"
+				:emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).filter(filterAvailable).map(e => `:${e.name}:`))"
+        :hasChildSection="child.children.length !== 0"
+        :customEmojiTree="child.children"
 				@chosen="chosen"
 			>
-				{{ category || i18n.ts.other }}
+				{{ child.value || i18n.ts.other }}
 			</XSection>
 		</div>
 		<div v-once class="group">
 			<header class="_acrylic">{{ i18n.ts.emoji }}</header>
-			<XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" @chosen="chosen">{{ category }}</XSection>
+			<XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" :hasChildSection="false" @chosen="chosen">{{ category }}</XSection>
 		</div>
 	</div>
 	<div class="tabs">
@@ -100,7 +102,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref, shallowRef, computed, watch, onMounted } from 'vue';
 import * as Misskey from 'misskey-js';
 import XSection from '@/components/MkEmojiPicker.section.vue';
-import { emojilist, emojiCharByCategory, UnicodeEmojiDef, unicodeEmojiCategories as categories, getEmojiName } from '@/scripts/emojilist';
+import {
+  emojilist,
+  emojiCharByCategory,
+  UnicodeEmojiDef,
+  unicodeEmojiCategories as categories,
+  getEmojiName,
+  CustomEmojiFolderTree
+} from '@/scripts/emojilist.js';
 import MkRippleEffect from '@/components/MkRippleEffect.vue';
 import * as os from '@/os';
 import { isTouchUsing } from '@/scripts/touch';
@@ -144,6 +153,34 @@ const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
 const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
 const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
 
+const customEmojiFolderRoot: CustomEmojiFolderTree = { value: "", category: "", children: [] };
+
+function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
+	const parts = input.split('/').map(p => p.trim());
+	let currentNode: CustomEmojiFolderTree = root;
+
+	for (const part of parts) {
+		let existingNode = currentNode.children.find((node) => node.value === part);
+		if (!existingNode) {
+			const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] };
+			currentNode.children.push(newNode);
+			existingNode = newNode;
+		}
+
+		currentNode = existingNode;
+	}
+
+	return currentNode;
+}
+
+customEmojiCategories.value.forEach(ec => {
+  if (ec !== null) {
+    parseAndMergeCategories(ec, customEmojiFolderRoot);
+  }
+});
+
+parseAndMergeCategories('', customEmojiFolderRoot);
+
 watch(q, () => {
 	if (emojisEl.value) emojisEl.value.scrollTop = 0;
 
diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts
index 4159da84c8..8885bf4b7f 100644
--- a/packages/frontend/src/scripts/emojilist.ts
+++ b/packages/frontend/src/scripts/emojilist.ts
@@ -43,3 +43,9 @@ export function getEmojiName(char: string): string | null {
 		return emojilist[idx].name;
 	}
 }
+
+export interface CustomEmojiFolderTree {
+	value: string;
+	category: string;
+	children: CustomEmojiFolderTree[];
+}

From 2bad8941d0048a89a29a3d5cb1195a54f234efde Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 30 Oct 2023 00:55:05 +0900
Subject: [PATCH 08/36] Bump up version to 13.14.2-io.13 (MisskeyIO#200)

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 1fb0662e14..5ae8e67189 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.14.2-io.12b",
+	"version": "13.14.2-io.13",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",

From 343ae413aeaec4d6d169874cd63faf7866d7df7b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 30 Oct 2023 08:50:21 +0900
Subject: [PATCH 09/36] =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=83=94?=
 =?UTF-8?q?=E3=83=83=E3=82=AB=E3=83=BC=E3=81=AE=E3=82=AB=E3=83=86=E3=82=B4?=
 =?UTF-8?q?=E3=83=AA=E3=81=AE=E9=9A=8E=E5=B1=A4=E3=82=92=E8=A1=A8=E7=A4=BA?=
 =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=E3=83=BB=E3=82=B5?=
 =?UTF-8?q?=E3=83=96=E3=82=AB=E3=83=86=E3=82=B4=E3=83=AA=E3=82=92=E7=B5=B5?=
 =?UTF-8?q?=E6=96=87=E5=AD=97=E3=82=88=E3=82=8A=E4=B8=8A=E3=81=AB=20(Missk?=
 =?UTF-8?q?eyIO#202)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/components/MkEmojiPicker.section.vue  | 26 +++++++++----------
 .../frontend/src/components/MkEmojiPicker.vue | 12 ++++++---
 2 files changed, 22 insertions(+), 16 deletions(-)

diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index fda847bef9..6f99fa9200 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -29,6 +29,19 @@ SPDX-License-Identifier: AGPL-3.0-only
   <header class="_acrylic" @click="shown = !shown">
     <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons"></i>:{{ emojis.length }})
   </header>
+  <div v-if="shown" style="padding-left: 9px;">
+    <MkEmojiPickerSection
+        v-for="child in customEmojiTree"
+        :key="`custom:${child.value}`"
+        :initialShown="initialShown"
+        :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
+        :hasChildSection="child.children.length !== 0"
+        :customEmojiTree="child.children"
+        @chosen="nestedChosen"
+    >
+      {{ child.value || i18n.ts.other }}
+    </MkEmojiPickerSection>
+  </div>
   <div v-if="shown" class="body">
     <button
         v-for="emoji in emojis"
@@ -42,19 +55,6 @@ SPDX-License-Identifier: AGPL-3.0-only
       <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
     </button>
   </div>
-  <div v-if="shown" style="padding-left: 9px;">
-    <MkEmojiPickerSection
-        v-for="child in customEmojiTree"
-        :key="`custom:${child.value}`"
-        :initialShown="initialShown"
-        :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
-        :hasChildSection="child.children.length !== 0"
-        :customEmojiTree="child.children"
-        @chosen="nestedChosen"
-    >
-			{{ child.value || i18n.ts.other }}
-    </MkEmojiPickerSection>
-  </div>
 </section>
 </template>
 
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 9d96ed3de6..9ebd352170 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -156,13 +156,19 @@ const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
 const customEmojiFolderRoot: CustomEmojiFolderTree = { value: "", category: "", children: [] };
 
 function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
-	const parts = input.split('/').map(p => p.trim());
+	const parts = (input && input !== 'null' ? input : '').split('/');
 	let currentNode: CustomEmojiFolderTree = root;
 
 	for (const part of parts) {
-		let existingNode = currentNode.children.find((node) => node.value === part);
+		const path = currentNode.value ? `${currentNode.value}/${part.trim()}` : part.trim();
+
+		let existingNode = currentNode.children.find((node) => node.value === path);
 		if (!existingNode) {
-			const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] };
+			const newNode: CustomEmojiFolderTree = {
+				value: path,
+				category: currentNode.category ? `${currentNode.category}/${part}` : part,
+				children: [],
+			};
 			currentNode.children.push(newNode);
 			existingNode = newNode;
 		}

From f66f78d912934cc1e57a3009537e25df675a441d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 30 Oct 2023 08:50:41 +0900
Subject: [PATCH 10/36]  Bump up version to 13.14.2-io.13a (MisskeyIO#203)

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 5ae8e67189..4a5d142200 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.14.2-io.13",
+	"version": "13.14.2-io.13a",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",

From c29ec98e3bdfecbe0aebc2ee352b53480d73b3c6 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 3 Nov 2023 01:54:47 +0900
Subject: [PATCH 11/36] =?UTF-8?q?=E7=B5=B5=E6=96=87=E5=AD=97=E3=83=94?=
 =?UTF-8?q?=E3=83=83=E3=82=AB=E3=83=BC=E3=81=AE=E3=82=AB=E3=83=86=E3=82=B4?=
 =?UTF-8?q?=E3=83=AA=E3=81=AE=E3=83=95=E3=82=A9=E3=83=AB=E3=83=80=E5=88=86?=
 =?UTF-8?q?=E3=81=91=E3=81=AE=E6=9D=A1=E4=BB=B6=E3=81=AE=E5=A4=89=E6=9B=B4?=
 =?UTF-8?q?=E3=83=BB=E9=95=B7=E3=81=84=E3=82=AB=E3=83=86=E3=82=B4=E3=83=AA?=
 =?UTF-8?q?=E5=90=8D=E3=81=AE=E8=A1=A8=E7=A4=BA=E8=AA=BF=E6=95=B4=E3=83=BB?=
 =?UTF-8?q?=E3=82=BB=E3=82=AF=E3=82=B7=E3=83=A7=E3=83=B3=E3=81=AE=E8=83=8C?=
 =?UTF-8?q?=E6=99=AF=E3=81=AE=E8=BF=BD=E5=8A=A0=20(MisskeyIO#204)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* ネストした場合にパネルでどこまでがどのフォルダのものかをわかりやすくした
Co-authored-by: meronmks <meronmks.8914@gmail.com>
---
 .../src/components/MkEmojiPicker.section.vue  |  8 ++++----
 .../frontend/src/components/MkEmojiPicker.vue | 20 +++++++++----------
 packages/frontend/src/scripts/emojilist.ts    |  1 -
 3 files changed, 13 insertions(+), 16 deletions(-)

diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index 6f99fa9200..c745c6b3fa 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -6,7 +6,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <template>
 <!-- このコンポーネントの要素のclassは親から利用されるのでむやみに弄らないこと -->
 <!-- フォルダの中にはカスタム絵文字だけ(Unicode絵文字もこっち) -->
-<section v-if="!hasChildSection">
+<section v-if="!hasChildSection" v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
 	<header class="_acrylic" @click="shown = !shown">
 		<i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-icons"></i>:{{ emojis.length }})
 	</header>
@@ -25,21 +25,21 @@ SPDX-License-Identifier: AGPL-3.0-only
 	</div>
 </section>
 <!-- フォルダの中にはカスタム絵文字やフォルダがある -->
-<section v-else>
+<section v-else v-panel style="border-radius: 6px; border-bottom: 0.5px solid var(--divider);">
   <header class="_acrylic" @click="shown = !shown">
     <i class="toggle ti-fw" :class="shown ? 'ti ti-chevron-down' : 'ti ti-chevron-up'"></i> <slot></slot> (<i class="ti ti-folder"></i>:{{ customEmojiTree.length }} <i class="ti ti-icons"></i>:{{ emojis.length }})
   </header>
   <div v-if="shown" style="padding-left: 9px;">
     <MkEmojiPickerSection
         v-for="child in customEmojiTree"
-        :key="`custom:${child.value}`"
+        :key="`custom:${child.category}`"
         :initialShown="initialShown"
         :emojis="computed(() => customEmojis.filter(e => e.category === child.category).map(e => `:${e.name}:`))"
         :hasChildSection="child.children.length !== 0"
         :customEmojiTree="child.children"
         @chosen="nestedChosen"
     >
-      {{ child.value || i18n.ts.other }}
+      {{ child.category || i18n.ts.other }}
     </MkEmojiPickerSection>
   </div>
   <div v-if="shown" class="body">
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 9ebd352170..df74571415 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -74,14 +74,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
 			<XSection
 				v-for="child in customEmojiFolderRoot.children"
-				:key="`custom:${child.value}`"
+				:key="`custom:${child.category}`"
 				:initialShown="false"
-				:emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value).filter(filterAvailable).map(e => `:${e.name}:`))"
+				:emojis="computed(() => customEmojis.filter(e => child.category === '' ? (e.category === 'null' || !e.category) : e.category === child.category).filter(filterAvailable).map(e => `:${e.name}:`))"
         :hasChildSection="child.children.length !== 0"
         :customEmojiTree="child.children"
 				@chosen="chosen"
 			>
-				{{ child.value || i18n.ts.other }}
+				{{ child.category || i18n.ts.other }}
 			</XSection>
 		</div>
 		<div v-once class="group">
@@ -153,20 +153,19 @@ const searchResultCustom = ref<Misskey.entities.CustomEmoji[]>([]);
 const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
 const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');
 
-const customEmojiFolderRoot: CustomEmojiFolderTree = { value: "", category: "", children: [] };
+const customEmojiFolderRoot: CustomEmojiFolderTree = { category: "", children: [] };
 
 function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
-	const parts = (input && input !== 'null' ? input : '').split('/');
+	const parts = (input && input !== 'null' ? input : '').split(' / ');
 	let currentNode: CustomEmojiFolderTree = root;
 
 	for (const part of parts) {
-		const path = currentNode.value ? `${currentNode.value}/${part.trim()}` : part.trim();
+		const path = currentNode.category ? `${currentNode.category} / ${part}` : part;
 
-		let existingNode = currentNode.children.find((node) => node.value === path);
+		let existingNode = currentNode.children.find((node) => node.category === path);
 		if (!existingNode) {
 			const newNode: CustomEmojiFolderTree = {
-				value: path,
-				category: currentNode.category ? `${currentNode.category}/${part}` : part,
+				category: path,
 				children: [],
 			};
 			currentNode.children.push(newNode);
@@ -616,8 +615,7 @@ defineExpose({
 				position: sticky;
 				top: 0;
 				left: 0;
-				height: 32px;
-				line-height: 32px;
+				line-height: 28px;
 				z-index: 1;
 				padding: 0 8px;
 				font-size: 12px;
diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend/src/scripts/emojilist.ts
index 8885bf4b7f..0e5f4f0c0b 100644
--- a/packages/frontend/src/scripts/emojilist.ts
+++ b/packages/frontend/src/scripts/emojilist.ts
@@ -45,7 +45,6 @@ export function getEmojiName(char: string): string | null {
 }
 
 export interface CustomEmojiFolderTree {
-	value: string;
 	category: string;
 	children: CustomEmojiFolderTree[];
 }

From ac3c6f3df8cec47d9f823f0eebacc26f569b7fae Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Fri, 3 Nov 2023 14:10:47 +0900
Subject: [PATCH 12/36] =?UTF-8?q?feat(frontend):=20=E3=82=B9=E3=83=AF?=
 =?UTF-8?q?=E3=82=A4=E3=83=97=E3=82=84=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=A7?=
 =?UTF-8?q?=E3=82=BF=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3=E3=82=92?=
 =?UTF-8?q?=E5=86=8D=E8=AA=AD=E8=BE=BC=E3=81=99=E3=82=8B=E6=A9=9F=E8=83=BD?=
 =?UTF-8?q?=20(misskey-dev#12113)=20(MisskeyIO#206)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* feat(frontend): スワイプやボタンでタイムラインを再読込する機能 (misskey-dev#12113)
* tweak MkPullToRefresh
cheery-picked from 52dbab56a47f2242df08d95de8e0a58c5eb8ab9c
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
* enhance(frontend): improve pull to refresh
cheery-picked from d0d32e88466cef53a0d4429cce1ce4b9cb97d5b1
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 locales/en-US.yml                             |   4 +
 locales/index.d.ts                            |   4 +
 locales/ja-JP.yml                             |   4 +
 packages/frontend/src/boot/main-boot.ts       |   3 +-
 .../src/components/MkNotifications.vue        |  53 ++--
 .../frontend/src/components/MkPageWindow.vue  |   2 +
 .../frontend/src/components/MkPagination.vue  |   6 +
 .../src/components/MkPullToRefresh.vue        | 248 ++++++++++++++++++
 .../frontend/src/components/MkTimeline.vue    | 146 ++++++-----
 .../frontend/src/pages/settings/general.vue   |   3 +
 packages/frontend/src/pages/timeline.vue      |   9 +-
 packages/frontend/src/store.ts                |   4 +
 packages/frontend/src/stream.ts               |  20 +-
 .../src/ui/_common_/stream-indicator.vue      |   4 +-
 packages/frontend/src/ui/universal.vue        |   2 +-
 15 files changed, 429 insertions(+), 83 deletions(-)
 create mode 100644 packages/frontend/src/components/MkPullToRefresh.vue

diff --git a/locales/en-US.yml b/locales/en-US.yml
index d448686187..b1725a2fb8 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -1098,6 +1098,10 @@ expired: "Expired"
 doYouAgree: "Agree?"
 beSureToReadThisAsItIsImportant: "Please read this important information."
 iHaveReadXCarefullyAndAgree: "I have read the text \"{x}\" and agree."
+releaseToRefresh: "Release to reload"
+refreshing: "Reloading"
+pullDownToRefresh: "Pull down to reload"
+disableStreamingTimeline: "Disable realtime update on timeline"
 _initialAccountSetting:
   accountCreated: "Your account was successfully created!"
   letsStartAccountSetup: "For starters, let's set up your profile."
diff --git a/locales/index.d.ts b/locales/index.d.ts
index b0b8a256f2..114247b437 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1109,6 +1109,10 @@ export interface Locale {
     "pastAnnouncements": string;
     "youHaveUnreadAnnouncements": string;
     "externalServices": string;
+    "releaseToRefresh": string;
+    "refreshing": string;
+    "pullDownToRefresh": string;
+    "disableStreamingTimeline": string;
     "_announcement": {
         "forExistingUsers": string;
         "forExistingUsersDescription": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 979022e33a..907cbf0f5f 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1106,6 +1106,10 @@ currentAnnouncements: "現在のお知らせ"
 pastAnnouncements: "過去のお知らせ"
 youHaveUnreadAnnouncements: "未読のお知らせがあります。"
 externalServices: "外部サービス"
+releaseToRefresh: "離してリロード"
+refreshing: "リロード中"
+pullDownToRefresh: "引っ張ってリロード"
+disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
 
 _announcement:
   forExistingUsers: "既存ユーザーのみ"
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 2038ef3455..6f5ddcac14 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -8,7 +8,7 @@ import { common } from './common';
 import { version, ui, lang, updateLocale } from '@/config';
 import { i18n, updateI18n } from '@/i18n';
 import { confirm, alert, post, popup, toast } from '@/os';
-import { useStream } from '@/stream';
+import { useStream, isReloading } from '@/stream';
 import * as sound from '@/scripts/sound';
 import { $i, refreshAccount, login, updateAccount, signout } from '@/account';
 import { defaultStore, ColdDeviceStorage } from '@/store';
@@ -39,6 +39,7 @@ export async function mainBoot() {
 
 	let reloadDialogShowing = false;
 	stream.on('_disconnected_', async () => {
+		if (isReloading) return;
 		if (defaultStore.state.serverDisconnectedBehavior === 'reload') {
 			location.reload();
 		} else if (defaultStore.state.serverDisconnectedBehavior === 'dialog') {
diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index cc7657ba97..b7910d475a 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -4,26 +4,29 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkPagination ref="pagingComponent" :pagination="pagination">
-	<template #empty>
-		<div class="_fullinfo">
-			<img :src="infoImageUrl" class="_ghost"/>
-			<div>{{ i18n.ts.noNotifications }}</div>
-		</div>
-	</template>
+<MkPullToRefresh :refresher="() => reload()">
+	<MkPagination ref="pagingComponent" :pagination="pagination">
+		<template #empty>
+			<div class="_fullinfo">
+				<img :src="infoImageUrl" class="_ghost"/>
+				<div>{{ i18n.ts.noNotifications }}</div>
+			</div>
+		</template>
 
-	<template #default="{ items: notifications }">
-		<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
-			<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
-			<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/>
-		</MkDateSeparatedList>
-	</template>
-</MkPagination>
+		<template #default="{ items: notifications }">
+			<MkDateSeparatedList v-slot="{ item: notification }" :class="$style.list" :items="notifications" :noGap="true">
+				<MkNote v-if="['reply', 'quote', 'mention'].includes(notification.type)" :key="notification.id" :note="notification.note"/>
+				<XNotification v-else :key="notification.id" :notification="notification" :withTime="true" :full="true" class="_panel notification"/>
+			</MkDateSeparatedList>
+		</template>
+	</MkPagination>
+</MkPullToRefresh>
 </template>
 
 <script lang="ts" setup>
-import { onUnmounted, onMounted, computed, shallowRef } from 'vue';
+import { onUnmounted, onActivated, onMounted, computed, shallowRef } from 'vue';
 import MkPagination, { Paging } from '@/components/MkPagination.vue';
+import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import XNotification from '@/components/MkNotification.vue';
 import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue';
 import MkNote from '@/components/MkNote.vue';
@@ -48,16 +51,24 @@ const pagination: Paging = {
 	})),
 };
 
-const onNotification = (notification) => {
+function onNotification(notification) {
 	const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type);
 	if (isMuted || document.visibilityState === 'visible') {
 		useStream().send('readNotification');
 	}
 
 	if (!isMuted) {
-		pagingComponent.value.prepend(notification);
+		pagingComponent.value?.prepend(notification);
 	}
-};
+}
+
+function reload() {
+	return new Promise<void>((res) => {
+		pagingComponent.value?.reload().then(() => {
+			res();
+		});
+	});
+}
 
 let connection;
 
@@ -66,6 +77,12 @@ onMounted(() => {
 	connection.on('notification', onNotification);
 });
 
+onActivated(() => {
+	pagingComponent.value?.reload();
+	connection = useStream().useChannel('main');
+	connection.on('notification', onNotification);
+});
+
 onUnmounted(() => {
 	if (connection) connection.dispose();
 });
diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index c090b4b691..64d6c4d2cc 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -166,6 +166,8 @@ defineExpose({
 
 <style lang="scss" module>
 .root {
+	overscroll-behavior: none;
+
 	min-height: 100%;
 	background: var(--bg);
 
diff --git a/packages/frontend/src/components/MkPagination.vue b/packages/frontend/src/components/MkPagination.vue
index bd471a1b1f..819bb0e746 100644
--- a/packages/frontend/src/components/MkPagination.vue
+++ b/packages/frontend/src/components/MkPagination.vue
@@ -90,6 +90,7 @@ const props = withDefaults(defineProps<{
 
 const emit = defineEmits<{
 	(ev: 'queue', count: number): void;
+	(ev: 'status', error: boolean): void;
 }>();
 
 let rootEl = $shallowRef<HTMLElement>();
@@ -164,6 +165,11 @@ watch(queue, (a, b) => {
 	emit('queue', queue.value.length);
 }, { deep: true });
 
+watch(error, (n, o) => {
+	if (n === o) return;
+	emit('status', n);
+});
+
 async function init(): Promise<void> {
 	queue.value = [];
 	fetching.value = true;
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
new file mode 100644
index 0000000000..2092faa209
--- /dev/null
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -0,0 +1,248 @@
+<!--
+SPDX-FileCopyrightText: syuilo and other misskey contributors
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+	<div ref="rootEl">
+		<div v-if="isPullStart" :class="$style.frame" :style="`--frame-min-height: ${pullDistance / (PULL_BRAKE_BASE + (pullDistance / PULL_BRAKE_FACTOR))}px;`">
+			<div :class="$style.frameContent">
+				<MkLoading v-if="isRefreshing" :class="$style.loader" :em="true"/>
+				<i v-else class="ti ti-arrow-bar-to-down" :class="[$style.icon, { [$style.refresh]: isPullEnd }]"></i>
+				<div :class="$style.text">
+					<template v-if="isPullEnd">{{ i18n.ts.releaseToRefresh }}</template>
+					<template v-else-if="isRefreshing">{{ i18n.ts.refreshing }}</template>
+					<template v-else>{{ i18n.ts.pullDownToRefresh }}</template>
+				</div>
+			</div>
+		</div>
+		<div :class="{ [$style.slotClip]: isPullStart }">
+			<slot/>
+		</div>
+	</div>
+</template>
+
+<script lang="ts" setup>
+import MkLoading from '@/components/global/MkLoading.vue';
+import { onMounted, onUnmounted } from 'vue';
+import { i18n } from '@/i18n.js';
+
+const SCROLL_STOP = 10;
+const MAX_PULL_DISTANCE = Infinity;
+const FIRE_THRESHOLD = 230;
+const RELEASE_TRANSITION_DURATION = 200;
+const PULL_BRAKE_BASE = 2;
+const PULL_BRAKE_FACTOR = 200;
+
+let isPullStart = $ref(false);
+let isPullEnd = $ref(false);
+let isRefreshing = $ref(false);
+let pullDistance = $ref(0);
+
+let supportPointerDesktop = false;
+let startScreenY: number | null = null;
+
+const rootEl = $shallowRef<HTMLDivElement>();
+let scrollEl: HTMLElement | null = null;
+
+let disabled = false;
+
+const props = withDefaults(defineProps<{
+	refresher: () => Promise<void>;
+}>(), {
+	refresher: () => Promise.resolve(),
+});
+
+const emits = defineEmits<(ev: "refresh") => void>();
+
+function getScrollableParentElement(node) {
+	if (node == null) {
+		return null;
+	}
+
+	if (node.scrollHeight > node.clientHeight) {
+		return node;
+	} else {
+		return getScrollableParentElement(node.parentNode);
+	}
+}
+
+function getScreenY(event) {
+	if (supportPointerDesktop) {
+		return event.screenY;
+	}
+	return event.touches[0].screenY;
+}
+
+function moveStart(event) {
+	if (!isPullStart && !isRefreshing && !disabled) {
+		isPullStart = true;
+		startScreenY = getScreenY(event);
+		pullDistance = 0;
+	}
+}
+
+function moveBySystem(to: number): Promise<void> {
+	return new Promise(r => {
+		const startHeight = pullDistance;
+		const overHeight = pullDistance - to;
+		if (overHeight < 1) {
+			r();
+			return;
+		}
+		const startTime = Date.now();
+		let intervalId = setInterval(() => {
+			const time = Date.now() - startTime;
+			if (time > RELEASE_TRANSITION_DURATION) {
+				pullDistance = to;
+				clearInterval(intervalId);
+				r();
+				return;
+			}
+			const nextHeight = startHeight - (overHeight / RELEASE_TRANSITION_DURATION) * time;
+			if (pullDistance < nextHeight) return;
+			pullDistance = nextHeight;
+		}, 1);
+	});
+}
+
+async function fixOverContent() {
+	if (pullDistance > FIRE_THRESHOLD) {
+		await moveBySystem(FIRE_THRESHOLD);
+	}
+}
+
+async function closeContent() {
+	if (pullDistance > 0) {
+		await moveBySystem(0);
+	}
+}
+
+function moveEnd() {
+	if (isPullStart && !isRefreshing) {
+		startScreenY = null;
+		if (isPullEnd) {
+			isPullEnd = false;
+			isRefreshing = true;
+			fixOverContent().then(() => {
+				emits('refresh');
+				props.refresher().then(() => {
+					refreshFinished();
+				});
+			});
+		} else {
+			closeContent().then(() => isPullStart = false);
+		}
+	}
+}
+
+function moving(event) {
+	if (!isPullStart || isRefreshing || disabled) return;
+
+	if (!scrollEl) {
+		scrollEl = getScrollableParentElement(rootEl);
+	}
+	if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
+		pullDistance = 0;
+		isPullEnd = false;
+		moveEnd();
+		return;
+	}
+
+	if (startScreenY === null) {
+		startScreenY = getScreenY(event);
+	}
+	const moveScreenY = getScreenY(event);
+
+	const moveHeight = moveScreenY - startScreenY!;
+	pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
+
+	isPullEnd = pullDistance >= FIRE_THRESHOLD;
+}
+
+/**
+ * emit(refresh)が完了したことを知らせる関数
+ *
+ * タイムアウトがないのでこれを最終的に実行しないと出たままになる
+ */
+function refreshFinished() {
+	closeContent().then(() => {
+		isPullStart = false;
+		isRefreshing = false;
+	});
+}
+
+function setDisabled(value) {
+	disabled = value;
+}
+
+onMounted(() => {
+	// マウス操作でpull to refreshするのは不便そう
+	//supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop';
+
+	if (supportPointerDesktop) {
+		rootEl.addEventListener('pointerdown', moveStart);
+		// ポインターの場合、ポップアップ系の動作をするとdownだけ発火されてupが発火されないため
+		window.addEventListener('pointerup', moveEnd);
+		rootEl.addEventListener('pointermove', moving, { passive: true });
+	} else {
+		rootEl.addEventListener('touchstart', moveStart);
+		rootEl.addEventListener('touchend', moveEnd);
+		rootEl.addEventListener('touchmove', moving, { passive: true });
+	}
+});
+
+onUnmounted(() => {
+	if (supportPointerDesktop) window.removeEventListener('pointerup', moveEnd);
+});
+
+defineExpose({
+	setDisabled,
+});
+</script>
+
+<style lang="scss" module>
+.frame {
+	position: relative;
+	overflow: clip;
+
+	width: 100%;
+	min-height: var(--frame-min-height, 0px);
+
+	mask-image: linear-gradient(90deg, #000 0%, #000 80%, transparent);
+	-webkit-mask-image: -webkit-linear-gradient(90deg, #000 0%, #000 80%, transparent);
+
+	pointer-events: none;
+}
+
+.frameContent {
+	position: absolute;
+	bottom: 0;
+	width: 100%;
+	margin: 5px 0;
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+	font-size: 14px;
+
+	> .icon, > .loader {
+		margin: 6px 0;
+	}
+
+	> .icon {
+		transition: transform .25s;
+
+		&.refresh {
+			transform: rotate(180deg);
+		}
+	}
+
+	> .text {
+		margin: 5px 0;
+	}
+}
+
+.slotClip {
+	overflow-y: clip;
+}
+</style>
diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue
index 714bc17e7b..0320d8df96 100644
--- a/packages/frontend/src/components/MkTimeline.vue
+++ b/packages/frontend/src/components/MkTimeline.vue
@@ -4,13 +4,16 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)"/>
+<MkPullToRefresh ref="prComponent" :refresher="() => reloadTimeline()">
+	<MkNotes ref="tlComponent" :noGap="!defaultStore.state.showGapBetweenNotesInTimeline" :pagination="pagination" @queue="emit('queue', $event)" @status="prComponent.setDisabled($event)"/>
+</MkPullToRefresh>
 </template>
 
 <script lang="ts" setup>
 import { computed, provide, onUnmounted } from 'vue';
 import MkNotes from '@/components/MkNotes.vue';
-import { useStream } from '@/stream';
+import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
+import { useStream, reloadStream } from '@/stream';
 import * as sound from '@/scripts/sound';
 import { $i } from '@/account';
 import { defaultStore } from '@/store';
@@ -31,6 +34,7 @@ const emit = defineEmits<{
 
 provide('inChannel', computed(() => props.src === 'channel'));
 
+const prComponent: InstanceType<typeof MkPullToRefresh> = $ref();
 const tlComponent: InstanceType<typeof MkNotes> = $ref();
 
 const prepend = note => {
@@ -49,108 +53,131 @@ let connection;
 let connection2;
 
 const stream = useStream();
+const connectChannel = () => {
+	if (props.src === 'antenna') {
+		connection = stream.useChannel('antenna', {
+			antennaId: props.antenna,
+		});
+		connection.on('note', prepend);
+	} else if (props.src === 'home') {
+		connection = stream.useChannel('homeTimeline', {
+			withReplies: defaultStore.state.showTimelineReplies,
+		});
+		connection.on('note', prepend);
+
+		connection2 = stream.useChannel('main');
+	} else if (props.src === 'local') {
+		connection = stream.useChannel('localTimeline', {
+			withReplies: defaultStore.state.showTimelineReplies,
+		});
+		connection.on('note', prepend);
+	} else if (props.src === 'media') {
+		connection = stream.useChannel('hybridTimeline', {
+			withFiles: true,
+			withReplies: defaultStore.state.showTimelineReplies,
+		});
+		connection.on('note', prepend);
+	} else if (props.src === 'social') {
+		connection = stream.useChannel('hybridTimeline', {
+			withReplies: defaultStore.state.showTimelineReplies,
+		});
+		connection.on('note', prepend);
+	} else if (props.src === 'global') {
+		connection = stream.useChannel('globalTimeline', {
+			withReplies: defaultStore.state.showTimelineReplies,
+		});
+		connection.on('note', prepend);
+	} else if (props.src === 'mentions') {
+		connection = stream.useChannel('main');
+		connection.on('mention', prepend);
+	} else if (props.src === 'directs') {
+		const onNote = note => {
+			if (note.visibility === 'specified') {
+				prepend(note);
+			}
+		};
+		connection = stream.useChannel('main');
+		connection.on('mention', onNote);
+	} else if (props.src === 'list') {
+		connection = stream.useChannel('userList', {
+			listId: props.list,
+		});
+		connection.on('note', prepend);
+	} else if (props.src === 'channel') {
+		connection = stream.useChannel('channel', {
+			channelId: props.channel,
+		});
+		connection.on('note', prepend);
+	} else if (props.src === 'role') {
+		connection = stream.useChannel('roleTimeline', {
+			roleId: props.role,
+		});
+		connection.on('note', prepend);
+	}
+};
 
 if (props.src === 'antenna') {
 	endpoint = 'antennas/notes';
 	query = {
 		antennaId: props.antenna,
 	};
-	connection = stream.useChannel('antenna', {
-		antennaId: props.antenna,
-	});
-	connection.on('note', prepend);
 } else if (props.src === 'home') {
 	endpoint = 'notes/timeline';
 	query = {
 		withReplies: defaultStore.state.showTimelineReplies,
 	};
-	connection = stream.useChannel('homeTimeline', {
-		withReplies: defaultStore.state.showTimelineReplies,
-	});
-	connection.on('note', prepend);
-
-	connection2 = stream.useChannel('main');
 } else if (props.src === 'local') {
 	endpoint = 'notes/local-timeline';
 	query = {
 		withReplies: defaultStore.state.showTimelineReplies,
 	};
-	connection = stream.useChannel('localTimeline', {
-		withReplies: defaultStore.state.showTimelineReplies,
-	});
-	connection.on('note', prepend);
 } else if (props.src === 'media') {
 	endpoint = 'notes/hybrid-timeline';
 	query = {
 		withFiles: true,
 		withReplies: defaultStore.state.showTimelineReplies,
 	};
-	connection = stream.useChannel('hybridTimeline', {
-		withFiles: true,
-		withReplies: defaultStore.state.showTimelineReplies,
-	});
-	connection.on('note', prepend);
 } else if (props.src === 'social') {
 	endpoint = 'notes/hybrid-timeline';
 	query = {
 		withReplies: defaultStore.state.showTimelineReplies,
 	};
-	connection = stream.useChannel('hybridTimeline', {
-		withReplies: defaultStore.state.showTimelineReplies,
-	});
-	connection.on('note', prepend);
 } else if (props.src === 'global') {
 	endpoint = 'notes/global-timeline';
 	query = {
 		withReplies: defaultStore.state.showTimelineReplies,
 	};
-	connection = stream.useChannel('globalTimeline', {
-		withReplies: defaultStore.state.showTimelineReplies,
-	});
-	connection.on('note', prepend);
 } else if (props.src === 'mentions') {
 	endpoint = 'notes/mentions';
-	connection = stream.useChannel('main');
-	connection.on('mention', prepend);
 } else if (props.src === 'directs') {
 	endpoint = 'notes/mentions';
 	query = {
 		visibility: 'specified',
 	};
-	const onNote = note => {
-		if (note.visibility === 'specified') {
-			prepend(note);
-		}
-	};
-	connection = stream.useChannel('main');
-	connection.on('mention', onNote);
 } else if (props.src === 'list') {
 	endpoint = 'notes/user-list-timeline';
 	query = {
 		listId: props.list,
 	};
-	connection = stream.useChannel('userList', {
-		listId: props.list,
-	});
-	connection.on('note', prepend);
 } else if (props.src === 'channel') {
 	endpoint = 'channels/timeline';
 	query = {
 		channelId: props.channel,
 	};
-	connection = stream.useChannel('channel', {
-		channelId: props.channel,
-	});
-	connection.on('note', prepend);
 } else if (props.src === 'role') {
 	endpoint = 'roles/notes';
 	query = {
 		roleId: props.role,
 	};
-	connection = stream.useChannel('roleTimeline', {
-		roleId: props.role,
+}
+
+if (!defaultStore.state.disableStreamingTimeline) {
+	connectChannel();
+
+	onUnmounted(() => {
+		connection.dispose();
+		if (connection2) connection2.dispose();
 	});
-	connection.on('note', prepend);
 }
 
 const pagination = {
@@ -159,15 +186,16 @@ const pagination = {
 	params: query,
 };
 
-onUnmounted(() => {
-	connection.dispose();
-	if (connection2) connection2.dispose();
-});
+function reloadTimeline() {
+	return new Promise<void>((res) => {
+		tlComponent.pagingComponent?.reload().then(() => {
+			reloadStream();
+			res();
+		});
+	});
+}
 
-/* TODO
-const timetravel = (date?: Date) => {
-	this.date = date;
-	this.$refs.tl.reload();
-};
-*/
+defineExpose({
+	reloadTimeline,
+});
 </script>
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 3b39a5c00a..1a62bfb343 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -135,6 +135,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			<div class="_gaps_s">
 				<MkSwitch v-model="imageNewTab">{{ i18n.ts.openImageInNewTab }}</MkSwitch>
 				<MkSwitch v-model="enableInfiniteScroll">{{ i18n.ts.enableInfiniteScroll }}</MkSwitch>
+				<MkSwitch v-model="disableStreamingTimeline">{{ i18n.ts.disableStreamingTimeline }}</MkSwitch>
 			</div>
 			<MkSelect v-model="serverDisconnectedBehavior">
 				<template #label>{{ i18n.ts.whenServerDisconnected }}</template>
@@ -231,6 +232,7 @@ const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('
 const notificationPosition = computed(defaultStore.makeGetterSetter('notificationPosition'));
 const notificationStackAxis = computed(defaultStore.makeGetterSetter('notificationStackAxis'));
 const showTimelineReplies = computed(defaultStore.makeGetterSetter('showTimelineReplies'));
+const disableStreamingTimeline = computed(defaultStore.makeGetterSetter('disableStreamingTimeline'));
 
 watch(lang, () => {
 	miLocalStorage.setItem('lang', lang.value as string);
@@ -264,6 +266,7 @@ watch([
 	instanceTicker,
 	overridedDeviceKind,
 	mediaListWithOneImageAppearance,
+	disableStreamingTimeline,
 ], async () => {
 	await reloadAsk();
 });
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index d7bccc8000..f16dcd66ed 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -38,6 +38,7 @@ import { i18n } from '@/i18n';
 import { instance } from '@/instance';
 import { $i } from '@/account';
 import { definePageMetadata } from '@/scripts/page-metadata';
+import { deviceKind } from '@/scripts/device-kind.js';
 
 provide('shouldOmitHeaderTitle', true);
 
@@ -121,7 +122,13 @@ function focus(): void {
 	tlComponent.focus();
 }
 
-const headerActions = $computed(() => []);
+const headerActions = $computed(() => [
+	...[deviceKind === 'desktop' ? {
+		icon: 'ti ti-refresh',
+		text: i18n.ts.reload,
+		handler: () => { tlComponent.reloadTimeline(); },
+	} : {}],
+]);
 
 const headerTabs = $computed(() => [{
 	key: 'home',
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 9aba8017ff..7a708917e0 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -351,6 +351,10 @@ export const defaultStore = markRaw(new Storage('base', {
 		where: 'device',
 		default: {} as Record<string, Record<string, string[]>>,
 	},
+	disableStreamingTimeline: {
+		where: 'device',
+		default: false,
+	},
 }));
 
 // TODO: 他のタブと永続化されたstateを同期
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index b241316648..ce07d39556 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -9,6 +9,9 @@ import { $i } from '@/account';
 import { url } from '@/config';
 
 let stream: Misskey.Stream | null = null;
+let timeoutHeartBeat: number | null = null;
+
+export let isReloading: boolean = false;
 
 export function useStream(): Misskey.Stream {
 	if (stream) return stream;
@@ -17,7 +20,20 @@ export function useStream(): Misskey.Stream {
 		token: $i.token,
 	} : null));
 
-	window.setTimeout(heartbeat, 1000 * 60);
+	timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
+
+	return stream;
+}
+
+export function reloadStream() {
+	if (!stream) return useStream();
+	if (timeoutHeartBeat) window.clearTimeout(timeoutHeartBeat);
+	isReloading = true;
+
+	stream.close();
+	stream.once('_connected_', () => isReloading = false);
+	stream.stream.reconnect();
+	timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
 
 	return stream;
 }
@@ -26,5 +42,5 @@ function heartbeat(): void {
 	if (stream != null && document.visibilityState === 'visible') {
 		stream.heartbeat();
 	}
-	window.setTimeout(heartbeat, 1000 * 60);
+	timeoutHeartBeat = window.setTimeout(heartbeat, 1000 * 60);
 }
diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue
index d252496829..a5af019582 100644
--- a/packages/frontend/src/ui/_common_/stream-indicator.vue
+++ b/packages/frontend/src/ui/_common_/stream-indicator.vue
@@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 
 <script lang="ts" setup>
 import { onUnmounted } from 'vue';
-import { useStream } from '@/stream';
+import { useStream, isReloading } from '@/stream';
 import { i18n } from '@/i18n';
 import MkButton from '@/components/MkButton.vue';
 import * as os from '@/os';
@@ -27,6 +27,8 @@ let hasDisconnected = $ref(false);
 let timeoutId = $ref<number>();
 
 function onDisconnected() {
+	if (isReloading) return;
+
 	window.clearTimeout(timeoutId);
 	timeoutId = window.setTimeout(() => {
 		hasDisconnected = true;
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index d9cb81b5ef..288587a4e0 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -319,7 +319,7 @@ $widgets-hide-threshold: 1090px;
 	min-width: 0;
 	overflow: auto;
 	overflow-y: scroll;
-	overscroll-behavior: contain;
+	overscroll-behavior: none;
 	background: var(--bg);
 }
 

From 6a06711ffd0a893f4048e6cb898053b672663602 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 6 Nov 2023 04:57:45 +0900
Subject: [PATCH 13/36] =?UTF-8?q?fix(backend):=20=E3=82=B8=E3=83=A7?=
 =?UTF-8?q?=E3=83=96=E3=82=AD=E3=83=A5=E3=83=BC=E7=AE=A1=E7=90=86=E7=94=BB?=
 =?UTF-8?q?=E9=9D=A2=E3=81=AE=E8=AA=8D=E8=A8=BC=E3=82=92=E5=9B=9E=E9=81=BF?=
 =?UTF-8?q?=E3=81=A7=E3=81=8D=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE?=
 =?UTF-8?q?=E6=AD=A3=20(MisskeyIO#207)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

cheery-picked from c9aeccb2ab260ceedc126e6e366da8cd13ece4b2
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 packages/backend/src/server/web/ClientServerService.ts | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/packages/backend/src/server/web/ClientServerService.ts b/packages/backend/src/server/web/ClientServerService.ts
index e3f35df194..482ff22d29 100644
--- a/packages/backend/src/server/web/ClientServerService.ts
+++ b/packages/backend/src/server/web/ClientServerService.ts
@@ -144,7 +144,9 @@ export class ClientServerService {
 
 		// Authenticate
 		fastify.addHook('onRequest', async (request, reply) => {
-			if (request.url === bullBoardPath || request.url.startsWith(bullBoardPath + '/')) {
+			// %71ueueとかでリクエストされたら困るため
+			const url = decodeURI(request.url);
+			if (url === bullBoardPath || url.startsWith(bullBoardPath + '/')) {
 				const token = request.cookies.token;
 				if (token == null) {
 					reply.code(401);

From ae9245abdcc049ca6e48b9016eed6fcf204eeef7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 6 Nov 2023 04:59:48 +0900
Subject: [PATCH 14/36] enhance(frontend): X (formally known as Twitter)
 (MisskeyIO#208)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* enhance(frontend): TwitterアイコンをXに変更 (misskey-dev#11436)
cheery-picked from 007ed5c9298ba6d7374dc913f65a5600ac88f331
Co-authored-by: Ebise Lutica <7106976+EbiseLutica@users.noreply.github.com>

* fix(frontend/MkUrlPreview): allow fullscreen from tweets (misskey-dev#11712)
cheery-picked from 2896fc6cb4f6f35df4b0a0c22c9be55ad3c8e19a
Co-authored-by: Kagami Sascha Rosylight <saschanaz@outlook.com>

* enhance(frontend): x.comでも展開ができるように (misskey-dev#11757)
cheery-picked from cb80dff7df07e9ea9292365fe82fc7af09cc63c1
Co-authored-by: maguroshouta <54607611+maguroshouta@users.noreply.github.com>
---
 packages/frontend/package.json                |  2 +-
 .../frontend/src/components/MkUrlPreview.vue  | 13 +++++++++---
 packages/frontend/test/url-preview.test.ts    | 20 ++++++++++++++++++-
 pnpm-lock.yaml                                | 14 ++++++-------
 4 files changed, 37 insertions(+), 12 deletions(-)

diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 84b5da261f..f7af1b1d5b 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -23,7 +23,7 @@
 		"@rollup/plugin-typescript": "^11.1.2",
 		"@rollup/pluginutils": "5.0.2",
 		"@syuilo/aiscript": "0.15.0",
-		"@tabler/icons-webfont": "2.25.0",
+		"@tabler/icons-webfont": "2.30.0",
 		"@vitejs/plugin-vue": "4.2.3",
 		"@vue-macros/reactivity-transform": "0.3.15",
 		"@vue/compiler-sfc": "3.3.4",
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 852999773a..19bbb3882f 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -28,7 +28,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 <template v-else-if="tweetId && tweetExpanded">
 	<div ref="twitter">
-		<iframe ref="tweet" scrolling="no" frameborder="no" :style="{ position: 'relative', width: '100%', height: `${tweetHeight}px` }" :src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"></iframe>
+		<iframe
+			ref="tweet"
+			allow="fullscreen;web-share"
+			sandbox="allow-popups allow-scripts allow-same-origin"
+			scrolling="no"
+			:style="{ position: 'relative', width: '100%', height: `${tweetHeight}px`, border: 0 }"
+			:src="`https://platform.twitter.com/embed/index.html?embedId=${embedId}&amp;hideCard=false&amp;hideThread=false&amp;lang=en&amp;theme=${defaultStore.state.darkMode ? 'dark' : 'light'}&amp;id=${tweetId}`"
+		></iframe>
 	</div>
 	<div :class="$style.action">
 		<MkButton :small="true" inline @click="tweetExpanded = false">
@@ -60,7 +67,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 	<template v-if="showActions">
 		<div v-if="tweetId" :class="$style.action">
 			<MkButton :small="true" inline @click="tweetExpanded = true">
-				<i class="ti ti-brand-twitter"></i> {{ i18n.ts.expandTweet }}
+				<i class="ti ti-brand-x"></i> {{ i18n.ts.expandTweet }}
 			</MkButton>
 		</div>
 		<div v-if="!playerEnabled && player.url" :class="$style.action">
@@ -126,7 +133,7 @@ let unknownUrl = $ref(false);
 const requestUrl = new URL(props.url);
 if (!['http:', 'https:'].includes(requestUrl.protocol)) throw new Error('invalid url');
 
-if (requestUrl.hostname === 'twitter.com' || requestUrl.hostname === 'mobile.twitter.com') {
+if (requestUrl.hostname === 'twitter.com' || requestUrl.hostname === 'mobile.twitter.com' || requestUrl.hostname === 'x.com' || requestUrl.hostname === 'mobile.x.com') {
 	const m = requestUrl.pathname.match(/^\/.+\/status(?:es)?\/(\d+)/);
 	if (m) tweetId = m[1];
 }
diff --git a/packages/frontend/test/url-preview.test.ts b/packages/frontend/test/url-preview.test.ts
index 1d43a628f2..0cf3a417e2 100644
--- a/packages/frontend/test/url-preview.test.ts
+++ b/packages/frontend/test/url-preview.test.ts
@@ -13,7 +13,7 @@ import MkUrlPreview from '@/components/MkUrlPreview.vue';
 
 type SummalyResult = Awaited<ReturnType<typeof summaly>>;
 
-describe('MkMediaImage', () => {
+describe('MkUrlPreview', () => {
 	const renderPreviewBy = async (summary: Partial<SummalyResult>): Promise<RenderResult> => {
 		if (!summary.player) {
 			summary.player = {
@@ -143,4 +143,22 @@ describe('MkMediaImage', () => {
 		assert.exists(iframe, 'iframe should exist');
 		assert.strictEqual(iframe?.parentElement?.style.paddingTop, '200px');
 	});
+
+	test('Loading a tweet in iframe', async () => {
+		const iframe = await renderAndOpenPreview({
+			url: 'https://twitter.com/i/web/status/1685072521782325249',
+		});
+		assert.exists(iframe, 'iframe should exist');
+		assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share');
+		assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-scripts allow-same-origin');
+	});
+
+	test('Loading a post in iframe', async () => {
+		const iframe = await renderAndOpenPreview({
+			url: 'https://x.com/i/web/status/1685072521782325249',
+		});
+		assert.exists(iframe, 'iframe should exist');
+		assert.strictEqual(iframe?.getAttribute('allow'), 'fullscreen;web-share');
+		assert.strictEqual(iframe?.getAttribute('sandbox'), 'allow-popups allow-scripts allow-same-origin');
+	});
 });
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 75a7e4242e..fedebc0356 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -638,8 +638,8 @@ importers:
         specifier: 0.15.0
         version: 0.15.0
       '@tabler/icons-webfont':
-        specifier: 2.25.0
-        version: 2.25.0
+        specifier: 2.30.0
+        version: 2.30.0
       '@vitejs/plugin-vue':
         specifier: 4.2.3
         version: 4.2.3(vite@4.4.4)(vue@3.3.4)
@@ -8394,14 +8394,14 @@ packages:
     dependencies:
       defer-to-connect: 2.0.1
 
-  /@tabler/icons-webfont@2.25.0:
-    resolution: {integrity: sha512-IWYVnYlCwlGC95kvpY5Hdiyn1/amXOUwsfRthtmgEtHCQly4JSLRuaD90xD0O+pQ+wZBXIVNsO3pKdg74zEaBg==}
+  /@tabler/icons-webfont@2.30.0:
+    resolution: {integrity: sha512-tGGKxeATvyHJBHl5FzY4oAShbAiR4ovstG62lqb2HGlOJwz4Io9TSk4eoB88nqxg3sT5no2YsAKXcr1UnlpnNQ==}
     dependencies:
-      '@tabler/icons': 2.25.0
+      '@tabler/icons': 2.30.0
     dev: false
 
-  /@tabler/icons@2.25.0:
-    resolution: {integrity: sha512-Z+FtSZoG/CM1TMCgg7elUew2m0+qMdh5gutMhvxiIY77KIIsE6L/6fUBy+rPXWE9v7MV296fsnCvbpfgwpXupQ==}
+  /@tabler/icons@2.30.0:
+    resolution: {integrity: sha512-tvtmkI4ALjKThVVORh++sB9JnkFY7eGInKxNy+Df7WVQiF7T85tlvGADzlgX4Ic+CK5MIUzZ0jhOlQ/RRlgXpg==}
     dev: false
 
   /@tensorflow/tfjs-backend-cpu@4.4.0(@tensorflow/tfjs-core@4.4.0):

From 78d3041f34ac701209bc93f6d5a0f1fe2a722279 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 6 Nov 2023 05:01:30 +0900
Subject: [PATCH 15/36] Bump up version to 13.14.2-io.14 (MisskeyIO#205)

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 4a5d142200..71b9166736 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.14.2-io.13a",
+	"version": "13.14.2-io.14",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",

From a946aa3df96a7ce3cb905e2666708b791af47cfd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 6 Nov 2023 22:25:30 +0900
Subject: [PATCH 16/36] enhance(frontend): improve pull to refresh
 (MisskeyIO#209)

* enhance(frontend): improve pull to refresh
cheery-picked from a656447aa58440e9a41fa39e1c5d6ff3d859b64e
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>

* tweak MkPullToRefresh
cheery-picked from 39f731804812d2e325c8b5c7c9077ff9c4835bc4
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
---
 .../frontend/src/components/MkPageWindow.vue  |  2 +-
 .../src/components/MkPullToRefresh.vue        | 74 +++++++++++--------
 packages/frontend/src/ui/universal.vue        |  2 +-
 3 files changed, 46 insertions(+), 32 deletions(-)

diff --git a/packages/frontend/src/components/MkPageWindow.vue b/packages/frontend/src/components/MkPageWindow.vue
index 64d6c4d2cc..846f5ef9b4 100644
--- a/packages/frontend/src/components/MkPageWindow.vue
+++ b/packages/frontend/src/components/MkPageWindow.vue
@@ -166,7 +166,7 @@ defineExpose({
 
 <style lang="scss" module>
 .root {
-	overscroll-behavior: none;
+	overscroll-behavior: contain;
 
 	min-height: 100%;
 	background: var(--bg);
diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index 2092faa209..4477864e4c 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -26,13 +26,14 @@ SPDX-License-Identifier: AGPL-3.0-only
 import MkLoading from '@/components/global/MkLoading.vue';
 import { onMounted, onUnmounted } from 'vue';
 import { i18n } from '@/i18n.js';
+import { getScrollContainer } from '@/scripts/scroll.js';
 
 const SCROLL_STOP = 10;
 const MAX_PULL_DISTANCE = Infinity;
 const FIRE_THRESHOLD = 230;
 const RELEASE_TRANSITION_DURATION = 200;
-const PULL_BRAKE_BASE = 2;
-const PULL_BRAKE_FACTOR = 200;
+const PULL_BRAKE_BASE = 1.5;
+const PULL_BRAKE_FACTOR = 170;
 
 let isPullStart = $ref(false);
 let isPullEnd = $ref(false);
@@ -55,18 +56,6 @@ const props = withDefaults(defineProps<{
 
 const emits = defineEmits<(ev: "refresh") => void>();
 
-function getScrollableParentElement(node) {
-	if (node == null) {
-		return null;
-	}
-
-	if (node.scrollHeight > node.clientHeight) {
-		return node;
-	} else {
-		return getScrollableParentElement(node.parentNode);
-	}
-}
-
 function getScreenY(event) {
 	if (supportPointerDesktop) {
 		return event.screenY;
@@ -136,12 +125,9 @@ function moveEnd() {
 	}
 }
 
-function moving(event) {
+function moving(event: TouchEvent | PointerEvent) {
 	if (!isPullStart || isRefreshing || disabled) return;
 
-	if (!scrollEl) {
-		scrollEl = getScrollableParentElement(rootEl);
-	}
 	if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
 		pullDistance = 0;
 		isPullEnd = false;
@@ -157,6 +143,10 @@ function moving(event) {
 	const moveHeight = moveScreenY - startScreenY!;
 	pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
 
+	if (pullDistance > 0) {
+		if (event.cancelable) event.preventDefault();
+	}
+
 	isPullEnd = pullDistance >= FIRE_THRESHOLD;
 }
 
@@ -176,24 +166,48 @@ function setDisabled(value) {
 	disabled = value;
 }
 
-onMounted(() => {
-	// マウス操作でpull to refreshするのは不便そう
-	//supportPointerDesktop = !!window.PointerEvent && deviceKind === 'desktop';
+function onScrollContainerScroll() {
+	const scrollPos = scrollEl!.scrollTop;
 
-	if (supportPointerDesktop) {
-		rootEl.addEventListener('pointerdown', moveStart);
-		// ポインターの場合、ポップアップ系の動作をするとdownだけ発火されてupが発火されないため
-		window.addEventListener('pointerup', moveEnd);
-		rootEl.addEventListener('pointermove', moving, { passive: true });
+	// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
+	if (scrollPos === 0) {
+		scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom';
+		registerEventListenersForReadyToPull();
 	} else {
-		rootEl.addEventListener('touchstart', moveStart);
-		rootEl.addEventListener('touchend', moveEnd);
-		rootEl.addEventListener('touchmove', moving, { passive: true });
+		scrollEl!.style.touchAction = 'auto';
+		unregisterEventListenersForReadyToPull();
 	}
+}
+
+function registerEventListenersForReadyToPull() {
+	if (rootEl == null) return;
+	rootEl.addEventListener('touchstart', moveStart, { passive: true });
+	rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
+}
+
+function unregisterEventListenersForReadyToPull() {
+	if (rootEl == null) return;
+	rootEl.removeEventListener('touchstart', moveStart);
+	rootEl.removeEventListener('touchmove', moving);
+}
+
+onMounted(() => {
+	if (rootEl == null) return;
+
+	scrollEl = getScrollContainer(rootEl);
+	if (scrollEl == null) return;
+
+	scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
+
+	rootEl.addEventListener('touchend', moveEnd, { passive: true });
+
+	registerEventListenersForReadyToPull();
 });
 
 onUnmounted(() => {
-	if (supportPointerDesktop) window.removeEventListener('pointerup', moveEnd);
+	if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll);
+
+	unregisterEventListenersForReadyToPull();
 });
 
 defineExpose({
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 288587a4e0..d9cb81b5ef 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -319,7 +319,7 @@ $widgets-hide-threshold: 1090px;
 	min-width: 0;
 	overflow: auto;
 	overflow-y: scroll;
-	overscroll-behavior: none;
+	overscroll-behavior: contain;
 	background: var(--bg);
 }
 

From 15249e6242f7096ff752a82dc0e5a6b519866745 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 6 Nov 2023 22:26:37 +0900
Subject: [PATCH 17/36]  Bump up version to 13.14.2-io.14a (MisskeyIO#210)

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 71b9166736..c7c87f0ae2 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.14.2-io.14",
+	"version": "13.14.2-io.14a",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",

From 48042557c2fadbb5f400178f42913d20df9745ef Mon Sep 17 00:00:00 2001
From: riku6460 <17585784+riku6460@users.noreply.github.com>
Date: Mon, 6 Nov 2023 23:39:33 +0900
Subject: [PATCH 18/36] =?UTF-8?q?Docker=20=E3=81=AE=E3=83=93=E3=83=AB?=
 =?UTF-8?q?=E3=83=89=E3=82=AD=E3=83=A3=E3=83=83=E3=82=B7=E3=83=A5=E3=81=AB?=
 =?UTF-8?q?=20type=3Dregistry=20=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=99?=
 =?UTF-8?q?=E3=82=8B=20(MisskeyIO#211)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

タグでビルドしているため、Actions のキャッシュが効かない
---
 .github/workflows/docker-io.yml | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/.github/workflows/docker-io.yml b/.github/workflows/docker-io.yml
index 1e8eff26d4..0e9531a40f 100644
--- a/.github/workflows/docker-io.yml
+++ b/.github/workflows/docker-io.yml
@@ -43,8 +43,8 @@ jobs:
           platforms: ${{ steps.buildx.outputs.platforms }}
           provenance: false
           labels: ${{ env.FORMATTED_BRANCH_NAME }}
-          cache-from: type=gha
-          cache-to: type=gha,mode=max
+          cache-from: type=registry,ref=ghcr.io/misskeyio/misskey:io-buildcache
+          cache-to: type=registry,ref=ghcr.io/misskeyio/misskey:io-buildcache,mode=max
           tags: |
             ghcr.io/misskeyio/misskey:latest
             ghcr.io/misskeyio/misskey:${{ env.FORMATTED_BRANCH_NAME }}

From e7942310973c7bda1252777fe122ff08ff656712 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Mon, 6 Nov 2023 23:58:47 +0900
Subject: [PATCH 19/36] =?UTF-8?q?fix(frontend/MkNotifications):=20?=
 =?UTF-8?q?=E9=80=9A=E7=9F=A5=E3=81=8C=E8=A4=87=E6=95=B0=E5=9B=9E=E8=A1=A8?=
 =?UTF-8?q?=E7=A4=BA=E3=81=95=E3=82=8C=E3=81=A6=E3=81=97=E3=81=BE=E3=81=86?=
 =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(MisskeyIO#212)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkNotifications.vue | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index b7910d475a..8046c99287 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onUnmounted, onActivated, onMounted, computed, shallowRef } from 'vue';
+import { onMounted, onActivated, onUnmounted, onDeactivated, computed, shallowRef } from 'vue';
 import MkPagination, { Paging } from '@/components/MkPagination.vue';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import XNotification from '@/components/MkNotification.vue';
@@ -86,6 +86,10 @@ onActivated(() => {
 onUnmounted(() => {
 	if (connection) connection.dispose();
 });
+
+onDeactivated(() => {
+	if (connection) connection.dispose();
+});
 </script>
 
 <style lang="scss" module>

From f229e263124b15028adaf4bffb3e1126ab13c660 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Tue, 7 Nov 2023 00:38:53 +0900
Subject: [PATCH 20/36] Bump up version to 13.14.2-io.14b (MisskeyIO#213)

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index c7c87f0ae2..6a35343821 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.14.2-io.14a",
+	"version": "13.14.2-io.14b",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",

From ec5e1df9f56aabf24815d8a0b036f51ea863f31f Mon Sep 17 00:00:00 2001
From: CyberRex <hspwinx86@gmail.com>
Date: Tue, 7 Nov 2023 02:31:26 +0900
Subject: [PATCH 21/36] =?UTF-8?q?URL=E3=83=97=E3=83=AC=E3=83=93=E3=83=A5?=
 =?UTF-8?q?=E3=83=BC=E3=81=AE=E3=82=B5=E3=83=A0=E3=83=8D=E3=82=A4=E3=83=AB?=
 =?UTF-8?q?=E3=82=92=E9=9A=A0=E3=81=99=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD?=
 =?UTF-8?q?=E5=8A=A0=20(MisskeyIO#214)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/index.d.ts                             |  2 ++
 locales/ja-JP.yml                              |  2 ++
 .../1699284486293-urlPreviewDenyList.js        | 16 ++++++++++++++++
 packages/backend/src/models/entities/Meta.ts   |  5 +++++
 .../src/server/api/endpoints/admin/meta.ts     |  9 +++++++++
 .../server/api/endpoints/admin/update-meta.ts  |  7 +++++++
 .../src/server/web/UrlPreviewService.ts        | 18 ++++++++++++++++++
 .../frontend/src/components/MkUrlPreview.vue   |  8 +++++++-
 .../frontend/src/pages/admin/moderation.vue    |  8 ++++++++
 packages/misskey-js/etc/misskey-js.api.md      |  1 +
 packages/misskey-js/src/entities.ts            |  1 +
 11 files changed, 76 insertions(+), 1 deletion(-)
 create mode 100644 packages/backend/migration/1699284486293-urlPreviewDenyList.js

diff --git a/locales/index.d.ts b/locales/index.d.ts
index 114247b437..6ce0fee334 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -1113,6 +1113,8 @@ export interface Locale {
     "refreshing": string;
     "pullDownToRefresh": string;
     "disableStreamingTimeline": string;
+    "urlPreviewDenyList": string;
+    "urlPreviewDenyListDescription": string;
     "_announcement": {
         "forExistingUsers": string;
         "forExistingUsersDescription": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 907cbf0f5f..ca678e3058 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1110,6 +1110,8 @@ releaseToRefresh: "離してリロード"
 refreshing: "リロード中"
 pullDownToRefresh: "引っ張ってリロード"
 disableStreamingTimeline: "タイムラインのリアルタイム更新を無効にする"
+urlPreviewDenyList: "サムネイルの表示を制限するURL"
+urlPreviewDenyListDescription: "スペースで区切るとAND指定になり、改行で区切るとOR指定になります。スラッシュで囲むと正規表現になります。一致した場合、サムネイルがぼかされて表示されます。"
 
 _announcement:
   forExistingUsers: "既存ユーザーのみ"
diff --git a/packages/backend/migration/1699284486293-urlPreviewDenyList.js b/packages/backend/migration/1699284486293-urlPreviewDenyList.js
new file mode 100644
index 0000000000..4b921ad579
--- /dev/null
+++ b/packages/backend/migration/1699284486293-urlPreviewDenyList.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class UrlPreviewDenyList1699284486293 {
+    name = 'UrlPreviewDenyList1699284486293'
+
+    async up(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" ADD "urlPreviewDenyList" character varying(3072) array NOT NULL DEFAULT '{}'`);
+    }
+
+    async down(queryRunner) {
+        await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "urlPreviewDenyList"`);
+    }
+}
diff --git a/packages/backend/src/models/entities/Meta.ts b/packages/backend/src/models/entities/Meta.ts
index 6015296a20..58251430b0 100644
--- a/packages/backend/src/models/entities/Meta.ts
+++ b/packages/backend/src/models/entities/Meta.ts
@@ -468,4 +468,9 @@ export class MiMeta {
 		default: 300,
 	})
 	public perUserListTimelineCacheMax: number;
+
+	@Column('varchar', {
+		length: 3072, array: true, default: '{}',
+	})
+	public urlPreviewDenyList: string[];
 }
diff --git a/packages/backend/src/server/api/endpoints/admin/meta.ts b/packages/backend/src/server/api/endpoints/admin/meta.ts
index d4da22e1a0..7d0ea65959 100644
--- a/packages/backend/src/server/api/endpoints/admin/meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/meta.ts
@@ -298,6 +298,14 @@ export const meta = {
 				type: 'number',
 				optional: false, nullable: false,
 			},
+			urlPreviewDenyList: {
+				type: 'array',
+				optional: true, nullable: false,
+				items: {
+					type: 'string',
+					optional: false, nullable: false,
+				},
+			},
 		},
 	},
 } as const;
@@ -404,6 +412,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				perRemoteUserUserTimelineCacheMax: instance.perRemoteUserUserTimelineCacheMax,
 				perUserHomeTimelineCacheMax: instance.perUserHomeTimelineCacheMax,
 				perUserListTimelineCacheMax: instance.perUserListTimelineCacheMax,
+				urlPreviewDenyList: instance.urlPreviewDenyList,
 			};
 		});
 	}
diff --git a/packages/backend/src/server/api/endpoints/admin/update-meta.ts b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
index 5d5812ff38..f94b8d3ba9 100644
--- a/packages/backend/src/server/api/endpoints/admin/update-meta.ts
+++ b/packages/backend/src/server/api/endpoints/admin/update-meta.ts
@@ -110,6 +110,9 @@ export const paramDef = {
 		perRemoteUserUserTimelineCacheMax: { type: 'integer' },
 		perUserHomeTimelineCacheMax: { type: 'integer' },
 		perUserListTimelineCacheMax: { type: 'integer' },
+		urlPreviewDenyList: { type: 'array', nullable: true, items: {
+			type: 'string',
+		} },
 	},
 	required: [],
 } as const;
@@ -147,6 +150,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
 				set.sensitiveWords = ps.sensitiveWords.filter(Boolean);
 			}
 
+			if (Array.isArray(ps.urlPreviewDenyList)) {
+				set.urlPreviewDenyList = ps.urlPreviewDenyList.filter(Boolean);
+			}
+
 			if (ps.themeColor !== undefined) {
 				set.themeColor = ps.themeColor;
 			}
diff --git a/packages/backend/src/server/web/UrlPreviewService.ts b/packages/backend/src/server/web/UrlPreviewService.ts
index d590244e34..76e847b7ce 100644
--- a/packages/backend/src/server/web/UrlPreviewService.ts
+++ b/packages/backend/src/server/web/UrlPreviewService.ts
@@ -5,6 +5,7 @@
 
 import { Inject, Injectable } from '@nestjs/common';
 import { summaly } from 'summaly';
+import RE2 from 're2';
 import { DI } from '@/di-symbols.js';
 import type { Config } from '@/config.js';
 import { MetaService } from '@/core/MetaService.js';
@@ -94,6 +95,23 @@ export class UrlPreviewService {
 			summary.icon = this.wrap(summary.icon);
 			summary.thumbnail = this.wrap(summary.thumbnail);
 
+			const includeDenyList = meta.urlPreviewDenyList.some(filter => {
+				// represents RegExp
+				const regexp = /^\/(.+)\/(.*)$/.exec(filter);
+				// This should never happen due to input sanitisation.
+				if (!regexp) {
+					const words = filter.split(' ');
+					return words.every(keyword => summary.url.includes(keyword));
+				}
+				try {
+					return new RE2(regexp[1], regexp[2]).test(summary.url);
+				} catch (err) {
+					// This should never happen due to input sanitisation.
+					return false;
+				}
+			});
+			if (includeDenyList) summary.sensitive = true;
+
 			// Cache 7days
 			reply.header('Cache-Control', 'max-age=604800, immutable');
 
diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index 19bbb3882f..a40957786b 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 <div v-else>
 	<component :is="self ? 'MkA' : 'a'" :class="[$style.link, { [$style.compact]: compact }]" :[attr]="self ? url.substring(local.length) : url" rel="nofollow noopener" :target="target" :title="url">
-		<div v-if="thumbnail" :class="$style.thumbnail" :style="`background-image: url('${thumbnail}')`">
+		<div v-if="thumbnail" :class="[$style.thumbnail, { [$style.thumbnailBlur]: sensitive }]" :style="`background-image: url('${thumbnail}')`">
 		</div>
 		<article :class="$style.body">
 			<header :class="$style.header">
@@ -118,6 +118,7 @@ let description = $ref<string | null>(null);
 let thumbnail = $ref<string | null>(null);
 let icon = $ref<string | null>(null);
 let sitename = $ref<string | null>(null);
+let sensitive = $ref<boolean | undefined>(undefined);
 let player = $ref({
 	url: null,
 	width: null,
@@ -170,6 +171,7 @@ window.fetch(`/url?url=${encodeURIComponent(requestUrl.href)}&lang=${versatileLa
 		icon = info.icon;
 		sitename = info.sitename;
 		player = info.player;
+		sensitive = info.sensitive;
 	});
 
 function adjustTweetHeight(message: any) {
@@ -319,6 +321,10 @@ onUnmounted(() => {
 	margin-top: 6px;
 }
 
+.thumbnailBlur {
+		filter: blur(8px);
+}
+
 @container (max-width: 400px) {
 	.link {
 		font-size: 12px;
diff --git a/packages/frontend/src/pages/admin/moderation.vue b/packages/frontend/src/pages/admin/moderation.vue
index 313d8412b2..95ab3b5b39 100644
--- a/packages/frontend/src/pages/admin/moderation.vue
+++ b/packages/frontend/src/pages/admin/moderation.vue
@@ -34,6 +34,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 						<template #label>{{ i18n.ts.sensitiveWords }}</template>
 						<template #caption>{{ i18n.ts.sensitiveWordsDescription }}<br>{{ i18n.ts.sensitiveWordsDescription2 }}</template>
 					</MkTextarea>
+
+					<MkTextarea v-model="urlPreviewDenyList">
+						<template #label>{{ i18n.ts.urlPreviewDenyList }}</template>
+						<template #caption>{{ i18n.ts.urlPreviewDenyListDescription }}</template>
+					</MkTextarea>
 				</div>
 			</FormSuspense>
 		</MkSpacer>
@@ -69,6 +74,7 @@ let emailRequiredForSignup: boolean = $ref(false);
 let sensitiveWords: string = $ref('');
 let preservedUsernames: string = $ref('');
 let tosUrl: string | null = $ref(null);
+let urlPreviewDenyList: string = $ref('');
 
 async function init() {
 	const meta = await os.api('admin/meta');
@@ -77,6 +83,7 @@ async function init() {
 	sensitiveWords = meta.sensitiveWords.join('\n');
 	preservedUsernames = meta.preservedUsernames.join('\n');
 	tosUrl = meta.tosUrl;
+	urlPreviewDenyList = meta.urlPreviewDenyList.join('\n');
 }
 
 function save() {
@@ -86,6 +93,7 @@ function save() {
 		tosUrl,
 		sensitiveWords: sensitiveWords.split('\n'),
 		preservedUsernames: preservedUsernames.split('\n'),
+		urlPreviewDenyList: urlPreviewDenyList.split('\n'),
 	}).then(() => {
 		fetchInstance();
 	});
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index c6084ce2c0..76bb34d97f 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -20,6 +20,7 @@ type Ad = TODO_2;
 // @public (undocumented)
 type AdminInstanceMetadata = DetailedInstanceMetadata & {
     blockedHosts: string[];
+    urlPreviewDenyList: string[];
 };
 
 // @public (undocumented)
diff --git a/packages/misskey-js/src/entities.ts b/packages/misskey-js/src/entities.ts
index d61e4204bb..b09fd01707 100644
--- a/packages/misskey-js/src/entities.ts
+++ b/packages/misskey-js/src/entities.ts
@@ -355,6 +355,7 @@ export type InstanceMetadata = LiteInstanceMetadata | DetailedInstanceMetadata;
 export type AdminInstanceMetadata = DetailedInstanceMetadata & {
 	// TODO: There are more fields.
 	blockedHosts: string[];
+	urlPreviewDenyList: string[];
 };
 
 export type ServerInfo = {

From 7b8810c900b46929c475251fc09e41ae77046a79 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Tue, 7 Nov 2023 02:32:09 +0900
Subject: [PATCH 22/36] =?UTF-8?q?fix(frontend/MkNotifications):=20?=
 =?UTF-8?q?=E9=80=9A=E7=9F=A5=E3=81=8C=E8=A4=87=E6=95=B0=E5=9B=9E=E8=A1=A8?=
 =?UTF-8?q?=E7=A4=BA=E3=81=95=E3=82=8C=E3=81=A6=E3=81=97=E3=81=BE=E3=81=86?=
 =?UTF-8?q?=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(MisskeyIO#215)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Revert "fix(frontend/MkNotifications): 通知が複数回表示されてしまう問題を修正 (MisskeyIO#212)"
This reverts commit e7942310973c7bda1252777fe122ff08ff656712.

* fix(frontend/MkNotifications): 通知が複数回表示されてしまう問題を修正
---
 packages/frontend/src/components/MkNotifications.vue | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index 8046c99287..d6c3becfcb 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -24,7 +24,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { onMounted, onActivated, onUnmounted, onDeactivated, computed, shallowRef } from 'vue';
+import { onUnmounted, onActivated, onMounted, computed, shallowRef } from 'vue';
 import MkPagination, { Paging } from '@/components/MkPagination.vue';
 import MkPullToRefresh from '@/components/MkPullToRefresh.vue';
 import XNotification from '@/components/MkNotification.vue';
@@ -79,17 +79,11 @@ onMounted(() => {
 
 onActivated(() => {
 	pagingComponent.value?.reload();
-	connection = useStream().useChannel('main');
-	connection.on('notification', onNotification);
 });
 
 onUnmounted(() => {
 	if (connection) connection.dispose();
 });
-
-onDeactivated(() => {
-	if (connection) connection.dispose();
-});
 </script>
 
 <style lang="scss" module>

From 825e99e46d32b2690dd0e9c33dde78ff80029587 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Tue, 7 Nov 2023 02:32:43 +0900
Subject: [PATCH 23/36]  Bump up version to 13.14.2-io.14c (MisskeyIO#216)

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 6a35343821..3d9fdb3c16 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.14.2-io.14b",
+	"version": "13.14.2-io.14c",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",

From fe16916c722e60a6aef1e7f22fc5b93fe9c1c1b3 Mon Sep 17 00:00:00 2001
From: riku6460 <17585784+riku6460@users.noreply.github.com>
Date: Tue, 7 Nov 2023 05:49:29 +0900
Subject: [PATCH 24/36] =?UTF-8?q?Docker=20=E3=82=A4=E3=83=A1=E3=83=BC?=
 =?UTF-8?q?=E3=82=B8=E3=83=93=E3=83=AB=E3=83=89=E6=99=82=E3=80=81package.j?=
 =?UTF-8?q?son=20=E3=82=92=E3=82=B3=E3=83=94=E3=83=BC=E3=81=99=E3=82=8B?=
 =?UTF-8?q?=E5=89=8D=E3=81=AB=20pnpm=20fetch=20=E3=81=99=E3=82=8B=E3=82=88?=
 =?UTF-8?q?=E3=81=86=E3=81=AB=20(MisskeyIO#217)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 Dockerfile | 18 ++++++++++++------
 1 file changed, 12 insertions(+), 6 deletions(-)

diff --git a/Dockerfile b/Dockerfile
index 64ebf8654f..bd08de841b 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -18,15 +18,18 @@ RUN corepack enable
 
 WORKDIR /misskey
 
-COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
+COPY --link pnpm-lock.yaml ./
+RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
+	pnpm fetch
+
+COPY --link ["pnpm-workspace.yaml", "package.json", "./"]
 COPY --link ["scripts", "./scripts"]
 COPY --link ["packages/backend/package.json", "./packages/backend/"]
 COPY --link ["packages/frontend/package.json", "./packages/frontend/"]
 COPY --link ["packages/sw/package.json", "./packages/sw/"]
 COPY --link ["packages/misskey-js/package.json", "./packages/misskey-js/"]
 
-RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
-	pnpm i --frozen-lockfile --aggregate-output
+RUN pnpm i --frozen-lockfile --aggregate-output --offline
 
 COPY --link . ./
 
@@ -48,12 +51,15 @@ RUN corepack enable
 
 WORKDIR /misskey
 
-COPY --link ["pnpm-lock.yaml", "pnpm-workspace.yaml", "package.json", "./"]
+COPY --link pnpm-lock.yaml ./
+RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
+	pnpm fetch
+
+COPY --link ["pnpm-workspace.yaml", "package.json", "./"]
 COPY --link ["scripts", "./scripts"]
 COPY --link ["packages/backend/package.json", "./packages/backend/"]
 
-RUN --mount=type=cache,target=/root/.local/share/pnpm/store,sharing=locked \
-	pnpm i --frozen-lockfile --aggregate-output
+RUN pnpm i --frozen-lockfile --aggregate-output --offline
 
 FROM --platform=$TARGETPLATFORM node:${NODE_VERSION}-slim AS runner
 

From 510c2b6808610b5d15e6b9f45ccf454bdc4e9706 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Tue, 7 Nov 2023 22:51:08 +0900
Subject: [PATCH 25/36] =?UTF-8?q?enhance(frontend):=20=E3=83=87=E3=83=83?=
 =?UTF-8?q?=E3=82=AD=E3=81=AE=E3=82=AB=E3=83=A9=E3=83=A0=E3=81=8B=E3=82=89?=
 =?UTF-8?q?=E3=83=AA=E3=83=AD=E3=83=BC=E3=83=89=E3=81=A7=E3=81=8D=E3=82=8B?=
 =?UTF-8?q?=E6=A9=9F=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=20(misskey-dev#12?=
 =?UTF-8?q?274)=20(MisskeyIO#220)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Co-authored-by: samunohito <46447427+sam-osamu@users.noreply.github.com>
---
 .../frontend/src/components/MkNotifications.vue  |  4 ++++
 packages/frontend/src/ui/deck/antenna-column.vue |  2 +-
 packages/frontend/src/ui/deck/channel-column.vue |  2 +-
 packages/frontend/src/ui/deck/column.vue         | 13 +++++++++++++
 packages/frontend/src/ui/deck/direct-column.vue  | 16 ++++++++++++++--
 packages/frontend/src/ui/deck/list-column.vue    |  2 +-
 .../frontend/src/ui/deck/mentions-column.vue     | 16 ++++++++++++++--
 .../src/ui/deck/notifications-column.vue         |  6 ++++--
 .../src/ui/deck/role-timeline-column.vue         |  2 +-
 packages/frontend/src/ui/deck/tl-column.vue      |  3 ++-
 10 files changed, 55 insertions(+), 11 deletions(-)

diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue
index d6c3becfcb..5a76fc0b35 100644
--- a/packages/frontend/src/components/MkNotifications.vue
+++ b/packages/frontend/src/components/MkNotifications.vue
@@ -84,6 +84,10 @@ onActivated(() => {
 onUnmounted(() => {
 	if (connection) connection.dispose();
 });
+
+defineExpose({
+	reload,
+});
 </script>
 
 <style lang="scss" module>
diff --git a/packages/frontend/src/ui/deck/antenna-column.vue b/packages/frontend/src/ui/deck/antenna-column.vue
index dfddb1bd05..f841e70dce 100644
--- a/packages/frontend/src/ui/deck/antenna-column.vue
+++ b/packages/frontend/src/ui/deck/antenna-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :menu="menu" :column="column" :isStacked="isStacked">
+<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
 	<template #header>
 		<i class="ti ti-antenna"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
diff --git a/packages/frontend/src/ui/deck/channel-column.vue b/packages/frontend/src/ui/deck/channel-column.vue
index de6d7f505b..bd7c198a43 100644
--- a/packages/frontend/src/ui/deck/channel-column.vue
+++ b/packages/frontend/src/ui/deck/channel-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :menu="menu" :column="column" :isStacked="isStacked">
+<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
 	<template #header>
 		<i class="ti ti-device-tv"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index d2eef4bea6..59de14fc45 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -57,6 +57,7 @@ const props = withDefaults(defineProps<{
 	isStacked?: boolean;
 	naked?: boolean;
 	menu?: MenuItem[];
+	refresher?: () => Promise<void>;
 }>(), {
 	isStacked: false,
 	naked: false,
@@ -177,6 +178,18 @@ function getMenu() {
 		},
 	}];
 
+	if (props.refresher) {
+		items = [{
+			icon: 'ti ti-refresh',
+			text: i18n.ts.reload,
+			action: () => {
+				if (props.refresher) {
+					props.refresher();
+				}
+			},
+		}, ...items];
+	}
+
 	if (props.menu) {
 		items.unshift(null);
 		items = props.menu.concat(items);
diff --git a/packages/frontend/src/ui/deck/direct-column.vue b/packages/frontend/src/ui/deck/direct-column.vue
index 228db37345..7e2ec7cf70 100644
--- a/packages/frontend/src/ui/deck/direct-column.vue
+++ b/packages/frontend/src/ui/deck/direct-column.vue
@@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :column="column" :isStacked="isStacked">
+<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
 	<template #header><i class="ti ti-mail" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
-	<MkNotes :pagination="pagination"/>
+	<MkNotes ref="tlComponent" :pagination="pagination"/>
 </XColumn>
 </template>
 
@@ -16,6 +16,7 @@ import { } from 'vue';
 import XColumn from './column.vue';
 import { Column } from './deck-store';
 import MkNotes from '@/components/MkNotes.vue';
+import { reloadStream } from '@/stream.js';
 
 defineProps<{
 	column: Column;
@@ -29,4 +30,15 @@ const pagination = {
 		visibility: 'specified',
 	},
 };
+
+const tlComponent: InstanceType<typeof MkNotes> = $ref();
+
+function reloadTimeline() {
+	return new Promise<void>((res) => {
+		tlComponent.pagingComponent?.reload().then(() => {
+			reloadStream();
+			res();
+		});
+	});
+}
 </script>
diff --git a/packages/frontend/src/ui/deck/list-column.vue b/packages/frontend/src/ui/deck/list-column.vue
index 225c4948b1..f671704666 100644
--- a/packages/frontend/src/ui/deck/list-column.vue
+++ b/packages/frontend/src/ui/deck/list-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :menu="menu" :column="column" :isStacked="isStacked">
+<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
 	<template #header>
 		<i class="ti ti-list"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
diff --git a/packages/frontend/src/ui/deck/mentions-column.vue b/packages/frontend/src/ui/deck/mentions-column.vue
index c8d0d21a02..3a0fedeacf 100644
--- a/packages/frontend/src/ui/deck/mentions-column.vue
+++ b/packages/frontend/src/ui/deck/mentions-column.vue
@@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :column="column" :isStacked="isStacked">
+<XColumn :column="column" :isStacked="isStacked" :refresher="() => reloadTimeline()">
 	<template #header><i class="ti ti-at" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
-	<MkNotes :pagination="pagination"/>
+	<MkNotes ref="tlComponent" :pagination="pagination"/>
 </XColumn>
 </template>
 
@@ -16,12 +16,24 @@ import { } from 'vue';
 import XColumn from './column.vue';
 import { Column } from './deck-store';
 import MkNotes from '@/components/MkNotes.vue';
+import { reloadStream } from '@/stream.js';
 
 defineProps<{
 	column: Column;
 	isStacked: boolean;
 }>();
 
+const tlComponent: InstanceType<typeof MkNotes> = $ref();
+
+function reloadTimeline() {
+	return new Promise<void>((res) => {
+		tlComponent.pagingComponent?.reload().then(() => {
+			reloadStream();
+			res();
+		});
+	});
+}
+
 const pagination = {
 	endpoint: 'notes/mentions' as const,
 	limit: 10,
diff --git a/packages/frontend/src/ui/deck/notifications-column.vue b/packages/frontend/src/ui/deck/notifications-column.vue
index 3308197c5e..8f204364d9 100644
--- a/packages/frontend/src/ui/deck/notifications-column.vue
+++ b/packages/frontend/src/ui/deck/notifications-column.vue
@@ -4,10 +4,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :column="column" :isStacked="isStacked" :menu="menu">
+<XColumn :column="column" :isStacked="isStacked" :menu="menu" :refresher="() => notificationsComponent.reload()">
 	<template #header><i class="ti ti-bell" style="margin-right: 8px;"></i>{{ column.name }}</template>
 
-	<XNotifications :includeTypes="column.includingTypes"/>
+	<XNotifications ref="notificationsComponent" :includeTypes="column.includingTypes"/>
 </XColumn>
 </template>
 
@@ -24,6 +24,8 @@ const props = defineProps<{
 	isStacked: boolean;
 }>();
 
+let notificationsComponent = $shallowRef<InstanceType<typeof XNotifications>>();
+
 function func() {
 	os.popup(defineAsyncComponent(() => import('@/components/MkNotificationSettingWindow.vue')), {
 		includingTypes: props.column.includingTypes,
diff --git a/packages/frontend/src/ui/deck/role-timeline-column.vue b/packages/frontend/src/ui/deck/role-timeline-column.vue
index 5406865f68..c4cacf2208 100644
--- a/packages/frontend/src/ui/deck/role-timeline-column.vue
+++ b/packages/frontend/src/ui/deck/role-timeline-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :menu="menu" :column="column" :isStacked="isStacked">
+<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
 	<template #header>
 		<i class="ti ti-badge"></i><span style="margin-left: 8px;">{{ column.name }}</span>
 	</template>
diff --git a/packages/frontend/src/ui/deck/tl-column.vue b/packages/frontend/src/ui/deck/tl-column.vue
index df8da8c901..0d524eea5c 100644
--- a/packages/frontend/src/ui/deck/tl-column.vue
+++ b/packages/frontend/src/ui/deck/tl-column.vue
@@ -4,7 +4,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 -->
 
 <template>
-<XColumn :menu="menu" :column="column" :isStacked="isStacked">
+<XColumn :menu="menu" :column="column" :isStacked="isStacked" :refresher="() => timeline.reloadTimeline()">
 	<template #header>
 		<i v-if="column.tl === 'home'" class="ti ti-home"></i>
 		<i v-else-if="column.tl === 'local'" class="ti ti-planet"></i>
@@ -41,6 +41,7 @@ const props = defineProps<{
 }>();
 
 let disabled = $ref(false);
+let timeline = $shallowRef<InstanceType<typeof MkTimeline>>();
 
 const isLocalTimelineAvailable = (($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable));
 const isGlobalTimelineAvailable = (($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable));

From 0ff83829baa94820172d3e5a658ddefe47c3ae63 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Tue, 7 Nov 2023 22:51:46 +0900
Subject: [PATCH 26/36] =?UTF-8?q?enhance(frontend):=20=E3=82=B7=E3=82=A7?=
 =?UTF-8?q?=E3=83=BC=E3=83=80=E3=83=BC=E3=82=B3=E3=83=B3=E3=83=91=E3=82=A4?=
 =?UTF-8?q?=E3=83=AB=E3=81=AB=E5=A4=B1=E6=95=97=E3=81=97=E3=81=9F=E5=A0=B4?=
 =?UTF-8?q?=E5=90=88=E8=A9=B3=E7=B4=B0=E3=81=AA=E3=82=A8=E3=83=A9=E3=83=BC?=
 =?UTF-8?q?=E5=86=85=E5=AE=B9=E3=82=92=E5=87=BA=E5=8A=9B=E3=81=A7=E3=81=8D?=
 =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(MisskeyIO#221)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkAnimBg.vue | 12 ++++++++++--
 1 file changed, 10 insertions(+), 2 deletions(-)

diff --git a/packages/frontend/src/components/MkAnimBg.vue b/packages/frontend/src/components/MkAnimBg.vue
index 70d101a9d3..6bda74a811 100644
--- a/packages/frontend/src/components/MkAnimBg.vue
+++ b/packages/frontend/src/components/MkAnimBg.vue
@@ -24,8 +24,16 @@ const props = withDefaults(defineProps<{
 function loadShader(gl, type, source) {
 	const shader = gl.createShader(type);
 
-	gl.shaderSource(shader, source);
-	gl.compileShader(shader);
+	try {
+		gl.shaderSource(shader, source);
+		gl.compileShader(shader);
+	} catch (error) {
+		alert(
+			`failed to compile shader: ${error} ${gl.getShaderInfoLog(shader)}`,
+		);
+		gl.deleteShader(shader);
+		return null;
+	}
 
 	if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
 		alert(

From 7b53f6654198a94044b66198c5483ef2c455eeb8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Wed, 8 Nov 2023 01:37:50 +0900
Subject: [PATCH 27/36] =?UTF-8?q?feat(moderation):=20=E3=83=A2=E3=83=87?=
 =?UTF-8?q?=E3=83=AC=E3=83=BC=E3=82=BF=E3=83=BC=E3=81=8C=E3=83=A6=E3=83=BC?=
 =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=81=AE=E3=82=A2=E3=82=A4=E3=82=B3=E3=83=B3?=
 =?UTF-8?q?=E3=82=82=E3=81=97=E3=81=8F=E3=81=AF=E3=83=90=E3=83=8A=E3=83=BC?=
 =?UTF-8?q?=E7=94=BB=E5=83=8F=E3=82=92=E6=9C=AA=E8=A8=AD=E5=AE=9A=E7=8A=B6?=
 =?UTF-8?q?=E6=85=8B=E3=81=AB=E3=81=A7=E3=81=8D=E3=82=8B=E6=A9=9F=E8=83=BD?=
 =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=20(MisskeyIO#222)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/en-US.yml                             |  4 ++
 locales/index.d.ts                            |  4 ++
 locales/ja-JP.yml                             |  4 ++
 .../backend/src/server/api/EndpointsModule.ts |  8 ++++
 packages/backend/src/server/api/endpoints.ts  |  4 ++
 .../api/endpoints/admin/delete-user-avatar.ts | 48 +++++++++++++++++++
 .../api/endpoints/admin/delete-user-banner.ts | 48 +++++++++++++++++++
 packages/frontend/src/pages/user-info.vue     | 42 ++++++++++++++++
 packages/misskey-js/etc/misskey-js.api.md     | 16 ++++++-
 packages/misskey-js/src/api.types.ts          |  2 +
 10 files changed, 178 insertions(+), 2 deletions(-)
 create mode 100644 packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts
 create mode 100644 packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts

diff --git a/locales/en-US.yml b/locales/en-US.yml
index b1725a2fb8..bf3f1ceba3 100644
--- a/locales/en-US.yml
+++ b/locales/en-US.yml
@@ -558,6 +558,10 @@ output: "Output"
 script: "Script"
 disablePagesScript: "Disable AiScript on Pages"
 updateRemoteUser: "Update remote user information"
+deleteUserAvatar: "Delete user icon"
+deleteUserAvatarConfirm: "Are you sure that you want to delete this user's icon?"
+deleteUserBanner: "Delete user banner"
+deleteUserBannerConfirm: "Are you sure that you want to delete this user's banner?"
 deleteAllFiles: "Delete all files"
 deleteAllFilesConfirm: "Are you sure that you want to delete all files?"
 removeAllFollowing: "Unfollow all followed users"
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 6ce0fee334..26c034c476 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -561,6 +561,10 @@ export interface Locale {
     "script": string;
     "disablePagesScript": string;
     "updateRemoteUser": string;
+    "deleteUserAvatar": string;
+    "deleteUserAvatarConfirm": string;
+    "deleteUserBanner": string;
+    "deleteUserBannerConfirm": string;
     "deleteAllFiles": string;
     "deleteAllFilesConfirm": string;
     "removeAllFollowing": string;
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index ca678e3058..c13102b379 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -558,6 +558,10 @@ output: "出力"
 script: "スクリプト"
 disablePagesScript: "Pagesのスクリプトを無効にする"
 updateRemoteUser: "リモートユーザー情報の更新"
+deleteUserAvatar: "アイコンを削除"
+deleteUserAvatarConfirm: "アイコンを削除しますか?"
+deleteUserBanner: "バナーを削除"
+deleteUserBannerConfirm: "バナーを削除しますか?"
 deleteAllFiles: "すべてのファイルを削除"
 deleteAllFilesConfirm: "すべてのファイルを削除しますか?"
 removeAllFollowing: "フォローを全解除"
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 17bec23c13..d3456a2c7d 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -23,6 +23,8 @@ import * as ep___admin_abuseReportResolver_update from './endpoints/admin/abuse-
 import * as ep___admin_abuseReportResolver_delete from './endpoints/admin/abuse-report-resolver/delete.js';
 import * as ep___admin_abuseReportResolver_list from './endpoints/admin/abuse-report-resolver/list.js';
 import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
+import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js';
+import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js';
 import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
 import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
 import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
@@ -372,6 +374,8 @@ const $admin_abuseReportResolver_update: Provider = { provide: 'ep:admin/abuse-r
 const $admin_abuseReportResolver_list: Provider = { provide: 'ep:admin/abuse-report-resolver/list', useClass: ep___admin_abuseReportResolver_list.default };
 const $admin_abuseReportResolver_delete: Provider = { provide: 'ep:admin/abuse-report-resolver/delete', useClass: ep___admin_abuseReportResolver_delete.default };
 const $admin_deleteAllFilesOfAUser: Provider = { provide: 'ep:admin/delete-all-files-of-a-user', useClass: ep___admin_deleteAllFilesOfAUser.default };
+const $admin_deleteUserAvatar: Provider = { provide: 'ep:admin/delete-user-avatar', useClass: ep___admin_deleteUserAvatar.default };
+const $admin_deleteUserBanner: Provider = { provide: 'ep:admin/delete-user-banner', useClass: ep___admin_deleteUserBanner.default };
 const $admin_drive_cleanRemoteFiles: Provider = { provide: 'ep:admin/drive/clean-remote-files', useClass: ep___admin_drive_cleanRemoteFiles.default };
 const $admin_drive_cleanup: Provider = { provide: 'ep:admin/drive/cleanup', useClass: ep___admin_drive_cleanup.default };
 const $admin_drive_files: Provider = { provide: 'ep:admin/drive/files', useClass: ep___admin_drive_files.default };
@@ -725,6 +729,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$admin_abuseReportResolver_list,
 		$admin_abuseReportResolver_update,
 		$admin_deleteAllFilesOfAUser,
+		$admin_deleteUserAvatar,
+		$admin_deleteUserBanner,
 		$admin_drive_cleanRemoteFiles,
 		$admin_drive_cleanup,
 		$admin_drive_files,
@@ -1072,6 +1078,8 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
 		$admin_abuseReportResolver_list,
 		$admin_abuseReportResolver_update,
 		$admin_deleteAllFilesOfAUser,
+		$admin_deleteUserAvatar,
+		$admin_deleteUserBanner,
 		$admin_drive_cleanRemoteFiles,
 		$admin_drive_cleanup,
 		$admin_drive_files,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 8c5ab17d07..39fe69a013 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -23,6 +23,8 @@ import * as ep___admin_abuseReportResolver_update from './endpoints/admin/abuse-
 import * as ep___admin_abuseReportResolver_delete from './endpoints/admin/abuse-report-resolver/delete.js';
 import * as ep___admin_abuseReportResolver_list from './endpoints/admin/abuse-report-resolver/list.js';
 import * as ep___admin_deleteAllFilesOfAUser from './endpoints/admin/delete-all-files-of-a-user.js';
+import * as ep___admin_deleteUserAvatar from './endpoints/admin/delete-user-avatar.js';
+import * as ep___admin_deleteUserBanner from './endpoints/admin/delete-user-banner.js';
 import * as ep___admin_drive_cleanRemoteFiles from './endpoints/admin/drive/clean-remote-files.js';
 import * as ep___admin_drive_cleanup from './endpoints/admin/drive/cleanup.js';
 import * as ep___admin_drive_files from './endpoints/admin/drive/files.js';
@@ -370,6 +372,8 @@ const eps = [
 	['admin/abuse-report-resolver/delete', ep___admin_abuseReportResolver_delete],
 	['admin/abuse-report-resolver/update', ep___admin_abuseReportResolver_update],
 	['admin/delete-all-files-of-a-user', ep___admin_deleteAllFilesOfAUser],
+	['admin/delete-user-avatar', ep___admin_deleteUserAvatar],
+	['admin/delete-user-banner', ep___admin_deleteUserBanner],
 	['admin/drive/clean-remote-files', ep___admin_drive_cleanRemoteFiles],
 	['admin/drive/cleanup', ep___admin_drive_cleanup],
 	['admin/drive/files', ep___admin_drive_files],
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts b/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts
new file mode 100644
index 0000000000..bdc1357d7f
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/delete-user-avatar.ts
@@ -0,0 +1,48 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import type { UsersRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		userId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['userId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const user = await this.usersRepository.findOneBy({ id: ps.userId });
+
+			if (user == null) {
+				throw new Error('user not found');
+			}
+
+			await this.usersRepository.update(user.id, {
+				avatar: null,
+				avatarId: null,
+				avatarUrl: null,
+				avatarBlurhash: null,
+			});
+		});
+	}
+}
diff --git a/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts b/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts
new file mode 100644
index 0000000000..4d8d1bcc56
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/delete-user-banner.ts
@@ -0,0 +1,48 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and other misskey contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import type { UsersRepository } from '@/models/index.js';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { DI } from '@/di-symbols.js';
+
+export const meta = {
+	tags: ['admin'],
+
+	requireCredential: true,
+	requireModerator: true,
+} as const;
+
+export const paramDef = {
+	type: 'object',
+	properties: {
+		userId: { type: 'string', format: 'misskey:id' },
+	},
+	required: ['userId'],
+} as const;
+
+// eslint-disable-next-line import/no-default-export
+@Injectable()
+export default class extends Endpoint<typeof meta, typeof paramDef> {
+	constructor(
+		@Inject(DI.usersRepository)
+		private usersRepository: UsersRepository,
+	) {
+		super(meta, paramDef, async (ps, me) => {
+			const user = await this.usersRepository.findOneBy({ id: ps.userId });
+
+			if (user == null) {
+				throw new Error('user not found');
+			}
+
+			await this.usersRepository.update(user.id, {
+				banner: null,
+				bannerId: null,
+				bannerUrl: null,
+				bannerBlurhash: null,
+			});
+		});
+	}
+}
diff --git a/packages/frontend/src/pages/user-info.vue b/packages/frontend/src/pages/user-info.vue
index d6ea1a5245..e371aceba7 100644
--- a/packages/frontend/src/pages/user-info.vue
+++ b/packages/frontend/src/pages/user-info.vue
@@ -100,6 +100,10 @@ SPDX-License-Identifier: AGPL-3.0-only
 					<MkButton v-if="user.host == null && iAmModerator" inline style="margin-right: 8px;" @click="resetPassword"><i class="ti ti-key"></i> {{ i18n.ts.resetPassword }}</MkButton>
 					<MkButton v-if="$i.isAdmin" inline danger @click="deleteAccount">{{ i18n.ts.deleteAccount }}</MkButton>
 				</div>
+				<div>
+					<MkButton v-if="iAmModerator" inline danger style="margin-right: 8px;" @click="deleteUserAvatar"><i class="ti ti-user-circle"></i> {{ i18n.ts.deleteUserAvatar }}</MkButton>
+					<MkButton v-if="iAmModerator" inline danger @click="deleteUserBanner"><i class="ti ti-photo"></i> {{ i18n.ts.deleteUserBanner }}</MkButton>
+				</div>
 
 				<MkFolder>
 					<template #icon><i class="ti ti-license"></i></template>
@@ -345,6 +349,44 @@ async function toggleSuspend(v) {
 	}
 }
 
+async function deleteUserAvatar() {
+  const confirm = await os.confirm({
+    type: 'warning',
+    text: i18n.ts.deleteUserAvatarConfirm,
+  });
+  if (confirm.canceled) return;
+  const process = async () => {
+    await os.api('admin/delete-user-avatar', { userId: user.id });
+    os.success();
+  };
+  await process().catch(err => {
+    os.alert({
+      type: 'error',
+      text: err.toString(),
+    });
+  });
+  refreshUser();
+}
+
+async function deleteUserBanner() {
+  const confirm = await os.confirm({
+    type: 'warning',
+    text: i18n.ts.deleteUserBannerConfirm,
+  });
+  if (confirm.canceled) return;
+  const process = async () => {
+    await os.api('admin/delete-user-banner', { userId: user.id });
+    os.success();
+  };
+  await process().catch(err => {
+    os.alert({
+      type: 'error',
+      text: err.toString(),
+    });
+  });
+  refreshUser();
+}
+
 async function deleteAllFiles() {
 	const confirm = await os.confirm({
 		type: 'warning',
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 76bb34d97f..43cf04f196 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -323,6 +323,18 @@ export type Endpoints = {
         };
         res: null;
     };
+    'admin/delete-user-avatar': {
+        req: {
+            userId: User['id'];
+        };
+        res: null;
+    };
+    'admin/delete-user-banner': {
+        req: {
+            userId: User['id'];
+        };
+        res: null;
+    };
     'admin/delete-logs': {
         req: NoParams;
         res: null;
@@ -2844,8 +2856,8 @@ type UserSorting = '+follower' | '-follower' | '+createdAt' | '-createdAt' | '+u
 // Warnings were encountered during analysis:
 //
 // src/api.types.ts:16:32 - (ae-forgotten-export) The symbol "TODO" needs to be exported by the entry point index.d.ts
-// src/api.types.ts:18:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
-// src/api.types.ts:633:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
+// src/api.types.ts:20:25 - (ae-forgotten-export) The symbol "NoParams" needs to be exported by the entry point index.d.ts
+// src/api.types.ts:635:18 - (ae-forgotten-export) The symbol "ShowUserReq" needs to be exported by the entry point index.d.ts
 // src/streaming.types.ts:33:4 - (ae-forgotten-export) The symbol "FIXME" needs to be exported by the entry point index.d.ts
 
 // (No @packageDocumentation comment for this package)
diff --git a/packages/misskey-js/src/api.types.ts b/packages/misskey-js/src/api.types.ts
index d75c47171a..7f058b3ba9 100644
--- a/packages/misskey-js/src/api.types.ts
+++ b/packages/misskey-js/src/api.types.ts
@@ -15,6 +15,8 @@ export type Endpoints = {
 	// admin
 	'admin/abuse-user-reports': { req: TODO; res: TODO; };
 	'admin/delete-all-files-of-a-user': { req: { userId: User['id']; }; res: null; };
+	'admin/delete-user-avatar': { req: { userId: User['id']; }; res: null; };
+	'admin/delete-user-banner': { req: { userId: User['id']; }; res: null; };
 	'admin/delete-logs': { req: NoParams; res: null; };
 	'admin/get-index-stats': { req: TODO; res: TODO; };
 	'admin/get-table-stats': { req: TODO; res: TODO; };

From 3bc84e1fdbb7ec62331ba9f0be7fbd88c0ffbb24 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Wed, 8 Nov 2023 01:39:08 +0900
Subject: [PATCH 28/36] =?UTF-8?q?enhance(frontend):=20PullToRefresh?=
 =?UTF-8?q?=E3=81=AE=E6=94=B9=E5=96=84=20(MisskeyIO#218)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 .../src/components/MkPullToRefresh.vue        | 57 ++++++++++---------
 1 file changed, 29 insertions(+), 28 deletions(-)

diff --git a/packages/frontend/src/components/MkPullToRefresh.vue b/packages/frontend/src/components/MkPullToRefresh.vue
index 4477864e4c..99a53b970e 100644
--- a/packages/frontend/src/components/MkPullToRefresh.vue
+++ b/packages/frontend/src/components/MkPullToRefresh.vue
@@ -31,6 +31,7 @@ import { getScrollContainer } from '@/scripts/scroll.js';
 const SCROLL_STOP = 10;
 const MAX_PULL_DISTANCE = Infinity;
 const FIRE_THRESHOLD = 230;
+const FIRE_THRESHOLD_RATIO = 1.1;
 const RELEASE_TRANSITION_DURATION = 200;
 const PULL_BRAKE_BASE = 1.5;
 const PULL_BRAKE_FACTOR = 170;
@@ -39,9 +40,11 @@ let isPullStart = $ref(false);
 let isPullEnd = $ref(false);
 let isRefreshing = $ref(false);
 let pullDistance = $ref(0);
+let moveRatio = $ref(0);
 
 let supportPointerDesktop = false;
 let startScreenY: number | null = null;
+let startClientX: number | null = null;
 
 const rootEl = $shallowRef<HTMLDivElement>();
 let scrollEl: HTMLElement | null = null;
@@ -63,11 +66,20 @@ function getScreenY(event) {
 	return event.touches[0].screenY;
 }
 
+function getClientX(event) {
+	if (supportPointerDesktop) {
+		return event.clientX;
+	}
+	return event.touches[0].clientX;
+}
+
 function moveStart(event) {
-	if (!isPullStart && !isRefreshing && !disabled) {
+	if (!isPullStart && !isRefreshing && !disabled && scrollEl?.scrollTop === 0) {
 		isPullStart = true;
 		startScreenY = getScreenY(event);
+		startClientX = getClientX(event);
 		pullDistance = 0;
+		moveRatio = 0;
 	}
 }
 
@@ -110,6 +122,7 @@ async function closeContent() {
 function moveEnd() {
 	if (isPullStart && !isRefreshing) {
 		startScreenY = null;
+		startClientX = null;
 		if (isPullEnd) {
 			isPullEnd = false;
 			isRefreshing = true;
@@ -126,6 +139,7 @@ function moveEnd() {
 }
 
 function moving(event: TouchEvent | PointerEvent) {
+	if (!isPullStart && scrollEl?.scrollTop === 0) moveStart(event);
 	if (!isPullStart || isRefreshing || disabled) return;
 
 	if ((scrollEl?.scrollTop ?? 0) > (supportPointerDesktop ? SCROLL_STOP : SCROLL_STOP + pullDistance)) {
@@ -135,19 +149,23 @@ function moving(event: TouchEvent | PointerEvent) {
 		return;
 	}
 
-	if (startScreenY === null) {
+	if (startScreenY === null || startClientX === null) {
 		startScreenY = getScreenY(event);
+		startClientX = getClientX(event);
 	}
 	const moveScreenY = getScreenY(event);
+	const moveClientX = getClientX(event);
 
 	const moveHeight = moveScreenY - startScreenY!;
+	const moveWidth = moveClientX - startClientX!;
 	pullDistance = Math.min(Math.max(moveHeight, 0), MAX_PULL_DISTANCE);
+	moveRatio = Math.max(Math.abs(moveHeight), 1) / Math.max(Math.abs(moveWidth), 1);
 
-	if (pullDistance > 0) {
+	if (pullDistance > 0 && moveRatio > FIRE_THRESHOLD_RATIO) {
 		if (event.cancelable) event.preventDefault();
 	}
 
-	isPullEnd = pullDistance >= FIRE_THRESHOLD;
+	isPullEnd = pullDistance >= FIRE_THRESHOLD && moveRatio > FIRE_THRESHOLD_RATIO;
 }
 
 /**
@@ -167,47 +185,30 @@ function setDisabled(value) {
 }
 
 function onScrollContainerScroll() {
-	const scrollPos = scrollEl!.scrollTop;
-
 	// When at the top of the page, disable vertical overscroll so passive touch listeners can take over.
-	if (scrollPos === 0) {
+	if (scrollEl?.scrollTop === 0) {
 		scrollEl!.style.touchAction = 'pan-x pan-down pinch-zoom';
-		registerEventListenersForReadyToPull();
 	} else {
 		scrollEl!.style.touchAction = 'auto';
-		unregisterEventListenersForReadyToPull();
 	}
 }
 
-function registerEventListenersForReadyToPull() {
-	if (rootEl == null) return;
-	rootEl.addEventListener('touchstart', moveStart, { passive: true });
-	rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
-}
-
-function unregisterEventListenersForReadyToPull() {
-	if (rootEl == null) return;
-	rootEl.removeEventListener('touchstart', moveStart);
-	rootEl.removeEventListener('touchmove', moving);
-}
-
 onMounted(() => {
 	if (rootEl == null) return;
-
 	scrollEl = getScrollContainer(rootEl);
 	if (scrollEl == null) return;
-
 	scrollEl.addEventListener('scroll', onScrollContainerScroll, { passive: true });
-
+	rootEl.addEventListener('touchstart', moveStart, { passive: true });
+	rootEl.addEventListener('touchmove', moving, { passive: false }); // passive: falseにしないとpreventDefaultが使えない
 	rootEl.addEventListener('touchend', moveEnd, { passive: true });
-
-	registerEventListenersForReadyToPull();
 });
 
 onUnmounted(() => {
 	if (scrollEl) scrollEl.removeEventListener('scroll', onScrollContainerScroll);
-
-	unregisterEventListenersForReadyToPull();
+	if (rootEl == null) return;
+	rootEl.removeEventListener('touchstart', moveStart);
+	rootEl.removeEventListener('touchmove', moving);
+	rootEl.removeEventListener('touchend', moveEnd);
 });
 
 defineExpose({

From 0ecda3d386305b6189da631bd794830d69364aad Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Wed, 8 Nov 2023 01:39:44 +0900
Subject: [PATCH 29/36] Bump up version to 13.14.2-io.14d (MisskeyIO#219)

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 3d9fdb3c16..70b2e81fbe 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.14.2-io.14c",
+	"version": "13.14.2-io.14d",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",

From 29f6267aa54add6782b1f74d9b684446b8a1cd02 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Thu, 9 Nov 2023 00:03:34 +0900
Subject: [PATCH 30/36] =?UTF-8?q?fix(frontend):=20=E3=83=A2=E3=83=90?=
 =?UTF-8?q?=E3=82=A4=E3=83=AB=E7=92=B0=E5=A2=83=E3=81=A7=E3=82=BF=E3=82=A4?=
 =?UTF-8?q?=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3=E3=81=AE=E4=B8=8A=E6=AE=B5?=
 =?UTF-8?q?=E3=83=A1=E3=83=8B=E3=83=A5=E3=83=BC=E3=81=AB=E7=A9=BA=E3=81=AE?=
 =?UTF-8?q?=E3=83=9C=E3=82=BF=E3=83=B3=E3=81=8C=E8=BF=BD=E5=8A=A0=E3=81=95?=
 =?UTF-8?q?=E3=82=8C=E3=81=A6=E3=81=84=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92?=
 =?UTF-8?q?=E4=BF=AE=E6=AD=A3=20(MisskeyIO#224)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/pages/timeline.vue | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index f16dcd66ed..09e2a6e4b9 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -122,13 +122,14 @@ function focus(): void {
 	tlComponent.focus();
 }
 
-const headerActions = $computed(() => [
-	...[deviceKind === 'desktop' ? {
+const headerActions = $computed(() => deviceKind === 'desktop'
+	? [{
 		icon: 'ti ti-refresh',
 		text: i18n.ts.reload,
 		handler: () => { tlComponent.reloadTimeline(); },
-	} : {}],
-]);
+	}]
+	: []
+);
 
 const headerTabs = $computed(() => [{
 	key: 'home',

From 5421f4f377aed8fe32b8d21ce17b17392933f502 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Thu, 9 Nov 2023 01:30:36 +0900
Subject: [PATCH 31/36] =?UTF-8?q?enhance(frontend):=20=E7=B5=B5=E6=96=87?=
 =?UTF-8?q?=E5=AD=97=E3=83=94=E3=83=83=E3=82=AB=E3=83=BC=E3=81=A7=E9=95=B7?=
 =?UTF-8?q?=E3=81=84=E3=82=AB=E3=83=86=E3=82=B4=E3=83=AA=E3=81=AE=E5=90=8D?=
 =?UTF-8?q?=E5=89=8D=E3=82=92=E5=89=8D=E3=81=AE=E9=83=A8=E5=88=86=E3=81=8B?=
 =?UTF-8?q?=E3=82=89=E7=9C=81=E7=95=A5=E3=81=97=E3=81=A6=E8=A1=A8=E7=A4=BA?=
 =?UTF-8?q?=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(MisskeyIO#223)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkEmojiPicker.vue | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index df74571415..87224b1928 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -615,11 +615,17 @@ defineExpose({
 				position: sticky;
 				top: 0;
 				left: 0;
-				line-height: 28px;
+				height: 32px;
+				line-height: 32px;
 				z-index: 1;
 				padding: 0 8px;
 				font-size: 12px;
 				cursor: pointer;
+				white-space: nowrap;
+				overflow: hidden;
+				text-overflow: ellipsis;
+				direction: rtl;
+				text-align: left;
 
 				&:hover {
 					color: var(--accent);

From 6c003041996de5779e273e05441fc66d50a404f5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Thu, 9 Nov 2023 02:41:48 +0900
Subject: [PATCH 32/36] =?UTF-8?q?enhance(frontend):=20URL=E3=83=97?=
 =?UTF-8?q?=E3=83=AC=E3=83=93=E3=83=A5=E3=83=BC=E3=81=AE=E3=82=B5=E3=83=A0?=
 =?UTF-8?q?=E3=83=8D=E3=82=A4=E3=83=AB=E3=82=92=E9=9A=A0=E3=81=99=E6=A9=9F?=
 =?UTF-8?q?=E8=83=BD=E3=81=AEblur=E5=8A=B9=E6=9E=9C=E3=81=8C=E3=81=AF?=
 =?UTF-8?q?=E3=81=BF=E5=87=BA=E3=81=A6=E3=81=97=E3=81=BE=E3=81=86=E5=95=8F?=
 =?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(MisskeyIO#227)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 packages/frontend/src/components/MkUrlPreview.vue | 1 +
 1 file changed, 1 insertion(+)

diff --git a/packages/frontend/src/components/MkUrlPreview.vue b/packages/frontend/src/components/MkUrlPreview.vue
index a40957786b..3e27218c4e 100644
--- a/packages/frontend/src/components/MkUrlPreview.vue
+++ b/packages/frontend/src/components/MkUrlPreview.vue
@@ -323,6 +323,7 @@ onUnmounted(() => {
 
 .thumbnailBlur {
 		filter: blur(8px);
+		clip-path: inset(0);
 }
 
 @container (max-width: 400px) {

From 5a85d06571050565ca5cc61d1357a083f96d544b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Thu, 9 Nov 2023 02:43:24 +0900
Subject: [PATCH 33/36] =?UTF-8?q?misc(GitHub=20Actions):=20io=E3=81=AB?=
 =?UTF-8?q?=E4=B8=8D=E8=A6=81=E3=81=AAGitHub=20Actions=E3=81=AEWorkflow?=
 =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92=E5=89=8A=E9=99=A4?=
 =?UTF-8?q?=20(MisskeyIO#225)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Dockle 公式のactionを使うように

Co-authored-by: riku6460 <17585784+riku6460@users.noreply.github.com>
---
 .github/workflows/docker-develop.yml     | 43 -----------
 .github/workflows/docker.yml             | 49 -------------
 .github/workflows/dockle.yml             | 36 +++++-----
 .github/workflows/ok-to-test.yml         | 36 ----------
 .github/workflows/pr-preview-deploy.yml  | 92 ------------------------
 .github/workflows/pr-preview-destroy.yml | 54 --------------
 6 files changed, 19 insertions(+), 291 deletions(-)
 delete mode 100644 .github/workflows/docker-develop.yml
 delete mode 100644 .github/workflows/docker.yml
 delete mode 100644 .github/workflows/ok-to-test.yml
 delete mode 100644 .github/workflows/pr-preview-deploy.yml
 delete mode 100644 .github/workflows/pr-preview-destroy.yml

diff --git a/.github/workflows/docker-develop.yml b/.github/workflows/docker-develop.yml
deleted file mode 100644
index 09a2c33e0c..0000000000
--- a/.github/workflows/docker-develop.yml
+++ /dev/null
@@ -1,43 +0,0 @@
-name: Publish Docker image (develop)
-
-on:
-  push:
-    branches:
-      - develop
-  workflow_dispatch:
-
-jobs:
-  push_to_registry:
-    name: Push Docker image to Docker Hub
-    runs-on: ubuntu-latest
-    if: github.repository == 'misskey-dev/misskey'
-    steps:
-      - name: Check out the repo
-        uses: actions/checkout@v3.3.0
-      - name: Set up Docker Buildx
-        id: buildx
-        uses: docker/setup-buildx-action@v2.3.0
-        with:
-          platforms: linux/amd64,linux/arm64
-      - name: Docker meta
-        id: meta
-        uses: docker/metadata-action@v4
-        with:
-          images: misskey/misskey
-      - name: Log in to Docker Hub
-        uses: docker/login-action@v2
-        with:
-          username: ${{ secrets.DOCKER_USERNAME }}
-          password: ${{ secrets.DOCKER_PASSWORD }}
-      - name: Build and Push to Docker Hub
-        uses: docker/build-push-action@v4
-        with:
-          builder: ${{ steps.buildx.outputs.name }}
-          context: .
-          push: true
-          platforms: ${{ steps.buildx.outputs.platforms }}
-          provenance: false
-          tags: misskey/misskey:develop
-          labels: develop
-          cache-from: type=gha
-          cache-to: type=gha,mode=max
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
deleted file mode 100644
index a465d92eaf..0000000000
--- a/.github/workflows/docker.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-name: Publish Docker image
-
-on:
-  release:
-    types: [published]
-  workflow_dispatch:
-
-jobs:
-  push_to_registry:
-    name: Push Docker image to Docker Hub
-    runs-on: ubuntu-latest
-
-    steps:
-      - name: Check out the repo
-        uses: actions/checkout@v3.3.0
-      - name: Set up Docker Buildx
-        id: buildx
-        uses: docker/setup-buildx-action@v2.3.0
-        with:
-          platforms: linux/amd64,linux/arm64
-      - name: Docker meta
-        id: meta
-        uses: docker/metadata-action@v4
-        with:
-          images: misskey/misskey
-          tags: |
-            type=edge
-            type=ref,event=pr
-            type=ref,event=branch
-            type=semver,pattern={{version}}
-            type=semver,pattern={{major}}.{{minor}}
-            type=semver,pattern={{major}}
-      - name: Log in to Docker Hub
-        uses: docker/login-action@v2
-        with:
-          username: ${{ secrets.DOCKER_USERNAME }}
-          password: ${{ secrets.DOCKER_PASSWORD }}
-      - name: Build and Push to Docker Hub
-        uses: docker/build-push-action@v4
-        with:
-          builder: ${{ steps.buildx.outputs.name }}
-          context: .
-          push: true
-          platforms: ${{ steps.buildx.outputs.platforms }}
-          provenance: false
-          tags: ${{ steps.meta.outputs.tags }}
-          labels: ${{ steps.meta.outputs.labels }}
-          cache-from: type=gha
-          cache-to: type=gha,mode=max
diff --git a/.github/workflows/dockle.yml b/.github/workflows/dockle.yml
index 9b79ee54f0..292e68be13 100644
--- a/.github/workflows/dockle.yml
+++ b/.github/workflows/dockle.yml
@@ -1,4 +1,3 @@
----
 name: Dockle
 
 on:
@@ -11,20 +10,23 @@ on:
 jobs:
   dockle:
     runs-on: ubuntu-latest
-    env:
-      DOCKER_CONTENT_TRUST: 1
     steps:
-      - uses: actions/checkout@v3.2.0
-      - run: |
-          curl -L -o dockle.deb "https://github.com/goodwithtech/dockle/releases/download/v0.4.10/dockle_0.4.10_Linux-64bit.deb"
-          sudo dpkg -i dockle.deb
-      - run: |
-          cp .config/docker_example.env .config/docker.env
-          cp ./docker-compose.yml.example ./docker-compose.yml
-      - run: |
-          docker compose up -d web
-          docker tag "$(docker compose images web | awk 'OFS=":" {print $4}' | tail -n +2)" misskey-web:latest
-      - run: |
-          cmd="dockle --exit-code 1 misskey-web:latest ${image_name}"
-          echo "> ${cmd}"
-          eval "${cmd}"
+      - name: Checkout code
+        uses: actions/checkout@v3
+      - name: Build an image from Dockerfile
+        uses: docker/build-push-action@v4
+        with:
+          context: .
+          push: false
+          provenance: false
+          cache-from: type=registry,ref=ghcr.io/misskeyio/misskey:io-buildcache
+          tags: |
+            misskey:scan
+      - name: Run dockle
+        uses: goodwithtech/dockle-action@main
+        with:
+          image: 'misskey:scan'
+          format: 'list'
+          exit-code: '1'
+          exit-level: 'warn'
+          ignore: 'CIS-DI-0005,CIS-DI-0010'
diff --git a/.github/workflows/ok-to-test.yml b/.github/workflows/ok-to-test.yml
deleted file mode 100644
index 87af3a6ba6..0000000000
--- a/.github/workflows/ok-to-test.yml
+++ /dev/null
@@ -1,36 +0,0 @@
-# If someone with write access comments "/ok-to-test" on a pull request, emit a repository_dispatch event
-name: Ok To Test
-
-on:
-  issue_comment:
-    types: [created]
-
-jobs:
-  ok-to-test:
-    runs-on: ubuntu-latest
-    # Only run for PRs, not issue comments
-    if: ${{ github.event.issue.pull_request }}
-    steps:
-    # Generate a GitHub App installation access token from an App ID and private key
-    # To create a new GitHub App:
-    #   https://developer.github.com/apps/building-github-apps/creating-a-github-app/
-    # See app.yml for an example app manifest
-    - name: Generate token
-      id: generate_token
-      uses: tibdex/github-app-token@v1
-      with:
-        app_id: ${{ secrets.DEPLOYBOT_APP_ID }}
-        private_key: ${{ secrets.DEPLOYBOT_PRIVATE_KEY }}
-
-    - name: Slash Command Dispatch
-      uses: peter-evans/slash-command-dispatch@v1
-      env:
-        TOKEN: ${{ steps.generate_token.outputs.token }}
-      with:
-        token: ${{ env.TOKEN }} # GitHub App installation access token
-        # token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} # PAT or OAuth token will also work
-        reaction-token: ${{ secrets.GITHUB_TOKEN }}
-        issue-type: pull-request
-        commands: deploy
-        named-args: true
-        permission: write
diff --git a/.github/workflows/pr-preview-deploy.yml b/.github/workflows/pr-preview-deploy.yml
deleted file mode 100644
index 9b786d34aa..0000000000
--- a/.github/workflows/pr-preview-deploy.yml
+++ /dev/null
@@ -1,92 +0,0 @@
-# Run secret-dependent integration tests only after /deploy approval
-on:
-  repository_dispatch:
-    types: [deploy-command]
-
-name: Deploy preview environment
-
-jobs:
-  # Repo owner has commented /deploy on a (fork-based) pull request
-  deploy-preview-environment:
-    runs-on: ubuntu-latest
-    if:
-      github.event.client_payload.slash_command.sha != '' &&
-      contains(github.event.client_payload.pull_request.head.sha, github.event.client_payload.slash_command.sha)
-    steps:
-    - uses: actions/github-script@v6.3.3
-      id: check-id
-      env:
-        number: ${{ github.event.client_payload.pull_request.number }}
-        job: ${{ github.job }}
-      with:
-        github-token: ${{ secrets.GITHUB_TOKEN }}
-        result-encoding: string
-        script: |
-          const { data: pull } = await github.rest.pulls.get({
-            ...context.repo,
-            pull_number: process.env.number
-          });
-          const ref = pull.head.sha;
-
-          const { data: checks } = await github.rest.checks.listForRef({
-            ...context.repo,
-            ref
-          });
-
-          const check = checks.check_runs.filter(c => c.name === process.env.job);
-
-          return check[0].id;
-
-    - uses: actions/github-script@v6.3.3
-      env:
-        check_id: ${{ steps.check-id.outputs.result }}
-        details_url: ${{ github.server_url }}/${{ github.repository }}/runs/${{ github.run_id }}
-      with:
-        github-token: ${{ secrets.GITHUB_TOKEN }}
-        script: |
-          await github.rest.checks.update({
-            ...context.repo,
-            check_run_id: process.env.check_id,
-            status: 'in_progress',
-            details_url: process.env.details_url
-          });
-
-    # Check out merge commit
-    - name: Fork based /deploy checkout
-      uses: actions/checkout@v3.3.0
-      with:
-        ref: 'refs/pull/${{ github.event.client_payload.pull_request.number }}/merge'
-
-    # <insert integration tests needing secrets>
-    - name: Context
-      uses: okteto/context@latest
-      with:
-        token: ${{ secrets.OKTETO_TOKEN }}
-
-    - name: Deploy preview environment
-      uses: ikuradon/deploy-preview@latest
-      env:
-        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
-      with:
-        name: pr-${{ github.event.client_payload.pull_request.number }}-syuilo
-        timeout: 15m
-
-    # Update check run called "integration-fork"
-    - uses: actions/github-script@v6.3.3
-      id: update-check-run
-      if: ${{ always() }}
-      env:
-        # Conveniently, job.status maps to https://developer.github.com/v3/checks/runs/#update-a-check-run
-        conclusion: ${{ job.status }}
-        check_id: ${{ steps.check-id.outputs.result }}
-      with:
-        github-token: ${{ secrets.GITHUB_TOKEN }}
-        script: |
-          const { data: result } = await github.rest.checks.update({
-            ...context.repo,
-            check_run_id: process.env.check_id,
-            status: 'completed',
-            conclusion: process.env.conclusion
-          });
-
-          return result;
diff --git a/.github/workflows/pr-preview-destroy.yml b/.github/workflows/pr-preview-destroy.yml
deleted file mode 100644
index 8adfad9dab..0000000000
--- a/.github/workflows/pr-preview-destroy.yml
+++ /dev/null
@@ -1,54 +0,0 @@
-# file: .github/workflows/preview-closed.yaml
-on:
-  pull_request:
-    types:
-      - closed
-
-name: Destroy preview environment
-
-jobs:
-  destroy-preview-environment:
-    runs-on: ubuntu-latest
-    steps:
-      - uses: actions/github-script@v6.3.3
-        id: check-conclusion
-        env:
-          number: ${{ github.event.number }}
-        with:
-          github-token: ${{ secrets.GITHUB_TOKEN }}
-          result-encoding: string
-          script: |
-            const { data: pull } = await github.rest.pulls.get({
-              ...context.repo,
-              pull_number: process.env.number
-            });
-            const ref = pull.head.sha;
-
-            const { data: checks } = await github.rest.checks.listForRef({
-              ...context.repo,
-              ref
-            });
-
-            const check = checks.check_runs.filter(c => c.name === 'deploy-preview-environment');
-
-            if (check.length === 0) {
-              return;
-            }
-
-            const { data: result } = await github.rest.checks.get({
-              ...context.repo,
-              check_run_id: check[0].id,
-            });
-
-            return result.conclusion;
-      - name: Context
-        if: steps.check-conclusion.outputs.result == 'success'
-        uses: okteto/context@latest
-        with:
-          token: ${{ secrets.OKTETO_TOKEN }}
-
-      - name: Destroy preview environment
-        if: steps.check-conclusion.outputs.result == 'success'
-        uses: okteto/destroy-preview@latest
-        with:
-          name: pr-${{ github.event.number }}-syuilo

From 375c725a44d7393ba0b68438fe78834d6734486e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Thu, 9 Nov 2023 02:45:46 +0900
Subject: [PATCH 34/36] Bump up version to 13.14.2-io.15 (MisskeyIO#226)

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 70b2e81fbe..caa9285473 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.14.2-io.14d",
+	"version": "13.14.2-io.15",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",

From 60f1ec86f846091e29c01434e4ae3e2940ee73e7 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Thu, 9 Nov 2023 04:41:47 +0900
Subject: [PATCH 35/36] =?UTF-8?q?Revert=20"enhance(frontend):=20=E7=B5=B5?=
 =?UTF-8?q?=E6=96=87=E5=AD=97=E3=83=94=E3=83=83=E3=82=AB=E3=83=BC=E3=81=A7?=
 =?UTF-8?q?=E9=95=B7=E3=81=84=E3=82=AB=E3=83=86=E3=82=B4=E3=83=AA=E3=81=AE?=
 =?UTF-8?q?=E5=90=8D=E5=89=8D=E3=82=92=E5=89=8D=E3=81=AE=E9=83=A8=E5=88=86?=
 =?UTF-8?q?=E3=81=8B=E3=82=89=E7=9C=81=E7=95=A5=E3=81=97=E3=81=A6=E8=A1=A8?=
 =?UTF-8?q?=E7=A4=BA=E3=81=99=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(Missk?=
 =?UTF-8?q?eyIO#223)"=20(MisskeyIO#228)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This reverts commit 5421f4f377aed8fe32b8d21ce17b17392933f502.
---
 packages/frontend/src/components/MkEmojiPicker.vue | 8 +-------
 1 file changed, 1 insertion(+), 7 deletions(-)

diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 87224b1928..df74571415 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -615,17 +615,11 @@ defineExpose({
 				position: sticky;
 				top: 0;
 				left: 0;
-				height: 32px;
-				line-height: 32px;
+				line-height: 28px;
 				z-index: 1;
 				padding: 0 8px;
 				font-size: 12px;
 				cursor: pointer;
-				white-space: nowrap;
-				overflow: hidden;
-				text-overflow: ellipsis;
-				direction: rtl;
-				text-align: left;
 
 				&:hover {
 					color: var(--accent);

From 85c15bfee07be1d4d75004a18dcae3d8a7ed7d29 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=BE=E3=81=A3=E3=81=A1=E3=82=83=E3=81=A8=E3=83=BC?=
 =?UTF-8?q?=E3=81=AB=E3=82=85?=
 <17376330+u1-liquid@users.noreply.github.com>
Date: Thu, 9 Nov 2023 04:42:14 +0900
Subject: [PATCH 36/36] Bump up version to 13.14.2-io.15a (MisskeyIO#229)

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index caa9285473..f7f9c10b4a 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
 	"name": "misskey",
-	"version": "13.14.2-io.15",
+	"version": "13.14.2-io.15a",
 	"codename": "nasubi",
 	"repository": {
 		"type": "git",