From 2511114c287d95010e15a08deb7ff20561b0c3b0 Mon Sep 17 00:00:00 2001
From: syuilo <Syuilotan@yahoo.co.jp>
Date: Mon, 10 Feb 2020 23:17:42 +0900
Subject: [PATCH] =?UTF-8?q?=E3=81=AA=E3=82=93=E3=81=8B=E3=82=82=E3=81=86?=
 =?UTF-8?q?=E3=82=81=E3=81=A3=E3=81=A1=E3=82=83=E5=A4=89=E3=81=88=E3=81=9F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Resolve #5846
---
 locales/ja-JP.yml                             |  1 +
 src/client/components/autocomplete.vue        |  2 +-
 src/client/components/emoji-picker.vue        |  2 +-
 src/client/components/emoji.vue               | 43 ++++----
 src/client/components/mfm.ts                  |  3 +-
 src/client/components/post-form.vue           |  8 +-
 src/client/components/reaction-icon.vue       | 12 +--
 src/client/components/signin.vue              | 11 ++-
 src/client/components/signup.vue              | 11 +--
 src/client/init.ts                            |  2 -
 src/client/mios.ts                            | 61 +-----------
 src/client/pages/about.vue                    | 11 ++-
 src/client/pages/explore.vue                  |  7 +-
 src/client/pages/index.home.vue               |  9 --
 src/client/pages/index.welcome.entrance.vue   | 27 ++----
 src/client/pages/index.welcome.vue            | 11 +--
 .../pages/instance/federation.instance.vue    | 34 +++----
 src/client/pages/instance/index.vue           | 97 ++++++++++---------
 src/client/pages/settings/integration.vue     | 14 +--
 src/client/store.ts                           | 28 +++++-
 20 files changed, 173 insertions(+), 221 deletions(-)

diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index 258b82dea9..c68f9408ad 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -376,6 +376,7 @@ next: "次"
 retype: "再入力"
 noteOf: "{user}のノート"
 inviteToGroup: "グループに招待"
+maxNoteTextLength: "ノートの文字数制限"
 
 _tutorial:
   title: "Misskeyの使い方"
