Merge branch 'develop' into pag-back
This commit is contained in:
commit
20ae59756f
49 changed files with 2561 additions and 1549 deletions
|
@ -82,6 +82,8 @@ redis:
|
|||
#pass: example-pass
|
||||
#prefix: example-prefix
|
||||
#db: 1
|
||||
# You can specify more ioredis options...
|
||||
#username: example-username
|
||||
|
||||
#redisForPubsub:
|
||||
# host: localhost
|
||||
|
@ -90,6 +92,8 @@ redis:
|
|||
# #pass: example-pass
|
||||
# #prefix: example-prefix
|
||||
# #db: 1
|
||||
# # You can specify more ioredis options...
|
||||
# #username: example-username
|
||||
|
||||
#redisForJobQueue:
|
||||
# host: localhost
|
||||
|
@ -98,6 +102,8 @@ redis:
|
|||
# #pass: example-pass
|
||||
# #prefix: example-prefix
|
||||
# #db: 1
|
||||
# # You can specify more ioredis options...
|
||||
# #username: example-username
|
||||
|
||||
# ┌───────────────────────────┐
|
||||
#───┘ MeiliSearch configuration └─────────────────────────────
|
||||
|
|
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -15,17 +15,19 @@
|
|||
## 13.x.x (unreleased)
|
||||
|
||||
### General
|
||||
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
|
||||
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
|
||||
- 招待機能を改善しました
|
||||
* 過去に発行した招待コードを確認できるようになりました
|
||||
* ロールごとに招待コードの発行数制限と制限対象期間、有効期限を設定できるようになりました
|
||||
* 招待コードを作成したユーザーと使用したユーザーを確認できるようになりました
|
||||
- ユーザーにロールが期限付きでアサインされている場合、その期限をユーザーのモデレーションページで確認できるようになりました
|
||||
- identicon生成を無効にしてパフォーマンスを向上させることができるようになりました
|
||||
- サーバーのマシン情報の公開を無効にしてパフォーマンスを向上させることができるようになりました
|
||||
|
||||
### Client
|
||||
- deck UIのカラムのメニューからアンテナとリストの編集画面を開けるように
|
||||
- ドライブファイルのメニューで画像をクロップできるように
|
||||
- 画像を動画と同様に簡単に隠せるように
|
||||
- Enhance: ノートの埋め込みが複数画像と動画を表示されるように
|
||||
- オリジナル画像を保持せずにアップロードする場合webpでアップロードされるように(Safari以外)
|
||||
- 見たことのあるRenoteを省略して表示をオンのときに自分のnoteのrenoteを省略するように
|
||||
- フォルダーやファイルに対しても開発者モード使用時、IDをコピーできるように
|
||||
|
@ -41,7 +43,9 @@
|
|||
- ロール設定画面でロールIDを確認できるように
|
||||
- コンテキストメニュー表示時のパフォーマンスを改善
|
||||
- フォロー/フォロワー非公開時の表示を改善
|
||||
- AiScriptを0.14.0に更新
|
||||
- 本文にMFMが含まれている場合に自動でたたまれる機能が、返信先や引用RNにも適用されるように
|
||||
- position は対象外になりました
|
||||
- AiScriptを0.15.0に更新
|
||||
- Fix: サーバーメトリクスが90度傾いている
|
||||
- Fix: 非ログイン時にクレデンシャルが必要なページに行くとエラーが出る問題を修正
|
||||
- Fix: sparkle内にリンクを入れるとクリック不能になる問題の修正
|
||||
|
@ -58,15 +62,19 @@
|
|||
- nsfwjs のモデルロードを排他することで、重複ロードによってメモリ使用量が増加しないように
|
||||
- 連合の配送ジョブのパフォーマンスを向上(ロック機構の見直し、Redisキャッシュの活用)
|
||||
- featuredノートのsignedGet回数を減らしました
|
||||
- リモートサーバーからのNSFW映像のキャッシュだけを無効化できるオプションを追加
|
||||
- ActivityPubの署名用鍵長を2048bitに変更しパフォーマンスを向上(新規アカウントのみ)
|
||||
- リモートサーバーのセンシティブなファイルのキャッシュだけを無効化できるオプションを追加
|
||||
- MeilisearchにIndexするノートの範囲を設定できるように
|
||||
- Export notes with file detail
|
||||
- Add unix socket support
|
||||
- 設定ファイルでioredisの全てのオプションを指定可能に
|
||||
- Fix: エクスポートしたカスタム絵文字のzipが大きいと読み込めない問題を修正
|
||||
- Fix: リモートサーバーに無意味なActivityPubの配信を行うことがあるのを修正
|
||||
- Fix: Remove Meilisearch index when notes are deleted
|
||||
- Fix: 非英語環境でのPostgreSQLのエラーハンドリングを修正
|
||||
- Fix: インスタンスのアイコンがbase64の場合の挙動を修正
|
||||
- Fix: ローカルの `Person` を指す `acct` URI を解析するときのバグを修正しました
|
||||
- Fix: 無効化されたアンテナが再度有効化されないことがある問題を修正
|
||||
|
||||
## 13.13.2
|
||||
|
||||
|
|
|
@ -214,30 +214,13 @@ Misskey uses [Storybook](https://storybook.js.org/) for UI development.
|
|||
|
||||
### Setup & Run
|
||||
|
||||
#### Universal
|
||||
|
||||
##### Setup
|
||||
|
||||
```bash
|
||||
pnpm --filter misskey-js build
|
||||
pnpm --filter frontend tsc -p .storybook && (node packages/frontend/.storybook/preload-locale.js & node packages/frontend/.storybook/preload-theme.js)
|
||||
```
|
||||
|
||||
##### Run
|
||||
|
||||
```bash
|
||||
node packages/frontend/.storybook/generate.js && pnpm --filter frontend storybook dev
|
||||
```
|
||||
|
||||
#### macOS & Linux
|
||||
|
||||
##### Setup
|
||||
#### Setup
|
||||
|
||||
```bash
|
||||
pnpm --filter misskey-js build
|
||||
```
|
||||
|
||||
##### Run
|
||||
#### Run
|
||||
|
||||
```bash
|
||||
pnpm --filter frontend storybook-dev
|
||||
|
|
|
@ -54,6 +54,7 @@ describe('After setup instance', () => {
|
|||
cy.get('[data-cy-signup]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
cy.get('[data-cy-modal-dialog-ok]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
||||
cy.get('[data-cy-signup-rules-continue]').click();
|
||||
|
||||
|
@ -78,6 +79,7 @@ describe('After setup instance', () => {
|
|||
cy.get('[data-cy-signup]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
cy.get('[data-cy-modal-dialog-ok]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
||||
cy.get('[data-cy-signup-rules-continue]').click();
|
||||
|
||||
|
|
|
@ -1091,6 +1091,9 @@ usedAt: "Benutzt am"
|
|||
unused: "Unbenutzt"
|
||||
used: "Benutzt"
|
||||
expired: "Abgelaufen"
|
||||
doYouAgree: "Zustimmen?"
|
||||
beSureToReadThisAsItIsImportant: "Lies bitte diese wichtige Informationen."
|
||||
iHaveReadXCarefullyAndAgree: "Ich habe den Text \"{x}\" gelesen und stimme zu."
|
||||
_initialAccountSetting:
|
||||
accountCreated: "Dein Konto wurde erfolgreich erstellt!"
|
||||
letsStartAccountSetup: "Lass uns nun dein Konto einrichten."
|
||||
|
|
|
@ -1091,6 +1091,9 @@ usedAt: "Used at"
|
|||
unused: "Unused"
|
||||
used: "Used"
|
||||
expired: "Expired"
|
||||
doYouAgree: "Agree?"
|
||||
beSureToReadThisAsItIsImportant: "Please read this important information."
|
||||
iHaveReadXCarefullyAndAgree: "I have read the text \"{x}\" and agree."
|
||||
_initialAccountSetting:
|
||||
accountCreated: "Your account was successfully created!"
|
||||
letsStartAccountSetup: "For starters, let's set up your profile."
|
||||
|
|
3
locales/index.d.ts
vendored
3
locales/index.d.ts
vendored
|
@ -1094,6 +1094,9 @@ export interface Locale {
|
|||
"unused": string;
|
||||
"used": string;
|
||||
"expired": string;
|
||||
"doYouAgree": string;
|
||||
"beSureToReadThisAsItIsImportant": string;
|
||||
"iHaveReadXCarefullyAndAgree": string;
|
||||
"_initialAccountSetting": {
|
||||
"accountCreated": string;
|
||||
"letsStartAccountSetup": string;
|
||||
|
|
|
@ -1042,7 +1042,7 @@ vertical: "縦"
|
|||
horizontal: "横"
|
||||
position: "位置"
|
||||
serverRules: "サーバールール"
|
||||
pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。"
|
||||
pleaseConfirmBelowBeforeSignup: "このサーバーに登録するには、以下の内容を確認し同意する必要があります。"
|
||||
pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。"
|
||||
continue: "続ける"
|
||||
preservedUsernames: "予約ユーザー名"
|
||||
|
@ -1091,6 +1091,9 @@ usedAt: "使用日時"
|
|||
unused: "未使用"
|
||||
used: "使用済み"
|
||||
expired: "期限切れ"
|
||||
doYouAgree: "同意しますか?"
|
||||
beSureToReadThisAsItIsImportant: "重要ですので必ずお読みください。"
|
||||
iHaveReadXCarefullyAndAgree: "「{x}」の内容をよく読み、同意します。"
|
||||
|
||||
_initialAccountSetting:
|
||||
accountCreated: "アカウントの作成が完了しました!"
|
||||
|
|
|
@ -1067,6 +1067,9 @@ branding: "あ"
|
|||
enableServerMachineStats: "サーバーのマシン情報見せびらかすで"
|
||||
enableIdenticonGeneration: "ユーザーごとのIdenticon生成を有効にする"
|
||||
turnOffToImprovePerformance: "オフにしたらえらい軽うなるで。"
|
||||
inviteCodeCreated: "招待コード作ったで"
|
||||
inviteLimitExceeded: "招待コード作りすぎやで。"
|
||||
createLimitRemaining: "作成できる招待コード: 残り {limit} 個やで"
|
||||
unused: "つこてへん"
|
||||
used: "もうつこてる"
|
||||
_initialAccountSetting:
|
||||
|
|
|
@ -40,7 +40,7 @@ favorites: "즐겨찾기"
|
|||
unfavorite: "즐겨찾기에서 제거"
|
||||
favorited: "즐겨찾기에 등록했습니다"
|
||||
alreadyFavorited: "이미 즐겨찾기에 등록되어 있습니다"
|
||||
cantFavorite: "즐겨찾기에 등록하지 못했습니다."
|
||||
cantFavorite: "즐겨찾기에 등록하지 못했습니다"
|
||||
pin: "프로필에 고정"
|
||||
unpin: "프로필에서 고정 해제"
|
||||
copyContent: "내용 복사"
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "misskey",
|
||||
"version": "13.14.0-beta.6",
|
||||
"version": "13.14.0-beta.7",
|
||||
"codename": "nasubi",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/misskey-dev/misskey.git"
|
||||
},
|
||||
"packageManager": "pnpm@8.6.0",
|
||||
"packageManager": "pnpm@8.6.9",
|
||||
"workspaces": [
|
||||
"packages/frontend",
|
||||
"packages/backend",
|
||||
|
|
|
@ -2,14 +2,7 @@ import Redis from 'ioredis';
|
|||
import { loadConfig } from './built/config.js';
|
||||
|
||||
const config = loadConfig();
|
||||
const redis = new Redis({
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
password: config.redis.pass,
|
||||
keyPrefix: `${config.redis.prefix}:`,
|
||||
db: config.redis.db ?? 0,
|
||||
});
|
||||
const redis = new Redis(config.redis);
|
||||
|
||||
redis.on('connect', () => redis.disconnect());
|
||||
redis.on('error', (e) => {
|
||||
|
|
|
@ -79,7 +79,6 @@
|
|||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
"async-mutex": "^0.4.0",
|
||||
"autwh": "0.1.0",
|
||||
"bcryptjs": "2.4.3",
|
||||
"blurhash": "2.0.5",
|
||||
"bullmq": "4.4.0",
|
||||
|
@ -93,7 +92,6 @@
|
|||
"content-disposition": "0.5.4",
|
||||
"date-fns": "2.30.0",
|
||||
"deep-email-validator": "0.1.21",
|
||||
"escape-regexp": "0.0.1",
|
||||
"fastify": "4.20.0",
|
||||
"feed": "4.2.2",
|
||||
"file-type": "18.5.0",
|
||||
|
@ -139,7 +137,6 @@
|
|||
"rename": "1.0.4",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.1",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.11.0",
|
||||
"semver": "7.5.4",
|
||||
"sharp": "0.32.3",
|
||||
|
@ -157,7 +154,6 @@
|
|||
"typeorm": "0.3.17",
|
||||
"typescript": "5.1.6",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.14",
|
||||
"vary": "1.1.2",
|
||||
"web-push": "3.6.3",
|
||||
"ws": "8.13.0",
|
||||
|
@ -172,7 +168,6 @@
|
|||
"@types/cbor": "6.0.0",
|
||||
"@types/color-convert": "2.0.0",
|
||||
"@types/content-disposition": "0.5.5",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
"@types/fluent-ffmpeg": "2.1.21",
|
||||
"@types/jest": "29.5.3",
|
||||
"@types/js-yaml": "4.0.5",
|
||||
|
@ -191,7 +186,6 @@
|
|||
"@types/qrcode": "1.5.1",
|
||||
"@types/random-seed": "0.3.3",
|
||||
"@types/ratelimiter": "3.4.4",
|
||||
"@types/redis": "4.0.11",
|
||||
"@types/rename": "1.0.4",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
"@types/semver": "7.5.0",
|
||||
|
@ -199,10 +193,8 @@
|
|||
"@types/sinonjs__fake-timers": "8.1.2",
|
||||
"@types/tinycolor2": "1.4.3",
|
||||
"@types/tmp": "0.2.3",
|
||||
"@types/unzipper": "0.10.6",
|
||||
"@types/vary": "1.1.0",
|
||||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.5",
|
||||
"@typescript-eslint/eslint-plugin": "5.61.0",
|
||||
"@typescript-eslint/parser": "5.61.0",
|
||||
|
|
|
@ -41,14 +41,7 @@ const $meilisearch: Provider = {
|
|||
const $redis: Provider = {
|
||||
provide: DI.redis,
|
||||
useFactory: (config: Config) => {
|
||||
return new Redis.Redis({
|
||||
port: config.redis.port,
|
||||
host: config.redis.host,
|
||||
family: config.redis.family == null ? 0 : config.redis.family,
|
||||
password: config.redis.pass,
|
||||
keyPrefix: `${config.redis.prefix}:`,
|
||||
db: config.redis.db ?? 0,
|
||||
});
|
||||
return new Redis.Redis(config.redis);
|
||||
},
|
||||
inject: [DI.config],
|
||||
};
|
||||
|
@ -56,14 +49,7 @@ const $redis: Provider = {
|
|||
const $redisForPub: Provider = {
|
||||
provide: DI.redisForPub,
|
||||
useFactory: (config: Config) => {
|
||||
const redis = new Redis.Redis({
|
||||
port: config.redisForPubsub.port,
|
||||
host: config.redisForPubsub.host,
|
||||
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
|
||||
password: config.redisForPubsub.pass,
|
||||
keyPrefix: `${config.redisForPubsub.prefix}:`,
|
||||
db: config.redisForPubsub.db ?? 0,
|
||||
});
|
||||
const redis = new Redis.Redis(config.redisForPubsub);
|
||||
return redis;
|
||||
},
|
||||
inject: [DI.config],
|
||||
|
@ -72,14 +58,7 @@ const $redisForPub: Provider = {
|
|||
const $redisForSub: Provider = {
|
||||
provide: DI.redisForSub,
|
||||
useFactory: (config: Config) => {
|
||||
const redis = new Redis.Redis({
|
||||
port: config.redisForPubsub.port,
|
||||
host: config.redisForPubsub.host,
|
||||
family: config.redisForPubsub.family == null ? 0 : config.redisForPubsub.family,
|
||||
password: config.redisForPubsub.pass,
|
||||
keyPrefix: `${config.redisForPubsub.prefix}:`,
|
||||
db: config.redisForPubsub.db ?? 0,
|
||||
});
|
||||
const redis = new Redis.Redis(config.redisForPubsub);
|
||||
redis.subscribe(config.host);
|
||||
return redis;
|
||||
},
|
||||
|
|
|
@ -6,6 +6,16 @@ import * as fs from 'node:fs';
|
|||
import { fileURLToPath } from 'node:url';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import * as yaml from 'js-yaml';
|
||||
import type { RedisOptions } from 'ioredis';
|
||||
|
||||
type RedisOptionsSource = Partial<RedisOptions> & {
|
||||
host: string;
|
||||
port: number;
|
||||
family?: number;
|
||||
pass: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* ユーザーが設定する必要のある情報
|
||||
|
@ -35,30 +45,9 @@ export type Source = {
|
|||
user: string;
|
||||
pass: string;
|
||||
}[];
|
||||
redis: {
|
||||
host: string;
|
||||
port: number;
|
||||
family?: number;
|
||||
pass: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
redisForPubsub?: {
|
||||
host: string;
|
||||
port: number;
|
||||
family?: number;
|
||||
pass: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
redisForJobQueue?: {
|
||||
host: string;
|
||||
port: number;
|
||||
family?: number;
|
||||
pass: string;
|
||||
db?: number;
|
||||
prefix?: string;
|
||||
};
|
||||
redis: RedisOptionsSource;
|
||||
redisForPubsub?: RedisOptionsSource;
|
||||
redisForJobQueue?: RedisOptionsSource;
|
||||
meilisearch?: {
|
||||
host: string;
|
||||
port: string;
|
||||
|
@ -119,8 +108,9 @@ export type Mixin = {
|
|||
mediaProxy: string;
|
||||
externalMediaProxyEnabled: boolean;
|
||||
videoThumbnailGenerator: string | null;
|
||||
redisForPubsub: NonNullable<Source['redisForPubsub']>;
|
||||
redisForJobQueue: NonNullable<Source['redisForJobQueue']>;
|
||||
redis: RedisOptions & RedisOptionsSource;
|
||||
redisForPubsub: RedisOptions & RedisOptionsSource;
|
||||
redisForJobQueue: RedisOptions & RedisOptionsSource;
|
||||
};
|
||||
|
||||
export type Config = Source & Mixin;
|
||||
|
@ -182,9 +172,9 @@ export function loadConfig() {
|
|||
config.videoThumbnailGenerator.endsWith('/') ? config.videoThumbnailGenerator.substring(0, config.videoThumbnailGenerator.length - 1) : config.videoThumbnailGenerator
|
||||
: null;
|
||||
|
||||
if (!config.redis.prefix) config.redis.prefix = mixin.host;
|
||||
if (config.redisForPubsub == null) config.redisForPubsub = config.redis;
|
||||
if (config.redisForJobQueue == null) config.redisForJobQueue = config.redis;
|
||||
mixin.redis = convertRedisOptions(config.redis, mixin.host);
|
||||
mixin.redisForPubsub = config.redisForPubsub ? convertRedisOptions(config.redisForPubsub, mixin.host) : mixin.redis;
|
||||
mixin.redisForJobQueue = config.redisForJobQueue ? convertRedisOptions(config.redisForJobQueue, mixin.host) : mixin.redis;
|
||||
|
||||
return Object.assign(config, mixin);
|
||||
}
|
||||
|
@ -196,3 +186,14 @@ function tryCreateUrl(url: string) {
|
|||
throw new Error(`url="${url}" is not a valid URL.`);
|
||||
}
|
||||
}
|
||||
|
||||
function convertRedisOptions(options: RedisOptionsSource, host: string): RedisOptions & RedisOptionsSource {
|
||||
return {
|
||||
...options,
|
||||
password: options.pass,
|
||||
prefix: options.prefix ?? host,
|
||||
family: options.family == null ? 0 : options.family,
|
||||
keyPrefix: `${options.prefix ?? host}:`,
|
||||
db: options.db ?? 0,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -33,7 +33,7 @@ export class CreateSystemUserService {
|
|||
// Generate secret
|
||||
const secret = generateNativeUserToken();
|
||||
|
||||
const keyPair = await genRsaKeyPair(4096);
|
||||
const keyPair = await genRsaKeyPair();
|
||||
|
||||
let account!: User;
|
||||
|
||||
|
|
|
@ -108,7 +108,7 @@ export class QueueService {
|
|||
removeOnFail: true,
|
||||
};
|
||||
|
||||
await this.deliverQueue.addBulk(Array.from(inboxes.entries()).map(d => ({
|
||||
await this.deliverQueue.addBulk(Array.from(inboxes.entries(), d => ({
|
||||
name: d[0],
|
||||
data: {
|
||||
user,
|
||||
|
|
|
@ -220,14 +220,19 @@ export class RoleService implements OnApplicationShutdown {
|
|||
}
|
||||
|
||||
@bindThis
|
||||
public async getUserRoles(userId: User['id']) {
|
||||
public async getUserAssigns(userId: User['id']) {
|
||||
const now = Date.now();
|
||||
let assigns = await this.roleAssignmentByUserIdCache.fetch(userId, () => this.roleAssignmentsRepository.findBy({ userId }));
|
||||
// 期限切れのロールを除外
|
||||
assigns = assigns.filter(a => a.expiresAt == null || (a.expiresAt.getTime() > now));
|
||||
const assignedRoleIds = assigns.map(x => x.roleId);
|
||||
return assigns;
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async getUserRoles(userId: User['id']) {
|
||||
const roles = await this.rolesCache.fetch(() => this.rolesRepository.findBy({}));
|
||||
const assignedRoles = roles.filter(r => assignedRoleIds.includes(r.id));
|
||||
const assigns = await this.getUserAssigns(userId);
|
||||
const assignedRoles = roles.filter(r => assigns.map(x => x.roleId).includes(r.id));
|
||||
const user = roles.some(r => r.target === 'conditional') ? await this.cacheService.findUserById(userId) : null;
|
||||
const matchedCondRoles = roles.filter(r => r.target === 'conditional' && this.evalCond(user!, r.condFormula));
|
||||
return [...assignedRoles, ...matchedCondRoles];
|
||||
|
|
|
@ -92,7 +92,7 @@ export class SignupService {
|
|||
|
||||
const keyPair = await new Promise<string[]>((res, rej) =>
|
||||
generateKeyPair('rsa', {
|
||||
modulusLength: 4096,
|
||||
modulusLength: 2048,
|
||||
publicKeyEncoding: {
|
||||
type: 'spki',
|
||||
format: 'pem',
|
||||
|
|
|
@ -220,6 +220,23 @@ export class ApPersonService implements OnModuleInit {
|
|||
return null;
|
||||
}
|
||||
|
||||
private async resolveAvatarAndBanner(user: RemoteUser, icon: any, image: any): Promise<Pick<RemoteUser, 'avatarId' | 'bannerId' | 'avatarUrl' | 'bannerUrl' | 'avatarBlurhash' | 'bannerBlurhash'>> {
|
||||
const [avatar, banner] = await Promise.all([icon, image].map(img => {
|
||||
if (img == null) return null;
|
||||
if (user == null) throw new Error('failed to create user: user is null');
|
||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||
}));
|
||||
|
||||
return {
|
||||
avatarId: avatar?.id ?? null,
|
||||
bannerId: banner?.id ?? null,
|
||||
avatarUrl: avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null,
|
||||
bannerUrl: banner ? this.driveFileEntityService.getPublicUrl(banner) : null,
|
||||
avatarBlurhash: avatar?.blurhash ?? null,
|
||||
bannerBlurhash: banner?.blurhash ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Personを作成します。
|
||||
*/
|
||||
|
@ -259,6 +276,16 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
// Create user
|
||||
let user: RemoteUser | null = null;
|
||||
|
||||
//#region カスタム絵文字取得
|
||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host)
|
||||
.then(_emojis => _emojis.map(emoji => emoji.name))
|
||||
.catch(err => {
|
||||
this.logger.error(`error occured while fetching user emojis`, { stack: err });
|
||||
return [];
|
||||
});
|
||||
//#endregion
|
||||
|
||||
try {
|
||||
// Start transaction
|
||||
await this.db.transaction(async transactionalEntityManager => {
|
||||
|
@ -285,6 +312,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
tags,
|
||||
isBot,
|
||||
isCat: (person as any).isCat === true,
|
||||
emojis,
|
||||
})) as RemoteUser;
|
||||
|
||||
await transactionalEntityManager.save(new UserProfile({
|
||||
|
@ -321,6 +349,9 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
if (user == null) throw new Error('failed to create user: user is null');
|
||||
|
||||
// Register to the cache
|
||||
this.cacheService.uriPersonCache.set(user.uri, user);
|
||||
|
||||
// Register host
|
||||
this.federatedInstanceService.fetch(host).then(async i => {
|
||||
this.instancesRepository.increment({ id: i.id }, 'usersCount', 1);
|
||||
|
@ -336,45 +367,16 @@ export class ApPersonService implements OnModuleInit {
|
|||
this.hashtagService.updateUsertags(user, tags);
|
||||
|
||||
//#region アバターとヘッダー画像をフェッチ
|
||||
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
|
||||
if (img == null) return null;
|
||||
if (user == null) throw new Error('failed to create user: user is null');
|
||||
return this.apImageService.resolveImage(user, img).catch(() => null);
|
||||
}));
|
||||
try {
|
||||
const updates = await this.resolveAvatarAndBanner(user, person.icon, person.image);
|
||||
await this.usersRepository.update(user.id, updates);
|
||||
user = { ...user, ...updates };
|
||||
|
||||
const avatarId = avatar?.id ?? null;
|
||||
const bannerId = banner?.id ?? null;
|
||||
const avatarUrl = avatar ? this.driveFileEntityService.getPublicUrl(avatar, 'avatar') : null;
|
||||
const bannerUrl = banner ? this.driveFileEntityService.getPublicUrl(banner) : null;
|
||||
const avatarBlurhash = avatar?.blurhash ?? null;
|
||||
const bannerBlurhash = banner?.blurhash ?? null;
|
||||
|
||||
await this.usersRepository.update(user.id, {
|
||||
avatarId,
|
||||
bannerId,
|
||||
avatarUrl,
|
||||
bannerUrl,
|
||||
avatarBlurhash,
|
||||
bannerBlurhash,
|
||||
});
|
||||
|
||||
user.avatarId = avatarId;
|
||||
user.bannerId = bannerId;
|
||||
user.avatarUrl = avatarUrl;
|
||||
user.bannerUrl = bannerUrl;
|
||||
user.avatarBlurhash = avatarBlurhash;
|
||||
user.bannerBlurhash = bannerBlurhash;
|
||||
//#endregion
|
||||
|
||||
//#region カスタム絵文字取得
|
||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], host).catch(err => {
|
||||
this.logger.info(`extractEmojis: ${err}`);
|
||||
return [];
|
||||
});
|
||||
|
||||
const emojiNames = emojis.map(emoji => emoji.name);
|
||||
|
||||
await this.usersRepository.update(user.id, { emojis: emojiNames });
|
||||
// Register to the cache
|
||||
this.cacheService.uriPersonCache.set(user.uri, user);
|
||||
} catch (err) {
|
||||
this.logger.error('error occured while fetching user avatar/banner', { stack: err });
|
||||
}
|
||||
//#endregion
|
||||
|
||||
await this.updateFeatured(user.id, resolver).catch(err => this.logger.error(err));
|
||||
|
@ -400,7 +402,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
if (uri.startsWith(`${this.config.url}/`)) return;
|
||||
|
||||
//#region このサーバーに既に登録されているか
|
||||
const exist = await this.usersRepository.findOneBy({ uri }) as RemoteUser | null;
|
||||
const exist = await this.fetchPerson(uri) as RemoteUser | null;
|
||||
if (exist === null) return;
|
||||
//#endregion
|
||||
|
||||
|
@ -413,12 +415,6 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
this.logger.info(`Updating the Person: ${person.id}`);
|
||||
|
||||
// アバターとヘッダー画像をフェッチ
|
||||
const [avatar, banner] = await Promise.all([person.icon, person.image].map(img => {
|
||||
if (img == null) return null;
|
||||
return this.apImageService.resolveImage(exist, img).catch(() => null);
|
||||
}));
|
||||
|
||||
// カスタム絵文字取得
|
||||
const emojis = await this.apNoteService.extractEmojis(person.tag ?? [], exist.host).catch(e => {
|
||||
this.logger.info(`extractEmojis: ${e}`);
|
||||
|
@ -454,6 +450,7 @@ export class ApPersonService implements OnModuleInit {
|
|||
movedToUri: person.movedTo ?? null,
|
||||
alsoKnownAs: person.alsoKnownAs ?? null,
|
||||
isExplorable: person.discoverable,
|
||||
...(await this.resolveAvatarAndBanner(exist, person.icon, person.image).catch(() => ({}))),
|
||||
} as Partial<RemoteUser> & Pick<RemoteUser, 'isBot' | 'isCat' | 'isLocked' | 'movedToUri' | 'alsoKnownAs' | 'isExplorable'>;
|
||||
|
||||
const moving = ((): boolean => {
|
||||
|
@ -476,18 +473,6 @@ export class ApPersonService implements OnModuleInit {
|
|||
|
||||
if (moving) updates.movedAt = new Date();
|
||||
|
||||
if (avatar) {
|
||||
updates.avatarId = avatar.id;
|
||||
updates.avatarUrl = this.driveFileEntityService.getPublicUrl(avatar, 'avatar');
|
||||
updates.avatarBlurhash = avatar.blurhash;
|
||||
}
|
||||
|
||||
if (banner) {
|
||||
updates.bannerId = banner.id;
|
||||
updates.bannerUrl = this.driveFileEntityService.getPublicUrl(banner);
|
||||
updates.bannerBlurhash = banner.blurhash;
|
||||
}
|
||||
|
||||
// Update user
|
||||
await this.usersRepository.update(exist.id, updates);
|
||||
|
||||
|
|
|
@ -15,11 +15,8 @@ export const QUEUE = {
|
|||
export function baseQueueOptions(config: Config, queueName: typeof QUEUE[keyof typeof QUEUE]): Bull.QueueOptions {
|
||||
return {
|
||||
connection: {
|
||||
port: config.redisForJobQueue.port,
|
||||
host: config.redisForJobQueue.host,
|
||||
family: config.redisForJobQueue.family == null ? 0 : config.redisForJobQueue.family,
|
||||
password: config.redisForJobQueue.pass,
|
||||
db: config.redisForJobQueue.db ?? 0,
|
||||
...config.redisForJobQueue,
|
||||
keyPrefix: undefined
|
||||
},
|
||||
prefix: config.redisForJobQueue.prefix ? `${config.redisForJobQueue.prefix}:queue:${queueName}` : `queue:${queueName}`,
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as fs from 'node:fs';
|
||||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ZipReader } from 'slacc';
|
||||
import { DataSource } from 'typeorm';
|
||||
import unzipper from 'unzipper';
|
||||
import { DI } from '@/di-symbols.js';
|
||||
import type { EmojisRepository, DriveFilesRepository, UsersRepository } from '@/models/index.js';
|
||||
import type { Config } from '@/config.js';
|
||||
|
@ -72,9 +72,9 @@ export class ImportCustomEmojisProcessorService {
|
|||
}
|
||||
|
||||
const outputPath = path + '/emojis';
|
||||
const unzipStream = fs.createReadStream(destPath);
|
||||
const extractor = unzipper.Extract({ path: outputPath });
|
||||
extractor.on('close', async () => {
|
||||
try {
|
||||
this.logger.succ(`Unzipping to ${outputPath}`);
|
||||
ZipReader.withDestinationPath(outputPath).viaBuffer(await fs.promises.readFile(destPath));
|
||||
const metaRaw = fs.readFileSync(outputPath + '/meta.json', 'utf-8');
|
||||
const meta = JSON.parse(metaRaw);
|
||||
|
||||
|
@ -115,8 +115,12 @@ export class ImportCustomEmojisProcessorService {
|
|||
cleanup();
|
||||
|
||||
this.logger.succ('Imported');
|
||||
});
|
||||
unzipStream.pipe(extractor);
|
||||
this.logger.succ(`Unzipping to ${outputPath}`);
|
||||
} catch (e) {
|
||||
if (e instanceof Error || typeof e === 'string') {
|
||||
this.logger.error(e);
|
||||
}
|
||||
cleanup();
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import type { DriveFilesRepository } from '@/models/index.js';
|
|||
import { DI } from '@/di-symbols.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
import { ModerationLogService } from '@/core/ModerationLogService.js';
|
||||
import { EmojiEntityService } from '@/core/entities/EmojiEntityService.js';
|
||||
import { ApiError } from '../../../error.js';
|
||||
|
||||
export const meta = {
|
||||
|
@ -55,6 +56,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
|
||||
private customEmojiService: CustomEmojiService,
|
||||
|
||||
private emojiEntityService: EmojiEntityService,
|
||||
private moderationLogService: ModerationLogService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
|
@ -77,9 +79,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
emojiId: emoji.id,
|
||||
});
|
||||
|
||||
return {
|
||||
id: emoji.id,
|
||||
};
|
||||
return this.emojiEntityService.packDetailed(emoji);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -61,6 +61,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
|
||||
const signins = await this.signinsRepository.findBy({ userId: user.id });
|
||||
|
||||
const roleAssigns = await this.roleService.getUserAssigns(user.id);
|
||||
const roles = await this.roleService.getUserRoles(user.id);
|
||||
|
||||
return {
|
||||
|
@ -85,6 +86,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
signins,
|
||||
policies: await this.roleService.getUserPolicies(user.id),
|
||||
roles: await this.roleEntityService.packMany(roles, me),
|
||||
roleAssigns: roleAssigns.map(a => ({
|
||||
createdAt: a.createdAt.toISOString(),
|
||||
expiresAt: a.expiresAt ? a.expiresAt.toISOString() : null,
|
||||
roleId: a.roleId,
|
||||
})),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -76,6 +76,11 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
throw new ApiError(meta.errors.noSuchAntenna);
|
||||
}
|
||||
|
||||
this.antennasRepository.update(antenna.id, {
|
||||
isActive: true,
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
const limit = ps.limit + (ps.untilId ? 1 : 0) + (ps.sinceId ? 1 : 0); // untilIdに指定したものも含まれるため+1
|
||||
const noteIdsRes = await this.redisClient.xrevrange(
|
||||
`antennaTimeline:${antenna.id}`,
|
||||
|
@ -112,11 +117,6 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
this.noteReadService.read(me.id, notes);
|
||||
}
|
||||
|
||||
this.antennasRepository.update(antenna.id, {
|
||||
isActive: true,
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
return await this.noteEntityService.packMany(notes, me);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -112,6 +112,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
withReplies: ps.withReplies,
|
||||
withFile: ps.withFile,
|
||||
notify: ps.notify,
|
||||
isActive: true,
|
||||
lastUsedAt: new Date(),
|
||||
});
|
||||
|
||||
this.globalEventService.publishInternalEvent('antennaUpdated', await this.antennasRepository.findOneByOrFail({ id: antenna.id }));
|
||||
|
|
|
@ -103,7 +103,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
const procedures = this.twoFactorAuthenticationService.getProcedures();
|
||||
|
||||
if (!(procedures as any)[attestation.fmt]) {
|
||||
throw new Error('unsupported fmt');
|
||||
throw new Error(`unsupported fmt: ${attestation.fmt}. Supported ones: ${Object.keys(procedures)}`);
|
||||
}
|
||||
|
||||
const verificationData = (procedures as any)[attestation.fmt].verify({
|
||||
|
|
|
@ -5,8 +5,8 @@ block vars
|
|||
- const title = user.name ? `${user.name} (@${user.username})` : `@${user.username}`;
|
||||
- const url = `${config.url}/notes/${note.id}`;
|
||||
- const isRenote = note.renote && note.text == null && note.fileIds.length == 0 && note.poll == null;
|
||||
- const image = (note.files || []).find(file => file.type.startsWith('image/') && !file.isSensitive)
|
||||
- const video = (note.files || []).find(file => file.type.startsWith('video/') && !file.isSensitive)
|
||||
- const images = (note.files || []).filter(file => file.type.startsWith('image/') && !file.isSensitive)
|
||||
- const videos = (note.files || []).filter(file => file.type.startsWith('video/') && !file.isSensitive)
|
||||
|
||||
block title
|
||||
= `${title} | ${instanceName}`
|
||||
|
@ -19,14 +19,16 @@ block og
|
|||
meta(property='og:title' content= title)
|
||||
meta(property='og:description' content= summary)
|
||||
meta(property='og:url' content= url)
|
||||
if video
|
||||
if videos.length
|
||||
each video in videos
|
||||
meta(property='og:video:url' content= video.url)
|
||||
meta(property='og:video:secure_url' content= video.url)
|
||||
meta(property='og:video:type' content= video.type)
|
||||
// FIXME: add width and height
|
||||
// FIXME: add embed player for Twitter
|
||||
if image
|
||||
if images.length
|
||||
meta(property='twitter:card' content='summary_large_image')
|
||||
each image in images
|
||||
meta(property='og:image' content= image.url)
|
||||
else
|
||||
meta(property='twitter:card' content='summary')
|
||||
|
|
|
@ -4,8 +4,9 @@
|
|||
"scripts": {
|
||||
"watch": "vite",
|
||||
"build": "vite build",
|
||||
"storybook-dev": "chokidar 'src/**/*.{mdx,ts,vue}' -d 1000 -t 1000 --initial -i '**/*.stories.ts' -c 'pkill -f node_modules/storybook/index.js; node_modules/.bin/tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js && node_modules/.bin/storybook dev -p 6006 --ci'",
|
||||
"build-storybook": "tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js && storybook build",
|
||||
"storybook-dev": "nodemon --verbose --watch src --ext \"mdx,ts,vue\" --ignore \"*.stories.ts\" --exec \"pnpm build-storybook-pre && pnpm exec storybook dev -p 6006 --ci\"",
|
||||
"build-storybook-pre": "tsc -p .storybook && node .storybook/generate.js && node .storybook/preload-locale.js && node .storybook/preload-theme.js",
|
||||
"build-storybook": "pnpm build-storybook-pre && storybook build",
|
||||
"chromatic": "chromatic",
|
||||
"test": "vitest --run",
|
||||
"test-and-coverage": "vitest --run --coverage",
|
||||
|
@ -19,7 +20,7 @@
|
|||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/plugin-replace": "5.0.2",
|
||||
"@rollup/pluginutils": "5.0.2",
|
||||
"@syuilo/aiscript": "0.14.0",
|
||||
"@syuilo/aiscript": "0.15.0",
|
||||
"@tabler/icons-webfont": "2.25.0",
|
||||
"@vitejs/plugin-vue": "4.2.3",
|
||||
"@vue-macros/reactivity-transform": "0.3.15",
|
||||
|
@ -77,24 +78,24 @@
|
|||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.1.0",
|
||||
"@storybook/addon-essentials": "7.1.0",
|
||||
"@storybook/addon-interactions": "7.1.0",
|
||||
"@storybook/addon-links": "7.1.0",
|
||||
"@storybook/addon-storysource": "7.1.0",
|
||||
"@storybook/addons": "7.1.0",
|
||||
"@storybook/blocks": "7.1.0",
|
||||
"@storybook/core-events": "7.1.0",
|
||||
"@storybook/addon-actions": "7.0.27",
|
||||
"@storybook/addon-essentials": "7.0.27",
|
||||
"@storybook/addon-interactions": "7.0.27",
|
||||
"@storybook/addon-links": "7.0.27",
|
||||
"@storybook/addon-storysource": "7.0.27",
|
||||
"@storybook/addons": "7.0.27",
|
||||
"@storybook/blocks": "7.0.27",
|
||||
"@storybook/core-events": "7.0.27",
|
||||
"@storybook/jest": "0.1.0",
|
||||
"@storybook/manager-api": "7.1.0",
|
||||
"@storybook/preview-api": "7.1.0",
|
||||
"@storybook/react": "7.1.0",
|
||||
"@storybook/react-vite": "7.1.0",
|
||||
"@storybook/manager-api": "7.0.27",
|
||||
"@storybook/preview-api": "7.0.27",
|
||||
"@storybook/react": "7.0.27",
|
||||
"@storybook/react-vite": "7.0.27",
|
||||
"@storybook/testing-library": "0.2.0",
|
||||
"@storybook/theming": "7.1.0",
|
||||
"@storybook/types": "7.1.0",
|
||||
"@storybook/vue3": "7.1.0",
|
||||
"@storybook/vue3-vite": "7.1.0",
|
||||
"@storybook/theming": "7.0.27",
|
||||
"@storybook/types": "7.0.27",
|
||||
"@storybook/vue3": "7.0.27",
|
||||
"@storybook/vue3-vite": "7.0.27",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
|
@ -117,7 +118,6 @@
|
|||
"@vitest/coverage-v8": "0.33.0",
|
||||
"@vue/runtime-core": "3.3.4",
|
||||
"acorn": "8.10.0",
|
||||
"chokidar-cli": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.17.1",
|
||||
"eslint": "8.45.0",
|
||||
|
@ -128,11 +128,12 @@
|
|||
"micromatch": "4.0.5",
|
||||
"msw": "1.2.2",
|
||||
"msw-storybook-addon": "1.8.0",
|
||||
"nodemon": "3.0.1",
|
||||
"prettier": "3.0.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"storybook": "7.1.0",
|
||||
"storybook": "7.0.27",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "1.0.2",
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
</div>
|
||||
<div v-if="file.isSensitive" :class="[$style.label, $style.red]">
|
||||
<img :class="$style.labelImg" src="/client-assets/label-red.svg"/>
|
||||
<p :class="$style.labelText">NSFW</p>
|
||||
<p :class="$style.labelText">{{ i18n.ts.sensitive }}</p>
|
||||
</div>
|
||||
|
||||
<MkDriveFileThumbnail :class="$style.thumbnail" :file="file" fit="contain"/>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
<template v-if="hide">
|
||||
<div :class="$style.hiddenText">
|
||||
<div :class="$style.hiddenTextWrapper">
|
||||
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
||||
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
|
||||
<b v-else style="display: block;"><i class="ti ti-photo"></i> {{ defaultStore.state.enableDataSaverMode && image.size ? bytes(image.size) : i18n.ts.image }}</b>
|
||||
<span style="display: block;">{{ i18n.ts.clickToShow }}</span>
|
||||
</div>
|
||||
|
@ -30,7 +30,7 @@
|
|||
<div :class="$style.indicators">
|
||||
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
|
||||
<div v-if="image.comment" :class="$style.indicator">ALT</div>
|
||||
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);">NSFW</div>
|
||||
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
|
||||
</div>
|
||||
<button :class="$style.menu" class="_button" @click.stop="showMenu"><i class="ti ti-dots" style="vertical-align: middle;"></i></button>
|
||||
<i class="ti ti-eye-off" :class="$style.hide" @click.stop="hide = true"></i>
|
||||
|
|
|
@ -165,6 +165,7 @@ import { getNoteSummary } from '@/scripts/get-note-summary';
|
|||
import { MenuItem } from '@/types/menu';
|
||||
import MkRippleEffect from '@/components/MkRippleEffect.vue';
|
||||
import { showMovedDialog } from '@/scripts/show-moved-dialog';
|
||||
import { shouldCollapsed } from '@/scripts/collapsed';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
|
@ -204,17 +205,7 @@ let appearNote = $computed(() => isRenote ? note.renote as misskey.entities.Note
|
|||
const isMyRenote = $i && ($i.id === note.userId);
|
||||
const showContent = ref(false);
|
||||
const urls = appearNote.text ? extractUrlFromMfm(mfm.parse(appearNote.text)) : null;
|
||||
const isLong = (appearNote.cw == null && appearNote.text != null && (
|
||||
(appearNote.text.includes('$[x2')) ||
|
||||
(appearNote.text.includes('$[x3')) ||
|
||||
(appearNote.text.includes('$[x4')) ||
|
||||
(appearNote.text.includes('$[scale')) ||
|
||||
(appearNote.text.includes('$[position')) ||
|
||||
(appearNote.text.split('\n').length > 9) ||
|
||||
(appearNote.text.length > 500) ||
|
||||
(appearNote.files.length >= 5) ||
|
||||
(urls && urls.length >= 4)
|
||||
));
|
||||
const isLong = shouldCollapsed(appearNote);
|
||||
const collapsed = ref(appearNote.cw == null && isLong);
|
||||
const isDeleted = ref(false);
|
||||
const muted = ref(checkWordMute(appearNote, $i, defaultStore.state.mutedWords));
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
<div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
|
||||
<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/>
|
||||
<div v-if="element.isSensitive" :class="$style.sensitive">
|
||||
<i class="ti ti-alert-triangle" style="margin: auto;"></i>
|
||||
<i class="ti ti-eye-exclamation" style="margin: auto;"></i>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -9,7 +9,10 @@
|
|||
<MkInfo warn>{{ i18n.ts.invitationRequiredToRegister }}</MkInfo>
|
||||
</div>
|
||||
|
||||
<div style="text-align: center;">{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div>
|
||||
<div style="text-align: center;">
|
||||
<div>{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div>
|
||||
<div style="font-weight: bold; margin-top: 0.5em;">{{ i18n.ts.beSureToReadThisAsItIsImportant }}</div>
|
||||
</div>
|
||||
|
||||
<MkFolder v-if="availableServerRules" :defaultOpen="true">
|
||||
<template #label>{{ i18n.ts.serverRules }}</template>
|
||||
|
@ -19,7 +22,7 @@
|
|||
<li v-for="item in instance.serverRules" :class="$style.rule"><div :class="$style.ruleText" v-html="item"></div></li>
|
||||
</ol>
|
||||
|
||||
<MkSwitch v-model="agreeServerRules" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch>
|
||||
<MkSwitch :modelValue="agreeServerRules" style="margin-top: 16px;" @update:modelValue="updateAgreeServerRules">{{ i18n.ts.agree }}</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="availableTos" :defaultOpen="true">
|
||||
|
@ -28,7 +31,7 @@
|
|||
|
||||
<a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.termsOfService }} <i class="ti ti-external-link"></i></a>
|
||||
|
||||
<MkSwitch v-model="agreeTos" style="margin-top: 16px;">{{ i18n.ts.agree }}</MkSwitch>
|
||||
<MkSwitch :modelValue="agreeTos" style="margin-top: 16px;" @update:modelValue="updateAgreeTos">{{ i18n.ts.agree }}</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder :defaultOpen="true">
|
||||
|
@ -37,7 +40,7 @@
|
|||
|
||||
<a href="https://misskey-hub.net/docs/notes.html" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }} <i class="ti ti-external-link"></i></a>
|
||||
|
||||
<MkSwitch v-model="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree>{{ i18n.ts.agree }}</MkSwitch>
|
||||
<MkSwitch :modelValue="agreeNote" style="margin-top: 16px;" data-cy-signup-rules-notes-agree @update:modelValue="updateAgreeNote">{{ i18n.ts.agree }}</MkSwitch>
|
||||
</MkFolder>
|
||||
|
||||
<div v-if="!agreed" style="text-align: center;">{{ i18n.ts.pleaseAgreeAllToContinue }}</div>
|
||||
|
@ -52,13 +55,14 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } from 'vue';
|
||||
import { computed, onMounted, ref, watch } from 'vue';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkFolder from '@/components/MkFolder.vue';
|
||||
import MkSwitch from '@/components/MkSwitch.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import * as os from '@/os';
|
||||
|
||||
const availableServerRules = instance.serverRules.length > 0;
|
||||
const availableTos = instance.tosUrl != null;
|
||||
|
@ -75,6 +79,48 @@ const emit = defineEmits<{
|
|||
(ev: 'cancel'): void;
|
||||
(ev: 'done'): void;
|
||||
}>();
|
||||
|
||||
async function updateAgreeServerRules(v: boolean) {
|
||||
if (v) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts.doYouAgree,
|
||||
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.serverRules }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
agreeServerRules.value = true;
|
||||
} else {
|
||||
agreeServerRules.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAgreeTos(v: boolean) {
|
||||
if (v) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts.doYouAgree,
|
||||
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.termsOfService }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
agreeTos.value = true;
|
||||
} else {
|
||||
agreeTos.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateAgreeNote(v: boolean) {
|
||||
if (v) {
|
||||
const confirm = await os.confirm({
|
||||
type: 'question',
|
||||
title: i18n.ts.doYouAgree,
|
||||
text: i18n.t('iHaveReadXCarefullyAndAgree', { x: i18n.ts.basicNotesBeforeCreateAccount }),
|
||||
});
|
||||
if (confirm.canceled) return;
|
||||
agreeNote.value = true;
|
||||
} else {
|
||||
agreeNote.value = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
|
|
|
@ -31,16 +31,13 @@ import MkMediaList from '@/components/MkMediaList.vue';
|
|||
import MkPoll from '@/components/MkPoll.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { $i } from '@/account';
|
||||
import { shouldCollapsed } from '@/scripts/collapsed';
|
||||
|
||||
const props = defineProps<{
|
||||
note: misskey.entities.Note;
|
||||
}>();
|
||||
|
||||
const isLong =
|
||||
props.note.cw == null && props.note.text != null && (
|
||||
(props.note.text.split('\n').length > 9) ||
|
||||
(props.note.text.length > 500)
|
||||
);
|
||||
const isLong = shouldCollapsed(props.note);
|
||||
|
||||
const collapsed = $ref(isLong);
|
||||
</script>
|
||||
|
|
|
@ -52,6 +52,7 @@
|
|||
</footer>
|
||||
</article>
|
||||
</component>
|
||||
<template v-if="showActions">
|
||||
<div v-if="tweetId" :class="$style.action">
|
||||
<MkButton :small="true" inline @click="tweetExpanded = true">
|
||||
<i class="ti ti-brand-twitter"></i> {{ i18n.ts.expandTweet }}
|
||||
|
@ -65,6 +66,7 @@
|
|||
<i class="ti ti-picture-in-picture"></i> {{ i18n.ts.openInWindow }}
|
||||
</MkButton>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -85,9 +87,11 @@ const props = withDefaults(defineProps<{
|
|||
url: string;
|
||||
detail?: boolean;
|
||||
compact?: boolean;
|
||||
showActions?: boolean;
|
||||
}>(), {
|
||||
detail: false,
|
||||
compact: false,
|
||||
showActions: true,
|
||||
});
|
||||
|
||||
const MOBILE_THRESHOLD = 500;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div :class="$style.root" :style="{ zIndex, top: top + 'px', left: left + 'px' }">
|
||||
<Transition :name="defaultStore.state.animation ? '_transition_zoom' : ''" @afterLeave="emit('closed')">
|
||||
<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url"/>
|
||||
<MkUrlPreview v-if="showing" class="_popup _shadow" :url="url" :showActions="false"/>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
|
|
@ -4,6 +4,9 @@ import { userEvent, waitFor, within } from '@storybook/testing-library';
|
|||
import { StoryObj } from '@storybook/vue3';
|
||||
import MkAd from './MkAd.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
let lock: Promise<undefined> | undefined;
|
||||
|
||||
const common = {
|
||||
render(args) {
|
||||
return {
|
||||
|
@ -26,6 +29,16 @@ const common = {
|
|||
};
|
||||
},
|
||||
async play({ canvasElement, args }) {
|
||||
if (lock) {
|
||||
console.warn('This test is unexpectedly running twice in parallel, fix it!');
|
||||
console.warn('See also: https://github.com/misskey-dev/misskey/issues/11267');
|
||||
await lock;
|
||||
}
|
||||
|
||||
let resolve: (value?: any) => void;
|
||||
lock = new Promise(r => resolve = r);
|
||||
|
||||
try {
|
||||
const canvas = within(canvasElement);
|
||||
const a = canvas.getByRole<HTMLAnchorElement>('link');
|
||||
await expect(a.href).toMatch(/^https?:\/\/.*#test$/);
|
||||
|
@ -59,6 +72,10 @@ const common = {
|
|||
await expect(aAgain).toBeInTheDocument();
|
||||
const imgAgain = within(aAgain).getByRole('img');
|
||||
await expect(imgAgain).toBeInTheDocument();
|
||||
} finally {
|
||||
resolve!();
|
||||
lock = undefined;
|
||||
}
|
||||
},
|
||||
args: {
|
||||
prefer: [],
|
||||
|
|
|
@ -179,6 +179,9 @@ const patronsWithIcon = [{
|
|||
}, {
|
||||
name: 'カガミ',
|
||||
icon: 'https://misskey-hub.net/patrons/226ea3a4617749548580ec2d9a263e24.jpg',
|
||||
}, {
|
||||
name: 'フランギ・シュウ',
|
||||
icon: 'https://misskey-hub.net/patrons/3016d37e35f3430b90420176c912d304.jpg',
|
||||
}];
|
||||
|
||||
const patrons = [
|
||||
|
@ -276,6 +279,7 @@ const patrons = [
|
|||
'ぷーざ',
|
||||
'越貝鯛丸',
|
||||
'Nick / pprmint.',
|
||||
'kino3277',
|
||||
];
|
||||
|
||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<MkUserCardMini :user="file.user"/>
|
||||
</MkA>
|
||||
<div>
|
||||
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">NSFW</MkSwitch>
|
||||
<MkSwitch v-model="isSensitive" @update:modelValue="toggleIsSensitive">{{ i18n.ts.sensitive }}</MkSwitch>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
|
|
|
@ -40,7 +40,7 @@
|
|||
</div>
|
||||
<div v-if="expandedItems.includes(item.id)" :class="$style.userItemSub">
|
||||
<div>Assigned: <MkTime :time="item.createdAt" mode="detail"/></div>
|
||||
<div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div>
|
||||
<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
|
||||
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<MkButton rounded style="margin: 0 auto;" @click="changeImage">{{ i18n.ts.selectFile }}</MkButton>
|
||||
<MkInput v-model="name">
|
||||
<MkInput v-model="name" pattern="[a-z0-9_]">
|
||||
<template #label>{{ i18n.ts.name }}</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="category" :datalist="customEmojiCategories">
|
||||
|
@ -70,6 +70,7 @@
|
|||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import * as misskey from 'misskey-js';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
@ -95,7 +96,7 @@ let isSensitive = $ref(props.emoji ? props.emoji.isSensitive : false);
|
|||
let localOnly = $ref(props.emoji ? props.emoji.localOnly : false);
|
||||
let roleIdsThatCanBeUsedThisEmojiAsReaction = $ref(props.emoji ? props.emoji.roleIdsThatCanBeUsedThisEmojiAsReaction : []);
|
||||
let rolesThatCanBeUsedThisEmojiAsReaction = $ref([]);
|
||||
let file = $ref();
|
||||
let file = $ref<misskey.entities.DriveFile>();
|
||||
|
||||
watch($$(roleIdsThatCanBeUsedThisEmojiAsReaction), async () => {
|
||||
rolesThatCanBeUsedThisEmojiAsReaction = (await Promise.all(roleIdsThatCanBeUsedThisEmojiAsReaction.map((id) => os.api('admin/roles/show', { roleId: id }).catch(() => null)))).filter(x => x != null);
|
||||
|
@ -110,6 +111,10 @@ const emit = defineEmits<{
|
|||
|
||||
async function changeImage(ev) {
|
||||
file = await selectFile(ev.currentTarget ?? ev.target, null);
|
||||
const candidate = file.name.replace(/\.(.+)$/, '');
|
||||
if (candidate.match(/^[a-z0-9_]+$/)) {
|
||||
name = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
async function addRole() {
|
||||
|
|
|
@ -33,7 +33,7 @@ import MkTextarea from '@/components/MkTextarea.vue';
|
|||
import MkInput from '@/components/MkInput.vue';
|
||||
import { useRouter } from '@/router';
|
||||
|
||||
const PRESET_DEFAULT = `/// @ 0.14.0
|
||||
const PRESET_DEFAULT = `/// @ 0.15.0
|
||||
|
||||
var name = ""
|
||||
|
||||
|
@ -51,7 +51,7 @@ Ui:render([
|
|||
])
|
||||
`;
|
||||
|
||||
const PRESET_OMIKUJI = `/// @ 0.14.0
|
||||
const PRESET_OMIKUJI = `/// @ 0.15.0
|
||||
// ユーザーごとに日替わりのおみくじのプリセット
|
||||
|
||||
// 選択肢
|
||||
|
@ -94,7 +94,7 @@ Ui:render([
|
|||
])
|
||||
`;
|
||||
|
||||
const PRESET_SHUFFLE = `/// @ 0.14.0
|
||||
const PRESET_SHUFFLE = `/// @ 0.15.0
|
||||
// 巻き戻し可能な文字シャッフルのプリセット
|
||||
|
||||
let string = "ペペロンチーノ"
|
||||
|
@ -173,7 +173,7 @@ var cursor = 0
|
|||
do()
|
||||
`;
|
||||
|
||||
const PRESET_QUIZ = `/// @ 0.14.0
|
||||
const PRESET_QUIZ = `/// @ 0.15.0
|
||||
let title = '地理クイズ'
|
||||
|
||||
let qas = [{
|
||||
|
@ -286,7 +286,7 @@ qaEls.push(Ui:C:container({
|
|||
Ui:render(qaEls)
|
||||
`;
|
||||
|
||||
const PRESET_TIMELINE = `/// @ 0.14.0
|
||||
const PRESET_TIMELINE = `/// @ 0.15.0
|
||||
// APIリクエストを行いローカルタイムラインを表示するプリセット
|
||||
|
||||
@fetch() {
|
||||
|
|
|
@ -236,6 +236,7 @@ definePageMetadata(computed(() => post ? {
|
|||
border-top: solid 0.5px var(--divider);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
|
||||
> .avatar {
|
||||
width: 52px;
|
||||
|
|
|
@ -55,7 +55,7 @@
|
|||
</div>
|
||||
<div v-if="expandedMuteItems.includes(item.id)" :class="$style.userItemSub">
|
||||
<div>Muted at: <MkTime :time="item.createdAt" mode="detail"/></div>
|
||||
<div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div>
|
||||
<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
|
||||
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -85,7 +85,7 @@
|
|||
</div>
|
||||
<div v-if="expandedBlockItems.includes(item.id)" :class="$style.userItemSub">
|
||||
<div>Blocked at: <MkTime :time="item.createdAt" mode="detail"/></div>
|
||||
<div v-if="item.expiresAt">Period: {{ item.expiresAt.toLocaleString() }}</div>
|
||||
<div v-if="item.expiresAt">Period: {{ new Date(item.expiresAt).toLocaleString() }}</div>
|
||||
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -112,10 +112,18 @@
|
|||
<MkButton v-if="user.host == null && iAmModerator" primary rounded @click="assignRole"><i class="ti ti-plus"></i> {{ i18n.ts.assign }}</MkButton>
|
||||
|
||||
<div v-for="role in info.roles" :key="role.id" :class="$style.roleItem">
|
||||
<div :class="$style.roleItemMain">
|
||||
<MkRolePreview :class="$style.role" :role="role" :forModeration="true"/>
|
||||
<button class="_button" :class="$style.roleToggle" @click="toggleRoleItem(role)"><i class="ti ti-chevron-down"></i></button>
|
||||
<button v-if="role.target === 'manual'" class="_button" :class="$style.roleUnassign" @click="unassignRole(role, $event)"><i class="ti ti-x"></i></button>
|
||||
<button v-else class="_button" :class="$style.roleUnassign" disabled><i class="ti ti-ban"></i></button>
|
||||
</div>
|
||||
<div v-if="expandedRoles.includes(role.id)" :class="$style.roleItemSub">
|
||||
<div>Assigned: <MkTime :time="info.roleAssigns.find(a => a.roleId === role.id).createdAt" mode="detail"/></div>
|
||||
<div v-if="info.roleAssigns.find(a => a.roleId === role.id).expiresAt">Period: {{ new Date(info.roleAssigns.find(a => a.roleId === role.id).expiresAt).toLocaleString() }}</div>
|
||||
<div v-else>Period: {{ i18n.ts.indefinitely }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</MkFolder>
|
||||
|
||||
|
@ -220,6 +228,7 @@ const filesPagination = {
|
|||
userId: props.userId,
|
||||
})),
|
||||
};
|
||||
let expandedRoles = $ref([]);
|
||||
|
||||
function createFetcher() {
|
||||
if (iAmModerator) {
|
||||
|
@ -384,6 +393,14 @@ async function unassignRole(role, ev) {
|
|||
}], ev.currentTarget ?? ev.target);
|
||||
}
|
||||
|
||||
function toggleRoleItem(role) {
|
||||
if (expandedRoles.includes(role.id)) {
|
||||
expandedRoles = expandedRoles.filter(x => x !== role.id);
|
||||
} else {
|
||||
expandedRoles.push(role.id);
|
||||
}
|
||||
}
|
||||
|
||||
watch(() => props.userId, () => {
|
||||
init = createFetcher();
|
||||
}, {
|
||||
|
@ -523,11 +540,22 @@ definePageMetadata(computed(() => ({
|
|||
}
|
||||
|
||||
.roleItem {
|
||||
}
|
||||
|
||||
.roleItemMain {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.role {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.roleItemSub {
|
||||
padding: 6px 12px;
|
||||
font-size: 85%;
|
||||
color: var(--fgTransparentWeak);
|
||||
}
|
||||
|
||||
.roleUnassign {
|
||||
|
|
19
packages/frontend/src/scripts/collapsed.ts
Normal file
19
packages/frontend/src/scripts/collapsed.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
import * as mfm from 'mfm-js';
|
||||
import * as misskey from 'misskey-js';
|
||||
import { extractUrlFromMfm } from './extract-url-from-mfm';
|
||||
|
||||
export function shouldCollapsed(note: misskey.entities.Note): boolean {
|
||||
const urls = note.text ? extractUrlFromMfm(mfm.parse(note.text)) : null;
|
||||
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) ||
|
||||
(note.files.length >= 5) ||
|
||||
(!!urls && urls.length >= 4)
|
||||
);
|
||||
|
||||
return collapsed;
|
||||
}
|
2856
pnpm-lock.yaml
2856
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue