From 98fe9c39ebd23bc6359960cf56e51604050f5b6c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 9 Apr 2018 18:52:29 +0900
Subject: [PATCH] Refactor

---
 .../common/views/components/autocomplete.vue  |  11 +-
 .../app/common/views/components/messaging.vue |  12 +-
 .../views/components/welcome-timeline.vue     |  10 +-
 src/client/app/common/views/filters/index.ts  |   2 +
 src/client/app/common/views/filters/note.ts   |   5 +
 src/client/app/common/views/filters/user.ts   |  15 +
 .../views/components/followers-window.vue     |  10 +-
 .../views/components/following-window.vue     |  10 +-
 .../views/components/friends-maker.vue        |  10 +-
 .../components/messaging-room-window.vue      |   6 +-
 .../views/components/note-detail.sub.vue      |  16 +-
 .../desktop/views/components/note-detail.vue  |  26 +-
 .../desktop/views/components/note-preview.vue |  16 +-
 .../views/components/notes.note.sub.vue       |  16 +-
 .../desktop/views/components/notes.note.vue   |  21 +-
 .../views/components/notifications.vue        |  32 +-
 .../views/components/post-detail.sub.vue      | 122 ++++
 .../desktop/views/components/post-detail.vue  | 434 +++++++++++++
 .../desktop/views/components/post-preview.vue |  99 +++
 .../views/components/posts.post.sub.vue       | 108 ++++
 .../desktop/views/components/posts.post.vue   | 585 ++++++++++++++++++
 .../views/components/settings.mute.vue        |   8 +-
 .../desktop/views/components/ui.header.vue    |   9 +-
 .../desktop/views/components/user-preview.vue |   6 +-
 .../views/components/users-list.item.vue      |  18 +-
 .../pages/user/user.followers-you-know.vue    |  10 +-
 .../desktop/views/pages/user/user.friends.vue |   6 +-
 .../desktop/views/pages/user/user.header.vue  |  14 +-
 .../app/desktop/views/pages/user/user.vue     |   2 +-
 .../app/desktop/views/pages/welcome.vue       |   2 +-
 .../views/widgets/channel.channel.note.vue    |  14 +-
 .../views/widgets/channel.channel.post.vue    |  65 ++
 .../app/desktop/views/widgets/profile.vue     |  10 +-
 .../app/desktop/views/widgets/users.vue       |  10 +-
 .../app/mobile/views/components/note-card.vue |  12 +-
 .../views/components/note-detail.sub.vue      |  20 +-
 .../mobile/views/components/note-detail.vue   |  28 +-
 .../mobile/views/components/note-preview.vue  |  20 +-
 .../app/mobile/views/components/note.sub.vue  |  18 +-
 .../app/mobile/views/components/note.vue      |  21 +-
 .../views/components/notification-preview.vue |  18 +-
 .../mobile/views/components/notification.vue  |  22 +-
 .../app/mobile/views/components/post-card.vue |  85 +++
 .../views/components/post-detail.sub.vue      | 103 +++
 .../mobile/views/components/post-detail.vue   | 444 +++++++++++++
 .../mobile/views/components/post-preview.vue  | 100 +++
 .../app/mobile/views/components/post.vue      | 523 ++++++++++++++++
 .../app/mobile/views/components/ui.header.vue |   8 +-
 .../app/mobile/views/components/ui.nav.vue    |  13 +-
 .../app/mobile/views/components/user-card.vue |  18 +-
 .../mobile/views/components/user-preview.vue  |  18 +-
 .../app/mobile/views/pages/following.vue      |   5 +-
 .../app/mobile/views/pages/messaging-room.vue |  10 +-
 .../app/mobile/views/pages/settings.vue       |   5 +-
 src/client/app/mobile/views/pages/user.vue    |  14 +-
 .../pages/user/home.followers-you-know.vue    |  14 +-
 .../app/mobile/views/widgets/profile.vue      |   9 +-
 57 files changed, 2846 insertions(+), 422 deletions(-)
 create mode 100644 src/client/app/common/views/filters/note.ts
 create mode 100644 src/client/app/common/views/filters/user.ts
 create mode 100644 src/client/app/desktop/views/components/post-detail.sub.vue
 create mode 100644 src/client/app/desktop/views/components/post-detail.vue
 create mode 100644 src/client/app/desktop/views/components/post-preview.vue
 create mode 100644 src/client/app/desktop/views/components/posts.post.sub.vue
 create mode 100644 src/client/app/desktop/views/components/posts.post.vue
 create mode 100644 src/client/app/desktop/views/widgets/channel.channel.post.vue
 create mode 100644 src/client/app/mobile/views/components/post-card.vue
 create mode 100644 src/client/app/mobile/views/components/post-detail.sub.vue
 create mode 100644 src/client/app/mobile/views/components/post-detail.vue
 create mode 100644 src/client/app/mobile/views/components/post-preview.vue
 create mode 100644 src/client/app/mobile/views/components/post.vue

diff --git a/src/client/app/common/views/components/autocomplete.vue b/src/client/app/common/views/components/autocomplete.vue
index 8837fde6b..5c8f61a2a 100644
--- a/src/client/app/common/views/components/autocomplete.vue
+++ b/src/client/app/common/views/components/autocomplete.vue
@@ -3,8 +3,8 @@
 	<ol class="users" ref="suggests" v-if="users.length > 0">
 		<li v-for="user in users" @click="complete(type, user)" @keydown="onKeydown" tabindex="-1">
 			<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
-			<span class="name">{{ getUserName(user) }}</span>
-			<span class="username">@{{ getAcct(user) }}</span>
+			<span class="name">{{ user | userName }}</span>
+			<span class="username">@{{ user | acct }}</span>
 		</li>
 	</ol>
 	<ol class="emojis" ref="suggests" v-if="emojis.length > 0">
@@ -21,17 +21,17 @@
 import Vue from 'vue';
 import * as emojilib from 'emojilib';
 import contains from '../../../common/scripts/contains';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 const lib = Object.entries(emojilib.lib).filter((x: any) => {
 	return x[1].category != 'flags';
 });
+
 const emjdb = lib.map((x: any) => ({
 	emoji: x[1].char,
 	name: x[0],
 	alias: null
 }));
+
 lib.forEach((x: any) => {
 	if (x[1].keywords) {
 		x[1].keywords.forEach(k => {
@@ -43,6 +43,7 @@ lib.forEach((x: any) => {
 		});
 	}
 });
+
 emjdb.sort((a, b) => a.name.length - b.name.length);
 
 export default Vue.extend({
@@ -107,8 +108,6 @@ export default Vue.extend({
 		});
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		exec() {
 			this.select = -1;
 			if (this.$refs.suggests) {
diff --git a/src/client/app/common/views/components/messaging.vue b/src/client/app/common/views/components/messaging.vue
index 9b1449daa..751e4de50 100644
--- a/src/client/app/common/views/components/messaging.vue
+++ b/src/client/app/common/views/components/messaging.vue
@@ -14,8 +14,8 @@
 					tabindex="-1"
 				>
 					<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=32`" alt=""/>
-					<span class="name">{{ getUserName(user) }}</span>
-					<span class="username">@{{ getAcct(user) }}</span>
+					<span class="name">{{ user | userName }}</span>
+					<span class="username">@{{ user | acct }}</span>
 				</li>
 			</ol>
 		</div>
@@ -33,8 +33,8 @@
 				<div>
 					<img class="avatar" :src="`${isMe(message) ? message.recipient.avatarUrl : message.user.avatarUrl}?thumbnail&size=64`" alt=""/>
 					<header>
-						<span class="name">{{ getUserName(isMe(message) ? message.recipient : message.user) }}</span>
-						<span class="username">@{{ getAcct(isMe(message) ? message.recipient : message.user) }}</span>
+						<span class="name">{{ isMe(message) ? message.recipient : message.use | userName }}</span>
+						<span class="username">@{{ isMe(message) ? message.recipient : message.user | acct }}</span>
 						<mk-time :time="message.createdAt"/>
 					</header>
 					<div class="body">
@@ -51,8 +51,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: {
@@ -94,8 +92,6 @@ export default Vue.extend({
 		(this as any).os.streams.messagingIndexStream.dispose(this.connectionId);
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		isMe(message) {
 			return message.userId == (this as any).os.i.id;
 		},
diff --git a/src/client/app/common/views/components/welcome-timeline.vue b/src/client/app/common/views/components/welcome-timeline.vue
index 61616da14..7571cfc5f 100644
--- a/src/client/app/common/views/components/welcome-timeline.vue
+++ b/src/client/app/common/views/components/welcome-timeline.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-welcome-timeline">
 	<div v-for="note in notes">
-		<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`" v-user-preview="note.user.id">
+		<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.user.id">
 			<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="body">
 			<header>
-				<router-link class="name" :to="`/@${getAcct(note.user)}`" v-user-preview="note.user.id">{{ getUserName(note.user) }}</router-link>
-				<span class="username">@{{ getAcct(note.user) }}</span>
+				<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link>
+				<span class="username">@{{ note.user | acct }}</span>
 				<div class="info">
 					<router-link class="created-at" :to="`/@${getAcct(note.user)}/${note.id}`">
 						<mk-time :time="note.createdAt"/>
@@ -24,8 +24,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -38,8 +36,6 @@ export default Vue.extend({
 		this.fetch();
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		fetch(cb?) {
 			this.fetching = true;
 			(this as any).api('notes', {
diff --git a/src/client/app/common/views/filters/index.ts b/src/client/app/common/views/filters/index.ts
index 3a1d1ac23..1759c19c2 100644
--- a/src/client/app/common/views/filters/index.ts
+++ b/src/client/app/common/views/filters/index.ts
@@ -1,2 +1,4 @@
 require('./bytes');
 require('./number');
+require('./user');
+require('./note');
diff --git a/src/client/app/common/views/filters/note.ts b/src/client/app/common/views/filters/note.ts
new file mode 100644
index 000000000..a611dc868
--- /dev/null
+++ b/src/client/app/common/views/filters/note.ts
@@ -0,0 +1,5 @@
+import Vue from 'vue';
+
+Vue.filter('notePage', note => {
+	return '/notes/' + note.id;
+});
diff --git a/src/client/app/common/views/filters/user.ts b/src/client/app/common/views/filters/user.ts
new file mode 100644
index 000000000..167bb7758
--- /dev/null
+++ b/src/client/app/common/views/filters/user.ts
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import getAcct from '../../../../../acct/render';
+import getUserName from '../../../../../renderers/get-user-name';
+
+Vue.filter('acct', user => {
+	return getAcct(user);
+});
+
+Vue.filter('userName', user => {
+	return getUserName(user);
+});
+
+Vue.filter('userPage', user => {
+	return '/@' + Vue.filter('acct')(user);
+});
diff --git a/src/client/app/desktop/views/components/followers-window.vue b/src/client/app/desktop/views/components/followers-window.vue
index d37ca745a..16206299d 100644
--- a/src/client/app/desktop/views/components/followers-window.vue
+++ b/src/client/app/desktop/views/components/followers-window.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロワー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user | userName }}のフォロワー
 	</span>
 	<mk-followers :user="user"/>
 </mk-window>
@@ -9,15 +9,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['user'],
-	computed {
-		name() {
-			return getUserName(this.user);
-		}
-	}
+	props: ['user']
 });
 </script>
 
diff --git a/src/client/app/desktop/views/components/following-window.vue b/src/client/app/desktop/views/components/following-window.vue
index cbd8ec5f9..cc3d77198 100644
--- a/src/client/app/desktop/views/components/following-window.vue
+++ b/src/client/app/desktop/views/components/following-window.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-window width="400px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ name }}のフォロー
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" alt=""/>{{ user | userName }}のフォロー
 	</span>
 	<mk-following :user="user"/>
 </mk-window>
@@ -9,15 +9,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['user'],
-	computed: {
-		name() {
-			return getUserName(this.user);
-		}
-	}
+	props: ['user']
 });
 </script>
 
diff --git a/src/client/app/desktop/views/components/friends-maker.vue b/src/client/app/desktop/views/components/friends-maker.vue
index acc4542d9..af5bde3ad 100644
--- a/src/client/app/desktop/views/components/friends-maker.vue
+++ b/src/client/app/desktop/views/components/friends-maker.vue
@@ -3,12 +3,12 @@
 	<p class="title">気になるユーザーをフォロー:</p>
 	<div class="users" v-if="!fetching && users.length > 0">
 		<div class="user" v-for="user in users" :key="user.id">
-			<router-link class="avatar-anchor" :to="`/@${getAcct(user)}`">
+			<router-link class="avatar-anchor" :to="user | userPage">
 				<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="user.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${getAcct(user)}`" v-user-preview="user.id">{{ getUserName(user) }}</router-link>
-				<p class="username">@{{ getAcct(user) }}</p>
+				<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
+				<p class="username">@{{ user | acct }}</p>
 			</div>
 			<mk-follow-button :user="user"/>
 		</div>
@@ -22,8 +22,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -38,8 +36,6 @@ export default Vue.extend({
 		this.fetch();
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		fetch() {
 			this.fetching = true;
 			this.users = [];
diff --git a/src/client/app/desktop/views/components/messaging-room-window.vue b/src/client/app/desktop/views/components/messaging-room-window.vue
index 7f8c35c2f..dbe326673 100644
--- a/src/client/app/desktop/views/components/messaging-room-window.vue
+++ b/src/client/app/desktop/views/components/messaging-room-window.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-window ref="window" width="500px" height="560px" :popout-url="popout" @closed="$destroy">
-	<span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ name }}</span>
+	<span slot="header" :class="$style.header">%fa:comments%メッセージ: {{ user | userName }}</span>
 	<mk-messaging-room :user="user" :class="$style.content"/>
 </mk-window>
 </template>
@@ -9,14 +9,10 @@
 import Vue from 'vue';
 import { url } from '../../../config';
 import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
 	computed: {
-		name(): string {
-			return getUserName(this.user);
-		},
 		popout(): string {
 			return `${url}/i/messaging/${getAcct(this.user)}`;
 		}
diff --git a/src/client/app/desktop/views/components/note-detail.sub.vue b/src/client/app/desktop/views/components/note-detail.sub.vue
index 79f5de1f8..16bc2a1d9 100644
--- a/src/client/app/desktop/views/components/note-detail.sub.vue
+++ b/src/client/app/desktop/views/components/note-detail.sub.vue
@@ -1,16 +1,16 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
 			<div class="left">
-				<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
-				<span class="username">@{{ acct }}</span>
+				<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+				<span class="username">@{{ note.user | acct }}</span>
 			</div>
 			<div class="right">
-				<router-link class="time" :to="`/@${acct}/${note.id}`">
+				<router-link class="time" :to="note | notePage">
 					<mk-time :time="note.createdAt"/>
 				</router-link>
 			</div>
@@ -28,18 +28,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
 	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		},
 		title(): string {
 			return dateStringify(this.note.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/note-detail.vue b/src/client/app/desktop/views/components/note-detail.vue
index eead82dd0..50bbb7698 100644
--- a/src/client/app/desktop/views/components/note-detail.vue
+++ b/src/client/app/desktop/views/components/note-detail.vue
@@ -18,22 +18,22 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="note.userId">
+			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
-			<router-link class="name" :href="`/@${acct}`">{{ name }}</router-link>
+			<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
 			がRenote
 		</p>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${pAcct}`">
+		<router-link class="avatar-anchor" :to="p.user | userPage">
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<header>
-			<router-link class="name" :to="`/@${pAcct}`" v-user-preview="p.user.id">{{ name }}</router-link>
-			<span class="username">@{{ pAcct }}</span>
-			<router-link class="time" :to="`/@${pAcct}/${p.id}`">
+			<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
+			<span class="username">@{{ p.user | acct }}</span>
+			<router-link class="time" :to="p | notePage">
 				<mk-time :time="p.createdAt"/>
 			</router-link>
 		</header>
@@ -78,8 +78,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
@@ -131,18 +129,6 @@ export default Vue.extend({
 		title(): string {
 			return dateStringify(this.p.createdAt);
 		},
-		acct(): string {
-			return getAcct(this.note.user);
-		},
-		name(): string {
-			return getUserName(this.note.user);
-		},
-		pAcct(): string {
-			return getAcct(this.p.user);
-		},
-		pName(): string {
-			return getUserName(this.p.user);
-		},
 		urls(): string[] {
 			if (this.p.text) {
 				const ast = parse(this.p.text);
diff --git a/src/client/app/desktop/views/components/note-preview.vue b/src/client/app/desktop/views/components/note-preview.vue
index bff199c09..ff3ecadc2 100644
--- a/src/client/app/desktop/views/components/note-preview.vue
+++ b/src/client/app/desktop/views/components/note-preview.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-note-preview" :title="title">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
-			<span class="username">@{{ acct }}</span>
-			<router-link class="time" :to="`/@${acct}/${note.id}`">
+			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -21,18 +21,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
 	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		},
 		title(): string {
 			return dateStringify(this.note.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/notes.note.sub.vue b/src/client/app/desktop/views/components/notes.note.sub.vue
index b49d12b92..e85478578 100644
--- a/src/client/app/desktop/views/components/notes.note.sub.vue
+++ b/src/client/app/desktop/views/components/notes.note.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="sub" :title="title">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="note.userId">{{ name }}</router-link>
-			<span class="username">@{{ acct }}</span>
-			<router-link class="created-at" :to="`/@${acct}/${note.id}`">
+			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="created-at" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -21,18 +21,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
 	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name(): string {
-			return getUserName(this.note.user);
-		},
 		title(): string {
 			return dateStringify(this.note.createdAt);
 		}
diff --git a/src/client/app/desktop/views/components/notes.note.vue b/src/client/app/desktop/views/components/notes.note.vue
index 0712069e5..8561643c9 100644
--- a/src/client/app/desktop/views/components/notes.note.vue
+++ b/src/client/app/desktop/views/components/notes.note.vue
@@ -5,29 +5,29 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`" v-user-preview="note.userId">
+			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
-			<a class="name" :href="`/@${getAcct(note.user)}`" v-user-preview="note.userId">{{ getUserName(note.user) }}</a>
+			<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
 			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(p.user)}`">
+		<router-link class="avatar-anchor" :to="p.user | userPage">
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/@${getAcct(p.user)}`" v-user-preview="p.user.id">{{ getUserName(p.user) }}</router-link>
+				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
 				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
-				<span class="username">@{{ getAcct(p.user) }}</span>
+				<span class="username">@{{ p.user | acct }}</span>
 				<div class="info">
 					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
 					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
-					<router-link class="created-at" :to="url">
+					<router-link class="created-at" :to="p | notePage">
 						<mk-time :time="p.createdAt"/>
 					</router-link>
 				</div>
@@ -85,8 +85,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import dateStringify from '../../../common/scripts/date-stringify';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkPostFormWindow from './post-form-window.vue';
@@ -117,9 +115,7 @@ export default Vue.extend({
 		return {
 			isDetailOpened: false,
 			connection: null,
-			connectionId: null,
-			getAcct,
-			getUserName
+			connectionId: null
 		};
 	},
 
@@ -144,9 +140,6 @@ export default Vue.extend({
 		title(): string {
 			return dateStringify(this.p.createdAt);
 		},
-		url(): string {
-			return `/@${this.acct}/${this.p.id}`;
-		},
 		urls(): string[] {
 			if (this.p.text) {
 				const ast = parse(this.p.text);
diff --git a/src/client/app/desktop/views/components/notifications.vue b/src/client/app/desktop/views/components/notifications.vue
index 100a803cc..8b17c8c43 100644
--- a/src/client/app/desktop/views/components/notifications.vue
+++ b/src/client/app/desktop/views/components/notifications.vue
@@ -5,13 +5,13 @@
 			<div class="notification" :class="notification.type" :key="notification.id">
 				<mk-time :time="notification.createdAt"/>
 				<template v-if="notification.type == 'reaction'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>
 							<mk-reaction-icon :reaction="notification.reaction"/>
-							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link>
+							<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
 						</p>
 						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 							%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
@@ -19,12 +19,12 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'renote'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+					<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
 						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:retweet%
-							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
+							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
 						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 							%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
@@ -32,54 +32,54 @@
 					</div>
 				</template>
 				<template v-if="notification.type == 'quote'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+					<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
 						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:quote-left%
-							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
+							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
 						<router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'follow'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:user-plus%
-							<router-link :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</router-link>
+							<router-link :to="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</router-link>
 						</p>
 					</div>
 				</template>
 				<template v-if="notification.type == 'reply'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+					<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
 						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:reply%
-							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
+							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
 						<router-link class="note-preview" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</router-link>
 					</div>
 				</template>
 				<template v-if="notification.type == 'mention'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">
+					<router-link class="avatar-anchor" :to="notification.note.user | userPage" v-user-preview="notification.note.userId">
 						<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
 						<p>%fa:at%
-							<router-link :to="`/@${getAcct(notification.note.user)}`" v-user-preview="notification.note.userId">{{ getUserName(notification.note.user) }}</router-link>
+							<router-link :to="notification.note.user | userPage" v-user-preview="notification.note.userId">{{ notification.note.user | userName }}</router-link>
 						</p>
 						<a class="note-preview" :href="`/@${getAcct(notification.note.user)}/${notification.note.id}`">{{ getNoteSummary(notification.note) }}</a>
 					</div>
 				</template>
 				<template v-if="notification.type == 'poll_vote'">
-					<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">
+					<router-link class="avatar-anchor" :to="notification.user | userPage" v-user-preview="notification.user.id">
 						<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=48`" alt="avatar"/>
 					</router-link>
 					<div class="text">
-						<p>%fa:chart-pie%<a :href="`/@${getAcct(notification.user)}`" v-user-preview="notification.user.id">{{ getUserName(notification.user) }}</a></p>
+						<p>%fa:chart-pie%<a :href="notification.user | userPage" v-user-preview="notification.user.id">{{ notification.user | userName }}</a></p>
 						<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 							%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
 						</router-link>
@@ -102,9 +102,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
 import getNoteSummary from '../../../../../renderers/get-note-summary';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -154,8 +152,6 @@ export default Vue.extend({
 		(this as any).os.stream.dispose(this.connectionId);
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		fetchMoreNotifications() {
 			this.fetchingMoreNotifications = true;
 
diff --git a/src/client/app/desktop/views/components/post-detail.sub.vue b/src/client/app/desktop/views/components/post-detail.sub.vue
new file mode 100644
index 000000000..16bc2a1d9
--- /dev/null
+++ b/src/client/app/desktop/views/components/post-detail.sub.vue
@@ -0,0 +1,122 @@
+<template>
+<div class="sub" :title="title">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<div class="left">
+				<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+				<span class="username">@{{ note.user | acct }}</span>
+			</div>
+			<div class="right">
+				<router-link class="time" :to="note | notePage">
+					<mk-time :time="note.createdAt"/>
+				</router-link>
+			</div>
+		</header>
+		<div class="body">
+			<mk-note-html v-if="note.text" :text="note.text" :i="os.i" :class="$style.text"/>
+			<div class="media" v-if="note.media > 0">
+				<mk-media-list :media-list="note.media"/>
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+
+export default Vue.extend({
+	props: ['note'],
+	computed: {
+		title(): string {
+			return dateStringify(this.note.createdAt);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.sub
+	margin 0
+	padding 20px 32px
+	background #fdfdfd
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	&:hover
+		> .main > footer > button
+			color #888
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 16px 0 0
+
+		> .avatar
+			display block
+			width 44px
+			height 44px
+			margin 0
+			border-radius 4px
+			vertical-align bottom
+
+	> .main
+		float left
+		width calc(100% - 60px)
+
+		> header
+			margin-bottom 4px
+			white-space nowrap
+
+			&:after
+				content ""
+				display block
+				clear both
+
+			> .left
+				float left
+
+				> .name
+					display inline
+					margin 0
+					padding 0
+					color #777
+					font-size 1em
+					font-weight 700
+					text-align left
+					text-decoration none
+
+					&:hover
+						text-decoration underline
+
+				> .username
+					text-align left
+					margin 0 0 0 8px
+					color #ccc
+
+			> .right
+				float right
+
+				> .time
+					font-size 0.9em
+					color #c0c0c0
+
+</style>
+
+<style lang="stylus" module>
+.text
+	cursor default
+	display block
+	margin 0
+	padding 0
+	overflow-wrap break-word
+	font-size 1em
+	color #717171
+</style>
diff --git a/src/client/app/desktop/views/components/post-detail.vue b/src/client/app/desktop/views/components/post-detail.vue
new file mode 100644
index 000000000..50bbb7698
--- /dev/null
+++ b/src/client/app/desktop/views/components/post-detail.vue
@@ -0,0 +1,434 @@
+<template>
+<div class="mk-note-detail" :title="title">
+	<button
+		class="read-more"
+		v-if="p.reply && p.reply.replyId && context == null"
+		title="会話をもっと読み込む"
+		@click="fetchContext"
+		:disabled="contextFetching"
+	>
+		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
+		<template v-if="contextFetching">%fa:spinner .pulse%</template>
+	</button>
+	<div class="context">
+		<x-sub v-for="note in context" :key="note.id" :note="note"/>
+	</div>
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<router-link class="name" :href="note.user | userPage">{{ note.user | userName }}</router-link>
+			がRenote
+		</p>
+	</div>
+	<article>
+		<router-link class="avatar-anchor" :to="p.user | userPage">
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
+		</router-link>
+		<header>
+			<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
+			<span class="username">@{{ p.user | acct }}</span>
+			<router-link class="time" :to="p | notePage">
+				<mk-time :time="p.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-note-html :class="$style.text" v-if="p.text" :text="p.text" :i="os.i"/>
+			<div class="media" v-if="p.media.length > 0">
+				<mk-media-list :media-list="p.media"/>
+			</div>
+			<mk-poll v-if="p.poll" :note="p"/>
+			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			<div class="tags" v-if="p.tags && p.tags.length > 0">
+				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+			</div>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<div class="map" v-if="p.geo" ref="map"></div>
+			<div class="renote" v-if="p.renote">
+				<mk-note-preview :note="p.renote"/>
+			</div>
+		</div>
+		<footer>
+			<mk-reactions-viewer :note="p"/>
+			<button @click="reply" title="返信">
+				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+			</button>
+			<button @click="renote" title="Renote">
+				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+			</button>
+			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="リアクション">
+				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
+			</button>
+			<button @click="menu" ref="menuButton">
+				%fa:ellipsis-h%
+			</button>
+		</footer>
+	</article>
+	<div class="replies" v-if="!compact">
+		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+import parse from '../../../../../text/parse';
+
+import MkPostFormWindow from './post-form-window.vue';
+import MkRenoteFormWindow from './renote-form-window.vue';
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './note-detail.sub.vue';
+
+export default Vue.extend({
+	components: {
+		XSub
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		compact: {
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			context: [],
+			contextFetching: false,
+			replies: []
+		};
+	},
+
+	computed: {
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds.length == 0 &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		title(): string {
+			return dateStringify(this.p.createdAt);
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+
+	mounted() {
+		// Get replies
+		if (!this.compact) {
+			(this as any).api('notes/replies', {
+				noteId: this.p.id,
+				limit: 8
+			}).then(replies => {
+				this.replies = replies;
+			});
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	methods: {
+		fetchContext() {
+			this.contextFetching = true;
+
+			// Fetch context
+			(this as any).api('notes/context', {
+				noteId: this.p.replyId
+			}).then(context => {
+				this.contextFetching = false;
+				this.context = context.reverse();
+			});
+		},
+		reply() {
+			(this as any).os.new(MkPostFormWindow, {
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).os.new(MkRenoteFormWindow, {
+				note: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.mk-note-detail
+	margin 0
+	padding 0
+	overflow hidden
+	text-align left
+	background #fff
+	border solid 1px rgba(0, 0, 0, 0.1)
+	border-radius 8px
+
+	> .read-more
+		display block
+		margin 0
+		padding 10px 0
+		width 100%
+		font-size 1em
+		text-align center
+		color #999
+		cursor pointer
+		background #fafafa
+		outline none
+		border none
+		border-bottom solid 1px #eef0f2
+		border-radius 6px 6px 0 0
+
+		&:hover
+			background #f6f6f6
+
+		&:active
+			background #f0f0f0
+
+		&:disabled
+			color #ccc
+
+	> .context
+		> *
+			border-bottom 1px solid #eef0f2
+
+	> .renote
+		color #9dbb00
+		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+
+		> p
+			margin 0
+			padding 16px 32px
+
+			.avatar-anchor
+				display inline-block
+
+				.avatar
+					vertical-align bottom
+					min-width 28px
+					min-height 28px
+					max-width 28px
+					max-height 28px
+					margin 0 8px 0 0
+					border-radius 6px
+
+			[data-fa]
+				margin-right 4px
+
+			.name
+				font-weight bold
+
+		& + article
+			padding-top 8px
+
+	> .reply-to
+		border-bottom 1px solid #eef0f2
+
+	> article
+		padding 28px 32px 18px 32px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		&:hover
+			> .main > footer > button
+				color #888
+
+		> .avatar-anchor
+			display block
+			width 60px
+			height 60px
+
+			> .avatar
+				display block
+				width 60px
+				height 60px
+				margin 0
+				border-radius 8px
+				vertical-align bottom
+
+		> header
+			position absolute
+			top 28px
+			left 108px
+			width calc(100% - 108px)
+
+			> .name
+				display inline-block
+				margin 0
+				line-height 24px
+				color #777
+				font-size 18px
+				font-weight 700
+				text-align left
+				text-decoration none
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				display block
+				text-align left
+				margin 0
+				color #ccc
+
+			> .time
+				position absolute
+				top 0
+				right 32px
+				font-size 1em
+				color #c0c0c0
+
+		> .body
+			padding 8px 0
+
+			> .renote
+				margin 8px 0
+
+				> .mk-note-preview
+					padding 16px
+					border dashed 1px #c0dac6
+					border-radius 8px
+
+			> .location
+				margin 4px 0
+				font-size 12px
+				color #ccc
+
+			> .map
+				width 100%
+				height 300px
+
+				&:empty
+					display none
+
+			> .mk-url-preview
+				margin-top 8px
+
+			> .tags
+				margin 4px 0 0 0
+
+				> *
+					display inline-block
+					margin 0 8px 0 0
+					padding 2px 8px 2px 16px
+					font-size 90%
+					color #8d969e
+					background #edf0f3
+					border-radius 4px
+
+					&:before
+						content ""
+						display block
+						position absolute
+						top 0
+						bottom 0
+						left 4px
+						width 8px
+						height 8px
+						margin auto 0
+						background #fff
+						border-radius 100%
+
+					&:hover
+						text-decoration none
+						background #e2e7ec
+
+		> footer
+			font-size 1.2em
+
+			> button
+				margin 0 28px 0 0
+				padding 8px
+				background transparent
+				border none
+				font-size 1em
+				color #ddd
+				cursor pointer
+
+				&:hover
+					color #666
+
+				> .count
+					display inline
+					margin 0 0 0 8px
+					color #999
+
+				&.reacted
+					color $theme-color
+
+	> .replies
+		> *
+			border-top 1px solid #eef0f2
+
+</style>
+
+<style lang="stylus" module>
+.text
+	cursor default
+	display block
+	margin 0
+	padding 0
+	overflow-wrap break-word
+	font-size 1.5em
+	color #717171
+</style>
diff --git a/src/client/app/desktop/views/components/post-preview.vue b/src/client/app/desktop/views/components/post-preview.vue
new file mode 100644
index 000000000..ff3ecadc2
--- /dev/null
+++ b/src/client/app/desktop/views/components/post-preview.vue
@@ -0,0 +1,99 @@
+<template>
+<div class="mk-note-preview" :title="title">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
+				<mk-time :time="note.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-sub-note-content class="text" :note="note"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+
+export default Vue.extend({
+	props: ['note'],
+	computed: {
+		title(): string {
+			return dateStringify(this.note.createdAt);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-note-preview
+	font-size 0.9em
+	background #fff
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	&:hover
+		> .main > footer > button
+			color #888
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 16px 0 0
+
+		> .avatar
+			display block
+			width 52px
+			height 52px
+			margin 0
+			border-radius 8px
+			vertical-align bottom
+
+	> .main
+		float left
+		width calc(100% - 68px)
+
+		> header
+			display flex
+			white-space nowrap
+
+			> .name
+				margin 0 .5em 0 0
+				padding 0
+				color #607073
+				font-size 1em
+				font-weight bold
+				text-decoration none
+				white-space normal
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				margin 0 .5em 0 0
+				color #d1d8da
+
+			> .time
+				margin-left auto
+				color #b2b8bb
+
+		> .body
+
+			> .text
+				cursor default
+				margin 0
+				padding 0
+				font-size 1.1em
+				color #717171
+
+</style>
diff --git a/src/client/app/desktop/views/components/posts.post.sub.vue b/src/client/app/desktop/views/components/posts.post.sub.vue
new file mode 100644
index 000000000..e85478578
--- /dev/null
+++ b/src/client/app/desktop/views/components/posts.post.sub.vue
@@ -0,0 +1,108 @@
+<template>
+<div class="sub" :title="title">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="note.userId"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="created-at" :to="note | notePage">
+				<mk-time :time="note.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-sub-note-content class="text" :note="note"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+
+export default Vue.extend({
+	props: ['note'],
+	computed: {
+		title(): string {
+			return dateStringify(this.note.createdAt);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.sub
+	margin 0
+	padding 16px
+	font-size 0.9em
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	&:hover
+		> .main > footer > button
+			color #888
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 14px 0 0
+
+		> .avatar
+			display block
+			width 52px
+			height 52px
+			margin 0
+			border-radius 8px
+			vertical-align bottom
+
+	> .main
+		float left
+		width calc(100% - 66px)
+
+		> header
+			display flex
+			margin-bottom 2px
+			white-space nowrap
+			line-height 21px
+
+			> .name
+				display block
+				margin 0 .5em 0 0
+				padding 0
+				overflow hidden
+				color #607073
+				font-size 1em
+				font-weight bold
+				text-decoration none
+				text-overflow ellipsis
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				margin 0 .5em 0 0
+				color #d1d8da
+
+			> .created-at
+				margin-left auto
+				color #b2b8bb
+
+		> .body
+
+			> .text
+				cursor default
+				margin 0
+				padding 0
+				font-size 1.1em
+				color #717171
+
+				pre
+					max-height 120px
+					font-size 80%
+
+</style>
diff --git a/src/client/app/desktop/views/components/posts.post.vue b/src/client/app/desktop/views/components/posts.post.vue
new file mode 100644
index 000000000..322bf2922
--- /dev/null
+++ b/src/client/app/desktop/views/components/posts.post.vue
@@ -0,0 +1,585 @@
+<template>
+<div class="note" tabindex="-1" :title="title" @keydown="onKeydown">
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="note.user | userPage" v-user-preview="note.userId">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
+			<a class="name" :href="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</a>
+			<span>{{ '%i18n:desktop.tags.mk-timeline-note.reposted-by%'.substr('%i18n:desktop.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
+		</p>
+		<mk-time :time="note.createdAt"/>
+	</div>
+	<article>
+		<router-link class="avatar-anchor" :to="p.user | userPage">
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar" v-user-preview="p.user.id"/>
+		</router-link>
+		<div class="main">
+			<header>
+				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link>
+				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
+				<span class="username">@{{ p.user | acct }}</span>
+				<div class="info">
+					<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
+					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
+					<router-link class="created-at" :to="url">
+						<mk-time :time="p.createdAt"/>
+					</router-link>
+				</div>
+			</header>
+			<div class="body">
+				<p class="channel" v-if="p.channel">
+					<a :href="`${_CH_URL_}/${p.channel.id}`" target="_blank">{{ p.channel.title }}</a>:
+				</p>
+				<div class="text">
+					<a class="reply" v-if="p.reply">%fa:reply%</a>
+					<mk-note-html v-if="p.textHtml" :text="p.text" :i="os.i" :class="$style.text"/>
+					<a class="rp" v-if="p.renote">RP:</a>
+				</div>
+				<div class="media" v-if="p.media.length > 0">
+					<mk-media-list :media-list="p.media"/>
+				</div>
+				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
+				<div class="tags" v-if="p.tags && p.tags.length > 0">
+					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+				</div>
+				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+				<div class="map" v-if="p.geo" ref="map"></div>
+				<div class="renote" v-if="p.renote">
+					<mk-note-preview :note="p.renote"/>
+				</div>
+				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			</div>
+			<footer>
+				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
+				<button @click="reply" title="%i18n:desktop.tags.mk-timeline-note.reply%">
+					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+				</button>
+				<button @click="renote" title="%i18n:desktop.tags.mk-timeline-note.renote%">
+					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+				</button>
+				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:desktop.tags.mk-timeline-note.add-reaction%">
+					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
+				</button>
+				<button @click="menu" ref="menuButton">
+					%fa:ellipsis-h%
+				</button>
+				<button title="%i18n:desktop.tags.mk-timeline-note.detail">
+					<template v-if="!isDetailOpened">%fa:caret-down%</template>
+					<template v-if="isDetailOpened">%fa:caret-up%</template>
+				</button>
+			</footer>
+		</div>
+	</article>
+	<div class="detail" v-if="isDetailOpened">
+		<mk-note-status-graph width="462" height="130" :note="p"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import dateStringify from '../../../common/scripts/date-stringify';
+import parse from '../../../../../text/parse';
+
+import MkPostFormWindow from './post-form-window.vue';
+import MkRenoteFormWindow from './renote-form-window.vue';
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './notes.note.sub.vue';
+
+function focus(el, fn) {
+	const target = fn(el);
+	if (target) {
+		if (target.hasAttribute('tabindex')) {
+			target.focus();
+		} else {
+			focus(target, fn);
+		}
+	}
+}
+
+export default Vue.extend({
+	components: {
+		XSub
+	},
+
+	props: ['note'],
+
+	data() {
+		return {
+			isDetailOpened: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+
+	computed: {
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds.length == 0 &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		title(): string {
+			return dateStringify(this.p.createdAt);
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+
+	created() {
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	beforeDestroy() {
+		this.decapture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.off('_connected_', this.onStreamConnected);
+			(this as any).os.stream.dispose(this.connectionId);
+		}
+	},
+
+	methods: {
+		capture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'capture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		decapture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'decapture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		onStreamConnected() {
+			this.capture();
+		},
+		onStreamNoteUpdated(data) {
+			const note = data.note;
+			if (note.id == this.note.id) {
+				this.$emit('update:note', note);
+			} else if (note.id == this.note.renoteId) {
+				this.note.renote = note;
+			}
+		},
+		reply() {
+			(this as any).os.new(MkPostFormWindow, {
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).os.new(MkRenoteFormWindow, {
+				note: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p
+			});
+		},
+		onKeydown(e) {
+			let shouldBeCancel = true;
+
+			switch (true) {
+				case e.which == 38: // [↑]
+				case e.which == 74: // [j]
+				case e.which == 9 && e.shiftKey: // [Shift] + [Tab]
+					focus(this.$el, e => e.previousElementSibling);
+					break;
+
+				case e.which == 40: // [↓]
+				case e.which == 75: // [k]
+				case e.which == 9: // [Tab]
+					focus(this.$el, e => e.nextElementSibling);
+					break;
+
+				case e.which == 81: // [q]
+				case e.which == 69: // [e]
+					this.renote();
+					break;
+
+				case e.which == 70: // [f]
+				case e.which == 76: // [l]
+					//this.like();
+					break;
+
+				case e.which == 82: // [r]
+					this.reply();
+					break;
+
+				default:
+					shouldBeCancel = false;
+			}
+
+			if (shouldBeCancel) e.preventDefault();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.note
+	margin 0
+	padding 0
+	background #fff
+	border-bottom solid 1px #eaeaea
+
+	&:first-child
+		border-top-left-radius 6px
+		border-top-right-radius 6px
+
+		> .renote
+			border-top-left-radius 6px
+			border-top-right-radius 6px
+
+	&:last-of-type
+		border-bottom none
+
+	&:focus
+		z-index 1
+
+		&:after
+			content ""
+			pointer-events none
+			position absolute
+			top 2px
+			right 2px
+			bottom 2px
+			left 2px
+			border 2px solid rgba($theme-color, 0.3)
+			border-radius 4px
+
+	> .renote
+		color #9dbb00
+		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+
+		> p
+			margin 0
+			padding 16px 32px
+			line-height 28px
+
+			.avatar-anchor
+				display inline-block
+
+				.avatar
+					vertical-align bottom
+					width 28px
+					height 28px
+					margin 0 8px 0 0
+					border-radius 6px
+
+			[data-fa]
+				margin-right 4px
+
+			.name
+				font-weight bold
+
+		> .mk-time
+			position absolute
+			top 16px
+			right 32px
+			font-size 0.9em
+			line-height 28px
+
+		& + article
+			padding-top 8px
+
+	> .reply-to
+		padding 0 16px
+		background rgba(0, 0, 0, 0.0125)
+
+		> .mk-note-preview
+			background transparent
+
+	> article
+		padding 28px 32px 18px 32px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		&:hover
+			> .main > footer > button
+				color #888
+
+		> .avatar-anchor
+			display block
+			float left
+			margin 0 16px 10px 0
+			//position -webkit-sticky
+			//position sticky
+			//top 74px
+
+			> .avatar
+				display block
+				width 58px
+				height 58px
+				margin 0
+				border-radius 8px
+				vertical-align bottom
+
+		> .main
+			float left
+			width calc(100% - 74px)
+
+			> header
+				display flex
+				align-items center
+				margin-bottom 4px
+				white-space nowrap
+
+				> .name
+					display block
+					margin 0 .5em 0 0
+					padding 0
+					overflow hidden
+					color #627079
+					font-size 1em
+					font-weight bold
+					text-decoration none
+					text-overflow ellipsis
+
+					&:hover
+						text-decoration underline
+
+				> .is-bot
+					margin 0 .5em 0 0
+					padding 1px 6px
+					font-size 12px
+					color #aaa
+					border solid 1px #ddd
+					border-radius 3px
+
+				> .username
+					margin 0 .5em 0 0
+					color #ccc
+
+				> .info
+					margin-left auto
+					font-size 0.9em
+
+					> .mobile
+						margin-right 8px
+						color #ccc
+
+					> .app
+						margin-right 8px
+						padding-right 8px
+						color #ccc
+						border-right solid 1px #eaeaea
+
+					> .created-at
+						color #c0c0c0
+
+			> .body
+
+				> .text
+					cursor default
+					display block
+					margin 0
+					padding 0
+					overflow-wrap break-word
+					font-size 1.1em
+					color #717171
+
+					>>> .quote
+						margin 8px
+						padding 6px 12px
+						color #aaa
+						border-left solid 3px #eee
+
+					> .reply
+						margin-right 8px
+						color #717171
+
+					> .rp
+						margin-left 4px
+						font-style oblique
+						color #a0bf46
+
+				> .location
+					margin 4px 0
+					font-size 12px
+					color #ccc
+
+				> .map
+					width 100%
+					height 300px
+
+					&:empty
+						display none
+
+				> .tags
+					margin 4px 0 0 0
+
+					> *
+						display inline-block
+						margin 0 8px 0 0
+						padding 2px 8px 2px 16px
+						font-size 90%
+						color #8d969e
+						background #edf0f3
+						border-radius 4px
+
+						&:before
+							content ""
+							display block
+							position absolute
+							top 0
+							bottom 0
+							left 4px
+							width 8px
+							height 8px
+							margin auto 0
+							background #fff
+							border-radius 100%
+
+						&:hover
+							text-decoration none
+							background #e2e7ec
+
+				.mk-url-preview
+					margin-top 8px
+
+				> .channel
+					margin 0
+
+				> .mk-poll
+					font-size 80%
+
+				> .renote
+					margin 8px 0
+
+					> .mk-note-preview
+						padding 16px
+						border dashed 1px #c0dac6
+						border-radius 8px
+
+			> footer
+				> button
+					margin 0 28px 0 0
+					padding 0 8px
+					line-height 32px
+					font-size 1em
+					color #ddd
+					background transparent
+					border none
+					cursor pointer
+
+					&:hover
+						color #666
+
+					> .count
+						display inline
+						margin 0 0 0 8px
+						color #999
+
+					&.reacted
+						color $theme-color
+
+					&:last-child
+						position absolute
+						right 0
+						margin 0
+
+	> .detail
+		padding-top 4px
+		background rgba(0, 0, 0, 0.0125)
+
+</style>
+
+<style lang="stylus" module>
+.text
+
+	code
+		padding 4px 8px
+		margin 0 0.5em
+		font-size 80%
+		color #525252
+		background #f8f8f8
+		border-radius 2px
+
+	pre > code
+		padding 16px
+		margin 0
+
+	[data-is-me]:after
+		content "you"
+		padding 0 4px
+		margin-left 4px
+		font-size 80%
+		color $theme-color-foreground
+		background $theme-color
+		border-radius 4px
+</style>
diff --git a/src/client/app/desktop/views/components/settings.mute.vue b/src/client/app/desktop/views/components/settings.mute.vue
index 6bdc76653..94492ad26 100644
--- a/src/client/app/desktop/views/components/settings.mute.vue
+++ b/src/client/app/desktop/views/components/settings.mute.vue
@@ -5,7 +5,7 @@
 	</div>
 	<div class="users" v-if="users.length != 0">
 		<div v-for="user in users" :key="user.id">
-			<p><b>{{ getUserName(user) }}</b> @{{ getAcct(user) }}</p>
+			<p><b>{{ user | userName }}</b> @{{ user | acct }}</p>
 		</div>
 	</div>
 </div>
@@ -13,8 +13,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -23,10 +21,6 @@ export default Vue.extend({
 			users: []
 		};
 	},
-	methods: {
-		getAcct,
-		getUserName
-	},
 	mounted() {
 		(this as any).api('mute/list').then(x => {
 			this.users = x.users;
diff --git a/src/client/app/desktop/views/components/ui.header.vue b/src/client/app/desktop/views/components/ui.header.vue
index 527d10843..2b63030cd 100644
--- a/src/client/app/desktop/views/components/ui.header.vue
+++ b/src/client/app/desktop/views/components/ui.header.vue
@@ -4,7 +4,7 @@
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
 		<div class="main">
-			<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p>
+			<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i | userName }}</b>さん</p>
 			<div class="container" ref="mainContainer">
 				<div class="left">
 					<x-nav/>
@@ -33,14 +33,7 @@ import XNotifications from './ui.header.notifications.vue';
 import XPost from './ui.header.post.vue';
 import XClock from './ui.header.clock.vue';
 
-import getUserName from '../../../../../renderers/get-user-name';
-
 export default Vue.extend({
-	computed: {
-		name(): string {
-			return getUserName((this as any).os.i);
-		}
-	},
 	components: {
 		XNav,
 		XSearch,
diff --git a/src/client/app/desktop/views/components/user-preview.vue b/src/client/app/desktop/views/components/user-preview.vue
index 1cc53743a..24337eea2 100644
--- a/src/client/app/desktop/views/components/user-preview.vue
+++ b/src/client/app/desktop/views/components/user-preview.vue
@@ -2,12 +2,12 @@
 <div class="mk-user-preview">
 	<template v-if="u != null">
 		<div class="banner" :style="u.bannerUrl ? `background-image: url(${u.bannerUrl}?thumbnail&size=512)` : ''"></div>
-		<router-link class="avatar" :to="`/@${getAcct(u)}`">
+		<router-link class="avatar" :to="u | userPage">
 			<img :src="`${u.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="title">
-			<router-link class="name" :to="`/@${getAcct(u)}`">{{ u.name }}</router-link>
-			<p class="username">@{{ getAcct(u) }}</p>
+			<router-link class="name" :to="u | userPage">{{ u.name }}</router-link>
+			<p class="username">@{{ u | acct }}</p>
 		</div>
 		<div class="description">{{ u.description }}</div>
 		<div class="status">
diff --git a/src/client/app/desktop/views/components/users-list.item.vue b/src/client/app/desktop/views/components/users-list.item.vue
index c7a132ecf..005c9cd6d 100644
--- a/src/client/app/desktop/views/components/users-list.item.vue
+++ b/src/client/app/desktop/views/components/users-list.item.vue
@@ -1,12 +1,12 @@
 <template>
 <div class="root item">
-	<router-link class="avatar-anchor" :to="`/@${acct}`" v-user-preview="user.id">
+	<router-link class="avatar-anchor" :to="user | userPage" v-user-preview="user.id">
 		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`" v-user-preview="user.id">{{ name }}</router-link>
-			<span class="username">@{{ acct }}</span>
+			<router-link class="name" :to="user | userPage" v-user-preview="user.id">{{ user | userName }}</router-link>
+			<span class="username">@{{ user | acct }}</span>
 		</header>
 		<div class="body">
 			<p class="followed" v-if="user.isFollowed">フォローされています</p>
@@ -19,19 +19,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['user'],
-	computed: {
-		acct() {
-			return getAcct(this.user);
-		},
-		name() {
-			return getUserName(this.user);
-		}
-	}
+	props: ['user']
 });
 </script>
 
diff --git a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
index 351b1264f..4113ef13a 100644
--- a/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
+++ b/src/client/app/desktop/views/pages/user/user.followers-you-know.vue
@@ -3,8 +3,8 @@
 	<p class="title">%fa:users%%i18n:desktop.tags.mk-user.followers-you-know.title%</p>
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.followers-you-know.loading%<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
-	<router-link v-for="user in users" :to="`/@${getAcct(user)}`" :key="user.id">
-		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)" v-user-preview="user.id"/>
+	<router-link v-for="user in users" :to="user | userPage" :key="user.id">
+		<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName" v-user-preview="user.id"/>
 	</router-link>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:desktop.tags.mk-user.followers-you-know.no-users%</p>
@@ -13,8 +13,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../acct/render';
-import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
@@ -24,10 +22,6 @@ export default Vue.extend({
 			fetching: true
 		};
 	},
-	methods: {
-		getAcct,
-		getUserName
-	},
 	mounted() {
 		(this as any).api('users/followers', {
 			userId: this.user.id,
diff --git a/src/client/app/desktop/views/pages/user/user.friends.vue b/src/client/app/desktop/views/pages/user/user.friends.vue
index c9213cb50..8512e8027 100644
--- a/src/client/app/desktop/views/pages/user/user.friends.vue
+++ b/src/client/app/desktop/views/pages/user/user.friends.vue
@@ -4,12 +4,12 @@
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:desktop.tags.mk-user.frequently-replied-users.loading%<mk-ellipsis/></p>
 	<template v-if="!fetching && users.length != 0">
 		<div class="user" v-for="friend in users">
-			<router-link class="avatar-anchor" :to="`/@${getAcct(friend)}`">
+			<router-link class="avatar-anchor" :to="friend | userPage">
 				<img class="avatar" :src="`${friend.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="friend.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${getAcct(friend)}`" v-user-preview="friend.id">{{ friend.name }}</router-link>
-				<p class="username">@{{ getAcct(friend) }}</p>
+				<router-link class="name" :to="friend | userPage" v-user-preview="friend.id">{{ friend.name }}</router-link>
+				<p class="username">@{{ friend | acct }}</p>
 			</div>
 			<mk-follow-button :user="friend"/>
 		</div>
diff --git a/src/client/app/desktop/views/pages/user/user.header.vue b/src/client/app/desktop/views/pages/user/user.header.vue
index f67bf5da2..67e52d173 100644
--- a/src/client/app/desktop/views/pages/user/user.header.vue
+++ b/src/client/app/desktop/views/pages/user/user.header.vue
@@ -7,8 +7,8 @@
 	<div class="container">
 		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=150`" alt="avatar"/>
 		<div class="title">
-			<p class="name">{{ name }}</p>
-			<p class="username">@{{ acct }}</p>
+			<p class="name">{{ user | userName }}</p>
+			<p class="username">@{{ user | acct }}</p>
 			<p class="location" v-if="user.host === null && user.profile.location">%fa:map-marker%{{ user.profile.location }}</p>
 		</div>
 		<footer>
@@ -22,19 +22,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../acct/render';
-import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
-	computed: {
-		acct() {
-			return getAcct(this.user);
-		},
-		name() {
-			return getUserName(this.user);
-		}
-	},
 	mounted() {
 		window.addEventListener('load', this.onScroll);
 		window.addEventListener('scroll', this.onScroll);
diff --git a/src/client/app/desktop/views/pages/user/user.vue b/src/client/app/desktop/views/pages/user/user.vue
index d07b462b5..3644286fb 100644
--- a/src/client/app/desktop/views/pages/user/user.vue
+++ b/src/client/app/desktop/views/pages/user/user.vue
@@ -45,7 +45,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 				Progress.done();
-				document.title = getUserName(user) + ' | Misskey';
+				document.title = getUserName(this.user) + ' | Misskey';
 			});
 		}
 	}
diff --git a/src/client/app/desktop/views/pages/welcome.vue b/src/client/app/desktop/views/pages/welcome.vue
index 41b015b8a..bc6ebae77 100644
--- a/src/client/app/desktop/views/pages/welcome.vue
+++ b/src/client/app/desktop/views/pages/welcome.vue
@@ -8,7 +8,7 @@
 					<p>ようこそ! <b>Misskey</b>はTwitter風ミニブログSNSです。思ったことや皆と共有したいことを投稿しましょう。タイムラインを見れば、皆の関心事をすぐにチェックすることもできます。<a :href="aboutUrl">詳しく...</a></p>
 					<p><button class="signup" @click="signup">はじめる</button><button class="signin" @click="signin">ログイン</button></p>
 					<div class="users">
-						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="`/@${getAcct(user)}`" v-user-preview="user.id">
+						<router-link v-for="user in users" :key="user.id" class="avatar-anchor" :to="user | userPage" v-user-preview="user.id">
 							<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 						</router-link>
 					</div>
diff --git a/src/client/app/desktop/views/widgets/channel.channel.note.vue b/src/client/app/desktop/views/widgets/channel.channel.note.vue
index 313a2e3f4..776791906 100644
--- a/src/client/app/desktop/views/widgets/channel.channel.note.vue
+++ b/src/client/app/desktop/views/widgets/channel.channel.note.vue
@@ -2,8 +2,8 @@
 <div class="note">
 	<header>
 		<a class="index" @click="reply">{{ note.index }}:</a>
-		<router-link class="name" :to="`/@${acct}`" v-user-preview="note.user.id"><b>{{ name }}</b></router-link>
-		<span>ID:<i>{{ acct }}</i></span>
+		<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"><b>{{ note.user | userName }}</b></router-link>
+		<span>ID:<i>{{ note.user | acct }}</i></span>
 	</header>
 	<div>
 		<a v-if="note.reply">&gt;&gt;{{ note.reply.index }}</a>
@@ -19,19 +19,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
-	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		}
-	},
 	methods: {
 		reply() {
 			this.$emit('reply', this.note);
diff --git a/src/client/app/desktop/views/widgets/channel.channel.post.vue b/src/client/app/desktop/views/widgets/channel.channel.post.vue
new file mode 100644
index 000000000..776791906
--- /dev/null
+++ b/src/client/app/desktop/views/widgets/channel.channel.post.vue
@@ -0,0 +1,65 @@
+<template>
+<div class="note">
+	<header>
+		<a class="index" @click="reply">{{ note.index }}:</a>
+		<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id"><b>{{ note.user | userName }}</b></router-link>
+		<span>ID:<i>{{ note.user | acct }}</i></span>
+	</header>
+	<div>
+		<a v-if="note.reply">&gt;&gt;{{ note.reply.index }}</a>
+		{{ note.text }}
+		<div class="media" v-if="note.media">
+			<a v-for="file in note.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: ['note'],
+	methods: {
+		reply() {
+			this.$emit('reply', this.note);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.note
+	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/client/app/desktop/views/widgets/profile.vue b/src/client/app/desktop/views/widgets/profile.vue
index 98e42222e..1b4b11de3 100644
--- a/src/client/app/desktop/views/widgets/profile.vue
+++ b/src/client/app/desktop/views/widgets/profile.vue
@@ -15,14 +15,13 @@
 		title="クリックでアバター編集"
 		v-user-preview="os.i.id"
 	/>
-	<router-link class="name" :to="`/@${os.i.username}`">{{ name }}</router-link>
-	<p class="username">@{{ os.i.username }}</p>
+	<router-link class="name" :to="os.i | userPage">{{ os.i | userName }}</router-link>
+	<p class="username">@{{ os.i | acct }}</p>
 </div>
 </template>
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default define({
 	name: 'profile',
@@ -30,11 +29,6 @@ export default define({
 		design: 0
 	})
 }).extend({
-	computed: {
-		name() {
-			return getUserName(this.os.i);
-		}
-	},
 	methods: {
 		func() {
 			if (this.props.design == 2) {
diff --git a/src/client/app/desktop/views/widgets/users.vue b/src/client/app/desktop/views/widgets/users.vue
index a5dabb68f..c7075d9a5 100644
--- a/src/client/app/desktop/views/widgets/users.vue
+++ b/src/client/app/desktop/views/widgets/users.vue
@@ -7,12 +7,12 @@
 	<p class="fetching" v-if="fetching">%fa:spinner .pulse .fw%%i18n:common.loading%<mk-ellipsis/></p>
 	<template v-else-if="users.length != 0">
 		<div class="user" v-for="_user in users">
-			<router-link class="avatar-anchor" :to="`/@${getAcct(_user)}`">
+			<router-link class="avatar-anchor" :to="_user | userPage">
 				<img class="avatar" :src="`${_user.avatarUrl}?thumbnail&size=42`" alt="" v-user-preview="_user.id"/>
 			</router-link>
 			<div class="body">
-				<router-link class="name" :to="`/@${getAcct(_user)}`" v-user-preview="_user.id">{{ getUserName(_user) }}</router-link>
-				<p class="username">@{{ getAcct(_user) }}</p>
+				<router-link class="name" :to="_user | userPage" v-user-preview="_user.id">{{ _user | userName }}</router-link>
+				<p class="username">@{{ _user | acct }}</p>
 			</div>
 			<mk-follow-button :user="_user"/>
 		</div>
@@ -23,8 +23,6 @@
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 const limit = 3;
 
@@ -45,8 +43,6 @@ export default define({
 		this.fetch();
 	},
 	methods: {
-		getAcct,
-		getUserName,
 		func() {
 			this.props.compact = !this.props.compact;
 		},
diff --git a/src/client/app/mobile/views/components/note-card.vue b/src/client/app/mobile/views/components/note-card.vue
index 9ad0d3e29..393fa9b83 100644
--- a/src/client/app/mobile/views/components/note-card.vue
+++ b/src/client/app/mobile/views/components/note-card.vue
@@ -1,8 +1,8 @@
 <template>
 <div class="mk-note-card">
-	<a :href="`/@${acct}/${note.id}`">
+	<a :href="note | notePage">
 		<header>
-			<img :src="`${acct}?thumbnail&size=64`" alt="avatar"/><h3>{{ name }}</h3>
+			<img :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/><h3>{{ note.user | userName }}</h3>
 		</header>
 		<div>
 			{{ text }}
@@ -15,18 +15,10 @@
 <script lang="ts">
 import Vue from 'vue';
 import summary from '../../../../../renderers/get-note-summary';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['note'],
 	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		},
 		text(): string {
 			return summary(this.note);
 		}
diff --git a/src/client/app/mobile/views/components/note-detail.sub.vue b/src/client/app/mobile/views/components/note-detail.sub.vue
index 38aea4ba2..06f442d30 100644
--- a/src/client/app/mobile/views/components/note-detail.sub.vue
+++ b/src/client/app/mobile/views/components/note-detail.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="root sub">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ getUserName(note.user) }}</router-link>
-			<span class="username">@{{ acct }}</span>
-			<router-link class="time" :to="`/@${acct}/${note.id}`">
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -20,19 +20,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['note'],
-	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		}
-	}
+	props: ['note']
 });
 </script>
 
diff --git a/src/client/app/mobile/views/components/note-detail.vue b/src/client/app/mobile/views/components/note-detail.vue
index 483f5aaf3..de32f0a74 100644
--- a/src/client/app/mobile/views/components/note-detail.vue
+++ b/src/client/app/mobile/views/components/note-detail.vue
@@ -17,24 +17,20 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${acct}`">
+			<router-link class="avatar-anchor" :to="note.user | userPage">
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
 			</router-link>
-			%fa:retweet%
-			<router-link class="name" :to="`/@${acct}`">
-				{{ name }}
-			</router-link>
-			がRenote
+			%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
 		</p>
 	</div>
 	<article>
 		<header>
-			<router-link class="avatar-anchor" :to="`/@${pAcct}`">
+			<router-link class="avatar-anchor" :to="p.user | userPage">
 				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			<div>
-				<router-link class="name" :to="`/@${pAcct}`">{{ pName }}</router-link>
-				<span class="username">@{{ pAcct }}</span>
+				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
+				<span class="username">@{{ p.user | acct }}</span>
 			</div>
 		</header>
 		<div class="body">
@@ -80,8 +76,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkNoteMenu from '../../../common/views/components/note-menu.vue';
@@ -112,18 +106,6 @@ export default Vue.extend({
 	},
 
 	computed: {
-		acct(): string {
-			return getAcct(this.note.user);
-		},
-		name(): string {
-			return getUserName(this.note.user);
-		},
-		pAcct(): string {
-			return getAcct(this.p.user);
-		},
-		pName(): string {
-			return getUserName(this.p.user);
-		},
 		isRenote(): boolean {
 			return (this.note.renote &&
 				this.note.text == null &&
diff --git a/src/client/app/mobile/views/components/note-preview.vue b/src/client/app/mobile/views/components/note-preview.vue
index 8c8d8645b..b9a6db315 100644
--- a/src/client/app/mobile/views/components/note-preview.vue
+++ b/src/client/app/mobile/views/components/note-preview.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="mk-note-preview">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
-			<span class="username">@{{ acct }}</span>
-			<router-link class="time" :to="`/@${acct}/${note.id}`">
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -20,19 +20,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['note'],
-	computed: {
-		acct() {
-			return getAcct(this.note.user);
-		},
-		name() {
-			return getUserName(this.note.user);
-		}
-	}
+	props: ['note']
 });
 </script>
 
diff --git a/src/client/app/mobile/views/components/note.sub.vue b/src/client/app/mobile/views/components/note.sub.vue
index 96f8265cc..d489f3a05 100644
--- a/src/client/app/mobile/views/components/note.sub.vue
+++ b/src/client/app/mobile/views/components/note.sub.vue
@@ -1,13 +1,13 @@
 <template>
 <div class="sub">
-	<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
 		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${getAcct(note.user)}`">{{ getUserName(note.user) }}</router-link>
-			<span class="username">@{{ getAcct(note.user) }}</span>
-			<router-link class="created-at" :to="`/@${getAcct(note.user)}/${note.id}`">
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="created-at" :to="note | notePage">
 				<mk-time :time="note.createdAt"/>
 			</router-link>
 		</header>
@@ -20,17 +20,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['note'],
-	data() {
-		return {
-			getAcct,
-			getUserName
-		};
-	}
+	props: ['note']
 });
 </script>
 
diff --git a/src/client/app/mobile/views/components/note.vue b/src/client/app/mobile/views/components/note.vue
index 295fe4d6a..033de4f42 100644
--- a/src/client/app/mobile/views/components/note.vue
+++ b/src/client/app/mobile/views/components/note.vue
@@ -5,28 +5,28 @@
 	</div>
 	<div class="renote" v-if="isRenote">
 		<p>
-			<router-link class="avatar-anchor" :to="`/@${getAcct(note.user)}`">
+			<router-link class="avatar-anchor" :to="note.user | userPage">
 				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 			</router-link>
 			%fa:retweet%
 			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
-			<router-link class="name" :to="`/@${getAcct(note.user)}`">{{ getUserName(note.user) }}</router-link>
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
 			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
 		</p>
 		<mk-time :time="note.createdAt"/>
 	</div>
 	<article>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(p.user)}`">
+		<router-link class="avatar-anchor" :to="p.user | userPage">
 			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
 		</router-link>
 		<div class="main">
 			<header>
-				<router-link class="name" :to="`/@${getAcct(p.user)}`">{{ getUserName(p.user) }}</router-link>
+				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
 				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
-				<span class="username">@{{ getAcct(p.user) }}</span>
+				<span class="username">@{{ p.user | acct }}</span>
 				<div class="info">
 					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
-					<router-link class="created-at" :to="url">
+					<router-link class="created-at" :to="p | notePage">
 						<mk-time :time="p.createdAt"/>
 					</router-link>
 				</div>
@@ -77,8 +77,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 import parse from '../../../../../text/parse';
 
 import MkNoteMenu from '../../../common/views/components/note-menu.vue';
@@ -95,9 +93,7 @@ export default Vue.extend({
 	data() {
 		return {
 			connection: null,
-			connectionId: null,
-			getAcct,
-			getUserName
+			connectionId: null
 		};
 	},
 
@@ -118,9 +114,6 @@ export default Vue.extend({
 					.reduce((a, b) => a + b)
 				: 0;
 		},
-		url(): string {
-			return `/@${this.pAcct}/${this.p.id}`;
-		},
 		urls(): string[] {
 			if (this.p.text) {
 				const ast = parse(this.p.text);
diff --git a/src/client/app/mobile/views/components/notification-preview.vue b/src/client/app/mobile/views/components/notification-preview.vue
index f0921f91d..d39b2fbf9 100644
--- a/src/client/app/mobile/views/components/notification-preview.vue
+++ b/src/client/app/mobile/views/components/notification-preview.vue
@@ -3,7 +3,7 @@
 	<template v-if="notification.type == 'reaction'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ getUserName(notification.user) }}</p>
+			<p><mk-reaction-icon :reaction="notification.reaction"/>{{ notification.user | userName }}</p>
 			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -11,7 +11,7 @@
 	<template v-if="notification.type == 'renote'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:retweet%{{ getUserName(notification.note.user) }}</p>
+			<p>%fa:retweet%{{ notification.note.user | userName }}</p>
 			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -19,7 +19,7 @@
 	<template v-if="notification.type == 'quote'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:quote-left%{{ getUserName(notification.note.user) }}</p>
+			<p>%fa:quote-left%{{ notification.note.user | userName }}</p>
 			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
@@ -27,14 +27,14 @@
 	<template v-if="notification.type == 'follow'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:user-plus%{{ getUserName(notification.user) }}</p>
+			<p>%fa:user-plus%{{ notification.user | userName }}</p>
 		</div>
 	</template>
 
 	<template v-if="notification.type == 'reply'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:reply%{{ getUserName(notification.note.user) }}</p>
+			<p>%fa:reply%{{ notification.note.user | userName }}</p>
 			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
@@ -42,7 +42,7 @@
 	<template v-if="notification.type == 'mention'">
 		<img class="avatar" :src="`${notification.note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:at%{{ getUserName(notification.note.user) }}</p>
+			<p>%fa:at%{{ notification.note.user | userName }}</p>
 			<p class="note-preview">{{ getNoteSummary(notification.note) }}</p>
 		</div>
 	</template>
@@ -50,7 +50,7 @@
 	<template v-if="notification.type == 'poll_vote'">
 		<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		<div class="text">
-			<p>%fa:chart-pie%{{ getUserName(notification.user) }}</p>
+			<p>%fa:chart-pie%{{ notification.user | userName }}</p>
 			<p class="note-ref">%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%</p>
 		</div>
 	</template>
@@ -60,14 +60,12 @@
 <script lang="ts">
 import Vue from 'vue';
 import getNoteSummary from '../../../../../renderers/get-note-summary';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['notification'],
 	data() {
 		return {
-			getNoteSummary,
-			getUserName
+			getNoteSummary
 		};
 	}
 });
diff --git a/src/client/app/mobile/views/components/notification.vue b/src/client/app/mobile/views/components/notification.vue
index 4c98e1990..5456c2c17 100644
--- a/src/client/app/mobile/views/components/notification.vue
+++ b/src/client/app/mobile/views/components/notification.vue
@@ -2,13 +2,13 @@
 <div class="mk-notification">
 	<div class="notification reaction" v-if="notification.type == 'reaction'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
+		<router-link class="avatar-anchor" :to="notification.user | userPage">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				<mk-reaction-icon :reaction="notification.reaction"/>
-				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
 			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 				%fa:quote-left%{{ getNoteSummary(notification.note) }}
@@ -19,13 +19,13 @@
 
 	<div class="notification renote" v-if="notification.type == 'renote'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
+		<router-link class="avatar-anchor" :to="notification.user | userPage">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:retweet%
-				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
 			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 				%fa:quote-left%{{ getNoteSummary(notification.note.renote) }}%fa:quote-right%
@@ -39,13 +39,13 @@
 
 	<div class="notification follow" v-if="notification.type == 'follow'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
+		<router-link class="avatar-anchor" :to="notification.user | userPage">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:user-plus%
-				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
 		</div>
 	</div>
@@ -60,13 +60,13 @@
 
 	<div class="notification poll_vote" v-if="notification.type == 'poll_vote'">
 		<mk-time :time="notification.createdAt"/>
-		<router-link class="avatar-anchor" :to="`/@${getAcct(notification.user)}`">
+		<router-link class="avatar-anchor" :to="notification.user | userPage">
 			<img class="avatar" :src="`${notification.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 		</router-link>
 		<div class="text">
 			<p>
 				%fa:chart-pie%
-				<router-link :to="`/@${getAcct(notification.user)}`">{{ getUserName(notification.user) }}</router-link>
+				<router-link :to="notification.user | userPage">{{ notification.user | userName }}</router-link>
 			</p>
 			<router-link class="note-ref" :to="`/@${getAcct(notification.note.user)}/${notification.note.id}`">
 				%fa:quote-left%{{ getNoteSummary(notification.note) }}%fa:quote-right%
@@ -79,16 +79,12 @@
 <script lang="ts">
 import Vue from 'vue';
 import getNoteSummary from '../../../../../renderers/get-note-summary';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['notification'],
 	data() {
 		return {
-			getNoteSummary,
-			getAcct,
-			getUserName
+			getNoteSummary
 		};
 	}
 });
diff --git a/src/client/app/mobile/views/components/post-card.vue b/src/client/app/mobile/views/components/post-card.vue
new file mode 100644
index 000000000..393fa9b83
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-card.vue
@@ -0,0 +1,85 @@
+<template>
+<div class="mk-note-card">
+	<a :href="note | notePage">
+		<header>
+			<img :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/><h3>{{ note.user | userName }}</h3>
+		</header>
+		<div>
+			{{ text }}
+		</div>
+		<mk-time :time="note.createdAt"/>
+	</a>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import summary from '../../../../../renderers/get-note-summary';
+
+export default Vue.extend({
+	props: ['note'],
+	computed: {
+		text(): string {
+			return summary(this.note);
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-note-card
+	display inline-block
+	width 150px
+	//height 120px
+	font-size 12px
+	background #fff
+	border-radius 4px
+
+	> a
+		display block
+		color #2c3940
+
+		&:hover
+			text-decoration none
+
+		> header
+			> img
+				position absolute
+				top 8px
+				left 8px
+				width 28px
+				height 28px
+				border-radius 6px
+
+			> h3
+				display inline-block
+				overflow hidden
+				width calc(100% - 45px)
+				margin 8px 0 0 42px
+				line-height 28px
+				white-space nowrap
+				text-overflow ellipsis
+				font-size 12px
+
+		> div
+			padding 2px 8px 8px 8px
+			height 60px
+			overflow hidden
+			white-space normal
+
+			&:after
+				content ""
+				display block
+				position absolute
+				top 40px
+				left 0
+				width 100%
+				height 20px
+				background linear-gradient(to bottom, rgba(255, 255, 255, 0) 0%, #fff 100%)
+
+		> .mk-time
+			display inline-block
+			padding 8px
+			color #aaa
+
+</style>
diff --git a/src/client/app/mobile/views/components/post-detail.sub.vue b/src/client/app/mobile/views/components/post-detail.sub.vue
new file mode 100644
index 000000000..06f442d30
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-detail.sub.vue
@@ -0,0 +1,103 @@
+<template>
+<div class="root sub">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
+				<mk-time :time="note.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-sub-note-content class="text" :note="note"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['note']
+});
+</script>
+
+<style lang="stylus" scoped>
+.root.sub
+	padding 8px
+	font-size 0.9em
+	background #fdfdfd
+
+	@media (min-width 500px)
+		padding 12px
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	&:hover
+		> .main > footer > button
+			color #888
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 12px 0 0
+
+		> .avatar
+			display block
+			width 48px
+			height 48px
+			margin 0
+			border-radius 8px
+			vertical-align bottom
+
+	> .main
+		float left
+		width calc(100% - 60px)
+
+		> header
+			display flex
+			margin-bottom 4px
+			white-space nowrap
+
+			> .name
+				display block
+				margin 0 .5em 0 0
+				padding 0
+				overflow hidden
+				color #607073
+				font-size 1em
+				font-weight 700
+				text-align left
+				text-decoration none
+				text-overflow ellipsis
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				text-align left
+				margin 0 .5em 0 0
+				color #d1d8da
+
+			> .time
+				margin-left auto
+				color #b2b8bb
+
+		> .body
+
+			> .text
+				cursor default
+				margin 0
+				padding 0
+				font-size 1.1em
+				color #717171
+
+</style>
+
diff --git a/src/client/app/mobile/views/components/post-detail.vue b/src/client/app/mobile/views/components/post-detail.vue
new file mode 100644
index 000000000..de32f0a74
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-detail.vue
@@ -0,0 +1,444 @@
+<template>
+<div class="mk-note-detail">
+	<button
+		class="more"
+		v-if="p.reply && p.reply.replyId && context == null"
+		@click="fetchContext"
+		:disabled="fetchingContext"
+	>
+		<template v-if="!contextFetching">%fa:ellipsis-v%</template>
+		<template v-if="contextFetching">%fa:spinner .pulse%</template>
+	</button>
+	<div class="context">
+		<x-sub v-for="note in context" :key="note.id" :note="note"/>
+	</div>
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="note.user | userPage">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=32`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>がRenote
+		</p>
+	</div>
+	<article>
+		<header>
+			<router-link class="avatar-anchor" :to="p.user | userPage">
+				<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+			</router-link>
+			<div>
+				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
+				<span class="username">@{{ p.user | acct }}</span>
+			</div>
+		</header>
+		<div class="body">
+			<mk-note-html v-if="p.text" :ast="p.text" :i="os.i" :class="$style.text"/>
+			<div class="tags" v-if="p.tags && p.tags.length > 0">
+				<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+			</div>
+			<div class="media" v-if="p.media.length > 0">
+				<mk-media-list :media-list="p.media"/>
+			</div>
+			<mk-poll v-if="p.poll" :note="p"/>
+			<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+			<div class="map" v-if="p.geo" ref="map"></div>
+			<div class="renote" v-if="p.renote">
+				<mk-note-preview :note="p.renote"/>
+			</div>
+		</div>
+		<router-link class="time" :to="`/@${pAcct}/${p.id}`">
+			<mk-time :time="p.createdAt" mode="detail"/>
+		</router-link>
+		<footer>
+			<mk-reactions-viewer :note="p"/>
+			<button @click="reply" title="%i18n:mobile.tags.mk-note-detail.reply%">
+				%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+			</button>
+			<button @click="renote" title="Renote">
+				%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+			</button>
+			<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton" title="%i18n:mobile.tags.mk-note-detail.reaction%">
+				%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
+			</button>
+			<button @click="menu" ref="menuButton">
+				%fa:ellipsis-h%
+			</button>
+		</footer>
+	</article>
+	<div class="replies" v-if="!compact">
+		<x-sub v-for="note in replies" :key="note.id" :note="note"/>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import parse from '../../../../../text/parse';
+
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './note-detail.sub.vue';
+
+export default Vue.extend({
+	components: {
+		XSub
+	},
+
+	props: {
+		note: {
+			type: Object,
+			required: true
+		},
+		compact: {
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			context: [],
+			contextFetching: false,
+			replies: []
+		};
+	},
+
+	computed: {
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds.length == 0 &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+
+	mounted() {
+		// Get replies
+		if (!this.compact) {
+			(this as any).api('notes/replies', {
+				noteId: this.p.id,
+				limit: 8
+			}).then(replies => {
+				this.replies = replies;
+			});
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	methods: {
+		fetchContext() {
+			this.contextFetching = true;
+
+			// Fetch context
+			(this as any).api('notes/context', {
+				noteId: this.p.replyId
+			}).then(context => {
+				this.contextFetching = false;
+				this.context = context.reverse();
+			});
+		},
+		reply() {
+			(this as any).apis.post({
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).apis.post({
+				renote: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p,
+				compact: true
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p,
+				compact: true
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.mk-note-detail
+	overflow hidden
+	margin 0 auto
+	padding 0
+	width 100%
+	text-align left
+	background #fff
+	border-radius 8px
+	box-shadow 0 0 0 1px rgba(0, 0, 0, 0.2)
+
+	> .fetching
+		padding 64px 0
+
+	> .more
+		display block
+		margin 0
+		padding 10px 0
+		width 100%
+		font-size 1em
+		text-align center
+		color #999
+		cursor pointer
+		background #fafafa
+		outline none
+		border none
+		border-bottom solid 1px #eef0f2
+		border-radius 6px 6px 0 0
+		box-shadow none
+
+		&:hover
+			background #f6f6f6
+
+		&:active
+			background #f0f0f0
+
+		&:disabled
+			color #ccc
+
+	> .context
+		> *
+			border-bottom 1px solid #eef0f2
+
+	> .renote
+		color #9dbb00
+		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+
+		> p
+			margin 0
+			padding 16px 32px
+
+			.avatar-anchor
+				display inline-block
+
+				.avatar
+					vertical-align bottom
+					min-width 28px
+					min-height 28px
+					max-width 28px
+					max-height 28px
+					margin 0 8px 0 0
+					border-radius 6px
+
+			[data-fa]
+				margin-right 4px
+
+			.name
+				font-weight bold
+
+		& + article
+			padding-top 8px
+
+	> .reply-to
+		border-bottom 1px solid #eef0f2
+
+	> article
+		padding 14px 16px 9px 16px
+
+		@media (min-width 500px)
+			padding 28px 32px 18px 32px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		&:hover
+			> .main > footer > button
+				color #888
+
+		> header
+			display flex
+			line-height 1.1
+
+			> .avatar-anchor
+				display block
+				padding 0 .5em 0 0
+
+				> .avatar
+					display block
+					width 54px
+					height 54px
+					margin 0
+					border-radius 8px
+					vertical-align bottom
+
+					@media (min-width 500px)
+						width 60px
+						height 60px
+
+			> div
+
+				> .name
+					display inline-block
+					margin .4em 0
+					color #777
+					font-size 16px
+					font-weight bold
+					text-align left
+					text-decoration none
+
+					&:hover
+						text-decoration underline
+
+				> .username
+					display block
+					text-align left
+					margin 0
+					color #ccc
+
+		> .body
+			padding 8px 0
+
+			> .renote
+				margin 8px 0
+
+				> .mk-note-preview
+					padding 16px
+					border dashed 1px #c0dac6
+					border-radius 8px
+
+			> .location
+				margin 4px 0
+				font-size 12px
+				color #ccc
+
+			> .map
+				width 100%
+				height 200px
+
+				&:empty
+					display none
+
+			> .mk-url-preview
+				margin-top 8px
+
+			> .media
+				> img
+					display block
+					max-width 100%
+
+			> .tags
+				margin 4px 0 0 0
+
+				> *
+					display inline-block
+					margin 0 8px 0 0
+					padding 2px 8px 2px 16px
+					font-size 90%
+					color #8d969e
+					background #edf0f3
+					border-radius 4px
+
+					&:before
+						content ""
+						display block
+						position absolute
+						top 0
+						bottom 0
+						left 4px
+						width 8px
+						height 8px
+						margin auto 0
+						background #fff
+						border-radius 100%
+
+		> .time
+			font-size 16px
+			color #c0c0c0
+
+		> footer
+			font-size 1.2em
+
+			> button
+				margin 0
+				padding 8px
+				background transparent
+				border none
+				box-shadow none
+				font-size 1em
+				color #ddd
+				cursor pointer
+
+				&:not(:last-child)
+					margin-right 28px
+
+				&:hover
+					color #666
+
+				> .count
+					display inline
+					margin 0 0 0 8px
+					color #999
+
+				&.reacted
+					color $theme-color
+
+	> .replies
+		> *
+			border-top 1px solid #eef0f2
+
+</style>
+
+<style lang="stylus" module>
+.text
+	display block
+	margin 0
+	padding 0
+	overflow-wrap break-word
+	font-size 16px
+	color #717171
+
+	@media (min-width 500px)
+		font-size 24px
+
+</style>
diff --git a/src/client/app/mobile/views/components/post-preview.vue b/src/client/app/mobile/views/components/post-preview.vue
new file mode 100644
index 000000000..b9a6db315
--- /dev/null
+++ b/src/client/app/mobile/views/components/post-preview.vue
@@ -0,0 +1,100 @@
+<template>
+<div class="mk-note-preview">
+	<router-link class="avatar-anchor" :to="note.user | userPage">
+		<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+	</router-link>
+	<div class="main">
+		<header>
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span class="username">@{{ note.user | acct }}</span>
+			<router-link class="time" :to="note | notePage">
+				<mk-time :time="note.createdAt"/>
+			</router-link>
+		</header>
+		<div class="body">
+			<mk-sub-note-content class="text" :note="note"/>
+		</div>
+	</div>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	props: ['note']
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-note-preview
+	margin 0
+	padding 0
+	font-size 0.9em
+	background #fff
+
+	&:after
+		content ""
+		display block
+		clear both
+
+	&:hover
+		> .main > footer > button
+			color #888
+
+	> .avatar-anchor
+		display block
+		float left
+		margin 0 12px 0 0
+
+		> .avatar
+			display block
+			width 48px
+			height 48px
+			margin 0
+			border-radius 8px
+			vertical-align bottom
+
+	> .main
+		float left
+		width calc(100% - 60px)
+
+		> header
+			display flex
+			margin-bottom 4px
+			white-space nowrap
+
+			> .name
+				display block
+				margin 0 .5em 0 0
+				padding 0
+				overflow hidden
+				color #607073
+				font-size 1em
+				font-weight 700
+				text-align left
+				text-decoration none
+				text-overflow ellipsis
+
+				&:hover
+					text-decoration underline
+
+			> .username
+				text-align left
+				margin 0 .5em 0 0
+				color #d1d8da
+
+			> .time
+				margin-left auto
+				color #b2b8bb
+
+		> .body
+
+			> .text
+				cursor default
+				margin 0
+				padding 0
+				font-size 1.1em
+				color #717171
+
+</style>
diff --git a/src/client/app/mobile/views/components/post.vue b/src/client/app/mobile/views/components/post.vue
new file mode 100644
index 000000000..033de4f42
--- /dev/null
+++ b/src/client/app/mobile/views/components/post.vue
@@ -0,0 +1,523 @@
+<template>
+<div class="note" :class="{ renote: isRenote }">
+	<div class="reply-to" v-if="p.reply">
+		<x-sub :note="p.reply"/>
+	</div>
+	<div class="renote" v-if="isRenote">
+		<p>
+			<router-link class="avatar-anchor" :to="note.user | userPage">
+				<img class="avatar" :src="`${note.user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
+			</router-link>
+			%fa:retweet%
+			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr(0, '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('{')) }}</span>
+			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link>
+			<span>{{ '%i18n:mobile.tags.mk-timeline-note.reposted-by%'.substr('%i18n:mobile.tags.mk-timeline-note.reposted-by%'.indexOf('}') + 1) }}</span>
+		</p>
+		<mk-time :time="note.createdAt"/>
+	</div>
+	<article>
+		<router-link class="avatar-anchor" :to="p.user | userPage">
+			<img class="avatar" :src="`${p.user.avatarUrl}?thumbnail&size=96`" alt="avatar"/>
+		</router-link>
+		<div class="main">
+			<header>
+				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link>
+				<span class="is-bot" v-if="p.user.host === null && p.user.isBot">bot</span>
+				<span class="username">@{{ p.user | acct }}</span>
+				<div class="info">
+					<span class="mobile" v-if="p.viaMobile">%fa:mobile-alt%</span>
+					<router-link class="created-at" :to="p | notePage">
+						<mk-time :time="p.createdAt"/>
+					</router-link>
+				</div>
+			</header>
+			<div class="body">
+				<p class="channel" v-if="p.channel != null"><a target="_blank">{{ p.channel.title }}</a>:</p>
+				<div class="text">
+					<a class="reply" v-if="p.reply">
+						%fa:reply%
+					</a>
+					<mk-note-html v-if="p.text" :text="p.text" :i="os.i" :class="$style.text"/>
+					<a class="rp" v-if="p.renote != null">RP:</a>
+				</div>
+				<div class="media" v-if="p.media.length > 0">
+					<mk-media-list :media-list="p.media"/>
+				</div>
+				<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
+				<div class="tags" v-if="p.tags && p.tags.length > 0">
+					<router-link v-for="tag in p.tags" :key="tag" :to="`/search?q=#${tag}`">{{ tag }}</router-link>
+				</div>
+				<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
+				<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
+				<div class="map" v-if="p.geo" ref="map"></div>
+				<span class="app" v-if="p.app">via <b>{{ p.app.name }}</b></span>
+				<div class="renote" v-if="p.renote">
+					<mk-note-preview :note="p.renote"/>
+				</div>
+			</div>
+			<footer>
+				<mk-reactions-viewer :note="p" ref="reactionsViewer"/>
+				<button @click="reply">
+					%fa:reply%<p class="count" v-if="p.repliesCount > 0">{{ p.repliesCount }}</p>
+				</button>
+				<button @click="renote" title="Renote">
+					%fa:retweet%<p class="count" v-if="p.renoteCount > 0">{{ p.renoteCount }}</p>
+				</button>
+				<button :class="{ reacted: p.myReaction != null }" @click="react" ref="reactButton">
+					%fa:plus%<p class="count" v-if="p.reactions_count > 0">{{ p.reactions_count }}</p>
+				</button>
+				<button class="menu" @click="menu" ref="menuButton">
+					%fa:ellipsis-h%
+				</button>
+			</footer>
+		</div>
+	</article>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import parse from '../../../../../text/parse';
+
+import MkNoteMenu from '../../../common/views/components/note-menu.vue';
+import MkReactionPicker from '../../../common/views/components/reaction-picker.vue';
+import XSub from './note.sub.vue';
+
+export default Vue.extend({
+	components: {
+		XSub
+	},
+
+	props: ['note'],
+
+	data() {
+		return {
+			connection: null,
+			connectionId: null
+		};
+	},
+
+	computed: {
+		isRenote(): boolean {
+			return (this.note.renote &&
+				this.note.text == null &&
+				this.note.mediaIds.length == 0 &&
+				this.note.poll == null);
+		},
+		p(): any {
+			return this.isRenote ? this.note.renote : this.note;
+		},
+		reactionsCount(): number {
+			return this.p.reactionCounts
+				? Object.keys(this.p.reactionCounts)
+					.map(key => this.p.reactionCounts[key])
+					.reduce((a, b) => a + b)
+				: 0;
+		},
+		urls(): string[] {
+			if (this.p.text) {
+				const ast = parse(this.p.text);
+				return ast
+					.filter(t => (t.type == 'url' || t.type == 'link') && !t.silent)
+					.map(t => t.url);
+			} else {
+				return null;
+			}
+		}
+	},
+
+	created() {
+		if ((this as any).os.isSignedIn) {
+			this.connection = (this as any).os.stream.getConnection();
+			this.connectionId = (this as any).os.stream.use();
+		}
+	},
+
+	mounted() {
+		this.capture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.on('_connected_', this.onStreamConnected);
+		}
+
+		// Draw map
+		if (this.p.geo) {
+			const shouldShowMap = (this as any).os.isSignedIn ? (this as any).os.i.clientSettings.showMaps : true;
+			if (shouldShowMap) {
+				(this as any).os.getGoogleMaps().then(maps => {
+					const uluru = new maps.LatLng(this.p.geo.coordinates[1], this.p.geo.coordinates[0]);
+					const map = new maps.Map(this.$refs.map, {
+						center: uluru,
+						zoom: 15
+					});
+					new maps.Marker({
+						position: uluru,
+						map: map
+					});
+				});
+			}
+		}
+	},
+
+	beforeDestroy() {
+		this.decapture(true);
+
+		if ((this as any).os.isSignedIn) {
+			this.connection.off('_connected_', this.onStreamConnected);
+			(this as any).os.stream.dispose(this.connectionId);
+		}
+	},
+
+	methods: {
+		capture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'capture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.on('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		decapture(withHandler = false) {
+			if ((this as any).os.isSignedIn) {
+				this.connection.send({
+					type: 'decapture',
+					id: this.p.id
+				});
+				if (withHandler) this.connection.off('note-updated', this.onStreamNoteUpdated);
+			}
+		},
+		onStreamConnected() {
+			this.capture();
+		},
+		onStreamNoteUpdated(data) {
+			const note = data.note;
+			if (note.id == this.note.id) {
+				this.$emit('update:note', note);
+			} else if (note.id == this.note.renoteId) {
+				this.note.renote = note;
+			}
+		},
+		reply() {
+			(this as any).apis.post({
+				reply: this.p
+			});
+		},
+		renote() {
+			(this as any).apis.post({
+				renote: this.p
+			});
+		},
+		react() {
+			(this as any).os.new(MkReactionPicker, {
+				source: this.$refs.reactButton,
+				note: this.p,
+				compact: true
+			});
+		},
+		menu() {
+			(this as any).os.new(MkNoteMenu, {
+				source: this.$refs.menuButton,
+				note: this.p,
+				compact: true
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+@import '~const.styl'
+
+.note
+	font-size 12px
+	border-bottom solid 1px #eaeaea
+
+	&:first-child
+		border-radius 8px 8px 0 0
+
+		> .renote
+			border-radius 8px 8px 0 0
+
+	&:last-of-type
+		border-bottom none
+
+	@media (min-width 350px)
+		font-size 14px
+
+	@media (min-width 500px)
+		font-size 16px
+
+	> .renote
+		color #9dbb00
+		background linear-gradient(to bottom, #edfde2 0%, #fff 100%)
+
+		> p
+			margin 0
+			padding 8px 16px
+			line-height 28px
+
+			@media (min-width 500px)
+				padding 16px
+
+			.avatar-anchor
+				display inline-block
+
+				.avatar
+					vertical-align bottom
+					width 28px
+					height 28px
+					margin 0 8px 0 0
+					border-radius 6px
+
+			[data-fa]
+				margin-right 4px
+
+			.name
+				font-weight bold
+
+		> .mk-time
+			position absolute
+			top 8px
+			right 16px
+			font-size 0.9em
+			line-height 28px
+
+			@media (min-width 500px)
+				top 16px
+
+		& + article
+			padding-top 8px
+
+	> .reply-to
+		background rgba(0, 0, 0, 0.0125)
+
+		> .mk-note-preview
+			background transparent
+
+	> article
+		padding 14px 16px 9px 16px
+
+		&:after
+			content ""
+			display block
+			clear both
+
+		> .avatar-anchor
+			display block
+			float left
+			margin 0 10px 8px 0
+			position -webkit-sticky
+			position sticky
+			top 62px
+
+			@media (min-width 500px)
+				margin-right 16px
+
+			> .avatar
+				display block
+				width 48px
+				height 48px
+				margin 0
+				border-radius 6px
+				vertical-align bottom
+
+				@media (min-width 500px)
+					width 58px
+					height 58px
+					border-radius 8px
+
+		> .main
+			float left
+			width calc(100% - 58px)
+
+			@media (min-width 500px)
+				width calc(100% - 74px)
+
+			> header
+				display flex
+				align-items center
+				white-space nowrap
+
+				@media (min-width 500px)
+					margin-bottom 2px
+
+				> .name
+					display block
+					margin 0 0.5em 0 0
+					padding 0
+					overflow hidden
+					color #627079
+					font-size 1em
+					font-weight bold
+					text-decoration none
+					text-overflow ellipsis
+
+					&:hover
+						text-decoration underline
+
+				> .is-bot
+					margin 0 0.5em 0 0
+					padding 1px 6px
+					font-size 12px
+					color #aaa
+					border solid 1px #ddd
+					border-radius 3px
+
+				> .username
+					margin 0 0.5em 0 0
+					color #ccc
+
+				> .info
+					margin-left auto
+					font-size 0.9em
+
+					> .mobile
+						margin-right 6px
+						color #c0c0c0
+
+					> .created-at
+						color #c0c0c0
+
+			> .body
+
+				> .text
+					display block
+					margin 0
+					padding 0
+					overflow-wrap break-word
+					font-size 1.1em
+					color #717171
+
+					>>> .quote
+						margin 8px
+						padding 6px 12px
+						color #aaa
+						border-left solid 3px #eee
+
+					> .reply
+						margin-right 8px
+						color #717171
+
+					> .rp
+						margin-left 4px
+						font-style oblique
+						color #a0bf46
+
+					[data-is-me]:after
+						content "you"
+						padding 0 4px
+						margin-left 4px
+						font-size 80%
+						color $theme-color-foreground
+						background $theme-color
+						border-radius 4px
+
+				.mk-url-preview
+					margin-top 8px
+
+				> .channel
+					margin 0
+
+				> .tags
+					margin 4px 0 0 0
+
+					> *
+						display inline-block
+						margin 0 8px 0 0
+						padding 2px 8px 2px 16px
+						font-size 90%
+						color #8d969e
+						background #edf0f3
+						border-radius 4px
+
+						&:before
+							content ""
+							display block
+							position absolute
+							top 0
+							bottom 0
+							left 4px
+							width 8px
+							height 8px
+							margin auto 0
+							background #fff
+							border-radius 100%
+
+				> .media
+					> img
+						display block
+						max-width 100%
+
+				> .location
+					margin 4px 0
+					font-size 12px
+					color #ccc
+
+				> .map
+					width 100%
+					height 200px
+
+					&:empty
+						display none
+
+				> .app
+					font-size 12px
+					color #ccc
+
+				> .mk-poll
+					font-size 80%
+
+				> .renote
+					margin 8px 0
+
+					> .mk-note-preview
+						padding 16px
+						border dashed 1px #c0dac6
+						border-radius 8px
+
+			> footer
+				> button
+					margin 0
+					padding 8px
+					background transparent
+					border none
+					box-shadow none
+					font-size 1em
+					color #ddd
+					cursor pointer
+
+					&:not(:last-child)
+						margin-right 28px
+
+					&:hover
+						color #666
+
+					> .count
+						display inline
+						margin 0 0 0 8px
+						color #999
+
+					&.reacted
+						color $theme-color
+
+					&.menu
+						@media (max-width 350px)
+							display none
+
+</style>
+
+<style lang="stylus" module>
+.text
+	code
+		padding 4px 8px
+		margin 0 0.5em
+		font-size 80%
+		color #525252
+		background #f8f8f8
+		border-radius 2px
+
+	pre > code
+		padding 16px
+		margin 0
+</style>
diff --git a/src/client/app/mobile/views/components/ui.header.vue b/src/client/app/mobile/views/components/ui.header.vue
index f664341cd..f1b24bf2d 100644
--- a/src/client/app/mobile/views/components/ui.header.vue
+++ b/src/client/app/mobile/views/components/ui.header.vue
@@ -3,7 +3,7 @@
 	<mk-special-message/>
 	<div class="main" ref="main">
 		<div class="backdrop"></div>
-		<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ name }}</b>さん</p>
+		<p ref="welcomeback" v-if="os.isSignedIn">おかえりなさい、<b>{{ os.i | userName }}</b>さん</p>
 		<div class="content" ref="mainContainer">
 			<button class="nav" @click="$parent.isDrawerOpening = true">%fa:bars%</button>
 			<template v-if="hasUnreadNotifications || hasUnreadMessagingMessages || hasGameInvitations">%fa:circle%</template>
@@ -19,15 +19,9 @@
 <script lang="ts">
 import Vue from 'vue';
 import * as anime from 'animejs';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['func'],
-	computed: {
-		name() {
-			return getUserName(this.os.i);
-		}
-	},
 	data() {
 		return {
 			hasUnreadNotifications: false,
diff --git a/src/client/app/mobile/views/components/ui.nav.vue b/src/client/app/mobile/views/components/ui.nav.vue
index 61dd8ca9d..764f9374e 100644
--- a/src/client/app/mobile/views/components/ui.nav.vue
+++ b/src/client/app/mobile/views/components/ui.nav.vue
@@ -11,7 +11,7 @@
 		<div class="body" v-if="isOpen">
 			<router-link class="me" v-if="os.isSignedIn" :to="`/@${os.i.username}`">
 				<img class="avatar" :src="`${os.i.avatarUrl}?thumbnail&size=128`" alt="avatar"/>
-				<p class="name">{{ name }}</p>
+				<p class="name">{{ os.i | userName }}</p>
 			</router-link>
 			<div class="links">
 				<ul>
@@ -39,16 +39,10 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { docsUrl, chUrl, lang } from '../../../config';
-import getUserName from '../../../../../renderers/get-user-name';
+import { docsUrl, lang } from '../../../config';
 
 export default Vue.extend({
 	props: ['isOpen'],
-	computed: {
-		name() {
-			return getUserName(this.os.i);
-		}
-	},
 	data() {
 		return {
 			hasUnreadNotifications: false,
@@ -56,8 +50,7 @@ export default Vue.extend({
 			hasGameInvitations: false,
 			connection: null,
 			connectionId: null,
-			aboutUrl: `${docsUrl}/${lang}/about`,
-			chUrl
+			aboutUrl: `${docsUrl}/${lang}/about`
 		};
 	},
 	mounted() {
diff --git a/src/client/app/mobile/views/components/user-card.vue b/src/client/app/mobile/views/components/user-card.vue
index e8698a62f..432560a54 100644
--- a/src/client/app/mobile/views/components/user-card.vue
+++ b/src/client/app/mobile/views/components/user-card.vue
@@ -1,31 +1,21 @@
 <template>
 <div class="mk-user-card">
 	<header :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''">
-		<a :href="`/@${acct}`">
+		<a :href="user | userPage">
 			<img :src="`${user.avatarUrl}?thumbnail&size=200`" alt="avatar"/>
 		</a>
 	</header>
-	<a class="name" :href="`/@${acct}`" target="_blank">{{ name }}</a>
-	<p class="username">@{{ acct }}</p>
+	<a class="name" :href="user | userPage" target="_blank">{{ user | userName }}</a>
+	<p class="username">@{{ user | acct }}</p>
 	<mk-follow-button :user="user"/>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['user'],
-	computed: {
-		acct() {
-			return getAcct(this.user);
-		},
-		name() {
-			return getUserName(this.user);
-		}
-	}
+	props: ['user']
 });
 </script>
 
diff --git a/src/client/app/mobile/views/components/user-preview.vue b/src/client/app/mobile/views/components/user-preview.vue
index 72a6bcf8a..23a83b5e3 100644
--- a/src/client/app/mobile/views/components/user-preview.vue
+++ b/src/client/app/mobile/views/components/user-preview.vue
@@ -1,12 +1,12 @@
 <template>
 <div class="mk-user-preview">
-	<router-link class="avatar-anchor" :to="`/@${acct}`">
+	<router-link class="avatar-anchor" :to="user | userPage">
 		<img class="avatar" :src="`${user.avatarUrl}?thumbnail&size=64`" alt="avatar"/>
 	</router-link>
 	<div class="main">
 		<header>
-			<router-link class="name" :to="`/@${acct}`">{{ name }}</router-link>
-			<span class="username">@{{ acct }}</span>
+			<router-link class="name" :to="user | userPage">{{ user | userName }}</router-link>
+			<span class="username">@{{ user | acct }}</span>
 		</header>
 		<div class="body">
 			<div class="description">{{ user.description }}</div>
@@ -17,19 +17,9 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
-	props: ['user'],
-	computed: {
-		acct() {
-			return getAcct(this.user);
-		},
-		name() {
-			return getUserName(this.user);
-		}
-	}
+	props: ['user']
 });
 </script>
 
diff --git a/src/client/app/mobile/views/pages/following.vue b/src/client/app/mobile/views/pages/following.vue
index cc2442e47..d0dcc117c 100644
--- a/src/client/app/mobile/views/pages/following.vue
+++ b/src/client/app/mobile/views/pages/following.vue
@@ -20,7 +20,6 @@
 import Vue from 'vue';
 import Progress from '../../../common/scripts/loading';
 import parseAcct from '../../../../../acct/parse';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -30,8 +29,8 @@ export default Vue.extend({
 		};
 	},
 	computed: {
-		name() {
-			return getUserName(this.user);
+		name(): string {
+			return Vue.filter('userName')(this.user);
 		}
 	},
 	watch: {
diff --git a/src/client/app/mobile/views/pages/messaging-room.vue b/src/client/app/mobile/views/pages/messaging-room.vue
index ae6662dc0..3b6fb11db 100644
--- a/src/client/app/mobile/views/pages/messaging-room.vue
+++ b/src/client/app/mobile/views/pages/messaging-room.vue
@@ -1,7 +1,7 @@
 <template>
 <mk-ui>
 	<span slot="header">
-		<template v-if="user">%fa:R comments%{{ name }}</template>
+		<template v-if="user">%fa:R comments%{{ user | userName }}</template>
 		<template v-else><mk-ellipsis/></template>
 	</span>
 	<mk-messaging-room v-if="!fetching" :user="user" :is-naked="true"/>
@@ -11,7 +11,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import parseAcct from '../../../../../acct/parse';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -20,11 +19,6 @@ export default Vue.extend({
 			user: null
 		};
 	},
-	computed: {
-		name() {
-			return getUserName(this.user);
-		}
-	},
 	watch: {
 		$route: 'fetch'
 	},
@@ -39,7 +33,7 @@ export default Vue.extend({
 				this.user = user;
 				this.fetching = false;
 
-				document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${this.name} | Misskey`;
+				document.title = `%i18n:mobile.tags.mk-messaging-room-page.message%: ${Vue.filter('userName')(this.user)} | Misskey`;
 			});
 		}
 	}
diff --git a/src/client/app/mobile/views/pages/settings.vue b/src/client/app/mobile/views/pages/settings.vue
index 58a9d4e37..8d248f5cb 100644
--- a/src/client/app/mobile/views/pages/settings.vue
+++ b/src/client/app/mobile/views/pages/settings.vue
@@ -20,7 +20,6 @@
 <script lang="ts">
 import Vue from 'vue';
 import { version, codename } from '../../../config';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	data() {
@@ -30,8 +29,8 @@ export default Vue.extend({
 		};
 	},
 	computed: {
-		name() {
-			return getUserName(this.os.i);
+		name(): string {
+			return Vue.filter('userName')((this as any).os.i);
 		}
 	},
 	mounted() {
diff --git a/src/client/app/mobile/views/pages/user.vue b/src/client/app/mobile/views/pages/user.vue
index aac8b628b..fb9220ee8 100644
--- a/src/client/app/mobile/views/pages/user.vue
+++ b/src/client/app/mobile/views/pages/user.vue
@@ -1,6 +1,6 @@
 <template>
 <mk-ui>
-	<span slot="header" v-if="!fetching">%fa:user% {{ user }}</span>
+	<span slot="header" v-if="!fetching">%fa:user% {{ user | userName }}</span>
 	<main v-if="!fetching">
 		<header>
 			<div class="banner" :style="user.bannerUrl ? `background-image: url(${user.bannerUrl}?thumbnail&size=1024)` : ''"></div>
@@ -12,8 +12,8 @@
 					<mk-follow-button v-if="os.isSignedIn && os.i.id != user.id" :user="user"/>
 				</div>
 				<div class="title">
-					<h1>{{ getUserName(user) }}</h1>
-					<span class="username">@{{ getAcct(user) }}</span>
+					<h1>{{ user | userName }}</h1>
+					<span class="username">@{{ user | acct }}</span>
 					<span class="followed" v-if="user.isFollowed">%i18n:mobile.tags.mk-user.follows-you%</span>
 				</div>
 				<div class="description">{{ user.description }}</div>
@@ -61,8 +61,6 @@
 import Vue from 'vue';
 import * as age from 's-age';
 import parseAcct from '../../../../../acct/parse';
-import getAcct from '../../../../../acct/render';
-import getUserName from '../../../../../renderers/get-user-name';
 import Progress from '../../../common/scripts/loading';
 import XHome from './user/home.vue';
 
@@ -74,9 +72,7 @@ export default Vue.extend({
 		return {
 			fetching: true,
 			user: null,
-			page: 'home',
-			getAcct,
-			getUserName
+			page: 'home'
 		};
 	},
 	computed: {
@@ -102,7 +98,7 @@ export default Vue.extend({
 				this.fetching = false;
 
 				Progress.done();
-				document.title = this.getUserName(this.user) + ' | Misskey';
+				document.title = Vue.filter('userName')(this.user) + ' | Misskey';
 			});
 		}
 	}
diff --git a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
index 1b128e2f2..2841c0d63 100644
--- a/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
+++ b/src/client/app/mobile/views/pages/user/home.followers-you-know.vue
@@ -2,8 +2,8 @@
 <div class="root followers-you-know">
 	<p class="initializing" v-if="fetching">%fa:spinner .pulse .fw%%i18n:mobile.tags.mk-user-overview-followers-you-know.loading%<mk-ellipsis/></p>
 	<div v-if="!fetching && users.length > 0">
-		<a v-for="user in users" :key="user.id" :href="`/@${getAcct(user)}`">
-			<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="getUserName(user)"/>
+		<a v-for="user in users" :key="user.id" :href="user | userPage">
+			<img :src="`${user.avatarUrl}?thumbnail&size=64`" :alt="user | userName"/>
 		</a>
 	</div>
 	<p class="empty" v-if="!fetching && users.length == 0">%i18n:mobile.tags.mk-user-overview-followers-you-know.no-users%</p>
@@ -12,8 +12,6 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import getAcct from '../../../../../../acct/render';
-import getUserName from '../../../../../../renderers/get-user-name';
 
 export default Vue.extend({
 	props: ['user'],
@@ -23,14 +21,6 @@ export default Vue.extend({
 			users: []
 		};
 	},
-	computed: {
-		name() {
-			return getUserName(this.user);
-		}
-	},
-	methods: {
-		getAcct
-	},
 	mounted() {
 		(this as any).api('users/followers', {
 			userId: this.user.id,
diff --git a/src/client/app/mobile/views/widgets/profile.vue b/src/client/app/mobile/views/widgets/profile.vue
index bd257a3ff..502f886ce 100644
--- a/src/client/app/mobile/views/widgets/profile.vue
+++ b/src/client/app/mobile/views/widgets/profile.vue
@@ -8,23 +8,16 @@
 			:src="`${os.i.avatarUrl}?thumbnail&size=96`"
 			alt="avatar"
 		/>
-		<router-link :class="$style.name" :to="`/@${os.i.username}`">{{ name }}</router-link>
+		<router-link :class="$style.name" :to="os.i | userPage">{{ os.i | userName }}</router-link>
 	</mk-widget-container>
 </div>
 </template>
 
 <script lang="ts">
 import define from '../../../common/define-widget';
-import getUserName from '../../../../../renderers/get-user-name';
 
 export default define({
 	name: 'profile'
-}).extend({
-	computed: {
-		name() {
-			return getUserName(this.os.i);
-		}
-	}
 });
 </script>