/** * Client entry point */ // https://vitejs.dev/config/build-options.html#build-modulepreload import "vite/modulepreload-polyfill"; import "@/style.scss"; import "@phosphor-icons/web/bold"; import "@phosphor-icons/web/fill"; //#region account indexedDB migration import { set } from "@/scripts/idb-proxy"; import { App, computed, createApp, defineAsyncComponent, markRaw, version as vueVersion, watch, } from "vue"; import { compareVersions } from "compare-versions"; import widgets from "@/widgets"; import directives from "@/directives"; import { lang, ui, version } from "@/config"; import { applyTheme } from "@/scripts/theme"; import { isDeviceDarkmode } from "@/scripts/is-device-darkmode"; import { i18n } from "@/i18n"; import { alert, confirm, popup, post, toast } from "@/os"; import { stream } from "@/stream"; import * as sound from "@/scripts/sound"; import { $i, login, refreshAccount, signout, updateAccount } from "@/account"; import { ColdDeviceStorage, defaultStore } from "@/store"; import { fetchInstance, instance } from "@/instance"; import { makeHotkey } from "@/scripts/hotkey"; import { search } from "@/scripts/search"; import { deviceKind } from "@/scripts/device-kind"; import { initializeSw } from "@/scripts/initialize-sw"; import { reloadChannel } from "@/scripts/unison-reload"; import { reactionPicker } from "@/scripts/reaction-picker"; import { getUrlWithoutLoginId } from "@/scripts/login-id"; import { getAccountFromId } from "@/scripts/get-account-from-id"; import Mfm from "./components/global/MkMisskeyFlavoredMarkdown.vue"; import MkA from "./components/global/MkA.vue"; import MkAcct from "./components/global/MkAcct.vue"; import MagAvatar from "./components/global/MagAvatar.vue"; import MagAvatarResolvingProxy from "./components/global/MagAvatarResolvingProxy.vue"; import MagEmoji from "./components/global/MagEmoji.vue"; import MkUserName from "./components/global/MkUserName.vue"; import MkEllipsis from "./components/global/MkEllipsis.vue"; import MkTime from "./components/global/MkTime.vue"; import MkUrl from "./components/global/MkUrl.vue"; import I18n from "./components/global/i18n"; import RouterView from "./components/global/RouterView.vue"; import MkLoading from "./components/global/MkLoading.vue"; import MkError from "./components/global/MkError.vue"; import MkPageHeader from "./components/global/MkPageHeader.vue"; import MkSpacer from "./components/global/MkSpacer.vue"; import MkStickyContainer from "./components/global/MkStickyContainer.vue"; function globalComponents(app: App) { app.component("I18n", I18n); app.component("RouterView", RouterView); app.component("Mfm", Mfm); app.component("MkA", MkA); app.component("MkAcct", MkAcct); app.component("MagAvatar", MagAvatar); app.component("MagEmoji", MagEmoji); app.component("MkUserName", MkUserName); app.component("MkEllipsis", MkEllipsis); app.component("MkTime", MkTime); app.component("MkUrl", MkUrl); app.component("MkLoading", MkLoading); app.component("MkError", MkError); app.component("MkPageHeader", MkPageHeader); app.component("MkSpacer", MkSpacer); app.component("MkStickyContainer", MkStickyContainer); app.component("MagAvatarResolvingProxy", MagAvatarResolvingProxy); } declare module "@vue/runtime-core" { export interface GlobalComponents { I18n: typeof I18n; RouterView: typeof RouterView; Mfm: typeof Mfm; MkA: typeof MkA; MkAcct: typeof MkAcct; MagAvatar: typeof MagAvatar; MagEmoji: typeof MagEmoji; MkUserName: typeof MkUserName; MkEllipsis: typeof MkEllipsis; MkTime: typeof MkTime; MkUrl: typeof MkUrl; MkLoading: typeof MkLoading; MkError: typeof MkError; MkPageHeader: typeof MkPageHeader; MkSpacer: typeof MkSpacer; MkStickyContainer: typeof MkStickyContainer; MagAvatarResolvingProxy: typeof MagAvatarResolvingProxy; } } const accounts = localStorage.getItem("accounts"); if (accounts) { set("accounts", JSON.parse(accounts)); localStorage.removeItem("accounts"); } //#endregion function checkForSplash() { const splash = document.getElementById("splash"); // 念のためnullチェック(HTMLが古い場合があるため(そのうち消す)) if (splash) { splash.style.opacity = "0"; splash.style.pointerEvents = "none"; splash.addEventListener("transitionend", () => { splash.remove(); }); } } (async () => { console.info(`Magnetar v${version}`); if (_DEV_) { console.warn("Development mode!!!"); console.info(`vue ${vueVersion}`); (window as any).$i = $i; (window as any).$store = defaultStore; window.addEventListener("error", (event) => { console.error(event); /* alert({ type: 'error', title: 'DEV: Unhandled error', text: event.message }); */ }); window.addEventListener("unhandledrejection", (event) => { console.error(event); /* alert({ type: 'error', title: 'DEV: Unhandled promise rejection', text: event.reason }); */ }); } // タッチデバイスでCSSの:hoverを機能させる document.addEventListener("touchend", () => {}, { passive: true }); // 一斉リロード reloadChannel.addEventListener("message", (path) => { if (path !== null) location.href = path; else location.reload(); }); //#region SEE: https://css-tricks.com/the-trick-to-viewport-units-on-mobile/ // TODO: いつの日にか消したい const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty("--vh", `${vh}px`); window.addEventListener("resize", () => { const vh = window.innerHeight * 0.01; document.documentElement.style.setProperty("--vh", `${vh}px`); }); //#endregion //#region Set lang attr const html = document.documentElement; html.setAttribute("lang", lang || "en-US"); //#endregion //#region loginId const params = new URLSearchParams(location.search); const loginId = params.get("loginId"); if (loginId) { const target = getUrlWithoutLoginId(location.href); if (!$i || $i.id !== loginId) { const account = await getAccountFromId(loginId); if (account) { await login(account.token, target); } } history.replaceState({ misskey: "loginId" }, "", target); } //#endregion //#region Fetch user if ($i?.token) { if (_DEV_) { console.log("account cache found. refreshing..."); } refreshAccount(); } else { if (_DEV_) { console.log("no account cache found."); } // 連携ログインの場合用にCookieを参照する const i = (document.cookie.match(/igi=(\w+)/) || [null, null])[1]; if (i != null && i !== "null") { if (_DEV_) { console.log("signing..."); } try { document.body.innerHTML = "
Please wait...
"; await login(i); } catch (err) { // Render the error screen // TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな) document.body.innerHTML = '
Oops!
'; } } else { if (_DEV_) { console.log("not signed in"); } } } //#endregion const fetchInstanceMetaPromise = fetchInstance(); fetchInstanceMetaPromise.then(() => { localStorage.setItem("v", instance.version); // Init service worker initializeSw(); }); const app = createApp( window.location.search === "?zen" ? defineAsyncComponent(() => import("@/ui/zen.vue")) : !$i ? defineAsyncComponent(() => import("@/ui/visitor.vue")) : ui === "deck" ? defineAsyncComponent(() => import("@/ui/deck.vue")) : defineAsyncComponent(() => import("@/ui/universal.vue")) ); if (_DEV_) { app.config.performance = true; } app.config.globalProperties = { $i, $store: defaultStore, $instance: instance, $t: i18n.t, $ts: i18n.ts, }; widgets(app); directives(app); globalComponents(app); checkForSplash(); // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 // なぜかinit.tsの内容が2回実行されることがあるため、mountするdivを1つに制限する const rootEl = (() => { const MISSKEY_MOUNT_DIV_ID = "calckey_app"; const currentEl = document.getElementById(MISSKEY_MOUNT_DIV_ID); if (currentEl) { console.warn("multiple import detected"); return currentEl; } const rootEl = document.createElement("div"); rootEl.id = MISSKEY_MOUNT_DIV_ID; document.body.appendChild(rootEl); return rootEl; })(); app.mount(rootEl); // boot.jsのやつを解除 window.onerror = null; window.onunhandledrejection = null; reactionPicker.init(); checkForSplash(); // クライアントが更新されたか? const lastVersion = localStorage.getItem("lastVersion"); if (lastVersion !== version) { localStorage.setItem("lastVersion", version); // テーマリビルドするため localStorage.removeItem("theme"); try { // 変なバージョン文字列来るとcompareVersionsでエラーになるため if ( lastVersion != null && compareVersions(version, lastVersion) === 1 && defaultStore.state.showUpdates ) { // ログインしてる場合だけ if ($i) { popup( defineAsyncComponent( () => import("@/components/MkUpdated.vue") ), {}, {}, "closed" ); } } } catch (err) { console.error(err); } } // NOTE: この処理は必ず↑のクライアント更新時処理より後に来ること(テーマ再構築のため) watch( defaultStore.reactiveState.darkMode, (darkMode) => { applyTheme( darkMode ? ColdDeviceStorage.get("darkTheme") : ColdDeviceStorage.get("lightTheme") ); }, { immediate: localStorage.theme == null } ); const darkTheme = computed(ColdDeviceStorage.makeGetterSetter("darkTheme")); const lightTheme = computed( ColdDeviceStorage.makeGetterSetter("lightTheme") ); watch(darkTheme, (theme) => { if (defaultStore.state.darkMode) { applyTheme(theme); } }); watch(lightTheme, (theme) => { if (!defaultStore.state.darkMode) { applyTheme(theme); } }); //#region Sync dark mode if (ColdDeviceStorage.get("syncDeviceDarkMode")) { defaultStore.set("darkMode", isDeviceDarkmode()); } window.matchMedia("(prefers-color-scheme: dark)").onchange = (mql) => { if (ColdDeviceStorage.get("syncDeviceDarkMode")) { defaultStore.set("darkMode", mql.matches); } }; //#endregion fetchInstanceMetaPromise.then(() => { if (defaultStore.state.themeInitial) { if (instance.defaultLightTheme != null) ColdDeviceStorage.set( "lightTheme", JSON.parse(instance.defaultLightTheme) ); if (instance.defaultDarkTheme != null) ColdDeviceStorage.set( "darkTheme", JSON.parse(instance.defaultDarkTheme) ); defaultStore.set("themeInitial", false); } }); watch( defaultStore.reactiveState.useBlurEffectForModal, (v) => { document.documentElement.style.setProperty( "--modalBgFilter", v ? "blur(4px)" : "none" ); }, { immediate: true } ); watch( defaultStore.reactiveState.useBlurEffect, (v) => { if (v && deviceKind !== "smartphone") { document.documentElement.style.removeProperty("--blur"); } else { document.documentElement.style.setProperty("--blur", "none"); } }, { immediate: true } ); let reloadDialogShowing = false; stream.on("_disconnected_", async () => { if (defaultStore.state.serverDisconnectedBehavior === "reload") { location.reload(); } else if (defaultStore.state.serverDisconnectedBehavior === "dialog") { if (reloadDialogShowing) return; reloadDialogShowing = true; const { canceled } = await confirm({ type: "warning", title: i18n.ts.disconnectedFromServer, text: i18n.ts.reloadConfirm, }); reloadDialogShowing = false; if (!canceled) { location.reload(); } } }); stream.on("emojiAdded", (emojiData) => { // TODO //store.commit('instance/set', ); }); const hotkeys = { d: (): void => { defaultStore.set("darkMode", !defaultStore.state.darkMode); }, s: search, }; if ($i) { // only add post shortcuts if logged in hotkeys["p|n"] = post; if ($i.isDeleted) { alert({ type: "warning", text: i18n.ts.accountDeletionInProgress, }); } const lastUsed = localStorage.getItem("lastUsed"); if (lastUsed) { const lastUsedDate = parseInt(lastUsed, 10); // 二時間以上前なら if (Date.now() - lastUsedDate > 1000 * 60 * 60 * 2) { toast( i18n.t("welcomeBackWithName", { name: $i.name || $i.username, }) ); } } localStorage.setItem("lastUsed", Date.now().toString()); if ("Notification" in window) { // 許可を得ていなかったらリクエスト if (Notification.permission === "default") { Notification.requestPermission(); } } const main = markRaw(stream.useChannel("main", null, "System")); // 自分の情報が更新されたとき main.on("meUpdated", (i) => { updateAccount(i); }); main.on("readAllNotifications", () => { updateAccount({ hasUnreadNotification: false }); }); main.on("unreadNotification", () => { updateAccount({ hasUnreadNotification: true }); }); main.on("unreadMention", () => { updateAccount({ hasUnreadMentions: true }); }); main.on("readAllUnreadMentions", () => { updateAccount({ hasUnreadMentions: false }); }); main.on("unreadSpecifiedNote", () => { updateAccount({ hasUnreadSpecifiedNotes: true }); }); main.on("readAllUnreadSpecifiedNotes", () => { updateAccount({ hasUnreadSpecifiedNotes: false }); }); main.on("readAllAntennas", () => { updateAccount({ hasUnreadAntenna: false }); }); main.on("unreadAntenna", () => { updateAccount({ hasUnreadAntenna: true }); sound.play("antenna"); }); main.on("readAllAnnouncements", () => { updateAccount({ hasUnreadAnnouncement: false }); }); // トークンが再生成されたとき // このままではMisskeyが利用できないので強制的にサインアウトさせる main.on("myTokenRegenerated", () => { signout(); }); } // shortcut document.addEventListener("keydown", makeHotkey(hotkeys)); })();