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

@ -1,5 +1,5 @@
<template>
<ButtonsBase class="hover:bg-white/20 !rounded-sm !text-left flex flex-row gap-x-3 !ring-0 !p-4 sm:!p-2">
<ButtonsBase class="enabled:hover:bg-white/20 !rounded-sm !text-left flex flex-row gap-x-3 !ring-0 !p-4 sm:!p-2">
<iconify-icon :icon="icon" width="none" class="text-gray-200 size-5" aria-hidden="true" />
<slot />
</ButtonsBase>

View file

@ -71,7 +71,7 @@ const { input: content } = useTextareaAutosize({
const { Control_Enter, Command_Enter, Control_Alt } = useMagicKeys();
const respondingTo = ref<Status | null>(null);
const respondingType = ref<"reply" | "quote" | "edit" | null>(null);
const me = useMe();
const identity = useCurrentIdentity();
const cw = ref(false);
const cwContent = ref("");
const markdown = ref(true);
@ -151,7 +151,7 @@ onMounted(() => {
useListen("composer:reply", (note: Status) => {
respondingTo.value = note;
respondingType.value = "reply";
if (note.account.id !== me.value?.id)
if (note.account.id !== identity.value?.account.id)
content.value = `@${note.account.acct} `;
textarea.value?.focus();
});
@ -159,7 +159,7 @@ onMounted(() => {
useListen("composer:quote", (note: Status) => {
respondingTo.value = note;
respondingType.value = "quote";
if (note.account.id !== me.value?.id)
if (note.account.id !== identity.value?.account.id)
content.value = `@${note.account.acct} `;
textarea.value?.focus();
});
@ -175,7 +175,7 @@ onMounted(() => {
}));
// Fetch source
const source = await client.value?.getStatusSource(note.id);
const source = await client.value.getStatusSource(note.id);
if (source?.data) {
respondingTo.value = note;
@ -205,12 +205,11 @@ const canSubmit = computed(
(content.value?.trim().length > 0 || files.value.length > 0) &&
content.value?.trim().length <= characterLimit.value,
);
const tokenData = useTokenData();
const client = useClient(tokenData);
const client = useClient();
const send = async () => {
loading.value = true;
if (!tokenData.value || !client.value) {
if (!identity.value || !client.value) {
throw new Error("Not authenticated");
}

View file

@ -10,14 +10,14 @@
</template>
<script lang="ts" setup>
import type { LysandClient } from "@lysand-org/client";
import { distance } from "fastest-levenshtein";
import type { UnwrapRef } from "vue";
import type { CustomEmoji } from "~/composables/Identities";
const props = defineProps<{
currentlyTypingEmoji: string | null;
}>();
const emojiRefs = ref<Element[]>([]);
const customEmojis = useCustomEmojis();
const { Tab, ArrowRight, ArrowLeft, Enter } = useMagicKeys({
passive: false,
onEventFired(e) {
@ -28,12 +28,15 @@ const { Tab, ArrowRight, ArrowLeft, Enter } = useMagicKeys({
e.preventDefault();
},
});
const topEmojis = ref<UnwrapRef<typeof customEmojis> | null>(null);
const identity = useCurrentIdentity();
const topEmojis = ref<CustomEmoji[] | null>(null);
const selectedEmojiIndex = ref<number | null>(null);
watchEffect(() => {
if (!identity.value) return;
if (props.currentlyTypingEmoji !== null)
topEmojis.value = customEmojis.value
topEmojis.value = identity.value.emojis
.map((emoji) => ({
...emoji,
distance: distance(

View file

@ -2,7 +2,7 @@
<div>
<input type="file" ref="fileInput" @change="handleFileInput" style="display: none" multiple />
<div class="flex flex-row gap-2 overflow-x-auto *:shrink-0 p-1 mb-4" v-if="files.length > 0">
<div v-for="(data) in files.toReversed()" :key="data.id" role="button" tabindex="0"
<div v-for="(data) in files" :key="data.id" role="button" tabindex="0"
:class="['size-28 bg-dark-800 rounded flex items-center relative justify-center ring-1 ring-white/20 overflow-hidden', data.progress !== 1.0 && 'animate-pulse']"
@keydown.enter="removeFile(data.id)">
<template v-if="data.file.type.startsWith('image/')">
@ -73,8 +73,7 @@ const files = defineModel<
required: true,
});
const tokenData = useTokenData();
const client = useClient(tokenData);
const client = useClient();
const fileInput = ref<HTMLInputElement | null>(null);
const openFilePicker = () => {
@ -165,7 +164,7 @@ const uploadFile = async (file: File) => {
return data;
});
client.value?.uploadMedia(file).then((response) => {
client.value.uploadMedia(file).then((response) => {
const attachment = response.data;
files.value = files.value.map((data) => {

View file

@ -19,7 +19,7 @@
<Dialog.Content class="overflow-y-auto w-full max-h-full md:py-16">
<div
class="relative overflow-hidden max-w-xl mx-auto rounded-lg bg-dark-700 ring-1 ring-dark-800 text-left shadow-xl transition-all w-full">
<Composer v-if="instance" :instance="instance" />
<Composer v-if="instance" :instance="instance as any" />
</div>
</Dialog.Content>
</HeadlessTransitionChild>
@ -32,6 +32,8 @@
<script lang="ts" setup>
import { Dialog } from "@ark-ui/vue";
const open = ref(false);
const identity = useCurrentIdentity();
useListen("note:reply", async (note) => {
open.value = true;
await nextTick();
@ -48,11 +50,10 @@ useListen("note:edit", async (note) => {
useEvent("composer:edit", note);
});
useListen("composer:open", () => {
if (tokenData.value) open.value = true;
if (identity.value) open.value = true;
});
useListen("composer:close", () => {
open.value = false;
});
const tokenData = useTokenData();
const instance = useInstance();
</script>

View file

@ -39,20 +39,18 @@ const id = useId();
// HACK: Fix the menu children not reacting to touch events as click for some reason
const registerClickHandlers = () => {
const targetElement = document.querySelector(`.${id}`);
if (targetElement) {
for (const el of targetElement.children) {
el.addEventListener("touchstart", (e) => {
e.stopPropagation();
e.preventDefault();
// Click all element children
for (const elChild of Array.from(el.children)) {
if (elChild instanceof HTMLElement) {
elChild.click();
}
const targetElements = document.querySelectorAll(`.${id} [data-part=item]`);
for (const el of targetElements) {
el.addEventListener("touchstart", (e) => {
e.stopPropagation();
e.preventDefault();
// Click all element children
for (const elChild of Array.from(el.children)) {
if (elChild instanceof HTMLElement) {
elChild.click();
}
});
}
}
});
}
};

View file

@ -1,27 +1,27 @@
<template>
<ClientOnly>
<div v-if="me" class="bg-dark-800 p-6 my-5 rounded ring-1 ring-white/5">
<div class="sm:flex sm:items-center sm:justify-between">
<div class="sm:flex sm:space-x-5">
<AvatarsCentered :src="me.avatar"
<div v-if="identity" class="bg-dark-800 z-0 p-6 my-5 relative overflow-hidden rounded ring-1 ring-white/5">
<div class="sm:flex sm:items-center sm:justify-between gap-3">
<div class="sm:flex sm:space-x-5 grow">
<AvatarsCentered :src="identity.account.avatar"
class="mx-auto shrink-0 size-20 rounded overflow-hidden ring-1 ring-white/10" />
<div class="mt-4 text-center sm:mt-0 sm:pt-1 sm:text-left">
<div
class="mt-4 text-center flex flex-col justify-center sm:mt-0 sm:text-left bg-dark-800 py-2 px-4 rounded grow ring-1 ring-white/10">
<p class="text-sm font-medium text-gray-300">Welcome back,</p>
<p class="text-xl font-bold text-gray-50 sm:text-2xl line-clamp-1"
v-html="useParsedContent(me.display_name, []).value"></p>
<p class="text-sm font-medium text-gray-500">@{{ me.acct }}</p>
v-html="useParsedContent(identity.account.display_name, []).value"></p>
</div>
</div>
<div class="mt-5 flex justify-center sm:mt-0">
<!-- <div class="mt-5 flex justify-center sm:mt-0">
<ButtonsSecondary @click="useEvent('composer:open')">
Compose
</ButtonsSecondary>
</div>
</div> -->
</div>
</div>
</ClientOnly>
</template>
<script lang="ts" setup>
const me = useMe();
const identity = useCurrentIdentity();
</script>

View file

@ -1,43 +1,46 @@
<template>
<div aria-live="assertive" class="pointer-events-none fixed inset-0 flex items-end px-4 py-6 sm:items-start sm:p-6">
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
<!-- Notification panel, dynamically insert this into the live region when it needs to be displayed -->
<TransitionGroup enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transition transform ease-in duration-100"
leave-from-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-to-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2">
<div v-for="notification in notifications" :key="notification.id"
class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-dark-500 shadow-lg ring-1 ring-white/10">
<div class="p-4">
<div class="flex items-start">
<div class="shrink-0 h-6 w-6">
<iconify-icon v-if="notification.type === 'success'" icon="tabler:check" height="none"
class="h-6 w-6 text-green-400" aria-hidden="true" />
<iconify-icon v-else-if="notification.type === 'error'" icon="tabler:alert-triangle"
height="none" class="h-6 w-6 text-red-400" aria-hidden="true" />
<iconify-icon v-else-if="notification.type === 'progress'" icon="tabler:loader"
height="none" class="h-6 w-6 text-pink-500 animate-spin" aria-hidden="true" />
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-semibold text-gray-50">{{ notification.title }}</p>
<p class="mt-1 text-sm text-gray-400" v-if="notification.message">{{
notification.message }}</p>
</div>
<div class="ml-4 flex flex-shrink-0">
<button type="button" title="Close this notification"
@click="notifications.splice(notifications.indexOf(notification), 1); notification.onDismiss?.()"
class="inline-flex rounded-md text-gray-400 hover:text-gray-300 duration-200">
<iconify-icon icon="tabler:x" class="h-5 w-5" aria-hidden="true" />
</button>
<Teleport to="body">
<div aria-live="assertive"
class="pointer-events-none fixed inset-0 flex items-end px-4 pt-6 pb-24 sm:pb-6 sm:items-start sm:p-6 z-50">
<div class="flex w-full flex-col items-center space-y-4 sm:items-end">
<!-- Notification panel, dynamically insert this into the live region when it needs to be displayed -->
<TransitionGroup enter-active-class="transform ease-out duration-300 transition"
enter-from-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2"
enter-to-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-active-class="transition transform ease-in duration-100"
leave-from-class="translate-y-0 opacity-100 sm:translate-x-0"
leave-to-class="translate-y-2 opacity-0 sm:translate-y-0 sm:translate-x-2">
<div v-for="notification in notifications" :key="notification.id"
class="pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-dark-500 shadow-lg ring-1 ring-white/10">
<div class="p-4">
<div class="flex items-start">
<div class="shrink-0 h-6 w-6">
<iconify-icon v-if="notification.type === 'success'" icon="tabler:check"
height="none" class="h-6 w-6 text-green-400" aria-hidden="true" />
<iconify-icon v-else-if="notification.type === 'error'" icon="tabler:alert-triangle"
height="none" class="h-6 w-6 text-red-400" aria-hidden="true" />
<iconify-icon v-else-if="notification.type === 'progress'" icon="tabler:loader"
height="none" class="h-6 w-6 text-pink-500 animate-spin" aria-hidden="true" />
</div>
<div class="ml-3 w-0 flex-1 pt-0.5">
<p class="text-sm font-semibold text-gray-50">{{ notification.title }}</p>
<p class="mt-1 text-sm text-gray-400" v-if="notification.message">{{
notification.message }}</p>
</div>
<div class="ml-4 flex flex-shrink-0">
<button type="button" title="Close this notification"
@click="notifications.splice(notifications.indexOf(notification), 1); notification.onDismiss?.()"
class="inline-flex rounded-md text-gray-400 hover:text-gray-300 duration-200">
<iconify-icon icon="tabler:x" class="h-5 w-5" aria-hidden="true" />
</button>
</div>
</div>
</div>
</div>
</div>
</TransitionGroup>
</TransitionGroup>
</div>
</div>
</div>
</Teleport>
</template>
<script lang="ts" setup>

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>

View file

@ -25,7 +25,7 @@
class="absolute top-2 right-2 p-1 bg-dark-800 ring-1 ring-white/5 text-white text-xs rounded size-8">
<iconify-icon icon="tabler:alt" width="none" class="size-6" />
</Popover.Trigger>
<Popover.Positioner>
<Popover.Positioner class="!z-10">
<Popover.Content class="p-4 bg-dark-400 rounded text-sm ring-1 ring-dark-100 shadow-lg text-gray-300">
<Popover.Description>{{ attachment.description }}</Popover.Description>
</Popover.Content>

View file

@ -20,19 +20,19 @@
<div v-if="showInteractions"
class="mt-6 flex flex-row items-stretch disabled:*:opacity-70 [&>button]:max-w-28 disabled:*:cursor-not-allowed relative justify-around text-sm h-10 hover:enabled:[&>button]:bg-dark-800 [&>button]:duration-200 [&>button]:rounded [&>button]:flex [&>button]:flex-1 [&>button]:flex-row [&>button]:items-center [&>button]:justify-center">
<button class="group" @click="outputtedNote && useEvent('note:reply', outputtedNote)"
:disabled="!isSignedIn">
:disabled="!identity">
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:arrow-back-up"
class="text-gray-200 group-hover:group-enabled:text-blue-600" aria-hidden="true" />
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(outputtedNote?.replies_count) }}</span>
</button>
<button class="group" @click="likeFn" :disabled="!isSignedIn">
<button class="group" @click="likeFn" :disabled="!identity">
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:heart" v-if="!outputtedNote?.favourited"
class="size-5 text-gray-200 group-hover:group-enabled:text-pink-600" aria-hidden="true" />
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:heart-filled" v-else
class="size-5 text-pink-600 group-hover:group-enabled:text-gray-200" aria-hidden="true" />
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(outputtedNote?.favourites_count) }}</span>
</button>
<button class="group" @click="reblogFn" :disabled="!isSignedIn">
<button class="group" @click="reblogFn" :disabled="!identity">
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:repeat" v-if="!outputtedNote?.reblogged"
class="size-5 text-gray-200 group-hover:group-enabled:text-green-600" aria-hidden="true" />
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:repeat" v-else
@ -40,7 +40,7 @@
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(outputtedNote?.reblogs_count) }}</span>
</button>
<button class="group" @click="outputtedNote && useEvent('note:quote', outputtedNote)"
:disabled="!isSignedIn">
:disabled="!identity">
<iconify-icon width="1.25rem" height="1.25rem" icon="tabler:quote"
class="size-5 text-gray-200 group-hover:group-enabled:text-blue-600" aria-hidden="true" />
<span class="text-gray-400 mt-0.5 ml-2">{{ numberFormat(0) }}</span>
@ -53,30 +53,93 @@
</template>
<template #items>
<Menu.Item value="" v-if="isSignedIn && outputtedNote?.account.id === me?.id">
<ButtonsDropdownElement @click="outputtedNote && useEvent('note:edit', outputtedNote)"
icon="tabler:pencil" class="w-full">
Edit
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="">
<ButtonsDropdownElement @click="copy(JSON.stringify(outputtedNote, null, 4))"
icon="tabler:code" class="w-full">
Copy API
Response
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="">
<ButtonsDropdownElement @click="copy(url)" icon="tabler:link" class="w-full">
Copy Link
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="">
<ButtonsDropdownElement @click="remove" icon="tabler:backspace" :disabled="!isSignedIn"
class="w-full border-r-2 border-red-500">
Delete
</ButtonsDropdownElement>
</Menu.Item>
<Menu.ItemGroup>
<Menu.Item value="" v-if="isMyAccount">
<ButtonsDropdownElement @click="outputtedNote && useEvent('note:edit', outputtedNote)"
icon="tabler:pencil" class="w-full">
Edit
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="">
<ButtonsDropdownElement @click="copy(JSON.stringify(outputtedNote, null, 4))"
icon="tabler:code" class="w-full">
Copy API
Response
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="">
<ButtonsDropdownElement @click="copy(url)" icon="tabler:link" class="w-full">
Copy Link
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="" v-if="outputtedNote?.url && isRemote">
<ButtonsDropdownElement @click="copy(outputtedNote.url)" icon="tabler:link"
class="w-full">
Copy Link (Origin)
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="" v-if="outputtedNote?.url && isRemote">
<ButtonsDropdownElement @click="openBlank(outputtedNote.url)"
icon="tabler:external-link" class="w-full">
View on Remote
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="" v-if="isMyAccount">
<ButtonsDropdownElement @click="remove" icon="tabler:backspace" :disabled="!identity"
class="w-full border-r-2 border-red-500">
Delete
</ButtonsDropdownElement>
</Menu.Item>
</Menu.ItemGroup>
<hr class="border-white/10 rounded" />
<Menu.ItemGroup>
<Menu.Item value="">
<ButtonsDropdownElement @click="outputtedNote && useEvent('note:reply', outputtedNote)"
icon="tabler:arrow-back-up" class="w-full">
Reply
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="">
<ButtonsDropdownElement @click="likeFn" icon="tabler:heart" class="w-full"
v-if="!outputtedNote?.favourited">
Like
</ButtonsDropdownElement>
<ButtonsDropdownElement @click="likeFn" icon="tabler:heart-filled" class="w-full"
v-else>
Unlike
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="">
<ButtonsDropdownElement @click="reblogFn" icon="tabler:repeat" class="w-full"
v-if="!outputtedNote?.reblogged">
Reblog
</ButtonsDropdownElement>
<ButtonsDropdownElement @click="reblogFn" icon="tabler:repeat" class="w-full" v-else>
Unreblog
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="">
<ButtonsDropdownElement @click="outputtedNote && useEvent('note:quote', outputtedNote)"
icon="tabler:quote" class="w-full">
Quote
</ButtonsDropdownElement>
</Menu.Item>
</Menu.ItemGroup>
<hr class="border-white/10 rounded" />
<Menu.ItemGroup>
<Menu.Item value="">
<ButtonsDropdownElement @click="outputtedNote && useEvent('note:report', outputtedNote)"
icon="tabler:flag" class="w-full"
:disabled="!permissions.includes(RolePermissions.MANAGE_OWN_REPORTS)">
Report
</ButtonsDropdownElement>
</Menu.Item>
<Menu.Item value="" v-if="permissions.includes(RolePermissions.MANAGE_ACCOUNTS)">
<ButtonsDropdownElement icon="tabler:shield-bolt" class="w-full">
Open Moderation Panel
</ButtonsDropdownElement>
</Menu.Item>
</Menu.ItemGroup>
</template>
</DropdownsAdaptiveDropdown>
</div>
@ -109,10 +172,8 @@ useListen("composer:send-edit", (note) => {
}
});
const tokenData = useTokenData();
const isSignedIn = useSignedIn();
const me = useMe();
const client = useClient(tokenData);
const client = useClient();
const identity = useCurrentIdentity();
const {
loaded,
note: outputtedNote,
@ -126,7 +187,16 @@ const {
reblogDisplayName,
} = useNoteData(noteRef, client);
const openBlank = (url: string) => window.open(url, "_blank");
const { copy } = useClipboard();
const isMyAccount = computed(
() => identity.value?.account.id === outputtedNote.value?.account.id,
);
const isRemote = computed(() =>
outputtedNote.value?.account.acct.includes("@"),
);
const permissions = usePermissions();
const numberFormat = (number = 0) =>
new Intl.NumberFormat(undefined, {
notation: "compact",
@ -137,7 +207,7 @@ const numberFormat = (number = 0) =>
const likeFn = async () => {
if (!outputtedNote.value) return;
if (outputtedNote.value.favourited) {
const output = await client.value?.unfavouriteStatus(
const output = await client.value.unfavouriteStatus(
outputtedNote.value.id,
);
@ -145,7 +215,7 @@ const likeFn = async () => {
noteRef.value = output.data;
}
} else {
const output = await client.value?.favouriteStatus(
const output = await client.value.favouriteStatus(
outputtedNote.value.id,
);
@ -158,7 +228,7 @@ const likeFn = async () => {
const reblogFn = async () => {
if (!outputtedNote.value) return;
if (outputtedNote.value?.reblogged) {
const output = await client.value?.unreblogStatus(
const output = await client.value.unreblogStatus(
outputtedNote.value.id,
);
@ -166,7 +236,7 @@ const reblogFn = async () => {
noteRef.value = output.data;
}
} else {
const output = await client.value?.reblogStatus(outputtedNote.value.id);
const output = await client.value.reblogStatus(outputtedNote.value.id);
if (output?.data.reblog) {
noteRef.value = output.data.reblog;

View file

@ -15,7 +15,6 @@ const props = defineProps<{
account_id: string | null;
}>();
const tokenData = useTokenData();
const client = useClient(tokenData);
const client = useClient();
const account = useAccount(client, props.account_id);
</script>

View file

@ -9,18 +9,18 @@
class="h-32 w-32 -mt-[4.5rem] z-10 shrink-0 rounded ring-2 ring-dark-800" />
<ClientOnly>
<ButtonsSecondary v-if="account && account?.id === me?.id">Edit Profile
<ButtonsSecondary v-if="account && account?.id === identity?.account?.id">Edit Profile
</ButtonsSecondary>
<ButtonsSecondary :loading="isLoading" @click="follow()"
v-if="account && account?.id !== me?.id && relationship && !relationship.following && !relationship.requested">
v-if="account && account?.id !== identity?.account?.id && relationship && !relationship.following && !relationship.requested">
<span>Follow</span>
</ButtonsSecondary>
<ButtonsSecondary :loading="isLoading" @click="unfollow()"
v-if="account && account?.id !== me?.id && relationship && relationship.following">
v-if="account && account?.id !== identity?.account?.id && relationship && relationship.following">
<span>Unfollow</span>
</ButtonsSecondary>
<ButtonsSecondary :loading="isLoading" :disabled="true"
v-if="account && account?.id !== me?.id && relationship && !relationship.following && relationship.requested">
v-if="account && account?.id !== identity?.account?.id && relationship && !relationship.following && relationship.requested">
<span>Requested</span>
</ButtonsSecondary>
</ClientOnly>
@ -103,14 +103,13 @@ const props = defineProps<{
}>();
const skeleton = computed(() => !props.account);
const tokenData = useTokenData();
const me = useMe();
const client = useClient(tokenData);
const identity = useCurrentIdentity();
const client = useClient();
const accountId = computed(() => props.account?.id ?? null);
const { relationship, isLoading } = useRelationship(client, accountId);
const follow = () => {
if (!tokenData || !props.account || !relationship.value) return;
if (!identity.value || !props.account || !relationship.value) return;
relationship.value = {
...relationship.value,
following: true,
@ -118,7 +117,7 @@ const follow = () => {
};
const unfollow = () => {
if (!tokenData || !props.account || !relationship.value) return;
if (!identity.value || !props.account || !relationship.value) return;
relationship.value = {
...relationship.value,
following: false,

View file

@ -1,5 +1,5 @@
<template>
<NuxtLink :href="accountUrl" class="flex flex-row">
<component :is="disableLink ? 'div' : NuxtLink" :href="accountUrl" class="flex flex-row">
<Skeleton :enabled="!account" shape="rect" class="!h-12 w-12">
<div class="shrink-0">
<AvatarsCentered class="h-12 w-12 rounded ring-1 ring-white/5" :src="account?.avatar"
@ -23,14 +23,16 @@
</Skeleton>
</span>
</div>
</NuxtLink>
</component>
</template>
<script lang="ts" setup>
import type { Account } from "~/types/mastodon/account";
import { NuxtLink } from "#components";
const props = defineProps<{
account?: Account;
disableLink?: boolean;
}>();
const accountUrl = props.account && `/@${props.account.acct}`;

View file

@ -3,8 +3,7 @@
</template>
<script lang="ts" setup>
const tokenData = useTokenData();
const client = useClient(tokenData);
const client = useClient();
const timelineParameters = ref({});
const { timeline, loadNext, loadPrev } = useHomeTimeline(
client.value,

View file

@ -14,8 +14,7 @@
</template>
<script lang="ts" setup>
const tokenData = useTokenData();
const client = useClient(tokenData);
const client = useClient();
const isLoading = ref(true);