Merge branch 'develop' into bh-worker
This commit is contained in:
@@ -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',
|
||||
|
@@ -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 ? {
|
||||
|
@@ -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),
|
||||
|
@@ -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));
|
||||
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
});
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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: [
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
|
@@ -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');
|
||||
}
|
||||
|
@@ -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'));
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user