feat: チュートリアルを独立ページにして初期設定ウィザードと統合

This commit is contained in:
kakkokari-gtyih 2024-02-26 16:56:57 +09:00
parent 30fe072606
commit 3604c470aa
30 changed files with 935 additions and 943 deletions

165
locales/index.d.ts vendored
View File

@ -4884,6 +4884,14 @@ export interface Locale extends ILocale {
*
*/
"enableHorizontalSwipe": string;
/**
*
*/
"prohibitSkippingTutorial": string;
/**
*
*/
"prohibitSkippingTutorialDescription": string;
"_bubbleGame": {
/**
*
@ -4954,68 +4962,6 @@ export interface Locale extends ILocale {
*/
"silenceDescription": string;
};
"_initialAccountSetting": {
/**
*
*/
"accountCreated": string;
/**
*
*/
"letsStartAccountSetup": string;
/**
*
*/
"letsFillYourProfile": string;
/**
*
*/
"profileSetting": string;
/**
*
*/
"privacySetting": string;
/**
*
*/
"theseSettingsCanEditLater": string;
/**
*
*/
"youCanEditMoreSettingsInSettingsPageLater": string;
/**
*
*/
"followUsers": string;
/**
* {name}使
*/
"pushNotificationDescription": ParameterizedString<"name">;
/**
*
*/
"initialAccountSettingCompleted": string;
/**
* {name}
*/
"haveFun": ParameterizedString<"name">;
/**
* {name}(Misskey)使使
*/
"youCanContinueTutorial": ParameterizedString<"name">;
/**
*
*/
"startTutorial": string;
/**
*
*/
"skipAreYouSure": string;
/**
*
*/
"laterAreYouSure": string;
};
"_initialTutorial": {
/**
*
@ -5129,6 +5075,16 @@ export interface Locale extends ILocale {
*/
"description3": ParameterizedString<"link">;
};
"_followUsers": {
/**
*
*/
"description1": string;
/**
*
*/
"description2": string;
};
"_postNote": {
/**
* 稿
@ -5229,6 +5185,30 @@ export interface Locale extends ILocale {
*/
"doItToContinue": string;
};
"_pushNotification": {
/**
* {name}使
*/
"description": ParameterizedString<"name">;
};
"_privacySettings": {
/**
*
*/
"title": string;
/**
*
*/
"description1": string;
/**
*
*/
"theseSettingsCanEditLater": string;
/**
*
*/
"youCanEditMoreSettingsInSettingsPageLater": string;
};
"_done": {
/**
* 🎉
@ -5238,6 +5218,67 @@ export interface Locale extends ILocale {
* Misskeyの使い方をより詳しく知るには{link}
*/
"description": ParameterizedString<"link">;
/**
* {name}
*/
"haveFun": ParameterizedString<"name">;
/**
*
*/
"youCanReferTutorialBy": string;
};
"_onboardingLanding": {
/**
*
*/
"accountCreated": string;
/**
* {name}
*/
"welcomeToX": ParameterizedString<"name">;
/**
* {name}使💦Misskeyの基本的な使い方を学びましょう
*/
"description": ParameterizedString<"name">;
/**
* {min}
*
*/
"takesAbout": ParameterizedString<"min">;
};
"_onboardingDone": {
/**
* {name}
*/
"description": ParameterizedString<"name">;
/**
*
*/
"backToOriginalPath": string;
/**
*
*/
"backToOriginalPathDescription": string;
/**
*
*/
"profile": string;
/**
*
*/
"profileDescription": string;
/**
*
*/
"exploreDescription": string;
/**
*
*/
"goToTimeline": string;
/**
*
*/
"goToTimelineDescription": string;
};
};
"_timelineDescription": {

View File

@ -1217,6 +1217,8 @@ hemisphere: "お住まいの地域"
withSensitive: "センシティブなファイルを含むノートを表示"
userSaysSomethingSensitive: "{name}のセンシティブなファイルを含む投稿"
enableHorizontalSwipe: "スワイプしてタブを切り替える"
prohibitSkippingTutorial: "チュートリアルをスキップできないようにする"
prohibitSkippingTutorialDescription: "新規登録したユーザーに表示されるチュートリアルをスキップできないようにします。チュートリアルを完了せずチュートリアルページを回避した場合でも、強制的にリダイレクトされます。"
_bubbleGame:
howToPlay: "遊び方"
@ -1239,23 +1241,6 @@ _announcement:
silence: "非通知"
silenceDescription: "オンにすると、このお知らせは通知されず、既読にする必要もなくなります。"
_initialAccountSetting:
accountCreated: "アカウントの作成が完了しました!"
letsStartAccountSetup: "さっそくアカウントの初期設定を行いましょう。"
letsFillYourProfile: "まずはあなたのプロフィールを設定しましょう。"
profileSetting: "プロフィール設定"
privacySetting: "プライバシー設定"
theseSettingsCanEditLater: "これらの設定は後から変更できます。"
youCanEditMoreSettingsInSettingsPageLater: "この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。"
followUsers: "タイムラインを構築するため、気になるユーザーをフォローしてみましょう。"
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。"
initialAccountSettingCompleted: "初期設定が完了しました!"
haveFun: "{name}をお楽しみください!"
youCanContinueTutorial: "このまま{name}(Misskey)の使い方についてのチュートリアルに進むこともできますが、ここで中断してすぐに使い始めることもできます。"
startTutorial: "チュートリアルを開始"
skipAreYouSure: "初期設定をスキップしますか?"
laterAreYouSure: "初期設定をあとでやり直しますか?"
_initialTutorial:
launchTutorial: "チュートリアルを見る"
title: "チュートリアル"
@ -1287,6 +1272,9 @@ _initialTutorial:
global: "接続している他のすべてのサーバーからの投稿を見られます。"
description2: "それぞれのタイムラインは、画面上部でいつでも切り替えられます。"
description3: "その他にも、リストタイムラインやチャンネルタイムラインなどがあります。詳しくは{link}をご覧ください。"
_followUsers:
description1: "誰もフォローしていない状態だと、ホームタイムラインには何も表示されません。"
description2: "タイムラインを構築するため、気になるユーザーをフォローしてみましょう。"
_postNote:
title: "ノートの投稿設定"
description1: "Misskeyにートを投稿する際には、様々なオプションの設定が可能です。投稿フォームはこのようになっています。"
@ -1315,9 +1303,32 @@ _initialTutorial:
method: "添付ファイルをセンシティブにする際は、そのファイルをクリックしてメニューを開き、「センシティブとして設定」をクリックします。"
sensitiveSucceeded: "ファイルを添付する際は、サーバーのガイドラインに従ってセンシティブを適切に設定してください。"
doItToContinue: "画像をセンシティブに設定すると先に進めるようになります。"
_pushNotification:
description: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。"
_privacySettings:
title: "プライバシー設定"
description1: "多くのユーザーが利用しているプライバシー関連の設定項目をリストアップしました。必要に応じて変更してください。"
theseSettingsCanEditLater: "これらの設定は後から変更できます。"
youCanEditMoreSettingsInSettingsPageLater: "この他にも様々な設定を「設定」ページから行えます。ぜひ後で確認してみてください。"
_done:
title: "チュートリアルは終了です🎉"
description: "ここで紹介した機能はほんの一部にすぎません。Misskeyの使い方をより詳しく知るには、{link}をご覧ください。"
haveFun: "{name}をお楽しみください!"
youCanReferTutorialBy: "このチュートリアルは、「もっと!」→「情報」→「チュートリアルを見る」からいつでも見返すことができます。"
_onboardingLanding:
accountCreated: "アカウントの作成が完了しました!"
welcomeToX: "ようこそ、{name}へ!"
description: "「{name}に登録したは良いものの、どう使えばいいか分からない…💦」といったことを防ぐために、まずはMisskeyの基本的な使い方を学びましょう。"
takesAbout: "このチュートリアルの所要時間は{min}分程度です。\nチュートリアルを完了すると実績が解除されます。"
_onboardingDone:
description: "お疲れ様でした!次のステップに進んで、{name}をもっと楽しめるようにしましょう。"
backToOriginalPath: "元のページに戻る"
backToOriginalPathDescription: "あなたがアクセスしようとしていたページに戻ります。"
profile: "プロフィール設定"
profileDescription: "他のユーザーが親しみやすいように、プロフィールをつくりましょう。"
exploreDescription: "人気のノートやユーザーを見つけて交流をはじめましょう。"
goToTimeline: "ホーム画面に進む"
goToTimelineDescription: "設定等を行わず、通常のホーム画面(タイムライン)に進みます。"
_timelineDescription:
home: "ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。"

View File

@ -406,8 +406,6 @@ function toStories(component: string): Promise<string> {
glob('src/components/MkDigitalClock.vue'),
glob('src/components/MkGalleryPostPreview.vue'),
glob('src/components/MkSignupServerRules.vue'),
glob('src/components/MkUserSetupDialog.vue'),
glob('src/components/MkUserSetupDialog.*.vue'),
glob('src/components/MkInviteCode.vue'),
glob('src/pages/user/home.vue'),
]);

