Merge branch 'develop' into l10n_develop
This commit is contained in:
		
						commit
						4e11da98d9
					
				
					 171 changed files with 2193 additions and 14550 deletions
				
			
		| 
						 | 
					@ -138,3 +138,6 @@ drive:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Clustering
 | 
					# Clustering
 | 
				
			||||||
# clusterLimit: 1
 | 
					# clusterLimit: 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Summaly proxy
 | 
				
			||||||
 | 
					# summalyProxy: "http://example.com"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										10
									
								
								CHANGELOG.md
									
										
									
									
									
								
							
							
						
						
									
										10
									
								
								CHANGELOG.md
									
										
									
									
									
								
							| 
						 | 
					@ -5,6 +5,16 @@ ChangeLog
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This document describes breaking changes only.
 | 
					This document describes breaking changes only.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					8.0.0
 | 
				
			||||||
 | 
					-----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Migration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					起動する前に、`node cli/migration/8.0.0`してください。
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Please run `node cli/migration/8.0.0` before launch.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
7.0.0
 | 
					7.0.0
 | 
				
			||||||
-----
 | 
					-----
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,40 +0,0 @@
 | 
				
			||||||
const { default: User, deleteUser } = require('../built/models/user');
 | 
					 | 
				
			||||||
const { default: zip } = require('@prezzemolo/zip')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const migrate = async (user) => {
 | 
					 | 
				
			||||||
	try {
 | 
					 | 
				
			||||||
		await deleteUser(user._id);
 | 
					 | 
				
			||||||
		return true;
 | 
					 | 
				
			||||||
	} catch (e) {
 | 
					 | 
				
			||||||
		return false;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
async function main() {
 | 
					 | 
				
			||||||
	const count = await User.count({
 | 
					 | 
				
			||||||
		uri: /#/
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const dop = 1
 | 
					 | 
				
			||||||
	const idop = ((count - (count % dop)) / dop) + 1
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	return zip(
 | 
					 | 
				
			||||||
		1,
 | 
					 | 
				
			||||||
		async (time) => {
 | 
					 | 
				
			||||||
			console.log(`${time} / ${idop}`)
 | 
					 | 
				
			||||||
			const doc = await User.find({
 | 
					 | 
				
			||||||
				uri: /#/
 | 
					 | 
				
			||||||
			}, {
 | 
					 | 
				
			||||||
				limit: dop, skip: time * dop
 | 
					 | 
				
			||||||
			})
 | 
					 | 
				
			||||||
			return Promise.all(doc.map(migrate))
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		idop
 | 
					 | 
				
			||||||
	).then(a => {
 | 
					 | 
				
			||||||
		const rv = []
 | 
					 | 
				
			||||||
		a.forEach(e => rv.push(...e))
 | 
					 | 
				
			||||||
		return rv
 | 
					 | 
				
			||||||
	})
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
main().then(console.dir).catch(console.error)
 | 
					 | 
				
			||||||
							
								
								
									
										144
									
								
								cli/migration/8.0.0.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										144
									
								
								cli/migration/8.0.0.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,144 @@
 | 
				
			||||||
 | 
					const { default: Stats } = require('../../built/models/stats');
 | 
				
			||||||
 | 
					const { default: User } = require('../../built/models/user');
 | 
				
			||||||
 | 
					const { default: Note } = require('../../built/models/note');
 | 
				
			||||||
 | 
					const { default: DriveFile } = require('../../built/models/drive-file');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const now = new Date();
 | 
				
			||||||
 | 
					const y = now.getFullYear();
 | 
				
			||||||
 | 
					const m = now.getMonth();
 | 
				
			||||||
 | 
					const d = now.getDate();
 | 
				
			||||||
 | 
					const h = now.getHours();
 | 
				
			||||||
 | 
					const date = new Date(y, m, d, h);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async function main() {
 | 
				
			||||||
 | 
						await Stats.update({}, {
 | 
				
			||||||
 | 
							$set: {
 | 
				
			||||||
 | 
								span: 'day'
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}, {
 | 
				
			||||||
 | 
							multi: true
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const localUsersCount = await User.count({
 | 
				
			||||||
 | 
							host: null
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const remoteUsersCount = await User.count({
 | 
				
			||||||
 | 
							host: { $ne: null }
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const localNotesCount = await Note.count({
 | 
				
			||||||
 | 
							'_user.host': null
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const remoteNotesCount = await Note.count({
 | 
				
			||||||
 | 
							'_user.host': { $ne: null }
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const localDriveFilesCount = await DriveFile.count({
 | 
				
			||||||
 | 
							'metadata._user.host': null
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const remoteDriveFilesCount = await DriveFile.count({
 | 
				
			||||||
 | 
							'metadata._user.host': { $ne: null }
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const localDriveFilesSize = await DriveFile
 | 
				
			||||||
 | 
							.aggregate([{
 | 
				
			||||||
 | 
								$match: {
 | 
				
			||||||
 | 
									'metadata._user.host': null,
 | 
				
			||||||
 | 
									'metadata.deletedAt': { $exists: false }
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								$project: {
 | 
				
			||||||
 | 
									length: true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								$group: {
 | 
				
			||||||
 | 
									_id: null,
 | 
				
			||||||
 | 
									usage: { $sum: '$length' }
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}])
 | 
				
			||||||
 | 
							.then(aggregates => {
 | 
				
			||||||
 | 
								if (aggregates.length > 0) {
 | 
				
			||||||
 | 
									return aggregates[0].usage;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return 0;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const remoteDriveFilesSize = await DriveFile
 | 
				
			||||||
 | 
							.aggregate([{
 | 
				
			||||||
 | 
								$match: {
 | 
				
			||||||
 | 
									'metadata._user.host': { $ne: null },
 | 
				
			||||||
 | 
									'metadata.deletedAt': { $exists: false }
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								$project: {
 | 
				
			||||||
 | 
									length: true
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}, {
 | 
				
			||||||
 | 
								$group: {
 | 
				
			||||||
 | 
									_id: null,
 | 
				
			||||||
 | 
									usage: { $sum: '$length' }
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}])
 | 
				
			||||||
 | 
							.then(aggregates => {
 | 
				
			||||||
 | 
								if (aggregates.length > 0) {
 | 
				
			||||||
 | 
									return aggregates[0].usage;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								return 0;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						await Stats.insert({
 | 
				
			||||||
 | 
							date: date,
 | 
				
			||||||
 | 
							span: 'hour',
 | 
				
			||||||
 | 
							users: {
 | 
				
			||||||
 | 
								local: {
 | 
				
			||||||
 | 
									total: localUsersCount,
 | 
				
			||||||
 | 
									diff: 0
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								remote: {
 | 
				
			||||||
 | 
									total: remoteUsersCount,
 | 
				
			||||||
 | 
									diff: 0
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							notes: {
 | 
				
			||||||
 | 
								local: {
 | 
				
			||||||
 | 
									total: localNotesCount,
 | 
				
			||||||
 | 
									diff: 0,
 | 
				
			||||||
 | 
									diffs: {
 | 
				
			||||||
 | 
										normal: 0,
 | 
				
			||||||
 | 
										reply: 0,
 | 
				
			||||||
 | 
										renote: 0
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								remote: {
 | 
				
			||||||
 | 
									total: remoteNotesCount,
 | 
				
			||||||
 | 
									diff: 0,
 | 
				
			||||||
 | 
									diffs: {
 | 
				
			||||||
 | 
										normal: 0,
 | 
				
			||||||
 | 
										reply: 0,
 | 
				
			||||||
 | 
										renote: 0
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							drive: {
 | 
				
			||||||
 | 
								local: {
 | 
				
			||||||
 | 
									totalCount: localDriveFilesCount,
 | 
				
			||||||
 | 
									totalSize: localDriveFilesSize,
 | 
				
			||||||
 | 
									diffCount: 0,
 | 
				
			||||||
 | 
									diffSize: 0
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								remote: {
 | 
				
			||||||
 | 
									totalCount: remoteDriveFilesCount,
 | 
				
			||||||
 | 
									totalSize: remoteDriveFilesSize,
 | 
				
			||||||
 | 
									diffCount: 0,
 | 
				
			||||||
 | 
									diffSize: 0
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						console.log('done');
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					main();
 | 
				
			||||||
							
								
								
									
										11
									
								
								gulpfile.ts
									
										
									
									
									
								
							
							
						
						
									
										11
									
								
								gulpfile.ts
									
										
									
									
									
								
							| 
						 | 
					@ -59,7 +59,16 @@ gulp.task('build:copy:views', () =>
 | 
				
			||||||
	gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
 | 
						gulp.src('./src/server/web/views/**/*').pipe(gulp.dest('./built/server/web/views'))
 | 
				
			||||||
);
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gulp.task('build:copy', ['build:copy:views'], () =>
 | 
					// 互換性のため
 | 
				
			||||||
 | 
					gulp.task('build:copy:lang', () =>
 | 
				
			||||||
 | 
						gulp.src(['./built/client/assets/*.*-*.js'])
 | 
				
			||||||
 | 
							.pipe(rename(path => {
 | 
				
			||||||
 | 
								path.basename = path.basename.replace(/\-(.*)$/, '');
 | 
				
			||||||
 | 
							}))
 | 
				
			||||||
 | 
							.pipe(gulp.dest('./built/client/assets/'))
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gulp.task('build:copy', ['build:copy:views', 'build:copy:lang'], () =>
 | 
				
			||||||
	gulp.src([
 | 
						gulp.src([
 | 
				
			||||||
		'./build/Release/crypto_key.node',
 | 
							'./build/Release/crypto_key.node',
 | 
				
			||||||
		'./src/const.json',
 | 
							'./src/const.json',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1219
									
								
								locales/ca.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/ca.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/de.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/de.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/en.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/en.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/es.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/es.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/fr.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/fr.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
					@ -11,13 +11,13 @@ const loadLang = lang => yaml.safeLoad(
 | 
				
			||||||
const native = loadLang('ja-JP');
 | 
					const native = loadLang('ja-JP');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const langs = {
 | 
					const langs = {
 | 
				
			||||||
	'de': loadLang('de'),
 | 
						'de-DE': loadLang('de-DE'),
 | 
				
			||||||
	'en': loadLang('en'),
 | 
						'en-US': loadLang('en-US'),
 | 
				
			||||||
	'fr': loadLang('fr'),
 | 
						'fr-FR': loadLang('fr-FR'),
 | 
				
			||||||
	'ja': native,
 | 
						'ja-JP': native,
 | 
				
			||||||
	'ja-KS': loadLang('ja-KS'),
 | 
						'ja-KS': loadLang('ja-KS'),
 | 
				
			||||||
	'pl': loadLang('pl'),
 | 
						'pl-PL': loadLang('pl-PL'),
 | 
				
			||||||
	'es': loadLang('es')
 | 
						'es-ES': loadLang('es-ES')
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Object.values(langs).forEach(locale => {
 | 
					Object.values(langs).forEach(locale => {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1219
									
								
								locales/it.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/it.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							| 
						 | 
					@ -456,6 +456,7 @@ desktop:
 | 
				
			||||||
  uploading-avatar: "新しいアバターをアップロードしています"
 | 
					  uploading-avatar: "新しいアバターをアップロードしています"
 | 
				
			||||||
  avatar-updated: "アバターを更新しました"
 | 
					  avatar-updated: "アバターを更新しました"
 | 
				
			||||||
  choose-avatar: "アバターにする画像を選択"
 | 
					  choose-avatar: "アバターにする画像を選択"
 | 
				
			||||||
 | 
					  invalid-filetype: "この形式のファイルはサポートされていません"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desktop/views/components/activity.chart.vue:
 | 
					desktop/views/components/activity.chart.vue:
 | 
				
			||||||
  total: "Black ... Total"
 | 
					  total: "Black ... Total"
 | 
				
			||||||
| 
						 | 
					@ -473,6 +474,25 @@ desktop/views/components/calendar.vue:
 | 
				
			||||||
  next: "次の月"
 | 
					  next: "次の月"
 | 
				
			||||||
  go: "クリックして時間遡行"
 | 
					  go: "クリックして時間遡行"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					desktop/views/components/charts.vue:
 | 
				
			||||||
 | 
					  title: "チャート"
 | 
				
			||||||
 | 
					  per-day: "1日ごと"
 | 
				
			||||||
 | 
					  per-hour: "1時間ごと"
 | 
				
			||||||
 | 
					  notes: "投稿"
 | 
				
			||||||
 | 
					  users: "ユーザー"
 | 
				
			||||||
 | 
					  drive: "ドライブ"
 | 
				
			||||||
 | 
					  charts:
 | 
				
			||||||
 | 
					    notes: "投稿の増減 (統合)"
 | 
				
			||||||
 | 
					    local-notes: "投稿の増減 (ローカル)"
 | 
				
			||||||
 | 
					    remote-notes: "投稿の増減 (リモート)"
 | 
				
			||||||
 | 
					    notes-total: "投稿の累計"
 | 
				
			||||||
 | 
					    users: "ユーザーの増減"
 | 
				
			||||||
 | 
					    users-total: "ユーザーの累計"
 | 
				
			||||||
 | 
					    drive: "ドライブ使用量の増減"
 | 
				
			||||||
 | 
					    drive-total: "ドライブ使用量の累計"
 | 
				
			||||||
 | 
					    drive-files: "ドライブのファイル数の増減"
 | 
				
			||||||
 | 
					    drive-files-total: "ドライブのファイル数の累計"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desktop/views/components/choose-file-from-drive-window.vue:
 | 
					desktop/views/components/choose-file-from-drive-window.vue:
 | 
				
			||||||
  choose-file: "ファイル選択中"
 | 
					  choose-file: "ファイル選択中"
 | 
				
			||||||
  upload: "PCからドライブにファイルをアップロード"
 | 
					  upload: "PCからドライブにファイルをアップロード"
 | 
				
			||||||
| 
						 | 
					@ -713,6 +733,7 @@ desktop/views/components/settings.vue:
 | 
				
			||||||
  gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
 | 
					  gradient-window-header: "ウィンドウのタイトルバーにグラデーションを使用"
 | 
				
			||||||
  post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
 | 
					  post-form-on-timeline: "タイムライン上部に投稿フォームを表示する"
 | 
				
			||||||
  suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
 | 
					  suggest-recent-hashtags: "最近のハッシュタグを投稿フォームに表示する"
 | 
				
			||||||
 | 
					  show-clock-on-header: "右上に時計を表示する"
 | 
				
			||||||
  show-reply-target: "リプライ先を表示する"
 | 
					  show-reply-target: "リプライ先を表示する"
 | 
				
			||||||
  show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
 | 
					  show-my-renotes: "自分の行ったRenoteをタイムラインに表示する"
 | 
				
			||||||
  show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する"
 | 
					  show-renoted-my-notes: "自分の投稿のRenoteをタイムラインに表示する"
 | 
				
			||||||
| 
						 | 
					@ -857,6 +878,7 @@ desktop/views/components/ui.header.account.vue:
 | 
				
			||||||
  lists: "リスト"
 | 
					  lists: "リスト"
 | 
				
			||||||
  follow-requests: "フォロー申請"
 | 
					  follow-requests: "フォロー申請"
 | 
				
			||||||
  customize: "ホームのカスタマイズ"
 | 
					  customize: "ホームのカスタマイズ"
 | 
				
			||||||
 | 
					  admin: "管理"
 | 
				
			||||||
  settings: "設定"
 | 
					  settings: "設定"
 | 
				
			||||||
  signout: "サインアウト"
 | 
					  signout: "サインアウト"
 | 
				
			||||||
  dark: "闇に飲まれる"
 | 
					  dark: "闇に飲まれる"
 | 
				
			||||||
| 
						 | 
					@ -914,8 +936,8 @@ desktop/views/pages/admin/admin.dashboard.vue:
 | 
				
			||||||
  dashboard: "ダッシュボード"
 | 
					  dashboard: "ダッシュボード"
 | 
				
			||||||
  all-users: "全てのユーザー"
 | 
					  all-users: "全てのユーザー"
 | 
				
			||||||
  original-users: "このインスタンスのユーザー"
 | 
					  original-users: "このインスタンスのユーザー"
 | 
				
			||||||
  all-notes: "全てのノート"
 | 
					  all-notes: "全ての投稿"
 | 
				
			||||||
  original-notes: "このインスタンスのノート"
 | 
					  original-notes: "このインスタンスの投稿"
 | 
				
			||||||
  invite: "招待"
 | 
					  invite: "招待"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desktop/views/pages/admin/admin.suspend-user.vue:
 | 
					desktop/views/pages/admin/admin.suspend-user.vue:
 | 
				
			||||||
| 
						 | 
					@ -938,21 +960,6 @@ desktop/views/pages/admin/admin.unverify-user.vue:
 | 
				
			||||||
  unverify: "公式アカウントを解除する"
 | 
					  unverify: "公式アカウントを解除する"
 | 
				
			||||||
  unverified: "公式アカウントを解除しました"
 | 
					  unverified: "公式アカウントを解除しました"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desktop/views/pages/admin/admin.notes-chart.vue:
 | 
					 | 
				
			||||||
  title: "投稿"
 | 
					 | 
				
			||||||
  local: "ローカル"
 | 
					 | 
				
			||||||
  remote: "リモート"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
desktop/views/pages/admin/admin.users-chart.vue:
 | 
					 | 
				
			||||||
  title: "ユーザー"
 | 
					 | 
				
			||||||
  local: "ローカル"
 | 
					 | 
				
			||||||
  remote: "リモート"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
desktop/views/pages/admin/admin.drive-chart.vue:
 | 
					 | 
				
			||||||
  title: "ドライブ"
 | 
					 | 
				
			||||||
  local: "ローカル"
 | 
					 | 
				
			||||||
  remote: "リモート"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
desktop/views/pages/deck/deck.tl-column.vue:
 | 
					desktop/views/pages/deck/deck.tl-column.vue:
 | 
				
			||||||
  is-media-only: "メディア投稿のみ"
 | 
					  is-media-only: "メディア投稿のみ"
 | 
				
			||||||
  is-media-view: "メディアビュー"
 | 
					  is-media-view: "メディアビュー"
 | 
				
			||||||
| 
						 | 
					@ -963,6 +970,12 @@ desktop/views/pages/deck/deck.note.vue:
 | 
				
			||||||
  private: "この投稿は非公開です"
 | 
					  private: "この投稿は非公開です"
 | 
				
			||||||
  deleted: "この投稿は削除されました"
 | 
					  deleted: "この投稿は削除されました"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					desktop/views/pages/stats/stats.vue:
 | 
				
			||||||
 | 
					  all-users: "全てのユーザー"
 | 
				
			||||||
 | 
					  original-users: "このインスタンスのユーザー"
 | 
				
			||||||
 | 
					  all-notes: "全ての投稿"
 | 
				
			||||||
 | 
					  original-notes: "このインスタンスの投稿"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desktop/views/pages/welcome.vue:
 | 
					desktop/views/pages/welcome.vue:
 | 
				
			||||||
  about: "詳しく..."
 | 
					  about: "詳しく..."
 | 
				
			||||||
  gotit: "わかった"
 | 
					  gotit: "わかった"
 | 
				
			||||||
| 
						 | 
					@ -1214,6 +1227,7 @@ mobile/views/components/ui.nav.vue:
 | 
				
			||||||
  game: "ゲーム"
 | 
					  game: "ゲーム"
 | 
				
			||||||
  darkmode: "ダークモード"
 | 
					  darkmode: "ダークモード"
 | 
				
			||||||
  settings: "設定"
 | 
					  settings: "設定"
 | 
				
			||||||
 | 
					  admin: "管理"
 | 
				
			||||||
  about: "Misskeyについて"
 | 
					  about: "Misskeyについて"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mobile/views/components/user-timeline.vue:
 | 
					mobile/views/components/user-timeline.vue:
 | 
				
			||||||
| 
						 | 
					@ -1355,6 +1369,8 @@ mobile/views/pages/settings.vue:
 | 
				
			||||||
  update-available-desc: "ページを再度読み込みすると更新が適用されます。"
 | 
					  update-available-desc: "ページを再度読み込みすると更新が適用されます。"
 | 
				
			||||||
  settings: "設定"
 | 
					  settings: "設定"
 | 
				
			||||||
  signout: "サインアウト"
 | 
					  signout: "サインアウト"
 | 
				
			||||||
 | 
					  sound: "サウンド"
 | 
				
			||||||
 | 
					  enableSounds: "サウンドを有効にする"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
mobile/views/pages/user.vue:
 | 
					mobile/views/pages/user.vue:
 | 
				
			||||||
  follows-you: "フォローされています"
 | 
					  follows-you: "フォローされています"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										1219
									
								
								locales/ko.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/ko.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/pl.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/pl.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/pt.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/pt.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/ru.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/ru.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										1219
									
								
								locales/zh.yml
									
										
									
									
									
								
							
							
						
						
									
										1219
									
								
								locales/zh.yml
									
										
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load diff
											
										
									
								
							
							
								
								
									
										27
									
								
								package.json
									
										
									
									
									
								
							
							
						
						
									
										27
									
								
								package.json
									
										
									
									
									
								
							| 
						 | 
					@ -1,8 +1,8 @@
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	"name": "misskey",
 | 
						"name": "misskey",
 | 
				
			||||||
	"author": "syuilo <i@syuilo.com>",
 | 
						"author": "syuilo <i@syuilo.com>",
 | 
				
			||||||
	"version": "7.3.0",
 | 
						"version": "8.15.0",
 | 
				
			||||||
	"clientVersion": "1.0.8741",
 | 
						"clientVersion": "1.0.9031",
 | 
				
			||||||
	"codename": "nighthike",
 | 
						"codename": "nighthike",
 | 
				
			||||||
	"main": "./built/index.js",
 | 
						"main": "./built/index.js",
 | 
				
			||||||
	"private": true,
 | 
						"private": true,
 | 
				
			||||||
| 
						 | 
					@ -32,7 +32,7 @@
 | 
				
			||||||
		"@types/debug": "0.0.30",
 | 
							"@types/debug": "0.0.30",
 | 
				
			||||||
		"@types/deep-equal": "1.0.1",
 | 
							"@types/deep-equal": "1.0.1",
 | 
				
			||||||
		"@types/double-ended-queue": "2.1.0",
 | 
							"@types/double-ended-queue": "2.1.0",
 | 
				
			||||||
		"@types/elasticsearch": "5.0.25",
 | 
							"@types/elasticsearch": "5.0.26",
 | 
				
			||||||
		"@types/file-type": "5.2.1",
 | 
							"@types/file-type": "5.2.1",
 | 
				
			||||||
		"@types/gulp": "3.8.36",
 | 
							"@types/gulp": "3.8.36",
 | 
				
			||||||
		"@types/gulp-htmlmin": "1.3.32",
 | 
							"@types/gulp-htmlmin": "1.3.32",
 | 
				
			||||||
| 
						 | 
					@ -60,7 +60,7 @@
 | 
				
			||||||
		"@types/mocha": "5.2.3",
 | 
							"@types/mocha": "5.2.3",
 | 
				
			||||||
		"@types/mongodb": "3.1.4",
 | 
							"@types/mongodb": "3.1.4",
 | 
				
			||||||
		"@types/ms": "0.7.30",
 | 
							"@types/ms": "0.7.30",
 | 
				
			||||||
		"@types/node": "10.7.1",
 | 
							"@types/node": "10.9.3",
 | 
				
			||||||
		"@types/portscanner": "2.1.0",
 | 
							"@types/portscanner": "2.1.0",
 | 
				
			||||||
		"@types/pug": "2.0.4",
 | 
							"@types/pug": "2.0.4",
 | 
				
			||||||
		"@types/qrcode": "1.2.0",
 | 
							"@types/qrcode": "1.2.0",
 | 
				
			||||||
| 
						 | 
					@ -70,14 +70,14 @@
 | 
				
			||||||
		"@types/request-promise-native": "1.0.15",
 | 
							"@types/request-promise-native": "1.0.15",
 | 
				
			||||||
		"@types/rimraf": "2.0.2",
 | 
							"@types/rimraf": "2.0.2",
 | 
				
			||||||
		"@types/seedrandom": "2.4.27",
 | 
							"@types/seedrandom": "2.4.27",
 | 
				
			||||||
		"@types/sharp": "0.17.9",
 | 
							"@types/sharp": "0.17.10",
 | 
				
			||||||
		"@types/showdown": "1.7.5",
 | 
							"@types/showdown": "1.7.5",
 | 
				
			||||||
		"@types/single-line-log": "1.1.0",
 | 
							"@types/single-line-log": "1.1.0",
 | 
				
			||||||
		"@types/speakeasy": "2.0.2",
 | 
							"@types/speakeasy": "2.0.2",
 | 
				
			||||||
		"@types/systeminformation": "3.23.0",
 | 
							"@types/systeminformation": "3.23.0",
 | 
				
			||||||
		"@types/tmp": "0.0.33",
 | 
							"@types/tmp": "0.0.33",
 | 
				
			||||||
		"@types/uuid": "3.4.3",
 | 
							"@types/uuid": "3.4.3",
 | 
				
			||||||
		"@types/webpack": "4.4.10",
 | 
							"@types/webpack": "4.4.11",
 | 
				
			||||||
		"@types/webpack-stream": "3.2.10",
 | 
							"@types/webpack-stream": "3.2.10",
 | 
				
			||||||
		"@types/websocket": "0.0.39",
 | 
							"@types/websocket": "0.0.39",
 | 
				
			||||||
		"@types/ws": "6.0.0",
 | 
							"@types/ws": "6.0.0",
 | 
				
			||||||
| 
						 | 
					@ -89,6 +89,7 @@
 | 
				
			||||||
		"bootstrap-vue": "2.0.0-rc.11",
 | 
							"bootstrap-vue": "2.0.0-rc.11",
 | 
				
			||||||
		"cafy": "11.3.0",
 | 
							"cafy": "11.3.0",
 | 
				
			||||||
		"chalk": "2.4.1",
 | 
							"chalk": "2.4.1",
 | 
				
			||||||
 | 
							"chart.js": "2.7.2",
 | 
				
			||||||
		"commander": "2.17.1",
 | 
							"commander": "2.17.1",
 | 
				
			||||||
		"crc-32": "1.2.0",
 | 
							"crc-32": "1.2.0",
 | 
				
			||||||
		"css-loader": "1.0.0",
 | 
							"css-loader": "1.0.0",
 | 
				
			||||||
| 
						 | 
					@ -149,6 +150,7 @@
 | 
				
			||||||
		"loader-utils": "1.1.0",
 | 
							"loader-utils": "1.1.0",
 | 
				
			||||||
		"lodash.assign": "4.2.0",
 | 
							"lodash.assign": "4.2.0",
 | 
				
			||||||
		"mecab-async": "0.1.2",
 | 
							"mecab-async": "0.1.2",
 | 
				
			||||||
 | 
							"merge-options": "1.0.1",
 | 
				
			||||||
		"minio": "7.0.0",
 | 
							"minio": "7.0.0",
 | 
				
			||||||
		"mkdirp": "0.5.1",
 | 
							"mkdirp": "0.5.1",
 | 
				
			||||||
		"mocha": "5.2.0",
 | 
							"mocha": "5.2.0",
 | 
				
			||||||
| 
						 | 
					@ -156,7 +158,7 @@
 | 
				
			||||||
		"mongodb": "3.1.1",
 | 
							"mongodb": "3.1.1",
 | 
				
			||||||
		"monk": "6.0.6",
 | 
							"monk": "6.0.6",
 | 
				
			||||||
		"ms": "2.1.1",
 | 
							"ms": "2.1.1",
 | 
				
			||||||
		"nan": "2.10.0",
 | 
							"nan": "2.11.0",
 | 
				
			||||||
		"nested-property": "0.0.7",
 | 
							"nested-property": "0.0.7",
 | 
				
			||||||
		"node-sass": "4.9.3",
 | 
							"node-sass": "4.9.3",
 | 
				
			||||||
		"node-sass-json-importer": "3.3.1",
 | 
							"node-sass-json-importer": "3.3.1",
 | 
				
			||||||
| 
						 | 
					@ -188,11 +190,11 @@
 | 
				
			||||||
		"single-line-log": "1.1.2",
 | 
							"single-line-log": "1.1.2",
 | 
				
			||||||
		"speakeasy": "2.0.0",
 | 
							"speakeasy": "2.0.0",
 | 
				
			||||||
		"stringz": "1.0.0",
 | 
							"stringz": "1.0.0",
 | 
				
			||||||
		"style-loader": "0.22.1",
 | 
							"style-loader": "0.23.0",
 | 
				
			||||||
		"stylus": "0.54.5",
 | 
							"stylus": "0.54.5",
 | 
				
			||||||
		"stylus-loader": "3.0.2",
 | 
							"stylus-loader": "3.0.2",
 | 
				
			||||||
		"summaly": "2.1.4",
 | 
							"summaly": "2.1.4",
 | 
				
			||||||
		"systeminformation": "3.42.9",
 | 
							"systeminformation": "3.44.2",
 | 
				
			||||||
		"syuilo-password-strength": "0.0.1",
 | 
							"syuilo-password-strength": "0.0.1",
 | 
				
			||||||
		"textarea-caret": "3.1.0",
 | 
							"textarea-caret": "3.1.0",
 | 
				
			||||||
		"tmp": "0.0.33",
 | 
							"tmp": "0.0.33",
 | 
				
			||||||
| 
						 | 
					@ -206,10 +208,11 @@
 | 
				
			||||||
		"uuid": "3.3.2",
 | 
							"uuid": "3.3.2",
 | 
				
			||||||
		"v-animate-css": "0.0.2",
 | 
							"v-animate-css": "0.0.2",
 | 
				
			||||||
		"vue": "2.5.17",
 | 
							"vue": "2.5.17",
 | 
				
			||||||
 | 
							"vue-chartjs": "3.4.0",
 | 
				
			||||||
		"vue-cropperjs": "2.2.1",
 | 
							"vue-cropperjs": "2.2.1",
 | 
				
			||||||
		"vue-js-modal": "1.3.17",
 | 
							"vue-js-modal": "1.3.23",
 | 
				
			||||||
		"vue-json-tree-view": "2.1.4",
 | 
							"vue-json-tree-view": "2.1.4",
 | 
				
			||||||
		"vue-loader": "15.4.0",
 | 
							"vue-loader": "15.4.1",
 | 
				
			||||||
		"vue-router": "3.0.1",
 | 
							"vue-router": "3.0.1",
 | 
				
			||||||
		"vue-style-loader": "4.1.2",
 | 
							"vue-style-loader": "4.1.2",
 | 
				
			||||||
		"vue-template-compiler": "2.5.17",
 | 
							"vue-template-compiler": "2.5.17",
 | 
				
			||||||
| 
						 | 
					@ -218,7 +221,7 @@
 | 
				
			||||||
		"vuex-persistedstate": "2.5.4",
 | 
							"vuex-persistedstate": "2.5.4",
 | 
				
			||||||
		"web-push": "3.3.2",
 | 
							"web-push": "3.3.2",
 | 
				
			||||||
		"webfinger.js": "2.6.6",
 | 
							"webfinger.js": "2.6.6",
 | 
				
			||||||
		"webpack": "4.17.0",
 | 
							"webpack": "4.17.1",
 | 
				
			||||||
		"webpack-cli": "3.1.0",
 | 
							"webpack-cli": "3.1.0",
 | 
				
			||||||
		"websocket": "1.0.26",
 | 
							"websocket": "1.0.26",
 | 
				
			||||||
		"ws": "6.0.0",
 | 
							"ws": "6.0.0",
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -38,15 +38,22 @@
 | 
				
			||||||
	//#endregion
 | 
						//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//#region Detect the user language
 | 
						//#region Detect the user language
 | 
				
			||||||
	let lang = navigator.language;
 | 
						let lang = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (!LANGS.includes(lang)) lang = lang.split('-')[0];
 | 
						if (LANGS.includes(navigator.language)) {
 | 
				
			||||||
 | 
							lang = navigator.language;
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							lang = LANGS.find(x => x.split('-')[0] == navigator.language);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// The default language is English
 | 
							if (lang == null) {
 | 
				
			||||||
	if (!LANGS.includes(lang)) lang = 'en';
 | 
								// Fallback
 | 
				
			||||||
 | 
								lang = 'en-US';
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (settings) {
 | 
						if (settings && settings.device.lang &&
 | 
				
			||||||
		if (settings.device.lang) lang = settings.device.lang;
 | 
							LANGS.includes(settings.device.lang)) {
 | 
				
			||||||
 | 
							lang = settings.device.lang;
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	//#endregion
 | 
						//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,8 +26,8 @@ export default Vue.extend({
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	created() {
 | 
						created() {
 | 
				
			||||||
		(this as any).os.getMeta().then(meta => {
 | 
							(this as any).os.getMeta().then(meta => {
 | 
				
			||||||
			if (meta.repositoryUrl) this.repositoryUrl = meta.repositoryUrl;
 | 
								if (meta.maintainer.repository_url) this.repositoryUrl = meta.maintainer.repository_url;
 | 
				
			||||||
			if (meta.feedbackUrl) this.feedbackUrl = meta.feedbackUrl;
 | 
								if (meta.maintainer.feedback_url) this.feedbackUrl = meta.maintainer.feedback_url;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -28,18 +28,99 @@
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
import { url as misskeyUrl } from '../../../config';
 | 
					import { url as misskeyUrl } from '../../../config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// THIS IS THE WHITELIST FOR THE EMBED PLAYER
 | 
				
			||||||
 | 
					const whiteList = [
 | 
				
			||||||
 | 
						'afreecatv.com',
 | 
				
			||||||
 | 
						'aparat.com',
 | 
				
			||||||
 | 
						'applemusic.com',
 | 
				
			||||||
 | 
						'amazon.com',
 | 
				
			||||||
 | 
						'awa.fm',
 | 
				
			||||||
 | 
						'bandcamp.com',
 | 
				
			||||||
 | 
						'bbc.co.uk',
 | 
				
			||||||
 | 
						'beatport.com',
 | 
				
			||||||
 | 
						'bilibili.com',
 | 
				
			||||||
 | 
						'boomstream.com',
 | 
				
			||||||
 | 
						'breakers.tv',
 | 
				
			||||||
 | 
						'cam4.com',
 | 
				
			||||||
 | 
						'cavelis.net',
 | 
				
			||||||
 | 
						'chaturbate.com',
 | 
				
			||||||
 | 
						'cnn.com',
 | 
				
			||||||
 | 
						'cybergame.tv',
 | 
				
			||||||
 | 
						'dailymotion.com',
 | 
				
			||||||
 | 
						'deezer.com',
 | 
				
			||||||
 | 
						'djlive.pl',
 | 
				
			||||||
 | 
						'e-onkyo.com',
 | 
				
			||||||
 | 
						'eventials.com',
 | 
				
			||||||
 | 
						'facebook.com',
 | 
				
			||||||
 | 
						'fc2.com',
 | 
				
			||||||
 | 
						'gameplank.tv',
 | 
				
			||||||
 | 
						'goodgame.ru',
 | 
				
			||||||
 | 
						'google.com',
 | 
				
			||||||
 | 
						'hardtunes.com',
 | 
				
			||||||
 | 
						'instagram.com',
 | 
				
			||||||
 | 
						'johnnylooch.com',
 | 
				
			||||||
 | 
						'kexp.org',
 | 
				
			||||||
 | 
						'lahzenegar.com',
 | 
				
			||||||
 | 
						'liveedu.tv',
 | 
				
			||||||
 | 
						'livetube.cc',
 | 
				
			||||||
 | 
						'livestream.com',
 | 
				
			||||||
 | 
						'meridix.com',
 | 
				
			||||||
 | 
						'mixcloud.com',
 | 
				
			||||||
 | 
						'mixer.com',
 | 
				
			||||||
 | 
						'mobcrush.com',
 | 
				
			||||||
 | 
						'mylive.in.th',
 | 
				
			||||||
 | 
						'myspace.com',
 | 
				
			||||||
 | 
						'netflix.com',
 | 
				
			||||||
 | 
						'newretrowave.com',
 | 
				
			||||||
 | 
						'nhk.or.jp',
 | 
				
			||||||
 | 
						'nicovideo.jp',
 | 
				
			||||||
 | 
						'nico.ms',
 | 
				
			||||||
 | 
						'noisetrade.com',
 | 
				
			||||||
 | 
						'nood.tv',
 | 
				
			||||||
 | 
						'npr.org',
 | 
				
			||||||
 | 
						'openrec.tv',
 | 
				
			||||||
 | 
						'pandora.com',
 | 
				
			||||||
 | 
						'pandora.tv',
 | 
				
			||||||
 | 
						'picarto.tv',
 | 
				
			||||||
 | 
						'pscp.tv',
 | 
				
			||||||
 | 
						'restream.io',
 | 
				
			||||||
 | 
						'reverbnation.com',
 | 
				
			||||||
 | 
						'sermonaudio.com',
 | 
				
			||||||
 | 
						'smashcast.tv',
 | 
				
			||||||
 | 
						'songkick.com',
 | 
				
			||||||
 | 
						'soundcloud.com',
 | 
				
			||||||
 | 
						'spinninrecords.com',
 | 
				
			||||||
 | 
						'spotify.com',
 | 
				
			||||||
 | 
						'stitcher.com',
 | 
				
			||||||
 | 
						'stream.me',
 | 
				
			||||||
 | 
						'switchboard.live',
 | 
				
			||||||
 | 
						'tunein.com',
 | 
				
			||||||
 | 
						'twitcasting.tv',
 | 
				
			||||||
 | 
						'twitch.tv',
 | 
				
			||||||
 | 
						'twitter.com',
 | 
				
			||||||
 | 
						'vaughnlive.tv',
 | 
				
			||||||
 | 
						'veoh.com',
 | 
				
			||||||
 | 
						'vimeo.com',
 | 
				
			||||||
 | 
						'watchpeoplecode.com',
 | 
				
			||||||
 | 
						'web.tv',
 | 
				
			||||||
 | 
						'youtube.com',
 | 
				
			||||||
 | 
						'youtu.be'
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Vue.extend({
 | 
					export default Vue.extend({
 | 
				
			||||||
	props: {
 | 
						props: {
 | 
				
			||||||
		url: {
 | 
							url: {
 | 
				
			||||||
			type: String,
 | 
								type: String,
 | 
				
			||||||
			require: true
 | 
								require: true
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		detail: {
 | 
							detail: {
 | 
				
			||||||
			type: Boolean,
 | 
								type: Boolean,
 | 
				
			||||||
			required: false,
 | 
								required: false,
 | 
				
			||||||
			default: false
 | 
								default: false
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	data() {
 | 
						data() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			fetching: true,
 | 
								fetching: true,
 | 
				
			||||||
| 
						 | 
					@ -57,6 +138,7 @@ export default Vue.extend({
 | 
				
			||||||
			misskeyUrl
 | 
								misskeyUrl
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	created() {
 | 
						created() {
 | 
				
			||||||
		const url = new URL(this.url);
 | 
							const url = new URL(this.url);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -81,102 +163,27 @@ export default Vue.extend({
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
			return;
 | 
								return;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
 | 
							fetch('/url?url=' + encodeURIComponent(this.url)).then(res => {
 | 
				
			||||||
			res.json().then(info => {
 | 
								res.json().then(info => {
 | 
				
			||||||
				if (info.url != null) {
 | 
									if (info.url == null) return;
 | 
				
			||||||
					this.title = info.title;
 | 
									this.title = info.title;
 | 
				
			||||||
					this.description = info.description;
 | 
									this.description = info.description;
 | 
				
			||||||
					this.thumbnail = info.thumbnail;
 | 
									this.thumbnail = info.thumbnail;
 | 
				
			||||||
					this.icon = info.icon;
 | 
									this.icon = info.icon;
 | 
				
			||||||
					this.sitename = info.sitename;
 | 
									this.sitename = info.sitename;
 | 
				
			||||||
					this.fetching = false;
 | 
									this.fetching = false;
 | 
				
			||||||
					if ([ // THIS IS THE WHITELIST FOR THE EMBED PLAYER
 | 
									if (whiteList.some(x => x == url.hostname || url.hostname.endsWith(`.${x}`))) {
 | 
				
			||||||
						'afreecatv.com',
 | 
										this.player = info.player;
 | 
				
			||||||
						'aparat.com',
 | 
									}
 | 
				
			||||||
						'applemusic.com',
 | 
								})
 | 
				
			||||||
						'amazon.com',
 | 
							});
 | 
				
			||||||
						'awa.fm',
 | 
						}
 | 
				
			||||||
						'bandcamp.com',
 | 
					 | 
				
			||||||
						'bbc.co.uk',
 | 
					 | 
				
			||||||
						'beatport.com',
 | 
					 | 
				
			||||||
						'bilibili.com',
 | 
					 | 
				
			||||||
						'boomstream.com',
 | 
					 | 
				
			||||||
						'breakers.tv',
 | 
					 | 
				
			||||||
						'cam4.com',
 | 
					 | 
				
			||||||
						'cavelis.net',
 | 
					 | 
				
			||||||
						'chaturbate.com',
 | 
					 | 
				
			||||||
						'cnn.com',
 | 
					 | 
				
			||||||
						'cybergame.tv',
 | 
					 | 
				
			||||||
						'dailymotion.com',
 | 
					 | 
				
			||||||
						'deezer.com',
 | 
					 | 
				
			||||||
						'djlive.pl',
 | 
					 | 
				
			||||||
						'e-onkyo.com',
 | 
					 | 
				
			||||||
						'eventials.com',
 | 
					 | 
				
			||||||
						'facebook.com',
 | 
					 | 
				
			||||||
						'fc2.com',
 | 
					 | 
				
			||||||
						'gameplank.tv',
 | 
					 | 
				
			||||||
						'goodgame.ru',
 | 
					 | 
				
			||||||
						'google.com',
 | 
					 | 
				
			||||||
						'hardtunes.com',
 | 
					 | 
				
			||||||
						'instagram.com',
 | 
					 | 
				
			||||||
						'johnnylooch.com',
 | 
					 | 
				
			||||||
						'kexp.org',
 | 
					 | 
				
			||||||
						'lahzenegar.com',
 | 
					 | 
				
			||||||
						'liveedu.tv',
 | 
					 | 
				
			||||||
						'livetube.cc',
 | 
					 | 
				
			||||||
						'livestream.com',
 | 
					 | 
				
			||||||
						'meridix.com',
 | 
					 | 
				
			||||||
						'mixcloud.com',
 | 
					 | 
				
			||||||
						'mixer.com',
 | 
					 | 
				
			||||||
						'mobcrush.com',
 | 
					 | 
				
			||||||
						'mylive.in.th',
 | 
					 | 
				
			||||||
						'myspace.com',
 | 
					 | 
				
			||||||
						'netflix.com',
 | 
					 | 
				
			||||||
						'newretrowave.com',
 | 
					 | 
				
			||||||
						'nhk.or.jp',
 | 
					 | 
				
			||||||
						'nicovideo.jp',
 | 
					 | 
				
			||||||
						'nico.ms',
 | 
					 | 
				
			||||||
						'noisetrade.com',
 | 
					 | 
				
			||||||
						'nood.tv',
 | 
					 | 
				
			||||||
						'npr.org',
 | 
					 | 
				
			||||||
						'openrec.tv',
 | 
					 | 
				
			||||||
						'pandora.com',
 | 
					 | 
				
			||||||
						'pandora.tv',
 | 
					 | 
				
			||||||
						'picarto.tv',
 | 
					 | 
				
			||||||
						'pscp.tv',
 | 
					 | 
				
			||||||
						'restream.io',
 | 
					 | 
				
			||||||
						'reverbnation.com',
 | 
					 | 
				
			||||||
						'sermonaudio.com',
 | 
					 | 
				
			||||||
						'smashcast.tv',
 | 
					 | 
				
			||||||
						'songkick.com',
 | 
					 | 
				
			||||||
						'soundcloud.com',
 | 
					 | 
				
			||||||
						'spinninrecords.com',
 | 
					 | 
				
			||||||
						'spotify.com',
 | 
					 | 
				
			||||||
						'stitcher.com',
 | 
					 | 
				
			||||||
						'stream.me',
 | 
					 | 
				
			||||||
						'switchboard.live',
 | 
					 | 
				
			||||||
						'tunein.com',
 | 
					 | 
				
			||||||
						'twitcasting.tv',
 | 
					 | 
				
			||||||
						'twitch.tv',
 | 
					 | 
				
			||||||
						'twitter.com',
 | 
					 | 
				
			||||||
						'vaughnlive.tv',
 | 
					 | 
				
			||||||
						'veoh.com',
 | 
					 | 
				
			||||||
						'vimeo.com',
 | 
					 | 
				
			||||||
						'watchpeoplecode.com',
 | 
					 | 
				
			||||||
						'web.tv',
 | 
					 | 
				
			||||||
						'youtube.com',
 | 
					 | 
				
			||||||
						'youtu.be'
 | 
					 | 
				
			||||||
					].some(x => x == url.hostname || url.hostname.endsWith(`.${x}`)))
 | 
					 | 
				
			||||||
						this.player = info.player;
 | 
					 | 
				
			||||||
				}	// info.url
 | 
					 | 
				
			||||||
			})	// json
 | 
					 | 
				
			||||||
		});	// fetch
 | 
					 | 
				
			||||||
	}	// created
 | 
					 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="stylus" scoped>
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
.twitter
 | 
					.player
 | 
				
			||||||
	position relative
 | 
						position relative
 | 
				
			||||||
	width 100%
 | 
						width 100%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,10 @@
 | 
				
			||||||
import Vue from 'vue';
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Vue.filter('bytes', (v, digits = 0) => {
 | 
					Vue.filter('bytes', (v, digits = 0) => {
 | 
				
			||||||
	const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
 | 
						const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
 | 
				
			||||||
	if (v == 0) return '0Byte';
 | 
						if (v == 0) return '0';
 | 
				
			||||||
 | 
						const isMinus = v < 0;
 | 
				
			||||||
 | 
						if (isMinus) v = -v;
 | 
				
			||||||
	const i = Math.floor(Math.log(v) / Math.log(1024));
 | 
						const i = Math.floor(Math.log(v) / Math.log(1024));
 | 
				
			||||||
	return (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
 | 
						return (isMinus ? '-' : '') + (v / Math.pow(1024, i)).toFixed(digits).replace(/\.0+$/, '') + sizes[i];
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,9 +2,9 @@
 | 
				
			||||||
<div class="mkw-donation" :data-mobile="platform == 'mobile'">
 | 
					<div class="mkw-donation" :data-mobile="platform == 'mobile'">
 | 
				
			||||||
	<article>
 | 
						<article>
 | 
				
			||||||
		<h1>%fa:heart%%i18n:@title%</h1>
 | 
							<h1>%fa:heart%%i18n:@title%</h1>
 | 
				
			||||||
		<p>
 | 
							<p v-if="meta">
 | 
				
			||||||
			{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
 | 
								{{ '%i18n:@text%'.substr(0, '%i18n:@text%'.indexOf('{')) }}
 | 
				
			||||||
			<a href="https://syuilo.com">@syuilo</a>
 | 
								<a :href="meta.maintainer.url">{{ meta.maintainer.name }}</a>
 | 
				
			||||||
			{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
 | 
								{{ '%i18n:@text%'.substr('%i18n:@text%'.indexOf('}') + 1) }}
 | 
				
			||||||
		</p>
 | 
							</p>
 | 
				
			||||||
	</article>
 | 
						</article>
 | 
				
			||||||
| 
						 | 
					@ -15,6 +15,17 @@
 | 
				
			||||||
import define from '../../../common/define-widget';
 | 
					import define from '../../../common/define-widget';
 | 
				
			||||||
export default define({
 | 
					export default define({
 | 
				
			||||||
	name: 'donation'
 | 
						name: 'donation'
 | 
				
			||||||
 | 
					}).extend({
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								meta: null
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							(this as any).os.getMeta().then(meta => {
 | 
				
			||||||
 | 
								this.meta = meta;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -3,8 +3,21 @@ import { apiUrl } from '../../config';
 | 
				
			||||||
import CropWindow from '../views/components/crop-window.vue';
 | 
					import CropWindow from '../views/components/crop-window.vue';
 | 
				
			||||||
import ProgressDialog from '../views/components/progress-dialog.vue';
 | 
					import ProgressDialog from '../views/components/progress-dialog.vue';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default (os: OS) => (cb, file = null) => {
 | 
					export default (os: OS) => {
 | 
				
			||||||
	const fileSelected = file => {
 | 
					
 | 
				
			||||||
 | 
						const cropImage = file => new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$');
 | 
				
			||||||
 | 
							if (!regex.test(file.name) ) {
 | 
				
			||||||
 | 
								os.apis.dialog({
 | 
				
			||||||
 | 
									title: '%fa:info-circle% %i18n:desktop.invalid-filetype%',
 | 
				
			||||||
 | 
									text: null,
 | 
				
			||||||
 | 
									actions: [{
 | 
				
			||||||
 | 
										text: '%i18n:common.got-it%'
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								reject();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const w = os.new(CropWindow, {
 | 
							const w = os.new(CropWindow, {
 | 
				
			||||||
			image: file,
 | 
								image: file,
 | 
				
			||||||
| 
						 | 
					@ -19,27 +32,29 @@ export default (os: OS) => (cb, file = null) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			os.api('drive/folders/find', {
 | 
								os.api('drive/folders/find', {
 | 
				
			||||||
				name: '%i18n:desktop.avatar%'
 | 
									name: '%i18n:desktop.avatar%'
 | 
				
			||||||
			}).then(iconFolder => {
 | 
								}).then(avatarFolder => {
 | 
				
			||||||
				if (iconFolder.length === 0) {
 | 
									if (avatarFolder.length === 0) {
 | 
				
			||||||
					os.api('drive/folders/create', {
 | 
										os.api('drive/folders/create', {
 | 
				
			||||||
						name: '%i18n:desktop.avatar%'
 | 
											name: '%i18n:desktop.avatar%'
 | 
				
			||||||
					}).then(iconFolder => {
 | 
										}).then(iconFolder => {
 | 
				
			||||||
						upload(data, iconFolder);
 | 
											resolve(upload(data, iconFolder));
 | 
				
			||||||
					});
 | 
										});
 | 
				
			||||||
				} else {
 | 
									} else {
 | 
				
			||||||
					upload(data, iconFolder[0]);
 | 
										resolve(upload(data, avatarFolder[0]));
 | 
				
			||||||
				}
 | 
									}
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		w.$once('skipped', () => {
 | 
							w.$once('skipped', () => {
 | 
				
			||||||
			set(file);
 | 
								resolve(file);
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		document.body.appendChild(w.$el);
 | 
							w.$once('cancelled', reject);
 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const upload = (data, folder) => {
 | 
							document.body.appendChild(w.$el);
 | 
				
			||||||
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const upload = (data, folder) => new Promise((resolve, reject) => {
 | 
				
			||||||
		const dialog = os.new(ProgressDialog, {
 | 
							const dialog = os.new(ProgressDialog, {
 | 
				
			||||||
			title: '%i18n:desktop.uploading-avatar%'
 | 
								title: '%i18n:desktop.uploading-avatar%'
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
| 
						 | 
					@ -52,18 +67,19 @@ export default (os: OS) => (cb, file = null) => {
 | 
				
			||||||
		xhr.onload = e => {
 | 
							xhr.onload = e => {
 | 
				
			||||||
			const file = JSON.parse((e.target as any).response);
 | 
								const file = JSON.parse((e.target as any).response);
 | 
				
			||||||
			(dialog as any).close();
 | 
								(dialog as any).close();
 | 
				
			||||||
			set(file);
 | 
								resolve(file);
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
							xhr.onerror = reject;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		xhr.upload.onprogress = e => {
 | 
							xhr.upload.onprogress = e => {
 | 
				
			||||||
			if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
 | 
								if (e.lengthComputable) (dialog as any).update(e.loaded, e.total);
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		xhr.send(data);
 | 
							xhr.send(data);
 | 
				
			||||||
	};
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const set = file => {
 | 
						const setAvatar = file => {
 | 
				
			||||||
		os.api('i/update', {
 | 
							return os.api('i/update', {
 | 
				
			||||||
			avatarId: file.id
 | 
								avatarId: file.id
 | 
				
			||||||
		}).then(i => {
 | 
							}).then(i => {
 | 
				
			||||||
			os.store.commit('updateIKeyValue', {
 | 
								os.store.commit('updateIKeyValue', {
 | 
				
			||||||
| 
						 | 
					@ -83,18 +99,21 @@ export default (os: OS) => (cb, file = null) => {
 | 
				
			||||||
				}]
 | 
									}]
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (cb) cb(i);
 | 
								return i;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (file) {
 | 
						return (file = null) => {
 | 
				
			||||||
		fileSelected(file);
 | 
							const selectedFile = file
 | 
				
			||||||
	} else {
 | 
								? Promise.resolve(file)
 | 
				
			||||||
		os.apis.chooseDriveFile({
 | 
								: os.apis.chooseDriveFile({
 | 
				
			||||||
			multiple: false,
 | 
									multiple: false,
 | 
				
			||||||
			title: '%fa:image% %i18n:desktop.choose-avatar%'
 | 
									title: '%fa:image% %i18n:desktop.choose-avatar%'
 | 
				
			||||||
		}).then(file => {
 | 
								});
 | 
				
			||||||
			fileSelected(file);
 | 
					
 | 
				
			||||||
		});
 | 
							return selectedFile
 | 
				
			||||||
	}
 | 
								.then(cropImage)
 | 
				
			||||||
 | 
								.then(setAvatar)
 | 
				
			||||||
 | 
								.catch(err => err && console.warn(err));
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,19 @@ import ProgressDialog from '../views/components/progress-dialog.vue';
 | 
				
			||||||
export default (os: OS) => {
 | 
					export default (os: OS) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const cropImage = file => new Promise((resolve, reject) => {
 | 
						const cropImage = file => new Promise((resolve, reject) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const regex = RegExp('\.(jpg|jpeg|png|gif|webp|bmp|tiff)$');
 | 
				
			||||||
 | 
							if (!regex.test(file.name) ) {
 | 
				
			||||||
 | 
								os.apis.dialog({
 | 
				
			||||||
 | 
									title: '%fa:info-circle% %i18n:desktop.invalid-filetype%',
 | 
				
			||||||
 | 
									text: null,
 | 
				
			||||||
 | 
									actions: [{
 | 
				
			||||||
 | 
										text: '%i18n:common.got-it%'
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
								reject();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		const w = os.new(CropWindow, {
 | 
							const w = os.new(CropWindow, {
 | 
				
			||||||
			image: file,
 | 
								image: file,
 | 
				
			||||||
			title: '%i18n:desktop.banner-crop-title%',
 | 
								title: '%i18n:desktop.banner-crop-title%',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,7 @@ import updateBanner from './api/update-banner';
 | 
				
			||||||
import MkIndex from './views/pages/index.vue';
 | 
					import MkIndex from './views/pages/index.vue';
 | 
				
			||||||
import MkDeck from './views/pages/deck/deck.vue';
 | 
					import MkDeck from './views/pages/deck/deck.vue';
 | 
				
			||||||
import MkAdmin from './views/pages/admin/admin.vue';
 | 
					import MkAdmin from './views/pages/admin/admin.vue';
 | 
				
			||||||
 | 
					import MkStats from './views/pages/stats/stats.vue';
 | 
				
			||||||
import MkUser from './views/pages/user/user.vue';
 | 
					import MkUser from './views/pages/user/user.vue';
 | 
				
			||||||
import MkFavorites from './views/pages/favorites.vue';
 | 
					import MkFavorites from './views/pages/favorites.vue';
 | 
				
			||||||
import MkSelectDrive from './views/pages/selectdrive.vue';
 | 
					import MkSelectDrive from './views/pages/selectdrive.vue';
 | 
				
			||||||
| 
						 | 
					@ -57,6 +58,7 @@ init(async (launch) => {
 | 
				
			||||||
			{ path: '/', name: 'index', component: MkIndex },
 | 
								{ path: '/', name: 'index', component: MkIndex },
 | 
				
			||||||
			{ path: '/deck', name: 'deck', component: MkDeck },
 | 
								{ path: '/deck', name: 'deck', component: MkDeck },
 | 
				
			||||||
			{ path: '/admin', name: 'admin', component: MkAdmin },
 | 
								{ path: '/admin', name: 'admin', component: MkAdmin },
 | 
				
			||||||
 | 
								{ path: '/stats', name: 'stats', component: MkStats },
 | 
				
			||||||
			{ path: '/i/customize-home', component: MkHomeCustomize },
 | 
								{ path: '/i/customize-home', component: MkHomeCustomize },
 | 
				
			||||||
			{ path: '/i/favorites', component: MkFavorites },
 | 
								{ path: '/i/favorites', component: MkFavorites },
 | 
				
			||||||
			{ path: '/i/messaging/:user', component: MkMessagingRoom },
 | 
								{ path: '/i/messaging/:user', component: MkMessagingRoom },
 | 
				
			||||||
| 
						 | 
					@ -94,7 +96,7 @@ init(async (launch) => {
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * Init Notification
 | 
						 * Init Notification
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	if ('Notification' in window) {
 | 
						if ('Notification' in window && os.store.getters.isSignedIn) {
 | 
				
			||||||
		// 許可を得ていなかったらリクエスト
 | 
							// 許可を得ていなかったらリクエスト
 | 
				
			||||||
		if ((Notification as any).permission == 'default') {
 | 
							if ((Notification as any).permission == 'default') {
 | 
				
			||||||
			await Notification.requestPermission();
 | 
								await Notification.requestPermission();
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										42
									
								
								src/client/app/desktop/views/components/charts.chart.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								src/client/app/desktop/views/components/charts.chart.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,42 @@
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import { Line } from 'vue-chartjs';
 | 
				
			||||||
 | 
					import * as mergeOptions from 'merge-options';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						extends: Line,
 | 
				
			||||||
 | 
						props: {
 | 
				
			||||||
 | 
							data: {
 | 
				
			||||||
 | 
								required: true
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							opts: {
 | 
				
			||||||
 | 
								required: false
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						watch: {
 | 
				
			||||||
 | 
							data() {
 | 
				
			||||||
 | 
								this.render();
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						mounted() {
 | 
				
			||||||
 | 
							this.render();
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							render() {
 | 
				
			||||||
 | 
								this.renderChart(this.data, mergeOptions({
 | 
				
			||||||
 | 
									responsive: true,
 | 
				
			||||||
 | 
									maintainAspectRatio: false,
 | 
				
			||||||
 | 
									scales: {
 | 
				
			||||||
 | 
										xAxes: [{
 | 
				
			||||||
 | 
											type: 'time',
 | 
				
			||||||
 | 
											distribution: 'series'
 | 
				
			||||||
 | 
										}]
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									tooltips: {
 | 
				
			||||||
 | 
										intersect: false,
 | 
				
			||||||
 | 
										mode: 'x',
 | 
				
			||||||
 | 
										position: 'nearest'
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}, this.opts || {}));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
							
								
								
									
										587
									
								
								src/client/app/desktop/views/components/charts.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										587
									
								
								src/client/app/desktop/views/components/charts.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,587 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div class="gkgckalzgidaygcxnugepioremxvxvpt">
 | 
				
			||||||
 | 
						<header>
 | 
				
			||||||
 | 
							<b>%i18n:@title%:</b>
 | 
				
			||||||
 | 
							<select v-model="chartType">
 | 
				
			||||||
 | 
								<optgroup label="%i18n:@users%">
 | 
				
			||||||
 | 
									<option value="users">%i18n:@charts.users%</option>
 | 
				
			||||||
 | 
									<option value="users-total">%i18n:@charts.users-total%</option>
 | 
				
			||||||
 | 
								</optgroup>
 | 
				
			||||||
 | 
								<optgroup label="%i18n:@notes%">
 | 
				
			||||||
 | 
									<option value="notes">%i18n:@charts.notes%</option>
 | 
				
			||||||
 | 
									<option value="local-notes">%i18n:@charts.local-notes%</option>
 | 
				
			||||||
 | 
									<option value="remote-notes">%i18n:@charts.remote-notes%</option>
 | 
				
			||||||
 | 
									<option value="notes-total">%i18n:@charts.notes-total%</option>
 | 
				
			||||||
 | 
								</optgroup>
 | 
				
			||||||
 | 
								<optgroup label="%i18n:@drive%">
 | 
				
			||||||
 | 
									<option value="drive-files">%i18n:@charts.drive-files%</option>
 | 
				
			||||||
 | 
									<option value="drive-files-total">%i18n:@charts.drive-files-total%</option>
 | 
				
			||||||
 | 
									<option value="drive">%i18n:@charts.drive%</option>
 | 
				
			||||||
 | 
									<option value="drive-total">%i18n:@charts.drive-total%</option>
 | 
				
			||||||
 | 
								</optgroup>
 | 
				
			||||||
 | 
							</select>
 | 
				
			||||||
 | 
							<div>
 | 
				
			||||||
 | 
								<span @click="span = 'day'" :class="{ active: span == 'day' }">%i18n:@per-day%</span> | <span @click="span = 'hour'" :class="{ active: span == 'hour' }">%i18n:@per-hour%</span>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						</header>
 | 
				
			||||||
 | 
						<div>
 | 
				
			||||||
 | 
							<x-chart v-if="chart" :data="data[0]" :opts="data[1]"/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from 'vue';
 | 
				
			||||||
 | 
					import XChart from './charts.chart.ts';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const colors = {
 | 
				
			||||||
 | 
						local: 'rgb(246, 88, 79)',
 | 
				
			||||||
 | 
						remote: 'rgb(65, 221, 222)',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						localPlus: 'rgb(52, 178, 118)',
 | 
				
			||||||
 | 
						remotePlus: 'rgb(158, 255, 209)',
 | 
				
			||||||
 | 
						localMinus: 'rgb(255, 97, 74)',
 | 
				
			||||||
 | 
						remoteMinus: 'rgb(255, 149, 134)'
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const rgba = (color: string): string => {
 | 
				
			||||||
 | 
						return color.replace('rgb', 'rgba').replace(')', ', 0.1)');
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XChart
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								chart: null,
 | 
				
			||||||
 | 
								chartType: 'notes',
 | 
				
			||||||
 | 
								span: 'hour'
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						computed: {
 | 
				
			||||||
 | 
							data(): any {
 | 
				
			||||||
 | 
								if (this.chart == null) return null;
 | 
				
			||||||
 | 
								switch (this.chartType) {
 | 
				
			||||||
 | 
									case 'users': return this.usersChart(false);
 | 
				
			||||||
 | 
									case 'users-total': return this.usersChart(true);
 | 
				
			||||||
 | 
									case 'notes': return this.notesChart('combined');
 | 
				
			||||||
 | 
									case 'local-notes': return this.notesChart('local');
 | 
				
			||||||
 | 
									case 'remote-notes': return this.notesChart('remote');
 | 
				
			||||||
 | 
									case 'notes-total': return this.notesTotalChart();
 | 
				
			||||||
 | 
									case 'drive': return this.driveChart();
 | 
				
			||||||
 | 
									case 'drive-total': return this.driveTotalChart();
 | 
				
			||||||
 | 
									case 'drive-files': return this.driveFilesChart();
 | 
				
			||||||
 | 
									case 'drive-files-total': return this.driveFilesTotalChart();
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							stats(): any[] {
 | 
				
			||||||
 | 
								return (
 | 
				
			||||||
 | 
									this.span == 'day' ? this.chart.perDay :
 | 
				
			||||||
 | 
									this.span == 'hour' ? this.chart.perHour :
 | 
				
			||||||
 | 
									null
 | 
				
			||||||
 | 
								);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							(this as any).api('chart', {
 | 
				
			||||||
 | 
								limit: 32
 | 
				
			||||||
 | 
							}).then(chart => {
 | 
				
			||||||
 | 
								this.chart = chart;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						methods: {
 | 
				
			||||||
 | 
							notesChart(type: string): any {
 | 
				
			||||||
 | 
								const data = this.stats.slice().reverse().map(x => ({
 | 
				
			||||||
 | 
									date: new Date(x.date),
 | 
				
			||||||
 | 
									normal: type == 'local' ? x.notes.local.diffs.normal : type == 'remote' ? x.notes.remote.diffs.normal : x.notes.local.diffs.normal + x.notes.remote.diffs.normal,
 | 
				
			||||||
 | 
									reply: type == 'local' ? x.notes.local.diffs.reply : type == 'remote' ? x.notes.remote.diffs.reply : x.notes.local.diffs.reply + x.notes.remote.diffs.reply,
 | 
				
			||||||
 | 
									renote: type == 'local' ? x.notes.local.diffs.renote : type == 'remote' ? x.notes.remote.diffs.renote : x.notes.local.diffs.renote + x.notes.remote.diffs.renote,
 | 
				
			||||||
 | 
									all: type == 'local' ? (x.notes.local.inc + -x.notes.local.dec) : type == 'remote' ? (x.notes.remote.inc + -x.notes.remote.dec) : (x.notes.local.inc + -x.notes.local.dec) + (x.notes.remote.inc + -x.notes.remote.dec)
 | 
				
			||||||
 | 
								}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return [{
 | 
				
			||||||
 | 
									datasets: [{
 | 
				
			||||||
 | 
										label: 'All',
 | 
				
			||||||
 | 
										fill: false,
 | 
				
			||||||
 | 
										borderColor: '#555',
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										borderDash: [4, 4],
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.all }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Renotes',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: 'rgba(161, 222, 65, 0.1)',
 | 
				
			||||||
 | 
										borderColor: '#a1de41',
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.renote }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Replies',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: 'rgba(247, 121, 108, 0.1)',
 | 
				
			||||||
 | 
										borderColor: '#f7796c',
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.reply }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Normal',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: 'rgba(65, 221, 222, 0.1)',
 | 
				
			||||||
 | 
										borderColor: '#41ddde',
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.normal }))
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									scales: {
 | 
				
			||||||
 | 
										yAxes: [{
 | 
				
			||||||
 | 
											ticks: {
 | 
				
			||||||
 | 
												callback: value => {
 | 
				
			||||||
 | 
													return Vue.filter('number')(value);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}]
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									tooltips: {
 | 
				
			||||||
 | 
										callbacks: {
 | 
				
			||||||
 | 
											label: (tooltipItem, data) => {
 | 
				
			||||||
 | 
												const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
				
			||||||
 | 
												return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}];
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							notesTotalChart(): any {
 | 
				
			||||||
 | 
								const data = this.stats.slice().reverse().map(x => ({
 | 
				
			||||||
 | 
									date: new Date(x.date),
 | 
				
			||||||
 | 
									localCount: x.notes.local.total,
 | 
				
			||||||
 | 
									remoteCount: x.notes.remote.total
 | 
				
			||||||
 | 
								}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return [{
 | 
				
			||||||
 | 
									datasets: [{
 | 
				
			||||||
 | 
										label: 'Combined',
 | 
				
			||||||
 | 
										fill: false,
 | 
				
			||||||
 | 
										borderColor: '#555',
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										borderDash: [4, 4],
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Local',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.local),
 | 
				
			||||||
 | 
										borderColor: colors.local,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localCount }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Remote',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.remote),
 | 
				
			||||||
 | 
										borderColor: colors.remote,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteCount }))
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									scales: {
 | 
				
			||||||
 | 
										yAxes: [{
 | 
				
			||||||
 | 
											ticks: {
 | 
				
			||||||
 | 
												callback: value => {
 | 
				
			||||||
 | 
													return Vue.filter('number')(value);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}]
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									tooltips: {
 | 
				
			||||||
 | 
										callbacks: {
 | 
				
			||||||
 | 
											label: (tooltipItem, data) => {
 | 
				
			||||||
 | 
												const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
				
			||||||
 | 
												return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}];
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							usersChart(total: boolean): any {
 | 
				
			||||||
 | 
								const data = this.stats.slice().reverse().map(x => ({
 | 
				
			||||||
 | 
									date: new Date(x.date),
 | 
				
			||||||
 | 
									localCount: total ? x.users.local.total : (x.users.local.inc + -x.users.local.dec),
 | 
				
			||||||
 | 
									remoteCount: total ? x.users.remote.total : (x.users.remote.inc + -x.users.remote.dec)
 | 
				
			||||||
 | 
								}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return [{
 | 
				
			||||||
 | 
									datasets: [{
 | 
				
			||||||
 | 
										label: 'Combined',
 | 
				
			||||||
 | 
										fill: false,
 | 
				
			||||||
 | 
										borderColor: '#555',
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										borderDash: [4, 4],
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteCount + x.localCount }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Local',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.local),
 | 
				
			||||||
 | 
										borderColor: colors.local,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localCount }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Remote',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.remote),
 | 
				
			||||||
 | 
										borderColor: colors.remote,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteCount }))
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									scales: {
 | 
				
			||||||
 | 
										yAxes: [{
 | 
				
			||||||
 | 
											ticks: {
 | 
				
			||||||
 | 
												callback: value => {
 | 
				
			||||||
 | 
													return Vue.filter('number')(value);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}]
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									tooltips: {
 | 
				
			||||||
 | 
										callbacks: {
 | 
				
			||||||
 | 
											label: (tooltipItem, data) => {
 | 
				
			||||||
 | 
												const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
				
			||||||
 | 
												return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}];
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							driveChart(): any {
 | 
				
			||||||
 | 
								const data = this.stats.slice().reverse().map(x => ({
 | 
				
			||||||
 | 
									date: new Date(x.date),
 | 
				
			||||||
 | 
									localInc: x.drive.local.incSize,
 | 
				
			||||||
 | 
									localDec: -x.drive.local.decSize,
 | 
				
			||||||
 | 
									remoteInc: x.drive.remote.incSize,
 | 
				
			||||||
 | 
									remoteDec: -x.drive.remote.decSize,
 | 
				
			||||||
 | 
								}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return [{
 | 
				
			||||||
 | 
									datasets: [{
 | 
				
			||||||
 | 
										label: 'All',
 | 
				
			||||||
 | 
										fill: false,
 | 
				
			||||||
 | 
										borderColor: '#555',
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										borderDash: [4, 4],
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Local +',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.localPlus),
 | 
				
			||||||
 | 
										borderColor: colors.localPlus,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localInc }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Local -',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.localMinus),
 | 
				
			||||||
 | 
										borderColor: colors.localMinus,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localDec }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Remote +',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.remotePlus),
 | 
				
			||||||
 | 
										borderColor: colors.remotePlus,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteInc }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Remote -',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.remoteMinus),
 | 
				
			||||||
 | 
										borderColor: colors.remoteMinus,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteDec }))
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									scales: {
 | 
				
			||||||
 | 
										yAxes: [{
 | 
				
			||||||
 | 
											ticks: {
 | 
				
			||||||
 | 
												callback: value => {
 | 
				
			||||||
 | 
													return Vue.filter('bytes')(value, 1);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}]
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									tooltips: {
 | 
				
			||||||
 | 
										callbacks: {
 | 
				
			||||||
 | 
											label: (tooltipItem, data) => {
 | 
				
			||||||
 | 
												const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
				
			||||||
 | 
												return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}];
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							driveTotalChart(): any {
 | 
				
			||||||
 | 
								const data = this.stats.slice().reverse().map(x => ({
 | 
				
			||||||
 | 
									date: new Date(x.date),
 | 
				
			||||||
 | 
									localSize: x.drive.local.totalSize,
 | 
				
			||||||
 | 
									remoteSize: x.drive.remote.totalSize
 | 
				
			||||||
 | 
								}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return [{
 | 
				
			||||||
 | 
									datasets: [{
 | 
				
			||||||
 | 
										label: 'Combined',
 | 
				
			||||||
 | 
										fill: false,
 | 
				
			||||||
 | 
										borderColor: '#555',
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										borderDash: [4, 4],
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteSize + x.localSize }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Local',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.local),
 | 
				
			||||||
 | 
										borderColor: colors.local,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localSize }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Remote',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.remote),
 | 
				
			||||||
 | 
										borderColor: colors.remote,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteSize }))
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									scales: {
 | 
				
			||||||
 | 
										yAxes: [{
 | 
				
			||||||
 | 
											ticks: {
 | 
				
			||||||
 | 
												callback: value => {
 | 
				
			||||||
 | 
													return Vue.filter('bytes')(value, 1);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}]
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									tooltips: {
 | 
				
			||||||
 | 
										callbacks: {
 | 
				
			||||||
 | 
											label: (tooltipItem, data) => {
 | 
				
			||||||
 | 
												const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
				
			||||||
 | 
												return `${label}: ${Vue.filter('bytes')(tooltipItem.yLabel, 1)}`;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}];
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							driveFilesChart(): any {
 | 
				
			||||||
 | 
								const data = this.stats.slice().reverse().map(x => ({
 | 
				
			||||||
 | 
									date: new Date(x.date),
 | 
				
			||||||
 | 
									localInc: x.drive.local.incCount,
 | 
				
			||||||
 | 
									localDec: -x.drive.local.decCount,
 | 
				
			||||||
 | 
									remoteInc: x.drive.remote.incCount,
 | 
				
			||||||
 | 
									remoteDec: -x.drive.remote.decCount
 | 
				
			||||||
 | 
								}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return [{
 | 
				
			||||||
 | 
									datasets: [{
 | 
				
			||||||
 | 
										label: 'All',
 | 
				
			||||||
 | 
										fill: false,
 | 
				
			||||||
 | 
										borderColor: '#555',
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										borderDash: [4, 4],
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localInc + x.localDec + x.remoteInc + x.remoteDec }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Local +',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.localPlus),
 | 
				
			||||||
 | 
										borderColor: colors.localPlus,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localInc }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Local -',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.localMinus),
 | 
				
			||||||
 | 
										borderColor: colors.localMinus,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localDec }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Remote +',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.remotePlus),
 | 
				
			||||||
 | 
										borderColor: colors.remotePlus,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteInc }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Remote -',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.remoteMinus),
 | 
				
			||||||
 | 
										borderColor: colors.remoteMinus,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteDec }))
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									scales: {
 | 
				
			||||||
 | 
										yAxes: [{
 | 
				
			||||||
 | 
											ticks: {
 | 
				
			||||||
 | 
												callback: value => {
 | 
				
			||||||
 | 
													return Vue.filter('number')(value);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}]
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									tooltips: {
 | 
				
			||||||
 | 
										callbacks: {
 | 
				
			||||||
 | 
											label: (tooltipItem, data) => {
 | 
				
			||||||
 | 
												const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
				
			||||||
 | 
												return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}];
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							driveFilesTotalChart(): any {
 | 
				
			||||||
 | 
								const data = this.stats.slice().reverse().map(x => ({
 | 
				
			||||||
 | 
									date: new Date(x.date),
 | 
				
			||||||
 | 
									localCount: x.drive.local.totalCount,
 | 
				
			||||||
 | 
									remoteCount: x.drive.remote.totalCount,
 | 
				
			||||||
 | 
								}));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								return [{
 | 
				
			||||||
 | 
									datasets: [{
 | 
				
			||||||
 | 
										label: 'Combined',
 | 
				
			||||||
 | 
										fill: false,
 | 
				
			||||||
 | 
										borderColor: '#555',
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										borderDash: [4, 4],
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localCount + x.remoteCount }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Local',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.local),
 | 
				
			||||||
 | 
										borderColor: colors.local,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.localCount }))
 | 
				
			||||||
 | 
									}, {
 | 
				
			||||||
 | 
										label: 'Remote',
 | 
				
			||||||
 | 
										fill: true,
 | 
				
			||||||
 | 
										backgroundColor: rgba(colors.remote),
 | 
				
			||||||
 | 
										borderColor: colors.remote,
 | 
				
			||||||
 | 
										borderWidth: 2,
 | 
				
			||||||
 | 
										pointBackgroundColor: '#fff',
 | 
				
			||||||
 | 
										lineTension: 0,
 | 
				
			||||||
 | 
										data: data.map(x => ({ t: x.date, y: x.remoteCount }))
 | 
				
			||||||
 | 
									}]
 | 
				
			||||||
 | 
								}, {
 | 
				
			||||||
 | 
									scales: {
 | 
				
			||||||
 | 
										yAxes: [{
 | 
				
			||||||
 | 
											ticks: {
 | 
				
			||||||
 | 
												callback: value => {
 | 
				
			||||||
 | 
													return Vue.filter('number')(value);
 | 
				
			||||||
 | 
												}
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}]
 | 
				
			||||||
 | 
									},
 | 
				
			||||||
 | 
									tooltips: {
 | 
				
			||||||
 | 
										callbacks: {
 | 
				
			||||||
 | 
											label: (tooltipItem, data) => {
 | 
				
			||||||
 | 
												const label = data.datasets[tooltipItem.datasetIndex].label || '';
 | 
				
			||||||
 | 
												return `${label}: ${Vue.filter('number')(tooltipItem.yLabel)}`;
 | 
				
			||||||
 | 
											}
 | 
				
			||||||
 | 
										}
 | 
				
			||||||
 | 
									}
 | 
				
			||||||
 | 
								}];
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					@import '~const.styl'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.gkgckalzgidaygcxnugepioremxvxvpt
 | 
				
			||||||
 | 
						padding 32px
 | 
				
			||||||
 | 
						background #fff
 | 
				
			||||||
 | 
						box-shadow 0 2px 8px rgba(#000, 0.1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*
 | 
				
			||||||
 | 
							user-select none
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> header
 | 
				
			||||||
 | 
							display flex
 | 
				
			||||||
 | 
							margin 0 0 1em 0
 | 
				
			||||||
 | 
							padding 0 0 8px 0
 | 
				
			||||||
 | 
							font-size 1em
 | 
				
			||||||
 | 
							color #555
 | 
				
			||||||
 | 
							border-bottom solid 1px #eee
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> b
 | 
				
			||||||
 | 
								margin-right 8px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> *:last-child
 | 
				
			||||||
 | 
								margin-left auto
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								*
 | 
				
			||||||
 | 
									&:not(.active)
 | 
				
			||||||
 | 
										color $theme-color
 | 
				
			||||||
 | 
										cursor pointer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> div
 | 
				
			||||||
 | 
							> *
 | 
				
			||||||
 | 
								display block
 | 
				
			||||||
 | 
								height 320px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -47,7 +47,7 @@
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<mk-poll v-if="p.poll" :note="p"/>
 | 
								<mk-poll v-if="p.poll" :note="p"/>
 | 
				
			||||||
			<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
 | 
								<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
 | 
				
			||||||
			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
								<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
				
			||||||
			<div class="map" v-if="p.geo" ref="map"></div>
 | 
								<div class="map" v-if="p.geo" ref="map"></div>
 | 
				
			||||||
			<div class="renote" v-if="p.renote">
 | 
								<div class="renote" v-if="p.renote">
 | 
				
			||||||
				<mk-note-preview :note="p.renote"/>
 | 
									<mk-note-preview :note="p.renote"/>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,7 +32,7 @@
 | 
				
			||||||
						<mk-media-list :media-list="p.media"/>
 | 
											<mk-media-list :media-list="p.media"/>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 | 
										<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 | 
				
			||||||
					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 | 
										<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% 位置情報</a>
 | 
				
			||||||
					<div class="map" v-if="p.geo" ref="map"></div>
 | 
										<div class="map" v-if="p.geo" ref="map"></div>
 | 
				
			||||||
					<div class="renote" v-if="p.renote">
 | 
										<div class="renote" v-if="p.renote">
 | 
				
			||||||
						<mk-note-preview :note="p.renote"/>
 | 
											<mk-note-preview :note="p.renote"/>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -49,6 +49,7 @@
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
 | 
								<mk-switch v-model="$store.state.settings.showPostFormOnTopOfTl" @change="onChangeShowPostFormOnTopOfTl" text="%i18n:@post-form-on-timeline%"/>
 | 
				
			||||||
			<mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
 | 
								<mk-switch v-model="$store.state.settings.suggestRecentHashtags" @change="onChangeSuggestRecentHashtags" text="%i18n:@suggest-recent-hashtags%"/>
 | 
				
			||||||
 | 
								<mk-switch v-model="$store.state.settings.showClockOnHeader" @change="onChangeShowClockOnHeader" text="%i18n:@show-clock-on-header%"/>
 | 
				
			||||||
			<mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/>
 | 
								<mk-switch v-model="$store.state.settings.showReplyTarget" @change="onChangeShowReplyTarget" text="%i18n:@show-reply-target%"/>
 | 
				
			||||||
			<mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/>
 | 
								<mk-switch v-model="$store.state.settings.showMyRenotes" @change="onChangeShowMyRenotes" text="%i18n:@show-my-renotes%"/>
 | 
				
			||||||
			<mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
 | 
								<mk-switch v-model="$store.state.settings.showRenotedMyNotes" @change="onChangeShowRenotedMyNotes" text="%i18n:@show-renoted-my-notes%"/>
 | 
				
			||||||
| 
						 | 
					@ -333,6 +334,12 @@ export default Vue.extend({
 | 
				
			||||||
				value: v
 | 
									value: v
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
							onChangeShowClockOnHeader(v) {
 | 
				
			||||||
 | 
								this.$store.dispatch('settings/set', {
 | 
				
			||||||
 | 
									key: 'showClockOnHeader',
 | 
				
			||||||
 | 
									value: v
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
		onChangeShowReplyTarget(v) {
 | 
							onChangeShowReplyTarget(v) {
 | 
				
			||||||
			this.$store.dispatch('settings/set', {
 | 
								this.$store.dispatch('settings/set', {
 | 
				
			||||||
				key: 'showReplyTarget',
 | 
									key: 'showReplyTarget',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,10 +30,8 @@
 | 
				
			||||||
				<li @click="settings">
 | 
									<li @click="settings">
 | 
				
			||||||
					<p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p>
 | 
										<p>%fa:cog%<span>%i18n:@settings%</span>%fa:angle-right%</p>
 | 
				
			||||||
				</li>
 | 
									</li>
 | 
				
			||||||
			</ul>
 | 
									<li v-if="$store.state.i.isAdmin">
 | 
				
			||||||
			<ul>
 | 
										<router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link>
 | 
				
			||||||
				<li @click="signout">
 | 
					 | 
				
			||||||
					<p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p>
 | 
					 | 
				
			||||||
				</li>
 | 
									</li>
 | 
				
			||||||
			</ul>
 | 
								</ul>
 | 
				
			||||||
			<ul>
 | 
								<ul>
 | 
				
			||||||
| 
						 | 
					@ -41,6 +39,11 @@
 | 
				
			||||||
					<p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
 | 
										<p><span>%i18n:@dark%</span><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template></p>
 | 
				
			||||||
				</li>
 | 
									</li>
 | 
				
			||||||
			</ul>
 | 
								</ul>
 | 
				
			||||||
 | 
								<ul>
 | 
				
			||||||
 | 
									<li @click="signout">
 | 
				
			||||||
 | 
										<p class="signout">%fa:power-off%<span>%i18n:@signout%</span></p>
 | 
				
			||||||
 | 
									</li>
 | 
				
			||||||
 | 
								</ul>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
	</transition>
 | 
						</transition>
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@
 | 
				
			||||||
			<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
 | 
								<li class="deck" :class="{ active: $route.name == 'deck' }" @click="goToTop">
 | 
				
			||||||
				<router-link to="/deck">
 | 
									<router-link to="/deck">
 | 
				
			||||||
					%fa:columns%
 | 
										%fa:columns%
 | 
				
			||||||
					<p>%i18n:@deck% <small>(beta)</small></p>
 | 
										<p>%i18n:@deck%</p>
 | 
				
			||||||
				</router-link>
 | 
									</router-link>
 | 
				
			||||||
			</li>
 | 
								</li>
 | 
				
			||||||
			<li class="messaging">
 | 
								<li class="messaging">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -17,7 +17,7 @@
 | 
				
			||||||
					<x-account v-if="$store.getters.isSignedIn"/>
 | 
										<x-account v-if="$store.getters.isSignedIn"/>
 | 
				
			||||||
					<x-notifications v-if="$store.getters.isSignedIn"/>
 | 
										<x-notifications v-if="$store.getters.isSignedIn"/>
 | 
				
			||||||
					<x-post v-if="$store.getters.isSignedIn"/>
 | 
										<x-post v-if="$store.getters.isSignedIn"/>
 | 
				
			||||||
					<x-clock/>
 | 
										<x-clock v-if="$store.state.settings.showClockOnHeader"/>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,7 +48,7 @@ export default Vue.extend({
 | 
				
			||||||
				this.open();
 | 
									this.open();
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			const query = this.user[0] == '@' ?
 | 
								const query = this.user.startsWith('@') ?
 | 
				
			||||||
				parseAcct(this.user.substr(1)) :
 | 
									parseAcct(this.user.substr(1)) :
 | 
				
			||||||
				{ userId: this.user };
 | 
									{ userId: this.user };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,16 +1,20 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div class="obdskegsannmntldydackcpzezagxqfy card">
 | 
					<div class="obdskegsannmntldydackcpzezagxqfy mk-admin-card">
 | 
				
			||||||
	<header>%i18n:@dashboard%</header>
 | 
						<header>%i18n:@dashboard%</header>
 | 
				
			||||||
	<div v-if="stats" class="stats">
 | 
						<div v-if="stats" class="stats">
 | 
				
			||||||
		<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
 | 
							<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
 | 
				
			||||||
		<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
 | 
							<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
 | 
				
			||||||
		<div><b>%fa:pen% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
 | 
							<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
 | 
				
			||||||
		<div><span>%fa:pen% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
 | 
							<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div class="cpu-memory">
 | 
						<div class="cpu-memory">
 | 
				
			||||||
		<x-cpu-memory :connection="connection"/>
 | 
							<x-cpu-memory :connection="connection"/>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
	<div>
 | 
						<div>
 | 
				
			||||||
 | 
							<label>
 | 
				
			||||||
 | 
								<input type="checkbox" v-model="disableRegistration" @change="updateMeta">
 | 
				
			||||||
 | 
								<span>disableRegistration</span>
 | 
				
			||||||
 | 
							</label>
 | 
				
			||||||
		<button class="ui" @click="invite">%i18n:@invite%</button>
 | 
							<button class="ui" @click="invite">%i18n:@invite%</button>
 | 
				
			||||||
		<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
 | 
							<p v-if="inviteCode">Code: <code>{{ inviteCode }}</code></p>
 | 
				
			||||||
	</div>
 | 
						</div>
 | 
				
			||||||
| 
						 | 
					@ -28,6 +32,7 @@ export default Vue.extend({
 | 
				
			||||||
	data() {
 | 
						data() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			stats: null,
 | 
								stats: null,
 | 
				
			||||||
 | 
								disableRegistration: false,
 | 
				
			||||||
			inviteCode: null,
 | 
								inviteCode: null,
 | 
				
			||||||
			connection: null,
 | 
								connection: null,
 | 
				
			||||||
			connectionId: null
 | 
								connectionId: null
 | 
				
			||||||
| 
						 | 
					@ -37,6 +42,10 @@ export default Vue.extend({
 | 
				
			||||||
		this.connection = (this as any).os.streams.serverStatsStream.getConnection();
 | 
							this.connection = (this as any).os.streams.serverStatsStream.getConnection();
 | 
				
			||||||
		this.connectionId = (this as any).os.streams.serverStatsStream.use();
 | 
							this.connectionId = (this as any).os.streams.serverStatsStream.use();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							(this as any).os.getMeta().then(meta => {
 | 
				
			||||||
 | 
								this.disableRegistration = meta.disableRegistration;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		(this as any).api('stats').then(stats => {
 | 
							(this as any).api('stats').then(stats => {
 | 
				
			||||||
			this.stats = stats;
 | 
								this.stats = stats;
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
| 
						 | 
					@ -49,6 +58,11 @@ export default Vue.extend({
 | 
				
			||||||
			(this as any).api('admin/invite').then(x => {
 | 
								(this as any).api('admin/invite').then(x => {
 | 
				
			||||||
				this.inviteCode = x.code;
 | 
									this.inviteCode = x.code;
 | 
				
			||||||
			});
 | 
								});
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							updateMeta() {
 | 
				
			||||||
 | 
								(this as any).api('admin/update-meta', {
 | 
				
			||||||
 | 
									disableRegistration: this.disableRegistration
 | 
				
			||||||
 | 
								});
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,51 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
 | 
					 | 
				
			||||||
	<polyline
 | 
					 | 
				
			||||||
		:points="points"
 | 
					 | 
				
			||||||
		fill="none"
 | 
					 | 
				
			||||||
		stroke-width="1"
 | 
					 | 
				
			||||||
		stroke="#555"/>
 | 
					 | 
				
			||||||
</svg>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import Vue from 'vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Vue.extend({
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		chart: {
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		type: {
 | 
					 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			viewBoxX: 365,
 | 
					 | 
				
			||||||
			viewBoxY: 70,
 | 
					 | 
				
			||||||
			points: null
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	created() {
 | 
					 | 
				
			||||||
		const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.drive.local.totalSize : d.drive.remote.totalSize));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (peak != 0) {
 | 
					 | 
				
			||||||
			const data = this.chart.slice().reverse().map(x => ({
 | 
					 | 
				
			||||||
				size: this.type == 'local' ? x.drive.local.totalSize : x.drive.remote.totalSize
 | 
					 | 
				
			||||||
			}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.points = data.map((d, i) => `${i},${(1 - (d.size / peak)) * this.viewBoxY}`).join(' ');
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="stylus" scoped>
 | 
					 | 
				
			||||||
svg
 | 
					 | 
				
			||||||
	display block
 | 
					 | 
				
			||||||
	padding 10px
 | 
					 | 
				
			||||||
	width 100%
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,34 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
<div class="card">
 | 
					 | 
				
			||||||
	<header>%i18n:@title%</header>
 | 
					 | 
				
			||||||
	<div class="card">
 | 
					 | 
				
			||||||
		<header>%i18n:@local%</header>
 | 
					 | 
				
			||||||
		<x-chart v-if="chart" :chart="chart" type="local"/>
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
	<div class="card">
 | 
					 | 
				
			||||||
		<header>%i18n:@remote%</header>
 | 
					 | 
				
			||||||
		<x-chart v-if="chart" :chart="chart" type="remote"/>
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import Vue from "vue";
 | 
					 | 
				
			||||||
import XChart from "./admin.drive-chart.chart.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Vue.extend({
 | 
					 | 
				
			||||||
	components: {
 | 
					 | 
				
			||||||
		XChart
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		chart: {
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="stylus" scoped>
 | 
					 | 
				
			||||||
@import '~const.styl'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,76 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
 | 
					 | 
				
			||||||
	<polyline
 | 
					 | 
				
			||||||
		:points="pointsNote"
 | 
					 | 
				
			||||||
		fill="none"
 | 
					 | 
				
			||||||
		stroke-width="1"
 | 
					 | 
				
			||||||
		stroke="#41ddde"/>
 | 
					 | 
				
			||||||
	<polyline
 | 
					 | 
				
			||||||
		:points="pointsReply"
 | 
					 | 
				
			||||||
		fill="none"
 | 
					 | 
				
			||||||
		stroke-width="1"
 | 
					 | 
				
			||||||
		stroke="#f7796c"/>
 | 
					 | 
				
			||||||
	<polyline
 | 
					 | 
				
			||||||
		:points="pointsRenote"
 | 
					 | 
				
			||||||
		fill="none"
 | 
					 | 
				
			||||||
		stroke-width="1"
 | 
					 | 
				
			||||||
		stroke="#a1de41"/>
 | 
					 | 
				
			||||||
	<polyline
 | 
					 | 
				
			||||||
		:points="pointsTotal"
 | 
					 | 
				
			||||||
		fill="none"
 | 
					 | 
				
			||||||
		stroke-width="1"
 | 
					 | 
				
			||||||
		stroke="#555"
 | 
					 | 
				
			||||||
		stroke-dasharray="2 2"/>
 | 
					 | 
				
			||||||
</svg>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import Vue from 'vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Vue.extend({
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		chart: {
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		type: {
 | 
					 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			viewBoxX: 365,
 | 
					 | 
				
			||||||
			viewBoxY: 70,
 | 
					 | 
				
			||||||
			pointsNote: null,
 | 
					 | 
				
			||||||
			pointsReply: null,
 | 
					 | 
				
			||||||
			pointsRenote: null,
 | 
					 | 
				
			||||||
			pointsTotal: null
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	created() {
 | 
					 | 
				
			||||||
		const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.notes.local.diff : d.notes.remote.diff));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (peak != 0) {
 | 
					 | 
				
			||||||
			const data = this.chart.slice().reverse().map(x => ({
 | 
					 | 
				
			||||||
				normal: this.type == 'local' ? x.notes.local.diffs.normal : x.notes.remote.diffs.normal,
 | 
					 | 
				
			||||||
				reply: this.type == 'local' ? x.notes.local.diffs.reply : x.notes.remote.diffs.reply,
 | 
					 | 
				
			||||||
				renote: this.type == 'local' ? x.notes.local.diffs.renote : x.notes.remote.diffs.renote,
 | 
					 | 
				
			||||||
				total: this.type == 'local' ? x.notes.local.diff : x.notes.remote.diff
 | 
					 | 
				
			||||||
			}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.pointsNote = data.map((d, i) => `${i},${(1 - (d.normal / peak)) * this.viewBoxY}`).join(' ');
 | 
					 | 
				
			||||||
			this.pointsReply = data.map((d, i) => `${i},${(1 - (d.reply / peak)) * this.viewBoxY}`).join(' ');
 | 
					 | 
				
			||||||
			this.pointsRenote = data.map((d, i) => `${i},${(1 - (d.renote / peak)) * this.viewBoxY}`).join(' ');
 | 
					 | 
				
			||||||
			this.pointsTotal = data.map((d, i) => `${i},${(1 - (d.total / peak)) * this.viewBoxY}`).join(' ');
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="stylus" scoped>
 | 
					 | 
				
			||||||
svg
 | 
					 | 
				
			||||||
	display block
 | 
					 | 
				
			||||||
	padding 10px
 | 
					 | 
				
			||||||
	width 100%
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,34 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
<div class="card">
 | 
					 | 
				
			||||||
	<header>%i18n:@title%</header>
 | 
					 | 
				
			||||||
	<div class="card">
 | 
					 | 
				
			||||||
		<header>%i18n:@local%</header>
 | 
					 | 
				
			||||||
		<x-chart v-if="chart" :chart="chart" type="local"/>
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
	<div class="card">
 | 
					 | 
				
			||||||
		<header>%i18n:@remote%</header>
 | 
					 | 
				
			||||||
		<x-chart v-if="chart" :chart="chart" type="remote"/>
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import Vue from "vue";
 | 
					 | 
				
			||||||
import XChart from "./admin.notes-chart.chart.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Vue.extend({
 | 
					 | 
				
			||||||
	components: {
 | 
					 | 
				
			||||||
		XChart
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		chart: {
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="stylus" scoped>
 | 
					 | 
				
			||||||
@import '~const.styl'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div class="card">
 | 
					<div class="mk-admin-card">
 | 
				
			||||||
	<header>%i18n:@suspend-user%</header>
 | 
						<header>%i18n:@suspend-user%</header>
 | 
				
			||||||
	<input v-model="username" type="text" class="ui"/>
 | 
						<input v-model="username" type="text" class="ui"/>
 | 
				
			||||||
	<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button>
 | 
						<button class="ui" @click="suspendUser" :disabled="suspending">%i18n:@suspend%</button>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div class="card">
 | 
					<div class="mk-admin-card">
 | 
				
			||||||
	<header>%i18n:@unsuspend-user%</header>
 | 
						<header>%i18n:@unsuspend-user%</header>
 | 
				
			||||||
	<input v-model="username" type="text" class="ui"/>
 | 
						<input v-model="username" type="text" class="ui"/>
 | 
				
			||||||
	<button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button>
 | 
						<button class="ui" @click="unsuspendUser" :disabled="unsuspending">%i18n:@unsuspend%</button>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div class="card">
 | 
					<div class="mk-admin-card">
 | 
				
			||||||
	<header>%i18n:@unverify-user%</header>
 | 
						<header>%i18n:@unverify-user%</header>
 | 
				
			||||||
	<input v-model="username" type="text" class="ui"/>
 | 
						<input v-model="username" type="text" class="ui"/>
 | 
				
			||||||
	<button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button>
 | 
						<button class="ui" @click="unverifyUser" :disabled="unverifying">%i18n:@unverify%</button>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,51 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
 | 
					 | 
				
			||||||
	<polyline
 | 
					 | 
				
			||||||
		:points="points"
 | 
					 | 
				
			||||||
		fill="none"
 | 
					 | 
				
			||||||
		stroke-width="1"
 | 
					 | 
				
			||||||
		stroke="#555"/>
 | 
					 | 
				
			||||||
</svg>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import Vue from 'vue';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Vue.extend({
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		chart: {
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		type: {
 | 
					 | 
				
			||||||
			type: String,
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	data() {
 | 
					 | 
				
			||||||
		return {
 | 
					 | 
				
			||||||
			viewBoxX: 365,
 | 
					 | 
				
			||||||
			viewBoxY: 70,
 | 
					 | 
				
			||||||
			points: null
 | 
					 | 
				
			||||||
		};
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	created() {
 | 
					 | 
				
			||||||
		const peak = Math.max.apply(null, this.chart.map(d => this.type == 'local' ? d.users.local.diff : d.users.remote.diff));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (peak != 0) {
 | 
					 | 
				
			||||||
			const data = this.chart.slice().reverse().map(x => ({
 | 
					 | 
				
			||||||
				count: this.type == 'local' ? x.users.local.diff : x.users.remote.diff
 | 
					 | 
				
			||||||
			}));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			this.points = data.map((d, i) => `${i},${(1 - (d.count / peak)) * this.viewBoxY}`).join(' ');
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="stylus" scoped>
 | 
					 | 
				
			||||||
svg
 | 
					 | 
				
			||||||
	display block
 | 
					 | 
				
			||||||
	padding 10px
 | 
					 | 
				
			||||||
	width 100%
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,34 +0,0 @@
 | 
				
			||||||
<template>
 | 
					 | 
				
			||||||
<div class="card">
 | 
					 | 
				
			||||||
	<header>%i18n:@title%</header>
 | 
					 | 
				
			||||||
	<div class="card">
 | 
					 | 
				
			||||||
		<header>%i18n:@local%</header>
 | 
					 | 
				
			||||||
		<x-chart v-if="chart" :chart="chart" type="local"/>
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
	<div class="card">
 | 
					 | 
				
			||||||
		<header>%i18n:@remote%</header>
 | 
					 | 
				
			||||||
		<x-chart v-if="chart" :chart="chart" type="remote"/>
 | 
					 | 
				
			||||||
	</div>
 | 
					 | 
				
			||||||
</div>
 | 
					 | 
				
			||||||
</template>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<script lang="ts">
 | 
					 | 
				
			||||||
import Vue from "vue";
 | 
					 | 
				
			||||||
import XChart from "./admin.users-chart.chart.vue";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default Vue.extend({
 | 
					 | 
				
			||||||
	components: {
 | 
					 | 
				
			||||||
		XChart
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	props: {
 | 
					 | 
				
			||||||
		chart: {
 | 
					 | 
				
			||||||
			required: true
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
</script>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
<style lang="stylus" scoped>
 | 
					 | 
				
			||||||
@import '~const.styl'
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
</style>
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
<template>
 | 
					<template>
 | 
				
			||||||
<div class="card">
 | 
					<div class="mk-admin-card">
 | 
				
			||||||
	<header>%i18n:@verify-user%</header>
 | 
						<header>%i18n:@verify-user%</header>
 | 
				
			||||||
	<input v-model="username" type="text" class="ui"/>
 | 
						<input v-model="username" type="text" class="ui"/>
 | 
				
			||||||
	<button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button>
 | 
						<button class="ui" @click="verifyUser" :disabled="verifying">%i18n:@verify%</button>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,9 +11,7 @@
 | 
				
			||||||
	<main>
 | 
						<main>
 | 
				
			||||||
		<div v-show="page == 'dashboard'">
 | 
							<div v-show="page == 'dashboard'">
 | 
				
			||||||
			<x-dashboard/>
 | 
								<x-dashboard/>
 | 
				
			||||||
			<x-users-chart :chart="chart"/>
 | 
								<x-charts/>
 | 
				
			||||||
			<x-notes-chart :chart="chart"/>
 | 
					 | 
				
			||||||
			<x-drive-chart :chart="chart"/>
 | 
					 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
		<div v-if="page == 'users'">
 | 
							<div v-if="page == 'users'">
 | 
				
			||||||
			<x-suspend-user/>
 | 
								<x-suspend-user/>
 | 
				
			||||||
| 
						 | 
					@ -34,9 +32,7 @@ import XSuspendUser from "./admin.suspend-user.vue";
 | 
				
			||||||
import XUnsuspendUser from "./admin.unsuspend-user.vue";
 | 
					import XUnsuspendUser from "./admin.unsuspend-user.vue";
 | 
				
			||||||
import XVerifyUser from "./admin.verify-user.vue";
 | 
					import XVerifyUser from "./admin.verify-user.vue";
 | 
				
			||||||
import XUnverifyUser from "./admin.unverify-user.vue";
 | 
					import XUnverifyUser from "./admin.unverify-user.vue";
 | 
				
			||||||
import XUsersChart from "./admin.users-chart.vue";
 | 
					import XCharts from "../../components/charts.vue";
 | 
				
			||||||
import XNotesChart from "./admin.notes-chart.vue";
 | 
					 | 
				
			||||||
import XDriveChart from "./admin.drive-chart.vue";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default Vue.extend({
 | 
					export default Vue.extend({
 | 
				
			||||||
	components: {
 | 
						components: {
 | 
				
			||||||
| 
						 | 
					@ -45,21 +41,13 @@ export default Vue.extend({
 | 
				
			||||||
		XUnsuspendUser,
 | 
							XUnsuspendUser,
 | 
				
			||||||
		XVerifyUser,
 | 
							XVerifyUser,
 | 
				
			||||||
		XUnverifyUser,
 | 
							XUnverifyUser,
 | 
				
			||||||
		XUsersChart,
 | 
							XCharts
 | 
				
			||||||
		XNotesChart,
 | 
					 | 
				
			||||||
		XDriveChart
 | 
					 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	data() {
 | 
						data() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			page: 'dashboard',
 | 
								page: 'dashboard'
 | 
				
			||||||
			chart: null
 | 
					 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
	created() {
 | 
					 | 
				
			||||||
		(this as any).api('admin/chart').then(chart => {
 | 
					 | 
				
			||||||
			this.chart = chart;
 | 
					 | 
				
			||||||
		});
 | 
					 | 
				
			||||||
	},
 | 
					 | 
				
			||||||
	methods: {
 | 
						methods: {
 | 
				
			||||||
		nav(page: string) {
 | 
							nav(page: string) {
 | 
				
			||||||
			this.page = page;
 | 
								this.page = page;
 | 
				
			||||||
| 
						 | 
					@ -115,7 +103,7 @@ export default Vue.extend({
 | 
				
			||||||
			> div
 | 
								> div
 | 
				
			||||||
				max-width 800px
 | 
									max-width 800px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.card
 | 
					.mk-admin-card
 | 
				
			||||||
	padding 32px
 | 
						padding 32px
 | 
				
			||||||
	background #fff
 | 
						background #fff
 | 
				
			||||||
	box-shadow 0 2px 8px rgba(#000, 0.1)
 | 
						box-shadow 0 2px 8px rgba(#000, 0.1)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,7 +32,7 @@
 | 
				
			||||||
						<mk-media-list :media-list="p.media"/>
 | 
											<mk-media-list :media-list="p.media"/>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 | 
										<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 | 
				
			||||||
					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
										<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
				
			||||||
					<div class="renote" v-if="p.renote">
 | 
										<div class="renote" v-if="p.renote">
 | 
				
			||||||
						<mk-note-preview :note="p.renote" :mini="true"/>
 | 
											<mk-note-preview :note="p.renote" :mini="true"/>
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ import Vue from 'vue';
 | 
				
			||||||
export default Vue.extend({
 | 
					export default Vue.extend({
 | 
				
			||||||
	data() {
 | 
						data() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			name: (this as any).os.instanceName,
 | 
								name: null,
 | 
				
			||||||
			posted: false,
 | 
								posted: false,
 | 
				
			||||||
			text: new URLSearchParams(location.search).get('text')
 | 
								text: new URLSearchParams(location.search).get('text')
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,11 @@ export default Vue.extend({
 | 
				
			||||||
		close() {
 | 
							close() {
 | 
				
			||||||
			window.close();
 | 
								window.close();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						mounted() {
 | 
				
			||||||
 | 
							(this as any).os.getMeta().then(meta => {
 | 
				
			||||||
 | 
								this.name = meta.name;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										64
									
								
								src/client/app/desktop/views/pages/stats/stats.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										64
									
								
								src/client/app/desktop/views/pages/stats/stats.vue
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,64 @@
 | 
				
			||||||
 | 
					<template>
 | 
				
			||||||
 | 
					<div class="tcrwdhwpuxrwmcttxjcsehgpagpstqey">
 | 
				
			||||||
 | 
						<div v-if="stats" class="stats">
 | 
				
			||||||
 | 
							<div><b>%fa:user% {{ stats.originalUsersCount | number }}</b><span>%i18n:@original-users%</span></div>
 | 
				
			||||||
 | 
							<div><span>%fa:user% {{ stats.usersCount | number }}</span><span>%i18n:@all-users%</span></div>
 | 
				
			||||||
 | 
							<div><b>%fa:pencil-alt% {{ stats.originalNotesCount | number }}</b><span>%i18n:@original-notes%</span></div>
 | 
				
			||||||
 | 
							<div><span>%fa:pencil-alt% {{ stats.notesCount | number }}</span><span>%i18n:@all-notes%</span></div>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						<div>
 | 
				
			||||||
 | 
							<x-charts/>
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
					import Vue from "vue";
 | 
				
			||||||
 | 
					import XCharts from "../../components/charts.vue";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Vue.extend({
 | 
				
			||||||
 | 
						components: {
 | 
				
			||||||
 | 
							XCharts
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						data() {
 | 
				
			||||||
 | 
							return {
 | 
				
			||||||
 | 
								stats: null
 | 
				
			||||||
 | 
							};
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						created() {
 | 
				
			||||||
 | 
							(this as any).api('stats').then(stats => {
 | 
				
			||||||
 | 
								this.stats = stats;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
					});
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<style lang="stylus">
 | 
				
			||||||
 | 
					@import '~const.styl'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.tcrwdhwpuxrwmcttxjcsehgpagpstqey
 | 
				
			||||||
 | 
						width 100%
 | 
				
			||||||
 | 
						padding 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> .stats
 | 
				
			||||||
 | 
							display flex
 | 
				
			||||||
 | 
							justify-content center
 | 
				
			||||||
 | 
							margin-bottom 16px
 | 
				
			||||||
 | 
							padding 32px
 | 
				
			||||||
 | 
							background #fff
 | 
				
			||||||
 | 
							box-shadow 0 2px 8px rgba(#000, 0.1)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							> div
 | 
				
			||||||
 | 
								flex 1
 | 
				
			||||||
 | 
								text-align center
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> *:first-child
 | 
				
			||||||
 | 
									display block
 | 
				
			||||||
 | 
									color $theme-color
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								> *:last-child
 | 
				
			||||||
 | 
									font-size 70%
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						> div
 | 
				
			||||||
 | 
							max-width 850px
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					@ -40,10 +40,12 @@ export default Vue.extend({
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="stylus" scoped>
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					root(isDark)
 | 
				
			||||||
.friends
 | 
					.friends
 | 
				
			||||||
	background #fff
 | 
						background isDark ? #282C37 : #fff
 | 
				
			||||||
	border solid 1px rgba(#000, 0.075)
 | 
						border solid 1px rgba(#000, 0.075)
 | 
				
			||||||
	border-radius 6px
 | 
						border-radius 6px
 | 
				
			||||||
 | 
						overflow hidden
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	> .title
 | 
						> .title
 | 
				
			||||||
		z-index 1
 | 
							z-index 1
 | 
				
			||||||
| 
						 | 
					@ -52,7 +54,8 @@ export default Vue.extend({
 | 
				
			||||||
		line-height 42px
 | 
							line-height 42px
 | 
				
			||||||
		font-size 0.9em
 | 
							font-size 0.9em
 | 
				
			||||||
		font-weight bold
 | 
							font-weight bold
 | 
				
			||||||
		color #888
 | 
							background isDark ? #313543 : inherit
 | 
				
			||||||
 | 
							color isDark ? #e3e5e8 : #888
 | 
				
			||||||
		box-shadow 0 1px rgba(#000, 0.07)
 | 
							box-shadow 0 1px rgba(#000, 0.07)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		> i
 | 
							> i
 | 
				
			||||||
| 
						 | 
					@ -70,7 +73,7 @@ export default Vue.extend({
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	> .user
 | 
						> .user
 | 
				
			||||||
		padding 16px
 | 
							padding 16px
 | 
				
			||||||
		border-bottom solid 1px #eee
 | 
							border-bottom solid 1px isDark ? #21242f : #eee
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		&:last-child
 | 
							&:last-child
 | 
				
			||||||
			border-bottom none
 | 
								border-bottom none
 | 
				
			||||||
| 
						 | 
					@ -96,18 +99,24 @@ export default Vue.extend({
 | 
				
			||||||
				margin 0
 | 
									margin 0
 | 
				
			||||||
				font-size 16px
 | 
									font-size 16px
 | 
				
			||||||
				line-height 24px
 | 
									line-height 24px
 | 
				
			||||||
				color #555
 | 
									color isDark ? #ccc : #555
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			> .username
 | 
								> .username
 | 
				
			||||||
				display block
 | 
									display block
 | 
				
			||||||
				margin 0
 | 
									margin 0
 | 
				
			||||||
				font-size 15px
 | 
									font-size 15px
 | 
				
			||||||
				line-height 16px
 | 
									line-height 16px
 | 
				
			||||||
				color #ccc
 | 
									color isDark ? #555 : #ccc
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		> .mk-follow-button
 | 
							> .mk-follow-button
 | 
				
			||||||
			position absolute
 | 
								position absolute
 | 
				
			||||||
			top 16px
 | 
								top 16px
 | 
				
			||||||
			right 16px
 | 
								right 16px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.friends[data-darkmode]
 | 
				
			||||||
 | 
						root(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.friends:not([data-darkmode])
 | 
				
			||||||
 | 
						root(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -39,10 +39,12 @@ export default Vue.extend({
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<style lang="stylus" scoped>
 | 
					<style lang="stylus" scoped>
 | 
				
			||||||
 | 
					root(isDark)
 | 
				
			||||||
.photos
 | 
					.photos
 | 
				
			||||||
	background #fff
 | 
						background isDark ? #282C37 : #fff
 | 
				
			||||||
	border solid 1px rgba(#000, 0.075)
 | 
						border solid 1px rgba(#000, 0.075)
 | 
				
			||||||
	border-radius 6px
 | 
						border-radius 6px
 | 
				
			||||||
 | 
						overflow hidden
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	> .title
 | 
						> .title
 | 
				
			||||||
		z-index 1
 | 
							z-index 1
 | 
				
			||||||
| 
						 | 
					@ -51,7 +53,8 @@ export default Vue.extend({
 | 
				
			||||||
		line-height 42px
 | 
							line-height 42px
 | 
				
			||||||
		font-size 0.9em
 | 
							font-size 0.9em
 | 
				
			||||||
		font-weight bold
 | 
							font-weight bold
 | 
				
			||||||
		color #888
 | 
							background: isDark ? #313543 : inherit
 | 
				
			||||||
 | 
							color isDark ? #e3e5e8 : #888
 | 
				
			||||||
		box-shadow 0 1px rgba(#000, 0.07)
 | 
							box-shadow 0 1px rgba(#000, 0.07)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		> i
 | 
							> i
 | 
				
			||||||
| 
						 | 
					@ -85,4 +88,10 @@ export default Vue.extend({
 | 
				
			||||||
		> i
 | 
							> i
 | 
				
			||||||
			margin-right 4px
 | 
								margin-right 4px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.photos[data-darkmode]
 | 
				
			||||||
 | 
						root(true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.photos:not([data-darkmode])
 | 
				
			||||||
 | 
						root(false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</style>
 | 
					</style>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -138,7 +138,7 @@ root(isDark)
 | 
				
			||||||
				padding 16px
 | 
									padding 16px
 | 
				
			||||||
				font-size 12px
 | 
									font-size 12px
 | 
				
			||||||
				color #aaa
 | 
									color #aaa
 | 
				
			||||||
				background #fff
 | 
									background isDark ? #21242f : #fff
 | 
				
			||||||
				border solid 1px rgba(#000, 0.075)
 | 
									border solid 1px rgba(#000, 0.075)
 | 
				
			||||||
				border-radius 6px
 | 
									border-radius 6px
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,8 +19,8 @@ import { version, codename, lang } from './config';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
let elementLocale;
 | 
					let elementLocale;
 | 
				
			||||||
switch (lang) {
 | 
					switch (lang) {
 | 
				
			||||||
	case 'ja': elementLocale = ElementLocaleJa; break;
 | 
						case 'ja-JP': elementLocale = ElementLocaleJa; break;
 | 
				
			||||||
	case 'en': elementLocale = ElementLocaleEn; break;
 | 
						case 'en-US': elementLocale = ElementLocaleEn; break;
 | 
				
			||||||
	default: elementLocale = ElementLocaleEn; break;
 | 
						default: elementLocale = ElementLocaleEn; break;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,7 +45,7 @@
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
			<mk-poll v-if="p.poll" :note="p"/>
 | 
								<mk-poll v-if="p.poll" :note="p"/>
 | 
				
			||||||
			<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
 | 
								<mk-url-preview v-for="url in urls" :url="url" :key="url" :detail="true"/>
 | 
				
			||||||
			<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
								<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
				
			||||||
			<div class="map" v-if="p.geo" ref="map"></div>
 | 
								<div class="map" v-if="p.geo" ref="map"></div>
 | 
				
			||||||
			<div class="renote" v-if="p.renote">
 | 
								<div class="renote" v-if="p.renote">
 | 
				
			||||||
				<mk-note-preview :note="p.renote"/>
 | 
									<mk-note-preview :note="p.renote"/>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,7 @@
 | 
				
			||||||
					</div>
 | 
										</div>
 | 
				
			||||||
					<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 | 
										<mk-poll v-if="p.poll" :note="p" ref="pollViewer"/>
 | 
				
			||||||
					<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 | 
										<mk-url-preview v-for="url in urls" :url="url" :key="url"/>
 | 
				
			||||||
					<a class="location" v-if="p.geo" :href="`http://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
										<a class="location" v-if="p.geo" :href="`https://maps.google.com/maps?q=${p.geo.coordinates[1]},${p.geo.coordinates[0]}`" target="_blank">%fa:map-marker-alt% %i18n:@location%</a>
 | 
				
			||||||
					<div class="map" v-if="p.geo" ref="map"></div>
 | 
										<div class="map" v-if="p.geo" ref="map"></div>
 | 
				
			||||||
					<div class="renote" v-if="p.renote">
 | 
										<div class="renote" v-if="p.renote">
 | 
				
			||||||
						<mk-note-preview :note="p.renote"/>
 | 
											<mk-note-preview :note="p.renote"/>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -30,6 +30,7 @@
 | 
				
			||||||
				<ul>
 | 
									<ul>
 | 
				
			||||||
					<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li>
 | 
										<li><a @click="search">%fa:search%%i18n:@search%%fa:angle-right%</a></li>
 | 
				
			||||||
					<li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
 | 
										<li><router-link to="/i/settings" :data-active="$route.name == 'settings'">%fa:cog%%i18n:@settings%%fa:angle-right%</router-link></li>
 | 
				
			||||||
 | 
										<li v-if="$store.getters.isSignedIn && $store.state.i.isAdmin"><router-link to="/admin">%fa:terminal%<span>%i18n:@admin%</span>%fa:angle-right%</router-link></li>
 | 
				
			||||||
					<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
 | 
										<li @click="dark"><p><template v-if="$store.state.device.darkmode">%fa:moon%</template><template v-else>%fa:R moon%</template><span>%i18n:@darkmode%</span></p></li>
 | 
				
			||||||
				</ul>
 | 
									</ul>
 | 
				
			||||||
			</div>
 | 
								</div>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -41,6 +41,12 @@
 | 
				
			||||||
				<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
 | 
									<ui-switch v-model="lightmode">%i18n:@i-am-under-limited-internet%</ui-switch>
 | 
				
			||||||
			</ui-card>
 | 
								</ui-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<ui-card>
 | 
				
			||||||
 | 
									<div slot="title">%fa:volume-up% %i18n:@sound%</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
									<ui-switch v-model="enableSounds">%i18n:@enable-sounds%</ui-switch>
 | 
				
			||||||
 | 
								</ui-card>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			<ui-card>
 | 
								<ui-card>
 | 
				
			||||||
				<div slot="title">%fa:language% %i18n:@lang%</div>
 | 
									<div slot="title">%fa:language% %i18n:@lang%</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -142,6 +148,11 @@ export default Vue.extend({
 | 
				
			||||||
			get() { return this.$store.state.device.lang; },
 | 
								get() { return this.$store.state.device.lang; },
 | 
				
			||||||
			set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
 | 
								set(value) { this.$store.commit('device/set', { key: 'lang', value }); }
 | 
				
			||||||
		},
 | 
							},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							enableSounds: {
 | 
				
			||||||
 | 
								get() { return this.$store.state.device.enableSounds; },
 | 
				
			||||||
 | 
								set(value) { this.$store.commit('device/set', { key: 'enableSounds', value }); }
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	mounted() {
 | 
						mounted() {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ import Vue from 'vue';
 | 
				
			||||||
export default Vue.extend({
 | 
					export default Vue.extend({
 | 
				
			||||||
	data() {
 | 
						data() {
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			name: (this as any).os.instanceName,
 | 
								name: null,
 | 
				
			||||||
			posted: false,
 | 
								posted: false,
 | 
				
			||||||
			text: new URLSearchParams(location.search).get('text')
 | 
								text: new URLSearchParams(location.search).get('text')
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
| 
						 | 
					@ -25,6 +25,11 @@ export default Vue.extend({
 | 
				
			||||||
		close() {
 | 
							close() {
 | 
				
			||||||
			window.close();
 | 
								window.close();
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						mounted() {
 | 
				
			||||||
 | 
							(this as any).os.getMeta().then(meta => {
 | 
				
			||||||
 | 
								this.name = meta.name;
 | 
				
			||||||
 | 
							});
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
</script>
 | 
					</script>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,7 +11,7 @@
 | 
				
			||||||
					<a class="avatar">
 | 
										<a class="avatar">
 | 
				
			||||||
						<img :src="user.avatarUrl" alt="avatar"/>
 | 
											<img :src="user.avatarUrl" alt="avatar"/>
 | 
				
			||||||
					</a>
 | 
										</a>
 | 
				
			||||||
					<mk-mute-button v-if="$store.state.i.id != user.id" :user="user"/>
 | 
										<mk-mute-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
 | 
				
			||||||
					<mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
 | 
										<mk-follow-button v-if="$store.getters.isSignedIn && $store.state.i.id != user.id" :user="user"/>
 | 
				
			||||||
				</div>
 | 
									</div>
 | 
				
			||||||
				<div class="title">
 | 
									<div class="title">
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,6 +13,7 @@ const defaultSettings = {
 | 
				
			||||||
	showMaps: true,
 | 
						showMaps: true,
 | 
				
			||||||
	showPostFormOnTopOfTl: false,
 | 
						showPostFormOnTopOfTl: false,
 | 
				
			||||||
	suggestRecentHashtags: true,
 | 
						suggestRecentHashtags: true,
 | 
				
			||||||
 | 
						showClockOnHeader: true,
 | 
				
			||||||
	circleIcons: true,
 | 
						circleIcons: true,
 | 
				
			||||||
	gradientWindowHeader: false,
 | 
						gradientWindowHeader: false,
 | 
				
			||||||
	showReplyTarget: true,
 | 
						showReplyTarget: true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -53,5 +53,5 @@ export default function load() {
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function normalizeUrl(url: string) {
 | 
					function normalizeUrl(url: string) {
 | 
				
			||||||
	return url[url.length - 1] === '/' ? url.substr(0, url.length - 1) : url;
 | 
						return url.endsWith('/') ? url.substr(0, url.length - 1) : url;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -62,6 +62,8 @@ export type Source = {
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	ghost?: string;
 | 
						ghost?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						summalyProxy?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	accesslog?: string;
 | 
						accesslog?: string;
 | 
				
			||||||
	twitter?: {
 | 
						twitter?: {
 | 
				
			||||||
		consumer_key: string;
 | 
							consumer_key: string;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ block main
 | 
				
			||||||
		span.path= endpointUrl.path
 | 
							span.path= endpointUrl.path
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if endpoint.desc
 | 
						if endpoint.desc
 | 
				
			||||||
		p#desc= endpoint.desc[lang] || endpoint.desc['ja']
 | 
							p#desc= endpoint.desc[lang] || endpoint.desc['ja-JP']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if endpoint.requireCredential
 | 
						if endpoint.requireCredential
 | 
				
			||||||
		div.ui.info: p
 | 
							div.ui.info: p
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,90 +1,90 @@
 | 
				
			||||||
name: "DriveFile"
 | 
					name: "DriveFile"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desc:
 | 
					desc:
 | 
				
			||||||
  ja: "ドライブのファイル。"
 | 
					  ja-JP: "ドライブのファイル。"
 | 
				
			||||||
  en: "A file of Drive."
 | 
					  en-US: "A file of Drive."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
props:
 | 
					props:
 | 
				
			||||||
  id:
 | 
					  id:
 | 
				
			||||||
    type: "id"
 | 
					    type: "id"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ファイルID"
 | 
					      ja-JP: "ファイルID"
 | 
				
			||||||
      en: "The ID of this file"
 | 
					      en-US: "The ID of this file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createdAt:
 | 
					  createdAt:
 | 
				
			||||||
    type: "date"
 | 
					    type: "date"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "アップロード日時"
 | 
					      ja-JP: "アップロード日時"
 | 
				
			||||||
      en: "The upload date of this file"
 | 
					      en-US: "The upload date of this file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  userId:
 | 
					  userId:
 | 
				
			||||||
    type: "id(User)"
 | 
					    type: "id(User)"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "所有者ID"
 | 
					      ja-JP: "所有者ID"
 | 
				
			||||||
      en: "The ID of the owner of this file"
 | 
					      en-US: "The ID of the owner of this file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  user:
 | 
					  user:
 | 
				
			||||||
    type: "entity(User)"
 | 
					    type: "entity(User)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "所有者"
 | 
					      ja-JP: "所有者"
 | 
				
			||||||
      en: "The owner of this file"
 | 
					      en-US: "The owner of this file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  name:
 | 
					  name:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ファイル名"
 | 
					      ja-JP: "ファイル名"
 | 
				
			||||||
      en: "The name of this file"
 | 
					      en-US: "The name of this file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  md5:
 | 
					  md5:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ファイルのMD5ハッシュ値"
 | 
					      ja-JP: "ファイルのMD5ハッシュ値"
 | 
				
			||||||
      en: "The md5 hash value of this file"
 | 
					      en-US: "The md5 hash value of this file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  type:
 | 
					  type:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ファイルの種類"
 | 
					      ja-JP: "ファイルの種類"
 | 
				
			||||||
      en: "The type of this file"
 | 
					      en-US: "The type of this file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  datasize:
 | 
					  datasize:
 | 
				
			||||||
    type: "number"
 | 
					    type: "number"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ファイルサイズ(bytes)"
 | 
					      ja-JP: "ファイルサイズ(bytes)"
 | 
				
			||||||
      en: "The size of this file (bytes)"
 | 
					      en-US: "The size of this file (bytes)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  url:
 | 
					  url:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ファイルのURL"
 | 
					      ja-JP: "ファイルのURL"
 | 
				
			||||||
      en: "The URL of this file"
 | 
					      en-US: "The URL of this file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  folderId:
 | 
					  folderId:
 | 
				
			||||||
    type: "id(DriveFolder)"
 | 
					    type: "id(DriveFolder)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "フォルダID"
 | 
					      ja-JP: "フォルダID"
 | 
				
			||||||
      en: "The ID of the folder of this file"
 | 
					      en-US: "The ID of the folder of this file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  folder:
 | 
					  folder:
 | 
				
			||||||
    type: "entity(DriveFolder)"
 | 
					    type: "entity(DriveFolder)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "フォルダ"
 | 
					      ja-JP: "フォルダ"
 | 
				
			||||||
      en: "The folder of this file"
 | 
					      en-US: "The folder of this file"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isSensitive:
 | 
					  isSensitive:
 | 
				
			||||||
    type: "boolean"
 | 
					    type: "boolean"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "このメディアが「閲覧注意」(NSFW)かどうか"
 | 
					      ja-JP: "このメディアが「閲覧注意」(NSFW)かどうか"
 | 
				
			||||||
      en: "Whether this media is NSFW"
 | 
					      en-US: "Whether this media is NSFW"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,41 +1,41 @@
 | 
				
			||||||
name: "DriveFolder"
 | 
					name: "DriveFolder"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desc:
 | 
					desc:
 | 
				
			||||||
  ja: "ドライブのフォルダを表します。"
 | 
					  ja-JP: "ドライブのフォルダを表します。"
 | 
				
			||||||
  en: "A folder of Drive."
 | 
					  en-US: "A folder of Drive."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
props:
 | 
					props:
 | 
				
			||||||
  id:
 | 
					  id:
 | 
				
			||||||
    type: "id"
 | 
					    type: "id"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "フォルダID"
 | 
					      ja-JP: "フォルダID"
 | 
				
			||||||
      en: "The ID of this folder"
 | 
					      en-US: "The ID of this folder"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createdAt:
 | 
					  createdAt:
 | 
				
			||||||
    type: "date"
 | 
					    type: "date"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "作成日時"
 | 
					      ja-JP: "作成日時"
 | 
				
			||||||
      en: "The created date of this folder"
 | 
					      en-US: "The created date of this folder"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  userId:
 | 
					  userId:
 | 
				
			||||||
    type: "id(User)"
 | 
					    type: "id(User)"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "所有者ID"
 | 
					      ja-JP: "所有者ID"
 | 
				
			||||||
      en: "The ID of the owner of this folder"
 | 
					      en-US: "The ID of the owner of this folder"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  parentId:
 | 
					  parentId:
 | 
				
			||||||
    type: "entity(DriveFolder)"
 | 
					    type: "entity(DriveFolder)"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "親フォルダのID (ルートなら null)"
 | 
					      ja-JP: "親フォルダのID (ルートなら null)"
 | 
				
			||||||
      en: "The ID of parent folder"
 | 
					      en-US: "The ID of parent folder"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  name:
 | 
					  name:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "フォルダ名"
 | 
					      ja-JP: "フォルダ名"
 | 
				
			||||||
      en: "The name of this folder"
 | 
					      en-US: "The name of this folder"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,190 +1,190 @@
 | 
				
			||||||
name: "Note"
 | 
					name: "Note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desc:
 | 
					desc:
 | 
				
			||||||
  ja: "投稿。"
 | 
					  ja-JP: "投稿。"
 | 
				
			||||||
  en: "A note."
 | 
					  en-US: "A note."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
props:
 | 
					props:
 | 
				
			||||||
  id:
 | 
					  id:
 | 
				
			||||||
    type: "id"
 | 
					    type: "id"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "投稿ID"
 | 
					      ja-JP: "投稿ID"
 | 
				
			||||||
      en: "The ID of this note"
 | 
					      en-US: "The ID of this note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createdAt:
 | 
					  createdAt:
 | 
				
			||||||
    type: "date"
 | 
					    type: "date"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "投稿日時"
 | 
					      ja-JP: "投稿日時"
 | 
				
			||||||
      en: "The posted date of this note"
 | 
					      en-US: "The posted date of this note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  viaMobile:
 | 
					  viaMobile:
 | 
				
			||||||
    type: "boolean"
 | 
					    type: "boolean"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "モバイル端末から投稿したか否か(自己申告であることに留意)"
 | 
					      ja-JP: "モバイル端末から投稿したか否か(自己申告であることに留意)"
 | 
				
			||||||
      en: "Whether this note sent via a mobile device"
 | 
					      en-US: "Whether this note sent via a mobile device"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  text:
 | 
					  text:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "投稿の本文"
 | 
					      ja-JP: "投稿の本文"
 | 
				
			||||||
      en: "The text of this note"
 | 
					      en-US: "The text of this note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  mediaIds:
 | 
					  mediaIds:
 | 
				
			||||||
    type: "id(DriveFile)[]"
 | 
					    type: "id(DriveFile)[]"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "添付されているメディアのID (なければレスポンスでは空配列)"
 | 
					      ja-JP: "添付されているメディアのID (なければレスポンスでは空配列)"
 | 
				
			||||||
      en: "The IDs of the attached media (empty array for response if no media is attached)"
 | 
					      en-US: "The IDs of the attached media (empty array for response if no media is attached)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  media:
 | 
					  media:
 | 
				
			||||||
    type: "entity(DriveFile)[]"
 | 
					    type: "entity(DriveFile)[]"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "添付されているメディア"
 | 
					      ja-JP: "添付されているメディア"
 | 
				
			||||||
      en: "The attached media"
 | 
					      en-US: "The attached media"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  userId:
 | 
					  userId:
 | 
				
			||||||
    type: "id(User)"
 | 
					    type: "id(User)"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "投稿者ID"
 | 
					      ja-JP: "投稿者ID"
 | 
				
			||||||
      en: "The ID of author of this note"
 | 
					      en-US: "The ID of author of this note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  user:
 | 
					  user:
 | 
				
			||||||
    type: "entity(User)"
 | 
					    type: "entity(User)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "投稿者"
 | 
					      ja-JP: "投稿者"
 | 
				
			||||||
      en: "The author of this note"
 | 
					      en-US: "The author of this note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  myReaction:
 | 
					  myReaction:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
 | 
					      ja-JP: "この投稿に対する自分の<a href='/docs/api/reactions'>リアクション</a>"
 | 
				
			||||||
      en: "The your <a href='/docs/api/reactions'>reaction</a> of this note"
 | 
					      en-US: "The your <a href='/docs/api/reactions'>reaction</a> of this note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  reactionCounts:
 | 
					  reactionCounts:
 | 
				
			||||||
    type: "object"
 | 
					    type: "object"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
 | 
					      ja-JP: "<a href='/docs/api/reactions'>リアクション</a>をキーとし、この投稿に対するそのリアクションの数を値としたオブジェクト"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  replyId:
 | 
					  replyId:
 | 
				
			||||||
    type: "id(Note)"
 | 
					    type: "id(Note)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "返信した投稿のID"
 | 
					      ja-JP: "返信した投稿のID"
 | 
				
			||||||
      en: "The ID of the replyed note"
 | 
					      en-US: "The ID of the replyed note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  reply:
 | 
					  reply:
 | 
				
			||||||
    type: "entity(Note)"
 | 
					    type: "entity(Note)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "返信した投稿"
 | 
					      ja-JP: "返信した投稿"
 | 
				
			||||||
      en: "The replyed note"
 | 
					      en-US: "The replyed note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  renoteId:
 | 
					  renoteId:
 | 
				
			||||||
    type: "id(Note)"
 | 
					    type: "id(Note)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "引用した投稿のID"
 | 
					      ja-JP: "引用した投稿のID"
 | 
				
			||||||
      en: "The ID of the quoted note"
 | 
					      en-US: "The ID of the quoted note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  renote:
 | 
					  renote:
 | 
				
			||||||
    type: "entity(Note)"
 | 
					    type: "entity(Note)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "引用した投稿"
 | 
					      ja-JP: "引用した投稿"
 | 
				
			||||||
      en: "The quoted note"
 | 
					      en-US: "The quoted note"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  poll:
 | 
					  poll:
 | 
				
			||||||
    type: "object"
 | 
					    type: "object"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "投票"
 | 
					      ja-JP: "投票"
 | 
				
			||||||
      en: "The poll"
 | 
					      en-US: "The poll"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    props:
 | 
					    props:
 | 
				
			||||||
      choices:
 | 
					      choices:
 | 
				
			||||||
        type: "object[]"
 | 
					        type: "object[]"
 | 
				
			||||||
        optional: false
 | 
					        optional: false
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "投票の選択肢"
 | 
					          ja-JP: "投票の選択肢"
 | 
				
			||||||
          en: "The choices of this poll"
 | 
					          en-US: "The choices of this poll"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        props:
 | 
					        props:
 | 
				
			||||||
          id:
 | 
					          id:
 | 
				
			||||||
            type: "number"
 | 
					            type: "number"
 | 
				
			||||||
            optional: false
 | 
					            optional: false
 | 
				
			||||||
            desc:
 | 
					            desc:
 | 
				
			||||||
              ja: "選択肢ID"
 | 
					              ja-JP: "選択肢ID"
 | 
				
			||||||
              en: "The ID of this choice"
 | 
					              en-US: "The ID of this choice"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          isVoted:
 | 
					          isVoted:
 | 
				
			||||||
            type: "boolean"
 | 
					            type: "boolean"
 | 
				
			||||||
            optional: true
 | 
					            optional: true
 | 
				
			||||||
            desc:
 | 
					            desc:
 | 
				
			||||||
              ja: "自分がこの選択肢に投票したかどうか"
 | 
					              ja-JP: "自分がこの選択肢に投票したかどうか"
 | 
				
			||||||
              en: "Whether you voted to this choice"
 | 
					              en-US: "Whether you voted to this choice"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          text:
 | 
					          text:
 | 
				
			||||||
            type: "string"
 | 
					            type: "string"
 | 
				
			||||||
            optional: false
 | 
					            optional: false
 | 
				
			||||||
            desc:
 | 
					            desc:
 | 
				
			||||||
              ja: "選択肢本文"
 | 
					              ja-JP: "選択肢本文"
 | 
				
			||||||
              en: "The text of this choice"
 | 
					              en-US: "The text of this choice"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          votes:
 | 
					          votes:
 | 
				
			||||||
            type: "number"
 | 
					            type: "number"
 | 
				
			||||||
            optional: false
 | 
					            optional: false
 | 
				
			||||||
            desc:
 | 
					            desc:
 | 
				
			||||||
              ja: "この選択肢に投票された数"
 | 
					              ja-JP: "この選択肢に投票された数"
 | 
				
			||||||
              en: "The number voted for this choice"
 | 
					              en-US: "The number voted for this choice"
 | 
				
			||||||
  geo:
 | 
					  geo:
 | 
				
			||||||
    type: "object"
 | 
					    type: "object"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "位置情報"
 | 
					      ja-JP: "位置情報"
 | 
				
			||||||
      en: "Geo location"
 | 
					      en-US: "Geo location"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    props:
 | 
					    props:
 | 
				
			||||||
      coordinates:
 | 
					      coordinates:
 | 
				
			||||||
        type: "number[]"
 | 
					        type: "number[]"
 | 
				
			||||||
        optional: false
 | 
					        optional: false
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。"
 | 
					          ja-JP: "座標。最初に経度:-180〜180で表す。最後に緯度:-90〜90で表す。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      altitude:
 | 
					      altitude:
 | 
				
			||||||
        type: "number"
 | 
					        type: "number"
 | 
				
			||||||
        optional: false
 | 
					        optional: false
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "高度。メートル単位で表す。"
 | 
					          ja-JP: "高度。メートル単位で表す。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      accuracy:
 | 
					      accuracy:
 | 
				
			||||||
        type: "number"
 | 
					        type: "number"
 | 
				
			||||||
        optional: false
 | 
					        optional: false
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "緯度、経度の精度。メートル単位で表す。"
 | 
					          ja-JP: "緯度、経度の精度。メートル単位で表す。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      altitudeAccuracy:
 | 
					      altitudeAccuracy:
 | 
				
			||||||
        type: "number"
 | 
					        type: "number"
 | 
				
			||||||
        optional: false
 | 
					        optional: false
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "高度の精度。メートル単位で表す。"
 | 
					          ja-JP: "高度の精度。メートル単位で表す。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      heading:
 | 
					      heading:
 | 
				
			||||||
        type: "number"
 | 
					        type: "number"
 | 
				
			||||||
        optional: false
 | 
					        optional: false
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。"
 | 
					          ja-JP: "方角。0〜360の角度で表す。0が北、90が東、180が南、270が西。"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      speed:
 | 
					      speed:
 | 
				
			||||||
        type: "number"
 | 
					        type: "number"
 | 
				
			||||||
        optional: false
 | 
					        optional: false
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "速度。メートル / 秒数で表す。"
 | 
					          ja-JP: "速度。メートル / 秒数で表す。"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,174 +1,174 @@
 | 
				
			||||||
name: "User"
 | 
					name: "User"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
desc:
 | 
					desc:
 | 
				
			||||||
  ja: "ユーザー。"
 | 
					  ja-JP: "ユーザー。"
 | 
				
			||||||
  en: "A user."
 | 
					  en-US: "A user."
 | 
				
			||||||
 | 
					
 | 
				
			||||||
props:
 | 
					props:
 | 
				
			||||||
  id:
 | 
					  id:
 | 
				
			||||||
    type: "id"
 | 
					    type: "id"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ユーザーID"
 | 
					      ja-JP: "ユーザーID"
 | 
				
			||||||
      en: "The ID of this user"
 | 
					      en-US: "The ID of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  createdAt:
 | 
					  createdAt:
 | 
				
			||||||
    type: "date"
 | 
					    type: "date"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "アカウント作成日時"
 | 
					      ja-JP: "アカウント作成日時"
 | 
				
			||||||
      en: "The registered date of this user"
 | 
					      en-US: "The registered date of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  username:
 | 
					  username:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ユーザー名"
 | 
					      ja-JP: "ユーザー名"
 | 
				
			||||||
      en: "The username of this user"
 | 
					      en-US: "The username of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  description:
 | 
					  description:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "アカウントの説明(自己紹介)"
 | 
					      ja-JP: "アカウントの説明(自己紹介)"
 | 
				
			||||||
      en: "The description of this user"
 | 
					      en-US: "The description of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  avatarId:
 | 
					  avatarId:
 | 
				
			||||||
    type: "id(DriveFile)"
 | 
					    type: "id(DriveFile)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "アバターのID"
 | 
					      ja-JP: "アバターのID"
 | 
				
			||||||
      en: "The ID of the avatar of this user"
 | 
					      en-US: "The ID of the avatar of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  avatarUrl:
 | 
					  avatarUrl:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "アバターのURL"
 | 
					      ja-JP: "アバターのURL"
 | 
				
			||||||
      en: "The URL of the avatar of this user"
 | 
					      en-US: "The URL of the avatar of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bannerId:
 | 
					  bannerId:
 | 
				
			||||||
    type: "id(DriveFile)"
 | 
					    type: "id(DriveFile)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "バナーのID"
 | 
					      ja-JP: "バナーのID"
 | 
				
			||||||
      en: "The ID of the banner of this user"
 | 
					      en-US: "The ID of the banner of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  bannerUrl:
 | 
					  bannerUrl:
 | 
				
			||||||
    type: "string"
 | 
					    type: "string"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "バナーのURL"
 | 
					      ja-JP: "バナーのURL"
 | 
				
			||||||
      en: "The URL of the banner of this user"
 | 
					      en-US: "The URL of the banner of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  followersCount:
 | 
					  followersCount:
 | 
				
			||||||
    type: "number"
 | 
					    type: "number"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "フォロワーの数"
 | 
					      ja-JP: "フォロワーの数"
 | 
				
			||||||
      en: "The number of the followers for this user"
 | 
					      en-US: "The number of the followers for this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  followingCount:
 | 
					  followingCount:
 | 
				
			||||||
    type: "number"
 | 
					    type: "number"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "フォローしているユーザーの数"
 | 
					      ja-JP: "フォローしているユーザーの数"
 | 
				
			||||||
      en: "The number of the following users for this user"
 | 
					      en-US: "The number of the following users for this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isFollowing:
 | 
					  isFollowing:
 | 
				
			||||||
    type: "boolean"
 | 
					    type: "boolean"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "自分がこのユーザーをフォローしているか"
 | 
					      ja-JP: "自分がこのユーザーをフォローしているか"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isFollowed:
 | 
					  isFollowed:
 | 
				
			||||||
    type: "boolean"
 | 
					    type: "boolean"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "自分がこのユーザーにフォローされているか"
 | 
					      ja-JP: "自分がこのユーザーにフォローされているか"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isMuted:
 | 
					  isMuted:
 | 
				
			||||||
    type: "boolean"
 | 
					    type: "boolean"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "自分がこのユーザーをミュートしているか"
 | 
					      ja-JP: "自分がこのユーザーをミュートしているか"
 | 
				
			||||||
      en: "Whether you muted this user"
 | 
					      en-US: "Whether you muted this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  notesCount:
 | 
					  notesCount:
 | 
				
			||||||
    type: "number"
 | 
					    type: "number"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "投稿の数"
 | 
					      ja-JP: "投稿の数"
 | 
				
			||||||
      en: "The number of the notes of this user"
 | 
					      en-US: "The number of the notes of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pinnedNote:
 | 
					  pinnedNote:
 | 
				
			||||||
    type: "entity(Note)"
 | 
					    type: "entity(Note)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ピン留めされた投稿"
 | 
					      ja-JP: "ピン留めされた投稿"
 | 
				
			||||||
      en: "The pinned note of this user"
 | 
					      en-US: "The pinned note of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  pinnedNoteId:
 | 
					  pinnedNoteId:
 | 
				
			||||||
    type: "id(Note)"
 | 
					    type: "id(Note)"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ピン留めされた投稿のID"
 | 
					      ja-JP: "ピン留めされた投稿のID"
 | 
				
			||||||
      en: "The ID of the pinned note of this user"
 | 
					      en-US: "The ID of the pinned note of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  host:
 | 
					  host:
 | 
				
			||||||
    type: "string | null"
 | 
					    type: "string | null"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "ホスト (例: example.com:3000)"
 | 
					      ja-JP: "ホスト (例: example.com:3000)"
 | 
				
			||||||
      en: "Host (e.g. example.com:3000)"
 | 
					      en-US: "Host (e.g. example.com:3000)"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  twitter:
 | 
					  twitter:
 | 
				
			||||||
    type: "object"
 | 
					    type: "object"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "連携されているTwitterアカウント情報"
 | 
					      ja-JP: "連携されているTwitterアカウント情報"
 | 
				
			||||||
      en: "The info of the connected twitter account of this user"
 | 
					      en-US: "The info of the connected twitter account of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    props:
 | 
					    props:
 | 
				
			||||||
      userId:
 | 
					      userId:
 | 
				
			||||||
        type: "string"
 | 
					        type: "string"
 | 
				
			||||||
        optional: false
 | 
					        optional: false
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "ユーザーID"
 | 
					          ja-JP: "ユーザーID"
 | 
				
			||||||
          en: "The user ID"
 | 
					          en-US: "The user ID"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      screenName:
 | 
					      screenName:
 | 
				
			||||||
        type: "string"
 | 
					        type: "string"
 | 
				
			||||||
        optional: false
 | 
					        optional: false
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "ユーザー名"
 | 
					          ja-JP: "ユーザー名"
 | 
				
			||||||
          en: "The screen name of this user"
 | 
					          en-US: "The screen name of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  isBot:
 | 
					  isBot:
 | 
				
			||||||
    type: "boolean"
 | 
					    type: "boolean"
 | 
				
			||||||
    optional: true
 | 
					    optional: true
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "botか否か(自己申告であることに留意)"
 | 
					      ja-JP: "botか否か(自己申告であることに留意)"
 | 
				
			||||||
      en: "Whether is bot or not"
 | 
					      en-US: "Whether is bot or not"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  profile:
 | 
					  profile:
 | 
				
			||||||
    type: "object"
 | 
					    type: "object"
 | 
				
			||||||
    optional: false
 | 
					    optional: false
 | 
				
			||||||
    desc:
 | 
					    desc:
 | 
				
			||||||
      ja: "プロフィール"
 | 
					      ja-JP: "プロフィール"
 | 
				
			||||||
      en: "The profile of this user"
 | 
					      en-US: "The profile of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    props:
 | 
					    props:
 | 
				
			||||||
      location:
 | 
					      location:
 | 
				
			||||||
        type: "string"
 | 
					        type: "string"
 | 
				
			||||||
        optional: true
 | 
					        optional: true
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "場所"
 | 
					          ja-JP: "場所"
 | 
				
			||||||
          en: "The location of this user"
 | 
					          en-US: "The location of this user"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      birthday:
 | 
					      birthday:
 | 
				
			||||||
        type: "string"
 | 
					        type: "string"
 | 
				
			||||||
        optional: true
 | 
					        optional: true
 | 
				
			||||||
        desc:
 | 
					        desc:
 | 
				
			||||||
          ja: "誕生日 (YYYY-MM-DD)"
 | 
					          ja-JP: "誕生日 (YYYY-MM-DD)"
 | 
				
			||||||
          en: "The birthday of this user (YYYY-MM-DD)"
 | 
					          en-US: "The birthday of this user (YYYY-MM-DD)"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ block meta
 | 
				
			||||||
block main
 | 
					block main
 | 
				
			||||||
	h1= name
 | 
						h1= name
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	p#desc= desc[lang] || desc['ja']
 | 
						p#desc= desc[lang] || desc['ja-JP']
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	section
 | 
						section
 | 
				
			||||||
		h2= i18n('docs.api.entities.properties')
 | 
							h2= i18n('docs.api.entities.properties')
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -31,4 +31,4 @@ mixin propTable(props)
 | 
				
			||||||
					td.name= prop.name
 | 
										td.name= prop.name
 | 
				
			||||||
					td.type
 | 
										td.type
 | 
				
			||||||
						+type(prop)
 | 
											+type(prop)
 | 
				
			||||||
					td.desc!= prop.desc ? prop.desc[lang] || prop.desc['ja'] : null
 | 
										td.desc!= prop.desc ? prop.desc[lang] || prop.desc['ja-JP'] : null
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -16,7 +16,7 @@ html(lang= lang)
 | 
				
			||||||
		nav
 | 
							nav
 | 
				
			||||||
			ul
 | 
								ul
 | 
				
			||||||
				each doc in docs
 | 
									each doc in docs
 | 
				
			||||||
					li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja']
 | 
										li: a(href=`/docs/${lang}/${doc.name}`)= doc.title[lang] || doc.title['ja-JP']
 | 
				
			||||||
			section
 | 
								section
 | 
				
			||||||
				h2 API
 | 
									h2 API
 | 
				
			||||||
				ul
 | 
									ul
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -197,7 +197,7 @@ const elements: Element[] = [
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (thisIsNotARegexp) return null;
 | 
							if (thisIsNotARegexp) return null;
 | 
				
			||||||
		if (regexp == '') return null;
 | 
							if (regexp == '') return null;
 | 
				
			||||||
		if (regexp[0] == ' ' && regexp[regexp.length - 1] == ' ') return null;
 | 
							if (regexp.startsWith(' ') && regexp.endsWith(' ')) return null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		return {
 | 
							return {
 | 
				
			||||||
			html: `<span class="regexp">/${escape(regexp)}/</span>`,
 | 
								html: `<span class="regexp">/${escape(regexp)}/</span>`,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ export type TextElementHashtag = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export default function(text: string, i: number) {
 | 
					export default function(text: string, i: number) {
 | 
				
			||||||
	if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null;
 | 
						if (!(/^\s#[^\s]+/.test(text) || (i == 0 && /^#[^\s]+/.test(text)))) return null;
 | 
				
			||||||
	const isHead = text[0] == '#';
 | 
						const isHead = text.startsWith('#');
 | 
				
			||||||
	const hashtag = text.match(/^\s?#[^\s]+/)[0];
 | 
						const hashtag = text.match(/^\s?#[^\s]+/)[0];
 | 
				
			||||||
	const res: any[] = !isHead ? [{
 | 
						const res: any[] = !isHead ? [{
 | 
				
			||||||
		type: 'text',
 | 
							type: 'text',
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -13,7 +13,7 @@ export type TextElementLink = {
 | 
				
			||||||
export default function(text: string) {
 | 
					export default function(text: string) {
 | 
				
			||||||
	const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/);
 | 
						const match = text.match(/^\??\[([^\[\]]+?)\]\((https?:\/\/[\w\/:%#@\$&\?!\(\)\[\]~\.=\+\-]+?)\)/);
 | 
				
			||||||
	if (!match) return null;
 | 
						if (!match) return null;
 | 
				
			||||||
	const silent = text[0] == '?';
 | 
						const silent = text.startsWith('?');
 | 
				
			||||||
	const link = match[0];
 | 
						const link = match[0];
 | 
				
			||||||
	const title = match[1];
 | 
						const title = match[1];
 | 
				
			||||||
	const url = match[2];
 | 
						const url = match[2];
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,9 +25,9 @@ export const replacement = (match: string, key: string) => {
 | 
				
			||||||
				arg == 'S' ? 'fas' :
 | 
									arg == 'S' ? 'fas' :
 | 
				
			||||||
				arg == 'B' ? 'fab' :
 | 
									arg == 'B' ? 'fab' :
 | 
				
			||||||
				'';
 | 
									'';
 | 
				
			||||||
		} else if (arg[0] == '.') {
 | 
							} else if (arg.startsWith('.')) {
 | 
				
			||||||
			classes.push('fa-' + arg.substr(1));
 | 
								classes.push('fa-' + arg.substr(1));
 | 
				
			||||||
		} else if (arg[0] == '-') {
 | 
							} else if (arg.startsWith('-')) {
 | 
				
			||||||
			transform = arg.substr(1).split('|').join(' ');
 | 
								transform = arg.substr(1).split('|').join(' ');
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			name = arg;
 | 
								name = arg;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -27,10 +27,12 @@ export default class Replacer {
 | 
				
			||||||
		let text = texts;
 | 
							let text = texts;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (path) {
 | 
							if (path) {
 | 
				
			||||||
 | 
								path = path.replace('.ts', '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			if (text.hasOwnProperty(path)) {
 | 
								if (text.hasOwnProperty(path)) {
 | 
				
			||||||
				text = text[path];
 | 
									text = text[path];
 | 
				
			||||||
			} else {
 | 
								} else {
 | 
				
			||||||
				if (this.lang === 'ja') console.warn(`path '${path}' not found`);
 | 
									if (this.lang === 'ja-JP') console.warn(`path '${path}' not found`);
 | 
				
			||||||
				return key; // Fallback
 | 
									return key; // Fallback
 | 
				
			||||||
			}
 | 
								}
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
| 
						 | 
					@ -46,10 +48,10 @@ export default class Replacer {
 | 
				
			||||||
		});
 | 
							});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		if (error) {
 | 
							if (error) {
 | 
				
			||||||
			if (this.lang === 'ja') console.warn(`key '${key}' not found in '${path}'`);
 | 
								if (this.lang === 'ja-JP') console.warn(`key '${key}' not found in '${path}'`);
 | 
				
			||||||
			return key; // Fallback
 | 
								return key; // Fallback
 | 
				
			||||||
		} else if (typeof text !== 'string') {
 | 
							} else if (typeof text !== 'string') {
 | 
				
			||||||
			if (this.lang === 'ja') console.warn(`key '${key}' is not string in '${path}'`);
 | 
								if (this.lang === 'ja-JP') console.warn(`key '${key}' is not string in '${path}'`);
 | 
				
			||||||
			return key; // Fallback
 | 
								return key; // Fallback
 | 
				
			||||||
		} else {
 | 
							} else {
 | 
				
			||||||
			return text;
 | 
								return text;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -2,40 +2,59 @@ import * as mongo from 'mongodb';
 | 
				
			||||||
import db from '../db/mongodb';
 | 
					import db from '../db/mongodb';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const Stats = db.get<IStats>('stats');
 | 
					const Stats = db.get<IStats>('stats');
 | 
				
			||||||
Stats.createIndex({ date: -1 }, { unique: true });
 | 
					Stats.dropIndex({ date: -1 }); // 後方互換性のため
 | 
				
			||||||
 | 
					Stats.createIndex({ span: -1, date: -1 }, { unique: true });
 | 
				
			||||||
export default Stats;
 | 
					export default Stats;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface IStats {
 | 
					export interface IStats {
 | 
				
			||||||
	_id: mongo.ObjectID;
 | 
						_id: mongo.ObjectID;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * 集計日時
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
	date: Date;
 | 
						date: Date;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/**
 | 
				
			||||||
 | 
						 * 集計期間
 | 
				
			||||||
 | 
						 */
 | 
				
			||||||
 | 
						span: 'day' | 'hour';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	/**
 | 
						/**
 | 
				
			||||||
	 * ユーザーに関する統計
 | 
						 * ユーザーに関する統計
 | 
				
			||||||
	 */
 | 
						 */
 | 
				
			||||||
	users: {
 | 
						users: {
 | 
				
			||||||
		local: {
 | 
							local: {
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * この日時点での、ローカルのユーザーの総計
 | 
								 * 集計期間時点での、全ユーザー数 (ローカル)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			total: number;
 | 
								total: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * ローカルのユーザー数の前日比
 | 
								 * 増加したユーザー数 (ローカル)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			diff: number;
 | 
								inc: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								 * 減少したユーザー数 (ローカル)
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
 | 
								dec: number;
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		remote: {
 | 
							remote: {
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * この日時点での、リモートのユーザーの総計
 | 
								 * 集計期間時点での、全ユーザー数 (リモート)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			total: number;
 | 
								total: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * リモートのユーザー数の前日比
 | 
								 * 増加したユーザー数 (リモート)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			diff: number;
 | 
								inc: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								 * 減少したユーザー数 (リモート)
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
 | 
								dec: number;
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -45,28 +64,33 @@ export interface IStats {
 | 
				
			||||||
	notes: {
 | 
						notes: {
 | 
				
			||||||
		local: {
 | 
							local: {
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * この日時点での、ローカルの投稿の総計
 | 
								 * 集計期間時点での、全投稿数 (ローカル)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			total: number;
 | 
								total: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * ローカルの投稿数の前日比
 | 
								 * 増加した投稿数 (ローカル)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			diff: number;
 | 
								inc: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								 * 減少した投稿数 (ローカル)
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
 | 
								dec: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			diffs: {
 | 
								diffs: {
 | 
				
			||||||
				/**
 | 
									/**
 | 
				
			||||||
				 * ローカルの通常の投稿数の前日比
 | 
									 * 通常の投稿数の差分 (ローカル)
 | 
				
			||||||
				 */
 | 
									 */
 | 
				
			||||||
				normal: number;
 | 
									normal: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				/**
 | 
									/**
 | 
				
			||||||
				 * ローカルのリプライの投稿数の前日比
 | 
									 * リプライの投稿数の差分 (ローカル)
 | 
				
			||||||
				 */
 | 
									 */
 | 
				
			||||||
				reply: number;
 | 
									reply: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				/**
 | 
									/**
 | 
				
			||||||
				 * ローカルのRenoteの投稿数の前日比
 | 
									 * Renoteの投稿数の差分 (ローカル)
 | 
				
			||||||
				 */
 | 
									 */
 | 
				
			||||||
				renote: number;
 | 
									renote: number;
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
| 
						 | 
					@ -74,28 +98,33 @@ export interface IStats {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		remote: {
 | 
							remote: {
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * この日時点での、リモートの投稿の総計
 | 
								 * 集計期間時点での、全投稿数 (リモート)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			total: number;
 | 
								total: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * リモートの投稿数の前日比
 | 
								 * 増加した投稿数 (リモート)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			diff: number;
 | 
								inc: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								 * 減少した投稿数 (リモート)
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
 | 
								dec: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			diffs: {
 | 
								diffs: {
 | 
				
			||||||
				/**
 | 
									/**
 | 
				
			||||||
				 * リモートの通常の投稿数の前日比
 | 
									 * 通常の投稿数の差分 (リモート)
 | 
				
			||||||
				 */
 | 
									 */
 | 
				
			||||||
				normal: number;
 | 
									normal: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				/**
 | 
									/**
 | 
				
			||||||
				 * リモートのリプライの投稿数の前日比
 | 
									 * リプライの投稿数の差分 (リモート)
 | 
				
			||||||
				 */
 | 
									 */
 | 
				
			||||||
				reply: number;
 | 
									reply: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
				/**
 | 
									/**
 | 
				
			||||||
				 * リモートのRenoteの投稿数の前日比
 | 
									 * Renoteの投稿数の差分 (リモート)
 | 
				
			||||||
				 */
 | 
									 */
 | 
				
			||||||
				renote: number;
 | 
									renote: number;
 | 
				
			||||||
			};
 | 
								};
 | 
				
			||||||
| 
						 | 
					@ -108,46 +137,66 @@ export interface IStats {
 | 
				
			||||||
	drive: {
 | 
						drive: {
 | 
				
			||||||
		local: {
 | 
							local: {
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * この日時点での、ローカルのドライブファイル数の総計
 | 
								 * 集計期間時点での、全ドライブファイル数 (ローカル)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			totalCount: number;
 | 
								totalCount: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * この日時点での、ローカルのドライブファイルサイズの総計
 | 
								 * 集計期間時点での、全ドライブファイルの合計サイズ (ローカル)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			totalSize: number;
 | 
								totalSize: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * ローカルのドライブファイル数の前日比
 | 
								 * 増加したドライブファイル数 (ローカル)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			diffCount: number;
 | 
								incCount: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * ローカルのドライブファイルサイズの前日比
 | 
								 * 増加したドライブ使用量 (ローカル)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			diffSize: number;
 | 
								incSize: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								 * 減少したドライブファイル数 (ローカル)
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
 | 
								decCount: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								 * 減少したドライブ使用量 (ローカル)
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
 | 
								decSize: number;
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		remote: {
 | 
							remote: {
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * この日時点での、リモートのドライブファイル数の総計
 | 
								 * 集計期間時点での、全ドライブファイル数 (リモート)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			totalCount: number;
 | 
								totalCount: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * この日時点での、リモートのドライブファイルサイズの総計
 | 
								 * 集計期間時点での、全ドライブファイルの合計サイズ (リモート)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			totalSize: number;
 | 
								totalSize: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * リモートのドライブファイル数の前日比
 | 
								 * 増加したドライブファイル数 (リモート)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			diffCount: number;
 | 
								incCount: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
			/**
 | 
								/**
 | 
				
			||||||
			 * リモートのドライブファイルサイズの前日比
 | 
								 * 増加したドライブ使用量 (リモート)
 | 
				
			||||||
			 */
 | 
								 */
 | 
				
			||||||
			diffSize: number;
 | 
								incSize: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								 * 減少したドライブファイル数 (リモート)
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
 | 
								decCount: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								/**
 | 
				
			||||||
 | 
								 * 減少したドライブ使用量 (リモート)
 | 
				
			||||||
 | 
								 */
 | 
				
			||||||
 | 
								decSize: number;
 | 
				
			||||||
		};
 | 
							};
 | 
				
			||||||
	};
 | 
						};
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -46,7 +46,7 @@ export default async (job: bq.Job, done: any): Promise<void> => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
		// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
 | 
							// アクティビティを送信してきたユーザーがまだMisskeyサーバーに登録されていなかったら登録する
 | 
				
			||||||
		if (user === null) {
 | 
							if (user === null) {
 | 
				
			||||||
			user = await resolvePerson(signature.keyId) as IRemoteUser;
 | 
								user = await resolvePerson(activity.actor) as IRemoteUser;
 | 
				
			||||||
		}
 | 
							}
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -131,5 +131,7 @@ export async function resolveNote(value: string | IObject, resolver?: Resolver):
 | 
				
			||||||
	//#endregion
 | 
						//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// リモートサーバーからフェッチしてきて登録
 | 
						// リモートサーバーからフェッチしてきて登録
 | 
				
			||||||
	return await createNote(value, resolver);
 | 
						// ここでuriの代わりに添付されてきたNote Objectが指定されていると、サーバーフェッチを経ずにノートが生成されるが
 | 
				
			||||||
 | 
						// 添付されてきたNote Objectは偽装されている可能性があるため、常にuriを指定してサーバーフェッチを行う。
 | 
				
			||||||
 | 
						return await createNote(uri, resolver);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -4,18 +4,25 @@ import * as debug from 'debug';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import config from '../../../config';
 | 
					import config from '../../../config';
 | 
				
			||||||
import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user';
 | 
					import User, { validateUsername, isValidName, IUser, IRemoteUser } from '../../../models/user';
 | 
				
			||||||
import webFinger from '../../webfinger';
 | 
					 | 
				
			||||||
import Resolver from '../resolver';
 | 
					import Resolver from '../resolver';
 | 
				
			||||||
import { resolveImage } from './image';
 | 
					import { resolveImage } from './image';
 | 
				
			||||||
import { isCollectionOrOrderedCollection, IObject, IPerson } from '../type';
 | 
					import { isCollectionOrOrderedCollection, IPerson } from '../type';
 | 
				
			||||||
import { IDriveFile } from '../../../models/drive-file';
 | 
					import { IDriveFile } from '../../../models/drive-file';
 | 
				
			||||||
import Meta from '../../../models/meta';
 | 
					import Meta from '../../../models/meta';
 | 
				
			||||||
import htmlToMFM from '../../../mfm/html-to-mfm';
 | 
					import htmlToMFM from '../../../mfm/html-to-mfm';
 | 
				
			||||||
import { updateUserStats } from '../../../services/update-chart';
 | 
					import { updateUserStats } from '../../../services/update-chart';
 | 
				
			||||||
 | 
					import { URL } from 'url';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const log = debug('misskey:activitypub');
 | 
					const log = debug('misskey:activitypub');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function validatePerson(x: any) {
 | 
					/**
 | 
				
			||||||
 | 
					 * Validate Person object
 | 
				
			||||||
 | 
					 * @param x Fetched person object
 | 
				
			||||||
 | 
					 * @param uri Fetch target URI
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					function validatePerson(x: any, uri: string) {
 | 
				
			||||||
 | 
						const expectHost = toUnicode(new URL(uri).hostname.toLowerCase());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (x == null) {
 | 
						if (x == null) {
 | 
				
			||||||
		return new Error('invalid person: object is null');
 | 
							return new Error('invalid person: object is null');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
| 
						 | 
					@ -40,6 +47,24 @@ function validatePerson(x: any) {
 | 
				
			||||||
		return new Error('invalid person: invalid name');
 | 
							return new Error('invalid person: invalid name');
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (typeof x.id !== 'string') {
 | 
				
			||||||
 | 
							return new Error('invalid person: id is not a string');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const idHost = toUnicode(new URL(x.id).hostname.toLowerCase());
 | 
				
			||||||
 | 
						if (idHost !== expectHost) {
 | 
				
			||||||
 | 
							return new Error('invalid person: id has different host');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (typeof x.publicKey.id !== 'string') {
 | 
				
			||||||
 | 
							return new Error('invalid person: publicKey.id is not a string');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const publicKeyIdHost = toUnicode(new URL(x.publicKey.id).hostname.toLowerCase());
 | 
				
			||||||
 | 
						if (publicKeyIdHost !== expectHost) {
 | 
				
			||||||
 | 
							return new Error('invalid person: publicKey.id has different host');
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return null;
 | 
						return null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -48,8 +73,8 @@ function validatePerson(x: any) {
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * Misskeyに対象のPersonが登録されていればそれを返します。
 | 
					 * Misskeyに対象のPersonが登録されていればそれを返します。
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export async function fetchPerson(value: string | IObject, resolver?: Resolver): Promise<IUser> {
 | 
					export async function fetchPerson(uri: string, resolver?: Resolver): Promise<IUser> {
 | 
				
			||||||
	const uri = typeof value == 'string' ? value : value.id;
 | 
						if (typeof uri !== 'string') throw 'uri is not string';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// URIがこのサーバーを指しているならデータベースからフェッチ
 | 
						// URIがこのサーバーを指しているならデータベースからフェッチ
 | 
				
			||||||
	if (uri.startsWith(config.url + '/')) {
 | 
						if (uri.startsWith(config.url + '/')) {
 | 
				
			||||||
| 
						 | 
					@ -71,12 +96,14 @@ export async function fetchPerson(value: string | IObject, resolver?: Resolver):
 | 
				
			||||||
/**
 | 
					/**
 | 
				
			||||||
 * Personを作成します。
 | 
					 * Personを作成します。
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export async function createPerson(value: any, resolver?: Resolver): Promise<IUser> {
 | 
					export async function createPerson(uri: string, resolver?: Resolver): Promise<IUser> {
 | 
				
			||||||
 | 
						if (typeof uri !== 'string') throw 'uri is not string';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (resolver == null) resolver = new Resolver();
 | 
						if (resolver == null) resolver = new Resolver();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const object = await resolver.resolve(value) as any;
 | 
						const object = await resolver.resolve(uri) as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const err = validatePerson(object);
 | 
						const err = validatePerson(object, uri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (err) {
 | 
						if (err) {
 | 
				
			||||||
		throw err;
 | 
							throw err;
 | 
				
			||||||
| 
						 | 
					@ -86,7 +113,7 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	log(`Creating the Person: ${person.id}`);
 | 
						log(`Creating the Person: ${person.id}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const [followersCount = 0, followingCount = 0, notesCount = 0, finger] = await Promise.all([
 | 
						const [followersCount = 0, followingCount = 0, notesCount = 0] = await Promise.all([
 | 
				
			||||||
		resolver.resolve(person.followers).then(
 | 
							resolver.resolve(person.followers).then(
 | 
				
			||||||
			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
 | 
								resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
 | 
				
			||||||
			() => undefined
 | 
								() => undefined
 | 
				
			||||||
| 
						 | 
					@ -98,11 +125,10 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 | 
				
			||||||
		resolver.resolve(person.outbox).then(
 | 
							resolver.resolve(person.outbox).then(
 | 
				
			||||||
			resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
 | 
								resolved => isCollectionOrOrderedCollection(resolved) ? resolved.totalItems : undefined,
 | 
				
			||||||
			() => undefined
 | 
								() => undefined
 | 
				
			||||||
		),
 | 
							)
 | 
				
			||||||
		webFinger(person.id)
 | 
					 | 
				
			||||||
	]);
 | 
						]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const host = toUnicode(finger.subject.replace(/^.*?@/, '')).toLowerCase();
 | 
						const host = toUnicode(new URL(object.id).hostname.toLowerCase());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const isBot = object.type == 'Service';
 | 
						const isBot = object.type == 'Service';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -166,8 +192,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const avatarId = avatar ? avatar._id : null;
 | 
						const avatarId = avatar ? avatar._id : null;
 | 
				
			||||||
	const bannerId = banner ? banner._id : null;
 | 
						const bannerId = banner ? banner._id : null;
 | 
				
			||||||
	const avatarUrl = avatar && avatar.metadata.url ? avatar.metadata.url : null;
 | 
						const avatarUrl = (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null;
 | 
				
			||||||
	const bannerUrl = banner && banner.metadata.url ? banner.metadata.url : null;
 | 
						const bannerUrl = (banner && banner.metadata.url) ? banner.metadata.url : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	await User.update({ _id: user._id }, {
 | 
						await User.update({ _id: user._id }, {
 | 
				
			||||||
		$set: {
 | 
							$set: {
 | 
				
			||||||
| 
						 | 
					@ -192,8 +218,8 @@ export async function createPerson(value: any, resolver?: Resolver): Promise<IUs
 | 
				
			||||||
 *
 | 
					 *
 | 
				
			||||||
 * Misskeyに対象のPersonが登録されていなければ無視します。
 | 
					 * Misskeyに対象のPersonが登録されていなければ無視します。
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export async function updatePerson(value: string | IObject, resolver?: Resolver): Promise<void> {
 | 
					export async function updatePerson(uri: string, resolver?: Resolver): Promise<void> {
 | 
				
			||||||
	const uri = typeof value == 'string' ? value : value.id;
 | 
						if (typeof uri !== 'string') throw 'uri is not string';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// URIがこのサーバーを指しているならスキップ
 | 
						// URIがこのサーバーを指しているならスキップ
 | 
				
			||||||
	if (uri.startsWith(config.url + '/')) {
 | 
						if (uri.startsWith(config.url + '/')) {
 | 
				
			||||||
| 
						 | 
					@ -210,9 +236,9 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (resolver == null) resolver = new Resolver();
 | 
						if (resolver == null) resolver = new Resolver();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const object = await resolver.resolve(value) as any;
 | 
						const object = await resolver.resolve(uri) as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const err = validatePerson(object);
 | 
						const err = validatePerson(object, uri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (err) {
 | 
						if (err) {
 | 
				
			||||||
		throw err;
 | 
							throw err;
 | 
				
			||||||
| 
						 | 
					@ -255,7 +281,7 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
 | 
				
			||||||
			sharedInbox: person.sharedInbox,
 | 
								sharedInbox: person.sharedInbox,
 | 
				
			||||||
			avatarId: avatar ? avatar._id : null,
 | 
								avatarId: avatar ? avatar._id : null,
 | 
				
			||||||
			bannerId: banner ? banner._id : null,
 | 
								bannerId: banner ? banner._id : null,
 | 
				
			||||||
			avatarUrl: avatar && avatar.metadata.url ? avatar.metadata.url : null,
 | 
								avatarUrl: (avatar && avatar.metadata.thumbnailUrl) ? avatar.metadata.thumbnailUrl : (avatar && avatar.metadata.url) ? avatar.metadata.url : null,
 | 
				
			||||||
			bannerUrl: banner && banner.metadata.url ? banner.metadata.url : null,
 | 
								bannerUrl: banner && banner.metadata.url ? banner.metadata.url : null,
 | 
				
			||||||
			description: htmlToMFM(person.summary),
 | 
								description: htmlToMFM(person.summary),
 | 
				
			||||||
			followersCount,
 | 
								followersCount,
 | 
				
			||||||
| 
						 | 
					@ -275,8 +301,8 @@ export async function updatePerson(value: string | IObject, resolver?: Resolver)
 | 
				
			||||||
 * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
 | 
					 * Misskeyに対象のPersonが登録されていればそれを返し、そうでなければ
 | 
				
			||||||
 * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
 | 
					 * リモートサーバーからフェッチしてMisskeyに登録しそれを返します。
 | 
				
			||||||
 */
 | 
					 */
 | 
				
			||||||
export async function resolvePerson(value: string | IObject, verifier?: string): Promise<IUser> {
 | 
					export async function resolvePerson(uri: string, verifier?: string): Promise<IUser> {
 | 
				
			||||||
	const uri = typeof value == 'string' ? value : value.id;
 | 
						if (typeof uri !== 'string') throw 'uri is not string';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	//#region このサーバーに既に登録されていたらそれを返す
 | 
						//#region このサーバーに既に登録されていたらそれを返す
 | 
				
			||||||
	const exist = await fetchPerson(uri);
 | 
						const exist = await fetchPerson(uri);
 | 
				
			||||||
| 
						 | 
					@ -287,5 +313,5 @@ export async function resolvePerson(value: string | IObject, verifier?: string):
 | 
				
			||||||
	//#endregion
 | 
						//#endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// リモートサーバーからフェッチしてきて登録
 | 
						// リモートサーバーからフェッチしてきて登録
 | 
				
			||||||
	return await createPerson(value);
 | 
						return await createPerson(uri);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -6,6 +6,7 @@ export default (object: any, note: INote) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	return {
 | 
						return {
 | 
				
			||||||
		id: `${config.url}/notes/${note._id}`,
 | 
							id: `${config.url}/notes/${note._id}`,
 | 
				
			||||||
 | 
							actor: `${config.url}/users/${note.userId}`,
 | 
				
			||||||
		type: 'Announce',
 | 
							type: 'Announce',
 | 
				
			||||||
		published: note.createdAt.toISOString(),
 | 
							published: note.createdAt.toISOString(),
 | 
				
			||||||
		to: ['https://www.w3.org/ns/activitystreams#Public'],
 | 
							to: ['https://www.w3.org/ns/activitystreams#Public'],
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,17 @@
 | 
				
			||||||
export default (object: any) => ({
 | 
					import config from '../../../config';
 | 
				
			||||||
	type: 'Create',
 | 
					import { INote } from '../../../models/note';
 | 
				
			||||||
	object
 | 
					
 | 
				
			||||||
});
 | 
					export default (object: any, note: INote) => {
 | 
				
			||||||
 | 
						const activity = {
 | 
				
			||||||
 | 
							id: `${config.url}/notes/${note._id}/activity`,
 | 
				
			||||||
 | 
							actor: `${config.url}/users/${note.userId}`,
 | 
				
			||||||
 | 
							type: 'Create',
 | 
				
			||||||
 | 
							published: note.createdAt.toISOString(),
 | 
				
			||||||
 | 
							object
 | 
				
			||||||
 | 
						} as any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (object.to) activity.to = object.to;
 | 
				
			||||||
 | 
						if (object.cc) activity.cc = object.cc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return activity;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,8 @@
 | 
				
			||||||
export default (object: any) => ({
 | 
					import config from '../../../config';
 | 
				
			||||||
 | 
					import { ILocalUser } from "../../../models/user";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default (object: any, user: ILocalUser) => ({
 | 
				
			||||||
	type: 'Delete',
 | 
						type: 'Delete',
 | 
				
			||||||
 | 
						actor: `${config.url}/users/${user._id}`,
 | 
				
			||||||
	object
 | 
						object
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,7 +1,16 @@
 | 
				
			||||||
export default (x: any) => Object.assign({
 | 
					import config from '../../../config';
 | 
				
			||||||
	'@context': [
 | 
					import * as uuid from 'uuid';
 | 
				
			||||||
		'https://www.w3.org/ns/activitystreams',
 | 
					
 | 
				
			||||||
		'https://w3id.org/security/v1',
 | 
					export default (x: any) => {
 | 
				
			||||||
		{ Hashtag: 'as:Hashtag' }
 | 
						if (x !== null && typeof x === 'object' && x.id == null) {
 | 
				
			||||||
	]
 | 
							x.id = `${config.url}/${uuid.v4()}`;
 | 
				
			||||||
}, x);
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return Object.assign({
 | 
				
			||||||
 | 
							'@context': [
 | 
				
			||||||
 | 
								'https://www.w3.org/ns/activitystreams',
 | 
				
			||||||
 | 
								'https://w3id.org/security/v1',
 | 
				
			||||||
 | 
								{ Hashtag: 'as:Hashtag' }
 | 
				
			||||||
 | 
							]
 | 
				
			||||||
 | 
						}, x);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,8 @@
 | 
				
			||||||
export default (object: any) => ({
 | 
					import config from '../../../config';
 | 
				
			||||||
 | 
					import { ILocalUser, IUser } from "../../../models/user";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default (object: any, user: ILocalUser | IUser) => ({
 | 
				
			||||||
	type: 'Undo',
 | 
						type: 'Undo',
 | 
				
			||||||
 | 
						actor: `${config.url}/users/${user._id}`,
 | 
				
			||||||
	object
 | 
						object
 | 
				
			||||||
});
 | 
					});
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -19,6 +19,9 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
 | 
				
			||||||
		port,
 | 
							port,
 | 
				
			||||||
		method: 'POST',
 | 
							method: 'POST',
 | 
				
			||||||
		path: pathname + search,
 | 
							path: pathname + search,
 | 
				
			||||||
 | 
							headers: {
 | 
				
			||||||
 | 
								'Content-Type': 'application/activity+json'
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
	}, res => {
 | 
						}, res => {
 | 
				
			||||||
		log(`${url} --> ${res.statusCode}`);
 | 
							log(`${url} --> ${res.statusCode}`);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -32,7 +35,7 @@ export default (user: ILocalUser, url: string, object: any) => new Promise((reso
 | 
				
			||||||
	sign(req, {
 | 
						sign(req, {
 | 
				
			||||||
		authorizationHeaderName: 'Signature',
 | 
							authorizationHeaderName: 'Signature',
 | 
				
			||||||
		key: user.keypair,
 | 
							key: user.keypair,
 | 
				
			||||||
		keyId: `acct:${user.username}@${config.host}`
 | 
							keyId: `${config.url}/users/${user._id}/publickey`
 | 
				
			||||||
	});
 | 
						});
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// Signature: Signature ... => Signature: ...
 | 
						// Signature: Signature ... => Signature: ...
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -15,7 +15,7 @@ export default async (username: string, _host: string, option?: any): Promise<IU
 | 
				
			||||||
	const host = toUnicode(hostAscii);
 | 
						const host = toUnicode(hostAscii);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	if (config.host == host) {
 | 
						if (config.host == host) {
 | 
				
			||||||
		return await User.findOne({ usernameLower });
 | 
							return await User.findOne({ usernameLower, host: null });
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let user = await User.findOne({ usernameLower, host }, option);
 | 
						let user = await User.findOne({ usernameLower, host }, option);
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -25,7 +25,7 @@ function inbox(ctx: Router.IRouterContext) {
 | 
				
			||||||
	ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature;
 | 
						ctx.req.headers.authorization = 'Signature ' + ctx.req.headers.signature;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	try {
 | 
						try {
 | 
				
			||||||
		signature = httpSignature.parseRequest(ctx.req);
 | 
							signature = httpSignature.parseRequest(ctx.req, { 'headers': [] });
 | 
				
			||||||
	} catch (e) {
 | 
						} catch (e) {
 | 
				
			||||||
		ctx.status = 401;
 | 
							ctx.status = 401;
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1 +1 @@
 | 
				
			||||||
export default (token: string) => token[0] == '!';
 | 
					export default (token: string) => token.startsWith('!');
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,101 +0,0 @@
 | 
				
			||||||
import Stats, { IStats } from '../../../../models/stats';
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const meta = {
 | 
					 | 
				
			||||||
	requireCredential: true,
 | 
					 | 
				
			||||||
	requireAdmin: true
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export default (params: any) => new Promise(async (res, rej) => {
 | 
					 | 
				
			||||||
	const now = new Date();
 | 
					 | 
				
			||||||
	const y = now.getFullYear();
 | 
					 | 
				
			||||||
	const m = now.getMonth();
 | 
					 | 
				
			||||||
	const d = now.getDate();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const stats = await Stats.find({
 | 
					 | 
				
			||||||
		date: {
 | 
					 | 
				
			||||||
			$gt: new Date(y - 1, m, d)
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}, {
 | 
					 | 
				
			||||||
		sort: {
 | 
					 | 
				
			||||||
			date: -1
 | 
					 | 
				
			||||||
		},
 | 
					 | 
				
			||||||
		fields: {
 | 
					 | 
				
			||||||
			_id: 0
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const chart: Array<Omit<IStats, '_id'>> = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for (let i = 364; i >= 0; i--) {
 | 
					 | 
				
			||||||
		const day = new Date(y, m, d - i);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		const stat = stats.find(s => s.date.getTime() == day.getTime());
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (stat) {
 | 
					 | 
				
			||||||
			chart.unshift(stat);
 | 
					 | 
				
			||||||
		} else { // 隙間埋め
 | 
					 | 
				
			||||||
			const mostRecent = stats.find(s => s.date.getTime() < day.getTime());
 | 
					 | 
				
			||||||
			if (mostRecent) {
 | 
					 | 
				
			||||||
				chart.unshift(Object.assign({}, mostRecent, {
 | 
					 | 
				
			||||||
					date: day
 | 
					 | 
				
			||||||
				}));
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				chart.unshift({
 | 
					 | 
				
			||||||
					date: day,
 | 
					 | 
				
			||||||
					users: {
 | 
					 | 
				
			||||||
						local: {
 | 
					 | 
				
			||||||
							total: 0,
 | 
					 | 
				
			||||||
							diff: 0
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
						remote: {
 | 
					 | 
				
			||||||
							total: 0,
 | 
					 | 
				
			||||||
							diff: 0
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					notes: {
 | 
					 | 
				
			||||||
						local: {
 | 
					 | 
				
			||||||
							total: 0,
 | 
					 | 
				
			||||||
							diff: 0,
 | 
					 | 
				
			||||||
							diffs: {
 | 
					 | 
				
			||||||
								normal: 0,
 | 
					 | 
				
			||||||
								reply: 0,
 | 
					 | 
				
			||||||
								renote: 0
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
						remote: {
 | 
					 | 
				
			||||||
							total: 0,
 | 
					 | 
				
			||||||
							diff: 0,
 | 
					 | 
				
			||||||
							diffs: {
 | 
					 | 
				
			||||||
								normal: 0,
 | 
					 | 
				
			||||||
								reply: 0,
 | 
					 | 
				
			||||||
								renote: 0
 | 
					 | 
				
			||||||
							}
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					},
 | 
					 | 
				
			||||||
					drive: {
 | 
					 | 
				
			||||||
						local: {
 | 
					 | 
				
			||||||
							totalCount: 0,
 | 
					 | 
				
			||||||
							totalSize: 0,
 | 
					 | 
				
			||||||
							diffCount: 0,
 | 
					 | 
				
			||||||
							diffSize: 0
 | 
					 | 
				
			||||||
						},
 | 
					 | 
				
			||||||
						remote: {
 | 
					 | 
				
			||||||
							totalCount: 0,
 | 
					 | 
				
			||||||
							totalSize: 0,
 | 
					 | 
				
			||||||
							diffCount: 0,
 | 
					 | 
				
			||||||
							diffSize: 0
 | 
					 | 
				
			||||||
						}
 | 
					 | 
				
			||||||
					}
 | 
					 | 
				
			||||||
				});
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	chart.forEach(x => {
 | 
					 | 
				
			||||||
		delete x.date;
 | 
					 | 
				
			||||||
	});
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	res(chart);
 | 
					 | 
				
			||||||
});
 | 
					 | 
				
			||||||
| 
						 | 
					@ -3,7 +3,7 @@ import RegistrationTicket from '../../../../models/registration-tickets';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const meta = {
 | 
					export const meta = {
 | 
				
			||||||
	desc: {
 | 
						desc: {
 | 
				
			||||||
		ja: '招待コードを発行します。'
 | 
							'ja-JP': '招待コードを発行します。'
 | 
				
			||||||
	},
 | 
						},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	requireCredential: true,
 | 
						requireCredential: true,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
Some files were not shown because too many files have changed in this diff Show more
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue