Merge branch 'develop' into enhance-migration
This commit is contained in:
commit
8be2aacb7c
|
@ -1,12 +1,21 @@
|
|||
'⚙️Server':
|
||||
'packages/backend':
|
||||
- packages/backend/**/*
|
||||
|
||||
'🖥️Client':
|
||||
- packages/frontend/**/*
|
||||
|
||||
'🧪Test':
|
||||
- cypress/**/*
|
||||
'packages/backend:test':
|
||||
- packages/backend/test/**/*
|
||||
|
||||
'‼️ wrong locales':
|
||||
- any: ['locales/*.yml', '!locales/ja-JP.yml']
|
||||
'packages/frontend':
|
||||
- packages/frontend/**/*
|
||||
|
||||
'packages/frontend:test':
|
||||
- cypress/**/*
|
||||
|
||||
'packages/sw':
|
||||
- packages/sw/**/*
|
||||
|
||||
'packages/misskey-js':
|
||||
- packages/misskey-js/**/*
|
||||
|
||||
'packages/misskey-js:test':
|
||||
- packages/misskey-js/test/**/*
|
||||
- packages/misskey-js/test-d/**/*
|
||||
|
|
|
@ -19,5 +19,6 @@ https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md
|
|||
## Checklist
|
||||
- [ ] Read the [contribution guide](https://github.com/misskey-dev/misskey/blob/develop/CONTRIBUTING.md)
|
||||
- [ ] Test working in a local environment
|
||||
- [ ] (If needed) Add story of storybook
|
||||
- [ ] (If needed) Update CHANGELOG.md
|
||||
- [ ] (If possible) Add tests
|
||||
|
|
|
@ -106,7 +106,7 @@ jobs:
|
|||
install: false
|
||||
start: pnpm start:test
|
||||
wait-on: 'http://localhost:61812'
|
||||
headless: false
|
||||
headed: true
|
||||
browser: ${{ matrix.browser }}
|
||||
- uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
### Client
|
||||
-
|
||||
- カスタム絵文字のライセンスを複数でセットできるようになりました。
|
||||
|
||||
### Server
|
||||
-
|
||||
|
@ -17,13 +18,14 @@
|
|||
- Node.js 18.6.0以上が必要になりました
|
||||
|
||||
### General
|
||||
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
|
||||
* 一度引っ越したアカウントは利用に制限がかかります
|
||||
- 新規登録前に簡潔なルールをユーザーに表示できる、サーバールール機能を追加
|
||||
- ユーザーへの自分用メモ機能
|
||||
* ユーザーに対して、自分だけが見られるメモを追加できるようになりました。
|
||||
(自分自身に対してもメモを追加できます。)
|
||||
* ユーザーメニューから追加できます。
|
||||
(デスクトップ表示ではusernameの右側のボタンからも追加可能)
|
||||
- アカウントの引っ越し(フォロワー引き継ぎ)に対応
|
||||
* 一度引っ越したアカウントは利用に制限がかかります
|
||||
|
||||
### Client
|
||||
- 通知の表示をカスタマイズできるように
|
||||
|
|
|
@ -52,6 +52,12 @@ describe('After setup instance', () => {
|
|||
cy.intercept('POST', '/api/signup').as('signup');
|
||||
|
||||
cy.get('[data-cy-signup]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-rules-notes] [data-cy-folder-header]').click();
|
||||
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
||||
cy.get('[data-cy-signup-rules-continue]').click();
|
||||
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
cy.get('[data-cy-signup-username] input').type('alice');
|
||||
cy.get('[data-cy-signup-submit]').should('be.disabled');
|
||||
|
@ -71,6 +77,12 @@ 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] [data-cy-folder-header]').click();
|
||||
cy.get('[data-cy-signup-rules-notes-agree] [data-cy-switch-toggle]').click();
|
||||
cy.get('[data-cy-signup-rules-continue]').should('not.be.disabled');
|
||||
cy.get('[data-cy-signup-rules-continue]').click();
|
||||
|
||||
cy.get('[data-cy-signup-username] input').type('alice');
|
||||
cy.get('[data-cy-signup-password] input').type('alice1234');
|
||||
cy.get('[data-cy-signup-password-retype] input').type('alice1234');
|
||||
|
|
|
@ -263,9 +263,10 @@ noMoreHistory: "これより過去の履歴はありません"
|
|||
startMessaging: "チャットを開始"
|
||||
nUsersRead: "{n}人が読みました"
|
||||
agreeTo: "{0}に同意"
|
||||
agree: "同意する"
|
||||
agreeBelow: "下記に同意する"
|
||||
basicNotesBeforeCreateAccount: "基本的な注意事項"
|
||||
tos: "利用規約"
|
||||
termsOfService: "利用規約"
|
||||
start: "始める"
|
||||
home: "ホーム"
|
||||
remoteUserCaution: "リモートユーザーのため、情報が不完全です。"
|
||||
|
@ -1010,6 +1011,12 @@ stackAxis: "スタック方向"
|
|||
vertical: "縦"
|
||||
horizontal: "横"
|
||||
position: "位置"
|
||||
serverRules: "サーバールール"
|
||||
pleaseConfirmBelowBeforeSignup: "このサーバーに登録する前に、以下を確認してください。"
|
||||
continue: "続ける"
|
||||
|
||||
_serverRules:
|
||||
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
||||
|
||||
_accountMigration:
|
||||
moveTo: "このアカウントを新しいアカウントに引っ越す"
|
||||
|
|
12
package.json
12
package.json
|
@ -51,19 +51,19 @@
|
|||
"gulp-replace": "1.1.4",
|
||||
"gulp-terser": "2.1.0",
|
||||
"js-yaml": "4.1.0",
|
||||
"typescript": "5.0.3"
|
||||
"typescript": "5.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.0",
|
||||
"@typescript-eslint/parser": "5.59.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.9.0",
|
||||
"eslint": "8.37.0",
|
||||
"cypress": "12.10.0",
|
||||
"eslint": "8.38.0",
|
||||
"start-server-and-test": "2.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@tensorflow/tfjs-core": "4.2.0"
|
||||
"@tensorflow/tfjs-core": "4.4.0"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
export class ServerRules1681400427971 {
|
||||
name = 'ServerRules1681400427971'
|
||||
|
||||
async up(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" ADD "serverRules" character varying(280) array NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
|
||||
async down(queryRunner) {
|
||||
await queryRunner.query(`ALTER TABLE "meta" DROP COLUMN "serverRules"`);
|
||||
}
|
||||
}
|
|
@ -23,33 +23,33 @@
|
|||
},
|
||||
"optionalDependencies": {
|
||||
"@swc/core-android-arm64": "1.3.11",
|
||||
"@swc/core-darwin-arm64": "1.3.46",
|
||||
"@swc/core-darwin-x64": "1.3.46",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.46",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.46",
|
||||
"@swc/core-linux-arm64-musl": "1.3.46",
|
||||
"@swc/core-linux-x64-gnu": "1.3.46",
|
||||
"@swc/core-linux-x64-musl": "1.3.46",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.46",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.46",
|
||||
"@swc/core-win32-x64-msvc": "1.3.46",
|
||||
"@tensorflow/tfjs": "4.2.0",
|
||||
"@tensorflow/tfjs-node": "4.2.0"
|
||||
"@swc/core-darwin-arm64": "1.3.51",
|
||||
"@swc/core-darwin-x64": "1.3.51",
|
||||
"@swc/core-linux-arm-gnueabihf": "1.3.51",
|
||||
"@swc/core-linux-arm64-gnu": "1.3.51",
|
||||
"@swc/core-linux-arm64-musl": "1.3.51",
|
||||
"@swc/core-linux-x64-gnu": "1.3.51",
|
||||
"@swc/core-linux-x64-musl": "1.3.51",
|
||||
"@swc/core-win32-arm64-msvc": "1.3.51",
|
||||
"@swc/core-win32-ia32-msvc": "1.3.51",
|
||||
"@swc/core-win32-x64-msvc": "1.3.51",
|
||||
"@tensorflow/tfjs": "4.4.0",
|
||||
"@tensorflow/tfjs-node": "4.4.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@aws-sdk/client-s3": "3.306.0",
|
||||
"@aws-sdk/lib-storage": "3.306.0",
|
||||
"@aws-sdk/node-http-handler": "3.306.0",
|
||||
"@bull-board/api": "5.0.0",
|
||||
"@bull-board/fastify": "5.0.0",
|
||||
"@bull-board/ui": "5.0.0",
|
||||
"@aws-sdk/client-s3": "3.315.0",
|
||||
"@aws-sdk/lib-storage": "3.315.0",
|
||||
"@aws-sdk/node-http-handler": "3.310.0",
|
||||
"@bull-board/api": "5.0.1",
|
||||
"@bull-board/fastify": "5.0.1",
|
||||
"@bull-board/ui": "5.0.1",
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@fastify/accepts": "4.1.0",
|
||||
"@fastify/cookie": "8.3.0",
|
||||
"@fastify/cors": "8.2.1",
|
||||
"@fastify/http-proxy": "9.0.0",
|
||||
"@fastify/multipart": "7.5.0",
|
||||
"@fastify/static": "6.10.0",
|
||||
"@fastify/multipart": "7.6.0",
|
||||
"@fastify/static": "6.10.1",
|
||||
"@fastify/view": "7.4.1",
|
||||
"@nestjs/common": "9.4.0",
|
||||
"@nestjs/core": "9.4.0",
|
||||
|
@ -57,7 +57,7 @@
|
|||
"@peertube/http-signature": "1.7.0",
|
||||
"@sinonjs/fake-timers": "10.0.2",
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.46",
|
||||
"@swc/core": "1.3.51",
|
||||
"accepts": "1.3.8",
|
||||
"ajv": "8.12.0",
|
||||
"archiver": "5.3.1",
|
||||
|
@ -82,16 +82,16 @@
|
|||
"fluent-ffmpeg": "2.1.2",
|
||||
"form-data": "4.0.0",
|
||||
"got": "12.6.0",
|
||||
"happy-dom": "8.9.0",
|
||||
"happy-dom": "9.8.2",
|
||||
"hpagent": "1.2.0",
|
||||
"ioredis": "5.3.1",
|
||||
"ioredis": "5.3.2",
|
||||
"ip-cidr": "3.1.0",
|
||||
"is-svg": "4.3.2",
|
||||
"js-yaml": "4.1.0",
|
||||
"jsdom": "21.1.1",
|
||||
"json5": "2.2.3",
|
||||
"jsonld": "8.1.1",
|
||||
"jsrsasign": "10.7.0",
|
||||
"jsrsasign": "10.8.2",
|
||||
"mfm-js": "0.23.3",
|
||||
"mime-types": "2.1.35",
|
||||
"misskey-js": "workspace:*",
|
||||
|
@ -119,7 +119,7 @@
|
|||
"reflect-metadata": "0.1.13",
|
||||
"rename": "1.0.4",
|
||||
"rndstr": "1.0.0",
|
||||
"rss-parser": "3.12.0",
|
||||
"rss-parser": "3.13.0",
|
||||
"rxjs": "7.8.0",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.10.0",
|
||||
|
@ -136,8 +136,8 @@
|
|||
"tsc-alias": "1.8.5",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typeorm": "0.3.13",
|
||||
"typescript": "5.0.3",
|
||||
"typeorm": "0.3.15",
|
||||
"typescript": "5.0.4",
|
||||
"ulid": "2.3.0",
|
||||
"unzipper": "0.10.11",
|
||||
"uuid": "9.0.0",
|
||||
|
@ -149,7 +149,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@jest/globals": "29.5.0",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@swc/jest": "0.2.26",
|
||||
"@types/accepts": "1.3.5",
|
||||
"@types/archiver": "5.3.2",
|
||||
"@types/bcryptjs": "2.4.2",
|
||||
|
@ -189,11 +189,11 @@
|
|||
"@types/web-push": "3.3.2",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.0",
|
||||
"@typescript-eslint/parser": "5.59.0",
|
||||
"aws-sdk-client-mock": "^2.1.1",
|
||||
"cross-env": "7.0.3",
|
||||
"eslint": "8.37.0",
|
||||
"eslint": "8.38.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"execa": "6.1.0",
|
||||
"jest": "29.5.0",
|
||||
|
|
|
@ -197,6 +197,22 @@ export class CustomEmojiService {
|
|||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async setLicenseBulk(ids: Emoji['id'][], license: string | null) {
|
||||
await this.emojisRepository.update({
|
||||
id: In(ids),
|
||||
}, {
|
||||
updatedAt: new Date(),
|
||||
license: license,
|
||||
});
|
||||
|
||||
this.localEmojisCache.refresh();
|
||||
|
||||
this.globalEventService.publishBroadcastStream('emojiUpdated', {
|
||||
emojis: await this.emojiEntityService.packDetailedMany(ids),
|
||||
});
|
||||
}
|
||||
|
||||
@bindThis
|
||||
public async delete(id: Emoji['id']) {
|
||||
|
|
|
@ -405,4 +405,11 @@ export class Meta {
|
|||
default: { },
|
||||
})
|
||||
public policies: Record<string, any>;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 280,
|
||||
array: true,
|
||||
default: '{}',
|
||||
})
|
||||
public serverRules: string[];
|
||||
}
|
||||
|
|
|
@ -29,6 +29,7 @@ import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
|
|||
import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
|
||||
import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
|
||||
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
|
||||
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
|
||||
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
|
||||
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
|
||||
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
|
||||
|
@ -365,6 +366,7 @@ const $admin_emoji_list: Provider = { provide: 'ep:admin/emoji/list', useClass:
|
|||
const $admin_emoji_removeAliasesBulk: Provider = { provide: 'ep:admin/emoji/remove-aliases-bulk', useClass: ep___admin_emoji_removeAliasesBulk.default };
|
||||
const $admin_emoji_setAliasesBulk: Provider = { provide: 'ep:admin/emoji/set-aliases-bulk', useClass: ep___admin_emoji_setAliasesBulk.default };
|
||||
const $admin_emoji_setCategoryBulk: Provider = { provide: 'ep:admin/emoji/set-category-bulk', useClass: ep___admin_emoji_setCategoryBulk.default };
|
||||
const $admin_emoji_setLicenseBulk: Provider = { provide: 'ep:admin/emoji/set-license-bulk', useClass: ep___admin_emoji_setLicenseBulk.default };
|
||||
const $admin_emoji_update: Provider = { provide: 'ep:admin/emoji/update', useClass: ep___admin_emoji_update.default };
|
||||
const $admin_federation_deleteAllFiles: Provider = { provide: 'ep:admin/federation/delete-all-files', useClass: ep___admin_federation_deleteAllFiles.default };
|
||||
const $admin_federation_refreshRemoteInstanceMetadata: Provider = { provide: 'ep:admin/federation/refresh-remote-instance-metadata', useClass: ep___admin_federation_refreshRemoteInstanceMetadata.default };
|
||||
|
@ -705,6 +707,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$admin_emoji_removeAliasesBulk,
|
||||
$admin_emoji_setAliasesBulk,
|
||||
$admin_emoji_setCategoryBulk,
|
||||
$admin_emoji_setLicenseBulk,
|
||||
$admin_emoji_update,
|
||||
$admin_federation_deleteAllFiles,
|
||||
$admin_federation_refreshRemoteInstanceMetadata,
|
||||
|
@ -1039,6 +1042,7 @@ const $retention: Provider = { provide: 'ep:retention', useClass: ep___retention
|
|||
$admin_emoji_removeAliasesBulk,
|
||||
$admin_emoji_setAliasesBulk,
|
||||
$admin_emoji_setCategoryBulk,
|
||||
$admin_emoji_setLicenseBulk,
|
||||
$admin_emoji_update,
|
||||
$admin_federation_deleteAllFiles,
|
||||
$admin_federation_refreshRemoteInstanceMetadata,
|
||||
|
|
|
@ -29,6 +29,7 @@ import * as ep___admin_emoji_list from './endpoints/admin/emoji/list.js';
|
|||
import * as ep___admin_emoji_removeAliasesBulk from './endpoints/admin/emoji/remove-aliases-bulk.js';
|
||||
import * as ep___admin_emoji_setAliasesBulk from './endpoints/admin/emoji/set-aliases-bulk.js';
|
||||
import * as ep___admin_emoji_setCategoryBulk from './endpoints/admin/emoji/set-category-bulk.js';
|
||||
import * as ep___admin_emoji_setLicenseBulk from './endpoints/admin/emoji/set-license-bulk.js';
|
||||
import * as ep___admin_emoji_update from './endpoints/admin/emoji/update.js';
|
||||
import * as ep___admin_federation_deleteAllFiles from './endpoints/admin/federation/delete-all-files.js';
|
||||
import * as ep___admin_federation_refreshRemoteInstanceMetadata from './endpoints/admin/federation/refresh-remote-instance-metadata.js';
|
||||
|
@ -363,6 +364,7 @@ const eps = [
|
|||
['admin/emoji/remove-aliases-bulk', ep___admin_emoji_removeAliasesBulk],
|
||||
['admin/emoji/set-aliases-bulk', ep___admin_emoji_setAliasesBulk],
|
||||
['admin/emoji/set-category-bulk', ep___admin_emoji_setCategoryBulk],
|
||||
['admin/emoji/set-license-bulk', ep___admin_emoji_setLicenseBulk],
|
||||
['admin/emoji/update', ep___admin_emoji_update],
|
||||
['admin/federation/delete-all-files', ep___admin_federation_deleteAllFiles],
|
||||
['admin/federation/refresh-remote-instance-metadata', ep___admin_federation_refreshRemoteInstanceMetadata],
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { Endpoint } from '@/server/api/endpoint-base.js';
|
||||
import { CustomEmojiService } from '@/core/CustomEmojiService.js';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireRolePolicy: 'canManageCustomEmojis',
|
||||
} as const;
|
||||
|
||||
export const paramDef = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
ids: { type: 'array', items: {
|
||||
type: 'string', format: 'misskey:id',
|
||||
} },
|
||||
license: {
|
||||
type: 'string',
|
||||
nullable: true,
|
||||
description: 'Use `null` to reset the license.',
|
||||
},
|
||||
},
|
||||
required: ['ids'],
|
||||
} as const;
|
||||
|
||||
// eslint-disable-next-line import/no-default-export
|
||||
@Injectable()
|
||||
export default class extends Endpoint<typeof meta, typeof paramDef> {
|
||||
constructor(
|
||||
private customEmojiService: CustomEmojiService,
|
||||
) {
|
||||
super(meta, paramDef, async (ps, me) => {
|
||||
await this.customEmojiService.setLicenseBulk(ps.ids, ps.license ?? null);
|
||||
});
|
||||
}
|
||||
}
|
|
@ -94,6 +94,7 @@ export const paramDef = {
|
|||
enableActiveEmailValidation: { type: 'boolean' },
|
||||
enableChartsForRemoteUser: { type: 'boolean' },
|
||||
enableChartsForFederatedInstances: { type: 'boolean' },
|
||||
serverRules: { type: 'array', items: { type: 'string' } },
|
||||
},
|
||||
required: [],
|
||||
} as const;
|
||||
|
@ -387,6 +388,10 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
set.enableChartsForFederatedInstances = ps.enableChartsForFederatedInstances;
|
||||
}
|
||||
|
||||
if (ps.serverRules !== undefined) {
|
||||
set.serverRules = ps.serverRules;
|
||||
}
|
||||
|
||||
await this.metaService.update(set);
|
||||
this.moderationLogService.insertModerationLog(me, 'updateMeta');
|
||||
});
|
||||
|
|
|
@ -310,6 +310,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
|
|||
|
||||
translatorAvailable: instance.deeplAuthKey != null,
|
||||
|
||||
serverRules: instance.serverRules,
|
||||
|
||||
policies: { ...DEFAULT_POLICIES, ...instance.policies },
|
||||
|
||||
mediaProxy: this.config.mediaProxy,
|
||||
|
|
|
@ -398,6 +398,7 @@ Promise.all([
|
|||
glob('src/components/global/*.vue'),
|
||||
glob('src/components/Mk{A,B}*.vue'),
|
||||
glob('src/components/MkGalleryPostPreview.vue'),
|
||||
glob('src/components/MkSignupServerRules.vue'),
|
||||
glob('src/pages/user/home.vue'),
|
||||
])
|
||||
.then((globs) => globs.flat())
|
||||
|
|
|
@ -15,12 +15,12 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"@discordapp/twemoji": "14.1.2",
|
||||
"@rollup/plugin-alias": "4.0.3",
|
||||
"@rollup/plugin-alias": "5.0.0",
|
||||
"@rollup/plugin-json": "6.0.0",
|
||||
"@rollup/plugin-replace": "^5.0.2",
|
||||
"@rollup/pluginutils": "5.0.2",
|
||||
"@syuilo/aiscript": "0.13.1",
|
||||
"@tabler/icons-webfont": "2.12.0",
|
||||
"@tabler/icons-webfont": "2.16.0",
|
||||
"@vitejs/plugin-vue": "4.1.0",
|
||||
"@vue/compiler-sfc": "3.2.47",
|
||||
"autosize": "5.0.2",
|
||||
|
@ -52,10 +52,10 @@
|
|||
"punycode": "2.3.0",
|
||||
"querystring": "0.2.1",
|
||||
"rndstr": "1.0.0",
|
||||
"rollup": "3.20.2",
|
||||
"rollup": "3.20.6",
|
||||
"s-age": "1.1.2",
|
||||
"sanitize-html": "2.10.0",
|
||||
"sass": "1.60.0",
|
||||
"sass": "1.62.0",
|
||||
"seedrandom": "3.0.5",
|
||||
"strict-event-emitter-types": "2.0.0",
|
||||
"syuilo-password-strength": "0.0.1",
|
||||
|
@ -66,34 +66,34 @@
|
|||
"tsc-alias": "1.8.5",
|
||||
"tsconfig-paths": "4.2.0",
|
||||
"twemoji-parser": "14.0.0",
|
||||
"typescript": "5.0.3",
|
||||
"typescript": "5.0.4",
|
||||
"uuid": "9.0.0",
|
||||
"vanilla-tilt": "1.8.0",
|
||||
"vite": "4.2.1",
|
||||
"vite": "4.2.2",
|
||||
"vue": "3.2.47",
|
||||
"vue-plyr": "7.0.0",
|
||||
"vue-prism-editor": "2.0.0-alpha.2",
|
||||
"vuedraggable": "next"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addon-actions": "7.0.2",
|
||||
"@storybook/addon-essentials": "7.0.2",
|
||||
"@storybook/addon-interactions": "7.0.2",
|
||||
"@storybook/addon-links": "7.0.2",
|
||||
"@storybook/addon-storysource": "7.0.2",
|
||||
"@storybook/addons": "7.0.2",
|
||||
"@storybook/blocks": "7.0.2",
|
||||
"@storybook/core-events": "7.0.2",
|
||||
"@storybook/addon-actions": "7.0.6",
|
||||
"@storybook/addon-essentials": "7.0.6",
|
||||
"@storybook/addon-interactions": "7.0.6",
|
||||
"@storybook/addon-links": "7.0.6",
|
||||
"@storybook/addon-storysource": "7.0.6",
|
||||
"@storybook/addons": "7.0.6",
|
||||
"@storybook/blocks": "7.0.6",
|
||||
"@storybook/core-events": "7.0.6",
|
||||
"@storybook/jest": "0.1.0",
|
||||
"@storybook/manager-api": "7.0.2",
|
||||
"@storybook/preview-api": "7.0.2",
|
||||
"@storybook/react": "7.0.2",
|
||||
"@storybook/react-vite": "7.0.2",
|
||||
"@storybook/testing-library": "0.0.14-next.1",
|
||||
"@storybook/theming": "7.0.2",
|
||||
"@storybook/manager-api": "7.0.6",
|
||||
"@storybook/preview-api": "7.0.6",
|
||||
"@storybook/react": "7.0.6",
|
||||
"@storybook/react-vite": "7.0.6",
|
||||
"@storybook/testing-library": "0.1.0",
|
||||
"@storybook/theming": "7.0.6",
|
||||
"@storybook/types": "7.0.2",
|
||||
"@storybook/vue3": "7.0.2",
|
||||
"@storybook/vue3-vite": "7.0.2",
|
||||
"@storybook/vue3": "7.0.6",
|
||||
"@storybook/vue3-vite": "7.0.6",
|
||||
"@testing-library/jest-dom": "5.16.5",
|
||||
"@testing-library/vue": "7.0.0",
|
||||
"@types/escape-regexp": "0.0.1",
|
||||
|
@ -101,7 +101,7 @@
|
|||
"@types/gulp": "4.0.10",
|
||||
"@types/gulp-rename": "2.0.1",
|
||||
"@types/matter-js": "0.18.2",
|
||||
"@types/micromatch": "3.1.1",
|
||||
"@types/micromatch": "4.0.2",
|
||||
"@types/node": "18.15.11",
|
||||
"@types/punycode": "2.1.0",
|
||||
"@types/sanitize-html": "2.9.0",
|
||||
|
@ -112,19 +112,19 @@
|
|||
"@types/uuid": "9.0.1",
|
||||
"@types/websocket": "1.0.5",
|
||||
"@types/ws": "8.5.4",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.0",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"@vitest/coverage-c8": "^0.29.8",
|
||||
"@vitest/coverage-c8": "0.30.1",
|
||||
"@vue/runtime-core": "3.2.47",
|
||||
"astring": "1.8.4",
|
||||
"chokidar-cli": "3.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"cypress": "12.9.0",
|
||||
"cypress": "12.10.0",
|
||||
"eslint": "8.37.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"eslint-plugin-vue": "9.10.0",
|
||||
"eslint-plugin-vue": "9.11.0",
|
||||
"fast-glob": "3.2.12",
|
||||
"happy-dom": "8.9.0",
|
||||
"happy-dom": "9.8.2",
|
||||
"micromatch": "3.1.10",
|
||||
"msw": "1.2.1",
|
||||
"msw-storybook-addon": "1.8.0",
|
||||
|
@ -132,11 +132,11 @@
|
|||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"start-server-and-test": "2.0.0",
|
||||
"storybook": "7.0.2",
|
||||
"storybook": "7.0.6",
|
||||
"storybook-addon-misskey-theme": "github:misskey-dev/storybook-addon-misskey-theme",
|
||||
"summaly": "github:misskey-dev/summaly",
|
||||
"vite-plugin-turbosnap": "^1.0.1",
|
||||
"vitest": "0.29.8",
|
||||
"vitest": "0.30.1",
|
||||
"vitest-fetch-mock": "0.2.2",
|
||||
"vue-eslint-parser": "9.1.1",
|
||||
"vue-tsc": "1.2.0"
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
<template>
|
||||
<div ref="rootEl" :class="$style.root">
|
||||
<div ref="rootEl" :class="$style.root" role="group" :aria-expanded="opened">
|
||||
<MkStickyContainer>
|
||||
<template #header>
|
||||
<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" @click="toggle">
|
||||
<div :class="[$style.header, { [$style.opened]: opened }]" class="_button" role="button" data-cy-folder-header @click="toggle">
|
||||
<div :class="$style.headerIcon"><slot name="icon"></slot></div>
|
||||
<div :class="$style.headerText">
|
||||
<div :class="$style.headerTextMain">
|
||||
|
@ -20,7 +20,7 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }">
|
||||
<div v-if="openedAtLeastOnce" :class="[$style.body, { [$style.bgSame]: bgSame }]" :style="{ maxHeight: maxHeight ? `${maxHeight}px` : null, overflow: maxHeight ? `auto` : null }" :aria-hidden="!opened">
|
||||
<Transition
|
||||
:enter-active-class="defaultStore.state.animation ? $style.transition_toggle_enterActive : ''"
|
||||
:leave-active-class="defaultStore.state.animation ? $style.transition_toggle_leaveActive : ''"
|
||||
|
@ -196,7 +196,7 @@ onMounted(() => {
|
|||
|
||||
.headerRight {
|
||||
margin-left: auto;
|
||||
opacity: 0.7;
|
||||
color: var(--fgTransparentWeak);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
|
|
@ -178,7 +178,7 @@ onBeforeUnmount(() => {
|
|||
}
|
||||
|
||||
&.active {
|
||||
color: #fff;
|
||||
color: var(--fgOnAccent);
|
||||
background: var(--accent);
|
||||
|
||||
&:hover {
|
||||
|
|
|
@ -404,16 +404,10 @@ defineExpose({
|
|||
right: 0;
|
||||
margin: auto;
|
||||
padding: 32px;
|
||||
// TODO: mask-imageはiOSだとやたら重い。なんとかしたい
|
||||
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
|
||||
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 32px, rgba(0,0,0,1) calc(100% - 32px), rgba(0,0,0,0) 100%);
|
||||
overflow: auto;
|
||||
display: flex;
|
||||
|
||||
@media (max-width: 500px) {
|
||||
padding: 16px;
|
||||
-webkit-mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
|
||||
mask-image: linear-gradient(0deg, rgba(0,0,0,0) 0%, rgba(0,0,0,1) 16px, rgba(0,0,0,1) calc(100% - 16px), rgba(0,0,0,0) 100%);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<MkModal ref="modal" :prefer-type="'dialog'" @click="onBgClick" @closed="$emit('closed')">
|
||||
<div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: scroll ? (height ? `${height}px` : null) : (height ? `min(${height}px, 100%)` : '100%') }" @keydown="onKeydown">
|
||||
<div ref="rootEl" class="ebkgoccj" :style="{ width: `${width}px`, height: height ? `${height}px` : null }" @keydown="onKeydown">
|
||||
<div ref="headerEl" class="header">
|
||||
<button v-if="withOkButton" class="_button" @click="$emit('close')"><i class="ti ti-x"></i></button>
|
||||
<span class="title">
|
||||
|
@ -25,13 +25,11 @@ const props = withDefaults(defineProps<{
|
|||
okButtonDisabled: boolean;
|
||||
width: number;
|
||||
height: number | null;
|
||||
scroll: boolean;
|
||||
}>(), {
|
||||
withOkButton: false,
|
||||
okButtonDisabled: false,
|
||||
width: 400,
|
||||
height: null,
|
||||
scroll: true,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
@ -86,6 +84,7 @@ defineExpose({
|
|||
<style lang="scss" scoped>
|
||||
.ebkgoccj {
|
||||
margin: auto;
|
||||
max-height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
|
|
@ -1,263 +0,0 @@
|
|||
<template>
|
||||
<form class="qlvuhzng _gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.invitationCode }}</template>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
|
||||
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-question-circle"></i></div></template>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
<template #caption>
|
||||
<div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div>
|
||||
<span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
|
||||
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
|
||||
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
|
||||
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
|
||||
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
|
||||
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
|
||||
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-question-circle"></i></div></template>
|
||||
<template #prefix><i class="ti ti-mail"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
|
||||
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
|
||||
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
|
||||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
|
||||
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
|
||||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
|
||||
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
|
||||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkSwitch v-model="ToSAgreement" class="tou">
|
||||
<template #label>{{ i18n.ts.agreeBelow }}</template>
|
||||
</MkSwitch>
|
||||
<ul style="margin: 0; padding-left: 2em;">
|
||||
<li v-if="instance.tosUrl"><a :href="instance.tosUrl" class="_link" target="_blank">{{ i18n.ts.tos }}</a></li>
|
||||
<li><a href="https://misskey-hub.net/docs/notes.html" class="_link" target="_blank">{{ i18n.ts.basicNotesBeforeCreateAccount }}</a></li>
|
||||
</ul>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" class="captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" class="captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" class="captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
<MkButton type="submit" :disabled="shouldDisableSubmitting" gradate data-cy-signup-submit>{{ i18n.ts.start }}</MkButton>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import getPasswordStrength from 'syuilo-password-strength';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
||||
import * as config from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { login } from '@/account';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
autoSet?: boolean;
|
||||
}>(), {
|
||||
autoSet: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'signup', user: Record<string, any>): void;
|
||||
(ev: 'signupEmailPending'): void;
|
||||
}>();
|
||||
|
||||
const host = toUnicode(config.host);
|
||||
|
||||
let hcaptcha = $ref<Captcha | undefined>();
|
||||
let recaptcha = $ref<Captcha | undefined>();
|
||||
let turnstile = $ref<Captcha | undefined>();
|
||||
|
||||
let username: string = $ref('');
|
||||
let password: string = $ref('');
|
||||
let retypedPassword: string = $ref('');
|
||||
let invitationCode: string = $ref('');
|
||||
let email = $ref('');
|
||||
let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
|
||||
let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
|
||||
let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref('');
|
||||
let passwordRetypeState: null | 'match' | 'not-match' = $ref(null);
|
||||
let submitting: boolean = $ref(false);
|
||||
let ToSAgreement: boolean = $ref(false);
|
||||
let hCaptchaResponse = $ref(null);
|
||||
let reCaptchaResponse = $ref(null);
|
||||
let turnstileResponse = $ref(null);
|
||||
let usernameAbortController: null | AbortController = $ref(null);
|
||||
let emailAbortController: null | AbortController = $ref(null);
|
||||
|
||||
const shouldDisableSubmitting = $computed((): boolean => {
|
||||
return submitting ||
|
||||
instance.tosUrl && !ToSAgreement ||
|
||||
instance.enableHcaptcha && !hCaptchaResponse ||
|
||||
instance.enableRecaptcha && !reCaptchaResponse ||
|
||||
instance.enableTurnstile && !turnstileResponse ||
|
||||
instance.emailRequiredForSignup && emailState !== 'ok' ||
|
||||
usernameState !== 'ok' ||
|
||||
passwordRetypeState !== 'match';
|
||||
});
|
||||
|
||||
function onChangeUsername(): void {
|
||||
if (username === '') {
|
||||
usernameState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
const err =
|
||||
!username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
|
||||
username.length < 1 ? 'min-range' :
|
||||
username.length > 20 ? 'max-range' :
|
||||
null;
|
||||
|
||||
if (err) {
|
||||
usernameState = err;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (usernameAbortController != null) {
|
||||
usernameAbortController.abort();
|
||||
}
|
||||
usernameState = 'wait';
|
||||
usernameAbortController = new AbortController();
|
||||
|
||||
os.api('username/available', {
|
||||
username,
|
||||
}, undefined, usernameAbortController.signal).then(result => {
|
||||
usernameState = result.available ? 'ok' : 'unavailable';
|
||||
}).catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
usernameState = 'error';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onChangeEmail(): void {
|
||||
if (email === '') {
|
||||
emailState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (emailAbortController != null) {
|
||||
emailAbortController.abort();
|
||||
}
|
||||
emailState = 'wait';
|
||||
emailAbortController = new AbortController();
|
||||
|
||||
os.api('email-address/available', {
|
||||
emailAddress: email,
|
||||
}, undefined, emailAbortController.signal).then(result => {
|
||||
emailState = result.available ? 'ok' :
|
||||
result.reason === 'used' ? 'unavailable:used' :
|
||||
result.reason === 'format' ? 'unavailable:format' :
|
||||
result.reason === 'disposable' ? 'unavailable:disposable' :
|
||||
result.reason === 'mx' ? 'unavailable:mx' :
|
||||
result.reason === 'smtp' ? 'unavailable:smtp' :
|
||||
'unavailable';
|
||||
}).catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
emailState = 'error';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onChangePassword(): void {
|
||||
if (password === '') {
|
||||
passwordStrength = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const strength = getPasswordStrength(password);
|
||||
passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||
}
|
||||
|
||||
function onChangePasswordRetype(): void {
|
||||
if (retypedPassword === '') {
|
||||
passwordRetypeState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
passwordRetypeState = password === retypedPassword ? 'match' : 'not-match';
|
||||
}
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
if (submitting) return;
|
||||
submitting = true;
|
||||
|
||||
try {
|
||||
await os.api('signup', {
|
||||
username,
|
||||
password,
|
||||
emailAddress: email,
|
||||
invitationCode,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'g-recaptcha-response': reCaptchaResponse,
|
||||
'turnstile-response': turnstileResponse,
|
||||
});
|
||||
if (instance.emailRequiredForSignup) {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.t('_signup.emailSent', { email }),
|
||||
});
|
||||
emit('signupEmailPending');
|
||||
} else {
|
||||
const res = await os.api('signin', {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
emit('signup', res);
|
||||
|
||||
if (props.autoSet) {
|
||||
return login(res.i);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
submitting = false;
|
||||
hcaptcha?.reset?.();
|
||||
recaptcha?.reset?.();
|
||||
turnstile?.reset?.();
|
||||
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.somethingHappened,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.qlvuhzng {
|
||||
.captcha {
|
||||
margin: 16px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,272 @@
|
|||
<template>
|
||||
<div>
|
||||
<div :class="$style.banner">
|
||||
<i class="ti ti-user-edit"></i>
|
||||
</div>
|
||||
<MkSpacer :margin-min="20" :margin-max="32">
|
||||
<form class="_gaps_m" autocomplete="new-password" @submit.prevent="onSubmit">
|
||||
<MkInput v-if="instance.disableRegistration" v-model="invitationCode" type="text" :spellcheck="false" required>
|
||||
<template #label>{{ i18n.ts.invitationCode }}</template>
|
||||
<template #prefix><i class="ti ti-key"></i></template>
|
||||
</MkInput>
|
||||
<MkInput v-model="username" type="text" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-signup-username @update:model-value="onChangeUsername">
|
||||
<template #label>{{ i18n.ts.username }} <div v-tooltip:dialog="i18n.ts.usernameInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
<template #caption>
|
||||
<div><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.cannotBeChangedLater }}</div>
|
||||
<span v-if="usernameState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
|
||||
<span v-else-if="usernameState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
|
||||
<span v-else-if="usernameState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
|
||||
<span v-else-if="usernameState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
<span v-else-if="usernameState === 'invalid-format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.usernameInvalidFormat }}</span>
|
||||
<span v-else-if="usernameState === 'min-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooShort }}</span>
|
||||
<span v-else-if="usernameState === 'max-range'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.tooLong }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-if="instance.emailRequiredForSignup" v-model="email" :debounce="true" type="email" :spellcheck="false" required data-cy-signup-email @update:model-value="onChangeEmail">
|
||||
<template #label>{{ i18n.ts.emailAddress }} <div v-tooltip:dialog="i18n.ts._signup.emailAddressInfo" class="_button _help"><i class="ti ti-help-circle"></i></div></template>
|
||||
<template #prefix><i class="ti ti-mail"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="emailState === 'wait'" style="color:#999"><MkLoading :em="true"/> {{ i18n.ts.checking }}</span>
|
||||
<span v-else-if="emailState === 'ok'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.available }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:used'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.used }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:format'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.format }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:disposable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.disposable }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:mx'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.mx }}</span>
|
||||
<span v-else-if="emailState === 'unavailable:smtp'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts._emailUnavailable.smtp }}</span>
|
||||
<span v-else-if="emailState === 'unavailable'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.unavailable }}</span>
|
||||
<span v-else-if="emailState === 'error'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.error }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="password" type="password" autocomplete="new-password" required data-cy-signup-password @update:model-value="onChangePassword">
|
||||
<template #label>{{ i18n.ts.password }}</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordStrength == 'low'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.weakPassword }}</span>
|
||||
<span v-if="passwordStrength == 'medium'" style="color: var(--warn)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.normalPassword }}</span>
|
||||
<span v-if="passwordStrength == 'high'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.strongPassword }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkInput v-model="retypedPassword" type="password" autocomplete="new-password" required data-cy-signup-password-retype @update:model-value="onChangePasswordRetype">
|
||||
<template #label>{{ i18n.ts.password }} ({{ i18n.ts.retype }})</template>
|
||||
<template #prefix><i class="ti ti-lock"></i></template>
|
||||
<template #caption>
|
||||
<span v-if="passwordRetypeState == 'match'" style="color: var(--success)"><i class="ti ti-check ti-fw"></i> {{ i18n.ts.passwordMatched }}</span>
|
||||
<span v-if="passwordRetypeState == 'not-match'" style="color: var(--error)"><i class="ti ti-alert-triangle ti-fw"></i> {{ i18n.ts.passwordNotMatched }}</span>
|
||||
</template>
|
||||
</MkInput>
|
||||
<MkCaptcha v-if="instance.enableHcaptcha" ref="hcaptcha" v-model="hCaptchaResponse" :class="$style.captcha" provider="hcaptcha" :sitekey="instance.hcaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableRecaptcha" ref="recaptcha" v-model="reCaptchaResponse" :class="$style.captcha" provider="recaptcha" :sitekey="instance.recaptchaSiteKey"/>
|
||||
<MkCaptcha v-if="instance.enableTurnstile" ref="turnstile" v-model="turnstileResponse" :class="$style.captcha" provider="turnstile" :sitekey="instance.turnstileSiteKey"/>
|
||||
<MkButton type="submit" :disabled="shouldDisableSubmitting" large gradate rounded data-cy-signup-submit style="margin: 0 auto;">
|
||||
<template v-if="submitting">
|
||||
<MkLoading :em="true" :colored="false"/>
|
||||
</template>
|
||||
<template v-else>{{ i18n.ts.start }}</template>
|
||||
</MkButton>
|
||||
</form>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import getPasswordStrength from 'syuilo-password-strength';
|
||||
import { toUnicode } from 'punycode/';
|
||||
import MkButton from './MkButton.vue';
|
||||
import MkInput from './MkInput.vue';
|
||||
import MkSwitch from './MkSwitch.vue';
|
||||
import MkCaptcha, { type Captcha } from '@/components/MkCaptcha.vue';
|
||||
import * as config from '@/config';
|
||||
import * as os from '@/os';
|
||||
import { login } from '@/account';
|
||||
import { instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
autoSet?: boolean;
|
||||
}>(), {
|
||||
autoSet: false,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'signup', user: Record<string, any>): void;
|
||||
(ev: 'signupEmailPending'): void;
|
||||
}>();
|
||||
|
||||
const host = toUnicode(config.host);
|
||||
|
||||
let hcaptcha = $ref<Captcha | undefined>();
|
||||
let recaptcha = $ref<Captcha | undefined>();
|
||||
let turnstile = $ref<Captcha | undefined>();
|
||||
|
||||
let username: string = $ref('');
|
||||
let password: string = $ref('');
|
||||
let retypedPassword: string = $ref('');
|
||||
let invitationCode: string = $ref('');
|
||||
let email = $ref('');
|
||||
let usernameState: null | 'wait' | 'ok' | 'unavailable' | 'error' | 'invalid-format' | 'min-range' | 'max-range' = $ref(null);
|
||||
let emailState: null | 'wait' | 'ok' | 'unavailable:used' | 'unavailable:format' | 'unavailable:disposable' | 'unavailable:mx' | 'unavailable:smtp' | 'unavailable' | 'error' = $ref(null);
|
||||
let passwordStrength: '' | 'low' | 'medium' | 'high' = $ref('');
|
||||
let passwordRetypeState: null | 'match' | 'not-match' = $ref(null);
|
||||
let submitting: boolean = $ref(false);
|
||||
let hCaptchaResponse = $ref(null);
|
||||
let reCaptchaResponse = $ref(null);
|
||||
let turnstileResponse = $ref(null);
|
||||
let usernameAbortController: null | AbortController = $ref(null);
|
||||
let emailAbortController: null | AbortController = $ref(null);
|
||||
|
||||
const shouldDisableSubmitting = $computed((): boolean => {
|
||||
return submitting ||
|
||||
instance.enableHcaptcha && !hCaptchaResponse ||
|
||||
instance.enableRecaptcha && !reCaptchaResponse ||
|
||||
instance.enableTurnstile && !turnstileResponse ||
|
||||
instance.emailRequiredForSignup && emailState !== 'ok' ||
|
||||
usernameState !== 'ok' ||
|
||||
passwordRetypeState !== 'match';
|
||||
});
|
||||
|
||||
function onChangeUsername(): void {
|
||||
if (username === '') {
|
||||
usernameState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
const err =
|
||||
!username.match(/^[a-zA-Z0-9_]+$/) ? 'invalid-format' :
|
||||
username.length < 1 ? 'min-range' :
|
||||
username.length > 20 ? 'max-range' :
|
||||
null;
|
||||
|
||||
if (err) {
|
||||
usernameState = err;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (usernameAbortController != null) {
|
||||
usernameAbortController.abort();
|
||||
}
|
||||
usernameState = 'wait';
|
||||
usernameAbortController = new AbortController();
|
||||
|
||||
os.api('username/available', {
|
||||
username,
|
||||
}, undefined, usernameAbortController.signal).then(result => {
|
||||
usernameState = result.available ? 'ok' : 'unavailable';
|
||||
}).catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
usernameState = 'error';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onChangeEmail(): void {
|
||||
if (email === '') {
|
||||
emailState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (emailAbortController != null) {
|
||||
emailAbortController.abort();
|
||||
}
|
||||
emailState = 'wait';
|
||||
emailAbortController = new AbortController();
|
||||
|
||||
os.api('email-address/available', {
|
||||
emailAddress: email,
|
||||
}, undefined, emailAbortController.signal).then(result => {
|
||||
emailState = result.available ? 'ok' :
|
||||
result.reason === 'used' ? 'unavailable:used' :
|
||||
result.reason === 'format' ? 'unavailable:format' :
|
||||
result.reason === 'disposable' ? 'unavailable:disposable' :
|
||||
result.reason === 'mx' ? 'unavailable:mx' :
|
||||
result.reason === 'smtp' ? 'unavailable:smtp' :
|
||||
'unavailable';
|
||||
}).catch((err) => {
|
||||
if (err.name !== 'AbortError') {
|
||||
emailState = 'error';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function onChangePassword(): void {
|
||||
if (password === '') {
|
||||
passwordStrength = '';
|
||||
return;
|
||||
}
|
||||
|
||||
const strength = getPasswordStrength(password);
|
||||
passwordStrength = strength > 0.7 ? 'high' : strength > 0.3 ? 'medium' : 'low';
|
||||
}
|
||||
|
||||
function onChangePasswordRetype(): void {
|
||||
if (retypedPassword === '') {
|
||||
passwordRetypeState = null;
|
||||
return;
|
||||
}
|
||||
|
||||
passwordRetypeState = password === retypedPassword ? 'match' : 'not-match';
|
||||
}
|
||||
|
||||
async function onSubmit(): Promise<void> {
|
||||
if (submitting) return;
|
||||
submitting = true;
|
||||
|
||||
try {
|
||||
await os.api('signup', {
|
||||
username,
|
||||
password,
|
||||
emailAddress: email,
|
||||
invitationCode,
|
||||
'hcaptcha-response': hCaptchaResponse,
|
||||
'g-recaptcha-response': reCaptchaResponse,
|
||||
'turnstile-response': turnstileResponse,
|
||||
});
|
||||
if (instance.emailRequiredForSignup) {
|
||||
os.alert({
|
||||
type: 'success',
|
||||
title: i18n.ts._signup.almostThere,
|
||||
text: i18n.t('_signup.emailSent', { email }),
|
||||
});
|
||||
emit('signupEmailPending');
|
||||
} else {
|
||||
const res = await os.api('signin', {
|
||||
username,
|
||||
password,
|
||||
});
|
||||
emit('signup', res);
|
||||
|
||||
if (props.autoSet) {
|
||||
return login(res.i);
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
submitting = false;
|
||||
hcaptcha?.reset?.();
|
||||
recaptcha?.reset?.();
|
||||
turnstile?.reset?.();
|
||||
|
||||
os.alert({
|
||||
type: 'error',
|
||||
text: i18n.ts.somethingHappened,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.banner {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.captcha {
|
||||
margin: 16px 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,94 @@
|
|||
/* eslint-disable @typescript-eslint/explicit-function-return-type */
|
||||
import { expect } from '@storybook/jest';
|
||||
import { userEvent, waitFor, within } from '@storybook/testing-library';
|
||||
import { StoryObj } from '@storybook/vue3';
|
||||
import { onBeforeUnmount } from 'vue';
|
||||
import MkSignupServerRules from './MkSignupDialog,rules.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
export const Empty = {
|
||||
render(args) {
|
||||
return {
|
||||
components: {
|
||||
MkSignupServerRules,
|
||||
},
|
||||
setup() {
|
||||
return {
|
||||
args,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
props() {
|
||||
return {
|
||||
...this.args,
|
||||
};
|
||||
},
|
||||
},
|
||||
template: '<MkSignupServerRules v-bind="props" />',
|
||||
};
|
||||
},
|
||||
async play({ canvasElement }) {
|
||||
const canvas = within(canvasElement);
|
||||
const groups = await canvas.findAllByRole('group');
|
||||
const buttons = await canvas.findAllByRole('button');
|
||||
for (const group of groups) {
|
||||
if (group.ariaExpanded === 'true') {
|
||||
continue;
|
||||
}
|
||||
const button = await within(group).findByRole('button');
|
||||
userEvent.click(button);
|
||||
await waitFor(() => expect(group).toHaveAttribute('aria-expanded', 'true'));
|
||||
}
|
||||
const labels = await canvas.findAllByText(i18n.ts.agree);
|
||||
for (const label of labels) {
|
||||
expect(buttons.at(-1)).toBeDisabled();
|
||||
await waitFor(() => userEvent.click(label));
|
||||
}
|
||||
expect(buttons.at(-1)).toBeEnabled();
|
||||
},
|
||||
args: {
|
||||
serverRules: [],
|
||||
tosUrl: null,
|
||||
},
|
||||
decorators: [
|
||||
(_, context) => ({
|
||||
setup() {
|
||||
instance.serverRules = context.args.serverRules;
|
||||
instance.tosUrl = context.args.tosUrl;
|
||||
onBeforeUnmount(() => {
|
||||
// FIXME: 呼び出されない
|
||||
instance.serverRules = [];
|
||||
instance.tosUrl = null;
|
||||
});
|
||||
},
|
||||
template: '<story/>',
|
||||
}),
|
||||
],
|
||||
parameters: {
|
||||
layout: 'centered',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkSignupServerRules>;
|
||||
export const ServerRulesOnly = {
|
||||
...Empty,
|
||||
args: {
|
||||
...Empty.args,
|
||||
serverRules: [
|
||||
'ルール',
|
||||
],
|
||||
},
|
||||
} satisfies StoryObj<typeof MkSignupServerRules>;
|
||||
export const TOSOnly = {
|
||||
...Empty,
|
||||
args: {
|
||||
...Empty.args,
|
||||
tosUrl: 'https://example.com/tos',
|
||||
},
|
||||
} satisfies StoryObj<typeof MkSignupServerRules>;
|
||||
export const ServerRulesAndTOS = {
|
||||
...Empty,
|
||||
args: {
|
||||
...Empty.args,
|
||||
serverRules: ServerRulesOnly.args.serverRules,
|
||||
tosUrl: TOSOnly.args.tosUrl,
|
||||
},
|
||||
} satisfies StoryObj<typeof MkSignupServerRules>;
|
|
@ -0,0 +1,114 @@
|
|||
<template>
|
||||
<div>
|
||||
<div :class="$style.banner">
|
||||
<i class="ti ti-checklist"></i>
|
||||
</div>
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<div class="_gaps_m">
|
||||
<div style="text-align: center;">{{ i18n.ts.pleaseConfirmBelowBeforeSignup }}</div>
|
||||
|
||||
<MkFolder v-if="availableServerRules" :default-open="true">
|
||||
<template #label>{{ i18n.ts.serverRules }}</template>
|
||||
<template #suffix><i v-if="agreeServerRules" class="ti ti-check" style="color: var(--success)"></i></template>
|
||||
|
||||
<ol class="_gaps_s" :class="$style.rules">
|
||||
<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>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder v-if="availableTos">
|
||||
<template #label>{{ i18n.ts.termsOfService }}</template>
|
||||
<template #suffix><i v-if="agreeTos" class="ti ti-check" style="color: var(--success)"></i></template>
|
||||
|
||||
<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>
|
||||
</MkFolder>
|
||||
|
||||
<MkFolder data-cy-signup-rules-notes>
|
||||
<template #label>{{ i18n.ts.basicNotesBeforeCreateAccount }}</template>
|
||||
<template #suffix><i v-if="agreeNote" class="ti ti-check" style="color: var(--success)"></i></template>
|
||||
|
||||
<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>
|
||||
</MkFolder>
|
||||
|
||||
<MkButton primary rounded gradate style="margin: 0 auto;" :disabled="!agreed" data-cy-signup-rules-continue @click="emit('accept')">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref } 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';
|
||||
|
||||
const availableServerRules = instance.serverRules.length > 0;
|
||||
const availableTos = instance.tosUrl != null;
|
||||
|
||||
const agreeServerRules = ref(false);
|
||||
const agreeTos = ref(false);
|
||||
const agreeNote = ref(false);
|
||||
|
||||
const agreed = computed(() => {
|
||||
return (!availableServerRules || agreeServerRules.value) && (!availableTos || agreeTos.value) && agreeNote.value;
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'accept'): void;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.banner {
|
||||
padding: 16px;
|
||||
text-align: center;
|
||||
font-size: 26px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.rules {
|
||||
counter-reset: item;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.rule {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
word-break: break-word;
|
||||
|
||||
&::before {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
position: sticky;
|
||||
top: calc(var(--stickyTop, 0px) + 8px);
|
||||
counter-increment: item;
|
||||
content: counter(item);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
font-size: 13px;
|
||||
font-weight: bold;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
}
|
||||
}
|
||||
|
||||
.ruleText {
|
||||
padding-top: 6px;
|
||||
}
|
||||
</style>
|
|
@ -1,24 +1,40 @@
|
|||
<template>
|
||||
<MkModalWindow
|
||||
ref="dialog"
|
||||
:width="366"
|
||||
:height="500"
|
||||
:width="500"
|
||||
:height="600"
|
||||
@close="dialog.close()"
|
||||
@closed="$emit('closed')"
|
||||
>
|
||||
<template #header>{{ i18n.ts.signup }}</template>
|
||||
|
||||
<MkSpacer :margin-min="20" :margin-max="28">
|
||||
<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
|
||||
</MkSpacer>
|
||||
<div style="overflow-x: clip;">
|
||||
<Transition
|
||||
mode="out-in"
|
||||
:enter-active-class="$style.transition_x_enterActive"
|
||||
:leave-active-class="$style.transition_x_leaveActive"
|
||||
:enter-from-class="$style.transition_x_enterFrom"
|
||||
:leave-to-class="$style.transition_x_leaveTo"
|
||||
>
|
||||
<template v-if="!isAcceptedServerRule">
|
||||
<XServerRules @accept="isAcceptedServerRule = true"/>
|
||||
</template>
|
||||
<template v-else>
|
||||
<XSignup :auto-set="autoSet" @signup="onSignup" @signup-email-pending="onSignupEmailPending"/>
|
||||
</template>
|
||||
</Transition>
|
||||
</div>
|
||||
</MkModalWindow>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { } from 'vue';
|
||||
import XSignup from '@/components/MkSignup.vue';
|
||||
import { $ref } from 'vue/macros';
|
||||
import XSignup from '@/components/MkSignupDialog.form.vue';
|
||||
import XServerRules from '@/components/MkSignupDialog.rules.vue';
|
||||
import MkModalWindow from '@/components/MkModalWindow.vue';
|
||||
import { i18n } from '@/i18n';
|
||||
import { instance } from '@/instance';
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
autoSet?: boolean;
|
||||
|
@ -33,6 +49,8 @@ const emit = defineEmits<{
|
|||
|
||||
const dialog = $shallowRef<InstanceType<typeof MkModalWindow>>();
|
||||
|
||||
const isAcceptedServerRule = $ref(false);
|
||||
|
||||
function onSignup(res) {
|
||||
emit('done', res);
|
||||
dialog.close();
|
||||
|
@ -42,3 +60,18 @@ function onSignupEmailPending() {
|
|||
dialog.close();
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.transition_x_enterActive,
|
||||
.transition_x_leaveActive {
|
||||
transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1);
|
||||
}
|
||||
.transition_x_enterFrom {
|
||||
opacity: 0;
|
||||
transform: translateX(50px);
|
||||
}
|
||||
.transition_x_leaveTo {
|
||||
opacity: 0;
|
||||
transform: translateX(-50px);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
:disabled="disabled"
|
||||
@keydown.enter="toggle"
|
||||
>
|
||||
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" @click.prevent="toggle">
|
||||
<span ref="button" v-tooltip="checked ? i18n.ts.itsOn : i18n.ts.itsOff" class="button" data-cy-switch-toggle @click.prevent="toggle">
|
||||
<div class="knob"></div>
|
||||
</span>
|
||||
<span class="label">
|
||||
|
|
|
@ -220,6 +220,9 @@ const patrons = [
|
|||
'ふぇいぽむ',
|
||||
'依古田イコ',
|
||||
'戸塚こだま',
|
||||
'すー。',
|
||||
'秋雨/Slime-hatena.jp',
|
||||
'けそ',
|
||||
];
|
||||
|
||||
let thereIsTreasure = $ref($i && !claimedAchievements.includes('foundTreasure'));
|
||||
|
|
|
@ -41,7 +41,7 @@
|
|||
<template #value>{{ instance.maintainerEmail }}</template>
|
||||
</MkKeyValue>
|
||||
</FormSplit>
|
||||
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.tos }}</FormLink>
|
||||
<FormLink v-if="instance.tosUrl" :to="instance.tosUrl" external>{{ i18n.ts.termsOfService }}</FormLink>
|
||||
</div>
|
||||
</FormSection>
|
||||
|
||||
|
|
|
@ -3,10 +3,15 @@
|
|||
<MkStickyContainer>
|
||||
<template #header><XHeader :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<FormLink to="/admin/server-rules">{{ i18n.ts.serverRules }}</FormLink>
|
||||
<FormSuspense :p="init">
|
||||
<div class="_gaps_m">
|
||||
<FormSection first>
|
||||
<div class="_gaps_m">
|
||||
<MkInput v-model="tosUrl">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||
</MkInput>
|
||||
<MkTextarea v-model="sensitiveWords">
|
||||
<template #label>{{ i18n.ts.sensitiveWords }}</template>
|
||||
<template #caption>{{ i18n.ts.sensitiveWordsDescription }}</template>
|
||||
|
@ -41,16 +46,20 @@ import { fetchInstance } from '@/instance';
|
|||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import FormLink from "@/components/form/link.vue";
|
||||
|
||||
let sensitiveWords: string = $ref('');
|
||||
let tosUrl: string | null = $ref(null);
|
||||
|
||||
async function init() {
|
||||
const meta = await os.api('admin/meta');
|
||||
sensitiveWords = meta.sensitiveWords.join('\n');
|
||||
tosUrl = meta.tosUrl;
|
||||
}
|
||||
|
||||
function save() {
|
||||
os.apiWithDialog('admin/update-meta', {
|
||||
tosUrl,
|
||||
sensitiveWords: sensitiveWords.split('\n'),
|
||||
}).then(() => {
|
||||
fetchInstance();
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
<template>
|
||||
<div>
|
||||
<MkStickyContainer>
|
||||
<template #header><XHeader :tabs="headerTabs"/></template>
|
||||
<MkSpacer :content-max="700" :margin-min="16" :margin-max="32">
|
||||
<div class="_gaps_m">
|
||||
<div>{{ i18n.ts._serverRules.description }}</div>
|
||||
<Sortable
|
||||
v-model="serverRules"
|
||||
class="_gaps_m"
|
||||
:item-key="(_, i) => i"
|
||||
:animation="150"
|
||||
:handle="'.' + $style.itemHandle"
|
||||
@start="e => e.item.classList.add('active')"
|
||||
@end="e => e.item.classList.remove('active')"
|
||||
>
|
||||
<template #item="{element,index}">
|
||||
<div :class="$style.item">
|
||||
<div :class="$style.itemHeader">
|
||||
<div :class="$style.itemNumber" v-text="String(index + 1)"/>
|
||||
<span :class="$style.itemHandle"><i class="ti ti-menu"/></span>
|
||||
<button class="_button" :class="$style.itemRemove" @click="remove(index)"><i class="ti ti-x"></i></button>
|
||||
</div>
|
||||
<MkInput v-model="serverRules[index]"/>
|
||||
</div>
|
||||
</template>
|
||||
</Sortable>
|
||||
<div :class="$style.commands">
|
||||
<MkButton rounded @click="serverRules.push('')"><i class="ti ti-plus"></i> {{ i18n.ts.add }}</MkButton>
|
||||
<MkButton primary rounded :class="$style.buttonSave" @click="save"><i class="ti ti-check"></i> {{ i18n.ts.save }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</MkStickyContainer>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { defineAsyncComponent } from 'vue';
|
||||
import XHeader from './_header_.vue';
|
||||
import * as os from '@/os';
|
||||
import { fetchInstance, instance } from '@/instance';
|
||||
import { i18n } from '@/i18n';
|
||||
import { definePageMetadata } from '@/scripts/page-metadata';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import MkInput from '@/components/MkInput.vue';
|
||||
|
||||
const Sortable = defineAsyncComponent(() => import('vuedraggable').then(x => x.default));
|
||||
|
||||
let serverRules: string[] = $ref(instance.serverRules);
|
||||
|
||||
const save = async () => {
|
||||
await os.apiWithDialog('admin/update-meta', {
|
||||
serverRules,
|
||||
});
|
||||
fetchInstance();
|
||||
};
|
||||
|
||||
const remove = (index: number): void => {
|
||||
serverRules.splice(index, 1);
|
||||
};
|
||||
|
||||
const headerTabs = $computed(() => []);
|
||||
|
||||
definePageMetadata({
|
||||
title: i18n.ts.serverRules,
|
||||
icon: 'ti ti-checkbox',
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.item {
|
||||
display: block;
|
||||
color: var(--navFg);
|
||||
}
|
||||
|
||||
.itemHeader {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.itemHandle {
|
||||
display: flex;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: move;
|
||||
}
|
||||
|
||||
.itemNumber {
|
||||
display: flex;
|
||||
background-color: var(--accentedBg);
|
||||
color: var(--accent);
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 999px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.itemEdit {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.itemRemove {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: var(--error);
|
||||
margin-left: auto;
|
||||
border-radius: 6px;
|
||||
|
||||
&:hover {
|
||||
background: var(--X5);
|
||||
}
|
||||
}
|
||||
|
||||
.commands {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
</style>
|
|
@ -13,11 +13,6 @@
|
|||
<template #label>{{ i18n.ts.instanceDescription }}</template>
|
||||
</MkTextarea>
|
||||
|
||||
<MkInput v-model="tosUrl">
|
||||
<template #prefix><i class="ti ti-link"></i></template>
|
||||
<template #label>{{ i18n.ts.tosUrl }}</template>
|
||||
</MkInput>
|
||||
|
||||
<FormSplit :min-width="300">
|
||||
<MkInput v-model="maintainerName">
|
||||
<template #label>{{ i18n.ts.maintainerName }}</template>
|
||||
|
@ -169,7 +164,6 @@ import MkButton from '@/components/MkButton.vue';
|
|||
|
||||
let name: string | null = $ref(null);
|
||||
let description: string | null = $ref(null);
|
||||
let tosUrl: string | null = $ref(null);
|
||||
let maintainerName: string | null = $ref(null);
|
||||
let maintainerEmail: string | null = $ref(null);
|
||||
let iconUrl: string | null = $ref(null);
|
||||
|
@ -194,7 +188,6 @@ async function init() {
|
|||
const meta = await os.api('admin/meta');
|
||||
name = meta.name;
|
||||
description = meta.description;
|
||||
tosUrl = meta.tosUrl;
|
||||
iconUrl = meta.iconUrl;
|
||||
bannerUrl = meta.bannerUrl;
|
||||
backgroundImageUrl = meta.backgroundImageUrl;
|
||||
|
@ -220,7 +213,6 @@ function save() {
|
|||
os.apiWithDialog('admin/update-meta', {
|
||||
name,
|
||||
description,
|
||||
tosUrl,
|
||||
iconUrl,
|
||||
bannerUrl,
|
||||
backgroundImageUrl,
|
||||
|
|
|
@ -15,9 +15,10 @@
|
|||
<div v-if="selectMode" class="_buttons">
|
||||
<MkButton inline @click="selectAll">Select all</MkButton>
|
||||
<MkButton inline @click="setCategoryBulk">Set category</MkButton>
|
||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
||||
<MkButton inline @click="addTagBulk">Add tag</MkButton>
|
||||
<MkButton inline @click="removeTagBulk">Remove tag</MkButton>
|
||||
<MkButton inline @click="setTagBulk">Set tag</MkButton>
|
||||
<MkButton inline @click="setLisenceBulk">Set Lisence</MkButton>
|
||||
<MkButton inline danger @click="delBulk">Delete</MkButton>
|
||||
</div>
|
||||
<MkPagination ref="emojisPaginationComponent" :pagination="pagination">
|
||||
|
@ -221,6 +222,18 @@ const setCategoryBulk = async () => {
|
|||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const setLisenceBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'License',
|
||||
});
|
||||
if (canceled) return;
|
||||
await os.apiWithDialog('admin/emoji/set-license-bulk', {
|
||||
ids: selectedEmojis.value,
|
||||
license: result,
|
||||
});
|
||||
emojisPaginationComponent.value.reload();
|
||||
};
|
||||
|
||||
const addTagBulk = async () => {
|
||||
const { canceled, result } = await os.inputText({
|
||||
title: 'Tag',
|
||||
|
|
|
@ -427,6 +427,10 @@ export const routes = [{
|
|||
path: '/other-settings',
|
||||
name: 'other-settings',
|
||||
component: page(() => import('./pages/admin/other-settings.vue')),
|
||||
}, {
|
||||
path: '/server-rules',
|
||||
name: 'server-rules',
|
||||
component: page(() => import('./pages/admin/server-rules.vue')),
|
||||
}, {
|
||||
path: '/',
|
||||
component: page(() => import('./pages/_empty_.vue')),
|
||||
|
|
|
@ -164,7 +164,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
},
|
||||
animation: {
|
||||
where: 'device',
|
||||
default: !matchMedia('(prefers-reduced-motion)').matches,
|
||||
default: !window.matchMedia('(prefers-reduced-motion)').matches,
|
||||
},
|
||||
animatedMfm: {
|
||||
where: 'device',
|
||||
|
@ -188,7 +188,7 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
},
|
||||
disableShowingAnimatedImages: {
|
||||
where: 'device',
|
||||
default: matchMedia('(prefers-reduced-motion)').matches,
|
||||
default: window.matchMedia('(prefers-reduced-motion)').matches,
|
||||
},
|
||||
emojiStyle: {
|
||||
where: 'device',
|
||||
|
|
|
@ -2348,6 +2348,7 @@ type LiteInstanceMetadata = {
|
|||
imageUrl: string;
|
||||
}[];
|
||||
translatorAvailable: boolean;
|
||||
serverRules: string[];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
|
|
|
@ -21,25 +21,25 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/api-extractor": "7.34.4",
|
||||
"@swc/jest": "0.2.24",
|
||||
"@swc/jest": "0.2.26",
|
||||
"@types/jest": "29.5.0",
|
||||
"@types/node": "18.15.11",
|
||||
"@typescript-eslint/eslint-plugin": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"eslint": "8.37.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.0",
|
||||
"@typescript-eslint/parser": "5.59.0",
|
||||
"eslint": "8.38.0",
|
||||
"jest": "29.5.0",
|
||||
"jest-fetch-mock": "3.0.3",
|
||||
"jest-websocket-mock": "2.4.0",
|
||||
"mock-socket": "9.2.1",
|
||||
"tsd": "0.28.1",
|
||||
"typescript": "5.0.3"
|
||||
"typescript": "5.0.4"
|
||||
},
|
||||
"files": [
|
||||
"built"
|
||||
],
|
||||
"dependencies": {
|
||||
"@swc/cli": "0.1.62",
|
||||
"@swc/core": "1.3.46",
|
||||
"@swc/core": "1.3.51",
|
||||
"eventemitter3": "5.0.0",
|
||||
"reconnecting-websocket": "4.4.0"
|
||||
}
|
||||
|
|
|
@ -315,6 +315,7 @@ export type LiteInstanceMetadata = {
|
|||
imageUrl: string;
|
||||
}[];
|
||||
translatorAvailable: boolean;
|
||||
serverRules: string[];
|
||||
};
|
||||
|
||||
export type DetailedInstanceMetadata = LiteInstanceMetadata & {
|
||||
|
|
|
@ -9,15 +9,15 @@
|
|||
"lint": "pnpm typecheck && pnpm eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild": "0.17.15",
|
||||
"esbuild": "0.17.17",
|
||||
"idb-keyval": "6.2.0",
|
||||
"misskey-js": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@typescript-eslint/parser": "5.57.1",
|
||||
"@typescript-eslint/parser": "5.59.0",
|
||||
"@typescript/lib-webworker": "npm:@types/serviceworker@0.0.67",
|
||||
"eslint": "8.37.0",
|
||||
"eslint": "8.38.0",
|
||||
"eslint-plugin-import": "2.27.5",
|
||||
"typescript": "5.0.3"
|
||||
"typescript": "5.0.4"
|
||||
}
|
||||
}
|
||||
|
|
3123
pnpm-lock.yaml
3123
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue