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

@ -44,7 +44,7 @@ const toggleBlockDomain = async () => {
</button> </button>
<template #popper> <template #popper>
<NuxtLink :to="account.url" target="_blank"> <NuxtLink :to="account.url" external target="_blank">
<CommonDropdownItem <CommonDropdownItem
:text="$t('menu.open_in_original_site')" :text="$t('menu.open_in_original_site')"
icon="i-ri:arrow-right-up-line" icon="i-ri:arrow-right-up-line"

View file

@ -37,9 +37,9 @@ const toggleTranslation = async () => {
} }
const copyLink = async (status: Status) => { const copyLink = async (status: Status) => {
const url = getStatusPermalinkRoute(status)?.href const url = getStatusPermalinkRoute(status)
if (url) if (url)
await clipboard.copy(`${location.origin}${url}`) await clipboard.copy(`${location.origin}/${url}`)
} }
const deleteStatus = async () => { const deleteStatus = async () => {
// TODO confirm to delete // TODO confirm to delete
@ -145,7 +145,7 @@ function editStatus() {
@click="copyLink(status)" @click="copyLink(status)"
/> />
<NuxtLink :to="status.url" target="_blank"> <NuxtLink :to="status.url" external target="_blank">
<CommonDropdownItem <CommonDropdownItem
v-if="status.url" v-if="status.url"
:text="$t('menu.open_in_original_site')" :text="$t('menu.open_in_original_site')"

View file

@ -19,7 +19,7 @@ const originalUrl = computed(() => {
<div flex="~ col center gap2"> <div flex="~ col center gap2">
<div>{{ $t('error.status_not_found') }}</div> <div>{{ $t('error.status_not_found') }}</div>
<NuxtLink v-if="originalUrl" :to="originalUrl" target="_blank"> <NuxtLink v-if="originalUrl" :to="originalUrl" external target="_blank">
<button btn-solid flex="~ center gap-2" text-sm px2 py1> <button btn-solid flex="~ center gap-2" text-sm px2 py1>
<div i-ri:arrow-right-up-line /> <div i-ri:arrow-right-up-line />
{{ $t('status.try_original_site') }} {{ $t('status.try_original_site') }}

View file

@ -18,15 +18,14 @@ function handleMention(el: Element) {
const matchUser = href.value.match(UserLinkRE) const matchUser = href.value.match(UserLinkRE)
if (matchUser) { if (matchUser) {
const [, server, username] = matchUser const [, server, username] = matchUser
// Handles need to ignore server subdomains
const handle = `@${username}@${server.replace(/(.+\.)(.+\..+)/, '$2')}` const handle = `@${username}@${server.replace(/(.+\.)(.+\..+)/, '$2')}`
href.value = `/${handle}` href.value = `/${server}/@${username}`
return h(AccountHoverWrapper, { handle, class: 'inline-block' }, () => nodeToVNode(el)) return h(AccountHoverWrapper, { handle, class: 'inline-block' }, () => nodeToVNode(el))
} }
const matchTag = href.value.match(TagLinkRE) const matchTag = href.value.match(TagLinkRE)
if (matchTag) { if (matchTag) {
const [, , name] = matchTag const [, , name] = matchTag
href.value = `/tags/${name}` href.value = `/${currentServer.value}/tags/${name}`
} }
} }
} }

View file

@ -64,10 +64,15 @@ export function toShortHandle(fullHandle: string) {
} }
export function getAccountRoute(account: Account) { export function getAccountRoute(account: Account) {
let handle = getFullHandle(account).slice(1)
if (handle.endsWith(`@${currentServer.value}`))
handle = handle.slice(0, -currentServer.value.length - 1)
return useRouter().resolve({ return useRouter().resolve({
name: 'account-index', name: 'account-index',
params: { params: {
account: getFullHandle(account).slice(1), server: currentServer.value,
account: handle,
}, },
state: { state: {
account: account as any, account: account as any,
@ -78,6 +83,7 @@ export function getAccountFollowingRoute(account: Account) {
return useRouter().resolve({ return useRouter().resolve({
name: 'account-following', name: 'account-following',
params: { params: {
server: currentServer.value,
account: getFullHandle(account).slice(1), account: getFullHandle(account).slice(1),
}, },
state: { state: {
@ -89,6 +95,7 @@ export function getAccountFollowersRoute(account: Account) {
return useRouter().resolve({ return useRouter().resolve({
name: 'account-followers', name: 'account-followers',
params: { params: {
server: currentServer.value,
account: getFullHandle(account).slice(1), account: getFullHandle(account).slice(1),
}, },
state: { state: {
@ -101,6 +108,7 @@ export function getStatusRoute(status: Status) {
return useRouter().resolve({ return useRouter().resolve({
name: 'status', name: 'status',
params: { params: {
server: currentServer.value,
account: getFullHandle(status.account).slice(1), account: getFullHandle(status.account).slice(1),
status: status.id, status: status.id,
}, },
@ -111,18 +119,14 @@ export function getStatusRoute(status: Status) {
} }
export function getStatusPermalinkRoute(status: Status) { export function getStatusPermalinkRoute(status: Status) {
return status.url return status.url ? withoutProtocol(status.url) : null
? useRouter().resolve({
name: 'permalink',
params: { permalink: withoutProtocol(status.url) },
})
: null
} }
export function getStatusInReplyToRoute(status: Status) { export function getStatusInReplyToRoute(status: Status) {
return useRouter().resolve({ return useRouter().resolve({
name: 'status-by-id', name: 'status-by-id',
params: { params: {
server: currentServer.value,
status: status.inReplyToId, status: status.inReplyToId,
}, },
}) })
@ -143,6 +147,8 @@ const requestedRelationships = new Map<string, Ref<Relationship | undefined>>()
let timeoutHandle: NodeJS.Timeout | undefined let timeoutHandle: NodeJS.Timeout | undefined
export function useRelationship(account: Account): Ref<Relationship | undefined> { export function useRelationship(account: Account): Ref<Relationship | undefined> {
if (!currentUser.value)
return ref()
let relationship = requestedRelationships.get(account.id) let relationship = requestedRelationships.get(account.id)
if (relationship) if (relationship)
return relationship return relationship

View file

@ -21,11 +21,12 @@ export const currentUser = computed<UserLogin | undefined>(() => {
}) })
export const publicServer = ref(DEFAULT_SERVER) export const publicServer = ref(DEFAULT_SERVER)
const publicInstance = ref<Instance | null>(null)
export const currentServer = computed<string>(() => currentUser.value?.server || publicServer.value) export const currentServer = computed<string>(() => currentUser.value?.server || publicServer.value)
export const useUsers = () => users export const useUsers = () => users
export const currentInstance = computed<null | Instance>(() => currentUserId.value ? servers.value[currentUserId.value] ?? null : null) export const currentInstance = computed<null | Instance>(() => currentUserId.value ? servers.value[currentUserId.value] ?? null : publicInstance.value)
export const characterLimit = computed(() => currentInstance.value?.configuration.statuses.maxCharacters ?? DEFAULT_POST_CHARS_LIMIT) export const characterLimit = computed(() => currentInstance.value?.configuration.statuses.maxCharacters ?? DEFAULT_POST_CHARS_LIMIT)
@ -37,14 +38,18 @@ export async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: Ac
} }
const config = useRuntimeConfig() const config = useRuntimeConfig()
const route = useRoute()
const router = useRouter()
const server = user?.server || route.params.server as string || publicServer.value
const masto = await loginMasto({ const masto = await loginMasto({
url: `https://${user?.server || DEFAULT_SERVER}`, url: `https://${server}`,
accessToken: user?.token, accessToken: user?.token,
disableVersionCheck: !!config.public.disableVersionCheck, disableVersionCheck: !!config.public.disableVersionCheck,
}) })
if (!user?.token) { if (!user?.token) {
publicServer.value = user?.server || DEFAULT_SERVER publicServer.value = server
publicInstance.value = await masto.instances.fetch()
} }
else { else {
@ -71,6 +76,13 @@ export async function loginTo(user?: Omit<UserLogin, 'account'> & { account?: Ac
setMasto(masto) setMasto(masto)
if ('server' in route.params) {
await router.push({
...route,
force: true,
})
}
return masto return masto
} }
@ -98,7 +110,7 @@ export async function signout() {
currentUserId.value = users.value[0]?.account?.id currentUserId.value = users.value[0]?.account?.id
if (!currentUserId.value) if (!currentUserId.value)
await useRouter().push('/public') await useRouter().push(`/${currentServer.value}/public`)
await loginTo(currentUser.value) await loginTo(currentUser.value)
} }

View file

@ -1,6 +1,6 @@
export default defineNuxtRouteMiddleware((from) => { export default defineNuxtRouteMiddleware((to) => {
if (!currentUser.value) if (!currentUser.value)
return navigateTo('/public') return navigateTo('/public')
else if (from.path === '/') if (to.path === '/')
return navigateTo('/home') return navigateTo('/home')
}) })

View file

@ -0,0 +1,51 @@
export default defineNuxtRouteMiddleware(async (to, from) => {
// Skip running middleware before masto has been initialised
if (!useNuxtApp().$masto)
return
if (!('server' in to.params))
return
if (!currentUser.value) {
if (from.params.server !== to.params.server) {
await loginTo({
server: to.params.server as string,
})
}
return
}
// No need to additionally resolve an id if we're already logged in
if (currentUser.value.server === to.params.server)
return
// Tags don't need to be redirected to a local id
if (to.params.tag)
return
// Handle redirecting to new permalink structure for users with old links
if (!to.params.server) {
return {
...to,
params: {
...to.params,
server: currentUser.value.server,
},
}
}
try {
// If we're logged in, search for the local id the account or status corresponds to
const { value } = await useMasto().search({ q: `https:/${to.fullPath}`, resolve: true, limit: 1 }).next()
const { accounts, statuses } = value
if (statuses[0])
return getStatusRoute(statuses[0])
if (accounts[0])
return getAccountRoute(accounts[0])
}
catch {}
return '/home'
})

View file

@ -1,43 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { parseURL } from 'ufo' import { hasProtocol, parseURL } from 'ufo'
import { HANDLED_MASTO_URLS } from '~/constants'
definePageMeta({ definePageMeta({
name: 'permalink',
middleware: async (to) => { middleware: async (to) => {
try { const permalink = Array.isArray(to.params.permalink)
let permalink = Array.isArray(to.params.permalink) ? to.params.permalink.join('/')
? to.params.permalink.join('/') : to.params.permalink
: to.params.permalink
if (!HANDLED_MASTO_URLS.test(permalink)) if (hasProtocol(permalink)) {
return '/home' const { host, pathname } = parseURL(permalink)
if (!permalink.startsWith('http'))
permalink = `https://${permalink}`
if (!currentUser.value) {
const { host, pathname } = parseURL(permalink)
await loginTo({ server: host! })
if (pathname.match(/^\/@[^/]+$/))
return `${pathname}@${host}`
if (host) {
await loginTo({ server: host })
return pathname return pathname
} }
const { value } = await useMasto().search({ q: permalink, resolve: true, limit: 1 }).next()
const { accounts, statuses } = value
if (statuses[0])
return getStatusRoute(statuses[0])
if (accounts[0])
return getAccountRoute(accounts[0])
} }
catch {}
return '/home' // We've reached a page that doesn't exist
return false
}, },
}) })
</script> </script>