From 22049b10ff55da7adc37ba7caaf3d4b722b2f0b5 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 29 Dec 2020 11:33:21 +0900
Subject: [PATCH] Improve timeline page

---
 src/client/components/timeline.vue |  16 +++
 src/client/pages/timeline.vue      | 185 ++++++++++++++++-------------
 src/client/ui/_common_/header.vue  |  32 +----
 3 files changed, 125 insertions(+), 108 deletions(-)

diff --git a/src/client/components/timeline.vue b/src/client/components/timeline.vue
index 33e7256325..9a3d3232cf 100644
--- a/src/client/components/timeline.vue
+++ b/src/client/components/timeline.vue
@@ -115,6 +115,22 @@ export default defineComponent({
 			endpoint = 'notes/global-timeline';
 			this.connection = os.stream.useSharedConnection('globalTimeline');
 			this.connection.on('note', prepend);
+		} else if (this.src == 'mentions') {
+			endpoint = 'notes/mentions';
+			this.connection = os.stream.useSharedConnection('main');
+			this.connection.on('mention', prepend);
+		} else if (this.src == 'directs') {
+			endpoint = 'notes/mentions';
+			this.query = {
+				visibility: 'specified'
+			};
+			const onNote = note => {
+				if (note.visibility == 'specified') {
+					prepend(note);
+				}
+			};
+			this.connection = os.stream.useSharedConnection('main');
+			this.connection.on('mention', onNote);
 		} else if (this.src == 'list') {
 			endpoint = 'notes/user-list-timeline';
 			this.query = {
diff --git a/src/client/pages/timeline.vue b/src/client/pages/timeline.vue
index a5dd097b38..f9afdd51a1 100644
--- a/src/client/pages/timeline.vue
+++ b/src/client/pages/timeline.vue
@@ -1,10 +1,26 @@
 <template>
-<div class="mk-home" v-hotkey.global="keymap">
+<div class="cmuxhskf" v-hotkey.global="keymap">
 	<div class="new" v-if="queue > 0" :style="{ width: width + 'px' }"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div>
 
 	<div class="_section">
 		<XTutorial v-if="$store.reactiveState.tutorial.value != -1" class="tutorial _content _vMargin"/>
 		<XPostForm v-if="$store.reactiveState.showFixedPostForm.value" class="post-form _panel _content _vMargin" fixed/>
+		<div class="tabs _panel _vMargin">
+			<div class="left">
+				<button class="_button tab" @click="() => { src = 'home'; saveSrc(); }" :class="{ active: src === 'home' }" v-tooltip="$ts._timelines.home"><Fa :icon="faHome"/></button>
+				<button class="_button tab" @click="() => { src = 'local'; saveSrc(); }" :class="{ active: src === 'local' }" v-tooltip="$ts._timelines.local"><Fa :icon="faComments"/></button>
+				<button class="_button tab" @click="() => { src = 'social'; saveSrc(); }" :class="{ active: src === 'social' }" v-tooltip="$ts._timelines.social"><Fa :icon="faShareAlt"/></button>
+				<button class="_button tab" @click="() => { src = 'global'; saveSrc(); }" :class="{ active: src === 'global' }" v-tooltip="$ts._timelines.global"><Fa :icon="faGlobe"/></button>
+			</div>
+			<div class="right">
+				<button class="_button tab" @click="chooseChannel" :class="{ active: src === 'channel' }" v-tooltip="$ts.channel"><Fa :icon="faSatelliteDish"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadChannel"/></button>
+				<button class="_button tab" @click="chooseAntenna" :class="{ active: src === 'antenna' }" v-tooltip="$ts.antennas"><Fa :icon="faSatellite"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadAntenna"/></button>
+				<button class="_button tab" @click="chooseList" :class="{ active: src === 'list' }" v-tooltip="$ts.lists"><Fa :icon="faListUl"/></button>
+				<button class="_button tab" @click="() => { src = 'directs'; saveSrc(); }" :class="{ active: src === 'directs' }" v-tooltip="$ts.directNotes"><Fa :icon="faEnvelope"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadSpecifiedNotes"/></button>
+				<button class="_button tab" @click="() => { src = 'mentions'; saveSrc(); }" :class="{ active: src === 'mentions' }" v-tooltip="$ts.mentions"><Fa :icon="faAt"/><Fa :icon="faCircle" class="i" v-if="$i.hasUnreadMentions"/></button>
+				<button class="_button tab" @click="chooseTl"><Fa :icon="faEllipsisH"/></button>
+			</div>
+		</div>
 		<XTimeline ref="tl"
 			class="_content _vMargin"
 			:key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src"
@@ -23,8 +39,8 @@
 
 <script lang="ts">
 import { defineComponent, defineAsyncComponent, computed } from 'vue';
-import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faSatelliteDish, faCircle, faEllipsisH, faPencilAlt } from '@fortawesome/free-solid-svg-icons';
-import { faComments } from '@fortawesome/free-regular-svg-icons';
+import { faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faListUl, faSatellite, faSatelliteDish, faCircle, faEllipsisH, faPencilAlt, faAt } from '@fortawesome/free-solid-svg-icons';
+import { faComments, faEnvelope } from '@fortawesome/free-regular-svg-icons';
 import Progress from '@/scripts/loading';
 import XTimeline from '@/components/timeline.vue';
 import XPostForm from '@/components/post-form.vue';
@@ -49,64 +65,15 @@ export default defineComponent({
 			menuOpened: false,
 			queue: 0,
 			width: 0,
-			INFO: computed(() => {
-				const tabs = [{
-					id: 'home',
-					title: null,
-					tooltip: this.$ts._timelines.home,
-					icon: faHome,
-					onClick: () => { this.src = 'home'; this.saveSrc(); },
-					selected: computed(() => this.src === 'home')
-				}];
-
-				if (!this.$instance.disableLocalTimeline || this.$i.isModerator || this.$i.isAdmin) {
-					tabs.push({
-						id: 'local',
-						title: null,
-						tooltip: this.$ts._timelines.local,
-						icon: faComments,
-						onClick: () => { this.src = 'local'; this.saveSrc(); },
-						selected: computed(() => this.src === 'local')
-					});
-
-					tabs.push({
-						id: 'social',
-						title: null,
-						tooltip: this.$ts._timelines.social,
-						icon: faShareAlt,
-						onClick: () => { this.src = 'social'; this.saveSrc(); },
-						selected: computed(() => this.src === 'social')
-					});
+			INFO: computed(() => ({
+				title: this.$ts.timeline,
+				icon: this.src === 'local' ? faComments : this.src === 'social' ? faShareAlt : this.src === 'global' ? faGlobe : faHome,
+				action: {
+					icon: faPencilAlt,
+					handler: () => os.post()
 				}
-
-				if (!this.$instance.disableGlobalTimeline || this.$i.isModerator || this.$i.isAdmin) {
-					tabs.push({
-						id: 'global',
-						title: null,
-						tooltip: this.$ts._timelines.global,
-						icon: faGlobe,
-						onClick: () => { this.src = 'global'; this.saveSrc(); },
-						selected: computed(() => this.src === 'global')
-					});
-				}
-
-				tabs.push({
-					id: 'other',
-					title: null,
-					icon: faEllipsisH,
-					onClick: this.choose,
-					indicate: computed(() => this.$i.hasUnreadAntenna || this.$i.hasUnreadChannel)
-				});
-
-				return {
-					tabs,
-					action: {
-						icon: faPencilAlt,
-						handler: () => os.post()
-					}
-				};
-			}),
-			faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faSatelliteDish, faCircle
+			})),
+			faAngleDown, faAngleUp, faHome, faShareAlt, faGlobe, faComments, faListUl, faSatellite, faSatelliteDish, faCircle, faEllipsisH, faAt, faEnvelope,
 		};
 	},
 
@@ -176,35 +143,37 @@ export default defineComponent({
 			scroll(this.$el, 0);
 		},
 
-		async choose(ev) {
-			if (this.meta == null) return;
-			const [antennas, lists, channels] = await Promise.all([
-				os.api('antennas/list'),
-				os.api('users/lists/list'),
-				os.api('channels/followed'),
-			]);
-			const antennaItems = antennas.map(antenna => ({
-				text: antenna.name,
-				icon: faSatellite,
-				indicate: antenna.hasUnreadNote,
+		async chooseList(ev) {
+			const lists = await os.api('users/lists/list');
+			const items = lists.map(list => ({
+				text: list.name,
 				action: () => {
-					this.antenna = antenna;
-					this.src = 'antenna';
+					this.list = list;
+					this.src = 'list';
 					this.saveSrc();
 				}
 			}));
-			const listItems = lists.map(list => ({
-				text: list.name,
-				icon: faListUl,
+			os.modalMenu(items, ev.currentTarget || ev.target);
+		},
+
+		async chooseAntenna(ev) {
+			const antennas = await os.api('antennas/list');
+			const items = antennas.map(antenna => ({
+				text: antenna.name,
+				indicate: antenna.hasUnreadNote,
 				action: () => {
-					this.list = list;
-					this.src = 'list';
+					this.antenna = antenna;
+					this.src = 'antenna';
 					this.saveSrc();
 				}
 			}));
-			const channelItems = channels.map(channel => ({
+			os.modalMenu(items, ev.currentTarget || ev.target);
+		},
+
+		async chooseChannel(ev) {
+			const channels = await os.api('channels/followed');
+			const items = channels.map(channel => ({
 				text: channel.name,
-				icon: faSatelliteDish,
 				indicate: channel.hasUnreadNote,
 				action: () => {
 					// NOTE: チャンネルタイムラインをこのコンポーネントで表示するようにすると投稿フォームはどうするかなどの問題が生じるのでとりあえずページ遷移で
@@ -214,7 +183,7 @@ export default defineComponent({
 					this.$router.push(`/channels/${channel.id}`);
 				}
 			}));
-			os.modalMenu([...antennaItems, listItems.length > 0 ? null : undefined, ...listItems, channelItems.length > 0 ? null : undefined, ...channelItems], ev.currentTarget || ev.target);
+			os.modalMenu(items, ev.currentTarget || ev.target);
 		},
 
 		saveSrc() {
@@ -235,7 +204,7 @@ export default defineComponent({
 </script>
 
 <style lang="scss" scoped>
-.mk-home {
+.cmuxhskf {
 	> .new {
 		position: fixed;
 		z-index: 1000;
@@ -249,7 +218,59 @@ export default defineComponent({
 	}
 
 	> ._section {
+		> .tabs {
+			display: flex;
+			box-sizing: border-box;
+			padding: 0 8px;
+			max-width: var(--baseContentWidth);
+			margin-left: auto;
+			margin-right: auto;
+			white-space: nowrap;
+			overflow: auto;
+
+			> .right {
+				margin-left: auto;
+			}
+
+			> .left, > .right {
+				> .tab {
+					position: relative;
+					height: 50px;
+					padding: 0 12px;
 
+					&:hover {
+						color: var(--fgHighlighted);
+					}
+
+					&.active {
+						color: var(--fgHighlighted);
+
+						&:after {
+							content: "";
+							display: block;
+							position: absolute;
+							bottom: 0;
+							left: 0;
+							right: 0;
+							margin: 0 auto;
+							width: calc(100% - 16px);
+							height: 4px;
+							background: var(--accent);
+							border-radius: 8px 8px 0 0;
+						}
+					}
+
+					> .i {
+						position: absolute;
+						top: 16px;
+						right: 8px;
+						color: var(--indicator);
+						font-size: 8px;
+						animation: blink 1s infinite;
+					}
+				}
+			}
+		}
 	}
 }
 </style>
diff --git a/src/client/ui/_common_/header.vue b/src/client/ui/_common_/header.vue
index e7944e4b44..f662f6144d 100644
--- a/src/client/ui/_common_/header.vue
+++ b/src/client/ui/_common_/header.vue
@@ -5,21 +5,12 @@
 	</transition>
 	<template v-if="info">
 		<div class="titleContainer">
-			<template v-if="info.tabs">
-				<div class="title" v-for="tab in info.tabs" :key="tab.id" :class="{ _button: tab.onClick, selected: tab.selected }" @click.stop="tab.onClick" v-tooltip="tab.tooltip">
-					<Fa v-if="tab.icon" :icon="tab.icon" :key="tab.icon" class="icon"/>
-					<span v-if="tab.title" class="text">{{ tab.title }}</span>
-					<Fa class="indicator" v-if="tab.indicate" :icon="faCircle"/>
-				</div>
-			</template>
-			<template v-else>
-				<div class="title">
-					<Fa v-if="info.icon" :icon="info.icon" :key="info.icon" class="icon"/>
-					<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true"/>
-					<span v-if="info.title" class="text">{{ info.title }}</span>
-					<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/>
-				</div>
-			</template>
+			<div class="title">
+				<Fa v-if="info.icon" :icon="info.icon" :key="info.icon" class="icon"/>
+				<MkAvatar v-else-if="info.avatar" class="avatar" :user="info.avatar" :disable-preview="true"/>
+				<span v-if="info.title" class="text">{{ info.title }}</span>
+				<MkUserName v-else-if="info.userName" :user="info.userName" :nowrap="false" class="text"/>
+			</div>
 		</div>
 		<button class="_button action" v-if="info.action" @click.stop="info.action.handler"><Fa :icon="info.action.icon" :key="info.action.icon"/></button>
 	</template>
@@ -155,17 +146,6 @@ export default defineComponent({
 				height: $size;
 				vertical-align: bottom;
 			}
-
-			&._button {
-				&:hover {
-					color: var(--fgHighlighted);
-				}
-			}
-
-			&.selected {
-				box-shadow: 0 -2px 0 0 var(--accent) inset;
-				color: var(--fgHighlighted);
-			}
 		}
 	}
 }
-- 
GitLab