From a0e640b1189a55c28aafe7d586d531731ad450a4 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Tue, 17 Apr 2018 14:52:28 +0900
Subject: [PATCH] =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=82=AB=E3=83=AB=E3=82=BF?=
 =?UTF-8?q?=E3=82=A4=E3=83=A0=E3=83=A9=E3=82=A4=E3=83=B3=E3=81=A8=E3=82=B0?=
 =?UTF-8?q?=E3=83=AD=E3=83=BC=E3=83=90=E3=83=AB=E3=82=BF=E3=82=A4=E3=83=A0?=
 =?UTF-8?q?=E3=83=A9=E3=82=A4=E3=83=B3=E3=82=92=E5=AE=9F=E8=A3=85?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 src/client/app/common/mios.ts                 |  12 +-
 .../scripts/streaming/global-timeline.ts      |  34 ++++
 .../scripts/streaming/local-timeline.ts       |  34 ++++
 .../app/common/scripts/streaming/requests.ts  |  30 ---
 .../views/components/timeline.core.vue        | 175 ++++++++++++++++++
 .../app/desktop/views/components/timeline.vue | 148 ++++-----------
 src/models/mute.ts                            |   2 +-
 src/publishers/stream.ts                      |  16 +-
 src/server/api/endpoints.ts                   |  16 ++
 src/server/api/endpoints/mute/create.ts       |   5 +-
 src/server/api/endpoints/mute/delete.ts       |  15 +-
 .../api/endpoints/notes/global-timeline.ts    |  91 +++++++++
 .../api/endpoints/notes/local-timeline.ts     |  94 ++++++++++
 src/server/api/endpoints/notes/timeline.ts    |   9 +-
 src/server/api/stream/global-timeline.ts      |  39 ++++
 src/server/api/stream/home.ts                 |   7 +-
 src/server/api/stream/local-timeline.ts       |  39 ++++
 src/server/api/streaming.ts                   |   6 +-
 src/services/note/create.ts                   |  10 +-
 19 files changed, 599 insertions(+), 183 deletions(-)
 create mode 100644 src/client/app/common/scripts/streaming/global-timeline.ts
 create mode 100644 src/client/app/common/scripts/streaming/local-timeline.ts
 delete mode 100644 src/client/app/common/scripts/streaming/requests.ts
 create mode 100644 src/client/app/desktop/views/components/timeline.core.vue
 create mode 100644 src/server/api/endpoints/notes/global-timeline.ts
 create mode 100644 src/server/api/endpoints/notes/local-timeline.ts
 create mode 100644 src/server/api/stream/global-timeline.ts
 create mode 100644 src/server/api/stream/local-timeline.ts

diff --git a/src/client/app/common/mios.ts b/src/client/app/common/mios.ts
index 96a04bad3c..6d6d6b3e68 100644
--- a/src/client/app/common/mios.ts
+++ b/src/client/app/common/mios.ts
@@ -9,11 +9,12 @@ import Connection from './scripts/streaming/stream';
 import { HomeStreamManager } from './scripts/streaming/home';
 import { DriveStreamManager } from './scripts/streaming/drive';
 import { ServerStreamManager } from './scripts/streaming/server';
-import { RequestsStreamManager } from './scripts/streaming/requests';
 import { MessagingIndexStreamManager } from './scripts/streaming/messaging-index';
 import { OthelloStreamManager } from './scripts/streaming/othello';
 
 import Err from '../common/views/components/connect-failed.vue';
+import { LocalTimelineStreamManager } from './scripts/streaming/local-timeline';
+import { GlobalTimelineStreamManager } from './scripts/streaming/global-timeline';
 
 //#region api requests
 let spinner = null;
