From caa47cb38cfc3950539c78ca2e70f2c50e815d2c Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 30 Oct 2017 22:12:10 +0900
Subject: [PATCH 1/2] =?UTF-8?q?=E6=9C=AA=E8=AA=AD=E3=81=AE=E9=80=9A?=
 =?UTF-8?q?=E7=9F=A5=E3=81=8C=E3=81=82=E3=82=8B=E5=A0=B4=E5=90=88=E3=82=A2?=
 =?UTF-8?q?=E3=82=A4=E3=82=B3=E3=83=B3=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99?=
 =?UTF-8?q?=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 CHANGELOG.md                                  |   4 +
 locales/en.yml                                |   1 +
 locales/ja.yml                                |   1 +
 src/api/common/read-notification.ts           |  52 +++
 src/api/endpoints.ts                          |  10 +-
 src/api/endpoints/i/notifications.ts          |  14 +-
 .../notifications/get_unread_count.ts         |  23 ++
 .../endpoints/notifications/mark_as_read.ts   |  47 ---
 .../notifications/mark_as_read_all.ts         |  32 ++
 src/api/models/notification.ts                |   5 +
 src/api/stream/home.ts                        |   6 +
 src/web/app/desktop/tags/notifications.tag    |   6 +
 src/web/app/mobile/tags/index.js              |   2 -
 src/web/app/mobile/tags/notifications.tag     |   6 +
 .../app/mobile/tags/page/notifications.tag    |  14 +
 src/web/app/mobile/tags/ui-header.tag         | 156 --------
 src/web/app/mobile/tags/ui-nav.tag            | 170 --------
 src/web/app/mobile/tags/ui.tag                | 368 ++++++++++++++++++
 18 files changed, 525 insertions(+), 392 deletions(-)
 create mode 100644 src/api/common/read-notification.ts
 create mode 100644 src/api/endpoints/notifications/get_unread_count.ts
 delete mode 100644 src/api/endpoints/notifications/mark_as_read.ts
 create mode 100644 src/api/endpoints/notifications/mark_as_read_all.ts
 delete mode 100644 src/web/app/mobile/tags/ui-header.tag
 delete mode 100644 src/web/app/mobile/tags/ui-nav.tag

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ca41d016c1..bf5c1fcb2c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
+unreleased
+----------
+* New: 未読の通知がある場合アイコンを表示するように
+
 2747 (2017/10/25)
 -----------------
 * Fix: 非ログイン状態ですべてのページが致命的な問題を発生させる (#89)
diff --git a/locales/en.yml b/locales/en.yml
index 03d5306d3e..020813ddbb 100644
--- a/locales/en.yml
+++ b/locales/en.yml
@@ -389,6 +389,7 @@ mobile:
 
     mk-notifications-page:
       notifications: "Notifications"
+      read-all: "Are you sure you want to mark as read all your notifications?"
 
     mk-post-page:
       title: "Post"
diff --git a/locales/ja.yml b/locales/ja.yml
index b640f0f248..1b3058fe02 100644
--- a/locales/ja.yml
+++ b/locales/ja.yml
@@ -389,6 +389,7 @@ mobile:
 
     mk-notifications-page:
       notifications: "通知"
+      read-all: "すべての通知を既読にしますか?"
 
     mk-post-page:
       title: "投稿"
diff --git a/src/api/common/read-notification.ts b/src/api/common/read-notification.ts
new file mode 100644
index 0000000000..3009cc5d08
--- /dev/null
+++ b/src/api/common/read-notification.ts
@@ -0,0 +1,52 @@
+import * as mongo from 'mongodb';
+import { default as Notification, INotification } from '../models/notification';
+import publishUserStream from '../event';
+
+/**
+ * Mark as read notification(s)
+ */
+export default (
+	user: string | mongo.ObjectID,
+	message: string | string[] | INotification | INotification[] | mongo.ObjectID | mongo.ObjectID[]
+) => new Promise<any>(async (resolve, reject) => {
+
+	const userId = mongo.ObjectID.prototype.isPrototypeOf(user)
+		? user
+		: new mongo.ObjectID(user);
+
+	const ids: mongo.ObjectID[] = Array.isArray(message)
+		? mongo.ObjectID.prototype.isPrototypeOf(message[0])
+			? (message as mongo.ObjectID[])
+			: typeof message[0] === 'string'
+				? (message as string[]).map(m => new mongo.ObjectID(m))
+				: (message as INotification[]).map(m => m._id)
+		: mongo.ObjectID.prototype.isPrototypeOf(message)
+			? [(message as mongo.ObjectID)]
+			: typeof message === 'string'
+				? [new mongo.ObjectID(message)]
+				: [(message as INotification)._id];
+
+	// Update documents
+	await Notification.update({
+		_id: { $in: ids },
+		is_read: false
+	}, {
+		$set: {
+			is_read: true
+		}
+	}, {
+		multi: true
+	});
+
+	// Calc count of my unread notifications
+	const count = await Notification
+		.count({
+			notifiee_id: userId,
+			is_read: false
+		});
+
+	if (count == 0) {
+		// 全ての(いままで未読だった)通知を(これで)読みましたよというイベントを発行
+		publishUserStream(userId, 'read_all_notifications');
+	}
+});
diff --git a/src/api/endpoints.ts b/src/api/endpoints.ts
index f05762340c..29a97bcb8a 100644
--- a/src/api/endpoints.ts
+++ b/src/api/endpoints.ts
@@ -195,6 +195,11 @@ const endpoints: Endpoint[] = [
 		withCredential: true,
 		kind: 'notification-read'
 	},
+	{
+		name: 'notifications/get_unread_count',
+		withCredential: true,
+		kind: 'notification-read'
+	},
 	{
 		name: 'notifications/delete',
 		withCredential: true,
@@ -205,11 +210,6 @@ const endpoints: Endpoint[] = [
 		withCredential: true,
 		kind: 'notification-write'
 	},
-	{
-		name: 'notifications/mark_as_read',
-		withCredential: true,
-		kind: 'notification-write'
-	},
 	{
 		name: 'notifications/mark_as_read_all',
 		withCredential: true,
diff --git a/src/api/endpoints/i/notifications.ts b/src/api/endpoints/i/notifications.ts
index 5575fb7412..607e0768a4 100644
--- a/src/api/endpoints/i/notifications.ts
+++ b/src/api/endpoints/i/notifications.ts
@@ -5,6 +5,7 @@ import $ from 'cafy';
 import Notification from '../../models/notification';
 import serialize from '../../serializers/notification';
 import getFriends from '../../common/get-friends';
+import read from '../../common/read-notification';
 
 /**
  * Get notifications
@@ -91,17 +92,6 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 
 	// Mark as read all
 	if (notifications.length > 0 && markAsRead) {
-		const ids = notifications
-			.filter(x => x.is_read == false)
-			.map(x => x._id);
-
-		// Update documents
-		await Notification.update({
-			_id: { $in: ids }
-		}, {
-			$set: { is_read: true }
-		}, {
-			multi: true
-		});
+		read(user._id, notifications);
 	}
 });
diff --git a/src/api/endpoints/notifications/get_unread_count.ts b/src/api/endpoints/notifications/get_unread_count.ts
new file mode 100644
index 0000000000..9514e78713
--- /dev/null
+++ b/src/api/endpoints/notifications/get_unread_count.ts
@@ -0,0 +1,23 @@
+/**
+ * Module dependencies
+ */
+import Notification from '../../models/notification';
+
+/**
+ * Get count of unread notifications
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	const count = await Notification
+		.count({
+			notifiee_id: user._id,
+			is_read: false
+		});
+
+	res({
+		count: count
+	});
+});
diff --git a/src/api/endpoints/notifications/mark_as_read.ts b/src/api/endpoints/notifications/mark_as_read.ts
deleted file mode 100644
index 5cce33e850..0000000000
--- a/src/api/endpoints/notifications/mark_as_read.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Module dependencies
- */
-import $ from 'cafy';
-import Notification from '../../models/notification';
-import serialize from '../../serializers/notification';
-import event from '../../event';
-
-/**
- * Mark as read a notification
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
- */
-module.exports = (params, user) => new Promise(async (res, rej) => {
-	const [notificationId, notificationIdErr] = $(params.notification_id).id().$;
-	if (notificationIdErr) return rej('invalid notification_id param');
-
-	// Get notification
-	const notification = await Notification
-		.findOne({
-			_id: notificationId,
-			i: user._id
-		});
-
-	if (notification === null) {
-		return rej('notification-not-found');
-	}
-
-	// Update
-	notification.is_read = true;
-	Notification.update({ _id: notification._id }, {
-		$set: {
-			is_read: true
-		}
-	});
-
-	// Response
-	res();
-
-	// Serialize
-	const notificationObj = await serialize(notification);
-
-	// Publish read_notification event
-	event(user._id, 'read_notification', notificationObj);
-});
diff --git a/src/api/endpoints/notifications/mark_as_read_all.ts b/src/api/endpoints/notifications/mark_as_read_all.ts
new file mode 100644
index 0000000000..3550e344c4
--- /dev/null
+++ b/src/api/endpoints/notifications/mark_as_read_all.ts
@@ -0,0 +1,32 @@
+/**
+ * Module dependencies
+ */
+import Notification from '../../models/notification';
+import event from '../../event';
+
+/**
+ * Mark as read all notifications
+ *
+ * @param {any} params
+ * @param {any} user
+ * @return {Promise<any>}
+ */
+module.exports = (params, user) => new Promise(async (res, rej) => {
+	// Update documents
+	await Notification.update({
+		notifiee_id: user._id,
+		is_read: false
+	}, {
+		$set: {
+			is_read: true
+		}
+	}, {
+		multi: true
+	});
+
+	// Response
+	res();
+
+	// 全ての通知を読みましたよというイベントを発行
+	event(user._id, 'read_all_notifications');
+});
diff --git a/src/api/models/notification.ts b/src/api/models/notification.ts
index 1c1f429a0d..1065e8baaa 100644
--- a/src/api/models/notification.ts
+++ b/src/api/models/notification.ts
@@ -1,3 +1,8 @@
+import * as mongo from 'mongodb';
 import db from '../../db/mongodb';
 
 export default db.get('notifications') as any; // fuck type definition
+
+export interface INotification {
+	_id: mongo.ObjectID;
+}
diff --git a/src/api/stream/home.ts b/src/api/stream/home.ts
index d5fe01c261..7c8f3bfec8 100644
--- a/src/api/stream/home.ts
+++ b/src/api/stream/home.ts
@@ -4,6 +4,7 @@ import * as debug from 'debug';
 
 import User from '../models/user';
 import serializePost from '../serializers/post';
+import readNotification from '../common/read-notification';
 
 const log = debug('misskey');
 
@@ -45,6 +46,11 @@ export default function homeStream(request: websocket.request, connection: webso
 				});
 				break;
 
+			case 'read_notification':
+				if (!msg.id) return;
+				readNotification(user._id, msg.id);
+				break;
+
 			case 'capture':
 				if (!msg.id) return;
 				const postId = msg.id;
diff --git a/src/web/app/desktop/tags/notifications.tag b/src/web/app/desktop/tags/notifications.tag
index 1046358ce9..a4f66105a8 100644
--- a/src/web/app/desktop/tags/notifications.tag
+++ b/src/web/app/desktop/tags/notifications.tag
@@ -252,6 +252,12 @@
 		});
 
 		this.onNotification = notification => {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.stream.send({
+				type: 'read_notification',
+				id: notification.id
+			});
+
 			this.notifications.unshift(notification);
 			this.update();
 		};
