diff options
Diffstat (limited to 'src/client/init.ts')
| -rw-r--r-- | src/client/init.ts | 559 |
1 files changed, 257 insertions, 302 deletions
diff --git a/src/client/init.ts b/src/client/init.ts index 3931329aa5..96e8e90552 100644 --- a/src/client/init.ts +++ b/src/client/init.ts @@ -2,62 +2,56 @@ * Client entry point */ -import Vue from 'vue'; -import Vuex from 'vuex'; -import VueMeta from 'vue-meta'; -import PortalVue from 'portal-vue'; -import VAnimateCss from 'v-animate-css'; -import VueI18n from 'vue-i18n'; +import '@/style.scss'; + +import { createApp } from 'vue'; import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome'; -import { AiScript } from '@syuilo/aiscript'; -import { deserialize } from '@syuilo/aiscript/built/serializer'; -import VueHotkey from './scripts/hotkey'; -import App from './app.vue'; -import Deck from './deck.vue'; -import MiOS from './mios'; -import { version, langs, instanceName, getLocale, deckmode } from './config'; -import PostFormDialog from './components/post-form-dialog.vue'; -import Dialog from './components/dialog.vue'; -import Menu from './components/menu.vue'; -import Form from './components/form-window.vue'; +import Root from './root.vue'; +import widgets from './widgets'; +import directives from './directives'; +import components from '@/components'; +import { version, apiUrl } from '@/config'; +import { store } from './store'; import { router } from './router'; -import { applyTheme, lightTheme } from './scripts/theme'; -import { isDeviceDarkmode } from './scripts/is-device-darkmode'; -import createStore from './store'; -import { clientDb, get, count } from './db'; -import { setI18nContexts } from './scripts/set-i18n-contexts'; -import { createPluginEnv } from './scripts/aiscript/api'; +import { applyTheme } from '@/scripts/theme'; +import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; +import { i18n, lang } from './i18n'; +import { stream, sound, isMobile, dialog } from '@/os'; -Vue.use(Vuex); -Vue.use(VueHotkey); -Vue.use(VueMeta); -Vue.use(PortalVue); -Vue.use(VAnimateCss); -Vue.use(VueI18n); -Vue.component('fa', FontAwesomeIcon); +console.info(`Misskey v${version}`); -require('./directives'); -require('./components'); -require('./widgets'); -require('./filters'); +if (_DEV_) { + console.warn('Development mode!!!'); -Vue.mixin({ - methods: { - destroyDom() { - this.$destroy(); + window.addEventListener('error', event => { + console.error(event); + /* + dialog({ + type: 'error', + title: 'DEV: Unhandled error', + text: event.message + }); + */ + }); - if (this.$el.parentNode) { - this.$el.parentNode.removeChild(this.$el); - } - } - } -}); + window.addEventListener('unhandledrejection', event => { + console.error(event); + /* + dialog({ + type: 'error', + title: 'DEV: Unhandled promise rejection', + text: event.reason + }); + */ + }); +} -console.info(`Misskey v${version}`); +// タッチデバイスでCSSの:hoverを機能させる +document.addEventListener('touchend', () => {}, { passive: true }); if (localStorage.getItem('theme') == null) { - applyTheme(lightTheme); + applyTheme(require('@/themes/white.json5')); } //#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ @@ -70,29 +64,6 @@ window.addEventListener('resize', () => { }); //#endregion -//#region Detect the user language -let lang = localStorage.getItem('lang'); - -if (lang == null) { - if (langs.map(x => x[0]).includes(navigator.language)) { - lang = navigator.language; - } else { - lang = langs.map(x => x[0]).find(x => x.split('-')[0] == navigator.language); - - if (lang == null) { - // Fallback - lang = 'en-US'; - } - } - - localStorage.setItem('lang', lang); -} -//#endregion - -// Detect the user agent -const ua = navigator.userAgent.toLowerCase(); -const isMobile = /mobile|iphone|ipad|android/.test(ua); - // Get the <head> element const head = document.getElementsByTagName('head')[0]; @@ -109,10 +80,99 @@ const html = document.documentElement; html.setAttribute('lang', lang); //#endregion -// アプリ基底要素マウント -document.body.innerHTML = '<div id="app"></div>'; +//#region Fetch user +const signout = () => { + store.dispatch('logout'); + location.href = '/'; +}; + +// ユーザーをフェッチしてコールバックする +const fetchme = (token) => new Promise((done, fail) => { + // Fetch user + fetch(`${apiUrl}/i`, { + method: 'POST', + body: JSON.stringify({ + i: token + }) + }) + .then(res => { + // When failed to authenticate user + if (res.status !== 200 && res.status < 500) { + return signout(); + } + + // Parse response + res.json().then(i => { + i.token = token; + done(i); + }); + }) + .catch(fail); +}); + +// キャッシュがあったとき +if (store.state.i != null) { + // TODO: i.token が null になるケースってどんな時だっけ? + if (store.state.i.token == null) { + signout(); + } + + // 後から新鮮なデータをフェッチ + fetchme(store.state.i.token).then(freshData => { + store.dispatch('mergeMe', freshData); + }); +} else { + // Get token from localStorage + let i = localStorage.getItem('i'); + + // 連携ログインの場合用にCookieを参照する + if (i == null || i === 'null') { + i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1]; + } + + if (i != null && i !== 'null') { + try { + document.body.innerHTML = '<div>Please wait...</div>'; + const me = await fetchme(i); + await store.dispatch('login', me); + location.reload(); + } catch (e) { + // Render the error screen + // TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな) + document.body.innerHTML = '<div id="err">Oops!</div>'; + } + } +} +//#endregion + +store.dispatch('instance/fetch').then(() => { + // Init service worker + //if (this.store.state.instance.meta.swPublickey) this.registerSw(this.store.state.instance.meta.swPublickey); +}); + +stream.init(store.state.i); + +const app = createApp(Root); + +if (_DEV_) { + app.config.performance = true; +} + +app.use(store); +app.use(router); +app.use(i18n); +// eslint-disable-next-line vue/component-definition-name-casing +app.component('Fa', FontAwesomeIcon); -const store = createStore(); +widgets(app); +directives(app); +components(app); + +await router.isReady(); + +//document.body.innerHTML = '<div id="app"></div>'; + +app.mount('body'); // 他のタブと永続化されたstateを同期 window.addEventListener('storage', e => { @@ -126,281 +186,176 @@ window.addEventListener('storage', e => { } }, false); -const os = new MiOS(store); - -os.init(async () => { - //#region Fetch locale data - const i18n = new VueI18n(); - - await count(clientDb.i18n).then(async n => { - if (n === 0) return setI18nContexts(lang, version, i18n); - if ((await get('_version_', clientDb.i18n) !== version)) return setI18nContexts(lang, version, i18n, true); - - i18n.locale = lang; - i18n.setLocaleMessage(lang, await getLocale()); +store.watch(state => state.device.darkMode, darkMode => { + import('@/scripts/theme').then(({ builtinThemes }) => { + const themes = builtinThemes.concat(store.state.device.themes); + applyTheme(themes.find(x => x.id === (darkMode ? store.state.device.darkTheme : store.state.device.lightTheme))); }); - //#endregion - - const app = new Vue({ - store: store, - i18n, - metaInfo: { - title: null, - titleTemplate: title => title ? `${title} | ${(instanceName || 'Misskey')}` : (instanceName || 'Misskey') - }, - data() { - return { - stream: os.stream, - isMobile: isMobile, - i18n // TODO: 消せないか考える SEE: https://github.com/syuilo/misskey/pull/6396#discussion_r429511030 - }; - }, - // TODO: ここらへんのメソッド全部Vuexに移したい - methods: { - api: (endpoint: string, data: { [x: string]: any } = {}, token?) => store.dispatch('api', { endpoint, data, token }), - signout: os.signout, - new(vm, props) { - const x = new vm({ - parent: this, - propsData: props - }).$mount(); - document.body.appendChild(x.$el); - return x; - }, - dialog(opts) { - const vm = this.new(Dialog, opts); - const p: any = new Promise((res) => { - vm.$once('ok', result => res({ canceled: false, result })); - vm.$once('cancel', () => res({ canceled: true })); - }); - p.close = () => { - vm.close(); - }; - return p; - }, - menu(opts) { - const vm = this.new(Menu, opts); - const p: any = new Promise((res) => { - vm.$once('closed', () => res()); - }); - return p; - }, - form(title, form) { - const vm = this.new(Form, { title, form }); - return new Promise((res) => { - vm.$once('ok', result => res({ canceled: false, result })); - vm.$once('cancel', () => res({ canceled: true })); - }); - }, - post(opts, cb) { - if (!this.$store.getters.isSignedIn) return; - const vm = this.new(PostFormDialog, opts); - if (cb) vm.$once('closed', cb); - (vm as any).focus(); - }, - sound(type: string) { - if (this.$store.state.device.sfxVolume === 0) return; - const sound = this.$store.state.device['sfx' + type.substr(0, 1).toUpperCase() + type.substr(1)]; - if (sound == null) return; - const audio = new Audio(`/assets/sounds/${sound}.mp3`); - audio.volume = this.$store.state.device.sfxVolume; - audio.play(); - } - }, - router: router, - render: createEl => createEl(deckmode ? Deck : App) - }); - - // マウント - app.$mount('#app'); +}); - store.watch(state => state.device.darkMode, darkMode => { - import('./scripts/theme').then(({ builtinThemes }) => { - 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 (store.state.device.syncDeviceDarkMode) { + store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() }); +} - //#region Sync dark mode +window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { if (store.state.device.syncDeviceDarkMode) { - store.commit('device/set', { key: 'darkMode', value: isDeviceDarkmode() }); + store.commit('device/set', { key: 'darkMode', value: mql.matches }); } +}); +//#endregion - window.matchMedia('(prefers-color-scheme: dark)').addListener(mql => { - if (store.state.device.syncDeviceDarkMode) { - store.commit('device/set', { key: 'darkMode', value: mql.matches }); - } - }); - //#endregion - - store.watch(state => state.device.useBlurEffectForModal, v => { - document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); - }, { immediate: true }); +store.watch(state => state.device.useBlurEffectForModal, v => { + document.documentElement.style.setProperty('--modalBgFilter', v ? 'blur(4px)' : 'none'); +}, { immediate: true }); - let reloadDialogShowing = false; - os.stream.on('_disconnected_', async () => { - if (store.state.device.serverDisconnectedBehavior === 'reload') { +let reloadDialogShowing = false; +stream.on('_disconnected_', async () => { + if (store.state.device.serverDisconnectedBehavior === 'reload') { + location.reload(); + } else if (store.state.device.serverDisconnectedBehavior === 'dialog') { + if (reloadDialogShowing) return; + reloadDialogShowing = true; + const { canceled } = await dialog({ + type: 'warning', + title: i18n.global.t('disconnectedFromServer'), + text: i18n.global.t('reloadConfirm'), + showCancelButton: true + }); + reloadDialogShowing = false; + if (!canceled) { location.reload(); - } else if (store.state.device.serverDisconnectedBehavior === 'dialog') { - if (reloadDialogShowing) return; - reloadDialogShowing = true; - const { canceled } = await app.dialog({ - type: 'warning', - title: app.$t('disconnectedFromServer'), - text: app.$t('reloadConfirm'), - showCancelButton: true - }); - reloadDialogShowing = false; - if (!canceled) { - location.reload(); - } } - }); - - os.stream.on('emojiAdded', data => { - // TODO - //store.commit('instance/set', ); - }); - - for (const plugin of store.state.deviceUser.plugins.filter(p => p.active)) { - console.info('Plugin installed:', plugin.name, 'v' + plugin.version); + } +}); - const aiscript = new AiScript(createPluginEnv(app, { - plugin: plugin, - storageKey: 'plugins:' + plugin.id - }), { - in: (q) => { - return new Promise(ok => { - app.dialog({ - title: q, - input: {} - }).then(({ canceled, result: a }) => { - ok(a); - }); - }); - }, - out: (value) => { - console.log(value); - }, - log: (type, params) => { - }, - }); +stream.on('emojiAdded', data => { + // TODO + //store.commit('instance/set', ); +}); - store.commit('initPlugin', { plugin, aiscript }); +for (const plugin of store.state.deviceUser.plugins.filter(p => p.active)) { + import('./plugin').then(({ install }) => { + install(plugin); + }); +} - aiscript.exec(deserialize(plugin.ast)); +if (store.getters.isSignedIn) { + if ('Notification' in window) { + // 許可を得ていなかったらリクエスト + if (Notification.permission === 'default') { + Notification.requestPermission(); + } } - if (store.getters.isSignedIn) { - if ('Notification' in window) { - // 許可を得ていなかったらリクエスト - if (Notification.permission === 'default') { - Notification.requestPermission(); - } - } + const main = stream.useSharedConnection('main'); - const main = os.stream.useSharedConnection('main'); + // 自分の情報が更新されたとき + main.on('meUpdated', i => { + store.dispatch('mergeMe', i); + }); - // 自分の情報が更新されたとき - main.on('meUpdated', i => { - store.dispatch('mergeMe', i); + main.on('readAllNotifications', () => { + store.dispatch('mergeMe', { + hasUnreadNotification: false }); + }); - main.on('readAllNotifications', () => { - store.dispatch('mergeMe', { - hasUnreadNotification: false - }); + main.on('unreadNotification', () => { + store.dispatch('mergeMe', { + hasUnreadNotification: true }); + }); - main.on('unreadNotification', () => { - store.dispatch('mergeMe', { - hasUnreadNotification: true - }); + main.on('unreadMention', () => { + store.dispatch('mergeMe', { + hasUnreadMentions: true }); + }); - main.on('unreadMention', () => { - store.dispatch('mergeMe', { - hasUnreadMentions: true - }); + main.on('readAllUnreadMentions', () => { + store.dispatch('mergeMe', { + hasUnreadMentions: false }); + }); - main.on('readAllUnreadMentions', () => { - store.dispatch('mergeMe', { - hasUnreadMentions: false - }); + main.on('unreadSpecifiedNote', () => { + store.dispatch('mergeMe', { + hasUnreadSpecifiedNotes: true }); + }); - main.on('unreadSpecifiedNote', () => { - store.dispatch('mergeMe', { - hasUnreadSpecifiedNotes: true - }); + main.on('readAllUnreadSpecifiedNotes', () => { + store.dispatch('mergeMe', { + hasUnreadSpecifiedNotes: false }); + }); - main.on('readAllUnreadSpecifiedNotes', () => { - store.dispatch('mergeMe', { - hasUnreadSpecifiedNotes: false - }); + main.on('readAllMessagingMessages', () => { + store.dispatch('mergeMe', { + hasUnreadMessagingMessage: false }); + }); - main.on('readAllMessagingMessages', () => { - store.dispatch('mergeMe', { - hasUnreadMessagingMessage: false - }); + main.on('unreadMessagingMessage', () => { + store.dispatch('mergeMe', { + hasUnreadMessagingMessage: true }); - main.on('unreadMessagingMessage', () => { - store.dispatch('mergeMe', { - hasUnreadMessagingMessage: true - }); + sound('chatBg'); + }); - app.sound('chatBg'); + main.on('readAllAntennas', () => { + store.dispatch('mergeMe', { + hasUnreadAntenna: false }); + }); - main.on('readAllAntennas', () => { - store.dispatch('mergeMe', { - hasUnreadAntenna: false - }); + main.on('unreadAntenna', () => { + store.dispatch('mergeMe', { + hasUnreadAntenna: true }); - main.on('unreadAntenna', () => { - store.dispatch('mergeMe', { - hasUnreadAntenna: true - }); + sound('antenna'); + }); - app.sound('antenna'); + main.on('readAllAnnouncements', () => { + store.dispatch('mergeMe', { + hasUnreadAnnouncement: false }); + }); - main.on('readAllChannels', () => { - store.dispatch('mergeMe', { - hasUnreadChannel: false - }); + main.on('readAllChannels', () => { + store.dispatch('mergeMe', { + hasUnreadChannel: false }); + }); - main.on('unreadChannel', () => { - store.dispatch('mergeMe', { - hasUnreadChannel: true - }); - - app.sound('channel'); + main.on('unreadChannel', () => { + store.dispatch('mergeMe', { + hasUnreadChannel: true }); - main.on('readAllAnnouncements', () => { - store.dispatch('mergeMe', { - hasUnreadAnnouncement: false - }); - }); + sound('channel'); + }); - main.on('clientSettingUpdated', x => { - store.commit('settings/set', { - key: x.key, - value: x.value - }); + main.on('readAllAnnouncements', () => { + store.dispatch('mergeMe', { + hasUnreadAnnouncement: false }); + }); - // トークンが再生成されたとき - // このままではMisskeyが利用できないので強制的にサインアウトさせる - main.on('myTokenRegenerated', () => { - os.signout(); + main.on('clientSettingUpdated', x => { + store.commit('settings/set', { + key: x.key, + value: x.value }); - } -}); + }); + + // トークンが再生成されたとき + // このままではMisskeyが利用できないので強制的にサインアウトさせる + main.on('myTokenRegenerated', () => { + signout(); + }); +} + |