diff --git a/src/client/components/autocomplete.vue b/src/client/components/autocomplete.vue
index 2ab837a2c5..f17351a6f0 100644
--- a/src/client/components/autocomplete.vue
+++ b/src/client/components/autocomplete.vue
@@ -143,7 +143,7 @@ export default Vue.extend({
 		this.setPosition();
 
 		//#region Construct Emoji DB
-		const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
+		const customEmojis = this.$store.state.instance.meta.emojis;
 		const emojiDefinitions: EmojiDef[] = [];
 
 		for (const x of customEmojis) {
diff --git a/src/client/components/emoji-picker.vue b/src/client/components/emoji-picker.vue
index 61d641a023..a647b0ea04 100644
--- a/src/client/components/emoji-picker.vue
+++ b/src/client/components/emoji-picker.vue
@@ -140,7 +140,7 @@ export default Vue.extend({
 	},
 
 	created() {
-		let local = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
+		let local = this.$store.state.instance.meta.emojis;
 		local = groupByX(local, (x: any) => x.category || '');
 		this.customEmojis = local;
 	},
diff --git a/src/client/components/emoji.vue b/src/client/components/emoji.vue
index 2e8bddb803..7784a1bf17 100644
--- a/src/client/components/emoji.vue
+++ b/src/client/components/emoji.vue
@@ -55,38 +55,35 @@ export default Vue.extend({
 
 		useOsDefaultEmojis(): boolean {
 			return this.$store.state.device.useOsDefaultEmojis && !this.isReaction;
+		},
+
+		ce() {
+			let ce = [];
+			if (this.customEmojis) ce = ce.concat(this.customEmojis);
+			if (this.$store.state.instance.meta && this.$store.state.instance.meta.emojis) ce = ce.concat(this.$store.state.instance.meta.emojis);
+			return ce;
 		}
 	},
 
 	watch: {
-		customEmojis() {
-			if (this.name) {
-				const customEmoji = this.customEmojis.find(x => x.name == this.name);
-				if (customEmoji) {
-					this.customEmoji = customEmoji;
-					this.url = this.$store.state.device.disableShowingAnimatedImages
-						? getStaticImageUrl(customEmoji.url)
-						: customEmoji.url;
+		ce: {
+			handler() {
+				if (this.name) {
+					const customEmoji = this.ce.find(x => x.name == this.name);
+					if (customEmoji) {
+						this.customEmoji = customEmoji;
+						this.url = this.$store.state.device.disableShowingAnimatedImages
+							? getStaticImageUrl(customEmoji.url)
+							: customEmoji.url;
+					}
 				}
-			}
+			},
+			immediate: true
 		},
 	},
 
 	created() {
-		if (this.name) {
-			const customEmoji = this.customEmojis.find(x => x.name == this.name);
-			if (customEmoji) {
-				this.customEmoji = customEmoji;
-				this.url = this.$store.state.device.disableShowingAnimatedImages
-					? getStaticImageUrl(customEmoji.url)
-					: customEmoji.url;
-			} else {
-				//const emoji = lib[this.name];
-				//if (emoji) {
-				//	this.char = emoji.char;
-				//}
-			}
-		} else {
+		if (!this.name) {
 			this.char = this.emoji;
 		}
 
diff --git a/src/client/components/mfm.ts b/src/client/components/mfm.ts
index 719e9fe94a..275167836e 100644
--- a/src/client/components/mfm.ts
+++ b/src/client/components/mfm.ts
@@ -234,7 +234,6 @@ export default Vue.component('misskey-flavored-markdown', {
 				}
 
 				case 'emoji': {
-					const customEmojis = (this.$root.getMetaSync() || { emojis: [] }).emojis || [];
 					return [createElement('mk-emoji', {
 						key: Math.random(),
 						attrs: {
@@ -242,7 +241,7 @@ export default Vue.component('misskey-flavored-markdown', {
 							name: token.node.props.name
 						},
 						props: {
-							customEmojis: this.customEmojis || customEmojis,
+							customEmojis: this.customEmojis,
 							normal: this.plain
 						}
 					})];
diff --git a/src/client/components/post-form.vue b/src/client/components/post-form.vue
index 6716fa1b7c..6645c4f1f4 100644
--- a/src/client/components/post-form.vue
+++ b/src/client/components/post-form.vue
@@ -8,7 +8,7 @@
 	<header>
 		<button class="cancel _button" @click="cancel"><fa :icon="faTimes"/></button>
 		<div>
-			<span class="text-count" :class="{ over: trimmedLength(text) > 500 }">{{ 500 - trimmedLength(text) }}</span>
+			<span class="text-count" :class="{ over: trimmedLength(text) > max }">{{ max - trimmedLength(text) }}</span>
 			<button class="_button visibility" @click="setVisibility" ref="visibilityButton">
 				<span v-if="visibility === 'public'"><fa :icon="faGlobe"/></span>
 				<span v-if="visibility === 'home'"><fa :icon="faHome"/></span>
@@ -172,8 +172,12 @@ export default Vue.extend({
 		canPost(): boolean {
 			return !this.posting &&
 				(1 <= this.text.length || 1 <= this.files.length || this.poll || this.renote) &&
-				(length(this.text.trim()) <= 500) &&
+				(length(this.text.trim()) <= this.max) &&
 				(!this.poll || this.pollChoices.length >= 2);
+		},
+
+		max(): number {
+			return this.$store.state.instance.meta ? this.$store.state.instance.meta.maxNoteTextLength : 1000;
 		}
 	},
 
diff --git a/src/client/components/reaction-icon.vue b/src/client/components/reaction-icon.vue
index 368ddc0efc..9155c59440 100644
--- a/src/client/components/reaction-icon.vue
+++ b/src/client/components/reaction-icon.vue
@@ -1,5 +1,5 @@
 <template>
-<mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :is-reaction="true" :custom-emojis="customEmojis" :normal="true" :no-style="noStyle"/>
+<mk-emoji :emoji="reaction.startsWith(':') ? null : reaction" :name="reaction.startsWith(':') ? reaction.substr(1, reaction.length - 2) : null" :is-reaction="true" :normal="true" :no-style="noStyle"/>
 </template>
 
 <script lang="ts">
@@ -18,15 +18,5 @@ export default Vue.extend({
 			default: false
 		},
 	},
-	data() {
-		return {
-			customEmojis: []
-		};
-	},
-	created() {
-		this.$root.getMeta().then(meta => {
-			if (meta && meta.emojis) this.customEmojis = meta.emojis;
-		});
-	},
 });
 </script>
diff --git a/src/client/components/signin.vue b/src/client/components/signin.vue
index 1526f554a4..22b5ec804c 100644
--- a/src/client/components/signin.vue
+++ b/src/client/components/signin.vue
@@ -82,7 +82,6 @@ export default Vue.extend({
 			token: '',
 			apiUrl,
 			host: toUnicode(host),
-			meta: null,
 			totpLogin: false,
 			credential: null,
 			challengeData: null,
@@ -91,11 +90,13 @@ export default Vue.extend({
 		};
 	},
 
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-		});
+	computed: {
+		meta() {
+			return this.$store.state.instance.meta;
+		},
+	},
 
+	created() {
 		if (this.autoSet) {
 			this.$once('login', res => {
 				localStorage.setItem('i', res.i);
diff --git a/src/client/components/signup.vue b/src/client/components/signup.vue
index c03a99def6..47c32a3364 100644
--- a/src/client/components/signup.vue
+++ b/src/client/components/signup.vue
@@ -79,7 +79,6 @@ export default Vue.extend({
 			usernameState: null,
 			passwordStrength: '',
 			passwordRetypeState: null,
-			meta: {},
 			submitting: false,
 			ToSAgreement: false,
 			faLock, faExclamationTriangle, faSpinner, faCheck
@@ -87,6 +86,10 @@ export default Vue.extend({
 	},
 
 	computed: {
+		meta() {
+			return this.$store.state.instance.meta;
+		},
+		
 		shouldShowProfileUrl(): boolean {
 			return (this.username != '' &&
 				this.usernameState != 'invalid-format' &&
@@ -95,12 +98,6 @@ export default Vue.extend({
 		}
 	},
 
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-		});
-	},
-
 	mounted() {
 		const head = document.getElementsByTagName('head')[0];
 		const script = document.createElement('script');
diff --git a/src/client/init.ts b/src/client/init.ts
index 6cd7734fd6..9a9ba8be6a 100644
--- a/src/client/init.ts
+++ b/src/client/init.ts
@@ -157,8 +157,6 @@ os.init(async () => {
 		},
 		methods: {
 			api: os.api,
-			getMeta: os.getMeta,
-			getMetaSync: os.getMetaSync,
 			signout: os.signout,
 			new(vm, props) {
 				const x = new vm({
diff --git a/src/client/mios.ts b/src/client/mios.ts
index 282c51185f..39719a2670 100644
--- a/src/client/mios.ts
+++ b/src/client/mios.ts
@@ -17,16 +17,6 @@ let pending = 0;
  * Misskey Operating System
  */
 export default class MiOS extends EventEmitter {
-	/**
-	 * Misskeyの /meta で取得できるメタ情報
-	 */
-	private meta: {
-		data: { [x: string]: any };
-		chachedAt: Date;
-	};
-
-	private isMetaFetching = false;
-
 	public app: Vue;
 
 	public store: ReturnType<typeof initStore>;
@@ -88,7 +78,7 @@ export default class MiOS extends EventEmitter {
 			// When failure
 			.catch(() => {
 				// Render the error screen
-				document.body.innerHTML = '<div id="err">Error</div>';
+				document.body.innerHTML = '<div id="err">Oops!</div>';
 
 				Progress.done();
 			});
@@ -107,9 +97,9 @@ export default class MiOS extends EventEmitter {
 			// Finish init
 			callback();
 
-			// Init service worker
-			this.getMeta().then(data => {
-				if (data.swPublickey) this.registerSw(data.swPublickey);
+			this.store.dispatch('instance/fetch').then(() => {
+				// Init service worker
+				if (this.store.state.instance.meta.swPublickey) this.registerSw(this.store.state.instance.meta.swPublickey);
 			});
 		};
 
@@ -350,49 +340,6 @@ export default class MiOS extends EventEmitter {
 
 		return promise;
 	}
-
-	/**
-	 * Misskeyのメタ情報を取得します
-	 */
-	@autobind
-	public getMetaSync() {
-		return this.meta ? this.meta.data : null;
-	}
-
-	/**
-	 * Misskeyのメタ情報を取得します
-	 * @param force キャッシュを無視するか否か
-	 */
-	@autobind
-	public getMeta(force = false) {
-		return new Promise<{ [x: string]: any }>(async (res, rej) => {
-			if (this.isMetaFetching) {
-				this.once('_meta_fetched_', () => {
-					res(this.meta.data);
-				});
-				return;
-			}
-
-			const expire = 1000 * 60; // 1min
-
-			// forceが有効, meta情報を保持していない or 期限切れ
-			if (force || this.meta == null || Date.now() - this.meta.chachedAt.getTime() > expire) {
-				this.isMetaFetching = true;
-				const meta = await this.api('meta', {
-					detail: false
-				});
-				this.meta = {
-					data: meta,
-					chachedAt: new Date()
-				};
-				this.isMetaFetching = false;
-				this.emit('_meta_fetched_');
-				res(meta);
-			} else {
-				res(this.meta.data);
-			}
-		});
-	}
 }
 
 /**
diff --git a/src/client/pages/about.vue b/src/client/pages/about.vue
index f9b92df80e..7fb335fc9d 100644
--- a/src/client/pages/about.vue
+++ b/src/client/pages/about.vue
@@ -84,18 +84,19 @@ export default Vue.extend({
 	data() {
 		return {
 			version,
-			meta: null,
 			stats: null,
 			serverInfo: null,
 			faInfoCircle
 		}
 	},
 
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-		});
+	computed: {
+		meta() {
+			return this.$store.state.instance.meta;
+		},
+	},
 
+	created() {
 		this.$root.api('stats').then(res => {
 			this.stats = res;
 		});
diff --git a/src/client/pages/explore.vue b/src/client/pages/explore.vue
index ba2c3faa6c..7ff4b5ed60 100644
--- a/src/client/pages/explore.vue
+++ b/src/client/pages/explore.vue
@@ -115,13 +115,15 @@ export default Vue.extend({
 			tagsLocal: [],
 			tagsRemote: [],
 			stats: null,
-			meta: null,
 			num: Vue.filter('number'),
 			faBookmark, faChartLine, faCommentAlt, faPlus, faHashtag, faRocket
 		};
 	},
 
 	computed: {
+		meta() {
+			return this.$store.state.instance.meta;
+		},
 		tagUsers(): any {
 			return {
 				endpoint: 'hashtags/users',
@@ -159,9 +161,6 @@ export default Vue.extend({
 		this.$root.api('stats').then(stats => {
 			this.stats = stats;
 		});
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-		});
 	},
 });
 </script>
diff --git a/src/client/pages/index.home.vue b/src/client/pages/index.home.vue
index 0f32ca4a50..c2bd187e80 100644
--- a/src/client/pages/index.home.vue
+++ b/src/client/pages/index.home.vue
@@ -83,15 +83,6 @@ export default Vue.extend({
 	},
 
 	created() {
-		this.$root.getMeta().then((meta: Record<string, any>) => {
-			if (!(
-				this.enableGlobalTimeline = !meta.disableGlobalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
-			) && this.src === 'global') this.src = 'local';
-			if (!(
-				this.enableLocalTimeline = !meta.disableLocalTimeline || this.$store.state.i.isModerator || this.$store.state.i.isAdmin
-			) && ['local', 'social'].includes(this.src)) this.src = 'home';
-		});
-
 		this.src = this.$store.state.deviceUser.tl.src;
 		if (this.src === 'list') {
 			this.list = this.$store.state.deviceUser.tl.arg;
diff --git a/src/client/pages/index.welcome.entrance.vue b/src/client/pages/index.welcome.entrance.vue
index 1b0cc7d034..f63e0c2c2b 100644
--- a/src/client/pages/index.welcome.entrance.vue
+++ b/src/client/pages/index.welcome.entrance.vue
@@ -1,10 +1,10 @@
 <template>
 <div class="rsqzvsbo">
-	<div class="_panel about">
-		<div class="banner" :style="{ backgroundImage: `url(${ banner })` }"></div>
+	<div class="_panel about" v-if="meta">
+		<div class="banner" :style="{ backgroundImage: `url(${ meta.bannerUrl })` }"></div>
 		<div class="body">
-			<h1 class="name" v-html="name || host"></h1>
-			<div class="desc" v-html="description || $t('introMisskey')"></div>
+			<h1 class="name" v-html="meta.name || host"></h1>
+			<div class="desc" v-html="meta.description || $t('introMisskey')"></div>
 			<mk-button @click="signup()" style="display: inline-block; margin-right: 16px;" primary>{{ $t('signup') }}</mk-button>
 			<mk-button @click="signin()" style="display: inline-block;">{{ $t('login') }}</mk-button>
 		</div>
@@ -39,23 +39,16 @@ export default Vue.extend({
 				noPaging: true,
 			},
 			host: toUnicode(host),
-			meta: null,
-			name: null,
-			description: null,
-			banner: null,
-			announcements: [],
 		};
 	},
 
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-			this.name = meta.name;
-			this.description = meta.description;
-			this.announcements = meta.announcements;
-			this.banner = meta.bannerUrl;
-		});
+	computed: {
+		meta() {
+			return this.$store.state.instance.meta;
+		},
+	},
 
+	created() {
 		this.$root.api('stats').then(stats => {
 			this.stats = stats;
 		});
diff --git a/src/client/pages/index.welcome.vue b/src/client/pages/index.welcome.vue
index 213c3db22c..21fc0c3aba 100644
--- a/src/client/pages/index.welcome.vue
+++ b/src/client/pages/index.welcome.vue
@@ -20,15 +20,14 @@ export default Vue.extend({
 
 	data() {
 		return {
-			meta: null,
 			instanceName: getInstanceName(),
 		}
 	},
 
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-		});
-	}
+	computed: {
+		meta() {
+			return this.$store.state.instance.meta;
+		},
+	},
 });
 </script>
diff --git a/src/client/pages/instance/federation.instance.vue b/src/client/pages/instance/federation.instance.vue
index a27556064a..b86f52809e 100644
--- a/src/client/pages/instance/federation.instance.vue
+++ b/src/client/pages/instance/federation.instance.vue
@@ -98,7 +98,7 @@
 		<div class="operations">
 			<span class="label">{{ $t('operations') }}</span>
 			<mk-switch v-model="isSuspended" class="switch">{{ $t('stopActivityDelivery') }}</mk-switch>
-			<mk-switch v-model="isBlocked" class="switch">{{ $t('blockThisInstance') }}</mk-switch>
+			<mk-switch :value="isBlocked" class="switch" @change="changeBlock">{{ $t('blockThisInstance') }}</mk-switch>
 		</div>
 		<details class="metadata">
 			<summary class="label">{{ $t('metadata') }}</summary>
@@ -147,9 +147,7 @@ export default Vue.extend({
 
 	data() {
 		return {
-			meta: null,
-			isSuspended: false,
-			isBlocked: false,
+			isSuspended: this.instance.isSuspended,
 			now: null,
 			chart: null,
 			chartInstance: null,
@@ -184,6 +182,14 @@ export default Vue.extend({
 				null;
 
 			return stats;
+		},
+
+		meta() {
+			return this.$store.state.instance.meta;
+		},
+
+		isBlocked() {
+			return this.meta && this.meta.blockedHosts.includes(this.instance.host);
 		}
 	},
 
@@ -195,12 +201,6 @@ export default Vue.extend({
 			});
 		},
 
-		isBlocked() {
-			this.$root.api('admin/update-meta', {
-				blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
-			});
-		},
-
 		chartSrc() {
 			this.renderChart();
 		},
@@ -210,13 +210,7 @@ export default Vue.extend({
 		}
 	},
 
-	async created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-			this.isSuspended = this.instance.isSuspended;
-			this.isBlocked = this.meta.blockedHosts.includes(this.instance.host);
-		});
-	
+	async created() {	
 		this.now = new Date();
 
 		const [perHour, perDay] = await Promise.all([
@@ -235,6 +229,12 @@ export default Vue.extend({
 	},
 
 	methods: {
+		changeBlock(e) {
+			this.$root.api('admin/update-meta', {
+				blockedHosts: this.isBlocked ? this.meta.blockedHosts.concat([this.instance.host]) : this.meta.blockedHosts.filter(x => x !== this.instance.host)
+			});
+		},
+
 		setSrc(src) {
 			this.chartSrc = src;
 		},
diff --git a/src/client/pages/instance/index.vue b/src/client/pages/instance/index.vue
index e3e7332bc3..6ffdec2a65 100644
--- a/src/client/pages/instance/index.vue
+++ b/src/client/pages/instance/index.vue
@@ -20,6 +20,9 @@
 	</section>
 
 	<section class="_card info">
+		<div class="_content">
+			<mk-input v-model="maxNoteTextLength" type="number" :save="() => save()" style="margin:0;"><template #icon><fa :icon="faPencilAlt"/></template>{{ $t('maxNoteTextLength') }}</mk-input>
+		</div>
 		<div class="_content">
 			<mk-switch v-model="enableLocalTimeline" @change="save()">{{ $t('enableLocalTimeline') }}</mk-switch>
 			<mk-switch v-model="enableGlobalTimeline" @change="save()">{{ $t('enableGlobalTimeline') }}</mk-switch>
@@ -171,7 +174,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
-import { faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
+import { faPencilAlt, faShareAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faThumbtack, faUser, faShieldAlt, faKey, faBolt } from '@fortawesome/free-solid-svg-icons';
 import { faTrashAlt, faEnvelope } from '@fortawesome/free-regular-svg-icons';
 import { faTwitter, faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons';
 import MkButton from '../../components/ui/button.vue';
@@ -205,7 +208,6 @@ export default Vue.extend({
 		return {
 			version,
 			url,
-			meta: null,
 			stats: null,
 			serverInfo: null,
 			proxyAccount: null,
@@ -223,6 +225,7 @@ export default Vue.extend({
 			tosUrl: null,
 			bannerUrl: null,
 			iconUrl: null,
+			maxNoteTextLength: 0,
 			enableRegistration: false,
 			enableLocalTimeline: false,
 			enableGlobalTimeline: false,
@@ -241,52 +244,56 @@ export default Vue.extend({
 			enableDiscordIntegration: false,
 			discordClientId: null,
 			discordClientSecret: null,
-			faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
+			faPencilAlt, faTwitter, faDiscord, faGithub, faShareAlt, faTrashAlt, faGhost, faCog, faPlus, faCloud, faInfoCircle, faBan, faSave, faServer, faLink, faEnvelope, faThumbtack, faUser, faShieldAlt, faKey, faBolt
 		}
 	},
 
-	created() {
-		this.$root.getMeta().then(meta => {
-			this.meta = meta;
-			this.name = this.meta.name;
-			this.description = this.meta.description;
-			this.tosUrl = this.meta.tosUrl;
-			this.bannerUrl = this.meta.bannerUrl;
-			this.iconUrl = this.meta.iconUrl;
-			this.maintainerName = this.meta.maintainerName;
-			this.maintainerEmail = this.meta.maintainerEmail;
-			this.enableRegistration = !this.meta.disableRegistration;
-			this.enableLocalTimeline = !this.meta.disableLocalTimeline;
-			this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
-			this.enableRecaptcha = this.meta.enableRecaptcha;
-			this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
-			this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
-			this.proxyAccountId = this.meta.proxyAccountId;
-			this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
-			this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
-			this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
-			this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
-			this.blockedHosts = this.meta.blockedHosts.join('\n');
-			this.pinnedUsers = this.meta.pinnedUsers.join('\n');
-			this.enableServiceWorker = this.meta.enableServiceWorker;
-			this.swPublicKey = this.meta.swPublickey;
-			this.swPrivateKey = this.meta.swPrivateKey;
-			this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
-			this.twitterConsumerKey = this.meta.twitterConsumerKey;
-			this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
-			this.enableGithubIntegration = this.meta.enableGithubIntegration;
-			this.githubClientId = this.meta.githubClientId;
-			this.githubClientSecret = this.meta.githubClientSecret;
-			this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
-			this.discordClientId = this.meta.discordClientId;
-			this.discordClientSecret = this.meta.discordClientSecret;
+	computed: {
+		meta() {
+			return this.$store.state.instance.meta;
+		},
+	},
 
-			if (this.proxyAccountId) {
-				this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
-					this.proxyAccount = proxyAccount;
-				});
-			}
-		});
+	created() {
+		this.name = this.meta.name;
+		this.description = this.meta.description;
+		this.tosUrl = this.meta.tosUrl;
+		this.bannerUrl = this.meta.bannerUrl;
+		this.iconUrl = this.meta.iconUrl;
+		this.maintainerName = this.meta.maintainerName;
+		this.maintainerEmail = this.meta.maintainerEmail;
+		this.maxNoteTextLength = this.meta.maxNoteTextLength;
+		this.enableRegistration = !this.meta.disableRegistration;
+		this.enableLocalTimeline = !this.meta.disableLocalTimeline;
+		this.enableGlobalTimeline = !this.meta.disableGlobalTimeline;
+		this.enableRecaptcha = this.meta.enableRecaptcha;
+		this.recaptchaSiteKey = this.meta.recaptchaSiteKey;
+		this.recaptchaSecretKey = this.meta.recaptchaSecretKey;
+		this.proxyAccountId = this.meta.proxyAccountId;
+		this.cacheRemoteFiles = this.meta.cacheRemoteFiles;
+		this.proxyRemoteFiles = this.meta.proxyRemoteFiles;
+		this.localDriveCapacityMb = this.meta.driveCapacityPerLocalUserMb;
+		this.remoteDriveCapacityMb = this.meta.driveCapacityPerRemoteUserMb;
+		this.blockedHosts = this.meta.blockedHosts.join('\n');
+		this.pinnedUsers = this.meta.pinnedUsers.join('\n');
+		this.enableServiceWorker = this.meta.enableServiceWorker;
+		this.swPublicKey = this.meta.swPublickey;
+		this.swPrivateKey = this.meta.swPrivateKey;
+		this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
+		this.twitterConsumerKey = this.meta.twitterConsumerKey;
+		this.twitterConsumerSecret = this.meta.twitterConsumerSecret;
+		this.enableGithubIntegration = this.meta.enableGithubIntegration;
+		this.githubClientId = this.meta.githubClientId;
+		this.githubClientSecret = this.meta.githubClientSecret;
+		this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
+		this.discordClientId = this.meta.discordClientId;
+		this.discordClientSecret = this.meta.discordClientSecret;
+
+		if (this.proxyAccountId) {
+			this.$root.api('users/show', { userId: this.proxyAccountId }).then(proxyAccount => {
+				this.proxyAccount = proxyAccount;
+			});
+		}
 
 		this.$root.api('admin/server-info').then(res => {
 			this.serverInfo = res;
@@ -347,6 +354,7 @@ export default Vue.extend({
 				iconUrl: this.iconUrl,
 				maintainerName: this.maintainerName,
 				maintainerEmail: this.maintainerEmail,
+				maxNoteTextLength: this.maxNoteTextLength,
 				disableRegistration: !this.enableRegistration,
 				disableLocalTimeline: !this.enableLocalTimeline,
 				disableGlobalTimeline: !this.enableGlobalTimeline,
@@ -373,6 +381,7 @@ export default Vue.extend({
 				discordClientId: this.discordClientId,
 				discordClientSecret: this.discordClientSecret,
 			}).then(() => {
+				this.$store.dispatch('instance/fetch');
 				if (withDialog) {
 					this.$root.dialog({
 						type: 'success',
diff --git a/src/client/pages/settings/integration.vue b/src/client/pages/settings/integration.vue
index 74efe3941c..742d432018 100644
--- a/src/client/pages/settings/integration.vue
+++ b/src/client/pages/settings/integration.vue
@@ -56,15 +56,17 @@ export default Vue.extend({
 	computed: {
 		integrations() {
 			return this.$store.state.i.integrations;
-		}
+		},
+		
+		meta() {
+			return this.$store.state.instance.meta;
+		},
 	},
 
 	created() {
-		this.$root.getMeta().then(meta => {
-			this.enableTwitterIntegration = meta.enableTwitterIntegration;
-			this.enableDiscordIntegration = meta.enableDiscordIntegration;
-			this.enableGithubIntegration = meta.enableGithubIntegration;
-		});
+		this.enableTwitterIntegration = this.meta.enableTwitterIntegration;
+		this.enableDiscordIntegration = this.meta.enableDiscordIntegration;
+		this.enableGithubIntegration = this.meta.enableGithubIntegration;
 	},
 
 	mounted() {
diff --git a/src/client/store.ts b/src/client/store.ts
index bd6187b91c..711b808d53 100644
--- a/src/client/store.ts
+++ b/src/client/store.ts
@@ -41,13 +41,13 @@ const defaultDeviceSettings = {
 	userData: {},
 };
 
-function copy(data) {
+function copy<T>(data: T): T {
 	return JSON.parse(JSON.stringify(data));
 }
 
 export default (os: MiOS) => new Vuex.Store({
 	plugins: [createPersistedState({
-		paths: ['i', 'device', 'deviceUser', 'settings']
+		paths: ['i', 'device', 'deviceUser', 'settings', 'instance']
 	})],
 
 	state: {
@@ -111,6 +111,30 @@ export default (os: MiOS) => new Vuex.Store({
 	},
 
 	modules: {
+		instance: {
+			namespaced: true,
+
+			state: {
+				meta: null
+			},
+
+			mutations: {
+				set(state, meta) {
+					state.meta = meta;
+				},
+			},
+
+			actions: {
+				async fetch(ctx) {
+					const meta = await os.api('meta', {
+						detail: false
+					});
+
+					ctx.commit('set', meta);
+				}
+			}
+		},
+
 		device: {
 			namespaced: true,