View File

@ -97,6 +97,7 @@
"@storybook/vue3": "8.0.0-beta.2",
"@storybook/vue3-vite": "8.0.0-beta.2",
"@testing-library/vue": "8.0.2",
"@types/canvas-confetti": "^1.6.4",
"@types/escape-regexp": "0.0.3",
"@types/estree": "1.0.5",
"@types/matter-js": "0.19.6",

View File

@ -10,7 +10,7 @@ import '@/style.scss';
import { mainBoot } from '@/boot/main-boot.js';
import { subBoot } from '@/boot/sub-boot.js';
const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete'];
const subBootPaths = ['/share', '/auth', '/miauth', '/signup-complete', '/onboarding'];
if (subBootPaths.some(i => location.pathname === i || location.pathname.startsWith(i + '/'))) {
subBoot();

View File

@ -21,6 +21,7 @@ import { getUrlWithoutLoginId } from '@/scripts/login-id.js';
import { getAccountFromId } from '@/scripts/get-account-from-id.js';
import { deckStore } from '@/ui/deck/deck-store.js';
import { miLocalStorage } from '@/local-storage.js';
import { claimedAchievements } from '@/scripts/achievements.js';
import { fetchCustomEmojis } from '@/custom-emojis.js';
import { setupRouter } from '@/router/definition.js';
@ -118,6 +119,14 @@ export async function common(createVue: () => App<Element>) {
await defaultStore.ready;
await deckStore.ready;
// 2024年3月1日JST以降に作成されたアカウントで、チュートリアル完了していない場合、チュートリアルにリダイレクト
if ($i && new Date($i.createdAt).getTime() >= 1709218800000 && !claimedAchievements.includes('tutorialCompleted') && !location.pathname.startsWith('/onboarding') && !location.pathname.startsWith('/signup-complete')) {
const param = new URLSearchParams();
param.set('redirected_from', location.pathname + location.search + location.hash);
location.replace('/onboarding?' + param.toString());
return;
}
const fetchInstanceMetaPromise = fetchInstance();
fetchInstanceMetaPromise.then(() => {

View File

@ -102,12 +102,6 @@ export async function mainBoot() {
// only add post shortcuts if logged in
hotkeys['p|n'] = post;
defaultStore.loaded.then(() => {
if (defaultStore.state.accountSetupWizard !== -1) {
popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed');
}
});
for (const announcement of ($i.unreadAnnouncements ?? []).filter(x => x.display === 'dialog')) {
popup(defineAsyncComponent(() => import('@/components/MkAnnouncementDialog.vue')), {
announcement,

View File

@ -5,7 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<div style="text-align: center;">{{ i18n.ts._initialAccountSetting.followUsers }}</div>
<div style="word-break: auto-phrase; text-align: center;">{{ i18n.ts._initialTutorial._followUsers.description1 }}</div>
<div style="word-break: auto-phrase; text-align: center;">{{ i18n.ts._initialTutorial._followUsers.description2 }}</div>
<MkFolder :defaultOpen="true">
<template #label>{{ i18n.ts.recommended }}</template>
@ -37,7 +38,7 @@ SPDX-License-Identifier: AGPL-3.0-only
import * as Misskey from 'misskey-js';
import { i18n } from '@/i18n.js';
import MkFolder from '@/components/MkFolder.vue';
import XUser from '@/components/MkUserSetupDialog.User.vue';
import XUser from '@/components/MkTutorial.FollowUsers.UserCard.vue';
import MkPagination, { type Paging } from '@/components/MkPagination.vue';
const pinnedUsers: Paging = {

View File

@ -5,8 +5,8 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div v-if="phase === 'aboutNote'" class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._note.description }}</div>
<MkNote :class="$style.exampleNoteRoot" style="pointer-events: none;" :note="exampleNote" :mock="true"/>
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._note.description }}</div>
<MkNote tabindex="-1" :class="$style.exampleNoteRoot" style="pointer-events: none;" :note="exampleNote" :mock="true"/>
<div class="_gaps_s">
<div><i class="ti ti-arrow-back-up"></i> <b>{{ i18n.ts.reply }}</b> {{ i18n.ts._initialTutorial._note.reply }}</div>
<div><i class="ti ti-repeat"></i> <b>{{ i18n.ts.renote }}</b> {{ i18n.ts._initialTutorial._note.renote }}</div>
@ -15,7 +15,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</div>
</div>
<div v-else-if="phase === 'howToReact'" class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._reaction.description }}</div>
<div>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div>
<MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction"/>
<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._reaction.reactNotification }}<br>{{ i18n.ts._initialTutorial._reaction.reactDone }}</div>

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._postNote.description1 }}</div>
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._postNote.description1 }}</div>
<MkPostForm :class="$style.exampleRoot" :mock="true" :autofocus="false"/>
<MkFormSection>
<template #label>{{ i18n.ts.visibility }}</template>

View File

@ -0,0 +1,48 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps">
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._privacySettings.description1 }}</div>
<MkInfo>{{ i18n.ts._initialTutorial._privacySettings.theseSettingsCanEditLater }}</MkInfo>
<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
<MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
<MkInfo>{{ i18n.ts._initialTutorial._privacySettings.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { i18n } from '@/i18n.js';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
const isLocked = ref(false);
const hideOnlineStatus = ref(false);
const noCrawle = ref(false);
const preventAiLearning = ref(true);
watch([isLocked, hideOnlineStatus, noCrawle, preventAiLearning], () => {
misskeyApi('i/update', {
isLocked: !!isLocked.value,
hideOnlineStatus: !!hideOnlineStatus.value,
noCrawle: !!noCrawle.value,
preventAiLearning: !!preventAiLearning.value,
});
});
</script>
<style lang="scss" module>
</style>

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.description }}</div>
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.description }}</div>
<div>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.tryThisFile }}</div>
<MkInfo>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.method }}</MkInfo>
<MkPostForm

View File

