enhance(frontend): ウェルカムタイムラインのデザインを調整 (#14156)
* enhance(frontend): 非ログイン時のハイライトTLのデザイン調整 * Update Changelog * fix cw handling * ホバーしてたらスクロールを止めるように * fix * lint
This commit is contained in:
		
							parent
							
								
									f8ac3fe343
								
							
						
					
					
						commit
						6b876da44a
					
				
					 3 changed files with 162 additions and 46 deletions
				
			
		| 
						 | 
				
			
			@ -7,6 +7,7 @@
 | 
			
		|||
 | 
			
		||||
### Client
 | 
			
		||||
- Enhance: 内蔵APIドキュメントのデザイン・パフォーマンスを改善
 | 
			
		||||
- Enhance: 非ログイン時のハイライトTLのデザインを改善
 | 
			
		||||
- Fix: `/about#federation` ページなどで各インスタンスのチャートが表示されなくなっていた問題を修正
 | 
			
		||||
- Fix: ユーザーページの追加情報のラベルを投稿者のサーバーの絵文字で表示する (#13968)
 | 
			
		||||
- Fix: リバーシの対局を正しく共有できないことがある問題を修正
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										109
									
								
								packages/frontend/src/pages/welcome.timeline.note.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										109
									
								
								packages/frontend/src/pages/welcome.timeline.note.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
				
			
			@ -0,0 +1,109 @@
 | 
			
		|||
<!--
 | 
			
		||||
SPDX-FileCopyrightText: syuilo and misskey-project
 | 
			
		||||
SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		||||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div :key="note.id" :class="$style.note">
 | 
			
		||||
	<div class="_panel _gaps_s" :class="$style.content">
 | 
			
		||||
		<div v-if="note.cw != null" :class="$style.richcontent">
 | 
			
		||||
			<div><Mfm :text="note.cw" :author="note.user"/></div>
 | 
			
		||||
			<MkCwButton v-model="showContent" :text="note.text" :renote="note.renote" :files="note.files" :poll="note.poll" style="margin: 4px 0;"/>
 | 
			
		||||
			<div v-if="showContent">
 | 
			
		||||
				<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
 | 
			
		||||
				<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
 | 
			
		||||
				<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
 | 
			
		||||
			</div>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-else ref="noteTextEl" :class="[$style.text, { [$style.collapsed]: shouldCollapse }]">
 | 
			
		||||
			<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
 | 
			
		||||
			<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
 | 
			
		||||
			<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="note.files && note.files.length > 0" :class="$style.richcontent">
 | 
			
		||||
			<MkMediaList :mediaList="note.files.slice(0, 4)"/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="note.poll">
 | 
			
		||||
			<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
 | 
			
		||||
		</div>
 | 
			
		||||
		<div v-if="note.reactionCount > 0" :class="$style.reactions">
 | 
			
		||||
			<MkReactionsViewer :note="note" :maxNumber="16"/>
 | 
			
		||||
		</div>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
 | 
			
		||||
<script lang="ts" setup>
 | 
			
		||||
import { ref, shallowRef, onUpdated, onMounted } from 'vue';
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
 | 
			
		||||
import MkMediaList from '@/components/MkMediaList.vue';
 | 
			
		||||
import MkPoll from '@/components/MkPoll.vue';
 | 
			
		||||
import MkCwButton from '@/components/MkCwButton.vue';
 | 
			
		||||
 | 
			
		||||
defineProps<{
 | 
			
		||||
	note: Misskey.entities.Note;
 | 
			
		||||
}>();
 | 
			
		||||
 | 
			
		||||
const noteTextEl = shallowRef<HTMLDivElement>();
 | 
			
		||||
const shouldCollapse = ref(false);
 | 
			
		||||
const showContent = ref(false);
 | 
			
		||||
 | 
			
		||||
function calcCollapse() {
 | 
			
		||||
	if (noteTextEl.value) {
 | 
			
		||||
		const height = noteTextEl.value.scrollHeight;
 | 
			
		||||
		if (height > 200) {
 | 
			
		||||
			shouldCollapse.value = true;
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onMounted(() => {
 | 
			
		||||
	calcCollapse();
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
onUpdated(() => {
 | 
			
		||||
	calcCollapse();
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
.note {
 | 
			
		||||
	margin-left: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.text {
 | 
			
		||||
	position: relative;
 | 
			
		||||
	max-height: 200px;
 | 
			
		||||
	overflow: hidden;
 | 
			
		||||
 | 
			
		||||
	&.collapsed::after {
 | 
			
		||||
		content: '';
 | 
			
		||||
		position: absolute;
 | 
			
		||||
		bottom: 0;
 | 
			
		||||
		left: 0;
 | 
			
		||||
		width: 100%;
 | 
			
		||||
		height: 64px;
 | 
			
		||||
		background: linear-gradient(0deg, var(--panel), var(--X15));
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content {
 | 
			
		||||
	padding: 16px;
 | 
			
		||||
	margin: 0 0 0 auto;
 | 
			
		||||
	max-width: max-content;
 | 
			
		||||
	border-radius: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.reactions {
 | 
			
		||||
	box-sizing: border-box;
 | 
			
		||||
	margin: 8px -16px -8px;
 | 
			
		||||
	padding: 8px 16px 0;
 | 
			
		||||
	width: calc(100% + 32px);
 | 
			
		||||
	border-top: 1px solid var(--divider);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.richcontent {
 | 
			
		||||
	min-width: 250px;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			@ -4,24 +4,17 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
-->
 | 
			
		||||
 | 
			
		||||
<template>
 | 
			
		||||
<div :class="$style.root">
 | 
			
		||||
	<div ref="scrollEl" :class="[$style.scrollbox, { [$style.scroll]: isScrolling }]">
 | 
			
		||||
		<div v-for="note in notes" :key="note.id" :class="$style.note">
 | 
			
		||||
			<div class="_panel" :class="$style.content">
 | 
			
		||||
				<div>
 | 
			
		||||
					<MkA v-if="note.replyId" class="reply" :to="`/notes/${note.replyId}`"><i class="ti ti-arrow-back-up"></i></MkA>
 | 
			
		||||
					<Mfm v-if="note.text" :text="note.text" :author="note.user"/>
 | 
			
		||||
					<MkA v-if="note.renoteId" class="rp" :to="`/notes/${note.renoteId}`">RN: ...</MkA>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div v-if="note.files.length > 0" :class="$style.richcontent">
 | 
			
		||||
					<MkMediaList :mediaList="note.files"/>
 | 
			
		||||
				</div>
 | 
			
		||||
				<div v-if="note.poll">
 | 
			
		||||
					<MkPoll :noteId="note.id" :poll="note.poll" :readOnly="true"/>
 | 
			
		||||
				</div>
 | 
			
		||||
			</div>
 | 
			
		||||
			<MkReactionsViewer ref="reactionsViewer" :note="note"/>
 | 
			
		||||
		</div>
 | 
			
		||||
<div :class="$style.root" class="_gaps">
 | 
			
		||||
	<div
 | 
			
		||||
		ref="notesMainContainerEl"
 | 
			
		||||
		class="_gaps"
 | 
			
		||||
		:class="[$style.scrollBoxMain, { [$style.scrollIntro]: (scrollState === 'intro'), [$style.scrollLoop]: (scrollState === 'loop') }]"
 | 
			
		||||
		@animationend="changeScrollState"
 | 
			
		||||
	>
 | 
			
		||||
		<XNote v-for="note in notes" :key="`${note.id}_1`" :class="$style.note" :note="note"/>
 | 
			
		||||
	</div>
 | 
			
		||||
	<div v-if="isScrolling" class="_gaps" :class="[$style.scrollBoxSub, { [$style.scrollIntro]: (scrollState === 'intro'), [$style.scrollLoop]: (scrollState === 'loop') }]">
 | 
			
		||||
		<XNote v-for="note in notes" :key="`${note.id}_2`" :class="$style.note" :note="note"/>
 | 
			
		||||
	</div>
 | 
			
		||||
</div>
 | 
			
		||||
</template>
 | 
			
		||||
| 
						 | 
				
			
			@ -29,43 +22,54 @@ SPDX-License-Identifier: AGPL-3.0-only
 | 
			
		|||
<script lang="ts" setup>
 | 
			
		||||
import * as Misskey from 'misskey-js';
 | 
			
		||||
import { onUpdated, ref, shallowRef } from 'vue';
 | 
			
		||||
import MkReactionsViewer from '@/components/MkReactionsViewer.vue';
 | 
			
		||||
import MkMediaList from '@/components/MkMediaList.vue';
 | 
			
		||||
import MkPoll from '@/components/MkPoll.vue';
 | 
			
		||||
import XNote from '@/pages/welcome.timeline.note.vue';
 | 
			
		||||
import { misskeyApiGet } from '@/scripts/misskey-api.js';
 | 
			
		||||
import { getScrollContainer } from '@/scripts/scroll.js';
 | 
			
		||||
 | 
			
		||||
const notes = ref<Misskey.entities.Note[]>([]);
 | 
			
		||||
const isScrolling = ref(false);
 | 
			
		||||
const scrollEl = shallowRef<HTMLElement>();
 | 
			
		||||
const scrollState = ref<null | 'intro' | 'loop'>(null);
 | 
			
		||||
const notesMainContainerEl = shallowRef<HTMLElement>();
 | 
			
		||||
 | 
			
		||||
misskeyApiGet('notes/featured').then(_notes => {
 | 
			
		||||
	notes.value = _notes;
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
function changeScrollState() {
 | 
			
		||||
	if (scrollState.value !== 'loop') {
 | 
			
		||||
		scrollState.value = 'loop';
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
onUpdated(() => {
 | 
			
		||||
	if (!scrollEl.value) return;
 | 
			
		||||
	const container = getScrollContainer(scrollEl.value);
 | 
			
		||||
	if (!notesMainContainerEl.value) return;
 | 
			
		||||
	const container = getScrollContainer(notesMainContainerEl.value);
 | 
			
		||||
	const containerHeight = container ? container.clientHeight : window.innerHeight;
 | 
			
		||||
	if (scrollEl.value.offsetHeight > containerHeight) {
 | 
			
		||||
	if (notesMainContainerEl.value.offsetHeight > containerHeight) {
 | 
			
		||||
		if (scrollState.value === null) {
 | 
			
		||||
			scrollState.value = 'intro';
 | 
			
		||||
		}
 | 
			
		||||
		isScrolling.value = true;
 | 
			
		||||
	}
 | 
			
		||||
});
 | 
			
		||||
</script>
 | 
			
		||||
 | 
			
		||||
<style lang="scss" module>
 | 
			
		||||
@keyframes scroll {
 | 
			
		||||
@keyframes scrollIntro {
 | 
			
		||||
	0% {
 | 
			
		||||
		transform: translate3d(0, 0, 0);
 | 
			
		||||
	}
 | 
			
		||||
	5% {
 | 
			
		||||
		transform: translate3d(0, 0, 0);
 | 
			
		||||
	100% {
 | 
			
		||||
		transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0);
 | 
			
		||||
	}
 | 
			
		||||
	75% {
 | 
			
		||||
		transform: translate3d(0, calc(-100% + 90vh), 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes scrollConstant {
 | 
			
		||||
	0% {
 | 
			
		||||
		transform: translate3d(0, -128px, 0);
 | 
			
		||||
	}
 | 
			
		||||
	90% {
 | 
			
		||||
		transform: translate3d(0, calc(-100% + 90vh), 0);
 | 
			
		||||
	100% {
 | 
			
		||||
		transform: translate3d(0, calc(calc(-100% - 128px) - var(--margin)), 0);
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -73,24 +77,26 @@ onUpdated(() => {
 | 
			
		|||
	text-align: right;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.scrollbox {
 | 
			
		||||
	&.scroll {
 | 
			
		||||
		animation: scroll 45s linear infinite;
 | 
			
		||||
.scrollBoxMain {
 | 
			
		||||
	&.scrollIntro {
 | 
			
		||||
		animation: scrollIntro 30s linear forwards;
 | 
			
		||||
	}
 | 
			
		||||
	&.scrollLoop {
 | 
			
		||||
		animation: scrollConstant 30s linear infinite;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.note {
 | 
			
		||||
	margin: 16px 0 16px auto;
 | 
			
		||||
.scrollBoxSub {
 | 
			
		||||
	&.scrollIntro {
 | 
			
		||||
		animation: scrollIntro 30s linear forwards;
 | 
			
		||||
	}
 | 
			
		||||
	&.scrollLoop {
 | 
			
		||||
		animation: scrollConstant 30s linear infinite;
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content {
 | 
			
		||||
	padding: 16px;
 | 
			
		||||
	margin: 0 0 0 auto;
 | 
			
		||||
	max-width: max-content;
 | 
			
		||||
	border-radius: 16px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.richcontent {
 | 
			
		||||
	min-width: 250px;
 | 
			
		||||
.root:has(.note:hover) .scrollBoxMain,
 | 
			
		||||
.root:has(.note:hover) .scrollBoxSub {
 | 
			
		||||
	animation-play-state: paused;
 | 
			
		||||
}
 | 
			
		||||
</style>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue