enhance(client): リスト、アンテナタイムラインを個別ページとして分割
This commit is contained in:
		
							parent
							
								
									b35ca3b739
								
							
						
					
					
						commit
						72a49f334a
					
				|  | @ -8,13 +8,14 @@ | |||
| --> | ||||
| 
 | ||||
| ## 12.x.x (unreleased) | ||||
| - ActivityPub: deliverキューのメモリ使用量を削減 | ||||
| 
 | ||||
| ### Improvements | ||||
| - ActivityPub: リモートユーザーのDeleteアクティビティに対応 | ||||
| - ActivityPub: add resolver check for blocked instance | ||||
| - ActivityPub: deliverキューのメモリ使用量を削減 | ||||
| - アカウントが凍結された場合に、凍結された旨を表示してからログアウトするように | ||||
| - 凍結されたアカウントにログインしようとしたときに、凍結されている旨を表示するように | ||||
| - リスト、アンテナタイムラインを個別ページとして分割 | ||||
| - UIの改善 | ||||
| 
 | ||||
| ### Bugfixes | ||||
|  |  | |||
|  | @ -41,7 +41,7 @@ | |||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, ref } from 'vue'; | ||||
| import { defineComponent, ref, unref } from 'vue'; | ||||
| import { focusPrev, focusNext } from '@client/scripts/focus'; | ||||
| import contains from '@client/scripts/contains'; | ||||
| 
 | ||||