diff --git a/src/web/app/mobile/tags/index.js b/src/web/app/mobile/tags/index.js
index c5aafd20ba..a79f4f7e7e 100644
--- a/src/web/app/mobile/tags/index.js
+++ b/src/web/app/mobile/tags/index.js
@@ -1,6 +1,4 @@
 require('./ui.tag');
-require('./ui-header.tag');
-require('./ui-nav.tag');
 require('./page/entrance.tag');
 require('./page/entrance/signin.tag');
 require('./page/entrance/signup.tag');
diff --git a/src/web/app/mobile/tags/notifications.tag b/src/web/app/mobile/tags/notifications.tag
index 7370aa84d3..2e95990314 100644
--- a/src/web/app/mobile/tags/notifications.tag
+++ b/src/web/app/mobile/tags/notifications.tag
@@ -123,6 +123,12 @@
 		});
 
 		this.onNotification = notification => {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.stream.send({
+				type: 'read_notification',
+				id: notification.id
+			});
+
 			this.notifications.unshift(notification);
 			this.update();
 		};
diff --git a/src/web/app/mobile/tags/page/notifications.tag b/src/web/app/mobile/tags/page/notifications.tag
index 06a5be039f..743de04393 100644
--- a/src/web/app/mobile/tags/page/notifications.tag
+++ b/src/web/app/mobile/tags/page/notifications.tag
@@ -10,16 +10,30 @@
 		import ui from '../../scripts/ui-event';
 		import Progress from '../../../common/scripts/loading';
 
