};
type ObjectSchemaTypeDef =
p['ref'] extends keyof typeof refs ? Packed
:
@@ -232,6 +235,12 @@ export type SchemaTypeDef
=
p['items']['allOf'] extends ReadonlyArray ? UnionToIntersection>>[] :
never
) :
+ p['prefixItems'] extends ReadonlyArray ? (
+ p['items'] extends NonNullable ? [...ArrayToTuple, ...SchemaType
[]] :
+ p['items'] extends false ? ArrayToTuple
:
+ p['unevaluatedItems'] extends false ? ArrayToTuple
:
+ [...ArrayToTuple
, ...unknown[]]
+ ) :
p['items'] extends NonNullable ? SchemaType[] :
any[]
) :
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
index 7a3410ffa751..f3e440b4cb51 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/deliver-delayed.ts
@@ -21,16 +21,15 @@ export const meta = {
items: {
type: 'array',
optional: false, nullable: false,
- items: {
- anyOf: [
- {
- type: 'string',
- },
- {
- type: 'number',
- },
- ],
- },
+ prefixItems: [
+ {
+ type: 'string',
+ },
+ {
+ type: 'number',
+ },
+ ],
+ unevaluatedItems: false,
},
example: [[
'example.com',
diff --git a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
index 305ae1af1da5..e7589cba8112 100644
--- a/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
+++ b/packages/backend/src/server/api/endpoints/admin/queue/inbox-delayed.ts
@@ -21,16 +21,15 @@ export const meta = {
items: {
type: 'array',
optional: false, nullable: false,
- items: {
- anyOf: [
- {
- type: 'string',
- },
- {
- type: 'number',
- },
- ],
- },
+ prefixItems: [
+ {
+ type: 'string',
+ },
+ {
+ type: 'number',
+ },
+ ],
+ unevaluatedItems: false,
},
example: [[
'example.com',
diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue
index c7478f252a26..fb190f53258b 100644
--- a/packages/frontend/src/pages/admin/overview.queue.vue
+++ b/packages/frontend/src/pages/admin/overview.queue.vue
@@ -36,7 +36,9 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
From cdb0566c5b823f0ce4ecc493bd459cb726431be2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 6 Sep 2024 16:12:14 +0900
Subject: [PATCH 017/222] =?UTF-8?q?refactor(frontend):=20scss=20deprecated?=
=?UTF-8?q?=20=E8=AD=A6=E5=91=8A=E3=81=AB=E5=AF=BE=E5=BF=9C=20(#14513)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../frontend/src/components/MkModalWindow.vue | 6 +++---
packages/frontend/src/components/MkSuperMenu.vue | 6 +++---
packages/frontend/src/components/MkWindow.vue | 8 ++++----
.../frontend/src/pages/admin/overview.users.vue | 8 ++++----
packages/frontend/src/pages/page.vue | 3 +--
packages/frontend/src/style.scss | 8 ++++----
packages/frontend/src/ui/_common_/statusbars.vue | 16 ++++++++--------
packages/frontend/src/ui/deck/column.vue | 6 +++---
8 files changed, 30 insertions(+), 31 deletions(-)
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index c3c781203621..f26959888b42 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -94,12 +94,12 @@ defineExpose({
--root-margin: 24px;
+ --headerHeight: 46px;
+ --headerHeightNarrow: 42px;
+
@media (max-width: 500px) {
--root-margin: 16px;
}
-
- --headerHeight: 46px;
- --headerHeightNarrow: 42px;
}
.header {
diff --git a/packages/frontend/src/components/MkSuperMenu.vue b/packages/frontend/src/components/MkSuperMenu.vue
index 1a880170bec3..3746ffd8f3e9 100644
--- a/packages/frontend/src/components/MkSuperMenu.vue
+++ b/packages/frontend/src/components/MkSuperMenu.vue
@@ -100,14 +100,14 @@ defineProps<{
&.grid {
> .group {
+ margin-left: 0;
+ margin-right: 0;
+
& + .group {
padding-top: 0;
border-top: none;
}
- margin-left: 0;
- margin-right: 0;
-
> .title {
font-size: 1em;
opacity: 0.7;
diff --git a/packages/frontend/src/components/MkWindow.vue b/packages/frontend/src/components/MkWindow.vue
index 303e49de00fa..26ba598498d7 100644
--- a/packages/frontend/src/components/MkWindow.vue
+++ b/packages/frontend/src/components/MkWindow.vue
@@ -508,10 +508,6 @@ defineExpose({
.header {
--height: 39px;
- &.mini {
- --height: 32px;
- }
-
display: flex;
position: relative;
z-index: 1;
@@ -524,6 +520,10 @@ defineExpose({
//border-bottom: solid 1px var(--divider);
font-size: 90%;
font-weight: bold;
+
+ &.mini {
+ --height: 32px;
+ }
}
.headerButton {
diff --git a/packages/frontend/src/pages/admin/overview.users.vue b/packages/frontend/src/pages/admin/overview.users.vue
index 408be88d4792..a7dd4c0a485f 100644
--- a/packages/frontend/src/pages/admin/overview.users.vue
+++ b/packages/frontend/src/pages/admin/overview.users.vue
@@ -47,14 +47,14 @@ useInterval(fetch, 1000 * 60, {
.root {
&:global {
> .users {
- .chart-move {
- transition: transform 1s ease;
- }
-
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
grid-gap: 12px;
+ .chart-move {
+ transition: transform 1s ease;
+ }
+
> .user:hover {
text-decoration: none;
}
diff --git a/packages/frontend/src/pages/page.vue b/packages/frontend/src/pages/page.vue
index cb1ce9b91848..7ae61236e85f 100644
--- a/packages/frontend/src/pages/page.vue
+++ b/packages/frontend/src/pages/page.vue
@@ -433,13 +433,12 @@ definePageMetadata(() => ({
.pageBannerTitleUser {
--height: 32px;
flex-shrink: 0;
+ line-height: var(--height);
.avatar {
height: var(--height);
width: var(--height);
}
-
- line-height: var(--height);
}
.pageBannerTitleSubActions {
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 44ef740a2eb1..caaf9fca6fda 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -17,10 +17,6 @@
--minBottomSpacingMobile: calc(72px + max(12px, env(safe-area-inset-bottom, 0px)));
--minBottomSpacing: var(--minBottomSpacingMobile);
- @media (max-width: 500px) {
- --margin: var(--marginHalf);
- }
-
//--ad: rgb(255 169 0 / 10%);
--eventFollow: #36aed2;
--eventRenote: #36d298;
@@ -29,6 +25,10 @@
--eventReaction: #e99a0b;
--eventAchievement: #cb9a11;
--eventOther: #88a6b7;
+
+ @media (max-width: 500px) {
+ --margin: var(--marginHalf);
+ }
}
::selection {
diff --git a/packages/frontend/src/ui/_common_/statusbars.vue b/packages/frontend/src/ui/_common_/statusbars.vue
index 872c69810c75..690366307b1c 100644
--- a/packages/frontend/src/ui/_common_/statusbars.vue
+++ b/packages/frontend/src/ui/_common_/statusbars.vue
@@ -40,6 +40,14 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
--nameMargin: 10px;
font-size: 0.85em;
+ display: flex;
+ vertical-align: bottom;
+ width: 100%;
+ line-height: var(--height);
+ height: var(--height);
+ overflow: clip;
+ contain: strict;
+
&.verySmall {
--nameMargin: 7px;
--height: 16px;
@@ -64,14 +72,6 @@ const XUserList = defineAsyncComponent(() => import('./statusbar-user-list.vue')
font-size: 0.9em;
}
- display: flex;
- vertical-align: bottom;
- width: 100%;
- line-height: var(--height);
- height: var(--height);
- overflow: clip;
- contain: strict;
-
&.black {
background: #000;
color: #fff;
diff --git a/packages/frontend/src/ui/deck/column.vue b/packages/frontend/src/ui/deck/column.vue
index e96402d13bb8..893301122e0a 100644
--- a/packages/frontend/src/ui/deck/column.vue
+++ b/packages/frontend/src/ui/deck/column.vue
@@ -324,11 +324,11 @@ function onDrop(ev) {
> .body {
background: transparent !important;
+ scrollbar-color: var(--scrollbarHandle) transparent;
&::-webkit-scrollbar-track {
background: transparent;
}
- scrollbar-color: var(--scrollbarHandle) transparent;
}
}
@@ -338,11 +338,11 @@ function onDrop(ev) {
> .body {
background: var(--bg) !important;
overflow-y: scroll !important;
+ scrollbar-color: var(--scrollbarHandle) transparent;
&::-webkit-scrollbar-track {
background: inherit;
}
- scrollbar-color: var(--scrollbarHandle) transparent;
}
}
}
@@ -423,10 +423,10 @@ function onDrop(ev) {
box-sizing: border-box;
container-type: size;
background-color: var(--bg);
+ scrollbar-color: var(--scrollbarHandle) var(--panel);
&::-webkit-scrollbar-track {
background: var(--panel);
}
- scrollbar-color: var(--scrollbarHandle) var(--panel);
}
From 8d19bdbb65c79e2425bf5c73fd8b6310670a8c10 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 6 Sep 2024 17:22:45 +0900
Subject: [PATCH 018/222] =?UTF-8?q?fix(misskey-js):=20content-type?=
=?UTF-8?q?=E3=81=AFapplication/json=E3=81=A7=E3=81=AA=E3=81=84=E3=82=82?=
=?UTF-8?q?=E3=81=AE=E3=81=AE=E3=81=BF=E3=82=92=E8=A8=98=E9=8C=B2=E3=81=99?=
=?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#14508)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../misskey-js/generator/src/generator.ts | 21 +-
packages/misskey-js/src/api.ts | 9 +-
packages/misskey-js/src/autogen/endpoint.ts | 385 +-----------------
3 files changed, 22 insertions(+), 393 deletions(-)
diff --git a/packages/misskey-js/generator/src/generator.ts b/packages/misskey-js/generator/src/generator.ts
index 4ae00a4522db..88f2ae9ee96e 100644
--- a/packages/misskey-js/generator/src/generator.ts
+++ b/packages/misskey-js/generator/src/generator.ts
@@ -96,15 +96,11 @@ async function generateEndpoints(
endpoint.request = req;
const reqType = new EndpointReqMediaType(path, req);
- endpointReqMediaTypesSet.add(reqType.getMediaType());
- endpointReqMediaTypes.push(reqType);
- } else {
- endpointReqMediaTypesSet.add('application/json');
- endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json'));
+ if (reqType.getMediaType() !== 'application/json') {
+ endpointReqMediaTypesSet.add(reqType.getMediaType());
+ endpointReqMediaTypes.push(reqType);
+ }
}
- } else {
- endpointReqMediaTypesSet.add('application/json');
- endpointReqMediaTypes.push(new EndpointReqMediaType(path, undefined, 'application/json'));
}
if (operation.responses && isResponseObject(operation.responses['200']) && operation.responses['200'].content) {
@@ -158,16 +154,19 @@ async function generateEndpoints(
endpointOutputLine.push('');
function generateEndpointReqMediaTypesType() {
- return `Record `'${t}'`).join(' | ')}>`;
+ return `{ [K in keyof Endpoints]?: ${[...endpointReqMediaTypesSet].map((t) => `'${t}'`).join(' | ')}; }`;
}
- endpointOutputLine.push(`export const endpointReqTypes: ${generateEndpointReqMediaTypesType()} = {`);
+ endpointOutputLine.push(`/**
+ * NOTE: The content-type for all endpoints not listed here is application/json.
+ */`);
+ endpointOutputLine.push('export const endpointReqTypes = {');
endpointOutputLine.push(
...endpointReqMediaTypes.map(it => '\t' + it.toLine()),
);
- endpointOutputLine.push('};');
+ endpointOutputLine.push(`} as const satisfies ${generateEndpointReqMediaTypesType()};`);
endpointOutputLine.push('');
await writeFile(endpointOutputPath, endpointOutputLine.join('\n'));
diff --git a/packages/misskey-js/src/api.ts b/packages/misskey-js/src/api.ts
index ea1df57f3d77..659a29a221bb 100644
--- a/packages/misskey-js/src/api.ts
+++ b/packages/misskey-js/src/api.ts
@@ -56,6 +56,10 @@ export class APIClient {
return obj !== null && typeof obj === 'object' && !Array.isArray(obj);
}
+ private assertSpecialEpReqType(ep: keyof Endpoints): ep is keyof typeof endpointReqTypes {
+ return ep in endpointReqTypes;
+ }
+
public request(
endpoint: E,
params: P = {} as P,
@@ -63,9 +67,10 @@ export class APIClient {
): Promise> {
return new Promise((resolve, reject) => {
let mediaType = 'application/json';
- if (endpoint in endpointReqTypes) {
+ if (this.assertSpecialEpReqType(endpoint) && endpointReqTypes[endpoint] != null) {
mediaType = endpointReqTypes[endpoint];
}
+
let payload: FormData | string = '{}';
if (mediaType === 'application/json') {
@@ -100,7 +105,7 @@ export class APIClient {
method: 'POST',
body: payload,
headers: {
- 'Content-Type': endpointReqTypes[endpoint],
+ 'Content-Type': mediaType,
},
credentials: 'omit',
cache: 'no-cache',
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index be41951e4dbe..8fbdbbb629ad 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -955,384 +955,9 @@ export type Endpoints = {
'reversi/verify': { req: ReversiVerifyRequest; res: ReversiVerifyResponse };
}
-export const endpointReqTypes: Record = {
- 'admin/meta': 'application/json',
- 'admin/abuse-user-reports': 'application/json',
- 'admin/abuse-report/notification-recipient/list': 'application/json',
- 'admin/abuse-report/notification-recipient/show': 'application/json',
- 'admin/abuse-report/notification-recipient/create': 'application/json',
- 'admin/abuse-report/notification-recipient/update': 'application/json',
- 'admin/abuse-report/notification-recipient/delete': 'application/json',
- 'admin/accounts/create': 'application/json',
- 'admin/accounts/delete': 'application/json',
- 'admin/accounts/find-by-email': 'application/json',
- 'admin/ad/create': 'application/json',
- 'admin/ad/delete': 'application/json',
- 'admin/ad/list': 'application/json',
- 'admin/ad/update': 'application/json',
- 'admin/announcements/create': 'application/json',
- 'admin/announcements/delete': 'application/json',
- 'admin/announcements/list': 'application/json',
- 'admin/announcements/update': 'application/json',
- 'admin/avatar-decorations/create': 'application/json',
- 'admin/avatar-decorations/delete': 'application/json',
- 'admin/avatar-decorations/list': 'application/json',
- 'admin/avatar-decorations/update': 'application/json',
- 'admin/delete-all-files-of-a-user': 'application/json',
- 'admin/unset-user-avatar': 'application/json',
- 'admin/unset-user-banner': 'application/json',
- 'admin/drive/clean-remote-files': 'application/json',
- 'admin/drive/cleanup': 'application/json',
- 'admin/drive/files': 'application/json',
- 'admin/drive/show-file': 'application/json',
- 'admin/emoji/add-aliases-bulk': 'application/json',
- 'admin/emoji/add': 'application/json',
- 'admin/emoji/copy': 'application/json',
- 'admin/emoji/delete-bulk': 'application/json',
- 'admin/emoji/delete': 'application/json',
- 'admin/emoji/import-zip': 'application/json',
- 'admin/emoji/list-remote': 'application/json',
- 'admin/emoji/list': 'application/json',
- 'admin/emoji/remove-aliases-bulk': 'application/json',
- 'admin/emoji/set-aliases-bulk': 'application/json',
- 'admin/emoji/set-category-bulk': 'application/json',
- 'admin/emoji/set-license-bulk': 'application/json',
- 'admin/emoji/update': 'application/json',
- 'admin/federation/delete-all-files': 'application/json',
- 'admin/federation/refresh-remote-instance-metadata': 'application/json',
- 'admin/federation/remove-all-following': 'application/json',
- 'admin/federation/update-instance': 'application/json',
- 'admin/get-index-stats': 'application/json',
- 'admin/get-table-stats': 'application/json',
- 'admin/get-user-ips': 'application/json',
- 'admin/invite/create': 'application/json',
- 'admin/invite/list': 'application/json',
- 'admin/promo/create': 'application/json',
- 'admin/queue/clear': 'application/json',
- 'admin/queue/deliver-delayed': 'application/json',
- 'admin/queue/inbox-delayed': 'application/json',
- 'admin/queue/promote': 'application/json',
- 'admin/queue/stats': 'application/json',
- 'admin/relays/add': 'application/json',
- 'admin/relays/list': 'application/json',
- 'admin/relays/remove': 'application/json',
- 'admin/reset-password': 'application/json',
- 'admin/resolve-abuse-user-report': 'application/json',
- 'admin/send-email': 'application/json',
- 'admin/server-info': 'application/json',
- 'admin/show-moderation-logs': 'application/json',
- 'admin/show-user': 'application/json',
- 'admin/show-users': 'application/json',
- 'admin/suspend-user': 'application/json',
- 'admin/unsuspend-user': 'application/json',
- 'admin/update-meta': 'application/json',
- 'admin/delete-account': 'application/json',
- 'admin/update-user-note': 'application/json',
- 'admin/roles/create': 'application/json',
- 'admin/roles/delete': 'application/json',
- 'admin/roles/list': 'application/json',
- 'admin/roles/show': 'application/json',
- 'admin/roles/update': 'application/json',
- 'admin/roles/assign': 'application/json',
- 'admin/roles/unassign': 'application/json',
- 'admin/roles/update-default-policies': 'application/json',
- 'admin/roles/users': 'application/json',
- 'admin/system-webhook/create': 'application/json',
- 'admin/system-webhook/delete': 'application/json',
- 'admin/system-webhook/list': 'application/json',
- 'admin/system-webhook/show': 'application/json',
- 'admin/system-webhook/update': 'application/json',
- 'announcements': 'application/json',
- 'announcements/show': 'application/json',
- 'antennas/create': 'application/json',
- 'antennas/delete': 'application/json',
- 'antennas/list': 'application/json',
- 'antennas/notes': 'application/json',
- 'antennas/show': 'application/json',
- 'antennas/update': 'application/json',
- 'ap/get': 'application/json',
- 'ap/show': 'application/json',
- 'app/create': 'application/json',
- 'app/show': 'application/json',
- 'auth/accept': 'application/json',
- 'auth/session/generate': 'application/json',
- 'auth/session/show': 'application/json',
- 'auth/session/userkey': 'application/json',
- 'blocking/create': 'application/json',
- 'blocking/delete': 'application/json',
- 'blocking/list': 'application/json',
- 'channels/create': 'application/json',
- 'channels/featured': 'application/json',
- 'channels/follow': 'application/json',
- 'channels/followed': 'application/json',
- 'channels/owned': 'application/json',
- 'channels/show': 'application/json',
- 'channels/timeline': 'application/json',
- 'channels/unfollow': 'application/json',
- 'channels/update': 'application/json',
- 'channels/favorite': 'application/json',
- 'channels/unfavorite': 'application/json',
- 'channels/my-favorites': 'application/json',
- 'channels/search': 'application/json',
- 'charts/active-users': 'application/json',
- 'charts/ap-request': 'application/json',
- 'charts/drive': 'application/json',
- 'charts/federation': 'application/json',
- 'charts/instance': 'application/json',
- 'charts/notes': 'application/json',
- 'charts/user/drive': 'application/json',
- 'charts/user/following': 'application/json',
- 'charts/user/notes': 'application/json',
- 'charts/user/pv': 'application/json',
- 'charts/user/reactions': 'application/json',
- 'charts/users': 'application/json',
- 'clips/add-note': 'application/json',
- 'clips/remove-note': 'application/json',
- 'clips/create': 'application/json',
- 'clips/delete': 'application/json',
- 'clips/list': 'application/json',
- 'clips/notes': 'application/json',
- 'clips/show': 'application/json',
- 'clips/update': 'application/json',
- 'clips/favorite': 'application/json',
- 'clips/unfavorite': 'application/json',
- 'clips/my-favorites': 'application/json',
- 'drive': 'application/json',
- 'drive/files': 'application/json',
- 'drive/files/attached-notes': 'application/json',
- 'drive/files/check-existence': 'application/json',
+/**
+ * NOTE: The content-type for all endpoints not listed here is application/json.
+ */
+export const endpointReqTypes = {
'drive/files/create': 'multipart/form-data',
- 'drive/files/delete': 'application/json',
- 'drive/files/find-by-hash': 'application/json',
- 'drive/files/find': 'application/json',
- 'drive/files/show': 'application/json',
- 'drive/files/update': 'application/json',
- 'drive/files/upload-from-url': 'application/json',
- 'drive/folders': 'application/json',
- 'drive/folders/create': 'application/json',
- 'drive/folders/delete': 'application/json',
- 'drive/folders/find': 'application/json',
- 'drive/folders/show': 'application/json',
- 'drive/folders/update': 'application/json',
- 'drive/stream': 'application/json',
- 'email-address/available': 'application/json',
- 'endpoint': 'application/json',
- 'endpoints': 'application/json',
- 'export-custom-emojis': 'application/json',
- 'federation/followers': 'application/json',
- 'federation/following': 'application/json',
- 'federation/instances': 'application/json',
- 'federation/show-instance': 'application/json',
- 'federation/update-remote-user': 'application/json',
- 'federation/users': 'application/json',
- 'federation/stats': 'application/json',
- 'following/create': 'application/json',
- 'following/delete': 'application/json',
- 'following/update': 'application/json',
- 'following/update-all': 'application/json',
- 'following/invalidate': 'application/json',
- 'following/requests/accept': 'application/json',
- 'following/requests/cancel': 'application/json',
- 'following/requests/list': 'application/json',
- 'following/requests/reject': 'application/json',
- 'gallery/featured': 'application/json',
- 'gallery/popular': 'application/json',
- 'gallery/posts': 'application/json',
- 'gallery/posts/create': 'application/json',
- 'gallery/posts/delete': 'application/json',
- 'gallery/posts/like': 'application/json',
- 'gallery/posts/show': 'application/json',
- 'gallery/posts/unlike': 'application/json',
- 'gallery/posts/update': 'application/json',
- 'get-online-users-count': 'application/json',
- 'get-avatar-decorations': 'application/json',
- 'hashtags/list': 'application/json',
- 'hashtags/search': 'application/json',
- 'hashtags/show': 'application/json',
- 'hashtags/trend': 'application/json',
- 'hashtags/users': 'application/json',
- 'i': 'application/json',
- 'i/2fa/done': 'application/json',
- 'i/2fa/key-done': 'application/json',
- 'i/2fa/password-less': 'application/json',
- 'i/2fa/register-key': 'application/json',
- 'i/2fa/register': 'application/json',
- 'i/2fa/update-key': 'application/json',
- 'i/2fa/remove-key': 'application/json',
- 'i/2fa/unregister': 'application/json',
- 'i/apps': 'application/json',
- 'i/authorized-apps': 'application/json',
- 'i/claim-achievement': 'application/json',
- 'i/change-password': 'application/json',
- 'i/delete-account': 'application/json',
- 'i/export-blocking': 'application/json',
- 'i/export-following': 'application/json',
- 'i/export-mute': 'application/json',
- 'i/export-notes': 'application/json',
- 'i/export-clips': 'application/json',
- 'i/export-favorites': 'application/json',
- 'i/export-user-lists': 'application/json',
- 'i/export-antennas': 'application/json',
- 'i/favorites': 'application/json',
- 'i/gallery/likes': 'application/json',
- 'i/gallery/posts': 'application/json',
- 'i/import-blocking': 'application/json',
- 'i/import-following': 'application/json',
- 'i/import-muting': 'application/json',
- 'i/import-user-lists': 'application/json',
- 'i/import-antennas': 'application/json',
- 'i/notifications': 'application/json',
- 'i/notifications-grouped': 'application/json',
- 'i/page-likes': 'application/json',
- 'i/pages': 'application/json',
- 'i/pin': 'application/json',
- 'i/read-all-unread-notes': 'application/json',
- 'i/read-announcement': 'application/json',
- 'i/regenerate-token': 'application/json',
- 'i/registry/get-all': 'application/json',
- 'i/registry/get-detail': 'application/json',
- 'i/registry/get': 'application/json',
- 'i/registry/keys-with-type': 'application/json',
- 'i/registry/keys': 'application/json',
- 'i/registry/remove': 'application/json',
- 'i/registry/scopes-with-domain': 'application/json',
- 'i/registry/set': 'application/json',
- 'i/revoke-token': 'application/json',
- 'i/signin-history': 'application/json',
- 'i/unpin': 'application/json',
- 'i/update-email': 'application/json',
- 'i/update': 'application/json',
- 'i/move': 'application/json',
- 'i/webhooks/create': 'application/json',
- 'i/webhooks/list': 'application/json',
- 'i/webhooks/show': 'application/json',
- 'i/webhooks/update': 'application/json',
- 'i/webhooks/delete': 'application/json',
- 'invite/create': 'application/json',
- 'invite/delete': 'application/json',
- 'invite/list': 'application/json',
- 'invite/limit': 'application/json',
- 'meta': 'application/json',
- 'emojis': 'application/json',
- 'emoji': 'application/json',
- 'miauth/gen-token': 'application/json',
- 'mute/create': 'application/json',
- 'mute/delete': 'application/json',
- 'mute/list': 'application/json',
- 'renote-mute/create': 'application/json',
- 'renote-mute/delete': 'application/json',
- 'renote-mute/list': 'application/json',
- 'my/apps': 'application/json',
- 'notes': 'application/json',
- 'notes/children': 'application/json',
- 'notes/clips': 'application/json',
- 'notes/conversation': 'application/json',
- 'notes/create': 'application/json',
- 'notes/delete': 'application/json',
- 'notes/favorites/create': 'application/json',
- 'notes/favorites/delete': 'application/json',
- 'notes/featured': 'application/json',
- 'notes/global-timeline': 'application/json',
- 'notes/hybrid-timeline': 'application/json',
- 'notes/local-timeline': 'application/json',
- 'notes/mentions': 'application/json',
- 'notes/polls/recommendation': 'application/json',
- 'notes/polls/vote': 'application/json',
- 'notes/reactions': 'application/json',
- 'notes/reactions/create': 'application/json',
- 'notes/reactions/delete': 'application/json',
- 'notes/renotes': 'application/json',
- 'notes/replies': 'application/json',
- 'notes/search-by-tag': 'application/json',
- 'notes/search': 'application/json',
- 'notes/show': 'application/json',
- 'notes/state': 'application/json',
- 'notes/thread-muting/create': 'application/json',
- 'notes/thread-muting/delete': 'application/json',
- 'notes/timeline': 'application/json',
- 'notes/translate': 'application/json',
- 'notes/unrenote': 'application/json',
- 'notes/user-list-timeline': 'application/json',
- 'notifications/create': 'application/json',
- 'notifications/flush': 'application/json',
- 'notifications/mark-all-as-read': 'application/json',
- 'notifications/test-notification': 'application/json',
- 'page-push': 'application/json',
- 'pages/create': 'application/json',
- 'pages/delete': 'application/json',
- 'pages/featured': 'application/json',
- 'pages/like': 'application/json',
- 'pages/show': 'application/json',
- 'pages/unlike': 'application/json',
- 'pages/update': 'application/json',
- 'flash/create': 'application/json',
- 'flash/delete': 'application/json',
- 'flash/featured': 'application/json',
- 'flash/like': 'application/json',
- 'flash/show': 'application/json',
- 'flash/unlike': 'application/json',
- 'flash/update': 'application/json',
- 'flash/my': 'application/json',
- 'flash/my-likes': 'application/json',
- 'ping': 'application/json',
- 'pinned-users': 'application/json',
- 'promo/read': 'application/json',
- 'roles/list': 'application/json',
- 'roles/show': 'application/json',
- 'roles/users': 'application/json',
- 'roles/notes': 'application/json',
- 'request-reset-password': 'application/json',
- 'reset-db': 'application/json',
- 'reset-password': 'application/json',
- 'server-info': 'application/json',
- 'stats': 'application/json',
- 'sw/show-registration': 'application/json',
- 'sw/update-registration': 'application/json',
- 'sw/register': 'application/json',
- 'sw/unregister': 'application/json',
- 'test': 'application/json',
- 'username/available': 'application/json',
- 'users': 'application/json',
- 'users/clips': 'application/json',
- 'users/followers': 'application/json',
- 'users/following': 'application/json',
- 'users/gallery/posts': 'application/json',
- 'users/get-frequently-replied-users': 'application/json',
- 'users/featured-notes': 'application/json',
- 'users/lists/create': 'application/json',
- 'users/lists/delete': 'application/json',
- 'users/lists/list': 'application/json',
- 'users/lists/pull': 'application/json',
- 'users/lists/push': 'application/json',
- 'users/lists/show': 'application/json',
- 'users/lists/favorite': 'application/json',
- 'users/lists/unfavorite': 'application/json',
- 'users/lists/update': 'application/json',
- 'users/lists/create-from-public': 'application/json',
- 'users/lists/update-membership': 'application/json',
- 'users/lists/get-memberships': 'application/json',
- 'users/notes': 'application/json',
- 'users/pages': 'application/json',
- 'users/flashs': 'application/json',
- 'users/reactions': 'application/json',
- 'users/recommendation': 'application/json',
- 'users/relation': 'application/json',
- 'users/report-abuse': 'application/json',
- 'users/search-by-username-and-host': 'application/json',
- 'users/search': 'application/json',
- 'users/show': 'application/json',
- 'users/achievements': 'application/json',
- 'users/update-memo': 'application/json',
- 'fetch-rss': 'application/json',
- 'fetch-external-resources': 'application/json',
- 'retention': 'application/json',
- 'bubble-game/register': 'application/json',
- 'bubble-game/ranking': 'application/json',
- 'reversi/cancel-match': 'application/json',
- 'reversi/games': 'application/json',
- 'reversi/match': 'application/json',
- 'reversi/invitations': 'application/json',
- 'reversi/show-game': 'application/json',
- 'reversi/surrender': 'application/json',
- 'reversi/verify': 'application/json',
-};
+} as const satisfies { [K in keyof Endpoints]?: 'multipart/form-data'; };
From 567acea2a3a040dbde69748deb2112e3ff2b92b8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 6 Sep 2024 17:23:40 +0900
Subject: [PATCH 019/222] =?UTF-8?q?fix(frontend):=20instance=20info?=
=?UTF-8?q?=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=A7=E4=B8=8D=E5=BF=85=E8=A6=81?=
=?UTF-8?q?=E3=81=AAapi=E3=83=AA=E3=82=AF=E3=82=A8=E3=82=B9=E3=83=88?=
=?UTF-8?q?=E3=81=8C=E9=A3=9B=E3=81=B6=E3=81=AE=E3=82=92=E6=8A=91=E6=AD=A2?=
=?UTF-8?q?=20(#14515)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(frontend): instance infoページで不必要なapiリクエストが飛ぶのを抑止
* fix
---
packages/frontend/src/components/MkChart.vue | 50 ++++++++++---------
packages/frontend/src/pages/instance-info.vue | 25 +++++++---
2 files changed, 43 insertions(+), 32 deletions(-)
diff --git a/packages/frontend/src/components/MkChart.vue b/packages/frontend/src/components/MkChart.vue
index 4b2456224991..57d325b11ad3 100644
--- a/packages/frontend/src/components/MkChart.vue
+++ b/packages/frontend/src/components/MkChart.vue
@@ -13,29 +13,8 @@ SPDX-License-Identifier: AGPL-3.0-only
-
+
+
diff --git a/packages/frontend-embed/src/components/EmAcct.vue b/packages/frontend-embed/src/components/EmAcct.vue
new file mode 100644
index 000000000000..07315e6a8b7c
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmAcct.vue
@@ -0,0 +1,24 @@
+
+
+
+
+ @{{ user.username }}
+ @{{ user.host || host }}
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmAvatar.vue b/packages/frontend-embed/src/components/EmAvatar.vue
new file mode 100644
index 000000000000..58c35c8ef0cd
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmAvatar.vue
@@ -0,0 +1,250 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmCustomEmoji.vue b/packages/frontend-embed/src/components/EmCustomEmoji.vue
new file mode 100644
index 000000000000..e4149cf363f3
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmCustomEmoji.vue
@@ -0,0 +1,101 @@
+
+
+
+
+:{{ customEmojiName }}:
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmEmoji.vue b/packages/frontend-embed/src/components/EmEmoji.vue
new file mode 100644
index 000000000000..224979707b97
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmEmoji.vue
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmError.vue b/packages/frontend-embed/src/components/EmError.vue
new file mode 100644
index 000000000000..d376b29a7f85
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmError.vue
@@ -0,0 +1,43 @@
+
+
+
+
+
{{ i18n.ts.somethingHappened }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmImgWithBlurhash.vue b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
new file mode 100644
index 000000000000..d19cd08d0a07
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmImgWithBlurhash.vue
@@ -0,0 +1,240 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmInstanceTicker.vue b/packages/frontend-embed/src/components/EmInstanceTicker.vue
new file mode 100644
index 000000000000..eeeaee528e75
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmInstanceTicker.vue
@@ -0,0 +1,87 @@
+
+
+
+
+
+
{{ instance.name }}
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmLink.vue b/packages/frontend-embed/src/components/EmLink.vue
new file mode 100644
index 000000000000..319ad723990e
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmLink.vue
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmLoading.vue b/packages/frontend-embed/src/components/EmLoading.vue
new file mode 100644
index 000000000000..49d8ace37bee
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmLoading.vue
@@ -0,0 +1,112 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmMediaBanner.vue b/packages/frontend-embed/src/components/EmMediaBanner.vue
new file mode 100644
index 000000000000..435da238a4e6
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMediaBanner.vue
@@ -0,0 +1,55 @@
+
+
+
+
+
+ {{ i18n.ts.audio }}
+ {{ i18n.ts.file }}
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmMediaImage.vue b/packages/frontend-embed/src/components/EmMediaImage.vue
new file mode 100644
index 000000000000..fe1aa5a877be
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMediaImage.vue
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.sensitive }}
+ {{ i18n.ts.image }}
+ {{ i18n.ts.clickToShow }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmMediaList.vue b/packages/frontend-embed/src/components/EmMediaList.vue
new file mode 100644
index 000000000000..0b2d835abe9e
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMediaList.vue
@@ -0,0 +1,146 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmMediaVideo.vue b/packages/frontend-embed/src/components/EmMediaVideo.vue
new file mode 100644
index 000000000000..ce751f9acdf8
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMediaVideo.vue
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmMention.vue b/packages/frontend-embed/src/components/EmMention.vue
new file mode 100644
index 000000000000..5eadf828c765
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMention.vue
@@ -0,0 +1,46 @@
+
+
+
+
+
+ @{{ username }}
+ @{{ toUnicode(host) }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmMfm.ts b/packages/frontend-embed/src/components/EmMfm.ts
new file mode 100644
index 000000000000..7543d3cd540d
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmMfm.ts
@@ -0,0 +1,461 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { VNode, h, SetupContext, provide } from 'vue';
+import * as mfm from 'mfm-js';
+import * as Misskey from 'misskey-js';
+import EmUrl from '@/components/EmUrl.vue';
+import EmTime from '@/components/EmTime.vue';
+import EmLink from '@/components/EmLink.vue';
+import EmMention from '@/components/EmMention.vue';
+import EmEmoji from '@/components/EmEmoji.vue';
+import EmCustomEmoji from '@/components/EmCustomEmoji.vue';
+import EmA from '@/components/EmA.vue';
+import { host } from '@/config.js';
+
+function safeParseFloat(str: unknown): number | null {
+ if (typeof str !== 'string' || str === '') return null;
+ const num = parseFloat(str);
+ if (isNaN(num)) return null;
+ return num;
+}
+
+const QUOTE_STYLE = `
+display: block;
+margin: 8px;
+padding: 6px 0 6px 12px;
+color: var(--fg);
+border-left: solid 3px var(--fg);
+opacity: 0.7;
+`.split('\n').join(' ');
+
+type MfmProps = {
+ text: string;
+ plain?: boolean;
+ nowrap?: boolean;
+ author?: Misskey.entities.UserLite;
+ isNote?: boolean;
+ emojiUrls?: Record;
+ rootScale?: number;
+ nyaize?: boolean | 'respect';
+ parsedNodes?: mfm.MfmNode[] | null;
+ enableEmojiMenu?: boolean;
+ enableEmojiMenuReaction?: boolean;
+ linkNavigationBehavior?: string;
+};
+
+type MfmEvents = {
+ clickEv(id: string): void;
+};
+
+// eslint-disable-next-line import/no-default-export
+export default function (props: MfmProps, { emit }: { emit: SetupContext['emit'] }) {
+ provide('linkNavigationBehavior', props.linkNavigationBehavior);
+
+ const isNote = props.isNote ?? true;
+ const shouldNyaize = props.nyaize ? props.nyaize === 'respect' ? props.author?.isCat : false : false;
+
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (props.text == null || props.text === '') return;
+
+ const rootAst = props.parsedNodes ?? (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
+
+ const validTime = (t: string | boolean | null | undefined) => {
+ if (t == null) return null;
+ if (typeof t === 'boolean') return null;
+ return t.match(/^\-?[0-9.]+s$/) ? t : null;
+ };
+
+ const validColor = (c: unknown): string | null => {
+ if (typeof c !== 'string') return null;
+ return c.match(/^[0-9a-f]{3,6}$/i) ? c : null;
+ };
+
+ const useAnim = true;
+
+ /**
+ * Gen Vue Elements from MFM AST
+ * @param ast MFM AST
+ * @param scale How times large the text is
+ * @param disableNyaize Whether nyaize is disabled or not
+ */
+ const genEl = (ast: mfm.MfmNode[], scale: number, disableNyaize = false) => ast.map((token): VNode | string | (VNode | string)[] => {
+ switch (token.type) {
+ case 'text': {
+ let text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
+ if (!disableNyaize && shouldNyaize) {
+ text = Misskey.nyaize(text);
+ }
+
+ if (!props.plain) {
+ const res: (VNode | string)[] = [];
+ for (const t of text.split('\n')) {
+ res.push(h('br'));
+ res.push(t);
+ }
+ res.shift();
+ return res;
+ } else {
+ return [text.replace(/\n/g, ' ')];
+ }
+ }
+
+ case 'bold': {
+ return [h('b', genEl(token.children, scale))];
+ }
+
+ case 'strike': {
+ return [h('del', genEl(token.children, scale))];
+ }
+
+ case 'italic': {
+ return h('i', {
+ style: 'font-style: oblique;',
+ }, genEl(token.children, scale));
+ }
+
+ case 'fn': {
+ // TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
+ let style: string | undefined;
+ switch (token.props.name) {
+ case 'tada': {
+ const speed = validTime(token.props.args.speed) ?? '1s';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = 'font-size: 150%;' + (useAnim ? `animation: global-tada ${speed} linear infinite both; animation-delay: ${delay};` : '');
+ break;
+ }
+ case 'jelly': {
+ const speed = validTime(token.props.args.speed) ?? '1s';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both; animation-delay: ${delay};` : '');
+ break;
+ }
+ case 'twitch': {
+ const speed = validTime(token.props.args.speed) ?? '0.5s';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-twitch ${speed} ease infinite; animation-delay: ${delay};` : '';
+ break;
+ }
+ case 'shake': {
+ const speed = validTime(token.props.args.speed) ?? '0.5s';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-shake ${speed} ease infinite; animation-delay: ${delay};` : '';
+ break;
+ }
+ case 'spin': {
+ const direction =
+ token.props.args.left ? 'reverse' :
+ token.props.args.alternate ? 'alternate' :
+ 'normal';
+ const anime =
+ token.props.args.x ? 'mfm-spinX' :
+ token.props.args.y ? 'mfm-spinY' :
+ 'mfm-spin';
+ const speed = validTime(token.props.args.speed) ?? '1.5s';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction}; animation-delay: ${delay};` : '';
+ break;
+ }
+ case 'jump': {
+ const speed = validTime(token.props.args.speed) ?? '0.75s';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-jump ${speed} linear infinite; animation-delay: ${delay};` : '';
+ break;
+ }
+ case 'bounce': {
+ const speed = validTime(token.props.args.speed) ?? '0.75s';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom; animation-delay: ${delay};` : '';
+ break;
+ }
+ case 'flip': {
+ const transform =
+ (token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
+ token.props.args.v ? 'scaleY(-1)' :
+ 'scaleX(-1)';
+ style = `transform: ${transform};`;
+ break;
+ }
+ case 'x2': {
+ return h('span', {
+ class: 'mfm-x2',
+ }, genEl(token.children, scale * 2));
+ }
+ case 'x3': {
+ return h('span', {
+ class: 'mfm-x3',
+ }, genEl(token.children, scale * 3));
+ }
+ case 'x4': {
+ return h('span', {
+ class: 'mfm-x4',
+ }, genEl(token.children, scale * 4));
+ }
+ case 'font': {
+ const family =
+ token.props.args.serif ? 'serif' :
+ token.props.args.monospace ? 'monospace' :
+ token.props.args.cursive ? 'cursive' :
+ token.props.args.fantasy ? 'fantasy' :
+ token.props.args.emoji ? 'emoji' :
+ token.props.args.math ? 'math' :
+ null;
+ if (family) style = `font-family: ${family};`;
+ break;
+ }
+ case 'blur': {
+ return h('span', {
+ class: '_mfm_blur_',
+ }, genEl(token.children, scale));
+ }
+ case 'rainbow': {
+ if (!useAnim) {
+ return h('span', {
+ class: '_mfm_rainbow_fallback_',
+ }, genEl(token.children, scale));
+ }
+ const speed = validTime(token.props.args.speed) ?? '1s';
+ const delay = validTime(token.props.args.delay) ?? '0s';
+ style = `animation: mfm-rainbow ${speed} linear infinite; animation-delay: ${delay};`;
+ break;
+ }
+ case 'sparkle': {
+ return genEl(token.children, scale);
+ }
+ case 'rotate': {
+ const degrees = safeParseFloat(token.props.args.deg) ?? 90;
+ style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
+ break;
+ }
+ case 'position': {
+ const x = safeParseFloat(token.props.args.x) ?? 0;
+ const y = safeParseFloat(token.props.args.y) ?? 0;
+ style = `transform: translateX(${x}em) translateY(${y}em);`;
+ break;
+ }
+ case 'scale': {
+ const x = Math.min(safeParseFloat(token.props.args.x) ?? 1, 5);
+ const y = Math.min(safeParseFloat(token.props.args.y) ?? 1, 5);
+ style = `transform: scale(${x}, ${y});`;
+ scale = scale * Math.max(x, y);
+ break;
+ }
+ case 'fg': {
+ let color = validColor(token.props.args.color);
+ color = color ?? 'f00';
+ style = `color: #${color}; overflow-wrap: anywhere;`;
+ break;
+ }
+ case 'bg': {
+ let color = validColor(token.props.args.color);
+ color = color ?? 'f00';
+ style = `background-color: #${color}; overflow-wrap: anywhere;`;
+ break;
+ }
+ case 'border': {
+ let color = validColor(token.props.args.color);
+ color = color ? `#${color}` : 'var(--accent)';
+ let b_style = token.props.args.style;
+ if (
+ typeof b_style !== 'string' ||
+ !['hidden', 'dotted', 'dashed', 'solid', 'double', 'groove', 'ridge', 'inset', 'outset']
+ .includes(b_style)
+ ) b_style = 'solid';
+ const width = safeParseFloat(token.props.args.width) ?? 1;
+ const radius = safeParseFloat(token.props.args.radius) ?? 0;
+ style = `border: ${width}px ${b_style} ${color}; border-radius: ${radius}px;${token.props.args.noclip ? '' : ' overflow: clip;'}`;
+ break;
+ }
+ case 'ruby': {
+ if (token.children.length === 1) {
+ const child = token.children[0];
+ let text = child.type === 'text' ? child.props.text : '';
+ if (!disableNyaize && shouldNyaize) {
+ text = Misskey.nyaize(text);
+ }
+ return h('ruby', {}, [text.split(' ')[0], h('rt', text.split(' ')[1])]);
+ } else {
+ const rt = token.children.at(-1)!;
+ let text = rt.type === 'text' ? rt.props.text : '';
+ if (!disableNyaize && shouldNyaize) {
+ text = Misskey.nyaize(text);
+ }
+ return h('ruby', {}, [...genEl(token.children.slice(0, token.children.length - 1), scale), h('rt', text.trim())]);
+ }
+ }
+ case 'unixtime': {
+ const child = token.children[0];
+ const unixtime = parseInt(child.type === 'text' ? child.props.text : '');
+ return h('span', {
+ style: 'display: inline-block; font-size: 90%; border: solid 1px var(--divider); border-radius: 999px; padding: 4px 10px 4px 6px;',
+ }, [
+ h('i', {
+ class: 'ti ti-clock',
+ style: 'margin-right: 0.25em;',
+ }),
+ h(EmTime, {
+ key: Math.random(),
+ time: unixtime * 1000,
+ mode: 'detail',
+ }),
+ ]);
+ }
+ case 'clickable': {
+ return h('span', { onClick(ev: MouseEvent): void {
+ ev.stopPropagation();
+ ev.preventDefault();
+ const clickEv = typeof token.props.args.ev === 'string' ? token.props.args.ev : '';
+ emit('clickEv', clickEv);
+ } }, genEl(token.children, scale));
+ }
+ }
+ if (style === undefined) {
+ return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
+ } else {
+ return h('span', {
+ style: 'display: inline-block; ' + style,
+ }, genEl(token.children, scale));
+ }
+ }
+
+ case 'small': {
+ return [h('small', {
+ style: 'opacity: 0.7;',
+ }, genEl(token.children, scale))];
+ }
+
+ case 'center': {
+ return [h('div', {
+ style: 'text-align:center;',
+ }, genEl(token.children, scale))];
+ }
+
+ case 'url': {
+ return [h(EmUrl, {
+ key: Math.random(),
+ url: token.props.url,
+ rel: 'nofollow noopener',
+ })];
+ }
+
+ case 'link': {
+ return [h(EmLink, {
+ key: Math.random(),
+ url: token.props.url,
+ rel: 'nofollow noopener',
+ }, genEl(token.children, scale, true))];
+ }
+
+ case 'mention': {
+ return [h(EmMention, {
+ key: Math.random(),
+ host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) ?? host,
+ username: token.props.username,
+ })];
+ }
+
+ case 'hashtag': {
+ return [h(EmA, {
+ key: Math.random(),
+ to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
+ style: 'color:var(--hashtag);',
+ }, `#${token.props.hashtag}`)];
+ }
+
+ case 'blockCode': {
+ return [h('code', {
+ key: Math.random(),
+ lang: token.props.lang ?? undefined,
+ }, token.props.code)];
+ }
+
+ case 'inlineCode': {
+ return [h('code', {
+ key: Math.random(),
+ }, token.props.code)];
+ }
+
+ case 'quote': {
+ if (!props.nowrap) {
+ return [h('div', {
+ style: QUOTE_STYLE,
+ }, genEl(token.children, scale, true))];
+ } else {
+ return [h('span', {
+ style: QUOTE_STYLE,
+ }, genEl(token.children, scale, true))];
+ }
+ }
+
+ case 'emojiCode': {
+ if (props.author?.host == null) {
+ return [h(EmCustomEmoji, {
+ key: Math.random(),
+ name: token.props.name,
+ normal: props.plain,
+ host: null,
+ useOriginalSize: scale >= 2.5,
+ menu: props.enableEmojiMenu,
+ menuReaction: props.enableEmojiMenuReaction,
+ fallbackToImage: false,
+ })];
+ } else {
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+ if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
+ return [h('span', `:${token.props.name}:`)];
+ } else {
+ return [h(EmCustomEmoji, {
+ key: Math.random(),
+ name: token.props.name,
+ url: props.emojiUrls && props.emojiUrls[token.props.name],
+ normal: props.plain,
+ host: props.author.host,
+ useOriginalSize: scale >= 2.5,
+ })];
+ }
+ }
+ }
+
+ case 'unicodeEmoji': {
+ return [h(EmEmoji, {
+ key: Math.random(),
+ emoji: token.props.emoji,
+ menu: props.enableEmojiMenu,
+ menuReaction: props.enableEmojiMenuReaction,
+ })];
+ }
+
+ case 'mathInline': {
+ return [h('code', token.props.formula)];
+ }
+
+ case 'mathBlock': {
+ return [h('code', token.props.formula)];
+ }
+
+ case 'search': {
+ return [h('div', {
+ key: Math.random(),
+ }, token.props.query)];
+ }
+
+ case 'plain': {
+ return [h('span', genEl(token.children, scale, true))];
+ }
+
+ default: {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ console.error('unrecognized ast type:', (token as any).type);
+
+ return [];
+ }
+ }
+ }).flat(Infinity) as (VNode | string)[];
+
+ return h('span', {
+ // https://codeday.me/jp/qa/20190424/690106.html
+ style: props.nowrap ? 'white-space: pre; word-wrap: normal; overflow: hidden; text-overflow: ellipsis;' : 'white-space: pre-wrap;',
+ }, genEl(rootAst, props.rootScale ?? 1));
+}
diff --git a/packages/frontend-embed/src/components/EmNote.vue b/packages/frontend-embed/src/components/EmNote.vue
new file mode 100644
index 000000000000..7c4d5910660b
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNote.vue
@@ -0,0 +1,609 @@
+
+
+
+
+
+
{{ i18n.ts.pinnedNote }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ({{ i18n.ts.private }})
+
+
+
+
+
+
+
+
+
+
+
+
{{ appearNote.channel.name }}
+
+
+
+ {{ i18n.ts.more }}
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmNoteDetailed.vue b/packages/frontend-embed/src/components/EmNoteDetailed.vue
new file mode 100644
index 000000000000..74a26856c84c
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNoteDetailed.vue
@@ -0,0 +1,486 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
({{ i18n.ts.private }})
+
+
+
RN:
+
+
+
+
+
+
+
+
+
{{ appearNote.channel.name }}
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmNoteHeader.vue b/packages/frontend-embed/src/components/EmNoteHeader.vue
new file mode 100644
index 000000000000..e4add9501f41
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNoteHeader.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+ bot
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmNoteSimple.vue b/packages/frontend-embed/src/components/EmNoteSimple.vue
new file mode 100644
index 000000000000..828b6cd2e2ba
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNoteSimple.vue
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmNoteSub.vue b/packages/frontend-embed/src/components/EmNoteSub.vue
new file mode 100644
index 000000000000..c98b956805bf
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNoteSub.vue
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.continueThread }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmNotes.vue b/packages/frontend-embed/src/components/EmNotes.vue
new file mode 100644
index 000000000000..3970d050988f
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmNotes.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
{{ i18n.ts.noNotes }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmPagination.vue b/packages/frontend-embed/src/components/EmPagination.vue
new file mode 100644
index 000000000000..5d5317a91281
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmPagination.vue
@@ -0,0 +1,504 @@
+
+
+
+
+
+
+
+
+
+
+
{{ i18n.ts.nothing }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmPoll.vue b/packages/frontend-embed/src/components/EmPoll.vue
new file mode 100644
index 000000000000..a2b120344940
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmPoll.vue
@@ -0,0 +1,82 @@
+
+
+
+
+
+ -
+
+
+
+
+ ({{ i18n.tsx._poll.votesCount({ n: choice.votes }) }})
+
+
+
+
+ {{ i18n.tsx._poll.totalVotes({ n: total }) }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmReactionIcon.vue b/packages/frontend-embed/src/components/EmReactionIcon.vue
new file mode 100644
index 000000000000..5c38ecb0ed3b
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmReactionIcon.vue
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue
new file mode 100644
index 000000000000..2e43eb8d170b
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmReactionsViewer.reaction.vue
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmReactionsViewer.vue b/packages/frontend-embed/src/components/EmReactionsViewer.vue
new file mode 100644
index 000000000000..014dd1c935f8
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmReactionsViewer.vue
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmSubNoteContent.vue b/packages/frontend-embed/src/components/EmSubNoteContent.vue
new file mode 100644
index 000000000000..382e39e4928b
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmSubNoteContent.vue
@@ -0,0 +1,113 @@
+
+
+
+
+
+ ({{ i18n.ts.private }})
+ ({{ i18n.ts.deletedNote }})
+
+
+ RN: ...
+
+
+ ({{ i18n.tsx.withNFiles({ n: note.files.length }) }})
+
+
+
+ {{ i18n.ts.poll }}
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmTime.vue b/packages/frontend-embed/src/components/EmTime.vue
new file mode 100644
index 000000000000..a8627e02c847
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmTime.vue
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmTimelineContainer.vue b/packages/frontend-embed/src/components/EmTimelineContainer.vue
new file mode 100644
index 000000000000..6c30b1102d9e
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmTimelineContainer.vue
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmUrl.vue b/packages/frontend-embed/src/components/EmUrl.vue
new file mode 100644
index 000000000000..a96bfdb49311
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmUrl.vue
@@ -0,0 +1,96 @@
+
+
+
+ {}"
+>
+
+ {{ schema }}//
+ {{ hostname }}
+ :{{ port }}
+
+
+ {{ hostname }}
+
+ {{ self ? pathname.substring(1) : pathname }}
+ {{ query }}
+ {{ hash }}
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/EmUserName.vue b/packages/frontend-embed/src/components/EmUserName.vue
new file mode 100644
index 000000000000..c0c7c443caeb
--- /dev/null
+++ b/packages/frontend-embed/src/components/EmUserName.vue
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/components/I18n.vue b/packages/frontend-embed/src/components/I18n.vue
new file mode 100644
index 000000000000..b621110ec9ed
--- /dev/null
+++ b/packages/frontend-embed/src/components/I18n.vue
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/config.ts b/packages/frontend-embed/src/config.ts
new file mode 100644
index 000000000000..f9850ba461a6
--- /dev/null
+++ b/packages/frontend-embed/src/config.ts
@@ -0,0 +1,18 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+const address = new URL(document.querySelector('meta[property="instance_url"]')?.content || location.href);
+const siteName = document.querySelector('meta[property="og:site_name"]')?.content;
+
+export const host = address.host;
+export const hostname = address.hostname;
+export const url = address.origin;
+export const apiUrl = location.origin + '/api';
+export const lang = localStorage.getItem('lang') ?? 'en-US';
+export const langs = _LANGS_;
+const preParseLocale = localStorage.getItem('locale');
+export const locale = preParseLocale ? JSON.parse(preParseLocale) : null;
+export const instanceName = siteName === 'Misskey' || siteName == null ? host : siteName;
+export const debug = localStorage.getItem('debug') === 'true';
diff --git a/packages/frontend-embed/src/custom-emojis.ts b/packages/frontend-embed/src/custom-emojis.ts
new file mode 100644
index 000000000000..d5b40885c1d4
--- /dev/null
+++ b/packages/frontend-embed/src/custom-emojis.ts
@@ -0,0 +1,61 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { shallowRef, watch } from 'vue';
+import * as Misskey from 'misskey-js';
+import { misskeyApi, misskeyApiGet } from '@/misskey-api.js';
+
+function get(key: string) {
+ const value = localStorage.getItem(key);
+ if (value === null) return null;
+ return JSON.parse(value);
+}
+
+function set(key: string, value: any) {
+ localStorage.setItem(key, JSON.stringify(value));
+}
+
+const storageCache = await get('emojis');
+export const customEmojis = shallowRef(Array.isArray(storageCache) ? storageCache : []);
+
+export const customEmojisMap = new Map();
+watch(customEmojis, emojis => {
+ customEmojisMap.clear();
+ for (const emoji of emojis) {
+ customEmojisMap.set(emoji.name, emoji);
+ }
+}, { immediate: true });
+
+export async function fetchCustomEmojis(force = false) {
+ const now = Date.now();
+
+ let res;
+ if (force) {
+ res = await misskeyApi('emojis', {});
+ } else {
+ const lastFetchedAt = await get('lastEmojisFetchedAt');
+ if (lastFetchedAt && (now - lastFetchedAt) < 1000 * 60 * 60) return;
+ res = await misskeyApiGet('emojis', {});
+ }
+
+ customEmojis.value = res.emojis;
+ set('emojis', res.emojis);
+ set('lastEmojisFetchedAt', now);
+}
+
+let cachedTags;
+export function getCustomEmojiTags() {
+ if (cachedTags) return cachedTags;
+
+ const tags = new Set();
+ for (const emoji of customEmojis.value) {
+ for (const tag of emoji.aliases) {
+ tags.add(tag);
+ }
+ }
+ const res = Array.from(tags);
+ cachedTags = res;
+ return res;
+}
diff --git a/packages/frontend-embed/src/di.ts b/packages/frontend-embed/src/di.ts
new file mode 100644
index 000000000000..799bbed598a1
--- /dev/null
+++ b/packages/frontend-embed/src/di.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import type { InjectionKey } from 'vue';
+import * as Misskey from 'misskey-js';
+import { MediaProxy } from '@@/js/media-proxy.js';
+import type { ParsedEmbedParams } from '@@/js/embed-page.js';
+
+export const DI = {
+ serverMetadata: Symbol() as InjectionKey,
+ embedParams: Symbol() as InjectionKey,
+ mediaProxy: Symbol() as InjectionKey,
+};
diff --git a/packages/frontend-embed/src/i18n.ts b/packages/frontend-embed/src/i18n.ts
new file mode 100644
index 000000000000..17e787f9fc17
--- /dev/null
+++ b/packages/frontend-embed/src/i18n.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { markRaw } from 'vue';
+import { I18n } from '@@/js/i18n.js';
+import type { Locale } from '../../../locales/index.js';
+import { locale } from '@/config.js';
+
+export const i18n = markRaw(new I18n(locale, _DEV_));
+
+export function updateI18n(newLocale: Locale) {
+ i18n.locale = newLocale;
+}
diff --git a/packages/frontend-embed/src/index.html b/packages/frontend-embed/src/index.html
new file mode 100644
index 000000000000..47b0b0e84e5f
--- /dev/null
+++ b/packages/frontend-embed/src/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+ [DEV] Loading...
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/misskey-api.ts b/packages/frontend-embed/src/misskey-api.ts
new file mode 100644
index 000000000000..13630590b674
--- /dev/null
+++ b/packages/frontend-embed/src/misskey-api.ts
@@ -0,0 +1,99 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { ref } from 'vue';
+import { apiUrl } from '@/config.js';
+
+export const pendingApiRequestsCount = ref(0);
+
+// Implements Misskey.api.ApiClient.request
+export function misskeyApi<
+ ResT = void,
+ E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
+ P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
+ _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType : ResT,
+>(
+ endpoint: E,
+ data: P = {} as any,
+ signal?: AbortSignal,
+): Promise<_ResT> {
+ if (endpoint.includes('://')) throw new Error('invalid endpoint');
+ pendingApiRequestsCount.value++;
+
+ const onFinally = () => {
+ pendingApiRequestsCount.value--;
+ };
+
+ const promise = new Promise<_ResT>((resolve, reject) => {
+ // Send request
+ window.fetch(`${apiUrl}/${endpoint}`, {
+ method: 'POST',
+ body: JSON.stringify(data),
+ credentials: 'omit',
+ cache: 'no-cache',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ signal,
+ }).then(async (res) => {
+ const body = res.status === 204 ? null : await res.json();
+
+ if (res.status === 200) {
+ resolve(body);
+ } else if (res.status === 204) {
+ resolve(undefined as _ResT); // void -> undefined
+ } else {
+ reject(body.error);
+ }
+ }).catch(reject);
+ });
+
+ promise.then(onFinally, onFinally);
+
+ return promise;
+}
+
+// Implements Misskey.api.ApiClient.request
+export function misskeyApiGet<
+ ResT = void,
+ E extends keyof Misskey.Endpoints = keyof Misskey.Endpoints,
+ P extends Misskey.Endpoints[E]['req'] = Misskey.Endpoints[E]['req'],
+ _ResT = ResT extends void ? Misskey.api.SwitchCaseResponseType : ResT,
+>(
+ endpoint: E,
+ data: P = {} as any,
+): Promise<_ResT> {
+ pendingApiRequestsCount.value++;
+
+ const onFinally = () => {
+ pendingApiRequestsCount.value--;
+ };
+
+ const query = new URLSearchParams(data as any);
+
+ const promise = new Promise<_ResT>((resolve, reject) => {
+ // Send request
+ window.fetch(`${apiUrl}/${endpoint}?${query}`, {
+ method: 'GET',
+ credentials: 'omit',
+ cache: 'default',
+ }).then(async (res) => {
+ const body = res.status === 204 ? null : await res.json();
+
+ if (res.status === 200) {
+ resolve(body);
+ } else if (res.status === 204) {
+ resolve(undefined as _ResT); // void -> undefined
+ } else {
+ reject(body.error);
+ }
+ }).catch(reject);
+ });
+
+ promise.then(onFinally, onFinally);
+
+ return promise;
+}
diff --git a/packages/frontend-embed/src/pages/clip.vue b/packages/frontend-embed/src/pages/clip.vue
new file mode 100644
index 000000000000..6564eecd7584
--- /dev/null
+++ b/packages/frontend-embed/src/pages/clip.vue
@@ -0,0 +1,140 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ i18n.tsx.fromX({ x: instanceName }) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/pages/not-found.vue b/packages/frontend-embed/src/pages/not-found.vue
new file mode 100644
index 000000000000..bbb03b4e642d
--- /dev/null
+++ b/packages/frontend-embed/src/pages/not-found.vue
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
{{ i18n.ts.notFoundDescription }}
+
+
+
+
+
diff --git a/packages/frontend-embed/src/pages/note.vue b/packages/frontend-embed/src/pages/note.vue
new file mode 100644
index 000000000000..86aebe072a12
--- /dev/null
+++ b/packages/frontend-embed/src/pages/note.vue
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/pages/tag.vue b/packages/frontend-embed/src/pages/tag.vue
new file mode 100644
index 000000000000..d69555287adb
--- /dev/null
+++ b/packages/frontend-embed/src/pages/tag.vue
@@ -0,0 +1,125 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ i18n.tsx.fromX({ x: instanceName }) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/pages/user-timeline.vue b/packages/frontend-embed/src/pages/user-timeline.vue
new file mode 100644
index 000000000000..d590f6e65085
--- /dev/null
+++ b/packages/frontend-embed/src/pages/user-timeline.vue
@@ -0,0 +1,138 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i18n.ts.user }}
+
+
+
{{ i18n.tsx.fromX({ x: instanceName }) }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/post-message.ts b/packages/frontend-embed/src/post-message.ts
new file mode 100644
index 000000000000..fd8eb8a5d2a0
--- /dev/null
+++ b/packages/frontend-embed/src/post-message.ts
@@ -0,0 +1,49 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export const postMessageEventTypes = [
+ 'misskey:embed:ready',
+ 'misskey:embed:changeHeight',
+] as const;
+
+export type PostMessageEventType = typeof postMessageEventTypes[number];
+
+export interface PostMessageEventPayload extends Record {
+ 'misskey:embed:ready': undefined;
+ 'misskey:embed:changeHeight': {
+ height: number;
+ };
+}
+
+export type MiPostMessageEvent = {
+ type: T;
+ iframeId?: string;
+ payload?: PostMessageEventPayload[T];
+}
+
+let defaultIframeId: string | null = null;
+
+export function setIframeId(id: string): void {
+ if (defaultIframeId != null) return;
+
+ if (_DEV_) console.log('setIframeId', id);
+ defaultIframeId = id;
+}
+
+/**
+ * 親フレームにイベントを送信
+ */
+export function postMessageToParentWindow(type: T, payload?: PostMessageEventPayload[T], iframeId: string | null = null): void {
+ let _iframeId = iframeId;
+ if (_iframeId == null) {
+ _iframeId = defaultIframeId;
+ }
+ if (_DEV_) console.log('postMessageToParentWindow', type, _iframeId, payload);
+ window.parent.postMessage({
+ type,
+ iframeId: _iframeId,
+ payload,
+ }, '*');
+}
diff --git a/packages/frontend-embed/src/server-metadata.ts b/packages/frontend-embed/src/server-metadata.ts
new file mode 100644
index 000000000000..2bd57a0990c5
--- /dev/null
+++ b/packages/frontend-embed/src/server-metadata.ts
@@ -0,0 +1,15 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { misskeyApi } from '@/misskey-api.js';
+
+const providedMetaEl = document.getElementById('misskey_meta');
+
+const _serverMetadata = (providedMetaEl && providedMetaEl.textContent) ? JSON.parse(providedMetaEl.textContent) : null;
+
+// NOTE: devモードのときしか _serverMetadata が null になることは無い
+export const serverMetadata = _serverMetadata ?? await misskeyApi('meta', {
+ detail: true,
+});
diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss
new file mode 100644
index 000000000000..02008ddbd05b
--- /dev/null
+++ b/packages/frontend-embed/src/style.scss
@@ -0,0 +1,453 @@
+@charset "utf-8";
+
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+:root {
+ --radius: 12px;
+ --marginFull: 14px;
+ --marginHalf: 10px;
+
+ --margin: var(--marginFull);
+}
+
+html {
+ background-color: transparent;
+ color-scheme: light dark;
+ color: var(--fg);
+ accent-color: var(--accent);
+ overflow: clip;
+ overflow-wrap: break-word;
+ font-family: 'Hiragino Maru Gothic Pro', "BIZ UDGothic", Roboto, HelveticaNeue, Arial, sans-serif;
+ font-size: 14px;
+ line-height: 1.35;
+ text-size-adjust: 100%;
+ tab-size: 2;
+ -webkit-text-size-adjust: 100%;
+
+ &, * {
+ scrollbar-color: var(--scrollbarHandle) transparent;
+ scrollbar-width: thin;
+
+ &::-webkit-scrollbar {
+ width: 6px;
+ height: 6px;
+ }
+
+ &::-webkit-scrollbar-track {
+ background: inherit;
+ }
+
+ &::-webkit-scrollbar-thumb {
+ background: var(--scrollbarHandle);
+
+ &:hover {
+ background: var(--scrollbarHandleHover);
+ }
+
+ &:active {
+ background: var(--accent);
+ }
+ }
+ }
+}
+
+html, body {
+ height: 100%;
+ touch-action: manipulation;
+ margin: 0;
+ padding: 0;
+ scroll-behavior: smooth;
+}
+
+#misskey_app {
+ height: 100%;
+}
+
+a {
+ text-decoration: none;
+ cursor: pointer;
+ color: inherit;
+ tap-highlight-color: transparent;
+ -webkit-tap-highlight-color: transparent;
+ -webkit-touch-callout: none;
+
+ &:focus-visible {
+ outline-offset: 2px;
+ }
+
+ &:hover {
+ text-decoration: underline;
+ }
+
+ &[target="_blank"] {
+ -webkit-touch-callout: default;
+ }
+}
+
+rt {
+ white-space: initial;
+}
+
+:focus-visible {
+ outline: var(--focus) solid 2px;
+ outline-offset: -2px;
+
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.ti {
+ width: 1.28em;
+ vertical-align: -12%;
+ line-height: 1em;
+
+ &::before {
+ font-size: 128%;
+ }
+}
+
+.ti-fw {
+ display: inline-block;
+ text-align: center;
+}
+
+._nowrap {
+ white-space: pre !important;
+ word-wrap: normal !important; // https://codeday.me/jp/qa/20190424/690106.html
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+._button {
+ user-select: none;
+ -webkit-user-select: none;
+ -webkit-touch-callout: none;
+ appearance: none;
+ display: inline-block;
+ padding: 0;
+ margin: 0; // for Safari
+ background: none;
+ border: none;
+ cursor: pointer;
+ color: inherit;
+ touch-action: manipulation;
+ tap-highlight-color: transparent;
+ -webkit-tap-highlight-color: transparent;
+ font-size: 1em;
+ font-family: inherit;
+ line-height: inherit;
+ max-width: 100%;
+
+ &:disabled {
+ opacity: 0.5;
+ cursor: default;
+ }
+}
+
+._buttonGray {
+ @extend ._button;
+ background: var(--buttonBg);
+
+ &:not(:disabled):hover {
+ background: var(--buttonHoverBg);
+ }
+}
+
+._buttonPrimary {
+ @extend ._button;
+ color: var(--fgOnAccent);
+ background: var(--accent);
+
+ &:not(:disabled):hover {
+ background: hsl(from var(--accent) h s calc(l + 5));
+ }
+
+ &:not(:disabled):active {
+ background: hsl(from var(--accent) h s calc(l - 5));
+ }
+}
+
+._buttonGradate {
+ @extend ._buttonPrimary;
+ color: var(--fgOnAccent);
+ background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
+
+ &:not(:disabled):hover {
+ background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ }
+
+ &:not(:disabled):active {
+ background: linear-gradient(90deg, hsl(from var(--accent) h s calc(l + 5)), hsl(from var(--accent) h s calc(l + 5)));
+ }
+}
+
+._buttonRounded {
+ font-size: 0.95em;
+ padding: 0.5em 1em;
+ min-width: 100px;
+ border-radius: 99rem;
+
+ &._buttonPrimary,
+ &._buttonGradate {
+ font-weight: 700;
+ }
+}
+
+._help {
+ color: var(--accent);
+ cursor: help;
+}
+
+._textButton {
+ @extend ._button;
+ color: var(--accent);
+
+ &:focus-visible {
+ outline-offset: 2px;
+ }
+
+ &:not(:disabled):hover {
+ text-decoration: underline;
+ }
+}
+
+._panel {
+ background: var(--panel);
+ border-radius: var(--radius);
+ overflow: clip;
+}
+
+._margin {
+ margin: var(--margin) 0;
+}
+
+._gaps_m {
+ display: flex;
+ flex-direction: column;
+ gap: 1.5em;
+}
+
+._gaps_s {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75em;
+}
+
+._gaps {
+ display: flex;
+ flex-direction: column;
+ gap: var(--margin);
+}
+
+._buttons {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+._buttonsCenter {
+ @extend ._buttons;
+
+ justify-content: center;
+}
+
+._borderButton {
+ @extend ._button;
+ display: block;
+ width: 100%;
+ padding: 10px;
+ box-sizing: border-box;
+ text-align: center;
+ border: solid 0.5px var(--divider);
+ border-radius: var(--radius);
+
+ &:active {
+ border-color: var(--accent);
+ }
+}
+
+._popup {
+ background: var(--popup);
+ border-radius: var(--radius);
+ contain: content;
+}
+
+._acrylic {
+ background: var(--acrylicPanel);
+ -webkit-backdrop-filter: var(--blur, blur(15px));
+ backdrop-filter: var(--blur, blur(15px));
+}
+
+._fullinfo {
+ padding: 64px 32px;
+ text-align: center;
+
+ > img {
+ vertical-align: bottom;
+ height: 128px;
+ margin-bottom: 16px;
+ border-radius: 16px;
+ }
+}
+
+._link {
+ color: var(--link);
+}
+
+._caption {
+ font-size: 0.8em;
+ opacity: 0.7;
+}
+
+._monospace {
+ font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important;
+}
+
+// MFM -----------------------------
+
+._mfm_blur_ {
+ filter: blur(6px);
+ transition: filter 0.3s;
+
+ &:hover {
+ filter: blur(0px);
+ }
+}
+
+.mfm-x2 {
+ --mfm-zoom-size: 200%;
+}
+
+.mfm-x3 {
+ --mfm-zoom-size: 400%;
+}
+
+.mfm-x4 {
+ --mfm-zoom-size: 600%;
+}
+
+.mfm-x2, .mfm-x3, .mfm-x4 {
+ font-size: var(--mfm-zoom-size);
+
+ .mfm-x2, .mfm-x3, .mfm-x4 {
+ /* only half effective */
+ font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
+
+ .mfm-x2, .mfm-x3, .mfm-x4 {
+ /* disabled */
+ font-size: 100%;
+ }
+ }
+}
+
+._mfm_rainbow_fallback_ {
+ background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+
+@keyframes mfm-spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+@keyframes mfm-spinX {
+ 0% { transform: perspective(128px) rotateX(0deg); }
+ 100% { transform: perspective(128px) rotateX(360deg); }
+}
+
+@keyframes mfm-spinY {
+ 0% { transform: perspective(128px) rotateY(0deg); }
+ 100% { transform: perspective(128px) rotateY(360deg); }
+}
+
+@keyframes mfm-jump {
+ 0% { transform: translateY(0); }
+ 25% { transform: translateY(-16px); }
+ 50% { transform: translateY(0); }
+ 75% { transform: translateY(-8px); }
+ 100% { transform: translateY(0); }
+}
+
+@keyframes mfm-bounce {
+ 0% { transform: translateY(0) scale(1, 1); }
+ 25% { transform: translateY(-16px) scale(1, 1); }
+ 50% { transform: translateY(0) scale(1, 1); }
+ 75% { transform: translateY(0) scale(1.5, 0.75); }
+ 100% { transform: translateY(0) scale(1, 1); }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-twitch {
+ 0% { transform: translate(7px, -2px) }
+ 5% { transform: translate(-3px, 1px) }
+ 10% { transform: translate(-7px, -1px) }
+ 15% { transform: translate(0px, -1px) }
+ 20% { transform: translate(-8px, 6px) }
+ 25% { transform: translate(-4px, -3px) }
+ 30% { transform: translate(-4px, -6px) }
+ 35% { transform: translate(-8px, -8px) }
+ 40% { transform: translate(4px, 6px) }
+ 45% { transform: translate(-3px, 1px) }
+ 50% { transform: translate(2px, -10px) }
+ 55% { transform: translate(-7px, 0px) }
+ 60% { transform: translate(-2px, 4px) }
+ 65% { transform: translate(3px, -8px) }
+ 70% { transform: translate(6px, 7px) }
+ 75% { transform: translate(-7px, -2px) }
+ 80% { transform: translate(-7px, -8px) }
+ 85% { transform: translate(9px, 3px) }
+ 90% { transform: translate(-3px, -2px) }
+ 95% { transform: translate(-10px, 2px) }
+ 100% { transform: translate(-2px, -6px) }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-shake {
+ 0% { transform: translate(-3px, -1px) rotate(-8deg) }
+ 5% { transform: translate(0px, -1px) rotate(-10deg) }
+ 10% { transform: translate(1px, -3px) rotate(0deg) }
+ 15% { transform: translate(1px, 1px) rotate(11deg) }
+ 20% { transform: translate(-2px, 1px) rotate(1deg) }
+ 25% { transform: translate(-1px, -2px) rotate(-2deg) }
+ 30% { transform: translate(-1px, 2px) rotate(-3deg) }
+ 35% { transform: translate(2px, 1px) rotate(6deg) }
+ 40% { transform: translate(-2px, -3px) rotate(-9deg) }
+ 45% { transform: translate(0px, -1px) rotate(-12deg) }
+ 50% { transform: translate(1px, 2px) rotate(10deg) }
+ 55% { transform: translate(0px, -3px) rotate(8deg) }
+ 60% { transform: translate(1px, -1px) rotate(8deg) }
+ 65% { transform: translate(0px, -1px) rotate(-7deg) }
+ 70% { transform: translate(-1px, -3px) rotate(6deg) }
+ 75% { transform: translate(0px, -2px) rotate(4deg) }
+ 80% { transform: translate(-2px, -1px) rotate(3deg) }
+ 85% { transform: translate(1px, -3px) rotate(-10deg) }
+ 90% { transform: translate(1px, 0px) rotate(3deg) }
+ 95% { transform: translate(-2px, 0px) rotate(-3deg) }
+ 100% { transform: translate(2px, 1px) rotate(2deg) }
+}
+
+@keyframes mfm-rubberBand {
+ from { transform: scale3d(1, 1, 1); }
+ 30% { transform: scale3d(1.25, 0.75, 1); }
+ 40% { transform: scale3d(0.75, 1.25, 1); }
+ 50% { transform: scale3d(1.15, 0.85, 1); }
+ 65% { transform: scale3d(0.95, 1.05, 1); }
+ 75% { transform: scale3d(1.05, 0.95, 1); }
+ to { transform: scale3d(1, 1, 1); }
+}
+
+@keyframes mfm-rainbow {
+ 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
+ 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
+}
diff --git a/packages/frontend-embed/src/theme.ts b/packages/frontend-embed/src/theme.ts
new file mode 100644
index 000000000000..050d8cf63ba4
--- /dev/null
+++ b/packages/frontend-embed/src/theme.ts
@@ -0,0 +1,102 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import tinycolor from 'tinycolor2';
+import lightTheme from '@@/themes/_light.json5';
+import darkTheme from '@@/themes/_dark.json5';
+import type { BundledTheme } from 'shiki/themes';
+
+export type Theme = {
+ id: string;
+ name: string;
+ author: string;
+ desc?: string;
+ base?: 'dark' | 'light';
+ props: Record;
+ codeHighlighter?: {
+ base: BundledTheme;
+ overrides?: Record;
+ } | {
+ base: '_none_';
+ overrides: Record;
+ };
+};
+
+let timeout: number | null = null;
+
+export function applyTheme(theme: Theme, persist = true) {
+ if (timeout) window.clearTimeout(timeout);
+
+ document.documentElement.classList.add('_themeChanging_');
+
+ timeout = window.setTimeout(() => {
+ document.documentElement.classList.remove('_themeChanging_');
+ }, 1000);
+
+ const colorScheme = theme.base === 'dark' ? 'dark' : 'light';
+
+ // Deep copy
+ const _theme = JSON.parse(JSON.stringify(theme));
+
+ if (_theme.base) {
+ const base = [lightTheme, darkTheme].find(x => x.id === _theme.base);
+ if (base) _theme.props = Object.assign({}, base.props, _theme.props);
+ }
+
+ const props = compile(_theme);
+
+ for (const tag of document.head.children) {
+ if (tag.tagName === 'META' && tag.getAttribute('name') === 'theme-color') {
+ tag.setAttribute('content', props['htmlThemeColor']);
+ break;
+ }
+ }
+
+ for (const [k, v] of Object.entries(props)) {
+ document.documentElement.style.setProperty(`--${k}`, v.toString());
+ }
+
+ document.documentElement.style.setProperty('color-scheme', colorScheme);
+}
+
+function compile(theme: Theme): Record {
+ function getColor(val: string): tinycolor.Instance {
+ if (val[0] === '@') { // ref (prop)
+ return getColor(theme.props[val.substring(1)]);
+ } else if (val[0] === '$') { // ref (const)
+ return getColor(theme.props[val]);
+ } else if (val[0] === ':') { // func
+ const parts = val.split('<');
+ const func = parts.shift().substring(1);
+ const arg = parseFloat(parts.shift());
+ const color = getColor(parts.join('<'));
+
+ switch (func) {
+ case 'darken': return color.darken(arg);
+ case 'lighten': return color.lighten(arg);
+ case 'alpha': return color.setAlpha(arg);
+ case 'hue': return color.spin(arg);
+ case 'saturate': return color.saturate(arg);
+ }
+ }
+
+ // other case
+ return tinycolor(val);
+ }
+
+ const props = {};
+
+ for (const [k, v] of Object.entries(theme.props)) {
+ if (k.startsWith('$')) continue; // ignore const
+
+ props[k] = v.startsWith('"') ? v.replace(/^"\s*/, '') : genValue(getColor(v));
+ }
+
+ return props;
+}
+
+function genValue(c: tinycolor.Instance): string {
+ return c.toRgbString();
+}
diff --git a/packages/frontend-embed/src/to-be-shared/collapsed.ts b/packages/frontend-embed/src/to-be-shared/collapsed.ts
new file mode 100644
index 000000000000..4ec88a3c6573
--- /dev/null
+++ b/packages/frontend-embed/src/to-be-shared/collapsed.ts
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+
+export function shouldCollapsed(note: Misskey.entities.Note, urls: string[]): boolean {
+ const collapsed = note.cw == null && (
+ note.text != null && (
+ (note.text.includes('$[x2')) ||
+ (note.text.includes('$[x3')) ||
+ (note.text.includes('$[x4')) ||
+ (note.text.includes('$[scale')) ||
+ (note.text.split('\n').length > 9) ||
+ (note.text.length > 500) ||
+ (urls.length >= 4)
+ ) || note.files.length >= 5
+ );
+
+ return collapsed;
+}
diff --git a/packages/frontend-embed/src/to-be-shared/intl-const.ts b/packages/frontend-embed/src/to-be-shared/intl-const.ts
new file mode 100644
index 000000000000..aaa4f0a86edf
--- /dev/null
+++ b/packages/frontend-embed/src/to-be-shared/intl-const.ts
@@ -0,0 +1,50 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { lang } from '@/config.js';
+
+export const versatileLang = (lang ?? 'ja-JP').replace('ja-KS', 'ja-JP');
+
+let _dateTimeFormat: Intl.DateTimeFormat;
+try {
+ _dateTimeFormat = new Intl.DateTimeFormat(versatileLang, {
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ second: 'numeric',
+ });
+} catch (err) {
+ console.warn(err);
+ if (_DEV_) console.log('[Intl] Fallback to en-US');
+
+ // Fallback to en-US
+ _dateTimeFormat = new Intl.DateTimeFormat('en-US', {
+ year: 'numeric',
+ month: 'numeric',
+ day: 'numeric',
+ hour: 'numeric',
+ minute: 'numeric',
+ second: 'numeric',
+ });
+}
+export const dateTimeFormat = _dateTimeFormat;
+
+export const timeZone = dateTimeFormat.resolvedOptions().timeZone;
+
+export const hemisphere = /^(australia|pacific|antarctica|indian)\//i.test(timeZone) ? 'S' : 'N';
+
+let _numberFormat: Intl.NumberFormat;
+try {
+ _numberFormat = new Intl.NumberFormat(versatileLang);
+} catch (err) {
+ console.warn(err);
+ if (_DEV_) console.log('[Intl] Fallback to en-US');
+
+ // Fallback to en-US
+ _numberFormat = new Intl.NumberFormat('en-US');
+}
+export const numberFormat = _numberFormat;
diff --git a/packages/frontend-embed/src/to-be-shared/is-link.ts b/packages/frontend-embed/src/to-be-shared/is-link.ts
new file mode 100644
index 000000000000..946f86400e16
--- /dev/null
+++ b/packages/frontend-embed/src/to-be-shared/is-link.ts
@@ -0,0 +1,12 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function isLink(el: HTMLElement) {
+ if (el.tagName === 'A') return true;
+ if (el.parentElement) {
+ return isLink(el.parentElement);
+ }
+ return false;
+}
diff --git a/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts b/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts
new file mode 100644
index 000000000000..6b3fcd938334
--- /dev/null
+++ b/packages/frontend-embed/src/to-be-shared/worker-multi-dispatch.ts
@@ -0,0 +1,82 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+function defaultUseWorkerNumber(prev: number, totalWorkers: number) {
+ return prev + 1;
+}
+
+export class WorkerMultiDispatch {
+ private symbol = Symbol('WorkerMultiDispatch');
+ private workers: Worker[] = [];
+ private terminated = false;
+ private prevWorkerNumber = 0;
+ private getUseWorkerNumber = defaultUseWorkerNumber;
+ private finalizationRegistry: FinalizationRegistry;
+
+ constructor(workerConstructor: () => Worker, concurrency: number, getUseWorkerNumber = defaultUseWorkerNumber) {
+ this.getUseWorkerNumber = getUseWorkerNumber;
+ for (let i = 0; i < concurrency; i++) {
+ this.workers.push(workerConstructor());
+ }
+
+ this.finalizationRegistry = new FinalizationRegistry(() => {
+ this.terminate();
+ });
+ this.finalizationRegistry.register(this, this.symbol);
+
+ if (_DEV_) console.log('WorkerMultiDispatch: Created', this);
+ }
+
+ public postMessage(message: POST, options?: Transferable[] | StructuredSerializeOptions, useWorkerNumber: typeof defaultUseWorkerNumber = this.getUseWorkerNumber) {
+ let workerNumber = useWorkerNumber(this.prevWorkerNumber, this.workers.length);
+ workerNumber = Math.abs(Math.round(workerNumber)) % this.workers.length;
+ if (_DEV_) console.log('WorkerMultiDispatch: Posting message to worker', workerNumber, useWorkerNumber);
+ this.prevWorkerNumber = workerNumber;
+
+ // 不毛だがunionをoverloadに突っ込めない
+ // https://stackoverflow.com/questions/66507585/overload-signatures-union-types-and-no-overload-matches-this-call-error
+ // https://github.com/microsoft/TypeScript/issues/14107
+ if (Array.isArray(options)) {
+ this.workers[workerNumber].postMessage(message, options);
+ } else {
+ this.workers[workerNumber].postMessage(message, options);
+ }
+ return workerNumber;
+ }
+
+ public addListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) {
+ this.workers.forEach(worker => {
+ worker.addEventListener('message', callback, options);
+ });
+ }
+
+ public removeListener(callback: (this: Worker, ev: MessageEvent) => any, options?: boolean | AddEventListenerOptions) {
+ this.workers.forEach(worker => {
+ worker.removeEventListener('message', callback, options);
+ });
+ }
+
+ public terminate() {
+ this.terminated = true;
+ if (_DEV_) console.log('WorkerMultiDispatch: Terminating', this);
+ this.workers.forEach(worker => {
+ worker.terminate();
+ });
+ this.workers = [];
+ this.finalizationRegistry.unregister(this);
+ }
+
+ public isTerminated() {
+ return this.terminated;
+ }
+
+ public getWorkers() {
+ return this.workers;
+ }
+
+ public getSymbol() {
+ return this.symbol;
+ }
+}
diff --git a/packages/frontend-embed/src/ui.vue b/packages/frontend-embed/src/ui.vue
new file mode 100644
index 000000000000..3b8449dac84c
--- /dev/null
+++ b/packages/frontend-embed/src/ui.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
diff --git a/packages/frontend-embed/src/utils.ts b/packages/frontend-embed/src/utils.ts
new file mode 100644
index 000000000000..9a2fd0beef7f
--- /dev/null
+++ b/packages/frontend-embed/src/utils.ts
@@ -0,0 +1,23 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { url } from '@/config.js';
+
+export const acct = (user: Misskey.Acct) => {
+ return Misskey.acct.toString(user);
+};
+
+export const userName = (user: Misskey.entities.User) => {
+ return user.name || user.username;
+};
+
+export const userPage = (user: Misskey.Acct, path?: string, absolute = false) => {
+ return `${absolute ? url : ''}/@${acct(user)}${(path ? `/${path}` : '')}`;
+};
+
+export const notePage = note => {
+ return `/notes/${note.id}`;
+};
diff --git a/packages/frontend-embed/src/workers/draw-blurhash.ts b/packages/frontend-embed/src/workers/draw-blurhash.ts
new file mode 100644
index 000000000000..22de6cd3a8c5
--- /dev/null
+++ b/packages/frontend-embed/src/workers/draw-blurhash.ts
@@ -0,0 +1,22 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { render } from 'buraha';
+
+const canvas = new OffscreenCanvas(64, 64);
+
+onmessage = (event) => {
+ // console.log(event.data);
+ if (!('id' in event.data && typeof event.data.id === 'string')) {
+ return;
+ }
+ if (!('hash' in event.data && typeof event.data.hash === 'string')) {
+ return;
+ }
+
+ render(event.data.hash, canvas);
+ const bitmap = canvas.transferToImageBitmap();
+ postMessage({ id: event.data.id, bitmap });
+};
diff --git a/packages/frontend-embed/src/workers/test-webgl2.ts b/packages/frontend-embed/src/workers/test-webgl2.ts
new file mode 100644
index 000000000000..b203ebe666b8
--- /dev/null
+++ b/packages/frontend-embed/src/workers/test-webgl2.ts
@@ -0,0 +1,14 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+const canvas = globalThis.OffscreenCanvas && new OffscreenCanvas(1, 1);
+// 環境によってはOffscreenCanvasが存在しないため
+// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+const gl = canvas?.getContext('webgl2');
+if (gl) {
+ postMessage({ result: true });
+} else {
+ postMessage({ result: false });
+}
diff --git a/packages/frontend-embed/src/workers/tsconfig.json b/packages/frontend-embed/src/workers/tsconfig.json
new file mode 100644
index 000000000000..8ee893046590
--- /dev/null
+++ b/packages/frontend-embed/src/workers/tsconfig.json
@@ -0,0 +1,5 @@
+{
+ "compilerOptions": {
+ "lib": ["esnext", "webworker"],
+ }
+}
diff --git a/packages/frontend-embed/tsconfig.json b/packages/frontend-embed/tsconfig.json
new file mode 100644
index 000000000000..3701343623da
--- /dev/null
+++ b/packages/frontend-embed/tsconfig.json
@@ -0,0 +1,53 @@
+{
+ "compilerOptions": {
+ "allowJs": true,
+ "noEmitOnError": false,
+ "noImplicitAny": false,
+ "noImplicitReturns": true,
+ "noUnusedParameters": false,
+ "noUnusedLocals": false,
+ "noFallthroughCasesInSwitch": true,
+ "declaration": false,
+ "sourceMap": false,
+ "target": "ES2022",
+ "module": "nodenext",
+ "moduleResolution": "nodenext",
+ "removeComments": false,
+ "noLib": false,
+ "strict": true,
+ "strictNullChecks": true,
+ "experimentalDecorators": true,
+ "resolveJsonModule": true,
+ "allowSyntheticDefaultImports": true,
+ "isolatedModules": true,
+ "useDefineForClassFields": true,
+ "baseUrl": ".",
+ "paths": {
+ "@/*": ["./src/*"],
+ "@@/*": ["../frontend-shared/*"]
+ },
+ "typeRoots": [
+ "./@types",
+ "./node_modules/@types",
+ "./node_modules/@vue-macros",
+ "./node_modules"
+ ],
+ "types": [
+ "vite/client",
+ ],
+ "lib": [
+ "esnext",
+ "dom",
+ "dom.iterable"
+ ],
+ "jsx": "preserve"
+ },
+ "compileOnSave": false,
+ "include": [
+ "./**/*.ts",
+ "./**/*.vue"
+ ],
+ "exclude": [
+ ".storybook/**/*"
+ ]
+}
diff --git a/packages/frontend-embed/vite.config.local-dev.ts b/packages/frontend-embed/vite.config.local-dev.ts
new file mode 100644
index 000000000000..bf2f478887d6
--- /dev/null
+++ b/packages/frontend-embed/vite.config.local-dev.ts
@@ -0,0 +1,96 @@
+import dns from 'dns';
+import { readFile } from 'node:fs/promises';
+import type { IncomingMessage } from 'node:http';
+import { defineConfig } from 'vite';
+import type { UserConfig } from 'vite';
+import * as yaml from 'js-yaml';
+import locales from '../../locales/index.js';
+import { getConfig } from './vite.config.js';
+
+dns.setDefaultResultOrder('ipv4first');
+
+const defaultConfig = getConfig();
+
+const { port } = yaml.load(await readFile('../../.config/default.yml', 'utf-8'));
+
+const httpUrl = `http://localhost:${port}/`;
+const websocketUrl = `ws://localhost:${port}/`;
+
+// activitypubリクエストはProxyを通し、それ以外はViteの開発サーバーを返す
+function varyHandler(req: IncomingMessage) {
+ if (req.headers.accept?.includes('application/activity+json')) {
+ return null;
+ }
+ return '/index.html';
+}
+
+const devConfig: UserConfig = {
+ // 基本の設定は vite.config.js から引き継ぐ
+ ...defaultConfig,
+ root: 'src',
+ publicDir: '../assets',
+ base: '/embed',
+ server: {
+ host: 'localhost',
+ port: 5174,
+ proxy: {
+ '/api': {
+ changeOrigin: true,
+ target: httpUrl,
+ },
+ '/assets': httpUrl,
+ '/static-assets': httpUrl,
+ '/client-assets': httpUrl,
+ '/files': httpUrl,
+ '/twemoji': httpUrl,
+ '/fluent-emoji': httpUrl,
+ '/sw.js': httpUrl,
+ '/streaming': {
+ target: websocketUrl,
+ ws: true,
+ },
+ '/favicon.ico': httpUrl,
+ '/robots.txt': httpUrl,
+ '/embed.js': httpUrl,
+ '/identicon': {
+ target: httpUrl,
+ rewrite(path) {
+ return path.replace('@localhost:5173', '');
+ },
+ },
+ '/url': httpUrl,
+ '/proxy': httpUrl,
+ '/_info_card_': httpUrl,
+ '/bios': httpUrl,
+ '/cli': httpUrl,
+ '/inbox': httpUrl,
+ '/emoji/': httpUrl,
+ '/notes': {
+ target: httpUrl,
+ bypass: varyHandler,
+ },
+ '/users': {
+ target: httpUrl,
+ bypass: varyHandler,
+ },
+ '/.well-known': {
+ target: httpUrl,
+ },
+ },
+ },
+ build: {
+ ...defaultConfig.build,
+ rollupOptions: {
+ ...defaultConfig.build?.rollupOptions,
+ input: 'index.html',
+ },
+ },
+
+ define: {
+ ...defaultConfig.define,
+ _LANGS_FULL_: JSON.stringify(Object.entries(locales)),
+ },
+};
+
+export default defineConfig(({ command, mode }) => devConfig);
+
diff --git a/packages/frontend-embed/vite.config.ts b/packages/frontend-embed/vite.config.ts
new file mode 100644
index 000000000000..64e67401c2f8
--- /dev/null
+++ b/packages/frontend-embed/vite.config.ts
@@ -0,0 +1,156 @@
+import path from 'path';
+import pluginVue from '@vitejs/plugin-vue';
+import { type UserConfig, defineConfig } from 'vite';
+
+import locales from '../../locales/index.js';
+import meta from '../../package.json';
+import packageInfo from './package.json' with { type: 'json' };
+import pluginJson5 from './vite.json5.js';
+
+const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
+
+/**
+ * Misskeyのフロントエンドにバンドルせず、CDNなどから別途読み込むリソースを記述する。
+ * CDNを使わずにバンドルしたい場合、以下の配列から該当要素を削除orコメントアウトすればOK
+ */
+const externalPackages = [
+ // shiki(コードブロックのシンタックスハイライトで使用中)はテーマ・言語の定義の容量が大きいため、それらはCDNから読み込む
+ {
+ name: 'shiki',
+ match: /^shiki\/(?(langs|themes))$/,
+ path(id: string, pattern: RegExp): string {
+ const match = pattern.exec(id)?.groups;
+ return match
+ ? `https://esm.sh/shiki@${packageInfo.dependencies.shiki}/${match['subPkg']}`
+ : id;
+ },
+ },
+];
+
+const hash = (str: string, seed = 0): number => {
+ let h1 = 0xdeadbeef ^ seed,
+ h2 = 0x41c6ce57 ^ seed;
+ for (let i = 0, ch; i < str.length; i++) {
+ ch = str.charCodeAt(i);
+ h1 = Math.imul(h1 ^ ch, 2654435761);
+ h2 = Math.imul(h2 ^ ch, 1597334677);
+ }
+
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
+
+ return 4294967296 * (2097151 & h2) + (h1 >>> 0);
+};
+
+const BASE62_DIGITS = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
+
+function toBase62(n: number): string {
+ if (n === 0) {
+ return '0';
+ }
+ let result = '';
+ while (n > 0) {
+ result = BASE62_DIGITS[n % BASE62_DIGITS.length] + result;
+ n = Math.floor(n / BASE62_DIGITS.length);
+ }
+
+ return result;
+}
+
+export function getConfig(): UserConfig {
+ return {
+ base: '/embed_vite/',
+
+ server: {
+ port: 5174,
+ },
+
+ plugins: [
+ pluginVue(),
+ pluginJson5(),
+ ],
+
+ resolve: {
+ extensions,
+ alias: {
+ '@/': __dirname + '/src/',
+ '@@/': __dirname + '/../frontend-shared/',
+ '/client-assets/': __dirname + '/assets/',
+ '/static-assets/': __dirname + '/../backend/assets/'
+ },
+ },
+
+ css: {
+ modules: {
+ generateScopedName(name, filename, _css): string {
+ const id = (path.relative(__dirname, filename.split('?')[0]) + '-' + name).replace(/[\\\/\.\?&=]/g, '-').replace(/(src-|vue-)/g, '');
+ if (process.env.NODE_ENV === 'production') {
+ return 'x' + toBase62(hash(id)).substring(0, 4);
+ } else {
+ return id;
+ }
+ },
+ },
+ },
+
+ define: {
+ _VERSION_: JSON.stringify(meta.version),
+ _LANGS_: JSON.stringify(Object.entries(locales).map(([k, v]) => [k, v._lang_])),
+ _ENV_: JSON.stringify(process.env.NODE_ENV),
+ _DEV_: process.env.NODE_ENV !== 'production',
+ _PERF_PREFIX_: JSON.stringify('Misskey:'),
+ __VUE_OPTIONS_API__: false,
+ __VUE_PROD_DEVTOOLS__: false,
+ },
+
+ build: {
+ target: [
+ 'chrome116',
+ 'firefox116',
+ 'safari16',
+ ],
+ manifest: 'manifest.json',
+ rollupOptions: {
+ input: {
+ app: './src/boot.ts',
+ },
+ external: externalPackages.map(p => p.match),
+ output: {
+ manualChunks: {
+ vue: ['vue'],
+ },
+ chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js',
+ assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]',
+ paths(id) {
+ for (const p of externalPackages) {
+ if (p.match.test(id)) {
+ return p.path(id, p.match);
+ }
+ }
+
+ return id;
+ },
+ },
+ },
+ cssCodeSplit: true,
+ outDir: __dirname + '/../../built/_frontend_embed_vite_',
+ assetsDir: '.',
+ emptyOutDir: false,
+ sourcemap: process.env.NODE_ENV === 'development',
+ reportCompressedSize: false,
+
+ // https://vitejs.dev/guide/dep-pre-bundling.html#monorepos-and-linked-dependencies
+ commonjsOptions: {
+ include: [/misskey-js/, /node_modules/],
+ },
+ },
+
+ worker: {
+ format: 'es',
+ },
+ };
+}
+
+const config = defineConfig(({ command, mode }) => getConfig());
+
+export default config;
diff --git a/packages/frontend-embed/vite.json5.ts b/packages/frontend-embed/vite.json5.ts
new file mode 100644
index 000000000000..87b67c214241
--- /dev/null
+++ b/packages/frontend-embed/vite.json5.ts
@@ -0,0 +1,48 @@
+// Original: https://github.com/rollup/plugins/tree/8835dd2aed92f408d7dc72d7cc25a9728e16face/packages/json
+
+import JSON5 from 'json5';
+import { Plugin } from 'rollup';
+import { createFilter, dataToEsm } from '@rollup/pluginutils';
+import { RollupJsonOptions } from '@rollup/plugin-json';
+
+// json5 extends SyntaxError with additional fields (without subclassing)
+// https://github.com/json5/json5/blob/de344f0619bda1465a6e25c76f1c0c3dda8108d9/lib/parse.js#L1111-L1112
+interface Json5SyntaxError extends SyntaxError {
+ lineNumber: number;
+ columnNumber: number;
+}
+
+export default function json5(options: RollupJsonOptions = {}): Plugin {
+ const filter = createFilter(options.include, options.exclude);
+ const indent = 'indent' in options ? options.indent : '\t';
+
+ return {
+ name: 'json5',
+
+ // eslint-disable-next-line no-shadow
+ transform(json, id) {
+ if (id.slice(-6) !== '.json5' || !filter(id)) return null;
+
+ try {
+ const parsed = JSON5.parse(json);
+ return {
+ code: dataToEsm(parsed, {
+ preferConst: options.preferConst,
+ compact: options.compact,
+ namedExports: options.namedExports,
+ indent,
+ }),
+ map: { mappings: '' },
+ };
+ } catch (err) {
+ if (!(err instanceof SyntaxError)) {
+ throw err;
+ }
+ const message = 'Could not parse JSON5 file';
+ const { lineNumber, columnNumber } = err as Json5SyntaxError;
+ this.warn({ message, id, loc: { line: lineNumber, column: columnNumber } });
+ return null;
+ }
+ },
+ };
+}
diff --git a/packages/frontend-embed/vue-shims.d.ts b/packages/frontend-embed/vue-shims.d.ts
new file mode 100644
index 000000000000..eba994772dd4
--- /dev/null
+++ b/packages/frontend-embed/vue-shims.d.ts
@@ -0,0 +1,6 @@
+/* eslint-disable */
+declare module "*.vue" {
+ import { defineComponent } from "vue";
+ const component: ReturnType;
+ export default component;
+}
diff --git a/packages/frontend-shared/.gitignore b/packages/frontend-shared/.gitignore
new file mode 100644
index 000000000000..5f6be09d7c01
--- /dev/null
+++ b/packages/frontend-shared/.gitignore
@@ -0,0 +1,2 @@
+/storybook-static
+js-built
diff --git a/packages/frontend-shared/build.js b/packages/frontend-shared/build.js
new file mode 100644
index 000000000000..17b6da8d30a2
--- /dev/null
+++ b/packages/frontend-shared/build.js
@@ -0,0 +1,106 @@
+import fs from 'node:fs';
+import { fileURLToPath } from 'node:url';
+import { dirname } from 'node:path';
+import * as esbuild from 'esbuild';
+import { build } from 'esbuild';
+import { globSync } from 'glob';
+import { execa } from 'execa';
+
+const _filename = fileURLToPath(import.meta.url);
+const _dirname = dirname(_filename);
+const _package = JSON.parse(fs.readFileSync(_dirname + '/package.json', 'utf-8'));
+
+const entryPoints = globSync('./js/**/**.{ts,tsx}');
+
+/** @type {import('esbuild').BuildOptions} */
+const options = {
+ entryPoints,
+ minify: process.env.NODE_ENV === 'production',
+ outdir: './js-built',
+ target: 'es2022',
+ platform: 'browser',
+ format: 'esm',
+ sourcemap: 'linked',
+};
+
+// js-built配下をすべて削除する
+fs.rmSync('./js-built', { recursive: true, force: true });
+
+if (process.argv.map(arg => arg.toLowerCase()).includes('--watch')) {
+ await watchSrc();
+} else {
+ await buildSrc();
+}
+
+async function buildSrc() {
+ console.log(`[${_package.name}] start building...`);
+
+ await build(options)
+ .then(() => {
+ console.log(`[${_package.name}] build succeeded.`);
+ })
+ .catch((err) => {
+ process.stderr.write(err.stderr);
+ process.exit(1);
+ });
+
+ if (process.env.NODE_ENV === 'production') {
+ console.log(`[${_package.name}] skip building d.ts because NODE_ENV is production.`);
+ } else {
+ await buildDts();
+ }
+
+ fs.copyFileSync('./js/emojilist.json', './js-built/emojilist.json');
+
+ console.log(`[${_package.name}] finish building.`);
+}
+
+function buildDts() {
+ return execa(
+ 'tsc',
+ [
+ '--project', 'tsconfig.json',
+ '--outDir', 'js-built',
+ '--declaration', 'true',
+ '--emitDeclarationOnly', 'true',
+ ],
+ {
+ stdout: process.stdout,
+ stderr: process.stderr,
+ },
+ );
+}
+
+async function watchSrc() {
+ const plugins = [{
+ name: 'gen-dts',
+ setup(build) {
+ build.onStart(() => {
+ console.log(`[${_package.name}] detect changed...`);
+ });
+ build.onEnd(async result => {
+ if (result.errors.length > 0) {
+ console.error(`[${_package.name}] watch build failed:`, result);
+ return;
+ }
+ await buildDts();
+ });
+ },
+ }];
+
+ console.log(`[${_package.name}] start watching...`);
+
+ const context = await esbuild.context({ ...options, plugins });
+ await context.watch();
+
+ await new Promise((resolve, reject) => {
+ process.on('SIGHUP', resolve);
+ process.on('SIGINT', resolve);
+ process.on('SIGTERM', resolve);
+ process.on('uncaughtException', reject);
+ process.on('exit', resolve);
+ }).finally(async () => {
+ await context.dispose();
+ console.log(`[${_package.name}] finish watching.`);
+ });
+}
diff --git a/packages/frontend-shared/eslint.config.js b/packages/frontend-shared/eslint.config.js
new file mode 100644
index 000000000000..a15fb29e3711
--- /dev/null
+++ b/packages/frontend-shared/eslint.config.js
@@ -0,0 +1,96 @@
+import globals from 'globals';
+import tsParser from '@typescript-eslint/parser';
+import parser from 'vue-eslint-parser';
+import pluginVue from 'eslint-plugin-vue';
+import pluginMisskey from '@misskey-dev/eslint-plugin';
+import sharedConfig from '../shared/eslint.config.js';
+
+// eslint-disable-next-line import/no-default-export
+export default [
+ ...sharedConfig,
+ {
+ files: ['**/*.vue'],
+ ...pluginMisskey.configs.typescript,
+ },
+ ...pluginVue.configs['flat/recommended'],
+ {
+ files: ['js/**/*.{ts,vue}', '**/*.vue'],
+ languageOptions: {
+ globals: {
+ ...Object.fromEntries(Object.entries(globals.node).map(([key]) => [key, 'off'])),
+ ...globals.browser,
+
+ // Node.js
+ module: false,
+ require: false,
+ __dirname: false,
+
+ // Misskey
+ _DEV_: false,
+ _LANGS_: false,
+ _VERSION_: false,
+ _ENV_: false,
+ _PERF_PREFIX_: false,
+ _DATA_TRANSFER_DRIVE_FILE_: false,
+ _DATA_TRANSFER_DRIVE_FOLDER_: false,
+ _DATA_TRANSFER_DECK_COLUMN_: false,
+ },
+ parser,
+ parserOptions: {
+ extraFileExtensions: ['.vue'],
+ parser: tsParser,
+ project: ['./tsconfig.json'],
+ sourceType: 'module',
+ tsconfigRootDir: import.meta.dirname,
+ },
+ },
+ rules: {
+ '@typescript-eslint/no-empty-interface': ['error', {
+ allowSingleExtends: true,
+ }],
+ // window の禁止理由: グローバルスコープと衝突し、予期せぬ結果を招くため
+ // e の禁止理由: error や event など、複数のキーワードの頭文字であり分かりにくいため
+ 'id-denylist': ['error', 'window', 'e'],
+ 'no-shadow': ['warn'],
+ 'vue/attributes-order': ['error', {
+ alphabetical: false,
+ }],
+ 'vue/no-use-v-if-with-v-for': ['error', {
+ allowUsingIterationVar: false,
+ }],
+ 'vue/no-ref-as-operand': 'error',
+ 'vue/no-multi-spaces': ['error', {
+ ignoreProperties: false,
+ }],
+ 'vue/no-v-html': 'warn',
+ 'vue/order-in-components': 'error',
+ 'vue/html-indent': ['warn', 'tab', {
+ attribute: 1,
+ baseIndent: 0,
+ closeBracket: 0,
+ alignAttributesVertically: true,
+ ignores: [],
+ }],
+ 'vue/html-closing-bracket-spacing': ['warn', {
+ startTag: 'never',
+ endTag: 'never',
+ selfClosingTag: 'never',
+ }],
+ 'vue/multi-word-component-names': 'warn',
+ 'vue/require-v-for-key': 'warn',
+ 'vue/no-unused-components': 'warn',
+ 'vue/no-unused-vars': 'warn',
+ 'vue/no-dupe-keys': 'warn',
+ 'vue/valid-v-for': 'warn',
+ 'vue/return-in-computed-property': 'warn',
+ 'vue/no-setup-props-reactivity-loss': 'warn',
+ 'vue/max-attributes-per-line': 'off',
+ 'vue/html-self-closing': 'off',
+ 'vue/singleline-html-element-content-newline': 'off',
+ 'vue/v-on-event-hyphenation': ['error', 'never', {
+ autofix: true,
+ }],
+ 'vue/attribute-hyphenation': ['error', 'never'],
+ },
+ },
+];
diff --git a/packages/frontend/src/const.ts b/packages/frontend-shared/js/const.ts
similarity index 98%
rename from packages/frontend/src/const.ts
rename to packages/frontend-shared/js/const.ts
index e135bc69a0f3..8391fb638c5e 100644
--- a/packages/frontend/src/const.ts
+++ b/packages/frontend-shared/js/const.ts
@@ -127,7 +127,7 @@ export const MFM_PARAMS: Record = {
position: ['x=', 'y='],
fg: ['color='],
bg: ['color='],
- border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
+ border: ['width=', 'style=', 'color=', 'radius=', 'noclip'],
font: ['serif', 'monospace', 'cursive', 'fantasy', 'emoji', 'math'],
blur: [],
rainbow: ['speed=', 'delay='],
diff --git a/packages/frontend-shared/js/embed-page.ts b/packages/frontend-shared/js/embed-page.ts
new file mode 100644
index 000000000000..d5555a98c3b0
--- /dev/null
+++ b/packages/frontend-shared/js/embed-page.ts
@@ -0,0 +1,97 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+//#region Embed関連の定義
+
+/** 埋め込みの対象となるエンティティ(/embed/xxx の xxx の部分と対応させる) */
+const embeddableEntities = [
+ 'notes',
+ 'user-timeline',
+ 'clips',
+ 'tags',
+] as const;
+
+/** 埋め込みの対象となるエンティティ */
+export type EmbeddableEntity = typeof embeddableEntities[number];
+
+/** 内部でスクロールがあるページ */
+export const embedRouteWithScrollbar: EmbeddableEntity[] = [
+ 'clips',
+ 'tags',
+ 'user-timeline',
+];
+
+/** 埋め込みコードのパラメータ */
+export type EmbedParams = {
+ maxHeight?: number;
+ colorMode?: 'light' | 'dark';
+ rounded?: boolean;
+ border?: boolean;
+ autoload?: boolean;
+ header?: boolean;
+};
+
+/** 正規化されたパラメータ */
+export type ParsedEmbedParams = Required> & Pick;
+
+/** パラメータのデフォルトの値 */
+export const defaultEmbedParams = {
+ maxHeight: undefined,
+ colorMode: undefined,
+ rounded: true,
+ border: true,
+ autoload: false,
+ header: true,
+} as const satisfies EmbedParams;
+
+//#endregion
+
+/**
+ * パラメータを正規化する(埋め込みページ初期化用)
+ * @param searchParams URLSearchParamsもしくはクエリ文字列
+ * @returns 正規化されたパラメータ
+ */
+export function parseEmbedParams(searchParams: URLSearchParams | string): ParsedEmbedParams {
+ let _searchParams: URLSearchParams;
+ if (typeof searchParams === 'string') {
+ _searchParams = new URLSearchParams(searchParams);
+ } else if (searchParams instanceof URLSearchParams) {
+ _searchParams = searchParams;
+ } else {
+ throw new Error('searchParams must be URLSearchParams or string');
+ }
+
+ function convertBoolean(value: string | null): boolean | undefined {
+ if (value === 'true') {
+ return true;
+ } else if (value === 'false') {
+ return false;
+ }
+ return undefined;
+ }
+
+ function convertNumber(value: string | null): number | undefined {
+ if (value != null && !isNaN(Number(value))) {
+ return Number(value);
+ }
+ return undefined;
+ }
+
+ function convertColorMode(value: string | null): 'light' | 'dark' | undefined {
+ if (value != null && ['light', 'dark'].includes(value)) {
+ return value as 'light' | 'dark';
+ }
+ return undefined;
+ }
+
+ return {
+ maxHeight: convertNumber(_searchParams.get('maxHeight')) ?? defaultEmbedParams.maxHeight,
+ colorMode: convertColorMode(_searchParams.get('colorMode')) ?? defaultEmbedParams.colorMode,
+ rounded: convertBoolean(_searchParams.get('rounded')) ?? defaultEmbedParams.rounded,
+ border: convertBoolean(_searchParams.get('border')) ?? defaultEmbedParams.border,
+ autoload: convertBoolean(_searchParams.get('autoload')) ?? defaultEmbedParams.autoload,
+ header: convertBoolean(_searchParams.get('header')) ?? defaultEmbedParams.header,
+ };
+}
diff --git a/packages/frontend/src/scripts/emoji-base.ts b/packages/frontend-shared/js/emoji-base.ts
similarity index 100%
rename from packages/frontend/src/scripts/emoji-base.ts
rename to packages/frontend-shared/js/emoji-base.ts
diff --git a/packages/frontend/src/emojilist.json b/packages/frontend-shared/js/emojilist.json
similarity index 100%
rename from packages/frontend/src/emojilist.json
rename to packages/frontend-shared/js/emojilist.json
diff --git a/packages/frontend/src/scripts/emojilist.ts b/packages/frontend-shared/js/emojilist.ts
similarity index 96%
rename from packages/frontend/src/scripts/emojilist.ts
rename to packages/frontend-shared/js/emojilist.ts
index 6565feba97d2..bde30a864fd0 100644
--- a/packages/frontend/src/scripts/emojilist.ts
+++ b/packages/frontend-shared/js/emojilist.ts
@@ -12,12 +12,12 @@ export type UnicodeEmojiDef = {
}
// initial converted from https://github.com/muan/emojilib/commit/242fe68be86ed6536843b83f7e32f376468b38fb
-import _emojilist from '../emojilist.json';
+import _emojilist from './emojilist.json';
export const emojilist: UnicodeEmojiDef[] = _emojilist.map(x => ({
name: x[1] as string,
char: x[0] as string,
- category: unicodeEmojiCategories[x[2]],
+ category: unicodeEmojiCategories[x[2] as number],
}));
const unicodeEmojisMap = new Map(
diff --git a/packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts b/packages/frontend-shared/js/extract-avg-color-from-blurhash.ts
similarity index 100%
rename from packages/frontend/src/scripts/extract-avg-color-from-blurhash.ts
rename to packages/frontend-shared/js/extract-avg-color-from-blurhash.ts
diff --git a/packages/frontend/src/scripts/i18n.ts b/packages/frontend-shared/js/i18n.ts
similarity index 91%
rename from packages/frontend/src/scripts/i18n.ts
rename to packages/frontend-shared/js/i18n.ts
index b258a2a6781f..18232691fa7e 100644
--- a/packages/frontend/src/scripts/i18n.ts
+++ b/packages/frontend-shared/js/i18n.ts
@@ -2,7 +2,10 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import type { ILocale, ParameterizedString } from '../../../../locales/index.js';
+import type { ILocale, ParameterizedString } from '../../../locales/index.js';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+type TODO = any;
type FlattenKeys = keyof {
[K in keyof T as T[K] extends ILocale
@@ -32,15 +35,18 @@ type Tsx = {
export class I18n {
private tsxCache?: Tsx;
+ private devMode: boolean;
+
+ constructor(public locale: T, devMode = false) {
+ this.devMode = devMode;
- constructor(public locale: T) {
//#region BIND
this.t = this.t.bind(this);
//#endregion
}
public get ts(): T {
- if (_DEV_) {
+ if (this.devMode) {
class Handler implements ProxyHandler {
get(target: TTarget, p: string | symbol): unknown {
const value = target[p as keyof TTarget];
@@ -72,7 +78,7 @@ export class I18n {
}
public get tsx(): Tsx {
- if (_DEV_) {
+ if (this.devMode) {
if (this.tsxCache) {
return this.tsxCache;
}
@@ -113,7 +119,7 @@ export class I18n {
return () => value;
}
- return (arg) => {
+ return (arg: TODO) => {
let str = quasis[0];
for (let i = 0; i < expressions.length; i++) {
@@ -152,7 +158,7 @@ export class I18n {
const value = target[k as keyof typeof target];
if (typeof value === 'object') {
- result[k] = build(value as ILocale);
+ (result as TODO)[k] = build(value as ILocale);
} else if (typeof value === 'string') {
const quasis: string[] = [];
const expressions: string[] = [];
@@ -179,7 +185,7 @@ export class I18n {
continue;
}
- result[k] = (arg) => {
+ (result as TODO)[k] = (arg: TODO) => {
let str = quasis[0];
for (let i = 0; i < expressions.length; i++) {
@@ -208,9 +214,9 @@ export class I18n {
let str: string | ParameterizedString | ILocale = this.locale;
for (const k of key.split('.')) {
- str = str[k];
+ str = (str as TODO)[k];
- if (_DEV_) {
+ if (this.devMode) {
if (typeof str === 'undefined') {
console.error(`Unexpected locale key: ${key}`);
return key;
@@ -219,7 +225,7 @@ export class I18n {
}
if (args) {
- if (_DEV_) {
+ if (this.devMode) {
const missing = Array.from((str as string).matchAll(/\{(\w+)\}/g), ([, parameter]) => parameter).filter(parameter => !Object.hasOwn(args, parameter));
if (missing.length) {
@@ -230,7 +236,7 @@ export class I18n {
for (const [k, v] of Object.entries(args)) {
const search = `{${k}}`;
- if (_DEV_) {
+ if (this.devMode) {
if (!(str as string).includes(search)) {
console.error(`Unexpected locale parameter: ${k} at ${key}`);
}
diff --git a/packages/frontend-shared/js/media-proxy.ts b/packages/frontend-shared/js/media-proxy.ts
new file mode 100644
index 000000000000..2837870c9a63
--- /dev/null
+++ b/packages/frontend-shared/js/media-proxy.ts
@@ -0,0 +1,63 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import * as Misskey from 'misskey-js';
+import { query } from './url.js';
+
+export class MediaProxy {
+ private serverMetadata: Misskey.entities.MetaDetailed;
+ private url: string;
+
+ constructor(serverMetadata: Misskey.entities.MetaDetailed, url: string) {
+ this.serverMetadata = serverMetadata;
+ this.url = url;
+ }
+
+ public getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
+ const localProxy = `${this.url}/proxy`;
+ let _imageUrl = imageUrl;
+
+ if (imageUrl.startsWith(this.serverMetadata.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
+ // もう既にproxyっぽそうだったらurlを取り出す
+ _imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
+ }
+
+ return `${mustOrigin ? localProxy : this.serverMetadata.mediaProxy}/${
+ type === 'preview' ? 'preview.webp'
+ : 'image.webp'
+ }?${query({
+ url: _imageUrl,
+ ...(!noFallback ? { 'fallback': '1' } : {}),
+ ...(type ? { [type]: '1' } : {}),
+ ...(mustOrigin ? { origin: '1' } : {}),
+ })}`;
+ }
+
+ public getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
+ if (imageUrl == null) return null;
+ return this.getProxiedImageUrl(imageUrl, type);
+ }
+
+ public getStaticImageUrl(baseUrl: string): string {
+ const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, this.url);
+
+ if (u.href.startsWith(`${this.url}/emoji/`)) {
+ // もう既にemojiっぽそうだったらsearchParams付けるだけ
+ u.searchParams.set('static', '1');
+ return u.href;
+ }
+
+ if (u.href.startsWith(this.serverMetadata.mediaProxy + '/')) {
+ // もう既にproxyっぽそうだったらsearchParams付けるだけ
+ u.searchParams.set('static', '1');
+ return u.href;
+ }
+
+ return `${this.serverMetadata.mediaProxy}/static.webp?${query({
+ url: u.href,
+ static: '1',
+ })}`;
+ }
+}
diff --git a/packages/frontend/src/scripts/scroll.ts b/packages/frontend-shared/js/scroll.ts
similarity index 98%
rename from packages/frontend/src/scripts/scroll.ts
rename to packages/frontend-shared/js/scroll.ts
index f0274034b5b3..1062e5252feb 100644
--- a/packages/frontend/src/scripts/scroll.ts
+++ b/packages/frontend-shared/js/scroll.ts
@@ -45,7 +45,7 @@ export function onScrollTop(el: HTMLElement, cb: () => unknown, tolerance = 1, o
const container = getScrollContainer(el) ?? window;
- const onScroll = ev => {
+ const onScroll = () => {
if (!document.body.contains(el)) return;
if (isTopVisible(el, tolerance)) {
cb();
@@ -69,7 +69,7 @@ export function onScrollBottom(el: HTMLElement, cb: () => unknown, tolerance = 1
}
const containerOrWindow = container ?? window;
- const onScroll = ev => {
+ const onScroll = () => {
if (!document.body.contains(el)) return;
if (isBottomVisible(el, 1, container)) {
cb();
diff --git a/packages/frontend/src/scripts/url.ts b/packages/frontend-shared/js/url.ts
similarity index 70%
rename from packages/frontend/src/scripts/url.ts
rename to packages/frontend-shared/js/url.ts
index 5a8265af9e10..eb830b1eeaec 100644
--- a/packages/frontend/src/scripts/url.ts
+++ b/packages/frontend-shared/js/url.ts
@@ -8,18 +8,18 @@
* 2. プロパティがundefinedの時はクエリを付けない
* (new URLSearchParams(obj)ではそこまで丁寧なことをしてくれない)
*/
-export function query(obj: Record): string {
+export function query(obj: Record): string {
const params = Object.entries(obj)
- .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined)
- .reduce((a, [k, v]) => (a[k] = v, a), {} as Record);
+ .filter(([, v]) => Array.isArray(v) ? v.length : v !== undefined) // eslint-disable-line @typescript-eslint/no-unnecessary-condition
+ .reduce>((a, [k, v]) => (a[k] = v, a), {});
return Object.entries(params)
.map((p) => `${p[0]}=${encodeURIComponent(p[1])}`)
.join('&');
}
-export function appendQuery(url: string, query: string): string {
- return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${query}`;
+export function appendQuery(url: string, queryString: string): string {
+ return `${url}${/\?/.test(url) ? url.endsWith('?') ? '' : '&' : '?'}${queryString}`;
}
export function extractDomain(url: string) {
diff --git a/packages/frontend/src/scripts/use-document-visibility.ts b/packages/frontend-shared/js/use-document-visibility.ts
similarity index 85%
rename from packages/frontend/src/scripts/use-document-visibility.ts
rename to packages/frontend-shared/js/use-document-visibility.ts
index a8f4d5e03ae2..b1197e68dae2 100644
--- a/packages/frontend/src/scripts/use-document-visibility.ts
+++ b/packages/frontend-shared/js/use-document-visibility.ts
@@ -3,7 +3,8 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { onMounted, onUnmounted, ref, Ref } from 'vue';
+import { onMounted, onUnmounted, ref } from 'vue';
+import type { Ref } from 'vue';
export function useDocumentVisibility(): Ref {
const visibility = ref(document.visibilityState);
diff --git a/packages/frontend/src/scripts/use-interval.ts b/packages/frontend-shared/js/use-interval.ts
similarity index 100%
rename from packages/frontend/src/scripts/use-interval.ts
rename to packages/frontend-shared/js/use-interval.ts
diff --git a/packages/frontend-shared/package.json b/packages/frontend-shared/package.json
new file mode 100644
index 000000000000..9981d10dd25d
--- /dev/null
+++ b/packages/frontend-shared/package.json
@@ -0,0 +1,39 @@
+{
+ "name": "frontend-shared",
+ "type": "module",
+ "main": "./js-built/index.js",
+ "types": "./js-built/index.d.ts",
+ "exports": {
+ ".": {
+ "import": "./js-built/index.js",
+ "types": "./js-built/index.d.ts"
+ },
+ "./*": {
+ "import": "./js-built/*",
+ "types": "./js-built/*"
+ }
+ },
+ "scripts": {
+ "build": "node ./build.js",
+ "watch": "nodemon -w package.json -e json --exec \"node ./build.js --watch\"",
+ "eslint": "eslint './**/*.{js,jsx,ts,tsx}'",
+ "typecheck": "tsc --noEmit",
+ "lint": "pnpm typecheck && pnpm eslint"
+ },
+ "devDependencies": {
+ "@types/node": "20.14.12",
+ "@typescript-eslint/eslint-plugin": "7.17.0",
+ "@typescript-eslint/parser": "7.17.0",
+ "esbuild": "0.23.0",
+ "eslint-plugin-vue": "9.27.0",
+ "typescript": "5.5.4",
+ "vue-eslint-parser": "9.4.3"
+ },
+ "files": [
+ "js-built"
+ ],
+ "dependencies": {
+ "misskey-js": "workspace:*",
+ "vue": "3.4.37"
+ }
+}
diff --git a/packages/frontend/src/themes/_dark.json5 b/packages/frontend-shared/themes/_dark.json5
similarity index 100%
rename from packages/frontend/src/themes/_dark.json5
rename to packages/frontend-shared/themes/_dark.json5
diff --git a/packages/frontend/src/themes/_light.json5 b/packages/frontend-shared/themes/_light.json5
similarity index 100%
rename from packages/frontend/src/themes/_light.json5
rename to packages/frontend-shared/themes/_light.json5
diff --git a/packages/frontend/src/themes/d-astro.json5 b/packages/frontend-shared/themes/d-astro.json5
similarity index 100%
rename from packages/frontend/src/themes/d-astro.json5
rename to packages/frontend-shared/themes/d-astro.json5
diff --git a/packages/frontend/src/themes/d-botanical.json5 b/packages/frontend-shared/themes/d-botanical.json5
similarity index 100%
rename from packages/frontend/src/themes/d-botanical.json5
rename to packages/frontend-shared/themes/d-botanical.json5
diff --git a/packages/frontend/src/themes/d-cherry.json5 b/packages/frontend-shared/themes/d-cherry.json5
similarity index 100%
rename from packages/frontend/src/themes/d-cherry.json5
rename to packages/frontend-shared/themes/d-cherry.json5
diff --git a/packages/frontend/src/themes/d-dark.json5 b/packages/frontend-shared/themes/d-dark.json5
similarity index 100%
rename from packages/frontend/src/themes/d-dark.json5
rename to packages/frontend-shared/themes/d-dark.json5
diff --git a/packages/frontend/src/themes/d-future.json5 b/packages/frontend-shared/themes/d-future.json5
similarity index 100%
rename from packages/frontend/src/themes/d-future.json5
rename to packages/frontend-shared/themes/d-future.json5
diff --git a/packages/frontend/src/themes/d-green-lime.json5 b/packages/frontend-shared/themes/d-green-lime.json5
similarity index 100%
rename from packages/frontend/src/themes/d-green-lime.json5
rename to packages/frontend-shared/themes/d-green-lime.json5
diff --git a/packages/frontend/src/themes/d-green-orange.json5 b/packages/frontend-shared/themes/d-green-orange.json5
similarity index 100%
rename from packages/frontend/src/themes/d-green-orange.json5
rename to packages/frontend-shared/themes/d-green-orange.json5
diff --git a/packages/frontend/src/themes/d-ice.json5 b/packages/frontend-shared/themes/d-ice.json5
similarity index 100%
rename from packages/frontend/src/themes/d-ice.json5
rename to packages/frontend-shared/themes/d-ice.json5
diff --git a/packages/frontend/src/themes/d-persimmon.json5 b/packages/frontend-shared/themes/d-persimmon.json5
similarity index 100%
rename from packages/frontend/src/themes/d-persimmon.json5
rename to packages/frontend-shared/themes/d-persimmon.json5
diff --git a/packages/frontend/src/themes/d-u0.json5 b/packages/frontend-shared/themes/d-u0.json5
similarity index 100%
rename from packages/frontend/src/themes/d-u0.json5
rename to packages/frontend-shared/themes/d-u0.json5
diff --git a/packages/frontend/src/themes/l-apricot.json5 b/packages/frontend-shared/themes/l-apricot.json5
similarity index 100%
rename from packages/frontend/src/themes/l-apricot.json5
rename to packages/frontend-shared/themes/l-apricot.json5
diff --git a/packages/frontend/src/themes/l-botanical.json5 b/packages/frontend-shared/themes/l-botanical.json5
similarity index 100%
rename from packages/frontend/src/themes/l-botanical.json5
rename to packages/frontend-shared/themes/l-botanical.json5
diff --git a/packages/frontend/src/themes/l-cherry.json5 b/packages/frontend-shared/themes/l-cherry.json5
similarity index 100%
rename from packages/frontend/src/themes/l-cherry.json5
rename to packages/frontend-shared/themes/l-cherry.json5
diff --git a/packages/frontend/src/themes/l-coffee.json5 b/packages/frontend-shared/themes/l-coffee.json5
similarity index 100%
rename from packages/frontend/src/themes/l-coffee.json5
rename to packages/frontend-shared/themes/l-coffee.json5
diff --git a/packages/frontend/src/themes/l-light.json5 b/packages/frontend-shared/themes/l-light.json5
similarity index 100%
rename from packages/frontend/src/themes/l-light.json5
rename to packages/frontend-shared/themes/l-light.json5
diff --git a/packages/frontend/src/themes/l-rainy.json5 b/packages/frontend-shared/themes/l-rainy.json5
similarity index 100%
rename from packages/frontend/src/themes/l-rainy.json5
rename to packages/frontend-shared/themes/l-rainy.json5
diff --git a/packages/frontend/src/themes/l-sushi.json5 b/packages/frontend-shared/themes/l-sushi.json5
similarity index 100%
rename from packages/frontend/src/themes/l-sushi.json5
rename to packages/frontend-shared/themes/l-sushi.json5
diff --git a/packages/frontend/src/themes/l-u0.json5 b/packages/frontend-shared/themes/l-u0.json5
similarity index 100%
rename from packages/frontend/src/themes/l-u0.json5
rename to packages/frontend-shared/themes/l-u0.json5
diff --git a/packages/frontend/src/themes/l-vivid.json5 b/packages/frontend-shared/themes/l-vivid.json5
similarity index 100%
rename from packages/frontend/src/themes/l-vivid.json5
rename to packages/frontend-shared/themes/l-vivid.json5
diff --git a/packages/frontend-shared/tsconfig.json b/packages/frontend-shared/tsconfig.json
new file mode 100644
index 000000000000..fa0b765534bb
--- /dev/null
+++ b/packages/frontend-shared/tsconfig.json
@@ -0,0 +1,34 @@
+{
+ "$schema": "https://json.schemastore.org/tsconfig",
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "nodenext",
+ "moduleResolution": "nodenext",
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": false,
+ "outDir": "./js-built/",
+ "removeComments": true,
+ "resolveJsonModule": true,
+ "strict": true,
+ "strictFunctionTypes": true,
+ "strictNullChecks": true,
+ "experimentalDecorators": true,
+ "noImplicitReturns": true,
+ "esModuleInterop": true,
+ "typeRoots": [
+ "./node_modules/@types"
+ ],
+ "lib": [
+ "esnext",
+ "dom"
+ ]
+ },
+ "include": [
+ "js/**/*"
+ ],
+ "exclude": [
+ "node_modules",
+ "test/**/*"
+ ]
+}
diff --git a/packages/frontend/.storybook/preload-theme.ts b/packages/frontend/.storybook/preload-theme.ts
index fb93d7be1353..e5573f2ac3a0 100644
--- a/packages/frontend/.storybook/preload-theme.ts
+++ b/packages/frontend/.storybook/preload-theme.ts
@@ -30,7 +30,7 @@ const keys = [
'd-u0',
]
-await Promise.all(keys.map((key) => readFile(new URL(`../src/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => {
+await Promise.all(keys.map((key) => readFile(new URL(`../../frontend-shared/themes/${key}.json5`, import.meta.url), 'utf8'))).then((sources) => {
writeFile(
new URL('./themes.ts', import.meta.url),
`export default ${JSON.stringify(
diff --git a/packages/frontend/@types/theme.d.ts b/packages/frontend/@types/theme.d.ts
index 0a7281898d98..70afc356c19d 100644
--- a/packages/frontend/@types/theme.d.ts
+++ b/packages/frontend/@types/theme.d.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-declare module '@/themes/*.json5' {
+declare module '@@/themes/*.json5' {
import { Theme } from '@/scripts/theme.js';
const theme: Theme;
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 1464be18a7fa..67be7f0598a0 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -55,6 +55,7 @@
"misskey-bubble-game": "workspace:*",
"misskey-js": "workspace:*",
"misskey-reversi": "workspace:*",
+ "frontend-shared": "workspace:*",
"photoswipe": "5.4.4",
"punycode": "2.3.1",
"rollup": "4.19.1",
diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index d86ae18ffeac..19d30f64cebd 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -22,7 +22,8 @@ import { getAccountFromId } from '@/scripts/get-account-from-id.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { miLocalStorage } from '@/local-storage.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
-import { setupRouter } from '@/router/definition.js';
+import { setupRouter } from '@/router/main.js';
+import { createMainRouter } from '@/router/definition.js';
export async function common(createVue: () => App) {
console.info(`Misskey v${version}`);
@@ -239,7 +240,7 @@ export async function common(createVue: () => App) {
const app = createVue();
- setupRouter(app);
+ setupRouter(app, createMainRouter);
if (_DEV_) {
app.config.performance = true;
diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts
index 3e7c4f26f807..b31281dcf286 100644
--- a/packages/frontend/src/boot/main-boot.ts
+++ b/packages/frontend/src/boot/main-boot.ts
@@ -22,6 +22,7 @@ import { deckStore } from '@/ui/deck/deck-store.js';
import { emojiPicker } from '@/scripts/emoji-picker.js';
import { mainRouter } from '@/router/main.js';
import { type Keymap, makeHotkey } from '@/scripts/hotkey.js';
+import { addCustomEmoji, removeCustomEmojis, updateCustomEmojis } from '@/custom-emojis.js';
export async function mainBoot() {
const { isClientUpdated } = await common(() => createApp(
@@ -62,6 +63,18 @@ export async function mainBoot() {
}
});
+ stream.on('emojiAdded', emojiData => {
+ addCustomEmoji(emojiData.emoji);
+ });
+
+ stream.on('emojiUpdated', emojiData => {
+ updateCustomEmojis(emojiData.emojis);
+ });
+
+ stream.on('emojiDeleted', emojiData => {
+ removeCustomEmojis(emojiData.emojis);
+ });
+
for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) {
import('@/plugin.js').then(async ({ install }) => {
// Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740
diff --git a/packages/frontend/src/components/MkAutocomplete.vue b/packages/frontend/src/components/MkAutocomplete.vue
index 932c4ecb2e9c..f54799136937 100644
--- a/packages/frontend/src/components/MkAutocomplete.vue
+++ b/packages/frontend/src/components/MkAutocomplete.vue
@@ -46,17 +46,17 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
diff --git a/packages/frontend/src/components/MkEmojiPicker.section.vue b/packages/frontend/src/components/MkEmojiPicker.section.vue
index c13164c2968d..fca7aa2f4ec1 100644
--- a/packages/frontend/src/components/MkEmojiPicker.section.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.section.vue
@@ -62,7 +62,7 @@ SPDX-License-Identifier: AGPL-3.0-only
`,
+ ];
+ return iframeCode.join('\n');
+}
+
+/**
+ * 埋め込みコードを生成してコピーする(カスタマイズ機能つき)
+ *
+ * カスタマイズ機能がいらない場合(事前にパラメータを指定する場合)は getEmbedCode を直接使ってください
+ */
+export function genEmbedCode(entity: EmbeddableEntity, id: string, params?: EmbedParams) {
+ const _params = { ...params };
+
+ if (embedRouteWithScrollbar.includes(entity) && _params.maxHeight == null) {
+ _params.maxHeight = 700;
+ }
+
+ // PCじゃない場合はコードカスタマイズ画面を出さずにそのままコピー
+ if (window.innerWidth < MOBILE_THRESHOLD) {
+ copyToClipboard(getEmbedCode(`/embed/${entity}/${id}`, _params));
+ os.success();
+ } else {
+ const { dispose } = os.popup(defineAsyncComponent(() => import('@/components/MkEmbedCodeGenDialog.vue')), {
+ entity,
+ id,
+ params: _params,
+ }, {
+ closed: () => dispose(),
+ });
+ }
+}
diff --git a/packages/frontend/src/scripts/get-note-menu.ts b/packages/frontend/src/scripts/get-note-menu.ts
index b5d7350a41b6..e0ccea813dc3 100644
--- a/packages/frontend/src/scripts/get-note-menu.ts
+++ b/packages/frontend/src/scripts/get-note-menu.ts
@@ -21,6 +21,7 @@ import { MenuItem } from '@/types/menu.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import { isSupportShare } from '@/scripts/navigator.js';
import { getAppearNote } from '@/scripts/get-appear-note.js';
+import { genEmbedCode } from '@/scripts/get-embed-code.js';
export async function getNoteClipMenu(props: {
note: Misskey.entities.Note;
@@ -156,6 +157,19 @@ export function getCopyNoteLinkMenu(note: Misskey.entities.Note, text: string):
};
}
+function getNoteEmbedCodeMenu(note: Misskey.entities.Note, text: string): MenuItem | undefined {
+ if (note.url != null || note.uri != null) return undefined;
+ if (['specified', 'followers'].includes(note.visibility)) return undefined;
+
+ return {
+ icon: 'ti ti-code',
+ text,
+ action: (): void => {
+ genEmbedCode('notes', note.id);
+ },
+ };
+}
+
export function getNoteMenu(props: {
note: Misskey.entities.Note;
translation: Ref;
@@ -310,7 +324,7 @@ export function getNoteMenu(props: {
action: () => {
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
},
- } : undefined,
+ } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode),
...(isSupportShare() ? [{
icon: 'ti ti-share',
text: i18n.ts.share,
@@ -443,14 +457,14 @@ export function getNoteMenu(props: {
icon: 'ti ti-copy',
text: i18n.ts.copyContent,
action: copyContent,
- }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink)
- , (appearNote.url || appearNote.uri) ? {
+ }, getCopyNoteLinkMenu(appearNote, i18n.ts.copyLink),
+ (appearNote.url || appearNote.uri) ? {
icon: 'ti ti-external-link',
text: i18n.ts.showOnRemote,
action: () => {
window.open(appearNote.url ?? appearNote.uri, '_blank', 'noopener');
},
- } : undefined]
+ } : getNoteEmbedCodeMenu(appearNote, i18n.ts.genEmbedCode)]
.filter(x => x !== undefined);
}
diff --git a/packages/frontend/src/scripts/get-user-menu.ts b/packages/frontend/src/scripts/get-user-menu.ts
index 33f16a68aa61..035abc7bd061 100644
--- a/packages/frontend/src/scripts/get-user-menu.ts
+++ b/packages/frontend/src/scripts/get-user-menu.ts
@@ -17,6 +17,7 @@ import { notesSearchAvailable, canSearchNonLocalNotes } from '@/scripts/check-pe
import { IRouter } from '@/nirax.js';
import { antennasCache, rolesCache, userListsCache } from '@/cache.js';
import { mainRouter } from '@/router/main.js';
+import { genEmbedCode } from '@/scripts/get-embed-code.js';
import { MenuItem } from '@/types/menu.js';
export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter = mainRouter) {
@@ -179,7 +180,17 @@ export function getUserMenu(user: Misskey.entities.UserDetailed, router: IRouter
if (user.url == null) return;
window.open(user.url, '_blank', 'noopener');
},
- }] : []), {
+ }] : [{
+ icon: 'ti ti-code',
+ text: i18n.ts.genEmbedCode,
+ type: 'parent' as const,
+ children: [{
+ text: i18n.ts.noteOfThisUser,
+ action: () => {
+ genEmbedCode('user-timeline', user.id);
+ },
+ }], // TODO: ユーザーカードの埋め込みなど
+ }]), {
icon: 'ti ti-share',
text: i18n.ts.copyProfileUrl,
action: () => {
diff --git a/packages/frontend/src/scripts/idb-proxy.ts b/packages/frontend/src/scripts/idb-proxy.ts
index 6b511f2a5fc7..20f51660c725 100644
--- a/packages/frontend/src/scripts/idb-proxy.ts
+++ b/packages/frontend/src/scripts/idb-proxy.ts
@@ -10,10 +10,11 @@ import {
set as iset,
del as idel,
} from 'idb-keyval';
+import { miLocalStorage } from '@/local-storage.js';
-const fallbackName = (key: string) => `idbfallback::${key}`;
+const PREFIX = 'idbfallback::';
-let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && window.indexedDB.open) : true;
+let idbAvailable = typeof window !== 'undefined' ? !!(window.indexedDB && typeof window.indexedDB.open === 'function') : true;
// iframe.contentWindow.indexedDB.deleteDatabase() がchromeのバグで使用できないため、indexedDBを無効化している。
// バグが治って再度有効化するのであれば、cypressのコマンド内のコメントアウトを外すこと
@@ -38,15 +39,15 @@ if (idbAvailable) {
export async function get(key: string) {
if (idbAvailable) return iget(key);
- return JSON.parse(window.localStorage.getItem(fallbackName(key)));
+ return miLocalStorage.getItemAsJson(`${PREFIX}${key}`);
}
export async function set(key: string, val: any) {
if (idbAvailable) return iset(key, val);
- return window.localStorage.setItem(fallbackName(key), JSON.stringify(val));
+ return miLocalStorage.setItemAsJson(`${PREFIX}${key}`, val);
}
export async function del(key: string) {
if (idbAvailable) return idel(key);
- return window.localStorage.removeItem(fallbackName(key));
+ return miLocalStorage.removeItem(`${PREFIX}${key}`);
}
diff --git a/packages/frontend/src/scripts/is-link.ts b/packages/frontend/src/scripts/is-link.ts
new file mode 100644
index 000000000000..946f86400e16
--- /dev/null
+++ b/packages/frontend/src/scripts/is-link.ts
@@ -0,0 +1,12 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export function isLink(el: HTMLElement) {
+ if (el.tagName === 'A') return true;
+ if (el.parentElement) {
+ return isLink(el.parentElement);
+ }
+ return false;
+}
diff --git a/packages/frontend/src/scripts/media-proxy.ts b/packages/frontend/src/scripts/media-proxy.ts
index 099a22163af4..68a5a1dcf886 100644
--- a/packages/frontend/src/scripts/media-proxy.ts
+++ b/packages/frontend/src/scripts/media-proxy.ts
@@ -3,51 +3,32 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { query } from '@/scripts/url.js';
+import { MediaProxy } from '@@/js/media-proxy.js';
import { url } from '@/config.js';
import { instance } from '@/instance.js';
-export function getProxiedImageUrl(imageUrl: string, type?: 'preview' | 'emoji' | 'avatar', mustOrigin = false, noFallback = false): string {
- const localProxy = `${url}/proxy`;
+let _mediaProxy: MediaProxy | null = null;
- if (imageUrl.startsWith(instance.mediaProxy + '/') || imageUrl.startsWith('/proxy/') || imageUrl.startsWith(localProxy + '/')) {
- // もう既にproxyっぽそうだったらurlを取り出す
- imageUrl = (new URL(imageUrl)).searchParams.get('url') ?? imageUrl;
+export function getProxiedImageUrl(...args: Parameters): string {
+ if (_mediaProxy == null) {
+ _mediaProxy = new MediaProxy(instance, url);
}
- return `${mustOrigin ? localProxy : instance.mediaProxy}/${
- type === 'preview' ? 'preview.webp'
- : 'image.webp'
- }?${query({
- url: imageUrl,
- ...(!noFallback ? { 'fallback': '1' } : {}),
- ...(type ? { [type]: '1' } : {}),
- ...(mustOrigin ? { origin: '1' } : {}),
- })}`;
+ return _mediaProxy.getProxiedImageUrl(...args);
}
-export function getProxiedImageUrlNullable(imageUrl: string | null | undefined, type?: 'preview'): string | null {
- if (imageUrl == null) return null;
- return getProxiedImageUrl(imageUrl, type);
-}
-
-export function getStaticImageUrl(baseUrl: string): string {
- const u = baseUrl.startsWith('http') ? new URL(baseUrl) : new URL(baseUrl, url);
-
- if (u.href.startsWith(`${url}/emoji/`)) {
- // もう既にemojiっぽそうだったらsearchParams付けるだけ
- u.searchParams.set('static', '1');
- return u.href;
+export function getProxiedImageUrlNullable(...args: Parameters): string | null {
+ if (_mediaProxy == null) {
+ _mediaProxy = new MediaProxy(instance, url);
}
- if (u.href.startsWith(instance.mediaProxy + '/')) {
- // もう既にproxyっぽそうだったらsearchParams付けるだけ
- u.searchParams.set('static', '1');
- return u.href;
+ return _mediaProxy.getProxiedImageUrlNullable(...args);
+}
+
+export function getStaticImageUrl(...args: Parameters): string {
+ if (_mediaProxy == null) {
+ _mediaProxy = new MediaProxy(instance, url);
}
- return `${instance.mediaProxy}/static.webp?${query({
- url: u.href,
- static: '1',
- })}`;
+ return _mediaProxy.getStaticImageUrl(...args);
}
diff --git a/packages/frontend/src/scripts/mfm-function-picker.ts b/packages/frontend/src/scripts/mfm-function-picker.ts
index 9938e534c139..bf59fe98a0ed 100644
--- a/packages/frontend/src/scripts/mfm-function-picker.ts
+++ b/packages/frontend/src/scripts/mfm-function-picker.ts
@@ -6,7 +6,7 @@
import { Ref, nextTick } from 'vue';
import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
-import { MFM_TAGS } from '@/const.js';
+import { MFM_TAGS } from '@@/js/const.js';
import type { MenuItem } from '@/types/menu.js';
/**
diff --git a/packages/frontend/src/scripts/popout.ts b/packages/frontend/src/scripts/popout.ts
index 1caa2dfc2101..ed49611b4f37 100644
--- a/packages/frontend/src/scripts/popout.ts
+++ b/packages/frontend/src/scripts/popout.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { appendQuery } from './url.js';
+import { appendQuery } from '@@/js/url.js';
import * as config from '@/config.js';
export function popout(path: string, w?: HTMLElement) {
diff --git a/packages/frontend/src/scripts/post-message.ts b/packages/frontend/src/scripts/post-message.ts
index 31a9ac1ad9dc..11b6f52ddd0d 100644
--- a/packages/frontend/src/scripts/post-message.ts
+++ b/packages/frontend/src/scripts/post-message.ts
@@ -18,7 +18,7 @@ export type MiPostMessageEvent = {
* 親フレームにイベントを送信
*/
export function postMessageToParentWindow(type: PostMessageEventType, payload?: any): void {
- window.postMessage({
+ window.parent.postMessage({
type,
payload,
}, '*');
diff --git a/packages/frontend/src/scripts/safe-parse.ts b/packages/frontend/src/scripts/safe-parse.ts
deleted file mode 100644
index 6bfcef6c362c..000000000000
--- a/packages/frontend/src/scripts/safe-parse.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function safeParseFloat(str: unknown): number | null {
- if (typeof str !== 'string' || str === '') return null;
- const num = parseFloat(str);
- if (isNaN(num)) return null;
- return num;
-}
diff --git a/packages/frontend/src/scripts/safe-uri-decode.ts b/packages/frontend/src/scripts/safe-uri-decode.ts
deleted file mode 100644
index 0edf4e9eba0f..000000000000
--- a/packages/frontend/src/scripts/safe-uri-decode.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-export function safeURIDecode(str: string): string {
- try {
- return decodeURIComponent(str);
- } catch {
- return str;
- }
-}
diff --git a/packages/frontend/src/scripts/stream-mock.ts b/packages/frontend/src/scripts/stream-mock.ts
new file mode 100644
index 000000000000..cb0e607fcb6a
--- /dev/null
+++ b/packages/frontend/src/scripts/stream-mock.ts
@@ -0,0 +1,81 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { EventEmitter } from 'eventemitter3';
+import * as Misskey from 'misskey-js';
+import type { Channels, StreamEvents, IStream, IChannelConnection } from 'misskey-js';
+
+type AnyOf> = T[keyof T];
+type OmitFirst = T extends [any, ...infer R] ? R : never;
+
+/**
+ * Websocket無効化時に使うStreamのモック(なにもしない)
+ */
+export class StreamMock extends EventEmitter implements IStream {
+ public readonly state = 'initializing';
+
+ constructor(...args: ConstructorParameters) {
+ super();
+ // do nothing
+ }
+
+ public useChannel(channel: C, params?: Channels[C]['params'], name?: string): ChannelConnectionMock {
+ return new ChannelConnectionMock(this, channel, name);
+ }
+
+ public removeSharedConnection(connection: any): void {
+ // do nothing
+ }
+
+ public removeSharedConnectionPool(pool: any): void {
+ // do nothing
+ }
+
+ public disconnectToChannel(): void {
+ // do nothing
+ }
+
+ public send(typeOrPayload: string): void
+ public send(typeOrPayload: string, payload: any): void
+ public send(typeOrPayload: Record | any[]): void
+ public send(typeOrPayload: string | Record | any[], payload?: any): void {
+ // do nothing
+ }
+
+ public ping(): void {
+ // do nothing
+ }
+
+ public heartbeat(): void {
+ // do nothing
+ }
+
+ public close(): void {
+ // do nothing
+ }
+}
+
+class ChannelConnectionMock = any> extends EventEmitter implements IChannelConnection {
+ public id = '';
+ public name?: string; // for debug
+ public inCount = 0; // for debug
+ public outCount = 0; // for debug
+ public channel: string;
+
+ constructor(stream: IStream, ...args: OmitFirst>>) {
+ super();
+
+ this.channel = args[0];
+ this.name = args[1];
+ }
+
+ public send(type: T, body: Channel['receives'][T]): void {
+ // do nothing
+ }
+
+ public dispose(): void {
+ // do nothing
+ }
+}
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index c7f8b3d59663..9b9f1f030c33 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -5,11 +5,11 @@
import { ref } from 'vue';
import tinycolor from 'tinycolor2';
+import lightTheme from '@@/themes/_light.json5';
+import darkTheme from '@@/themes/_dark.json5';
import { deepClone } from './clone.js';
import type { BundledTheme } from 'shiki/themes';
import { globalEvents } from '@/events.js';
-import lightTheme from '@/themes/_light.json5';
-import darkTheme from '@/themes/_dark.json5';
import { miLocalStorage } from '@/local-storage.js';
export type Theme = {
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index 437314074a0c..0bf499bb4d60 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -458,10 +458,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
- contextMenu: {
+ contextMenu: {
where: 'device',
default: 'app' as 'app' | 'appWithShift' | 'native',
- },
+ },
sound_masterVolume: {
where: 'device',
@@ -520,8 +520,8 @@ interface Watcher {
/**
* 常にメモリにロードしておく必要がないような設定情報を保管するストレージ(非リアクティブ)
*/
-import lightTheme from '@/themes/l-light.json5';
-import darkTheme from '@/themes/d-green-lime.json5';
+import lightTheme from '@@/themes/l-light.json5';
+import darkTheme from '@@/themes/d-green-lime.json5';
export class ColdDeviceStorage {
public static default = {
@@ -558,7 +558,7 @@ export class ColdDeviceStorage {
public static set(key: T, value: typeof ColdDeviceStorage.default[T]): void {
// 呼び出し側のバグ等で undefined が来ることがある
// undefined を文字列として miLocalStorage に入れると参照する際の JSON.parse でコケて不具合の元になるため無視
- // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+
if (value === undefined) {
console.error(`attempt to store undefined value for key '${key}'`);
return;
diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts
index 0d5bd78b09af..9d7edce890f6 100644
--- a/packages/frontend/src/stream.ts
+++ b/packages/frontend/src/stream.ts
@@ -7,17 +7,20 @@ import * as Misskey from 'misskey-js';
import { markRaw } from 'vue';
import { $i } from '@/account.js';
import { wsOrigin } from '@/config.js';
+// TODO: No WebsocketモードでStreamMockが使えそう
+//import { StreamMock } from '@/scripts/stream-mock.js';
// heart beat interval in ms
const HEART_BEAT_INTERVAL = 1000 * 60;
-let stream: Misskey.Stream | null = null;
-let timeoutHeartBeat: ReturnType | null = null;
+let stream: Misskey.IStream | null = null;
+let timeoutHeartBeat: number | null = null;
let lastHeartbeatCall = 0;
-export function useStream(): Misskey.Stream {
+export function useStream(): Misskey.IStream {
if (stream) return stream;
+ // TODO: No Websocketモードもここで判定
stream = markRaw(new Misskey.Stream(wsOrigin, $i ? {
token: $i.token,
} : null));
diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue
index 8dad6666235d..e234bb3a33a0 100644
--- a/packages/frontend/src/ui/_common_/statusbar-federation.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue
@@ -35,7 +35,7 @@ import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MarqueeText from '@/components/MkMarquee.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
import { getProxiedImageUrlNullable } from '@/scripts/media-proxy.js';
const props = defineProps<{
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index 6e1d06eec1f9..550fc39b001b 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import { ref } from 'vue';
import * as Misskey from 'misskey-js';
import MarqueeText from '@/components/MkMarquee.vue';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
import { shuffle } from '@/scripts/shuffle.js';
const props = defineProps<{
diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
index 67f8b109c484..078b595dca74 100644
--- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
@@ -35,7 +35,7 @@ import { ref, watch } from 'vue';
import * as Misskey from 'misskey-js';
import MarqueeText from '@/components/MkMarquee.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
-import { useInterval } from '@/scripts/use-interval.js';
+import { useInterval } from '@@/js/use-interval.js';
import { getNoteSummary } from '@/scripts/get-note-summary.js';
import { notePage } from '@/filters/note.js';
diff --git a/packages/frontend/src/ui/deck/main-column.vue b/packages/frontend/src/ui/deck/main-column.vue
index 79c967191709..e7ecf7fd2022 100644
--- a/packages/frontend/src/ui/deck/main-column.vue
+++ b/packages/frontend/src/ui/deck/main-column.vue
@@ -26,7 +26,7 @@ import * as os from '@/os.js';
import { i18n } from '@/i18n.js';
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { useScrollPositionManager } from '@/nirax.js';
-import { getScrollContainer } from '@/scripts/scroll.js';
+import { getScrollContainer } from '@@/js/scroll.js';
import { mainRouter } from '@/router/main.js';
defineProps<{
diff --git a/packages/frontend/src/ui/universal.vue b/packages/frontend/src/ui/universal.vue
index 073acbd4db72..00a6811fc98d 100644
--- a/packages/frontend/src/ui/universal.vue
+++ b/packages/frontend/src/ui/universal.vue
@@ -108,7 +108,7 @@ import { $i } from '@/account.js';
import { PageMetadata, provideMetadataReceiver, provideReactiveMetadata } from '@/scripts/page-metadata.js';
import { deviceKind } from '@/scripts/device-kind.js';
import { miLocalStorage } from '@/local-storage.js';
-import { CURRENT_STICKY_BOTTOM } from '@/const.js';
+import { CURRENT_STICKY_BOTTOM } from '@@/js/const.js';
import { useScrollPositionManager } from '@/nirax.js';
import { mainRouter } from '@/router/main.js';
diff --git a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
index 49fd103d37eb..bcfaaf00ab4f 100644
--- a/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
+++ b/packages/frontend/src/widgets/WidgetBirthdayFollowings.vue
@@ -25,11 +25,11 @@ SPDX-License-Identifier: AGPL-3.0-only
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index b0ffac93d70e..6710d9826e8d 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-import { getHighlighterCore, loadWasm } from 'shiki/core';
+import { createHighlighterCore, loadWasm } from 'shiki/core';
import darkPlus from 'shiki/themes/dark-plus.mjs';
import { bundledThemesInfo } from 'shiki/themes';
import { bundledLanguagesInfo } from 'shiki/langs';
@@ -69,7 +69,7 @@ async function initHighlighter() {
]);
const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript');
- const highlighter = await getHighlighterCore({
+ const highlighter = await createHighlighterCore({
themes,
langs: [
...(jsLangInfo ? [async () => await jsLangInfo.import()] : []),
From 6b2072f4b1e6a191634b51b448442aaf57df5434 Mon Sep 17 00:00:00 2001
From: Kisaragi <48310258+KisaragiEffective@users.noreply.github.com>
Date: Sun, 15 Sep 2024 15:13:46 +0900
Subject: [PATCH 030/222] =?UTF-8?q?fix(backend/antenna):=20=E3=82=AD?=
=?UTF-8?q?=E3=83=BC=E3=83=AF=E3=83=BC=E3=83=89=E3=81=8C=E4=B8=8E=E3=81=88?=
=?UTF-8?q?=E3=82=89=E3=82=8C=E3=81=AA=E3=81=8B=E3=81=A3=E3=81=9F=E5=A0=B4?=
=?UTF-8?q?=E5=90=88=E3=81=AE=E3=82=A8=E3=83=A9=E3=83=BC=E3=82=92ApiError?=
=?UTF-8?q?=E3=81=A8=E3=81=97=E3=81=A6=E6=8A=95=E3=81=92=E3=82=8B=20(#1449?=
=?UTF-8?q?1)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(backend/antenna): report validation failure as ApiError on update
* test(backend/antenna): reflect change in previous commit
* fix(backend/antenna): report validation failure as ApiError on create
* test(backend/antenna): reflect change in previous commit
* test(backend/antenna): semi
* test(backend/antenna): bring being spread parameters first in object literal
* chore: add CHANGELOG entry
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
CHANGELOG.md | 4 +++-
.../server/api/endpoints/antennas/create.ts | 8 ++++++-
.../server/api/endpoints/antennas/update.ts | 8 ++++++-
packages/backend/test/e2e/antennas.ts | 23 +++++++++++++++++++
4 files changed, 40 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc2d9f102eff..62d9d4defaaa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,11 +12,13 @@
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
### Server
+- ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
+- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
+ - この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます
- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
- Fix: 外部ページを解析する際に、ページに紐づけられた関連リソースも読み込まれてしまう問題を修正
(Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/26e0412fbb91447c37e8fb06ffb0487346063bb8)
-
## 2024.8.0
### General
diff --git a/packages/backend/src/server/api/endpoints/antennas/create.ts b/packages/backend/src/server/api/endpoints/antennas/create.ts
index 577b9e1b1f8c..e0c8ddcc8478 100644
--- a/packages/backend/src/server/api/endpoints/antennas/create.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/create.ts
@@ -34,6 +34,12 @@ export const meta = {
code: 'TOO_MANY_ANTENNAS',
id: 'faf47050-e8b5-438c-913c-db2b1576fde4',
},
+
+ emptyKeyword: {
+ message: 'Either keywords or excludeKeywords is required.',
+ code: 'EMPTY_KEYWORD',
+ id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a',
+ },
},
res: {
@@ -87,7 +93,7 @@ export default class extends Endpoint { // eslint-
) {
super(meta, paramDef, async (ps, me) => {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
- throw new Error('either keywords or excludeKeywords is required.');
+ throw new ApiError(meta.errors.emptyKeyword);
}
const currentAntennasCount = await this.antennasRepository.countBy({
diff --git a/packages/backend/src/server/api/endpoints/antennas/update.ts b/packages/backend/src/server/api/endpoints/antennas/update.ts
index 0c30bca9e0bf..10f26b19126e 100644
--- a/packages/backend/src/server/api/endpoints/antennas/update.ts
+++ b/packages/backend/src/server/api/endpoints/antennas/update.ts
@@ -32,6 +32,12 @@ export const meta = {
code: 'NO_SUCH_USER_LIST',
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
},
+
+ emptyKeyword: {
+ message: 'Either keywords or excludeKeywords is required.',
+ code: 'EMPTY_KEYWORD',
+ id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4',
+ },
},
res: {
@@ -85,7 +91,7 @@ export default class extends Endpoint { // eslint-
super(meta, paramDef, async (ps, me) => {
if (ps.keywords && ps.excludeKeywords) {
if (ps.keywords.flat().every(x => x === '') && ps.excludeKeywords.flat().every(x => x === '')) {
- throw new Error('either keywords or excludeKeywords is required.');
+ throw new ApiError(meta.errors.emptyKeyword);
}
}
// Fetch the antenna
diff --git a/packages/backend/test/e2e/antennas.ts b/packages/backend/test/e2e/antennas.ts
index 6ac14cd8dcde..a544db955a07 100644
--- a/packages/backend/test/e2e/antennas.ts
+++ b/packages/backend/test/e2e/antennas.ts
@@ -228,6 +228,17 @@ describe('アンテナ', () => {
assert.deepStrictEqual(response, expected);
});
+ test('を作成する時キーワードが指定されていないとエラーになる', async () => {
+ await failedApiCall({
+ endpoint: 'antennas/create',
+ parameters: { ...defaultParam, keywords: [[]], excludeKeywords: [[]] },
+ user: alice
+ }, {
+ status: 400,
+ code: 'EMPTY_KEYWORD',
+ id: '53ee222e-1ddd-4f9a-92e5-9fb82ddb463a'
+ })
+ });
//#endregion
//#region 更新(antennas/update)
@@ -255,6 +266,18 @@ describe('アンテナ', () => {
id: '1c6b35c9-943e-48c2-81e4-2844989407f7',
});
});
+ test('を変更する時キーワードが指定されていないとエラーになる', async () => {
+ const antenna = await successfulApiCall({ endpoint: 'antennas/create', parameters: defaultParam, user: alice });
+ await failedApiCall({
+ endpoint: 'antennas/update',
+ parameters: { ...defaultParam, antennaId: antenna.id, keywords: [[]], excludeKeywords: [[]] },
+ user: alice
+ }, {
+ status: 400,
+ code: 'EMPTY_KEYWORD',
+ id: '721aaff6-4e1b-4d88-8de6-877fae9f68c4'
+ })
+ });
//#endregion
//#region 表示(antennas/show)
From 366b79e4595b709f5a6b8b4700eb93510d41072a Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Sun, 15 Sep 2024 15:14:13 +0900
Subject: [PATCH 031/222] Update CHANGELOG.md
---
CHANGELOG.md | 1 -
1 file changed, 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 62d9d4defaaa..ffe03c13c7ad 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,7 +12,6 @@
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
### Server
-- ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
- この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます
- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
From 07f26bc8dd199ff366e6278a8ac1521497922b95 Mon Sep 17 00:00:00 2001
From: Juan Aguilar Santillana
Date: Sun, 15 Sep 2024 10:43:24 +0200
Subject: [PATCH 032/222] refactor(backend): use Reflet for autobind deco
(#14482)
Using Reflect.defineProperty instead of Object.defineProperty
gives a more consistent behavior with the rest of the modern
JavaScript features.
---
packages/backend/src/decorators.ts | 21 +++++++--------------
1 file changed, 7 insertions(+), 14 deletions(-)
diff --git a/packages/backend/src/decorators.ts b/packages/backend/src/decorators.ts
index 21777657d185..42f925e1251c 100644
--- a/packages/backend/src/decorators.ts
+++ b/packages/backend/src/decorators.ts
@@ -10,8 +10,9 @@
* The getter will return a .bind version of the function
* and memoize the result against a symbol on the instance
*/
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function bindThis(target: any, key: string, descriptor: any) {
- let fn = descriptor.value;
+ const fn = descriptor.value;
if (typeof fn !== 'function') {
throw new TypeError(`@bindThis decorator can only be applied to methods not: ${typeof fn}`);
@@ -21,26 +22,18 @@ export function bindThis(target: any, key: string, descriptor: any) {
configurable: true,
get() {
// eslint-disable-next-line no-prototype-builtins
- if (this === target.prototype || this.hasOwnProperty(key) ||
- typeof fn !== 'function') {
+ if (this === target.prototype || this.hasOwnProperty(key)) {
return fn;
}
const boundFn = fn.bind(this);
- Object.defineProperty(this, key, {
+ Reflect.defineProperty(this, key, {
+ value: boundFn,
configurable: true,
- get() {
- return boundFn;
- },
- set(value) {
- fn = value;
- delete this[key];
- },
+ writable: true,
});
+
return boundFn;
},
- set(value: any) {
- fn = value;
- },
};
}
From 0e4b6d1dade90673af58af3480081c95984c0274 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sun, 15 Sep 2024 17:50:25 +0900
Subject: [PATCH 033/222] =?UTF-8?q?enhance(frontend):=20admin=E3=81=AE?=
=?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=83=AA=E3=82=B9=E3=83=88?=
=?UTF-8?q?=E3=81=A7=E3=82=BB=E3=83=B3=E3=82=B7=E3=83=86=E3=82=A3=E3=83=96?=
=?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AB=E6=9E=A0=E7=B7=9A?=
=?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=20(#14510)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* enhance(frontend): adminのファイルリストでセンシティブファイルに枠線を追加
* Update Changelog
---
CHANGELOG.md | 1 +
.../src/components/MkDriveFileThumbnail.vue | 21 ++++++++++++++++++-
.../src/components/MkFileListForAdmin.vue | 2 +-
3 files changed, 22 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ffe03c13c7ad..c01d284bdbd4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,7 @@
- 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
- Enhance: アイコンデコレーション管理画面にプレビューを追加
+- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue
index 2c47a709709b..eb93aaab6e76 100644
--- a/packages/frontend/src/components/MkDriveFileThumbnail.vue
+++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue
@@ -4,7 +4,13 @@ SPDX-License-Identifier: AGPL-3.0-only
-->
-
+
@@ -27,6 +33,7 @@ import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
const props = defineProps<{
file: Misskey.entities.DriveFile;
fit: 'cover' | 'contain';
+ highlightWhenSensitive?: boolean;
}>();
const is = computed(() => {
@@ -67,6 +74,18 @@ const isThumbnailAvailable = computed(() => {
overflow: clip;
}
+.sensitiveHighlight::after {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ border-radius: inherit;
+ box-shadow: inset 0 0 0 4px var(--warn);
+}
+
.iconSub {
position: absolute;
width: 30%;
diff --git a/packages/frontend/src/components/MkFileListForAdmin.vue b/packages/frontend/src/components/MkFileListForAdmin.vue
index 30822ef655bd..13295c455b7c 100644
--- a/packages/frontend/src/components/MkFileListForAdmin.vue
+++ b/packages/frontend/src/components/MkFileListForAdmin.vue
@@ -14,7 +14,7 @@ SPDX-License-Identifier: AGPL-3.0-only
class="file _button"
>
{{ i18n.ts.sensitive }}
-
+
{{ file.name }}
From 887c709647bff7e60464a00176ba4c9ba7e6c127 Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Sun, 15 Sep 2024 20:54:26 +0900
Subject: [PATCH 034/222] chore(deps): bump body-parser from 1.20.2 to 1.20.3
in /packages/backend (#14550)
Bumps [body-parser](https://github.com/expressjs/body-parser) from 1.20.2 to 1.20.3.
- [Release notes](https://github.com/expressjs/body-parser/releases)
- [Changelog](https://github.com/expressjs/body-parser/blob/master/HISTORY.md)
- [Commits](https://github.com/expressjs/body-parser/compare/1.20.2...1.20.3)
---
updated-dependencies:
- dependency-name: body-parser
dependency-type: direct:production
...
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
packages/backend/package.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 797eddcf7de8..a06fd9156b7a 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -100,7 +100,7 @@
"async-mutex": "0.5.0",
"bcryptjs": "2.4.3",
"blurhash": "2.0.5",
- "body-parser": "1.20.2",
+ "body-parser": "1.20.3",
"bullmq": "5.10.4",
"cacheable-lookup": "7.0.0",
"cbor": "9.0.2",
From 7d7a12d7d6bd742a8729d7645d3327e3f6ef9123 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Sun, 15 Sep 2024 21:57:22 +0900
Subject: [PATCH 035/222] fix(deps): broken lockfile (#14556)
---
pnpm-lock.yaml | 278 +++++++++++++++++++++++++++----------------------
1 file changed, 155 insertions(+), 123 deletions(-)
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 3e5250ce7e05..e3240f3108d4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -189,8 +189,8 @@ importers:
specifier: 2.0.5
version: 2.0.5
body-parser:
- specifier: 1.20.2
- version: 1.20.2
+ specifier: 1.20.3
+ version: 1.20.3
bullmq:
specifier: 5.10.4
version: 5.10.4
@@ -1202,7 +1202,7 @@ importers:
version: 7.17.0(eslint@9.8.0)(typescript@5.5.4)
'@vitest/coverage-v8':
specifier: 1.6.0
- version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.77.8)(terser@5.31.3))
+ version: 1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1(bufferutil@4.0.8)(utf-8-validate@6.0.4))(sass@1.77.8)(terser@5.31.3))
'@vue/runtime-core':
specifier: 3.4.37
version: 3.4.37
@@ -6132,6 +6132,10 @@ packages:
resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+ body-parser@1.20.3:
+ resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
+ engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
+
boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
@@ -6258,6 +6262,10 @@ packages:
call-bind@1.0.2:
resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
+ call-bind@1.0.7:
+ resolution: {integrity: sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==}
+ engines: {node: '>= 0.4'}
+
call-me-maybe@1.0.2:
resolution: {integrity: sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ==}
@@ -6847,6 +6855,10 @@ packages:
resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==}
engines: {node: '>=10'}
+ define-data-property@1.1.4:
+ resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==}
+ engines: {node: '>= 0.4'}
+
define-lazy-prop@2.0.0:
resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
engines: {node: '>=8'}
@@ -7055,6 +7067,14 @@ packages:
resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==}
engines: {node: '>= 0.4'}
+ es-define-property@1.0.0:
+ resolution: {integrity: sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==}
+ engines: {node: '>= 0.4'}
+
+ es-errors@1.3.0:
+ resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==}
+ engines: {node: '>= 0.4'}
+
es-get-iterator@1.1.3:
resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==}
@@ -7615,6 +7635,10 @@ packages:
get-intrinsic@1.2.1:
resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==}
+ get-intrinsic@1.2.4:
+ resolution: {integrity: sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==}
+ engines: {node: '>= 0.4'}
+
get-package-type@0.1.0:
resolution: {integrity: sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==}
engines: {node: '>=8.0.0'}
@@ -7813,6 +7837,9 @@ packages:
has-property-descriptors@1.0.0:
resolution: {integrity: sha512-62DVLZGoiEBDHQyqG4w9xCuZ7eJEwNmJRWw2VY84Oedb7WFcA27fiEVe8oUQx9hAUJ4ekurquucTGwsyO1XGdQ==}
+ has-property-descriptors@1.0.2:
+ resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
+
has-proto@1.0.1:
resolution: {integrity: sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==}
engines: {node: '>= 0.4'}
@@ -9487,6 +9514,10 @@ packages:
object-inspect@1.12.3:
resolution: {integrity: sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==}
+ object-inspect@1.13.2:
+ resolution: {integrity: sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==}
+ engines: {node: '>= 0.4'}
+
object-is@1.1.5:
resolution: {integrity: sha512-3cyDsyHgtmi7I7DfSSI2LDp6SK2lwvtbg0p0R1e0RvTqF5ceGx+K2dfSjm1bKDMVCFEDAQvy+o8c6a7VujOddw==}
engines: {node: '>= 0.4'}
@@ -10286,6 +10317,10 @@ packages:
resolution: {integrity: sha512-0wsrzgTz/kAVIeuxSjnpGC56rzYtr6JT/2BwEvMaPhFIoYa1aGO8LbzuU1R0uUYQkLpWBTOj0l/CLAJB64J6nQ==}
engines: {node: '>=0.6'}
+ qs@6.13.0:
+ resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
+ engines: {node: '>=0.6'}
+
qs@6.5.3:
resolution: {integrity: sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==}
engines: {node: '>=0.6'}
@@ -10692,6 +10727,10 @@ packages:
set-cookie-parser@2.6.0:
resolution: {integrity: sha512-RVnVQxTXuerk653XfuliOxBP81Sf0+qfQE73LIYKcyMYHG94AuH0kgrQpRDuTZnSmjpysHmzxJXKNfa6PjFhyQ==}
+ set-function-length@1.2.2:
+ resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
+ engines: {node: '>= 0.4'}
+
setimmediate@1.0.5:
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
@@ -10735,6 +10774,10 @@ packages:
side-channel@1.0.4:
resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==}
+ side-channel@1.0.6:
+ resolution: {integrity: sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==}
+ engines: {node: '>= 0.4'}
+
siginfo@2.0.0:
resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==}
@@ -11337,6 +11380,7 @@ packages:
ts-case-convert@2.0.2:
resolution: {integrity: sha512-vdKfx1VAdpvEBOBv5OpVu5ZFqRg9HdTI4sYt6qqMeICBeNyXvitrarCnFWNDAki51IKwCyx+ZssY46Q9jH5otA==}
+ bundledDependencies: []
ts-dedent@2.2.0:
resolution: {integrity: sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==}
@@ -12678,7 +12722,7 @@ snapshots:
'@babel/traverse': 7.23.5
'@babel/types': 7.24.7
convert-source-map: 2.0.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -12698,7 +12742,7 @@ snapshots:
'@babel/traverse': 7.24.7
'@babel/types': 7.24.7
convert-source-map: 2.0.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@@ -12773,7 +12817,7 @@ snapshots:
'@babel/core': 7.24.7
'@babel/helper-compilation-targets': 7.24.7
'@babel/helper-plugin-utils': 7.24.7
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
lodash.debounce: 4.0.8
resolve: 1.22.8
transitivePeerDependencies:
@@ -13640,7 +13684,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.24.7
'@babel/types': 7.24.7
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -13655,7 +13699,7 @@ snapshots:
'@babel/helper-split-export-declaration': 7.24.7
'@babel/parser': 7.24.7
'@babel/types': 7.24.7
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@@ -14115,7 +14159,7 @@ snapshots:
'@eslint/config-array@0.17.1':
dependencies:
'@eslint/object-schema': 2.1.4
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@@ -14123,7 +14167,7 @@ snapshots:
'@eslint/eslintrc@3.1.0':
dependencies:
ajv: 6.12.6
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
espree: 10.1.0
globals: 14.0.0
ignore: 5.3.1
@@ -17289,7 +17333,7 @@ snapshots:
'@typescript-eslint/types': 7.17.0
'@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4)
'@typescript-eslint/visitor-keys': 7.17.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
eslint: 9.8.0
optionalDependencies:
typescript: 5.5.4
@@ -17315,7 +17359,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3)
'@typescript-eslint/utils': 6.11.0(eslint@9.8.0)(typescript@5.3.3)
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
eslint: 9.8.0
ts-api-utils: 1.0.1(typescript@5.3.3)
optionalDependencies:
@@ -17327,7 +17371,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 7.1.0(typescript@5.3.3)
'@typescript-eslint/utils': 7.1.0(eslint@9.8.0)(typescript@5.3.3)
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
eslint: 9.8.0
ts-api-utils: 1.0.1(typescript@5.3.3)
optionalDependencies:
@@ -17339,7 +17383,7 @@ snapshots:
dependencies:
'@typescript-eslint/typescript-estree': 7.17.0(typescript@5.5.4)
'@typescript-eslint/utils': 7.17.0(eslint@9.8.0)(typescript@5.5.4)
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
eslint: 9.8.0
ts-api-utils: 1.3.0(typescript@5.5.4)
optionalDependencies:
@@ -17357,7 +17401,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 6.11.0
'@typescript-eslint/visitor-keys': 6.11.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
semver: 7.6.0
@@ -17371,7 +17415,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 7.1.0
'@typescript-eslint/visitor-keys': 7.1.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
@@ -17386,7 +17430,7 @@ snapshots:
dependencies:
'@typescript-eslint/types': 7.17.0
'@typescript-eslint/visitor-keys': 7.17.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.4
@@ -17462,7 +17506,7 @@ snapshots:
dependencies:
'@ampproject/remapping': 2.2.1
'@bcoe/v8-coverage': 0.2.3
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
istanbul-lib-report: 3.0.1
istanbul-lib-source-maps: 5.0.4
@@ -17477,25 +17521,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@vitest/coverage-v8@1.6.0(vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.77.8)(terser@5.31.3))':
- dependencies:
- '@ampproject/remapping': 2.2.1
- '@bcoe/v8-coverage': 0.2.3
- debug: 4.3.5(supports-color@5.5.0)
- istanbul-lib-coverage: 3.2.2
- istanbul-lib-report: 3.0.1
- istanbul-lib-source-maps: 5.0.4
- istanbul-reports: 3.1.6
- magic-string: 0.30.10
- magicast: 0.3.4
- picocolors: 1.0.1
- std-env: 3.7.0
- strip-literal: 2.1.0
- test-exclude: 6.0.0
- vitest: 1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.77.8)(terser@5.31.3)
- transitivePeerDependencies:
- - supports-color
-
'@vitest/expect@1.6.0':
dependencies:
'@vitest/spy': 1.6.0
@@ -17723,13 +17748,13 @@ snapshots:
agent-base@6.0.2:
dependencies:
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
agent-base@7.1.0:
dependencies:
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@@ -17987,7 +18012,7 @@ snapshots:
dependencies:
'@fastify/error': 3.4.0
archy: 1.0.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
fastq: 1.17.1
transitivePeerDependencies:
- supports-color
@@ -18169,6 +18194,23 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ body-parser@1.20.3:
+ dependencies:
+ bytes: 3.1.2
+ content-type: 1.0.5
+ debug: 2.6.9
+ depd: 2.0.0
+ destroy: 1.2.0
+ http-errors: 2.0.0
+ iconv-lite: 0.4.24
+ on-finished: 2.4.1
+ qs: 6.13.0
+ raw-body: 2.5.2
+ type-is: 1.6.18
+ unpipe: 1.0.0
+ transitivePeerDependencies:
+ - supports-color
+
boolbase@1.0.0: {}
bowser@2.11.0: {}
@@ -18334,6 +18376,14 @@ snapshots:
function-bind: 1.1.2
get-intrinsic: 1.2.1
+ call-bind@1.0.7:
+ dependencies:
+ es-define-property: 1.0.0
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.2.4
+ set-function-length: 1.2.2
+
call-me-maybe@1.0.2: {}
callsites@3.1.0: {}
@@ -19006,6 +19056,12 @@ snapshots:
defer-to-connect@2.0.1: {}
+ define-data-property@1.1.4:
+ dependencies:
+ es-define-property: 1.0.0
+ es-errors: 1.3.0
+ gopd: 1.0.1
+
define-lazy-prop@2.0.0: {}
define-properties@1.2.0:
@@ -19041,7 +19097,7 @@ snapshots:
detect-port@1.5.1:
dependencies:
address: 1.2.2
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@@ -19212,6 +19268,12 @@ snapshots:
unbox-primitive: 1.0.2
which-typed-array: 1.1.11
+ es-define-property@1.0.0:
+ dependencies:
+ get-intrinsic: 1.2.4
+
+ es-errors@1.3.0: {}
+
es-get-iterator@1.1.3:
dependencies:
call-bind: 1.0.2
@@ -19254,7 +19316,7 @@ snapshots:
esbuild-register@3.5.0(esbuild@0.19.11):
dependencies:
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
esbuild: 0.19.11
transitivePeerDependencies:
- supports-color
@@ -19484,7 +19546,7 @@ snapshots:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
escape-string-regexp: 4.0.0
eslint-scope: 8.0.2
eslint-visitor-keys: 4.0.0
@@ -19937,7 +19999,7 @@ snapshots:
follow-redirects@1.15.2(debug@4.3.5):
optionalDependencies:
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
for-each@0.3.3:
dependencies:
@@ -20056,6 +20118,14 @@ snapshots:
has-proto: 1.0.1
has-symbols: 1.0.3
+ get-intrinsic@1.2.4:
+ dependencies:
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ has-proto: 1.0.1
+ has-symbols: 1.0.3
+ hasown: 2.0.0
+
get-package-type@0.1.0: {}
get-pixels-frame-info-update@3.3.2:
@@ -20221,7 +20291,7 @@ snapshots:
gopd@1.0.1:
dependencies:
- get-intrinsic: 1.2.1
+ get-intrinsic: 1.2.4
got@11.8.5:
dependencies:
@@ -20318,6 +20388,10 @@ snapshots:
dependencies:
get-intrinsic: 1.2.1
+ has-property-descriptors@1.0.2:
+ dependencies:
+ es-define-property: 1.0.0
+
has-proto@1.0.1: {}
has-symbols@1.0.3: {}
@@ -20403,7 +20477,7 @@ snapshots:
http-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@@ -20442,28 +20516,28 @@ snapshots:
https-proxy-agent@5.0.1:
dependencies:
agent-base: 6.0.2
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.2:
dependencies:
agent-base: 7.1.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.4:
dependencies:
agent-base: 7.1.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
https-proxy-agent@7.0.5:
dependencies:
agent-base: 7.1.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
transitivePeerDependencies:
- supports-color
@@ -20811,7 +20885,7 @@ snapshots:
istanbul-lib-source-maps@4.0.1:
dependencies:
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@@ -20820,7 +20894,7 @@ snapshots:
istanbul-lib-source-maps@5.0.4:
dependencies:
'@jridgewell/trace-mapping': 0.3.25
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
istanbul-lib-coverage: 3.2.2
transitivePeerDependencies:
- supports-color
@@ -21244,35 +21318,6 @@ snapshots:
transitivePeerDependencies:
- supports-color
- jsdom@24.1.1:
- dependencies:
- cssstyle: 4.0.1
- data-urls: 5.0.0
- decimal.js: 10.4.3
- form-data: 4.0.0
- html-encoding-sniffer: 4.0.0
- http-proxy-agent: 7.0.2
- https-proxy-agent: 7.0.5
- is-potential-custom-element-name: 1.0.1
- nwsapi: 2.2.12
- parse5: 7.1.2
- rrweb-cssom: 0.7.1
- saxes: 6.0.0
- symbol-tree: 3.2.4
- tough-cookie: 4.1.4
- w3c-xmlserializer: 5.0.0
- webidl-conversions: 7.0.0
- whatwg-encoding: 3.1.1
- whatwg-mimetype: 4.0.0
- whatwg-url: 14.0.0
- ws: 8.18.0(bufferutil@4.0.8)(utf-8-validate@6.0.4)
- xml-name-validator: 5.0.0
- transitivePeerDependencies:
- - bufferutil
- - supports-color
- - utf-8-validate
- optional: true
-
jsdom@24.1.1(bufferutil@4.0.7)(utf-8-validate@6.0.3):
dependencies:
cssstyle: 4.0.1
@@ -21958,7 +22003,7 @@ snapshots:
micromark@4.0.0:
dependencies:
'@types/debug': 4.1.12
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
decode-named-character-reference: 1.0.2
devlop: 1.1.0
micromark-core-commonmark: 2.0.0
@@ -22433,6 +22478,8 @@ snapshots:
object-inspect@1.12.3: {}
+ object-inspect@1.13.2: {}
+
object-is@1.1.5:
dependencies:
call-bind: 1.0.2
@@ -23219,6 +23266,10 @@ snapshots:
dependencies:
side-channel: 1.0.4
+ qs@6.13.0:
+ dependencies:
+ side-channel: 1.0.6
+
qs@6.5.3: {}
querystringify@2.2.0: {}
@@ -23515,7 +23566,7 @@ snapshots:
require-in-the-middle@7.3.0:
dependencies:
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
module-details-from-path: 1.0.3
resolve: 1.22.8
transitivePeerDependencies:
@@ -23722,6 +23773,15 @@ snapshots:
set-cookie-parser@2.6.0: {}
+ set-function-length@1.2.2:
+ dependencies:
+ define-data-property: 1.1.4
+ es-errors: 1.3.0
+ function-bind: 1.1.2
+ get-intrinsic: 1.2.4
+ gopd: 1.0.1
+ has-property-descriptors: 1.0.2
+
setimmediate@1.0.5: {}
setprototypeof@1.2.0: {}
@@ -23786,6 +23846,13 @@ snapshots:
get-intrinsic: 1.2.1
object-inspect: 1.12.3
+ side-channel@1.0.6:
+ dependencies:
+ call-bind: 1.0.7
+ es-errors: 1.3.0
+ get-intrinsic: 1.2.4
+ object-inspect: 1.13.2
+
siginfo@2.0.0: {}
signal-exit@3.0.7: {}
@@ -23796,7 +23863,7 @@ snapshots:
dependencies:
'@hapi/hoek': 11.0.4
'@hapi/wreck': 18.0.1
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
joi: 17.11.0
transitivePeerDependencies:
- supports-color
@@ -23896,7 +23963,7 @@ snapshots:
socks-proxy-agent@8.0.2:
dependencies:
agent-base: 7.1.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
socks: 2.7.1
transitivePeerDependencies:
- supports-color
@@ -23991,7 +24058,7 @@ snapshots:
arg: 5.0.2
bluebird: 3.7.2
check-more-types: 2.24.0
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
execa: 5.1.1
lazy-ass: 1.6.0
ps-tree: 1.2.0
@@ -24732,7 +24799,7 @@ snapshots:
vite-node@1.6.0(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3):
dependencies:
cac: 6.7.14
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
pathe: 1.1.2
picocolors: 1.0.1
vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
@@ -24801,41 +24868,6 @@ snapshots:
- supports-color
- terser
- vitest@1.6.0(@types/node@20.14.12)(happy-dom@10.0.3)(jsdom@24.1.1)(sass@1.77.8)(terser@5.31.3):
- dependencies:
- '@vitest/expect': 1.6.0
- '@vitest/runner': 1.6.0
- '@vitest/snapshot': 1.6.0
- '@vitest/spy': 1.6.0
- '@vitest/utils': 1.6.0
- acorn-walk: 8.3.2
- chai: 4.3.10
- debug: 4.3.4(supports-color@5.5.0)
- execa: 8.0.1
- local-pkg: 0.5.0
- magic-string: 0.30.10
- pathe: 1.1.2
- picocolors: 1.0.0
- std-env: 3.7.0
- strip-literal: 2.1.0
- tinybench: 2.6.0
- tinypool: 0.8.4
- vite: 5.3.5(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
- vite-node: 1.6.0(@types/node@20.14.12)(sass@1.77.8)(terser@5.31.3)
- why-is-node-running: 2.2.2
- optionalDependencies:
- '@types/node': 20.14.12
- happy-dom: 10.0.3
- jsdom: 24.1.1
- transitivePeerDependencies:
- - less
- - lightningcss
- - sass
- - stylus
- - sugarss
- - supports-color
- - terser
-
void-elements@3.1.0: {}
vscode-jsonrpc@8.2.0: {}
@@ -24899,7 +24931,7 @@ snapshots:
vue-eslint-parser@9.4.3(eslint@9.8.0):
dependencies:
- debug: 4.3.5(supports-color@5.5.0)
+ debug: 4.3.5(supports-color@8.1.1)
eslint: 9.8.0
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3
From 6bd6af440f8d0c98543091d241430295ca4ced71 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Tue, 17 Sep 2024 15:41:52 +0900
Subject: [PATCH 036/222] =?UTF-8?q?fix(frontend):=20=E7=B5=B5=E6=96=87?=
=?UTF-8?q?=E5=AD=97=E9=96=A2=E9=80=A3=E3=81=AE=E3=82=B9=E3=82=BF=E3=82=A4?=
=?UTF-8?q?=E3=83=AB=E3=81=8C=E5=B4=A9=E3=82=8C=E3=81=A6=E3=81=84=E3=82=8B?=
=?UTF-8?q?=E3=81=AE=E3=82=92=E4=BF=AE=E6=AD=A3=20(#14559)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(frontend): 絵文字関連のスタイルが崩れていたのを修正 (MisskeyIO#725)
(cherry picked from commit 00fd684a7b382aaeb3355a1c80dc24078a5caa61)
* Update Changelog
* :v:
---------
Co-authored-by: Yuuki
---
CHANGELOG.md | 2 ++
packages/frontend/src/components/MkEmojiPicker.vue | 3 ++-
packages/frontend/src/components/MkNotification.vue | 4 ++--
packages/frontend/src/components/MkReactionTooltip.vue | 1 +
.../frontend/src/components/MkReactionsViewer.details.vue | 1 +
packages/frontend/src/pages/custom-emojis-manager.vue | 2 ++
6 files changed, 10 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c01d284bdbd4..7af74f86f2de 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,6 +11,8 @@
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
+- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
+ (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
### Server
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
diff --git a/packages/frontend/src/components/MkEmojiPicker.vue b/packages/frontend/src/components/MkEmojiPicker.vue
index 5ba175fc3504..3bad8da06f72 100644
--- a/packages/frontend/src/components/MkEmojiPicker.vue
+++ b/packages/frontend/src/components/MkEmojiPicker.vue
@@ -611,6 +611,7 @@ defineExpose({
width: auto;
height: auto;
min-width: 0;
+ padding: 0;
&:disabled {
cursor: not-allowed;
@@ -717,7 +718,7 @@ defineExpose({
> .item {
position: relative;
- padding: 0;
+ padding: 0 3px;
width: var(--eachSize);
height: var(--eachSize);
contain: strict;
diff --git a/packages/frontend/src/components/MkNotification.vue b/packages/frontend/src/components/MkNotification.vue
index ee65743574c2..738cba21344d 100644
--- a/packages/frontend/src/components/MkNotification.vue
+++ b/packages/frontend/src/components/MkNotification.vue
@@ -46,7 +46,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:withTooltip="true"
:reaction="notification.reaction.replace(/^:(\w+):$/, ':$1@.:')"
:noStyle="true"
- style="width: 100%; height: 100%;"
+ style="width: 100%; height: 100% !important; object-fit: contain;"
/>
@@ -122,7 +122,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:withTooltip="true"
:reaction="reaction.reaction.replace(/^:(\w+):$/, ':$1@.:')"
:noStyle="true"
- style="width: 100%; height: 100%;"
+ style="width: 100%; height: 100% !important; object-fit: contain;"
/>
diff --git a/packages/frontend/src/components/MkReactionTooltip.vue b/packages/frontend/src/components/MkReactionTooltip.vue
index 15409a216a30..77ca841ad0c0 100644
--- a/packages/frontend/src/components/MkReactionTooltip.vue
+++ b/packages/frontend/src/components/MkReactionTooltip.vue
@@ -36,6 +36,7 @@ const emit = defineEmits<{
.icon {
display: block;
width: 60px;
+ max-height: 60px;
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
margin: 0 auto;
object-fit: contain;
diff --git a/packages/frontend/src/components/MkReactionsViewer.details.vue b/packages/frontend/src/components/MkReactionsViewer.details.vue
index 3dd02b261c7f..8038ec74292a 100644
--- a/packages/frontend/src/components/MkReactionsViewer.details.vue
+++ b/packages/frontend/src/components/MkReactionsViewer.details.vue
@@ -63,6 +63,7 @@ function getReactionName(reaction: string): string {
.reactionIcon {
display: block;
width: 60px;
+ max-height: 60px;
font-size: 60px; // unicodeな絵文字についてはwidthが効かないため
object-fit: contain;
margin: 0 auto;
diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue
index eea3f681301b..4747aa5205e3 100644
--- a/packages/frontend/src/pages/custom-emojis-manager.vue
+++ b/packages/frontend/src/pages/custom-emojis-manager.vue
@@ -344,6 +344,7 @@ definePageMetadata(() => ({
> .img {
width: 42px;
height: 42px;
+ object-fit: contain;
}
> .body {
@@ -390,6 +391,7 @@ definePageMetadata(() => ({
> .img {
width: 32px;
height: 32px;
+ object-fit: contain;
}
> .body {
From 0134e6e420e5a060bccd03b8489e5b07bee99262 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 17 Sep 2024 17:00:48 +0900
Subject: [PATCH 037/222] refactor
---
packages/frontend-embed/src/boot.ts | 8 +-
packages/frontend-embed/src/style.scss | 144 ----------------------
packages/frontend-shared/styles/mfm.scss | 147 +++++++++++++++++++++++
packages/frontend/.storybook/preview.ts | 1 +
packages/frontend/src/_boot_.ts | 1 +
packages/frontend/src/style.scss | 144 ----------------------
6 files changed, 153 insertions(+), 292 deletions(-)
create mode 100644 packages/frontend-shared/styles/mfm.scss
diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts
index fcea7d32eac7..9a363ab3e34d 100644
--- a/packages/frontend-embed/src/boot.ts
+++ b/packages/frontend-embed/src/boot.ts
@@ -9,20 +9,20 @@ import 'vite/modulepreload-polyfill';
import '@tabler/icons-webfont/dist/tabler-icons.scss';
import '@/style.scss';
+import '@@/styles/mfm.scss';
import { createApp, defineAsyncComponent } from 'vue';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-dark.json5';
import { MediaProxy } from '@@/js/media-proxy.js';
+import { url } from '@@/js/config.js';
+import { parseEmbedParams } from '@@/js/embed-page.js';
+import type { Theme } from '@/theme.js';
import { applyTheme, assertIsTheme } from '@/theme.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { DI } from '@/di.js';
import { serverMetadata } from '@/server-metadata.js';
-import { url } from '@@/js/config.js';
-import { parseEmbedParams } from '@@/js/embed-page.js';
import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
-import type { Theme } from '@/theme.js';
-
console.log('Misskey Embed');
const params = new URLSearchParams(location.search);
diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss
index 02008ddbd05b..4d169863c89c 100644
--- a/packages/frontend-embed/src/style.scss
+++ b/packages/frontend-embed/src/style.scss
@@ -307,147 +307,3 @@ rt {
._monospace {
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important;
}
-
-// MFM -----------------------------
-
-._mfm_blur_ {
- filter: blur(6px);
- transition: filter 0.3s;
-
- &:hover {
- filter: blur(0px);
- }
-}
-
-.mfm-x2 {
- --mfm-zoom-size: 200%;
-}
-
-.mfm-x3 {
- --mfm-zoom-size: 400%;
-}
-
-.mfm-x4 {
- --mfm-zoom-size: 600%;
-}
-
-.mfm-x2, .mfm-x3, .mfm-x4 {
- font-size: var(--mfm-zoom-size);
-
- .mfm-x2, .mfm-x3, .mfm-x4 {
- /* only half effective */
- font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
-
- .mfm-x2, .mfm-x3, .mfm-x4 {
- /* disabled */
- font-size: 100%;
- }
- }
-}
-
-._mfm_rainbow_fallback_ {
- background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
-}
-
-@keyframes mfm-spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
-}
-
-@keyframes mfm-spinX {
- 0% { transform: perspective(128px) rotateX(0deg); }
- 100% { transform: perspective(128px) rotateX(360deg); }
-}
-
-@keyframes mfm-spinY {
- 0% { transform: perspective(128px) rotateY(0deg); }
- 100% { transform: perspective(128px) rotateY(360deg); }
-}
-
-@keyframes mfm-jump {
- 0% { transform: translateY(0); }
- 25% { transform: translateY(-16px); }
- 50% { transform: translateY(0); }
- 75% { transform: translateY(-8px); }
- 100% { transform: translateY(0); }
-}
-
-@keyframes mfm-bounce {
- 0% { transform: translateY(0) scale(1, 1); }
- 25% { transform: translateY(-16px) scale(1, 1); }
- 50% { transform: translateY(0) scale(1, 1); }
- 75% { transform: translateY(0) scale(1.5, 0.75); }
- 100% { transform: translateY(0) scale(1, 1); }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-twitch {
- 0% { transform: translate(7px, -2px) }
- 5% { transform: translate(-3px, 1px) }
- 10% { transform: translate(-7px, -1px) }
- 15% { transform: translate(0px, -1px) }
- 20% { transform: translate(-8px, 6px) }
- 25% { transform: translate(-4px, -3px) }
- 30% { transform: translate(-4px, -6px) }
- 35% { transform: translate(-8px, -8px) }
- 40% { transform: translate(4px, 6px) }
- 45% { transform: translate(-3px, 1px) }
- 50% { transform: translate(2px, -10px) }
- 55% { transform: translate(-7px, 0px) }
- 60% { transform: translate(-2px, 4px) }
- 65% { transform: translate(3px, -8px) }
- 70% { transform: translate(6px, 7px) }
- 75% { transform: translate(-7px, -2px) }
- 80% { transform: translate(-7px, -8px) }
- 85% { transform: translate(9px, 3px) }
- 90% { transform: translate(-3px, -2px) }
- 95% { transform: translate(-10px, 2px) }
- 100% { transform: translate(-2px, -6px) }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-shake {
- 0% { transform: translate(-3px, -1px) rotate(-8deg) }
- 5% { transform: translate(0px, -1px) rotate(-10deg) }
- 10% { transform: translate(1px, -3px) rotate(0deg) }
- 15% { transform: translate(1px, 1px) rotate(11deg) }
- 20% { transform: translate(-2px, 1px) rotate(1deg) }
- 25% { transform: translate(-1px, -2px) rotate(-2deg) }
- 30% { transform: translate(-1px, 2px) rotate(-3deg) }
- 35% { transform: translate(2px, 1px) rotate(6deg) }
- 40% { transform: translate(-2px, -3px) rotate(-9deg) }
- 45% { transform: translate(0px, -1px) rotate(-12deg) }
- 50% { transform: translate(1px, 2px) rotate(10deg) }
- 55% { transform: translate(0px, -3px) rotate(8deg) }
- 60% { transform: translate(1px, -1px) rotate(8deg) }
- 65% { transform: translate(0px, -1px) rotate(-7deg) }
- 70% { transform: translate(-1px, -3px) rotate(6deg) }
- 75% { transform: translate(0px, -2px) rotate(4deg) }
- 80% { transform: translate(-2px, -1px) rotate(3deg) }
- 85% { transform: translate(1px, -3px) rotate(-10deg) }
- 90% { transform: translate(1px, 0px) rotate(3deg) }
- 95% { transform: translate(-2px, 0px) rotate(-3deg) }
- 100% { transform: translate(2px, 1px) rotate(2deg) }
-}
-
-@keyframes mfm-rubberBand {
- from { transform: scale3d(1, 1, 1); }
- 30% { transform: scale3d(1.25, 0.75, 1); }
- 40% { transform: scale3d(0.75, 1.25, 1); }
- 50% { transform: scale3d(1.15, 0.85, 1); }
- 65% { transform: scale3d(0.95, 1.05, 1); }
- 75% { transform: scale3d(1.05, 0.95, 1); }
- to { transform: scale3d(1, 1, 1); }
-}
-
-@keyframes mfm-rainbow {
- 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
- 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
-}
diff --git a/packages/frontend-shared/styles/mfm.scss b/packages/frontend-shared/styles/mfm.scss
new file mode 100644
index 000000000000..5ca744bf7883
--- /dev/null
+++ b/packages/frontend-shared/styles/mfm.scss
@@ -0,0 +1,147 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ *
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+._mfm_blur_ {
+ filter: blur(6px);
+ transition: filter 0.3s;
+
+ &:hover {
+ filter: blur(0px);
+ }
+}
+
+.mfm-x2 {
+ --mfm-zoom-size: 200%;
+}
+
+.mfm-x3 {
+ --mfm-zoom-size: 400%;
+}
+
+.mfm-x4 {
+ --mfm-zoom-size: 600%;
+}
+
+.mfm-x2, .mfm-x3, .mfm-x4 {
+ font-size: var(--mfm-zoom-size);
+
+ .mfm-x2, .mfm-x3, .mfm-x4 {
+ /* only half effective */
+ font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
+
+ .mfm-x2, .mfm-x3, .mfm-x4 {
+ /* disabled */
+ font-size: 100%;
+ }
+ }
+}
+
+._mfm_rainbow_fallback_ {
+ background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+
+@keyframes mfm-spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+@keyframes mfm-spinX {
+ 0% { transform: perspective(128px) rotateX(0deg); }
+ 100% { transform: perspective(128px) rotateX(360deg); }
+}
+
+@keyframes mfm-spinY {
+ 0% { transform: perspective(128px) rotateY(0deg); }
+ 100% { transform: perspective(128px) rotateY(360deg); }
+}
+
+@keyframes mfm-jump {
+ 0% { transform: translateY(0); }
+ 25% { transform: translateY(-16px); }
+ 50% { transform: translateY(0); }
+ 75% { transform: translateY(-8px); }
+ 100% { transform: translateY(0); }
+}
+
+@keyframes mfm-bounce {
+ 0% { transform: translateY(0) scale(1, 1); }
+ 25% { transform: translateY(-16px) scale(1, 1); }
+ 50% { transform: translateY(0) scale(1, 1); }
+ 75% { transform: translateY(0) scale(1.5, 0.75); }
+ 100% { transform: translateY(0) scale(1, 1); }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-twitch {
+ 0% { transform: translate(7px, -2px) }
+ 5% { transform: translate(-3px, 1px) }
+ 10% { transform: translate(-7px, -1px) }
+ 15% { transform: translate(0px, -1px) }
+ 20% { transform: translate(-8px, 6px) }
+ 25% { transform: translate(-4px, -3px) }
+ 30% { transform: translate(-4px, -6px) }
+ 35% { transform: translate(-8px, -8px) }
+ 40% { transform: translate(4px, 6px) }
+ 45% { transform: translate(-3px, 1px) }
+ 50% { transform: translate(2px, -10px) }
+ 55% { transform: translate(-7px, 0px) }
+ 60% { transform: translate(-2px, 4px) }
+ 65% { transform: translate(3px, -8px) }
+ 70% { transform: translate(6px, 7px) }
+ 75% { transform: translate(-7px, -2px) }
+ 80% { transform: translate(-7px, -8px) }
+ 85% { transform: translate(9px, 3px) }
+ 90% { transform: translate(-3px, -2px) }
+ 95% { transform: translate(-10px, 2px) }
+ 100% { transform: translate(-2px, -6px) }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-shake {
+ 0% { transform: translate(-3px, -1px) rotate(-8deg) }
+ 5% { transform: translate(0px, -1px) rotate(-10deg) }
+ 10% { transform: translate(1px, -3px) rotate(0deg) }
+ 15% { transform: translate(1px, 1px) rotate(11deg) }
+ 20% { transform: translate(-2px, 1px) rotate(1deg) }
+ 25% { transform: translate(-1px, -2px) rotate(-2deg) }
+ 30% { transform: translate(-1px, 2px) rotate(-3deg) }
+ 35% { transform: translate(2px, 1px) rotate(6deg) }
+ 40% { transform: translate(-2px, -3px) rotate(-9deg) }
+ 45% { transform: translate(0px, -1px) rotate(-12deg) }
+ 50% { transform: translate(1px, 2px) rotate(10deg) }
+ 55% { transform: translate(0px, -3px) rotate(8deg) }
+ 60% { transform: translate(1px, -1px) rotate(8deg) }
+ 65% { transform: translate(0px, -1px) rotate(-7deg) }
+ 70% { transform: translate(-1px, -3px) rotate(6deg) }
+ 75% { transform: translate(0px, -2px) rotate(4deg) }
+ 80% { transform: translate(-2px, -1px) rotate(3deg) }
+ 85% { transform: translate(1px, -3px) rotate(-10deg) }
+ 90% { transform: translate(1px, 0px) rotate(3deg) }
+ 95% { transform: translate(-2px, 0px) rotate(-3deg) }
+ 100% { transform: translate(2px, 1px) rotate(2deg) }
+}
+
+@keyframes mfm-rubberBand {
+ from { transform: scale3d(1, 1, 1); }
+ 30% { transform: scale3d(1.25, 0.75, 1); }
+ 40% { transform: scale3d(0.75, 1.25, 1); }
+ 50% { transform: scale3d(1.15, 0.85, 1); }
+ 65% { transform: scale3d(0.95, 1.05, 1); }
+ 75% { transform: scale3d(1.05, 0.95, 1); }
+ to { transform: scale3d(1, 1, 1); }
+}
+
+@keyframes mfm-rainbow {
+ 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
+ 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
+}
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index d000a282328f..b101748397be 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -13,6 +13,7 @@ import locale from './locale.js';
import { commonHandlers, onUnhandledRequest } from './mocks.js';
import themes from './themes.js';
import '../src/style.scss';
+import '../../frontend-shared/styles/mfm.scss';
const appInitialized = Symbol();
diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts
index 13a97e433ca1..fb9d6317394f 100644
--- a/packages/frontend/src/_boot_.ts
+++ b/packages/frontend/src/_boot_.ts
@@ -9,6 +9,7 @@ import 'vite/modulepreload-polyfill';
import '@tabler/icons-webfont/dist/tabler-icons.scss';
import '@/style.scss';
+import '@@/styles/mfm.scss';
import { mainBoot } from '@/boot/main-boot.js';
import { subBoot } from '@/boot/sub-boot.js';
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index caaf9fca6fda..5b95864a12ab 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -541,147 +541,3 @@ html[data-color-mode=dark] ._woodenFrame {
transform: scaleX(1.00) scaleY(1.00) ;
}
}
-
-// MFM -----------------------------
-
-._mfm_blur_ {
- filter: blur(6px);
- transition: filter 0.3s;
-
- &:hover {
- filter: blur(0px);
- }
-}
-
-.mfm-x2 {
- --mfm-zoom-size: 200%;
-}
-
-.mfm-x3 {
- --mfm-zoom-size: 400%;
-}
-
-.mfm-x4 {
- --mfm-zoom-size: 600%;
-}
-
-.mfm-x2, .mfm-x3, .mfm-x4 {
- font-size: var(--mfm-zoom-size);
-
- .mfm-x2, .mfm-x3, .mfm-x4 {
- /* only half effective */
- font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
-
- .mfm-x2, .mfm-x3, .mfm-x4 {
- /* disabled */
- font-size: 100%;
- }
- }
-}
-
-._mfm_rainbow_fallback_ {
- background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
-}
-
-@keyframes mfm-spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
-}
-
-@keyframes mfm-spinX {
- 0% { transform: perspective(128px) rotateX(0deg); }
- 100% { transform: perspective(128px) rotateX(360deg); }
-}
-
-@keyframes mfm-spinY {
- 0% { transform: perspective(128px) rotateY(0deg); }
- 100% { transform: perspective(128px) rotateY(360deg); }
-}
-
-@keyframes mfm-jump {
- 0% { transform: translateY(0); }
- 25% { transform: translateY(-16px); }
- 50% { transform: translateY(0); }
- 75% { transform: translateY(-8px); }
- 100% { transform: translateY(0); }
-}
-
-@keyframes mfm-bounce {
- 0% { transform: translateY(0) scale(1, 1); }
- 25% { transform: translateY(-16px) scale(1, 1); }
- 50% { transform: translateY(0) scale(1, 1); }
- 75% { transform: translateY(0) scale(1.5, 0.75); }
- 100% { transform: translateY(0) scale(1, 1); }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-twitch {
- 0% { transform: translate(7px, -2px) }
- 5% { transform: translate(-3px, 1px) }
- 10% { transform: translate(-7px, -1px) }
- 15% { transform: translate(0px, -1px) }
- 20% { transform: translate(-8px, 6px) }
- 25% { transform: translate(-4px, -3px) }
- 30% { transform: translate(-4px, -6px) }
- 35% { transform: translate(-8px, -8px) }
- 40% { transform: translate(4px, 6px) }
- 45% { transform: translate(-3px, 1px) }
- 50% { transform: translate(2px, -10px) }
- 55% { transform: translate(-7px, 0px) }
- 60% { transform: translate(-2px, 4px) }
- 65% { transform: translate(3px, -8px) }
- 70% { transform: translate(6px, 7px) }
- 75% { transform: translate(-7px, -2px) }
- 80% { transform: translate(-7px, -8px) }
- 85% { transform: translate(9px, 3px) }
- 90% { transform: translate(-3px, -2px) }
- 95% { transform: translate(-10px, 2px) }
- 100% { transform: translate(-2px, -6px) }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-shake {
- 0% { transform: translate(-3px, -1px) rotate(-8deg) }
- 5% { transform: translate(0px, -1px) rotate(-10deg) }
- 10% { transform: translate(1px, -3px) rotate(0deg) }
- 15% { transform: translate(1px, 1px) rotate(11deg) }
- 20% { transform: translate(-2px, 1px) rotate(1deg) }
- 25% { transform: translate(-1px, -2px) rotate(-2deg) }
- 30% { transform: translate(-1px, 2px) rotate(-3deg) }
- 35% { transform: translate(2px, 1px) rotate(6deg) }
- 40% { transform: translate(-2px, -3px) rotate(-9deg) }
- 45% { transform: translate(0px, -1px) rotate(-12deg) }
- 50% { transform: translate(1px, 2px) rotate(10deg) }
- 55% { transform: translate(0px, -3px) rotate(8deg) }
- 60% { transform: translate(1px, -1px) rotate(8deg) }
- 65% { transform: translate(0px, -1px) rotate(-7deg) }
- 70% { transform: translate(-1px, -3px) rotate(6deg) }
- 75% { transform: translate(0px, -2px) rotate(4deg) }
- 80% { transform: translate(-2px, -1px) rotate(3deg) }
- 85% { transform: translate(1px, -3px) rotate(-10deg) }
- 90% { transform: translate(1px, 0px) rotate(3deg) }
- 95% { transform: translate(-2px, 0px) rotate(-3deg) }
- 100% { transform: translate(2px, 1px) rotate(2deg) }
-}
-
-@keyframes mfm-rubberBand {
- from { transform: scale3d(1, 1, 1); }
- 30% { transform: scale3d(1.25, 0.75, 1); }
- 40% { transform: scale3d(0.75, 1.25, 1); }
- 50% { transform: scale3d(1.15, 0.85, 1); }
- 65% { transform: scale3d(0.95, 1.05, 1); }
- 75% { transform: scale3d(1.05, 0.95, 1); }
- to { transform: scale3d(1, 1, 1); }
-}
-
-@keyframes mfm-rainbow {
- 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
- 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
-}
From cacdf9d9392ccdf960452c9fec03fb7dc7c679e2 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 17 Sep 2024 17:03:09 +0900
Subject: [PATCH 038/222] refactor
MkMisskeyFlavoredMarkdown -> MkMfm
---
...wn.stories.impl.ts => MkMfm.stories.impl.ts} | 17 ++++++++---------
.../{MkMisskeyFlavoredMarkdown.ts => MkMfm.ts} | 0
packages/frontend/src/components/index.ts | 2 +-
3 files changed, 9 insertions(+), 10 deletions(-)
rename packages/frontend/src/components/global/{MkMisskeyFlavoredMarkdown.stories.impl.ts => MkMfm.stories.impl.ts} (78%)
rename packages/frontend/src/components/global/{MkMisskeyFlavoredMarkdown.ts => MkMfm.ts} (100%)
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts b/packages/frontend/src/components/global/MkMfm.stories.impl.ts
similarity index 78%
rename from packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
rename to packages/frontend/src/components/global/MkMfm.stories.impl.ts
index 730351f79534..1daf7a29cb60 100644
--- a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.stories.impl.ts
+++ b/packages/frontend/src/components/global/MkMfm.stories.impl.ts
@@ -2,16 +2,15 @@
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
-
-/* eslint-disable @typescript-eslint/explicit-function-return-type */
+
import { StoryObj } from '@storybook/vue3';
import { expect, within } from '@storybook/test';
-import MkMisskeyFlavoredMarkdown from './MkMisskeyFlavoredMarkdown.js';
+import MkMfm from './MkMfm.js';
export const Default = {
render(args) {
return {
components: {
- MkMisskeyFlavoredMarkdown,
+ MkMfm,
},
setup() {
return {
@@ -25,7 +24,7 @@ export const Default = {
};
},
},
- template: '',
+ template: '',
};
},
async play({ canvasElement, args }) {
@@ -54,25 +53,25 @@ export const Default = {
parameters: {
layout: 'centered',
},
-} satisfies StoryObj;
+} satisfies StoryObj;
export const Plain = {
...Default,
args: {
...Default.args,
plain: true,
},
-} satisfies StoryObj;
+} satisfies StoryObj;
export const Nowrap = {
...Default,
args: {
...Default.args,
nowrap: true,
},
-} satisfies StoryObj;
+} satisfies StoryObj;
export const IsNotNote = {
...Default,
args: {
...Default.args,
isNote: false,
},
-} satisfies StoryObj;
+} satisfies StoryObj;
diff --git a/packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts b/packages/frontend/src/components/global/MkMfm.ts
similarity index 100%
rename from packages/frontend/src/components/global/MkMisskeyFlavoredMarkdown.ts
rename to packages/frontend/src/components/global/MkMfm.ts
diff --git a/packages/frontend/src/components/index.ts b/packages/frontend/src/components/index.ts
index 44d8d59941b3..b36625ed1b47 100644
--- a/packages/frontend/src/components/index.ts
+++ b/packages/frontend/src/components/index.ts
@@ -5,7 +5,7 @@
import { App } from 'vue';
-import Mfm from './global/MkMisskeyFlavoredMarkdown.js';
+import Mfm from './global/MkMfm.js';
import MkA from './global/MkA.vue';
import MkAcct from './global/MkAcct.vue';
import MkAvatar from './global/MkAvatar.vue';
From a5e61b8c193d5d1935805e0fd7394758b33f0923 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Tue, 17 Sep 2024 17:05:52 +0900
Subject: [PATCH 039/222] Revert "refactor"
This reverts commit 0134e6e420e5a060bccd03b8489e5b07bee99262.
---
packages/frontend-embed/src/boot.ts | 8 +-
packages/frontend-embed/src/style.scss | 144 ++++++++++++++++++++++
packages/frontend-shared/styles/mfm.scss | 147 -----------------------
packages/frontend/.storybook/preview.ts | 1 -
packages/frontend/src/_boot_.ts | 1 -
packages/frontend/src/style.scss | 144 ++++++++++++++++++++++
6 files changed, 292 insertions(+), 153 deletions(-)
delete mode 100644 packages/frontend-shared/styles/mfm.scss
diff --git a/packages/frontend-embed/src/boot.ts b/packages/frontend-embed/src/boot.ts
index 9a363ab3e34d..fcea7d32eac7 100644
--- a/packages/frontend-embed/src/boot.ts
+++ b/packages/frontend-embed/src/boot.ts
@@ -9,20 +9,20 @@ import 'vite/modulepreload-polyfill';
import '@tabler/icons-webfont/dist/tabler-icons.scss';
import '@/style.scss';
-import '@@/styles/mfm.scss';
import { createApp, defineAsyncComponent } from 'vue';
import defaultLightTheme from '@@/themes/l-light.json5';
import defaultDarkTheme from '@@/themes/d-dark.json5';
import { MediaProxy } from '@@/js/media-proxy.js';
-import { url } from '@@/js/config.js';
-import { parseEmbedParams } from '@@/js/embed-page.js';
-import type { Theme } from '@/theme.js';
import { applyTheme, assertIsTheme } from '@/theme.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { DI } from '@/di.js';
import { serverMetadata } from '@/server-metadata.js';
+import { url } from '@@/js/config.js';
+import { parseEmbedParams } from '@@/js/embed-page.js';
import { postMessageToParentWindow, setIframeId } from '@/post-message.js';
+import type { Theme } from '@/theme.js';
+
console.log('Misskey Embed');
const params = new URLSearchParams(location.search);
diff --git a/packages/frontend-embed/src/style.scss b/packages/frontend-embed/src/style.scss
index 4d169863c89c..02008ddbd05b 100644
--- a/packages/frontend-embed/src/style.scss
+++ b/packages/frontend-embed/src/style.scss
@@ -307,3 +307,147 @@ rt {
._monospace {
font-family: Fira code, Fira Mono, Consolas, Menlo, Courier, monospace !important;
}
+
+// MFM -----------------------------
+
+._mfm_blur_ {
+ filter: blur(6px);
+ transition: filter 0.3s;
+
+ &:hover {
+ filter: blur(0px);
+ }
+}
+
+.mfm-x2 {
+ --mfm-zoom-size: 200%;
+}
+
+.mfm-x3 {
+ --mfm-zoom-size: 400%;
+}
+
+.mfm-x4 {
+ --mfm-zoom-size: 600%;
+}
+
+.mfm-x2, .mfm-x3, .mfm-x4 {
+ font-size: var(--mfm-zoom-size);
+
+ .mfm-x2, .mfm-x3, .mfm-x4 {
+ /* only half effective */
+ font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
+
+ .mfm-x2, .mfm-x3, .mfm-x4 {
+ /* disabled */
+ font-size: 100%;
+ }
+ }
+}
+
+._mfm_rainbow_fallback_ {
+ background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+
+@keyframes mfm-spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+@keyframes mfm-spinX {
+ 0% { transform: perspective(128px) rotateX(0deg); }
+ 100% { transform: perspective(128px) rotateX(360deg); }
+}
+
+@keyframes mfm-spinY {
+ 0% { transform: perspective(128px) rotateY(0deg); }
+ 100% { transform: perspective(128px) rotateY(360deg); }
+}
+
+@keyframes mfm-jump {
+ 0% { transform: translateY(0); }
+ 25% { transform: translateY(-16px); }
+ 50% { transform: translateY(0); }
+ 75% { transform: translateY(-8px); }
+ 100% { transform: translateY(0); }
+}
+
+@keyframes mfm-bounce {
+ 0% { transform: translateY(0) scale(1, 1); }
+ 25% { transform: translateY(-16px) scale(1, 1); }
+ 50% { transform: translateY(0) scale(1, 1); }
+ 75% { transform: translateY(0) scale(1.5, 0.75); }
+ 100% { transform: translateY(0) scale(1, 1); }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-twitch {
+ 0% { transform: translate(7px, -2px) }
+ 5% { transform: translate(-3px, 1px) }
+ 10% { transform: translate(-7px, -1px) }
+ 15% { transform: translate(0px, -1px) }
+ 20% { transform: translate(-8px, 6px) }
+ 25% { transform: translate(-4px, -3px) }
+ 30% { transform: translate(-4px, -6px) }
+ 35% { transform: translate(-8px, -8px) }
+ 40% { transform: translate(4px, 6px) }
+ 45% { transform: translate(-3px, 1px) }
+ 50% { transform: translate(2px, -10px) }
+ 55% { transform: translate(-7px, 0px) }
+ 60% { transform: translate(-2px, 4px) }
+ 65% { transform: translate(3px, -8px) }
+ 70% { transform: translate(6px, 7px) }
+ 75% { transform: translate(-7px, -2px) }
+ 80% { transform: translate(-7px, -8px) }
+ 85% { transform: translate(9px, 3px) }
+ 90% { transform: translate(-3px, -2px) }
+ 95% { transform: translate(-10px, 2px) }
+ 100% { transform: translate(-2px, -6px) }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-shake {
+ 0% { transform: translate(-3px, -1px) rotate(-8deg) }
+ 5% { transform: translate(0px, -1px) rotate(-10deg) }
+ 10% { transform: translate(1px, -3px) rotate(0deg) }
+ 15% { transform: translate(1px, 1px) rotate(11deg) }
+ 20% { transform: translate(-2px, 1px) rotate(1deg) }
+ 25% { transform: translate(-1px, -2px) rotate(-2deg) }
+ 30% { transform: translate(-1px, 2px) rotate(-3deg) }
+ 35% { transform: translate(2px, 1px) rotate(6deg) }
+ 40% { transform: translate(-2px, -3px) rotate(-9deg) }
+ 45% { transform: translate(0px, -1px) rotate(-12deg) }
+ 50% { transform: translate(1px, 2px) rotate(10deg) }
+ 55% { transform: translate(0px, -3px) rotate(8deg) }
+ 60% { transform: translate(1px, -1px) rotate(8deg) }
+ 65% { transform: translate(0px, -1px) rotate(-7deg) }
+ 70% { transform: translate(-1px, -3px) rotate(6deg) }
+ 75% { transform: translate(0px, -2px) rotate(4deg) }
+ 80% { transform: translate(-2px, -1px) rotate(3deg) }
+ 85% { transform: translate(1px, -3px) rotate(-10deg) }
+ 90% { transform: translate(1px, 0px) rotate(3deg) }
+ 95% { transform: translate(-2px, 0px) rotate(-3deg) }
+ 100% { transform: translate(2px, 1px) rotate(2deg) }
+}
+
+@keyframes mfm-rubberBand {
+ from { transform: scale3d(1, 1, 1); }
+ 30% { transform: scale3d(1.25, 0.75, 1); }
+ 40% { transform: scale3d(0.75, 1.25, 1); }
+ 50% { transform: scale3d(1.15, 0.85, 1); }
+ 65% { transform: scale3d(0.95, 1.05, 1); }
+ 75% { transform: scale3d(1.05, 0.95, 1); }
+ to { transform: scale3d(1, 1, 1); }
+}
+
+@keyframes mfm-rainbow {
+ 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
+ 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
+}
diff --git a/packages/frontend-shared/styles/mfm.scss b/packages/frontend-shared/styles/mfm.scss
deleted file mode 100644
index 5ca744bf7883..000000000000
--- a/packages/frontend-shared/styles/mfm.scss
+++ /dev/null
@@ -1,147 +0,0 @@
-/*
- * SPDX-FileCopyrightText: syuilo and misskey-project
- *
- * SPDX-License-Identifier: AGPL-3.0-only
- */
-
-._mfm_blur_ {
- filter: blur(6px);
- transition: filter 0.3s;
-
- &:hover {
- filter: blur(0px);
- }
-}
-
-.mfm-x2 {
- --mfm-zoom-size: 200%;
-}
-
-.mfm-x3 {
- --mfm-zoom-size: 400%;
-}
-
-.mfm-x4 {
- --mfm-zoom-size: 600%;
-}
-
-.mfm-x2, .mfm-x3, .mfm-x4 {
- font-size: var(--mfm-zoom-size);
-
- .mfm-x2, .mfm-x3, .mfm-x4 {
- /* only half effective */
- font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
-
- .mfm-x2, .mfm-x3, .mfm-x4 {
- /* disabled */
- font-size: 100%;
- }
- }
-}
-
-._mfm_rainbow_fallback_ {
- background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%);
- -webkit-background-clip: text;
- background-clip: text;
- color: transparent;
-}
-
-@keyframes mfm-spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
-}
-
-@keyframes mfm-spinX {
- 0% { transform: perspective(128px) rotateX(0deg); }
- 100% { transform: perspective(128px) rotateX(360deg); }
-}
-
-@keyframes mfm-spinY {
- 0% { transform: perspective(128px) rotateY(0deg); }
- 100% { transform: perspective(128px) rotateY(360deg); }
-}
-
-@keyframes mfm-jump {
- 0% { transform: translateY(0); }
- 25% { transform: translateY(-16px); }
- 50% { transform: translateY(0); }
- 75% { transform: translateY(-8px); }
- 100% { transform: translateY(0); }
-}
-
-@keyframes mfm-bounce {
- 0% { transform: translateY(0) scale(1, 1); }
- 25% { transform: translateY(-16px) scale(1, 1); }
- 50% { transform: translateY(0) scale(1, 1); }
- 75% { transform: translateY(0) scale(1.5, 0.75); }
- 100% { transform: translateY(0) scale(1, 1); }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-twitch {
- 0% { transform: translate(7px, -2px) }
- 5% { transform: translate(-3px, 1px) }
- 10% { transform: translate(-7px, -1px) }
- 15% { transform: translate(0px, -1px) }
- 20% { transform: translate(-8px, 6px) }
- 25% { transform: translate(-4px, -3px) }
- 30% { transform: translate(-4px, -6px) }
- 35% { transform: translate(-8px, -8px) }
- 40% { transform: translate(4px, 6px) }
- 45% { transform: translate(-3px, 1px) }
- 50% { transform: translate(2px, -10px) }
- 55% { transform: translate(-7px, 0px) }
- 60% { transform: translate(-2px, 4px) }
- 65% { transform: translate(3px, -8px) }
- 70% { transform: translate(6px, 7px) }
- 75% { transform: translate(-7px, -2px) }
- 80% { transform: translate(-7px, -8px) }
- 85% { transform: translate(9px, 3px) }
- 90% { transform: translate(-3px, -2px) }
- 95% { transform: translate(-10px, 2px) }
- 100% { transform: translate(-2px, -6px) }
-}
-
-// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
-// let css = '';
-// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
-@keyframes mfm-shake {
- 0% { transform: translate(-3px, -1px) rotate(-8deg) }
- 5% { transform: translate(0px, -1px) rotate(-10deg) }
- 10% { transform: translate(1px, -3px) rotate(0deg) }
- 15% { transform: translate(1px, 1px) rotate(11deg) }
- 20% { transform: translate(-2px, 1px) rotate(1deg) }
- 25% { transform: translate(-1px, -2px) rotate(-2deg) }
- 30% { transform: translate(-1px, 2px) rotate(-3deg) }
- 35% { transform: translate(2px, 1px) rotate(6deg) }
- 40% { transform: translate(-2px, -3px) rotate(-9deg) }
- 45% { transform: translate(0px, -1px) rotate(-12deg) }
- 50% { transform: translate(1px, 2px) rotate(10deg) }
- 55% { transform: translate(0px, -3px) rotate(8deg) }
- 60% { transform: translate(1px, -1px) rotate(8deg) }
- 65% { transform: translate(0px, -1px) rotate(-7deg) }
- 70% { transform: translate(-1px, -3px) rotate(6deg) }
- 75% { transform: translate(0px, -2px) rotate(4deg) }
- 80% { transform: translate(-2px, -1px) rotate(3deg) }
- 85% { transform: translate(1px, -3px) rotate(-10deg) }
- 90% { transform: translate(1px, 0px) rotate(3deg) }
- 95% { transform: translate(-2px, 0px) rotate(-3deg) }
- 100% { transform: translate(2px, 1px) rotate(2deg) }
-}
-
-@keyframes mfm-rubberBand {
- from { transform: scale3d(1, 1, 1); }
- 30% { transform: scale3d(1.25, 0.75, 1); }
- 40% { transform: scale3d(0.75, 1.25, 1); }
- 50% { transform: scale3d(1.15, 0.85, 1); }
- 65% { transform: scale3d(0.95, 1.05, 1); }
- 75% { transform: scale3d(1.05, 0.95, 1); }
- to { transform: scale3d(1, 1, 1); }
-}
-
-@keyframes mfm-rainbow {
- 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
- 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
-}
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index b101748397be..d000a282328f 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -13,7 +13,6 @@ import locale from './locale.js';
import { commonHandlers, onUnhandledRequest } from './mocks.js';
import themes from './themes.js';
import '../src/style.scss';
-import '../../frontend-shared/styles/mfm.scss';
const appInitialized = Symbol();
diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts
index fb9d6317394f..13a97e433ca1 100644
--- a/packages/frontend/src/_boot_.ts
+++ b/packages/frontend/src/_boot_.ts
@@ -9,7 +9,6 @@ import 'vite/modulepreload-polyfill';
import '@tabler/icons-webfont/dist/tabler-icons.scss';
import '@/style.scss';
-import '@@/styles/mfm.scss';
import { mainBoot } from '@/boot/main-boot.js';
import { subBoot } from '@/boot/sub-boot.js';
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 5b95864a12ab..caaf9fca6fda 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -541,3 +541,147 @@ html[data-color-mode=dark] ._woodenFrame {
transform: scaleX(1.00) scaleY(1.00) ;
}
}
+
+// MFM -----------------------------
+
+._mfm_blur_ {
+ filter: blur(6px);
+ transition: filter 0.3s;
+
+ &:hover {
+ filter: blur(0px);
+ }
+}
+
+.mfm-x2 {
+ --mfm-zoom-size: 200%;
+}
+
+.mfm-x3 {
+ --mfm-zoom-size: 400%;
+}
+
+.mfm-x4 {
+ --mfm-zoom-size: 600%;
+}
+
+.mfm-x2, .mfm-x3, .mfm-x4 {
+ font-size: var(--mfm-zoom-size);
+
+ .mfm-x2, .mfm-x3, .mfm-x4 {
+ /* only half effective */
+ font-size: calc(var(--mfm-zoom-size) / 2 + 50%);
+
+ .mfm-x2, .mfm-x3, .mfm-x4 {
+ /* disabled */
+ font-size: 100%;
+ }
+ }
+}
+
+._mfm_rainbow_fallback_ {
+ background-image: linear-gradient(to right, rgb(255, 0, 0) 0%, rgb(255, 165, 0) 17%, rgb(255, 255, 0) 33%, rgb(0, 255, 0) 50%, rgb(0, 255, 255) 67%, rgb(0, 0, 255) 83%, rgb(255, 0, 255) 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ color: transparent;
+}
+
+@keyframes mfm-spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+@keyframes mfm-spinX {
+ 0% { transform: perspective(128px) rotateX(0deg); }
+ 100% { transform: perspective(128px) rotateX(360deg); }
+}
+
+@keyframes mfm-spinY {
+ 0% { transform: perspective(128px) rotateY(0deg); }
+ 100% { transform: perspective(128px) rotateY(360deg); }
+}
+
+@keyframes mfm-jump {
+ 0% { transform: translateY(0); }
+ 25% { transform: translateY(-16px); }
+ 50% { transform: translateY(0); }
+ 75% { transform: translateY(-8px); }
+ 100% { transform: translateY(0); }
+}
+
+@keyframes mfm-bounce {
+ 0% { transform: translateY(0) scale(1, 1); }
+ 25% { transform: translateY(-16px) scale(1, 1); }
+ 50% { transform: translateY(0) scale(1, 1); }
+ 75% { transform: translateY(0) scale(1.5, 0.75); }
+ 100% { transform: translateY(0) scale(1, 1); }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 20) - 10}px, ${Math.floor(Math.random() * 20) - 10}px)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-twitch {
+ 0% { transform: translate(7px, -2px) }
+ 5% { transform: translate(-3px, 1px) }
+ 10% { transform: translate(-7px, -1px) }
+ 15% { transform: translate(0px, -1px) }
+ 20% { transform: translate(-8px, 6px) }
+ 25% { transform: translate(-4px, -3px) }
+ 30% { transform: translate(-4px, -6px) }
+ 35% { transform: translate(-8px, -8px) }
+ 40% { transform: translate(4px, 6px) }
+ 45% { transform: translate(-3px, 1px) }
+ 50% { transform: translate(2px, -10px) }
+ 55% { transform: translate(-7px, 0px) }
+ 60% { transform: translate(-2px, 4px) }
+ 65% { transform: translate(3px, -8px) }
+ 70% { transform: translate(6px, 7px) }
+ 75% { transform: translate(-7px, -2px) }
+ 80% { transform: translate(-7px, -8px) }
+ 85% { transform: translate(9px, 3px) }
+ 90% { transform: translate(-3px, -2px) }
+ 95% { transform: translate(-10px, 2px) }
+ 100% { transform: translate(-2px, -6px) }
+}
+
+// const val = () => `translate(${Math.floor(Math.random() * 6) - 3}px, ${Math.floor(Math.random() * 6) - 3}px) rotate(${Math.floor(Math.random() * 24) - 12}deg)`;
+// let css = '';
+// for (let i = 0; i <= 100; i += 5) { css += `${i}% { transform: ${val()} }\n`; }
+@keyframes mfm-shake {
+ 0% { transform: translate(-3px, -1px) rotate(-8deg) }
+ 5% { transform: translate(0px, -1px) rotate(-10deg) }
+ 10% { transform: translate(1px, -3px) rotate(0deg) }
+ 15% { transform: translate(1px, 1px) rotate(11deg) }
+ 20% { transform: translate(-2px, 1px) rotate(1deg) }
+ 25% { transform: translate(-1px, -2px) rotate(-2deg) }
+ 30% { transform: translate(-1px, 2px) rotate(-3deg) }
+ 35% { transform: translate(2px, 1px) rotate(6deg) }
+ 40% { transform: translate(-2px, -3px) rotate(-9deg) }
+ 45% { transform: translate(0px, -1px) rotate(-12deg) }
+ 50% { transform: translate(1px, 2px) rotate(10deg) }
+ 55% { transform: translate(0px, -3px) rotate(8deg) }
+ 60% { transform: translate(1px, -1px) rotate(8deg) }
+ 65% { transform: translate(0px, -1px) rotate(-7deg) }
+ 70% { transform: translate(-1px, -3px) rotate(6deg) }
+ 75% { transform: translate(0px, -2px) rotate(4deg) }
+ 80% { transform: translate(-2px, -1px) rotate(3deg) }
+ 85% { transform: translate(1px, -3px) rotate(-10deg) }
+ 90% { transform: translate(1px, 0px) rotate(3deg) }
+ 95% { transform: translate(-2px, 0px) rotate(-3deg) }
+ 100% { transform: translate(2px, 1px) rotate(2deg) }
+}
+
+@keyframes mfm-rubberBand {
+ from { transform: scale3d(1, 1, 1); }
+ 30% { transform: scale3d(1.25, 0.75, 1); }
+ 40% { transform: scale3d(0.75, 1.25, 1); }
+ 50% { transform: scale3d(1.15, 0.85, 1); }
+ 65% { transform: scale3d(0.95, 1.05, 1); }
+ 75% { transform: scale3d(1.05, 0.95, 1); }
+ to { transform: scale3d(1, 1, 1); }
+}
+
+@keyframes mfm-rainbow {
+ 0% { filter: hue-rotate(0deg) contrast(150%) saturate(150%); }
+ 100% { filter: hue-rotate(360deg) contrast(150%) saturate(150%); }
+}
From daf9ae5d4a31cfe5eaf85985e78449bb0eebbe1e Mon Sep 17 00:00:00 2001
From: FineArchs <133759614+FineArchs@users.noreply.github.com>
Date: Tue, 17 Sep 2024 20:11:50 +0900
Subject: [PATCH 040/222] =?UTF-8?q?Scratchpad=E3=81=ABUI=E3=82=A4=E3=83=B3?=
=?UTF-8?q?=E3=82=B9=E3=83=9A=E3=82=AF=E3=82=BF=E3=83=BC=E3=82=92=E8=BF=BD?=
=?UTF-8?q?=E5=8A=A0=20(#14565)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* add ui list
* Update scratchpad.vue
* experiment
* design change
* redesign
* redesign
* Update ja-JP.yml
* redesign
* component properties
* whole json
* use textarea
* fix import
* stringify function
* Update CHANGELOG.md
* UI Component Monitor -> UI Inspector
* uiInspectorOpenedFlags -> uiInspectorOpenedComponents
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
* fix
* change key i -> c.value.id
---------
Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com>
---
CHANGELOG.md | 1 +
locales/ja-JP.yml | 2 +
packages/frontend/src/pages/scratchpad.vue | 59 ++++++++++++++++++++++
3 files changed, 62 insertions(+)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7af74f86f2de..ff633c5a1f77 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -9,6 +9,7 @@
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
- Enhance: アイコンデコレーション管理画面にプレビューを追加
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
+- Enhance: ScratchpadにUIインスペクターを追加
- Fix: サーバーメトリクスが2つ以上あるとリロード直後の表示がおかしくなる問題を修正
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a1210bad2948..2877c8fe38cd 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -592,6 +592,8 @@ ascendingOrder: "昇順"
descendingOrder: "降順"
scratchpad: "スクラッチパッド"
scratchpadDescription: "スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。"
+uiInspector: "UIインスペクター"
+uiInspectorDescription: "メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。"
output: "出力"
script: "スクリプト"
disablePagesScript: "Pagesのスクリプトを無効にする"
diff --git a/packages/frontend/src/pages/scratchpad.vue b/packages/frontend/src/pages/scratchpad.vue
index 9aaa8ff9c633..897ff6acdf09 100644
--- a/packages/frontend/src/pages/scratchpad.vue
+++ b/packages/frontend/src/pages/scratchpad.vue
@@ -30,6 +30,24 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ {{ i18n.ts.uiInspector }}
+
+
+
{{ c.value.type }}
+
{{ c.value.id }}
+
+
+
+
+
+
{{ i18n.ts.uiInspectorDescription }}
+
+
+
{{ i18n.ts.scratchpadDescription }}
@@ -43,6 +61,7 @@ import { onDeactivated, onUnmounted, Ref, ref, watch, computed } from 'vue';
import { Interpreter, Parser, utils } from '@syuilo/aiscript';
import MkContainer from '@/components/MkContainer.vue';
import MkButton from '@/components/MkButton.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
import MkCodeEditor from '@/components/MkCodeEditor.vue';
import { aiScriptReadline, createAiScriptEnv } from '@/scripts/aiscript/api.js';
import * as os from '@/os.js';
@@ -61,6 +80,7 @@ const logs = ref([]);
const root = ref();
const components = ref[[]>([]);
const uiKey = ref(0);
+const uiInspectorOpenedComponents = ref(new Map);
const saved = miLocalStorage.getItem('scratchpad');
if (saved) {
@@ -71,6 +91,14 @@ watch(code, () => {
miLocalStorage.setItem('scratchpad', code.value);
});
+function stringifyUiProps(uiProps) {
+ return JSON.stringify(
+ { ...uiProps, type: undefined, id: undefined },
+ (k, v) => typeof v === 'function' ? '' : v,
+ 2
+ );
+}
+
async function run() {
if (aiscript) aiscript.abort();
root.value = undefined;
@@ -192,4 +220,35 @@ definePageMetadata(() => ({
}
}
}
+
+.uiInspector {
+ display: grid;
+ gap: 8px;
+ padding: 16px;
+}
+
+.uiInspectorType {
+ display: inline-block;
+ border: hidden;
+ border-radius: 10px;
+ background-color: var(--panelHighlight);
+ padding: 2px 8px;
+ font-size: 12px;
+}
+
+.uiInspectorId {
+ display: inline-block;
+ padding-left: 8px;
+}
+
+.uiInspectorDescription {
+ display: block;
+ font-size: 12px;
+ padding-top: 16px;
+}
+
+.uiInspectorPropsToggle {
+ background: none;
+ border: none;
+}
From ce95323e494a6ae914a98cb149e3e64ddc48c689 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Tue, 17 Sep 2024 22:02:34 +0900
Subject: [PATCH 041/222] =?UTF-8?q?fix(antenna):=20src=3Dlist=20&&=20userL?=
=?UTF-8?q?istId=3Dnull=20=E3=81=AE=E5=A0=B4=E5=90=88=E3=82=AF=E3=82=A8?=
=?UTF-8?q?=E3=83=AA=E3=83=BC=E3=82=BF=E3=82=A4=E3=83=A0=E3=82=A2=E3=82=A6?=
=?UTF-8?q?=E3=83=88=E3=81=8C=E7=99=BA=E7=94=9F=E3=81=99=E3=82=8B=E5=95=8F?=
=?UTF-8?q?=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(MisskeyIO#721)=20(#1456?=
=?UTF-8?q?8)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
(cherry picked from commit 47b6b97c9c6d9583dd1b11acbf8f94059e81ebaf)
Co-authored-by: まっちゃとーにゅ <17376330+u1-liquid@users.noreply.github.com>
---
packages/backend/src/core/AntennaService.ts | 13 ++++++++-----
1 file changed, 8 insertions(+), 5 deletions(-)
diff --git a/packages/backend/src/core/AntennaService.ts b/packages/backend/src/core/AntennaService.ts
index 793d8974b34b..e827ffa68c0f 100644
--- a/packages/backend/src/core/AntennaService.ts
+++ b/packages/backend/src/core/AntennaService.ts
@@ -123,11 +123,14 @@ export class AntennaService implements OnApplicationShutdown {
if (antenna.src === 'home') {
// TODO
} else if (antenna.src === 'list') {
- const listUsers = (await this.userListMembershipsRepository.findBy({
- userListId: antenna.userListId!,
- })).map(x => x.userId);
-
- if (!listUsers.includes(note.userId)) return false;
+ if (antenna.userListId == null) return false;
+ const exists = await this.userListMembershipsRepository.exists({
+ where: {
+ userListId: antenna.userListId,
+ userId: note.userId,
+ },
+ });
+ if (!exists) return false;
} else if (antenna.src === 'users') {
const accts = antenna.users.map(x => {
const { username, host } = Acct.parse(x);
From 3bf63dd9c5b47f42bcbe70a96c0a5186f087330a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Tue, 17 Sep 2024 22:18:06 +0900
Subject: [PATCH 042/222] =?UTF-8?q?fix(frontend):=20=E8=A8=AD=E5=AE=9A?=
=?UTF-8?q?=E5=A4=89=E6=9B=B4=E6=99=82=E3=81=AE=E3=83=AA=E3=83=AD=E3=83=BC?=
=?UTF-8?q?=E3=83=89=E7=A2=BA=E8=AA=8D=E3=83=80=E3=82=A4=E3=82=A2=E3=83=AD?=
=?UTF-8?q?=E3=82=B0=E3=81=8C=E8=A4=87=E6=95=B0=E5=80=8B=E8=A1=A8=E7=A4=BA?=
=?UTF-8?q?=E3=81=95=E3=82=8C=E3=82=8B=E3=81=93=E3=81=A8=E3=81=8C=E3=81=82?=
=?UTF-8?q?=E3=82=8B=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#1454?=
=?UTF-8?q?3)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* fix(frontend): reloadAskが同時に複数実行されないように
* Update Changelog
* fix
* フラグ解除が確実に行われるように
* reloadAskを汎用化、理由を受け取るように
* fix
---
CHANGELOG.md | 1 +
locales/index.d.ts | 2 +-
locales/ja-JP.yml | 2 +-
.../frontend/src/pages/settings/general.vue | 14 +------
.../frontend/src/pages/settings/navbar.vue | 16 ++------
.../frontend/src/pages/settings/other.vue | 14 +------
.../frontend/src/pages/settings/theme.vue | 16 ++------
packages/frontend/src/scripts/reload-ask.ts | 40 +++++++++++++++++++
8 files changed, 53 insertions(+), 52 deletions(-)
create mode 100644 packages/frontend/src/scripts/reload-ask.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index ff633c5a1f77..7c727cea782d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -14,6 +14,7 @@
- Fix: 月の違う同じ日はセパレータが表示されないのを修正
- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
+- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
### Server
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
diff --git a/locales/index.d.ts b/locales/index.d.ts
index fecc5703950d..b06e0f245b19 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -3121,7 +3121,7 @@ export interface Locale extends ILocale {
*/
"narrow": string;
/**
- * 設定はページリロード後に反映されます。今すぐリロードしますか?
+ * 設定はページリロード後に反映されます。
*/
"reloadToApplySetting": string;
/**
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 2877c8fe38cd..292569cc5afa 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -778,7 +778,7 @@ left: "左"
center: "中央"
wide: "広い"
narrow: "狭い"
-reloadToApplySetting: "設定はページリロード後に反映されます。今すぐリロードしますか?"
+reloadToApplySetting: "設定はページリロード後に反映されます。"
needReloadToApply: "反映には再起動が必要です。"
showTitlebar: "タイトルバーを表示する"
clearCache: "キャッシュをクリア"
diff --git a/packages/frontend/src/pages/settings/general.vue b/packages/frontend/src/pages/settings/general.vue
index 15af5617cc5a..69238b043693 100644
--- a/packages/frontend/src/pages/settings/general.vue
+++ b/packages/frontend/src/pages/settings/general.vue
@@ -258,7 +258,7 @@ import { langs } from '@@/js/config.js';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
import { misskeyApi } from '@/scripts/misskey-api.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { reloadAsk } from '@/scripts/reload-ask.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
@@ -270,16 +270,6 @@ const fontSize = ref(miLocalStorage.getItem('fontSize'));
const useSystemFont = ref(miLocalStorage.getItem('useSystemFont') != null);
const dataSaver = ref(defaultStore.state.dataSaver);
-async function reloadAsk() {
- const { canceled } = await os.confirm({
- type: 'info',
- text: i18n.ts.reloadToApplySetting,
- });
- if (canceled) return;
-
- unisonReload();
-}
-
const hemisphere = computed(defaultStore.makeGetterSetter('hemisphere'));
const overridedDeviceKind = computed(defaultStore.makeGetterSetter('overridedDeviceKind'));
const serverDisconnectedBehavior = computed(defaultStore.makeGetterSetter('serverDisconnectedBehavior'));
@@ -369,7 +359,7 @@ watch([
confirmWhenRevealingSensitiveMedia,
contextMenu,
], async () => {
- await reloadAsk();
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
const emojiIndexLangs = ['en-US', 'ja-JP', 'ja-JP_hira'] as const;
diff --git a/packages/frontend/src/pages/settings/navbar.vue b/packages/frontend/src/pages/settings/navbar.vue
index 7f8460e31640..a0e6cad9c8d4 100644
--- a/packages/frontend/src/pages/settings/navbar.vue
+++ b/packages/frontend/src/pages/settings/navbar.vue
@@ -54,7 +54,7 @@ import MkContainer from '@/components/MkContainer.vue';
import * as os from '@/os.js';
import { navbarItemDef } from '@/navbar.js';
import { defaultStore } from '@/store.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { reloadAsk } from '@/scripts/reload-ask.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
@@ -67,16 +67,6 @@ const items = ref(defaultStore.state.menu.map(x => ({
const menuDisplay = computed(defaultStore.makeGetterSetter('menuDisplay'));
-async function reloadAsk() {
- const { canceled } = await os.confirm({
- type: 'info',
- text: i18n.ts.reloadToApplySetting,
- });
- if (canceled) return;
-
- unisonReload();
-}
-
async function addItem() {
const menu = Object.keys(navbarItemDef).filter(k => !defaultStore.state.menu.includes(k));
const { canceled, result: item } = await os.select({
@@ -100,7 +90,7 @@ function removeItem(index: number) {
async function save() {
defaultStore.set('menu', items.value.map(x => x.type));
- await reloadAsk();
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
}
function reset() {
@@ -111,7 +101,7 @@ function reset() {
}
watch(menuDisplay, async () => {
- await reloadAsk();
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
const headerActions = computed(() => []);
diff --git a/packages/frontend/src/pages/settings/other.vue b/packages/frontend/src/pages/settings/other.vue
index a1cb2ea1c428..0f7609c83e95 100644
--- a/packages/frontend/src/pages/settings/other.vue
+++ b/packages/frontend/src/pages/settings/other.vue
@@ -98,7 +98,7 @@ import { defaultStore } from '@/store.js';
import { signout, signinRequired } from '@/account.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { reloadAsk } from '@/scripts/reload-ask.js';
import FormSection from '@/components/form/section.vue';
const $i = signinRequired();
@@ -132,16 +132,6 @@ async function deleteAccount() {
await signout();
}
-async function reloadAsk() {
- const { canceled } = await os.confirm({
- type: 'info',
- text: i18n.ts.reloadToApplySetting,
- });
- if (canceled) return;
-
- unisonReload();
-}
-
async function updateRepliesAll(withReplies: boolean) {
const { canceled } = await os.confirm({
type: 'warning',
@@ -155,7 +145,7 @@ async function updateRepliesAll(withReplies: boolean) {
watch([
enableCondensedLineForAcct,
], async () => {
- await reloadAsk();
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
const headerActions = computed(() => []);
diff --git a/packages/frontend/src/pages/settings/theme.vue b/packages/frontend/src/pages/settings/theme.vue
index 7d192bcbea6e..ce8ec686928a 100644
--- a/packages/frontend/src/pages/settings/theme.vue
+++ b/packages/frontend/src/pages/settings/theme.vue
@@ -88,19 +88,9 @@ import { uniqueBy } from '@/scripts/array.js';
import { fetchThemes, getThemes } from '@/theme-store.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { miLocalStorage } from '@/local-storage.js';
-import { unisonReload } from '@/scripts/unison-reload.js';
+import { reloadAsk } from '@/scripts/reload-ask.js';
import * as os from '@/os.js';
-async function reloadAsk() {
- const { canceled } = await os.confirm({
- type: 'info',
- text: i18n.ts.reloadToApplySetting,
- });
- if (canceled) return;
-
- unisonReload();
-}
-
const installedThemes = ref(getThemes());
const builtinThemes = getBuiltinThemesRef();
@@ -148,13 +138,13 @@ watch(syncDeviceDarkMode, () => {
}
});
-watch(wallpaper, () => {
+watch(wallpaper, async () => {
if (wallpaper.value == null) {
miLocalStorage.removeItem('wallpaper');
} else {
miLocalStorage.setItem('wallpaper', wallpaper.value);
}
- reloadAsk();
+ await reloadAsk({ reason: i18n.ts.reloadToApplySetting, unison: true });
});
onActivated(() => {
diff --git a/packages/frontend/src/scripts/reload-ask.ts b/packages/frontend/src/scripts/reload-ask.ts
new file mode 100644
index 000000000000..733d91b85ab4
--- /dev/null
+++ b/packages/frontend/src/scripts/reload-ask.ts
@@ -0,0 +1,40 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { i18n } from '@/i18n.js';
+import * as os from '@/os.js';
+import { unisonReload } from '@/scripts/unison-reload.js';
+
+let isReloadConfirming = false;
+
+export async function reloadAsk(opts: {
+ unison?: boolean;
+ reason?: string;
+}) {
+ if (isReloadConfirming) {
+ return;
+ }
+
+ isReloadConfirming = true;
+
+ const { canceled } = await os.confirm(opts.reason == null ? {
+ type: 'info',
+ text: i18n.ts.reloadConfirm,
+ } : {
+ type: 'info',
+ title: i18n.ts.reloadConfirm,
+ text: opts.reason,
+ }).finally(() => {
+ isReloadConfirming = false;
+ });
+
+ if (canceled) return;
+
+ if (opts.unison) {
+ unisonReload();
+ } else {
+ location.reload();
+ }
+}
From ceb4640669c10d7ddc5c63f68a9f629f7dc81191 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 18 Sep 2024 19:23:05 +0900
Subject: [PATCH 043/222] =?UTF-8?q?fix(frontend):=20vite=E3=81=AE=E4=B8=80?=
=?UTF-8?q?=E6=99=82=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=8Cgit?=
=?UTF-8?q?=E3=81=AE=E5=A4=89=E6=9B=B4=E3=81=AB=E5=90=AB=E3=81=BE=E3=82=8C?=
=?UTF-8?q?=E3=81=AA=E3=81=84=E3=82=88=E3=81=86=E3=81=AB=20(#14571)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/.gitignore b/.gitignore
index 4d5bd1ce0819..b270d5cb3a36 100644
--- a/.gitignore
+++ b/.gitignore
@@ -65,6 +65,10 @@ temp
tsdoc-metadata.json
misskey-assets
+# Vite temporary files
+vite.config.js.timestamp-*
+vite.config.ts.timestamp-*
+
# blender backups
*.blend1
*.blend2
From 4ac8aad50a1a1ef2ac2a13a04baca445294397ed Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?=
<46447427+samunohito@users.noreply.github.com>
Date: Thu, 19 Sep 2024 17:20:50 +0900
Subject: [PATCH 044/222] =?UTF-8?q?feat:=20UserWebhook/SystemWebhook?=
=?UTF-8?q?=E3=81=AE=E3=83=86=E3=82=B9=E3=83=88=E9=80=81=E4=BF=A1=E6=A9=9F?=
=?UTF-8?q?=E8=83=BD=E3=82=92=E8=BF=BD=E5=8A=A0=20(#14489)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* feat: UserWebhook/SystemWebhookのテスト送信機能を追加
* fix CHANGELOG.md
* 一部設定をパラメータから上書き出来るように修正
* remove async
* regenerate autogen
---
CHANGELOG.md | 2 +-
locales/index.d.ts | 4 +
locales/ja-JP.yml | 1 +
packages/backend/src/core/CoreModule.ts | 6 +
packages/backend/src/core/QueueService.ts | 22 +-
.../backend/src/core/SystemWebhookService.ts | 13 +-
.../backend/src/core/UserWebhookService.ts | 29 +-
.../backend/src/core/WebhookTestService.ts | 434 ++++++++++++++++++
packages/backend/src/models/Webhook.ts | 1 +
.../backend/src/server/api/EndpointsModule.ts | 8 +
packages/backend/src/server/api/endpoints.ts | 4 +
.../endpoints/admin/system-webhook/test.ts | 77 ++++
.../server/api/endpoints/i/webhooks/create.ts | 1 +
.../server/api/endpoints/i/webhooks/list.ts | 1 +
.../server/api/endpoints/i/webhooks/show.ts | 1 +
.../server/api/endpoints/i/webhooks/test.ts | 76 +++
.../backend/test/unit/SystemWebhookService.ts | 11 +-
.../backend/test/unit/UserWebhookService.ts | 245 ++++++++++
.../backend/test/unit/WebhookTestService.ts | 225 +++++++++
.../components/MkSystemWebhookEditor.impl.ts | 3 +-
.../src/components/MkSystemWebhookEditor.vue | 76 ++-
.../src/pages/settings/webhook.edit.vue | 88 +++-
packages/misskey-js/etc/misskey-js.api.md | 8 +
.../misskey-js/src/autogen/apiClientJSDoc.ts | 24 +
packages/misskey-js/src/autogen/endpoint.ts | 4 +
packages/misskey-js/src/autogen/entities.ts | 2 +
packages/misskey-js/src/autogen/types.ts | 150 ++++++
27 files changed, 1477 insertions(+), 39 deletions(-)
create mode 100644 packages/backend/src/core/WebhookTestService.ts
create mode 100644 packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
create mode 100644 packages/backend/src/server/api/endpoints/i/webhooks/test.ts
create mode 100644 packages/backend/test/unit/UserWebhookService.ts
create mode 100644 packages/backend/test/unit/WebhookTestService.ts
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7c727cea782d..4f3cd133bf17 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
## Unreleased
### General
--
+- UserWebhookとSystemWebhookのテスト送信機能を追加 ( #14445 )
### Client
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
diff --git a/locales/index.d.ts b/locales/index.d.ts
index b06e0f245b19..bd2421a5ca58 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -9477,6 +9477,10 @@ export interface Locale extends ILocale {
* Webhookを削除しますか?
*/
"deleteConfirm": string;
+ /**
+ * スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。
+ */
+ "testRemarks": string;
};
"_abuseReport": {
"_notificationRecipient": {
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 292569cc5afa..2a5b530c9f56 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -2514,6 +2514,7 @@ _webhookSettings:
abuseReportResolved: "ユーザーからの通報を処理したとき"
userCreated: "ユーザーが作成されたとき"
deleteConfirm: "Webhookを削除しますか?"
+ testRemarks: "スイッチの右にあるボタンをクリックするとダミーのデータを使用したテスト用Webhookを送信できます。"
_abuseReport:
_notificationRecipient:
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index c9427bbeb7bd..674241ac120b 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -13,6 +13,7 @@ import {
import { AbuseReportNotificationService } from '@/core/AbuseReportNotificationService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
import { UserSearchService } from '@/core/UserSearchService.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
import { AccountMoveService } from './AccountMoveService.js';
import { AccountUpdateService } from './AccountUpdateService.js';
import { AiService } from './AiService.js';
@@ -211,6 +212,7 @@ const $UserAuthService: Provider = { provide: 'UserAuthService', useExisting: Us
const $VideoProcessingService: Provider = { provide: 'VideoProcessingService', useExisting: VideoProcessingService };
const $UserWebhookService: Provider = { provide: 'UserWebhookService', useExisting: UserWebhookService };
const $SystemWebhookService: Provider = { provide: 'SystemWebhookService', useExisting: SystemWebhookService };
+const $WebhookTestService: Provider = { provide: 'WebhookTestService', useExisting: WebhookTestService };
const $UtilityService: Provider = { provide: 'UtilityService', useExisting: UtilityService };
const $FileInfoService: Provider = { provide: 'FileInfoService', useExisting: FileInfoService };
const $SearchService: Provider = { provide: 'SearchService', useExisting: SearchService };
@@ -359,6 +361,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
VideoProcessingService,
UserWebhookService,
SystemWebhookService,
+ WebhookTestService,
UtilityService,
FileInfoService,
SearchService,
@@ -503,6 +506,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$VideoProcessingService,
$UserWebhookService,
$SystemWebhookService,
+ $WebhookTestService,
$UtilityService,
$FileInfoService,
$SearchService,
@@ -648,6 +652,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
VideoProcessingService,
UserWebhookService,
SystemWebhookService,
+ WebhookTestService,
UtilityService,
FileInfoService,
SearchService,
@@ -791,6 +796,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$VideoProcessingService,
$UserWebhookService,
$SystemWebhookService,
+ $WebhookTestService,
$UtilityService,
$FileInfoService,
$SearchService,
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index 80827a500b56..ddb90a051fc7 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -452,10 +452,15 @@ export class QueueService {
/**
* @see UserWebhookDeliverJobData
- * @see WebhookDeliverProcessorService
+ * @see UserWebhookDeliverProcessorService
*/
@bindThis
- public userWebhookDeliver(webhook: MiWebhook, type: typeof webhookEventTypes[number], content: unknown) {
+ public userWebhookDeliver(
+ webhook: MiWebhook,
+ type: typeof webhookEventTypes[number],
+ content: unknown,
+ opts?: { attempts?: number },
+ ) {
const data: UserWebhookDeliverJobData = {
type,
content,
@@ -468,7 +473,7 @@ export class QueueService {
};
return this.userWebhookDeliverQueue.add(webhook.id, data, {
- attempts: 4,
+ attempts: opts?.attempts ?? 4,
backoff: {
type: 'custom',
},
@@ -479,10 +484,15 @@ export class QueueService {
/**
* @see SystemWebhookDeliverJobData
- * @see WebhookDeliverProcessorService
+ * @see SystemWebhookDeliverProcessorService
*/
@bindThis
- public systemWebhookDeliver(webhook: MiSystemWebhook, type: SystemWebhookEventType, content: unknown) {
+ public systemWebhookDeliver(
+ webhook: MiSystemWebhook,
+ type: SystemWebhookEventType,
+ content: unknown,
+ opts?: { attempts?: number },
+ ) {
const data: SystemWebhookDeliverJobData = {
type,
content,
@@ -494,7 +504,7 @@ export class QueueService {
};
return this.systemWebhookDeliverQueue.add(webhook.id, data, {
- attempts: 4,
+ attempts: opts?.attempts ?? 4,
backoff: {
type: 'custom',
},
diff --git a/packages/backend/src/core/SystemWebhookService.ts b/packages/backend/src/core/SystemWebhookService.ts
index bc6851f788da..bb7c6b8c0e02 100644
--- a/packages/backend/src/core/SystemWebhookService.ts
+++ b/packages/backend/src/core/SystemWebhookService.ts
@@ -54,7 +54,7 @@ export class SystemWebhookService implements OnApplicationShutdown {
* SystemWebhook の一覧を取得する.
*/
@bindThis
- public async fetchSystemWebhooks(params?: {
+ public fetchSystemWebhooks(params?: {
ids?: MiSystemWebhook['id'][];
isActive?: MiSystemWebhook['isActive'];
on?: MiSystemWebhook['on'];
@@ -165,19 +165,24 @@ export class SystemWebhookService implements OnApplicationShutdown {
/**
* SystemWebhook をWebhook配送キューに追加する
* @see QueueService.systemWebhookDeliver
+ * // TODO: contentの型を厳格化する
*/
@bindThis
- public async enqueueSystemWebhook(webhook: MiSystemWebhook | MiSystemWebhook['id'], type: SystemWebhookEventType, content: unknown) {
+ public async enqueueSystemWebhook(
+ webhook: MiSystemWebhook | MiSystemWebhook['id'],
+ type: T,
+ content: unknown,
+ ) {
const webhookEntity = typeof webhook === 'string'
? (await this.fetchActiveSystemWebhooks()).find(a => a.id === webhook)
: webhook;
if (!webhookEntity || !webhookEntity.isActive) {
- this.logger.info(`Webhook is not active or not found : ${webhook}`);
+ this.logger.info(`SystemWebhook is not active or not found : ${webhook}`);
return;
}
if (!webhookEntity.on.includes(type)) {
- this.logger.info(`Webhook ${webhookEntity.id} is not listening to ${type}`);
+ this.logger.info(`SystemWebhook ${webhookEntity.id} is not listening to ${type}`);
return;
}
diff --git a/packages/backend/src/core/UserWebhookService.ts b/packages/backend/src/core/UserWebhookService.ts
index e96bfeea9581..8a40a5368804 100644
--- a/packages/backend/src/core/UserWebhookService.ts
+++ b/packages/backend/src/core/UserWebhookService.ts
@@ -5,8 +5,8 @@
import { Inject, Injectable } from '@nestjs/common';
import * as Redis from 'ioredis';
-import type { WebhooksRepository } from '@/models/_.js';
-import type { MiWebhook } from '@/models/Webhook.js';
+import { type WebhooksRepository } from '@/models/_.js';
+import { MiWebhook } from '@/models/Webhook.js';
import { DI } from '@/di-symbols.js';
import { bindThis } from '@/decorators.js';
import { GlobalEvents } from '@/core/GlobalEventService.js';
@@ -38,6 +38,31 @@ export class UserWebhookService implements OnApplicationShutdown {
return this.activeWebhooks;
}
+ /**
+ * UserWebhook の一覧を取得する.
+ */
+ @bindThis
+ public fetchWebhooks(params?: {
+ ids?: MiWebhook['id'][];
+ isActive?: MiWebhook['active'];
+ on?: MiWebhook['on'];
+ }): Promise {
+ const query = this.webhooksRepository.createQueryBuilder('webhook');
+ if (params) {
+ if (params.ids && params.ids.length > 0) {
+ query.andWhere('webhook.id IN (:...ids)', { ids: params.ids });
+ }
+ if (params.isActive !== undefined) {
+ query.andWhere('webhook.active = :isActive', { isActive: params.isActive });
+ }
+ if (params.on && params.on.length > 0) {
+ query.andWhere(':on <@ webhook.on', { on: params.on });
+ }
+ }
+
+ return query.getMany();
+ }
+
@bindThis
private async onMessage(_: string, data: string): Promise {
const obj = JSON.parse(data);
diff --git a/packages/backend/src/core/WebhookTestService.ts b/packages/backend/src/core/WebhookTestService.ts
new file mode 100644
index 000000000000..0b4e107d21b9
--- /dev/null
+++ b/packages/backend/src/core/WebhookTestService.ts
@@ -0,0 +1,434 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import { MiAbuseUserReport, MiNote, MiUser, MiWebhook } from '@/models/_.js';
+import { bindThis } from '@/decorators.js';
+import { MiSystemWebhook, type SystemWebhookEventType } from '@/models/SystemWebhook.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { Packed } from '@/misc/json-schema.js';
+import { type WebhookEventTypes } from '@/models/Webhook.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
+import { QueueService } from '@/core/QueueService.js';
+
+const oneDayMillis = 24 * 60 * 60 * 1000;
+
+function generateAbuseReport(override?: Partial): MiAbuseUserReport {
+ return {
+ id: 'dummy-abuse-report1',
+ targetUserId: 'dummy-target-user',
+ targetUser: null,
+ reporterId: 'dummy-reporter-user',
+ reporter: null,
+ assigneeId: null,
+ assignee: null,
+ resolved: false,
+ forwarded: false,
+ comment: 'This is a dummy report for testing purposes.',
+ targetUserHost: null,
+ reporterHost: null,
+ ...override,
+ };
+}
+
+function generateDummyUser(override?: Partial): MiUser {
+ return {
+ id: 'dummy-user-1',
+ updatedAt: new Date(Date.now() - oneDayMillis * 7),
+ lastFetchedAt: new Date(Date.now() - oneDayMillis * 5),
+ lastActiveDate: new Date(Date.now() - oneDayMillis * 3),
+ hideOnlineStatus: false,
+ username: 'dummy1',
+ usernameLower: 'dummy1',
+ name: 'DummyUser1',
+ followersCount: 10,
+ followingCount: 5,
+ movedToUri: null,
+ movedAt: null,
+ alsoKnownAs: null,
+ notesCount: 30,
+ avatarId: null,
+ avatar: null,
+ bannerId: null,
+ banner: null,
+ avatarUrl: null,
+ bannerUrl: null,
+ avatarBlurhash: null,
+ bannerBlurhash: null,
+ avatarDecorations: [],
+ tags: [],
+ isSuspended: false,
+ isLocked: false,
+ isBot: false,
+ isCat: true,
+ isRoot: false,
+ isExplorable: true,
+ isHibernated: false,
+ isDeleted: false,
+ emojis: [],
+ host: null,
+ inbox: null,
+ sharedInbox: null,
+ featured: null,
+ uri: null,
+ followersUri: null,
+ token: null,
+ ...override,
+ };
+}
+
+function generateDummyNote(override?: Partial): MiNote {
+ return {
+ id: 'dummy-note-1',
+ replyId: null,
+ reply: null,
+ renoteId: null,
+ renote: null,
+ threadId: null,
+ text: 'This is a dummy note for testing purposes.',
+ name: null,
+ cw: null,
+ userId: 'dummy-user-1',
+ user: null,
+ localOnly: true,
+ reactionAcceptance: 'likeOnly',
+ renoteCount: 10,
+ repliesCount: 5,
+ clippedCount: 0,
+ reactions: {},
+ visibility: 'public',
+ uri: null,
+ url: null,
+ fileIds: [],
+ attachedFileTypes: [],
+ visibleUserIds: [],
+ mentions: [],
+ mentionedRemoteUsers: '[]',
+ reactionAndUserPairCache: [],
+ emojis: [],
+ tags: [],
+ hasPoll: false,
+ channelId: null,
+ channel: null,
+ userHost: null,
+ replyUserId: null,
+ replyUserHost: null,
+ renoteUserId: null,
+ renoteUserHost: null,
+ ...override,
+ };
+}
+
+function toPackedNote(note: MiNote, detail = true, override?: Packed<'Note'>): Packed<'Note'> {
+ return {
+ id: note.id,
+ createdAt: new Date().toISOString(),
+ deletedAt: null,
+ text: note.text,
+ cw: note.cw,
+ userId: note.userId,
+ user: toPackedUserLite(note.user ?? generateDummyUser()),
+ replyId: note.replyId,
+ renoteId: note.renoteId,
+ isHidden: false,
+ visibility: note.visibility,
+ mentions: note.mentions,
+ visibleUserIds: note.visibleUserIds,
+ fileIds: note.fileIds,
+ files: [],
+ tags: note.tags,
+ poll: null,
+ emojis: note.emojis,
+ channelId: note.channelId,
+ channel: note.channel,
+ localOnly: note.localOnly,
+ reactionAcceptance: note.reactionAcceptance,
+ reactionEmojis: {},
+ reactions: {},
+ reactionCount: 0,
+ renoteCount: note.renoteCount,
+ repliesCount: note.repliesCount,
+ uri: note.uri ?? undefined,
+ url: note.url ?? undefined,
+ reactionAndUserPairCache: note.reactionAndUserPairCache,
+ ...(detail ? {
+ clippedCount: note.clippedCount,
+ reply: note.reply ? toPackedNote(note.reply, false) : null,
+ renote: note.renote ? toPackedNote(note.renote, true) : null,
+ myReaction: null,
+ } : {}),
+ ...override,
+ };
+}
+
+function toPackedUserLite(user: MiUser, override?: Packed<'UserLite'>): Packed<'UserLite'> {
+ return {
+ id: user.id,
+ name: user.name,
+ username: user.username,
+ host: user.host,
+ avatarUrl: user.avatarUrl,
+ avatarBlurhash: user.avatarBlurhash,
+ avatarDecorations: user.avatarDecorations.map(it => ({
+ id: it.id,
+ angle: it.angle,
+ flipH: it.flipH,
+ url: 'https://example.com/dummy-image001.png',
+ offsetX: it.offsetX,
+ offsetY: it.offsetY,
+ })),
+ isBot: user.isBot,
+ isCat: user.isCat,
+ emojis: user.emojis,
+ onlineStatus: 'active',
+ badgeRoles: [],
+ ...override,
+ };
+}
+
+function toPackedUserDetailedNotMe(user: MiUser, override?: Packed<'UserDetailedNotMe'>): Packed<'UserDetailedNotMe'> {
+ return {
+ ...toPackedUserLite(user),
+ url: null,
+ uri: null,
+ movedTo: null,
+ alsoKnownAs: [],
+ createdAt: new Date().toISOString(),
+ updatedAt: user.updatedAt?.toISOString() ?? null,
+ lastFetchedAt: user.lastFetchedAt?.toISOString() ?? null,
+ bannerUrl: user.bannerUrl,
+ bannerBlurhash: user.bannerBlurhash,
+ isLocked: user.isLocked,
+ isSilenced: false,
+ isSuspended: user.isSuspended,
+ description: null,
+ location: null,
+ birthday: null,
+ lang: null,
+ fields: [],
+ verifiedLinks: [],
+ followersCount: user.followersCount,
+ followingCount: user.followingCount,
+ notesCount: user.notesCount,
+ pinnedNoteIds: [],
+ pinnedNotes: [],
+ pinnedPageId: null,
+ pinnedPage: null,
+ publicReactions: true,
+ followersVisibility: 'public',
+ followingVisibility: 'public',
+ twoFactorEnabled: false,
+ usePasswordLessLogin: false,
+ securityKeys: false,
+ roles: [],
+ memo: null,
+ moderationNote: undefined,
+ isFollowing: false,
+ isFollowed: false,
+ hasPendingFollowRequestFromYou: false,
+ hasPendingFollowRequestToYou: false,
+ isBlocking: false,
+ isBlocked: false,
+ isMuted: false,
+ isRenoteMuted: false,
+ notify: 'none',
+ withReplies: true,
+ ...override,
+ };
+}
+
+const dummyUser1 = generateDummyUser();
+const dummyUser2 = generateDummyUser({
+ id: 'dummy-user-2',
+ updatedAt: new Date(Date.now() - oneDayMillis * 30),
+ lastFetchedAt: new Date(Date.now() - oneDayMillis),
+ lastActiveDate: new Date(Date.now() - oneDayMillis),
+ username: 'dummy2',
+ usernameLower: 'dummy2',
+ name: 'DummyUser2',
+ followersCount: 40,
+ followingCount: 50,
+ notesCount: 900,
+});
+const dummyUser3 = generateDummyUser({
+ id: 'dummy-user-3',
+ updatedAt: new Date(Date.now() - oneDayMillis * 15),
+ lastFetchedAt: new Date(Date.now() - oneDayMillis * 2),
+ lastActiveDate: new Date(Date.now() - oneDayMillis * 2),
+ username: 'dummy3',
+ usernameLower: 'dummy3',
+ name: 'DummyUser3',
+ followersCount: 60,
+ followingCount: 70,
+ notesCount: 15900,
+});
+
+@Injectable()
+export class WebhookTestService {
+ public static NoSuchWebhookError = class extends Error {};
+
+ constructor(
+ private userWebhookService: UserWebhookService,
+ private systemWebhookService: SystemWebhookService,
+ private queueService: QueueService,
+ ) {
+ }
+
+ /**
+ * UserWebhookのテスト送信を行う.
+ * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
+ *
+ * また、この関数経由で送信されるWebhookは以下の設定を無視する.
+ * - Webhookそのものの有効・無効設定(active)
+ * - 送信対象イベント(on)に関する設定
+ */
+ @bindThis
+ public async testUserWebhook(
+ params: {
+ webhookId: MiWebhook['id'],
+ type: WebhookEventTypes,
+ override?: Partial>,
+ },
+ sender: MiUser | null,
+ ) {
+ const webhooks = await this.userWebhookService.fetchWebhooks({ ids: [params.webhookId] })
+ .then(it => it.filter(it => it.userId === sender?.id));
+ if (webhooks.length === 0) {
+ throw new WebhookTestService.NoSuchWebhookError();
+ }
+
+ const webhook = webhooks[0];
+ const send = (contents: unknown) => {
+ const merged = {
+ ...webhook,
+ ...params.override,
+ };
+
+ // テスト目的なのでUserWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
+ // また、Jobの試行回数も1回だけ.
+ this.queueService.userWebhookDeliver(merged, params.type, contents, { attempts: 1 });
+ };
+
+ const dummyNote1 = generateDummyNote({
+ userId: dummyUser1.id,
+ user: dummyUser1,
+ });
+ const dummyReply1 = generateDummyNote({
+ id: 'dummy-reply-1',
+ replyId: dummyNote1.id,
+ reply: dummyNote1,
+ userId: dummyUser1.id,
+ user: dummyUser1,
+ });
+ const dummyRenote1 = generateDummyNote({
+ id: 'dummy-renote-1',
+ renoteId: dummyNote1.id,
+ renote: dummyNote1,
+ userId: dummyUser2.id,
+ user: dummyUser2,
+ text: null,
+ });
+ const dummyMention1 = generateDummyNote({
+ id: 'dummy-mention-1',
+ userId: dummyUser1.id,
+ user: dummyUser1,
+ text: `@${dummyUser2.username} This is a mention to you.`,
+ mentions: [dummyUser2.id],
+ });
+
+ switch (params.type) {
+ case 'note': {
+ send(toPackedNote(dummyNote1));
+ break;
+ }
+ case 'reply': {
+ send(toPackedNote(dummyReply1));
+ break;
+ }
+ case 'renote': {
+ send(toPackedNote(dummyRenote1));
+ break;
+ }
+ case 'mention': {
+ send(toPackedNote(dummyMention1));
+ break;
+ }
+ case 'follow': {
+ send(toPackedUserDetailedNotMe(dummyUser1));
+ break;
+ }
+ case 'followed': {
+ send(toPackedUserLite(dummyUser2));
+ break;
+ }
+ case 'unfollow': {
+ send(toPackedUserDetailedNotMe(dummyUser3));
+ break;
+ }
+ }
+ }
+
+ /**
+ * SystemWebhookのテスト送信を行う.
+ * 送信されるペイロードはいずれもダミーの値で、実際にはデータベース上に存在しない.
+ *
+ * また、この関数経由で送信されるWebhookは以下の設定を無視する.
+ * - Webhookそのものの有効・無効設定(isActive)
+ * - 送信対象イベント(on)に関する設定
+ */
+ @bindThis
+ public async testSystemWebhook(
+ params: {
+ webhookId: MiSystemWebhook['id'],
+ type: SystemWebhookEventType,
+ override?: Partial>,
+ },
+ ) {
+ const webhooks = await this.systemWebhookService.fetchSystemWebhooks({ ids: [params.webhookId] });
+ if (webhooks.length === 0) {
+ throw new WebhookTestService.NoSuchWebhookError();
+ }
+
+ const webhook = webhooks[0];
+ const send = (contents: unknown) => {
+ const merged = {
+ ...webhook,
+ ...params.override,
+ };
+
+ // テスト目的なのでSystemWebhookServiceの機能を経由せず直接キューに追加する(チェック処理などをスキップする意図).
+ // また、Jobの試行回数も1回だけ.
+ this.queueService.systemWebhookDeliver(merged, params.type, contents, { attempts: 1 });
+ };
+
+ switch (params.type) {
+ case 'abuseReport': {
+ send(generateAbuseReport({
+ targetUserId: dummyUser1.id,
+ targetUser: dummyUser1,
+ reporterId: dummyUser2.id,
+ reporter: dummyUser2,
+ }));
+ break;
+ }
+ case 'abuseReportResolved': {
+ send(generateAbuseReport({
+ targetUserId: dummyUser1.id,
+ targetUser: dummyUser1,
+ reporterId: dummyUser2.id,
+ reporter: dummyUser2,
+ assigneeId: dummyUser3.id,
+ assignee: dummyUser3,
+ resolved: true,
+ }));
+ break;
+ }
+ case 'userCreated': {
+ send(toPackedUserLite(dummyUser1));
+ break;
+ }
+ }
+ }
+}
diff --git a/packages/backend/src/models/Webhook.ts b/packages/backend/src/models/Webhook.ts
index db24c03b3dba..b4cab4edc8ec 100644
--- a/packages/backend/src/models/Webhook.ts
+++ b/packages/backend/src/models/Webhook.ts
@@ -8,6 +8,7 @@ import { id } from './util/id.js';
import { MiUser } from './User.js';
export const webhookEventTypes = ['mention', 'unfollow', 'follow', 'followed', 'note', 'reply', 'renote', 'reaction'] as const;
+export type WebhookEventTypes = typeof webhookEventTypes[number];
@Entity('webhook')
export class MiWebhook {
diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts
index 41576bedaae7..08a0468ab29f 100644
--- a/packages/backend/src/server/api/EndpointsModule.ts
+++ b/packages/backend/src/server/api/EndpointsModule.ts
@@ -92,6 +92,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
+import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
@@ -258,6 +259,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
+import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
@@ -475,6 +477,7 @@ const $admin_systemWebhook_delete: Provider = { provide: 'ep:admin/system-webhoo
const $admin_systemWebhook_list: Provider = { provide: 'ep:admin/system-webhook/list', useClass: ep___admin_systemWebhook_list.default };
const $admin_systemWebhook_show: Provider = { provide: 'ep:admin/system-webhook/show', useClass: ep___admin_systemWebhook_show.default };
const $admin_systemWebhook_update: Provider = { provide: 'ep:admin/system-webhook/update', useClass: ep___admin_systemWebhook_update.default };
+const $admin_systemWebhook_test: Provider = { provide: 'ep:admin/system-webhook/test', useClass: ep___admin_systemWebhook_test.default };
const $announcements: Provider = { provide: 'ep:announcements', useClass: ep___announcements.default };
const $announcements_show: Provider = { provide: 'ep:announcements/show', useClass: ep___announcements_show.default };
const $antennas_create: Provider = { provide: 'ep:antennas/create', useClass: ep___antennas_create.default };
@@ -641,6 +644,7 @@ const $i_webhooks_list: Provider = { provide: 'ep:i/webhooks/list', useClass: ep
const $i_webhooks_show: Provider = { provide: 'ep:i/webhooks/show', useClass: ep___i_webhooks_show.default };
const $i_webhooks_update: Provider = { provide: 'ep:i/webhooks/update', useClass: ep___i_webhooks_update.default };
const $i_webhooks_delete: Provider = { provide: 'ep:i/webhooks/delete', useClass: ep___i_webhooks_delete.default };
+const $i_webhooks_test: Provider = { provide: 'ep:i/webhooks/test', useClass: ep___i_webhooks_test.default };
const $invite_create: Provider = { provide: 'ep:invite/create', useClass: ep___invite_create.default };
const $invite_delete: Provider = { provide: 'ep:invite/delete', useClass: ep___invite_delete.default };
const $invite_list: Provider = { provide: 'ep:invite/list', useClass: ep___invite_list.default };
@@ -862,6 +866,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
+ $admin_systemWebhook_test,
$announcements,
$announcements_show,
$antennas_create,
@@ -1028,6 +1033,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
+ $i_webhooks_test,
$invite_create,
$invite_delete,
$invite_list,
@@ -1243,6 +1249,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$admin_systemWebhook_list,
$admin_systemWebhook_show,
$admin_systemWebhook_update,
+ $admin_systemWebhook_test,
$announcements,
$announcements_show,
$antennas_create,
@@ -1409,6 +1416,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__
$i_webhooks_show,
$i_webhooks_update,
$i_webhooks_delete,
+ $i_webhooks_test,
$invite_create,
$invite_delete,
$invite_list,
diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts
index 3dfb7fdad4c2..2462781f7b54 100644
--- a/packages/backend/src/server/api/endpoints.ts
+++ b/packages/backend/src/server/api/endpoints.ts
@@ -98,6 +98,7 @@ import * as ep___admin_systemWebhook_delete from './endpoints/admin/system-webho
import * as ep___admin_systemWebhook_list from './endpoints/admin/system-webhook/list.js';
import * as ep___admin_systemWebhook_show from './endpoints/admin/system-webhook/show.js';
import * as ep___admin_systemWebhook_update from './endpoints/admin/system-webhook/update.js';
+import * as ep___admin_systemWebhook_test from './endpoints/admin/system-webhook/test.js';
import * as ep___announcements from './endpoints/announcements.js';
import * as ep___announcements_show from './endpoints/announcements/show.js';
import * as ep___antennas_create from './endpoints/antennas/create.js';
@@ -264,6 +265,7 @@ import * as ep___i_webhooks_show from './endpoints/i/webhooks/show.js';
import * as ep___i_webhooks_list from './endpoints/i/webhooks/list.js';
import * as ep___i_webhooks_update from './endpoints/i/webhooks/update.js';
import * as ep___i_webhooks_delete from './endpoints/i/webhooks/delete.js';
+import * as ep___i_webhooks_test from './endpoints/i/webhooks/test.js';
import * as ep___invite_create from './endpoints/invite/create.js';
import * as ep___invite_delete from './endpoints/invite/delete.js';
import * as ep___invite_list from './endpoints/invite/list.js';
@@ -479,6 +481,7 @@ const eps = [
['admin/system-webhook/list', ep___admin_systemWebhook_list],
['admin/system-webhook/show', ep___admin_systemWebhook_show],
['admin/system-webhook/update', ep___admin_systemWebhook_update],
+ ['admin/system-webhook/test', ep___admin_systemWebhook_test],
['announcements', ep___announcements],
['announcements/show', ep___announcements_show],
['antennas/create', ep___antennas_create],
@@ -645,6 +648,7 @@ const eps = [
['i/webhooks/show', ep___i_webhooks_show],
['i/webhooks/update', ep___i_webhooks_update],
['i/webhooks/delete', ep___i_webhooks_delete],
+ ['i/webhooks/test', ep___i_webhooks_test],
['invite/create', ep___invite_create],
['invite/delete', ep___invite_delete],
['invite/list', ep___invite_list],
diff --git a/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
new file mode 100644
index 000000000000..fb2ddf4b446d
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/admin/system-webhook/test.ts
@@ -0,0 +1,77 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { ApiError } from '@/server/api/error.js';
+import { systemWebhookEventTypes } from '@/models/SystemWebhook.js';
+
+export const meta = {
+ tags: ['webhooks'],
+
+ requireCredential: true,
+ requireModerator: true,
+ secure: true,
+ kind: 'read:admin:system-webhook',
+
+ limit: {
+ duration: ms('15min'),
+ max: 60,
+ },
+
+ errors: {
+ noSuchWebhook: {
+ message: 'No such webhook.',
+ code: 'NO_SUCH_WEBHOOK',
+ id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ webhookId: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ type: {
+ type: 'string',
+ enum: systemWebhookEventTypes,
+ },
+ override: {
+ type: 'object',
+ properties: {
+ url: { type: 'string', nullable: false },
+ secret: { type: 'string', nullable: false },
+ },
+ },
+ },
+ required: ['webhookId', 'type'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private webhookTestService: WebhookTestService,
+ ) {
+ super(meta, paramDef, async (ps) => {
+ try {
+ await this.webhookTestService.testSystemWebhook({
+ webhookId: ps.webhookId,
+ type: ps.type,
+ override: ps.override,
+ });
+ } catch (e) {
+ if (e instanceof WebhookTestService.NoSuchWebhookError) {
+ throw new ApiError(meta.errors.noSuchWebhook);
+ }
+ throw e;
+ }
+ });
+ }
+}
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
index 9eb7f5b3a033..6e84603f7a45 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/create.ts
@@ -13,6 +13,7 @@ import { DI } from '@/di-symbols.js';
import { RoleService } from '@/core/RoleService.js';
import { ApiError } from '@/server/api/error.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
index fe07afb2d089..394c178f2adf 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/list.ts
@@ -9,6 +9,7 @@ import { webhookEventTypes } from '@/models/Webhook.js';
import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks', 'account'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
index 5ddb79caf283..4a0c09ff0ccb 100644
--- a/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/show.ts
@@ -10,6 +10,7 @@ import type { WebhooksRepository } from '@/models/_.js';
import { DI } from '@/di-symbols.js';
import { ApiError } from '../../../error.js';
+// TODO: UserWebhook schemaの適用
export const meta = {
tags: ['webhooks'],
diff --git a/packages/backend/src/server/api/endpoints/i/webhooks/test.ts b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts
new file mode 100644
index 000000000000..2bf6df9ce2f9
--- /dev/null
+++ b/packages/backend/src/server/api/endpoints/i/webhooks/test.ts
@@ -0,0 +1,76 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Injectable } from '@nestjs/common';
+import ms from 'ms';
+import { Endpoint } from '@/server/api/endpoint-base.js';
+import { webhookEventTypes } from '@/models/Webhook.js';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { ApiError } from '@/server/api/error.js';
+
+export const meta = {
+ tags: ['webhooks'],
+
+ requireCredential: true,
+ secure: true,
+ kind: 'read:account',
+
+ limit: {
+ duration: ms('15min'),
+ max: 60,
+ },
+
+ errors: {
+ noSuchWebhook: {
+ message: 'No such webhook.',
+ code: 'NO_SUCH_WEBHOOK',
+ id: '0c52149c-e913-18f8-5dc7-74870bfe0cf9',
+ },
+ },
+} as const;
+
+export const paramDef = {
+ type: 'object',
+ properties: {
+ webhookId: {
+ type: 'string',
+ format: 'misskey:id',
+ },
+ type: {
+ type: 'string',
+ enum: webhookEventTypes,
+ },
+ override: {
+ type: 'object',
+ properties: {
+ url: { type: 'string' },
+ secret: { type: 'string' },
+ },
+ },
+ },
+ required: ['webhookId', 'type'],
+} as const;
+
+@Injectable()
+export default class extends Endpoint { // eslint-disable-line import/no-default-export
+ constructor(
+ private webhookTestService: WebhookTestService,
+ ) {
+ super(meta, paramDef, async (ps, me) => {
+ try {
+ await this.webhookTestService.testUserWebhook({
+ webhookId: ps.webhookId,
+ type: ps.type,
+ override: ps.override,
+ }, me);
+ } catch (e) {
+ if (e instanceof WebhookTestService.NoSuchWebhookError) {
+ throw new ApiError(meta.errors.noSuchWebhook);
+ }
+ throw e;
+ }
+ });
+ }
+}
diff --git a/packages/backend/test/unit/SystemWebhookService.ts b/packages/backend/test/unit/SystemWebhookService.ts
index 790cd1490eb5..5401dd74d80b 100644
--- a/packages/backend/test/unit/SystemWebhookService.ts
+++ b/packages/backend/test/unit/SystemWebhookService.ts
@@ -1,3 +1,4 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
@@ -6,6 +7,7 @@
import { setTimeout } from 'node:timers/promises';
import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
import { Test, TestingModule } from '@nestjs/testing';
+import { randomString } from '../utils.js';
import { MiUser } from '@/models/User.js';
import { MiSystemWebhook, SystemWebhookEventType } from '@/models/SystemWebhook.js';
import { SystemWebhooksRepository, UsersRepository } from '@/models/_.js';
@@ -17,7 +19,6 @@ import { DI } from '@/di-symbols.js';
import { QueueService } from '@/core/QueueService.js';
import { LoggerService } from '@/core/LoggerService.js';
import { SystemWebhookService } from '@/core/SystemWebhookService.js';
-import { randomString } from '../utils.js';
describe('SystemWebhookService', () => {
let app: TestingModule;
@@ -313,7 +314,7 @@ describe('SystemWebhookService', () => {
isActive: true,
on: ['abuseReport'],
});
- await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
+ await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).toHaveBeenCalled();
});
@@ -323,7 +324,7 @@ describe('SystemWebhookService', () => {
isActive: false,
on: ['abuseReport'],
});
- await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' });
+ await service.enqueueSystemWebhook(webhook.id, 'abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
});
@@ -337,8 +338,8 @@ describe('SystemWebhookService', () => {
isActive: true,
on: ['abuseReportResolved'],
});
- await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' });
- await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' });
+ await service.enqueueSystemWebhook(webhook1.id, 'abuseReport', { foo: 'bar' } as any);
+ await service.enqueueSystemWebhook(webhook2.id, 'abuseReport', { foo: 'bar' } as any);
expect(queueService.systemWebhookDeliver).not.toHaveBeenCalled();
});
diff --git a/packages/backend/test/unit/UserWebhookService.ts b/packages/backend/test/unit/UserWebhookService.ts
new file mode 100644
index 000000000000..0e88835a0243
--- /dev/null
+++ b/packages/backend/test/unit/UserWebhookService.ts
@@ -0,0 +1,245 @@
+
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { afterEach, beforeEach, describe, expect, jest } from '@jest/globals';
+import { Test, TestingModule } from '@nestjs/testing';
+import { randomString } from '../utils.js';
+import { MiUser } from '@/models/User.js';
+import { MiWebhook, UsersRepository, WebhooksRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { GlobalEventService } from '@/core/GlobalEventService.js';
+import { DI } from '@/di-symbols.js';
+import { QueueService } from '@/core/QueueService.js';
+import { LoggerService } from '@/core/LoggerService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
+
+describe('UserWebhookService', () => {
+ let app: TestingModule;
+ let service: UserWebhookService;
+
+ // --------------------------------------------------------------------------------------
+
+ let usersRepository: UsersRepository;
+ let userWebhooksRepository: WebhooksRepository;
+ let idService: IdService;
+ let queueService: jest.Mocked;
+
+ // --------------------------------------------------------------------------------------
+
+ let root: MiUser;
+
+ // --------------------------------------------------------------------------------------
+
+ async function createUser(data: Partial = {}) {
+ return await usersRepository
+ .insert({
+ id: idService.gen(),
+ ...data,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+ }
+
+ async function createWebhook(data: Partial = {}) {
+ return userWebhooksRepository
+ .insert({
+ id: idService.gen(),
+ name: randomString(),
+ on: ['mention'],
+ url: 'https://example.com',
+ secret: randomString(),
+ userId: root.id,
+ ...data,
+ })
+ .then(x => userWebhooksRepository.findOneByOrFail(x.identifiers[0]));
+ }
+
+ // --------------------------------------------------------------------------------------
+
+ async function beforeAllImpl() {
+ app = await Test
+ .createTestingModule({
+ imports: [
+ GlobalModule,
+ ],
+ providers: [
+ UserWebhookService,
+ IdService,
+ LoggerService,
+ GlobalEventService,
+ {
+ provide: QueueService, useFactory: () => ({ systemWebhookDeliver: jest.fn() }),
+ },
+ ],
+ })
+ .compile();
+
+ usersRepository = app.get(DI.usersRepository);
+ userWebhooksRepository = app.get(DI.webhooksRepository);
+
+ service = app.get(UserWebhookService);
+ idService = app.get(IdService);
+ queueService = app.get(QueueService) as jest.Mocked;
+
+ app.enableShutdownHooks();
+ }
+
+ async function afterAllImpl() {
+ await app.close();
+ }
+
+ async function beforeEachImpl() {
+ root = await createUser({ isRoot: true, username: 'root', usernameLower: 'root' });
+ }
+
+ async function afterEachImpl() {
+ await usersRepository.delete({});
+ await userWebhooksRepository.delete({});
+ }
+
+ // --------------------------------------------------------------------------------------
+
+ describe('アプリを毎回作り直す必要のないグループ', () => {
+ beforeAll(beforeAllImpl);
+ afterAll(afterAllImpl);
+ beforeEach(beforeEachImpl);
+ afterEach(afterEachImpl);
+
+ describe('fetchSystemWebhooks', () => {
+ test('フィルタなし', async () => {
+ const webhook1 = await createWebhook({
+ active: true,
+ on: ['mention'],
+ });
+ const webhook2 = await createWebhook({
+ active: false,
+ on: ['mention'],
+ });
+ const webhook3 = await createWebhook({
+ active: true,
+ on: ['reply'],
+ });
+ const webhook4 = await createWebhook({
+ active: false,
+ on: [],
+ });
+
+ const fetchedWebhooks = await service.fetchWebhooks();
+ expect(fetchedWebhooks).toEqual([webhook1, webhook2, webhook3, webhook4]);
+ });
+
+ test('activeのみ', async () => {
+ const webhook1 = await createWebhook({
+ active: true,
+ on: ['mention'],
+ });
+ const webhook2 = await createWebhook({
+ active: false,
+ on: ['mention'],
+ });
+ const webhook3 = await createWebhook({
+ active: true,
+ on: ['reply'],
+ });
+ const webhook4 = await createWebhook({
+ active: false,
+ on: [],
+ });
+
+ const fetchedWebhooks = await service.fetchWebhooks({ isActive: true });
+ expect(fetchedWebhooks).toEqual([webhook1, webhook3]);
+ });
+
+ test('特定のイベントのみ', async () => {
+ const webhook1 = await createWebhook({
+ active: true,
+ on: ['mention'],
+ });
+ const webhook2 = await createWebhook({
+ active: false,
+ on: ['mention'],
+ });
+ const webhook3 = await createWebhook({
+ active: true,
+ on: ['reply'],
+ });
+ const webhook4 = await createWebhook({
+ active: false,
+ on: [],
+ });
+
+ const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'] });
+ expect(fetchedWebhooks).toEqual([webhook1, webhook2]);
+ });
+
+ test('activeな特定のイベントのみ', async () => {
+ const webhook1 = await createWebhook({
+ active: true,
+ on: ['mention'],
+ });
+ const webhook2 = await createWebhook({
+ active: false,
+ on: ['mention'],
+ });
+ const webhook3 = await createWebhook({
+ active: true,
+ on: ['reply'],
+ });
+ const webhook4 = await createWebhook({
+ active: false,
+ on: [],
+ });
+
+ const fetchedWebhooks = await service.fetchWebhooks({ on: ['mention'], isActive: true });
+ expect(fetchedWebhooks).toEqual([webhook1]);
+ });
+
+ test('ID指定', async () => {
+ const webhook1 = await createWebhook({
+ active: true,
+ on: ['mention'],
+ });
+ const webhook2 = await createWebhook({
+ active: false,
+ on: ['mention'],
+ });
+ const webhook3 = await createWebhook({
+ active: true,
+ on: ['reply'],
+ });
+ const webhook4 = await createWebhook({
+ active: false,
+ on: [],
+ });
+
+ const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id] });
+ expect(fetchedWebhooks).toEqual([webhook1, webhook4]);
+ });
+
+ test('ID指定(他条件とANDになるか見たい)', async () => {
+ const webhook1 = await createWebhook({
+ active: true,
+ on: ['mention'],
+ });
+ const webhook2 = await createWebhook({
+ active: false,
+ on: ['mention'],
+ });
+ const webhook3 = await createWebhook({
+ active: true,
+ on: ['reply'],
+ });
+ const webhook4 = await createWebhook({
+ active: false,
+ on: [],
+ });
+
+ const fetchedWebhooks = await service.fetchWebhooks({ ids: [webhook1.id, webhook4.id], isActive: false });
+ expect(fetchedWebhooks).toEqual([webhook4]);
+ });
+ });
+ });
+});
diff --git a/packages/backend/test/unit/WebhookTestService.ts b/packages/backend/test/unit/WebhookTestService.ts
new file mode 100644
index 000000000000..5e63b86f8fb3
--- /dev/null
+++ b/packages/backend/test/unit/WebhookTestService.ts
@@ -0,0 +1,225 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Test, TestingModule } from '@nestjs/testing';
+import { beforeAll, describe, jest } from '@jest/globals';
+import { WebhookTestService } from '@/core/WebhookTestService.js';
+import { UserWebhookService } from '@/core/UserWebhookService.js';
+import { SystemWebhookService } from '@/core/SystemWebhookService.js';
+import { GlobalModule } from '@/GlobalModule.js';
+import { MiSystemWebhook, MiUser, MiWebhook, UserProfilesRepository, UsersRepository } from '@/models/_.js';
+import { IdService } from '@/core/IdService.js';
+import { DI } from '@/di-symbols.js';
+import { QueueService } from '@/core/QueueService.js';
+
+describe('WebhookTestService', () => {
+ let app: TestingModule;
+ let service: WebhookTestService;
+
+ // --------------------------------------------------------------------------------------
+
+ let usersRepository: UsersRepository;
+ let userProfilesRepository: UserProfilesRepository;
+ let queueService: jest.Mocked;
+ let userWebhookService: jest.Mocked;
+ let systemWebhookService: jest.Mocked;
+ let idService: IdService;
+
+ let root: MiUser;
+ let alice: MiUser;
+
+ async function createUser(data: Partial = {}) {
+ const user = await usersRepository
+ .insert({
+ id: idService.gen(),
+ ...data,
+ })
+ .then(x => usersRepository.findOneByOrFail(x.identifiers[0]));
+
+ await userProfilesRepository.insert({
+ userId: user.id,
+ });
+
+ return user;
+ }
+
+ // --------------------------------------------------------------------------------------
+
+ beforeAll(async () => {
+ app = await Test.createTestingModule({
+ imports: [
+ GlobalModule,
+ ],
+ providers: [
+ WebhookTestService,
+ IdService,
+ {
+ provide: QueueService, useFactory: () => ({
+ systemWebhookDeliver: jest.fn(),
+ userWebhookDeliver: jest.fn(),
+ }),
+ },
+ {
+ provide: UserWebhookService, useFactory: () => ({
+ fetchWebhooks: jest.fn(),
+ }),
+ },
+ {
+ provide: SystemWebhookService, useFactory: () => ({
+ fetchSystemWebhooks: jest.fn(),
+ }),
+ },
+ ],
+ }).compile();
+
+ usersRepository = app.get(DI.usersRepository);
+ userProfilesRepository = app.get(DI.userProfilesRepository);
+
+ service = app.get(WebhookTestService);
+ idService = app.get(IdService);
+ queueService = app.get(QueueService) as jest.Mocked;
+ userWebhookService = app.get(UserWebhookService) as jest.Mocked;
+ systemWebhookService = app.get(SystemWebhookService) as jest.Mocked;
+
+ app.enableShutdownHooks();
+ });
+
+ beforeEach(async () => {
+ root = await createUser({ username: 'root', usernameLower: 'root', isRoot: true });
+ alice = await createUser({ username: 'alice', usernameLower: 'alice', isRoot: false });
+
+ userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
+ { id: 'dummy-webhook', active: true, userId: alice.id } as MiWebhook,
+ ]));
+ systemWebhookService.fetchSystemWebhooks.mockReturnValue(Promise.resolve([
+ { id: 'dummy-webhook', isActive: true } as MiSystemWebhook,
+ ]));
+ });
+
+ afterEach(async () => {
+ queueService.systemWebhookDeliver.mockClear();
+ queueService.userWebhookDeliver.mockClear();
+ userWebhookService.fetchWebhooks.mockClear();
+ systemWebhookService.fetchSystemWebhooks.mockClear();
+
+ await usersRepository.delete({});
+ await userProfilesRepository.delete({});
+ });
+
+ afterAll(async () => {
+ await app.close();
+ });
+
+ // --------------------------------------------------------------------------------------
+
+ describe('testUserWebhook', () => {
+ test('note', async () => {
+ await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, alice);
+
+ const calls = queueService.userWebhookDeliver.mock.calls[0];
+ expect((calls[0] as any).id).toBe('dummy-webhook');
+ expect(calls[1]).toBe('note');
+ expect((calls[2] as any).id).toBe('dummy-note-1');
+ });
+
+ test('reply', async () => {
+ await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'reply' }, alice);
+
+ const calls = queueService.userWebhookDeliver.mock.calls[0];
+ expect((calls[0] as any).id).toBe('dummy-webhook');
+ expect(calls[1]).toBe('reply');
+ expect((calls[2] as any).id).toBe('dummy-reply-1');
+ });
+
+ test('renote', async () => {
+ await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'renote' }, alice);
+
+ const calls = queueService.userWebhookDeliver.mock.calls[0];
+ expect((calls[0] as any).id).toBe('dummy-webhook');
+ expect(calls[1]).toBe('renote');
+ expect((calls[2] as any).id).toBe('dummy-renote-1');
+ });
+
+ test('mention', async () => {
+ await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'mention' }, alice);
+
+ const calls = queueService.userWebhookDeliver.mock.calls[0];
+ expect((calls[0] as any).id).toBe('dummy-webhook');
+ expect(calls[1]).toBe('mention');
+ expect((calls[2] as any).id).toBe('dummy-mention-1');
+ });
+
+ test('follow', async () => {
+ await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'follow' }, alice);
+
+ const calls = queueService.userWebhookDeliver.mock.calls[0];
+ expect((calls[0] as any).id).toBe('dummy-webhook');
+ expect(calls[1]).toBe('follow');
+ expect((calls[2] as any).id).toBe('dummy-user-1');
+ });
+
+ test('followed', async () => {
+ await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'followed' }, alice);
+
+ const calls = queueService.userWebhookDeliver.mock.calls[0];
+ expect((calls[0] as any).id).toBe('dummy-webhook');
+ expect(calls[1]).toBe('followed');
+ expect((calls[2] as any).id).toBe('dummy-user-2');
+ });
+
+ test('unfollow', async () => {
+ await service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'unfollow' }, alice);
+
+ const calls = queueService.userWebhookDeliver.mock.calls[0];
+ expect((calls[0] as any).id).toBe('dummy-webhook');
+ expect(calls[1]).toBe('unfollow');
+ expect((calls[2] as any).id).toBe('dummy-user-3');
+ });
+
+ describe('NoSuchWebhookError', () => {
+ test('user not match', async () => {
+ userWebhookService.fetchWebhooks.mockClear();
+ userWebhookService.fetchWebhooks.mockReturnValue(Promise.resolve([
+ { id: 'dummy-webhook', active: true } as MiWebhook,
+ ]));
+
+ await expect(service.testUserWebhook({ webhookId: 'dummy-webhook', type: 'note' }, root))
+ .rejects.toThrow(WebhookTestService.NoSuchWebhookError);
+ });
+ });
+ });
+
+ describe('testSystemWebhook', () => {
+ test('abuseReport', async () => {
+ await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReport' });
+
+ const calls = queueService.systemWebhookDeliver.mock.calls[0];
+ expect((calls[0] as any).id).toBe('dummy-webhook');
+ expect(calls[1]).toBe('abuseReport');
+ expect((calls[2] as any).id).toBe('dummy-abuse-report1');
+ expect((calls[2] as any).resolved).toBe(false);
+ });
+
+ test('abuseReportResolved', async () => {
+ await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'abuseReportResolved' });
+
+ const calls = queueService.systemWebhookDeliver.mock.calls[0];
+ expect((calls[0] as any).id).toBe('dummy-webhook');
+ expect(calls[1]).toBe('abuseReportResolved');
+ expect((calls[2] as any).id).toBe('dummy-abuse-report1');
+ expect((calls[2] as any).resolved).toBe(true);
+ });
+
+ test('userCreated', async () => {
+ await service.testSystemWebhook({ webhookId: 'dummy-webhook', type: 'userCreated' });
+
+ const calls = queueService.systemWebhookDeliver.mock.calls[0];
+ expect((calls[0] as any).id).toBe('dummy-webhook');
+ expect(calls[1]).toBe('userCreated');
+ expect((calls[2] as any).id).toBe('dummy-user-1');
+ });
+ });
+});
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
index 69b8edd85aec..19e4eea733cd 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.impl.ts
@@ -4,9 +4,10 @@
*/
import { defineAsyncComponent } from 'vue';
+import * as Misskey from 'misskey-js';
import * as os from '@/os.js';
-export type SystemWebhookEventType = 'abuseReport' | 'abuseReportResolved';
+export type SystemWebhookEventType = Misskey.entities.SystemWebhook['on'][number];
export type MkSystemWebhookEditorProps = {
mode: 'create' | 'edit';
diff --git a/packages/frontend/src/components/MkSystemWebhookEditor.vue b/packages/frontend/src/components/MkSystemWebhookEditor.vue
index f5c7a3160bb4..ec3b1c90caf9 100644
--- a/packages/frontend/src/components/MkSystemWebhookEditor.vue
+++ b/packages/frontend/src/components/MkSystemWebhookEditor.vue
@@ -35,16 +35,31 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts._webhookSettings.trigger }}
- ]
-
- {{ i18n.ts._webhookSettings._systemEvents.abuseReport }}
-
-
- {{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}
-
-
- {{ i18n.ts._webhookSettings._systemEvents.userCreated }}
-
+
+
+
+
+ {{ i18n.ts._webhookSettings._systemEvents.abuseReport }}
+
+
+
+
+
+ {{ i18n.ts._webhookSettings._systemEvents.abuseReportResolved }}
+
+
+
+
+
+ {{ i18n.ts._webhookSettings._systemEvents.userCreated }}
+
+
+
+
+
+
+ {{ i18n.ts._webhookSettings.testRemarks }}
+
@@ -66,6 +81,7 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
diff --git a/packages/misskey-js/etc/misskey-js.api.md b/packages/misskey-js/etc/misskey-js.api.md
index 1ec7f0ec7f39..d1050d4727e4 100644
--- a/packages/misskey-js/etc/misskey-js.api.md
+++ b/packages/misskey-js/etc/misskey-js.api.md
@@ -358,6 +358,9 @@ type AdminSystemWebhookShowRequest = operations['admin___system-webhook___show']
// @public (undocumented)
type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
+// @public (undocumented)
+type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
+
// @public (undocumented)
type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
@@ -1308,6 +1311,7 @@ declare namespace entities {
AdminSystemWebhookShowResponse,
AdminSystemWebhookUpdateRequest,
AdminSystemWebhookUpdateResponse,
+ AdminSystemWebhookTestRequest,
AnnouncementsRequest,
AnnouncementsResponse,
AnnouncementsShowRequest,
@@ -1567,6 +1571,7 @@ declare namespace entities {
IWebhooksShowResponse,
IWebhooksUpdateRequest,
IWebhooksDeleteRequest,
+ IWebhooksTestRequest,
InviteCreateResponse,
InviteDeleteRequest,
InviteListRequest,
@@ -2369,6 +2374,9 @@ type IWebhooksShowRequest = operations['i___webhooks___show']['requestBody']['co
// @public (undocumented)
type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
+// @public (undocumented)
+type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json'];
+
// @public (undocumented)
type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/apiClientJSDoc.ts b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
index e799d4a0c5e0..1d96196d1c68 100644
--- a/packages/misskey-js/src/autogen/apiClientJSDoc.ts
+++ b/packages/misskey-js/src/autogen/apiClientJSDoc.ts
@@ -960,6 +960,18 @@ declare module '../api.js' {
credential?: string | null,
): Promise
>;
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
/**
* No description provided.
*
@@ -2819,6 +2831,18 @@ declare module '../api.js' {
credential?: string | null,
): Promise>;
+ /**
+ * No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:account*
+ */
+ request(
+ endpoint: E,
+ params: P,
+ credential?: string | null,
+ ): Promise>;
+
/**
* No description provided.
*
diff --git a/packages/misskey-js/src/autogen/endpoint.ts b/packages/misskey-js/src/autogen/endpoint.ts
index 8fbdbbb629ad..42c74599a505 100644
--- a/packages/misskey-js/src/autogen/endpoint.ts
+++ b/packages/misskey-js/src/autogen/endpoint.ts
@@ -117,6 +117,7 @@ import type {
AdminSystemWebhookShowResponse,
AdminSystemWebhookUpdateRequest,
AdminSystemWebhookUpdateResponse,
+ AdminSystemWebhookTestRequest,
AnnouncementsRequest,
AnnouncementsResponse,
AnnouncementsShowRequest,
@@ -376,6 +377,7 @@ import type {
IWebhooksShowResponse,
IWebhooksUpdateRequest,
IWebhooksDeleteRequest,
+ IWebhooksTestRequest,
InviteCreateResponse,
InviteDeleteRequest,
InviteListRequest,
@@ -660,6 +662,7 @@ export type Endpoints = {
'admin/system-webhook/list': { req: AdminSystemWebhookListRequest; res: AdminSystemWebhookListResponse };
'admin/system-webhook/show': { req: AdminSystemWebhookShowRequest; res: AdminSystemWebhookShowResponse };
'admin/system-webhook/update': { req: AdminSystemWebhookUpdateRequest; res: AdminSystemWebhookUpdateResponse };
+ 'admin/system-webhook/test': { req: AdminSystemWebhookTestRequest; res: EmptyResponse };
'announcements': { req: AnnouncementsRequest; res: AnnouncementsResponse };
'announcements/show': { req: AnnouncementsShowRequest; res: AnnouncementsShowResponse };
'antennas/create': { req: AntennasCreateRequest; res: AntennasCreateResponse };
@@ -826,6 +829,7 @@ export type Endpoints = {
'i/webhooks/show': { req: IWebhooksShowRequest; res: IWebhooksShowResponse };
'i/webhooks/update': { req: IWebhooksUpdateRequest; res: EmptyResponse };
'i/webhooks/delete': { req: IWebhooksDeleteRequest; res: EmptyResponse };
+ 'i/webhooks/test': { req: IWebhooksTestRequest; res: EmptyResponse };
'invite/create': { req: EmptyRequest; res: InviteCreateResponse };
'invite/delete': { req: InviteDeleteRequest; res: EmptyResponse };
'invite/list': { req: InviteListRequest; res: InviteListResponse };
diff --git a/packages/misskey-js/src/autogen/entities.ts b/packages/misskey-js/src/autogen/entities.ts
index 357b5e9eaf68..87ed653d44ce 100644
--- a/packages/misskey-js/src/autogen/entities.ts
+++ b/packages/misskey-js/src/autogen/entities.ts
@@ -120,6 +120,7 @@ export type AdminSystemWebhookShowRequest = operations['admin___system-webhook__
export type AdminSystemWebhookShowResponse = operations['admin___system-webhook___show']['responses']['200']['content']['application/json'];
export type AdminSystemWebhookUpdateRequest = operations['admin___system-webhook___update']['requestBody']['content']['application/json'];
export type AdminSystemWebhookUpdateResponse = operations['admin___system-webhook___update']['responses']['200']['content']['application/json'];
+export type AdminSystemWebhookTestRequest = operations['admin___system-webhook___test']['requestBody']['content']['application/json'];
export type AnnouncementsRequest = operations['announcements']['requestBody']['content']['application/json'];
export type AnnouncementsResponse = operations['announcements']['responses']['200']['content']['application/json'];
export type AnnouncementsShowRequest = operations['announcements___show']['requestBody']['content']['application/json'];
@@ -379,6 +380,7 @@ export type IWebhooksShowRequest = operations['i___webhooks___show']['requestBod
export type IWebhooksShowResponse = operations['i___webhooks___show']['responses']['200']['content']['application/json'];
export type IWebhooksUpdateRequest = operations['i___webhooks___update']['requestBody']['content']['application/json'];
export type IWebhooksDeleteRequest = operations['i___webhooks___delete']['requestBody']['content']['application/json'];
+export type IWebhooksTestRequest = operations['i___webhooks___test']['requestBody']['content']['application/json'];
export type InviteCreateResponse = operations['invite___create']['responses']['200']['content']['application/json'];
export type InviteDeleteRequest = operations['invite___delete']['requestBody']['content']['application/json'];
export type InviteListRequest = operations['invite___list']['requestBody']['content']['application/json'];
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index b99a5373bbe3..03828b6552a9 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -797,6 +797,16 @@ export type paths = {
*/
post: operations['admin___system-webhook___update'];
};
+ '/admin/system-webhook/test': {
+ /**
+ * admin/system-webhook/test
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
+ */
+ post: operations['admin___system-webhook___test'];
+ };
'/announcements': {
/**
* announcements
@@ -2436,6 +2446,16 @@ export type paths = {
*/
post: operations['i___webhooks___delete'];
};
+ '/i/webhooks/test': {
+ /**
+ * i/webhooks/test
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:account*
+ */
+ post: operations['i___webhooks___test'];
+ };
'/invite/create': {
/**
* invite/create
@@ -10327,6 +10347,71 @@ export type operations = {
};
};
};
+ /**
+ * admin/system-webhook/test
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:admin:system-webhook*
+ */
+ 'admin___system-webhook___test': {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ webhookId: string;
+ /** @enum {string} */
+ type: 'abuseReport' | 'abuseReportResolved' | 'userCreated';
+ override?: {
+ url?: string;
+ secret?: string;
+ };
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description To many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
/**
* announcements
* @description No description provided.
@@ -20146,6 +20231,71 @@ export type operations = {
};
};
};
+ /**
+ * i/webhooks/test
+ * @description No description provided.
+ *
+ * **Internal Endpoint**: This endpoint is an API for the misskey mainframe and is not intended for use by third parties.
+ * **Credential required**: *Yes* / **Permission**: *read:account*
+ */
+ i___webhooks___test: {
+ requestBody: {
+ content: {
+ 'application/json': {
+ /** Format: misskey:id */
+ webhookId: string;
+ /** @enum {string} */
+ type: 'mention' | 'unfollow' | 'follow' | 'followed' | 'note' | 'reply' | 'renote' | 'reaction';
+ override?: {
+ url?: string;
+ secret?: string;
+ };
+ };
+ };
+ };
+ responses: {
+ /** @description OK (without any results) */
+ 204: {
+ content: never;
+ };
+ /** @description Client error */
+ 400: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Authentication error */
+ 401: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Forbidden error */
+ 403: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description I'm Ai */
+ 418: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description To many requests */
+ 429: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ /** @description Internal server error */
+ 500: {
+ content: {
+ 'application/json': components['schemas']['Error'];
+ };
+ };
+ };
+ };
/**
* invite/create
* @description No description provided.
From f5563c8304cea47aead629382425a394a48ba8fa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Thu, 19 Sep 2024 17:30:13 +0900
Subject: [PATCH 045/222] =?UTF-8?q?Update=20CHANGELOG.md=20(=E6=9B=B8?=
=?UTF-8?q?=E3=81=8D=E6=96=B9=E3=82=92=E6=8F=83=E3=81=88=E3=82=8B)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f3cd133bf17..35c787d565d5 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,7 +1,7 @@
## Unreleased
### General
-- UserWebhookとSystemWebhookのテスト送信機能を追加 ( #14445 )
+- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
### Client
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
From 2d0e9e05441db782e40406552047f34be7f34e63 Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
Date: Thu, 19 Sep 2024 11:55:43 +0000
Subject: [PATCH 046/222] Bump version to 2024.9.0-alpha.0
---
CHANGELOG.md | 2 +-
package.json | 2 +-
packages/misskey-js/package.json | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 35c787d565d5..82b7f4f3550e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-## Unreleased
+## 2024.9.0
### General
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
diff --git a/package.json b/package.json
index 85b4f62752bc..d03960b5b2e4 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "misskey",
- "version": "2024.8.0",
+ "version": "2024.9.0-alpha.0",
"codename": "nasubi",
"repository": {
"type": "git",
diff --git a/packages/misskey-js/package.json b/packages/misskey-js/package.json
index 39e687d4af34..3c23e4e9a117 100644
--- a/packages/misskey-js/package.json
+++ b/packages/misskey-js/package.json
@@ -1,7 +1,7 @@
{
"type": "module",
"name": "misskey-js",
- "version": "2024.8.0",
+ "version": "2024.9.0-alpha.0",
"description": "Misskey SDK for JavaScript",
"license": "MIT",
"main": "./built/index.js",
From 8d23122fd664564dc069ca8e8e337f4d4a1727fc Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 20 Sep 2024 00:08:14 +0900
Subject: [PATCH 047/222] fix(frontend): run pnpm build-assets (#14585)
---
locales/index.d.ts | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/locales/index.d.ts b/locales/index.d.ts
index bd2421a5ca58..339e62568436 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -2384,6 +2384,14 @@ export interface Locale extends ILocale {
* スクラッチパッドは、AiScriptの実験環境を提供します。Misskeyと対話するコードの記述、実行、結果の確認ができます。
*/
"scratchpadDescription": string;
+ /**
+ * UIインスペクター
+ */
+ "uiInspector": string;
+ /**
+ * メモリ上に存在しているUIコンポーネントのインスタンスの一覧を見ることができます。UIコンポーネントはUi:C:系関数により生成されます。
+ */
+ "uiInspectorDescription": string;
/**
* 出力
*/
From f585f70dcbb7d57b59eff62ccbf7d27db97e87c2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 20 Sep 2024 14:36:36 +0900
Subject: [PATCH 048/222] =?UTF-8?q?Update=20CHANGELOG.md=20(=E5=9F=8B?=
=?UTF-8?q?=E3=82=81=E8=BE=BC=E3=81=BF=E6=A9=9F=E8=83=BD=E3=81=AE=E3=83=89?=
=?UTF-8?q?=E3=82=AD=E3=83=A5=E3=83=A1=E3=83=B3=E3=83=88=E3=81=B8=E3=81=AE?=
=?UTF-8?q?=E3=83=AA=E3=83=B3=E3=82=AF)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 82b7f4f3550e..65ed505c0e5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,7 +5,7 @@
### Client
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
- - 埋め込みコードやウェブサイトへの実装方法の詳細はMisskey Hubに掲載予定です
+ - 埋め込みコードやウェブサイトへの実装方法の詳細は https://misskey-hub.net/docs/for-users/features/embed/ をご覧ください
- Enhance: サイズ制限を超過するファイルをアップロードしようとした際にエラーを出すように
- Enhance: アイコンデコレーション管理画面にプレビューを追加
- Enhance: コントロールパネル内のファイル一覧でセンシティブなファイルを区別しやすく
From 0b062f1407688906483e2427d87b708ce1a2dc47 Mon Sep 17 00:00:00 2001
From: syuilo <4439005+syuilo@users.noreply.github.com>
Date: Fri, 20 Sep 2024 21:03:53 +0900
Subject: [PATCH 049/222] =?UTF-8?q?Misskey=C2=AE=20Reactions=20Buffering?=
=?UTF-8?q?=20Technology=E2=84=A2=20(#14579)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* wip
* wip
* Update ReactionsBufferingService.ts
* Update ReactionsBufferingService.ts
* wip
* wip
* wip
* Update ReactionsBufferingService.ts
* wip
* wip
* wip
* Update NoteEntityService.ts
* wip
* wip
* wip
* wip
* Update CHANGELOG.md
---
.config/cypress-devcontainer.yml | 8 +
.config/docker_example.yml | 8 +
.config/example.yml | 10 ++
.devcontainer/devcontainer.yml | 8 +
CHANGELOG.md | 1 +
chart/files/default.yml | 8 +
locales/index.d.ts | 4 +
locales/ja-JP.yml | 1 +
.../1726804538569-reactions-buffering.js | 16 ++
packages/backend/src/GlobalModule.ts | 14 +-
packages/backend/src/config.ts | 3 +
packages/backend/src/const.ts | 2 +
packages/backend/src/core/CoreModule.ts | 6 +
packages/backend/src/core/QueueService.ts | 6 +
packages/backend/src/core/ReactionService.ts | 60 ++++---
.../src/core/ReactionsBufferingService.ts | 162 ++++++++++++++++++
.../src/core/entities/NoteEntityService.ts | 80 +++++++--
packages/backend/src/di-symbols.ts | 1 +
packages/backend/src/models/Meta.ts | 5 +
.../backend/src/queue/QueueProcessorModule.ts | 2 +
.../src/queue/QueueProcessorService.ts | 3 +
.../BakeBufferedReactionsProcessorService.ts | 40 +++++
.../backend/src/server/HealthServerService.ts | 4 +
.../src/server/api/endpoints/admin/meta.ts | 5 +
.../server/api/endpoints/admin/update-meta.ts | 5 +
.../test/unit/entities/UserEntityService.ts | 4 +-
.../src/pages/admin/other-settings.vue | 72 ++++++++
.../frontend/src/pages/admin/settings.vue | 50 ------
packages/misskey-js/src/autogen/types.ts | 2 +
29 files changed, 498 insertions(+), 92 deletions(-)
create mode 100644 packages/backend/migration/1726804538569-reactions-buffering.js
create mode 100644 packages/backend/src/core/ReactionsBufferingService.ts
create mode 100644 packages/backend/src/queue/processors/BakeBufferedReactionsProcessorService.ts
diff --git a/.config/cypress-devcontainer.yml b/.config/cypress-devcontainer.yml
index e8da5f5e276a..91dce3515585 100644
--- a/.config/cypress-devcontainer.yml
+++ b/.config/cypress-devcontainer.yml
@@ -103,6 +103,14 @@ redis:
# #prefix: example-prefix
# #db: 1
+#redisForReactions:
+# host: redis
+# port: 6379
+# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
+# #pass: example-pass
+# #prefix: example-prefix
+# #db: 1
+
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
diff --git a/.config/docker_example.yml b/.config/docker_example.yml
index d347882d1a91..3f8e5734ce87 100644
--- a/.config/docker_example.yml
+++ b/.config/docker_example.yml
@@ -106,6 +106,14 @@ redis:
# #prefix: example-prefix
# #db: 1
+#redisForReactions:
+# host: redis
+# port: 6379
+# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
+# #pass: example-pass
+# #prefix: example-prefix
+# #db: 1
+
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
diff --git a/.config/example.yml b/.config/example.yml
index b11cbd137328..7080159117ac 100644
--- a/.config/example.yml
+++ b/.config/example.yml
@@ -172,6 +172,16 @@ redis:
# # You can specify more ioredis options...
# #username: example-username
+#redisForReactions:
+# host: localhost
+# port: 6379
+# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
+# #pass: example-pass
+# #prefix: example-prefix
+# #db: 1
+# # You can specify more ioredis options...
+# #username: example-username
+
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
diff --git a/.devcontainer/devcontainer.yml b/.devcontainer/devcontainer.yml
index beefcfd0a2d5..3eb4fc28794b 100644
--- a/.devcontainer/devcontainer.yml
+++ b/.devcontainer/devcontainer.yml
@@ -103,6 +103,14 @@ redis:
# #prefix: example-prefix
# #db: 1
+#redisForReactions:
+# host: redis
+# port: 6379
+# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
+# #pass: example-pass
+# #prefix: example-prefix
+# #db: 1
+
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 65ed505c0e5f..a2d2e62a62a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -17,6 +17,7 @@
- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
### Server
+- Feat: Misskey® Reactions Buffering Technology™ (RBT)により、リアクションの作成負荷を低減することが可能に
- Fix: アンテナの書き込み時にキーワードが与えられなかった場合のエラーをApiErrorとして投げるように
- この変更により、公式フロントエンドでは入力の不備が内部エラーとして報告される代わりに一般的なエラーダイアログで報告されます
- Fix: ファイルがサイズの制限を超えてアップロードされた際にエラーを返さなかった問題を修正
diff --git a/chart/files/default.yml b/chart/files/default.yml
index f98b8ebfee04..4d17131c2546 100644
--- a/chart/files/default.yml
+++ b/chart/files/default.yml
@@ -124,6 +124,14 @@ redis:
# #prefix: example-prefix
# #db: 1
+#redisForReactions:
+# host: redis
+# port: 6379
+# #family: 0 # 0=Both, 4=IPv4, 6=IPv6
+# #pass: example-pass
+# #prefix: example-prefix
+# #db: 1
+
# ┌───────────────────────────┐
#───┘ MeiliSearch configuration └─────────────────────────────
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 339e62568436..798cb89f8336 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -5583,6 +5583,10 @@ export interface Locale extends ILocale {
* 有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。
*/
"fanoutTimelineDbFallbackDescription": string;
+ /**
+ * 有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。
+ */
+ "reactionsBufferingDescription": string;
/**
* 問い合わせ先URL
*/
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 2a5b530c9f56..726e4f4ef450 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1411,6 +1411,7 @@ _serverSettings:
fanoutTimelineDescription: "有効にすると、各種タイムラインを取得する際のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。サーバーのメモリ容量が少ない場合、または動作が不安定な場合は無効にすることができます。"
fanoutTimelineDbFallback: "データベースへのフォールバック"
fanoutTimelineDbFallbackDescription: "有効にすると、タイムラインがキャッシュされていない場合にDBへ追加で問い合わせを行うフォールバック処理を行います。無効にすると、フォールバック処理を行わないことでさらにサーバーの負荷を軽減することができますが、タイムラインが取得できる範囲に制限が生じます。"
+ reactionsBufferingDescription: "有効にすると、リアクション作成時のパフォーマンスが大幅に向上し、データベースへの負荷を軽減することが可能です。ただし、Redisのメモリ使用量は増加します。"
inquiryUrl: "問い合わせ先URL"
inquiryUrlDescription: "サーバー運営者へのお問い合わせフォームのURLや、運営者の連絡先等が記載されたWebページのURLを指定します。"
diff --git a/packages/backend/migration/1726804538569-reactions-buffering.js b/packages/backend/migration/1726804538569-reactions-buffering.js
new file mode 100644
index 000000000000..bc19e9cc8aa6
--- /dev/null
+++ b/packages/backend/migration/1726804538569-reactions-buffering.js
@@ -0,0 +1,16 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+export class ReactionsBuffering1726804538569 {
+ name = 'ReactionsBuffering1726804538569'
+
+ async up(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" ADD "enableReactionsBuffering" boolean NOT NULL DEFAULT false`);
+ }
+
+ async down(queryRunner) {
+ await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "enableReactionsBuffering"`);
+ }
+}
diff --git a/packages/backend/src/GlobalModule.ts b/packages/backend/src/GlobalModule.ts
index 09971e8ca022..2ecc1f474259 100644
--- a/packages/backend/src/GlobalModule.ts
+++ b/packages/backend/src/GlobalModule.ts
@@ -78,11 +78,19 @@ const $redisForTimelines: Provider = {
inject: [DI.config],
};
+const $redisForReactions: Provider = {
+ provide: DI.redisForReactions,
+ useFactory: (config: Config) => {
+ return new Redis.Redis(config.redisForReactions);
+ },
+ inject: [DI.config],
+};
+
@Global()
@Module({
imports: [RepositoryModule],
- providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines],
- exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, RepositoryModule],
+ providers: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions],
+ exports: [$config, $db, $meilisearch, $redis, $redisForPub, $redisForSub, $redisForTimelines, $redisForReactions, RepositoryModule],
})
export class GlobalModule implements OnApplicationShutdown {
constructor(
@@ -91,6 +99,7 @@ export class GlobalModule implements OnApplicationShutdown {
@Inject(DI.redisForPub) private redisForPub: Redis.Redis,
@Inject(DI.redisForSub) private redisForSub: Redis.Redis,
@Inject(DI.redisForTimelines) private redisForTimelines: Redis.Redis,
+ @Inject(DI.redisForReactions) private redisForReactions: Redis.Redis,
) { }
public async dispose(): Promise {
@@ -103,6 +112,7 @@ export class GlobalModule implements OnApplicationShutdown {
this.redisForPub.disconnect(),
this.redisForSub.disconnect(),
this.redisForTimelines.disconnect(),
+ this.redisForReactions.disconnect(),
]);
}
diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts
index cbd6d1c086dc..97ba79c5743b 100644
--- a/packages/backend/src/config.ts
+++ b/packages/backend/src/config.ts
@@ -49,6 +49,7 @@ type Source = {
redisForPubsub?: RedisOptionsSource;
redisForJobQueue?: RedisOptionsSource;
redisForTimelines?: RedisOptionsSource;
+ redisForReactions?: RedisOptionsSource;
meilisearch?: {
host: string;
port: string;
@@ -171,6 +172,7 @@ export type Config = {
redisForPubsub: RedisOptions & RedisOptionsSource;
redisForJobQueue: RedisOptions & RedisOptionsSource;
redisForTimelines: RedisOptions & RedisOptionsSource;
+ redisForReactions: RedisOptions & RedisOptionsSource;
sentryForBackend: { options: Partial; enableNodeProfiling: boolean; } | undefined;
sentryForFrontend: { options: Partial } | undefined;
perChannelMaxNoteCacheCount: number;
@@ -251,6 +253,7 @@ export function loadConfig(): Config {
redisForPubsub: config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, host) : redis,
redisForJobQueue: config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, host) : redis,
redisForTimelines: config.redisForTimelines ? convertRedisOptions(config.redisForTimelines, host) : redis,
+ redisForReactions: config.redisForReactions ? convertRedisOptions(config.redisForReactions, host) : redis,
sentryForBackend: config.sentryForBackend,
sentryForFrontend: config.sentryForFrontend,
id: config.id,
diff --git a/packages/backend/src/const.ts b/packages/backend/src/const.ts
index a238f4973a95..e3a61861f425 100644
--- a/packages/backend/src/const.ts
+++ b/packages/backend/src/const.ts
@@ -8,6 +8,8 @@ export const MAX_NOTE_TEXT_LENGTH = 3000;
export const USER_ONLINE_THRESHOLD = 1000 * 60 * 10; // 10min
export const USER_ACTIVE_THRESHOLD = 1000 * 60 * 60 * 24 * 3; // 3days
+export const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
+
//#region hard limits
// If you change DB_* values, you must also change the DB schema.
diff --git a/packages/backend/src/core/CoreModule.ts b/packages/backend/src/core/CoreModule.ts
index 674241ac120b..3b3c35f97671 100644
--- a/packages/backend/src/core/CoreModule.ts
+++ b/packages/backend/src/core/CoreModule.ts
@@ -50,6 +50,7 @@ import { PollService } from './PollService.js';
import { PushNotificationService } from './PushNotificationService.js';
import { QueryService } from './QueryService.js';
import { ReactionService } from './ReactionService.js';
+import { ReactionsBufferingService } from './ReactionsBufferingService.js';
import { RelayService } from './RelayService.js';
import { RoleService } from './RoleService.js';
import { S3Service } from './S3Service.js';
@@ -193,6 +194,7 @@ const $ProxyAccountService: Provider = { provide: 'ProxyAccountService', useExis
const $PushNotificationService: Provider = { provide: 'PushNotificationService', useExisting: PushNotificationService };
const $QueryService: Provider = { provide: 'QueryService', useExisting: QueryService };
const $ReactionService: Provider = { provide: 'ReactionService', useExisting: ReactionService };
+const $ReactionsBufferingService: Provider = { provide: 'ReactionsBufferingService', useExisting: ReactionsBufferingService };
const $RelayService: Provider = { provide: 'RelayService', useExisting: RelayService };
const $RoleService: Provider = { provide: 'RoleService', useExisting: RoleService };
const $S3Service: Provider = { provide: 'S3Service', useExisting: S3Service };
@@ -342,6 +344,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PushNotificationService,
QueryService,
ReactionService,
+ ReactionsBufferingService,
RelayService,
RoleService,
S3Service,
@@ -487,6 +490,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PushNotificationService,
$QueryService,
$ReactionService,
+ $ReactionsBufferingService,
$RelayService,
$RoleService,
$S3Service,
@@ -633,6 +637,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
PushNotificationService,
QueryService,
ReactionService,
+ ReactionsBufferingService,
RelayService,
RoleService,
S3Service,
@@ -777,6 +782,7 @@ const $ApQuestionService: Provider = { provide: 'ApQuestionService', useExisting
$PushNotificationService,
$QueryService,
$ReactionService,
+ $ReactionsBufferingService,
$RelayService,
$RoleService,
$S3Service,
diff --git a/packages/backend/src/core/QueueService.ts b/packages/backend/src/core/QueueService.ts
index ddb90a051fc7..f35e456556df 100644
--- a/packages/backend/src/core/QueueService.ts
+++ b/packages/backend/src/core/QueueService.ts
@@ -87,6 +87,12 @@ export class QueueService {
repeat: { pattern: '*/5 * * * *' },
removeOnComplete: true,
});
+
+ this.systemQueue.add('bakeBufferedReactions', {
+ }, {
+ repeat: { pattern: '0 0 * * *' },
+ removeOnComplete: true,
+ });
}
@bindThis
diff --git a/packages/backend/src/core/ReactionService.ts b/packages/backend/src/core/ReactionService.ts
index 371207c33a7d..5993c42a1f4b 100644
--- a/packages/backend/src/core/ReactionService.ts
+++ b/packages/backend/src/core/ReactionService.ts
@@ -4,7 +4,6 @@
*/
import { Inject, Injectable } from '@nestjs/common';
-import * as Redis from 'ioredis';
import { DI } from '@/di-symbols.js';
import type { EmojisRepository, NoteReactionsRepository, UsersRepository, NotesRepository } from '@/models/_.js';
import { IdentifiableError } from '@/misc/identifiable-error.js';
@@ -30,9 +29,10 @@ import { RoleService } from '@/core/RoleService.js';
import { FeaturedService } from '@/core/FeaturedService.js';
import { trackPromise } from '@/misc/promise-tracker.js';
import { isQuote, isRenote } from '@/misc/is-renote.js';
+import { ReactionsBufferingService } from '@/core/ReactionsBufferingService.js';
+import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
const FALLBACK = '\u2764';
-const PER_NOTE_REACTION_USER_PAIR_CACHE_MAX = 16;
const legacies: Record = {
'like': '👍',
@@ -71,9 +71,6 @@ const decodeCustomEmojiRegexp = /^:([\w+-]+)(?:@([\w.-]+))?:$/;
@Injectable()
export class ReactionService {
constructor(
- @Inject(DI.redis)
- private redisClient: Redis.Redis,
-
@Inject(DI.usersRepository)
private usersRepository: UsersRepository,
@@ -93,6 +90,7 @@ export class ReactionService {
private userEntityService: UserEntityService,
private noteEntityService: NoteEntityService,
private userBlockingService: UserBlockingService,
+ private reactionsBufferingService: ReactionsBufferingService,
private idService: IdService,
private featuredService: FeaturedService,
private globalEventService: GlobalEventService,
@@ -174,7 +172,6 @@ export class ReactionService {
reaction,
};
- // Create reaction
try {
await this.noteReactionsRepository.insert(record);
} catch (e) {
@@ -198,16 +195,25 @@ export class ReactionService {
}
// Increment reactions count
- const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
- await this.notesRepository.createQueryBuilder().update()
- .set({
- reactions: () => sql,
- ...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
- reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
- } : {}),
- })
- .where('id = :id', { id: note.id })
- .execute();
+ if (meta.enableReactionsBuffering) {
+ await this.reactionsBufferingService.create(note.id, user.id, reaction, note.reactionAndUserPairCache);
+
+ // for debugging
+ if (reaction === ':angry_ai:') {
+ this.reactionsBufferingService.bake();
+ }
+ } else {
+ const sql = `jsonb_set("reactions", '{${reaction}}', (COALESCE("reactions"->>'${reaction}', '0')::int + 1)::text::jsonb)`;
+ await this.notesRepository.createQueryBuilder().update()
+ .set({
+ reactions: () => sql,
+ ...(note.reactionAndUserPairCache.length < PER_NOTE_REACTION_USER_PAIR_CACHE_MAX ? {
+ reactionAndUserPairCache: () => `array_append("reactionAndUserPairCache", '${user.id}/${reaction}')`,
+ } : {}),
+ })
+ .where('id = :id', { id: note.id })
+ .execute();
+ }
// 30%の確率、セルフではない、3日以内に投稿されたノートの場合ハイライト用ランキング更新
if (
@@ -304,15 +310,21 @@ export class ReactionService {
throw new IdentifiableError('60527ec9-b4cb-4a88-a6bd-32d3ad26817d', 'not reacted');
}
+ const meta = await this.metaService.fetch();
+
// Decrement reactions count
- const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
- await this.notesRepository.createQueryBuilder().update()
- .set({
- reactions: () => sql,
- reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
- })
- .where('id = :id', { id: note.id })
- .execute();
+ if (meta.enableReactionsBuffering) {
+ await this.reactionsBufferingService.delete(note.id, user.id, exist.reaction);
+ } else {
+ const sql = `jsonb_set("reactions", '{${exist.reaction}}', (COALESCE("reactions"->>'${exist.reaction}', '0')::int - 1)::text::jsonb)`;
+ await this.notesRepository.createQueryBuilder().update()
+ .set({
+ reactions: () => sql,
+ reactionAndUserPairCache: () => `array_remove("reactionAndUserPairCache", '${user.id}/${exist.reaction}')`,
+ })
+ .where('id = :id', { id: note.id })
+ .execute();
+ }
this.globalEventService.publishNoteStream(note.id, 'unreacted', {
reaction: this.decodeReaction(exist.reaction).reaction,
diff --git a/packages/backend/src/core/ReactionsBufferingService.ts b/packages/backend/src/core/ReactionsBufferingService.ts
new file mode 100644
index 000000000000..b1a197feeb17
--- /dev/null
+++ b/packages/backend/src/core/ReactionsBufferingService.ts
@@ -0,0 +1,162 @@
+/*
+ * SPDX-FileCopyrightText: syuilo and misskey-project
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+import { Inject, Injectable } from '@nestjs/common';
+import * as Redis from 'ioredis';
+import { DI } from '@/di-symbols.js';
+import type { MiNote } from '@/models/Note.js';
+import { bindThis } from '@/decorators.js';
+import type { MiUser, NotesRepository } from '@/models/_.js';
+import type { Config } from '@/config.js';
+import { PER_NOTE_REACTION_USER_PAIR_CACHE_MAX } from '@/const.js';
+
+const REDIS_DELTA_PREFIX = 'reactionsBufferDeltas';
+const REDIS_PAIR_PREFIX = 'reactionsBufferPairs';
+
+@Injectable()
+export class ReactionsBufferingService {
+ constructor(
+ @Inject(DI.config)
+ private config: Config,
+
+ @Inject(DI.redisForReactions)
+ private redisForReactions: Redis.Redis, // TODO: 専用のRedisインスタンスにする
+
+ @Inject(DI.notesRepository)
+ private notesRepository: NotesRepository,
+ ) {
+ }
+
+ @bindThis
+ public async create(noteId: MiNote['id'], userId: MiUser['id'], reaction: string, currentPairs: string[]): Promise {
+ const pipeline = this.redisForReactions.pipeline();
+ pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, 1);
+ for (let i = 0; i < currentPairs.length; i++) {
+ pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, i, currentPairs[i]);
+ }
+ pipeline.zadd(`${REDIS_PAIR_PREFIX}:${noteId}`, Date.now(), `${userId}/${reaction}`);
+ pipeline.zremrangebyrank(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -(PER_NOTE_REACTION_USER_PAIR_CACHE_MAX + 1));
+ await pipeline.exec();
+ }
+
+ @bindThis
+ public async delete(noteId: MiNote['id'], userId: MiUser['id'], reaction: string): Promise {
+ const pipeline = this.redisForReactions.pipeline();
+ pipeline.hincrby(`${REDIS_DELTA_PREFIX}:${noteId}`, reaction, -1);
+ pipeline.zrem(`${REDIS_PAIR_PREFIX}:${noteId}`, `${userId}/${reaction}`);
+ // TODO: 「消した要素一覧」も持っておかないとcreateされた時に上書きされて復活する
+ await pipeline.exec();
+ }
+
+ @bindThis
+ public async get(noteId: MiNote['id']): Promise<{
+ deltas: Record;
+ pairs: ([MiUser['id'], string])[];
+ }> {
+ const pipeline = this.redisForReactions.pipeline();
+ pipeline.hgetall(`${REDIS_DELTA_PREFIX}:${noteId}`);
+ pipeline.zrange(`${REDIS_PAIR_PREFIX}:${noteId}`, 0, -1);
+ const results = await pipeline.exec();
+
+ const resultDeltas = results![0][1] as Record;
+ const resultPairs = results![1][1] as string[];
+
+ const deltas = {} as Record;
+ for (const [name, count] of Object.entries(resultDeltas)) {
+ deltas[name] = parseInt(count);
+ }
+
+ const pairs = resultPairs.map(x => x.split('/') as [MiUser['id'], string]);
+
+ return {
+ deltas,
+ pairs,
+ };
+ }
+
+ @bindThis
+ public async getMany(noteIds: MiNote['id'][]): Promise
+
+
+
+ Misskey® Fan-out Timeline Technology™ (FTT)
+ Enabled
+ Disabled
+
+
+
+ {{ i18n.ts.enable }}
+ {{ i18n.ts._serverSettings.fanoutTimelineDescription }}
+
+
+
+ {{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}
+ {{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}
+
+
+
+ perLocalUserUserTimelineCacheMax
+
+
+
+ perRemoteUserUserTimelineCacheMax
+
+
+
+ perUserHomeTimelineCacheMax
+
+
+
+ perUserListTimelineCacheMax
+
+
+
+
+
+
+ Misskey® Reactions Buffering Technology™ (RBT){{ i18n.ts.beta }}
+ Enabled
+ Disabled
+
+
+
+ {{ i18n.ts.enable }}
+ {{ i18n.ts._serverSettings.reactionsBufferingDescription }}
+
+
+
@@ -52,11 +101,20 @@ import { fetchInstance } from '@/instance.js';
import { i18n } from '@/i18n.js';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import MkSwitch from '@/components/MkSwitch.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import MkInput from '@/components/MkInput.vue';
const enableServerMachineStats = ref(false);
const enableIdenticonGeneration = ref(false);
const enableChartsForRemoteUser = ref(false);
const enableChartsForFederatedInstances = ref(false);
+const enableFanoutTimeline = ref(false);
+const enableFanoutTimelineDbFallback = ref(false);
+const perLocalUserUserTimelineCacheMax = ref(0);
+const perRemoteUserUserTimelineCacheMax = ref(0);
+const perUserHomeTimelineCacheMax = ref(0);
+const perUserListTimelineCacheMax = ref(0);
+const enableReactionsBuffering = ref(false);
async function init() {
const meta = await misskeyApi('admin/meta');
@@ -64,6 +122,13 @@ async function init() {
enableIdenticonGeneration.value = meta.enableIdenticonGeneration;
enableChartsForRemoteUser.value = meta.enableChartsForRemoteUser;
enableChartsForFederatedInstances.value = meta.enableChartsForFederatedInstances;
+ enableFanoutTimeline.value = meta.enableFanoutTimeline;
+ enableFanoutTimelineDbFallback.value = meta.enableFanoutTimelineDbFallback;
+ perLocalUserUserTimelineCacheMax.value = meta.perLocalUserUserTimelineCacheMax;
+ perRemoteUserUserTimelineCacheMax.value = meta.perRemoteUserUserTimelineCacheMax;
+ perUserHomeTimelineCacheMax.value = meta.perUserHomeTimelineCacheMax;
+ perUserListTimelineCacheMax.value = meta.perUserListTimelineCacheMax;
+ enableReactionsBuffering.value = meta.enableReactionsBuffering;
}
function save() {
@@ -72,6 +137,13 @@ function save() {
enableIdenticonGeneration: enableIdenticonGeneration.value,
enableChartsForRemoteUser: enableChartsForRemoteUser.value,
enableChartsForFederatedInstances: enableChartsForFederatedInstances.value,
+ enableFanoutTimeline: enableFanoutTimeline.value,
+ enableFanoutTimelineDbFallback: enableFanoutTimelineDbFallback.value,
+ perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value,
+ perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value,
+ perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value,
+ perUserListTimelineCacheMax: perUserListTimelineCacheMax.value,
+ enableReactionsBuffering: enableReactionsBuffering.value,
}).then(() => {
fetchInstance(true);
});
diff --git a/packages/frontend/src/pages/admin/settings.vue b/packages/frontend/src/pages/admin/settings.vue
index 6f45c212ece1..ffff57b45453 100644
--- a/packages/frontend/src/pages/admin/settings.vue
+++ b/packages/frontend/src/pages/admin/settings.vue
@@ -96,38 +96,6 @@ SPDX-License-Identifier: AGPL-3.0-only
-
- Misskey® Fan-out Timeline Technology™ (FTT)
-
-
-
- {{ i18n.ts.enable }}
- {{ i18n.ts._serverSettings.fanoutTimelineDescription }}
-
-
-
- {{ i18n.ts._serverSettings.fanoutTimelineDbFallback }}
- {{ i18n.ts._serverSettings.fanoutTimelineDbFallbackDescription }}
-
-
-
- perLocalUserUserTimelineCacheMax
-
-
-
- perRemoteUserUserTimelineCacheMax
-
-
-
- perUserHomeTimelineCacheMax
-
-
-
- perUserListTimelineCacheMax
-
-
-
-
{{ i18n.ts._ad.adsSettings }}
@@ -236,12 +204,6 @@ const cacheRemoteSensitiveFiles = ref(false);
const enableServiceWorker = ref(false);
const swPublicKey = ref(null);
const swPrivateKey = ref(null);
-const enableFanoutTimeline = ref(false);
-const enableFanoutTimelineDbFallback = ref(false);
-const perLocalUserUserTimelineCacheMax = ref(0);
-const perRemoteUserUserTimelineCacheMax = ref(0);
-const perUserHomeTimelineCacheMax = ref(0);
-const perUserListTimelineCacheMax = ref(0);
const notesPerOneAd = ref(0);
const urlPreviewEnabled = ref(true);
const urlPreviewTimeout = ref(10000);
@@ -265,12 +227,6 @@ async function init(): Promise {
enableServiceWorker.value = meta.enableServiceWorker;
swPublicKey.value = meta.swPublickey;
swPrivateKey.value = meta.swPrivateKey;
- enableFanoutTimeline.value = meta.enableFanoutTimeline;
- enableFanoutTimelineDbFallback.value = meta.enableFanoutTimelineDbFallback;
- perLocalUserUserTimelineCacheMax.value = meta.perLocalUserUserTimelineCacheMax;
- perRemoteUserUserTimelineCacheMax.value = meta.perRemoteUserUserTimelineCacheMax;
- perUserHomeTimelineCacheMax.value = meta.perUserHomeTimelineCacheMax;
- perUserListTimelineCacheMax.value = meta.perUserListTimelineCacheMax;
notesPerOneAd.value = meta.notesPerOneAd;
urlPreviewEnabled.value = meta.urlPreviewEnabled;
urlPreviewTimeout.value = meta.urlPreviewTimeout;
@@ -295,12 +251,6 @@ async function save() {
enableServiceWorker: enableServiceWorker.value,
swPublicKey: swPublicKey.value,
swPrivateKey: swPrivateKey.value,
- enableFanoutTimeline: enableFanoutTimeline.value,
- enableFanoutTimelineDbFallback: enableFanoutTimelineDbFallback.value,
- perLocalUserUserTimelineCacheMax: perLocalUserUserTimelineCacheMax.value,
- perRemoteUserUserTimelineCacheMax: perRemoteUserUserTimelineCacheMax.value,
- perUserHomeTimelineCacheMax: perUserHomeTimelineCacheMax.value,
- perUserListTimelineCacheMax: perUserListTimelineCacheMax.value,
notesPerOneAd: notesPerOneAd.value,
urlPreviewEnabled: urlPreviewEnabled.value,
urlPreviewTimeout: urlPreviewTimeout.value,
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 03828b6552a9..672d75e2671c 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -5125,6 +5125,7 @@ export type operations = {
perRemoteUserUserTimelineCacheMax: number;
perUserHomeTimelineCacheMax: number;
perUserListTimelineCacheMax: number;
+ enableReactionsBuffering: boolean;
notesPerOneAd: number;
backgroundImageUrl: string | null;
deeplAuthKey: string | null;
@@ -9395,6 +9396,7 @@ export type operations = {
perRemoteUserUserTimelineCacheMax?: number;
perUserHomeTimelineCacheMax?: number;
perUserListTimelineCacheMax?: number;
+ enableReactionsBuffering?: boolean;
notesPerOneAd?: number;
silencedHosts?: string[] | null;
mediaSilencedHosts?: string[] | null;
From f0834ca14c75df429f7d8524f24bc4749639032a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 20 Sep 2024 21:04:58 +0900
Subject: [PATCH 050/222] =?UTF-8?q?enhance:=20=E3=83=A6=E3=83=BC=E3=82=B6?=
=?UTF-8?q?=E3=83=BC=E3=82=B3=E3=83=B3=E3=83=86=E3=83=B3=E3=83=84=E3=81=AE?=
=?UTF-8?q?=E3=82=A4=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=88=E6=93=8D=E4=BD=9C?=
=?UTF-8?q?=E3=81=AE=E5=AE=9F=E8=A1=8C=E5=8F=AF=E5=90=A6=E3=82=92=E3=83=AD?=
=?UTF-8?q?=E3=83=BC=E3=83=AB=E3=81=A7=E5=88=B6=E5=BE=A1=E3=81=A7=E3=81=8D?=
=?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB=20(#14583)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* enhance: インポート操作の実行可否をロールで制御できるように
* Update Changelog
---
CHANGELOG.md | 1 +
locales/index.d.ts | 20 ++++
locales/ja-JP.yml | 5 +
packages/backend/src/core/RoleService.ts | 15 +++
.../backend/src/models/json-schema/role.ts | 20 ++++
.../server/api/endpoints/i/import-antennas.ts | 1 +
.../server/api/endpoints/i/import-blocking.ts | 1 +
.../api/endpoints/i/import-following.ts | 1 +
.../server/api/endpoints/i/import-muting.ts | 1 +
.../api/endpoints/i/import-user-lists.ts | 1 +
packages/frontend-shared/js/const.ts | 5 +
.../frontend/src/pages/admin/roles.editor.vue | 100 ++++++++++++++++++
packages/frontend/src/pages/admin/roles.vue | 40 +++++++
.../src/pages/settings/import-export.vue | 10 +-
packages/misskey-js/src/autogen/types.ts | 5 +
15 files changed, 221 insertions(+), 5 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a2d2e62a62a0..cc8f9c50812d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,7 @@
### General
- Feat: UserWebhookとSystemWebhookのテスト送信機能を追加 (#14445)
+- Enhance: ユーザーによるコンテンツインポートの可否をロールポリシーで制御できるように
### Client
- Feat: ノート単体・ユーザーのノート・クリップのノートの埋め込み機能
diff --git a/locales/index.d.ts b/locales/index.d.ts
index 798cb89f8336..f23426219580 100644
--- a/locales/index.d.ts
+++ b/locales/index.d.ts
@@ -6766,6 +6766,26 @@ export interface Locale extends ILocale {
* アイコンデコレーションの最大取付個数
*/
"avatarDecorationLimit": string;
+ /**
+ * アンテナのインポートを許可
+ */
+ "canImportAntennas": string;
+ /**
+ * ブロックのインポートを許可
+ */
+ "canImportBlocking": string;
+ /**
+ * フォローのインポートを許可
+ */
+ "canImportFollowing": string;
+ /**
+ * ミュートのインポートを許可
+ */
+ "canImportMuting": string;
+ /**
+ * リストのインポートを許可
+ */
+ "canImportUserLists": string;
};
"_condition": {
/**
diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 726e4f4ef450..8e48508e788a 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -1748,6 +1748,11 @@ _role:
canSearchNotes: "ノート検索の利用"
canUseTranslator: "翻訳機能の利用"
avatarDecorationLimit: "アイコンデコレーションの最大取付個数"
+ canImportAntennas: "アンテナのインポートを許可"
+ canImportBlocking: "ブロックのインポートを許可"
+ canImportFollowing: "フォローのインポートを許可"
+ canImportMuting: "ミュートのインポートを許可"
+ canImportUserLists: "リストのインポートを許可"
_condition:
roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー"
diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts
index 0210012a0300..24752edcf6b5 100644
--- a/packages/backend/src/core/RoleService.ts
+++ b/packages/backend/src/core/RoleService.ts
@@ -58,6 +58,11 @@ export type RolePolicies = {
userEachUserListsLimit: number;
rateLimitFactor: number;
avatarDecorationLimit: number;
+ canImportAntennas: boolean;
+ canImportBlocking: boolean;
+ canImportFollowing: boolean;
+ canImportMuting: boolean;
+ canImportUserLists: boolean;
};
export const DEFAULT_POLICIES: RolePolicies = {
@@ -87,6 +92,11 @@ export const DEFAULT_POLICIES: RolePolicies = {
userEachUserListsLimit: 50,
rateLimitFactor: 1,
avatarDecorationLimit: 1,
+ canImportAntennas: true,
+ canImportBlocking: true,
+ canImportFollowing: true,
+ canImportMuting: true,
+ canImportUserLists: true,
};
@Injectable()
@@ -387,6 +397,11 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
userEachUserListsLimit: calc('userEachUserListsLimit', vs => Math.max(...vs)),
rateLimitFactor: calc('rateLimitFactor', vs => Math.max(...vs)),
avatarDecorationLimit: calc('avatarDecorationLimit', vs => Math.max(...vs)),
+ canImportAntennas: calc('canImportAntennas', vs => vs.some(v => v === true)),
+ canImportBlocking: calc('canImportBlocking', vs => vs.some(v => v === true)),
+ canImportFollowing: calc('canImportFollowing', vs => vs.some(v => v === true)),
+ canImportMuting: calc('canImportMuting', vs => vs.some(v => v === true)),
+ canImportUserLists: calc('canImportUserLists', vs => vs.some(v => v === true)),
};
}
diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts
index 7366f053560d..3537de94c891 100644
--- a/packages/backend/src/models/json-schema/role.ts
+++ b/packages/backend/src/models/json-schema/role.ts
@@ -272,6 +272,26 @@ export const packedRolePoliciesSchema = {
type: 'integer',
optional: false, nullable: false,
},
+ canImportAntennas: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportBlocking: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportFollowing: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportMuting: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
+ canImportUserLists: {
+ type: 'boolean',
+ optional: false, nullable: false,
+ },
},
} as const;
diff --git a/packages/backend/src/server/api/endpoints/i/import-antennas.ts b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
index bc46163e3d27..bdf6c065e89c 100644
--- a/packages/backend/src/server/api/endpoints/i/import-antennas.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-antennas.ts
@@ -16,6 +16,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportAntennas',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-blocking.ts b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
index 260610853930..d7bb6bcd2256 100644
--- a/packages/backend/src/server/api/endpoints/i/import-blocking.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-blocking.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportBlocking',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-following.ts b/packages/backend/src/server/api/endpoints/i/import-following.ts
index d5e824df2740..e03192d8c67b 100644
--- a/packages/backend/src/server/api/endpoints/i/import-following.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-following.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportFollowing',
prohibitMoved: true,
limit: {
duration: ms('1hour'),
diff --git a/packages/backend/src/server/api/endpoints/i/import-muting.ts b/packages/backend/src/server/api/endpoints/i/import-muting.ts
index 0f5800404eaf..76b285bb7e3f 100644
--- a/packages/backend/src/server/api/endpoints/i/import-muting.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-muting.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportMuting',
prohibitMoved: true,
limit: {
diff --git a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
index bacdd5c88f28..76ecfd082ca3 100644
--- a/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
+++ b/packages/backend/src/server/api/endpoints/i/import-user-lists.ts
@@ -15,6 +15,7 @@ import { ApiError } from '../../error.js';
export const meta = {
secure: true,
requireCredential: true,
+ requireRolePolicy: 'canImportUserLists',
prohibitMoved: true,
limit: {
duration: ms('1hour'),
diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts
index 8391fb638c5e..b62a69ba2441 100644
--- a/packages/frontend-shared/js/const.ts
+++ b/packages/frontend-shared/js/const.ts
@@ -98,6 +98,11 @@ export const ROLE_POLICIES = [
'userEachUserListsLimit',
'rateLimitFactor',
'avatarDecorationLimit',
+ 'canImportAntennas',
+ 'canImportBlocking',
+ 'canImportFollowing',
+ 'canImportMuting',
+ 'canImportUserLists',
] as const;
// なんか動かない
diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue
index b0137abb3f86..ae01432d0c74 100644
--- a/packages/frontend/src/pages/admin/roles.editor.vue
+++ b/packages/frontend/src/pages/admin/roles.editor.vue
@@ -590,6 +590,106 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+
+ {{ i18n.ts._role._options.canImportAntennas }}
+
+ {{ i18n.ts._role.useBaseValue }}
+ {{ role.policies.canImportAntennas.value ? i18n.ts.yes : i18n.ts.no }}
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+ {{ i18n.ts.enable }}
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canImportBlocking }}
+
+ {{ i18n.ts._role.useBaseValue }}
+ {{ role.policies.canImportBlocking.value ? i18n.ts.yes : i18n.ts.no }}
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+ {{ i18n.ts.enable }}
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canImportFollowing }}
+
+ {{ i18n.ts._role.useBaseValue }}
+ {{ role.policies.canImportFollowing.value ? i18n.ts.yes : i18n.ts.no }}
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+ {{ i18n.ts.enable }}
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canImportMuting }}
+
+ {{ i18n.ts._role.useBaseValue }}
+ {{ role.policies.canImportMuting.value ? i18n.ts.yes : i18n.ts.no }}
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+ {{ i18n.ts.enable }}
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
+
+
+ {{ i18n.ts._role._options.canImportUserLists }}
+
+ {{ i18n.ts._role.useBaseValue }}
+ {{ role.policies.canImportUserLists.value ? i18n.ts.yes : i18n.ts.no }}
+
+
+
+
+ {{ i18n.ts._role.useBaseValue }}
+
+
+ {{ i18n.ts.enable }}
+
+
+ {{ i18n.ts._role.priority }}
+
+
+
diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue
index 7e29f6e0d8bb..511e3c0fdf51 100644
--- a/packages/frontend/src/pages/admin/roles.vue
+++ b/packages/frontend/src/pages/admin/roles.vue
@@ -214,6 +214,46 @@ SPDX-License-Identifier: AGPL-3.0-only
+
+ {{ i18n.ts._role._options.canImportAntennas }}
+ {{ policies.canImportAntennas ? i18n.ts.yes : i18n.ts.no }}
+
+ {{ i18n.ts.enable }}
+
+
+
+
+ {{ i18n.ts._role._options.canImportBlocking }}
+ {{ policies.canImportBlocking ? i18n.ts.yes : i18n.ts.no }}
+
+ {{ i18n.ts.enable }}
+
+
+
+
+ {{ i18n.ts._role._options.canImportFollowing }}
+ {{ policies.canImportFollowing ? i18n.ts.yes : i18n.ts.no }}
+
+ {{ i18n.ts.enable }}
+
+
+
+
+ {{ i18n.ts._role._options.canImportMuting }}
+ {{ policies.canImportMuting ? i18n.ts.yes : i18n.ts.no }}
+
+ {{ i18n.ts.enable }}
+
+
+
+
+ {{ i18n.ts._role._options.canImportUserLists }}
+ {{ policies.canImportUserLists ? i18n.ts.yes : i18n.ts.no }}
+
+ {{ i18n.ts.enable }}
+
+
+
{{ i18n.ts.save }}
diff --git a/packages/frontend/src/pages/settings/import-export.vue b/packages/frontend/src/pages/settings/import-export.vue
index 9bb3957a84f9..5acbc5075615 100644
--- a/packages/frontend/src/pages/settings/import-export.vue
+++ b/packages/frontend/src/pages/settings/import-export.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.export }}
-
+
{{ i18n.ts.import }}
@@ -63,7 +63,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.export }}
-
+
{{ i18n.ts.import }}
{{ i18n.ts.import }}
@@ -78,7 +78,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.export }}
-
+
{{ i18n.ts.import }}
{{ i18n.ts.import }}
@@ -93,7 +93,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.export }}
-
+
{{ i18n.ts.import }}
{{ i18n.ts.import }}
@@ -108,7 +108,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ i18n.ts.export }}
-
+
{{ i18n.ts.import }}
{{ i18n.ts.import }}
diff --git a/packages/misskey-js/src/autogen/types.ts b/packages/misskey-js/src/autogen/types.ts
index 672d75e2671c..5d5bc52956f3 100644
--- a/packages/misskey-js/src/autogen/types.ts
+++ b/packages/misskey-js/src/autogen/types.ts
@@ -4822,6 +4822,11 @@ export type components = {
userEachUserListsLimit: number;
rateLimitFactor: number;
avatarDecorationLimit: number;
+ canImportAntennas: boolean;
+ canImportBlocking: boolean;
+ canImportFollowing: boolean;
+ canImportMuting: boolean;
+ canImportUserLists: boolean;
};
ReversiGameLite: {
/** Format: id */
From 7e9d54fa3a0f32e3ed9b98e352b74ebb720b5ab8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
<67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Fri, 20 Sep 2024 21:05:20 +0900
Subject: [PATCH 051/222] =?UTF-8?q?fix(frontend):=20=E3=83=95=E3=82=A1?=
=?UTF-8?q?=E3=82=A4=E3=83=AB=E3=81=AE=E8=A9=B3=E7=B4=B0=E3=83=9A=E3=83=BC?=
=?UTF-8?q?=E3=82=B8=E3=81=AE=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=81=AE?=
=?UTF-8?q?=E8=AA=AC=E6=98=8E=E3=81=A7=E6=94=B9=E8=A1=8C=E3=81=8C=E6=AD=A3?=
=?UTF-8?q?=E3=81=97=E3=81=8F=E8=A1=A8=E7=A4=BA=E3=81=95=E3=82=8C=E3=81=AA?=
=?UTF-8?q?=E3=81=84=E5=95=8F=E9=A1=8C=E3=82=92=E4=BF=AE=E6=AD=A3=20(#1458?=
=?UTF-8?q?8)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
* upd: don't ignore new lines on file info
* Update Changelog
* :v:
---------
Co-authored-by: Marie
---
CHANGELOG.md | 2 ++
packages/frontend/src/pages/drive.file.info.vue | 6 +++++-
2 files changed, 7 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index cc8f9c50812d..76c4e851df85 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -16,6 +16,8 @@
- Fix: 縦横比が極端なカスタム絵文字を表示する際にレイアウトが崩れる箇所があるのを修正
(Cherry-picked from https://github.com/MisskeyIO/misskey/pull/725)
- Fix: 設定変更時のリロード確認ダイアログが複数個表示されることがある問題を修正
+- Fix: ファイルの詳細ページのファイルの説明で改行が正しく表示されない問題を修正
+ (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/commit/bde6bb0bd2e8b0d027e724d2acdb8ae0585a8110)
### Server
- Feat: Misskey® Reactions Buffering Technology™ (RBT)により、リアクションの作成負荷を低減することが可能に
diff --git a/packages/frontend/src/pages/drive.file.info.vue b/packages/frontend/src/pages/drive.file.info.vue
index ffedaf27bfec..12ebbbe3ff46 100644
--- a/packages/frontend/src/pages/drive.file.info.vue
+++ b/packages/frontend/src/pages/drive.file.info.vue
@@ -45,7 +45,7 @@ SPDX-License-Identifier: AGPL-3.0-only