From 23f106a0c1d16e821a712d39be1c90d17cd6c901 Mon Sep 17 00:00:00 2001 From: syuilo <Syuilotan@yahoo.co.jp> Date: Mon, 15 May 2023 19:08:46 +0900 Subject: [PATCH] =?UTF-8?q?refactor(frontend):=20boot=E5=88=86=E5=89=B2?= =?UTF-8?q?=E3=81=97=E3=81=9F=E3=82=8A=E5=89=AF=E4=BD=9C=E7=94=A8=E6=B8=9B?= =?UTF-8?q?=E3=82=89=E3=81=97=E3=81=9F=E3=82=8A=E3=81=A8=E3=81=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit #10838 --- packages/backend/src/config.ts | 4 +- packages/frontend/src/_boot_.ts | 12 + packages/frontend/src/boot/common.ts | 263 +++++++++ packages/frontend/src/boot/main-boot.ts | 254 +++++++++ packages/frontend/src/boot/sub-boot.ts | 8 + packages/frontend/src/components/MkDrive.vue | 4 +- .../src/components/MkFollowButton.vue | 4 +- .../src/components/MkNotifications.vue | 6 +- .../frontend/src/components/MkTimeline.vue | 4 +- packages/frontend/src/custom-emojis.ts | 13 +- packages/frontend/src/init.ts | 523 ------------------ .../src/pages/admin/overview.queue.vue | 4 +- .../frontend/src/pages/admin/overview.vue | 4 +- .../frontend/src/pages/admin/queue.chart.vue | 4 +- .../pages/settings/preferences-backups.vue | 4 +- packages/frontend/src/pizzax.ts | 8 +- packages/frontend/src/scripts/select-file.ts | 4 +- .../frontend/src/scripts/use-note-capture.ts | 4 +- packages/frontend/src/stream.ts | 14 +- packages/frontend/src/ui/_common_/common.vue | 6 +- .../src/ui/_common_/stream-indicator.vue | 6 +- packages/frontend/src/ui/minimum.vue | 34 ++ .../frontend/src/widgets/WidgetJobQueue.vue | 4 +- .../frontend/src/widgets/WidgetPhotos.vue | 4 +- .../src/widgets/server-metric/index.vue | 4 +- packages/frontend/vite.config.ts | 2 +- 26 files changed, 628 insertions(+), 573 deletions(-) create mode 100644 packages/frontend/src/_boot_.ts create mode 100644 packages/frontend/src/boot/common.ts create mode 100644 packages/frontend/src/boot/main-boot.ts create mode 100644 packages/frontend/src/boot/sub-boot.ts delete mode 100644 packages/frontend/src/init.ts create mode 100644 packages/frontend/src/ui/minimum.vue diff --git a/packages/backend/src/config.ts b/packages/backend/src/config.ts index c6e1075389..744f999596 100644 --- a/packages/backend/src/config.ts +++ b/packages/backend/src/config.ts @@ -144,7 +144,7 @@ export function loadConfig() { const clientManifestExists = fs.existsSync(_dirname + '/../../../built/_vite_/manifest.json'); const clientManifest = clientManifestExists ? JSON.parse(fs.readFileSync(`${_dirname}/../../../built/_vite_/manifest.json`, 'utf-8')) - : { 'src/init.ts': { file: 'src/init.ts' } }; + : { 'src/_boot_.ts': { file: 'src/_boot_.ts' } }; const config = yaml.load(fs.readFileSync(path, 'utf-8')) as Source; const mixin = {} as Mixin; @@ -165,7 +165,7 @@ export function loadConfig() { mixin.authUrl = `${mixin.scheme}://${mixin.host}/auth`; mixin.driveUrl = `${mixin.scheme}://${mixin.host}/files`; mixin.userAgent = `Misskey/${meta.version} (${config.url})`; - mixin.clientEntry = clientManifest['src/init.ts']; + mixin.clientEntry = clientManifest['src/_boot_.ts']; mixin.clientManifestExists = clientManifestExists; const externalMediaProxy = config.mediaProxy ? diff --git a/packages/frontend/src/_boot_.ts b/packages/frontend/src/_boot_.ts new file mode 100644 index 0000000000..a8efafca61 --- /dev/null +++ b/packages/frontend/src/_boot_.ts @@ -0,0 +1,12 @@ +// https://vitejs.dev/config/build-options.html#build-modulepreload +import 'vite/modulepreload-polyfill'; + +import '@/style.scss'; +import { mainBoot } from './boot/main-boot'; +import { subBoot } from './boot/sub-boot'; + +if (['/share', '/auth', '/miauth'].includes(location.pathname)) { + subBoot(); +} else { + mainBoot(); +} diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts new file mode 100644 index 0000000000..e9d6586f85 --- /dev/null +++ b/packages/frontend/src/boot/common.ts @@ -0,0 +1,263 @@ +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent, App } from 'vue'; +import { compareVersions } from 'compare-versions'; +import JSON5 from 'json5'; +import widgets from '@/widgets'; +import directives from '@/directives'; +import components from '@/components'; +import { version, ui, lang, updateLocale } from '@/config'; +import { applyTheme } from '@/scripts/theme'; +import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; +import { i18n, updateI18n } from '@/i18n'; +import { confirm, alert, post, popup, toast } from '@/os'; +import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; +import { defaultStore, ColdDeviceStorage } from '@/store'; +import { fetchInstance, instance } from '@/instance'; +import { deviceKind } from '@/scripts/device-kind'; +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 { deckStore } from '@/ui/deck/deck-store'; +import { miLocalStorage } from '@/local-storage'; +import { fetchCustomEmojis } from '@/custom-emojis'; +import { mainRouter } from '@/router'; + +export async function common(createVue: () => App<Element>) { + console.info(`Misskey v${version}`); + + if (_DEV_) { + console.warn('Development mode!!!'); + + console.info(`vue ${vueVersion}`); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (window as any).$i = $i; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (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 + }); + */ + }); + } + + const splash = document.getElementById('splash'); + // 念ã®ãŸã‚nullãƒã‚§ãƒƒã‚¯(HTMLãŒå¤ã„å ´åˆãŒã‚ã‚‹ãŸã‚(ãã®ã†ã¡æ¶ˆã™)) + if (splash) splash.addEventListener('transitionend', () => { + splash.remove(); + }); + + let isClientUpdated = false; + + //#region クライアントãŒæ›´æ–°ã•ã‚ŒãŸã‹ãƒã‚§ãƒƒã‚¯ + const lastVersion = miLocalStorage.getItem('lastVersion'); + if (lastVersion !== version) { + miLocalStorage.setItem('lastVersion', version); + + // テーマリビルドã™ã‚‹ãŸã‚ + miLocalStorage.removeItem('theme'); + + try { // 変ãªãƒãƒ¼ã‚¸ãƒ§ãƒ³æ–‡å—列æ¥ã‚‹ã¨compareVersionsã§ã‚¨ãƒ©ãƒ¼ã«ãªã‚‹ãŸã‚ + if (lastVersion != null && compareVersions(version, lastVersion) === 1) { + isClientUpdated = true; + } + } catch (err) { /* empty */ } + } + //#endregion + + //#region Detect language & fetch translations + const localeVersion = miLocalStorage.getItem('localeVersion'); + const localeOutdated = (localeVersion == null || localeVersion !== version); + if (localeOutdated) { + const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); + if (res.status === 200) { + const newLocale = await res.text(); + const parsedNewLocale = JSON.parse(newLocale); + miLocalStorage.setItem('locale', newLocale); + miLocalStorage.setItem('localeVersion', version); + updateLocale(parsedNewLocale); + updateI18n(parsedNewLocale); + } + } + //#endregion + + // タッãƒãƒ‡ãƒã‚¤ã‚¹ã§CSSã®:hoverを機能ã•ã›ã‚‹ + document.addEventListener('touchend', () => {}, { passive: true }); + + // 一斉リãƒãƒ¼ãƒ‰ + reloadChannel.addEventListener('message', path => { + if (path !== null) location.href = path; + else location.reload(); + }); + + // If mobile, insert the viewport meta tag + if (['smartphone', 'tablet'].includes(deviceKind)) { + const viewport = document.getElementsByName('viewport').item(0); + viewport.setAttribute('content', + `${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`); + } + + //#region Set lang attr + const html = document.documentElement; + html.setAttribute('lang', lang); + //#endregion + + await defaultStore.ready; + await deckStore.ready; + + const fetchInstanceMetaPromise = fetchInstance(); + + fetchInstanceMetaPromise.then(() => { + miLocalStorage.setItem('v', instance.version); + }); + + //#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 + + // NOTE: ã“ã®å‡¦ç†ã¯å¿…ãšã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆæ›´æ–°ãƒã‚§ãƒƒã‚¯å‡¦ç†ã‚ˆã‚Šå¾Œã«æ¥ã‚‹ã“ã¨(テーマå†æ§‹ç¯‰ã®ãŸã‚) + watch(defaultStore.reactiveState.darkMode, (darkMode) => { + applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); + }, { immediate: miLocalStorage.getItem('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)').addListener(mql => { + if (ColdDeviceStorage.get('syncDeviceDarkMode')) { + defaultStore.set('darkMode', mql.matches); + } + }); + //#endregion + + fetchInstanceMetaPromise.then(() => { + if (defaultStore.state.themeInitial) { + if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme)); + if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.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) { + document.documentElement.style.removeProperty('--blur'); + } else { + document.documentElement.style.setProperty('--blur', 'none'); + } + }, { immediate: true }); + + //#region Fetch user + if ($i && $i.token) { + if (_DEV_) { + console.log('account cache found. refreshing...'); + } + + refreshAccount(); + } + //#endregion + + try { + await fetchCustomEmojis(); + } catch (err) { /* empty */ } + + const app = createVue(); + + if (_DEV_) { + app.config.performance = true; + } + + widgets(app); + directives(app); + components(app); + + // https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 + // ãªãœã‹2回実行ã•ã‚Œã‚‹ã“ã¨ãŒã‚ã‚‹ãŸã‚ã€mountã™ã‚‹divã‚’1ã¤ã«åˆ¶é™ã™ã‚‹ + const rootEl = ((): HTMLElement => { + const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; + + const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID); + + if (currentRoot) { + console.warn('multiple import detected'); + return currentRoot; + } + + const root = document.createElement('div'); + root.id = MISSKEY_MOUNT_DIV_ID; + document.body.appendChild(root); + return root; + })(); + + app.mount(rootEl); + + // boot.jsã®ã‚„ã¤ã‚’解除 + window.onerror = null; + window.onunhandledrejection = null; + + removeSplash(); + + return { + isClientUpdated, + app, + }; +} + +function removeSplash() { + const splash = document.getElementById('splash'); + if (splash) { + splash.style.opacity = '0'; + splash.style.pointerEvents = 'none'; + } +} diff --git a/packages/frontend/src/boot/main-boot.ts b/packages/frontend/src/boot/main-boot.ts new file mode 100644 index 0000000000..c0bfa4603e --- /dev/null +++ b/packages/frontend/src/boot/main-boot.ts @@ -0,0 +1,254 @@ +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; +import { common } from './common'; +import { version, ui, lang, updateLocale } from '@/config'; +import { i18n, updateI18n } from '@/i18n'; +import { confirm, alert, post, popup, toast } from '@/os'; +import { useStream } from '@/stream'; +import * as sound from '@/scripts/sound'; +import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; +import { defaultStore, ColdDeviceStorage } from '@/store'; +import { makeHotkey } from '@/scripts/hotkey'; +import { reactionPicker } from '@/scripts/reaction-picker'; +import { miLocalStorage } from '@/local-storage'; +import { claimAchievement, claimedAchievements } from '@/scripts/achievements'; +import { mainRouter } from '@/router'; +import { initializeSw } from '@/scripts/initialize-sw'; + +export async function mainBoot() { + const { isClientUpdated } = await common(() => createApp( + new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) : + !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : + ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : + ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : + defineAsyncComponent(() => import('@/ui/universal.vue')), + )); + + reactionPicker.init(); + + if (isClientUpdated && $i) { + popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); + } + + const stream = useStream(); + + 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(); + } + } + }); + + for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { + import('../plugin').then(async ({ install }) => { + // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 + await new Promise(r => setTimeout(r, 0)); + install(plugin); + }); + } + + const hotkeys = { + 'd': (): void => { + defaultStore.set('darkMode', !defaultStore.state.darkMode); + }, + 's': (): void => { + mainRouter.push('/search'); + }, + }; + + if ($i) { + // only add post shortcuts if logged in + hotkeys['p|n'] = post; + + defaultStore.loaded.then(() => { + if (defaultStore.state.accountSetupWizard !== -1) { + popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed'); + } + }); + + if ($i.isDeleted) { + alert({ + type: 'warning', + text: i18n.ts.accountDeletionInProgress, + }); + } + + const now = new Date(); + const m = now.getMonth() + 1; + const d = now.getDate(); + + if ($i.birthday) { + const bm = parseInt($i.birthday.split('-')[1]); + const bd = parseInt($i.birthday.split('-')[2]); + if (m === bm && d === bd) { + claimAchievement('loggedInOnBirthday'); + } + } + + if (m === 1 && d === 1) { + claimAchievement('loggedInOnNewYearsDay'); + } + + if ($i.loggedInDays >= 3) claimAchievement('login3'); + if ($i.loggedInDays >= 7) claimAchievement('login7'); + if ($i.loggedInDays >= 15) claimAchievement('login15'); + if ($i.loggedInDays >= 30) claimAchievement('login30'); + if ($i.loggedInDays >= 60) claimAchievement('login60'); + if ($i.loggedInDays >= 100) claimAchievement('login100'); + if ($i.loggedInDays >= 200) claimAchievement('login200'); + if ($i.loggedInDays >= 300) claimAchievement('login300'); + if ($i.loggedInDays >= 400) claimAchievement('login400'); + if ($i.loggedInDays >= 500) claimAchievement('login500'); + if ($i.loggedInDays >= 600) claimAchievement('login600'); + if ($i.loggedInDays >= 700) claimAchievement('login700'); + if ($i.loggedInDays >= 800) claimAchievement('login800'); + if ($i.loggedInDays >= 900) claimAchievement('login900'); + if ($i.loggedInDays >= 1000) claimAchievement('login1000'); + + if ($i.notesCount > 0) claimAchievement('notes1'); + if ($i.notesCount >= 10) claimAchievement('notes10'); + if ($i.notesCount >= 100) claimAchievement('notes100'); + if ($i.notesCount >= 500) claimAchievement('notes500'); + if ($i.notesCount >= 1000) claimAchievement('notes1000'); + if ($i.notesCount >= 5000) claimAchievement('notes5000'); + if ($i.notesCount >= 10000) claimAchievement('notes10000'); + if ($i.notesCount >= 20000) claimAchievement('notes20000'); + if ($i.notesCount >= 30000) claimAchievement('notes30000'); + if ($i.notesCount >= 40000) claimAchievement('notes40000'); + if ($i.notesCount >= 50000) claimAchievement('notes50000'); + if ($i.notesCount >= 60000) claimAchievement('notes60000'); + if ($i.notesCount >= 70000) claimAchievement('notes70000'); + if ($i.notesCount >= 80000) claimAchievement('notes80000'); + if ($i.notesCount >= 90000) claimAchievement('notes90000'); + if ($i.notesCount >= 100000) claimAchievement('notes100000'); + + if ($i.followersCount > 0) claimAchievement('followers1'); + if ($i.followersCount >= 10) claimAchievement('followers10'); + if ($i.followersCount >= 50) claimAchievement('followers50'); + if ($i.followersCount >= 100) claimAchievement('followers100'); + if ($i.followersCount >= 300) claimAchievement('followers300'); + if ($i.followersCount >= 500) claimAchievement('followers500'); + if ($i.followersCount >= 1000) claimAchievement('followers1000'); + + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) { + claimAchievement('passedSinceAccountCreated1'); + } + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) { + claimAchievement('passedSinceAccountCreated2'); + } + if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) { + claimAchievement('passedSinceAccountCreated3'); + } + + if (claimedAchievements.length >= 30) { + claimAchievement('collectAchievements30'); + } + + window.setInterval(() => { + if (Math.floor(Math.random() * 20000) === 0) { + claimAchievement('justPlainLucky'); + } + }, 1000 * 10); + + window.setTimeout(() => { + claimAchievement('client30min'); + }, 1000 * 60 * 30); + + window.setTimeout(() => { + claimAchievement('client60min'); + }, 1000 * 60 * 60); + + const lastUsed = miLocalStorage.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, + })); + } + } + miLocalStorage.setItem('lastUsed', Date.now().toString()); + + const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); + const neverShowDonationInfo = miLocalStorage.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('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)); + + initializeSw(); +} diff --git a/packages/frontend/src/boot/sub-boot.ts b/packages/frontend/src/boot/sub-boot.ts new file mode 100644 index 0000000000..c2664f6c1d --- /dev/null +++ b/packages/frontend/src/boot/sub-boot.ts @@ -0,0 +1,8 @@ +import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; +import { common } from './common'; + +export async function subBoot() { + const { isClientUpdated } = await common(() => createApp( + defineAsyncComponent(() => import('@/ui/minimum.vue')), + )); +} diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index bfec57d6a0..edeaabfba4 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -95,7 +95,7 @@ import XNavFolder from '@/components/MkDrive.navFolder.vue'; import XFolder from '@/components/MkDrive.folder.vue'; import XFile from '@/components/MkDrive.file.vue'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { defaultStore } from '@/store'; import { i18n } from '@/i18n'; import { uploadFile, uploads } from '@/scripts/upload'; @@ -131,7 +131,7 @@ const hierarchyFolders = ref<Misskey.entities.DriveFolder[]>([]); const selectedFiles = ref<Misskey.entities.DriveFile[]>([]); const selectedFolders = ref<Misskey.entities.DriveFolder[]>([]); const uploadings = uploads; -const connection = stream.useChannel('drive'); +const connection = useStream().useChannel('drive'); const keepOriginal = ref<boolean>(defaultStore.state.keepOriginalUploading); // 外部渡ã—ãŒå¤šã„ã®ã§$refã¯ä½¿ã‚ãªã„ã»ã†ãŒã‚ˆã„ // ドãƒãƒƒãƒ—ã•ã‚Œã‚ˆã†ã¨ã—ã¦ã„ã‚‹ã‹ diff --git a/packages/frontend/src/components/MkFollowButton.vue b/packages/frontend/src/components/MkFollowButton.vue index beee21c647..7d066fd033 100644 --- a/packages/frontend/src/components/MkFollowButton.vue +++ b/packages/frontend/src/components/MkFollowButton.vue @@ -33,7 +33,7 @@ import { onBeforeUnmount, onMounted } from 'vue'; import * as Misskey from 'misskey-js'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import { claimAchievement } from '@/scripts/achievements'; import { $i } from '@/account'; @@ -50,7 +50,7 @@ const props = withDefaults(defineProps<{ let isFollowing = $ref(props.user.isFollowing); let hasPendingFollowRequestFromYou = $ref(props.user.hasPendingFollowRequestFromYou); let wait = $ref(false); -const connection = stream.useChannel('main'); +const connection = useStream().useChannel('main'); if (props.user.isFollowing == null) { os.api('users/show', { diff --git a/packages/frontend/src/components/MkNotifications.vue b/packages/frontend/src/components/MkNotifications.vue index 1aea95fe0e..8cb1b064ba 100644 --- a/packages/frontend/src/components/MkNotifications.vue +++ b/packages/frontend/src/components/MkNotifications.vue @@ -22,7 +22,7 @@ import MkPagination, { Paging } from '@/components/MkPagination.vue'; import XNotification from '@/components/MkNotification.vue'; import MkDateSeparatedList from '@/components/MkDateSeparatedList.vue'; import MkNote from '@/components/MkNote.vue'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { notificationTypes } from '@/const'; @@ -45,7 +45,7 @@ const pagination: Paging = { const onNotification = (notification) => { const isMuted = props.includeTypes ? !props.includeTypes.includes(notification.type) : $i.mutingNotificationTypes.includes(notification.type); if (isMuted || document.visibilityState === 'visible') { - stream.send('readNotification'); + useStream().send('readNotification'); } if (!isMuted) { @@ -56,7 +56,7 @@ const onNotification = (notification) => { let connection; onMounted(() => { - connection = stream.useChannel('main'); + connection = useStream().useChannel('main'); connection.on('notification', onNotification); }); diff --git a/packages/frontend/src/components/MkTimeline.vue b/packages/frontend/src/components/MkTimeline.vue index fb0a3a4b67..00a0cb760d 100644 --- a/packages/frontend/src/components/MkTimeline.vue +++ b/packages/frontend/src/components/MkTimeline.vue @@ -5,7 +5,7 @@ <script lang="ts" setup> import { computed, provide, onUnmounted } from 'vue'; import MkNotes from '@/components/MkNotes.vue'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import * as sound from '@/scripts/sound'; import { $i } from '@/account'; import { defaultStore } from '@/store'; @@ -57,6 +57,8 @@ let query; let connection; let connection2; +const stream = useStream(); + if (props.src === 'antenna') { endpoint = 'antennas/notes'; query = { diff --git a/packages/frontend/src/custom-emojis.ts b/packages/frontend/src/custom-emojis.ts index a89a420d77..de1b5b8a63 100644 --- a/packages/frontend/src/custom-emojis.ts +++ b/packages/frontend/src/custom-emojis.ts @@ -1,8 +1,7 @@ import { shallowRef, computed, markRaw } from 'vue'; import * as Misskey from 'misskey-js'; import { api, apiGet } from './os'; -import { miLocalStorage } from './local-storage'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { get, set } from '@/scripts/idb-proxy'; const storageCache = await get('emojis'); @@ -17,6 +16,9 @@ export const customEmojiCategories = computed<[ ...string[], null ]>(() => { return markRaw([...Array.from(categories), null]); }); +// TODO: ã“ã“ら辺副作用ãªã®ã§ã„ã„æ„Ÿã˜ã«ã™ã‚‹ +const stream = useStream(); + stream.on('emojiAdded', emojiData => { customEmojis.value = [emojiData.emoji, ...customEmojis.value]; set('emojis', customEmojis.value); @@ -34,10 +36,9 @@ stream.on('emojiDeleted', emojiData => { export async function fetchCustomEmojis(force = false) { const now = Date.now(); - const needsMigration = miLocalStorage.getItem('emojis') != null; let res; - if (force || needsMigration) { + if (force) { res = await api('emojis', {}); } else { const lastFetchedAt = await get('lastEmojisFetchedAt'); @@ -48,10 +49,6 @@ export async function fetchCustomEmojis(force = false) { customEmojis.value = res.emojis; set('emojis', res.emojis); set('lastEmojisFetchedAt', now); - if (needsMigration) { - miLocalStorage.removeItem('emojis'); - miLocalStorage.removeItem('lastEmojisFetchedAt'); - } } let cachedTags; diff --git a/packages/frontend/src/init.ts b/packages/frontend/src/init.ts deleted file mode 100644 index 763b6d44b0..0000000000 --- a/packages/frontend/src/init.ts +++ /dev/null @@ -1,523 +0,0 @@ -/** - * Client entry point - */ -// https://vitejs.dev/config/build-options.html#build-modulepreload -import 'vite/modulepreload-polyfill'; - -import '@/style.scss'; - -import { computed, createApp, watch, markRaw, version as vueVersion, defineAsyncComponent } from 'vue'; -import { compareVersions } from 'compare-versions'; -import JSON5 from 'json5'; - -import widgets from '@/widgets'; -import directives from '@/directives'; -import components from '@/components'; -import { version, ui, lang, updateLocale } from '@/config'; -import { applyTheme } from '@/scripts/theme'; -import { isDeviceDarkmode } from '@/scripts/is-device-darkmode'; -import { i18n, updateI18n } from '@/i18n'; -import { confirm, alert, post, popup, toast } from '@/os'; -import { stream } from '@/stream'; -import * as sound from '@/scripts/sound'; -import { $i, refreshAccount, login, updateAccount, signout } from '@/account'; -import { defaultStore, ColdDeviceStorage } from '@/store'; -import { fetchInstance, instance } from '@/instance'; -import { makeHotkey } from '@/scripts/hotkey'; -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 { deckStore } from '@/ui/deck/deck-store'; -import { miLocalStorage } from '@/local-storage'; -import { claimAchievement, claimedAchievements } from '@/scripts/achievements'; -import { fetchCustomEmojis } from '@/custom-emojis'; -import { mainRouter } from '@/router'; - -console.info(`Misskey v${version}`); - -if (_DEV_) { - console.warn('Development mode!!!'); - - console.info(`vue ${vueVersion}`); - - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (window as any).$i = $i; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (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 - }); - */ - }); -} - -//#region Detect language & fetch translations -const localeVersion = miLocalStorage.getItem('localeVersion'); -const localeOutdated = (localeVersion == null || localeVersion !== version); -if (localeOutdated) { - const res = await window.fetch(`/assets/locales/${lang}.${version}.json`); - if (res.status === 200) { - const newLocale = await res.text(); - const parsedNewLocale = JSON.parse(newLocale); - miLocalStorage.setItem('locale', newLocale); - miLocalStorage.setItem('localeVersion', version); - updateLocale(parsedNewLocale); - updateI18n(parsedNewLocale); - } -} -//#endregion - -// タッãƒãƒ‡ãƒã‚¤ã‚¹ã§CSSã®:hoverを機能ã•ã›ã‚‹ -document.addEventListener('touchend', () => {}, { passive: true }); - -// 一斉リãƒãƒ¼ãƒ‰ -reloadChannel.addEventListener('message', path => { - if (path !== null) location.href = path; - else location.reload(); -}); - -// If mobile, insert the viewport meta tag -if (['smartphone', 'tablet'].includes(deviceKind)) { - const viewport = document.getElementsByName('viewport').item(0); - viewport.setAttribute('content', - `${viewport.getAttribute('content')}, minimum-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover`); -} - -//#region Set lang attr -const html = document.documentElement; -html.setAttribute('lang', lang); -//#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 && $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(() => { - miLocalStorage.setItem('v', instance.version); - - // Init service worker - initializeSw(); -}); - -try { - await fetchCustomEmojis(); -} catch (err) { /* empty */ } - -const app = createApp( - new URLSearchParams(window.location.search).has('zen') ? defineAsyncComponent(() => import('@/ui/zen.vue')) : - !$i ? defineAsyncComponent(() => import('@/ui/visitor.vue')) : - ui === 'deck' ? defineAsyncComponent(() => import('@/ui/deck.vue')) : - ui === 'classic' ? defineAsyncComponent(() => import('@/ui/classic.vue')) : - defineAsyncComponent(() => import('@/ui/universal.vue')), -); - -if (_DEV_) { - app.config.performance = true; -} - -widgets(app); -directives(app); -components(app); - -const splash = document.getElementById('splash'); -// 念ã®ãŸã‚nullãƒã‚§ãƒƒã‚¯(HTMLãŒå¤ã„å ´åˆãŒã‚ã‚‹ãŸã‚(ãã®ã†ã¡æ¶ˆã™)) -if (splash) splash.addEventListener('transitionend', () => { - splash.remove(); -}); - -await deckStore.ready; - -// https://github.com/misskey-dev/misskey/pull/8575#issuecomment-1114239210 -// ãªãœã‹init.tsã®å†…容ãŒ2回実行ã•ã‚Œã‚‹ã“ã¨ãŒã‚ã‚‹ãŸã‚ã€mountã™ã‚‹divã‚’1ã¤ã«åˆ¶é™ã™ã‚‹ -const rootEl = ((): HTMLElement => { - const MISSKEY_MOUNT_DIV_ID = 'misskey_app'; - - const currentRoot = document.getElementById(MISSKEY_MOUNT_DIV_ID); - - if (currentRoot) { - console.warn('multiple import detected'); - return currentRoot; - } - - const root = document.createElement('div'); - root.id = MISSKEY_MOUNT_DIV_ID; - document.body.appendChild(root); - return root; -})(); - -app.mount(rootEl); - -// boot.jsã®ã‚„ã¤ã‚’解除 -window.onerror = null; -window.onunhandledrejection = null; - -reactionPicker.init(); - -if (splash) { - splash.style.opacity = '0'; - splash.style.pointerEvents = 'none'; -} - -// クライアントãŒæ›´æ–°ã•ã‚ŒãŸã‹ï¼Ÿ -const lastVersion = miLocalStorage.getItem('lastVersion'); -if (lastVersion !== version) { - miLocalStorage.setItem('lastVersion', version); - - // テーマリビルドã™ã‚‹ãŸã‚ - miLocalStorage.removeItem('theme'); - - try { // 変ãªãƒãƒ¼ã‚¸ãƒ§ãƒ³æ–‡å—列æ¥ã‚‹ã¨compareVersionsã§ã‚¨ãƒ©ãƒ¼ã«ãªã‚‹ãŸã‚ - if (lastVersion != null && compareVersions(version, lastVersion) === 1) { - // ãƒã‚°ã‚¤ãƒ³ã—ã¦ã‚‹å ´åˆã ã‘ - if ($i) { - popup(defineAsyncComponent(() => import('@/components/MkUpdated.vue')), {}, {}, 'closed'); - } - } - } catch (err) { /* empty */ } -} - -await defaultStore.ready; - -// NOTE: ã“ã®å‡¦ç†ã¯å¿…ãšâ†‘ã®ã‚¯ãƒ©ã‚¤ã‚¢ãƒ³ãƒˆæ›´æ–°æ™‚処ç†ã‚ˆã‚Šå¾Œã«æ¥ã‚‹ã“ã¨(テーマå†æ§‹ç¯‰ã®ãŸã‚) -watch(defaultStore.reactiveState.darkMode, (darkMode) => { - applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme')); -}, { immediate: miLocalStorage.getItem('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)').addListener(mql => { - if (ColdDeviceStorage.get('syncDeviceDarkMode')) { - defaultStore.set('darkMode', mql.matches); - } -}); -//#endregion - -fetchInstanceMetaPromise.then(() => { - if (defaultStore.state.themeInitial) { - if (instance.defaultLightTheme != null) ColdDeviceStorage.set('lightTheme', JSON5.parse(instance.defaultLightTheme)); - if (instance.defaultDarkTheme != null) ColdDeviceStorage.set('darkTheme', JSON5.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) { - 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(); - } - } -}); - -for (const plugin of ColdDeviceStorage.get('plugins').filter(p => p.active)) { - import('./plugin').then(async ({ install }) => { - // Workaround for https://bugs.webkit.org/show_bug.cgi?id=242740 - await new Promise(r => setTimeout(r, 0)); - install(plugin); - }); -} - -const hotkeys = { - 'd': (): void => { - defaultStore.set('darkMode', !defaultStore.state.darkMode); - }, - 's': (): void => { - mainRouter.push('/search'); - }, -}; - -if ($i) { - // only add post shortcuts if logged in - hotkeys['p|n'] = post; - - defaultStore.loaded.then(() => { - if (defaultStore.state.accountSetupWizard !== -1) { - popup(defineAsyncComponent(() => import('@/components/MkUserSetupDialog.vue')), {}, {}, 'closed'); - } - }); - - if ($i.isDeleted) { - alert({ - type: 'warning', - text: i18n.ts.accountDeletionInProgress, - }); - } - - const now = new Date(); - const m = now.getMonth() + 1; - const d = now.getDate(); - - if ($i.birthday) { - const bm = parseInt($i.birthday.split('-')[1]); - const bd = parseInt($i.birthday.split('-')[2]); - if (m === bm && d === bd) { - claimAchievement('loggedInOnBirthday'); - } - } - - if (m === 1 && d === 1) { - claimAchievement('loggedInOnNewYearsDay'); - } - - if ($i.loggedInDays >= 3) claimAchievement('login3'); - if ($i.loggedInDays >= 7) claimAchievement('login7'); - if ($i.loggedInDays >= 15) claimAchievement('login15'); - if ($i.loggedInDays >= 30) claimAchievement('login30'); - if ($i.loggedInDays >= 60) claimAchievement('login60'); - if ($i.loggedInDays >= 100) claimAchievement('login100'); - if ($i.loggedInDays >= 200) claimAchievement('login200'); - if ($i.loggedInDays >= 300) claimAchievement('login300'); - if ($i.loggedInDays >= 400) claimAchievement('login400'); - if ($i.loggedInDays >= 500) claimAchievement('login500'); - if ($i.loggedInDays >= 600) claimAchievement('login600'); - if ($i.loggedInDays >= 700) claimAchievement('login700'); - if ($i.loggedInDays >= 800) claimAchievement('login800'); - if ($i.loggedInDays >= 900) claimAchievement('login900'); - if ($i.loggedInDays >= 1000) claimAchievement('login1000'); - - if ($i.notesCount > 0) claimAchievement('notes1'); - if ($i.notesCount >= 10) claimAchievement('notes10'); - if ($i.notesCount >= 100) claimAchievement('notes100'); - if ($i.notesCount >= 500) claimAchievement('notes500'); - if ($i.notesCount >= 1000) claimAchievement('notes1000'); - if ($i.notesCount >= 5000) claimAchievement('notes5000'); - if ($i.notesCount >= 10000) claimAchievement('notes10000'); - if ($i.notesCount >= 20000) claimAchievement('notes20000'); - if ($i.notesCount >= 30000) claimAchievement('notes30000'); - if ($i.notesCount >= 40000) claimAchievement('notes40000'); - if ($i.notesCount >= 50000) claimAchievement('notes50000'); - if ($i.notesCount >= 60000) claimAchievement('notes60000'); - if ($i.notesCount >= 70000) claimAchievement('notes70000'); - if ($i.notesCount >= 80000) claimAchievement('notes80000'); - if ($i.notesCount >= 90000) claimAchievement('notes90000'); - if ($i.notesCount >= 100000) claimAchievement('notes100000'); - - if ($i.followersCount > 0) claimAchievement('followers1'); - if ($i.followersCount >= 10) claimAchievement('followers10'); - if ($i.followersCount >= 50) claimAchievement('followers50'); - if ($i.followersCount >= 100) claimAchievement('followers100'); - if ($i.followersCount >= 300) claimAchievement('followers300'); - if ($i.followersCount >= 500) claimAchievement('followers500'); - if ($i.followersCount >= 1000) claimAchievement('followers1000'); - - if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365) { - claimAchievement('passedSinceAccountCreated1'); - } - if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 2) { - claimAchievement('passedSinceAccountCreated2'); - } - if (Date.now() - new Date($i.createdAt).getTime() > 1000 * 60 * 60 * 24 * 365 * 3) { - claimAchievement('passedSinceAccountCreated3'); - } - - if (claimedAchievements.length >= 30) { - claimAchievement('collectAchievements30'); - } - - window.setInterval(() => { - if (Math.floor(Math.random() * 20000) === 0) { - claimAchievement('justPlainLucky'); - } - }, 1000 * 10); - - window.setTimeout(() => { - claimAchievement('client30min'); - }, 1000 * 60 * 30); - - window.setTimeout(() => { - claimAchievement('client60min'); - }, 1000 * 60 * 60); - - const lastUsed = miLocalStorage.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, - })); - } - } - miLocalStorage.setItem('lastUsed', Date.now().toString()); - - const latestDonationInfoShownAt = miLocalStorage.getItem('latestDonationInfoShownAt'); - const neverShowDonationInfo = miLocalStorage.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('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)); diff --git a/packages/frontend/src/pages/admin/overview.queue.vue b/packages/frontend/src/pages/admin/overview.queue.vue index 1f56a2826a..69ca89e226 100644 --- a/packages/frontend/src/pages/admin/overview.queue.vue +++ b/packages/frontend/src/pages/admin/overview.queue.vue @@ -33,9 +33,9 @@ import { markRaw, onMounted, onUnmounted, ref } from 'vue'; import XChart from './overview.queue.chart.vue'; import number from '@/filters/number'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; -const connection = markRaw(stream.useChannel('queueStats')); +const connection = markRaw(useStream().useChannel('queueStats')); const activeSincePrevTick = ref(0); const active = ref(0); diff --git a/packages/frontend/src/pages/admin/overview.vue b/packages/frontend/src/pages/admin/overview.vue index bdfb200a88..46a93ac5d8 100644 --- a/packages/frontend/src/pages/admin/overview.vue +++ b/packages/frontend/src/pages/admin/overview.vue @@ -72,7 +72,7 @@ import XRetention from './overview.retention.vue'; import XModerators from './overview.moderators.vue'; import XHeatmap from './overview.heatmap.vue'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import { definePageMetadata } from '@/scripts/page-metadata'; import MkFoldableSection from '@/components/MkFoldableSection.vue'; @@ -87,7 +87,7 @@ let federationSubActive = $ref<number | null>(null); let federationSubActiveDiff = $ref<number | null>(null); let newUsers = $ref(null); let activeInstances = $shallowRef(null); -const queueStatsConnection = markRaw(stream.useChannel('queueStats')); +const queueStatsConnection = markRaw(useStream().useChannel('queueStats')); const now = new Date(); const filesPagination = { endpoint: 'admin/drive/files' as const, diff --git a/packages/frontend/src/pages/admin/queue.chart.vue b/packages/frontend/src/pages/admin/queue.chart.vue index 100d1ea545..728f3b2c80 100644 --- a/packages/frontend/src/pages/admin/queue.chart.vue +++ b/packages/frontend/src/pages/admin/queue.chart.vue @@ -47,11 +47,11 @@ import { markRaw, onMounted, onUnmounted, ref } from 'vue'; import XChart from './queue.chart.chart.vue'; import number from '@/filters/number'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import MkFolder from '@/components/MkFolder.vue'; -const connection = markRaw(stream.useChannel('queueStats')); +const connection = markRaw(useStream().useChannel('queueStats')); const activeSincePrevTick = ref(0); const active = ref(0); diff --git a/packages/frontend/src/pages/settings/preferences-backups.vue b/packages/frontend/src/pages/settings/preferences-backups.vue index 6613ce4c1d..86f633c445 100644 --- a/packages/frontend/src/pages/settings/preferences-backups.vue +++ b/packages/frontend/src/pages/settings/preferences-backups.vue @@ -40,7 +40,7 @@ import MkInfo from '@/components/MkInfo.vue'; import * as os from '@/os'; import { ColdDeviceStorage, defaultStore } from '@/store'; import { unisonReload } from '@/scripts/unison-reload'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { $i } from '@/account'; import { i18n } from '@/i18n'; import { version, host } from '@/config'; @@ -125,7 +125,7 @@ type Profile = { }; }; -const connection = $i && stream.useChannel('main'); +const connection = $i && useStream().useChannel('main'); let profiles = $ref<Record<string, Profile> | null>(null); diff --git a/packages/frontend/src/pizzax.ts b/packages/frontend/src/pizzax.ts index 2616a8a1d5..d97bd4be62 100644 --- a/packages/frontend/src/pizzax.ts +++ b/packages/frontend/src/pizzax.ts @@ -6,7 +6,7 @@ import { $i } from './account'; import { api } from './os'; import { get, set } from './scripts/idb-proxy'; import { defaultStore } from './store'; -import { stream } from './stream'; +import { useStream } from './stream'; import { deepClone } from './scripts/clone'; type StateDef = Record<string, { @@ -26,8 +26,6 @@ type PizzaxChannelMessage<T extends StateDef> = { userId?: string; }; -const connection = $i && stream.useChannel('main'); - export class Storage<T extends StateDef> { public readonly ready: Promise<void>; public readonly loaded: Promise<void>; @@ -105,8 +103,10 @@ export class Storage<T extends StateDef> { }); if ($i) { + const connection = useStream().useChannel('main'); + // streamingã®user storage updateイベントを監視ã—ã¦æ›´æ–° - connection?.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => { + connection.on('registryUpdated', ({ scope, key, value }: { scope?: string[], key: keyof T, value: T[typeof key]['default'] }) => { if (!scope || scope.length !== 2 || scope[0] !== 'client' || scope[1] !== this.key || this.state[key] === value) return; this.reactiveState[key].value = this.state[key] = value; diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index fe9f0a2447..44a58d6c7d 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -1,7 +1,7 @@ import { ref } from 'vue'; import { DriveFile } from 'misskey-js/built/entities'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; import { uploadFile } from '@/scripts/upload'; @@ -51,7 +51,7 @@ export function chooseFileFromUrl(): Promise<DriveFile> { const marker = Math.random().toString(); // TODO: UUIDã¨ã‹ä½¿ã† - const connection = stream.useChannel('main'); + const connection = useStream().useChannel('main'); connection.on('urlUploadFinished', urlResponse => { if (urlResponse.marker === marker) { res(urlResponse.file); diff --git a/packages/frontend/src/scripts/use-note-capture.ts b/packages/frontend/src/scripts/use-note-capture.ts index ffe33cccc0..22a01e066a 100644 --- a/packages/frontend/src/scripts/use-note-capture.ts +++ b/packages/frontend/src/scripts/use-note-capture.ts @@ -1,6 +1,6 @@ import { onUnmounted, Ref } from 'vue'; import * as misskey from 'misskey-js'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { $i } from '@/account'; export function useNoteCapture(props: { @@ -9,7 +9,7 @@ export function useNoteCapture(props: { isDeletedRef: Ref<boolean>; }) { const note = props.note; - const connection = $i ? stream : null; + const connection = $i ? useStream() : null; function onStreamNoteUpdated(noteData): void { const { type, id, body } = noteData; diff --git a/packages/frontend/src/stream.ts b/packages/frontend/src/stream.ts index dea3459b86..9cae58a26a 100644 --- a/packages/frontend/src/stream.ts +++ b/packages/frontend/src/stream.ts @@ -3,6 +3,14 @@ import { markRaw } from 'vue'; import { $i } from '@/account'; import { url } from '@/config'; -export const stream = markRaw(new Misskey.Stream(url, $i ? { - token: $i.token, -} : null)); +let stream: Misskey.Stream | null = null; + +export function useStream(): Misskey.Stream { + if (stream) return stream; + + stream = markRaw(new Misskey.Stream(url, $i ? { + token: $i.token, + } : null)); + + return stream; +} diff --git a/packages/frontend/src/ui/_common_/common.vue b/packages/frontend/src/ui/_common_/common.vue index 71a4285e9d..2beebb0390 100644 --- a/packages/frontend/src/ui/_common_/common.vue +++ b/packages/frontend/src/ui/_common_/common.vue @@ -40,7 +40,7 @@ import { popups, pendingApiRequestsCount } from '@/os'; import { uploads } from '@/scripts/upload'; import * as sound from '@/scripts/sound'; import { $i } from '@/account'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import { defaultStore } from '@/store'; @@ -55,7 +55,7 @@ function onNotification(notification) { if ($i.mutingNotificationTypes.includes(notification.type)) return; if (document.visibilityState === 'visible') { - stream.send('readNotification'); + useStream().send('readNotification'); notifications.unshift(notification); window.setTimeout(() => { @@ -71,7 +71,7 @@ function onNotification(notification) { } if ($i) { - const connection = stream.useChannel('main', null, 'UI'); + const connection = useStream().useChannel('main', null, 'UI'); connection.on('notification', onNotification); //#region Listen message from SW diff --git a/packages/frontend/src/ui/_common_/stream-indicator.vue b/packages/frontend/src/ui/_common_/stream-indicator.vue index 2a856e2a45..3e97df7ee5 100644 --- a/packages/frontend/src/ui/_common_/stream-indicator.vue +++ b/packages/frontend/src/ui/_common_/stream-indicator.vue @@ -10,7 +10,7 @@ <script lang="ts" setup> import { onUnmounted } from 'vue'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; import MkButton from '@/components/MkButton.vue'; import * as os from '@/os'; @@ -32,10 +32,10 @@ function reload() { location.reload(); } -stream.on('_disconnected_', onDisconnected); +useStream().on('_disconnected_', onDisconnected); onUnmounted(() => { - stream.off('_disconnected_', onDisconnected); + useStream().off('_disconnected_', onDisconnected); }); </script> diff --git a/packages/frontend/src/ui/minimum.vue b/packages/frontend/src/ui/minimum.vue new file mode 100644 index 0000000000..628390e3f7 --- /dev/null +++ b/packages/frontend/src/ui/minimum.vue @@ -0,0 +1,34 @@ +<template> +<div class="mk-app" style="container-type: inline-size;"> + <RouterView/> + + <XCommon/> +</div> +</template> + +<script lang="ts" setup> +import { provide, ComputedRef } from 'vue'; +import XCommon from './_common_/common.vue'; +import { mainRouter } from '@/router'; +import { PageMetadata, provideMetadataReceiver } from '@/scripts/page-metadata'; +import { instanceName } from '@/config'; + +let pageMetadata = $ref<null | ComputedRef<PageMetadata>>(); + +provide('router', mainRouter); +provideMetadataReceiver((info) => { + pageMetadata = info; + if (pageMetadata.value) { + document.title = `${pageMetadata.value.title} | ${instanceName}`; + } +}); + +document.documentElement.style.overflowY = 'scroll'; +</script> + +<style lang="scss" scoped> +.mk-app { + min-height: 100dvh; + box-sizing: border-box; +} +</style> diff --git a/packages/frontend/src/widgets/WidgetJobQueue.vue b/packages/frontend/src/widgets/WidgetJobQueue.vue index 84043cf13f..73a4802595 100644 --- a/packages/frontend/src/widgets/WidgetJobQueue.vue +++ b/packages/frontend/src/widgets/WidgetJobQueue.vue @@ -49,7 +49,7 @@ import { onUnmounted, reactive } from 'vue'; import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; import { GetFormResultType } from '@/scripts/form'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import number from '@/filters/number'; import * as sound from '@/scripts/sound'; import { deepClone } from '@/scripts/clone'; @@ -81,7 +81,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -const connection = stream.useChannel('queueStats'); +const connection = useStream().useChannel('queueStats'); const current = reactive({ inbox: { activeSincePrevTick: 0, diff --git a/packages/frontend/src/widgets/WidgetPhotos.vue b/packages/frontend/src/widgets/WidgetPhotos.vue index 716bbb4274..1484aa1941 100644 --- a/packages/frontend/src/widgets/WidgetPhotos.vue +++ b/packages/frontend/src/widgets/WidgetPhotos.vue @@ -20,7 +20,7 @@ import { onUnmounted, ref } from 'vue'; import { useWidgetPropsManager, Widget, WidgetComponentExpose } from './widget'; import { GetFormResultType } from '@/scripts/form'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { getStaticImageUrl } from '@/scripts/media-proxy'; import * as os from '@/os'; import MkContainer from '@/components/MkContainer.vue'; @@ -54,7 +54,7 @@ const { widgetProps, configure } = useWidgetPropsManager(name, emit, ); -const connection = stream.useChannel('main'); +const connection = useStream().useChannel('main'); const images = ref([]); const fetching = ref(true); diff --git a/packages/frontend/src/widgets/server-metric/index.vue b/packages/frontend/src/widgets/server-metric/index.vue index 357d0ab78b..df8de10f63 100644 --- a/packages/frontend/src/widgets/server-metric/index.vue +++ b/packages/frontend/src/widgets/server-metric/index.vue @@ -25,7 +25,7 @@ import XDisk from './disk.vue'; import MkContainer from '@/components/MkContainer.vue'; import { GetFormResultType } from '@/scripts/form'; import * as os from '@/os'; -import { stream } from '@/stream'; +import { useStream } from '@/stream'; import { i18n } from '@/i18n'; const name = 'serverMetric'; @@ -75,7 +75,7 @@ const toggleView = () => { save(); }; -const connection = stream.useChannel('serverStats'); +const connection = useStream().useChannel('serverStats'); onUnmounted(() => { connection.dispose(); }); diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts index fad0dd0177..339761e78c 100644 --- a/packages/frontend/vite.config.ts +++ b/packages/frontend/vite.config.ts @@ -117,7 +117,7 @@ export function getConfig(): UserConfig { manifest: 'manifest.json', rollupOptions: { input: { - app: './src/init.ts', + app: './src/_boot_.ts', }, output: { manualChunks: { -- GitLab