feat: render app shell with ssr to improve loading experience (#448)
parent
b545efeacc
commit
9395b7031e
|
@ -1,3 +1,4 @@
|
|||
*.css
|
||||
*.png
|
||||
*.ico
|
||||
*.toml
|
||||
|
|
5
app.vue
5
app.vue
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
setupI18n()
|
||||
setupLogging()
|
||||
setupPageHeader()
|
||||
await setupI18n()
|
||||
provideGlobalCommands()
|
||||
|
||||
// We want to trigger rerendering the page when account changes
|
||||
|
@ -11,7 +11,6 @@ const key = computed(() => `${currentServer.value}:${currentUser.value?.account.
|
|||
<template>
|
||||
<NuxtLoadingIndicator color="repeating-linear-gradient(to right,var(--c-primary) 0%,var(--c-primary-active) 100%)" />
|
||||
<NuxtLayout :key="key">
|
||||
<NuxtPage />
|
||||
<NuxtPage v-if="isMastoInitialised" />
|
||||
</NuxtLayout>
|
||||
<TeleportTarget id="teleport-end" />
|
||||
</template>
|
||||
|
|
|
@ -22,7 +22,7 @@ defineProps<{
|
|||
</div>
|
||||
<div flex items-center flex-shrink-0 gap-x-2>
|
||||
<slot name="actions" />
|
||||
<NavUser v-if="isMediumScreen" />
|
||||
<NavUser v-if="isHydrated && isMediumScreen" />
|
||||
</div>
|
||||
</div>
|
||||
<slot name="header" />
|
||||
|
|
|
@ -29,28 +29,30 @@ useEventListener('keydown', (e: KeyboardEvent) => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<ModalDialog v-model="isSigninDialogOpen" py-4 px-8 max-w-125>
|
||||
<UserSignIn />
|
||||
</ModalDialog>
|
||||
<ModalDialog v-model="isPreviewHelpOpen" max-w-125>
|
||||
<HelpPreview @close="closePreviewHelp()" />
|
||||
</ModalDialog>
|
||||
<ModalDialog v-model="isPublishDialogOpen" max-w-180 flex>
|
||||
<!-- This `w-0` style is used to avoid overflow problems in flex layouts,so don't remove it unless you know what you're doing -->
|
||||
<PublishWidget :draft-key="dialogDraftKey" expanded flex-1 w-0 />
|
||||
</ModalDialog>
|
||||
<ModalDialog
|
||||
v-model="isMediaPreviewOpen"
|
||||
pointer-events-none
|
||||
w-full max-w-full h-full max-h-full
|
||||
bg-transparent border-0 shadow-none
|
||||
>
|
||||
<ModalMediaPreview v-if="isMediaPreviewOpen" @close="closeMediaPreview()" />
|
||||
</ModalDialog>
|
||||
<ModalDialog v-model="isEditHistoryDialogOpen" max-w-125>
|
||||
<StatusEditPreview :edit="statusEdit" />
|
||||
</ModalDialog>
|
||||
<ModalDialog v-model="isCommandPanelOpen" max-w-fit flex>
|
||||
<CommandPanel @close="closeCommandPanel()" />
|
||||
</ModalDialog>
|
||||
<template v-if="isMastoInitialised">
|
||||
<ModalDialog v-model="isSigninDialogOpen" py-4 px-8 max-w-125>
|
||||
<UserSignIn />
|
||||
</ModalDialog>
|
||||
<ModalDialog v-model="isPreviewHelpOpen" max-w-125>
|
||||
<HelpPreview @close="closePreviewHelp()" />
|
||||
</ModalDialog>
|
||||
<ModalDialog v-model="isPublishDialogOpen" max-w-180 flex>
|
||||
<!-- This `w-0` style is used to avoid overflow problems in flex layouts,so don't remove it unless you know what you're doing -->
|
||||
<PublishWidget :draft-key="dialogDraftKey" expanded flex-1 w-0 />
|
||||
</ModalDialog>
|
||||
<ModalDialog
|
||||
v-model="isMediaPreviewOpen"
|
||||
pointer-events-none
|
||||
w-full max-w-full h-full max-h-full
|
||||
bg-transparent border-0 shadow-none
|
||||
>
|
||||
<ModalMediaPreview v-if="isMediaPreviewOpen" @close="closeMediaPreview()" />
|
||||
</ModalDialog>
|
||||
<ModalDialog v-model="isEditHistoryDialogOpen" max-w-125>
|
||||
<StatusEditPreview :edit="statusEdit" />
|
||||
</ModalDialog>
|
||||
<ModalDialog v-model="isCommandPanelOpen" max-w-fit flex>
|
||||
<CommandPanel @close="closeCommandPanel()" />
|
||||
</ModalDialog>
|
||||
</template>
|
||||
</template>
|
||||
|
|
|
@ -137,7 +137,7 @@ export default {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<SafeTeleport to="#teleport-end" @transitionend="trapFocusDialog">
|
||||
<Teleport to="body" @transitionend="trapFocusDialog">
|
||||
<!-- Dialog component -->
|
||||
<Transition name="dialog-visible">
|
||||
<div
|
||||
|
@ -173,7 +173,7 @@ export default {
|
|||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</SafeTeleport>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<style lang="postcss" scoped>
|
||||
|
|
|
@ -10,7 +10,7 @@ const moreMenuVisible = ref(false)
|
|||
class="after-content-empty after:(h-[calc(100%+0.5px)] w-0.1px pointer-events-none)"
|
||||
>
|
||||
<!-- These weird styles above are used for scroll locking, don't change it unless you know exactly what you're doing. -->
|
||||
<template v-if="currentUser">
|
||||
<template v-if="isMastoInitialised && currentUser">
|
||||
<NuxtLink to="/home" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:home-5-line />
|
||||
</NuxtLink>
|
||||
|
@ -24,12 +24,12 @@ const moreMenuVisible = ref(false)
|
|||
<NuxtLink group :to="`/${currentServer}/public/local`" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:group-2-line />
|
||||
</NuxtLink>
|
||||
<template v-if="!currentUser">
|
||||
<template v-if="!isMastoInitialised || !currentUser">
|
||||
<NuxtLink :to="`/${currentServer}/public`" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:earth-line />
|
||||
</NuxtLink>
|
||||
</template>
|
||||
<template v-if="currentUser">
|
||||
<template v-if="isMastoInitialised && currentUser">
|
||||
<NuxtLink to="/conversations" :active-class="moreMenuVisible ? '' : 'text-primary'" flex flex-row items-center place-content-center h-full flex-1 @click="$scrollToTop">
|
||||
<div i-ri:at-line />
|
||||
</NuxtLink>
|
||||
|
|
|
@ -96,7 +96,7 @@ onBeforeUnmount(() => {
|
|||
</button>
|
||||
</NavSelectLanguage>
|
||||
<!-- Toggle Feature Flags -->
|
||||
<NavSelectFeatureFlags v-if="currentUser">
|
||||
<NavSelectFeatureFlags v-if="isMastoInitialised && currentUser">
|
||||
<button
|
||||
flex flex-row items-center
|
||||
block px-5 py-2 focus-blue w-full
|
||||
|
|
|
@ -30,7 +30,7 @@ const buildTimeAgo = useTimeAgo(buildTime, timeAgoOptions)
|
|||
</button>
|
||||
</CommonTooltip>
|
||||
</NavSelectLanguage>
|
||||
<NavSelectFeatureFlags v-if="currentUser">
|
||||
<NavSelectFeatureFlags v-if="isMastoInitialised && currentUser">
|
||||
<CommonTooltip :content="$t('nav_footer.select_feature_flags')">
|
||||
<button flex :aria-label="$t('nav_footer.select_feature_flags')">
|
||||
<div i-ri:flag-line text-lg />
|
||||
|
@ -44,7 +44,7 @@ const buildTimeAgo = useTimeAgo(buildTime, timeAgoOptions)
|
|||
</button>
|
||||
</div>
|
||||
<div>{{ $t('app_desc_short') }}</div>
|
||||
<div>
|
||||
<div v-if="isMastoInitialised">
|
||||
<i18n-t keypath="nav_footer.built_at">
|
||||
<time :datetime="buildTime" :title="$d(buildTimeDate, 'long')">{{ buildTimeAgo }}</time>
|
||||
</i18n-t>
|
||||
|
|
|
@ -4,7 +4,7 @@ const { notifications } = useNotifications()
|
|||
|
||||
<template>
|
||||
<nav md:px3 md:py4 flex="~ col gap2" text-size-base leading-normal md:text-lg>
|
||||
<template v-if="currentUser">
|
||||
<template v-if="isMastoInitialised && currentUser">
|
||||
<NavSideItem :text="$t('nav_side.home')" to="/home" icon="i-ri:home-5-line" />
|
||||
<NavSideItem :text="$t('nav_side.notifications')" to="/notifications" icon="i-ri:notification-4-line">
|
||||
<template #icon>
|
||||
|
@ -20,12 +20,12 @@ const { notifications } = useNotifications()
|
|||
<NavSideItem :text="$t('nav_side.explore')" :to="`/${currentServer}/explore`" icon="i-ri:hashtag" />
|
||||
<NavSideItem :text="$t('nav_side.local')" :to="`/${currentServer}/public/local`" icon="i-ri:group-2-line " />
|
||||
<NavSideItem :text="$t('nav_side.federated')" :to="`/${currentServer}/public`" icon="i-ri:earth-line" />
|
||||
<template v-if="currentUser">
|
||||
<template v-if="isMastoInitialised && currentUser">
|
||||
<NavSideItem :text="$t('nav_side.conversations')" to="/conversations" icon="i-ri:at-line" />
|
||||
<NavSideItem :text="$t('nav_side.favourites')" to="/favourites" icon="i-ri:heart-3-line" />
|
||||
<NavSideItem :text="$t('nav_side.bookmarks')" to="/bookmarks" icon="i-ri:bookmark-line " />
|
||||
<NavSideItem
|
||||
v-if="isMediumScreen"
|
||||
v-if="isHydrated && isMediumScreen"
|
||||
:text="currentUser.account.displayName"
|
||||
:to="getAccountRoute(currentUser.account)"
|
||||
icon="i-ri:account-circle-line"
|
||||
|
|
|
@ -25,7 +25,7 @@ useCommand({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<NuxtLink :to="to" active-class="text-primary" group focus:outline-none @click="$scrollToTop">
|
||||
<NuxtLink :to="to" :active-class="isMastoInitialised ? 'text-primary' : ''" group focus:outline-none @click="$scrollToTop">
|
||||
<div flex w-fit px5 py2 md:gap2 gap4 items-center transition-100 rounded-full group-hover:bg-active group-focus-visible:ring="2 current">
|
||||
<slot name="icon">
|
||||
<div :class="icon" md:text-size-inherit text-xl />
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<VDropdown v-if="currentUser">
|
||||
<VDropdown v-if="isMastoInitialised && currentUser">
|
||||
<div style="-webkit-touch-callout: none;">
|
||||
<AccountAvatar
|
||||
ref="avatar"
|
||||
|
|
|
@ -27,11 +27,11 @@ const description = ref(props.attachment.description ?? '')
|
|||
v-if="removable"
|
||||
aria-label="Remove attachment"
|
||||
hover:bg="gray/40" transition-100 p-1 rounded-5 cursor-pointer
|
||||
:class="[isSmallScreen ? '' : 'op-0 group-hover:op-100hover:']"
|
||||
:class="[isHydrated && isSmallScreen ? '' : 'op-0 group-hover:op-100hover:']"
|
||||
mix-blend-difference
|
||||
@click="$emit('remove')"
|
||||
>
|
||||
<div i-ri:close-line text-3 :class="[isSmallScreen ? 'text-6' : 'text-3']" />
|
||||
<div i-ri:close-line text-3 :class="[isHydrated && isSmallScreen ? 'text-6' : 'text-3']" />
|
||||
</div>
|
||||
</div>
|
||||
<div absolute right-2 bottom-2>
|
||||
|
|
|
@ -167,7 +167,7 @@ defineExpose({
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="currentUser" flex="~ col gap-4" py4 px2 sm:px4>
|
||||
<div v-if="isMastoInitialised && currentUser" flex="~ col gap-4" py4 px2 sm:px4>
|
||||
<template v-if="draft.editingStatus">
|
||||
<div flex="~ col gap-1">
|
||||
<div id="state-editing" text-secondary self-center>
|
||||
|
|
|
@ -162,7 +162,7 @@ async function editStatus() {
|
|||
@click="toggleTranslation"
|
||||
/>
|
||||
|
||||
<template v-if="currentUser">
|
||||
<template v-if="isMastoInitialised && currentUser">
|
||||
<template v-if="isAuthor">
|
||||
<CommonDropdownItem
|
||||
:text="status.pinned ? $t('menu.unpin_on_profile') : $t('menu.pin_on_profile')"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div p8 flex="~ col gap4">
|
||||
<p text-sm>
|
||||
<p v-if="isMastoInitialised" text-sm>
|
||||
Viewing <strong>{{ currentServer }}</strong> public data
|
||||
</p>
|
||||
<p text-sm text-secondary>
|
||||
|
|
|
@ -44,7 +44,7 @@ const switchUser = (user: UserLogin) => {
|
|||
@click="openSigninDialog"
|
||||
/>
|
||||
<CommonDropdownItem
|
||||
v-if="currentUser"
|
||||
v-if="isMastoInitialised && currentUser"
|
||||
:text="$t('user.sign_out_account', [getFullHandle(currentUser.account)])"
|
||||
icon="i-ri:logout-box-line"
|
||||
@click="signout"
|
||||
|
|
|
@ -5,7 +5,7 @@ const cache = new LRU<string, any>({
|
|||
max: 1000,
|
||||
})
|
||||
|
||||
if (process.dev)
|
||||
if (process.dev && process.client)
|
||||
// eslint-disable-next-line no-console
|
||||
console.log({ cache })
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { Emoji } from 'masto'
|
|||
import type { Node } from 'ultrahtml'
|
||||
import { TEXT_NODE, parse, render, walkSync } from 'ultrahtml'
|
||||
|
||||
const decoder = document.createElement('textarea')
|
||||
const decoder = process.client ? document.createElement('textarea') : null as any as HTMLTextAreaElement
|
||||
function decode(text: string) {
|
||||
decoder.innerHTML = text
|
||||
return decoder.value
|
||||
|
|
|
@ -13,7 +13,7 @@ export function getDefaultFeatureFlags(): FeatureFlags {
|
|||
}
|
||||
}
|
||||
|
||||
export const currentUserFeatureFlags = useUserLocalStorage(STORAGE_KEY_FEATURE_FLAGS, getDefaultFeatureFlags)
|
||||
export const currentUserFeatureFlags = process.server ? computed(getDefaultFeatureFlags) : useUserLocalStorage(STORAGE_KEY_FEATURE_FLAGS, getDefaultFeatureFlags)
|
||||
|
||||
export function useFeatureFlags() {
|
||||
const featureFlags = currentUserFeatureFlags.value
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
export const isHydrated = computed(() => {
|
||||
if (process.server)
|
||||
return false
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
if (!nuxtApp.isHydrating)
|
||||
return false
|
||||
|
||||
const hydrated = ref(false)
|
||||
nuxtApp.hooks.hookOnce('app:suspense:resolve', () => {
|
||||
hydrated.value = true
|
||||
})
|
||||
return hydrated
|
||||
})
|
|
@ -1,12 +1,11 @@
|
|||
import type { Ref } from 'vue'
|
||||
import type { Account, MastoClient, Relationship, Status } from 'masto'
|
||||
import type { Account, Relationship, Status } from 'masto'
|
||||
import { withoutProtocol } from 'ufo'
|
||||
import type { ElkMasto } from '~/types'
|
||||
|
||||
export const useMasto = () => useNuxtApp().$masto.api as MastoClient
|
||||
export const useMasto = () => useNuxtApp().$masto as ElkMasto
|
||||
|
||||
export const setMasto = (masto: MastoClient) => {
|
||||
useNuxtApp().$masto?.replace(masto)
|
||||
}
|
||||
export const isMastoInitialised = computed(() => process.client && useMasto().loggedIn.value)
|
||||
|
||||
// @unocss-include
|
||||
export const STATUS_VISIBILITIES = [
|
||||
|
|
|
@ -56,23 +56,25 @@ export function usePaginator<T>(paginator: Paginator<any, T[]>, stream?: WsEvent
|
|||
bound.update()
|
||||
}
|
||||
|
||||
useIntervalFn(() => {
|
||||
bound.update()
|
||||
}, 1000)
|
||||
if (process.client) {
|
||||
useIntervalFn(() => {
|
||||
bound.update()
|
||||
}, 1000)
|
||||
|
||||
watch(
|
||||
() => [isInScreen, state],
|
||||
() => {
|
||||
if (
|
||||
isInScreen
|
||||
watch(
|
||||
() => [isInScreen, state],
|
||||
() => {
|
||||
if (
|
||||
isInScreen
|
||||
&& state.value === 'idle'
|
||||
// No new content is loaded when the keepAlive page enters the background
|
||||
&& deactivated.value === false
|
||||
)
|
||||
loadNext()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
)
|
||||
loadNext()
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
items,
|
||||
|
|
|
@ -19,22 +19,25 @@ export function setupPageHeader() {
|
|||
|
||||
export async function setupI18n() {
|
||||
const { locale, setLocale, locales } = useI18n()
|
||||
const isFirstVisit = !window.localStorage.getItem(STORAGE_KEY_LANG)
|
||||
const localeStorage = useLocalStorage(STORAGE_KEY_LANG, locale.value)
|
||||
const nuxtApp = useNuxtApp()
|
||||
nuxtApp.hook('app:suspense:resolve', async () => {
|
||||
const isFirstVisit = process.server ? false : !window.localStorage.getItem(STORAGE_KEY_LANG)
|
||||
const localeStorage = process.server ? ref('en-US') : useLocalStorage(STORAGE_KEY_LANG, locale.value)
|
||||
|
||||
if (isFirstVisit) {
|
||||
const userLang = (navigator.language || 'en-US').toLowerCase()
|
||||
// cause vue-i18n not explicit export LocaleObject type
|
||||
const supportLocales = unref(locales) as { code: string }[]
|
||||
const lang = supportLocales.find(locale => userLang.startsWith(locale.code.toLowerCase()))?.code
|
||||
if (isFirstVisit) {
|
||||
const userLang = (navigator.language || 'en-US').toLowerCase()
|
||||
// cause vue-i18n not explicit export LocaleObject type
|
||||
const supportLocales = unref(locales) as { code: string }[]
|
||||
const lang = supportLocales.find(locale => userLang.startsWith(locale.code.toLowerCase()))?.code
|
||||
|| supportLocales.find(locale => userLang.startsWith(locale.code.split('-')[0]))?.code
|
||||
localeStorage.value = lang || 'en-US'
|
||||
}
|
||||
localeStorage.value = lang || 'en-US'
|
||||
}
|
||||
|
||||
if (localeStorage.value !== locale.value)
|
||||
await setLocale(localeStorage.value)
|
||||
if (localeStorage.value !== locale.value)
|
||||
await setLocale(localeStorage.value)
|
||||
|
||||
watchEffect(() => {
|
||||
localeStorage.value = locale.value
|
||||
watchEffect(() => {
|
||||
localeStorage.value = locale.value
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import type { Account, Status } from 'masto'
|
|||
import { STORAGE_KEY_DRAFTS } from '~/constants'
|
||||
import type { Draft, DraftMap } from '~/types'
|
||||
|
||||
export const currentUserDrafts = useUserLocalStorage<DraftMap>(STORAGE_KEY_DRAFTS, () => ({}))
|
||||
export const currentUserDrafts = process.server ? computed<DraftMap>(() => ({})) : useUserLocalStorage<DraftMap>(STORAGE_KEY_DRAFTS, () => ({}))
|
||||
|
||||
export function getDefaultDraft(options: Partial<Draft['params'] & Omit<Draft, 'params'>> = {}): Draft {
|
||||
const {
|
||||
|
|
|
@ -8,10 +8,9 @@ export interface TranslationResponse {
|
|||
}
|
||||
}
|
||||
|
||||
const config = useRuntimeConfig()
|
||||
|
||||
export const languageCode = process.server ? 'en' : navigator.language.replace(/-.*$/, '')
|
||||
export async function translateText(text: string, from?: string | null, to?: string) {
|
||||
const config = useRuntimeConfig()
|
||||
const { translatedText } = await $fetch<TranslationResponse>(config.public.translateApi, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
|
@ -41,7 +40,7 @@ export function useTranslation(status: Status) {
|
|||
}
|
||||
|
||||
return {
|
||||
enabled: !!config.public.translateApi,
|
||||
enabled: !!useRuntimeConfig().public.translateApi,
|
||||
toggle,
|
||||
translation,
|
||||
}
|
||||
|
|
|
@ -73,8 +73,6 @@ export async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: Ac
|
|||
}
|
||||
}
|
||||
|
||||
setMasto(masto)
|
||||
|
||||
if ('server' in route.params && user?.token) {
|
||||
await router.push({
|
||||
...route,
|
||||
|
@ -117,6 +115,7 @@ const notifications = reactive<Record<string, undefined | [Promise<WsEvents>, nu
|
|||
|
||||
export const useNotifications = () => {
|
||||
const id = currentUser.value?.account.id
|
||||
const masto = useMasto()
|
||||
|
||||
const clearNotifications = () => {
|
||||
if (!id || !notifications[id])
|
||||
|
@ -125,10 +124,9 @@ export const useNotifications = () => {
|
|||
}
|
||||
|
||||
async function connect(): Promise<void> {
|
||||
if (!id || notifications[id] || !currentUser.value?.token)
|
||||
if (!isMastoInitialised.value || !id || notifications[id] || !currentUser.value?.token)
|
||||
return
|
||||
|
||||
const masto = useMasto()
|
||||
const stream = masto.stream.streamUser()
|
||||
notifications[id] = [stream, 0]
|
||||
;(await stream).on('notification', () => {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<NavTitle mx3 mt4 mb2 self-start />
|
||||
<div flex="~ col" overflow-y-auto>
|
||||
<NavSide />
|
||||
<PublishButton v-if="currentUser" m5 />
|
||||
<PublishButton v-if="isMastoInitialised && currentUser" m5 />
|
||||
<div flex-auto />
|
||||
</div>
|
||||
</slot>
|
||||
|
@ -18,15 +18,15 @@
|
|||
<slot />
|
||||
</div>
|
||||
<div sticky left-0 right-0 bottom-0 z-10 bg-base pb="[env(safe-area-inset-bottom)]" transition="padding 20">
|
||||
<CommonOfflineChecker :small-screen="isSmallScreen" />
|
||||
<NavBottom v-if="isSmallScreen" />
|
||||
<CommonOfflineChecker :small-screen="isHydrated && isSmallScreen" />
|
||||
<NavBottom v-if="isHydrated && isSmallScreen" />
|
||||
</div>
|
||||
</div>
|
||||
<aside class="hidden md:none lg:block w-1/4 zen-hide">
|
||||
<div sticky top-0 h-screen flex="~ col">
|
||||
<slot name="right">
|
||||
<UserSignInEntry v-if="!currentUser" />
|
||||
<div v-if="currentUser" py6 px4 w-full flex="~" items-center justify-between>
|
||||
<UserSignInEntry v-if="isMastoInitialised && !currentUser" />
|
||||
<div v-if="isMastoInitialised && currentUser" py6 px4 w-full flex="~" items-center justify-between>
|
||||
<NuxtLink
|
||||
p2 rounded-full text-start w-full
|
||||
hover:bg-active cursor-pointer transition-100
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
export default defineNuxtRouteMiddleware((to) => {
|
||||
if (process.server)
|
||||
return
|
||||
if (!currentUser.value)
|
||||
return navigateTo(`/${currentServer.value}/public`)
|
||||
if (to.path === '/')
|
||||
|
|
|
@ -6,7 +6,6 @@ import { i18n } from './config/i18n'
|
|||
const isPreview = process.env.PULL_REQUEST === 'true'
|
||||
|
||||
export default defineNuxtConfig({
|
||||
ssr: false,
|
||||
modules: [
|
||||
'@vueuse/nuxt',
|
||||
'@unocss/nuxt',
|
||||
|
|
|
@ -83,7 +83,6 @@
|
|||
"unplugin-auto-import": "^0.12.0",
|
||||
"vite-plugin-inspect": "^0.7.9",
|
||||
"vitest": "^0.25.3",
|
||||
"vue-safe-teleport": "^0.1.1",
|
||||
"vue-tsc": "^1.0.11",
|
||||
"vue-virtual-scroller": "2.0.0-beta.4"
|
||||
},
|
||||
|
|
|
@ -1,31 +1,60 @@
|
|||
import type { MastoClient } from 'masto'
|
||||
import { currentUser } from '../composables/users'
|
||||
import type { ElkMasto } from '~/types'
|
||||
|
||||
export default defineNuxtPlugin(async () => {
|
||||
let masto!: MastoClient
|
||||
try {
|
||||
export default defineNuxtPlugin(async (nuxtApp) => {
|
||||
const api = shallowRef<MastoClient | null>(null)
|
||||
const apiPromise = ref<Promise<MastoClient> | null>(null)
|
||||
const initialised = computed(() => !!api.value)
|
||||
|
||||
const masto = new Proxy({} as ElkMasto, {
|
||||
get(_, key: keyof ElkMasto) {
|
||||
if (key === 'loggedIn')
|
||||
return initialised
|
||||
|
||||
if (key === 'loginTo') {
|
||||
return (...args: any[]) => {
|
||||
apiPromise.value = loginTo(...args).then((r) => {
|
||||
api.value = r
|
||||
return masto
|
||||
}).catch(() => {
|
||||
// Show error page when Mastodon server is down
|
||||
throw createError({
|
||||
fatal: true,
|
||||
statusMessage: 'Could not log into account.',
|
||||
})
|
||||
})
|
||||
return apiPromise
|
||||
}
|
||||
}
|
||||
|
||||
if (api.value && key in api.value)
|
||||
return api.value[key as keyof MastoClient]
|
||||
|
||||
if (!api) {
|
||||
return new Proxy({}, {
|
||||
get(_, subkey) {
|
||||
return (...args: any[]) => apiPromise.value?.then((r: any) => r[key][subkey](...args))
|
||||
},
|
||||
})
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
if (process.client) {
|
||||
const { query } = useRoute()
|
||||
const user = typeof query.server === 'string' && typeof query.token === 'string'
|
||||
? { server: query.server, token: query.token }
|
||||
: currentUser.value
|
||||
|
||||
// TODO: improve upstream to make this synchronous (delayed auth)
|
||||
masto = await loginTo(user)
|
||||
}
|
||||
catch {
|
||||
// Show error page when Mastodon server is down
|
||||
showError({
|
||||
fatal: true,
|
||||
statusMessage: 'Could not log into account.',
|
||||
nuxtApp.hook('app:suspense:resolve', () => {
|
||||
// TODO: improve upstream to make this synchronous (delayed auth)
|
||||
masto.loginTo(user)
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
provide: {
|
||||
masto: shallowReactive({
|
||||
replace(api: MastoClient) { this.api = api },
|
||||
api: masto,
|
||||
}),
|
||||
masto,
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import VueSafeTeleport from 'vue-safe-teleport'
|
||||
import { defineNuxtPlugin } from '#app'
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.use(VueSafeTeleport)
|
||||
})
|
|
@ -63,7 +63,6 @@ specifiers:
|
|||
unplugin-auto-import: ^0.12.0
|
||||
vite-plugin-inspect: ^0.7.9
|
||||
vitest: ^0.25.3
|
||||
vue-safe-teleport: ^0.1.1
|
||||
vue-tsc: ^1.0.11
|
||||
vue-virtual-scroller: 2.0.0-beta.4
|
||||
|
||||
|
@ -132,7 +131,6 @@ devDependencies:
|
|||
unplugin-auto-import: 0.12.0
|
||||
vite-plugin-inspect: 0.7.9
|
||||
vitest: 0.25.3_jsdom@20.0.3
|
||||
vue-safe-teleport: 0.1.1
|
||||
vue-tsc: 1.0.11_typescript@4.9.3
|
||||
vue-virtual-scroller: 2.0.0-beta.4
|
||||
|
||||
|
@ -8710,12 +8708,6 @@ packages:
|
|||
vue: 3.2.45
|
||||
dev: true
|
||||
|
||||
/vue-safe-teleport/0.1.1:
|
||||
resolution: {integrity: sha512-fHA4mod2oF7am2yEUtT0CsxAwfNBt6hWuYTVWzGxrY8vzxxgHMFnPjdZTKl01qGcKEMYYO38LmWizL7oGMVPGw==}
|
||||
peerDependencies:
|
||||
vue: ^3.2.0
|
||||
dev: true
|
||||
|
||||
/vue-template-compiler/2.7.14:
|
||||
resolution: {integrity: sha512-zyA5Y3ArvVG0NacJDkkzJuPQDF8RFeRlzV2vLeSnhSpieO6LK2OVbdLPi5MPPs09Ii+gMO8nY4S3iKQxBxDmWQ==}
|
||||
dependencies:
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import type { Account, AccountCredentials, Attachment, CreateStatusParams, Emoji, Instance, Notification, Status } from 'masto'
|
||||
import type { Account, AccountCredentials, Attachment, CreateStatusParams, Emoji, Instance, MastoClient, Notification, Status } from 'masto'
|
||||
import type { Ref } from 'vue'
|
||||
import type { Mutable } from './utils'
|
||||
|
||||
export interface AppInfo {
|
||||
|
@ -17,6 +18,11 @@ export interface UserLogin {
|
|||
account: AccountCredentials
|
||||
}
|
||||
|
||||
export interface ElkMasto extends MastoClient {
|
||||
loginTo (user?: Omit<UserLogin, 'account'> & { account?: AccountCredentials }): Promise<MastoClient>
|
||||
loggedIn: Ref<boolean>
|
||||
}
|
||||
|
||||
export type PaginatorState = 'idle' | 'loading' | 'done' | 'error'
|
||||
|
||||
export interface ServerInfo extends Instance {
|
||||
|
|
|
@ -9,6 +9,10 @@ export default defineConfig({
|
|||
'~/': `${resolve(__dirname)}/`,
|
||||
},
|
||||
},
|
||||
define: {
|
||||
'process.server': 'false',
|
||||
'process.client': 'true',
|
||||
},
|
||||
plugins: [
|
||||
Vue(),
|
||||
AutoImport({
|
||||
|
|
Loading…
Reference in New Issue