diff --git a/src/web/app/common/-tags/authorized-apps.tag b/src/web/app/common/-tags/authorized-apps.tag
index 288c2fcc2d..ed1570650a 100644
--- a/src/web/app/common/-tags/authorized-apps.tag
+++ b/src/web/app/common/-tags/authorized-apps.tag
@@ -28,7 +28,6 @@
 			this.$root.$data.os.api('i/authorized_apps').then(apps => {
 				this.apps = apps;
 				this.fetching = false;
-				this.update();
 			});
 		});
 	</script>
diff --git a/src/web/app/common/views/components/messaging.vue b/src/web/app/common/views/components/messaging.vue
index c0b3a1924b..c1d5418944 100644
--- a/src/web/app/common/views/components/messaging.vue
+++ b/src/web/app/common/views/components/messaging.vue
@@ -78,8 +78,8 @@ export default Vue.extend({
 		this.connection.on('read', this.onRead);
 
 		(this as any).api('messaging/history').then(messages => {
-			this.fetching = false;
 			this.messages = messages;
+			this.fetching = false;
 		});
 	},
 	beforeDestroy() {
diff --git a/src/web/app/desktop/-tags/widgets/activity.tag b/src/web/app/desktop/-tags/widgets/activity.tag
deleted file mode 100644
index 1f9bee5ed5..0000000000
--- a/src/web/app/desktop/-tags/widgets/activity.tag
+++ /dev/null
@@ -1,246 +0,0 @@
-<mk-activity-widget data-melt={ design == 2 }>
-	<template v-if="design == 0">
-		<p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p>
-		<button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button>
-	</template>
-	<p class="initializing" v-if="initializing">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
-	<mk-activity-widget-calender v-if="!initializing && view == 0" data={ [].concat(activity) }/>
-	<mk-activity-widget-chart v-if="!initializing && view == 1" data={ [].concat(activity) }/>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-			background #fff
-			border solid 1px rgba(0, 0, 0, 0.075)
-			border-radius 6px
-
-			&[data-melt]
-				background transparent !important
-				border none !important
-
-			> .title
-				z-index 1
-				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
-
-			> .initializing
-				margin 0
-				padding 16px
-				text-align center
-				color #aaa
-
-				> [data-fa]
-					margin-right 4px
-
-	</style>
-	<script lang="typescript">
-		this.mixin('api');
-
-		this.design = this.opts.design || 0;
-		this.view = this.opts.view || 0;
-
-		this.user = this.opts.user;
-		this.initializing = true;
-
-		this.on('mount', () => {
-			this.$root.$data.os.api('aggregation/users/activity', {
-				user_id: this.user.id,
-				limit: 20 * 7
-			}).then(activity => {
-				this.update({
-					initializing: false,
-					activity
-				});
-			});
-		});
-
-		this.toggle = () => {
-			this.view++;
-			if (this.view == 2) this.view = 0;
-			this.update();
-			this.$emit('view-changed', this.view);
-		};
-	</script>
-</mk-activity-widget>
-
-<mk-activity-widget-calender>
-	<svg viewBox="0 0 21 7" preserveAspectRatio="none">
-		<rect each={ data } class="day"
-			width="1" height="1"
-			riot-x={ x } riot-y={ date.weekday }
-			rx="1" ry="1"
-			fill="transparent">
-			<title>{ date.year }/{ date.month }/{ date.day }<br/>Post: { posts }, Reply: { replies }, Repost: { reposts }</title>
-		</rect>
-		<rect each={ data }
-			riot-width={ v } riot-height={ v }
-			riot-x={ x + ((1 - v) / 2) } riot-y={ date.weekday + ((1 - v) / 2) }
-			rx="1" ry="1"
-			fill={ color }
-			style="pointer-events: none;"/>
-		<rect class="today"
-			width="1" height="1"
-			riot-x={ data[data.length - 1].x } riot-y={ data[data.length - 1].date.weekday }
-			rx="1" ry="1"
-			fill="none"
-			stroke-width="0.1"
-			stroke="#f73520"/>
-	</svg>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> svg
-				display block
-				padding 10px
-				width 100%
-
-				> rect
-					transform-origin center
-
-					&.day
-						&:hover
-							fill rgba(0, 0, 0, 0.05)
-
-	</style>
-	<script lang="typescript">
-		this.data = this.opts.data;
-		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
-		const peak = Math.max.apply(null, this.data.map(d => d.total));
-
-		let x = 0;
-		this.data.reverse().forEach(d => {
-			d.x = x;
-			d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
-
-			d.v = d.total / (peak / 2);
-			if (d.v > 1) d.v = 1;
-			const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
-			const cs = d.v * 100;
-			const cl = 15 + ((1 - d.v) * 80);
-			d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
-
-			if (d.date.weekday == 6) x++;
-		});
-	</script>
-</mk-activity-widget-calender>
-
-<mk-activity-widget-chart>
-	<svg riot-viewBox="0 0 { viewBoxX } { viewBoxY }" preserveAspectRatio="none" onmousedown={ onMousedown }>
-		<title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title>
-		<polyline
-			riot-points={ pointsPost }
-			fill="none"
-			stroke-width="1"
-			stroke="#41ddde"/>
-		<polyline
-			riot-points={ pointsReply }
-			fill="none"
-			stroke-width="1"
-			stroke="#f7796c"/>
-		<polyline
-			riot-points={ pointsRepost }
-			fill="none"
-			stroke-width="1"
-			stroke="#a1de41"/>
-		<polyline
-			riot-points={ pointsTotal }
-			fill="none"
-			stroke-width="1"
-			stroke="#555"
-			stroke-dasharray="2 2"/>
-	</svg>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> svg
-				display block
-				padding 10px
-				width 100%
-				cursor all-scroll
-	</style>
-	<script lang="typescript">
-		this.viewBoxX = 140;
-		this.viewBoxY = 60;
-		this.zoom = 1;
-		this.pos = 0;
-
-		this.data = this.opts.data.reverse();
-		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
-		const peak = Math.max.apply(null, this.data.map(d => d.total));
-
-		this.on('mount', () => {
-			this.render();
-		});
-
-		this.render = () => {
-			this.update({
-				pointsPost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' '),
-				pointsReply: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' '),
-				pointsRepost: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' '),
-				pointsTotal: this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ')
-			});
-		};
-
-		this.onMousedown = e => {
-			e.preventDefault();
-
-			const clickX = e.clientX;
-			const clickY = e.clientY;
-			const baseZoom = this.zoom;
-			const basePos = this.pos;
-
-			// 動かした時
-			dragListen(me => {
-				let moveLeft = me.clientX - clickX;
-				let moveTop = me.clientY - clickY;
-
-				this.zoom = baseZoom + (-moveTop / 20);
-				this.pos = basePos + moveLeft;
-				if (this.zoom < 1) this.zoom = 1;
-				if (this.pos > 0) this.pos = 0;
-				if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
-
-				this.render();
-			});
-		};
-
-		function dragListen(fn) {
-			window.addEventListener('mousemove',  fn);
-			window.addEventListener('mouseleave', dragClear.bind(null, fn));
-			window.addEventListener('mouseup',    dragClear.bind(null, fn));
-		}
-
-		function dragClear(fn) {
-			window.removeEventListener('mousemove',  fn);
-			window.removeEventListener('mouseleave', dragClear);
-			window.removeEventListener('mouseup',    dragClear);
-		}
-	</script>
-</mk-activity-widget-chart>
-
diff --git a/src/web/app/desktop/views/components/activity.calendar.vue b/src/web/app/desktop/views/components/activity.calendar.vue
new file mode 100644
index 0000000000..d9b8523152
--- /dev/null
+++ b/src/web/app/desktop/views/components/activity.calendar.vue
@@ -0,0 +1,66 @@
+<template>
+<svg viewBox="0 0 21 7" preserveAspectRatio="none">
+	<rect v-for="record in data" class="day"
+		width="1" height="1"
+		:x="record.x" :y="record.date.weekday"
+		rx="1" ry="1"
+		fill="transparent">
+		<title>{{ record.date.year }}/{{ record.date.month }}/{{ record.date.day }}</title>
+	</rect>
+	<rect v-for="record in data" class="day"
+		:width="record.v" :height="record.v"
+		:x="record.x + ((1 - record.v) / 2)" :y="record.date.weekday + ((1 - record.v) / 2)"
+		rx="1" ry="1"
+		:fill="record.color"
+		style="pointer-events: none;"/>
+	<rect class="today"
+		width="1" height="1"
+		:x="data[data.length - 1].x" :y="data[data.length - 1].date.weekday"
+		rx="1" ry="1"
+		fill="none"
+		stroke-width="0.1"
+		stroke="#f73520"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['data'],
+	created() {
+		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+		const peak = Math.max.apply(null, this.data.map(d => d.total));
+
+		let x = 0;
+		this.data.reverse().forEach(d => {
+			d.x = x;
+			d.date.weekday = (new Date(d.date.year, d.date.month - 1, d.date.day)).getDay();
+
+			d.v = d.total / (peak / 2);
+			if (d.v > 1) d.v = 1;
+			const ch = d.date.weekday == 0 || d.date.weekday == 6 ? 275 : 170;
+			const cs = d.v * 100;
+			const cl = 15 + ((1 - d.v) * 80);
+			d.color = `hsl(${ch}, ${cs}%, ${cl}%)`;
+
+			if (d.date.weekday == 6) x++;
+		});
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+svg
+	display block
+	padding 10px
+	width 100%
+
+	> rect
+		transform-origin center
+
+		&.day
+			&:hover
+				fill rgba(0, 0, 0, 0.05)
+
+</style>
diff --git a/src/web/app/desktop/views/components/activity.chart.vue b/src/web/app/desktop/views/components/activity.chart.vue
new file mode 100644
index 0000000000..e64b181ba1
--- /dev/null
+++ b/src/web/app/desktop/views/components/activity.chart.vue
@@ -0,0 +1,101 @@
+<template>
+<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" @mousedown.prevent="onMousedown">
+	<title>Black ... Total<br/>Blue ... Posts<br/>Red ... Replies<br/>Green ... Reposts</title>
+	<polyline
+		:points="pointsPost"
+		fill="none"
+		stroke-width="1"
+		stroke="#41ddde"/>
+	<polyline
+		:points="pointsReply"
+		fill="none"
+		stroke-width="1"
+		stroke="#f7796c"/>
+	<polyline
+		:points="pointsRepost"
+		fill="none"
+		stroke-width="1"
+		stroke="#a1de41"/>
+	<polyline
+		:points="pointsTotal"
+		fill="none"
+		stroke-width="1"
+		stroke="#555"
+		stroke-dasharray="2 2"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+function dragListen(fn) {
+	window.addEventListener('mousemove',  fn);
+	window.addEventListener('mouseleave', dragClear.bind(null, fn));
+	window.addEventListener('mouseup',    dragClear.bind(null, fn));
+}
+
+function dragClear(fn) {
+	window.removeEventListener('mousemove',  fn);
+	window.removeEventListener('mouseleave', dragClear);
+	window.removeEventListener('mouseup',    dragClear);
+}
+
+export default Vue.extend({
+	props: ['data'],
+	data() {
+		return {
+			viewBoxX: 140,
+			viewBoxY: 60,
+			zoom: 1,
+			pos: 0,
+			pointsPost: null,
+			pointsReply: null,
+			pointsRepost: null,
+			pointsTotal: null
+		};
+	},
+	created() {
+		this.data.reverse();
+		this.data.forEach(d => d.total = d.posts + d.replies + d.reposts);
+		this.render();
+	},
+	methods: {
+		render() {
+			const peak = Math.max.apply(null, this.data.map(d => d.total));
+			this.pointsPost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.posts / peak)) * this.viewBoxY}`).join(' ');
+			this.pointsReply = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.replies / peak)) * this.viewBoxY}`).join(' ');
+			this.pointsRepost = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.reposts / peak)) * this.viewBoxY}`).join(' ');
+			this.pointsTotal = this.data.map((d, i) => `${(i * this.zoom) + this.pos},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
+		},
+		onMousedown(e) {
+			const clickX = e.clientX;
+			const clickY = e.clientY;
+			const baseZoom = this.zoom;
+			const basePos = this.pos;
+
+			// 動かした時
+			dragListen(me => {
+				let moveLeft = me.clientX - clickX;
+				let moveTop = me.clientY - clickY;
+
+				this.zoom = baseZoom + (-moveTop / 20);
+				this.pos = basePos + moveLeft;
+				if (this.zoom < 1) this.zoom = 1;
+				if (this.pos > 0) this.pos = 0;
+				if (this.pos < -(((this.data.length - 1) * this.zoom) - this.viewBoxX)) this.pos = -(((this.data.length - 1) * this.zoom) - this.viewBoxX);
+
+				this.render();
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+svg
+	display block
+	padding 10px
+	width 100%
+	cursor all-scroll
+
+</style>
diff --git a/src/web/app/desktop/views/components/activity.vue b/src/web/app/desktop/views/components/activity.vue
new file mode 100644
index 0000000000..d1c44f0f5d
--- /dev/null
+++ b/src/web/app/desktop/views/components/activity.vue
@@ -0,0 +1,116 @@
+<template>
+<div class="mk-activity">
+	<template v-if="design == 0">
+		<p class="title">%fa:chart-bar%%i18n:desktop.tags.mk-activity-widget.title%</p>
+		<button @click="toggle" title="%i18n:desktop.tags.mk-activity-widget.toggle%">%fa:sort%</button>
+	</template>
+	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
+	<template v-else>
+		<mk-activity-widget-calender v-show="view == 0" :data="[].concat(activity)"/>
+		<mk-activity-widget-chart v-show="view == 1" :data="[].concat(activity)"/>
+	</template>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import Calendar from './activity.calendar.vue';
+import Chart from './activity.chart.vue';
+
+export default Vue.extend({
+	components: {
+		'mk-activity-widget-calender': Calendar,
+		'mk-activity-widget-chart': Chart
+	},
+	props: {
+		design: {
+			default: 0
+		},
+		initView: {
+			default: 0
+		},
+		user: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+			fetching: true,
+			activity: null,
+			view: this.initView
+		};
+	},
+	mounted() {
+		(this as any).api('aggregation/users/activity', {
+			user_id: this.user.id,
+			limit: 20 * 7
+		}).then(activity => {
+			this.activity = activity;
+			this.fetching = false;
+		});
+	},
+	methods: {
+		toggle() {
+			if (this.view == 1) {
+				this.view = 0;
+				this.$emit('viewChanged', this.view);
+			} else {
+				this.view++;
+				this.$emit('viewChanged', this.view);
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-activity
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+
+	&[data-melt]
+		background transparent !important
+		border none !important
+
+	> .title
+		z-index 1
+		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
+
+	> .fetching
+		margin 0
+		padding 16px
+		text-align center
+		color #aaa
+
+		> [data-fa]
+			margin-right 4px
+
+</style>
diff --git a/src/web/app/desktop/views/components/calendar.vue b/src/web/app/desktop/views/components/calendar.vue
index e548a82c57..a21d3e6148 100644
--- a/src/web/app/desktop/views/components/calendar.vue
+++ b/src/web/app/desktop/views/components/calendar.vue
@@ -47,7 +47,7 @@ export default Vue.extend({
 			default: 0
 		},
 		start: {
-			type: Object,
+			type: Date,
 			required: false
 		}
 	},
@@ -94,7 +94,7 @@ export default Vue.extend({
 		isOutOfRange(day) {
 			const test = (new Date(this.year, this.month - 1, day)).getTime();
 			return test > this.today.getTime() ||
-				(this.start ? test < this.start.getTime() : false);
+				(this.start ? test < (this.start as any).getTime() : false);
 		},
 
 		isDonichi(day) {
diff --git a/src/web/app/desktop/views/components/followers-window.vue b/src/web/app/desktop/views/components/followers-window.vue
index e56545cccb..ed439114c3 100644
--- a/src/web/app/desktop/views/components/followers-window.vue
+++ b/src/web/app/desktop/views/components/followers-window.vue
@@ -1,9 +1,9 @@
 <template>
-<mk-window width='400px' height='550px' @closed="$destroy">
+<mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
 		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロワー
 	</span>
-	<mk-user-followers :user="user"/>
+	<mk-followers-list :user="user"/>
 </mk-window>
 </template>
 
diff --git a/src/web/app/desktop/views/components/following-window.vue b/src/web/app/desktop/views/components/following-window.vue
index fa2edfa473..4e1fb0306f 100644
--- a/src/web/app/desktop/views/components/following-window.vue
+++ b/src/web/app/desktop/views/components/following-window.vue
@@ -1,9 +1,9 @@
 <template>
-<mk-window width='400px' height='550px' @closed="$destroy">
+<mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
 		<img :src="`${user.avatar_url}?thumbnail&size=64`" alt=""/>{{ user.name }}のフォロー
 	</span>
-	<mk-user-following :user="user"/>
+	<mk-following-list :user="user"/>
 </mk-window>
 </template>
 
diff --git a/src/web/app/desktop/views/components/friends-maker.vue b/src/web/app/desktop/views/components/friends-maker.vue
index b23373421d..61015b979f 100644
--- a/src/web/app/desktop/views/components/friends-maker.vue
+++ b/src/web/app/desktop/views/components/friends-maker.vue
@@ -43,8 +43,8 @@ export default Vue.extend({
 				limit: this.limit,
 				offset: this.limit * this.page
 			}).then(users => {
-				this.fetching = false;
 				this.users = users;
+				this.fetching = false;
 			});
 		},
 		refresh() {
diff --git a/src/web/app/desktop/views/components/index.ts b/src/web/app/desktop/views/components/index.ts
index 9a27369547..8e48d67b98 100644
--- a/src/web/app/desktop/views/components/index.ts
+++ b/src/web/app/desktop/views/components/index.ts
@@ -34,6 +34,7 @@ import driveNavFolder from './drive-nav-folder.vue';
 import postDetail from './post-detail.vue';
 import settings from './settings.vue';
 import calendar from './calendar.vue';
+import activity from './activity.vue';
 import wNav from './widgets/nav.vue';
 import wCalendar from './widgets/calendar.vue';
 import wPhotoStream from './widgets/photo-stream.vue';
@@ -78,6 +79,7 @@ Vue.component('mk-drive-nav-folder', driveNavFolder);
 Vue.component('mk-post-detail', postDetail);
 Vue.component('mk-settings', settings);
 Vue.component('mk-calendar', calendar);
+Vue.component('mk-activity', activity);
 Vue.component('mkw-nav', wNav);
 Vue.component('mkw-calendar', wCalendar);
 Vue.component('mkw-photo-stream', wPhotoStream);
diff --git a/src/web/app/desktop/views/components/mute-setting.vue b/src/web/app/desktop/views/components/mute-setting.vue
index 3fcc34c9e6..fe78401af9 100644
--- a/src/web/app/desktop/views/components/mute-setting.vue
+++ b/src/web/app/desktop/views/components/mute-setting.vue
@@ -23,8 +23,8 @@ export default Vue.extend({
 	},
 	mounted() {
 		(this as any).api('mute/list').then(x => {
-			this.fetching = false;
 			this.users = x.users;
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/desktop/views/components/post-detail.vue b/src/web/app/desktop/views/components/post-detail.vue
index c2c2559f63..429b3549b9 100644
--- a/src/web/app/desktop/views/components/post-detail.vue
+++ b/src/web/app/desktop/views/components/post-detail.vue
@@ -4,7 +4,7 @@
 		class="read-more"
 		v-if="p.reply && p.reply.reply_id && context == null"
 		title="会話をもっと読み込む"
-		@click="loadContext"
+		@click="fetchContext"
 		:disabled="contextFetching"
 	>
 		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
diff --git a/src/web/app/desktop/views/components/timeline.vue b/src/web/app/desktop/views/components/timeline.vue
index c638013380..3e06774753 100644
--- a/src/web/app/desktop/views/components/timeline.vue
+++ b/src/web/app/desktop/views/components/timeline.vue
@@ -57,8 +57,8 @@ export default Vue.extend({
 			(this as any).api('posts/timeline', {
 				until_date: this.date ? this.date.getTime() : undefined
 			}).then(posts => {
-				this.fetching = false;
 				this.posts = posts;
+				this.fetching = false;
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/desktop/views/components/users-list.vue b/src/web/app/desktop/views/components/users-list.vue
index 12abb372e7..b93a81630c 100644
--- a/src/web/app/desktop/views/components/users-list.vue
+++ b/src/web/app/desktop/views/components/users-list.vue
@@ -45,9 +45,9 @@ export default Vue.extend({
 		_fetch(cb) {
 			this.fetching = true;
 			this.fetch(this.mode == 'iknow', this.limit, null, obj => {
-				this.fetching = false;
 				this.users = obj.users;
 				this.next = obj.next;
+				this.fetching = false;
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/desktop/views/components/widgets/broadcast.vue b/src/web/app/desktop/views/components/widgets/broadcast.vue
index cdc65a2a7b..1a0fd9280c 100644
--- a/src/web/app/desktop/views/components/widgets/broadcast.vue
+++ b/src/web/app/desktop/views/components/widgets/broadcast.vue
@@ -46,8 +46,8 @@ export default define({
 					}
 				});
 			}
-			this.fetching = false;
 			this.broadcasts = broadcasts;
+			this.fetching = false;
 		});
 	},
 	methods: {
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 a3f37e8c7e..6ad7d2f064 100644
--- a/src/web/app/desktop/views/components/widgets/photo-stream.vue
+++ b/src/web/app/desktop/views/components/widgets/photo-stream.vue
@@ -35,8 +35,8 @@ export default define({
 			type: 'image/*',
 			limit: 9
 		}).then(images => {
-			this.fetching = false;
 			this.images = images;
+			this.fetching = false;
 		});
 	},
 	beforeDestroy() {
diff --git a/src/web/app/desktop/views/components/widgets/slideshow.vue b/src/web/app/desktop/views/components/widgets/slideshow.vue
index beda350666..3c2ef6da4f 100644
--- a/src/web/app/desktop/views/components/widgets/slideshow.vue
+++ b/src/web/app/desktop/views/components/widgets/slideshow.vue
@@ -93,8 +93,8 @@ export default define({
 				type: 'image/*',
 				limit: 100
 			}).then(images => {
-				this.fetching = false;
 				this.images = images;
+				this.fetching = false;
 				(this.$refs.slideA as any).style.backgroundImage = '';
 				(this.$refs.slideB as any).style.backgroundImage = '';
 				this.change();
diff --git a/src/web/app/desktop/views/pages/messaging-room.vue b/src/web/app/desktop/views/pages/messaging-room.vue
index 3e4fb256ac..ace9e1607b 100644
--- a/src/web/app/desktop/views/pages/messaging-room.vue
+++ b/src/web/app/desktop/views/pages/messaging-room.vue
@@ -24,8 +24,8 @@ export default Vue.extend({
 		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
-			this.fetching = false;
 			this.user = user;
+			this.fetching = false;
 
 			document.title = 'メッセージ: ' + this.user.name;
 
diff --git a/src/web/app/desktop/views/pages/post.vue b/src/web/app/desktop/views/pages/post.vue
index 186ee332f6..8b9f30f108 100644
--- a/src/web/app/desktop/views/pages/post.vue
+++ b/src/web/app/desktop/views/pages/post.vue
@@ -26,8 +26,8 @@ export default Vue.extend({
 		(this as any).api('posts/show', {
 			post_id: this.postId
 		}).then(post => {
-			this.fetching = false;
 			this.post = post;
+			this.fetching = false;
 
 			Progress.done();
 		});
diff --git a/src/web/app/desktop/views/pages/search.vue b/src/web/app/desktop/views/pages/search.vue
index 828aac8fe1..b8e8db2e79 100644
--- a/src/web/app/desktop/views/pages/search.vue
+++ b/src/web/app/desktop/views/pages/search.vue
@@ -45,8 +45,8 @@ export default Vue.extend({
 		window.addEventListener('scroll', this.onScroll);
 
 		(this as any).api('posts/search', parse(this.query)).then(posts => {
-			this.fetching = false;
 			this.posts = posts;
+			this.fetching = false;
 		});
 	},
 	beforeDestroy() {
diff --git a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
index 246ff865d1..c58eb75bcf 100644
--- a/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
+++ b/src/web/app/desktop/views/pages/user/user-followers-you-know.vue
@@ -27,8 +27,8 @@ export default Vue.extend({
 			iknow: true,
 			limit: 16
 		}).then(x => {
-			this.fetching = false;
 			this.users = x.users;
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/desktop/views/pages/user/user-friends.vue b/src/web/app/desktop/views/pages/user/user-friends.vue
index 9f324cfc08..a144ca2ad1 100644
--- a/src/web/app/desktop/views/pages/user/user-friends.vue
+++ b/src/web/app/desktop/views/pages/user/user-friends.vue
@@ -2,16 +2,18 @@
 <div class="mk-user-friends">
 	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.frequently-replied-users.title%</p>
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
-	<div class="user" v-if="!fetching && users.length != 0" each={ _user in users }>
-		<a class="avatar-anchor" href={ '/' + _user.username }>
-			<img class="avatar" src={ _user.avatar_url + '?thumbnail&size=42' } alt="" v-user-preview={ _user.id }/>
-		</a>
-		<div class="body">
-			<a class="name" href={ '/' + _user.username } v-user-preview={ _user.id }>{ _user.name }</a>
-			<p class="username">@{ _user.username }</p>
+	<template v-if="!fetching && users.length != 0">
+		<div class="user" v-for="friend in users">
+			<router-link class="avatar-anchor" to="`/${friend.username}`">
+				<img class="avatar" :src="`${friend.avatar_url}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
+			</router-link>
+			<div class="body">
+				<router-link class="name" to="`/${friend.username}`" v-user-preview="friend.id">{{ friend.name }}</router-link>
+				<p class="username">@{{ friend.username }}</p>
+			</div>
+			<mk-follow-button :user="friend"/>
 		</div>
-		<mk-follow-button user={ _user }/>
-	</div>
+	</template>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.frequently-replied-users.no-users%</p>
 </div>
 </template>
@@ -31,8 +33,8 @@ export default Vue.extend({
 			user_id: this.user.id,
 			limit: 4
 		}).then(docs => {
-			this.fetching = false;
 			this.users = docs.map(doc => doc.user);
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/desktop/views/pages/user/user-home.vue b/src/web/app/desktop/views/pages/user/user-home.vue
index ca2c68840c..5ed901579e 100644
--- a/src/web/app/desktop/views/pages/user/user-home.vue
+++ b/src/web/app/desktop/views/pages/user/user-home.vue
@@ -14,8 +14,8 @@
 	</main>
 	<div>
 		<div ref="right">
-			<mk-calendar-widget @warp="warp" :start="new Date(user.created_at)"/>
-			<mk-activity-widget :user="user"/>
+			<mk-calendar @chosen="warp" :start="new Date(user.created_at)"/>
+			<mk-activity :user="user"/>
 			<mk-user-friends :user="user"/>
 			<div class="nav"><mk-nav/></div>
 		</div>
@@ -25,7 +25,20 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkUserTimeline from './user-timeline.vue';
+import MkUserProfile from './user-profile.vue';
+import MkUserPhotos from './user-photos.vue';
+import MkUserFollowersYouKnow from './user-followers-you-know.vue';
+import MkUserFriends from './user-friends.vue';
+
 export default Vue.extend({
+	components: {
+		'mk-user-timeline': MkUserTimeline,
+		'mk-user-profile': MkUserProfile,
+		'mk-user-photos': MkUserPhotos,
+		'mk-user-followers-you-know': MkUserFollowersYouKnow,
+		'mk-user-friends': MkUserFriends
+	},
 	props: ['user'],
 	methods: {
 		warp(date) {
diff --git a/src/web/app/desktop/views/pages/user/user-photos.vue b/src/web/app/desktop/views/pages/user/user-photos.vue
index 789d9af85f..4029a95cc8 100644
--- a/src/web/app/desktop/views/pages/user/user-photos.vue
+++ b/src/web/app/desktop/views/pages/user/user-photos.vue
@@ -3,8 +3,7 @@
 	<p class="title">%fa:camera%%i18n:desktop.tags.mk-user.photos.title%</p>
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.photos.loading%<mk-ellipsis/></p>
 	<div class="stream" v-if="!fetching && images.length > 0">
-		<div v-for="image in images" :key="image.id"
-			class="img"
+		<div v-for="image in images" class="img"
 			:style="`background-image: url(${image.url}?thumbnail&size=256)`"
 		></div>
 	</div>
@@ -28,12 +27,12 @@ export default Vue.extend({
 			with_media: true,
 			limit: 9
 		}).then(posts => {
-			this.fetching = false;
 			posts.forEach(post => {
 				post.media.forEach(media => {
 					if (this.images.length < 9) this.images.push(media);
 				});
 			});
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/desktop/views/pages/user/user-profile.vue b/src/web/app/desktop/views/pages/user/user-profile.vue
index d389e01c19..32c28595e9 100644
--- a/src/web/app/desktop/views/pages/user/user-profile.vue
+++ b/src/web/app/desktop/views/pages/user/user-profile.vue
@@ -14,7 +14,7 @@
 		<p>%fa:B twitter%<a :href="`https://twitter.com/${user.twitter.screen_name}`" target="_blank">@{{ user.twitter.screen_name }}</a></p>
 	</div>
 	<div class="status">
-	  <p class="posts-count">%fa:angle-right%<a>{{ user.posts_count }}</a><b>投稿</b></p>
+		<p class="posts-count">%fa:angle-right%<a>{{ user.posts_count }}</a><b>投稿</b></p>
 		<p class="following">%fa:angle-right%<a @click="showFollowing">{{ user.following_count }}</a>人を<b>フォロー</b></p>
 		<p class="followers">%fa:angle-right%<a @click="showFollowers">{{ user.followers_count }}</a>人の<b>フォロワー</b></p>
 	</div>
@@ -23,7 +23,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-const age = require('s-age');
+import age from 's-age';
+import MkFollowingWindow from '../../components/following-window.vue';
+import MkFollowersWindow from '../../components/followers-window.vue';
 
 export default Vue.extend({
 	props: ['user'],
@@ -34,8 +36,7 @@ export default Vue.extend({
 	},
 	methods: {
 		showFollowing() {
-			document.body.appendChild(new MkUserFollowingWindow({
-
+			document.body.appendChild(new MkFollowingWindow({
 				propsData: {
 					user: this.user
 				}
@@ -43,8 +44,7 @@ export default Vue.extend({
 		},
 
 		showFollowers() {
-			document.body.appendChild(new MkUserFollowersWindow({
-
+			document.body.appendChild(new MkFollowersWindow({
 				propsData: {
 					user: this.user
 				}
@@ -56,7 +56,7 @@ export default Vue.extend({
 				user_id: this.user.id
 			}).then(() => {
 				this.user.is_muted = true;
-			}, e => {
+			}, () => {
 				alert('error');
 			});
 		},
@@ -66,7 +66,7 @@ export default Vue.extend({
 				user_id: this.user.id
 			}).then(() => {
 				this.user.is_muted = false;
-			}, e => {
+			}, () => {
 				alert('error');
 			});
 		}
diff --git a/src/web/app/desktop/views/components/user-timeline.vue b/src/web/app/desktop/views/pages/user/user-timeline.vue
similarity index 100%
rename from src/web/app/desktop/views/components/user-timeline.vue
rename to src/web/app/desktop/views/pages/user/user-timeline.vue
index fa5b32f225..9dd07653c4 100644
--- a/src/web/app/desktop/views/components/user-timeline.vue
+++ b/src/web/app/desktop/views/pages/user/user-timeline.vue
@@ -65,8 +65,8 @@ export default Vue.extend({
 				until_date: this.date ? this.date.getTime() : undefined,
 				with_replies: this.mode == 'with-replies'
 			}).then(posts => {
-				this.fetching = false;
 				this.posts = posts;
+				this.fetching = false;
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/desktop/views/pages/user/user.vue b/src/web/app/desktop/views/pages/user/user.vue
index 765057e651..def9ced362 100644
--- a/src/web/app/desktop/views/pages/user/user.vue
+++ b/src/web/app/desktop/views/pages/user/user.vue
@@ -35,8 +35,8 @@ export default Vue.extend({
 		(this as any).api('users/show', {
 			username: this.$route.params.user
 		}).then(user => {
-			this.fetching = false;
 			this.user = user;
+			this.fetching = false;
 			Progress.done();
 			document.title = user.name + ' | Misskey';
 		});
diff --git a/src/web/app/mobile/views/components/drive.vue b/src/web/app/mobile/views/components/drive.vue
index e581d3f053..0e54563323 100644
--- a/src/web/app/mobile/views/components/drive.vue
+++ b/src/web/app/mobile/views/components/drive.vue
@@ -351,13 +351,14 @@ export default Vue.extend({
 			(this as any).api('drive/files/show', {
 				file_id: file
 			}).then(file => {
-				this.fetching = false;
 				this.file = file;
 				this.folder = null;
 				this.hierarchyFolders = [];
 
 				if (file.folder) this.dive(file.folder);
 
+				this.fetching = false;
+
 				this.$emit('open-file', this.file, silent);
 			});
 		},
diff --git a/src/web/app/mobile/views/components/friends-maker.vue b/src/web/app/mobile/views/components/friends-maker.vue
index b069b988cc..8e7bf2d632 100644
--- a/src/web/app/mobile/views/components/friends-maker.vue
+++ b/src/web/app/mobile/views/components/friends-maker.vue
@@ -36,8 +36,8 @@ export default Vue.extend({
 				limit: this.limit,
 				offset: this.limit * this.page
 			}).then(users => {
-				this.fetching = false;
 				this.users = users;
+				this.fetching = false;
 			});
 		},
 		refresh() {
diff --git a/src/web/app/mobile/views/components/timeline.vue b/src/web/app/mobile/views/components/timeline.vue
index a04780e94d..80fda75605 100644
--- a/src/web/app/mobile/views/components/timeline.vue
+++ b/src/web/app/mobile/views/components/timeline.vue
@@ -63,8 +63,8 @@ export default Vue.extend({
 			(this as any).api('posts/timeline', {
 				until_date: this.date ? (this.date as any).getTime() : undefined
 			}).then(posts => {
-				this.fetching = false;
 				this.posts = posts;
+				this.fetching = false;
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/mobile/views/components/user-timeline.vue b/src/web/app/mobile/views/components/user-timeline.vue
index fb2a214198..ffd6288381 100644
--- a/src/web/app/mobile/views/components/user-timeline.vue
+++ b/src/web/app/mobile/views/components/user-timeline.vue
@@ -31,8 +31,8 @@ export default Vue.extend({
 			user_id: this.user.id,
 			with_media: this.withMedia
 		}).then(posts => {
-			this.fetching = false;
 			this.posts = posts;
+			this.fetching = false;
 			this.$emit('loaded');
 		});
 	}
diff --git a/src/web/app/mobile/views/components/users-list.vue b/src/web/app/mobile/views/components/users-list.vue
index 45629c5586..24c96aec79 100644
--- a/src/web/app/mobile/views/components/users-list.vue
+++ b/src/web/app/mobile/views/components/users-list.vue
@@ -41,9 +41,9 @@ export default Vue.extend({
 		_fetch(cb) {
 			this.fetching = true;
 			this.fetch(this.mode == 'iknow', this.limit, null, obj => {
-				this.fetching = false;
 				this.users = obj.users;
 				this.next = obj.next;
+				this.fetching = false;
 				if (cb) cb();
 			});
 		},
diff --git a/src/web/app/mobile/views/pages/followers.vue b/src/web/app/mobile/views/pages/followers.vue
index e9696dbd3c..2f102bd68f 100644
--- a/src/web/app/mobile/views/pages/followers.vue
+++ b/src/web/app/mobile/views/pages/followers.vue
@@ -26,8 +26,8 @@ export default Vue.extend({
 		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
-			this.fetching = false;
 			this.user = user;
+			this.fetching = false;
 
 			document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
 			document.documentElement.style.background = '#313a42';
diff --git a/src/web/app/mobile/views/pages/following.vue b/src/web/app/mobile/views/pages/following.vue
index c278abfd25..20f085a9f1 100644
--- a/src/web/app/mobile/views/pages/following.vue
+++ b/src/web/app/mobile/views/pages/following.vue
@@ -26,8 +26,8 @@ export default Vue.extend({
 		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
-			this.fetching = false;
 			this.user = user;
+			this.fetching = false;
 
 			document.title = '%i18n:mobile.tags.mk-user-followers-page.followers-of%'.replace('{}', user.name) + ' | Misskey';
 			document.documentElement.style.background = '#313a42';
diff --git a/src/web/app/mobile/views/pages/post.vue b/src/web/app/mobile/views/pages/post.vue
index c5b6750afa..03e9972a44 100644
--- a/src/web/app/mobile/views/pages/post.vue
+++ b/src/web/app/mobile/views/pages/post.vue
@@ -32,8 +32,8 @@ export default Vue.extend({
 		(this as any).api('posts/show', {
 			post_id: this.postId
 		}).then(post => {
-			this.fetching = false;
 			this.post = post;
+			this.fetching = false;
 
 			Progress.done();
 		});
diff --git a/src/web/app/mobile/views/pages/user.vue b/src/web/app/mobile/views/pages/user.vue
index f5babbd67f..53cde1fb68 100644
--- a/src/web/app/mobile/views/pages/user.vue
+++ b/src/web/app/mobile/views/pages/user.vue
@@ -88,8 +88,8 @@ export default Vue.extend({
 		(this as any).api('users/show', {
 			username: this.username
 		}).then(user => {
-			this.fetching = false;
 			this.user = user;
+			this.fetching = false;
 
 			Progress.done();
 			document.title = user.name + ' | Misskey';
diff --git a/src/web/app/mobile/views/pages/user/home-friends.vue b/src/web/app/mobile/views/pages/user/home-friends.vue
index 4f2f12a642..543ed9b30d 100644
--- a/src/web/app/mobile/views/pages/user/home-friends.vue
+++ b/src/web/app/mobile/views/pages/user/home-friends.vue
@@ -22,8 +22,8 @@ export default Vue.extend({
 		(this as any).api('users/get_frequently_replied_users', {
 			user_id: this.user.id
 		}).then(res => {
-			this.fetching = false;
 			this.users = res.map(x => x.user);
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/mobile/views/pages/user/home-photos.vue b/src/web/app/mobile/views/pages/user/home-photos.vue
index eb53eb89a9..dbb2a410aa 100644
--- a/src/web/app/mobile/views/pages/user/home-photos.vue
+++ b/src/web/app/mobile/views/pages/user/home-photos.vue
@@ -28,7 +28,6 @@ export default Vue.extend({
 			with_media: true,
 			limit: 6
 		}).then(posts => {
-			this.fetching = false;
 			posts.forEach(post => {
 				post.media.forEach(media => {
 					if (this.images.length < 9) this.images.push({
@@ -37,6 +36,7 @@ export default Vue.extend({
 					});
 				});
 			});
+			this.fetching = false;
 		});
 	}
 });
diff --git a/src/web/app/mobile/views/pages/user/home-posts.vue b/src/web/app/mobile/views/pages/user/home-posts.vue
index c60f114b88..8b1ea2de54 100644
--- a/src/web/app/mobile/views/pages/user/home-posts.vue
+++ b/src/web/app/mobile/views/pages/user/home-posts.vue
@@ -22,8 +22,8 @@ export default Vue.extend({
 		(this as any).api('users/posts', {
 			user_id: this.user.id
 		}).then(posts => {
-			this.fetching = false;
 			this.posts = posts;
+			this.fetching = false;
 		});
 	}
 });