@ -5,7 +5,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div class="_gaps">
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._timeline.description1 }}</div>
<div style="word-break: auto-phrase; text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._timeline.description1 }}</div>
<div class="_gaps_s">
<div><i class="ti ti-home"></i> <b>{{ i18n.ts._timelines.home }}</b> {{ i18n.ts._initialTutorial._timeline.home }}</div>
<div><i class="ti ti-planet"></i> <b>{{ i18n.ts._timelines.local }}</b> {{ i18n.ts._initialTutorial._timeline.local }}</div>
@ -22,7 +22,6 @@ SPDX-License-Identifier: AGPL-3.0-only
<a href="https://misskey-hub.net/docs/for-users/features/timeline/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
</div>
</template>

View File

@ -0,0 +1,326 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="$style.tutorialRoot">
<div v-if="showProgressbar" :class="$style.progressBar">
<div :class="$style.progressBarValue" :style="{ width: `${(page / MAX_PAGE) * 100}%` }"></div>
</div>
<div v-if="showProgressbar && page !== 0 && page !== MAX_PAGE" :class="$style.progressText">{{ page }}/{{ MAX_PAGE - 1 }}</div>
<div :class="$style.tutorialMain">
<Transition
mode="out-in"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
>
<slot v-if="page === 0" key="tutorialPage_0" name="welcome" :close="() => emit('close', true)" :next="next">
<div :class="$style.centerPage">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="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._initialTutorial._landing.title }}</div>
<div>{{ i18n.ts._initialTutorial._landing.description }}</div>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="next">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
<MkButton v-if="skippable" style="margin: 0 auto;" transparent rounded @click="emit('close', true)">{{ i18n.ts.close }}</MkButton>
</div>
</MkSpacer>
</div>
</slot>
<div v-else-if="page === 1" key="tutorialPage_1" :class="$style.pageContainer">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XNote phase="aboutNote"/>
</MkSpacer>
</div>
</div>
<div v-else-if="page === 2" key="tutorialPage_2" :class="$style.pageContainer">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<div class="_gaps">
<XNote phase="howToReact" @reacted="isReactionTutorialPushed = true"/>
<b v-if="!isReactionTutorialPushed">{{ i18n.ts._initialTutorial._reaction.reactToContinue }}</b>
</div>
</MkSpacer>
</div>
</div>
<div v-else-if="page === 3" key="tutorialPage_3" :class="$style.pageContainer">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XTimeline/>
</MkSpacer>
</div>
</div>
<div v-else-if="page === 4" key="tutorialPage_4" :class="$style.pageContainer">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XFollowUsers/>
</MkSpacer>
</div>
</div>
<div v-else-if="page === 5" key="tutorialPage_5" :class="$style.pageContainer">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XPostNote/>
</MkSpacer>
</div>
</div>
<div v-else-if="page === 6" key="tutorialPage_6" :class="$style.pageContainer">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<div class="_gaps">
<XSensitive @succeeded="isSensitiveTutorialSucceeded = true"/>
<b v-if="!isSensitiveTutorialSucceeded">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.doItToContinue }}</b>
</div>
</MkSpacer>
</div>
</div>
<div v-else-if="page === 7" key="tutorialPage_7" :class="$style.centerPage">
<MkSpacer :marginMin="20" :marginMax="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="word-break: auto-phrase; padding: 0 16px;">{{ i18n.tsx._initialTutorial._pushNotification.description({ name: instance.name ?? host }) }}</div>
<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
</div>
</MkSpacer>
</div>
<div v-else-if="page === 8" key="tutorialPage_8" :class="$style.pageContainer">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<div class="_gaps">
<XPrivacySettings/>
</div>
</MkSpacer>
</div>
</div>
<slot v-else-if="page === 9" key="tutorialPage_9" name="finish" :close="() => emit('close')" :prev="prev">
<div :class="$style.centerPage">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps">
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
<div style="text-align: center; font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="text-align: center; padding: 0 16px;">
<template #link>
<a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
<div style="text-align: center;">{{ i18n.ts._initialTutorial._done.youCanReferTutorialBy }}</div>
<div style="text-align: center;">{{ i18n.tsx._initialTutorial._done.haveFun({ name: instance.name ?? host }) }}</div>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton v-if="initialPage !== 4" rounded @click="prev"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton rounded primary gradate @click="emit('close')">{{ i18n.ts.close }}</MkButton>
</div>
</div>
</MkSpacer>
</div>
</slot>
</Transition>
</div>
<div :class="[$style.pageFooter, { [$style.pageFooterShown]: (page > 0 && page < MAX_PAGE) }]">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== page" rounded @click="prev"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate :disabled="!canContinue" @click="next">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch } from 'vue';
import MkButton from '@/components/MkButton.vue';
import XNote from '@/components/MkTutorial.Note.vue';
import XTimeline from '@/components/MkTutorial.Timeline.vue';
import XFollowUsers from '@/components/MkTutorial.FollowUsers.vue';
import XPostNote from '@/components/MkTutorial.PostNote.vue';
import XSensitive from '@/components/MkTutorial.Sensitive.vue';
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import XPrivacySettings from '@/components/MkTutorial.PrivacySettings.vue';
import MkAnimBg from '@/components/MkAnimBg.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { host } from '@/config.js';
import { claimAchievement } from '@/scripts/achievements.js';
const props = defineProps<{
initialPage?: number;
showProgressbar?: boolean;
skippable?: boolean;
withSetup?: boolean;
}>();
const emit = defineEmits<{
(ev: 'pageChanged', to: number): void;
(ev: 'close', withConfirm?: boolean): void;
}>();
// eslint-disable-next-line vue/no-setup-props-destructure
const page = ref(props.initialPage ?? 0);
//
const MAX_PAGE = 9;
watch(page, (to) => {
if (to === MAX_PAGE) {
claimAchievement('tutorialCompleted');
}
});
const isReactionTutorialPushed = ref<boolean>(false);
const isSensitiveTutorialSucceeded = ref<boolean>(false);
const canContinue = computed(() => {
if (page.value === 2) {
return isReactionTutorialPushed.value;
} else if (page.value === 6) {
return isSensitiveTutorialSucceeded.value;
} else {
return true;
}
});
function next() {
if (page.value === 3 && !props.withSetup) {
page.value += 2;
} else if (page.value === 6 && !props.withSetup) {
page.value += 3;
} else {
page.value++;
}
emit('pageChanged', page.value);
}
function prev() {
if (page.value === 5 && !props.withSetup) {
page.value -= 2;
} else if (page.value === 8 && !props.withSetup) {
page.value -= 3;
} else {
page.value--;
}
emit('pageChanged', page.value);
}
</script>
<style lang="scss" module>
.tutorialRoot {
box-sizing: border-box;
overflow: hidden;
width: 100%;
height: 100%;
}
.tutorialMain {
position: relative;
width: 100%;
height: 100%;
}
.progressBar {
position: absolute;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 4px;
}
.progressBarValue {
height: 100%;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
transition: all 0.5s cubic-bezier(0,.5,.5,1);
}
.progressText {
position: absolute;
top: 1em;
right: 1em;
font-size: 0.8em;
opacity: 0.7;
pointer-events: none;
}
.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);
}
.progressBar {
position: absolute;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 4px;
}
.progressBarValue {
height: 100%;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
transition: all 0.5s cubic-bezier(0,.5,.5,1);
}
.centerPage {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
box-sizing: border-box;
}
.pageContainer {
width: 100%;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
}
.pageRoot {
display: flex;
flex-direction: column;
min-height: 100%;
}
.pageMain {
flex-grow: 1;
line-height: 1.5;
margin-bottom: 56px;
}
.pageFooter {
position: sticky;
bottom: 0;
left: 0;
width: 100%;
height: 56px;
box-sizing: border-box;
flex-shrink: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
background: var(--acrylicBg);
transition: transform 0.3s cubic-bezier(0,0,.35,1);
transform: translateY(100%);
visibility: hidden;
&.pageFooterShown {
transform: translateY(0);
visibility: visible;
}
}
</style>

View File

@ -14,152 +14,24 @@ SPDX-License-Identifier: AGPL-3.0-only
<template v-if="page === 1" #header><i class="ti ti-pencil"></i> {{ i18n.ts._initialTutorial._note.title }}</template>
<template v-else-if="page === 2" #header><i class="ti ti-mood-smile"></i> {{ i18n.ts._initialTutorial._reaction.title }}</template>
<template v-else-if="page === 3" #header><i class="ti ti-home"></i> {{ i18n.ts._initialTutorial._timeline.title }}</template>
<template v-else-if="page === 4" #header><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template>
<template v-else-if="page === 5" #header><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template>
<template v-else-if="page === 5" #header><i class="ti ti-pencil-plus"></i> {{ i18n.ts._initialTutorial._postNote.title }}</template>
<template v-else-if="page === 6" #header><i class="ti ti-eye-exclamation"></i> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.title }}</template>
<template v-else #header>{{ i18n.ts._initialTutorial.title }}</template>
<div style="overflow-x: clip;">
<Transition
mode="out-in"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
>
<template v-if="page === 0">
<div :class="$style.centerPage">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="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._initialTutorial._landing.title }}</div>
<div>{{ i18n.ts._initialTutorial._landing.description }}</div>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" @click="page++">{{ i18n.ts._initialTutorial.launchTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
<MkButton style="margin: 0 auto;" transparent rounded @click="close(true)">{{ i18n.ts.close }}</MkButton>
</div>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 1">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XNote phase="aboutNote"/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== 1" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 2">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<div class="_gaps">
<XNote phase="howToReact" @reacted="isReactionTutorialPushed = true"/>
<div v-if="!isReactionTutorialPushed">{{ i18n.ts._initialTutorial._reaction.reactToContinue }}</div>
</div>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate :disabled="!isReactionTutorialPushed" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 3">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XTimeline/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 4">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XPostNote/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== 3" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 5">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<div class="_gaps">
<XSensitive @succeeded="isSensitiveTutorialSucceeded = true"/>
<div v-if="!isSensitiveTutorialSucceeded">{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.doItToContinue }}</div>
</div>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton v-if="initialPage !== 2" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate :disabled="!isSensitiveTutorialSucceeded" @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 6">
<div :class="$style.centerPage">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="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._initialTutorial._done.title }}</div>
<I18n :src="i18n.ts._initialTutorial._done.description" tag="div" style="padding: 0 16px;">
<template #link>
<a href="https://misskey-hub.net/docs/for-users/" target="_blank" class="_link">{{ i18n.ts.help }}</a>
</template>
</I18n>
<div>{{ i18n.tsx._initialAccountSetting.haveFun({ name: instance.name ?? host }) }}</div>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton v-if="initialPage !== 4" rounded @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton rounded primary gradate @click="close(false)">{{ i18n.ts.close }}</MkButton>
</div>
</div>
</MkSpacer>
</div>
</template>
</Transition>
</div>
<XTutorial
:initialPage="initialPage"
:skippable="true"
@pageChanged="handlePageChange"
@close="close"
/>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { ref, shallowRef, watch } from 'vue';
import { ref, shallowRef } from 'vue';
import MkModalWindow from '@/components/MkModalWindow.vue';
import MkButton from '@/components/MkButton.vue';
import XNote from '@/components/MkTutorialDialog.Note.vue';
import XTimeline from '@/components/MkTutorialDialog.Timeline.vue';
import XPostNote from '@/components/MkTutorialDialog.PostNote.vue';
import XSensitive from '@/components/MkTutorialDialog.Sensitive.vue';
import MkAnimBg from '@/components/MkAnimBg.vue';
import XTutorial from '@/components/MkTutorial.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { host } from '@/config.js';
import { claimAchievement } from '@/scripts/achievements.js';
import * as os from '@/os.js';
const props = defineProps<{
@ -175,17 +47,11 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
// eslint-disable-next-line vue/no-setup-props-destructure
const page = ref(props.initialPage ?? 0);
watch(page, (to) => {
//
if (to === 6) {
claimAchievement('tutorialCompleted');
function handlePageChange(to: number) {
page.value = to;
}
});
const isReactionTutorialPushed = ref<boolean>(false);
const isSensitiveTutorialSucceeded = ref<boolean>(false);
async function close(skip: boolean) {
async function close(skip?: boolean) {
if (skip) {
const { canceled } = await os.confirm({
type: 'warning',

View File

@ -1,56 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js';
import { userDetailed } from '../../.storybook/fakes.js';
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,
http.post('/api/users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]);
}),
http.post('/api/pinned-users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]);
}),
],
},
},
} satisfies StoryObj<typeof MkUserSetupDialog_Follow>;

View File

@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import MkUserSetupDialog_Privacy from './MkUserSetupDialog.Privacy.vue';
export const Default = {
render(args) {
return {
components: {
MkUserSetupDialog_Privacy,
},
setup() {
return {
args,
};
},
computed: {
props() {
return {
...this.args,
};
},
},
template: '<MkUserSetupDialog_Privacy v-bind="props" />',
};
},
args: {
},
parameters: {
layout: 'centered',
},
} satisfies StoryObj<typeof MkUserSetupDialog_Privacy>;

View File

@ -1,71 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div class="_gaps">
<MkInfo>{{ i18n.ts._initialAccountSetting.theseSettingsCanEditLater }}</MkInfo>
<MkFolder>
<template #label>{{ i18n.ts.makeFollowManuallyApprove }}</template>
<template #icon><i class="ti ti-lock"></i></template>
<template #suffix>{{ isLocked ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="isLocked">{{ i18n.ts.makeFollowManuallyApprove }}<template #caption>{{ i18n.ts.lockedAccountInfo }}</template></MkSwitch>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts.hideOnlineStatus }}</template>
<template #icon><i class="ti ti-eye-off"></i></template>
<template #suffix>{{ hideOnlineStatus ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="hideOnlineStatus">{{ i18n.ts.hideOnlineStatus }}<template #caption>{{ i18n.ts.hideOnlineStatusDescription }}</template></MkSwitch>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts.noCrawle }}</template>
<template #icon><i class="ti ti-world-x"></i></template>
<template #suffix>{{ noCrawle ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="noCrawle">{{ i18n.ts.noCrawle }}<template #caption>{{ i18n.ts.noCrawleDescription }}</template></MkSwitch>
</MkFolder>
<MkFolder>
<template #label>{{ i18n.ts.preventAiLearning }}</template>
<template #icon><i class="ti ti-photo-shield"></i></template>
<template #suffix>{{ preventAiLearning ? i18n.ts.on : i18n.ts.off }}</template>
<MkSwitch v-model="preventAiLearning">{{ i18n.ts.preventAiLearning }}<template #caption>{{ i18n.ts.preventAiLearningDescription }}</template></MkSwitch>
</MkFolder>
<MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { i18n } from '@/i18n.js';
import MkSwitch from '@/components/MkSwitch.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkFolder from '@/components/MkFolder.vue';
import { misskeyApi } from '@/scripts/misskey-api.js';
const isLocked = ref(false);
const hideOnlineStatus = ref(false);
const noCrawle = ref(false);
const preventAiLearning = ref(true);
watch([isLocked, hideOnlineStatus, noCrawle, preventAiLearning], () => {
misskeyApi('i/update', {
isLocked: !!isLocked.value,
hideOnlineStatus: !!hideOnlineStatus.value,
noCrawle: !!noCrawle.value,
preventAiLearning: !!preventAiLearning.value,
});
});
</script>
<style lang="scss" module>
</style>

View File

@ -1,36 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* 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>;

View File

@ -1,103 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<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" manualSave data-cy-user-setup-user-name>
<template #label>{{ i18n.ts._profile.name }}</template>
</MkInput>
<MkTextarea v-model="description" :max="500" tall manualSave data-cy-user-setup-user-description>
<template #label>{{ i18n.ts._profile.description }}</template>
</MkTextarea>
<MkInfo>{{ i18n.ts._initialAccountSetting.youCanEditMoreSettingsInSettingsPageLater }}</MkInfo>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import { i18n } from '@/i18n.js';
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.js';
import * as os from '@/os.js';
import { signinRequired } from '@/account.js';
const $i = signinRequired();
const name = ref($i.name ?? '');
const description = ref($i.description ?? '');
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.ts.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>

View File

@ -1,37 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { userDetailed } from '../../.storybook/fakes.js';
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>;

View File

@ -1,56 +0,0 @@
/*
* SPDX-FileCopyrightText: syuilo and misskey-project
* SPDX-License-Identifier: AGPL-3.0-only
*/
/* eslint-disable @typescript-eslint/explicit-function-return-type */
import { StoryObj } from '@storybook/vue3';
import { HttpResponse, http } from 'msw';
import { commonHandlers } from '../../.storybook/mocks.js';
import { userDetailed } from '../../.storybook/fakes.js';
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,
http.post('/api/users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]);
}),
http.post('/api/pinned-users', () => {
return HttpResponse.json([
userDetailed('44'),
userDetailed('49'),
]);
}),
],
},
},
} satisfies StoryObj<typeof MkUserSetupDialog>;

