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 @@