diff --git a/src/client/router.ts b/src/client/router.ts
index 2081c1020c..225ee44e32 100644
--- a/src/client/router.ts
+++ b/src/client/router.ts
@@ -4,85 +4,114 @@ import MkLoading from '@client/pages/_loading_.vue';
 import MkError from '@client/pages/_error_.vue';
 import MkTimeline from '@client/pages/timeline.vue';
 import { $i } from './account';
+import { ui } from '@client/config';
 
-const page = (path: string) => defineAsyncComponent({
-	loader: () => import(`./pages/${path}.vue`),
+const page = (path: string, ui?: string) => defineAsyncComponent({
+	loader: ui ? () => import(`./ui/${ui}/pages/${path}.vue`) : () => import(`./pages/${path}.vue`),
 	loadingComponent: MkLoading,
 	errorComponent: MkError,
 });
 
 let indexScrollPos = 0;
 
+const defaultRoutes = [
+	// NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる
+	{ path: '/', name: 'index', component: $i ? MkTimeline : page('welcome') },
+	{ path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) },
+	{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
+	{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
+	{ path: '/@:acct/room', props: true, component: page('room/room') },
+	{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
+	{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
+	{ path: '/announcements', component: page('announcements') },
+	{ path: '/about', component: page('about') },
+	{ path: '/about-misskey', component: page('about-misskey') },
+	{ path: '/featured', component: page('featured') },
+	{ path: '/docs', component: page('docs') },
+	{ path: '/theme-editor', component: page('theme-editor') },
+	{ path: '/advanced-theme-editor', component: page('advanced-theme-editor') },
+	{ path: '/docs/:doc(.*)', component: page('doc'), props: route => ({ doc: route.params.doc }) },
+	{ path: '/explore', component: page('explore') },
+	{ path: '/explore/tags/:tag', props: true, component: page('explore') },
+	{ path: '/federation', component: page('federation') },
+	{ path: '/emojis', component: page('emojis') },
+	{ path: '/search', component: page('search') },
+	{ path: '/pages', name: 'pages', component: page('pages') },
+	{ path: '/pages/new', component: page('page-editor/page-editor') },
+	{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
+	{ path: '/gallery', component: page('gallery/index') },
+	{ path: '/gallery/new', component: page('gallery/edit') },
+	{ path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) },
+	{ path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) },
+	{ path: '/channels', component: page('channels') },
+	{ path: '/channels/new', component: page('channel-editor') },
+	{ 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: '/my/notifications', component: page('notifications') },
+	{ path: '/my/favorites', component: page('favorites') },
+	{ path: '/my/messages', component: page('messages') },
+	{ path: '/my/mentions', component: page('mentions') },
+	{ path: '/my/messaging', name: 'messaging', component: page('messaging/index') },
+	{ path: '/my/messaging/:user', component: page('messaging/messaging-room'), props: route => ({ userAcct: route.params.user }) },
+	{ path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) },
+	{ path: '/my/drive', name: 'drive', component: page('drive') },
+	{ path: '/my/drive/folder/:folder', component: page('drive') },
+	{ path: '/my/follow-requests', component: page('follow-requests') },
+	{ path: '/my/lists', component: page('my-lists/index') },
+	{ path: '/my/lists/:list', component: page('my-lists/list') },
+	{ path: '/my/groups', component: page('my-groups/index') },
+	{ path: '/my/groups/:group', component: page('my-groups/group'), props: route => ({ groupId: route.params.group }) },
+	{ path: '/my/antennas', component: page('my-antennas/index') },
+	{ path: '/my/antennas/create', component: page('my-antennas/create') },
+	{ path: '/my/antennas/:antennaId', component: page('my-antennas/edit'), props: true },
+	{ path: '/my/clips', component: page('my-clips/index') },
+	{ path: '/scratchpad', component: page('scratchpad') },
+	{ path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
+	{ path: '/instance', component: page('instance/index') },
+	{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
+	{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
+	{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
+	{ path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) },
+	{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) },
+	{ path: '/games/reversi', component: page('reversi/index') },
+	{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
+	{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
+	{ path: '/api-console', component: page('api-console') },
+	{ path: '/preview', component: page('preview') },
+	{ path: '/test', component: page('test') },
+	{ path: '/auth/:token', component: page('auth') },
+	{ path: '/miauth/:session', component: page('miauth') },
+	{ path: '/authorize-follow', component: page('follow') },
+	{ path: '/share', component: page('share') },
+	{ path: '/:catchAll(.*)', component: page('not-found') }
+];
+
+const chatRoutes = [
+	{ path: '/timeline', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) },
+	{ path: '/timeline/home', component: page('timeline', 'chat'), props: route => ({ src: 'home' }) },
+	{ path: '/timeline/local', component: page('timeline', 'chat'), props: route => ({ src: 'local' }) },
+	{ path: '/timeline/social', component: page('timeline', 'chat'), props: route => ({ src: 'social' }) },
+	{ path: '/timeline/global', component: page('timeline', 'chat'), props: route => ({ src: 'global' }) },
+	{ path: '/channels/:channelId', component: page('channel', 'chat'), props: route => ({ channelId: route.params.channelId }) },
+];
+
+function margeRoutes(routes: any[]) {
+	const result = defaultRoutes;
+	for (const route of routes) {
+		const found = result.findIndex(x => x.path === route.path);
+		if (found > -1) {
+			result[found] = route;
+		} else {
+			result.unshift(route);
+		}
+	}
+	return result;
+}
+
 export const router = createRouter({
 	history: createWebHistory(),
-	routes: [
-		// NOTE: MkTimelineをdynamic importするとAsyncComponentWrapperが間に入るせいでkeep-aliveのコンポーネント指定が効かなくなる
-		{ path: '/', name: 'index', component: $i ? MkTimeline : page('welcome') },
-		{ path: '/@:acct/:page?', name: 'user', component: page('user/index'), props: route => ({ acct: route.params.acct, page: route.params.page || 'index' }) },
-		{ path: '/@:user/pages/:page', component: page('page'), props: route => ({ pageName: route.params.page, username: route.params.user }) },
-		{ path: '/@:user/pages/:pageName/view-source', component: page('page-editor/page-editor'), props: route => ({ initUser: route.params.user, initPageName: route.params.pageName }) },
-		{ path: '/@:acct/room', props: true, component: page('room/room') },
-		{ path: '/settings/:page(.*)?', name: 'settings', component: page('settings/index'), props: route => ({ initialPage: route.params.page || null }) },
-		{ path: '/reset-password/:token?', component: page('reset-password'), props: route => ({ token: route.params.token }) },
-		{ path: '/announcements', component: page('announcements') },
-		{ path: '/about', component: page('about') },
-		{ path: '/about-misskey', component: page('about-misskey') },
-		{ path: '/featured', component: page('featured') },
-		{ path: '/docs', component: page('docs') },
-		{ path: '/theme-editor', component: page('theme-editor') },
-		{ path: '/advanced-theme-editor', component: page('advanced-theme-editor') },
-		{ path: '/docs/:doc(.*)', component: page('doc'), props: route => ({ doc: route.params.doc }) },
-		{ path: '/explore', component: page('explore') },
-		{ path: '/explore/tags/:tag', props: true, component: page('explore') },
-		{ path: '/search', component: page('search') },
-		{ path: '/pages', name: 'pages', component: page('pages') },
-		{ path: '/pages/new', component: page('page-editor/page-editor') },
-		{ path: '/pages/edit/:pageId', component: page('page-editor/page-editor'), props: route => ({ initPageId: route.params.pageId }) },
-		{ path: '/gallery', component: page('gallery/index') },
-		{ path: '/gallery/new', component: page('gallery/edit') },
-		{ path: '/gallery/:postId/edit', component: page('gallery/edit'), props: route => ({ postId: route.params.postId }) },
-		{ path: '/gallery/:postId', component: page('gallery/post'), props: route => ({ postId: route.params.postId }) },
-		{ path: '/channels', component: page('channels') },
-		{ path: '/channels/new', component: page('channel-editor') },
-		{ 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: '/my/notifications', component: page('notifications') },
-		{ path: '/my/favorites', component: page('favorites') },
-		{ path: '/my/messages', component: page('messages') },
-		{ path: '/my/mentions', component: page('mentions') },
-		{ path: '/my/messaging', name: 'messaging', component: page('messaging/index') },
-		{ path: '/my/messaging/:user', component: page('messaging/messaging-room'), props: route => ({ userAcct: route.params.user }) },
-		{ path: '/my/messaging/group/:group', component: page('messaging/messaging-room'), props: route => ({ groupId: route.params.group }) },
-		{ path: '/my/drive', name: 'drive', component: page('drive') },
-		{ path: '/my/drive/folder/:folder', component: page('drive') },
-		{ path: '/my/follow-requests', component: page('follow-requests') },
-		{ path: '/my/lists', component: page('my-lists/index') },
-		{ path: '/my/lists/:list', component: page('my-lists/list') },
-		{ path: '/my/groups', component: page('my-groups/index') },
-		{ path: '/my/groups/:group', component: page('my-groups/group'), props: route => ({ groupId: route.params.group }) },
-		{ path: '/my/antennas', component: page('my-antennas/index') },
-		{ path: '/my/clips', component: page('my-clips/index') },
-		{ path: '/scratchpad', component: page('scratchpad') },
-		{ path: '/instance/:page(.*)?', component: page('instance/index'), props: route => ({ initialPage: route.params.page || null }) },
-		{ path: '/instance', component: page('instance/index') },
-		{ path: '/notes/:note', name: 'note', component: page('note'), props: route => ({ noteId: route.params.note }) },
-		{ path: '/tags/:tag', component: page('tag'), props: route => ({ tag: route.params.tag }) },
-		{ path: '/user-info/:user', component: page('user-info'), props: route => ({ userId: route.params.user }) },
-		{ path: '/user-ap-info/:user', component: page('user-ap-info'), props: route => ({ userId: route.params.user }) },
-		{ path: '/instance-info/:host', component: page('instance-info'), props: route => ({ host: route.params.host }) },
-		{ path: '/games/reversi', component: page('reversi/index') },
-		{ path: '/games/reversi/:gameId', component: page('reversi/game'), props: route => ({ gameId: route.params.gameId }) },
-		{ path: '/mfm-cheat-sheet', component: page('mfm-cheat-sheet') },
-		{ path: '/api-console', component: page('api-console') },
-		{ path: '/preview', component: page('preview') },
-		{ path: '/test', component: page('test') },
-		{ path: '/auth/:token', component: page('auth') },
-		{ path: '/miauth/:session', component: page('miauth') },
-		{ path: '/authorize-follow', component: page('follow') },
-		{ path: '/share', component: page('share') },
-		{ path: '/:catchAll(.*)', component: page('not-found') }
-	],
+	routes: margeRoutes(ui === 'chat' ? chatRoutes : []),
 	// なんかHacky
 	// 通常の使い方をすると scroll メソッドの behavior を設定できないため、自前で window.scroll するようにする
 	scrollBehavior(to) {
diff --git a/src/client/ui/chat/index.vue b/src/client/ui/chat/index.vue
index 6e433de126..db663c4530 100644
--- a/src/client/ui/chat/index.vue
+++ b/src/client/ui/chat/index.vue
@@ -73,54 +73,16 @@
 	</div>
 
 	<main class="main" @contextmenu.stop="onContextmenu">
-		<header class="header" ref="header" @click="onHeaderClick">
-			<div class="left">
-				<template v-if="tl === 'home'">
-					<i class="fas fa-home icon"></i>
-					<div class="title">{{ $ts._timelines.home }}</div>
-				</template>
-				<template v-else-if="tl === 'local'">
-					<i class="fas fa-comments icon"></i>
-					<div class="title">{{ $ts._timelines.local }}</div>
-				</template>
-				<template v-else-if="tl === 'social'">
-					<i class="fas fa-share-alt icon"></i>
-					<div class="title">{{ $ts._timelines.social }}</div>
-				</template>
-				<template v-else-if="tl === 'global'">
-					<i class="fas fa-globe icon"></i>
-					<div class="title">{{ $ts._timelines.global }}</div>
-				</template>
-				<template v-else-if="tl.startsWith('channel:')">
-					<i class="fas fa-satellite-dish icon"></i>
-					<div class="title" v-if="currentChannel">{{ currentChannel.name }}<div class="description">{{ currentChannel.description }}</div></div>
-				</template>
-			</div>
-
-			<div class="right">
-				<div class="instance">{{ instanceName }}</div>
-				<XHeaderClock class="clock"/>
-				<button class="_button button timetravel" @click="timetravel" v-tooltip="$ts.jumpToSpecifiedDate">
-					<i class="fas fa-calendar-alt"></i>
-				</button>
-				<button class="_button button search" v-if="tl.startsWith('channel:') && currentChannel" @click="inChannelSearch" v-tooltip="$ts.inChannelSearch">
-					<i class="fas fa-search"></i>
-				</button>
-				<button class="_button button search" v-else @click="search" v-tooltip="$ts.search">
-					<i class="fas fa-search"></i>
-				</button>
-				<button class="_button button follow" v-if="tl.startsWith('channel:') && currentChannel" :class="{ followed: currentChannel.isFollowing }" @click="toggleChannelFollow" v-tooltip="currentChannel.isFollowing ? $ts.unfollow : $ts.follow">
-					<i v-if="currentChannel.isFollowing" class="fas fa-star"></i>
-					<i v-else class="far fa-star"></i>
-				</button>
-				<button class="_button button menu" v-if="tl.startsWith('channel:') && currentChannel" @click="openChannelMenu">
-					<i class="fas fa-ellipsis-h"></i>
-				</button>
-			</div>
+		<header class="header">
+			<XHeader class="header" :info="pageInfo" :menu="menu" :center="false" :back-button="true" @back="back()" @click="onHeaderClick"/>
 		</header>
-
-		<XTimeline class="body" ref="tl" v-if="tl.startsWith('channel:')" src="channel" :key="tl" :channel="tl.replace('channel:', '')"/>
-		<XTimeline class="body" ref="tl" v-else :src="tl" :key="tl"/>
+		<router-view v-slot="{ Component }">
+			<transition :name="$store.state.animation ? 'page' : ''" mode="out-in" @enter="onTransition">
+				<keep-alive :include="['timeline']">
+					<component :is="Component" :ref="changePage" class="body"/>
+				</keep-alive>
+			</transition>
+		</router-view>
 	</main>
 
 	<XSide class="side" ref="side" @open="sideViewOpening = true" @close="sideViewOpening = false"/>
@@ -139,7 +101,7 @@ import XSidebar from '@client/ui/_common_/sidebar.vue';
 import XWidgets from './widgets.vue';
 import XCommon from '../_common_/common.vue';
 import XSide from './side.vue';
-import XTimeline from './timeline.vue';
+import XHeader from '../_common_/header.vue';
 import XHeaderClock from './header-clock.vue';
 import * as os from '@client/os';
 import { router } from '@client/router';
@@ -147,6 +109,7 @@ import { menuDef } from '@client/menu';
 import { search } from '@client/scripts/search';
 import copyToClipboard from '@client/scripts/copy-to-clipboard';
 import { store } from './store';
+import * as symbols from '@client/symbols';
 
 export default defineComponent({
 	components: {
@@ -154,29 +117,12 @@ export default defineComponent({
 		XSidebar,
 		XWidgets,
 		XSide, // NOTE: dynamic importするとAsyncComponentWrapperが間に入るせいでref取得できなくて面倒になる
-		XTimeline,
+		XHeader,
 		XHeaderClock,
 	},
 
 	provide() {
 		return {
-			navHook: (path) => {
-				switch (path) {
-					case '/timeline/home': this.tl = 'home'; return;
-					case '/timeline/local': this.tl = 'local'; return;
-					case '/timeline/social': this.tl = 'social'; return;
-					case '/timeline/global': this.tl = 'global'; return;
-
-					default:
-						if (path.startsWith('/channels/')) {
-							this.tl = `channel:${ path.replace('/channels/', '') }`;
-							return;
-						}
-						//os.pageWindow(path);
-						this.$refs.side.navigate(path);
-						break;
-				}
-			},
 			sideViewHook: (path) => {
 				this.$refs.side.navigate(path);
 			}
@@ -185,7 +131,7 @@ export default defineComponent({
 
 	data() {
 		return {
-			tl: store.state.tl,
+			pageInfo: null,
 			lists: null,
 			antennas: null,
 			followedChannels: null,
@@ -197,18 +143,30 @@ export default defineComponent({
 		};
 	},
 
+	computed: {
+		menu() {
+			return [{
+				icon: 'fas fa-columns',
+				text: this.$ts.openInSideView,
+				action: () => {
+					this.$refs.side.navigate(this.$route.path);
+				}
+			}, {
+				icon: 'fas fa-window-maximize',
+				text: this.$ts.openInWindow,
+				action: () => {
+					os.pageWindow(this.$route.path);
+				}
+			}];
+		}
+	},
+
 	created() {
 		if (window.innerWidth < 1024) {
 			localStorage.setItem('ui', 'default');
 			location.reload();
 		}
 
-		router.beforeEach((to, from) => {
-			this.$refs.side.navigate(to.fullPath);
-			// search?q=foo のようなクエリを受け取れるようにするため、return falseはできない
-			//return false;
-		});
-
 		os.api('users/lists/list').then(lists => {
 			this.lists = lists;
 		});
@@ -225,18 +183,22 @@ export default defineComponent({
 		os.api('channels/featured', { limit: 20 }).then(channels => {
 			this.featuredChannels = channels;
 		});
-
-		this.$watch('tl', () => {
-			if (this.tl.startsWith('channel:')) {
-				os.api('channels/show', { channelId: this.tl.replace('channel:', '') }).then(channel => {
-					this.currentChannel = channel;
-				});
-			}
-			store.set('tl', this.tl);
-		}, { immediate: true });
 	},
 
 	methods: {
+		changePage(page) {
+			console.log(page);
+			if (page == null) return;
+			if (page[symbols.PAGE_INFO]) {
+				this.pageInfo = page[symbols.PAGE_INFO];
+				document.title = `${this.pageInfo.title} | ${instanceName}`;
+			}
+		},
+
+		onTransition() {
+			if (window._scroll) window._scroll();
+		},
+
 		showMenu() {
 			this.$refs.menu.show();
 		},
@@ -245,59 +207,18 @@ export default defineComponent({
 			os.post();
 		},
 
-		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));
-		},
-
 		search() {
 			search();
 		},
 
-		async inChannelSearch() {
-			const { canceled, result: query } = await os.dialog({
-				title: this.$ts.inChannelSearch,
-				input: true
-			});
-			if (canceled || query == null || query === '') return;
-			router.push(`/search?q=${encodeURIComponent(query)}&channel=${this.currentChannel.id}`);
+		back() {
+			history.back();
 		},
 
 		top() {
 			window.scroll({ top: 0, behavior: 'smooth' });
 		},
 
-		async toggleChannelFollow() {
-			if (this.currentChannel.isFollowing) {
-				await os.apiWithDialog('channels/unfollow', {
-					channelId: this.currentChannel.id
-				});
-				this.currentChannel.isFollowing = false;
-			} else {
-				await os.apiWithDialog('channels/follow', {
-					channelId: this.currentChannel.id
-				});
-				this.currentChannel.isFollowing = true;
-			}
-		},
-
-		openChannelMenu(ev) {
-			os.modalMenu([{
-				text: this.$ts.copyUrl,
-				icon: 'fas fa-link',
-				action: () => {
-					copyToClipboard(`${url}/channels/${this.currentChannel.id}`);
-				}
-			}], ev.currentTarget || ev.target);
-		},
-
 		onTransition() {
 			if (window._scroll) window._scroll();
 		},
@@ -516,87 +437,24 @@ export default defineComponent({
 		background: var(--panel);
 
 		> .header {
-			$padding: 8px;
-			display: flex;
 			z-index: 1000;
 			height: $header-height;
-			padding: $padding;
-			box-sizing: border-box;
 			background-color: var(--panel);
 			border-bottom: solid 0.5px var(--divider);
 			user-select: none;
+		}
 
-			> .left {
-				display: flex;
-				align-items: center;
-				flex: 1;
-				min-width: 0;
-
-				> .icon {
-					height: ($header-height - ($padding * 2));
-					width: ($header-height - ($padding * 2));
-					padding: 10px;
-					box-sizing: border-box;
-					margin-right: 4px;
-					opacity: 0.6;
-				}
-
-				> .title {
-					overflow: hidden;
-					text-overflow: ellipsis;
-					white-space: nowrap;
-					min-width: 0;
-					font-weight: bold;
-
-					> .description {
-						opacity: 0.6;
-						font-size: 0.8em;
-						font-weight: normal;
-						white-space: nowrap;
-						overflow: hidden;
-						text-overflow: ellipsis;
-					}
-				}
-			}
-
-			> .right {
-				display: flex;
-				align-items: center;
-				min-width: 0;
-				margin-left: auto;
-				padding-left: 8px;
-
-				> .instance {
-					margin-right: 16px;
-					font-size: 0.9em;
-				}
-
-				> .clock {
-					margin-right: 16px;
-				}
-
-				> .button {
-					height: ($header-height - ($padding * 2));
-					width: ($header-height - ($padding * 2));
-					box-sizing: border-box;
-					position: relative;
-					border-radius: 5px;
-
-					&:hover {
-						background: rgba(0, 0, 0, 0.05);
-					}
-
-					&.follow.followed {
-						color: var(--accent);
-					}
-				}
-			}
+		> .body {
+			width: 100%;
+			box-sizing: border-box;
+			overflow: auto;
 		}
 	}
 
 	> .side {
 		width: 350px;
 		border-left: solid 4px var(--divider);
+		background: var(--panel);
 
 		&.widgets.sideViewOpening {
 			@media (max-width: 1400px) {
diff --git a/src/client/ui/chat/pages/channel.vue b/src/client/ui/chat/pages/channel.vue
new file mode 100644
index 0000000000..76b334487e
--- /dev/null
+++ b/src/client/ui/chat/pages/channel.vue
@@ -0,0 +1,259 @@
+<template>
+<div v-if="channel" class="hhizbblb">
+	<div class="info" v-if="date">
+		<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
+	</div>
+	<div class="tl" ref="body">
+		<div class="new" v-if="queue > 0" :style="{ width: width + 'px', bottom: bottom + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
+		<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated" v-follow="true"/>
+	</div>
+	<div class="bottom">
+		<div class="typers" v-if="typers.length > 0">
+			<I18n :src="$ts.typingUsers" text-tag="span" class="users">
+				<template #users>
+					<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
+				</template>
+			</I18n>
+			<MkEllipsis/>
+		</div>
+		<XPostForm :channel="channel"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, markRaw } from 'vue';
+import * as Misskey from 'misskey-js';
+import XNotes from '../notes.vue';
+import * as os from '@client/os';
+import * as sound from '@client/scripts/sound';
+import { scrollToBottom, getScrollPosition, getScrollContainer } from '@client/scripts/scroll';
+import follow from '@client/directives/follow-append';
+import XPostForm from '../post-form.vue';
+import MkInfo from '@client/components/ui/info.vue';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		XNotes,
+		XPostForm,
+		MkInfo,
+	},
+
+	directives: {
+		follow
+	},
+	
+	provide() {
+		return {
+			inChannel: true
+		};
+	},
+
+	props: {
+		channelId: {
+			type: String,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			channel: null as Misskey.entities.Channel | null,
+			connection: null,
+			pagination: null,
+			baseQuery: {
+				includeMyRenotes: this.$store.state.showMyRenotes,
+				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
+				includeLocalRenotes: this.$store.state.showLocalRenotes
+			},
+			queue: 0,
+			width: 0,
+			top: 0,
+			bottom: 0,
+			typers: [],
+			date: null,
+			[symbols.PAGE_INFO]: computed(() => ({
+				title: this.channel ? this.channel.name : '-',
+				subtitle: this.channel ? this.channel.description : '-',
+				icon: 'fas fa-satellite-dish',
+				actions: [{
+					icon: this.channel?.isFollowing ? 'fas fa-star' : 'far fa-star',
+					text: this.channel?.isFollowing ? this.$ts.unfollow : this.$ts.follow,
+					highlighted: this.channel?.isFollowing,
+					handler: this.toggleChannelFollow
+				}, {
+					icon: 'fas fa-search',
+					text: this.$ts.inChannelSearch,
+					handler: this.inChannelSearch
+				}, {
+					icon: 'fas fa-calendar-alt',
+					text: this.$ts.jumpToSpecifiedDate,
+					handler: this.timetravel
+				}]
+			})),
+		};
+	},
+
+	async created() {
+		this.channel = await os.api('channels/show', { channelId: this.channelId });
+
+		const prepend = note => {
+			(this.$refs.tl as any).prepend(note);
+
+			this.$emit('note');
+
+			sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
+		};
+
+		this.connection = markRaw(os.stream.useChannel('channel', {
+			channelId: this.channelId
+		}));
+		this.connection.on('note', prepend);
+		this.connection.on('typers', typers => {
+			this.typers = this.$i ? typers.filter(u => u.id !== this.$i.id) : typers;
+		});
+
+		this.pagination = {
+			endpoint: 'channels/timeline',
+			reversed: true,
+			limit: 10,
+			params: init => ({
+				channelId: this.channelId,
+				untilDate: this.date?.getTime(),
+				...this.baseQuery
+			})
+		};
+	},
+
+	mounted() {
+
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+	},
+
+	methods: {
+		focus() {
+			this.$refs.body.focus();
+		},
+
+		goTop() {
+			const container = getScrollContainer(this.$refs.body);
+			container.scrollTop = 0;
+		},
+
+		queueUpdated(q) {
+			if (this.$refs.body.offsetWidth !== 0) {
+				const rect = this.$refs.body.getBoundingClientRect();
+				this.width = this.$refs.body.offsetWidth;
+				this.top = rect.top;
+				this.bottom = this.$refs.body.offsetHeight;
+			}
+			this.queue = q;
+		},
+
+		async inChannelSearch() {
+			const { canceled, result: query } = await os.dialog({
+				title: this.$ts.inChannelSearch,
+				input: true
+			});
+			if (canceled || query == null || query === '') return;
+			router.push(`/search?q=${encodeURIComponent(query)}&channel=${this.channelId}`);
+		},
+
+		async toggleChannelFollow() {
+			if (this.channel.isFollowing) {
+				await os.apiWithDialog('channels/unfollow', {
+					channelId: this.channel.id
+				});
+				this.channel.isFollowing = false;
+			} else {
+				await os.apiWithDialog('channels/follow', {
+					channelId: this.channel.id
+				});
+				this.channel.isFollowing = true;
+			}
+		},
+
+		openChannelMenu(ev) {
+			os.modalMenu([{
+				text: this.$ts.copyUrl,
+				icon: 'fas fa-link',
+				action: () => {
+					copyToClipboard(`${url}/channels/${this.currentChannel.id}`);
+				}
+			}], ev.currentTarget || ev.target);
+		},
+
+		timetravel(date?: Date) {
+			this.date = date;
+			this.$refs.tl.reload();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.hhizbblb {
+	display: flex;
+	flex-direction: column;
+	flex: 1;
+	overflow: auto;
+
+	> .info {
+		padding: 16px 16px 0 16px;
+	}
+
+	> .top {
+		padding: 16px 16px 0 16px;
+	}
+
+	> .bottom {
+		padding: 0 16px 16px 16px;
+		position: relative;
+
+		> .typers {
+			position: absolute;
+			bottom: 100%;
+			padding: 0 8px 0 8px;
+			font-size: 0.9em;
+			background: var(--panel);
+			border-radius: 0 8px 0 0;
+			color: var(--fgTransparentWeak);
+
+			> .users {
+				> .user + .user:before {
+					content: ", ";
+					font-weight: normal;
+				}
+
+				> .user:last-of-type:after {
+					content: " ";
+				}
+			}
+		}
+	}
+
+	> .tl {
+		position: relative;
+		padding: 16px 0;
+		flex: 1;
+		min-width: 0;
+		overflow: auto;
+
+		> .new {
+			position: fixed;
+			z-index: 1000;
+
+			> button {
+				display: block;
+				margin: 16px auto;
+				padding: 8px 16px;
+				border-radius: 32px;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/ui/chat/pages/timeline.vue b/src/client/ui/chat/pages/timeline.vue
new file mode 100644
index 0000000000..0f9cd7f11e
--- /dev/null
+++ b/src/client/ui/chat/pages/timeline.vue
@@ -0,0 +1,221 @@
+<template>
+<div class="dbiokgaf">
+	<div class="info" v-if="date">
+		<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
+	</div>
+	<div class="top">
+		<XPostForm/>
+	</div>
+	<div class="tl" ref="body">
+		<div class="new" v-if="queue > 0" :style="{ width: width + 'px', top: top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
+		<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import { computed, defineComponent, markRaw } from 'vue';
+import XNotes from '../notes.vue';
+import * as os from '@client/os';
+import * as sound from '@client/scripts/sound';
+import { scrollToBottom, getScrollPosition, getScrollContainer } from '@client/scripts/scroll';
+import follow from '@client/directives/follow-append';
+import XPostForm from '../post-form.vue';
+import MkInfo from '@client/components/ui/info.vue';
+import * as symbols from '@client/symbols';
+
+export default defineComponent({
+	components: {
+		XNotes,
+		XPostForm,
+		MkInfo,
+	},
+
+	directives: {
+		follow
+	},
+
+	props: {
+		src: {
+			type: String,
+			required: true
+		},
+	},
+
+	data() {
+		return {
+			connection: null,
+			connection2: null,
+			pagination: null,
+			baseQuery: {
+				includeMyRenotes: this.$store.state.showMyRenotes,
+				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
+				includeLocalRenotes: this.$store.state.showLocalRenotes
+			},
+			query: {},
+			queue: 0,
+			width: 0,
+			top: 0,
+			bottom: 0,
+			typers: [],
+			date: null,
+			[symbols.PAGE_INFO]: computed(() => ({
+				title: this.$ts.timeline,
+				icon: 'fas fa-home',
+				actions: [{
+					icon: 'fas fa-calendar-alt',
+					text: this.$ts.jumpToSpecifiedDate,
+					handler: this.timetravel
+				}]
+			})),
+		};
+	},
+
+	created() {
+		const prepend = note => {
+			(this.$refs.tl as any).prepend(note);
+
+			this.$emit('note');
+
+			sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
+		};
+
+		const onChangeFollowing = () => {
+			if (!this.$refs.tl.backed) {
+				this.$refs.tl.reload();
+			}
+		};
+
+		let endpoint;
+
+		if (this.src == 'home') {
+			endpoint = 'notes/timeline';
+			this.connection = markRaw(os.stream.useChannel('homeTimeline'));
+			this.connection.on('note', prepend);
+
+			this.connection2 = markRaw(os.stream.useChannel('main'));
+			this.connection2.on('follow', onChangeFollowing);
+			this.connection2.on('unfollow', onChangeFollowing);
+		} else if (this.src == 'local') {
+			endpoint = 'notes/local-timeline';
+			this.connection = markRaw(os.stream.useChannel('localTimeline'));
+			this.connection.on('note', prepend);
+		} else if (this.src == 'social') {
+			endpoint = 'notes/hybrid-timeline';
+			this.connection = markRaw(os.stream.useChannel('hybridTimeline'));
+			this.connection.on('note', prepend);
+		} else if (this.src == 'global') {
+			endpoint = 'notes/global-timeline';
+			this.connection = markRaw(os.stream.useChannel('globalTimeline'));
+			this.connection.on('note', prepend);
+		}
+
+		this.pagination = {
+			endpoint: endpoint,
+			limit: 10,
+			params: init => ({
+				untilDate: this.date?.getTime(),
+				...this.baseQuery, ...this.query
+			})
+		};
+	},
+
+	mounted() {
+
+	},
+
+	beforeUnmount() {
+		this.connection.dispose();
+		if (this.connection2) this.connection2.dispose();
+	},
+
+	methods: {
+		focus() {
+			this.$refs.body.focus();
+		},
+
+		goTop() {
+			const container = getScrollContainer(this.$refs.body);
+			container.scrollTop = 0;
+		},
+
+		queueUpdated(q) {
+			if (this.$refs.body.offsetWidth !== 0) {
+				const rect = this.$refs.body.getBoundingClientRect();
+				this.width = this.$refs.body.offsetWidth;
+				this.top = rect.top;
+				this.bottom = this.$refs.body.offsetHeight;
+			}
+			this.queue = q;
+		},
+
+		timetravel(date?: Date) {
+			this.date = date;
+			this.$refs.tl.reload();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.dbiokgaf {
+	display: flex;
+	flex-direction: column;
+	flex: 1;
+	overflow: auto;
+
+	> .info {
+		padding: 16px 16px 0 16px;
+	}
+
+	> .top {
+		padding: 16px 16px 0 16px;
+	}
+
+	> .bottom {
+		padding: 0 16px 16px 16px;
+		position: relative;
+
+		> .typers {
+			position: absolute;
+			bottom: 100%;
+			padding: 0 8px 0 8px;
+			font-size: 0.9em;
+			background: var(--panel);
+			border-radius: 0 8px 0 0;
+			color: var(--fgTransparentWeak);
+
+			> .users {
+				> .user + .user:before {
+					content: ", ";
+					font-weight: normal;
+				}
+
+				> .user:last-of-type:after {
+					content: " ";
+				}
+			}
+		}
+	}
+
+	> .tl {
+		position: relative;
+		padding: 16px 0;
+		flex: 1;
+		min-width: 0;
+		overflow: auto;
+
+		> .new {
+			position: fixed;
+			z-index: 1000;
+
+			> button {
+				display: block;
+				margin: 16px auto;
+				padding: 8px 16px;
+				border-radius: 32px;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/ui/chat/side.vue b/src/client/ui/chat/side.vue
index 7ad39c7a19..5ccfad1b75 100644
--- a/src/client/ui/chat/side.vue
+++ b/src/client/ui/chat/side.vue
@@ -1,11 +1,9 @@
 <template>
 <div class="mrajymqm _narrow_" v-if="component">
 	<header class="header" @contextmenu.prevent.stop="onContextmenu">
-		<button class="_button" @click="back()" v-if="history.length > 0"><i class="fas fa-chevron-left"></i></button>
-		<XHeader class="title" :info="pageInfo" :with-back="false" :center="false"/>
-		<button class="_button" @click="close()"><i class="fas fa-times"></i></button>
+		<XHeader class="title" :info="pageInfo" :center="false" :back-button="history.length > 0" @back="back()" :close-button="true" @close="close()"/>
 	</header>
-	<component :is="component" v-bind="props" :ref="changePage" class="_flat_"/>
+	<component :is="component" v-bind="props" :ref="changePage" class="body _flat_"/>
 </div>
 </template>
 
@@ -130,7 +128,6 @@ export default defineComponent({
 		top: 0;
 		height: $header-height;
 		width: 100%;
-		line-height: $header-height;
 		font-weight: bold;
 		//background-color: var(--panel);
 		-webkit-backdrop-filter: blur(32px);
@@ -153,6 +150,10 @@ export default defineComponent({
 			position: relative;
 		}
 	}
+
+	> .body {
+
+	}
 }
 </style>
 
diff --git a/src/client/ui/chat/timeline.vue b/src/client/ui/chat/timeline.vue
deleted file mode 100644
index 0fbcbfb713..0000000000
--- a/src/client/ui/chat/timeline.vue
+++ /dev/null
@@ -1,292 +0,0 @@
-<template>
-<div class="dbiokgaf info" v-if="date">
-	<MkInfo>{{ $ts.showingPastTimeline }} <button class="_textButton clear" @click="timetravel()">{{ $ts.clear }}</button></MkInfo>
-</div>
-<div class="dbiokgaf top" v-if="['home', 'local', 'social', 'global'].includes(src)">
-	<XPostForm/>
-</div>
-<div class="dbiokgaf tl" ref="body">
-	<div class="new" v-if="queue > 0" :style="{ width: width + 'px', [pagination.reversed ? 'bottom' : 'top']: pagination.reversed ? bottom + 'px' : top + 'px' }"><button class="_buttonPrimary" @click="goTop()">{{ $ts.newNoteRecived }}</button></div>
-	<XNotes class="tl" ref="tl" :pagination="pagination" @queue="queueUpdated" v-follow="pagination.reversed"/>
-</div>
-<div class="dbiokgaf bottom" v-if="src === 'channel'">
-	<div class="typers" v-if="typers.length > 0">
-		<I18n :src="$ts.typingUsers" text-tag="span" class="users">
-			<template #users>
-				<b v-for="user in typers" :key="user.id" class="user">{{ user.username }}</b>
-			</template>
-		</I18n>
-		<MkEllipsis/>
-	</div>
-	<XPostForm :channel="channel"/>
-</div>
-</template>
-
-<script lang="ts">
-import { defineComponent, markRaw } from 'vue';
-import XNotes from './notes.vue';
-import * as os from '@client/os';
-import * as sound from '@client/scripts/sound';
-import { scrollToBottom, getScrollPosition, getScrollContainer } from '@client/scripts/scroll';
-import follow from '@client/directives/follow-append';
-import XPostForm from './post-form.vue';
-import MkInfo from '@client/components/ui/info.vue';
-
-export default defineComponent({
-	components: {
-		XNotes,
-		XPostForm,
-		MkInfo,
-	},
-
-	directives: {
-		follow
-	},
-	
-	provide() {
-		return {
-			inChannel: this.src === 'channel'
-		};
-	},
-
-	props: {
-		src: {
-			type: String,
-			required: true
-		},
-		list: {
-			type: String,
-			required: false
-		},
-		antenna: {
-			type: String,
-			required: false
-		},
-		channel: {
-			type: String,
-			required: false
-		},
-	},
-
-	emits: ['note', 'queue', 'before', 'after'],
-
-	data() {
-		return {
-			connection: null,
-			connection2: null,
-			pagination: null,
-			baseQuery: {
-				includeMyRenotes: this.$store.state.showMyRenotes,
-				includeRenotedMyNotes: this.$store.state.showRenotedMyNotes,
-				includeLocalRenotes: this.$store.state.showLocalRenotes
-			},
-			query: {},
-			queue: 0,
-			width: 0,
-			top: 0,
-			bottom: 0,
-			typers: [],
-			date: null
-		};
-	},
-
-	created() {
-		const prepend = note => {
-			(this.$refs.tl as any).prepend(note);
-
-			this.$emit('note');
-
-			sound.play(note.userId === this.$i.id ? 'noteMy' : 'note');
-		};
-
-		const onUserAdded = () => {
-			(this.$refs.tl as any).reload();
-		};
-
-		const onUserRemoved = () => {
-			(this.$refs.tl as any).reload();
-		};
-
-		const onChangeFollowing = () => {
-			if (!this.$refs.tl.backed) {
-				this.$refs.tl.reload();
-			}
-		};
-
-		let endpoint;
-		let reversed = false;
-
-		if (this.src == 'antenna') {
-			endpoint = 'antennas/notes';
-			this.query = {
-				antennaId: this.antenna
-			};
-			this.connection = markRaw(os.stream.useChannel('antenna', {
-				antennaId: this.antenna
-			}));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'home') {
-			endpoint = 'notes/timeline';
-			this.connection = markRaw(os.stream.useChannel('homeTimeline'));
-			this.connection.on('note', prepend);
-
-			this.connection2 = markRaw(os.stream.useChannel('main'));
-			this.connection2.on('follow', onChangeFollowing);
-			this.connection2.on('unfollow', onChangeFollowing);
-		} else if (this.src == 'local') {
-			endpoint = 'notes/local-timeline';
-			this.connection = markRaw(os.stream.useChannel('localTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'social') {
-			endpoint = 'notes/hybrid-timeline';
-			this.connection = markRaw(os.stream.useChannel('hybridTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'global') {
-			endpoint = 'notes/global-timeline';
-			this.connection = markRaw(os.stream.useChannel('globalTimeline'));
-			this.connection.on('note', prepend);
-		} else if (this.src == 'mentions') {
-			endpoint = 'notes/mentions';
-			this.connection = markRaw(os.stream.useChannel('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 = markRaw(os.stream.useChannel('main'));
-			this.connection.on('mention', onNote);
-		} else if (this.src == 'list') {
-			endpoint = 'notes/user-list-timeline';
-			this.query = {
-				listId: this.list
-			};
-			this.connection = markRaw(os.stream.useChannel('userList', {
-				listId: this.list
-			}));
-			this.connection.on('note', prepend);
-			this.connection.on('userAdded', onUserAdded);
-			this.connection.on('userRemoved', onUserRemoved);
-		} else if (this.src == 'channel') {
-			endpoint = 'channels/timeline';
-			reversed = true;
-			this.query = {
-				channelId: this.channel
-			};
-			this.connection = markRaw(os.stream.useChannel('channel', {
-				channelId: this.channel
-			}));
-			this.connection.on('note', prepend);
-			this.connection.on('typers', typers => {
-				this.typers = this.$i ? typers.filter(u => u.id !== this.$i.id) : typers;
-			});
-		}
-
-		this.pagination = {
-			endpoint: endpoint,
-			reversed,
-			limit: 10,
-			params: init => ({
-				untilDate: this.date?.getTime(),
-				...this.baseQuery, ...this.query
-			})
-		};
-	},
-
-	mounted() {
-
-	},
-
-	beforeUnmount() {
-		this.connection.dispose();
-		if (this.connection2) this.connection2.dispose();
-	},
-
-	methods: {
-		focus() {
-			this.$refs.body.focus();
-		},
-
-		goTop() {
-			const container = getScrollContainer(this.$refs.body);
-			container.scrollTop = 0;
-		},
-
-		queueUpdated(q) {
-			if (this.$refs.body.offsetWidth !== 0) {
-				const rect = this.$refs.body.getBoundingClientRect();
-				this.width = this.$refs.body.offsetWidth;
-				this.top = rect.top;
-				this.bottom = this.$refs.body.offsetHeight;
-			}
-			this.queue = q;
-		},
-
-		timetravel(date?: Date) {
-			this.date = date;
-			this.$refs.tl.reload();
-		}
-	}
-});
-</script>
-
-<style lang="scss" scoped>
-.dbiokgaf.info{
-	padding: 16px 16px 0 16px;
-}
-
-.dbiokgaf.top {
-	padding: 16px 16px 0 16px;
-}
-
-.dbiokgaf.bottom {
-	padding: 0 16px 16px 16px;
-	position: relative;
-
-	> .typers {
-		position: absolute;
-		bottom: 100%;
-		padding: 0 8px 0 8px;
-		font-size: 0.9em;
-		background: var(--panel);
-		border-radius: 0 8px 0 0;
-		color: var(--fgTransparentWeak);
-
-		> .users {
-			> .user + .user:before {
-				content: ", ";
-				font-weight: normal;
-			}
-
-			> .user:last-of-type:after {
-				content: " ";
-			}
-		}
-	}
-}
-
-.dbiokgaf.tl {
-	position: relative;
-	padding: 16px 0;
-	flex: 1;
-	min-width: 0;
-	overflow: auto;
-
-	> .new {
-		position: fixed;
-		z-index: 1000;
-
-		> button {
-			display: block;
-			margin: 16px auto;
-			padding: 8px 16px;
-			border-radius: 32px;
-		}
-	}
-}
-</style>