diff --git a/src/web/app/common/define-widget.ts b/src/web/app/common/define-widget.ts
index 6088efd7e5..930a7c5868 100644
--- a/src/web/app/common/define-widget.ts
+++ b/src/web/app/common/define-widget.ts
@@ -2,7 +2,7 @@ import Vue from 'vue';
 
 export default function<T extends object>(data: {
 	name: string;
-	props?: T;
+	props?: () => T;
 }) {
 	return Vue.extend({
 		props: {
@@ -17,20 +17,9 @@ export default function<T extends object>(data: {
 		},
 		data() {
 			return {
-				props: data.props || {} as T
+				props: data.props ? data.props() : {} as T
 			};
 		},
-		watch: {
-			props(newProps, oldProps) {
-				if (JSON.stringify(newProps) == JSON.stringify(oldProps)) return;
-				(this as any).api('i/update_home', {
-					id: this.id,
-					data: newProps
-				}).then(() => {
-					(this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
-				});
-			}
-		},
 		created() {
 			if (this.props) {
 				Object.keys(this.props).forEach(prop => {
@@ -39,6 +28,18 @@ export default function<T extends object>(data: {
 					}
 				});
 			}
+
+			this.$watch('props', newProps => {
+				console.log(this.id, newProps);
+				(this as any).api('i/update_home', {
+					id: this.id,
+					data: newProps
+				}).then(() => {
+					(this as any).os.i.client_settings.home.find(w => w.id == this.id).data = newProps;
+				});
+			}, {
+				deep: true
+			});
 		}
 	});
 }
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
index c4208aa913..4b9375f548 100644
--- a/src/web/app/common/mios.ts
+++ b/src/web/app/common/mios.ts
@@ -1,9 +1,8 @@
 import { EventEmitter } from 'eventemitter3';
-import * as riot from 'riot';
+import api from './scripts/api';
 import signout from './scripts/signout';
 import Progress from './scripts/loading';
 import HomeStreamManager from './scripts/streaming/home-stream-manager';
-import api from './scripts/api';
 import DriveStreamManager from './scripts/streaming/drive-stream-manager';
 import ServerStreamManager from './scripts/streaming/server-stream-manager';
 import RequestsStreamManager from './scripts/streaming/requests-stream-manager';
@@ -226,22 +225,8 @@ export default class MiOS extends EventEmitter {
 		// フェッチが完了したとき
 		const fetched = me => {
 			if (me) {
-				riot.observable(me);
-
-				// この me オブジェクトを更新するメソッド
-				me.update = data => {
-					if (data) Object.assign(me, data);
-					me.trigger('updated');
-				};
-
 				// ローカルストレージにキャッシュ
 				localStorage.setItem('me', JSON.stringify(me));
-
-				// 自分の情報が更新されたとき
-				me.on('updated', () => {
-					// キャッシュ更新
-					localStorage.setItem('me', JSON.stringify(me));
-				});
 			}
 
 			this.i = me;
@@ -270,8 +255,6 @@ export default class MiOS extends EventEmitter {
 			// 後から新鮮なデータをフェッチ
 			fetchme(cachedMe.token, freshData => {
 				Object.assign(cachedMe, freshData);
-				cachedMe.trigger('updated');
-				cachedMe.trigger('refreshed');
 			});
 		} else {
 			// Get token from cookie
diff --git a/src/web/app/common/scripts/streaming/home-stream.ts b/src/web/app/common/scripts/streaming/home-stream.ts
index 11ad754ef0..a92b61caed 100644
--- a/src/web/app/common/scripts/streaming/home-stream.ts
+++ b/src/web/app/common/scripts/streaming/home-stream.ts
@@ -16,7 +16,9 @@ export default class Connection extends Stream {
 		}, 1000 * 60);
 
 		// 自分の情報が更新されたとき
-		this.on('i_updated', me.update);
+		this.on('i_updated', i => {
+			Object.assign(me, i);
+		});
 
 		// トークンが再生成されたとき
 		// このままではAPIが利用できないので強制的にサインアウトさせる
diff --git a/src/web/app/desktop/-tags/home-widgets/channel.tag b/src/web/app/desktop/-tags/home-widgets/channel.tag
deleted file mode 100644
index c20a851e79..0000000000
--- a/src/web/app/desktop/-tags/home-widgets/channel.tag
+++ /dev/null
@@ -1,318 +0,0 @@
-<mk-channel-home-widget>
-	<template v-if="!data.compact">
-		<p class="title">%fa:tv%{
-			channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%'
-		}</p>
-		<button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
-	</template>
-	<p class="get-started" v-if="this.data.channel == null">%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
-	<mk-channel ref="channel" show={ this.data.channel }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-			overflow hidden
-
-			> .title
-				z-index 2
-				margin 0
-				padding 0 16px
-				line-height 42px
-				font-size 0.9em
-				font-weight bold
-				color #888
-				box-shadow 0 1px rgba(0, 0, 0, 0.07)
-
-				> [data-fa]
-					margin-right 4px
-
-			> button
-				position absolute
-				z-index 2
-				top 0
-				right 0
-				padding 0
-				width 42px
-				font-size 0.9em
-				line-height 42px
-				color #ccc
-
-				&:hover
-					color #aaa
-
-				&:active
-					color #999
-
-			> .get-started
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> mk-channel
-				height 200px
-
-	</style>
-	<script lang="typescript">
-		this.data = {
-			channel: null,
-			compact: false
-		};
-
-		this.mixin('widget');
-
-		this.on('mount', () => {
-			if (this.data.channel) {
-				this.zap();
-			}
-		});
-
-		this.zap = () => {
-			this.update({
-				fetching: true
-			});
-
-			this.$root.$data.os.api('channels/show', {
-				channel_id: this.data.channel
-			}).then(channel => {
-				this.update({
-					fetching: false,
-					channel: channel
-				});
-
-				this.$refs.channel.zap(channel);
-			});
-		};
-
-		this.settings = () => {
-			const id = window.prompt('チャンネルID');
-			if (!id) return;
-			this.data.channel = id;
-			this.zap();
-
-			// Save state
-			this.save();
-		};
-
-		this.func = () => {
-			this.data.compact = !this.data.compact;
-			this.save();
-		};
-	</script>
-</mk-channel-home-widget>
-
-<mk-channel>
-	<p v-if="fetching">読み込み中<mk-ellipsis/></p>
-	<div v-if="!fetching" ref="posts">
-		<p v-if="posts.length == 0">まだ投稿がありません</p>
-		<mk-channel-post each={ post in posts.slice().reverse() } post={ post } form={ parent.refs.form }/>
-	</div>
-	<mk-channel-form ref="form"/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> p
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-			> div
-				height calc(100% - 38px)
-				overflow auto
-				font-size 0.9em
-
-				> mk-channel-post
-					border-bottom solid 1px #eee
-
-					&:last-child
-						border-bottom none
-
-			> mk-channel-form
-				position absolute
-				left 0
-				bottom 0
-
-	</style>
-	<script lang="typescript">
-		import ChannelStream from '../../../common/scripts/streaming/channel-stream';
-
-		this.mixin('api');
-
-		this.fetching = true;
-		this.channel = null;
-		this.posts = [];
-
-		this.on('unmount', () => {
-			if (this.connection) {
-				this.connection.off('post', this.onPost);
-				this.connection.close();
-			}
-		});
-
-		this.zap = channel => {
-			this.update({
-				fetching: true,
-				channel: channel
-			});
-
-			this.$root.$data.os.api('channels/posts', {
-				channel_id: channel.id
-			}).then(posts => {
-				this.update({
-					fetching: false,
-					posts: posts
-				});
-
-				this.scrollToBottom();
-
-				if (this.connection) {
-					this.connection.off('post', this.onPost);
-					this.connection.close();
-				}
-				this.connection = new ChannelStream(this.channel.id);
-				this.connection.on('post', this.onPost);
-			});
-		};
-
-		this.onPost = post => {
-			this.posts.unshift(post);
-			this.update();
-			this.scrollToBottom();
-		};
-
-		this.scrollToBottom = () => {
-			this.$refs.posts.scrollTop = this.$refs.posts.scrollHeight;
-		};
-	</script>
-</mk-channel>
-
-<mk-channel-post>
-	<header>
-		<a class="index" @click="reply">{ post.index }:</a>
-		<a class="name" href={ _URL_ + '/' + post.user.username }><b>{ post.user.name }</b></a>
-		<span>ID:<i>{ post.user.username }</i></span>
-	</header>
-	<div>
-		<a v-if="post.reply">&gt;&gt;{ post.reply.index }</a>
-		{ post.text }
-		<div class="media" v-if="post.media">
-			<template each={ file in post.media }>
-				<a href={ file.url } target="_blank">
-					<img src={ file.url + '?thumbnail&size=512' } alt={ file.name } title={ file.name }/>
-				</a>
-			</template>
-		</div>
-	</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			margin 0
-			padding 0
-			color #444
-
-			> header
-				position -webkit-sticky
-				position sticky
-				z-index 1
-				top 0
-				padding 8px 4px 4px 16px
-				background rgba(255, 255, 255, 0.9)
-
-				> .index
-					margin-right 0.25em
-
-				> .name
-					margin-right 0.5em
-					color #008000
-
-			> div
-				padding 0 16px 16px 16px
-
-				> .media
-					> a
-						display inline-block
-
-						> img
-							max-width 100%
-							vertical-align bottom
-
-	</style>
-	<script lang="typescript">
-		this.post = this.opts.post;
-		this.form = this.opts.form;
-
-		this.reply = () => {
-			this.form.refs.text.value = `>>${ this.post.index } `;
-		};
-	</script>
-</mk-channel-post>
-
-<mk-channel-form>
-	<input ref="text" disabled={ wait } onkeydown={ onkeydown } placeholder="書いて">
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			width 100%
-			height 38px
-			padding 4px
-			border-top solid 1px #ddd
-
-			> input
-				padding 0 8px
-				width 100%
-				height 100%
-				font-size 14px
-				color #55595c
-				border solid 1px #dadada
-				border-radius 4px
-
-				&:hover
-				&:focus
-					border-color #aeaeae
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.clear = () => {
-			this.$refs.text.value = '';
-		};
-
-		this.onkeydown = e => {
-			if (e.which == 10 || e.which == 13) this.post();
-		};
-
-		this.post = () => {
-			this.update({
-				wait: true
-			});
-
-			let text = this.$refs.text.value;
-			let reply = null;
-
-			if (/^>>([0-9]+) /.test(text)) {
-				const index = text.match(/^>>([0-9]+) /)[1];
-				reply = this.parent.posts.find(p => p.index.toString() == index);
-				text = text.replace(/^>>([0-9]+) /, '');
-			}
-
-			this.$root.$data.os.api('posts/create', {
-				text: text,
-				reply_id: reply ? reply.id : undefined,
-				channel_id: this.parent.channel.id
-			}).then(data => {
-				this.clear();
-			}).catch(err => {
-				alert('失敗した');
-			}).then(() => {
-				this.update({
-					wait: false
-				});
-			});
-		};
-	</script>
-</mk-channel-form>
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index b647f4031d..4f2ac61ee2 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -23,6 +23,7 @@ import MkIndex from './views/pages/index.vue';
 import MkUser from './views/pages/user/user.vue';
 import MkSelectDrive from './views/pages/selectdrive.vue';
 import MkDrive from './views/pages/drive.vue';
+import MkHomeCustomize from './views/pages/home-customize.vue';
 
 /**
  * init
@@ -66,6 +67,8 @@ init(async (launch) => {
 
 	app.$router.addRoutes([{
 		path: '/', name: 'index', component: MkIndex
+	}, {
+		path: '/i/customize-home', component: MkHomeCustomize
 	}, {
 		path: '/i/drive', component: MkDrive
 	}, {
diff --git a/src/web/app/desktop/views/components/calendar.vue b/src/web/app/desktop/views/components/calendar.vue
index a21d3e6148..08b08f8d42 100644
--- a/src/web/app/desktop/views/components/calendar.vue
+++ b/src/web/app/desktop/views/components/calendar.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-calendar">
+<div class="mk-calendar" :data-melt="design == 4 || design == 5">
 	<template v-if="design == 0 || design == 1">
 		<button @click="prev" title="%i18n:desktop.tags.mk-calendar-widget.prev%">%fa:chevron-circle-left%</button>
 		<p class="title">{{ '%i18n:desktop.tags.mk-calendar-widget.title%'.replace('{1}', year).replace('{2}', month) }}</p>
diff --git a/src/web/app/desktop/views/components/home.vue b/src/web/app/desktop/views/components/home.vue
index 8e64a2d83d..6ab1512b01 100644
--- a/src/web/app/desktop/views/components/home.vue
+++ b/src/web/app/desktop/views/components/home.vue
@@ -40,7 +40,7 @@
 		<div v-for="place in ['left', 'main', 'right']" :class="place" :ref="place" :data-place="place">
 			<template v-if="place != 'main'">
 				<template v-for="widget in widgets[place]">
-					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)">
+					<div class="customize-container" v-if="customize" :key="widget.id" @contextmenu.stop.prevent="onWidgetContextmenu(widget.id)" :data-widget-id="widget.id">
 						<component :is="`mkw-${widget.name}`" :widget="widget" :ref="widget.id"/>
 					</div>
 					<template v-else>
@@ -60,7 +60,7 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as uuid from 'uuid';
-import Sortable from 'sortablejs';
+import * as Sortable from 'sortablejs';
 
 export default Vue.extend({
 	props: {
@@ -72,7 +72,6 @@ export default Vue.extend({
 	},
 	data() {
 		return {
-			home: [],
 			bakedHomeData: null,
 			widgetAdderSelected: null
 		};
@@ -95,16 +94,15 @@ export default Vue.extend({
 		},
 		rightEl(): Element {
 			return (this.$refs.right as Element[])[0];
+		},
+		home(): any {
+			return (this as any).os.i.client_settings.home;
 		}
 	},
 	created() {
 		this.bakedHomeData = this.bakeHomeData();
 	},
 	mounted() {
-		(this as any).os.i.on('refreshed', this.onMeRefreshed);
-
-		this.home = (this as any).os.i.client_settings.home;
-
 		this.$nextTick(() => {
 			if (!this.customize) {
 				if (this.leftEl.children.length == 0) {
@@ -132,7 +130,7 @@ export default Vue.extend({
 					animation: 150,
 					onMove: evt => {
 						const id = evt.dragged.getAttribute('data-widget-id');
-						this.home.find(tag => tag.id == id).widget.place = evt.to.getAttribute('data-place');
+						this.home.find(w => w.id == id).place = evt.to.getAttribute('data-place');
 					},
 					onSort: () => {
 						this.saveHome();
@@ -153,24 +151,15 @@ export default Vue.extend({
 			}
 		});
 	},
-	beforeDestroy() {
-		(this as any).os.i.off('refreshed', this.onMeRefreshed);
-	},
 	methods: {
 		bakeHomeData() {
-			return JSON.stringify((this as any).os.i.client_settings.home);
+			return JSON.stringify(this.home);
 		},
 		onTlLoaded() {
 			this.$emit('loaded');
 		},
-		onMeRefreshed() {
-			if (this.bakedHomeData != this.bakeHomeData()) {
-				// TODO: i18n
-				alert('別の場所でホームが編集されました。ページを再度読み込みすると編集が反映されます。');
-			}
-		},
 		onWidgetContextmenu(widgetId) {
-			(this.$refs[widgetId] as any).func();
+			(this.$refs[widgetId] as any)[0].func();
 		},
 		addWidget() {
 			const widget = {
@@ -180,29 +169,13 @@ export default Vue.extend({
 				data: {}
 			};
 
-			(this as any).os.i.client_settings.home.unshift(widget);
+			this.home.unshift(widget);
 
 			this.saveHome();
 		},
 		saveHome() {
-			const data = [];
-
-			Array.from(this.leftEl.children).forEach(el => {
-				const id = el.getAttribute('data-widget-id');
-				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
-				widget.place = 'left';
-				data.push(widget);
-			});
-
-			Array.from(this.rightEl.children).forEach(el => {
-				const id = el.getAttribute('data-widget-id');
-				const widget = (this as any).os.i.client_settings.home.find(w => w.id == id);
-				widget.place = 'right';
-				data.push(widget);
-			});
-
 			(this as any).api('i/update_home', {
-				home: data
+				home: this.home
 			});
 		},
 		warp(date) {
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index cbe145daf5..86606a14a2 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -35,6 +35,7 @@ import wDonation from './widgets/donation.vue';
 import wNotifications from './widgets/notifications.vue';
 import wBroadcast from './widgets/broadcast.vue';
 import wTimemachine from './widgets/timemachine.vue';
+import wProfile from './widgets/profile.vue';
 
 Vue.component('mk-ui', ui);
 Vue.component('mk-ui-notification', uiNotification);
@@ -71,3 +72,4 @@ Vue.component('mkw-donation', wDonation);
 Vue.component('mkw-notifications', wNotifications);
 Vue.component('mkw-broadcast', wBroadcast);
 Vue.component('mkw-timemachine', wTimemachine);
+Vue.component('mkw-profile', wProfile);
diff --git a/src/web/app/desktop/views/components/widgets/activity.vue b/src/web/app/desktop/views/components/widgets/activity.vue
index 8bf45a5562..2ff5fe4f03 100644
--- a/src/web/app/desktop/views/components/widgets/activity.vue
+++ b/src/web/app/desktop/views/components/widgets/activity.vue
@@ -10,10 +10,10 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'activity',
-	props: {
+	props: () => ({
 		design: 0,
 		view: 0
-	}
+	})
 }).extend({
 	methods: {
 		func() {
diff --git a/src/web/app/desktop/views/components/widgets/broadcast.vue b/src/web/app/desktop/views/components/widgets/broadcast.vue
index 1a0fd9280c..68c9cebfa2 100644
--- a/src/web/app/desktop/views/components/widgets/broadcast.vue
+++ b/src/web/app/desktop/views/components/widgets/broadcast.vue
@@ -25,9 +25,9 @@ import { lang } from '../../../../config';
 
 export default define({
 	name: 'broadcast',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/calendar.vue b/src/web/app/desktop/views/components/widgets/calendar.vue
index 8574bf59f9..c16602db46 100644
--- a/src/web/app/desktop/views/components/widgets/calendar.vue
+++ b/src/web/app/desktop/views/components/widgets/calendar.vue
@@ -38,9 +38,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'calendar',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/channel.channel.form.vue b/src/web/app/desktop/views/components/widgets/channel.channel.form.vue
new file mode 100644
index 0000000000..392ba5924b
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/channel.channel.form.vue
@@ -0,0 +1,67 @@
+<template>
+<div class="form">
+	<input v-model="text" :disabled="wait" @keydown="onKeydown" placeholder="書いて">
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	data() {
+		return {
+			text: '',
+			wait: false
+		};
+	},
+	methods: {
+		onKeydown(e) {
+			if (e.which == 10 || e.which == 13) this.post();
+		},
+		post() {
+			this.wait = true;
+
+			let reply = null;
+
+			if (/^>>([0-9]+) /.test(this.text)) {
+				const index = this.text.match(/^>>([0-9]+) /)[1];
+				reply = (this.$parent as any).posts.find(p => p.index.toString() == index);
+				this.text = this.text.replace(/^>>([0-9]+) /, '');
+			}
+
+			(this as any).api('posts/create', {
+				text: this.text,
+				reply_id: reply ? reply.id : undefined,
+				channel_id: (this.$parent as any).channel.id
+			}).then(data => {
+				this.text = '';
+			}).catch(err => {
+				alert('失敗した');
+			}).then(() => {
+				this.wait = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.form
+	width 100%
+	height 38px
+	padding 4px
+	border-top solid 1px #ddd
+
+	> input
+		padding 0 8px
+		width 100%
+		height 100%
+		font-size 14px
+		color #55595c
+		border solid 1px #dadada
+		border-radius 4px
+
+		&:hover
+		&:focus
+			border-color #aeaeae
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/channel.channel.post.vue b/src/web/app/desktop/views/components/widgets/channel.channel.post.vue
new file mode 100644
index 0000000000..faaf0fb731
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/channel.channel.post.vue
@@ -0,0 +1,64 @@
+<template>
+<div class="post">
+	<header>
+		<a class="index" @click="reply">{{ post.index }}:</a>
+		<router-link class="name" :to="`/${post.user.username}`" v-user-preview="post.user.id"><b>{{ post.user.name }}</b></router-link>
+		<span>ID:<i>{{ post.user.username }}</i></span>
+	</header>
+	<div>
+		<a v-if="post.reply">&gt;&gt;{{ post.reply.index }}</a>
+		{{ post.text }}
+		<div class="media" v-if="post.media">
+			<a v-for="file in post.media" :href="file.url" target="_blank">
+				<img :src="`${file.url}?thumbnail&size=512`" :alt="file.name" :title="file.name"/>
+			</a>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: ['post'],
+	methods: {
+		reply() {
+			this.$emit('reply', this.post);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.post
+	margin 0
+	padding 0
+	color #444
+
+	> header
+		position -webkit-sticky
+		position sticky
+		z-index 1
+		top 0
+		padding 8px 4px 4px 16px
+		background rgba(255, 255, 255, 0.9)
+
+		> .index
+			margin-right 0.25em
+
+		> .name
+			margin-right 0.5em
+			color #008000
+
+	> div
+		padding 0 16px 16px 16px
+
+		> .media
+			> a
+				display inline-block
+
+				> img
+					max-width 100%
+					vertical-align bottom
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/channel.channel.vue b/src/web/app/desktop/views/components/widgets/channel.channel.vue
new file mode 100644
index 0000000000..5de13aec03
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/channel.channel.vue
@@ -0,0 +1,104 @@
+<template>
+<div class="channel">
+	<p v-if="fetching">読み込み中<mk-ellipsis/></p>
+	<div v-if="!fetching" ref="posts">
+		<p v-if="posts.length == 0">まだ投稿がありません</p>
+		<x-post class="post" v-for="post in posts.slice().reverse()" :post="post" :key="post.id" @reply="reply"/>
+	</div>
+	<x-form class="form" ref="form"/>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import ChannelStream from '../../../../common/scripts/streaming/channel-stream';
+import XForm from './channel.channel.form.vue';
+import XPost from './channel.channel.post.vue';
+
+export default Vue.extend({
+	components: {
+		XForm,
+		XPost
+	},
+	props: ['channel'],
+	data() {
+		return {
+			fetching: true,
+			posts: [],
+			connection: null
+		};
+	},
+	watch: {
+		channel() {
+			this.zap();
+		}
+	},
+	mounted() {
+		this.zap();
+	},
+	beforeDestroy() {
+		this.disconnect();
+	},
+	methods: {
+		zap() {
+			this.fetching = true;
+
+			(this as any).api('channels/posts', {
+				channel_id: this.channel.id
+			}).then(posts => {
+				this.posts = posts;
+				this.fetching = false;
+
+				this.scrollToBottom();
+
+				this.disconnect();
+				this.connection = new ChannelStream(this.channel.id);
+				this.connection.on('post', this.onPost);
+			});
+		},
+		disconnect() {
+			if (this.connection) {
+				this.connection.off('post', this.onPost);
+				this.connection.close();
+			}
+		},
+		onPost(post) {
+			this.posts.unshift(post);
+			this.scrollToBottom();
+		},
+		scrollToBottom() {
+			(this.$refs.posts as any).scrollTop = (this.$refs.posts as any).scrollHeight;
+		},
+		reply(post) {
+			(this.$refs.form as any).text = `>>${ post.index } `;
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.channel
+
+	> p
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> div
+		height calc(100% - 38px)
+		overflow auto
+		font-size 0.9em
+
+		> .post
+			border-bottom solid 1px #eee
+
+			&:last-child
+				border-bottom none
+
+	> .form
+		position absolute
+		left 0
+		bottom 0
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/channel.vue b/src/web/app/desktop/views/components/widgets/channel.vue
new file mode 100644
index 0000000000..484dca9f68
--- /dev/null
+++ b/src/web/app/desktop/views/components/widgets/channel.vue
@@ -0,0 +1,107 @@
+<template>
+<div class="mkw-channel">
+	<template v-if="!data.compact">
+		<p class="title">%fa:tv%{{ channel ? channel.title : '%i18n:desktop.tags.mk-channel-home-widget.title%' }}</p>
+		<button @click="settings" title="%i18n:desktop.tags.mk-channel-home-widget.settings%">%fa:cog%</button>
+	</template>
+	<p class="get-started" v-if="props.channel == null">%i18n:desktop.tags.mk-channel-home-widget.get-started%</p>
+	<x-channel class="channel" :channel="channel" v-else/>
+</div>
+</template>
+
+<script lang="ts">
+import define from '../../../../common/define-widget';
+import XChannel from './channel.channel.vue';
+
+export default define({
+	name: 'server',
+	props: () => ({
+		channel: null,
+		compact: false
+	})
+}).extend({
+	components: {
+		XChannel
+	},
+	data() {
+		return {
+			fetching: true,
+			channel: null
+		};
+	},
+	mounted() {
+		if (this.props.channel) {
+				this.zap();
+			}
+	},
+	methods: {
+		func() {
+			this.props.compact = !this.props.compact;
+		},
+		settings() {
+			const id = window.prompt('チャンネルID');
+			if (!id) return;
+			this.props.channel = id;
+			this.zap();
+		},
+		zap() {
+			this.fetching = true;
+
+			(this as any).api('channels/show', {
+				channel_id: this.props.channel
+			}).then(channel => {
+				this.channel = channel;
+				this.fetching = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mkw-channel
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+	overflow hidden
+
+	> .title
+		z-index 2
+		margin 0
+		padding 0 16px
+		line-height 42px
+		font-size 0.9em
+		font-weight bold
+		color #888
+		box-shadow 0 1px rgba(0, 0, 0, 0.07)
+
+		> [data-fa]
+			margin-right 4px
+
+	> button
+		position absolute
+		z-index 2
+		top 0
+		right 0
+		padding 0
+		width 42px
+		font-size 0.9em
+		line-height 42px
+		color #ccc
+
+		&:hover
+			color #aaa
+
+		&:active
+			color #999
+
+	> .get-started
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+	> .channel
+		height 200px
+
+</style>
diff --git a/src/web/app/desktop/views/components/widgets/messaging.vue b/src/web/app/desktop/views/components/widgets/messaging.vue
index 733989b782..039a524f50 100644
--- a/src/web/app/desktop/views/components/widgets/messaging.vue
+++ b/src/web/app/desktop/views/components/widgets/messaging.vue
@@ -9,9 +9,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'messaging',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	methods: {
 		navigate(user) {
diff --git a/src/web/app/desktop/views/components/widgets/notifications.vue b/src/web/app/desktop/views/components/widgets/notifications.vue
index 2d613fa232..978cf5218e 100644
--- a/src/web/app/desktop/views/components/widgets/notifications.vue
+++ b/src/web/app/desktop/views/components/widgets/notifications.vue
@@ -12,9 +12,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'notifications',
-	props: {
+	props: () => ({
 		compact: false
-	}
+	})
 }).extend({
 	methods: {
 		settings() {
diff --git a/src/web/app/desktop/views/components/widgets/photo-stream.vue b/src/web/app/desktop/views/components/widgets/photo-stream.vue
index 6ad7d2f064..04b71975b3 100644
--- a/src/web/app/desktop/views/components/widgets/photo-stream.vue
+++ b/src/web/app/desktop/views/components/widgets/photo-stream.vue
@@ -13,9 +13,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'photo-stream',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/polls.vue b/src/web/app/desktop/views/components/widgets/polls.vue
index 71d5391b10..f1b34ceed0 100644
--- a/src/web/app/desktop/views/components/widgets/polls.vue
+++ b/src/web/app/desktop/views/components/widgets/polls.vue
@@ -18,9 +18,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'polls',
-	props: {
+	props: () => ({
 		compact: false
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/post-form.vue b/src/web/app/desktop/views/components/widgets/post-form.vue
index c32ad5761a..94b03f84a8 100644
--- a/src/web/app/desktop/views/components/widgets/post-form.vue
+++ b/src/web/app/desktop/views/components/widgets/post-form.vue
@@ -12,9 +12,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'post-form',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/profile.vue b/src/web/app/desktop/views/components/widgets/profile.vue
index 9a0d40a5c0..68cf469788 100644
--- a/src/web/app/desktop/views/components/widgets/profile.vue
+++ b/src/web/app/desktop/views/components/widgets/profile.vue
@@ -4,19 +4,19 @@
 	:data-melt="props.design == 2"
 >
 	<div class="banner"
-		style={ I.banner_url ? 'background-image: url(' + I.banner_url + '?thumbnail&size=256)' : '' }
+		:style="os.i.banner_url ? `background-image: url(${os.i.banner_url}?thumbnail&size=256)` : ''"
 		title="クリックでバナー編集"
-		@click="wapi_setBanner"
+		@click="os.apis.updateBanner"
 	></div>
 	<img class="avatar"
-		src={ I.avatar_url + '?thumbnail&size=96' }
-		@click="wapi_setAvatar"
+		:src="`${os.i.avatar_url}?thumbnail&size=96`"
+		@click="os.apis.updateAvatar"
 		alt="avatar"
 		title="クリックでアバター編集"
-		v-user-preview={ I.id }
+		v-user-preview="os.i.id"
 	/>
-	<a class="name" href={ '/' + I.username }>{ I.name }</a>
-	<p class="username">@{ I.username }</p>
+	<router-link class="name" :to="`/${os.i.username}`">{{ os.i.name }}</router-link>
+	<p class="username">@{{ os.i.username }}</p>
 </div>
 </template>
 
@@ -24,9 +24,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'profile',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	methods: {
 		func() {
diff --git a/src/web/app/desktop/views/components/widgets/rss.vue b/src/web/app/desktop/views/components/widgets/rss.vue
index 954edf3c57..3507129716 100644
--- a/src/web/app/desktop/views/components/widgets/rss.vue
+++ b/src/web/app/desktop/views/components/widgets/rss.vue
@@ -15,9 +15,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'rss',
-	props: {
+	props: () => ({
 		compact: false
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/server.vue b/src/web/app/desktop/views/components/widgets/server.vue
index 00e2f8f186..c08056691b 100644
--- a/src/web/app/desktop/views/components/widgets/server.vue
+++ b/src/web/app/desktop/views/components/widgets/server.vue
@@ -27,10 +27,10 @@ import XInfo from './server.info.vue';
 
 export default define({
 	name: 'server',
-	props: {
+	props: () => ({
 		design: 0,
 		view: 0
-	}
+	})
 }).extend({
 	components: {
 		XCpuMemory,
diff --git a/src/web/app/desktop/views/components/widgets/slideshow.vue b/src/web/app/desktop/views/components/widgets/slideshow.vue
index 3c2ef6da4f..75af3c0f1d 100644
--- a/src/web/app/desktop/views/components/widgets/slideshow.vue
+++ b/src/web/app/desktop/views/components/widgets/slideshow.vue
@@ -15,10 +15,10 @@ import * as anime from 'animejs';
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'slideshow',
-	props: {
+	props: () => ({
 		folder: undefined,
 		size: 0
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/timemachine.vue b/src/web/app/desktop/views/components/widgets/timemachine.vue
index d484ce6d74..7420482168 100644
--- a/src/web/app/desktop/views/components/widgets/timemachine.vue
+++ b/src/web/app/desktop/views/components/widgets/timemachine.vue
@@ -8,9 +8,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'timemachine',
-	props: {
+	props: () => ({
 		design: 0
-	}
+	})
 }).extend({
 	methods: {
 		chosen(date) {
diff --git a/src/web/app/desktop/views/components/widgets/trends.vue b/src/web/app/desktop/views/components/widgets/trends.vue
index 23d39563f2..a764639ce9 100644
--- a/src/web/app/desktop/views/components/widgets/trends.vue
+++ b/src/web/app/desktop/views/components/widgets/trends.vue
@@ -17,9 +17,9 @@
 import define from '../../../../common/define-widget';
 export default define({
 	name: 'trends',
-	props: {
+	props: () => ({
 		compact: false
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/components/widgets/users.vue b/src/web/app/desktop/views/components/widgets/users.vue
index 6876d0bf04..4a9ab2aa33 100644
--- a/src/web/app/desktop/views/components/widgets/users.vue
+++ b/src/web/app/desktop/views/components/widgets/users.vue
@@ -28,9 +28,9 @@ const limit = 3;
 
 export default define({
 	name: 'users',
-	props: {
+	props: () => ({
 		compact: false
-	}
+	})
 }).extend({
 	data() {
 		return {
diff --git a/src/web/app/desktop/views/pages/home-custmize.vue b/src/web/app/desktop/views/pages/home-customize.vue
similarity index 89%
rename from src/web/app/desktop/views/pages/home-custmize.vue
rename to src/web/app/desktop/views/pages/home-customize.vue
index 257e83cad6..8aa06be57f 100644
--- a/src/web/app/desktop/views/pages/home-custmize.vue
+++ b/src/web/app/desktop/views/pages/home-customize.vue
@@ -1,5 +1,5 @@
 <template>
-	<mk-home customize/>
+<mk-home customize/>
 </template>
 
 <script lang="ts">
diff --git a/src/web/app/init.ts b/src/web/app/init.ts
index 02c125efef..e4cb8f8bc0 100644
--- a/src/web/app/init.ts
+++ b/src/web/app/init.ts
@@ -103,6 +103,14 @@ export default (callback: (launch: (api: (os: MiOS) => API) => [Vue, MiOS]) => v
 				router: new VueRouter({
 					mode: 'history'
 				}),
+				created() {
+					this.$watch('os.i', i => {
+						// キャッシュ更新
+						localStorage.setItem('me', JSON.stringify(i));
+					}, {
+						deep: true
+					});
+				},
 				render: createEl => createEl(App)
 			}).$mount('#app');