From b12bf78c6dfe024df329d1f5258d9d7d2cb10e75 Mon Sep 17 00:00:00 2001
From: tamaina <tamaina@hotmail.co.jp>
Date: Mon, 23 Mar 2020 13:17:29 +0900
Subject: [PATCH 1/6] Update CHANGELOG.md

---
 CHANGELOG.md | 27 +++++++++++++++++++++++++++
 1 file changed, 27 insertions(+)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index ddc48dd37e..2e7e2d2b95 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,33 @@
 ChangeLog
 =========
 
+12.24.2 (2020/03/22)
+-------------------
+
+### 🐛Fixes
+* ダークモードの同期を修正
+
+12.24.1 (2020/03/22)
+-------------------
+
+### ✨Improvements
+* SVG形式のアイコンファイルを追加
+
+### 🐛Fixes
+* iOSで起動できない問題を修正
+* 画面が小さいとメニューがすべて見えない問題を修正
+* Pages画面にタイトルがない問題を修正
+
+12.24.0 (2020/03/22)
+-------------------
+
+### ✨Improvements
+* クライアント設定にアカウント設定へのリンクを追加
+* ダークモードの同期を強化
+
+### 🐛Fixes
+* 画面が小さいとメニューがすべて見えない問題を修正
+
 12.23.0 (2020/03/22)
 -------------------
 

From dac962580b40c5828e70bc4745ce33e049720c6b Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 23 Mar 2020 19:06:46 +0900
Subject: [PATCH 2/6] =?UTF-8?q?=E3=83=86=E3=83=BC=E3=83=9E=E3=82=A4?=
 =?UTF-8?q?=E3=83=B3=E3=83=9D=E3=83=BC=E3=83=88=E6=A9=9F=E8=83=BD=E3=82=92?=
 =?UTF-8?q?=E5=AE=9F=E8=A3=85=E3=81=99=E3=82=8B=E3=81=AA=E3=81=A9?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 locales/ja-JP.yml                      | 11 +++
 src/client/components/error.vue        |  2 +-
 src/client/components/notes.vue        |  2 +-
 src/client/init.ts                     |  1 +
 src/client/pages/follow-requests.vue   |  2 +-
 src/client/pages/messaging/index.vue   |  2 +-
 src/client/pages/not-found.vue         |  2 +-
 src/client/pages/preferences/theme.vue | 95 ++++++++++++++++++++++++--
 src/client/theme.ts                    |  7 ++
 src/server/web/views/base.pug          |  6 +-
 10 files changed, 118 insertions(+), 12 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 6eb90c9d02..40772f9b1f 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -466,6 +466,17 @@ details: "詳細"
 chooseEmoji: "絵文字を選択"
 unableToProcess: "操作を完了できません"
 recentUsed: "最近使用"
+install: "インストール"
+uninstall: "アンインストール"
+
+_theme:
+  explore: "テーマを探す"
+  install: "テーマのインストール"
+  manage: "テーマの管理"
+  code: "テーマコード"
+  installed: "{name}をインストールしました"
+  alreadyInstalled: "そのテーマは既にインストールされています"
+  invalid: "テーマの形式が間違っています"
 
 _sfx:
   note: "ノート"
diff --git a/src/client/components/error.vue b/src/client/components/error.vue
index 7446a7cb5d..dd9de43c16 100644
--- a/src/client/components/error.vue
+++ b/src/client/components/error.vue
@@ -1,6 +1,6 @@
 <template>
 <div class="mjndxjcg _panel">
-	<img src="https://xn--931a.moe/assets/error.png" class="_ghost"/>
+	<img src="https://xn--931a.moe/assets/error.jpg" class="_ghost"/>
 	<p><fa :icon="faExclamationTriangle"/> {{ $t('error') }}</p>
 	<mk-button @click="() => $emit('retry')" class="button">{{ $t('retry') }}</mk-button>
 </div>
diff --git a/src/client/components/notes.vue b/src/client/components/notes.vue
index bc2ae8472c..65dda17575 100644
--- a/src/client/components/notes.vue
+++ b/src/client/components/notes.vue
@@ -1,7 +1,7 @@
 <template>
 <div class="mk-notes" v-size="[{ max: 500 }]">
 	<div class="empty" v-if="empty">
-		<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
+		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 		<div>{{ $t('noNotes') }}</div>
 	</div>
 
diff --git a/src/client/init.ts b/src/client/init.ts
index c7587afb8c..b9c6aedae4 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -178,6 +178,7 @@ os.init(async () => {
 		},
 		watch: {
 			'$store.state.device.darkMode'() {
+				// TODO: このファイルでbuiltinThemesを参照するとcode splittingが効かず、初回読み込み時に全てのテーマコードを読み込むことになってしまい無駄なので何とかする
 				const themes = builtinThemes.concat(this.$store.state.device.themes);
 				applyTheme(themes.find(x => x.id === (this.$store.state.device.darkMode ? this.$store.state.device.darkTheme : this.$store.state.device.lightTheme)));
 			}
diff --git a/src/client/pages/follow-requests.vue b/src/client/pages/follow-requests.vue
index 14d60a12ec..a900bf735c 100644
--- a/src/client/pages/follow-requests.vue
+++ b/src/client/pages/follow-requests.vue
@@ -6,7 +6,7 @@
 	<mk-pagination :pagination="pagination" class="mk-follow-requests" ref="list">
 		<template #empty>
 			<div class="tkdrhpxr">
-				<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
+				<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 				<div>{{ $t('noFollowRequests') }}</div>
 			</div>
 		</template>
diff --git a/src/client/pages/messaging/index.vue b/src/client/pages/messaging/index.vue
index 702979a098..ed24f8ef54 100644
--- a/src/client/pages/messaging/index.vue
+++ b/src/client/pages/messaging/index.vue
@@ -32,7 +32,7 @@
 		</router-link>
 	</div>
 	<div class="no-history" v-if="!fetching && messages.length == 0">
-		<img src="https://xn--931a.moe/assets/info.png" class="_ghost"/>
+		<img src="https://xn--931a.moe/assets/info.jpg" class="_ghost"/>
 		<div>{{ $t('noHistory') }}</div>
 	</div>
 	<mk-loading v-if="fetching"/>
diff --git a/src/client/pages/not-found.vue b/src/client/pages/not-found.vue
index 6ddbd1932b..9608e07786 100644
--- a/src/client/pages/not-found.vue
+++ b/src/client/pages/not-found.vue
@@ -5,7 +5,7 @@
 
 	<section class="_card">
 		<div class="_content">
-			<img src="https://xn--931a.moe/assets/not-found.png" class="_ghost"/>
+			<img src="https://xn--931a.moe/assets/not-found.jpg" class="_ghost"/>
 			<div>{{ $t('notFoundDescription') }}</div>
 		</div>
 	</section>
diff --git a/src/client/pages/preferences/theme.vue b/src/client/pages/preferences/theme.vue
index 488935a0cd..fcea457396 100644
--- a/src/client/pages/preferences/theme.vue
+++ b/src/client/pages/preferences/theme.vue
@@ -42,6 +42,7 @@
 				<option v-for="x in lightThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
 			</optgroup>
 		</mk-select>
+		<a href="https://assets.msky.cafe/theme/list" rel="noopener" target="_blank" class="_link">{{ $t('_theme.explore') }}</a>
 	</div>
 	<div class="_content">
 		<mk-switch v-model="syncDeviceDarkMode">{{ $t('syncDeviceDarkMode') }}</mk-switch>
@@ -50,18 +51,43 @@
 		<mk-button primary v-if="wallpaper == null" @click="setWallpaper">{{ $t('setWallpaper') }}</mk-button>
 		<mk-button primary v-else @click="wallpaper = null">{{ $t('removeWallpaper') }}</mk-button>
 	</div>
+	<div class="_content">
+		<details>
+			<summary><fa :icon="faDownload"/> {{ $t('_theme.install') }}</summary>
+			<mk-textarea v-model="installThemeCode">
+				<span>{{ $t('_theme.code') }}</span>
+			</mk-textarea>
+			<mk-button @click="() => install(this.installThemeCode)" :disabled="installThemeCode == null"><fa :icon="faCheck"/> {{ $t('install') }}</mk-button>
+		</details>
+	</div>
+	<div class="_content">
+		<details>
+			<summary><fa :icon="faFolderOpen"/> {{ $t('_theme.manage') }}</summary>
+			<mk-select v-model="selectedThemeId">
+				<option v-for="x in installedThemes" :value="x.id" :key="x.id">{{ x.name }}</option>
+			</mk-select>
+			<template v-if="selectedTheme">
+				<mk-textarea readonly tall :value="selectedThemeCode">
+					<span>{{ $t('_theme.code') }}</span>
+				</mk-textarea>
+				<mk-button @click="uninstall()" v-if="!builtinThemes.some(t => t.id == selectedTheme.id)"><fa :icon="faTrashAlt"/> {{ $t('uninstall') }}</mk-button>
+			</template>
+		</details>
+	</div>
 </section>
 </template>
 
 <script lang="ts">
 import Vue from 'vue';
-import { faPalette } from '@fortawesome/free-solid-svg-icons';
+import { faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt } from '@fortawesome/free-solid-svg-icons';
+import * as JSON5 from 'json5';
 import MkInput from '../../components/ui/input.vue';
 import MkButton from '../../components/ui/button.vue';
 import MkSelect from '../../components/ui/select.vue';
 import MkSwitch from '../../components/ui/switch.vue';
+import MkTextarea from '../../components/ui/textarea.vue';
 import i18n from '../../i18n';
-import { Theme, builtinThemes, applyTheme } from '../../theme';
+import { Theme, builtinThemes, applyTheme, validateTheme } from '../../theme';
 import { selectFile } from '../../scripts/select-file';
 import { isDeviceDarkmode } from '../../scripts/is-device-darkmode';
 
@@ -73,12 +99,16 @@ export default Vue.extend({
 		MkButton,
 		MkSelect,
 		MkSwitch,
+		MkTextarea,
 	},
 	
 	data() {
 		return {
+			builtinThemes,
+			installThemeCode: null,
+			selectedThemeId: null,
 			wallpaper: localStorage.getItem('wallpaper'),
-			faPalette
+			faPalette, faDownload, faFolderOpen, faCheck, faTrashAlt
 		}
 	},
 
@@ -118,6 +148,16 @@ export default Vue.extend({
 			get() { return this.$store.state.device.syncDeviceDarkMode; },
 			set(value) { this.$store.commit('device/set', { key: 'syncDeviceDarkMode', value }); }
 		},
+
+		selectedTheme() {
+			if (this.selectedThemeId == null) return null;
+			return this.themes.find(x => x.id === this.selectedThemeId);
+		},
+
+		selectedThemeCode() {
+			if (this.selectedTheme == null) return null;
+			return JSON5.stringify(this.selectedTheme, null, '\t');
+		},
 	},
 
 	watch: {
@@ -155,6 +195,53 @@ export default Vue.extend({
 				this.wallpaper = file.url;
 			});
 		},
+
+		install(code) {
+			let theme;
+			try {
+				theme = JSON5.parse(code);
+			} catch (e) {
+				this.$root.dialog({
+					type: 'error',
+					text: this.$t('_theme.invalid')
+				});
+				return;
+			}
+			if (!validateTheme(theme)) {
+				this.$root.dialog({
+					type: 'error',
+					text: this.$t('_theme.invalid')
+				});
+				return;
+			}
+			if (this.$store.state.device.themes.some(t => t.id === theme.id)) {
+				this.$root.dialog({
+					type: 'info',
+					text: this.$t('_theme.alreadyInstalled')
+				});
+				return;
+			}
+			const themes = this.$store.state.device.themes.concat(theme);
+			this.$store.commit('device/set', {
+				key: 'themes', value: themes
+			});
+			this.$root.dialog({
+				type: 'success',
+				text: this.$t('_theme.installed', { name: theme.name })
+			});
+		},
+
+		uninstall() {
+			const theme = this.selectedTheme;
+			const themes = this.$store.state.device.themes.filter(t => t.id != theme.id);
+			this.$store.commit('device/set', {
+				key: 'themes', value: themes
+			});
+			this.$root.dialog({
+				type: 'info',
+				iconOnly: true, autoClose: true
+			});
+		},
 	}
 });
 </script>
@@ -179,7 +266,7 @@ export default Vue.extend({
 				top: 50%;
 				left: 50%;
 				overflow: hidden;
-				padding: 0 200px;
+				padding: 0 100px;
 				transform: translate3d(-50%, -50%, 0);
 
 				input {
diff --git a/src/client/theme.ts b/src/client/theme.ts
index 2a6adbffcc..e90c1f3a3b 100644
--- a/src/client/theme.ts
+++ b/src/client/theme.ts
@@ -102,3 +102,10 @@ function compile(theme: Theme): { [key: string]: string } {
 function genValue(c: tinycolor.Instance): string {
 	return c.toRgbString();
 }
+
+export function validateTheme(theme: Record<string, any>): boolean {
+	if (theme.id == null) return false;
+	if (theme.name == null) return false;
+	if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
+	return true;
+}
diff --git a/src/server/web/views/base.pug b/src/server/web/views/base.pug
index e6751ecca2..76114e6f5a 100644
--- a/src/server/web/views/base.pug
+++ b/src/server/web/views/base.pug
@@ -16,9 +16,9 @@ html
 		link(rel='icon' href= icon || '/favicon.ico')
 		link(rel='apple-touch-icon' href= icon || '/apple-touch-icon.png')
 		link(rel='manifest' href='/manifest.json')
-		link(rel='prefetch' href='https://xn--931a.moe/assets/info.png')
-		link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.png')
-		link(rel='prefetch' href='https://xn--931a.moe/assets/error.png')
+		link(rel='prefetch' href='https://xn--931a.moe/assets/info.jpg')
+		link(rel='prefetch' href='https://xn--931a.moe/assets/not-found.jpg')
+		link(rel='prefetch' href='https://xn--931a.moe/assets/error.jpg')
 
 		title
 			block title

From e25dea27e71581c2318431516ebf5aea205dc574 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 23 Mar 2020 19:09:20 +0900
Subject: [PATCH 3/6] Better theme validation

---
 src/client/theme.ts | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/src/client/theme.ts b/src/client/theme.ts
index e90c1f3a3b..cc69ee4060 100644
--- a/src/client/theme.ts
+++ b/src/client/theme.ts
@@ -104,8 +104,9 @@ function genValue(c: tinycolor.Instance): string {
 }
 
 export function validateTheme(theme: Record<string, any>): boolean {
-	if (theme.id == null) return false;
-	if (theme.name == null) return false;
+	if (theme.id == null || typeof theme.id !== 'string') return false;
+	if (theme.name == null || typeof theme.name !== 'string') return false;
 	if (theme.base == null || !['light', 'dark'].includes(theme.base)) return false;
+	if (theme.props == null || typeof theme.props !== 'object') return false;
 	return true;
 }

From 833c39969b89c0d5de0b6616a253afea03e5fbb7 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 23 Mar 2020 19:42:26 +0900
Subject: [PATCH 4/6] Refactor

---
 src/server/api/endpoints/notes/state.ts | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/server/api/endpoints/notes/state.ts b/src/server/api/endpoints/notes/state.ts
index 2ec4a93d5b..b41b56162c 100644
--- a/src/server/api/endpoints/notes/state.ts
+++ b/src/server/api/endpoints/notes/state.ts
@@ -28,15 +28,15 @@ export default define(meta, async (ps, user) => {
 	const [favorite, watching] = await Promise.all([
 		NoteFavorites.count({
 			where: {
-			userId: user.id,
-			noteId: ps.noteId
+				userId: user.id,
+				noteId: ps.noteId
 			},
 			take: 1
 		}),
 		NoteWatchings.count({
 			where: {
-			userId: user.id,
-			noteId: ps.noteId
+				userId: user.id,
+				noteId: ps.noteId
 			},
 			take: 1
 		})

From a43a225740a8c27455435022fec31a3599234a86 Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 23 Mar 2020 19:47:02 +0900
Subject: [PATCH 5/6] Fix #6180

---
 src/server/api/endpoints/notes/timeline.ts | 11 +++++++++--
 1 file changed, 9 insertions(+), 2 deletions(-)

diff --git a/src/server/api/endpoints/notes/timeline.ts b/src/server/api/endpoints/notes/timeline.ts
index 3eed9f0ca8..d60136a9ca 100644
--- a/src/server/api/endpoints/notes/timeline.ts
+++ b/src/server/api/endpoints/notes/timeline.ts
@@ -102,6 +102,13 @@ export const meta = {
 };
 
 export default define(meta, async (ps, user) => {
+	const hasFollowing = (await Followings.count({
+		where: {
+			followerId: user.id,
+		},
+		take: 1
+	})) !== 0;
+
 	//#region Construct query
 	const followingQuery = Followings.createQueryBuilder('following')
 		.select('following.followeeId')
@@ -110,8 +117,8 @@ export default define(meta, async (ps, user) => {
 	const query = makePaginationQuery(Notes.createQueryBuilder('note'),
 			ps.sinceId, ps.untilId, ps.sinceDate, ps.untilDate)
 		.andWhere(new Brackets(qb => { qb
-			.where(`note.userId IN (${ followingQuery.getQuery() })`)
-			.orWhere('note.userId = :meId', { meId: user.id });
+			.where('note.userId = :meId', { meId: user.id });
+			if (hasFollowing) qb.orWhere(`note.userId IN (${ followingQuery.getQuery() })`);
 		}))
 		.leftJoinAndSelect('note.user', 'user')
 		.setParameters(followingQuery.getParameters());

From 2f898aa037705883744dbd5cdbc62e4fc4228bea Mon Sep 17 00:00:00 2001
From: syuilo <syuilotan@yahoo.co.jp>
Date: Mon, 23 Mar 2020 19:48:19 +0900
Subject: [PATCH 6/6] 12.25.0

---
 package.json | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/package.json b/package.json
index 096ab58eda..86d5ab202a 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
 	"name": "misskey",
 	"author": "syuilo <syuilotan@yahoo.co.jp>",
-	"version": "12.24.2",
+	"version": "12.25.0",
 	"codename": "indigo",
 	"repository": {
 		"type": "git",