Merge branch 'develop' into bh-worker

This commit is contained in:
tamaina
2023-05-07 10:39:14 +00:00
44 changed files with 1354 additions and 248 deletions

View File

@@ -117,6 +117,7 @@ export class SearchService {
public async searchNote(q: string, me: User | null, opts: {
userId?: Note['userId'] | null;
channelId?: Note['channelId'] | null;
host?: string | null;
}, pagination: {
untilId?: Note['id'];
sinceId?: Note['id'];
@@ -131,6 +132,13 @@ export class SearchService {
if (pagination.sinceId) filter.qs.push({ op: '>', k: 'createdAt', v: this.idService.parse(pagination.sinceId).date.getTime() });
if (opts.userId) filter.qs.push({ op: '=', k: 'userId', v: opts.userId });
if (opts.channelId) filter.qs.push({ op: '=', k: 'channelId', v: opts.channelId });
if (opts.host) {
if (opts.host === '.') {
// TODO: Meilisearchが2023/05/07現在値がNULLかどうかのクエリが書けない
} else {
filter.qs.push({ op: '=', k: 'userHost', v: opts.host });
}
}
const res = await this.meilisearchNoteIndex!.search(q, {
sort: ['createdAt:desc'],
matchingStrategy: 'all',

View File

@@ -412,7 +412,7 @@ export class UserEntityService implements OnModuleInit {
userId: meId,
targetUserId: user.id,
}).then(row => row?.memo ?? null),
moderationNote: iAmModerator ? profile!.moderationNote : null,
moderationNote: iAmModerator ? (profile!.moderationNote ?? '') : undefined,
} : {}),
...(opts.detail && isMe ? {

View File

@@ -80,7 +80,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
isSilenced: isSilenced,
isSuspended: user.isSuspended,
lastActiveDate: user.lastActiveDate,
moderationNote: profile.moderationNote,
moderationNote: profile.moderationNote ?? '',
signins,
policies: await this.roleService.getUserPolicies(user.id),
roles: await this.roleEntityService.packMany(roles, me),

View File

@@ -44,7 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
) {
super(meta, paramDef, async (ps, me) => {
const query = this.usersRepository.createQueryBuilder('user')
.where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) });
.where(':tag = ANY(user.tags)', { tag: normalizeForSearch(ps.tag) })
.andWhere('user.isSuspended = FALSE');
const recent = new Date(Date.now() - (1000 * 60 * 60 * 24 * 5));

View File

@@ -42,8 +42,7 @@ export const paramDef = {
offset: { type: 'integer', default: 0 },
host: {
type: 'string',
nullable: true,
description: 'The local host is represented with `null`.',
description: 'The local host is represented with `.`.',
},
userId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
channelId: { type: 'string', format: 'misskey:id', nullable: true, default: null },
@@ -73,6 +72,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
const notes = await this.searchService.searchNote(ps.query, me, {
userId: ps.userId,
channelId: ps.channelId,
host: ps.host,
}, {
untilId: ps.untilId,
sinceId: ps.sinceId,

View File

@@ -50,8 +50,9 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private queryService: QueryService,
) {
super(meta, paramDef, async (ps, me) => {
const query = this.usersRepository.createQueryBuilder('user');
query.where('user.isExplorable = TRUE');
const query = this.usersRepository.createQueryBuilder('user')
.where('user.isExplorable = TRUE')
.andWhere('user.isSuspended = FALSE');
switch (ps.state) {
case 'alive': query.andWhere('user.updatedAt > :date', { date: new Date(Date.now() - 1000 * 60 * 60 * 24 * 5) }); break;

View File

@@ -51,7 +51,7 @@ describe('ユーザー', () => {
type User = MeDetailed & { token: string };
const show = async (id: string, me = alice): Promise<MeDetailed | UserDetailedNotMe> => {
const show = async (id: string, me = root): Promise<MeDetailed | UserDetailedNotMe> => {
return successfulApiCall({ endpoint: 'users/show', parameters: { userId: id }, user: me }) as any;
};
@@ -112,7 +112,6 @@ describe('ユーザー', () => {
securityKeys: user.securityKeys,
roles: user.roles,
memo: user.memo,
moderationNote: user.moderationNote,
});
};
@@ -222,8 +221,8 @@ describe('ユーザー', () => {
}, 1000 * 60 * 2);
beforeAll(async () => {
root = await signup({ username: 'alice' });
alice = root;
root = await signup({ username: 'root' });
alice = await signup({ username: 'alice' });
aliceNote = await post(alice, { text: 'test' }) as any;
alicePage = await page(alice);
aliceList = (await api('users/list/create', { name: 'aliceList' }, alice)).body;
@@ -567,10 +566,10 @@ describe('ユーザー', () => {
{ label: '空文字', memo: '', expects: null },
{ label: 'null', memo: null },
])('を書き換えることができる(メモを$labelに)', async ({ memo, expects }) => {
const expected = { ...await show(bob.id), memo: expects === undefined ? memo : expects };
const expected = { ...await show(bob.id, alice), memo: expects === undefined ? memo : expects };
const parameters = { userId: bob.id, memo };
await successfulApiCall({ endpoint: 'users/update-memo', parameters, user: alice });
const response = await show(bob.id);
const response = await show(bob.id, alice);
assert.deepStrictEqual(response, expected);
});
@@ -589,7 +588,7 @@ describe('ユーザー', () => {
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
// 結果の並びを事前にアサートするのは困難なので返ってきたidに対応するユーザーが返っており、ソート順が正しいことだけを検証する
const users = await Promise.all(response.map(u => show(u.id)));
const users = await Promise.all(response.map(u => show(u.id, alice)));
const expected = users.sort((x, y) => {
const index = (selector(x) < selector(y)) ? -1 : (selector(x) > selector(y)) ? 1 : 0;
return index * (parameters.sort?.startsWith('+') ? -1 : 1);
@@ -603,13 +602,13 @@ describe('ユーザー', () => {
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれ', user: (): User => userSuspended },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
] as const)('をリスト形式で取得することができ、結果に$label', async ({ user, excluded }) => {
const parameters = { limit: 100 };
const response = await successfulApiCall({ endpoint: 'users', parameters, user: alice });
const expected = (excluded ?? false) ? [] : [await show(user().id)];
const expected = (excluded ?? false) ? [] : [await show(user().id, alice)];
assert.deepStrictEqual(response.filter((u) => u.id === user().id), expected);
});
test.todo('をリスト形式で取得することができる(リモート, hostname指定');
@@ -636,7 +635,7 @@ describe('ユーザー', () => {
{ label: 'Moderatorになっている', user: (): User => userModerator, me: (): User => userModerator, selector: (user: User): unknown => user.isModerator },
{ label: '自分以外から見たときはModeratorか判定できない', user: (): User => userModerator, selector: (user: User): unknown => user.isModerator, expected: (): undefined => undefined },
{ label: 'サイレンスになっている', user: (): User => userSilenced, selector: (user: User): unknown => user.isSilenced },
{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended },
//{ label: 'サスペンドになっている', user: (): User => userSuspended, selector: (user: User): unknown => user.isSuspended },
{ label: '削除済みになっている', user: (): User => userDeletedBySelf, me: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted },
{ label: '自分以外から見たときは削除済みか判定できない', user: (): User => userDeletedBySelf, selector: (user: User): unknown => user.isDeleted, expected: (): undefined => undefined },
{ label: '削除済み(byAdmin)になっている', user: (): User => userDeletedByAdmin, me: (): User => userDeletedByAdmin, selector: (user: User): unknown => user.isDeleted },
@@ -718,13 +717,13 @@ describe('ユーザー', () => {
test('を検索することができる', async () => {
const parameters = { query: 'carol', limit: 10 };
const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
const expected = [await show(carol.id)];
const expected = [await show(carol.id, alice)];
assert.deepStrictEqual(response, expected);
});
test('を検索することができる(UserLite)', async () => {
const parameters = { query: 'carol', detail: false, limit: 10 };
const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
const expected = [userLite(await show(carol.id))];
const expected = [userLite(await show(carol.id, alice))];
assert.deepStrictEqual(response, expected);
});
test.each([
@@ -740,7 +739,7 @@ describe('ユーザー', () => {
] as const)('を検索することができ、結果に$labelが含まれる', async ({ user, excluded }) => {
const parameters = { query: user().username, limit: 1 };
const response = await successfulApiCall({ endpoint: 'users/search', parameters, user: alice });
const expected = (excluded ?? false) ? [] : [await show(user().id)];
const expected = (excluded ?? false) ? [] : [await show(user().id, alice)];
assert.deepStrictEqual(response, expected);
});
test.todo('を検索することができる(リモート)');
@@ -761,7 +760,7 @@ describe('ユーザー', () => {
{ label: 'ローカル', parameters: { host: '.', limit: 1 }, user: (): User[] => [userFollowedByAlice] },
])('をID&ホスト指定で検索できる($label)', async ({ parameters, user }) => {
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
const expected = await Promise.all(user().map(u => show(u.id)));
const expected = await Promise.all(user().map(u => show(u.id, alice)));
assert.deepStrictEqual(response, expected);
});
test.each([
@@ -777,7 +776,7 @@ describe('ユーザー', () => {
] as const)('をID&ホスト指定で検索でき、結果に$label', async ({ user, excluded }) => {
const parameters = { username: user().username };
const response = await successfulApiCall({ endpoint: 'users/search-by-username-and-host', parameters, user: alice });
const expected = (excluded ?? false) ? [] : [await show(user().id)];
const expected = (excluded ?? false) ? [] : [await show(user().id, alice)];
assert.deepStrictEqual(response, expected);
});
test.todo('をID&ホスト指定で検索できる(リモート)');
@@ -789,7 +788,7 @@ describe('ユーザー', () => {
const parameters = { userId: alice.id, limit: 5 };
const response = await successfulApiCall({ endpoint: 'users/get-frequently-replied-users', parameters, user: alice });
const expected = await Promise.all(usersReplying.slice(0, parameters.limit).map(async (s, i) => ({
user: await show(s.id),
user: await show(s.id, alice),
weight: (usersReplying.length - i) / usersReplying.length,
})));
assert.deepStrictEqual(response, expected);
@@ -801,7 +800,7 @@ describe('ユーザー', () => {
{ label: 'ブロックしてきているユーザーが含まれない', user: (): User => userBlockingAlice, excluded: true },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended },
//{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
] as const)('がよくリプライをするユーザーのリストを取得でき、結果に$label', async ({ user, excluded }) => {
@@ -809,7 +808,7 @@ describe('ユーザー', () => {
await post(alice, { text: `@${user().username} test`, replyId: replyTo.id });
const parameters = { userId: alice.id, limit: 100 };
const response = await successfulApiCall({ endpoint: 'users/get-frequently-replied-users', parameters, user: alice });
const expected = (excluded ?? false) ? [] : [await show(user().id)];
const expected = (excluded ?? false) ? [] : [await show(user().id, alice)];
assert.deepStrictEqual(response.map(s => s.user).filter((u) => u.id === user().id), expected);
});
@@ -828,7 +827,7 @@ describe('ユーザー', () => {
await successfulApiCall({ endpoint: 'i/update', parameters: { description: `#${hashtag}` }, user: alice });
const parameters = { tag: hashtag, limit: 5, ...sort };
const response = await successfulApiCall({ endpoint: 'hashtags/users', parameters, user: alice });
const users = await Promise.all(response.map(u => show(u.id)));
const users = await Promise.all(response.map(u => show(u.id, alice)));
const expected = users.sort((x, y) => {
const index = (selector(x) < selector(y)) ? -1 : (selector(x) > selector(y)) ? 1 : 0;
return index * (parameters.sort.startsWith('+') ? -1 : 1);
@@ -842,10 +841,10 @@ describe('ユーザー', () => {
{ label: 'ブロックしてきているユーザーが含まれる', user: (): User => userBlockingAlice },
{ label: '承認制ユーザーが含まれる', user: (): User => userLocking },
{ label: 'サイレンスユーザーが含まれる', user: (): User => userSilenced },
{ label: 'サスペンドユーザーが含まれ', user: (): User => userSuspended },
{ label: 'サスペンドユーザーが含まれない', user: (): User => userSuspended, excluded: true },
{ label: '削除済ユーザーが含まれる', user: (): User => userDeletedBySelf },
{ label: '削除済(byAdmin)ユーザーが含まれる', user: (): User => userDeletedByAdmin },
] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user }) => {
] as const)('をハッシュタグ指定で取得することができ、結果に$label', async ({ user, excluded }) => {
const hashtag = `user_test${user().username}`;
if (user() !== userSuspended) {
// サスペンドユーザーはupdateできない。
@@ -853,7 +852,7 @@ describe('ユーザー', () => {
}
const parameters = { tag: hashtag, limit: 100, sort: '-follower' } as const;
const response = await successfulApiCall({ endpoint: 'hashtags/users', parameters, user: alice });
const expected = [await show(user().id)];
const expected = (excluded ?? false) ? [] : [await show(user().id, alice)];
assert.deepStrictEqual(response, expected);
});
test.todo('をハッシュタグ指定で取得することができる(リモート)');
@@ -876,7 +875,7 @@ describe('ユーザー', () => {
await successfulApiCall({ endpoint: 'admin/update-meta', parameters: { pinnedUsers: [bob.username, `@${carol.username}`] }, user: root });
const parameters = {} as const;
const response = await successfulApiCall({ endpoint: 'pinned-users', parameters, user: alice });
const expected = await Promise.all([bob, carol].map(u => show(u.id)));
const expected = await Promise.all([bob, carol].map(u => show(u.id, alice)));
assert.deepStrictEqual(response, expected);
});

View File

@@ -34,6 +34,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>
<button v-tooltip="i18n.ts.hide" :class="$style.hide" class="_button" @click.stop.prevent="hide = true"><i class="ti ti-eye-off"></i></button>
</template>
@@ -48,6 +49,8 @@ import bytes from '@/filters/bytes';
import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
import * as os from '@/os';
import { iAmModerator } from '@/account';
const props = defineProps<{
image: misskey.entities.DriveFile;
@@ -77,6 +80,17 @@ watch(() => props.image, () => {
deep: true,
immediate: true,
});
function showMenu(ev: MouseEvent) {
os.popupMenu([...(iAmModerator ? [{
text: i18n.ts.markAsSensitive,
icon: 'ti ti-eye-off',
action: () => {
os.apiWithDialog('drive/files/update', { fileId: props.image.id, isSensitive: true });
},
}] : [])], ev.currentTarget ?? ev.target);
}
</script>
<style lang="scss" module>
@@ -126,6 +140,21 @@ watch(() => props.image, () => {
right: 12px;
}
.menu {
display: block;
position: absolute;
border-radius: 6px;
background-color: rgba(0, 0, 0, 0.3);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: #fff;
font-size: 0.8em;
padding: 6px 8px;
text-align: center;
bottom: 12px;
right: 12px;
}
.imageContainer {
display: block;
cursor: zoom-in;

View File

@@ -47,8 +47,24 @@ export const Long = {
...Default.args,
user: {
...userDetailed(),
username: '2c7cc62a697ea3a7826521f3fd34f0cb273693cbe5e9310f35449f43622a5cdc',
host: 'nostr.example',
username: 'the_quick_brown_fox_jumped_over_the_lazy_dog',
host: 'misskey.example',
},
},
decorators: [
() => ({
template: '<div style="width: 360px;"><story/></div>',
}),
],
} satisfies StoryObj<typeof MkAcct>;
export const VeryLong = {
...Default,
args: {
...Default.args,
user: {
...userDetailed(),
username: '2c7cc62a697ea3a7826521f3fd34f0cb273693cbe5e9310f35449f43622a5cdc',
host: 'the.quick.brown.fox.jumped.over.the.lazy.dog.very.long.hostname.nostr.example',
},
},
decorators: [

View File

@@ -1,5 +1,5 @@
<template>
<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct">
<MkCondensedLine v-if="defaultStore.state.enableCondensedLineForAcct" :min-scale="2 / 3">
<span>@{{ user.username }}</span>
<span v-if="user.host || detail || defaultStore.state.showFullAcct" style="opacity: 0.5;">@{{ user.host || host }}</span>
</MkCondensedLine>

View File

@@ -7,14 +7,19 @@
</template>
<script lang="ts">
interface Props {
readonly minScale?: number;
}
const contentSymbol = Symbol();
const observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const content = (entry.target[contentSymbol] ? entry.target : entry.target.firstElementChild) as HTMLSpanElement;
const props: Required<Props> = content[contentSymbol];
const container = content.parentElement as HTMLSpanElement;
const contentWidth = content.getBoundingClientRect().width;
const containerWidth = container.getBoundingClientRect().width;
container.style.transform = `scaleX(${Math.min(1, containerWidth / contentWidth)})`;
container.style.transform = `scaleX(${Math.max(props.minScale, Math.min(1, containerWidth / contentWidth))})`;
}
});
</script>
@@ -22,6 +27,10 @@ const observer = new ResizeObserver((entries) => {
<script setup lang="ts">
import { ref, watch } from 'vue';
const props = withDefaults(defineProps<Props>(), {
minScale: 0,
});
const content = ref<HTMLSpanElement>();
watch(content, (value, oldValue) => {
@@ -33,7 +42,7 @@ watch(content, (value, oldValue) => {
}
}
if (value) {
value[contentSymbol] = contentSymbol;
value[contentSymbol] = props;
observer.observe(value);
if (value.parentElement) {
observer.observe(value.parentElement);
@@ -45,7 +54,7 @@ watch(content, (value, oldValue) => {
<style module lang="scss">
.container {
display: inline-block;
width: 100%;
max-width: 100%;
transform-origin: 0;
}

View File

@@ -449,7 +449,7 @@ if ($i) {
const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt');
const neverShowDonationInfo = miLocalStorage.getItem('neverShowDonationInfo');
if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith("/miauth")) {
if (neverShowDonationInfo !== 'true' && (new Date($i.createdAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 3))) && !location.pathname.startsWith('/miauth')) {
if (latestDonationInfoShownAt == null || (new Date(latestDonationInfoShownAt).getTime() < (Date.now() - (1000 * 60 * 60 * 24 * 30)))) {
popup(defineAsyncComponent(() => import('@/components/MkDonation.vue')), {}, {}, 'closed');
}

View File

@@ -10,51 +10,55 @@
<MkSwitch v-model="reportError">{{ i18n.ts.sendErrorReports }}<template #caption>{{ i18n.ts.sendErrorReportsDescription }}</template></MkSwitch>
-->
<div class="_gaps_s">
<MkFolder>
<template #icon><i class="ti ti-info-circle"></i></template>
<template #label>{{ i18n.ts.accountInfo }}</template>
<FormSection first>
<div class="_gaps_s">
<MkFolder>
<template #icon><i class="ti ti-info-circle"></i></template>
<template #label>{{ i18n.ts.accountInfo }}</template>
<div class="_gaps_m">
<MkKeyValue>
<template #key>ID</template>
<template #value><span class="_monospace">{{ $i.id }}</span></template>
</MkKeyValue>
<div class="_gaps_m">
<MkKeyValue>
<template #key>ID</template>
<template #value><span class="_monospace">{{ $i.id }}</span></template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.registeredDate }}</template>
<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
</MkKeyValue>
<MkKeyValue>
<template #key>{{ i18n.ts.registeredDate }}</template>
<template #value><MkTime :time="$i.createdAt" mode="detail"/></template>
</MkKeyValue>
<FormLink to="/settings/account-stats"><template #icon><i class="ti ti-info-circle"></i></template>{{ i18n.ts.statistics }}</FormLink>
</div>
</MkFolder>
<FormLink to="/settings/account-stats"><template #icon><i class="ti ti-info-circle"></i></template>{{ i18n.ts.statistics }}</FormLink>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-alert-triangle"></i></template>
<template #label>{{ i18n.ts.closeAccount }}</template>
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
<FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-flask"></i></template>
<template #label>{{ i18n.ts.experimentalFeatures }}</template>
<div class="_gaps_m">
<MkSwitch v-model="enableCondensedLineForAcct">
<template #label>Enable condensed line for acct</template>
</MkSwitch>
</div>
</MkFolder>
</div>
</FormSection>
<FormSection>
<FormLink to="/registry"><template #icon><i class="ti ti-adjustments"></i></template>{{ i18n.ts.registry }}</FormLink>
<MkFolder>
<template #icon><i class="ti ti-alert-triangle"></i></template>
<template #label>{{ i18n.ts.closeAccount }}</template>
<div class="_gaps_m">
<FormInfo warn>{{ i18n.ts._accountDelete.mayTakeTime }}</FormInfo>
<FormInfo>{{ i18n.ts._accountDelete.sendEmail }}</FormInfo>
<MkButton v-if="!$i.isDeleted" danger @click="deleteAccount">{{ i18n.ts._accountDelete.requestAccountDelete }}</MkButton>
<MkButton v-else disabled>{{ i18n.ts._accountDelete.inProgress }}</MkButton>
</div>
</MkFolder>
<MkFolder>
<template #icon><i class="ti ti-flask"></i></template>
<template #label>{{ i18n.ts.experimentalFeatures }}</template>
<div class="_gaps_m">
<MkSwitch v-model="enableCondensedLineForAcct">
<template #label>Enable condensed line for acct</template>
</MkSwitch>
</div>
</MkFolder>
</div>
</FormSection>
</div>
</template>
@@ -72,6 +76,7 @@ import { signout, $i } from '@/account';
import { i18n } from '@/i18n';
import { definePageMetadata } from '@/scripts/page-metadata';
import { unisonReload } from '@/scripts/unison-reload';
import FormSection from '@/components/form/section.vue';
const reportError = computed(defaultStore.makeGetterSetter('reportError'));
const enableCondensedLineForAcct = computed(defaultStore.makeGetterSetter('enableCondensedLineForAcct'));

View File

@@ -1,8 +1,11 @@
<template>
<form class="mk-setup" @submit.prevent="submit()">
<h1>Welcome to Misskey!</h1>
<div class="_gaps_m">
<p>{{ i18n.ts.intro }}</p>
<form :class="$style.root" class="_panel" @submit.prevent="submit()">
<div :class="$style.title">
<div>Welcome to Misskey!</div>
<div :class="$style.version">v{{ version }}</div>
</div>
<div class="_gaps_m" style="padding: 32px;">
<div>{{ i18n.ts.intro }}</div>
<MkInput v-model="username" pattern="^[a-zA-Z0-9_]{1,20}$" :spellcheck="false" required data-cy-admin-username>
<template #label>{{ i18n.ts.username }}</template>
<template #prefix>@</template>
@@ -12,8 +15,8 @@
<template #label>{{ i18n.ts.password }}</template>
<template #prefix><i class="ti ti-lock"></i></template>
</MkInput>
<div class="bottom">
<MkButton gradate type="submit" :disabled="submitting" data-cy-admin-ok>
<div>
<MkButton gradate large rounded type="submit" :disabled="submitting" data-cy-admin-ok style="margin: 0 auto;">
{{ submitting ? i18n.ts.processing : i18n.ts.done }}<MkEllipsis v-if="submitting"/>
</MkButton>
</div>
@@ -25,7 +28,7 @@
import { } from 'vue';
import MkButton from '@/components/MkButton.vue';
import MkInput from '@/components/MkInput.vue';
import { host } from '@/config';
import { host, version } from '@/config';
import * as os from '@/os';
import { login } from '@/account';
import { i18n } from '@/i18n';
@@ -54,36 +57,28 @@ function submit() {
}
</script>
<style lang="scss" scoped>
.mk-setup {
<style lang="scss" module>
.root {
border-radius: var(--radius);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
overflow: hidden;
max-width: 500px;
margin: 32px auto;
}
> h1 {
margin: 0;
font-size: 1.5em;
text-align: center;
padding: 32px;
background: var(--accent);
color: #fff;
}
.title {
margin: 0;
font-size: 1.5em;
text-align: center;
padding: 32px;
background: var(--accentedBg);
color: var(--accent);
font-weight: bold;
}
> div {
padding: 32px;
background: var(--panel);
> p {
margin-top: 0;
}
> .bottom {
> * {
margin: 0 auto;
}
}
}
.version {
font-size: 70%;
font-weight: normal;
opacity: 0.7;
}
</style>