@@ -116,15 +117,17 @@ export default class MiOS extends EventEmitter {
 	 * Connection managers
 	 */
 	public streams: {
+		localTimelineStream: LocalTimelineStreamManager;
+		globalTimelineStream: GlobalTimelineStreamManager;
 		driveStream: DriveStreamManager;
 		serverStream: ServerStreamManager;
-		requestsStream: RequestsStreamManager;
 		messagingIndexStream: MessagingIndexStreamManager;
 		othelloStream: OthelloStreamManager;
 	} = {
+		localTimelineStream: null,
+		globalTimelineStream: null,
 		driveStream: null,
 		serverStream: null,
-		requestsStream: null,
 		messagingIndexStream: null,
 		othelloStream: null
 	};
@@ -231,13 +234,14 @@ export default class MiOS extends EventEmitter {
 	public async init(callback) {
 		//#region Init stream managers
 		this.streams.serverStream = new ServerStreamManager(this);
-		this.streams.requestsStream = new RequestsStreamManager(this);
 
 		this.once('signedin', () => {
 			// Init home stream manager
 			this.stream = new HomeStreamManager(this, this.i);
 
 			// Init other stream manager
+			this.streams.localTimelineStream = new LocalTimelineStreamManager(this, this.i);
+			this.streams.globalTimelineStream = new GlobalTimelineStreamManager(this, this.i);
 			this.streams.driveStream = new DriveStreamManager(this, this.i);
 			this.streams.messagingIndexStream = new MessagingIndexStreamManager(this, this.i);
 			this.streams.othelloStream = new OthelloStreamManager(this, this.i);
diff --git a/src/client/app/common/scripts/streaming/global-timeline.ts b/src/client/app/common/scripts/streaming/global-timeline.ts
new file mode 100644
index 0000000000..452ddbac03
--- /dev/null
+++ b/src/client/app/common/scripts/streaming/global-timeline.ts
@@ -0,0 +1,34 @@
+import Stream from './stream';
+import StreamManager from './stream-manager';
+import MiOS from '../../mios';
+
+/**
+ * Global timeline stream connection
+ */
+export class GlobalTimelineStream extends Stream {
+	constructor(os: MiOS, me) {
+		super(os, 'global-timeline', {
+			i: me.token
+		});
+	}
+}
+
+export class GlobalTimelineStreamManager extends StreamManager<GlobalTimelineStream> {
+	private me;
+	private os: MiOS;
+
+	constructor(os: MiOS, me) {
+		super();
+
+		this.me = me;
+		this.os = os;
+	}
+
+	public getConnection() {
+		if (this.connection == null) {
+			this.connection = new GlobalTimelineStream(this.os, this.me);
+		}
+
+		return this.connection;
+	}
+}
diff --git a/src/client/app/common/scripts/streaming/local-timeline.ts b/src/client/app/common/scripts/streaming/local-timeline.ts
new file mode 100644
index 0000000000..3d04e05cd4
--- /dev/null
+++ b/src/client/app/common/scripts/streaming/local-timeline.ts
@@ -0,0 +1,34 @@
+import Stream from './stream';
+import StreamManager from './stream-manager';
+import MiOS from '../../mios';
+
+/**
+ * Local timeline stream connection
+ */
+export class LocalTimelineStream extends Stream {
+	constructor(os: MiOS, me) {
+		super(os, 'local-timeline', {
+			i: me.token
+		});
+	}
+}
+
+export class LocalTimelineStreamManager extends StreamManager<LocalTimelineStream> {
+	private me;
+	private os: MiOS;
+
+	constructor(os: MiOS, me) {
+		super();
+
+		this.me = me;
+		this.os = os;
+	}
+
+	public getConnection() {
+		if (this.connection == null) {
+			this.connection = new LocalTimelineStream(this.os, this.me);
+		}
+
+		return this.connection;
+	}
+}
diff --git a/src/client/app/common/scripts/streaming/requests.ts b/src/client/app/common/scripts/streaming/requests.ts
deleted file mode 100644
index 5bec30143f..0000000000
--- a/src/client/app/common/scripts/streaming/requests.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import Stream from './stream';
-import StreamManager from './stream-manager';
-import MiOS from '../../mios';
-
-/**
- * Requests stream connection
- */
-export class RequestsStream extends Stream {
-	constructor(os: MiOS) {
-		super(os, 'requests');
-	}
-}
-
-export class RequestsStreamManager extends StreamManager<RequestsStream> {
-	private os: MiOS;
-
-	constructor(os: MiOS) {
-		super();
-
-		this.os = os;
-	}
-
-	public getConnection() {
-		if (this.connection == null) {
-			this.connection = new RequestsStream(this.os);
-		}
-
-		return this.connection;
-	}
-}
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
new file mode 100644
index 0000000000..8056e5b2c4
--- /dev/null
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -0,0 +1,175 @@
+<template>
+<div class="mk-home-timeline">
+	<mk-friends-maker v-if="src == 'home' && alone"/>
+	<div class="fetching" v-if="fetching">
+		<mk-ellipsis-icon/>
+	</div>
+	<p class="empty" v-if="notes.length == 0 && !fetching">
+		%fa:R comments%%i18n:@empty%
+	</p>
+	<mk-notes :notes="notes" ref="timeline">
+		<button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+			<template v-if="!moreFetching">%i18n:@load-more%</template>
+			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
+		</button>
+	</mk-notes>
+</div>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import { url } from '../../../config';
+
+export default Vue.extend({
+	props: {
+		src: {
+			type: String,
+			required: true
+		}
+	},
+
+	data() {
+		return {
+			fetching: true,
+			moreFetching: false,
+			existMore: false,
+			notes: [],
+			connection: null,
+			connectionId: null,
+			date: null
+		};
+	},
+
+	computed: {
+		alone(): boolean {
+			return (this as any).os.i.followingCount == 0;
+		},
+
+		stream(): any {
+			return this.src == 'home'
+				? (this as any).os.stream
+				: this.src == 'local'
+					? (this as any).os.streams.localTimelineStream
+					: (this as any).os.streams.globalTimelineStream;
+		},
+
+		endpoint(): string {
+			return this.src == 'home'
+				? 'notes/timeline'
+				: this.src == 'local'
+					? 'notes/local-timeline'
+					: 'notes/global-timeline';
+		}
+	},
+
+	mounted() {
+		this.connection = this.stream.getConnection();
+		this.connectionId = this.stream.use();
+
+		this.connection.on('note', this.onNote);
+		if (this.src == 'home') {
+			this.connection.on('follow', this.onChangeFollowing);
+			this.connection.on('unfollow', this.onChangeFollowing);
+		}
+
+		this.fetch();
+	},
+
+	beforeDestroy() {
+		this.connection.off('note', this.onNote);
+		if (this.src == 'home') {
+			this.connection.off('follow', this.onChangeFollowing);
+			this.connection.off('unfollow', this.onChangeFollowing);
+		}
+		this.stream.dispose(this.connectionId);
+	},
+
+	methods: {
+		fetch(cb?) {
+			this.fetching = true;
+
+			(this as any).api('notes/timeline', {
+				limit: 11,
+				untilDate: this.date ? this.date.getTime() : undefined
+			}).then(notes => {
+				if (notes.length == 11) {
+					notes.pop();
+					this.existMore = true;
+				}
+				this.notes = notes;
+				this.fetching = false;
+				this.$emit('loaded');
+				if (cb) cb();
+			});
+		},
+
+		more() {
+			if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return;
+			this.moreFetching = true;
+			(this as any).api('notes/timeline', {
+				limit: 11,
+				untilId: this.notes[this.notes.length - 1].id
+			}).then(notes => {
+				if (notes.length == 11) {
+					notes.pop();
+				} else {
+					this.existMore = false;
+				}
+				this.notes = this.notes.concat(notes);
+				this.moreFetching = false;
+			});
+		},
+
+		onNote(note) {
+			// サウンドを再生する
+			if ((this as any).os.isEnableSounds) {
+				const sound = new Audio(`${url}/assets/post.mp3`);
+				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+				sound.play();
+			}
+
+			this.notes.unshift(note);
+
+			const isTop = window.scrollY > 8;
+			if (isTop) this.notes.pop();
+		},
+
+		onChangeFollowing() {
+			this.fetch();
+		},
+
+		focus() {
+			(this.$refs.timeline as any).focus();
+		},
+
+		warp(date) {
+			this.date = date;
+			this.fetch();
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-home-timeline
+	> .mk-friends-maker
+		border-bottom solid 1px #eee
+
+	> .fetching
+		padding 64px 0
+
+	> .empty
+		display block
+		margin 0 auto
+		padding 32px
+		max-width 400px
+		text-align center
+		color #999
+
+		> [data-fa]
+			display block
+			margin-bottom 16px
+			font-size 3em
+			color #ccc
+
+</style>
diff --git a/src/client/app/desktop/views/components/timeline.vue b/src/client/app/desktop/views/components/timeline.vue
index 2db64d6e09..e0215ad1a2 100644
--- a/src/client/app/desktop/views/components/timeline.vue
+++ b/src/client/app/desktop/views/components/timeline.vue
@@ -1,169 +1,93 @@
 <template>
 <div class="mk-timeline">
-	<mk-friends-maker v-if="alone"/>
-	<div class="fetching" v-if="fetching">
-		<mk-ellipsis-icon/>
-	</div>
-	<p class="empty" v-if="notes.length == 0 && !fetching">
-		%fa:R comments%%i18n:@empty%
-	</p>
-	<mk-notes :notes="notes" ref="timeline">
-		<button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">%i18n:@load-more%</template>
-			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
-		</button>
-	</mk-notes>
+	<header>
+		<span :data-is-active="src == 'home'" @click="src = 'home'">%fa:home% ホーム</span>
+		<span :data-is-active="src == 'local'" @click="src = 'local'">%fa:R comments% ローカル</span>
+		<span :data-is-active="src == 'global'" @click="src = 'global'">%fa:globe% グローバル</span>
+	</header>
+	<x-core v-if="src == 'home'" ref="tl" key="home" src="home"/>
+	<x-core v-if="src == 'local'" ref="tl" key="local" src="local"/>
+	<x-core v-if="src == 'global'" ref="tl" key="global" src="global"/>
 </div>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import { url } from '../../../config';
+import XCore from './timeline.core.vue';
 
 export default Vue.extend({
+	components: {
+		XCore
+	},
+
 	data() {
 		return {
-			fetching: true,
-			moreFetching: false,
-			existMore: false,
-			notes: [],
-			connection: null,
-			connectionId: null,
-			date: null
+			src: 'home'
 		};
 	},
 
-	computed: {
-		alone(): boolean {
-			return (this as any).os.i.followingCount == 0;
-		}
-	},
-
 	mounted() {
-		this.connection = (this as any).os.stream.getConnection();
-		this.connectionId = (this as any).os.stream.use();
-
-		this.connection.on('note', this.onNote);
-		this.connection.on('follow', this.onChangeFollowing);
-		this.connection.on('unfollow', this.onChangeFollowing);
-
 		document.addEventListener('keydown', this.onKeydown);
 		window.addEventListener('scroll', this.onScroll);
 
-		this.fetch();
+		console.log(this.$refs.tl);
+
+		(this.$refs.tl as any).$once('loaded', () => {
+			this.$emit('loaded');
+		});
 	},
 
 	beforeDestroy() {
-		this.connection.off('note', this.onNote);
-		this.connection.off('follow', this.onChangeFollowing);
-		this.connection.off('unfollow', this.onChangeFollowing);
-		(this as any).os.stream.dispose(this.connectionId);
-
 		document.removeEventListener('keydown', this.onKeydown);
 		window.removeEventListener('scroll', this.onScroll);
 	},
 
 	methods: {
-		fetch(cb?) {
-			this.fetching = true;
-
-			(this as any).api('notes/timeline', {
-				limit: 11,
-				untilDate: this.date ? this.date.getTime() : undefined
-			}).then(notes => {
-				if (notes.length == 11) {
-					notes.pop();
-					this.existMore = true;
-				}
-				this.notes = notes;
-				this.fetching = false;
-				this.$emit('loaded');
-				if (cb) cb();
-			});
-		},
-
-		more() {
-			if (this.moreFetching || this.fetching || this.notes.length == 0 || !this.existMore) return;
-			this.moreFetching = true;
-			(this as any).api('notes/timeline', {
-				limit: 11,
-				untilId: this.notes[this.notes.length - 1].id
-			}).then(notes => {
-				if (notes.length == 11) {
-					notes.pop();
-				} else {
-					this.existMore = false;
-				}
-				this.notes = this.notes.concat(notes);
-				this.moreFetching = false;
-			});
-		},
-
-		onNote(note) {
-			// サウンドを再生する
-			if ((this as any).os.isEnableSounds) {
-				const sound = new Audio(`${url}/assets/post.mp3`);
-				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
-				sound.play();
-			}
-
-			this.notes.unshift(note);
-
-			const isTop = window.scrollY > 8;
-			if (isTop) this.notes.pop();
-		},
-
-		onChangeFollowing() {
-			this.fetch();
-		},
-
 		onScroll() {
 			if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
 				const current = window.scrollY + window.innerHeight;
-				if (current > document.body.offsetHeight - 8) this.more();
+				if (current > document.body.offsetHeight - 8) (this.$refs.tl as any).more();
 			}
 		},
 
 		onKeydown(e) {
 			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
 				if (e.which == 84) { // t
-					(this.$refs.timeline as any).focus();
+					(this.$refs.tl as any).focus();
 				}
 			}
 		},
 
 		warp(date) {
-			this.date = date;
-			this.fetch();
+			(this.$refs.tl as any).warp(date);
 		}
 	}
 });
 </script>
 
 <style lang="stylus" scoped>
+@import '~const.styl'
+
 .mk-timeline
 	background #fff
 	border solid 1px rgba(0, 0, 0, 0.075)
 	border-radius 6px
 
-	> .mk-friends-maker
+	> header
+		padding 8px 16px
 		border-bottom solid 1px #eee
 
-	> .fetching
-		padding 64px 0
+		> span
+			margin-right 16px
+			line-height 27px
+			font-size 14px
+			color #555
 
-	> .empty
-		display block
-		margin 0 auto
-		padding 32px
-		max-width 400px
-		text-align center
-		color #999
+			&:not([data-is-active])
+				color $theme-color
+				cursor pointer
 
-		> [data-fa]
-			display block
-			margin-bottom 16px
-			font-size 3em
-			color #ccc
+				&:hover
+					text-decoration underline
 
 </style>
diff --git a/src/models/mute.ts b/src/models/mute.ts
index e068215c94..8fe4eb2ee9 100644
--- a/src/models/mute.ts
+++ b/src/models/mute.ts
@@ -2,12 +2,12 @@ import * as mongo from 'mongodb';
 import db from '../db/mongodb';
 
 const Mute = db.get<IMute>('mute');
+Mute.createIndex(['muterId', 'muteeId'], { unique: true });
 export default Mute;
 
 export interface IMute {
 	_id: mongo.ObjectID;
 	createdAt: Date;
-	deletedAt: Date;
 	muterId: mongo.ObjectID;
 	muteeId: mongo.ObjectID;
 }
diff --git a/src/publishers/stream.ts b/src/publishers/stream.ts
index a6d2c2277c..2ecbfa0dd8 100644
--- a/src/publishers/stream.ts
+++ b/src/publishers/stream.ts
@@ -45,6 +45,14 @@ class MisskeyEvent {
 		this.publish(`channel-stream:${channelId}`, type, typeof value === 'undefined' ? null : value);
 	}
 
+	public publishLocalTimelineStream(note: any): void {
+		this.redisClient.publish('misskey:local-timeline', JSON.stringify(note));
+	}
+
+	public publishGlobalTimelineStream(note: any): void {
+		this.redisClient.publish('misskey:global-timeline', JSON.stringify(note));
+	}
+
 	private publish(channel: string, type: string, value?: any): void {
 		const message = value == null ?
 			{ type: type } :
@@ -58,16 +66,12 @@ const ev = new MisskeyEvent();
 
 export default ev.publishUserStream.bind(ev);
 
+export const publishLocalTimelineStream = ev.publishLocalTimelineStream.bind(ev);
+export const publishGlobalTimelineStream = ev.publishGlobalTimelineStream.bind(ev);
 export const publishDriveStream = ev.publishDriveStream.bind(ev);
-
 export const publishNoteStream = ev.publishNoteStream.bind(ev);
-
 export const publishMessagingStream = ev.publishMessagingStream.bind(ev);
-
 export const publishMessagingIndexStream = ev.publishMessagingIndexStream.bind(ev);
-
 export const publishOthelloStream = ev.publishOthelloStream.bind(ev);
-
 export const publishOthelloGameStream = ev.publishOthelloGameStream.bind(ev);
-
 export const publishChannelStream = ev.publishChannelStream.bind(ev);
diff --git a/src/server/api/endpoints.ts b/src/server/api/endpoints.ts
index 67f3217faf..e0223c23e0 100644
--- a/src/server/api/endpoints.ts
+++ b/src/server/api/endpoints.ts
@@ -463,6 +463,22 @@ const endpoints: Endpoint[] = [
 			max: 100
 		}
 	},
+	{
+		name: 'notes/local-timeline',
+		withCredential: true,
+		limit: {
+			duration: ms('10minutes'),
+			max: 100
+		}
+	},
+	{
+		name: 'notes/global-timeline',
+		withCredential: true,
+		limit: {
+			duration: ms('10minutes'),
+			max: 100
+		}
+	},
 	{
 		name: 'notes/mentions',
 		withCredential: true,
diff --git a/src/server/api/endpoints/mute/create.ts b/src/server/api/endpoints/mute/create.ts
index 19894d07af..26ae612cab 100644
--- a/src/server/api/endpoints/mute/create.ts
+++ b/src/server/api/endpoints/mute/create.ts
@@ -30,7 +30,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			'profile': false
+			profile: false
 		}
 	});
 
@@ -41,8 +41,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Check if already muting
 	const exist = await Mute.findOne({
 		muterId: muter._id,
-		muteeId: mutee._id,
-		deletedAt: { $exists: false }
+		muteeId: mutee._id
 	});
 
 	if (exist !== null) {
diff --git a/src/server/api/endpoints/mute/delete.ts b/src/server/api/endpoints/mute/delete.ts
index 10096352ba..6f617416c8 100644
--- a/src/server/api/endpoints/mute/delete.ts
+++ b/src/server/api/endpoints/mute/delete.ts
@@ -7,10 +7,6 @@ import Mute from '../../../../models/mute';
 
 /**
  * Unmute a user
- *
- * @param {any} params
- * @param {any} user
- * @return {Promise<any>}
  */
 module.exports = (params, user) => new Promise(async (res, rej) => {
 	const muter = user;
@@ -30,7 +26,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}, {
 		fields: {
 			data: false,
-			'profile': false
+			profile: false
 		}
 	});
 
@@ -41,8 +37,7 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	// Check not muting
 	const exist = await Mute.findOne({
 		muterId: muter._id,
-		muteeId: mutee._id,
-		deletedAt: { $exists: false }
+		muteeId: mutee._id
 	});
 
 	if (exist === null) {
@@ -50,12 +45,8 @@ module.exports = (params, user) => new Promise(async (res, rej) => {
 	}
 
 	// Delete mute
-	await Mute.update({
+	await Mute.remove({
 		_id: exist._id
-	}, {
-		$set: {
-			deletedAt: new Date()
-		}
 	});
 
 	// Send response
diff --git a/src/server/api/endpoints/notes/global-timeline.ts b/src/server/api/endpoints/notes/global-timeline.ts
new file mode 100644
index 0000000000..07e138ec54
--- /dev/null
+++ b/src/server/api/endpoints/notes/global-timeline.ts
@@ -0,0 +1,91 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Note from '../../../../models/note';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/note';
+
+/**
+ * Get timeline of global
+ */
+module.exports = async (params, user, app) => {
+	// Get 'limit' parameter
+	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
+	if (limitErr) throw 'invalid limit param';
+
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) throw 'invalid sinceId param';
+
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) throw 'invalid untilId param';
+
+	// Get 'sinceDate' parameter
+	const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
+	if (sinceDateErr) throw 'invalid sinceDate param';
+
+	// Get 'untilDate' parameter
+	const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
+	if (untilDateErr) throw 'invalid untilDate param';
+
+	// Check if only one of sinceId, untilId, sinceDate, untilDate specified
+	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
+	}
+
+	// ミュートしているユーザーを取得
+	const mutedUserIds = (await Mute.find({
+		muterId: user._id
+	})).map(m => m.muteeId);
+
+	//#region Construct query
+	const sort = {
+		_id: -1
+	};
+
+	const query = {
+		// mute
+		userId: {
+			$nin: mutedUserIds
+		},
+		'_reply.userId': {
+			$nin: mutedUserIds
+		},
+		'_renote.userId': {
+			$nin: mutedUserIds
+		}
+	} as any;
+
+	if (sinceId) {
+		sort._id = 1;
+		query._id = {
+			$gt: sinceId
+		};
+	} else if (untilId) {
+		query._id = {
+			$lt: untilId
+		};
+	} else if (sinceDate) {
+		sort._id = 1;
+		query.createdAt = {
+			$gt: new Date(sinceDate)
+		};
+	} else if (untilDate) {
+		query.createdAt = {
+			$lt: new Date(untilDate)
+		};
+	}
+	//#endregion
+
+	// Issue query
+	const timeline = await Note
+		.find(query, {
+			limit: limit,
+			sort: sort
+		});
+
+	// Serialize
+	return await Promise.all(timeline.map(note => pack(note, user)));
+};
diff --git a/src/server/api/endpoints/notes/local-timeline.ts b/src/server/api/endpoints/notes/local-timeline.ts
new file mode 100644
index 0000000000..d63528c3cd
--- /dev/null
+++ b/src/server/api/endpoints/notes/local-timeline.ts
@@ -0,0 +1,94 @@
+/**
+ * Module dependencies
+ */
+import $ from 'cafy';
+import Note from '../../../../models/note';
+import Mute from '../../../../models/mute';
+import { pack } from '../../../../models/note';
+
+/**
+ * Get timeline of local
+ */
+module.exports = async (params, user, app) => {
+	// Get 'limit' parameter
+	const [limit = 10, limitErr] = $(params.limit).optional.number().range(1, 100).$;
+	if (limitErr) throw 'invalid limit param';
+
+	// Get 'sinceId' parameter
+	const [sinceId, sinceIdErr] = $(params.sinceId).optional.id().$;
+	if (sinceIdErr) throw 'invalid sinceId param';
+
+	// Get 'untilId' parameter
+	const [untilId, untilIdErr] = $(params.untilId).optional.id().$;
+	if (untilIdErr) throw 'invalid untilId param';
+
+	// Get 'sinceDate' parameter
+	const [sinceDate, sinceDateErr] = $(params.sinceDate).optional.number().$;
+	if (sinceDateErr) throw 'invalid sinceDate param';
+
+	// Get 'untilDate' parameter
+	const [untilDate, untilDateErr] = $(params.untilDate).optional.number().$;
+	if (untilDateErr) throw 'invalid untilDate param';
+
+	// Check if only one of sinceId, untilId, sinceDate, untilDate specified
+	if ([sinceId, untilId, sinceDate, untilDate].filter(x => x != null).length > 1) {
+		throw 'only one of sinceId, untilId, sinceDate, untilDate can be specified';
+	}
+
+	// ミュートしているユーザーを取得
+	const mutedUserIds = (await Mute.find({
+		muterId: user._id
+	})).map(m => m.muteeId);
+
+	//#region Construct query
+	const sort = {
+		_id: -1
+	};
+
+	const query = {
+		// mute
+		userId: {
+			$nin: mutedUserIds
+		},
+		'_reply.userId': {
+			$nin: mutedUserIds
+		},
+		'_renote.userId': {
+			$nin: mutedUserIds
+		},
+
+		// local
+		'_user.host': null
+	} as any;
+
+	if (sinceId) {
+		sort._id = 1;
+		query._id = {
+			$gt: sinceId
+		};
+	} else if (untilId) {
+		query._id = {
+			$lt: untilId
+		};
+	} else if (sinceDate) {
+		sort._id = 1;
+		query.createdAt = {
+			$gt: new Date(sinceDate)
+		};
+	} else if (untilDate) {
+		query.createdAt = {
+			$lt: new Date(untilDate)
+		};
+	}
+	//#endregion
+
+	// Issue query
+	const timeline = await Note
+		.find(query, {
+			limit: limit,
+			sort: sort
+		});
+
+	// Serialize
+	return await Promise.all(timeline.map(note => pack(note, user)));
+};
diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index 5263cfb2aa..b5feaac817 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -11,11 +11,6 @@ import { pack } from '../../../../models/note';
 
 /**
  * Get timeline of myself
- *
- * @param {any} params
- * @param {any} user
- * @param {any} app
- * @return {Promise<any>}
  */
 module.exports = async (params, user, app) => {
 	// Get 'limit' parameter
@@ -56,9 +51,7 @@ module.exports = async (params, user, app) => {
 
 		// ミュートしているユーザーを取得
 		mutedUserIds: Mute.find({
-			muterId: user._id,
-			// 削除されたドキュメントは除く
-			deletedAt: { $exists: false }
+			muterId: user._id
 		}).then(ms => ms.map(m => m.muteeId))
 	});
 
diff --git a/src/server/api/stream/global-timeline.ts b/src/server/api/stream/global-timeline.ts
new file mode 100644
index 0000000000..f31ce17752
--- /dev/null
+++ b/src/server/api/stream/global-timeline.ts
@@ -0,0 +1,39 @@
+import * as websocket from 'websocket';
+import * as redis from 'redis';
+
+import { IUser } from '../../../models/user';
+import Mute from '../../../models/mute';
+
+export default async function(
+	request: websocket.request,
+	connection: websocket.connection,
+	subscriber: redis.RedisClient,
+	user: IUser
+) {
+	// Subscribe stream
+	subscriber.subscribe(`misskey:global-timeline`);
+
+	const mute = await Mute.find({ muterId: user._id });
+	const mutedUserIds = mute.map(m => m.muteeId.toString());
+
+	subscriber.on('message', async (_, data) => {
+		const note = JSON.parse(data);
+
+		//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (mutedUserIds.indexOf(note.userId) != -1) {
+			return;
+		}
+		if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
+			return;
+		}
+		if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
+			return;
+		}
+		//#endregion
+
+		connection.send(JSON.stringify({
+			type: 'note',
+			body: note
+		}));
+	});
+}
diff --git a/src/server/api/stream/home.ts b/src/server/api/stream/home.ts
index e9c0924f31..a9d6ff241e 100644
--- a/src/server/api/stream/home.ts
+++ b/src/server/api/stream/home.ts
@@ -21,10 +21,7 @@ export default async function(
 	// Subscribe Home stream channel
 	subscriber.subscribe(`misskey:user-stream:${user._id}`);
 
-	const mute = await Mute.find({
-		muterId: user._id,
-		deletedAt: { $exists: false }
-	});
+	const mute = await Mute.find({ muterId: user._id });
 	const mutedUserIds = mute.map(m => m.muteeId.toString());
 
 	subscriber.on('message', async (channel, data) => {
@@ -33,6 +30,7 @@ export default async function(
 				try {
 					const x = JSON.parse(data);
 
+					//#region 流れてきたメッセージがミュートしているユーザーが関わるものだったら無視する
 					if (x.type == 'note') {
 						if (mutedUserIds.indexOf(x.body.userId) != -1) {
 							return;
@@ -48,6 +46,7 @@ export default async function(
 							return;
 						}
 					}
+					//#endregion
 
 					connection.send(data);
 				} catch (e) {
diff --git a/src/server/api/stream/local-timeline.ts b/src/server/api/stream/local-timeline.ts
new file mode 100644
index 0000000000..a790ba878b
--- /dev/null
+++ b/src/server/api/stream/local-timeline.ts
@@ -0,0 +1,39 @@
+import * as websocket from 'websocket';
+import * as redis from 'redis';
+
+import { IUser } from '../../../models/user';
+import Mute from '../../../models/mute';
+
+export default async function(
+	request: websocket.request,
+	connection: websocket.connection,
+	subscriber: redis.RedisClient,
+	user: IUser
+) {
+	// Subscribe stream
+	subscriber.subscribe(`misskey:local-timeline`);
+
+	const mute = await Mute.find({ muterId: user._id });
+	const mutedUserIds = mute.map(m => m.muteeId.toString());
+
+	subscriber.on('message', async (_, data) => {
+		const note = JSON.parse(data);
+
+		//#region 流れてきたNoteがミュートしているユーザーが関わるものだったら無視する
+		if (mutedUserIds.indexOf(note.userId) != -1) {
+			return;
+		}
+		if (note.reply != null && mutedUserIds.indexOf(note.reply.userId) != -1) {
+			return;
+		}
+		if (note.renote != null && mutedUserIds.indexOf(note.renote.userId) != -1) {
+			return;
+		}
+		//#endregion
+
+		connection.send(JSON.stringify({
+			type: 'note',
+			body: note
+		}));
+	});
+}
diff --git a/src/server/api/streaming.ts b/src/server/api/streaming.ts
index d586d7c08f..ce13253649 100644
--- a/src/server/api/streaming.ts
+++ b/src/server/api/streaming.ts
@@ -4,6 +4,8 @@ import * as redis from 'redis';
 import config from '../../config';
 
 import homeStream from './stream/home';
+import localTimelineStream from './stream/local-timeline';
+import globalTimelineStream from './stream/global-timeline';
 import driveStream from './stream/drive';
 import messagingStream from './stream/messaging';
 import messagingIndexStream from './stream/messaging-index';
@@ -64,8 +66,10 @@ module.exports = (server: http.Server) => {
 			return;
 		}
 
-		const channel =
+		const channel: any =
 			request.resourceURL.pathname === '/' ? homeStream :
+			request.resourceURL.pathname === '/local-timeline' ? localTimelineStream :
+			request.resourceURL.pathname === '/global-timeline' ? globalTimelineStream :
 			request.resourceURL.pathname === '/drive' ? driveStream :
 			request.resourceURL.pathname === '/messaging' ? messagingStream :
 			request.resourceURL.pathname === '/messaging-index' ? messagingIndexStream :
diff --git a/src/services/note/create.ts b/src/services/note/create.ts
index 781337daf4..e35e5ecfbd 100644
--- a/src/services/note/create.ts
+++ b/src/services/note/create.ts
@@ -1,6 +1,6 @@
 import Note, { pack, INote } from '../../models/note';
 import User, { isLocalUser, IUser, isRemoteUser } from '../../models/user';
-import stream from '../../publishers/stream';
+import stream, { publishLocalTimelineStream, publishGlobalTimelineStream } from '../../publishers/stream';
 import Following from '../../models/following';
 import { deliver } from '../../queue';
 import renderNote from '../../remote/activitypub/renderer/note';
@@ -105,11 +105,17 @@ export default async (user: IUser, data: {
 
 	// タイムラインへの投稿
 	if (note.channelId == null) {
-		// Publish event to myself's stream
 		if (isLocalUser(user)) {
+			// Publish event to myself's stream
 			stream(note.userId, 'note', noteObj);
+
+			// Publish note to local timeline stream
+			publishLocalTimelineStream(noteObj);
 		}
 
+		// Publish note to global timeline stream
+		publishGlobalTimelineStream(noteObj);
+
 		// Fetch all followers
 		const followers = await Following.aggregate([{
 			$lookup: {