feat: make internal app URLs permalinks (#329)
This commit is contained in:
parent
4f8f2ed1f1
commit
eb022c92e8
|
@ -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"
|
||||||
|
|
|
@ -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')"
|
||||||
|
|
|
@ -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') }}
|
||||||
|
|
|
@ -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}`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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')
|
||||||
})
|
})
|
||||||
|
|
51
middleware/permalink.global.ts
Normal file
51
middleware/permalink.global.ts
Normal 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'
|
||||||
|
})
|
|
@ -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'
|
|
||||||
|
|
||||||
if (!permalink.startsWith('http'))
|
|
||||||
permalink = `https://${permalink}`
|
|
||||||
|
|
||||||
if (!currentUser.value) {
|
|
||||||
const { host, pathname } = parseURL(permalink)
|
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>
|
||||||
|
|
Loading…
Reference in a new issue