feat: wip

This commit is contained in:
三咲智子 2023-01-03 17:39:00 +08:00
parent f7df0e54f5
commit 313cafa23c
No known key found for this signature in database
GPG key ID: 69992F2250DFD93E
18 changed files with 146 additions and 129 deletions

View file

@ -3,9 +3,7 @@ setupPageHeader()
provideGlobalCommands() provideGlobalCommands()
// We want to trigger rerendering the page when account changes // We want to trigger rerendering the page when account changes
const key = computed(() => const key = computed(() => currentUser.value ? getUniqueUserId(currentUser.value) : '')
`${currentServer.value}:${checkUser(currentUser.value) ? currentUser.value.account.id : GUEST_ID}`,
)
</script> </script>
<template> <template>

View file

@ -0,0 +1,18 @@
<script setup lang="ts">
import type { UserLogin } from '~/types'
defineProps<{
user: UserLogin
}>()
</script>
<template>
<div flex="~ gap3" items-center>
<div bg="gray/40" rounded-full w-54px h-54px flex shrink-0 items-center justify-center text-5>
G
</div>
<div>
Guest from <span font-bold>{{ user.server }}</span>
</div>
</div>
</template>

View file

@ -7,7 +7,7 @@ const { account } = defineProps<{
}>() }>()
let relationship = $(useRelationship(account)) let relationship = $(useRelationship(account))
const isSelf = $computed(() => currentUser.value?.account.id === account.id) const isSelf = $computed(() => checkUser(currentUser.value) && currentUser.value.account.id === account.id)
const masto = useMasto() const masto = useMasto()
const toggleMute = async () => { const toggleMute = async () => {

View file

@ -9,8 +9,8 @@
w-8 w-8
:draggable="false" :draggable="false"
/> />
<div v-else> <div v-else bg="gray/40" rounded-full w-8 h-8 flex items-center justify-center text-5>
TODO: Guest G
</div> </div>
</div> </div>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
const paginator = useMasto().accounts.iterateStatuses(currentUser.value!.account.id, { pinned: true }) const paginator = useMasto().accounts.iterateStatuses(currentUser.value!.account!.id, { pinned: true })
</script> </script>
<template> <template>

View file

@ -16,7 +16,9 @@ const masto = useMasto()
@click="switchUser(user, masto)" @click="switchUser(user, masto)"
> >
<AccountAvatar v-if="!user.guest" w-13 h-13 :account="user.account" /> <AccountAvatar v-if="!user.guest" w-13 h-13 :account="user.account" />
<span v-else>TODO: Guest from {{ user.server }}</span> <div v-else bg="gray/40" rounded-full w-13 h-13 flex shrink-0 items-center justify-center text-5>
G
</div>
</button> </button>
</template> </template>
</div> </div>

View file

@ -48,7 +48,7 @@ async function oauth() {
} }
function explore() { function explore() {
masto.loginTo({ server, guest: true }) masto.loginTo({ server, guestOnly: true })
} }
async function handleInput() { async function handleInput() {

View file

@ -22,7 +22,7 @@ const masto = useMasto()
@click="switchUser(user, masto)" @click="switchUser(user, masto)"
> >
<AccountInfo v-if="!user.guest" :account="user.account" :hover-card="false" /> <AccountInfo v-if="!user.guest" :account="user.account" :hover-card="false" />
<span v-else>TODO: Guest from {{ user.server }}</span> <AccountGuest v-else :user="user" />
<div flex-auto /> <div flex-auto />
<div v-if="isSameUser(user, currentUser)" i-ri:check-line text-primary mya text-2xl /> <div v-if="isSameUser(user, currentUser)" i-ri:check-line text-primary mya text-2xl />
</button> </button>
@ -34,7 +34,7 @@ const masto = useMasto()
@click="openSigninDialog" @click="openSigninDialog"
/> />
<CommonDropdownItem <CommonDropdownItem
v-if="isMastoInitialised && currentUser" v-if="isMastoInitialised && canSignOut"
:text="$t('user.sign_out_account', [getFullHandle(currentUser)])" :text="$t('user.sign_out_account', [getFullHandle(currentUser)])"
icon="i-ri:logout-box-line rtl-flip" icon="i-ri:logout-box-line rtl-flip"
@click="signout" @click="signout"

View file

@ -248,7 +248,7 @@ export const provideGlobalCommands = () => {
useCommand({ useCommand({
scope: 'Actions', scope: 'Actions',
visible: () => currentUser.value, visible: () => !isGuest.value,
name: () => t('action.compose'), name: () => t('action.compose'),
icon: 'i-ri:quill-pen-line', icon: 'i-ri:quill-pen-line',
@ -344,9 +344,9 @@ export const provideGlobalCommands = () => {
parent: 'account-switch', parent: 'account-switch',
scope: 'Switch account', scope: 'Switch account',
visible: () => !user.guest && user.account.id !== currentUser.value?.account?.id, visible: () => !isSameUser(user, currentUser.value),
name: () => t('command.switch_account', [getFullHandle(user)]), name: () => t('command.switch_account', [user.guest ? `guest from ${user.server}` : getFullHandle(user)]),
icon: 'i-ri:user-shared-line', icon: 'i-ri:user-shared-line',
onActivate() { onActivate() {
@ -356,8 +356,8 @@ export const provideGlobalCommands = () => {
useCommand({ useCommand({
scope: 'Account', scope: 'Account',
visible: () => currentUser.value, visible: () => canSignOut.value,
name: () => currentUser.value ? t('user.sign_out_account', [getFullHandle(currentUser.value)]) : '', name: () => t('user.sign_out_account', [getFullHandle(currentUser.value)]),
icon: 'i-ri:logout-box-line', icon: 'i-ri:logout-box-line',
onActivate() { onActivate() {

View file

@ -97,7 +97,7 @@ async function subscribe(
async function unsubscribeFromBackend(fromSWPushManager: boolean, removePushNotification = true) { async function unsubscribeFromBackend(fromSWPushManager: boolean, removePushNotification = true) {
const cu = currentUser.value const cu = currentUser.value
if (cu) { if (checkUser(cu)) {
await removePushNotifications(cu) await removePushNotifications(cu)
removePushNotification && await removePushNotificationData(cu, fromSWPushManager) removePushNotification && await removePushNotificationData(cu, fromSWPushManager)
} }
@ -105,7 +105,7 @@ async function unsubscribeFromBackend(fromSWPushManager: boolean, removePushNoti
async function removePushNotificationDataOnError(e: Error) { async function removePushNotificationDataOnError(e: Error) {
const cu = currentUser.value const cu = currentUser.value
if (cu) if (checkUser(cu))
await removePushNotificationData(cu, true) await removePushNotificationData(cu, true)
throw e throw e

View file

@ -117,6 +117,9 @@ export const usePushManager = () => {
} }
const saveSettings = async (policy?: SubscriptionPolicy) => { const saveSettings = async (policy?: SubscriptionPolicy) => {
if (!checkUser(currentUser.value))
return
if (policy) if (policy)
pushNotificationData.value.policy = policy pushNotificationData.value.policy = policy
@ -133,6 +136,9 @@ export const usePushManager = () => {
} }
const undoChanges = () => { const undoChanges = () => {
if (!checkUser(currentUser.value))
return
const current = pushNotificationData.value const current = pushNotificationData.value
const previous = history.value[0].snapshot const previous = history.value[0].snapshot
current.favourite = previous.favourite current.favourite = previous.favourite

View file

@ -51,7 +51,7 @@ function mentionHTML(acct: string) {
export function getReplyDraft(status: Status) { export function getReplyDraft(status: Status) {
const accountsToMention: string[] = [] const accountsToMention: string[] = []
const userId = currentUser.value?.account.id const userId = currentUser.value!.account!.id
if (status.account.id !== userId) if (status.account.id !== userId)
accountsToMention.push(status.account.acct) accountsToMention.push(status.account.acct)
accountsToMention.push(...(status.mentions.filter(mention => mention.id !== userId).map(mention => mention.acct))) accountsToMention.push(...(status.mentions.filter(mention => mention.id !== userId).map(mention => mention.acct)))

View file

@ -45,63 +45,87 @@ const users = await initializeUsers()
const instances = useLocalStorage<Record<string, Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true }) const instances = useLocalStorage<Record<string, Instance>>(STORAGE_KEY_SERVERS, mock ? mock.server : {}, { deep: true })
const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '') const currentUserId = useLocalStorage<string>(STORAGE_KEY_CURRENT_USER, mock ? mock.user.account.id : '')
const isGuestId = computed(() => !currentUserId.value || currentUserId.value.startsWith(`${GUEST_ID}@`)) const isGuestId = computed(() => !currentUserId.value || currentUserId.value.startsWith(`${GUEST_ID}@`))
const defaultUser: UserLogin<false> = {
server: DEFAULT_SERVER,
guest: true,
}
export const currentUser = computed<UserLogin | undefined>(() => { export const currentUser = computed<UserLogin>(() => {
if (!currentUserId.value) let user: UserLogin | undefined
if (!currentUserId.value) {
// Fallback to the first account // Fallback to the first account
return users.value[0] user = users.value[0]
if (isGuestId.value) {
const server = currentUserId.value.replace(`${GUEST_ID}@`, '')
return users.value.find(user => user.guest && user.server === server)
} }
else if (isGuestId.value) {
return users.value.find(user => user.account?.id === currentUserId.value) const server = currentUserId.value.replace(`${GUEST_ID}@`, '')
user = users.value.find(user => user.guest && user.server === server)
}
else {
user = users.value.find(user => user.account?.id === currentUserId.value)
}
return user || defaultUser
}) })
export const currentServer = computed<string>(() => currentUser.value?.server || DEFAULT_SERVER) export const currentServer = computed<string>(() => currentUser.value.server)
export const currentInstance = computed<null | Instance>(() => { export const currentInstance = computed<null | Instance>(() => {
return instances.value[currentServer.value] ?? null return instances.value[currentServer.value] ?? null
}) })
export const checkUser = (val: UserLogin | undefined): val is UserLogin<true> => !!(val && !val.guest) export const checkUser = (val: UserLogin | undefined): val is UserLogin<true> => !!(val && !val.guest)
export const isGuest = computed(() => !checkUser(currentUser.value)) export const isGuest = computed(() => !checkUser(currentUser.value))
export const getUniqueUserId = (user: UserLogin) => user.guest ? `${GUEST_ID}@${user.server}` : user.account.id export const getUniqueUserId = (user: UserLogin) =>
user.guest ? `${GUEST_ID}@${user.server}` : user.account.id
export const isSameUser = (a: UserLogin | undefined, b: UserLogin | undefined) => export const isSameUser = (a: UserLogin | undefined, b: UserLogin | undefined) =>
a && b && getUniqueUserId(a) === getUniqueUserId(b) a && b && getUniqueUserId(a) === getUniqueUserId(b)
export const currentUserHandle = computed(() => export const currentUserHandle = computed(() =>
isGuestId.value ? GUEST_ID : currentUser.value!.account!.acct currentUser.value.guest ? GUEST_ID : currentUser.value.account!.acct,
,
) )
export const useUsers = () => users export const useUsers = () => users
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)
async function loginTo(user?: UserLogin) { async function loginTo({ server, token, vapidKey, pushSubscription, guest = false }: { guest?: boolean } & Omit<UserLogin, 'guest'>) {
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const server = user?.server || (route.params.server as string) || DEFAULT_SERVER
let user: UserLogin | undefined = token
? users.value.find(u => u.server === server && u.token === token)
: ((guest
? undefined
: users.value.find(u => u.server === server && u.token))
|| users.value.find(u => u.server === server && u.guest))
const needPush = !user
if (!user) {
if (token) {
user = {
server,
guest: false,
token,
vapidKey,
pushSubscription,
account: undefined as any, // to be assigned later
}
}
else {
user = { server, guest: true }
}
}
const masto = await loginMasto({ const masto = await loginMasto({
url: `https://${server}`, url: `https://${user.server}`,
accessToken: user?.token, accessToken: user.token,
disableVersionCheck: true, disableVersionCheck: true,
// Suppress warning of `masto/fetch` usage // Suppress warning of `masto/fetch` usage
disableExperimentalWarning: true, disableExperimentalWarning: true,
}) })
if (!user?.token) { if (user.guest) {
const instance = await masto.instances.fetch() const instance = await masto.instances.fetch()
instances.value[server] = instance instances.value[server] = instance
if (!users.value.some(u => u.server === server && u.guest))
users.value.push({ server, guest: true })
currentUserId.value = `${GUEST_ID}@${server}`
} }
else { else {
try {
const [me, instance, pushSubscription] = await Promise.all([ const [me, instance, pushSubscription] = await Promise.all([
masto.accounts.verifyCredentials(), masto.accounts.verifyCredentials(),
masto.instances.fetch(), masto.instances.fetch(),
@ -117,35 +141,37 @@ async function loginTo(user?: UserLogin) {
user.account = me user.account = me
user.pushSubscription = pushSubscription user.pushSubscription = pushSubscription
currentUserId.value = me.id
instances.value[server] = instance instances.value[server] = instance
}
if (!users.value.some(u => u.server === user.server && u.token === user.token)) if (needPush)
users.value.push(user) users.value.push(user)
}
catch { currentUserId.value = getUniqueUserId(user)
await signout()
}
}
// This only cleans up the URL; page content should stay the same // This only cleans up the URL; page content should stay the same
if (route.path === '/signin/callback') { if (route.path === '/signin/callback') {
await router.push('/home') await router.push('/home')
} }
else if ('server' in route.params && user?.token && !useNuxtApp()._processingMiddleware) { else if ('server' in route.params && user.server !== route.params.server) {
await router.push({ await router.push({
...route, ...route,
params: {
...route.params,
server: user.server,
},
force: true, force: true,
}) })
} }
return masto return masto
} }
export type LoginTo = typeof loginTo
export const switchUser = (user: UserLogin, masto: ElkMasto) => { export const switchUser = (user: UserLogin, masto: ElkMasto) => {
const router = useRouter() const router = useRouter()
if (!user.guest && !isGuest.value && user.account.id === currentUser.value!.account!.id) if (!user.guest && !isGuest.value && user.account.id === currentUser.value.account!.id)
router.push(getAccountRoute(user.account)) router.push(getAccountRoute(user.account))
else else
masto.loginTo(user) masto.loginTo(user)
@ -173,7 +199,7 @@ export function getUsersIndexByUserId(userId: string) {
return users.value.findIndex(u => u.account?.id === userId) return users.value.findIndex(u => u.account?.id === userId)
} }
export async function removePushNotificationData(user: UserLogin, fromSWPushManager = true) { export async function removePushNotificationData(user: UserLogin<true>, fromSWPushManager = true) {
// clear push subscription // clear push subscription
user.pushSubscription = undefined user.pushSubscription = undefined
const { acct } = user.account! const { acct } = user.account!
@ -212,13 +238,18 @@ export async function removePushNotifications(user: UserLogin<true>) {
} }
} }
// do not sign out if there is only one guest user
export const canSignOut = computed(() =>
users.value.length > 1 || !users.value[0].guest,
)
export async function signout() { export async function signout() {
// TODO: confirm // TODO: confirm
if (!currentUser.value)
if (!canSignOut.value)
return return
const index = users.value.findIndex(u => isSameUser(u, currentUser.value)) const index = users.value.findIndex(u => isSameUser(u, currentUser.value))
if (index !== -1) { if (index !== -1) {
// Clear stale data // Clear stale data
clearUserLocalStorage() clearUserLocalStorage()
@ -235,11 +266,8 @@ export async function signout() {
users.value.splice(index, 1) users.value.splice(index, 1)
} }
// Set currentUserId to next user if available // Set currentUserId to next user
currentUserId.value = users.value[0]?.account?.id currentUserId.value = getUniqueUserId(users.value[0] ? users.value[0] : defaultUser)
if (!currentUserId.value)
await useRouter().push('/')
const masto = useMasto() const masto = useMasto()
await masto.loginTo(currentUser.value) await masto.loginTo(currentUser.value)
@ -306,7 +334,7 @@ export function useUserLocalStorage<T extends object>(key: string, initial: () =
const all = storages.get(key) as Ref<Record<string, T>> const all = storages.get(key) as Ref<Record<string, T>>
return computed(() => { return computed(() => {
const id = isGuestId.value const id = currentUser.value.guest
? GUEST_ID ? GUEST_ID
: currentUser.value!.account!.acct : currentUser.value!.account!.acct
all.value[id] = Object.assign(initial(), all.value[id] || {}) all.value[id] = Object.assign(initial(), all.value[id] || {})
@ -343,10 +371,11 @@ export const createMasto = () => {
if (key === 'loginTo') { if (key === 'loginTo') {
return (...args: any[]): Promise<MastoClient> => { return (...args: any[]): Promise<MastoClient> => {
return apiPromise.value = loginTo(...args).then((r) => { return apiPromise.value = (loginTo as any)(...args).then((r: any) => {
api.value = r api.value = r
return masto return masto
}).catch(() => { }).catch((err: any) => {
console.error(err)
// Show error page when Mastodon server is down // Show error page when Mastodon server is down
throw createError({ throw createError({
fatal: true, fatal: true,

View file

@ -22,7 +22,7 @@ const reload = async () => {
try { try {
if (!masto.loggedIn.value) if (!masto.loggedIn.value)
await masto.loginTo(currentUser.value) await masto.loginTo(currentUser.value)
clearError({ redirect: currentUser.value ? '/home' : `/${currentServer.value}/public` }) clearError({ redirect: !isGuest.value ? '/home' : `/${currentServer.value}/public` })
} }
catch { catch {
state.value = 'error' state.value = 'error'

View file

@ -32,9 +32,8 @@ const wideLayout = computed(() => route.meta.wideLayout ?? false)
> >
<AccountInfo :account="currentUser.account" md:break-words /> <AccountInfo :account="currentUser.account" md:break-words />
</NuxtLink> </NuxtLink>
<div v-else> <AccountGuest v-else :user="currentUser" />
TODO: guest {{ currentUser.server }} @ default.vue
</div>
<UserDropdown /> <UserDropdown />
</div> </div>
</div> </div>

View file

@ -1,4 +1,4 @@
export default defineNuxtRouteMiddleware(async (to, from) => { export default defineNuxtRouteMiddleware(async (to) => {
if (process.server) if (process.server)
return return
@ -13,23 +13,6 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
const user = currentUser.value const user = currentUser.value
if (!user) {
if (from.params.server !== to.params.server) {
await masto.loginTo({
server: to.params.server as string,
})
}
return
}
// No need to additionally resolve an id if we're already logged in
if (user.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 // Handle redirecting to new permalink structure for users with old links
if (!to.params.server) { if (!to.params.server) {
return { return {
@ -41,28 +24,11 @@ export default defineNuxtRouteMiddleware(async (to, from) => {
} }
} }
try { // No need to additionally resolve an id if we're already logged in
// If we're already on an account page, we can search for this on the new instance if (user.server === to.params.server)
if (to.params.account && to.name !== 'status' && to.params.account.includes('@')) { return
const account = await fetchAccountByHandle(to.params.account as string)
if (account)
return getAccountRoute(account)
}
if (!masto.loggedIn.value) masto.loginTo({
await masto.loginTo(currentUser.value) server: to.params.server as string,
})
// If we're logged in, search for the local id the account or status corresponds to
const { value } = await masto.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

@ -71,9 +71,7 @@ async function importTokens() {
<div flex="~ col gap2"> <div flex="~ col gap2">
<div v-for="user of loggedInUsers" :key="getUniqueUserId(user)"> <div v-for="user of loggedInUsers" :key="getUniqueUserId(user)">
<AccountInfo v-if="!user.guest" :account="user.account" :hover-card="false" /> <AccountInfo v-if="!user.guest" :account="user.account" :hover-card="false" />
<div v-else> <AccountGuest v-else :user="user" />
TODO: Guest @ settings/users/index.vue
</div>
</div> </div>
</div> </div>
<div my4 border="t base" /> <div my4 border="t base" />

View file

@ -1,6 +1,7 @@
import type { Account, AccountCredentials, Attachment, CreateStatusParams, Emoji, Instance, MastoClient, Notification, PushSubscription, Status } from 'masto' import type { Account, AccountCredentials, Attachment, CreateStatusParams, Emoji, Instance, MastoClient, Notification, PushSubscription, Status } from 'masto'
import type { Ref } from 'vue' import type { Ref } from 'vue'
import type { MarkNonNullable, Mutable } from './utils' import type { MarkNonNullable, Mutable } from './utils'
import type { LoginTo } from '~/composables/users'
export interface AppInfo { export interface AppInfo {
id: string id: string
@ -30,7 +31,7 @@ export type UserLogin<WithToken extends boolean = boolean> = {
} & ((WithToken extends false ? UserLoginGuest : never) | (WithToken extends true ? UserLoginWithToken : never)) } & ((WithToken extends false ? UserLoginGuest : never) | (WithToken extends true ? UserLoginWithToken : never))
export interface ElkMasto extends MastoClient { export interface ElkMasto extends MastoClient {
loginTo(user?: UserLogin): Promise<MastoClient> loginTo: LoginTo
loggedIn: Ref<boolean> loggedIn: Ref<boolean>
} }