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