diff options
| author | かっこかり <67428053+kakkokari-gtyih@users.noreply.github.com> | 2023-11-03 15:35:07 +0900 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2023-11-03 15:35:07 +0900 |
| commit | 24e629ca5c50789ff0aba31532ae66b51148d70f (patch) | |
| tree | 513155452fd0644c6b69bf7e53e26ab6575977db /packages/frontend/src/components | |
| parent | enhance: アカウント登録時のメールアドレス認証に30分の有... (diff) | |
| download | sharkey-24e629ca5c50789ff0aba31532ae66b51148d70f.tar.gz sharkey-24e629ca5c50789ff0aba31532ae66b51148d70f.tar.bz2 sharkey-24e629ca5c50789ff0aba31532ae66b51148d70f.zip | |
enhance: 初期設定とチュートリアルを統合 (#12141)
* better onboarding experience
* enhance: iroiro
* (add) title
* (enhance) 戻る・次へボタンを全ページでstickyに
* fix merging
* (add) iroiro
* remove unnecessary file
* Update CHANGELOG.md
* tweak texts
* (fix) reactionViewer mock
* change strings
* Update MkTutorialDialog.Note.vue
* Update ja-JP.yml
* (fix) reactionViewer error
* (fix) path
* refactor
* fix
* Update MkPostForm.vue
* Update ja-JP.yml
* Update ja-JP.yml
* tweak text
* Update ja-JP.yml
* Update ja-JP.yml
* Update ja-JP.yml
* (add) achivement
* (add) もう一度見れますよメッセージを追加
* Revert "feat: レジストリAPIをサードパーティから利用可能に (#12229)"
This reverts commit 79346272f8792d35955efd3aaaa1e42e0cd2a6e3.
* Revert "(add) もう一度見れますよメッセージを追加"
This reverts commit 6123b35215133f0d5e5db356bb43f4acbafab8fa.
* Revert "Revert "feat: レジストリAPIをサードパーティから利用可能に (#12229)""
This reverts commit bae684e484ef99308d7ac816a822047117efe1c6.
* tweak
---------
Co-authored-by: syuilo <Syuilotan@yahoo.co.jp>
Diffstat (limited to 'packages/frontend/src/components')
13 files changed, 1027 insertions, 93 deletions
diff --git a/packages/frontend/src/components/MkInfo.vue b/packages/frontend/src/components/MkInfo.vue index 37490887e1..19402a44ce 100644 --- a/packages/frontend/src/components/MkInfo.vue +++ b/packages/frontend/src/components/MkInfo.vue @@ -7,7 +7,8 @@ SPDX-License-Identifier: AGPL-3.0-only <div :class="[$style.root, { [$style.warn]: warn }]"> <i v-if="warn" class="ti ti-alert-triangle" :class="$style.i"></i> <i v-else class="ti ti-info-circle" :class="$style.i"></i> - <slot></slot> + <div><slot></slot></div> + <button v-if="closable" :class="$style.button" class="_button" @click="close()"><i class="ti ti-x"></i></button> </div> </template> @@ -16,11 +17,23 @@ import { } from 'vue'; const props = defineProps<{ warn?: boolean; + closable?: boolean; }>(); + +const emit = defineEmits<{ + (ev: 'close'): void; +}>(); + +function close() { + // こいつの中では非表示動作は行わない + emit('close'); +} </script> <style lang="scss" module> .root { + display: flex; + align-items: center; padding: 12px 14px; font-size: 90%; background: var(--infoBg); @@ -37,4 +50,9 @@ const props = defineProps<{ .i { margin-right: 4px; } + +.button { + margin-left: auto; + padding: 4px; +} </style> diff --git a/packages/frontend/src/components/MkNote.vue b/packages/frontend/src/components/MkNote.vue index b31ee78532..d71b07c51b 100644 --- a/packages/frontend/src/components/MkNote.vue +++ b/packages/frontend/src/components/MkNote.vue @@ -47,7 +47,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <article v-else :class="$style.article" @contextmenu.stop="onContextmenu"> <div v-if="appearNote.channel" :class="$style.colorBar" :style="{ background: appearNote.channel.color }"></div> - <MkAvatar :class="$style.avatar" :user="appearNote.user" link preview/> + <MkAvatar :class="$style.avatar" :user="appearNote.user" :link="!mock" :preview="!mock"/> <div :class="$style.main"> <MkNoteHeader :note="appearNote" :mini="true"/> <MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/> @@ -84,7 +84,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> <MkA v-if="appearNote.channel && !inChannel" :class="$style.channel" :to="`/channels/${appearNote.channel.id}`"><i class="ti ti-device-tv"></i> {{ appearNote.channel.name }}</MkA> </div> - <MkReactionsViewer :note="appearNote" :maxNumber="16"> + <MkReactionsViewer :note="appearNote" :maxNumber="16" @mockUpdateMyReaction="emitUpdReaction"> <template #more> <div :class="$style.reactionOmitted">{{ i18n.ts.more }}</div> </template> @@ -136,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent } from 'vue'; +import { computed, inject, onMounted, ref, shallowRef, Ref, defineAsyncComponent, watch, provide } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import MkNoteSub from '@/components/MkNoteSub.vue'; @@ -170,9 +170,19 @@ import MkRippleEffect from '@/components/MkRippleEffect.vue'; import { showMovedDialog } from '@/scripts/show-moved-dialog.js'; import { shouldCollapsed } from '@/scripts/collapsed.js'; -const props = defineProps<{ +const props = withDefaults(defineProps<{ note: Misskey.entities.Note; pinned?: boolean; + mock?: boolean; +}>(), { + mock: false, +}); + +provide('mock', props.mock); + +const emit = defineEmits<{ + (ev: 'reaction', emoji: string): void; + (ev: 'removeReaction', emoji: string): void; }>(); const inChannel = inject('inChannel', null); @@ -232,30 +242,38 @@ const keymap = { 's': () => showContent.value !== showContent.value, }; -useNoteCapture({ - rootEl: el, - note: $$(appearNote), - pureNote: $$(note), - isDeletedRef: isDeleted, -}); - -useTooltip(renoteButton, async (showing) => { - const renotes = await os.api('notes/renotes', { - noteId: appearNote.id, - limit: 11, +if (props.mock) { + watch(() => props.note, (to) => { + note = deepClone(to); + }, { deep: true }); +} else { + useNoteCapture({ + rootEl: el, + note: $$(appearNote), + pureNote: $$(note), + isDeletedRef: isDeleted, }); +} + +if (!props.mock) { + useTooltip(renoteButton, async (showing) => { + const renotes = await os.api('notes/renotes', { + noteId: appearNote.id, + limit: 11, + }); - const users = renotes.map(x => x.user); + const users = renotes.map(x => x.user); - if (users.length < 1) return; + if (users.length < 1) return; - os.popup(MkUsersTooltip, { - showing, - users, - count: appearNote.renoteCount, - targetElement: renoteButton.value, - }, {}, 'closed'); -}); + os.popup(MkUsersTooltip, { + showing, + users, + count: appearNote.renoteCount, + targetElement: renoteButton.value, + }, {}, 'closed'); + }); +} type Visibility = 'public' | 'home' | 'followers' | 'specified'; @@ -287,21 +305,25 @@ function renote(viaKeyboard = false) { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } - os.api('notes/create', { - renoteId: appearNote.id, - channelId: appearNote.channelId, - }).then(() => { - os.toast(i18n.ts.renoted); - }); + if (!props.mock) { + os.api('notes/create', { + renoteId: appearNote.id, + channelId: appearNote.channelId, + }).then(() => { + os.toast(i18n.ts.renoted); + }); + } }, }, { text: i18n.ts.inChannelQuote, icon: 'ti ti-quote', action: () => { - os.post({ - renote: appearNote, - channel: appearNote.channel, - }); + if (!props.mock) { + os.post({ + renote: appearNote, + channel: appearNote.channel, + }); + } }, }, null]); } @@ -327,15 +349,17 @@ function renote(viaKeyboard = false) { visibility = smallerVisibility(visibility, 'home'); } - os.api('notes/create', { - localOnly, - visibility, - renoteId: appearNote.id, - }).then(() => { - os.toast(i18n.ts.renoted); - }); + if (!props.mock) { + os.api('notes/create', { + localOnly, + visibility, + renoteId: appearNote.id, + }).then(() => { + os.toast(i18n.ts.renoted); + }); + } }, - }, { + }, (props.mock) ? undefined : { text: i18n.ts.quote, icon: 'ti ti-quote', action: () => { @@ -352,6 +376,9 @@ function renote(viaKeyboard = false) { function reply(viaKeyboard = false): void { pleaseLogin(); + if (props.mock) { + return; + } os.post({ reply: appearNote, channel: appearNote.channel, @@ -365,6 +392,10 @@ function react(viaKeyboard = false): void { pleaseLogin(); showMovedDialog(); if (appearNote.reactionAcceptance === 'likeOnly') { + if (props.mock) { + return; + } + os.api('notes/reactions/create', { noteId: appearNote.id, reaction: '❤️', @@ -379,6 +410,11 @@ function react(viaKeyboard = false): void { } else { blur(); reactionPicker.show(reactButton.value, reaction => { + if (props.mock) { + emit('reaction', reaction); + return; + } + os.api('notes/reactions/create', { noteId: appearNote.id, reaction: reaction, @@ -395,12 +431,22 @@ function react(viaKeyboard = false): void { function undoReact(note): void { const oldReaction = note.myReaction; if (!oldReaction) return; + + if (props.mock) { + emit('removeReaction', oldReaction); + return; + } + os.api('notes/reactions/delete', { noteId: note.id, }); } function onContextmenu(ev: MouseEvent): void { + if (props.mock) { + return; + } + const isLink = (el: HTMLElement) => { if (el.tagName === 'A') return true; // 再生速度の選択などのために、Audio要素のコンテキストメニューはブラウザデフォルトとする。 @@ -422,6 +468,10 @@ function onContextmenu(ev: MouseEvent): void { } function menu(viaKeyboard = false): void { + if (props.mock) { + return; + } + const { menu, cleanup } = getNoteMenu({ note: note, translating, translation, menuButton, isDeleted, currentClip: currentClip?.value }); os.popupMenu(menu, menuButton.value, { viaKeyboard, @@ -429,10 +479,18 @@ function menu(viaKeyboard = false): void { } async function clip() { + if (props.mock) { + return; + } + os.popupMenu(await getNoteClipMenu({ note: note, isDeleted, currentClip: currentClip?.value }), clipButton.value).then(focus); } function showRenoteMenu(viaKeyboard = false): void { + if (props.mock) { + return; + } + function getUnrenote(): MenuItem { return { text: i18n.ts.unrenote, @@ -490,6 +548,14 @@ function readPromo() { }); isDeleted.value = true; } + +function emitUpdReaction(emoji: string, delta: number) { + if (delta < 0) { + emit('removeReaction', emoji); + } else if (delta > 0) { + emit('reaction', emoji); + } +} </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkNoteHeader.vue b/packages/frontend/src/components/MkNoteHeader.vue index 52d5b03685..b2236b99c2 100644 --- a/packages/frontend/src/components/MkNoteHeader.vue +++ b/packages/frontend/src/components/MkNoteHeader.vue @@ -5,7 +5,10 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <header :class="$style.root"> - <MkA v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> + <div v-if="mock" :class="$style.name"> + <MkUserName :user="note.user"/> + </div> + <MkA v-else v-user-preview="note.user.id" :class="$style.name" :to="userPage(note.user)"> <MkUserName :user="note.user"/> </MkA> <div v-if="note.user.isBot" :class="$style.isBot">bot</div> @@ -14,7 +17,10 @@ SPDX-License-Identifier: AGPL-3.0-only <img v-for="role in note.user.badgeRoles" :key="role.id" v-tooltip="role.name" :class="$style.badgeRole" :src="role.iconUrl"/> </div> <div :class="$style.info"> - <MkA :to="notePage(note)"> + <div v-if="mock"> + <MkTime :time="note.createdAt" colored/> + </div> + <MkA v-else :to="notePage(note)"> <MkTime :time="note.createdAt" colored/> </MkA> <span v-if="note.visibility !== 'public'" style="margin-left: 0.5em;" :title="i18n.ts._visibility[note.visibility]"> @@ -29,7 +35,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { } from 'vue'; +import { inject } from 'vue'; import * as Misskey from 'misskey-js'; import { i18n } from '@/i18n.js'; import { notePage } from '@/filters/note.js'; @@ -38,6 +44,8 @@ import { userPage } from '@/filters/user.js'; defineProps<{ note: Misskey.entities.Note; }>(); + +const mock = inject<boolean>('mock', false); </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 1fa5685861..46faae9523 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -98,7 +98,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { inject, watch, nextTick, onMounted, defineAsyncComponent } from 'vue'; +import { inject, watch, nextTick, onMounted, defineAsyncComponent, provide } from 'vue'; import * as mfm from 'mfm-js'; import * as Misskey from 'misskey-js'; import insertTextAtCursor from 'insert-text-at-cursor'; @@ -143,15 +143,22 @@ const props = withDefaults(defineProps<{ fixed?: boolean; autofocus?: boolean; freezeAfterPosted?: boolean; + mock?: boolean; }>(), { initialVisibleUsers: () => [], autofocus: true, + mock: false, }); +provide('mock', props.mock); + const emit = defineEmits<{ (ev: 'posted'): void; (ev: 'cancel'): void; (ev: 'esc'): void; + + // Mock用 + (ev: 'fileChangeSensitive', fileId: string, to: boolean): void; }>(); const textareaEl = $shallowRef<HTMLTextAreaElement | null>(null); @@ -239,7 +246,7 @@ const maxTextLength = $computed((): number => { }); const canPost = $computed((): boolean => { - return !posting && !posted && + return !props.mock && !posting && !posted && (1 <= textLength || 1 <= files.length || !!poll || !!props.renote) && (textLength <= maxTextLength) && (!poll || poll.choices.length >= 2); @@ -396,6 +403,8 @@ function focus() { } function chooseFileFrom(ev) { + if (props.mock) return; + selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { for (const file of files_) { files.push(file); @@ -408,6 +417,9 @@ function detachFile(id) { } function updateFileSensitive(file, sensitive) { + if (props.mock) { + emit('fileChangeSensitive', file.id, sensitive); + } files[files.findIndex(x => x.id === file.id)].isSensitive = sensitive; } @@ -420,6 +432,8 @@ function replaceFile(file: Misskey.entities.DriveFile, newFile: Misskey.entities } function upload(file: File, name?: string): void { + if (props.mock) return; + uploadFile(file, defaultStore.state.uploadFolder, name).then(res => { files.push(res); }); @@ -545,6 +559,8 @@ function onCompositionEnd(ev: CompositionEvent) { } async function onPaste(ev: ClipboardEvent) { + if (props.mock) return; + for (const { item, i } of Array.from(ev.clipboardData.items, (item, i) => ({ item, i }))) { if (item.kind === 'file') { const file = item.getAsFile(); @@ -629,7 +645,7 @@ function onDrop(ev): void { } function saveDraft() { - if (props.instant) return; + if (props.instant || props.mock) return; const draftData = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}'); @@ -674,6 +690,8 @@ async function post(ev?: MouseEvent) { os.popup(MkRippleEffect, { x, y }, {}, 'end'); } + if (props.mock) return; + const annoying = text.includes('$[x2') || text.includes('$[x3') || @@ -839,6 +857,8 @@ function showActions(ev) { let postAccount = $ref<Misskey.entities.UserDetailed | null>(null); function openAccountMenu(ev: MouseEvent) { + if (props.mock) return; + openAccountMenu_({ withExtraOperation: false, includeCurrentAccount: true, @@ -869,7 +889,7 @@ onMounted(() => { nextTick(() => { // 書きかけの投稿を復元 - if (!props.instant && !props.mention && !props.specified) { + if (!props.instant && !props.mention && !props.specified && !props.mock) { const draft = JSON.parse(miLocalStorage.getItem('drafts') ?? '{}')[draftKey]; if (draft) { text = draft.data.text; diff --git a/packages/frontend/src/components/MkPostFormAttaches.vue b/packages/frontend/src/components/MkPostFormAttaches.vue index d499a22ed6..28a09c571f 100644 --- a/packages/frontend/src/components/MkPostFormAttaches.vue +++ b/packages/frontend/src/components/MkPostFormAttaches.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { defineAsyncComponent } from 'vue'; +import { defineAsyncComponent, inject } from 'vue'; import * as Misskey from 'misskey-js'; import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; import * as os from '@/os.js'; @@ -33,6 +33,8 @@ const props = defineProps<{ detachMediaFn?: (id: string) => void; }>(); +const mock = inject<boolean>('mock', false); + const emit = defineEmits<{ (ev: 'update:modelValue', value: any[]): void; (ev: 'detach', id: string): void; @@ -44,6 +46,8 @@ const emit = defineEmits<{ let menuShowing = false; function detachMedia(id: string) { + if (mock) return; + if (props.detachMediaFn) { props.detachMediaFn(id); } else { @@ -52,6 +56,11 @@ function detachMedia(id: string) { } function toggleSensitive(file) { + if (mock) { + emit('changeSensitive', file, !file.isSensitive); + return; + } + os.api('drive/files/update', { fileId: file.id, isSensitive: !file.isSensitive, @@ -61,6 +70,8 @@ function toggleSensitive(file) { } async function rename(file) { + if (mock) return; + const { canceled, result } = await os.inputText({ title: i18n.ts.enterFileName, default: file.name, @@ -77,6 +88,8 @@ async function rename(file) { } async function describe(file) { + if (mock) return; + os.popup(defineAsyncComponent(() => import('@/components/MkFileCaptionEditWindow.vue')), { default: file.comment !== null ? file.comment : '', file: file, @@ -94,6 +107,8 @@ async function describe(file) { } async function crop(file: Misskey.entities.DriveFile): Promise<void> { + if (mock) return; + const newFile = await os.cropImage(file, { aspectRatio: NaN }); emit('replaceFile', file, newFile); } diff --git a/packages/frontend/src/components/MkReactionsViewer.reaction.vue b/packages/frontend/src/components/MkReactionsViewer.reaction.vue index d0db515219..d532ef9b66 100644 --- a/packages/frontend/src/components/MkReactionsViewer.reaction.vue +++ b/packages/frontend/src/components/MkReactionsViewer.reaction.vue @@ -17,7 +17,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { computed, onMounted, shallowRef, watch } from 'vue'; +import { computed, inject, onMounted, shallowRef, watch } from 'vue'; import * as Misskey from 'misskey-js'; import XDetails from '@/components/MkReactionsViewer.details.vue'; import MkReactionIcon from '@/components/MkReactionIcon.vue'; @@ -36,6 +36,12 @@ const props = defineProps<{ note: Misskey.entities.Note; }>(); +const mock = inject<boolean>('mock', false); + +const emit = defineEmits<{ + (ev: 'reactionToggled', emoji: string, newCount: number): void; +}>(); + const buttonEl = shallowRef<HTMLElement>(); const canToggle = computed(() => !props.reaction.match(/@\w/) && $i); @@ -53,6 +59,11 @@ async function toggleReaction() { }); if (confirm.canceled) return; + if (mock) { + emit('reactionToggled', props.reaction, (props.count - 1)); + return; + } + os.api('notes/reactions/delete', { noteId: props.note.id, }).then(() => { @@ -64,6 +75,11 @@ async function toggleReaction() { } }); } else { + if (mock) { + emit('reactionToggled', props.reaction, (props.count + 1)); + return; + } + os.api('notes/reactions/create', { noteId: props.note.id, reaction: props.reaction, @@ -92,24 +108,26 @@ onMounted(() => { if (!props.isInitial) anime(); }); -useTooltip(buttonEl, async (showing) => { - const reactions = await os.apiGet('notes/reactions', { - noteId: props.note.id, - type: props.reaction, - limit: 11, - _cacheKey_: props.count, - }); +if (!mock) { + useTooltip(buttonEl, async (showing) => { + const reactions = await os.apiGet('notes/reactions', { + noteId: props.note.id, + type: props.reaction, + limit: 11, + _cacheKey_: props.count, + }); - const users = reactions.map(x => x.user); + const users = reactions.map(x => x.user); - os.popup(XDetails, { - showing, - reaction: props.reaction, - users, - count: props.count, - targetElement: buttonEl.value, - }, {}, 'closed'); -}, 100); + os.popup(XDetails, { + showing, + reaction: props.reaction, + users, + count: props.count, + targetElement: buttonEl.value, + }, {}, 'closed'); + }, 100); +} </script> <style lang="scss" module> diff --git a/packages/frontend/src/components/MkReactionsViewer.vue b/packages/frontend/src/components/MkReactionsViewer.vue index 52ead19a4b..eaa7faa4f9 100644 --- a/packages/frontend/src/components/MkReactionsViewer.vue +++ b/packages/frontend/src/components/MkReactionsViewer.vue @@ -12,14 +12,14 @@ SPDX-License-Identifier: AGPL-3.0-only :moveClass="defaultStore.state.animation ? $style.transition_x_move : ''" tag="div" :class="$style.root" > - <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note"/> + <XReaction v-for="[reaction, count] in reactions" :key="reaction" :reaction="reaction" :count="count" :isInitial="initialReactions.has(reaction)" :note="note" @reactionToggled="onMockToggleReaction"/> <slot v-if="hasMoreReactions" name="more"/> </TransitionGroup> </template> <script lang="ts" setup> import * as Misskey from 'misskey-js'; -import { watch } from 'vue'; +import { inject, watch } from 'vue'; import XReaction from '@/components/MkReactionsViewer.reaction.vue'; import { defaultStore } from '@/store.js'; @@ -30,6 +30,12 @@ const props = withDefaults(defineProps<{ maxNumber: Infinity, }); +const mock = inject<boolean>('mock', false); + +const emit = defineEmits<{ + (ev: 'mockUpdateMyReaction', emoji: string, delta: number): void; +}>(); + const initialReactions = new Set(Object.keys(props.note.reactions)); let reactions = $ref<[string, number][]>([]); @@ -39,6 +45,15 @@ if (props.note.myReaction && !Object.keys(reactions).includes(props.note.myReact reactions[props.note.myReaction] = props.note.reactions[props.note.myReaction]; } +function onMockToggleReaction(emoji: string, count: number) { + if (!mock) return; + + const i = reactions.findIndex((item) => item[0] === emoji); + if (i < 0) return; + + emit('mockUpdateMyReaction', emoji, (count - reactions[i][1])); +} + watch([() => props.note.reactions, () => props.maxNumber], ([newSource, maxNumber]) => { let newReactions: [string, number][] = []; hasMoreReactions = Object.keys(newSource).length > maxNumber; diff --git a/packages/frontend/src/components/MkTutorialDialog.Note.vue b/packages/frontend/src/components/MkTutorialDialog.Note.vue new file mode 100644 index 0000000000..c7df1a576e --- /dev/null +++ b/packages/frontend/src/components/MkTutorialDialog.Note.vue @@ -0,0 +1,117 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +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 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> + <div><i class="ti ti-plus"></i> <b>{{ i18n.ts.reaction }}</b> … {{ i18n.ts._initialTutorial._note.reaction }}</div> + <div><i class="ti ti-dots"></i> <b>{{ i18n.ts.menu }}</b> … {{ i18n.ts._initialTutorial._note.menu }}</div> + </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>{{ i18n.ts._initialTutorial._reaction.letsTryReacting }}</div> + <MkNote :class="$style.exampleNoteRoot" :note="exampleNote" :mock="true" @reaction="addReaction" @removeReaction="removeReaction" @updateReaction="updateReaction"/> + <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> +</div> +</template> + +<script setup lang="ts"> +import * as Misskey from 'misskey-js'; +import { ref, reactive } from 'vue'; +import { i18n } from '@/i18n.js'; +import { globalEvents } from '@/events.js'; +import { $i } from '@/account.js'; +import MkNote from '@/components/MkNote.vue'; + +const props = defineProps<{ + phase: 'aboutNote' | 'howToReact'; +}>(); + +const emit = defineEmits<{ + (ev: 'reacted'): void; +}>(); + +const exampleNote = reactive<Misskey.entities.Note>({ + id: '0000000000', + createdAt: '2019-04-14T17:30:49.181Z', + userId: '0000000001', + user: { + id: '0000000001', + name: '藍', + username: 'ai', + host: null, + avatarDecorations: [], + avatarUrl: '/client-assets/tutorial/ai.webp', + avatarBlurhash: 'eiKmhHIByXxZ~qWXs:-pR*NbR*s:xuRjoL-oR*WCt6WWf6WVf6oeWB', + isBot: false, + isCat: true, + emojis: {}, + onlineStatus: null, + badgeRoles: [], + }, + text: 'just setting up my msky', + cw: null, + visibility: 'public', + localOnly: false, + reactionAcceptance: null, + renoteCount: 0, + repliesCount: 1, + reactions: {}, + reactionEmojis: {}, + fileIds: [], + files: [], + replyId: null, + renoteId: null, +}); +const onceReacted = ref<boolean>(false); + +function addReaction(emoji) { + onceReacted.value = true; + emit('reacted'); + exampleNote.reactions[emoji] = 1; + exampleNote.myReaction = emoji; + doNotification(emoji); +} + +function doNotification(emoji: string): void { + if (!$i || !emoji) return; + + const notification: Misskey.entities.Notification = { + id: Math.random().toString(), + createdAt: new Date().toUTCString(), + isRead: false, + type: 'reaction', + reaction: emoji, + user: $i, + userId: $i.id, + note: exampleNote, + }; + + globalEvents.emit('clientNotification', notification); +} + +function removeReaction(emoji) { + delete exampleNote.reactions[emoji]; + exampleNote.myReaction = undefined; +} +</script> + +<style lang="scss" module> +.exampleNoteRoot { + border-radius: var(--radius); + border: var(--panelBorder); + background: var(--panel); +} + +.divider { + height: 1px; + background: var(--divider); +} +</style> diff --git a/packages/frontend/src/components/MkTutorialDialog.PostNote.vue b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue new file mode 100644 index 0000000000..9b55a1dca7 --- /dev/null +++ b/packages/frontend/src/components/MkTutorialDialog.PostNote.vue @@ -0,0 +1,135 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +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> + <MkPostForm :class="$style.exampleRoot" :mock="true"/> + <MkFormSection> + <template #label>{{ i18n.ts.visibility }}</template> + <div class="_gaps"> + <div>{{ i18n.ts._initialTutorial._postNote._visibility.description }}</div> + <div><i class="ti ti-world"></i> <b>{{ i18n.ts._visibility.public }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.public }}</div> + <div><i class="ti ti-home"></i> <b>{{ i18n.ts._visibility.home }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.home }}</div> + <div><i class="ti ti-lock"></i> <b>{{ i18n.ts._visibility.followers }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.followers }}</div> + <div class="_gaps_s"> + <div><i class="ti ti-mail"></i> <b>{{ i18n.ts._visibility.specified }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.direct }}</div> + <MkInfo :warn="true"> + <b>{{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect1 }}</b> {{ i18n.ts._initialTutorial._postNote._visibility.doNotSendConfidencialOnDirect2 }} + </MkInfo> + </div> + <div><i class="ti ti-rocket-off"></i> <b>{{ i18n.ts._visibility.disableFederation }}</b> … {{ i18n.ts._initialTutorial._postNote._visibility.localOnly }}</div> + </div> + </MkFormSection> + <MkFormSection> + <template #label>{{ i18n.ts._initialTutorial._postNote._cw.title }}</template> + <div class="_gaps"> + <div>{{ i18n.ts._initialTutorial._postNote._cw.description }}</div> + <MkNote :class="$style.exampleRoot" :note="exampleCWNote" :mock="true"/> + <div>{{ i18n.ts._initialTutorial._postNote._cw.useCases }}</div> + </div> + </MkFormSection> +</div> +</template> + +<script setup lang="ts"> +import * as Misskey from 'misskey-js'; +import { reactive } from 'vue'; +import { i18n } from '@/i18n.js'; +import MkNote from '@/components/MkNote.vue'; +import MkPostForm from '@/components/MkPostForm.vue'; +import MkFormSection from '@/components/form/section.vue'; +import MkInfo from '@/components/MkInfo.vue'; + +const exampleCWNote = reactive<Misskey.entities.Note>({ + id: '0000000000', + createdAt: '2019-04-14T17:30:49.181Z', + userId: '0000000001', + user: { + id: '0000000001', + name: '藍', + username: 'ai', + host: null, + avatarDecorations: [], + avatarUrl: '/client-assets/tutorial/ai.webp', + avatarBlurhash: 'eiKmhHIByXxZ~qWXs:-pR*NbR*s:xuRjoL-oR*WCt6WWf6WVf6oeWB', + isBot: false, + isCat: true, + emojis: {}, + onlineStatus: null, + badgeRoles: [], + }, + text: i18n.ts._initialTutorial._postNote._cw._exampleNote.note, + cw: i18n.ts._initialTutorial._postNote._cw._exampleNote.cw, + visibility: 'public', + localOnly: false, + reactionAcceptance: null, + renoteCount: 0, + repliesCount: 1, + reactions: {}, + reactionEmojis: {}, + fileIds: [], + files: [], + replyId: null, + renoteId: null, +}); +</script> + +<style lang="scss" module> +.exampleRoot { + max-width: none!important; + border-radius: var(--radius); + border: var(--panelBorder); + background: var(--panel); +} + +.divider { + height: 1px; + background: var(--divider); +} + +.image { + max-width: 300px; + margin: 0 auto; +} + +.post { + position: relative; + display: block; + width: 100%; + height: 40px; + color: var(--fgOnAccent); + font-weight: bold; + text-align: left; + + &:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } + +} + +.postIcon { + position: relative; + margin-left: 30px; + margin-right: 8px; + width: 32px; +} + +.postText { + position: relative; + line-height: 40px; +} +</style> diff --git a/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue new file mode 100644 index 0000000000..768d00bb07 --- /dev/null +++ b/packages/frontend/src/components/MkTutorialDialog.Sensitive.vue @@ -0,0 +1,144 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +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>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.tryThisFile }}</div> + <MkInfo>{{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.method }}</MkInfo> + <MkPostForm + :class="$style.exampleRoot" + :mock="true" + :initialNote="exampleNote" + @fileChangeSensitive="doSucceeded" + ></MkPostForm> + <div v-if="onceSucceeded"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ i18n.ts._initialTutorial._howToMakeAttachmentsSensitive.sensitiveSucceeded }}</div> + <MkFolder> + <template #label>{{ i18n.ts.previewNoteText }}</template> + <MkNote :mock="true" :note="exampleNote" :class="$style.exampleRoot"></MkNote> + </MkFolder> +</div> +</template> + +<script setup lang="ts"> +import * as Misskey from 'misskey-js'; +import { ref, reactive } from 'vue'; +import { i18n } from '@/i18n.js'; +import MkPostForm from '@/components/MkPostForm.vue'; +import MkFolder from '@/components/MkFolder.vue'; +import MkInfo from '@/components/MkInfo.vue'; +import MkNote from '@/components/MkNote.vue'; +import { $i } from '@/account.js'; + +const emit = defineEmits<{ + (ev: 'succeeded'): void; +}>(); + +const onceSucceeded = ref<boolean>(false); + +function doSucceeded(fileId: string, to: boolean) { + if (fileId === exampleNote.fileIds[0] && to) { + onceSucceeded.value = true; + emit('succeeded'); + } +} + +const exampleNote = reactive<Misskey.entities.Note>({ + id: '0000000000', + createdAt: '2019-04-14T17:30:49.181Z', + userId: '0000000001', + user: $i!, + text: i18n.ts._initialTutorial._howToMakeAttachmentsSensitive._exampleNote.note, + cw: null, + visibility: 'public', + localOnly: false, + reactionAcceptance: null, + renoteCount: 0, + repliesCount: 1, + reactions: {}, + reactionEmojis: {}, + fileIds: ['0000000002'], + files: [{ + id: '0000000002', + createdAt: '2019-04-14T17:30:49.181Z', + name: 'natto_failed.webp', + type: 'image/webp', + md5: 'c44286cf152d0740be0ce5ad45ea85c3', + size: 827532, + isSensitive: false, + blurhash: 'LXNA3TD*XAIA%1%M%gt7.TofRioz', + properties: { + width: 256, + height: 256, + }, + url: '/client-assets/tutorial/natto_failed.webp', + thumbnailUrl: '/client-assets/tutorial/natto_failed.webp', + comment: null, + folderId: null, + folder: null, + userId: null, + user: null, + }], + replyId: null, + renoteId: null, +}); + +</script> + +<style lang="scss" module> +.exampleRoot { + border-radius: var(--radius); + border: var(--panelBorder); + background: var(--panel); +} + +.divider { + height: 1px; + background: var(--divider); +} + +.image { + max-width: 300px; + margin: 0 auto; +} + +.post { + position: relative; + display: block; + width: 100%; + height: 40px; + color: var(--fgOnAccent); + font-weight: bold; + text-align: left; + + &:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } + +} + +.postIcon { + position: relative; + margin-left: 30px; + margin-right: 8px; + width: 32px; +} + +.postText { + position: relative; + line-height: 40px; +} +</style> diff --git a/packages/frontend/src/components/MkTutorialDialog.Timeline.vue b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue new file mode 100644 index 0000000000..75b917f33c --- /dev/null +++ b/packages/frontend/src/components/MkTutorialDialog.Timeline.vue @@ -0,0 +1,87 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +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 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> + <div><i class="ti ti-universe"></i> <b>{{ i18n.ts._timelines.social }}</b> … {{ i18n.ts._initialTutorial._timeline.social }}</div> + <div><i class="ti ti-whirl"></i> <b>{{ i18n.ts._timelines.global }}</b> … {{ i18n.ts._initialTutorial._timeline.global }}</div> + </div> + <div class="_gaps_s"> + <div>{{ i18n.ts._initialTutorial._timeline.description2 }}</div> + <img :class="$style.image" src="/client-assets/tutorial/timeline_tab.png"/> + </div> + <div :class="$style.divider"></div> + <I18n :src="i18n.ts._initialTutorial._timeline.description3" tag="div" style="padding: 0 16px;"> + <template #link> + <a href="https://misskey-hub.net/docs/features/timeline.html" target="_blank" class="_link">{{ i18n.ts.help }}</a> + </template> + </I18n> + +</div> +</template> + +<script setup lang="ts"> +import { i18n } from '@/i18n.js'; +</script> + +<style lang="scss" module> +.exampleNoteRoot { + border-radius: var(--radius); + border: var(--panelBorder); + background: var(--panel); +} + +.divider { + height: 1px; + background: var(--divider); +} + +.image { + max-width: 300px; + margin: 0 auto; +} + +.post { + position: relative; + display: block; + width: 100%; + height: 40px; + color: var(--fgOnAccent); + font-weight: bold; + text-align: left; + + &:before { + content: ""; + display: block; + width: calc(100% - 38px); + height: 100%; + margin: auto; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-radius: 999px; + background: linear-gradient(90deg, var(--buttonGradateA), var(--buttonGradateB)); + } + +} + +.postIcon { + position: relative; + margin-left: 30px; + margin-right: 8px; + width: 32px; +} + +.postText { + position: relative; + line-height: 40px; +} +</style> diff --git a/packages/frontend/src/components/MkTutorialDialog.vue b/packages/frontend/src/components/MkTutorialDialog.vue new file mode 100644 index 0000000000..e28838425f --- /dev/null +++ b/packages/frontend/src/components/MkTutorialDialog.vue @@ -0,0 +1,260 @@ +<!-- +SPDX-FileCopyrightText: syuilo and other misskey contributors +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> +<MkModalWindow + ref="dialog" + :width="600" + :height="650" + @close="close(true)" + @closed="emit('closed')" +> + <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 #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/help.html" target="_blank" class="_link">{{ i18n.ts.help }}</a> + </template> + </I18n> + <div>{{ i18n.t('_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> +</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 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 { 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<{ + initialPage?: number; +}>(); + +const emit = defineEmits<{ + (ev: 'closed'): void; +}>(); + +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'); + } +}); + +const isReactionTutorialPushed = ref<boolean>(false); +const isSensitiveTutorialSucceeded = ref<boolean>(false); + +async function close(skip: boolean) { + if (skip) { + const { canceled } = await os.confirm({ + type: 'warning', + text: i18n.ts._initialTutorial.skipAreYouSure, + }); + if (canceled) return; + } + + dialog.value?.close(); +} +</script> + +<style lang="scss" module> +.transition_x_enterActive, +.transition_x_leaveActive { + transition: opacity 0.3s cubic-bezier(0,0,.35,1), transform 0.3s cubic-bezier(0,0,.35,1); +} +.transition_x_enterFrom { + opacity: 0; + transform: translateX(50px); +} +.transition_x_leaveTo { + opacity: 0; + transform: translateX(-50px); +} + +.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; + line-height: 1.5; +} + +.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> diff --git a/packages/frontend/src/components/MkUserSetupDialog.vue b/packages/frontend/src/components/MkUserSetupDialog.vue index d60e01c44d..05b55f77a7 100644 --- a/packages/frontend/src/components/MkUserSetupDialog.vue +++ b/packages/frontend/src/components/MkUserSetupDialog.vue @@ -46,24 +46,32 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <template v-else-if="page === 1"> <div style="height: 100cqh; overflow: auto;"> - <MkSpacer :marginMin="20" :marginMax="28"> - <XProfile/> - <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 :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> - </MkSpacer> + </div> </div> </template> <template v-else-if="page === 2"> <div style="height: 100cqh; overflow: auto;"> - <MkSpacer :marginMin="20" :marginMax="28"> - <XPrivacy/> - <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 :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> - </MkSpacer> + </div> </div> </template> <template v-else-if="page === 3"> @@ -102,16 +110,13 @@ SPDX-License-Identifier: AGPL-3.0-only <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> + <div>{{ i18n.t('_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 primary rounded gradate data-cy-user-setup-continue @click="close(false)">{{ i18n.ts.close }}</MkButton> + <MkButton rounded primary data-cy-user-setup-continue @click="setupComplete()">{{ i18n.ts.close }}</MkButton> </div> </div> </MkSpacer> @@ -123,7 +128,7 @@ SPDX-License-Identifier: AGPL-3.0-only </template> <script lang="ts" setup> -import { ref, shallowRef, watch } from 'vue'; +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'; @@ -143,6 +148,7 @@ const emit = defineEmits<{ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>(); +// eslint-disable-next-line vue/no-setup-props-destructure const page = ref(defaultStore.state.accountSetupWizard); watch(page, () => { @@ -158,8 +164,22 @@ async function close(skip: boolean) { if (canceled) return; } - dialog.value.close(); + 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) { @@ -171,7 +191,7 @@ async function later(later: boolean) { if (canceled) return; } - dialog.value.close(); + dialog.value?.close(); defaultStore.set('accountSetupWizard', 0); } </script> @@ -214,10 +234,21 @@ async function later(later: boolean) { 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); |