Compare commits
	
		
			17 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 6e8a1086d8 | ||
|   | c78945436e | ||
|   | 6eff8fde74 | ||
|   | 726d5a177e | ||
|   | 33495b5cb3 | ||
|   | fe159a13a9 | ||
|   | 22a1dc0566 | ||
|   | 02e6b732e9 | ||
|   | cc6fa135ac | ||
|   | 5747732156 | ||
|   | 581d1617d8 | ||
|   | 6152fd20bf | ||
|   | 19300ca65c | ||
|   | 2f3d744e19 | ||
|   | 724e812972 | ||
|   | 9a6246fd4e | ||
|   | 34f44de59c | 
| @@ -12,7 +12,7 @@ | |||||||
| > Lead Maintainer: [syuilo][syuilo-link] | > Lead Maintainer: [syuilo][syuilo-link] | ||||||
|  |  | ||||||
| **[Misskey](https://misskey.xyz)** is a completely open source, | **[Misskey](https://misskey.xyz)** is a completely open source, | ||||||
| ultimately sophisticated new type of mini-blog based SNS. | ultimately sophisticated professional microblogging software. | ||||||
|  |  | ||||||
| <a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a> | <a href="https://www.patreon.com/syuilo"><img src="https://c5.patreon.com/external/logo/become_a_patron_button@2x.png" alt="Become a Patron!" width="160" /></a> | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								cli/update-remote-user.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								cli/update-remote-user.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | const updatePerson = require('../built/remote/activitypub/models/person').updatePerson; | ||||||
|  |  | ||||||
|  | const args = process.argv.slice(2); | ||||||
|  | const user = args[0]; | ||||||
|  |  | ||||||
|  | console.log(`Updating ${user}...`); | ||||||
|  |  | ||||||
|  | updatePerson(user).then(() => { | ||||||
|  | 	console.log(`Updated ${user}`); | ||||||
|  | }, e => { | ||||||
|  | 	console.error(e); | ||||||
|  | }); | ||||||
| @@ -63,6 +63,7 @@ common: | |||||||
|     memo: "メモ" |     memo: "メモ" | ||||||
|     trends: "トレンド" |     trends: "トレンド" | ||||||
|     photo-stream: "フォトストリーム" |     photo-stream: "フォトストリーム" | ||||||
|  |     posts-monitor: "投稿チャート" | ||||||
|     slideshow: "スライドショー" |     slideshow: "スライドショー" | ||||||
|     version: "バージョン" |     version: "バージョン" | ||||||
|     broadcast: "ブロードキャスト" |     broadcast: "ブロードキャスト" | ||||||
| @@ -249,6 +250,10 @@ common/views/widgets/photo-stream.vue: | |||||||
|   title: "フォトストリーム" |   title: "フォトストリーム" | ||||||
|   no-photos: "写真はありません" |   no-photos: "写真はありません" | ||||||
|  |  | ||||||
|  | common/views/widgets/posts-monitor.vue: | ||||||
|  |   title: "投稿チャート" | ||||||
|  |   toggle: "表示を切り替え" | ||||||
|  |  | ||||||
| common/views/widgets/server.vue: | common/views/widgets/server.vue: | ||||||
|   title: "サーバー情報" |   title: "サーバー情報" | ||||||
|   toggle: "表示を切り替え" |   toggle: "表示を切り替え" | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| { | { | ||||||
| 	"name": "misskey", | 	"name": "misskey", | ||||||
| 	"author": "syuilo <i@syuilo.com>", | 	"author": "syuilo <i@syuilo.com>", | ||||||
| 	"version": "2.32.0", | 	"version": "2.34.0", | ||||||
| 	"clientVersion": "1.0.6285", | 	"clientVersion": "1.0.6302", | ||||||
| 	"codename": "nighthike", | 	"codename": "nighthike", | ||||||
| 	"main": "./built/index.js", | 	"main": "./built/index.js", | ||||||
| 	"private": true, | 	"private": true, | ||||||
| @@ -152,7 +152,7 @@ | |||||||
| 		"mkdirp": "0.5.1", | 		"mkdirp": "0.5.1", | ||||||
| 		"mocha": "5.2.0", | 		"mocha": "5.2.0", | ||||||
| 		"moji": "0.5.1", | 		"moji": "0.5.1", | ||||||
| 		"mongodb": "3.0.8", | 		"mongodb": "3.0.10", | ||||||
| 		"monk": "6.0.6", | 		"monk": "6.0.6", | ||||||
| 		"ms": "2.1.1", | 		"ms": "2.1.1", | ||||||
| 		"nan": "2.10.0", | 		"nan": "2.10.0", | ||||||
| @@ -218,6 +218,6 @@ | |||||||
| 		"webpack-cli": "2.1.4", | 		"webpack-cli": "2.1.4", | ||||||
| 		"websocket": "1.0.26", | 		"websocket": "1.0.26", | ||||||
| 		"ws": "5.2.0", | 		"ws": "5.2.0", | ||||||
| 		"xev": "2.0.0" | 		"xev": "2.0.1" | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
| @@ -3,15 +3,15 @@ import StreamManager from './stream-manager'; | |||||||
| import MiOS from '../../../mios'; | import MiOS from '../../../mios'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Server stream connection |  * Notes stats stream connection | ||||||
|  */ |  */ | ||||||
| export class ServerStream extends Stream { | export class NotesStatsStream extends Stream { | ||||||
| 	constructor(os: MiOS) { | 	constructor(os: MiOS) { | ||||||
| 		super(os, 'server'); | 		super(os, 'notes-stats'); | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class ServerStreamManager extends StreamManager<ServerStream> { | export class NotesStatsStreamManager extends StreamManager<NotesStatsStream> { | ||||||
| 	private os: MiOS; | 	private os: MiOS; | ||||||
| 
 | 
 | ||||||
| 	constructor(os: MiOS) { | 	constructor(os: MiOS) { | ||||||
| @@ -22,7 +22,7 @@ export class ServerStreamManager extends StreamManager<ServerStream> { | |||||||
| 
 | 
 | ||||||
| 	public getConnection() { | 	public getConnection() { | ||||||
| 		if (this.connection == null) { | 		if (this.connection == null) { | ||||||
| 			this.connection = new ServerStream(this.os); | 			this.connection = new NotesStatsStream(this.os); | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		return this.connection; | 		return this.connection; | ||||||
							
								
								
									
										30
									
								
								src/client/app/common/scripts/streaming/server-stats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/client/app/common/scripts/streaming/server-stats.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,30 @@ | |||||||
|  | import Stream from './stream'; | ||||||
|  | import StreamManager from './stream-manager'; | ||||||
|  | import MiOS from '../../../mios'; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * Server stats stream connection | ||||||
|  |  */ | ||||||
|  | export class ServerStatsStream extends Stream { | ||||||
|  | 	constructor(os: MiOS) { | ||||||
|  | 		super(os, 'server-stats'); | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export class ServerStatsStreamManager extends StreamManager<ServerStatsStream> { | ||||||
|  | 	private os: MiOS; | ||||||
|  |  | ||||||
|  | 	constructor(os: MiOS) { | ||||||
|  | 		super(); | ||||||
|  |  | ||||||
|  | 		this.os = os; | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	public getConnection() { | ||||||
|  | 		if (this.connection == null) { | ||||||
|  | 			this.connection = new ServerStatsStream(this.os); | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return this.connection; | ||||||
|  | 	} | ||||||
|  | } | ||||||
| @@ -2,6 +2,7 @@ import Vue from 'vue'; | |||||||
|  |  | ||||||
| import analogClock from './analog-clock.vue'; | import analogClock from './analog-clock.vue'; | ||||||
| import menu from './menu.vue'; | import menu from './menu.vue'; | ||||||
|  | import noteHeader from './note-header.vue'; | ||||||
| import signin from './signin.vue'; | import signin from './signin.vue'; | ||||||
| import signup from './signup.vue'; | import signup from './signup.vue'; | ||||||
| import forkit from './forkit.vue'; | import forkit from './forkit.vue'; | ||||||
| @@ -31,6 +32,7 @@ import welcomeTimeline from './welcome-timeline.vue'; | |||||||
|  |  | ||||||
| Vue.component('mk-analog-clock', analogClock); | Vue.component('mk-analog-clock', analogClock); | ||||||
| Vue.component('mk-menu', menu); | Vue.component('mk-menu', menu); | ||||||
|  | Vue.component('mk-note-header', noteHeader); | ||||||
| Vue.component('mk-signin', signin); | Vue.component('mk-signin', signin); | ||||||
| Vue.component('mk-signup', signup); | Vue.component('mk-signup', signup); | ||||||
| Vue.component('mk-forkit', forkit); | Vue.component('mk-forkit', forkit); | ||||||
|   | |||||||
							
								
								
									
										117
									
								
								src/client/app/common/views/components/note-header.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								src/client/app/common/views/components/note-header.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,117 @@ | |||||||
|  | <template> | ||||||
|  | <header class="bvonvjxbwzaiskogyhbwgyxvcgserpmu"> | ||||||
|  | 	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/> | ||||||
|  | 	<router-link class="name" :to="note.user | userPage" v-user-preview="note.user.id">{{ note.user | userName }}</router-link> | ||||||
|  | 	<span class="is-admin" v-if="note.user.isAdmin">admin</span> | ||||||
|  | 	<span class="is-bot" v-if="note.user.isBot">bot</span> | ||||||
|  | 	<span class="is-cat" v-if="note.user.isCat">cat</span> | ||||||
|  | 	<span class="username"><mk-acct :user="note.user"/></span> | ||||||
|  | 	<div class="info"> | ||||||
|  | 		<span class="app" v-if="note.app && !mini">via <b>{{ note.app.name }}</b></span> | ||||||
|  | 		<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> | ||||||
|  | 		<router-link class="created-at" :to="note | notePage"> | ||||||
|  | 			<mk-time :time="note.createdAt"/> | ||||||
|  | 		</router-link> | ||||||
|  | 		<span class="visibility" v-if="note.visibility != 'public'"> | ||||||
|  | 			<template v-if="note.visibility == 'home'">%fa:home%</template> | ||||||
|  | 			<template v-if="note.visibility == 'followers'">%fa:unlock%</template> | ||||||
|  | 			<template v-if="note.visibility == 'specified'">%fa:envelope%</template> | ||||||
|  | 			<template v-if="note.visibility == 'private'">%fa:lock%</template> | ||||||
|  | 		</span> | ||||||
|  | 	</div> | ||||||
|  | </header> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import Vue from 'vue'; | ||||||
|  |  | ||||||
|  | export default Vue.extend({ | ||||||
|  | 	props: { | ||||||
|  | 		note: { | ||||||
|  | 			type: Object, | ||||||
|  | 			required: true | ||||||
|  | 		}, | ||||||
|  | 		mini: { | ||||||
|  | 			type: Boolean, | ||||||
|  | 			required: false, | ||||||
|  | 			default: false | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="stylus" scoped> | ||||||
|  | @import '~const.styl' | ||||||
|  |  | ||||||
|  | root(isDark) | ||||||
|  | 	display flex | ||||||
|  | 	align-items baseline | ||||||
|  | 	white-space nowrap | ||||||
|  |  | ||||||
|  | 	> .avatar | ||||||
|  | 		flex-shrink 0 | ||||||
|  | 		margin-right 8px | ||||||
|  | 		width 20px | ||||||
|  | 		height 20px | ||||||
|  | 		border-radius 100% | ||||||
|  |  | ||||||
|  | 	> .name | ||||||
|  | 		display block | ||||||
|  | 		margin 0 .5em 0 0 | ||||||
|  | 		padding 0 | ||||||
|  | 		overflow hidden | ||||||
|  | 		color isDark ? #fff : #627079 | ||||||
|  | 		font-size 1em | ||||||
|  | 		font-weight bold | ||||||
|  | 		text-decoration none | ||||||
|  | 		text-overflow ellipsis | ||||||
|  |  | ||||||
|  | 		&:hover | ||||||
|  | 			text-decoration underline | ||||||
|  |  | ||||||
|  | 	> .is-admin | ||||||
|  | 	> .is-bot | ||||||
|  | 	> .is-cat | ||||||
|  | 		align-self center | ||||||
|  | 		margin 0 .5em 0 0 | ||||||
|  | 		padding 1px 6px | ||||||
|  | 		font-size 80% | ||||||
|  | 		color isDark ? #758188 : #aaa | ||||||
|  | 		border solid 1px isDark ? #57616f : #ddd | ||||||
|  | 		border-radius 3px | ||||||
|  |  | ||||||
|  | 		&.is-admin | ||||||
|  | 			border-color isDark ? #d42c41 : #f56a7b | ||||||
|  | 			color isDark ? #d42c41 : #f56a7b | ||||||
|  |  | ||||||
|  | 	> .username | ||||||
|  | 		margin 0 .5em 0 0 | ||||||
|  | 		overflow hidden | ||||||
|  | 		text-overflow ellipsis | ||||||
|  | 		color isDark ? #606984 : #ccc | ||||||
|  |  | ||||||
|  | 	> .info | ||||||
|  | 		margin-left auto | ||||||
|  | 		font-size 0.9em | ||||||
|  |  | ||||||
|  | 		> * | ||||||
|  | 			color isDark ? #606984 : #c0c0c0 | ||||||
|  |  | ||||||
|  | 		> .mobile | ||||||
|  | 			margin-right 8px | ||||||
|  |  | ||||||
|  | 		> .app | ||||||
|  | 			margin-right 8px | ||||||
|  | 			padding-right 8px | ||||||
|  | 			border-right solid 1px isDark ? #1c2023 : #eaeaea | ||||||
|  |  | ||||||
|  | 		> .visibility | ||||||
|  | 			margin-left 8px | ||||||
|  |  | ||||||
|  | .bvonvjxbwzaiskogyhbwgyxvcgserpmu[data-darkmode] | ||||||
|  | 	root(true) | ||||||
|  |  | ||||||
|  | .bvonvjxbwzaiskogyhbwgyxvcgserpmu:not([data-darkmode]) | ||||||
|  | 	root(false) | ||||||
|  |  | ||||||
|  | </style> | ||||||
| @@ -4,6 +4,7 @@ import wAnalogClock from './analog-clock.vue'; | |||||||
| import wVersion from './version.vue'; | import wVersion from './version.vue'; | ||||||
| import wRss from './rss.vue'; | import wRss from './rss.vue'; | ||||||
| import wServer from './server.vue'; | import wServer from './server.vue'; | ||||||
|  | import wPostsMonitor from './posts-monitor.vue'; | ||||||
| import wMemo from './memo.vue'; | import wMemo from './memo.vue'; | ||||||
| import wBroadcast from './broadcast.vue'; | import wBroadcast from './broadcast.vue'; | ||||||
| import wCalendar from './calendar.vue'; | import wCalendar from './calendar.vue'; | ||||||
| @@ -22,6 +23,7 @@ Vue.component('mkw-tips', wTips); | |||||||
| Vue.component('mkw-donation', wDonation); | Vue.component('mkw-donation', wDonation); | ||||||
| Vue.component('mkw-broadcast', wBroadcast); | Vue.component('mkw-broadcast', wBroadcast); | ||||||
| Vue.component('mkw-server', wServer); | Vue.component('mkw-server', wServer); | ||||||
|  | Vue.component('mkw-posts-monitor', wPostsMonitor); | ||||||
| Vue.component('mkw-memo', wMemo); | Vue.component('mkw-memo', wMemo); | ||||||
| Vue.component('mkw-rss', wRss); | Vue.component('mkw-rss', wRss); | ||||||
| Vue.component('mkw-version', wVersion); | Vue.component('mkw-version', wVersion); | ||||||
|   | |||||||
							
								
								
									
										182
									
								
								src/client/app/common/views/widgets/posts-monitor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										182
									
								
								src/client/app/common/views/widgets/posts-monitor.vue
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,182 @@ | |||||||
|  | <template> | ||||||
|  | <div class="mkw-posts-monitor"> | ||||||
|  | 	<mk-widget-container :show-header="props.design == 0" :naked="props.design == 2"> | ||||||
|  | 		<template slot="header">%fa:chart-line%%i18n:@title%</template> | ||||||
|  | 		<button slot="func" @click="toggle" title="%i18n:@toggle%">%fa:sort%</button> | ||||||
|  |  | ||||||
|  | 		<div class="qpdmibaztplkylerhdbllwcokyrfxeyj" :class="{ dual: props.view == 0 }" :data-darkmode="$store.state.device.darkmode"> | ||||||
|  | 			<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" v-show="props.view != 2"> | ||||||
|  | 				<defs> | ||||||
|  | 					<linearGradient :id="fediGradientId" x1="0" x2="0" y1="1" y2="0"> | ||||||
|  | 						<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop> | ||||||
|  | 						<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop> | ||||||
|  | 					</linearGradient> | ||||||
|  | 					<mask :id="fediMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> | ||||||
|  | 						<polyline | ||||||
|  | 							:points="fediPolylinePoints" | ||||||
|  | 							fill="none" | ||||||
|  | 							stroke="#fff" | ||||||
|  | 							stroke-width="1"/> | ||||||
|  | 					</mask> | ||||||
|  | 				</defs> | ||||||
|  | 				<rect | ||||||
|  | 					x="-1" y="-1" | ||||||
|  | 					:width="viewBoxX + 2" :height="viewBoxY + 2" | ||||||
|  | 					:style="`stroke: none; fill: url(#${ fediGradientId }); mask: url(#${ fediMaskId })`"/> | ||||||
|  | 				<text x="1" y="5">Fedi</text> | ||||||
|  | 			</svg> | ||||||
|  | 			<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`" preserveAspectRatio="none" v-show="props.view != 1"> | ||||||
|  | 				<defs> | ||||||
|  | 					<linearGradient :id="localGradientId" x1="0" x2="0" y1="1" y2="0"> | ||||||
|  | 						<stop offset="0%" stop-color="hsl(200, 80%, 70%)"></stop> | ||||||
|  | 						<stop offset="100%" stop-color="hsl(90, 80%, 70%)"></stop> | ||||||
|  | 					</linearGradient> | ||||||
|  | 					<mask :id="localMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY"> | ||||||
|  | 						<polyline | ||||||
|  | 							:points="localPolylinePoints" | ||||||
|  | 							fill="none" | ||||||
|  | 							stroke="#fff" | ||||||
|  | 							stroke-width="1"/> | ||||||
|  | 					</mask> | ||||||
|  | 				</defs> | ||||||
|  | 				<rect | ||||||
|  | 					x="-1" y="-1" | ||||||
|  | 					:width="viewBoxX + 2" :height="viewBoxY + 2" | ||||||
|  | 					:style="`stroke: none; fill: url(#${ localGradientId }); mask: url(#${ localMaskId })`"/> | ||||||
|  | 				<text x="1" y="5">Local</text> | ||||||
|  | 			</svg> | ||||||
|  | 		</div> | ||||||
|  | 	</mk-widget-container> | ||||||
|  | </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script lang="ts"> | ||||||
|  | import define from '../../../common/define-widget'; | ||||||
|  | import * as uuid from 'uuid'; | ||||||
|  |  | ||||||
|  | export default define({ | ||||||
|  | 	name: 'server', | ||||||
|  | 	props: () => ({ | ||||||
|  | 		design: 0, | ||||||
|  | 		view: 0 | ||||||
|  | 	}) | ||||||
|  | }).extend({ | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			connection: null, | ||||||
|  | 			connectionId: null, | ||||||
|  | 			viewBoxY: 30, | ||||||
|  | 			stats: [], | ||||||
|  | 			fediGradientId: uuid(), | ||||||
|  | 			fediMaskId: uuid(), | ||||||
|  | 			localGradientId: uuid(), | ||||||
|  | 			localMaskId: uuid(), | ||||||
|  | 			fediPolylinePoints: '', | ||||||
|  | 			localPolylinePoints: '', | ||||||
|  | 			fediPolygonPoints: '', | ||||||
|  | 			localPolygonPoints: '' | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
|  | 	computed: { | ||||||
|  | 		viewBoxX(): number { | ||||||
|  | 			return this.props.view == 0 ? 50 : 100; | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	watch: { | ||||||
|  | 		viewBoxX() { | ||||||
|  | 			this.draw(); | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  | 	mounted() { | ||||||
|  | 		this.connection = (this as any).os.streams.notesStatsStream.getConnection(); | ||||||
|  | 		this.connectionId = (this as any).os.streams.notesStatsStream.use(); | ||||||
|  |  | ||||||
|  | 		this.connection.on('stats', this.onStats); | ||||||
|  | 		this.connection.on('statsLog', this.onStatsLog); | ||||||
|  | 		this.connection.send({ | ||||||
|  | 			type: 'requestLog', | ||||||
|  | 			id: Math.random().toString() | ||||||
|  | 		}); | ||||||
|  | 	}, | ||||||
|  | 	beforeDestroy() { | ||||||
|  | 		this.connection.off('stats', this.onStats); | ||||||
|  | 		this.connection.off('statsLog', this.onStatsLog); | ||||||
|  | 		(this as any).os.streams.notesStatsStream.dispose(this.connectionId); | ||||||
|  | 	}, | ||||||
|  | 	methods: { | ||||||
|  | 		toggle() { | ||||||
|  | 			if (this.props.view == 2) { | ||||||
|  | 				this.props.view = 0; | ||||||
|  | 			} else { | ||||||
|  | 				this.props.view++; | ||||||
|  | 			} | ||||||
|  | 			this.save(); | ||||||
|  | 		}, | ||||||
|  | 		func() { | ||||||
|  | 			if (this.props.design == 2) { | ||||||
|  | 				this.props.design = 0; | ||||||
|  | 			} else { | ||||||
|  | 				this.props.design++; | ||||||
|  | 			} | ||||||
|  | 			this.save(); | ||||||
|  | 		}, | ||||||
|  | 		draw() { | ||||||
|  | 			const stats = this.props.view == 0 ? this.stats.slice(0, 50) : this.stats; | ||||||
|  | 			const fediPeak = Math.max.apply(null, this.stats.map(x => x.all)) || 1; | ||||||
|  | 			const localPeak = Math.max.apply(null, this.stats.map(x => x.local)) || 1; | ||||||
|  |  | ||||||
|  | 			this.fediPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.all / fediPeak)) * this.viewBoxY}`).join(' '); | ||||||
|  | 			this.localPolylinePoints = this.stats.map((s, i) => `${this.viewBoxX - ((this.stats.length - 1) - i)},${(1 - (s.local / localPeak)) * this.viewBoxY}`).join(' '); | ||||||
|  |  | ||||||
|  | 			this.fediPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.fediPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; | ||||||
|  | 			this.localPolygonPoints = `${this.viewBoxX - (this.stats.length - 1)},${ this.viewBoxY } ${ this.localPolylinePoints } ${ this.viewBoxX },${ this.viewBoxY }`; | ||||||
|  | 		}, | ||||||
|  | 		onStats(stats) { | ||||||
|  | 			this.stats.push(stats); | ||||||
|  | 			if (this.stats.length > 100) this.stats.shift(); | ||||||
|  | 			this.draw(); | ||||||
|  | 		}, | ||||||
|  | 		onStatsLog(statsLog) { | ||||||
|  | 			statsLog.forEach(stats => this.onStats(stats)); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="stylus" scoped> | ||||||
|  | root(isDark) | ||||||
|  | 	&.dual | ||||||
|  | 		> svg | ||||||
|  | 			width 50% | ||||||
|  | 			float left | ||||||
|  |  | ||||||
|  | 			&:first-child | ||||||
|  | 				padding-right 5px | ||||||
|  |  | ||||||
|  | 			&:last-child | ||||||
|  | 				padding-left 5px | ||||||
|  |  | ||||||
|  | 	> svg | ||||||
|  | 		display block | ||||||
|  | 		padding 10px | ||||||
|  | 		width 100% | ||||||
|  |  | ||||||
|  | 		> text | ||||||
|  | 			font-size 5px | ||||||
|  | 			fill isDark ? rgba(#fff, 0.55) : rgba(#000, 0.55) | ||||||
|  |  | ||||||
|  | 			> tspan | ||||||
|  | 				opacity 0.5 | ||||||
|  |  | ||||||
|  | 	&:after | ||||||
|  | 		content "" | ||||||
|  | 		display block | ||||||
|  | 		clear both | ||||||
|  |  | ||||||
|  | .qpdmibaztplkylerhdbllwcokyrfxeyj[data-darkmode] | ||||||
|  | 	root(true) | ||||||
|  |  | ||||||
|  | .qpdmibaztplkylerhdbllwcokyrfxeyj:not([data-darkmode]) | ||||||
|  | 	root(false) | ||||||
|  |  | ||||||
|  | </style> | ||||||
| @@ -76,9 +76,15 @@ export default Vue.extend({ | |||||||
| 	}, | 	}, | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		this.connection.on('stats', this.onStats); | 		this.connection.on('stats', this.onStats); | ||||||
|  | 		this.connection.on('statsLog', this.onStatsLog); | ||||||
|  | 		this.connection.send({ | ||||||
|  | 			type: 'requestLog', | ||||||
|  | 			id: Math.random().toString() | ||||||
|  | 		}); | ||||||
| 	}, | 	}, | ||||||
| 	beforeDestroy() { | 	beforeDestroy() { | ||||||
| 		this.connection.off('stats', this.onStats); | 		this.connection.off('stats', this.onStats); | ||||||
|  | 		this.connection.off('statsLog', this.onStatsLog); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		onStats(stats) { | 		onStats(stats) { | ||||||
| @@ -94,6 +100,9 @@ export default Vue.extend({ | |||||||
|  |  | ||||||
| 			this.cpuP = (stats.cpu_usage * 100).toFixed(0); | 			this.cpuP = (stats.cpu_usage * 100).toFixed(0); | ||||||
| 			this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); | 			this.memP = (stats.mem.used / stats.mem.total * 100).toFixed(0); | ||||||
|  | 		}, | ||||||
|  | 		onStatsLog(statsLog) { | ||||||
|  | 			statsLog.forEach(stats => this.onStats(stats)); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -55,11 +55,11 @@ export default define({ | |||||||
| 			this.fetching = false; | 			this.fetching = false; | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		this.connection = (this as any).os.streams.serverStream.getConnection(); | 		this.connection = (this as any).os.streams.serverStatsStream.getConnection(); | ||||||
| 		this.connectionId = (this as any).os.streams.serverStream.use(); | 		this.connectionId = (this as any).os.streams.serverStatsStream.use(); | ||||||
| 	}, | 	}, | ||||||
| 	beforeDestroy() { | 	beforeDestroy() { | ||||||
| 		(this as any).os.streams.serverStream.dispose(this.connectionId); | 		(this as any).os.streams.serverStatsStream.dispose(this.connectionId); | ||||||
| 	}, | 	}, | ||||||
| 	methods: { | 	methods: { | ||||||
| 		toggle() { | 		toggle() { | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ | |||||||
| 					<option value="post-form">%i18n:common.widgets.post-form%</option> | 					<option value="post-form">%i18n:common.widgets.post-form%</option> | ||||||
| 					<option value="messaging">%i18n:common.widgets.messaging%</option> | 					<option value="messaging">%i18n:common.widgets.messaging%</option> | ||||||
| 					<option value="memo">%i18n:common.widgets.memo%</option> | 					<option value="memo">%i18n:common.widgets.memo%</option> | ||||||
|  | 					<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option> | ||||||
| 					<option value="server">%i18n:common.widgets.server%</option> | 					<option value="server">%i18n:common.widgets.server%</option> | ||||||
| 					<option value="donation">%i18n:common.widgets.donation%</option> | 					<option value="donation">%i18n:common.widgets.donation%</option> | ||||||
| 					<option value="nav">%i18n:common.widgets.nav%</option> | 					<option value="nav">%i18n:common.widgets.nav%</option> | ||||||
|   | |||||||
| @@ -2,22 +2,7 @@ | |||||||
| <div class="mk-note-preview" :title="title"> | <div class="mk-note-preview" :title="title"> | ||||||
| 	<mk-avatar class="avatar" :user="note.user"/> | 	<mk-avatar class="avatar" :user="note.user"/> | ||||||
| 	<div class="main"> | 	<div class="main"> | ||||||
| 		<header> | 		<mk-note-header class="header" :note="note" :mini="true"/> | ||||||
| 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> |  | ||||||
| 			<span class="username"><mk-acct :user="note.user"/></span> |  | ||||||
| 			<div class="info"> |  | ||||||
| 				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> |  | ||||||
| 				<router-link class="created-at" :to="note | notePage"> |  | ||||||
| 					<mk-time :time="note.createdAt"/> |  | ||||||
| 				</router-link> |  | ||||||
| 				<span class="visibility" v-if="note.visibility != 'public'"> |  | ||||||
| 					<template v-if="note.visibility == 'home'">%fa:home%</template> |  | ||||||
| 					<template v-if="note.visibility == 'followers'">%fa:unlock%</template> |  | ||||||
| 					<template v-if="note.visibility == 'specified'">%fa:envelope%</template> |  | ||||||
| 					<template v-if="note.visibility == 'private'">%fa:lock%</template> |  | ||||||
| 				</span> |  | ||||||
| 			</div> |  | ||||||
| 		</header> |  | ||||||
| 		<div class="body"> | 		<div class="body"> | ||||||
| 			<mk-sub-note-content class="text" :note="note"/> | 			<mk-sub-note-content class="text" :note="note"/> | ||||||
| 		</div> | 		</div> | ||||||
| @@ -56,43 +41,6 @@ root(isDark) | |||||||
| 		flex 1 | 		flex 1 | ||||||
| 		min-width 0 | 		min-width 0 | ||||||
|  |  | ||||||
| 		> header |  | ||||||
| 			display flex |  | ||||||
| 			align-items baseline |  | ||||||
| 			white-space nowrap |  | ||||||
|  |  | ||||||
| 			> .name |  | ||||||
| 				margin 0 .5em 0 0 |  | ||||||
| 				padding 0 |  | ||||||
| 				overflow hidden |  | ||||||
| 				color isDark ? #fff : #607073 |  | ||||||
| 				font-size 1em |  | ||||||
| 				font-weight bold |  | ||||||
| 				text-decoration none |  | ||||||
| 				text-overflow ellipsis |  | ||||||
|  |  | ||||||
| 				&:hover |  | ||||||
| 					text-decoration underline |  | ||||||
|  |  | ||||||
| 			> .username |  | ||||||
| 				margin 0 .5em 0 0 |  | ||||||
| 				overflow hidden |  | ||||||
| 				text-overflow ellipsis |  | ||||||
| 				color isDark ? #606984 : #d1d8da |  | ||||||
|  |  | ||||||
| 			> .info |  | ||||||
| 				margin-left auto |  | ||||||
| 				font-size 0.9em |  | ||||||
|  |  | ||||||
| 				> * |  | ||||||
| 					color isDark ? #606984 : #b2b8bb |  | ||||||
|  |  | ||||||
| 				> .mobile |  | ||||||
| 					margin-right 6px |  | ||||||
|  |  | ||||||
| 				> .visibility |  | ||||||
| 					margin-left 6px |  | ||||||
|  |  | ||||||
| 		> .body | 		> .body | ||||||
|  |  | ||||||
| 			> .text | 			> .text | ||||||
|   | |||||||
| @@ -2,25 +2,7 @@ | |||||||
| <div class="sub" :title="title"> | <div class="sub" :title="title"> | ||||||
| 	<mk-avatar class="avatar" :user="note.user"/> | 	<mk-avatar class="avatar" :user="note.user"/> | ||||||
| 	<div class="main"> | 	<div class="main"> | ||||||
| 		<header> | 		<mk-note-header class="header" :note="note"/> | ||||||
| 			<router-link class="name" :to="note.user | userPage" v-user-preview="note.userId">{{ note.user | userName }}</router-link> |  | ||||||
| 			<span class="is-admin" v-if="note.user.isAdmin">admin</span> |  | ||||||
| 			<span class="is-bot" v-if="note.user.isBot">bot</span> |  | ||||||
| 			<span class="is-cat" v-if="note.user.isCat">cat</span> |  | ||||||
| 			<span class="username"><mk-acct :user="note.user"/></span> |  | ||||||
| 			<div class="info"> |  | ||||||
| 				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> |  | ||||||
| 				<router-link class="created-at" :to="note | notePage"> |  | ||||||
| 					<mk-time :time="note.createdAt"/> |  | ||||||
| 				</router-link> |  | ||||||
| 				<span class="visibility" v-if="note.visibility != 'public'"> |  | ||||||
| 					<template v-if="note.visibility == 'home'">%fa:home%</template> |  | ||||||
| 					<template v-if="note.visibility == 'followers'">%fa:unlock%</template> |  | ||||||
| 					<template v-if="note.visibility == 'specified'">%fa:envelope%</template> |  | ||||||
| 					<template v-if="note.visibility == 'private'">%fa:lock%</template> |  | ||||||
| 				</span> |  | ||||||
| 			</div> |  | ||||||
| 		</header> |  | ||||||
| 		<div class="body"> | 		<div class="body"> | ||||||
| 			<mk-sub-note-content class="text" :note="note"/> | 			<mk-sub-note-content class="text" :note="note"/> | ||||||
| 		</div> | 		</div> | ||||||
| @@ -62,57 +44,8 @@ root(isDark) | |||||||
| 		flex 1 | 		flex 1 | ||||||
| 		min-width 0 | 		min-width 0 | ||||||
|  |  | ||||||
| 		> header | 		> .header | ||||||
| 			display flex |  | ||||||
| 			align-items baseline |  | ||||||
| 			margin-bottom 2px | 			margin-bottom 2px | ||||||
| 			white-space nowrap |  | ||||||
|  |  | ||||||
| 			> .name |  | ||||||
| 				display block |  | ||||||
| 				margin 0 .5em 0 0 |  | ||||||
| 				padding 0 |  | ||||||
| 				overflow hidden |  | ||||||
| 				color isDark ? #fff : #607073 |  | ||||||
| 				font-size 1em |  | ||||||
| 				font-weight bold |  | ||||||
| 				text-decoration none |  | ||||||
| 				text-overflow ellipsis |  | ||||||
|  |  | ||||||
| 				&:hover |  | ||||||
| 					text-decoration underline |  | ||||||
|  |  | ||||||
| 			> .is-admin |  | ||||||
| 			> .is-bot |  | ||||||
| 			> .is-cat |  | ||||||
| 				align-self center |  | ||||||
| 				margin 0 0.5em 0 0 |  | ||||||
| 				padding 1px 5px |  | ||||||
| 				font-size 10px |  | ||||||
| 				color isDark ? #758188 : #aaa |  | ||||||
| 				border solid 1px isDark ? #57616f : #ddd |  | ||||||
| 				border-radius 3px |  | ||||||
|  |  | ||||||
| 				&.is-admin |  | ||||||
| 					border-color isDark ? #d42c41 : #f56a7b |  | ||||||
| 					color isDark ? #d42c41 : #f56a7b |  | ||||||
|  |  | ||||||
| 			> .username |  | ||||||
| 				margin 0 .5em 0 0 |  | ||||||
| 				color isDark ? #606984 : #d1d8da |  | ||||||
|  |  | ||||||
| 			> .info |  | ||||||
| 				margin-left auto |  | ||||||
| 				font-size 0.9em |  | ||||||
|  |  | ||||||
| 				> * |  | ||||||
| 					color isDark ? #606984 : #b2b8bb |  | ||||||
|  |  | ||||||
| 				> .mobile |  | ||||||
| 					margin-right 6px |  | ||||||
|  |  | ||||||
| 				> .visibility |  | ||||||
| 					margin-left 6px |  | ||||||
|  |  | ||||||
| 		> .body | 		> .body | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,26 +14,7 @@ | |||||||
| 	<article> | 	<article> | ||||||
| 		<mk-avatar class="avatar" :user="p.user"/> | 		<mk-avatar class="avatar" :user="p.user"/> | ||||||
| 		<div class="main"> | 		<div class="main"> | ||||||
| 			<header> | 			<mk-note-header class="header" :note="p"/> | ||||||
| 				<router-link class="name" :to="p.user | userPage" v-user-preview="p.user.id">{{ p.user | userName }}</router-link> |  | ||||||
| 				<span class="is-admin" v-if="p.user.isAdmin">admin</span> |  | ||||||
| 				<span class="is-bot" v-if="p.user.isBot">bot</span> |  | ||||||
| 				<span class="is-cat" v-if="p.user.isCat">cat</span> |  | ||||||
| 				<span class="username"><mk-acct :user="p.user"/></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="p | notePage"> |  | ||||||
| 						<mk-time :time="p.createdAt"/> |  | ||||||
| 					</router-link> |  | ||||||
| 					<span class="visibility" v-if="p.visibility != 'public'"> |  | ||||||
| 						<template v-if="p.visibility == 'home'">%fa:home%</template> |  | ||||||
| 						<template v-if="p.visibility == 'followers'">%fa:unlock%</template> |  | ||||||
| 						<template v-if="p.visibility == 'specified'">%fa:envelope%</template> |  | ||||||
| 						<template v-if="p.visibility == 'private'">%fa:lock%</template> |  | ||||||
| 					</span> |  | ||||||
| 				</div> |  | ||||||
| 			</header> |  | ||||||
| 			<div class="body"> | 			<div class="body"> | ||||||
| 				<p v-if="p.cw != null" class="cw"> | 				<p v-if="p.cw != null" class="cw"> | ||||||
| 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | ||||||
| @@ -409,64 +390,8 @@ root(isDark) | |||||||
| 			flex 1 | 			flex 1 | ||||||
| 			min-width 0 | 			min-width 0 | ||||||
|  |  | ||||||
| 			> header | 			> .header | ||||||
| 				display flex |  | ||||||
| 				align-items baseline |  | ||||||
| 				margin-bottom 4px | 				margin-bottom 4px | ||||||
| 				white-space nowrap |  | ||||||
|  |  | ||||||
| 				> .name |  | ||||||
| 					display block |  | ||||||
| 					margin 0 .5em 0 0 |  | ||||||
| 					padding 0 |  | ||||||
| 					overflow hidden |  | ||||||
| 					color isDark ? #fff : #627079 |  | ||||||
| 					font-size 1em |  | ||||||
| 					font-weight bold |  | ||||||
| 					text-decoration none |  | ||||||
| 					text-overflow ellipsis |  | ||||||
|  |  | ||||||
| 					&:hover |  | ||||||
| 						text-decoration underline |  | ||||||
|  |  | ||||||
| 				> .is-admin |  | ||||||
| 				> .is-bot |  | ||||||
| 				> .is-cat |  | ||||||
| 					align-self center |  | ||||||
| 					margin 0 .5em 0 0 |  | ||||||
| 					padding 1px 6px |  | ||||||
| 					font-size 12px |  | ||||||
| 					color isDark ? #758188 : #aaa |  | ||||||
| 					border solid 1px isDark ? #57616f : #ddd |  | ||||||
| 					border-radius 3px |  | ||||||
|  |  | ||||||
| 					&.is-admin |  | ||||||
| 						border-color isDark ? #d42c41 : #f56a7b |  | ||||||
| 						color isDark ? #d42c41 : #f56a7b |  | ||||||
|  |  | ||||||
| 				> .username |  | ||||||
| 					margin 0 .5em 0 0 |  | ||||||
| 					overflow hidden |  | ||||||
| 					text-overflow ellipsis |  | ||||||
| 					color isDark ? #606984 : #ccc |  | ||||||
|  |  | ||||||
| 				> .info |  | ||||||
| 					margin-left auto |  | ||||||
| 					font-size 0.9em |  | ||||||
|  |  | ||||||
| 					> * |  | ||||||
| 						color isDark ? #606984 : #c0c0c0 |  | ||||||
|  |  | ||||||
| 					> .mobile |  | ||||||
| 						margin-right 8px |  | ||||||
|  |  | ||||||
| 					> .app |  | ||||||
| 						margin-right 8px |  | ||||||
| 						padding-right 8px |  | ||||||
| 						border-right solid 1px isDark ? #1c2023 : #eaeaea |  | ||||||
|  |  | ||||||
| 					> .visibility |  | ||||||
| 						margin-left 8px |  | ||||||
|  |  | ||||||
| 			> .body | 			> .body | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| <template> | <template> | ||||||
| <div class="mk-ui" :style="style"> | <div class="mk-ui" :style="style"> | ||||||
| 	<x-header class="header"/> | 	<x-header class="header" v-show="!zenMode"/> | ||||||
| 	<div class="content"> | 	<div class="content"> | ||||||
| 		<slot></slot> | 		<slot></slot> | ||||||
| 	</div> | 	</div> | ||||||
| @@ -16,6 +16,11 @@ export default Vue.extend({ | |||||||
| 	components: { | 	components: { | ||||||
| 		XHeader | 		XHeader | ||||||
| 	}, | 	}, | ||||||
|  | 	data() { | ||||||
|  | 		return { | ||||||
|  | 			zenMode: false | ||||||
|  | 		}; | ||||||
|  | 	}, | ||||||
| 	computed: { | 	computed: { | ||||||
| 		style(): any { | 		style(): any { | ||||||
| 			if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {}; | 			if (!this.$store.getters.isSignedIn || this.$store.state.i.wallpaperUrl == null) return {}; | ||||||
| @@ -39,6 +44,11 @@ export default Vue.extend({ | |||||||
| 				e.preventDefault(); | 				e.preventDefault(); | ||||||
| 				(this as any).apis.post(); | 				(this as any).apis.post(); | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
|  | 			if (e.which == 90) { // z | ||||||
|  | 				e.preventDefault(); | ||||||
|  | 				this.zenMode = !this.zenMode; | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -2,25 +2,7 @@ | |||||||
| <div class="fnlfosztlhtptnongximhlbykxblytcq"> | <div class="fnlfosztlhtptnongximhlbykxblytcq"> | ||||||
| 	<mk-avatar class="avatar" :user="note.user"/> | 	<mk-avatar class="avatar" :user="note.user"/> | ||||||
| 	<div class="main"> | 	<div class="main"> | ||||||
| 		<header> | 		<mk-note-header class="header" :note="note" :mini="true"/> | ||||||
| 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> |  | ||||||
| 			<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span> |  | ||||||
| 			<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span> |  | ||||||
| 			<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span> |  | ||||||
| 			<span class="username"><mk-acct :user="note.user"/></span> |  | ||||||
| 			<div class="info"> |  | ||||||
| 				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> |  | ||||||
| 				<router-link class="created-at" :to="note | notePage"> |  | ||||||
| 					<mk-time :time="note.createdAt"/> |  | ||||||
| 				</router-link> |  | ||||||
| 				<span class="visibility" v-if="note.visibility != 'public'"> |  | ||||||
| 					<template v-if="note.visibility == 'home'">%fa:home%</template> |  | ||||||
| 					<template v-if="note.visibility == 'followers'">%fa:unlock%</template> |  | ||||||
| 					<template v-if="note.visibility == 'specified'">%fa:envelope%</template> |  | ||||||
| 					<template v-if="note.visibility == 'private'">%fa:lock%</template> |  | ||||||
| 				</span> |  | ||||||
| 			</div> |  | ||||||
| 		</header> |  | ||||||
| 		<div class="body"> | 		<div class="body"> | ||||||
| 			<mk-sub-note-content class="text" :note="note"/> | 			<mk-sub-note-content class="text" :note="note"/> | ||||||
| 		</div> | 		</div> | ||||||
| @@ -72,66 +54,8 @@ root(isDark) | |||||||
| 		flex 1 | 		flex 1 | ||||||
| 		min-width 0 | 		min-width 0 | ||||||
|  |  | ||||||
| 		> header | 		> .header | ||||||
| 			display flex |  | ||||||
| 			align-items baseline |  | ||||||
| 			margin-bottom 2px | 			margin-bottom 2px | ||||||
| 			white-space nowrap |  | ||||||
|  |  | ||||||
| 			> .avatar |  | ||||||
| 				flex-shrink 0 |  | ||||||
| 				margin-right 8px |  | ||||||
| 				width 18px |  | ||||||
| 				height 18px |  | ||||||
| 				border-radius 100% |  | ||||||
|  |  | ||||||
| 			> .name |  | ||||||
| 				display block |  | ||||||
| 				margin 0 0.5em 0 0 |  | ||||||
| 				padding 0 |  | ||||||
| 				overflow hidden |  | ||||||
| 				color isDark ? #fff : #607073 |  | ||||||
| 				font-size 1em |  | ||||||
| 				font-weight 700 |  | ||||||
| 				text-align left |  | ||||||
| 				text-decoration none |  | ||||||
| 				text-overflow ellipsis |  | ||||||
|  |  | ||||||
| 				&:hover |  | ||||||
| 					text-decoration underline |  | ||||||
|  |  | ||||||
| 			> .is-admin |  | ||||||
| 			> .is-bot |  | ||||||
| 			> .is-cat |  | ||||||
| 				align-self center |  | ||||||
| 				margin 0 0.5em 0 0 |  | ||||||
| 				padding 1px 5px |  | ||||||
| 				font-size 0.8em |  | ||||||
| 				color isDark ? #758188 : #aaa |  | ||||||
| 				border solid 1px isDark ? #57616f : #ddd |  | ||||||
| 				border-radius 3px |  | ||||||
|  |  | ||||||
| 				&.is-admin |  | ||||||
| 					border-color isDark ? #d42c41 : #f56a7b |  | ||||||
| 					color isDark ? #d42c41 : #f56a7b |  | ||||||
|  |  | ||||||
| 			> .username |  | ||||||
| 				text-align left |  | ||||||
| 				margin 0 |  | ||||||
| 				color isDark ? #606984 : #d1d8da |  | ||||||
|  |  | ||||||
| 			> .info |  | ||||||
| 				margin-left auto |  | ||||||
| 				font-size 0.9em |  | ||||||
|  |  | ||||||
| 				> * |  | ||||||
| 					color isDark ? #606984 : #b2b8bb |  | ||||||
|  |  | ||||||
| 				> .mobile |  | ||||||
| 					margin-right 6px |  | ||||||
|  |  | ||||||
| 				> .visibility |  | ||||||
| 					margin-left 6px |  | ||||||
|  |  | ||||||
| 		> .body | 		> .body | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,25 +14,7 @@ | |||||||
| 	<article> | 	<article> | ||||||
| 		<mk-avatar class="avatar" :user="p.user"/> | 		<mk-avatar class="avatar" :user="p.user"/> | ||||||
| 		<div class="main"> | 		<div class="main"> | ||||||
| 			<header> | 			<mk-note-header class="header" :note="p" :mini="true"/> | ||||||
| 				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> |  | ||||||
| 				<span class="is-admin" v-if="p.user.isAdmin">admin</span> |  | ||||||
| 				<span class="is-bot" v-if="p.user.isBot">bot</span> |  | ||||||
| 				<span class="is-cat" v-if="p.user.isCat">cat</span> |  | ||||||
| 				<span class="username"><mk-acct :user="p.user"/></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> |  | ||||||
| 					<span class="visibility" v-if="p.visibility != 'public'"> |  | ||||||
| 						<template v-if="p.visibility == 'home'">%fa:home%</template> |  | ||||||
| 						<template v-if="p.visibility == 'followers'">%fa:unlock%</template> |  | ||||||
| 						<template v-if="p.visibility == 'specified'">%fa:envelope%</template> |  | ||||||
| 						<template v-if="p.visibility == 'private'">%fa:lock%</template> |  | ||||||
| 					</span> |  | ||||||
| 				</div> |  | ||||||
| 			</header> |  | ||||||
| 			<div class="body"> | 			<div class="body"> | ||||||
| 				<p v-if="p.cw != null" class="cw"> | 				<p v-if="p.cw != null" class="cw"> | ||||||
| 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | ||||||
| @@ -234,7 +216,7 @@ root(isDark) | |||||||
| 	> .renote | 	> .renote | ||||||
| 		display flex | 		display flex | ||||||
| 		align-items center | 		align-items center | ||||||
| 		padding 8px 16px | 		padding 8px 16px 0 16px | ||||||
| 		line-height 28px | 		line-height 28px | ||||||
| 		white-space pre | 		white-space pre | ||||||
| 		color #9dbb00 | 		color #9dbb00 | ||||||
| @@ -292,62 +274,6 @@ root(isDark) | |||||||
| 			flex 1 | 			flex 1 | ||||||
| 			min-width 0 | 			min-width 0 | ||||||
|  |  | ||||||
| 			> header |  | ||||||
| 				display flex |  | ||||||
| 				align-items baseline |  | ||||||
| 				white-space nowrap |  | ||||||
|  |  | ||||||
| 				> .avatar |  | ||||||
| 					flex-shrink 0 |  | ||||||
| 					margin-right 8px |  | ||||||
| 					width 20px |  | ||||||
| 					height 20px |  | ||||||
| 					border-radius 100% |  | ||||||
|  |  | ||||||
| 				> .name |  | ||||||
| 					display block |  | ||||||
| 					margin 0 0.5em 0 0 |  | ||||||
| 					padding 0 |  | ||||||
| 					overflow hidden |  | ||||||
| 					color isDark ? #fff : #627079 |  | ||||||
| 					font-weight bold |  | ||||||
| 					text-decoration none |  | ||||||
| 					text-overflow ellipsis |  | ||||||
|  |  | ||||||
| 				> .is-admin |  | ||||||
| 				> .is-bot |  | ||||||
| 				> .is-cat |  | ||||||
| 					align-self center |  | ||||||
| 					margin 0 0.5em 0 0 |  | ||||||
| 					padding 1px 6px |  | ||||||
| 					font-size 0.8em |  | ||||||
| 					color isDark ? #758188 : #aaa |  | ||||||
| 					border solid 1px isDark ? #57616f : #ddd |  | ||||||
| 					border-radius 3px |  | ||||||
|  |  | ||||||
| 					&.is-admin |  | ||||||
| 						border-color isDark ? #d42c41 : #f56a7b |  | ||||||
| 						color isDark ? #d42c41 : #f56a7b |  | ||||||
|  |  | ||||||
| 				> .username |  | ||||||
| 					margin 0 0.5em 0 0 |  | ||||||
| 					overflow hidden |  | ||||||
| 					text-overflow ellipsis |  | ||||||
| 					color isDark ? #606984 : #ccc |  | ||||||
|  |  | ||||||
| 				> .info |  | ||||||
| 					margin-left auto |  | ||||||
| 					font-size 0.9em |  | ||||||
|  |  | ||||||
| 					> * |  | ||||||
| 						color isDark ? #606984 : #c0c0c0 |  | ||||||
|  |  | ||||||
| 					> .mobile |  | ||||||
| 						margin-right 6px |  | ||||||
|  |  | ||||||
| 					> .visibility |  | ||||||
| 						margin-left 6px |  | ||||||
|  |  | ||||||
| 			> .body | 			> .body | ||||||
|  |  | ||||||
| 				> .cw | 				> .cw | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ import Vue from 'vue'; | |||||||
|  |  | ||||||
| import XNote from './deck.note.vue'; | import XNote from './deck.note.vue'; | ||||||
|  |  | ||||||
| const displayLimit = 30; | const displayLimit = 20; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
|   | |||||||
| @@ -21,20 +21,27 @@ | |||||||
| import Vue from 'vue'; | import Vue from 'vue'; | ||||||
| import XNotification from './deck.notification.vue'; | import XNotification from './deck.notification.vue'; | ||||||
|  |  | ||||||
|  | const displayLimit = 20; | ||||||
|  |  | ||||||
| export default Vue.extend({ | export default Vue.extend({ | ||||||
| 	components: { | 	components: { | ||||||
| 		XNotification | 		XNotification | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | 	inject: ['column', 'isScrollTop', 'count'], | ||||||
|  |  | ||||||
| 	data() { | 	data() { | ||||||
| 		return { | 		return { | ||||||
| 			fetching: true, | 			fetching: true, | ||||||
| 			fetchingMoreNotifications: false, | 			fetchingMoreNotifications: false, | ||||||
| 			notifications: [], | 			notifications: [], | ||||||
|  | 			queue: [], | ||||||
| 			moreNotifications: false, | 			moreNotifications: false, | ||||||
| 			connection: null, | 			connection: null, | ||||||
| 			connectionId: null | 			connectionId: null | ||||||
| 		}; | 		}; | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	computed: { | 	computed: { | ||||||
| 		_notifications(): any[] { | 		_notifications(): any[] { | ||||||
| 			return (this.notifications as any).map(notification => { | 			return (this.notifications as any).map(notification => { | ||||||
| @@ -46,12 +53,22 @@ export default Vue.extend({ | |||||||
| 			}); | 			}); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
|  | 	watch: { | ||||||
|  | 		queue(q) { | ||||||
|  | 			this.count(q.length); | ||||||
|  | 		} | ||||||
|  | 	}, | ||||||
|  |  | ||||||
| 	mounted() { | 	mounted() { | ||||||
| 		this.connection = (this as any).os.stream.getConnection(); | 		this.connection = (this as any).os.stream.getConnection(); | ||||||
| 		this.connectionId = (this as any).os.stream.use(); | 		this.connectionId = (this as any).os.stream.use(); | ||||||
|  |  | ||||||
| 		this.connection.on('notification', this.onNotification); | 		this.connection.on('notification', this.onNotification); | ||||||
|  |  | ||||||
|  | 		this.column.$on('top', this.onTop); | ||||||
|  | 		this.column.$on('bottom', this.onBottom); | ||||||
|  |  | ||||||
| 		const max = 10; | 		const max = 10; | ||||||
|  |  | ||||||
| 		(this as any).api('i/notifications', { | 		(this as any).api('i/notifications', { | ||||||
| @@ -66,10 +83,15 @@ export default Vue.extend({ | |||||||
| 			this.fetching = false; | 			this.fetching = false; | ||||||
| 		}); | 		}); | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	beforeDestroy() { | 	beforeDestroy() { | ||||||
| 		this.connection.off('notification', this.onNotification); | 		this.connection.off('notification', this.onNotification); | ||||||
| 		(this as any).os.stream.dispose(this.connectionId); | 		(this as any).os.stream.dispose(this.connectionId); | ||||||
|  |  | ||||||
|  | 		this.column.$off('top', this.onTop); | ||||||
|  | 		this.column.$off('bottom', this.onBottom); | ||||||
| 	}, | 	}, | ||||||
|  |  | ||||||
| 	methods: { | 	methods: { | ||||||
| 		fetchMoreNotifications() { | 		fetchMoreNotifications() { | ||||||
| 			this.fetchingMoreNotifications = true; | 			this.fetchingMoreNotifications = true; | ||||||
| @@ -90,6 +112,7 @@ export default Vue.extend({ | |||||||
| 				this.fetchingMoreNotifications = false; | 				this.fetchingMoreNotifications = false; | ||||||
| 			}); | 			}); | ||||||
| 		}, | 		}, | ||||||
|  |  | ||||||
| 		onNotification(notification) { | 		onNotification(notification) { | ||||||
| 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない | 			// TODO: ユーザーが画面を見てないと思われるとき(ブラウザやタブがアクティブじゃないなど)は送信しない | ||||||
| 			this.connection.send({ | 			this.connection.send({ | ||||||
| @@ -97,7 +120,34 @@ export default Vue.extend({ | |||||||
| 				id: notification.id | 				id: notification.id | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
|  | 			this.prepend(notification); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		prepend(notification) { | ||||||
|  | 			if (this.isScrollTop()) { | ||||||
|  | 				// Prepend the notification | ||||||
| 				this.notifications.unshift(notification); | 				this.notifications.unshift(notification); | ||||||
|  |  | ||||||
|  | 				// オーバーフローしたら古い通知は捨てる | ||||||
|  | 				if (this.notifications.length >= displayLimit) { | ||||||
|  | 					this.notifications = this.notifications.slice(0, displayLimit); | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				this.queue.push(notification); | ||||||
|  | 			} | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		releaseQueue() { | ||||||
|  | 			this.queue.forEach(n => this.prepend(n)); | ||||||
|  | 			this.queue = []; | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		onTop() { | ||||||
|  | 			this.releaseQueue(); | ||||||
|  | 		}, | ||||||
|  |  | ||||||
|  | 		onBottom() { | ||||||
|  | 			this.fetchMoreNotifications(); | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ | |||||||
| 					<option value="post-form">%i18n:common.widgets.post-form%</option> | 					<option value="post-form">%i18n:common.widgets.post-form%</option> | ||||||
| 					<option value="messaging">%i18n:common.widgets.messaging%</option> | 					<option value="messaging">%i18n:common.widgets.messaging%</option> | ||||||
| 					<option value="memo">%i18n:common.widgets.memo%</option> | 					<option value="memo">%i18n:common.widgets.memo%</option> | ||||||
|  | 					<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option> | ||||||
| 					<option value="server">%i18n:common.widgets.server%</option> | 					<option value="server">%i18n:common.widgets.server%</option> | ||||||
| 					<option value="donation">%i18n:common.widgets.donation%</option> | 					<option value="donation">%i18n:common.widgets.donation%</option> | ||||||
| 					<option value="nav">%i18n:common.widgets.nav%</option> | 					<option value="nav">%i18n:common.widgets.nav%</option> | ||||||
|   | |||||||
| @@ -8,7 +8,8 @@ import Progress from './common/scripts/loading'; | |||||||
| import Connection from './common/scripts/streaming/stream'; | import Connection from './common/scripts/streaming/stream'; | ||||||
| import { HomeStreamManager } from './common/scripts/streaming/home'; | import { HomeStreamManager } from './common/scripts/streaming/home'; | ||||||
| import { DriveStreamManager } from './common/scripts/streaming/drive'; | import { DriveStreamManager } from './common/scripts/streaming/drive'; | ||||||
| import { ServerStreamManager } from './common/scripts/streaming/server'; | import { ServerStatsStreamManager } from './common/scripts/streaming/server-stats'; | ||||||
|  | import { NotesStatsStreamManager } from './common/scripts/streaming/notes-stats'; | ||||||
| import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index'; | import { MessagingIndexStreamManager } from './common/scripts/streaming/messaging-index'; | ||||||
| import { OthelloStreamManager } from './common/scripts/streaming/othello'; | import { OthelloStreamManager } from './common/scripts/streaming/othello'; | ||||||
|  |  | ||||||
| @@ -104,14 +105,16 @@ export default class MiOS extends EventEmitter { | |||||||
| 		localTimelineStream: LocalTimelineStreamManager; | 		localTimelineStream: LocalTimelineStreamManager; | ||||||
| 		globalTimelineStream: GlobalTimelineStreamManager; | 		globalTimelineStream: GlobalTimelineStreamManager; | ||||||
| 		driveStream: DriveStreamManager; | 		driveStream: DriveStreamManager; | ||||||
| 		serverStream: ServerStreamManager; | 		serverStatsStream: ServerStatsStreamManager; | ||||||
|  | 		notesStatsStream: NotesStatsStreamManager; | ||||||
| 		messagingIndexStream: MessagingIndexStreamManager; | 		messagingIndexStream: MessagingIndexStreamManager; | ||||||
| 		othelloStream: OthelloStreamManager; | 		othelloStream: OthelloStreamManager; | ||||||
| 	} = { | 	} = { | ||||||
| 		localTimelineStream: null, | 		localTimelineStream: null, | ||||||
| 		globalTimelineStream: null, | 		globalTimelineStream: null, | ||||||
| 		driveStream: null, | 		driveStream: null, | ||||||
| 		serverStream: null, | 		serverStatsStream: null, | ||||||
|  | 		notesStatsStream: null, | ||||||
| 		messagingIndexStream: null, | 		messagingIndexStream: null, | ||||||
| 		othelloStream: null | 		othelloStream: null | ||||||
| 	}; | 	}; | ||||||
| @@ -218,7 +221,8 @@ export default class MiOS extends EventEmitter { | |||||||
| 		this.store = initStore(this); | 		this.store = initStore(this); | ||||||
|  |  | ||||||
| 		//#region Init stream managers | 		//#region Init stream managers | ||||||
| 		this.streams.serverStream = new ServerStreamManager(this); | 		this.streams.serverStatsStream = new ServerStatsStreamManager(this); | ||||||
|  | 		this.streams.notesStatsStream = new NotesStatsStreamManager(this); | ||||||
|  |  | ||||||
| 		this.once('signedin', () => { | 		this.once('signedin', () => { | ||||||
| 			// Init home stream manager | 			// Init home stream manager | ||||||
|   | |||||||
| @@ -2,26 +2,7 @@ | |||||||
| <div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }"> | <div class="mk-note-preview" :class="{ smart: $store.state.device.postStyle == 'smart' }"> | ||||||
| 	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> | 	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> | ||||||
| 	<div class="main"> | 	<div class="main"> | ||||||
| 		<header> | 		<mk-note-header class="header" :note="note" :mini="true"/> | ||||||
| 			<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/> |  | ||||||
| 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> |  | ||||||
| 			<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span> |  | ||||||
| 			<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span> |  | ||||||
| 			<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span> |  | ||||||
| 			<span class="username"><mk-acct :user="note.user"/></span> |  | ||||||
| 			<div class="info"> |  | ||||||
| 				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> |  | ||||||
| 				<router-link class="created-at" :to="note | notePage"> |  | ||||||
| 					<mk-time :time="note.createdAt"/> |  | ||||||
| 				</router-link> |  | ||||||
| 				<span class="visibility" v-if="note.visibility != 'public'"> |  | ||||||
| 					<template v-if="note.visibility == 'home'">%fa:home%</template> |  | ||||||
| 					<template v-if="note.visibility == 'followers'">%fa:unlock%</template> |  | ||||||
| 					<template v-if="note.visibility == 'specified'">%fa:envelope%</template> |  | ||||||
| 					<template v-if="note.visibility == 'private'">%fa:lock%</template> |  | ||||||
| 				</span> |  | ||||||
| 			</div> |  | ||||||
| 		</header> |  | ||||||
| 		<div class="body"> | 		<div class="body"> | ||||||
| 			<mk-sub-note-content class="text" :note="note"/> | 			<mk-sub-note-content class="text" :note="note"/> | ||||||
| 		</div> | 		</div> | ||||||
| @@ -79,64 +60,8 @@ root(isDark) | |||||||
| 		flex 1 | 		flex 1 | ||||||
| 		min-width 0 | 		min-width 0 | ||||||
|  |  | ||||||
| 		> header | 		> .header | ||||||
| 			display flex |  | ||||||
| 			align-items baseline |  | ||||||
| 			margin-bottom 2px | 			margin-bottom 2px | ||||||
| 			white-space nowrap |  | ||||||
|  |  | ||||||
| 			> .avatar |  | ||||||
| 				flex-shrink 0 |  | ||||||
| 				margin-right 8px |  | ||||||
| 				width 18px |  | ||||||
| 				height 18px |  | ||||||
| 				border-radius 100% |  | ||||||
|  |  | ||||||
| 			> .name |  | ||||||
| 				display block |  | ||||||
| 				margin 0 .5em 0 0 |  | ||||||
| 				padding 0 |  | ||||||
| 				overflow hidden |  | ||||||
| 				color isDark ? #fff : #607073 |  | ||||||
| 				font-size 1em |  | ||||||
| 				font-weight 700 |  | ||||||
| 				text-align left |  | ||||||
| 				text-decoration none |  | ||||||
| 				text-overflow ellipsis |  | ||||||
|  |  | ||||||
| 			> .is-admin |  | ||||||
| 			> .is-bot |  | ||||||
| 			> .is-cat |  | ||||||
| 				align-self center |  | ||||||
| 				margin 0 0.5em 0 0 |  | ||||||
| 				padding 1px 6px |  | ||||||
| 				font-size 0.8em |  | ||||||
| 				color isDark ? #758188 : #aaa |  | ||||||
| 				border solid 1px isDark ? #57616f : #ddd |  | ||||||
| 				border-radius 3px |  | ||||||
|  |  | ||||||
| 				&.is-admin |  | ||||||
| 					border-color isDark ? #d42c41 : #f56a7b |  | ||||||
| 					color isDark ? #d42c41 : #f56a7b |  | ||||||
|  |  | ||||||
| 			> .username |  | ||||||
| 				margin 0 .5em 0 0 |  | ||||||
| 				overflow hidden |  | ||||||
| 				text-overflow ellipsis |  | ||||||
| 				color isDark ? #606984 : #d1d8da |  | ||||||
|  |  | ||||||
| 			> .info |  | ||||||
| 				margin-left auto |  | ||||||
| 				font-size 0.9em |  | ||||||
|  |  | ||||||
| 				> * |  | ||||||
| 					color isDark ? #606984 : #b2b8bb |  | ||||||
|  |  | ||||||
| 				> .mobile |  | ||||||
| 					margin-right 6px |  | ||||||
|  |  | ||||||
| 				> .visibility |  | ||||||
| 					margin-left 6px |  | ||||||
|  |  | ||||||
| 		> .body | 		> .body | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,26 +2,7 @@ | |||||||
| <div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }"> | <div class="sub" :class="{ smart: $store.state.device.postStyle == 'smart' }"> | ||||||
| 	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> | 	<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle != 'smart'"/> | ||||||
| 	<div class="main"> | 	<div class="main"> | ||||||
| 		<header> | 		<mk-note-header class="header" :note="note" :mini="true"/> | ||||||
| 			<mk-avatar class="avatar" :user="note.user" v-if="$store.state.device.postStyle == 'smart'"/> |  | ||||||
| 			<router-link class="name" :to="note.user | userPage">{{ note.user | userName }}</router-link> |  | ||||||
| 			<span class="is-admin" v-if="note.user.isAdmin">%i18n:@admin%</span> |  | ||||||
| 			<span class="is-bot" v-if="note.user.isBot">%i18n:@bot%</span> |  | ||||||
| 			<span class="is-cat" v-if="note.user.isCat">%i18n:@cat%</span> |  | ||||||
| 			<span class="username"><mk-acct :user="note.user"/></span> |  | ||||||
| 			<div class="info"> |  | ||||||
| 				<span class="mobile" v-if="note.viaMobile">%fa:mobile-alt%</span> |  | ||||||
| 				<router-link class="created-at" :to="note | notePage"> |  | ||||||
| 					<mk-time :time="note.createdAt"/> |  | ||||||
| 				</router-link> |  | ||||||
| 				<span class="visibility" v-if="note.visibility != 'public'"> |  | ||||||
| 					<template v-if="note.visibility == 'home'">%fa:home%</template> |  | ||||||
| 					<template v-if="note.visibility == 'followers'">%fa:unlock%</template> |  | ||||||
| 					<template v-if="note.visibility == 'specified'">%fa:envelope%</template> |  | ||||||
| 					<template v-if="note.visibility == 'private'">%fa:lock%</template> |  | ||||||
| 				</span> |  | ||||||
| 			</div> |  | ||||||
| 		</header> |  | ||||||
| 		<div class="body"> | 		<div class="body"> | ||||||
| 			<mk-sub-note-content class="text" :note="note"/> | 			<mk-sub-note-content class="text" :note="note"/> | ||||||
| 		</div> | 		</div> | ||||||
| @@ -92,66 +73,8 @@ root(isDark) | |||||||
| 		flex 1 | 		flex 1 | ||||||
| 		min-width 0 | 		min-width 0 | ||||||
|  |  | ||||||
| 		> header | 		> .header | ||||||
| 			display flex |  | ||||||
| 			align-items baseline |  | ||||||
| 			margin-bottom 2px | 			margin-bottom 2px | ||||||
| 			white-space nowrap |  | ||||||
|  |  | ||||||
| 			> .avatar |  | ||||||
| 				flex-shrink 0 |  | ||||||
| 				margin-right 8px |  | ||||||
| 				width 18px |  | ||||||
| 				height 18px |  | ||||||
| 				border-radius 100% |  | ||||||
|  |  | ||||||
| 			> .name |  | ||||||
| 				display block |  | ||||||
| 				margin 0 0.5em 0 0 |  | ||||||
| 				padding 0 |  | ||||||
| 				overflow hidden |  | ||||||
| 				color isDark ? #fff : #607073 |  | ||||||
| 				font-size 1em |  | ||||||
| 				font-weight 700 |  | ||||||
| 				text-align left |  | ||||||
| 				text-decoration none |  | ||||||
| 				text-overflow ellipsis |  | ||||||
|  |  | ||||||
| 				&:hover |  | ||||||
| 					text-decoration underline |  | ||||||
|  |  | ||||||
| 			> .is-admin |  | ||||||
| 			> .is-bot |  | ||||||
| 			> .is-cat |  | ||||||
| 				align-self center |  | ||||||
| 				margin 0 0.5em 0 0 |  | ||||||
| 				padding 1px 5px |  | ||||||
| 				font-size 0.8em |  | ||||||
| 				color isDark ? #758188 : #aaa |  | ||||||
| 				border solid 1px isDark ? #57616f : #ddd |  | ||||||
| 				border-radius 3px |  | ||||||
|  |  | ||||||
| 				&.is-admin |  | ||||||
| 					border-color isDark ? #d42c41 : #f56a7b |  | ||||||
| 					color isDark ? #d42c41 : #f56a7b |  | ||||||
|  |  | ||||||
| 			> .username |  | ||||||
| 				text-align left |  | ||||||
| 				margin 0 |  | ||||||
| 				color isDark ? #606984 : #d1d8da |  | ||||||
|  |  | ||||||
| 			> .info |  | ||||||
| 				margin-left auto |  | ||||||
| 				font-size 0.9em |  | ||||||
|  |  | ||||||
| 				> * |  | ||||||
| 					color isDark ? #606984 : #b2b8bb |  | ||||||
|  |  | ||||||
| 				> .mobile |  | ||||||
| 					margin-right 6px |  | ||||||
|  |  | ||||||
| 				> .visibility |  | ||||||
| 					margin-left 6px |  | ||||||
|  |  | ||||||
| 		> .body | 		> .body | ||||||
|  |  | ||||||
|   | |||||||
| @@ -14,26 +14,7 @@ | |||||||
| 	<article> | 	<article> | ||||||
| 		<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle != 'smart'"/> | 		<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle != 'smart'"/> | ||||||
| 		<div class="main"> | 		<div class="main"> | ||||||
| 			<header> | 			<mk-note-header class="header" :note="p" :mini="true"/> | ||||||
| 				<mk-avatar class="avatar" :user="p.user" v-if="$store.state.device.postStyle == 'smart'"/> |  | ||||||
| 				<router-link class="name" :to="p.user | userPage">{{ p.user | userName }}</router-link> |  | ||||||
| 				<span class="is-admin" v-if="p.user.isAdmin">admin</span> |  | ||||||
| 				<span class="is-bot" v-if="p.user.isBot">bot</span> |  | ||||||
| 				<span class="is-cat" v-if="p.user.isCat">cat</span> |  | ||||||
| 				<span class="username"><mk-acct :user="p.user"/></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> |  | ||||||
| 					<span class="visibility" v-if="p.visibility != 'public'"> |  | ||||||
| 						<template v-if="p.visibility == 'home'">%fa:home%</template> |  | ||||||
| 						<template v-if="p.visibility == 'followers'">%fa:unlock%</template> |  | ||||||
| 						<template v-if="p.visibility == 'specified'">%fa:envelope%</template> |  | ||||||
| 						<template v-if="p.visibility == 'private'">%fa:lock%</template> |  | ||||||
| 					</span> |  | ||||||
| 				</div> |  | ||||||
| 			</header> |  | ||||||
| 			<div class="body"> | 			<div class="body"> | ||||||
| 				<p v-if="p.cw != null" class="cw"> | 				<p v-if="p.cw != null" class="cw"> | ||||||
| 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | 					<span class="text" v-if="p.cw != ''">{{ p.cw }}</span> | ||||||
| @@ -358,65 +339,10 @@ root(isDark) | |||||||
| 			flex 1 | 			flex 1 | ||||||
| 			min-width 0 | 			min-width 0 | ||||||
|  |  | ||||||
| 			> header | 			> .header | ||||||
| 				display flex |  | ||||||
| 				align-items baseline |  | ||||||
| 				white-space nowrap |  | ||||||
|  |  | ||||||
| 				@media (min-width 500px) | 				@media (min-width 500px) | ||||||
| 					margin-bottom 2px | 					margin-bottom 2px | ||||||
|  |  | ||||||
| 				> .avatar |  | ||||||
| 					flex-shrink 0 |  | ||||||
| 					margin-right 8px |  | ||||||
| 					width 20px |  | ||||||
| 					height 20px |  | ||||||
| 					border-radius 100% |  | ||||||
|  |  | ||||||
| 				> .name |  | ||||||
| 					display block |  | ||||||
| 					margin 0 0.5em 0 0 |  | ||||||
| 					padding 0 |  | ||||||
| 					overflow hidden |  | ||||||
| 					color isDark ? #fff : #627079 |  | ||||||
| 					font-weight bold |  | ||||||
| 					text-decoration none |  | ||||||
| 					text-overflow ellipsis |  | ||||||
|  |  | ||||||
| 				> .is-admin |  | ||||||
| 				> .is-bot |  | ||||||
| 				> .is-cat |  | ||||||
| 					align-self center |  | ||||||
| 					margin 0 0.5em 0 0 |  | ||||||
| 					padding 1px 6px |  | ||||||
| 					font-size 0.8em |  | ||||||
| 					color isDark ? #758188 : #aaa |  | ||||||
| 					border solid 1px isDark ? #57616f : #ddd |  | ||||||
| 					border-radius 3px |  | ||||||
|  |  | ||||||
| 					&.is-admin |  | ||||||
| 						border-color isDark ? #d42c41 : #f56a7b |  | ||||||
| 						color isDark ? #d42c41 : #f56a7b |  | ||||||
|  |  | ||||||
| 				> .username |  | ||||||
| 					margin 0 0.5em 0 0 |  | ||||||
| 					overflow hidden |  | ||||||
| 					text-overflow ellipsis |  | ||||||
| 					color isDark ? #606984 : #ccc |  | ||||||
|  |  | ||||||
| 				> .info |  | ||||||
| 					margin-left auto |  | ||||||
| 					font-size 0.9em |  | ||||||
|  |  | ||||||
| 					> * |  | ||||||
| 						color isDark ? #606984 : #c0c0c0 |  | ||||||
|  |  | ||||||
| 					> .mobile |  | ||||||
| 						margin-right 6px |  | ||||||
|  |  | ||||||
| 					> .visibility |  | ||||||
| 						margin-left 6px |  | ||||||
|  |  | ||||||
| 			> .body | 			> .body | ||||||
| 				@media (min-width 700px) | 				@media (min-width 700px) | ||||||
| 					font-size 1.1em | 					font-size 1.1em | ||||||
|   | |||||||
| @@ -15,6 +15,7 @@ | |||||||
| 					<option value="rss">%i18n:common.widgets.rss%</option> | 					<option value="rss">%i18n:common.widgets.rss%</option> | ||||||
| 					<option value="photo-stream">%i18n:common.widgets.photo-stream%</option> | 					<option value="photo-stream">%i18n:common.widgets.photo-stream%</option> | ||||||
| 					<option value="slideshow">%i18n:common.widgets.slideshow%</option> | 					<option value="slideshow">%i18n:common.widgets.slideshow%</option> | ||||||
|  | 					<option value="posts-monitor">%i18n:common.widgets.posts-monitor%</option> | ||||||
| 					<option value="version">%i18n:common.widgets.version%</option> | 					<option value="version">%i18n:common.widgets.version%</option> | ||||||
| 					<option value="server">%i18n:common.widgets.server%</option> | 					<option value="server">%i18n:common.widgets.server%</option> | ||||||
| 					<option value="memo">%i18n:common.widgets.memo%</option> | 					<option value="memo">%i18n:common.widgets.memo%</option> | ||||||
|   | |||||||
| @@ -12,7 +12,10 @@ const uri = u && p | |||||||
|  */ |  */ | ||||||
| import mongo from 'monk'; | import mongo from 'monk'; | ||||||
|  |  | ||||||
| const db = mongo(uri); | const db = mongo(uri, { | ||||||
|  | 	poolSize: 16, | ||||||
|  | 	keepAlive: 1 | ||||||
|  | }); | ||||||
|  |  | ||||||
| export default db; | export default db; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -17,7 +17,8 @@ import ProgressBar from './utils/cli/progressbar'; | |||||||
| import EnvironmentInfo from './utils/environmentInfo'; | import EnvironmentInfo from './utils/environmentInfo'; | ||||||
| import MachineInfo from './utils/machineInfo'; | import MachineInfo from './utils/machineInfo'; | ||||||
| import DependencyInfo from './utils/dependencyInfo'; | import DependencyInfo from './utils/dependencyInfo'; | ||||||
| import stats from './utils/stats'; | import serverStats from './server-stats'; | ||||||
|  | import notesStats from './notes-stats'; | ||||||
|  |  | ||||||
| import loadConfig from './config/load'; | import loadConfig from './config/load'; | ||||||
| import { Config } from './config/types'; | import { Config } from './config/types'; | ||||||
| @@ -49,7 +50,8 @@ function main() { | |||||||
| 		masterMain(opt); | 		masterMain(opt); | ||||||
|  |  | ||||||
| 		ev.mount(); | 		ev.mount(); | ||||||
| 		stats(); | 		serverStats(); | ||||||
|  | 		notesStats(); | ||||||
| 	} else { | 	} else { | ||||||
| 		workerMain(opt); | 		workerMain(opt); | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -16,6 +16,9 @@ import Following from './following'; | |||||||
| const Note = db.get<INote>('notes'); | const Note = db.get<INote>('notes'); | ||||||
| Note.createIndex('uri', { sparse: true, unique: true }); | Note.createIndex('uri', { sparse: true, unique: true }); | ||||||
| Note.createIndex('userId'); | Note.createIndex('userId'); | ||||||
|  | Note.createIndex({ | ||||||
|  | 	createdAt: -1 | ||||||
|  | }); | ||||||
| export default Note; | export default Note; | ||||||
|  |  | ||||||
| export function isValidText(text: string): boolean { | export function isValidText(text: string): boolean { | ||||||
|   | |||||||
							
								
								
									
										20
									
								
								src/notes-stats-child.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/notes-stats-child.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | import Note from './models/note'; | ||||||
|  |  | ||||||
|  | setInterval(async () => { | ||||||
|  | 	const [all, local] = await Promise.all([Note.count({ | ||||||
|  | 		createdAt: { | ||||||
|  | 			$gte: new Date(Date.now() - 3000) | ||||||
|  | 		} | ||||||
|  | 	}), Note.count({ | ||||||
|  | 		createdAt: { | ||||||
|  | 			$gte: new Date(Date.now() - 3000) | ||||||
|  | 		}, | ||||||
|  | 		'_user.host': null | ||||||
|  | 	})]); | ||||||
|  |  | ||||||
|  | 	const stats = { | ||||||
|  | 		all, local | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	process.send(stats); | ||||||
|  | }, 3000); | ||||||
							
								
								
									
										20
									
								
								src/notes-stats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/notes-stats.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,20 @@ | |||||||
|  | import * as childProcess from 'child_process'; | ||||||
|  | import Xev from 'xev'; | ||||||
|  |  | ||||||
|  | const ev = new Xev(); | ||||||
|  |  | ||||||
|  | export default function() { | ||||||
|  | 	const log = []; | ||||||
|  |  | ||||||
|  | 	const p = childProcess.fork(__dirname + '/notes-stats-child.js'); | ||||||
|  |  | ||||||
|  | 	p.on('message', stats => { | ||||||
|  | 		ev.emit('notesStats', stats); | ||||||
|  | 		log.push(stats); | ||||||
|  | 		if (log.length > 100) log.shift(); | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	ev.on('requestNotesStatsLog', id => { | ||||||
|  | 		ev.emit('notesStatsLog:' + id, log); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
| @@ -6,13 +6,19 @@ import Xev from 'xev'; | |||||||
| const ev = new Xev(); | const ev = new Xev(); | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Report stats regularly |  * Report server stats regularly | ||||||
|  */ |  */ | ||||||
| export default function() { | export default function() { | ||||||
|  | 	const log = []; | ||||||
|  | 
 | ||||||
|  | 	ev.on('requestServerStatsLog', id => { | ||||||
|  | 		ev.emit('serverStatsLog:' + id, log); | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
| 	setInterval(() => { | 	setInterval(() => { | ||||||
| 		osUtils.cpuUsage(cpuUsage => { | 		osUtils.cpuUsage(cpuUsage => { | ||||||
| 			const disk = diskusage.checkSync(os.platform() == 'win32' ? 'c:' : '/'); | 			const disk = diskusage.checkSync(os.platform() == 'win32' ? 'c:' : '/'); | ||||||
| 			ev.emit('stats', { | 			const stats = { | ||||||
| 				cpu_usage: cpuUsage, | 				cpu_usage: cpuUsage, | ||||||
| 				mem: { | 				mem: { | ||||||
| 					total: os.totalmem(), | 					total: os.totalmem(), | ||||||
| @@ -21,7 +27,10 @@ export default function() { | |||||||
| 				disk, | 				disk, | ||||||
| 				os_uptime: os.uptime(), | 				os_uptime: os.uptime(), | ||||||
| 				process_uptime: process.uptime() | 				process_uptime: process.uptime() | ||||||
| 			}); | 			}; | ||||||
|  | 			ev.emit('serverStats', stats); | ||||||
|  | 			log.push(stats); | ||||||
|  | 			if (log.length > 50) log.shift(); | ||||||
| 		}); | 		}); | ||||||
| 	}, 1000); | 	}, 1000); | ||||||
| } | } | ||||||
| @@ -140,7 +140,7 @@ module.exports = (params, user: ILocalUser, app: IApp) => new Promise(async (res | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー | 	// テキストが無いかつ添付ファイルが無いかつRenoteも無いかつ投票も無かったらエラー | ||||||
| 	if (text === undefined && files === null && renote === null && poll === undefined) { | 	if ((text === undefined || text === null) && files === null && renote === null && poll === undefined) { | ||||||
| 		return rej('text, mediaIds, renoteId or poll is required'); | 		return rej('text, mediaIds, renoteId or poll is required'); | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										35
									
								
								src/server/api/stream/notes-stats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/server/api/stream/notes-stats.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | import * as websocket from 'websocket'; | ||||||
|  | import Xev from 'xev'; | ||||||
|  |  | ||||||
|  | const ev = new Xev(); | ||||||
|  |  | ||||||
|  | export default function(request: websocket.request, connection: websocket.connection): void { | ||||||
|  | 	const onStats = stats => { | ||||||
|  | 		connection.send(JSON.stringify({ | ||||||
|  | 			type: 'stats', | ||||||
|  | 			body: stats | ||||||
|  | 		})); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	connection.on('message', async data => { | ||||||
|  | 		const msg = JSON.parse(data.utf8Data); | ||||||
|  |  | ||||||
|  | 		switch (msg.type) { | ||||||
|  | 			case 'requestLog': | ||||||
|  | 				ev.once('notesStatsLog:' + msg.id, statsLog => { | ||||||
|  | 					connection.send(JSON.stringify({ | ||||||
|  | 						type: 'statsLog', | ||||||
|  | 						body: statsLog | ||||||
|  | 					})); | ||||||
|  | 				}); | ||||||
|  | 				ev.emit('requestNotesStatsLog', msg.id); | ||||||
|  | 				break; | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	ev.addListener('notesStats', onStats); | ||||||
|  |  | ||||||
|  | 	connection.on('close', () => { | ||||||
|  | 		ev.removeListener('notesStats', onStats); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
							
								
								
									
										35
									
								
								src/server/api/stream/server-stats.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/server/api/stream/server-stats.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,35 @@ | |||||||
|  | import * as websocket from 'websocket'; | ||||||
|  | import Xev from 'xev'; | ||||||
|  |  | ||||||
|  | const ev = new Xev(); | ||||||
|  |  | ||||||
|  | export default function(request: websocket.request, connection: websocket.connection): void { | ||||||
|  | 	const onStats = stats => { | ||||||
|  | 		connection.send(JSON.stringify({ | ||||||
|  | 			type: 'stats', | ||||||
|  | 			body: stats | ||||||
|  | 		})); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	connection.on('message', async data => { | ||||||
|  | 		const msg = JSON.parse(data.utf8Data); | ||||||
|  |  | ||||||
|  | 		switch (msg.type) { | ||||||
|  | 			case 'requestLog': | ||||||
|  | 				ev.once('serverStatsLog:' + msg.id, statsLog => { | ||||||
|  | 					connection.send(JSON.stringify({ | ||||||
|  | 						type: 'statsLog', | ||||||
|  | 						body: statsLog | ||||||
|  | 					})); | ||||||
|  | 				}); | ||||||
|  | 				ev.emit('requestServerStatsLog', msg.id); | ||||||
|  | 				break; | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  |  | ||||||
|  | 	ev.addListener('serverStats', onStats); | ||||||
|  |  | ||||||
|  | 	connection.on('close', () => { | ||||||
|  | 		ev.removeListener('serverStats', onStats); | ||||||
|  | 	}); | ||||||
|  | } | ||||||
| @@ -1,19 +0,0 @@ | |||||||
| import * as websocket from 'websocket'; |  | ||||||
| import Xev from 'xev'; |  | ||||||
|  |  | ||||||
| const ev = new Xev(); |  | ||||||
|  |  | ||||||
| export default function(request: websocket.request, connection: websocket.connection): void { |  | ||||||
| 	const onStats = stats => { |  | ||||||
| 		connection.send(JSON.stringify({ |  | ||||||
| 			type: 'stats', |  | ||||||
| 			body: stats |  | ||||||
| 		})); |  | ||||||
| 	}; |  | ||||||
|  |  | ||||||
| 	ev.addListener('stats', onStats); |  | ||||||
|  |  | ||||||
| 	connection.on('close', () => { |  | ||||||
| 		ev.removeListener('stats', onStats); |  | ||||||
| 	}); |  | ||||||
| } |  | ||||||
| @@ -12,7 +12,8 @@ import messagingStream from './stream/messaging'; | |||||||
| import messagingIndexStream from './stream/messaging-index'; | import messagingIndexStream from './stream/messaging-index'; | ||||||
| import othelloGameStream from './stream/othello-game'; | import othelloGameStream from './stream/othello-game'; | ||||||
| import othelloStream from './stream/othello'; | import othelloStream from './stream/othello'; | ||||||
| import serverStream from './stream/server'; | import serverStatsStream from './stream/server-stats'; | ||||||
|  | import notesStatsStream from './stream/notes-stats'; | ||||||
| import requestsStream from './stream/requests'; | import requestsStream from './stream/requests'; | ||||||
| import { ParsedUrlQuery } from 'querystring'; | import { ParsedUrlQuery } from 'querystring'; | ||||||
| import authenticate from './authenticate'; | import authenticate from './authenticate'; | ||||||
| @@ -28,8 +29,13 @@ module.exports = (server: http.Server) => { | |||||||
| 	ws.on('request', async (request) => { | 	ws.on('request', async (request) => { | ||||||
| 		const connection = request.accept(); | 		const connection = request.accept(); | ||||||
|  |  | ||||||
| 		if (request.resourceURL.pathname === '/server') { | 		if (request.resourceURL.pathname === '/server-stats') { | ||||||
| 			serverStream(request, connection); | 			serverStatsStream(request, connection); | ||||||
|  | 			return; | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if (request.resourceURL.pathname === '/notes-stats') { | ||||||
|  | 			notesStatsStream(request, connection); | ||||||
| 			return; | 			return; | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -164,8 +164,8 @@ export default async function( | |||||||
| 			'metadata.deletedAt': { $exists: false } | 			'metadata.deletedAt': { $exists: false } | ||||||
| 		}); | 		}); | ||||||
|  |  | ||||||
| 		if (much !== null) { | 		if (much) { | ||||||
| 			log('file with same hash is found'); | 			log(`file with same hash is found: ${much._id}`); | ||||||
| 			return much; | 			return much; | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user