forked from naskya/firefish
543 lines
13 KiB
TypeScript
543 lines
13 KiB
TypeScript
/**
|
|
* Client entry point
|
|
*/
|
|
|
|
// https://vitejs.dev/config/build-options.html#build-modulepreload
|
|
import "vite/modulepreload-polyfill";
|
|
|
|
import "@/style.scss";
|
|
|
|
import "@phosphor-icons/web/fill";
|
|
import "@phosphor-icons/web/bold";
|
|
import "@phosphor-icons/web/regular";
|
|
import "@phosphor-icons/web/light";
|
|
import "@phosphor-icons/web/duotone";
|
|
|
|
// #region account indexedDB migration
|
|
import { set } from "@/scripts/idb-proxy";
|
|
|
|
const accounts = localStorage.getItem("accounts");
|
|
if (accounts) {
|
|
set("accounts", JSON.parse(accounts));
|
|
localStorage.removeItem("accounts");
|
|
}
|
|
// #endregion
|
|
|
|
import {
|
|
computed,
|
|
createApp,
|
|
defineAsyncComponent,
|
|
markRaw,
|
|
version as vueVersion,
|
|
watch,
|
|
} from "vue";
|
|
|
|
import widgets from "@/widgets";
|
|
import directives from "@/directives";
|
|
import components from "@/components";
|
|
import { host, lang, ui, version } from "@/config";
|
|
import { applyTheme } from "@/scripts/theme";
|
|
import { isDeviceDarkmode } from "@/scripts/is-device-darkmode";
|
|
import { i18n } from "@/i18n";
|
|
import { alert, api, 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";
|
|
|
|
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(`Firefish v${version}`);
|
|
|
|
if (_DEV_) {
|
|
console.warn("Development mode!!!");
|
|
|
|
console.info(`vue ${vueVersion}`);
|
|
|
|
(window as any).$i = $i;
|
|
|
|
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 = "<div>Please wait...</div>";
|
|
await login(i);
|
|
} catch (err) {
|
|
// Render the error screen
|
|
// TODO: ちゃんとしたコンポーネントをレンダリングする(v10とかのトラブルシューティングゲーム付きのやつみたいな)
|
|
document.body.innerHTML = '<div id="err">Oops!</div>';
|
|
}
|
|
} 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,
|
|
$instance: instance,
|
|
};
|
|
|
|
widgets(app);
|
|
directives(app);
|
|
components(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 = "firefish_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");
|
|
|
|
if ($i && defaultStore.state.showUpdates) {
|
|
popup(
|
|
defineAsyncComponent(() => import("@/components/MkUpdated.vue")),
|
|
{},
|
|
{},
|
|
"closed",
|
|
);
|
|
}
|
|
}
|
|
|
|
if (
|
|
$i &&
|
|
defaultStore.state.tutorial === -1 &&
|
|
!["/announcements", "/announcements/"].includes(window.location.pathname)
|
|
) {
|
|
api("announcements", { withUnreads: true, limit: 10 })
|
|
.then((announcements) => {
|
|
const unreadAnnouncements = announcements.filter((item) => {
|
|
return !item.isRead;
|
|
});
|
|
if (unreadAnnouncements.length > 3) {
|
|
popup(
|
|
defineAsyncComponent(
|
|
() => import("@/components/MkManyAnnouncements.vue"),
|
|
),
|
|
{},
|
|
{},
|
|
"closed",
|
|
);
|
|
} else {
|
|
unreadAnnouncements.forEach((item) => {
|
|
if (item.showPopup)
|
|
popup(
|
|
defineAsyncComponent(
|
|
() => import("@/components/MkAnnouncement.vue"),
|
|
),
|
|
{ announcement: item },
|
|
{},
|
|
"closed",
|
|
);
|
|
});
|
|
}
|
|
})
|
|
.catch((err) => console.log(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', );
|
|
});
|
|
|
|
for (const plugin of ColdDeviceStorage.get("plugins").filter(
|
|
(p) => p.active,
|
|
)) {
|
|
import("./plugin").then(({ install }) => {
|
|
install(plugin);
|
|
});
|
|
}
|
|
|
|
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,
|
|
});
|
|
}
|
|
|
|
if (!defaultStore.state.disableToast) {
|
|
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());
|
|
}
|
|
|
|
const latestDonationInfoShownAt = localStorage.getItem(
|
|
"latestDonationInfoShownAt",
|
|
);
|
|
const neverShowDonationInfo = localStorage.getItem("neverShowDonationInfo");
|
|
if (
|
|
neverShowDonationInfo !== "true" &&
|
|
new Date($i.createdAt).getTime() < Date.now() - 1000 * 60 * 60 * 24 * 3 &&
|
|
!location.pathname.startsWith("/miauth")
|
|
) {
|
|
if (
|
|
latestDonationInfoShownAt == null ||
|
|
new Date(latestDonationInfoShownAt).getTime() <
|
|
Date.now() - 1000 * 60 * 60 * 24 * 30
|
|
) {
|
|
popup(
|
|
defineAsyncComponent(() => import("@/components/MkDonation.vue")),
|
|
{},
|
|
{},
|
|
"closed",
|
|
);
|
|
}
|
|
}
|
|
|
|
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("readAllMessagingMessages", () => {
|
|
updateAccount({ hasUnreadMessagingMessage: false });
|
|
});
|
|
|
|
main.on("unreadMessagingMessage", () => {
|
|
updateAccount({ hasUnreadMessagingMessage: true });
|
|
sound.play("chatBg");
|
|
});
|
|
|
|
main.on("readAllAntennas", () => {
|
|
updateAccount({ hasUnreadAntenna: false });
|
|
});
|
|
|
|
main.on("unreadAntenna", () => {
|
|
updateAccount({ hasUnreadAntenna: true });
|
|
sound.play("antenna");
|
|
});
|
|
|
|
main.on("readAllAnnouncements", () => {
|
|
updateAccount({ hasUnreadAnnouncement: false });
|
|
});
|
|
|
|
main.on("readAllChannels", () => {
|
|
updateAccount({ hasUnreadChannel: false });
|
|
});
|
|
|
|
main.on("unreadChannel", () => {
|
|
updateAccount({ hasUnreadChannel: true });
|
|
sound.play("channel");
|
|
});
|
|
|
|
// トークンが再生成されたとき
|
|
// このままではMisskeyが利用できないので強制的にサインアウトさせる
|
|
main.on("myTokenRegenerated", () => {
|
|
signout();
|
|
});
|
|
}
|
|
|
|
// shortcut
|
|
document.addEventListener("keydown", makeHotkey(hotkeys));
|
|
})();
|