feat: Add multi-account support, more options for posts, UI improvements

This commit is contained in:
Jesse Wierzbinski 2024-06-09 17:24:55 -10:00
parent 48954baf06
commit ef9a6f1da4
No known key found for this signature in database
36 changed files with 649 additions and 344 deletions

View 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>

View file

@ -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>