+		this.mixin('api');
+
 		this.on('mount', () => {
 			document.title = 'Misskey | %i18n:mobile.tags.mk-notifications-page.notifications%';
 			ui.trigger('title', '<i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-notifications-page.notifications%');
 			document.documentElement.style.background = '#313a42';
 
+			ui.trigger('func', () => {
+				this.readAll();
+			}, 'check');
+
 			Progress.start();
 
 			this.refs.ui.refs.notifications.on('fetched', () => {
 				Progress.done();
 			});
 		});
+
+		this.readAll = () => {
+			const ok = window.confirm('%i18n:mobile.tags.mk-notifications-page.read-all%');
+
+			if (!ok) return;
+
+			this.api('notifications/mark_as_read_all');
+		};
 	</script>
 </mk-notifications-page>
diff --git a/src/web/app/mobile/tags/ui-header.tag b/src/web/app/mobile/tags/ui-header.tag
deleted file mode 100644
index 10b44b2153..0000000000
--- a/src/web/app/mobile/tags/ui-header.tag
+++ /dev/null
@@ -1,156 +0,0 @@
-<mk-ui-header>
-	<mk-special-message/>
-	<div class="main">
-		<div class="backdrop"></div>
-		<div class="content">
-			<button class="nav" onclick={ parent.toggleDrawer }><i class="fa fa-bars"></i></button>
-			<i class="fa fa-circle" if={ hasUnreadMessagingMessages }></i>
-			<h1 ref="title">Misskey</h1>
-			<button if={ func } onclick={ func }><i class="fa fa-{ funcIcon }"></i></button>
-		</div>
-	</div>
-	<style>
-		:scope
-			$height = 48px
-
-			display block
-			position fixed
-			top 0
-			z-index 1024
-			width 100%
-			box-shadow 0 1px 0 rgba(#000, 0.075)
-
-			> .main
-				color rgba(#fff, 0.9)
-
-				> .backdrop
-					position absolute
-					top 0
-					z-index 1023
-					width 100%
-					height $height
-					-webkit-backdrop-filter blur(12px)
-					backdrop-filter blur(12px)
-					background-color rgba(#1b2023, 0.75)
-
-				> .content
-					z-index 1024
-
-					> h1
-						display block
-						margin 0 auto
-						padding 0
-						width 100%
-						max-width calc(100% - 112px)
-						text-align center
-						font-size 1.1em
-						font-weight normal
-						line-height $height
-						white-space nowrap
-						overflow hidden
-						text-overflow ellipsis
-
-						> i
-						> .icon
-							margin-right 8px
-
-						> img
-							display inline-block
-							vertical-align bottom
-							width ($height - 16px)
-							height ($height - 16px)
-							margin 8px
-							border-radius 6px
-
-					> .nav
-						display block
-						position absolute
-						top 0
-						left 0
-						width $height
-						font-size 1.4em
-						line-height $height
-						border-right solid 1px rgba(#000, 0.1)
-
-						> i
-							transition all 0.2s ease
-
-					> i
-						position absolute
-						top 8px
-						left 8px
-						pointer-events none
-						font-size 10px
-						color $theme-color
-
-					> button:last-child
-						display block
-						position absolute
-						top 0
-						right 0
-						width $height
-						text-align center
-						font-size 1.4em
-						color inherit
-						line-height $height
-						border-left solid 1px rgba(#000, 0.1)
-
-	</style>
-	<script>
-		import ui from '../scripts/ui-event';
-
-		this.mixin('api');
-		this.mixin('stream');
-
-		this.func = null;
-		this.funcIcon = null;
-
-		this.on('mount', () => {
-			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
-
-			// Fetch count of unread messaging messages
-			this.api('messaging/unread').then(res => {
-				if (res.count > 0) {
-					this.update({
-						hasUnreadMessagingMessages: true
-					});
-				}
-			});
-		});
-
-		this.on('unmount', () => {
-			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
-
-			ui.off('title', this.setTitle);
-			ui.off('func', this.setFunc);
-		});
-
-		this.onReadAllMessagingMessages = () => {
-			this.update({
-				hasUnreadMessagingMessages: false
-			});
-		};
-
-		this.onUnreadMessagingMessage = () => {
-			this.update({
-				hasUnreadMessagingMessages: true
-			});
-		};
-
-		this.setTitle = title => {
-			this.refs.title.innerHTML = title;
-		};
-
-		this.setFunc = (fn, icon) => {
-			this.update({
-				func: fn,
-				funcIcon: icon
-			});
-		};
-
-		ui.on('title', this.setTitle);
-		ui.on('func', this.setFunc);
-	</script>
-</mk-ui-header>
diff --git a/src/web/app/mobile/tags/ui-nav.tag b/src/web/app/mobile/tags/ui-nav.tag
deleted file mode 100644
index 34235ba4f1..0000000000
--- a/src/web/app/mobile/tags/ui-nav.tag
+++ /dev/null
@@ -1,170 +0,0 @@
-<mk-ui-nav>
-	<div class="backdrop" onclick={ parent.toggleDrawer }></div>
-	<div class="body">
-		<a class="me" if={ SIGNIN } href={ '/' + I.username }>
-			<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
-			<p class="name">{ I.name }</p>
-		</a>
-		<div class="links">
-			<ul>
-				<li><a href="/"><i class="fa fa-home"></i>%i18n:mobile.tags.mk-ui-nav.home%<i class="fa fa-angle-right"></i></a></li>
-				<li><a href="/i/notifications"><i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-ui-nav.notifications%<i class="fa fa-angle-right"></i></a></li>
-				<li><a href="/i/messaging"><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="i fa fa-circle" if={ hasUnreadMessagingMessages }></i><i class="fa fa-angle-right"></i></a></li>
-			</ul>
-			<ul>
-				<li><a onclick={ search }><i class="fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="fa fa-angle-right"></i></a></li>
-			</ul>
-			<ul>
-				<li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li>
-			</ul>
-			<ul>
-				<li><a href="/i/settings"><i class="fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="fa fa-angle-right"></i></a></li>
-			</ul>
-		</div>
-		<a href={ CONFIG.aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
-	</div>
-	<style>
-		:scope
-			display none
-
-			.backdrop
-				position fixed
-				top 0
-				left 0
-				z-index 1025
-				width 100%
-				height 100%
-				background rgba(0, 0, 0, 0.2)
-
-			.body
-				position fixed
-				top 0
-				left 0
-				z-index 1026
-				width 240px
-				height 100%
-				overflow auto
-				-webkit-overflow-scrolling touch
-				color #777
-				background #fff
-
-			.me
-				display block
-				margin 0
-				padding 16px
-
-				.avatar
-					display inline
-					max-width 64px
-					border-radius 32px
-					vertical-align middle
-
-				.name
-					display block
-					margin 0 16px
-					position absolute
-					top 0
-					left 80px
-					padding 0
-					width calc(100% - 112px)
-					color #777
-					line-height 96px
-					overflow hidden
-					text-overflow ellipsis
-					white-space nowrap
-
-			ul
-				display block
-				margin 16px 0
-				padding 0
-				list-style none
-
-				&:first-child
-					margin-top 0
-
-				li
-					display block
-					font-size 1em
-					line-height 1em
-
-					a
-						display block
-						padding 0 20px
-						line-height 3rem
-						line-height calc(1rem + 30px)
-						color #777
-						text-decoration none
-
-						> i:first-child
-							margin-right 0.5em
-
-						> .i
-							margin-left 6px
-							vertical-align super
-							font-size 10px
-							color $theme-color
-
-						> i:last-child
-							position absolute
-							top 0
-							right 0
-							padding 0 20px
-							font-size 1.2em
-							line-height calc(1rem + 30px)
-							color #ccc
-
-			.about
-				margin 0
-				padding 1em 0
-				text-align center
-				font-size 0.8em
-				opacity 0.5
-
-				a
-					color #777
-
-	</style>
-	<script>
-		this.mixin('i');
-		this.mixin('page');
-		this.mixin('api');
-		this.mixin('stream');
-
-		this.on('mount', () => {
-			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
-
-			// Fetch count of unread messaging messages
-			this.api('messaging/unread').then(res => {
-				if (res.count > 0) {
-					this.update({
-						hasUnreadMessagingMessages: true
-					});
-				}
-			});
-		});
-
-		this.on('unmount', () => {
-			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
-			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
-		});
-
-		this.onReadAllMessagingMessages = () => {
-			this.update({
-				hasUnreadMessagingMessages: false
-			});
-		};
-
-		this.onUnreadMessagingMessage = () => {
-			this.update({
-				hasUnreadMessagingMessages: true
-			});
-		};
-
-		this.search = () => {
-			const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
-			if (query == null || query == '') return;
-			this.page('/search:' + query);
-		};
-	</script>
-</mk-ui-nav>
diff --git a/src/web/app/mobile/tags/ui.tag b/src/web/app/mobile/tags/ui.tag
index 9d9cd4d74a..fb8cbcdbd2 100644
--- a/src/web/app/mobile/tags/ui.tag
+++ b/src/web/app/mobile/tags/ui.tag
@@ -30,9 +30,377 @@
 		};
 
 		this.onStreamNotification = notification => {
+			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない
+			this.stream.send({
+				type: 'read_notification',
+				id: notification.id
+			});
+
 			riot.mount(document.body.appendChild(document.createElement('mk-notify')), {
 				notification: notification
 			});
 		};
 	</script>
 </mk-ui>
+
+<mk-ui-header>
+	<mk-special-message/>
+	<div class="main">
+		<div class="backdrop"></div>
+		<div class="content">
+			<button class="nav" onclick={ parent.toggleDrawer }><i class="fa fa-bars"></i></button>
+			<i class="fa fa-circle" if={ hasUnreadNotifications || hasUnreadMessagingMessages }></i>
+			<h1 ref="title">Misskey</h1>
+			<button if={ func } onclick={ func }><i class="fa fa-{ funcIcon }"></i></button>
+		</div>
+	</div>
+	<style>
+		:scope
+			$height = 48px
+
+			display block
+			position fixed
+			top 0
+			z-index 1024
+			width 100%
+			box-shadow 0 1px 0 rgba(#000, 0.075)
+
+			> .main
+				color rgba(#fff, 0.9)
+
+				> .backdrop
+					position absolute
+					top 0
+					z-index 1023
+					width 100%
+					height $height
+					-webkit-backdrop-filter blur(12px)
+					backdrop-filter blur(12px)
+					background-color rgba(#1b2023, 0.75)
+
+				> .content
+					z-index 1024
+
+					> h1
+						display block
+						margin 0 auto
+						padding 0
+						width 100%
+						max-width calc(100% - 112px)
+						text-align center
+						font-size 1.1em
+						font-weight normal
+						line-height $height
+						white-space nowrap
+						overflow hidden
+						text-overflow ellipsis
+
+						> i
+						> .icon
+							margin-right 8px
+
+						> img
+							display inline-block
+							vertical-align bottom
+							width ($height - 16px)
+							height ($height - 16px)
+							margin 8px
+							border-radius 6px
+
+					> .nav
+						display block
+						position absolute
+						top 0
+						left 0
+						width $height
+						font-size 1.4em
+						line-height $height
+						border-right solid 1px rgba(#000, 0.1)
+
+						> i
+							transition all 0.2s ease
+
+					> i
+						position absolute
+						top 8px
+						left 8px
+						pointer-events none
+						font-size 10px
+						color $theme-color
+
+					> button:last-child
+						display block
+						position absolute
+						top 0
+						right 0
+						width $height
+						text-align center
+						font-size 1.4em
+						color inherit
+						line-height $height
+						border-left solid 1px rgba(#000, 0.1)
+
+	</style>
+	<script>
+		import ui from '../scripts/ui-event';
+
+		this.mixin('api');
+		this.mixin('stream');
+
+		this.func = null;
+		this.funcIcon = null;
+
+		this.on('mount', () => {
+			this.stream.on('read_all_notifications', this.onReadAllNotifications);
+			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
+
+			// Fetch count of unread notifications
+			this.api('notifications/get_unread_count').then(res => {
+				if (res.count > 0) {
+					this.update({
+						hasUnreadNotifications: true
+					});
+				}
+			});
+
+			// Fetch count of unread messaging messages
+			this.api('messaging/unread').then(res => {
+				if (res.count > 0) {
+					this.update({
+						hasUnreadMessagingMessages: true
+					});
+				}
+			});
+		});
+
+		this.on('unmount', () => {
+			this.stream.off('read_all_notifications', this.onReadAllNotifications);
+			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
+
+			ui.off('title', this.setTitle);
+			ui.off('func', this.setFunc);
+		});
+
+		this.onReadAllNotifications = () => {
+			this.update({
+				hasUnreadNotifications: false
+			});
+		};
+
+		this.onReadAllMessagingMessages = () => {
+			this.update({
+				hasUnreadMessagingMessages: false
+			});
+		};
+
+		this.onUnreadMessagingMessage = () => {
+			this.update({
+				hasUnreadMessagingMessages: true
+			});
+		};
+
+		this.setTitle = title => {
+			this.refs.title.innerHTML = title;
+		};
+
+		this.setFunc = (fn, icon) => {
+			this.update({
+				func: fn,
+				funcIcon: icon
+			});
+		};
+
+		ui.on('title', this.setTitle);
+		ui.on('func', this.setFunc);
+	</script>
+</mk-ui-header>
+
+<mk-ui-nav>
+	<div class="backdrop" onclick={ parent.toggleDrawer }></div>
+	<div class="body">
+		<a class="me" if={ SIGNIN } href={ '/' + I.username }>
+			<img class="avatar" src={ I.avatar_url + '?thumbnail&size=128' } alt="avatar"/>
+			<p class="name">{ I.name }</p>
+		</a>
+		<div class="links">
+			<ul>
+				<li><a href="/"><i class="fa fa-home"></i>%i18n:mobile.tags.mk-ui-nav.home%<i class="fa fa-angle-right"></i></a></li>
+				<li><a href="/i/notifications"><i class="fa fa-bell-o"></i>%i18n:mobile.tags.mk-ui-nav.notifications%<i class="i fa fa-circle" if={ hasUnreadNotifications }></i><i class="fa fa-angle-right"></i></a></li>
+				<li><a href="/i/messaging"><i class="fa fa-comments-o"></i>%i18n:mobile.tags.mk-ui-nav.messaging%<i class="i fa fa-circle" if={ hasUnreadMessagingMessages }></i><i class="fa fa-angle-right"></i></a></li>
+			</ul>
+			<ul>
+				<li><a onclick={ search }><i class="fa fa-search"></i>%i18n:mobile.tags.mk-ui-nav.search%<i class="fa fa-angle-right"></i></a></li>
+			</ul>
+			<ul>
+				<li><a href="/i/drive"><i class="fa fa-cloud"></i>%i18n:mobile.tags.mk-ui-nav.drive%<i class="fa fa-angle-right"></i></a></li>
+			</ul>
+			<ul>
+				<li><a href="/i/settings"><i class="fa fa-cog"></i>%i18n:mobile.tags.mk-ui-nav.settings%<i class="fa fa-angle-right"></i></a></li>
+			</ul>
+		</div>
+		<a href={ CONFIG.aboutUrl }><p class="about">%i18n:mobile.tags.mk-ui-nav.about%</p></a>
+	</div>
+	<style>
+		:scope
+			display none
+
+			.backdrop
+				position fixed
+				top 0
+				left 0
+				z-index 1025
+				width 100%
+				height 100%
+				background rgba(0, 0, 0, 0.2)
+
+			.body
+				position fixed
+				top 0
+				left 0
+				z-index 1026
+				width 240px
+				height 100%
+				overflow auto
+				-webkit-overflow-scrolling touch
+				color #777
+				background #fff
+
+			.me
+				display block
+				margin 0
+				padding 16px
+
+				.avatar
+					display inline
+					max-width 64px
+					border-radius 32px
+					vertical-align middle
+
+				.name
+					display block
+					margin 0 16px
+					position absolute
+					top 0
+					left 80px
+					padding 0
+					width calc(100% - 112px)
+					color #777
+					line-height 96px
+					overflow hidden
+					text-overflow ellipsis
+					white-space nowrap
+
+			ul
+				display block
+				margin 16px 0
+				padding 0
+				list-style none
+
+				&:first-child
+					margin-top 0
+
+				li
+					display block
+					font-size 1em
+					line-height 1em
+
+					a
+						display block
+						padding 0 20px
+						line-height 3rem
+						line-height calc(1rem + 30px)
+						color #777
+						text-decoration none
+
+						> i:first-child
+							margin-right 0.5em
+
+						> .i
+							margin-left 6px
+							vertical-align super
+							font-size 10px
+							color $theme-color
+
+						> i:last-child
+							position absolute
+							top 0
+							right 0
+							padding 0 20px
+							font-size 1.2em
+							line-height calc(1rem + 30px)
+							color #ccc
+
+			.about
+				margin 0
+				padding 1em 0
+				text-align center
+				font-size 0.8em
+				opacity 0.5
+
+				a
+					color #777
+
+	</style>
+	<script>
+		this.mixin('i');
+		this.mixin('page');
+		this.mixin('api');
+		this.mixin('stream');
+
+		this.on('mount', () => {
+			this.stream.on('read_all_notifications', this.onReadAllNotifications);
+			this.stream.on('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.stream.on('unread_messaging_message', this.onUnreadMessagingMessage);
+
+			// Fetch count of unread notifications
+			this.api('notifications/get_unread_count').then(res => {
+				if (res.count > 0) {
+					this.update({
+						hasUnreadNotifications: true
+					});
+				}
+			});
+
+			// Fetch count of unread messaging messages
+			this.api('messaging/unread').then(res => {
+				if (res.count > 0) {
+					this.update({
+						hasUnreadMessagingMessages: true
+					});
+				}
+			});
+		});
+
+		this.on('unmount', () => {
+			this.stream.off('read_all_notifications', this.onReadAllNotifications);
+			this.stream.off('read_all_messaging_messages', this.onReadAllMessagingMessages);
+			this.stream.off('unread_messaging_message', this.onUnreadMessagingMessage);
+		});
+
+		this.onReadAllNotifications = () => {
+			this.update({
+				hasUnreadNotifications: false
+			});
+		};
+
+		this.onReadAllMessagingMessages = () => {
+			this.update({
+				hasUnreadMessagingMessages: false
+			});
+		};
+
+		this.onUnreadMessagingMessage = () => {
+			this.update({
+				hasUnreadMessagingMessages: true
+			});
+		};
+
+		this.search = () => {
+			const query = window.prompt('%i18n:mobile.tags.mk-ui-nav.search%');
+			if (query == null || query == '') return;
+			this.page('/search:' + query);
+		};
+	</script>
+</mk-ui-nav>

From 460c6d448bc98a4006bda810fdb30a59f5955d65 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 30 Oct 2017 22:12:52 +0900
Subject: [PATCH 2/2] v2752

---
 CHANGELOG.md | 4 ++--
 package.json | 2 +-
 2 files changed, 3 insertions(+), 3 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index bf5c1fcb2c..2f75462e5f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,8 +2,8 @@ ChangeLog (Release Notes)
 =========================
 主に notable な changes を書いていきます
 
-unreleased
-----------
+2752 (2017/10/30)
+-----------------
 * New: 未読の通知がある場合アイコンを表示するように
 
 2747 (2017/10/25)
diff --git a/package.json b/package.json
index 43a0159619..7a81bed7a6 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "misskey",
   "author": "syuilo <i@syuilo.com>",
-  "version": "0.0.2747",
+  "version": "0.0.2752",
   "license": "MIT",
   "description": "A miniblog-based SNS",
   "bugs": "https://github.com/syuilo/misskey/issues",