better onboarding experience
This commit is contained in:
parent
4dd4a11cef
commit
9f306cc493
|
@ -1174,10 +1174,46 @@ export interface Locale {
|
|||
"pushNotificationDescription": string;
|
||||
"initialAccountSettingCompleted": string;
|
||||
"haveFun": string;
|
||||
"ifYouNeedLearnMore": string;
|
||||
"youCanContinueTutorial": string;
|
||||
"startTutorial": string;
|
||||
"skipAreYouSure": string;
|
||||
"laterAreYouSure": string;
|
||||
};
|
||||
"_initialTutorial": {
|
||||
"title": string;
|
||||
"wellDone": string;
|
||||
"_note": {
|
||||
"description": string;
|
||||
"date": string;
|
||||
"reply": string;
|
||||
"renote": string;
|
||||
"reaction": string;
|
||||
"howToNote": string;
|
||||
};
|
||||
"_reaction": {
|
||||
"description": string;
|
||||
"letsTryReacting": string;
|
||||
"reactDone": string;
|
||||
};
|
||||
"_timeline": {
|
||||
"description1": string;
|
||||
"home": string;
|
||||
"local": string;
|
||||
"social": string;
|
||||
"global": string;
|
||||
"description2": string;
|
||||
};
|
||||
"_done": {
|
||||
"title": string;
|
||||
"description": string;
|
||||
};
|
||||
};
|
||||
"_timelineDescription": {
|
||||
"home": string;
|
||||
"local": string;
|
||||
"social": string;
|
||||
"global": string;
|
||||
};
|
||||
"_serverRules": {
|
||||
"description": string;
|
||||
};
|
||||
|
@ -1851,17 +1887,6 @@ export interface Locale {
|
|||
"hour": string;
|
||||
"day": string;
|
||||
};
|
||||
"_timelineTutorial": {
|
||||
"title": string;
|
||||
"step1_1": string;
|
||||
"step1_2": string;
|
||||
"step2_1": string;
|
||||
"step2_2": string;
|
||||
"step3_1": string;
|
||||
"step3_2": string;
|
||||
"step4_1": string;
|
||||
"step4_2": string;
|
||||
};
|
||||
"_2fa": {
|
||||
"alreadyRegistered": string;
|
||||
"registerTOTP": string;
|
||||
|
|
|
@ -1172,10 +1172,42 @@ _initialAccountSetting:
|
|||
pushNotificationDescription: "プッシュ通知を有効にすると{name}の通知をお使いのデバイスで受け取ることができます。"
|
||||
initialAccountSettingCompleted: "初期設定が完了しました!"
|
||||
haveFun: "{name}をお楽しみください!"
|
||||
ifYouNeedLearnMore: "{name}(Misskey)の使い方などを詳しく知るには{link}をご覧ください。"
|
||||
youCanContinueTutorial: "このまま{name}(Misskey)の使い方についてのチュートリアルに進むこともできますが、ここで中断してすぐに使い始めることもできます。"
|
||||
startTutorial: "チュートリアルを開始"
|
||||
skipAreYouSure: "初期設定をスキップしますか?"
|
||||
laterAreYouSure: "初期設定をあとでやり直しますか?"
|
||||
|
||||
_initialTutorial:
|
||||
title: "チュートリアル"
|
||||
wellDone: "お見事!"
|
||||
_note:
|
||||
description: "Misskeyでの投稿は「ノート」と呼びます。ノートはタイムラインに時系列で並んでいて、リアルタイムで更新されていきます。"
|
||||
date: "日付をクリックすることで、ノートの詳細ページに移動することができます。"
|
||||
reply: "このボタンから返信できます。もちろん返信に返信することもできるので、気が済むまで会話を続けることができます。"
|
||||
renote: "このボタンを押すと、このノートを自分のタイムラインに投稿することができます。引用リノートも可能です。"
|
||||
reaction: "リアクションをつけることができます。詳しくは次のページで解説します。"
|
||||
howToNote: "ノートは、下のようなボタンから簡単に行なえます。チュートリアルが終わったら早速やってみてくださいね!"
|
||||
_reaction:
|
||||
description: "ノートには「リアクション」をつけることができます。「いいね」では伝わらないニュアンスも、リアクションで簡単・気軽に表現できます。"
|
||||
letsTryReacting: "リアクションは、ノートの「+」ボタンをクリックするとつけられます。試しにこのノートに好きなリアクションをつけてみましょう!"
|
||||
reactDone: "リアクションを外すときは、「ー」ボタンを押します。実際に試してみてくださいね。"
|
||||
_timeline:
|
||||
description1: "Misskeyには、使い方に応じて複数のタイムラインが用意されています(サーバーによってはいずれかが無効になっていることがあります)。"
|
||||
home: "あなたがフォローしているアカウントの投稿を見られます。"
|
||||
local: "このサーバーにいるユーザー全員の投稿を見られます。"
|
||||
social: "ホームタイムラインとローカルタイムラインの投稿が両方表示されます。"
|
||||
global: "接続している他のすべてのサーバーからの投稿を見られます。"
|
||||
description2: "その他にも、リストタイムラインやチャンネルタイムラインなどがあります。詳しくは{link}をご覧ください。"
|
||||
_done:
|
||||
title: "チュートリアルは終了です🎉"
|
||||
description: "ここで紹介した機能は、ほんの一部です!Misskeyの使い方をより詳しく知るには、{link}をご覧ください。"
|
||||
|
||||
_timelineDescription:
|
||||
home: "ホームタイムラインでは、あなたがフォローしているアカウントの投稿を見られます。"
|
||||
local: "ローカルタイムラインでは、このサーバーにいるユーザー全員の投稿を見られます。"
|
||||
social: "ソーシャルタイムラインには、ホームタイムラインとローカルタイムラインの投稿が両方表示されます。"
|
||||
global: "グローバルタイムラインでは、接続している他のすべてのサーバーからの投稿を見られます。"
|
||||
|
||||
_serverRules:
|
||||
description: "新規登録前に表示する、サーバーの簡潔なルールを設定します。内容は利用規約の要約とすることを推奨します。"
|
||||
|
||||
|
@ -1768,17 +1800,6 @@ _time:
|
|||
hour: "時間"
|
||||
day: "日"
|
||||
|
||||
_timelineTutorial:
|
||||
title: "Misskeyの使い方"
|
||||
step1_1: "この画面は「タイムライン」です。{name}に投稿された「ノート」が時系列で表示されます。"
|
||||
step1_2: "タイムラインにはいくつか種類があり、例えば「ホームタイムライン」にはあなたがフォローしている人のノートが流れ、「ローカルタイムライン」には{name}全体のノートが流れます。"
|
||||
step2_1: "試しに、何かノートを投稿してみましょう。画面上にある鉛筆マークのボタンを押すとフォームが開きます。"
|
||||
step2_2: "初めてのノートの内容は、あなたの自己紹介や「{name}始めました」などがおすすめです。"
|
||||
step3_1: "投稿できましたか?"
|
||||
step3_2: "あなたのノートがタイムラインに表示されていれば成功です。"
|
||||
step4_1: "ノートには、「リアクション」を付けることができます。"
|
||||
step4_2: "リアクションを付けるには、ノートの「+」マークをクリックして、好きな絵文字を選択します。"
|
||||
|
||||
_2fa:
|
||||
alreadyRegistered: "既に設定は完了しています。"
|
||||
registerTOTP: "認証アプリの設定を開始"
|
||||
|
|
|
@ -8,6 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<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>
|
||||
<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>
|
||||
|
|
|
@ -47,9 +47,9 @@ 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"/>
|
||||
<MkNoteHeader :note="appearNote" :mock="mock" :mini="true"/>
|
||||
<MkInstanceTicker v-if="showTicker" :instance="appearNote.user.instance"/>
|
||||
<div style="container-type: inline-size;">
|
||||
<p v-if="appearNote.cw != null" :class="$style.cw">
|
||||
|
@ -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 } from 'vue';
|
||||
import * as mfm from 'mfm-js';
|
||||
import * as Misskey from 'misskey-js';
|
||||
import MkNoteSub from '@/components/MkNoteSub.vue';
|
||||
|
@ -170,9 +170,17 @@ 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,
|
||||
});
|
||||
|
||||
const emit = defineEmits<{
|
||||
(ev: 'reaction', emoji: string): void;
|
||||
(ev: 'removeReaction', emoji: string): void;
|
||||
}>();
|
||||
|
||||
const inChannel = inject('inChannel', null);
|
||||
|
@ -229,30 +237,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,
|
||||
});
|
||||
}
|
||||
|
||||
const users = renotes.map(x => x.user);
|
||||
if (!props.mock) {
|
||||
useTooltip(renoteButton, async (showing) => {
|
||||
const renotes = await os.api('notes/renotes', {
|
||||
noteId: appearNote.id,
|
||||
limit: 11,
|
||||
});
|
||||
|
||||
if (users.length < 1) return;
|
||||
const users = renotes.map(x => x.user);
|
||||
|
||||
os.popup(MkUsersTooltip, {
|
||||
showing,
|
||||
users,
|
||||
count: appearNote.renoteCount,
|
||||
targetElement: renoteButton.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
if (users.length < 1) return;
|
||||
|
||||
os.popup(MkUsersTooltip, {
|
||||
showing,
|
||||
users,
|
||||
count: appearNote.renoteCount,
|
||||
targetElement: renoteButton.value,
|
||||
}, {}, 'closed');
|
||||
});
|
||||
}
|
||||
|
||||
type Visibility = 'public' | 'home' | 'followers' | 'specified';
|
||||
|
||||
|
@ -284,21 +300,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]);
|
||||
}
|
||||
|
@ -324,15 +344,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: () => {
|
||||
|
@ -349,6 +371,9 @@ function renote(viaKeyboard = false) {
|
|||
|
||||
function reply(viaKeyboard = false): void {
|
||||
pleaseLogin();
|
||||
if (props.mock) {
|
||||
return;
|
||||
}
|
||||
os.post({
|
||||
reply: appearNote,
|
||||
channel: appearNote.channel,
|
||||
|
@ -362,6 +387,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: '❤️',
|
||||
|
@ -376,6 +405,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,
|
||||
|
@ -392,12 +426,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要素のコンテキストメニューはブラウザデフォルトとする。
|
||||
|
@ -419,6 +463,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,
|
||||
|
@ -426,10 +474,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,
|
||||
|
|
|
@ -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]">
|
||||
|
@ -37,6 +43,7 @@ import { userPage } from '@/filters/user.js';
|
|||
|
||||
defineProps<{
|
||||
note: Misskey.entities.Note;
|
||||
mock?: boolean;
|
||||
}>();
|
||||
</script>
|
||||
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
<!--
|
||||
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><small><MkTime :time="exampleNote.createdAt" colored></MkTime></small> … {{ i18n.ts._initialTutorial._note.date }}</div>
|
||||
<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>
|
||||
<div :class="$style.divider"></div>
|
||||
<div style="text-align: center; padding: 0 16px;">{{ i18n.ts._initialTutorial._note.howToNote }}</div>
|
||||
<div style="width: 250px; margin: 0 auto;">
|
||||
<div :class="[$style.post]">
|
||||
<i class="ti ti-pencil ti-fw" :class="$style.postIcon"></i><span :class="$style.postText">{{ i18n.ts.note }}</span>
|
||||
</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"/>
|
||||
<div v-if="onceReacted"><b style="color: var(--accent);"><i class="ti ti-check"></i> {{ i18n.ts._initialTutorial.wellDone }}</b> {{ 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 MkTime from '@/components/global/MkTime.vue';
|
||||
import MkNote from '@/components/MkNote.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
phase: 'aboutNote' | 'howToReact';
|
||||
}>();
|
||||
|
||||
const exampleNote = reactive<Misskey.entities.Note>({
|
||||
id: '0000000000',
|
||||
createdAt: '2019-04-14T17:30:49.181Z',
|
||||
userId: '0000000001',
|
||||
user: {
|
||||
id: '0000000001',
|
||||
name: 'しゅいろ',
|
||||
username: 'syuilo',
|
||||
host: null,
|
||||
avatarDecorations: [],
|
||||
avatarUrl: 'https://proxy.misskeyusercontent.com/avatar.webp?url=https%3A%2F%2Fs3.arkjp.net%2Fmisskey%2Fwebpublic-b2dc591e-58b6-4df7-b7c9-1cba199f6619.png&avatar=1',
|
||||
avatarBlurhash: 'yFF5Kq0L00?a^*IBNG01^j-pV@D*o|xt58WB}@9at7s.Ip~AWB57%Laes:xaOEoLnis:ofIpoJr?NHtRV@oLoeNHNI%1M{kCWCjuxZ',
|
||||
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;
|
||||
exampleNote.reactions[emoji] = 1;
|
||||
exampleNote.myReaction = emoji;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
.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>
|
|
@ -0,0 +1,77 @@
|
|||
<!--
|
||||
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>
|
||||
<I18n :src="i18n.ts._initialTutorial._timeline.description2" 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>
|
||||
</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);
|
||||
}
|
||||
|
||||
.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>
|
|
@ -17,10 +17,11 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<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-if="page >= 6" #header>{{ i18n.ts._initialTutorial.title }}</template>
|
||||
<template v-else #header>{{ i18n.ts.initialAccountSetting }}</template>
|
||||
|
||||
<div style="overflow-x: clip;">
|
||||
<div :class="$style.progressBar">
|
||||
<div v-if="page <= 5" :class="$style.progressBar">
|
||||
<div :class="$style.progressBarValue" :style="{ width: `${(page / 5) * 100}%` }"></div>
|
||||
</div>
|
||||
<Transition
|
||||
|
@ -102,8 +103,65 @@ 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>
|
||||
<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="() => { setupComplete(); page++; }">{{ 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="close(false)">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</div>
|
||||
</template>
|
||||
<template v-else-if="page === 6">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<XTutorialNote phase="aboutNote"/>
|
||||
</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 === 7">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<XTutorialNote phase="howToReact"/>
|
||||
</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 === 8">
|
||||
<div style="height: 100cqh; overflow: auto;">
|
||||
<MkSpacer :marginMin="20" :marginMax="28">
|
||||
<XTutorialTimeline phase="howToReact"/>
|
||||
</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 === 9">
|
||||
<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>
|
||||
|
@ -111,7 +169,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<div>{{ i18n.t('_initialAccountSetting.haveFun', { name: instance.name ?? host }) }}</div>
|
||||
<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="close(false)">{{ i18n.ts.close }}</MkButton>
|
||||
<MkButton rounded primary gradate data-cy-user-setup-continue @click="close(false)">{{ i18n.ts.close }}</MkButton>
|
||||
</div>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
|
@ -129,6 +187,8 @@ 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 XTutorialNote from '@/components/MkUserSetupDialog.NoteTutorial.vue';
|
||||
import XTutorialTimeline from '@/components/MkUserSetupDialog.TimelineTutorial.vue';
|
||||
import MkAnimBg from '@/components/MkAnimBg.vue';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
|
@ -146,7 +206,9 @@ const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
|
|||
const page = ref(defaultStore.state.accountSetupWizard);
|
||||
|
||||
watch(page, () => {
|
||||
defaultStore.set('accountSetupWizard', page.value);
|
||||
if (page.value <= 5) {
|
||||
defaultStore.set('accountSetupWizard', page.value);
|
||||
}
|
||||
});
|
||||
|
||||
async function close(skip: boolean) {
|
||||
|
@ -162,6 +224,10 @@ async function close(skip: boolean) {
|
|||
defaultStore.set('accountSetupWizard', -1);
|
||||
}
|
||||
|
||||
function setupComplete() {
|
||||
defaultStore.set('accountSetupWizard', -1);
|
||||
}
|
||||
|
||||
async function later(later: boolean) {
|
||||
if (later) {
|
||||
const { canceled } = await os.confirm({
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
<!--
|
||||
SPDX-FileCopyrightText: syuilo and other misskey contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-only
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="$style.title">
|
||||
<div :class="$style.titleText"><i class="ti ti-info-circle"></i> {{ i18n.ts._timelineTutorial.title }}</div>
|
||||
<div :class="$style.step">
|
||||
<button class="_button" :class="$style.stepArrow" :disabled="tutorial === 0" @click="tutorial--">
|
||||
<i class="ti ti-chevron-left"></i>
|
||||
</button>
|
||||
<span :class="$style.stepNumber">{{ tutorial + 1 }} / {{ tutorialsNumber }}</span>
|
||||
<button class="_button" :class="$style.stepArrow" :disabled="tutorial === tutorialsNumber - 1" @click="tutorial++">
|
||||
<i class="ti ti-chevron-right"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tutorial === 0" :class="$style.body">
|
||||
<div>{{ i18n.t('_timelineTutorial.step1_1', { name: instance.name ?? host }) }}</div>
|
||||
<div>{{ i18n.t('_timelineTutorial.step1_2', { name: instance.name ?? host }) }}</div>
|
||||
</div>
|
||||
<div v-else-if="tutorial === 1" :class="$style.body">
|
||||
<div>{{ i18n.ts._timelineTutorial.step2_1 }}</div>
|
||||
<div>{{ i18n.t('_timelineTutorial.step2_2', { name: instance.name ?? host }) }}</div>
|
||||
</div>
|
||||
<div v-else-if="tutorial === 2" :class="$style.body">
|
||||
<div>{{ i18n.ts._timelineTutorial.step3_1 }}</div>
|
||||
<div>{{ i18n.ts._timelineTutorial.step3_2 }}</div>
|
||||
</div>
|
||||
<div v-else-if="tutorial === 3" :class="$style.body">
|
||||
<div>{{ i18n.ts._timelineTutorial.step4_1 }}</div>
|
||||
<div>{{ i18n.ts._timelineTutorial.step4_2 }}</div>
|
||||
</div>
|
||||
|
||||
<div :class="$style.footer">
|
||||
<template v-if="tutorial === tutorialsNumber - 1">
|
||||
<MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial = -1">{{ i18n.ts.done }} <i class="ti ti-check"></i></MkButton>
|
||||
</template>
|
||||
<template v-else>
|
||||
<MkButton :class="$style.footerItem" primary rounded gradate @click="tutorial++">{{ i18n.ts.next }} <i class="ti ti-arrow-right"></i></MkButton>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import MkButton from '@/components/MkButton.vue';
|
||||
import { defaultStore } from '@/store.js';
|
||||
import { i18n } from '@/i18n.js';
|
||||
import { instance } from '@/instance.js';
|
||||
import { host } from '@/config.js';
|
||||
|
||||
const tutorialsNumber = 4;
|
||||
|
||||
const tutorial = computed({
|
||||
get() { return defaultStore.reactiveState.timelineTutorial.value || 0; },
|
||||
set(value) { defaultStore.set('timelineTutorial', value); },
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
.small {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.container {
|
||||
border: solid 2px var(--accent);
|
||||
}
|
||||
|
||||
.title {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
padding: 22px 32px;
|
||||
font-weight: bold;
|
||||
|
||||
&Text {
|
||||
margin: 4px 0;
|
||||
padding-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.step {
|
||||
margin-left: auto;
|
||||
|
||||
&Arrow {
|
||||
padding: 4px;
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
&:first-child {
|
||||
padding-right: 8px;
|
||||
}
|
||||
&:last-child {
|
||||
padding-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
&Number {
|
||||
font-weight: normal;
|
||||
margin: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 0 32px;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-direction: row;
|
||||
justify-content: right;
|
||||
padding: 22px 32px;
|
||||
|
||||
&Item {
|
||||
margin: 4px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -8,7 +8,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
<template #header><MkPageHeader v-model:tab="src" :actions="headerActions" :tabs="$i ? headerTabs : headerTabsWhenNotLogin" :displayMyAvatar="true"/></template>
|
||||
<MkSpacer :contentMax="800">
|
||||
<div ref="rootEl" v-hotkey.global="keymap">
|
||||
<XTutorial v-if="$i && defaultStore.reactiveState.timelineTutorial.value != -1" class="_panel" style="margin-bottom: var(--margin);"/>
|
||||
<MkInfo v-if="['home', 'local', 'social', 'global'].includes(src) && !defaultStore.reactiveState.timelineTutorials.value[src]" style="margin-bottom: var(--margin);" closable @close="closeTutorial()">
|
||||
{{ i18n.ts._timelineDescription[src] }}
|
||||
</MkInfo>
|
||||
<MkPostForm v-if="defaultStore.reactiveState.showFixedPostForm.value" :class="$style.postForm" class="post-form _panel" fixed style="margin-bottom: var(--margin);"/>
|
||||
|
||||
<div v-if="queue > 0" :class="$style.new"><button class="_buttonPrimary" :class="$style.newButton" @click="top()">{{ i18n.ts.newNoteRecived }}</button></div>
|
||||
|
@ -34,6 +36,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
|||
import { defineAsyncComponent, computed, watch, provide } from 'vue';
|
||||
import type { Tab } from '@/components/global/MkPageHeader.tabs.vue';
|
||||
import MkTimeline from '@/components/MkTimeline.vue';
|
||||
import MkInfo from '@/components/MkInfo.vue';
|
||||
import MkPostForm from '@/components/MkPostForm.vue';
|
||||
import { scroll } from '@/scripts/scroll.js';
|
||||
import * as os from '@/os.js';
|
||||
|
@ -47,8 +50,6 @@ import { antennasCache, userListsCache } from '@/cache.js';
|
|||
|
||||
provide('shouldOmitHeaderTitle', true);
|
||||
|
||||
const XTutorial = defineAsyncComponent(() => import('./timeline.tutorial.vue'));
|
||||
|
||||
const isLocalTimelineAvailable = ($i == null && instance.policies.ltlAvailable) || ($i != null && $i.policies.ltlAvailable);
|
||||
const isGlobalTimelineAvailable = ($i == null && instance.policies.gtlAvailable) || ($i != null && $i.policies.gtlAvailable);
|
||||
const keymap = {
|
||||
|
@ -139,6 +140,13 @@ function focus(): void {
|
|||
tlComponent.focus();
|
||||
}
|
||||
|
||||
function closeTutorial(): void {
|
||||
if (!['home', 'local', 'social', 'global'].includes(src)) return;
|
||||
const before = defaultStore.state.timelineTutorials;
|
||||
before[src] = true;
|
||||
defaultStore.set('timelineTutorials', before);
|
||||
}
|
||||
|
||||
const headerActions = $computed(() => [{
|
||||
icon: 'ti ti-dots',
|
||||
text: i18n.ts.options,
|
||||
|
|
|
@ -49,9 +49,14 @@ export const defaultStore = markRaw(new Storage('base', {
|
|||
where: 'account',
|
||||
default: 0,
|
||||
},
|
||||
timelineTutorial: {
|
||||
timelineTutorials: {
|
||||
where: 'account',
|
||||
default: 0,
|
||||
default: {
|
||||
home: false,
|
||||
local: false,
|
||||
social: false,
|
||||
global: false,
|
||||
},
|
||||
},
|
||||
keepCw: {
|
||||
where: 'account',
|
||||
|
|
Loading…
Reference in New Issue