View File

@ -1,257 +0,0 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<MkModalWindow
ref="dialog"
:width="500"
:height="550"
data-cy-user-setup
@close="close(true)"
@closed="emit('closed')"
>
<template v-if="page === 1" #header><i class="ti ti-user-edit"></i> {{ i18n.ts._initialAccountSetting.profileSetting }}</template>
<template v-else-if="page === 2" #header><i class="ti ti-lock"></i> {{ i18n.ts._initialAccountSetting.privacySetting }}</template>
<template v-else-if="page === 3" #header><i class="ti ti-user-plus"></i> {{ i18n.ts.follow }}</template>
<template v-else-if="page === 4" #header><i class="ti ti-bell-plus"></i> {{ i18n.ts.pushNotification }}</template>
<template v-else-if="page === 5" #header>{{ i18n.ts.done }}</template>
<template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
<div style="overflow-x: clip;">
<div :class="$style.progressBar">
<div :class="$style.progressBarValue" :style="{ width: `${(page / 5) * 100}%` }"></div>
</div>
<Transition
mode="out-in"
:enterActiveClass="$style.transition_x_enterActive"
:leaveActiveClass="$style.transition_x_leaveActive"
:enterFromClass="$style.transition_x_enterFrom"
:leaveToClass="$style.transition_x_leaveTo"
>
<template v-if="page === 0">
<div :class="$style.centerPage">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="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.letsStartAccountSetup }}</div>
<MkButton primary rounded gradate style="margin: 16px auto 0 auto;" data-cy-user-setup-continue @click="page++">{{ i18n.ts._initialAccountSetting.profileSetting }} <i class="ti ti-arrow-right"></i></MkButton>
<MkButton style="margin: 0 auto;" transparent rounded @click="later(true)">{{ i18n.ts.later }}</MkButton>
</div>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 1">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XProfile/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 2">
<div style="height: 100cqh; overflow: auto;">
<div :class="$style.pageRoot">
<MkSpacer :marginMin="20" :marginMax="28" :class="$style.pageMain">
<XPrivacy/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</div>
</template>
<template v-else-if="page === 3">
<div style="height: 100cqh; overflow: auto;">
<MkSpacer :marginMin="20" :marginMax="28">
<XFollow/>
</MkSpacer>
<div :class="$style.pageFooter">
<div class="_buttonsCenter">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate style="" data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</div>
</template>
<template v-else-if="page === 4">
<div :class="$style.centerPage">
<MkSpacer :marginMin="20" :marginMax="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.tsx._initialAccountSetting.pushNotificationDescription({ name: instance.name ?? host }) }}</div>
<MkPushNotificationAllowButton primary showOnlyToRegister style="margin: 0 auto;"/>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton primary rounded gradate data-cy-user-setup-continue @click="page++">{{ i18n.ts.continue }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
</div>
</MkSpacer>
</div>
</template>
<template v-else-if="page === 5">
<div :class="$style.centerPage">
<MkAnimBg style="position: absolute; top: 0;" :scale="1.5"/>
<MkSpacer :marginMin="20" :marginMax="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>
<div>{{ i18n.tsx._initialAccountSetting.youCanContinueTutorial({ name: instance.name ?? host }) }}</div>
<div class="_buttonsCenter" style="margin-top: 16px;">
<MkButton rounded primary gradate data-cy-user-setup-continue @click="launchTutorial()">{{ i18n.ts._initialAccountSetting.startTutorial }} <i class="ti ti-arrow-right"></i></MkButton>
</div>
<div class="_buttonsCenter">
<MkButton rounded data-cy-user-setup-back @click="page--"><i class="ti ti-arrow-left"></i> {{ i18n.ts.goBack }}</MkButton>
<MkButton rounded primary data-cy-user-setup-continue @click="setupComplete()">{{ i18n.ts.close }}</MkButton>
</div>
</div>
</MkSpacer>
</div>
</template>
</Transition>
</div>
</MkModalWindow>
</template>
<script lang="ts" setup>
import { ref, shallowRef, watch, nextTick, defineAsyncComponent } 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 XPrivacy from '@/components/MkUserSetupDialog.Privacy.vue';
import MkAnimBg from '@/components/MkAnimBg.vue';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { host } from '@/config.js';
import MkPushNotificationAllowButton from '@/components/MkPushNotificationAllowButton.vue';
import { defaultStore } from '@/store.js';
import * as os from '@/os.js';
const emit = defineEmits<{
(ev: 'closed'): void;
}>();
const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
// eslint-disable-next-line vue/no-setup-props-destructure
const page = ref(defaultStore.state.accountSetupWizard);
watch(page, () => {
defaultStore.set('accountSetupWizard', page.value);
});
async function close(skip: boolean) {
if (skip) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._initialAccountSetting.skipAreYouSure,
});
if (canceled) return;
}
dialog.value?.close();
defaultStore.set('accountSetupWizard', -1);
}
function setupComplete() {
defaultStore.set('accountSetupWizard', -1);
dialog.value?.close();
}
function launchTutorial() {
setupComplete();
nextTick(() => {
os.popup(defineAsyncComponent(() => import('@/components/MkTutorialDialog.vue')), {
initialPage: 1,
}, {}, 'closed');
});
}
async function later(later: boolean) {
if (later) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._initialAccountSetting.laterAreYouSure,
});
if (canceled) return;
}
dialog.value?.close();
defaultStore.set('accountSetupWizard', 0);
}
</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);
}
.progressBar {
position: absolute;
top: 0;
left: 0;
z-index: 10;
width: 100%;
height: 4px;
}
.progressBarValue {
height: 100%;
background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB));
transition: all 0.5s cubic-bezier(0,.5,.5,1);
}
.centerPage {
display: flex;
justify-content: center;
align-items: center;
height: 100cqh;
padding-bottom: 30px;
box-sizing: border-box;
}
.pageRoot {
display: flex;
flex-direction: column;
min-height: 100%;
}
.pageMain {
flex-grow: 1;
}
.pageFooter {
position: sticky;
bottom: 0;
left: 0;
flex-shrink: 0;
padding: 12px;
border-top: solid 0.5px var(--divider);
-webkit-backdrop-filter: blur(15px);
backdrop-filter: blur(15px);
}
</style>

View File

@ -5,17 +5,31 @@ SPDX-License-Identifier: AGPL-3.0-only
<template>
<div :class="[$style.root, { [$style.inline]: inline }]">
<a v-if="external" :class="$style.main" class="_button" :href="to" target="_blank">
<a v-if="external" :class="[$style.main, { [$style.large]: large }]" class="_button" :href="to" target="_blank">
<span :class="$style.icon"><slot name="icon"></slot></span>
<span :class="$style.text"><slot></slot></span>
<div :class="$style.headerText">
<div>
<MkCondensedLine :minScale="2 / 3"><slot></slot></MkCondensedLine>
</div>
<div v-if="$slots.caption" :class="$style.headerTextSub">
<MkCondensedLine :minScale="2 / 3"><slot name="caption"></slot></MkCondensedLine>
</div>
</div>
<span :class="$style.suffix">
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
<i class="ti ti-external-link"></i>
</span>
</a>
<MkA v-else :class="[$style.main, { [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
<MkA v-else :class="[$style.main, { [$style.large]: large, [$style.active]: active }]" class="_button" :to="to" :behavior="behavior">
<span :class="$style.icon"><slot name="icon"></slot></span>
<span :class="$style.text"><slot></slot></span>
<div :class="$style.headerText">
<div>
<MkCondensedLine :minScale="2 / 3"><slot></slot></MkCondensedLine>
</div>
<div v-if="$slots.caption" :class="$style.headerTextSub">
<MkCondensedLine :minScale="2 / 3"><slot name="caption"></slot></MkCondensedLine>
</div>
</div>
<span :class="$style.suffix">
<span :class="$style.suffixText"><slot name="suffix"></slot></span>
<i class="ti ti-chevron-right"></i>
@ -33,6 +47,7 @@ const props = defineProps<{
external?: boolean;
behavior?: null | 'window' | 'browser';
inline?: boolean;
large?: boolean;
}>();
</script>
@ -55,6 +70,10 @@ const props = defineProps<{
border-radius: 6px;
font-size: 0.9em;
&.large {
font-size: 1em;
}
&:hover {
text-decoration: none;
background: var(--buttonHoverBg);
@ -81,11 +100,17 @@ const props = defineProps<{
}
}
.text {
flex-shrink: 1;
white-space: normal;
.headerText {
white-space: nowrap;
text-overflow: ellipsis;
text-align: start;
overflow: hidden;
padding-right: 12px;
text-align: center;
}
.headerTextSub {
color: var(--fgTransparentWeak);
font-size: .85em;
}
.suffix {

View File

@ -0,0 +1,312 @@
<!--
SPDX-FileCopyrightText: syuilo and misskey-project
SPDX-License-Identifier: AGPL-3.0-only
-->
<template>
<div :class="[$style.onboardingRoot, { [$style.ready]: animationPhase >= 1 }]">
<MkAnimBg :class="$style.onboardingBg"/>
<div :class="[$style.onboardingContainer]">
<MkTutorial
:showProgressbar="true"
:skippable="false"
:withSetup="true"
>
<template #welcome="{ next }">
<!-- Tips for large-scale server admins: you should customize this slide for better branding -->
<!-- 大規模サーバーの管理者さんへ: このスライドの内容をサーバー独自でアレンジすると良さそうなのでやってみてね -->
<div ref="welcomePageRootEl" :class="$style.welcomePageRoot">
<canvas ref="confettiEl" :class="$style.welcomePageConfetti"></canvas>
<div
:class="[
$style.centerPage,
$style.welcomePageMain,
{
[$style.appear]: animationPhase >= 3,
[$style.done]: animationPhase === 4,
}
]"
>
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps" style="word-break: auto-phrase; text-align: center;">
<img ref="instanceIconEl" :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
<div>
<div style="font-size: 135%;">{{ i18n.ts._initialTutorial._onboardingLanding.accountCreated }}</div>
<div>{{ i18n.tsx._initialTutorial._onboardingLanding.welcomeToX({ name: instance.name ?? host }) }}</div>
</div>
<div>{{ i18n.tsx._initialTutorial._onboardingLanding.description({ name: instance.name ?? host }) }}</div>
<MkButton large primary rounded gradate style="margin: 16px auto;" @click="next">{{ i18n.ts.start }} <i class="ti ti-arrow-right"></i></MkButton>
<MkInfo style="width: fit-content; margin: 0 auto; text-align: start; white-space: pre-wrap;">{{ i18n.tsx._initialTutorial._onboardingLanding.takesAbout({ min: 5 }) }}</MkInfo>
</div>
</MkSpacer>
</div>
<div
:class="[
$style.welcomePageAnimRoot,
{
[$style.appear]: animationPhase === 2,
[$style.move]: animationPhase === 3,
},
]"
>
<img :src="instance.iconUrl || '/favicon.ico'" alt="" :class="$style.instanceIcon"/>
</div>
</div>
</template>
<template #finish="{ prev }">
<div :class="$style.centerPage">
<MkSpacer :marginMin="20" :marginMax="28">
<div class="_gaps">
<i class="ti ti-check" style="display: block; margin: auto; font-size: 3em; color: var(--accent);"></i>
<div style="text-align: center; font-size: 120%;">{{ i18n.ts._initialTutorial._done.title }}</div>
<div style="text-align: center;">{{ i18n.tsx._initialTutorial._onboardingDone.description({ name: instance.name ?? host }) }}</div>
<div>
<FormLink v-if="originalPath && originalPath !== '/'" :to="originalPath" large :behavior="'browser'">
<template #icon><i class="ti ti-directions"></i></template>
{{ i18n.ts._initialTutorial._onboardingDone.backToOriginalPath }}
<template #caption>{{ i18n.ts._initialTutorial._onboardingDone.backToOriginalPathDescription }}</template>
</FormLink>
<hr v-if="originalPath && originalPath !== '/'">
<div class="_gaps_s">
<FormLink to="/settings/profile" large :behavior="'browser'">
<template #icon><i class="ti ti-user"></i></template>
{{ i18n.ts._initialTutorial._onboardingDone.profile }}
<template #caption>{{ i18n.ts._initialTutorial._onboardingDone.profileDescription }}</template>
</FormLink>
<FormLink to="/explore" large :behavior="'browser'">
<template #icon><i class="ti ti-hash"></i></template>
{{ i18n.ts.explore }}
<template #caption>{{ i18n.ts._initialTutorial._onboardingDone.exploreDescription }}</template>
</FormLink>
<FormLink to="/" large :behavior="'browser'">
<template #icon><i class="ti ti-home"></i></template>
{{ i18n.ts._initialTutorial._onboardingDone.goToTimeline }}
<template #caption>{{ i18n.ts._initialTutorial._onboardingDone.goToTimelineDescription }}</template>
</FormLink>
</div>
</div>
<MkInfo style="border-radius: 6px;">{{ i18n.ts._initialTutorial._done.youCanReferTutorialBy }}</MkInfo>
<div style="text-align: center;">{{ i18n.tsx._initialTutorial._done.haveFun({ name: instance.name ?? host }) }}</div>
</div>
</MkSpacer>
</div>
</template>
</MkTutorial>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, shallowRef, computed, onMounted } from 'vue';
import { create as createConfetti } from 'canvas-confetti';
import { definePageMetadata } from '@/scripts/page-metadata.js';
import { reactionPicker } from '@/scripts/reaction-picker.js';
import { i18n } from '@/i18n.js';
import { instance } from '@/instance.js';
import { host } from '@/config.js';
import MkAnimBg from '@/components/MkAnimBg.vue';
import MkButton from '@/components/MkButton.vue';
import MkInfo from '@/components/MkInfo.vue';
import MkTutorial from '@/components/MkTutorial.vue';
import FormLink from '@/components/form/link.vue';
const confettiEl = shallowRef<HTMLCanvasElement | null>(null);
const welcomePageRootEl = shallowRef<HTMLDivElement | null>(null);
const instanceIconEl = shallowRef<HTMLImageElement | null>(null);
const instanceIconY = ref(0);
const instanceIconYPx = computed(() => `${instanceIconY.value - 30}px`);
/**
* 0 なにもしない
* 1 背景表示mounted
* 2 サーバーロゴ出現
* 3 サーバーロゴ移動文字表示
* 4 完了オープニング用ロゴ消滅
*/
const animationPhase = ref(0);
// See: @/_boot_/common.ts L123 for details
const query = new URLSearchParams(location.search);
const originalPath = query.get('redirected_from');
// Y
function getIconY(instanceIconEl: HTMLImageElement, welcomePageRootEl: HTMLDivElement) {
const instanceIconElRect = instanceIconEl.getBoundingClientRect();
return instanceIconElRect.top - welcomePageRootEl.getBoundingClientRect().top;
}
function instanceIconElImageLoaded() {
return new Promise<void>((resolve) => {
if (instanceIconEl.value!.complete) {
resolve();
} else {
instanceIconEl.value!.addEventListener('load', () => resolve(), { once: true });
}
});
}
onMounted(() => {
const confetti = createConfetti(confettiEl.value!, {
resize: true,
});
instanceIconY.value = getIconY(instanceIconEl.value!, welcomePageRootEl.value!);
window.addEventListener('resize', () => {
instanceIconY.value = getIconY(instanceIconEl.value!, welcomePageRootEl.value!);
}, { passive: true });
// subBoot
Promise.all([
reactionPicker.init(),
instanceIconElImageLoaded(),
]).then(() => {
setTimeout(() => {
//
animationPhase.value = 1;
setTimeout(() => {
animationPhase.value = 2;
setTimeout(() => {
animationPhase.value = 3;
setTimeout(() => {
animationPhase.value = 4;
confetti({
spread: 70,
origin: { y: 0.5 },
});
}, 1000);
}, 1250);
}, 500);
}, 100);
});
});
definePageMetadata(() => ({
title: 'Onboarding',
description: 'Welcome to Misskey!',
}));
</script>
<style lang="scss" module>
.onboardingRoot {
box-sizing: border-box;
display: grid;
place-content: center;
min-height: 100svh;
padding: 32px 32px 64px 32px;
}
.onboardingBg {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0;
transition: opacity 2s ease;
}
.onboardingContainer {
position: relative;
border-radius: var(--radius);
background-color: var(--acrylicPanel);
overflow: clip;
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
max-width: 650px;
max-height: 700px;
width: 100vw;
height: 100svh;
container-type: inline-size;
}
.ready {
& .onboardingBg {
opacity: 1;
}
}
.centerPage {
display: flex;
justify-content: center;
align-items: center;
min-height: 100%;
box-sizing: border-box;
overflow-y: auto;
}
.welcomePageRoot {
position: relative;
height: 100%;
}
.welcomePageMain {
opacity: 0;
transform: translateY(30px);
visibility: hidden;
.instanceIcon {
opacity: 0;
}
&.appear {
transition: opacity 0.75s 0.25s ease, transform 0.75s 0.25s ease;
opacity: 1;
transform: translateY(0);
visibility: visible;
}
&.done .instanceIcon {
opacity: 1;
}
}
.instanceIcon {
height: 5em;
width: 5em;
margin: 0 auto;
object-fit: contain;
border-radius: calc(var(--radius) / 2);
}
.welcomePageConfetti,
.welcomePageAnimRoot {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: clip;
pointer-events: none;
.instanceIcon {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
opacity: 0;
}
&.appear {
.instanceIcon {
opacity: 1;
transform: translate(-50%, -50%) scale(1.5);
transition: opacity 1s cubic-bezier(0.22, 0.61, 0.36, 1), transform 1s cubic-bezier(0.22, 0.61, 0.36, 1);
}
}
&.move {
.instanceIcon {
opacity: 1;
transform: translate(-50%, 0) scale(1);
top: v-bind(instanceIconYPx);
transition: transform 1s ease, top 1s ease;
}
}
}
</style>

View File

@ -46,7 +46,7 @@ function submit() {
misskeyApi('signup-pending', {
code: props.code,
}).then(res => {
return login(res.i, '/');
return login(res.i, '/onboarding');
}).catch(() => {
submitting.value = false;

View File

@ -190,6 +190,10 @@ const routes: RouteDef[] = [{
}, {
path: '/signup-complete/:code',
component: page(() => import('@/pages/signup-complete.vue')),
}, {
path: '/onboarding',
component: page(() => import('@/pages/onboarding.vue')),
loginRequired: true,
}, {
path: '/announcements',
component: page(() => import('@/pages/announcements.vue')),

View File

@ -14,12 +14,15 @@ class ReactionPicker {
private targetNote: Ref<Misskey.entities.Note | null> = ref(null);
private onChosen?: (reaction: string) => void;
private onClosed?: () => void;
public isInitialized = false;
constructor() {
// nop
}
public async init() {
if (this.isInitialized) return;
const reactionsRef = defaultStore.reactiveState.reactions;
await popup(defineAsyncComponent(() => import('@/components/MkEmojiPickerDialog.vue')), {
src: this.src,
@ -39,6 +42,7 @@ class ReactionPicker {
if (this.onClosed) this.onClosed();
},
});
this.isInitialized = true;
}
public show(src: HTMLElement | null, targetNote: Misskey.entities.Note | null, onChosen?: ReactionPicker['onChosen'], onClosed?: ReactionPicker['onClosed']) {

View File

@ -911,6 +911,9 @@ importers:
'@testing-library/vue':
specifier: 8.0.2
version: 8.0.2(@vue/compiler-sfc@3.4.18)(vue@3.4.18)
'@types/canvas-confetti':
specifier: ^1.6.4
version: 1.6.4
'@types/escape-regexp':
specifier: 0.0.3
version: 0.0.3
@ -1891,7 +1894,7 @@ packages:
'@babel/traverse': 7.23.5
'@babel/types': 7.23.5
convert-source-map: 2.0.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
gensync: 1.0.0-beta.2
json5: 2.2.3
semver: 6.3.1
@ -1972,7 +1975,7 @@ packages:
'@babel/core': 7.23.5
'@babel/helper-compilation-targets': 7.22.15
'@babel/helper-plugin-utils': 7.22.5
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
lodash.debounce: 4.0.8
resolve: 1.22.8
transitivePeerDependencies:
@ -3112,7 +3115,7 @@ packages:
'@babel/helper-split-export-declaration': 7.22.6
'@babel/parser': 7.23.9
'@babel/types': 7.23.5
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
globals: 11.12.0
transitivePeerDependencies:
- supports-color
@ -3804,7 +3807,7 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dependencies:
ajv: 6.12.6
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
espree: 9.6.1
globals: 13.19.0
ignore: 5.2.4
@ -4011,7 +4014,7 @@ packages:
engines: {node: '>=10.10.0'}
dependencies:
'@humanwhocodes/object-schema': 2.0.1
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
minimatch: 3.1.2
transitivePeerDependencies:
- supports-color
@ -7273,6 +7276,10 @@ packages:
'@types/responselike': 1.0.0
dev: false
/@types/canvas-confetti@1.6.4:
resolution: {integrity: sha512-fNyZ/Fdw/Y92X0vv7B+BD6ysHL4xVU5dJcgzgxLdGbn8O3PezZNIJpml44lKM0nsGur+o/6+NZbZeNTt00U1uA==}
dev: true
/@types/chai-subset@1.3.5:
resolution: {integrity: sha512-c2mPnw+xHtXDoHmdtcCXGwyLMiauiAyxWMzhGpqHC4nqI/Y5G2XhTampslK2rb59kpcuHon03UH8W6iYUzw88A==}
dependencies:
@ -7831,7 +7838,7 @@ packages:
'@typescript-eslint/type-utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
'@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 6.11.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.53.0
graphemer: 1.4.0
ignore: 5.2.4
@ -7860,7 +7867,7 @@ packages:
'@typescript-eslint/type-utils': 6.18.1(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/utils': 6.18.1(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 6.18.1
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.56.0
graphemer: 1.4.0
ignore: 5.2.4
@ -7886,7 +7893,7 @@ packages:
'@typescript-eslint/types': 6.11.0
'@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 6.11.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.53.0
typescript: 5.3.3
transitivePeerDependencies:
@ -7907,7 +7914,7 @@ packages:
'@typescript-eslint/types': 6.18.1
'@typescript-eslint/typescript-estree': 6.18.1(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 6.18.1
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.56.0
typescript: 5.3.3
transitivePeerDependencies:
@ -7942,7 +7949,7 @@ packages:
dependencies:
'@typescript-eslint/typescript-estree': 6.11.0(typescript@5.3.3)
'@typescript-eslint/utils': 6.11.0(eslint@8.53.0)(typescript@5.3.3)
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.53.0
ts-api-utils: 1.0.1(typescript@5.3.3)
typescript: 5.3.3
@ -7962,7 +7969,7 @@ packages:
dependencies:
'@typescript-eslint/typescript-estree': 6.18.1(typescript@5.3.3)
'@typescript-eslint/utils': 6.18.1(eslint@8.56.0)(typescript@5.3.3)
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.56.0
ts-api-utils: 1.0.1(typescript@5.3.3)
typescript: 5.3.3
@ -7991,7 +7998,7 @@ packages:
dependencies:
'@typescript-eslint/types': 6.11.0
'@typescript-eslint/visitor-keys': 6.11.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
globby: 11.1.0
is-glob: 4.0.3
semver: 7.5.4
@ -8012,7 +8019,7 @@ packages:
dependencies:
'@typescript-eslint/types': 6.18.1
'@typescript-eslint/visitor-keys': 6.18.1
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
@ -8420,7 +8427,7 @@ packages:
engines: {node: '>= 6.0.0'}
requiresBuild: true
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@ -8428,7 +8435,7 @@ packages:
resolution: {integrity: sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==}
engines: {node: '>= 14'}
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
dev: false
@ -8799,7 +8806,7 @@ packages:
resolution: {integrity: sha512-TAlMYvOuwGyLK3PfBb5WKBXZmXz2fVCgv23d6zZFdle/q3gPjmxBaeuC0pY0Dzs5PWMSgfqqEZkrye19GlDTgw==}
dependencies:
archy: 1.0.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
fastq: 1.15.0
transitivePeerDependencies:
- supports-color
@ -10293,7 +10300,6 @@ packages:
dependencies:
ms: 2.1.2
supports-color: 5.5.0
dev: true
/debug@4.3.4(supports-color@8.1.1):
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
@ -10306,6 +10312,7 @@ packages:
dependencies:
ms: 2.1.2
supports-color: 8.1.1
dev: true
/decamelize-keys@1.1.1:
resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==}
@ -10513,7 +10520,7 @@ packages:
hasBin: true
dependencies:
address: 1.2.2
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
dev: true
@ -10838,7 +10845,7 @@ packages:
peerDependencies:
esbuild: '>=0.12 <1'
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
esbuild: 0.18.20
transitivePeerDependencies:
- supports-color
@ -11147,7 +11154,7 @@ packages:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.2.2
@ -11194,7 +11201,7 @@ packages:
ajv: 6.12.6
chalk: 4.1.2
cross-spawn: 7.0.3
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
doctrine: 3.0.0
escape-string-regexp: 4.0.0
eslint-scope: 7.2.2
@ -11809,7 +11816,7 @@ packages:
debug:
optional: true
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
/for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
@ -12343,7 +12350,6 @@ packages:
/has-flag@3.0.0:
resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==}
engines: {node: '>=4'}
dev: true
/has-flag@4.0.0:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
@ -12502,7 +12508,7 @@ packages:
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
dev: false
@ -12562,7 +12568,7 @@ packages:
engines: {node: '>= 6'}
dependencies:
agent-base: 6.0.2
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
@ -12571,7 +12577,7 @@ packages:
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
transitivePeerDependencies:
- supports-color
dev: false
@ -12733,7 +12739,7 @@ packages:
dependencies:
'@ioredis/commands': 1.2.0
cluster-key-slot: 1.1.2
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
@ -13168,7 +13174,7 @@ packages:
resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==}
engines: {node: '>=10'}
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
istanbul-lib-coverage: 3.2.2
source-map: 0.6.1
transitivePeerDependencies:
@ -14701,7 +14707,7 @@ packages:
resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==}
dependencies:
'@types/debug': 4.1.12
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
decode-named-character-reference: 1.0.2
devlop: 1.1.0
micromark-core-commonmark: 2.0.0
@ -17630,7 +17636,7 @@ packages:
dependencies:
'@hapi/hoek': 10.0.1
'@hapi/wreck': 18.0.1
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
joi: 17.11.0
transitivePeerDependencies:
- supports-color
@ -17831,7 +17837,7 @@ packages:
engines: {node: '>= 14'}
dependencies:
agent-base: 7.1.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
socks: 2.7.1
transitivePeerDependencies:
- supports-color
@ -17985,7 +17991,7 @@ packages:
arg: 5.0.2
bluebird: 3.7.2
check-more-types: 2.24.0
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
execa: 5.1.1
lazy-ass: 1.6.0
ps-tree: 1.2.0
@ -18243,7 +18249,6 @@ packages:
engines: {node: '>=4'}
dependencies:
has-flag: 3.0.0
dev: true
/supports-color@7.2.0:
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
@ -18857,7 +18862,7 @@ packages:
chalk: 4.1.2
cli-highlight: 2.1.11
dayjs: 1.11.10
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
dotenv: 16.0.3
glob: 10.3.10
ioredis: 5.3.2
@ -19178,7 +19183,7 @@ packages:
hasBin: true
dependencies:
cac: 6.7.14
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
mlly: 1.5.0
pathe: 1.1.2
picocolors: 1.0.0
@ -19290,7 +19295,7 @@ packages:
acorn-walk: 8.3.2
cac: 6.7.14
chai: 4.3.10
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
happy-dom: 10.0.3
local-pkg: 0.4.3
magic-string: 0.30.7
@ -19400,7 +19405,7 @@ packages:
peerDependencies:
eslint: '>=6.0.0'
dependencies:
debug: 4.3.4(supports-color@8.1.1)
debug: 4.3.4(supports-color@5.5.0)
eslint: 8.56.0
eslint-scope: 7.2.2
eslint-visitor-keys: 3.4.3