feat: disable SSR

This commit is contained in:
Anthony Fu 2022-11-23 07:08:36 +08:00
parent e59b3e5db2
commit a6578155ae
29 changed files with 109 additions and 175 deletions

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Account, MastoClient } from 'masto' import type { Account } from 'masto'
const { account } = defineProps<{ const { account } = defineProps<{
account: Account account: Account
@ -7,11 +7,8 @@ const { account } = defineProps<{
const relationship = $(useRelationship(account)) const relationship = $(useRelationship(account))
let masto: MastoClient
async function toggleFollow() { async function toggleFollow() {
relationship!.following = !relationship!.following relationship!.following = !relationship!.following
masto ??= await useMasto()
await masto.accounts[relationship!.following ? 'follow' : 'unfollow'](account.id) await masto.accounts[relationship!.following ? 'follow' : 'unfollow'](account.id)
} }
</script> </script>

View file

@ -1,7 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const { currentUser } = $(useClientState()) const account = $computed(() => currentUser.value?.account)
const account = $computed(() => currentUser?.account)
</script> </script>
<template> <template>

View file

@ -1,5 +1,3 @@
import type { Emoji } from 'masto'
export default defineComponent({ export default defineComponent({
props: { props: {
content: { content: {
@ -8,15 +6,11 @@ export default defineComponent({
}, },
}, },
setup(props) { setup(props) {
const emojis = shallowRef<Record<string, Emoji>>({}) const serverInfos = useServerInfo(currentServer.value)
return () => h(
onMounted(() => { 'div',
const { server } = useAppCookies() { class: 'rich-content' },
const { serverInfos } = useClientState() contentToVNode(props.content, undefined, serverInfos.value?.customEmojis),
if (server.value) )
emojis.value = serverInfos.value[server.value].customEmojis || {}
})
return () => h('div', { class: 'rich-content' }, contentToVNode(props.content, undefined, emojis.value))
}, },
}) })

View file

@ -1,10 +1,9 @@
<script setup lang="ts"> <script setup lang="ts">
const isLogin = useLoginState()
</script> </script>
<template> <template>
<div px6 py2 flex="~ col gap6" text-lg> <div px6 py2 flex="~ col gap6" text-lg>
<template v-if="isLogin"> <template v-if="currentUser">
<NuxtLink flex gap2 items-center to="/home" active-class="text-primary"> <NuxtLink flex gap2 items-center to="/home" active-class="text-primary">
<div i-ri:home-5-line /> <div i-ri:home-5-line />
<span>Home</span> <span>Home</span>
@ -26,7 +25,7 @@ const isLogin = useLoginState()
<div i-ri:earth-line /> <div i-ri:earth-line />
<span>Federated</span> <span>Federated</span>
</NuxtLink> </NuxtLink>
<template v-if="isLogin"> <template v-if="currentUser">
<NuxtLink flex gap2 items-center to="/conversations" active-class="text-primary"> <NuxtLink flex gap2 items-center to="/conversations" active-class="text-primary">
<div i-ri:at-line /> <div i-ri:at-line />
<span>Conversations</span> <span>Conversations</span>

View file

@ -11,8 +11,6 @@ const {
inReplyToId?: string inReplyToId?: string
}>() }>()
const masto = await useMasto()
let isSending = $ref(false) let isSending = $ref(false)
const storageKey = `nuxtodon-draft-${draftKey}` const storageKey = `nuxtodon-draft-${draftKey}`
function getDefaultStatus(): CreateStatusParamsWithStatus { function getDefaultStatus(): CreateStatusParamsWithStatus {

View file

@ -5,8 +5,6 @@ const { status } = defineProps<{
status: Status status: Status
}>() }>()
const masto = await useMasto()
// Use different states to let the user press different actions right after the other // Use different states to let the user press different actions right after the other
const isLoading = $ref({ reblogged: false, favourited: false, bookmarked: false }) const isLoading = $ref({ reblogged: false, favourited: false, bookmarked: false })
async function toggleStatusAction(action: 'reblogged' | 'favourited' | 'bookmarked', newStatus: Promise<Status>) { async function toggleStatusAction(action: 'reblogged' | 'favourited' | 'bookmarked', newStatus: Promise<Status>) {

40
composables/accounts.ts Normal file
View file

@ -0,0 +1,40 @@
import { login as loginMasto } from 'masto'
import type { UserLogin } from '~/types'
import { DEFAULT_SERVER } from '~/constants'
const accounts = useLocalStorage<UserLogin[]>('nuxtodon-accounts', [], { deep: true })
const currentId = useLocalStorage<string>('nuxtodon-current-user', '')
export const currentUser = computed<UserLogin | undefined>(() => {
let user: UserLogin | undefined
if (currentId.value) {
user = accounts.value.find(user => user.account?.id === currentId.value)
if (user)
return user
}
// Fallback to the first account
return accounts.value[0]
})
export const currentServer = computed<string>(() => currentUser.value?.server || DEFAULT_SERVER)
export async function loginCallback(user: UserLogin) {
const existing = accounts.value.findIndex(u => u.server === user.server && u.token === user.token)
if (existing !== -1) {
if (currentId.value === accounts.value[existing].account?.id)
return null
currentId.value = user.account?.id
return true
}
const masto = await loginMasto({
url: `https://${user.server}`,
accessToken: user.token,
})
const me = await masto.accounts.verifyCredentials()
user.account = me
accounts.value.push(user)
currentId.value = me.id
return true
}

View file

@ -1,10 +1,9 @@
import type { MastoClient } from 'masto' import { login } from 'masto'
import type { ClientState } from '~/plugins/store.client' import { currentUser } from './accounts'
import { DEFAULT_SERVER } from '~/constants'
export function useMasto() { // TODO: improve upsteam to make this synchronous (delayed auth)
return useNuxtApp().$masto as Promise<MastoClient> export const masto = await login({
} url: `https://${currentUser.value?.server || DEFAULT_SERVER}`,
accessToken: currentUser.value?.token || undefined,
export function useClientState() { })
return useNuxtApp().$clientState as ClientState
}

View file

@ -1,16 +0,0 @@
import { DEFAULT_SERVER } from '~/constants'
export function useAppCookies() {
const server = useCookie('nuxtodon-server', { default: () => DEFAULT_SERVER })
const token = useCookie('nuxtodon-token')
return {
server,
token,
}
}
export function useLoginState() {
const token = useCookie('nuxtodon-token')
return computed(() => !!token.value)
}

View file

@ -1,5 +1,5 @@
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { Account, MastoClient, Relationship } from 'masto' import type { Account, Relationship } from 'masto'
export function getDisplayName(account: Account) { export function getDisplayName(account: Account) {
return account.displayName || account.username return account.displayName || account.username
@ -28,8 +28,6 @@ export function useRelationship(account: Account): Ref<Relationship | undefined>
} }
async function fetchRelationships() { async function fetchRelationships() {
const masto = await useMasto()
const requested = Array.from(requestedRelationships.entries()) const requested = Array.from(requestedRelationships.entries())
requestedRelationships.clear() requestedRelationships.clear()

41
composables/servers.ts Normal file
View file

@ -0,0 +1,41 @@
import type { ServerInfo } from '~/types'
const ServerInfoTTL = 60 * 60 * 1000 * 12 // 12 hour
const serverInfoPromise = new Map<string, Promise<ServerInfo>>()
const serverInfos = useLocalStorage<Record<string, ServerInfo>>('nuxtodon-server-info', {})
async function _fetchServerInfo(server: string) {
if (!serverInfos.value[server]) {
// @ts-expect-error init
serverInfos.value[server] = {
timeUpdated: 0,
server,
}
}
if (serverInfos.value[server].timeUpdated + ServerInfoTTL < Date.now()) {
await Promise.allSettled([
masto.instances.fetch().then((r) => {
Object.assign(serverInfos.value[server], r)
}),
masto.customEmojis.fetchAll().then((r) => {
serverInfos.value[server].customEmojis = Object.fromEntries(r.map(i => [i.shortcode, i]))
}),
])
}
return serverInfos.value[server]
}
export function fetchServerInfo(server: string) {
if (!serverInfoPromise.has(server))
serverInfoPromise.set(server, _fetchServerInfo(server))
return serverInfoPromise.get(server)!
}
export function useServerInfo(server: string) {
const info = ref<ServerInfo | undefined>()
fetchServerInfo(server).then((r) => {
info.value = r
})
return info
}

View file

@ -1,7 +1,5 @@
export default defineNuxtRouteMiddleware((from) => { export default defineNuxtRouteMiddleware((from) => {
const token = useCookie('nuxtodon-token') if (!currentUser.value)
if (!token.value)
return navigateTo('/public') return navigateTo('/public')
else if (from.path === '/') else if (from.path === '/')
return navigateTo('/home') return navigateTo('/home')

View file

@ -1,4 +1,5 @@
export default defineNuxtConfig({ export default defineNuxtConfig({
ssr: false,
modules: [ modules: [
'@vueuse/nuxt', '@vueuse/nuxt',
'@unocss/nuxt', '@unocss/nuxt',

View file

@ -1,10 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
const { currentUser } = $(useClientState())
const params = useRoute().params const params = useRoute().params
const id = computed(() => params.post as string) const id = computed(() => params.post as string)
const masto = await useMasto()
const { data: status } = await useAsyncData(`${id}-status`, () => masto.statuses.fetch(params.post as string)) const { data: status } = await useAsyncData(`${id}-status`, () => masto.statuses.fetch(params.post as string))
const { data: context } = await useAsyncData(`${id}-context`, () => masto.statuses.fetchContext(params.post as string)) const { data: context } = await useAsyncData(`${id}-context`, () => masto.statuses.fetchContext(params.post as string))
</script> </script>

View file

@ -5,7 +5,7 @@ const props = defineProps<{
const params = useRoute().params const params = useRoute().params
const user = $computed(() => params.user as string) const user = $computed(() => params.user as string)
const masto = await useMasto()
const { data: account } = await useAsyncData(`${user}:info`, () => masto.accounts.lookup({ acct: user })) const { data: account } = await useAsyncData(`${user}:info`, () => masto.accounts.lookup({ acct: user }))
const paginator = masto.accounts.getFollowersIterable(account.value!.id!, {}) const paginator = masto.accounts.getFollowersIterable(account.value!.id!, {})
</script> </script>

View file

@ -5,7 +5,7 @@ const props = defineProps<{
const params = useRoute().params const params = useRoute().params
const user = $computed(() => params.user as string) const user = $computed(() => params.user as string)
const masto = await useMasto()
const { data: account } = await useAsyncData(`${user}:info`, () => masto.accounts.lookup({ acct: user })) const { data: account } = await useAsyncData(`${user}:info`, () => masto.accounts.lookup({ acct: user }))
const paginator = masto.accounts.getFollowingIterable(account.value!.id!, {}) const paginator = masto.accounts.getFollowingIterable(account.value!.id!, {})
</script> </script>

View file

@ -5,7 +5,7 @@ const props = defineProps<{
const params = useRoute().params const params = useRoute().params
const user = $computed(() => params.user as string) const user = $computed(() => params.user as string)
const masto = await useMasto()
const { data: account } = await useAsyncData(`${user}:info`, () => masto.accounts.lookup({ acct: user })) const { data: account } = await useAsyncData(`${user}:info`, () => masto.accounts.lookup({ acct: user }))
const tabNames = ['Posts', 'Posts and replies'] as const const tabNames = ['Posts', 'Posts and replies'] as const

View file

@ -3,7 +3,6 @@ definePageMeta({
middleware: 'auth', middleware: 'auth',
}) })
const masto = await useMasto()
const paginator = masto.bookmarks.getIterator() const paginator = masto.bookmarks.getIterator()
</script> </script>

View file

@ -3,7 +3,6 @@ definePageMeta({
middleware: 'auth', middleware: 'auth',
}) })
const masto = await useMasto()
const paginator = masto.conversations.getIterator() const paginator = masto.conversations.getIterator()
</script> </script>

View file

@ -3,7 +3,6 @@ definePageMeta({
middleware: 'auth', middleware: 'auth',
}) })
const masto = await useMasto()
const paginator = masto.trends.getStatuses() const paginator = masto.trends.getStatuses()
</script> </script>

View file

@ -3,7 +3,6 @@ definePageMeta({
middleware: 'auth', middleware: 'auth',
}) })
const masto = await useMasto()
const paginator = masto.favourites.getIterator() const paginator = masto.favourites.getIterator()
</script> </script>

View file

@ -3,7 +3,6 @@ definePageMeta({
middleware: 'auth', middleware: 'auth',
}) })
const masto = await useMasto()
const paginator = masto.timelines.getHomeIterable() const paginator = masto.timelines.getHomeIterable()
</script> </script>

View file

@ -6,8 +6,7 @@ definePageMeta({
const { query } = useRoute() const { query } = useRoute()
onMounted(async () => { onMounted(async () => {
const { login } = useClientState() await loginCallback(query as any)
await login(query as any)
await nextTick() await nextTick()
await nextTick() await nextTick()
location.pathname = '/' location.pathname = '/'

View file

@ -3,8 +3,6 @@ definePageMeta({
middleware: 'auth', middleware: 'auth',
}) })
const masto = await useMasto()
const tabNames = ['All', 'Mentions'] as const const tabNames = ['All', 'Mentions'] as const
const tab = $(useLocalStorage<typeof tabNames[number]>('nuxtodon-notifications-tab', 'All')) const tab = $(useLocalStorage<typeof tabNames[number]>('nuxtodon-notifications-tab', 'All'))

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const masto = await useMasto()
const paginator = masto.timelines.getPublicIterable() const paginator = masto.timelines.getPublicIterable()
</script> </script>

View file

@ -4,7 +4,7 @@ const router = useRouter()
if (!token.value) if (!token.value)
router.replace('/public') router.replace('/public')
const masto = await useMasto()
const { data: timelines } = await useAsyncData('timelines-home', () => masto.timelines.fetchPublic({ local: true }).then(r => r.value)) const { data: timelines } = await useAsyncData('timelines-home', () => masto.timelines.fetchPublic({ local: true }).then(r => r.value))
</script> </script>

View file

@ -1,7 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
const params = useRoute().params const params = useRoute().params
const tag = $computed(() => params.tag as string) const tag = $computed(() => params.tag as string)
const masto = await useMasto()
const paginator = masto.timelines.getHashtagIterable(tag) const paginator = masto.timelines.getHashtagIterable(tag)
</script> </script>

View file

@ -1,12 +0,0 @@
import { login } from 'masto'
export default defineNuxtPlugin((nuxt) => {
const { server, token } = useAppCookies()
const masto = login({
url: `https://${server.value}`,
accessToken: token.value || undefined,
})
nuxt.$masto = masto
})

View file

@ -1,88 +0,0 @@
import { login as loginMasto } from 'masto'
import type { ServerInfo, UserLogin } from '~/types'
const ServerInfoTTL = 60 * 60 * 1000 * 12 // 12 hour
function createClientState() {
const { server, token } = useAppCookies()
const accounts = useLocalStorage<UserLogin[]>('nuxtodon-accounts', [], { deep: true })
const currentId = useLocalStorage<string>('nuxtodon-current-user', '')
const currentUser = computed<UserLogin | undefined>(() => {
let user: UserLogin | undefined
if (currentId.value) {
user = accounts.value.find(user => user.account?.id === currentId.value)
if (user)
return user
}
// Fallback to the first account
return accounts.value[0]
})
async function login(user: UserLogin) {
const existing = accounts.value.findIndex(u => u.server === user.server && u.token === user.token)
if (existing !== -1) {
if (currentId.value === accounts.value[existing].account?.id)
return null
currentId.value = user.account?.id
server.value = user.server
token.value = user.token
return true
}
const masto = await loginMasto({
url: `https://${user.server}`,
accessToken: user.token,
})
const me = await masto.accounts.verifyCredentials()
user.account = me
accounts.value.push(user)
currentId.value = me.id
server.value = user.server
token.value = user.token
return true
}
const serverInfos = useLocalStorage<Record<string, ServerInfo>>('nuxtodon-server-info', {})
async function fetchServerInfo(server: string) {
if (!serverInfos.value[server]) {
// @ts-expect-error init
serverInfos.value[server] = {
timeUpdated: 0,
server,
}
}
if (serverInfos.value[server].timeUpdated + ServerInfoTTL < Date.now()) {
const masto = await useMasto()
await Promise.allSettled([
masto.instances.fetch().then((r) => {
Object.assign(serverInfos.value[server], r)
}),
masto.customEmojis.fetchAll().then((r) => {
serverInfos.value[server].customEmojis = Object.fromEntries(r.map(i => [i.shortcode, i]))
}),
])
}
return serverInfos.value[server]
}
if (server.value)
fetchServerInfo(server.value)
return {
currentUser,
accounts,
login,
serverInfos,
fetchServerInfo,
}
}
export type ClientState = ReturnType<typeof createClientState>
export default defineNuxtPlugin((nuxt) => {
nuxt.$clientState = createClientState()
})