mirror of
https://github.com/versia-pub/frontend.git
synced 2026-03-13 03:29:16 +01:00
feat: ✨ Add multi-account support, more options for posts, UI improvements
This commit is contained in:
parent
48954baf06
commit
ef9a6f1da4
36 changed files with 649 additions and 344 deletions
92
components/sidebars/account-picker.vue
Normal file
92
components/sidebars/account-picker.vue
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<template>
|
||||
<DropdownsAdaptiveDropdown>
|
||||
<template #button>
|
||||
<slot>
|
||||
<div class="rounded text-left flex flex-row gap-x-2 hover:scale-[95%] duration-100"
|
||||
v-if="currentIdentity">
|
||||
<div class="shrink-0">
|
||||
<AvatarsCentered class="size-12 rounded ring-1 ring-white/5"
|
||||
:src="currentIdentity.account.avatar" :alt="`${currentIdentity.account.acct}'s avatar'`" />
|
||||
</div>
|
||||
<div class="flex flex-col items-start p-1 justify-around grow overflow-hidden">
|
||||
<div class="flex flex-row items-center justify-between w-full">
|
||||
<div class="font-semibold text-gray-200 text-sm line-clamp-1 break-all">
|
||||
{{
|
||||
currentIdentity.account.display_name }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-gray-400 text-xs line-clamp-1 break-all w-full">
|
||||
Change account
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<ButtonsBase v-else class="flex flex-row text-left items-center justify-start gap-3 text-lg hover:ring-1 ring-white/10
|
||||
overflow-hidden h-12 w-full duration-200">
|
||||
<iconify-icon icon="tabler:login" class="shrink-0 text-2xl" />
|
||||
<span class="pr-28 line-clamp-1">Sign In</span>
|
||||
</ButtonsBase>
|
||||
</slot>
|
||||
</template>
|
||||
<template #items>
|
||||
<div class="p-2">
|
||||
<h3 class="text-gray-400 text-xs text-center md:text-left uppercase font-semibold">Switch to account
|
||||
</h3>
|
||||
</div>
|
||||
<div class="px-2 py-4 md:py-2 flex flex-col gap-3 max-w-[100vw]">
|
||||
<Menu.Item value="" v-for="identity of identities" class="hover:scale-[95%] duration-100">
|
||||
<div class="flex flex-row gap-x-4">
|
||||
<div class="shrink-0" data-part="item" @click="useEvent('identity:change', identity)">
|
||||
<AvatarsCentered class="h-12 w-12 rounded ring-1 ring-white/5"
|
||||
:src="identity.account.avatar" :alt="`${identity.account.acct}'s avatar'`" />
|
||||
</div>
|
||||
<div data-part="item" class="flex flex-col items-start justify-around grow overflow-hidden"
|
||||
@click="useEvent('identity:change', identity)">
|
||||
<div class="flex flex-row items-center justify-between w-full">
|
||||
<div class="font-semibold text-gray-200 line-clamp-1 break-all">
|
||||
{{
|
||||
identity.account.display_name }}
|
||||
</div>
|
||||
</div>
|
||||
<span class="text-gray-400 text-sm line-clamp-1 break-all w-full">
|
||||
@{{
|
||||
identity.account.acct
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<button data-part="item"
|
||||
class="shrink-0 ml-6 size-12 ring-white/5 ring-1 flex items-center justify-center rounded"
|
||||
@click="$emit('signOut', identity.id)">
|
||||
<iconify-icon icon="tabler:logout" class="size-6 text-gray-200" width="none" />
|
||||
</button>
|
||||
</div>
|
||||
</Menu.Item>
|
||||
<Menu.Item value="">
|
||||
<button @click="$emit('signIn')" class="w-full">
|
||||
<div class="rounded text-left flex flex-row gap-x-2 hover:scale-[95%]">
|
||||
<div
|
||||
class="shrink-0 size-12 border-dashed border-white/20 border flex items-center justify-center rounded">
|
||||
<iconify-icon icon="tabler:user-plus" class="size-6 text-gray-200" width="none" />
|
||||
</div>
|
||||
<div
|
||||
class="flex flex-col items-start font-semibold p-1 justify-around text-sm text-gray-300 grow overflow-hidden">
|
||||
Add new account
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</Menu.Item>
|
||||
</div>
|
||||
</template>
|
||||
</DropdownsAdaptiveDropdown>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { Menu } from "@ark-ui/vue";
|
||||
const identities = useIdentities();
|
||||
|
||||
const currentIdentity = useCurrentIdentity();
|
||||
|
||||
defineEmits<{
|
||||
signIn: [];
|
||||
signOut: [identityId: string];
|
||||
}>();
|
||||
</script>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<aside
|
||||
class="fixed h-dvh z-20 md:flex hidden flex-col p-4 bg-dark-800 gap-10 max-w-20 hover:max-w-72 duration-200 group ring-1 ring-dark-500"
|
||||
class="fixed h-dvh z-10 md:flex hidden flex-col p-4 bg-dark-800 gap-10 max-w-20 hover:max-w-72 duration-200 group ring-1 ring-dark-500"
|
||||
role="complementary">
|
||||
<NuxtLink href="/">
|
||||
<img crossorigin="anonymous" class="size-11 rounded ring-1 ring-white/10 hover:scale-105 duration-200"
|
||||
|
|
@ -25,28 +25,18 @@
|
|||
<h3 class="font-semibold text-gray-300 text-xs uppercase opacity-0 group-hover:opacity-100 duration-200">
|
||||
Account</h3>
|
||||
<ClientOnly>
|
||||
<ButtonsBase v-if="tokenData" @click="signOut().finally(() => loadingAuth = false)"
|
||||
:loading="loadingAuth"
|
||||
class="flex flex-row text-left items-center justify-start gap-3 text-lg hover:ring-1 ring-white/10 overflow-hidden h-12 w-full duration-200">
|
||||
<iconify-icon icon="tabler:logout" class="shrink-0 text-2xl" />
|
||||
<span class="pr-28 line-clamp-1">Sign Out</span>
|
||||
</ButtonsBase>
|
||||
<ButtonsBase v-else @click="signIn().finally(() => loadingAuth = false)" :loading="loadingAuth"
|
||||
class="flex flex-row text-left items-center justify-start gap-3 text-lg hover:ring-1 ring-white/10 overflow-hidden h-12 w-full duration-200">
|
||||
<iconify-icon icon="tabler:login" class="shrink-0 text-2xl" />
|
||||
<span class="pr-28 line-clamp-1">Sign In</span>
|
||||
</ButtonsBase>
|
||||
<NuxtLink href="/register" v-if="!tokenData">
|
||||
<SidebarsAccountPicker @sign-in="signIn().finally(() => loadingAuth = false)" />
|
||||
<NuxtLink href="/register" v-if="!identity">
|
||||
<ButtonsBase
|
||||
class="flex flex-row text-left items-center justify-start gap-3 text-lg hover:ring-1 ring-white/10 overflow-hidden h-12 w-full duration-200">
|
||||
<iconify-icon icon="tabler:certificate" class="shrink-0 text-2xl" />
|
||||
<span class="pr-28 line-clamp-1">Register</span>
|
||||
</ButtonsBase>
|
||||
</NuxtLink>
|
||||
<h3 v-if="tokenData"
|
||||
<h3 v-if="identity"
|
||||
class="font-semibold text-gray-300 text-xs uppercase opacity-0 group-hover:opacity-100 duration-200">
|
||||
Posts</h3>
|
||||
<ButtonsBase v-if="tokenData" @click="compose" title="Open composer (shortcut: n)"
|
||||
<ButtonsBase v-if="identity" @click="compose" title="Open composer (shortcut: n)"
|
||||
class="flex flex-row text-left items-center justify-start gap-3 text-lg hover:ring-1 ring-white/10 bg-gradient-to-tr from-pink-300 via-purple-300 to-indigo-400 overflow-hidden h-12 w-full duration-200">
|
||||
<iconify-icon icon="tabler:writing" class="shrink-0 text-2xl" />
|
||||
<span class="pr-28 line-clamp-1">Compose</span>
|
||||
|
|
@ -98,37 +88,14 @@
|
|||
<span class="text-xs">Update</span>
|
||||
</button>
|
||||
</ClientOnly>
|
||||
<DropdownsAdaptiveDropdown v-else>
|
||||
<template #button>
|
||||
<button class="flex flex-col items-center justify-center p-2 rounded">
|
||||
<iconify-icon icon="tabler:user" class="text-2xl" />
|
||||
<span class="text-xs">Account</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<template #items>
|
||||
<Menu.Item value="" v-if="tokenData">
|
||||
<ButtonsDropdownElement icon="tabler:logout" class="w-full"
|
||||
@click="signOut().finally(() => loadingAuth = false)" :loading="loadingAuth">
|
||||
Sign Out
|
||||
</ButtonsDropdownElement>
|
||||
</Menu.Item>
|
||||
<Menu.Item value="" v-if="!tokenData">
|
||||
<ButtonsDropdownElement icon="tabler:login" class="w-full"
|
||||
@click="signIn().finally(() => loadingAuth = false)" :loading="loadingAuth">
|
||||
Sign In
|
||||
</ButtonsDropdownElement>
|
||||
</Menu.Item>
|
||||
<Menu.Item value="" v-if="!tokenData">
|
||||
<NuxtLink href="/register">
|
||||
<ButtonsDropdownElement icon="tabler:certificate" class="w-full">
|
||||
Register
|
||||
</ButtonsDropdownElement>
|
||||
</NuxtLink>
|
||||
</Menu.Item>
|
||||
</template>
|
||||
</DropdownsAdaptiveDropdown>
|
||||
<button @click="compose" v-if="tokenData"
|
||||
<SidebarsAccountPicker v-else @sign-in="signIn().finally(() => loadingAuth = false)"
|
||||
@sign-out="id => signOut(id).finally(() => loadingAuth = false)">
|
||||
<button class="flex flex-col items-center justify-center p-2 rounded">
|
||||
<iconify-icon icon="tabler:user" class="text-2xl" />
|
||||
<span class="text-xs">Account</span>
|
||||
</button>
|
||||
</SidebarsAccountPicker>
|
||||
<button @click="compose" v-if="identity"
|
||||
class="flex flex-col items-center justify-center p-2 rounded bg-gradient-to-tr from-pink-300/70 via-purple-300/70 to-indigo-400/70">
|
||||
<iconify-icon icon="tabler:writing" class="text-2xl" />
|
||||
<span class="text-xs">Compose</span>
|
||||
|
|
@ -167,16 +134,16 @@ const timelines = ref([
|
|||
|
||||
const visibleTimelines = computed(() =>
|
||||
timelines.value.filter(
|
||||
(timeline) => !timeline.requiresAuth || tokenData.value,
|
||||
(timeline) => !timeline.requiresAuth || identity.value,
|
||||
),
|
||||
);
|
||||
|
||||
const loadingAuth = ref(false);
|
||||
|
||||
const appData = useAppData();
|
||||
const tokenData = useTokenData();
|
||||
const identity = useCurrentIdentity();
|
||||
const identities = useIdentities();
|
||||
const client = useClient();
|
||||
const me = useMe();
|
||||
|
||||
const compose = () => {
|
||||
useEvent("composer:open");
|
||||
|
|
@ -185,7 +152,7 @@ const compose = () => {
|
|||
const signIn = async () => {
|
||||
loadingAuth.value = true;
|
||||
|
||||
const output = await client.value?.createApp("Lysand", {
|
||||
const output = await client.value.createApp("Lysand", {
|
||||
scopes: ["read", "write", "follow", "push"],
|
||||
redirect_uris: new URL("/", useRequestURL().origin).toString(),
|
||||
website: useBaseUrl().value,
|
||||
|
|
@ -198,7 +165,7 @@ const signIn = async () => {
|
|||
|
||||
appData.value = output.data;
|
||||
|
||||
const url = await client.value?.generateAuthUrl(
|
||||
const url = await client.value.generateAuthUrl(
|
||||
output.data.client_id,
|
||||
output.data.client_secret,
|
||||
{
|
||||
|
|
@ -215,11 +182,20 @@ const signIn = async () => {
|
|||
window.location.href = url;
|
||||
};
|
||||
|
||||
const signOut = async () => {
|
||||
const signOut = async (id?: string) => {
|
||||
loadingAuth.value = true;
|
||||
|
||||
if (!appData.value || !tokenData.value) {
|
||||
console.error("No app or token data to sign out");
|
||||
if (!appData.value || !identity.value) {
|
||||
console.error("No app or identity data to sign out");
|
||||
return;
|
||||
}
|
||||
|
||||
const identityToRevoke = id
|
||||
? identities.value.find((i) => i.id === id)
|
||||
: identity.value;
|
||||
|
||||
if (!identityToRevoke) {
|
||||
console.error("No identity to revoke");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -227,13 +203,22 @@ const signOut = async () => {
|
|||
await client.value
|
||||
?.revokeToken(
|
||||
appData.value.client_id,
|
||||
tokenData.value.access_token,
|
||||
tokenData.value.access_token,
|
||||
identityToRevoke.tokens.access_token,
|
||||
identityToRevoke.tokens.access_token,
|
||||
)
|
||||
.catch(() => {});
|
||||
|
||||
tokenData.value = null;
|
||||
me.value = null;
|
||||
await navigateTo("/");
|
||||
if (id === identity.value.id) {
|
||||
identity.value = null;
|
||||
await navigateTo("/");
|
||||
return;
|
||||
}
|
||||
|
||||
identities.value = identities.value.filter((i) => i.id !== id);
|
||||
await useEvent("notification:new", {
|
||||
type: "success",
|
||||
title: "Signed out",
|
||||
message: "Account signed out successfully",
|
||||
});
|
||||
};
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue