summaryrefslogtreecommitdiff
path: root/src/client/init.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/init.ts')
-rw-r--r--src/client/init.ts559
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();
+ });
+}
+