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