summaryrefslogtreecommitdiff
path: root/packages
diff options
context:
space:
mode:
authorsyuilo <Syuilotan@yahoo.co.jp>2023-05-08 17:29:19 +0900
committerGitHub <noreply@github.com>2023-05-08 17:29:19 +0900
commit85a4c8dbb1cd455ac538d8004dafc47d4d6d400f (patch)
tree32cd21e25a3b2300e57f36f527f1f3e635f8b8e4 /packages
parentrefactor(frontend): use css modules (diff)
downloadmisskey-85a4c8dbb1cd455ac538d8004dafc47d4d6d400f.tar.gz
misskey-85a4c8dbb1cd455ac538d8004dafc47d4d6d400f.tar.bz2
misskey-85a4c8dbb1cd455ac538d8004dafc47d4d6d400f.zip
feat(frontend): アカウント初期設定ウィザード (#10799)
* wip * :art: * :art: * wip * wip * :art: * Update CHANGELOG.md * wip * Update MkUserSetupDialog.vue * add stories Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * update stories * Update MkUserSetupDialog.Follow.stories.impl.ts * test: load mock user account * :v: * :v: * test: reset on each render * test: use id to identify --------- Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com>
Diffstat (limited to 'packages')
-rw-r--r--packages/frontend/.storybook/generate.tsx2
-rw-r--r--packages/frontend/.storybook/preview.ts31
-rw-r--r--packages/frontend/src/components/MkModalWindow.vue2
-rw-r--r--packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts2
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.stories.impl.ts51
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Follow.vue63
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Profile.stories.impl.ts31
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.Profile.vue101
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.User.stories.impl.ts32
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.User.vue101
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.stories.impl.ts51
-rw-r--r--packages/frontend/src/components/MkUserSetupDialog.vue135
-rw-r--r--packages/frontend/src/init.ts4
-rw-r--r--packages/frontend/src/pages/timeline.tutorial.vue69
-rw-r--r--packages/frontend/src/pages/timeline.vue2
-rw-r--r--packages/frontend/src/store.ts6
16 files changed, 626 insertions, 57 deletions
diff --git a/packages/frontend/.storybook/generate.tsx b/packages/frontend/.storybook/generate.tsx
index dbe9729170..7c51d4c00c 100644
--- a/packages/frontend/.storybook/generate.tsx
+++ b/packages/frontend/.storybook/generate.tsx
@@ -399,6 +399,8 @@ Promise.all([
glob('src/components/Mk{A,B}*.vue'),
glob('src/components/MkGalleryPostPreview.vue'),
glob('src/components/MkSignupServerRules.vue'),
+ glob('src/components/MkUserSetupDialog.vue'),
+ glob('src/components/MkUserSetupDialog.*.vue'),
glob('src/pages/user/home.vue'),
])
.then((globs) => globs.flat())
diff --git a/packages/frontend/.storybook/preview.ts b/packages/frontend/.storybook/preview.ts
index 06aa7b6682..d91d76ff0f 100644
--- a/packages/frontend/.storybook/preview.ts
+++ b/packages/frontend/.storybook/preview.ts
@@ -3,6 +3,7 @@ import { FORCE_REMOUNT } from '@storybook/core-events';
import { type Preview, setup } from '@storybook/vue3';
import isChromatic from 'chromatic/isChromatic';
import { initialize, mswDecorator } from 'msw-storybook-addon';
+import { userDetailed } from './fakes';
import locale from './locale';
import { commonHandlers, onUnhandledRequest } from './mocks';
import themes from './themes';
@@ -10,6 +11,7 @@ import '../src/style.scss';
const appInitialized = Symbol();
+let lastStory = null;
let moduleInitialized = false;
let unobserve = () => {};
let misskeyOS = null;
@@ -42,10 +44,16 @@ function loadTheme(applyTheme: typeof import('../src/scripts/theme')['applyTheme
unobserve = () => observer.disconnect();
}
+function initLocalStorage() {
+ localStorage.clear();
+ localStorage.setItem('account', JSON.stringify(userDetailed()));
+ localStorage.setItem('locale', JSON.stringify(locale));
+}
+
initialize({
onUnhandledRequest,
});
-localStorage.setItem("locale", JSON.stringify(locale));
+initLocalStorage();
queueMicrotask(() => {
Promise.all([
import('../src/components'),
@@ -76,6 +84,27 @@ queueMicrotask(() => {
const preview = {
decorators: [
(Story, context) => {
+ if (lastStory === context.id) {
+ lastStory = null;
+ } else {
+ lastStory = context.id;
+ const channel = addons.getChannel();
+ const resetIndexedDBPromise = globalThis.indexedDB?.databases
+ ? indexedDB.databases().then((r) => {
+ for (var i = 0; i < r.length; i++) {
+ indexedDB.deleteDatabase(r[i].name!);
+ }
+ }).catch(() => {})
+ : Promise.resolve();
+ const resetDefaultStorePromise = import('../src/store').then(({ defaultStore }) => {
+ // @ts-expect-error
+ defaultStore.init();
+ }).catch(() => {});
+ Promise.all([resetIndexedDBPromise, resetDefaultStorePromise]).then(() => {
+ initLocalStorage();
+ channel.emit(FORCE_REMOUNT, { storyId: context.id });
+ });
+ }
const story = Story();
if (!moduleInitialized) {
const channel = addons.getChannel();
diff --git a/packages/frontend/src/components/MkModalWindow.vue b/packages/frontend/src/components/MkModalWindow.vue
index 2d2f8411f1..f983f77750 100644
--- a/packages/frontend/src/components/MkModalWindow.vue
+++ b/packages/frontend/src/components/MkModalWindow.vue
@@ -89,7 +89,6 @@ defineExpose({
display: flex;
flex-direction: column;
contain: content;
- container-type: inline-size;
border-radius: var(--radius);
--root-margin: 24px;
@@ -142,6 +141,7 @@ defineExpose({
flex: 1;
overflow: auto;
background: var(--panel);
+ container-type: size;
}
}
</style>
diff --git a/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts b/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts
index 1308dfff9a..2d95455730 100644
--- a/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts
+++ b/packages/frontend/src/components/MkSignupDialog.rules.stories.impl.ts
@@ -3,7 +3,7 @@ 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 MkSignupServerRules from './MkSignupDialog.rules.vue';
import { i18n } from '@/i18n';
import { instance } from '@/instance';
export const Empty = {
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.Follow.stories.impl.ts
new file mode 100644
index 0000000000..7d5a65f41a
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.stories.impl.ts
@@ -0,0 +1,51 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import { rest } from 'msw';
+import { commonHandlers } from '../../.storybook/mocks';
+import { userDetailed } from '../../.storybook/fakes';
+import MkUserSetupDialog_Follow from './MkUserSetupDialog.Follow.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkUserSetupDialog_Follow,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkUserSetupDialog_Follow v-bind="props" />',
+ };
+ },
+ args: {
+
+ },
+ parameters: {
+ layout: 'centered',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ rest.post('/api/users', (req, res, ctx) => {
+ return res(ctx.json([
+ userDetailed('44'),
+ userDetailed('49'),
+ ]));
+ }),
+ rest.post('/api/pinned-users', (req, res, ctx) => {
+ return res(ctx.json([
+ userDetailed('44'),
+ userDetailed('49'),
+ ]));
+ }),
+ ],
+ },
+ },
+} satisfies StoryObj<typeof MkUserSetupDialog_Follow>;
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Follow.vue b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
new file mode 100644
index 0000000000..b89e3e4c9d
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.Follow.vue
@@ -0,0 +1,63 @@
+<template>
+<div class="_gaps">
+ <div style="text-align: center;">{{ i18n.ts._initialAccountSetting.followUsers }}</div>
+
+ <MkFolder :default-open="true">
+ <template #label>{{ i18n.ts.recommended }}</template>
+
+ <MkPagination :pagination="pinnedUsers">
+ <template #default="{ items }">
+ <div :class="$style.users">
+ <XUser v-for="item in items" :key="item.id" :user="item"/>
+ </div>
+ </template>
+ </MkPagination>
+ </MkFolder>
+
+ <MkFolder :default-open="true">
+ <template #label>{{ i18n.ts.popularUsers }}</template>
+
+ <MkPagination :pagination="popularUsers">
+ <template #default="{ items }">
+ <div :class="$style.users">
+ <XUser v-for="item in items" :key="item.id" :user="item"/>
+ </div>
+ </template>
+ </MkPagination>
+ </MkFolder>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from 'vue';
+import { instance } from '@/instance';
+import { i18n } from '@/i18n';
+import MkButton from '@/components/MkButton.vue';
+import MkFolder from '@/components/MkFolder.vue';
+import XUser from '@/components/MkUserSetupDialog.User.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import * as os from '@/os';
+import { $i } from '@/account';
+import MkPagination from '@/components/MkPagination.vue';
+
+const emit = defineEmits<{
+ (ev: 'done'): void;
+}>();
+
+const pinnedUsers = { endpoint: 'pinned-users', noPaging: true };
+
+const popularUsers = { endpoint: 'users', limit: 10, noPaging: true, params: {
+ state: 'alive',
+ origin: 'local',
+ sort: '+follower',
+} };
+</script>
+
+<style lang="scss" module>
+.users {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(230px, 1fr));
+ grid-gap: var(--margin);
+ justify-content: center;
+}
+</style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.Profile.stories.impl.ts
new file mode 100644
index 0000000000..f4930aa26b
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.stories.impl.ts
@@ -0,0 +1,31 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import MkUserSetupDialog_Profile from './MkUserSetupDialog.Profile.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkUserSetupDialog_Profile,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkUserSetupDialog_Profile v-bind="props" />',
+ };
+ },
+ args: {
+
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkUserSetupDialog_Profile>;
diff --git a/packages/frontend/src/components/MkUserSetupDialog.Profile.vue b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
new file mode 100644
index 0000000000..373e2cf8dc
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.Profile.vue
@@ -0,0 +1,101 @@
+<template>
+<div class="_gaps">
+ <MkInfo>{{ i18n.ts._initialAccountSetting.theseSettingsCanEditLater }}</MkInfo>
+
+ <FormSlot>
+ <template #label>{{ i18n.ts.avatar }}</template>
+ <div v-adaptive-bg :class="$style.avatarSection" class="_panel">
+ <MkAvatar :class="$style.avatar" :user="$i" @click="setAvatar"/>
+ <div style="margin-top: 16px;">
+ <MkButton primary rounded inline @click="setAvatar">{{ i18n.ts._profile.changeAvatar }}</MkButton>
+ </div>
+ </div>
+ </FormSlot>
+
+ <MkInput v-model="name" :max="30" manual-save>
+ <template #label>{{ i18n.ts._profile.name }}</template>
+ </MkInput>
+
+ <MkTextarea v-model="description" :max="500" tall manual-save>
+ <template #label>{{ i18n.ts._profile.description }}</template>
+ </MkTextarea>
+
+ <MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { computed, ref, watch } from 'vue';
+import { instance } from '@/instance';
+import { i18n } from '@/i18n';
+import MkButton from '@/components/MkButton.vue';
+import MkInput from '@/components/MkInput.vue';
+import MkTextarea from '@/components/MkTextarea.vue';
+import FormSlot from '@/components/form/slot.vue';
+import MkInfo from '@/components/MkInfo.vue';
+import { chooseFileFromPc } from '@/scripts/select-file';
+import * as os from '@/os';
+import { $i } from '@/account';
+
+const emit = defineEmits<{
+ (ev: 'done'): void;
+}>();
+
+const name = ref('');
+const description = ref('');
+
+watch(name, () => {
+ os.apiWithDialog('i/update', {
+ // 空文字列をnullにしたいので??は使うな
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ name: name.value || null,
+ });
+});
+
+watch(description, () => {
+ os.apiWithDialog('i/update', {
+ // 空文字列をnullにしたいので??は使うな
+ // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
+ description: description.value || null,
+ });
+});
+
+function setAvatar(ev) {
+ chooseFileFromPc(false).then(async (files) => {
+ const file = files[0];
+
+ let originalOrCropped = file;
+
+ const { canceled } = await os.confirm({
+ type: 'question',
+ text: i18n.t('cropImageAsk'),
+ okText: i18n.ts.cropYes,
+ cancelText: i18n.ts.cropNo,
+ });
+
+ if (!canceled) {
+ originalOrCropped = await os.cropImage(file, {
+ aspectRatio: 1,
+ });
+ }
+
+ const i = await os.apiWithDialog('i/update', {
+ avatarId: originalOrCropped.id,
+ });
+ $i.avatarId = i.avatarId;
+ $i.avatarUrl = i.avatarUrl;
+ });
+}
+</script>
+
+<style lang="scss" module>
+.avatarSection {
+ text-align: center;
+ padding: 20px;
+}
+
+.avatar {
+ width: 100px;
+ height: 100px;
+}
+</style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.User.stories.impl.ts
new file mode 100644
index 0000000000..7413f4884b
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.stories.impl.ts
@@ -0,0 +1,32 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import { userDetailed } from '../../.storybook/fakes';
+import MkUserSetupDialog_User from './MkUserSetupDialog.User.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkUserSetupDialog_User,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkUserSetupDialog_User v-bind="props" />',
+ };
+ },
+ args: {
+ user: userDetailed(),
+ },
+ parameters: {
+ layout: 'centered',
+ },
+} satisfies StoryObj<typeof MkUserSetupDialog_User>;
diff --git a/packages/frontend/src/components/MkUserSetupDialog.User.vue b/packages/frontend/src/components/MkUserSetupDialog.User.vue
new file mode 100644
index 0000000000..d66f34f165
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.User.vue
@@ -0,0 +1,101 @@
+<template>
+<div v-adaptive-bg class="_panel" style="position: relative;">
+ <div :class="$style.banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl})` : ''"></div>
+ <MkAvatar :class="$style.avatar" :user="user" indicator/>
+ <div :class="$style.title">
+ <div :class="$style.name"><MkUserName :user="user" :nowrap="false"/></div>
+ <p :class="$style.username"><MkAcct :user="user"/></p>
+ </div>
+ <div :class="$style.description">
+ <div v-if="user.description" :class="$style.mfm">
+ <Mfm :text="user.description" :author="user" :i="$i"/>
+ </div>
+ <span v-else style="opacity: 0.7;">{{ i18n.ts.noAccountDescription }}</span>
+ </div>
+ <div :class="$style.footer">
+ <MkButton v-if="!isFollowing" primary gradate rounded full @click="follow"><i class="ti ti-plus"></i> {{ i18n.ts.follow }}</MkButton>
+ <div v-else style="opacity: 0.7; text-align: center;">{{ i18n.ts.youFollowing }} <i class="ti ti-check"></i></div>
+ </div>
+</div>
+</template>
+
+<script lang="ts" setup>
+import * as misskey from 'misskey-js';
+import { ref } from 'vue';
+import MkButton from '@/components/MkButton.vue';
+import { i18n } from '@/i18n';
+import { $i } from '@/account';
+import * as os from '@/os';
+
+const props = defineProps<{
+ user: misskey.entities.UserDetailed;
+}>();
+
+const isFollowing = ref(false);
+
+async function follow() {
+ isFollowing.value = true;
+ os.api('following/create', {
+ userId: props.user.id,
+ });
+}
+</script>
+
+<style lang="scss" module>
+.banner {
+ height: 60px;
+ background-color: rgba(0, 0, 0, 0.1);
+ background-size: cover;
+ background-position: center;
+}
+
+.avatar {
+ display: block;
+ position: absolute;
+ top: 30px;
+ left: 13px;
+ z-index: 2;
+ width: 58px;
+ height: 58px;
+ border: solid 4px var(--panel);
+}
+
+.title {
+ display: block;
+ padding: 10px 0 10px 88px;
+}
+
+.name {
+ display: inline-block;
+ margin: 0;
+ font-weight: bold;
+ line-height: 16px;
+ word-break: break-all;
+}
+
+.username {
+ display: block;
+ margin: 0;
+ line-height: 16px;
+ font-size: 0.8em;
+ color: var(--fg);
+ opacity: 0.7;
+}
+
+.description {
+ padding: 0 16px 16px 88px;
+ font-size: 0.9em;
+}
+
+.mfm {
+ display: -webkit-box;
+ -webkit-line-clamp: 5;
+ -webkit-box-orient: vertical;
+ overflow: hidden;
+}
+
+.footer {
+ border-top: solid 0.5px var(--divider);
+ padding: 16px;
+}
+</style>
diff --git a/packages/frontend/src/components/MkUserSetupDialog.stories.impl.ts b/packages/frontend/src/components/MkUserSetupDialog.stories.impl.ts
new file mode 100644
index 0000000000..55790602d5
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.stories.impl.ts
@@ -0,0 +1,51 @@
+/* eslint-disable @typescript-eslint/explicit-function-return-type */
+import { StoryObj } from '@storybook/vue3';
+import { rest } from 'msw';
+import { commonHandlers } from '../../.storybook/mocks';
+import { userDetailed } from '../../.storybook/fakes';
+import MkUserSetupDialog from './MkUserSetupDialog.vue';
+export const Default = {
+ render(args) {
+ return {
+ components: {
+ MkUserSetupDialog,
+ },
+ setup() {
+ return {
+ args,
+ };
+ },
+ computed: {
+ props() {
+ return {
+ ...this.args,
+ };
+ },
+ },
+ template: '<MkUserSetupDialog v-bind="props" />',
+ };
+ },
+ args: {
+
+ },
+ parameters: {
+ layout: 'centered',
+ msw: {
+ handlers: [
+ ...commonHandlers,
+ rest.post('/api/users', (req, res, ctx) => {
+ return res(ctx.json([
+ userDetailed('44'),
+ userDetailed('49'),
+ ]));
+ }),
+ rest.post('/api/pinned-users', (req, res, ctx) => {
+ return res(ctx.json([
+ userDetailed('44'),
+ userDetailed('49'),
+ ]));
+ }),
+ ],
+ },
+ },
+} satisfies StoryObj<typeof MkUserSetupDialog>;
diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue
new file mode 100644
index 0000000000..58afe09b61
--- /dev/null
+++ b/packages/frontend/src/components/MkUserSetupDialog.vue
@@ -0,0 +1,135 @@
+<template>
+<MkModalWindow
+ ref="dialog"
+ :width="500"
+ :height="550"
+ @close="close"
+ @closed="emit('closed')"
+>
+ <template #header>{{ i18n.ts.initialAccountSetting }}</template>
+
+ <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="page === 0">
+ <div :class="$style.centerPage">
+ <MkSpacer :margin-min="20" :margin-max="28">
+ <div class="_gaps" style="text-align: center;">
+ <i class="ti ti-confetti" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+ <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.accountCreated }}</div>
+ <div>{{ i18n.ts._initialAccountSetting.letsFillYourProfile }}</div>
+ <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
+ </MkSpacer>
+ </div>
+ </template>
+ <template v-else-if="page === 1">
+ <div style="height: 100cqh; overflow: auto;">
+ <MkSpacer :margin-min="20" :margin-max="28">
+ <XProfile/>
+ <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </MkSpacer>
+ </div>
+ </template>
+ <template v-else-if="page === 2">
+ <div style="height: 100cqh; overflow: auto;">
+ <MkSpacer :margin-min="20" :margin-max="28">
+ <XFollow/>
+ <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </MkSpacer>
+ </div>
+ </template>
+ <template v-else-if="page === 3">
+ <div :class="$style.centerPage">
+ <MkSpacer :margin-min="20" :margin-max="28">
+ <div class="_gaps" style="text-align: center;">
+ <i class="ti ti-bell-ringing-2" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+ <div style="font-size: 120%;">{{ i18n.ts.pushNotification }}</div>
+ <div style="padding: 0 16px;">{{ i18n.t('_initialAccountSetting.pushNotificationDescription', { name: instance.name ?? host }) }}</div>
+ <MkPushNotificationAllowButton primary show-only-to-register style="margin: 0 auto;"/>
+ <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
+ </div>
+ </MkSpacer>
+ </div>
+ </template>
+ <template v-else-if="page === 4">
+ <div :class="$style.centerPage">
+ <MkSpacer :margin-min="20" :margin-max="28">
+ <div class="_gaps" style="text-align: center;">
+ <i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
+ <div style="font-size: 120%;">{{ i18n.ts._initialAccountSetting.initialAccountSettingCompleted }}</div>
+ <I18n :src="i18n.ts._initialAccountSetting.ifYouNeedLearnMore" tag="div" style="padding: 0 16px;">
+ <template #name>{{ instance.name ?? host }}</template>
+ <template #link>
+ <a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
+ </template>
+ </I18n>
+ <div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
+ <MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="close">{{ i18n.ts.close }}</MkButton>
+ </div>
+ </MkSpacer>
+ </div>
+ </template>
+ </Transition>
+ </div>
+</MkModalWindow>
+</template>
+
+<script lang="ts" setup>
+import { ref, shallowRef, watch } from 'vue';
+import MkModalWindow from '@/components/MkModalWindow.vue';
+import MkButton from '@/components/MkButton.vue';
+import XProfile from '@/components/MkUserSetupDialog.Profile.vue';
+import XFollow from '@/components/MkUserSetupDialog.Follow.vue';
+import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+import { host } from '@/config';
+import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
+import { defaultStore } from '@/store';
+
+const emit = defineEmits<{
+ (ev: 'closed'): void;
+}>();
+
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+
+const page = ref(defaultStore.state.accountSetupWizard);
+
+watch(page, () => {
+ defaultStore.set('accountSetupWizard', page.value);
+});
+
+function close() {
+ dialog.value.close();
+ defaultStore.set('accountSetupWizard', -1);
+}
+</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);
+}
+
+.centerPage {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ height: 100cqh;
+ padding-bottom: 30px;
+ box-sizing: border-box;
+}
+</style>
diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts
index 91a009e8f8..13fbd08c56 100644
--- a/packages/frontend/src/init.ts
+++ b/packages/frontend/src/init.ts
@@ -343,6 +343,10 @@ if ($i) {
// only add post shortcuts if logged in
hotkeys['p|n'] = post;
+ if (defaultStore.state.accountSetupWizard !== -1) {
+ popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
+ }
+
if ($i.isDeleted) {
alert({
type: 'warning',
diff --git a/packages/frontend/src/pages/timeline.tutorial.vue b/packages/frontend/src/pages/timeline.tutorial.vue
index 0d0c932a5c..32228d28f4 100644
--- a/packages/frontend/src/pages/timeline.tutorial.vue
+++ b/packages/frontend/src/pages/timeline.tutorial.vue
@@ -1,7 +1,7 @@
<template>
<div :class="$style.container">
<div :class="$style.title">
- <div :class="$style.titleText"><i class="ti ti-info-circle"></i> {{ i18n.ts._tutorial.title }}</div>
+ <div :class="$style.titleText"><i class="ti ti-info-circle"></i> {{ i18n.ts._timelineTutorial.title }}</div>
<div :class="$style.step">
<button class="_button" :class="$style.stepArrow" :disabled="tutorial === 0" @click="tutorial--">
<i class="ti ti-chevron-left"></i>
@@ -12,66 +12,30 @@
</button>
</div>
</div>
+
<div v-if="tutorial === 0" :class="$style.body">
- <div>{{ i18n.ts._tutorial.step1_1 }}</div>
- <div>{{ i18n.ts._tutorial.step1_2 }}</div>
- <div>{{ i18n.ts._tutorial.step1_3 }}</div>
+ <div>{{ i18n.t('_timelineTutorial.step1_1', { name: instance.name ?? host }) }}</div>
+ <div>{{ i18n.t('_timelineTutorial.step1_2', { name: instance.name ?? host }) }}</div>
</div>
<div v-else-if="tutorial === 1" :class="$style.body">
- <div>{{ i18n.ts._tutorial.step2_1 }}</div>
- <div>{{ i18n.ts._tutorial.step2_2 }}</div>
- <MkA class="_link" to="/settings/profile">{{ i18n.ts.editProfile }}</MkA>
+ <div>{{ i18n.ts._timelineTutorial.step2_1 }}</div>
+ <div>{{ i18n.t('_timelineTutorial.step2_2', { name: instance.name ?? host }) }}</div>
</div>
<div v-else-if="tutorial === 2" :class="$style.body">
- <div>{{ i18n.ts._tutorial.step3_1 }}</div>
- <div>{{ i18n.ts._tutorial.step3_2 }}</div>
- <div>{{ i18n.ts._tutorial.step3_3 }}</div>
- <small :class="$style.small">{{ i18n.ts._tutorial.step3_4 }}</small>
+ <div>{{ i18n.ts._timelineTutorial.step3_1 }}</div>
+ <div>{{ i18n.ts._timelineTutorial.step3_2 }}</div>
</div>
<div v-else-if="tutorial === 3" :class="$style.body">
- <div>{{ i18n.ts._tutorial.step4_1 }}</div>
- <div>{{ i18n.ts._tutorial.step4_2 }}</div>
- </div>
- <div v-else-if="tutorial === 4" :class="$style.body">
- <div>{{ i18n.ts._tutorial.step5_1 }}</div>
- <I18n :src="i18n.ts._tutorial.step5_2" tag="div">
- <template #featured>
- <MkA class="_link" to="/explore">{{ i18n.ts.featured }}</MkA>
- </template>
- <template #explore>
- <MkA class="_link" to="/explore#users">{{ i18n.ts.explore }}</MkA>
- </template>
- </I18n>
- <div>{{ i18n.ts._tutorial.step5_3 }}</div>
- <small :class="$style.small">{{ i18n.ts._tutorial.step5_4 }}</small>
- </div>
- <div v-else-if="tutorial === 5" :class="$style.body">
- <div>{{ i18n.ts._tutorial.step6_1 }}</div>
- <div>{{ i18n.ts._tutorial.step6_2 }}</div>
- <div>{{ i18n.ts._tutorial.step6_3 }}</div>
- </div>
- <div v-else-if="tutorial === 6" :class="$style.body">
- <div>{{ i18n.ts._tutorial.step7_1 }}</div>
- <I18n :src="i18n.ts._tutorial.step7_2" tag="div">
- <template #help>
- <a href="https://misskey-hub.net/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a>
- </template>
- </I18n>
- <div>{{ i18n.ts._tutorial.step7_3 }}</div>
- </div>
- <div v-else-if="tutorial === 7" :class="$style.body">
- <div>{{ i18n.ts._tutorial.step8_1 }}</div>
- <div>{{ i18n.ts._tutorial.step8_2 }}</div>
- <small :class="$style.small">{{ i18n.ts._tutorial.step8_3 }}</small>
+ <div>{{ i18n.ts._timelineTutorial.step4_1 }}</div>
+ <div>{{ i18n.ts._timelineTutorial.step4_2 }}</div>
</div>
<div :class="$style.footer">
<template v-if="tutorial === tutorialsNumber - 1">
- <MkPushNotificationAllowButton :class="$style.footerItem" primary show-only-to-register @click="tutorial = -1"/>
- <MkButton :class="$style.footerItem" :primary="false" @click="tutorial = -1">{{ i18n.ts.noThankYou }}</MkButton>
+ <MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial = -1">{{ i18n.ts.done }} <i class="ti ti-check"></i></MkButton>
</template>
<template v-else>
- <MkButton :class="$style.footerItem" primary @click="tutorial++"><i class="ti ti-check"></i> {{ i18n.ts.next }}</MkButton>
+ <MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial++">{{ i18n.ts.next }} <i class="ti ti-arrow-right"></i></MkButton>
</template>
</div>
</div>
@@ -80,15 +44,16 @@
<script lang="ts" setup>
import { computed } from 'vue';
import MkButton from '@/components/MkButton.vue';
-import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { defaultStore } from '@/store';
import { i18n } from '@/i18n';
+import { instance } from '@/instance';
+import { host } from '@/config';
-const tutorialsNumber = 8;
+const tutorialsNumber = 4;
const tutorial = computed({
- get() { return defaultStore.reactiveState.tutorial.value || 0; },
- set(value) { defaultStore.set('tutorial', value); },
+ get() { return defaultStore.reactiveState.timelineTutorial.value || 0; },
+ set(value) { defaultStore.set('timelineTutorial', value); },
});
</script>
diff --git a/packages/frontend/src/pages/timeline.vue b/packages/frontend/src/pages/timeline.vue
index 9f13f7a1dd..1bf4cdc99a 100644
--- a/packages/frontend/src/pages/timeline.vue
+++ b/packages/frontend/src/pages/timeline.vue
@@ -3,7 +3,7 @@
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :display-my-avatar="true"/></template>
<MkSpacer :content-max="800">
<div ref="rootEl" v-hotkey.global="keymap">
- <XTutorial v-if="$i && defaultStore.reactiveState.tutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
+ <XTutorial v-if="$i && defaultStore.reactiveState.timelineTutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index c64d40edcc..245bcbefe1 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -38,7 +38,11 @@ export const pageViewInterruptors: PageViewInterruptor[] = [];
// TODO: それぞれいちいちwhereとかdefaultというキーを付けなきゃいけないの冗長なのでなんとかする(ただ型定義が面倒になりそう)
// あと、現行の定義の仕方なら「whereが何であるかに関わらずキー名の重複不可」という制約を付けられるメリットもあるからそのメリットを引き継ぐ方法も考えないといけない
export const defaultStore = markRaw(new Storage('base', {
- tutorial: {
+ accountSetupWizard: {
+ where: 'account',
+ default: 0,
+ },
+ timelineTutorial: {
where: 'account',
default: 0,
},