feat: make internal app URLs permalinks (#329)

This commit is contained in:
Daniel Roe 2022-12-04 19:56:33 +00:00 committed by GitHub
parent 4f8f2ed1f1
commit eb022c92e8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 99 additions and 51 deletions

View file

@ -0,0 +1,88 @@
<script setup lang="ts">
import type { Status } from 'masto'
import type { ComponentPublicInstance } from 'vue'
definePageMeta({
name: 'status',
})
const route = useRoute()
const id = $(computedEager(() => route.params.status as string))
const main = ref<ComponentPublicInstance | null>(null)
let bottomSpace = $ref(0)
const { data: status, pending, refresh: refreshStatus } = useAsyncData(`status:${id}`, async () => (
window.history.state?.status as Status | undefined)
?? await fetchStatus(id),
)
const { data: context, pending: pendingContext, refresh: refreshContext } = useAsyncData(`context:${id}`, () => useMasto().statuses.fetchContext(id))
const replyDraft = $computed(() => status.value ? getReplyDraft(status.value) : null)
function scrollTo() {
const statusElement = unrefElement(main)
if (!statusElement)
return
const statusRect = statusElement.getBoundingClientRect()
bottomSpace = window.innerHeight - statusRect.height
statusElement.scrollIntoView(true)
}
onMounted(scrollTo)
if (pendingContext) {
watchOnce(pendingContext, async () => {
await nextTick()
scrollTo()
})
}
onReactivated(() => {
// Silently update data when reentering the page
// The user will see the previous content first, and any changes will be updated to the UI when the request is completed
refreshStatus()
refreshContext()
})
</script>
<template>
<MainContent back>
<template v-if="!pending">
<div v-if="status" min-h-100vh>
<template v-if="context">
<template v-for="comment of context?.ancestors" :key="comment.id">
<StatusCard :status="comment" context="account" border="t base" py3 />
</template>
</template>
<StatusDetails
ref="main"
:status="status"
command
border="t base"
style="scroll-margin-top: 60px"
/>
<PublishWidget
v-if="currentUser"
:draft-key="replyDraft!.key"
:initial="replyDraft!.draft"
border="t base"
@published="refreshContext()"
/>
<template v-if="context">
<template v-for="comment of context?.descendants" :key="comment.id">
<StatusCard :status="comment" context="account" border="t base" py3 />
</template>
</template>
<div border="t base" :style="{ height: `${bottomSpace}px` }" />
</div>
<StatusNotFound v-else :account="$route.params.account" :status="id" />
</template>
<StatusCardSkeleton v-else border="b base" py-3 />
</MainContent>
</template>

View file

@ -0,0 +1,48 @@
<script setup lang="ts">
const params = useRoute().params
const accountName = $(computedEager(() => toShortHandle(params.account as string)))
const { t } = useI18n()
const { data: account, refresh } = $(await useAsyncData(() => fetchAccountByHandle(accountName).catch(() => null)))
const relationship = $computed(() => account ? useRelationship(account).value : undefined)
if (account) {
useHeadFixed({
title: () => `${getDisplayName(account)} (@${account.acct})`,
})
}
onReactivated(() => {
// Silently update data when reentering the page
// The user will see the previous content first, and any changes will be updated to the UI when the request is completed
refresh()
})
</script>
<template>
<MainContent back>
<template #title>
<span text-lg font-bold>{{ account ? getDisplayName(account) : t('nav_side.profile') }}</span>
</template>
<template v-if="account">
<AccountMoved v-if="account.moved" :account="account" />
<AccountHeader :account="account" command border="b base" :class="{ 'op-50 grayscale-50': !!account.moved }" />
<div v-if="relationship?.blockedBy" h-30 flex="~ col center gap-2">
<div text-secondary>
{{ $t('account.profile_unavailable') }}
</div>
<div text-secondary-light text-sm>
{{ $t('account.blocked_by') }}
</div>
</div>
<NuxtPage v-else />
</template>
<CommonNotFound v-else>
{{ $t('error.account_not_found', [`@${accountName}`]) }}
</CommonNotFound>
</MainContent>
</template>

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
const params = useRoute().params
const handle = $(computedEager(() => params.account as string))
definePageMeta({ name: 'account-followers' })
const account = await fetchAccountByHandle(handle)
const paginator = account ? useMasto().accounts.iterateFollowers(account.id, {}) : null
</script>
<template>
<template v-if="account">
<AccountPaginator :paginator="paginator" />
</template>
</template>

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
const params = useRoute().params
const handle = $(computedEager(() => params.account as string))
definePageMeta({ name: 'account-following' })
const account = await fetchAccountByHandle(handle)
const paginator = account ? useMasto().accounts.iterateFollowing(account.id, {}) : null
</script>
<template>
<template v-if="account">
<AccountPaginator :paginator="paginator" />
</template>
</template>

View file

@ -0,0 +1,52 @@
<script setup lang="ts">
import type { Account } from 'masto'
const params = useRoute().params
const handle = $(computedEager(() => params.account as string))
definePageMeta({ name: 'account-index' })
const { data: account } = await useAsyncData(`account:${handle}`, async () => (
window.history.state?.account as Account | undefined)
?? await fetchAccountByHandle(handle),
)
const { t } = useI18n()
const paginatorPosts = useMasto().accounts.iterateStatuses(account.value!.id, { excludeReplies: true })
const paginatorPostsWithReply = useMasto().accounts.iterateStatuses(account.value!.id, { excludeReplies: false })
const paginatorMedia = useMasto().accounts.iterateStatuses(account.value!.id, { onlyMedia: true, excludeReplies: false })
const tabs = $computed(() => [
{
name: 'posts',
display: t('tab.posts'),
icon: 'i-ri:file-list-2-line',
paginator: paginatorPosts,
},
{
name: 'relies',
display: t('tab.posts_with_replies'),
icon: 'i-ri:chat-3-line',
paginator: paginatorPostsWithReply,
},
{
name: 'media',
display: t('tab.media'),
icon: 'i-ri:camera-2-line',
paginator: paginatorMedia,
},
] as const)
// Don't use local storage because it is better to default to Posts every time you visit a user's profile.
const tab = $ref(tabs[0].name)
const paginator = $computed(() => tabs.find(t => t.name === tab)!.paginator)
</script>
<template>
<div>
<CommonTabs v-model="tab" :options="tabs" command />
<KeepAlive>
<TimelinePaginator :key="tab" :paginator="paginator" context="account" />
</KeepAlive>
</div>
</template>

View file

@ -0,0 +1,25 @@
<script setup lang="ts">
const paginator = useMasto().trends.getStatuses()
const { t } = useI18n()
useHeadFixed({
title: () => t('nav_side.explore'),
})
</script>
<template>
<MainContent>
<template #title>
<NuxtLink to="/explore" text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
<div i-ri:hashtag />
<span>{{ t('nav_side.explore') }}</span>
</NuxtLink>
</template>
<slot>
<!-- TODO: Tabs for trending statuses, tags, and links -->
<TimelinePaginator :paginator="paginator" context="public" />
</slot>
</MainContent>
</template>

View file

@ -0,0 +1,26 @@
<script setup lang="ts">
const paginator = useMasto().timelines.iteratePublic()
const stream = await useMasto().stream.streamPublicTimeline()
onBeforeUnmount(() => stream.disconnect())
const { t } = useI18n()
useHeadFixed({
title: () => t('title.federated_timeline'),
})
</script>
<template>
<MainContent>
<template #title>
<NuxtLink to="/public" text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
<div i-ri:group-2-line />
<span>{{ t('title.federated_timeline') }}</span>
</NuxtLink>
</template>
<slot>
<TimelinePaginator v-bind="{ paginator, stream }" context="public" />
</slot>
</MainContent>
</template>

View file

@ -0,0 +1,26 @@
<script setup lang="ts">
const paginator = useMasto().timelines.iteratePublic({ local: true })
const stream = await useMasto().stream.streamCommunityTimeline()
onBeforeUnmount(() => stream.disconnect())
const { t } = useI18n()
useHeadFixed({
title: () => t('title.local_timeline'),
})
</script>
<template>
<MainContent>
<template #title>
<NuxtLink to="/public/local" text-lg font-bold flex items-center gap-2 @click="$scrollToTop">
<div i-ri:group-2-line />
<span>{{ t('title.local_timeline') }}</span>
</NuxtLink>
</template>
<slot>
<TimelinePaginator v-bind="{ paginator, stream }" context="public" />
</slot>
</MainContent>
</template>

View file

@ -0,0 +1,15 @@
<script setup lang="ts">
definePageMeta({
name: 'status-by-id',
middleware: async (to) => {
const params = to.params
const id = params.status as string
const status = await fetchStatus(id)
return getStatusRoute(status)
},
})
</script>
<template>
<div />
</template>

View file

@ -0,0 +1,38 @@
<script setup lang="ts">
const params = useRoute().params
const tagName = $(computedEager(() => params.tag as string))
const { data: tag, refresh } = $(await useAsyncData(() => useMasto().tags.fetch(tagName)))
const paginator = useMasto().timelines.iterateHashtag(tagName)
const stream = await useMasto().stream.streamTagTimeline(tagName)
onBeforeUnmount(() => stream.disconnect())
if (tag) {
useHeadFixed({
title: () => `#${tag.name}`,
})
}
onReactivated(() => {
// Silently update data when reentering the page
// The user will see the previous content first, and any changes will be updated to the UI when the request is completed
refresh()
})
</script>
<template>
<MainContent back>
<template #title>
<span text-lg font-bold>#{{ tagName }}</span>
</template>
<template v-if="typeof tag?.following === 'boolean'" #actions>
<TagActionButton :tag="tag" @change="refresh()" />
</template>
<slot>
<TimelinePaginator v-bind="{ paginator, stream }" context="public" />
</slot>
</MainContent>
</template>