diff --git a/package.json b/package.json
index 7a8d57aedf..8cb457e8a2 100644
--- a/package.json
+++ b/package.json
@@ -32,6 +32,7 @@
"@types/deep-equal": "1.0.1",
"@types/elasticsearch": "5.0.17",
"@types/event-stream": "3.3.32",
+ "@types/eventemitter3": "^2.0.2",
"@types/express": "4.0.37",
"@types/gm": "1.17.33",
"@types/gulp": "4.0.3",
@@ -114,6 +115,7 @@
"diskusage": "0.2.2",
"elasticsearch": "13.3.1",
"escape-regexp": "0.0.1",
+ "eventemitter3": "^2.0.3",
"express": "4.15.4",
"file-type": "7.2.0",
"fuckadblock": "3.2.1",
diff --git a/src/web/app/auth/script.ts b/src/web/app/auth/script.ts
index fe7f9befe8..dd598d1ed6 100644
--- a/src/web/app/auth/script.ts
+++ b/src/web/app/auth/script.ts
@@ -14,7 +14,7 @@ document.title = 'Misskey | アプリの連携';
/**
* init
*/
-init(me => {
+init(() => {
mount(document.createElement('mk-index'));
});
diff --git a/src/web/app/ch/router.ts b/src/web/app/ch/router.ts
index fe014d4e31..f10c4acdf0 100644
--- a/src/web/app/ch/router.ts
+++ b/src/web/app/ch/router.ts
@@ -2,7 +2,7 @@ import * as riot from 'riot';
import * as route from 'page';
let page = null;
-export default me => {
+export default () => {
route('/', index);
route('/:channel', channel);
route('*', notFound);
diff --git a/src/web/app/ch/script.ts b/src/web/app/ch/script.ts
index 760d405c52..e23558037c 100644
--- a/src/web/app/ch/script.ts
+++ b/src/web/app/ch/script.ts
@@ -12,7 +12,7 @@ import route from './router';
/**
* init
*/
-init(me => {
+init(() => {
// Start routing
- route(me);
+ route();
});
diff --git a/src/web/app/common/mios.ts b/src/web/app/common/mios.ts
new file mode 100644
index 0000000000..36c851ac63
--- /dev/null
+++ b/src/web/app/common/mios.ts
@@ -0,0 +1,181 @@
+import { EventEmitter } from 'eventemitter3';
+import * as riot from 'riot';
+import signout from './scripts/signout';
+import Progress from './scripts/loading';
+import Connection from './scripts/home-stream';
+import CONFIG from './scripts/config';
+import api from './scripts/api';
+
+/**
+ * Misskey Operating System
+ */
+export default class MiOS extends EventEmitter {
+ /**
+ * Misskeyの /meta で取得できるメタ情報
+ */
+ private meta: {
+ data: { [x: string]: any };
+ chachedAt: Date;
+ };
+
+ private isMetaFetching = false;
+
+ /**
+ * A signing user
+ */
+ public i: any;
+
+ /**
+ * Whether signed in
+ */
+ public get isSignedin() {
+ return this.i != null;
+ }
+
+ /**
+ * A connection of home stream
+ */
+ public stream: Connection;
+
+ constructor() {
+ super();
+
+ //#region BIND
+ this.init = this.init.bind(this);
+ this.api = this.api.bind(this);
+ this.getMeta = this.getMeta.bind(this);
+ //#endregion
+ }
+
+ /**
+ * Initialize MiOS (boot)
+ * @param callback A function that call when initialized
+ */
+ public async init(callback) {
+ // ユーザーをフェッチしてコールバックする
+ const fetchme = (token, cb) => {
+ let me = null;
+
+ // Return when not signed in
+ if (token == null) {
+ return done();
+ }
+
+ // Fetch user
+ fetch(`${CONFIG.apiUrl}/i`, {
+ method: 'POST',
+ body: JSON.stringify({
+ i: token
+ })
+ }).then(res => { // When success
+ // When failed to authenticate user
+ if (res.status !== 200) {
+ return signout();
+ }
+
+ res.json().then(i => {
+ me = i;
+ me.token = token;
+ done();
+ });
+ }, () => { // When failure
+ // Render the error screen
+ document.body.innerHTML = '';
+ riot.mount('*');
+ Progress.done();
+ });
+
+ function done() {
+ if (cb) cb(me);
+ }
+ };
+
+ // フェッチが完了したとき
+ const fetched = me => {
+ if (me) {
+ riot.observable(me);
+
+ // この me オブジェクトを更新するメソッド
+ me.update = data => {
+ if (data) Object.assign(me, data);
+ me.trigger('updated');
+ };
+
+ // ローカルストレージにキャッシュ
+ localStorage.setItem('me', JSON.stringify(me));
+
+ me.on('updated', () => {
+ // キャッシュ更新
+ localStorage.setItem('me', JSON.stringify(me));
+ });
+ }
+
+ this.i = me;
+
+ // Init home stream connection
+ this.stream = this.i ? new Connection(this.i) : null;
+
+ // Finish init
+ callback();
+ };
+
+ // Get cached account data
+ const cachedMe = JSON.parse(localStorage.getItem('me'));
+
+ if (cachedMe) {
+ fetched(cachedMe);
+
+ // 後から新鮮なデータをフェッチ
+ fetchme(cachedMe.token, freshData => {
+ Object.assign(cachedMe, freshData);
+ cachedMe.trigger('updated');
+ });
+ } else {
+ // Get token from cookie
+ const i = (document.cookie.match(/i=(!\w+)/) || [null, null])[1];
+
+ fetchme(i, fetched);
+ }
+ }
+
+ /**
+ * Misskey APIにリクエストします
+ * @param endpoint エンドポイント名
+ * @param data パラメータ
+ */
+ public api(endpoint: string, data?: { [x: string]: any }) {
+ return api(this.i, endpoint, data);
+ }
+
+ /**
+ * Misskeyのメタ情報を取得します
+ * @param force キャッシュを無視するか否か
+ */
+ 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');
+ 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/web/app/common/mixins.ts b/src/web/app/common/mixins.ts
new file mode 100644
index 0000000000..b5eb1acc78
--- /dev/null
+++ b/src/web/app/common/mixins.ts
@@ -0,0 +1,39 @@
+import * as riot from 'riot';
+
+import MiOS from './mios';
+import ServerStreamManager from './scripts/server-stream-manager';
+import RequestsStreamManager from './scripts/requests-stream-manager';
+import MessagingIndexStream from './scripts/messaging-index-stream-manager';
+
+export default (mios: MiOS) => {
+ (riot as any).mixin('os', {
+ mios: mios
+ });
+
+ (riot as any).mixin('i', {
+ init: function() {
+ this.I = mios.i;
+ this.SIGNIN = mios.isSignedin;
+
+ if (this.SIGNIN) {
+ this.on('mount', () => {
+ mios.i.on('updated', this.update);
+ });
+ this.on('unmount', () => {
+ mios.i.off('updated', this.update);
+ });
+ }
+ },
+ me: mios.i
+ });
+
+ (riot as any).mixin('api', {
+ api: mios.api
+ });
+
+ (riot as any).mixin('stream', { stream: mios.stream });
+
+ (riot as any).mixin('server-stream', { serverStream: new ServerStreamManager() });
+ (riot as any).mixin('requests-stream', { requestsStream: new RequestsStreamManager() });
+ (riot as any).mixin('messaging-index-stream', { messagingIndexStream: new MessagingIndexStream(mios.i) });
+};
diff --git a/src/web/app/common/mixins/api.ts b/src/web/app/common/mixins/api.ts
deleted file mode 100644
index 9726caf510..0000000000
--- a/src/web/app/common/mixins/api.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import * as riot from 'riot';
-import api from '../scripts/api';
-
-export default me => {
- (riot as any).mixin('api', {
- api: api.bind(null, me ? me.token : null)
- });
-};
diff --git a/src/web/app/common/mixins/i.ts b/src/web/app/common/mixins/i.ts
deleted file mode 100644
index 0879d02d3d..0000000000
--- a/src/web/app/common/mixins/i.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import * as riot from 'riot';
-
-export default me => {
- (riot as any).mixin('i', {
- init: function() {
- this.I = me;
- this.SIGNIN = me != null;
-
- if (this.SIGNIN) {
- this.on('mount', () => {
- me.on('updated', this.update);
- });
- this.on('unmount', () => {
- me.off('updated', this.update);
- });
- }
- },
- me: me
- });
-};
diff --git a/src/web/app/common/mixins/index.ts b/src/web/app/common/mixins/index.ts
deleted file mode 100644
index c0c1c0555f..0000000000
--- a/src/web/app/common/mixins/index.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as riot from 'riot';
-
-import activateMe from './i';
-import activateApi from './api';
-import ServerStreamManager from '../scripts/server-stream-manager';
-import RequestsStreamManager from '../scripts/requests-stream-manager';
-import MessagingIndexStream from '../scripts/messaging-index-stream-manager';
-
-export default (me, stream) => {
- activateMe(me);
- activateApi(me);
-
- (riot as any).mixin('stream', { stream });
-
- (riot as any).mixin('server-stream', { serverStream: new ServerStreamManager() });
- (riot as any).mixin('requests-stream', { requestsStream: new RequestsStreamManager() });
- (riot as any).mixin('messaging-index-stream', { messagingIndexStream: new MessagingIndexStream(me) });
-};
diff --git a/src/web/app/common/scripts/api.ts b/src/web/app/common/scripts/api.ts
index 2a9d78e87d..5dcdb59710 100644
--- a/src/web/app/common/scripts/api.ts
+++ b/src/web/app/common/scripts/api.ts
@@ -14,7 +14,7 @@ let pending = 0;
* @param {any} [data={}] Data
* @return {Promise} Response
*/
-export default (i, endpoint, data = {}): Promise => {
+export default (i, endpoint, data = {}): Promise<{ [x: string]: any }> => {
if (++pending === 1) {
spinner = document.createElement('div');
spinner.setAttribute('id', 'wait');
diff --git a/src/web/app/common/scripts/check-for-update.ts b/src/web/app/common/scripts/check-for-update.ts
index 99d8b5d059..c1398ba54f 100644
--- a/src/web/app/common/scripts/check-for-update.ts
+++ b/src/web/app/common/scripts/check-for-update.ts
@@ -1,16 +1,12 @@
-import CONFIG from './config';
+import MiOS from '../mios';
declare var VERSION: string;
-export default function() {
- fetch(CONFIG.apiUrl + '/meta', {
- method: 'POST'
- }).then(res => {
- res.json().then(meta => {
- if (meta.version != VERSION) {
- localStorage.setItem('should-refresh', 'true');
- alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', VERSION));
- }
- });
- });
+export default async function(mios: MiOS) {
+ const meta = await mios.getMeta();
+
+ if (meta.version != VERSION) {
+ localStorage.setItem('should-refresh', 'true');
+ alert('%i18n:common.update-available%'.replace('{newer}', meta.version).replace('{current}', VERSION));
+ }
}
diff --git a/src/web/app/desktop/router.ts b/src/web/app/desktop/router.ts
index a74299b281..27b63ab2ef 100644
--- a/src/web/app/desktop/router.ts
+++ b/src/web/app/desktop/router.ts
@@ -4,9 +4,10 @@
import * as riot from 'riot';
import * as route from 'page';
+import MiOS from '../common/mios';
let page = null;
-export default me => {
+export default (mios: MiOS) => {
route('/', index);
route('/selectdrive', selectDrive);
route('/i/customize-home', customizeHome);
@@ -22,7 +23,7 @@ export default me => {
route('*', notFound);
function index() {
- me ? home() : entrance();
+ mios.isSignedin ? home() : entrance();
}
function home() {
diff --git a/src/web/app/desktop/script.ts b/src/web/app/desktop/script.ts
index a0453865ec..b4a8d829d6 100644
--- a/src/web/app/desktop/script.ts
+++ b/src/web/app/desktop/script.ts
@@ -12,11 +12,12 @@ import init from '../init';
import route from './router';
import fuckAdBlock from './scripts/fuck-ad-block';
import getPostSummary from '../../../common/get-post-summary';
+import MiOS from '../common/mios';
/**
* init
*/
-init(async (me, stream) => {
+init(async (mios: MiOS) => {
/**
* Fuck AD Block
*/
@@ -32,12 +33,12 @@ init(async (me, stream) => {
}
if ((Notification as any).permission == 'granted') {
- registerNotifications(stream);
+ registerNotifications(mios.stream);
}
}
// Start routing
- route(me);
+ route(mios);
});
function registerNotifications(stream) {
diff --git a/src/web/app/desktop/tags/home-widgets/server.tag b/src/web/app/desktop/tags/home-widgets/server.tag
index ce8c63c976..b37d347361 100644
--- a/src/web/app/desktop/tags/home-widgets/server.tag
+++ b/src/web/app/desktop/tags/home-widgets/server.tag
@@ -62,6 +62,8 @@