diff --git a/src/client/init.ts b/src/client/init.ts index 1fcd97190d..73e02145d5 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -20,6 +20,7 @@ import Menu from './components/menu.vue'; import { router } from './router'; import { applyTheme, lightTheme, builtinThemes } from './theme'; import { isDeviceDarkmode } from './scripts/is-device-darkmode'; +import createStore from './store'; Vue.use(Vuex); Vue.use(VueHotkey); @@ -134,36 +135,38 @@ document.body.setAttribute('ontouchstart', ''); // アプリ基底要素マウント document.body.innerHTML = '
'; -const os = new MiOS(); +const store = createStore(); + +const os = new MiOS(store); os.init(async () => { window.addEventListener('storage', e => { if (e.key === 'vuex') { - os.store.replaceState(JSON.parse(localStorage['vuex'])); + store.replaceState(JSON.parse(localStorage['vuex'])); } else if (e.key === 'i') { location.reload(); } }, false) - os.store.watch(state => state.device.darkMode, darkMode => { + store.watch(state => state.device.darkMode, darkMode => { // TODO: このファイルでbuiltinThemesを参照するとcode splittingが効かず、初回読み込み時に全てのテーマコードを読み込むことになってしまい無駄なので何とかする - const themes = builtinThemes.concat(os.store.state.device.themes); - applyTheme(themes.find(x => x.id === (darkMode ? os.store.state.device.darkTheme : os.store.state.device.lightTheme))); + const themes = builtinThemes.concat(store.state.device.themes); + applyTheme(themes.find(x => x.id === (darkMode ? store.state.device.darkTheme : store.state.device.lightTheme))); }); //#region Sync dark mode - if (os.store.state.device.syncDeviceDarkMode) { - os.store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() }); + if (store.state.device.syncDeviceDarkMode) { + store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() }); } window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { - if (os.store.state.device.syncDeviceDarkMode) { - os.store.commit('device/set', { key: 'darkMode', value: mql.matches }); + if (store.state.device.syncDeviceDarkMode) { + store.commit('device/set', { key: 'darkMode', value: mql.matches }); } }); //#endregion - if ('Notification' in window && os.store.getters.isSignedIn) { + if ('Notification' in window && store.getters.isSignedIn) { // 許可を得ていなかったらリクエスト if (Notification.permission === 'default') { Notification.requestPermission(); @@ -171,7 +174,7 @@ os.init(async () => { } const app = new Vue({ - store: os.store, + store: store, metaInfo: { title: null, titleTemplate: title => title ? `${title} | ${(instanceName || 'Misskey')}` : (instanceName || 'Misskey') @@ -183,7 +186,7 @@ os.init(async () => { }; }, methods: { - api: os.api, + api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }), signout: os.signout, new(vm, props) { const x = new vm({ @@ -234,58 +237,58 @@ os.init(async () => { // マウント app.$mount('#app'); - if (app.$store.getters.isSignedIn) { + if (store.getters.isSignedIn) { const main = os.stream.useSharedConnection('main'); // 自分の情報が更新されたとき main.on('meUpdated', i => { - app.$store.dispatch('mergeMe', i); + store.dispatch('mergeMe', i); }); main.on('readAllNotifications', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadNotification: false }); }); main.on('unreadNotification', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadNotification: true }); }); main.on('unreadMention', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadMentions: true }); }); main.on('readAllUnreadMentions', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadMentions: false }); }); main.on('unreadSpecifiedNote', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadSpecifiedNotes: true }); }); main.on('readAllUnreadSpecifiedNotes', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadSpecifiedNotes: false }); }); main.on('readAllMessagingMessages', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadMessagingMessage: false }); }); main.on('unreadMessagingMessage', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadMessagingMessage: true }); @@ -293,13 +296,13 @@ os.init(async () => { }); main.on('readAllAntennas', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadAntenna: false }); }); main.on('unreadAntenna', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadAntenna: true }); @@ -307,13 +310,13 @@ os.init(async () => { }); main.on('readAllAnnouncements', () => { - app.$store.dispatch('mergeMe', { + store.dispatch('mergeMe', { hasUnreadAnnouncement: false }); }); main.on('clientSettingUpdated', x => { - app.$store.commit('settings/set', { + store.commit('settings/set', { key: x.key, value: x.value }); diff --git a/src/client/mios.ts b/src/client/mios.ts index aa2b202abd..63d9f7d3a0 100644 --- a/src/client/mios.ts +++ b/src/client/mios.ts @@ -2,16 +2,11 @@ import autobind from 'autobind-decorator'; import Vue from 'vue'; import { EventEmitter } from 'eventemitter3'; -import initStore from './store'; import { apiUrl, version } from './config'; import Progress from './scripts/loading'; import Stream from './scripts/stream'; - -//#region api requests -let spinner = null; -let pending = 0; -//#endregion +import store from './store'; /** * Misskey Operating System @@ -19,7 +14,7 @@ let pending = 0; export default class MiOS extends EventEmitter { public app: Vue; - public store: ReturnType; + public store: ReturnType; /** * A connection manager of home stream @@ -31,6 +26,11 @@ export default class MiOS extends EventEmitter { */ private swRegistration: ServiceWorkerRegistration = null; + constructor(vuex: MiOS['store']) { + super(); + this.store = vuex; + } + @autobind public signout() { this.store.dispatch('logout'); @@ -52,8 +52,6 @@ export default class MiOS extends EventEmitter { }); }; - this.store = initStore(this); - // ユーザーをフェッチしてコールバックする const fetchme = (token, cb) => { let me = null; @@ -187,10 +185,13 @@ export default class MiOS extends EventEmitter { } // Register - this.api('sw/register', { - endpoint: subscription.endpoint, - auth: encode(subscription.getKey('auth')), - publickey: encode(subscription.getKey('p256dh')) + this.store.dispatch('api', { + endpoint: 'sw/register', + data: { + endpoint: subscription.endpoint, + auth: encode(subscription.getKey('auth')), + publickey: encode(subscription.getKey('p256dh')) + } }); }) // When subscribe failed @@ -214,52 +215,6 @@ export default class MiOS extends EventEmitter { // Register service worker navigator.serviceWorker.register(sw); } - - /** - * Misskey APIにリクエストします - * @param endpoint エンドポイント名 - * @param data パラメータ - */ - @autobind - public api(endpoint: string, data: { [x: string]: any } = {}, token?): Promise<{ [x: string]: any }> { - if (++pending === 1) { - spinner = document.createElement('div'); - spinner.setAttribute('id', 'wait'); - document.body.appendChild(spinner); - } - - const onFinally = () => { - if (--pending === 0) spinner.parentNode.removeChild(spinner); - }; - - const promise = new Promise((resolve, reject) => { - // Append a credential - if (this.store.getters.isSignedIn) (data as any).i = this.store.state.i.token; - if (token) (data as any).i = token; - - // Send request - fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { - method: 'POST', - body: JSON.stringify(data), - credentials: 'omit', - cache: 'no-cache' - }).then(async (res) => { - const body = res.status === 204 ? null : await res.json(); - - if (res.status === 200) { - resolve(body); - } else if (res.status === 204) { - resolve(); - } else { - reject(body.error); - } - }).catch(reject); - }); - - promise.then(onFinally, onFinally); - - return promise; - } } /** diff --git a/src/client/store.ts b/src/client/store.ts index 35b932d624..6d007493d8 100644 --- a/src/client/store.ts +++ b/src/client/store.ts @@ -1,8 +1,7 @@ import Vuex from 'vuex'; import createPersistedState from 'vuex-persistedstate'; import * as nestedProperty from 'nested-property'; - -import MiOS from './mios'; +import { apiUrl } from './config'; const defaultSettings = { tutorial: 0, @@ -57,13 +56,15 @@ function copy(data: T): T { return JSON.parse(JSON.stringify(data)); } -export default (os: MiOS) => new Vuex.Store({ +export default () => new Vuex.Store({ plugins: [createPersistedState({ paths: ['i', 'device', 'deviceUser', 'settings', 'instance'] })], state: { i: null, + pendingApiRequestsCount: 0, + spinner: null }, getters: { @@ -121,6 +122,47 @@ export default (os: MiOS) => new Vuex.Store({ ctx.commit('settings/init', me.clientData); } }, + + api(ctx, { endpoint, data, token }) { + if (++ctx.state.pendingApiRequestsCount === 1) { + // TODO: spinnerの表示はstoreでやらない + ctx.state.spinner = document.createElement('div'); + ctx.state.spinner.setAttribute('id', 'wait'); + document.body.appendChild(ctx.state.spinner); + } + + const onFinally = () => { + if (--ctx.state.pendingApiRequestsCount === 0) ctx.state.spinner.parentNode.removeChild(ctx.state.spinner); + }; + + const promise = new Promise((resolve, reject) => { + // Append a credential + if (ctx.getters.isSignedIn) (data as any).i = ctx.state.i.token; + if (token) (data as any).i = token; + + // Send request + fetch(endpoint.indexOf('://') > -1 ? endpoint : `${apiUrl}/${endpoint}`, { + method: 'POST', + body: JSON.stringify(data), + credentials: 'omit', + cache: 'no-cache' + }).then(async (res) => { + const body = res.status === 204 ? null : await res.json(); + + if (res.status === 200) { + resolve(body); + } else if (res.status === 204) { + resolve(); + } else { + reject(body.error); + } + }).catch(reject); + }); + + promise.then(onFinally, onFinally); + + return promise; + } }, modules: { @@ -139,9 +181,12 @@ export default (os: MiOS) => new Vuex.Store({ actions: { async fetch(ctx) { - const meta = await os.api('meta', { - detail: false - }); + const meta = await ctx.dispatch('api', { + endpoint: 'meta', + data: { + detail: false + } + }, { root: true }); ctx.commit('set', meta); } @@ -246,10 +291,13 @@ export default (os: MiOS) => new Vuex.Store({ ctx.commit('set', x); if (ctx.rootGetters.isSignedIn) { - os.api('i/update-client-setting', { - name: x.key, - value: x.value - }); + ctx.dispatch('api', { + endpoint: 'i/update-client-setting', + data: { + name: x.key, + value: x.value + } + }, { root: true }); } }, }