|  | @ -79,21 +79,26 @@ export default defineComponent({ | |||
| 			}; | ||||
| 		}, | ||||
| 	}, | ||||
| 	created() { | ||||
| 		const items = ref(this.items.filter(item => item !== undefined)); | ||||
| 	watch: { | ||||
| 		items: { | ||||
| 			handler() { | ||||
| 				const items = ref(unref(this.items).filter(item => item !== undefined)); | ||||
| 
 | ||||
| 		for (let i = 0; i < items.value.length; i++) { | ||||
| 			const item = items.value[i]; | ||||
| 			 | ||||
| 			if (item && item.then) { // if item is Promise | ||||
| 				items.value[i] = { type: 'pending' }; | ||||
| 				item.then(actualItem => { | ||||
| 					items.value[i] = actualItem; | ||||
| 				}); | ||||
| 			} | ||||
| 				for (let i = 0; i < items.value.length; i++) { | ||||
| 					const item = items.value[i]; | ||||
| 					 | ||||
| 					if (item && item.then) { // if item is Promise | ||||
| 						items.value[i] = { type: 'pending' }; | ||||
| 						item.then(actualItem => { | ||||
| 							items.value[i] = actualItem; | ||||
| 						}); | ||||
| 					} | ||||
| 				} | ||||
| 
 | ||||
| 				this._items = items; | ||||
| 			}, | ||||
| 			immediate: true | ||||
| 		} | ||||
| 
 | ||||
| 		this._items = items; | ||||
| 	}, | ||||
| 	mounted() { | ||||
| 		if (this.viaKeyboard) { | ||||
|  |  | |||
|  | @ -1,9 +1,10 @@ | |||
| import { computed } from 'vue'; | ||||
| import { computed, ref } from 'vue'; | ||||
| import { search } from '@client/scripts/search'; | ||||
| import * as os from '@client/os'; | ||||
| import { i18n } from '@client/i18n'; | ||||
| import { $i } from './account'; | ||||
| import { unisonReload } from '@client/scripts/unison-reload'; | ||||
| import { router } from './router'; | ||||
| 
 | ||||
| export const menuDef = { | ||||
| 	notifications: { | ||||
|  | @ -58,7 +59,26 @@ export const menuDef = { | |||
| 		title: 'lists', | ||||
| 		icon: 'fas fa-list-ul', | ||||
| 		show: computed(() => $i != null), | ||||
| 		to: '/my/lists', | ||||
| 		active: computed(() => router.currentRoute.value.path.startsWith('/timeline/list/') || router.currentRoute.value.path === '/my/lists' || router.currentRoute.value.path.startsWith('/my/lists/')), | ||||
| 		action: (ev) => { | ||||
| 			const items = ref([{ | ||||
| 				type: 'pending' | ||||
| 			}]); | ||||
| 			os.api('users/lists/list').then(lists => { | ||||
| 				const _items = [...lists.map(list => ({ | ||||
| 					type: 'link', | ||||
| 					text: list.name, | ||||
| 					to: `/timeline/list/${list.id}` | ||||
| 				})), null, { | ||||
| 					type: 'link', | ||||
| 					to: '/my/lists', | ||||
| 					text: i18n.locale.manageLists, | ||||
| 					icon: 'fas fa-cog', | ||||
| 				}]; | ||||
| 				items.value = _items; | ||||
| 			}); | ||||
| 			os.popupMenu(items, ev.currentTarget || ev.target); | ||||
| 		}, | ||||
| 	}, | ||||
| 	groups: { | ||||
| 		title: 'groups', | ||||
|  |  | |||
|  | @ -372,7 +372,7 @@ export async function openEmojiPicker(src?: HTMLElement, opts, initialTextarea: | |||
| 	}); | ||||
| } | ||||
| 
 | ||||
| export function popupMenu(items: any[], src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) { | ||||
| export function popupMenu(items: any[] | Ref<any[]>, src?: HTMLElement, options?: { align?: string; viaKeyboard?: boolean }) { | ||||
| 	return new Promise((resolve, reject) => { | ||||
| 		let dispose; | ||||
| 		popup(import('@client/components/ui/popup-menu.vue'), { | ||||
|  |  | |||
|  | @ -0,0 +1,147 @@ | |||
| <template> | ||||
| <div class="tqmomfks" v-hotkey.global="keymap" v-size="{ min: [800] }"> | ||||
| 	<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> | ||||
| 	<div class="tl _block"> | ||||
| 		<XTimeline ref="tl" class="tl" | ||||
| 			:key="antennaId" | ||||
| 			src="antenna" | ||||
| 			:antenna="antennaId" | ||||
| 			:sound="true" | ||||
| 			@before="before()" | ||||
| 			@after="after()" | ||||
| 			@queue="queueUpdated" | ||||
| 		/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, defineAsyncComponent, computed } from 'vue'; | ||||
| import Progress from '@client/scripts/loading'; | ||||
| import XTimeline from '@client/components/timeline.vue'; | ||||
| import { scroll } from '@client/scripts/scroll'; | ||||
| import * as os from '@client/os'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XTimeline, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		antennaId: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			antenna: null, | ||||
| 			queue: 0, | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.antenna ? { | ||||
| 				title: this.antenna.name, | ||||
| 				icon: 'fas fa-satellite', | ||||
| 				bg: 'var(--bg)', | ||||
| 				actions: [{ | ||||
| 					icon: 'fas fa-calendar-alt', | ||||
| 					text: this.$ts.jumpToSpecifiedDate, | ||||
| 					handler: this.timetravel | ||||
| 				}, { | ||||
| 					icon: 'fas fa-cog', | ||||
| 					text: this.$ts.settings, | ||||
| 					handler: this.settings | ||||
| 				}], | ||||
| 			} : null), | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		keymap(): any { | ||||
| 			return { | ||||
| 				't': this.focus | ||||
| 			}; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		antennaId: { | ||||
| 			async handler() { | ||||
| 				this.antenna = await os.api('antennas/show', { | ||||
| 					antennaId: this.antennaId | ||||
| 				}); | ||||
| 			}, | ||||
| 			immediate: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		before() { | ||||
| 			Progress.start(); | ||||
| 		}, | ||||
| 
 | ||||
| 		after() { | ||||
| 			Progress.done(); | ||||
| 		}, | ||||
| 
 | ||||
| 		queueUpdated(q) { | ||||
| 			this.queue = q; | ||||
| 		}, | ||||
| 
 | ||||
| 		top() { | ||||
| 			scroll(this.$el, 0); | ||||
| 		}, | ||||
| 
 | ||||
| 		async timetravel() { | ||||
| 			const { canceled, result: date } = await os.dialog({ | ||||
| 				title: this.$ts.date, | ||||
| 				input: { | ||||
| 					type: 'date' | ||||
| 				} | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 
 | ||||
| 			this.$refs.tl.timetravel(new Date(date)); | ||||
| 		}, | ||||
| 
 | ||||
| 		settings() { | ||||
| 			this.$router.push(`/my/antennas/${this.antennaId}`); | ||||
| 		}, | ||||
| 
 | ||||
| 		focus() { | ||||
| 			(this.$refs.tl as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .tqmomfks { | ||||
| 	padding: var(--margin); | ||||
| 
 | ||||
| 	> .new { | ||||
| 		position: sticky; | ||||
| 		top: calc(var(--stickyTop, 0px) + 16px); | ||||
| 		z-index: 1000; | ||||
| 		width: 100%; | ||||
| 
 | ||||
| 		> button { | ||||
| 			display: block; | ||||
| 			margin: var(--margin) auto 0 auto; | ||||
| 			padding: 8px 16px; | ||||
| 			border-radius: 32px; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .tl { | ||||
| 		background: var(--bg); | ||||
| 		border-radius: var(--radius); | ||||
| 		overflow: clip; | ||||
| 	} | ||||
| 
 | ||||
| 	&.min-width_800px { | ||||
| 		max-width: 800px; | ||||
| 		margin: 0 auto; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -6,11 +6,8 @@ | |||
| 	<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> | ||||
| 	<div class="tl _block"> | ||||
| 		<XTimeline ref="tl" class="tl" | ||||
| 			:key="src === 'list' ? `list:${list.id}` : src === 'antenna' ? `antenna:${antenna.id}` : src === 'channel' ? `channel:${channel.id}` : src" | ||||
| 			:key="src" | ||||
| 			:src="src" | ||||
| 			:list="list ? list.id : null" | ||||
| 			:antenna="antenna ? antenna.id : null" | ||||
| 			:channel="channel ? channel.id : null" | ||||
| 			:sound="true" | ||||
| 			@before="before()" | ||||
| 			@after="after()" | ||||
|  | @ -41,10 +38,6 @@ export default defineComponent({ | |||
| 	data() { | ||||
| 		return { | ||||
| 			src: 'home', | ||||
| 			list: null, | ||||
| 			antenna: null, | ||||
| 			channel: null, | ||||
| 			menuOpened: false, | ||||
| 			queue: 0, | ||||
| 			[symbols.PAGE_INFO]: computed(() => ({ | ||||
| 				title: this.$ts.timeline, | ||||
|  | @ -116,32 +109,10 @@ export default defineComponent({ | |||
| 		src() { | ||||
| 			this.showNav = false; | ||||
| 		}, | ||||
| 		list(x) { | ||||
| 			this.showNav = false; | ||||
| 			if (x != null) this.antenna = null; | ||||
| 			if (x != null) this.channel = null; | ||||
| 		}, | ||||
| 		antenna(x) { | ||||
| 			this.showNav = false; | ||||
| 			if (x != null) this.list = null; | ||||
| 			if (x != null) this.channel = null; | ||||
| 		}, | ||||
| 		channel(x) { | ||||
| 			this.showNav = false; | ||||
| 			if (x != null) this.antenna = null; | ||||
| 			if (x != null) this.list = null; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	created() { | ||||
| 		this.src = this.$store.state.tl.src; | ||||
| 		if (this.src === 'list') { | ||||
| 			this.list = this.$store.state.tl.arg; | ||||
| 		} else if (this.src === 'antenna') { | ||||
| 			this.antenna = this.$store.state.tl.arg; | ||||
| 		} else if (this.src === 'channel') { | ||||
| 			this.channel = this.$store.state.tl.arg; | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
|  | @ -164,12 +135,9 @@ export default defineComponent({ | |||
| 		async chooseList(ev) { | ||||
| 			const lists = await os.api('users/lists/list'); | ||||
| 			const items = lists.map(list => ({ | ||||
| 				type: 'link', | ||||
| 				text: list.name, | ||||
| 				action: () => { | ||||
| 					this.list = list; | ||||
| 					this.src = 'list'; | ||||
| 					this.saveSrc(); | ||||
| 				} | ||||
| 				to: `/timeline/list/${list.id}` | ||||
| 			})); | ||||
| 			os.popupMenu(items, ev.currentTarget || ev.target); | ||||
| 		}, | ||||
|  | @ -177,13 +145,10 @@ export default defineComponent({ | |||
| 		async chooseAntenna(ev) { | ||||
| 			const antennas = await os.api('antennas/list'); | ||||
| 			const items = antennas.map(antenna => ({ | ||||
| 				type: 'link', | ||||
| 				text: antenna.name, | ||||
| 				indicate: antenna.hasUnreadNote, | ||||
| 				action: () => { | ||||
| 					this.antenna = antenna; | ||||
| 					this.src = 'antenna'; | ||||
| 					this.saveSrc(); | ||||
| 				} | ||||
| 				to: `/timeline/antenna/${antenna.id}` | ||||
| 			})); | ||||
| 			os.popupMenu(items, ev.currentTarget || ev.target); | ||||
| 		}, | ||||
|  | @ -191,15 +156,10 @@ export default defineComponent({ | |||
| 		async chooseChannel(ev) { | ||||
| 			const channels = await os.api('channels/followed'); | ||||
| 			const items = channels.map(channel => ({ | ||||
| 				type: 'link', | ||||
| 				text: channel.name, | ||||
| 				indicate: channel.hasUnreadNote, | ||||
| 				action: () => { | ||||
| 					// NOTE: チャンネルタイムラインをこのコンポーネントで表示するようにすると投稿フォームはどうするかなどの問題が生じるのでとりあえずページ遷移で | ||||
| 					//this.channel = channel; | ||||
| 					//this.src = 'channel'; | ||||
| 					//this.saveSrc(); | ||||
| 					this.$router.push(`/channels/${channel.id}`); | ||||
| 				} | ||||
| 				to: `/channels/${channel.id}` | ||||
| 			})); | ||||
| 			os.popupMenu(items, ev.currentTarget || ev.target); | ||||
| 		}, | ||||
|  | @ -207,10 +167,6 @@ export default defineComponent({ | |||
| 		saveSrc() { | ||||
| 			this.$store.set('tl', { | ||||
| 				src: this.src, | ||||
| 				arg: | ||||
| 					this.src === 'list' ? this.list : | ||||
| 					this.src === 'antenna' ? this.antenna : | ||||
| 					this.channel | ||||
| 			}); | ||||
| 		}, | ||||
| 
 | ||||
|  |  | |||
|  | @ -0,0 +1,147 @@ | |||
| <template> | ||||
| <div class="eqqrhokj" v-hotkey.global="keymap" v-size="{ min: [800] }"> | ||||
| 	<div class="new" v-if="queue > 0"><button class="_buttonPrimary" @click="top()">{{ $ts.newNoteRecived }}</button></div> | ||||
| 	<div class="tl _block"> | ||||
| 		<XTimeline ref="tl" class="tl" | ||||
| 			:key="listId" | ||||
| 			src="list" | ||||
| 			:list="listId" | ||||
| 			:sound="true" | ||||
| 			@before="before()" | ||||
| 			@after="after()" | ||||
| 			@queue="queueUpdated" | ||||
| 		/> | ||||
| 	</div> | ||||
| </div> | ||||
| </template> | ||||
| 
 | ||||
| <script lang="ts"> | ||||
| import { defineComponent, defineAsyncComponent, computed } from 'vue'; | ||||
| import Progress from '@client/scripts/loading'; | ||||
| import XTimeline from '@client/components/timeline.vue'; | ||||
| import { scroll } from '@client/scripts/scroll'; | ||||
| import * as os from '@client/os'; | ||||
| import * as symbols from '@client/symbols'; | ||||
| 
 | ||||
| export default defineComponent({ | ||||
| 	components: { | ||||
| 		XTimeline, | ||||
| 	}, | ||||
| 
 | ||||
| 	props: { | ||||
| 		listId: { | ||||
| 			type: String, | ||||
| 			required: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	data() { | ||||
| 		return { | ||||
| 			list: null, | ||||
| 			queue: 0, | ||||
| 			[symbols.PAGE_INFO]: computed(() => this.list ? { | ||||
| 				title: this.list.name, | ||||
| 				icon: 'fas fa-list-ul', | ||||
| 				bg: 'var(--bg)', | ||||
| 				actions: [{ | ||||
| 					icon: 'fas fa-calendar-alt', | ||||
| 					text: this.$ts.jumpToSpecifiedDate, | ||||
| 					handler: this.timetravel | ||||
| 				}, { | ||||
| 					icon: 'fas fa-cog', | ||||
| 					text: this.$ts.settings, | ||||
| 					handler: this.settings | ||||
| 				}], | ||||
| 			} : null), | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	computed: { | ||||
| 		keymap(): any { | ||||
| 			return { | ||||
| 				't': this.focus | ||||
| 			}; | ||||
| 		}, | ||||
| 	}, | ||||
| 
 | ||||
| 	watch: { | ||||
| 		listId: { | ||||
| 			async handler() { | ||||
| 				this.list = await os.api('users/lists/show', { | ||||
| 					listId: this.listId | ||||
| 				}); | ||||
| 			}, | ||||
| 			immediate: true | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	methods: { | ||||
| 		before() { | ||||
| 			Progress.start(); | ||||
| 		}, | ||||
| 
 | ||||
| 		after() { | ||||
| 			Progress.done(); | ||||
| 		}, | ||||
| 
 | ||||
| 		queueUpdated(q) { | ||||
| 			this.queue = q; | ||||
| 		}, | ||||
| 
 | ||||
| 		top() { | ||||
| 			scroll(this.$el, 0); | ||||
| 		}, | ||||
| 
 | ||||
| 		settings() { | ||||
| 			this.$router.push(`/my/lists/${this.listId}`); | ||||
| 		}, | ||||
| 
 | ||||
| 		async timetravel() { | ||||
| 			const { canceled, result: date } = await os.dialog({ | ||||
| 				title: this.$ts.date, | ||||
| 				input: { | ||||
| 					type: 'date' | ||||
| 				} | ||||
| 			}); | ||||
| 			if (canceled) return; | ||||
| 
 | ||||
| 			this.$refs.tl.timetravel(new Date(date)); | ||||
| 		}, | ||||
| 
 | ||||
| 		focus() { | ||||
| 			(this.$refs.tl as any).focus(); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| </script> | ||||
| 
 | ||||
| <style lang="scss" scoped> | ||||
| .eqqrhokj { | ||||
| 	padding: var(--margin); | ||||
| 
 | ||||
| 	> .new { | ||||
| 		position: sticky; | ||||
| 		top: calc(var(--stickyTop, 0px) + 16px); | ||||
| 		z-index: 1000; | ||||
| 		width: 100%; | ||||
| 
 | ||||
| 		> button { | ||||
| 			display: block; | ||||
| 			margin: var(--margin) auto 0 auto; | ||||
| 			padding: 8px 16px; | ||||
| 			border-radius: 32px; | ||||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	> .tl { | ||||
| 		background: var(--bg); | ||||
| 		border-radius: var(--radius); | ||||
| 		overflow: clip; | ||||
| 	} | ||||
| 
 | ||||
| 	&.min-width_800px { | ||||
| 		max-width: 800px; | ||||
| 		margin: 0 auto; | ||||
| 	} | ||||
| } | ||||
| </style> | ||||
|  | @ -48,6 +48,8 @@ const defaultRoutes = [ | |||
| 	{ path: '/channels/:channelId/edit', component: page('channel-editor'), props: true }, | ||||
| 	{ path: '/channels/:channelId', component: page('channel'), props: route => ({ channelId: route.params.channelId }) }, | ||||
| 	{ path: '/clips/:clipId', component: page('clip'), props: route => ({ clipId: route.params.clipId }) }, | ||||
| 	{ path: '/timeline/list/:listId', component: page('user-list-timeline'), props: route => ({ listId: route.params.listId }) }, | ||||
| 	{ path: '/timeline/antenna/:antennaId', component: page('antenna-timeline'), props: route => ({ antennaId: route.params.antennaId }) }, | ||||
| 	{ path: '/my/notifications', component: page('notifications') }, | ||||
| 	{ path: '/my/favorites', component: page('favorites') }, | ||||
| 	{ path: '/my/messages', component: page('messages') }, | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ | |||
| 				</MkA> | ||||
| 				<template v-for="item in menu"> | ||||
| 					<div v-if="item === '-'" class="divider"></div> | ||||
| 					<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="item" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime> | ||||
| 					<component v-else-if="menuDef[item] && (menuDef[item].show !== false)" :is="menuDef[item].to ? 'MkA' : 'button'" class="item _button" :class="[item, { active: menuDef[item].active }]" active-class="active" v-on="menuDef[item].action ? { click: menuDef[item].action } : {}" :to="menuDef[item].to" v-click-anime> | ||||
| 						<i class="fa-fw" :class="menuDef[item].icon"></i><span class="text">{{ $ts[menuDef[item].title] }}</span> | ||||
| 						<span v-if="menuDef[item].indicated" class="indicator"><i class="fas fa-circle"></i></span> | ||||
| 					</component> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue