feat: Give more functionality to note menu

This commit is contained in:
Jesse Wierzbinski 2024-11-30 18:21:40 +01:00
parent 97566289cd
commit 49d356e2ab
No known key found for this signature in database
12 changed files with 95 additions and 35 deletions

View file

@ -11,6 +11,8 @@
</NuxtLayout>
<NotificationsRenderer />
<ConfirmationModal />
<!-- pointer-events-auto fixes https://github.com/unovue/shadcn-vue/issues/462 -->
<Toaster class="pointer-events-auto" />
</ClientOnly>
</template>
@ -21,6 +23,7 @@ import { convert } from "html-to-text";
import "iconify-icon";
import ConfirmationModal from "./components/modals/confirmation.vue";
import NotificationsRenderer from "./components/notifications/notifications-renderer.vue";
import { Toaster } from "./components/ui/sonner";
import { SettingIds } from "./settings";
// Use SSR-safe IDs for Headless UI
provideHeadlessUseId(() => useId());

BIN
bun.lockb

Binary file not shown.

View file

@ -15,7 +15,7 @@
<Button variant="ghost">
<Quote class="size-5 text-primary" />
</Button>
<Menu>
<Menu :api-note-string="apiNoteString" :url="url" :remote-url="remoteUrl" :is-remote="isRemote" :author-id="authorId">
<Button variant="ghost">
<Ellipsis class="size-5 text-primary" />
</Button>
@ -32,6 +32,11 @@ defineProps<{
replyCount: number;
likeCount: number;
reblogCount: number;
apiNoteString: string;
isRemote: boolean;
url: string;
remoteUrl: string;
authorId: string;
}>();
const numberFormat = (number = 0) =>

View file

@ -4,36 +4,31 @@
<slot />
</span>
<span class="hidden group-hover:inline">
<span @click="copyText" v-if="!hasCopied"
<span @click="copyText"
class="select-none cursor-pointer space-x-1">
<Clipboard class="size-4 -translate-y-0.5 inline" />
Click to copy
</span>
<span v-else class="select-none space-x-1">
<Check class="size-4 -translate-y-0.5 inline" />
Copied!
</span>
</span>
</span>
</template>
<script lang="ts" setup>
<script lang="tsx" setup>
import { cn } from "@/lib/utils";
import { Check, Clipboard } from "lucide-vue-next";
import type { HTMLAttributes } from "vue";
import { toast } from "vue-sonner";
const { text } = defineProps<{
text: string;
class?: HTMLAttributes["class"];
}>();
const hasCopied = ref(false);
const { copy } = useClipboard();
const copyText = () => {
copy(text);
hasCopied.value = true;
setTimeout(() => {
hasCopied.value = false;
}, 2000);
toast("Copied to clipboard", {
icon: <Check class="size-5 text-green-500" />,
});
};
</script>

View file

@ -1,6 +1,6 @@
<template>
<NuxtLink :href="url" class="rounded flex flex-row items-center gap-3">
<div :class="cn('relative size-14', smallLayout && 'size-6')">
<div class="rounded flex flex-row items-center gap-3">
<NuxtLink :href="url" :class="cn('relative size-14', smallLayout && 'size-6')">
<Avatar :class="cn('size-14 rounded-md border border-card', smallLayout && 'size-6')">
<AvatarImage :src="avatar" alt="" />
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
@ -9,7 +9,7 @@
<AvatarImage :src="cornerAvatar" alt="" />
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
</Avatar>
</div>
</NuxtLink>
<div :class="cn('flex flex-col gap-0.5 justify-center flex-1 text-left leading-tight', smallLayout && 'flex-row justify-start items-center gap-2')">
<span class="truncate font-semibold">{{
displayName
@ -31,7 +31,7 @@
<component :is="visibilities[visibility].icon" class="size-5" />
</span>
</div>
</NuxtLink>
</div>
</template>
<script lang="ts" setup>

View file

@ -1,4 +1,4 @@
<script setup lang="ts">
<script setup lang="tsx">
import {
DropdownMenu,
DropdownMenuContent,
@ -11,6 +11,7 @@ import {
} from "@/components/ui/dropdown-menu";
import {
Ban,
Check,
Code,
Delete,
ExternalLink,
@ -19,6 +20,32 @@ import {
Pencil,
Trash,
} from "lucide-vue-next";
import { toast } from "vue-sonner";
defineProps<{
apiNoteString: string;
isRemote: boolean;
url: string;
remoteUrl: string;
authorId: string;
}>();
const { copy } = useClipboard();
const copyText = (text: string) => {
copy(text);
toast("Copied to clipboard", {
icon: <Check class="size-5 text-green-500" />,
});
};
const blockUser = async (id: string) => {
await client.value.blockAccount(id);
toast("User blocked", {
icon: <Ban class="size-5 text-destructive" />,
});
};
</script>
<template>
@ -30,27 +57,27 @@ import {
<DropdownMenuLabel>Note Actions</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<DropdownMenuItem as="button">
<Pencil class="mr-2 size-4" />
<span>Edit</span>
<DropdownMenuShortcut>E</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem as="button" @click="copyText(apiNoteString)">
<Code class="mr-2 size-4" />
<span>Copy API data</span>
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem as="button" @click="copyText(url)">
<Link class="mr-2 size-4" />
<span>Copy link</span>
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem as="button" v-if="isRemote" @click="copyText(remoteUrl)">
<Link class="mr-2 size-4" />
<span>Copy link (origin)</span>
<DropdownMenuShortcut>K</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem as="a" v-if="isRemote" target="_blank" rel="noopener noreferrer" :href="remoteUrl">
<ExternalLink class="mr-2 size-4" />
<span>Open on remote</span>
<DropdownMenuShortcut>F</DropdownMenuShortcut>
@ -58,11 +85,11 @@ import {
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<DropdownMenuItem as="button">
<Delete class="mr-2 size-4" />
<span>Delete and redraft</span>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem as="button">
<Trash class="mr-2 size-4" />
<span>Delete</span>
<DropdownMenuShortcut>D</DropdownMenuShortcut>
@ -70,11 +97,11 @@ import {
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<DropdownMenuItem as="button" :disabled="true">
<MessageSquare class="mr-2 size-4" />
<span>Report</span>
</DropdownMenuItem>
<DropdownMenuItem>
<DropdownMenuItem as="button" @click="blockUser(authorId)">
<Ban class="mr-2 size-4" />
<span>Block user</span>
</DropdownMenuItem>

View file

@ -1,18 +1,19 @@
<template>
<Card as="article" class="rounded-none border-0 duration-200 shadow-none">
<CardHeader class="pb-4" as="header">
<ReblogHeader v-if="note.reblog" :avatar="note.account.avatar"
:display-name="note.account.display_name" :url="reblogAccountUrl" />
<ReblogHeader v-if="note.reblog" :avatar="note.account.avatar" :display-name="note.account.display_name"
:url="reblogAccountUrl" />
<Header :avatar="noteToUse.account.avatar" :corner-avatar="note.reblog ? note.account.avatar : undefined"
:acct="noteToUse.account.acct" :display-name="noteToUse.account.display_name"
:visibility="noteToUse.visibility" :url="accountUrl" :created-at="new Date(noteToUse.created_at)" :small-layout="smallLayout" />
:visibility="noteToUse.visibility" :url="accountUrl" :created-at="new Date(noteToUse.created_at)"
:small-layout="smallLayout" />
</CardHeader>
<CardContent>
<Content :content="noteToUse.content" :quote="note.quote ?? undefined" />
</CardContent>
<CardFooter v-if="!hideActions">
<Actions :reply-count="noteToUse.replies_count" :like-count="noteToUse.favourites_count"
:reblog-count="noteToUse.reblogs_count" />
<Actions :reply-count="noteToUse.replies_count" :like-count="noteToUse.favourites_count" :url="url"
:api-note-string="JSON.stringify(note, null, 4)" :reblog-count="noteToUse.reblogs_count" :remote-url="noteToUse.url" :is-remote="isRemote" :author-id="noteToUse.account.id" />
</CardFooter>
</Card>
</template>
@ -34,7 +35,8 @@ const { note } = defineProps<{
// Notes can be reblogs, in which case the actual thing to render is inside the reblog property
const noteToUse = note.reblog ? note.reblog : note;
const url = `/@${noteToUse.account.acct}/${noteToUse.id}`;
const accountUrl = `/@${noteToUse.account.acct}`;
const reblogAccountUrl = `/@${note.account.acct}`;
const url = wrapUrl(`/@${noteToUse.account.acct}/${noteToUse.id}`);
const accountUrl = wrapUrl(`/@${noteToUse.account.acct}`);
const reblogAccountUrl = wrapUrl(`/@${note.account.acct}`);
const isRemote = noteToUse.account.acct.includes("@");
</script>

View file

@ -24,7 +24,7 @@ const forwardedProps = useForwardProps(delegatedProps);
<DropdownMenuItem
v-bind="forwardedProps"
:class="cn(
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0',
'relative flex cursor-default select-none items-center rounded-sm gap-2 px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&>svg]:size-4 [&>svg]:shrink-0 w-full',
inset && 'pl-8',
props.class,
)"

View file

@ -0,0 +1,18 @@
<script lang="ts" setup>
import { Toaster as Sonner, type ToasterProps } from "vue-sonner";
const props = defineProps<ToasterProps>();
</script>
<template>
<Sonner class="toaster group" v-bind="props" :toast-options="{
classes: {
toast: 'group toast group-[.toaster]:bg-background group-[.toaster]:text-foreground group-[.toaster]:border-border group-[.toaster]:shadow-lg',
description: 'group-[.toast]:text-muted-foreground',
actionButton:
'group-[.toast]:bg-primary group-[.toast]:text-primary-foreground',
cancelButton:
'group-[.toast]:bg-muted group-[.toast]:text-muted-foreground',
},
}" :position="'top-right'" />
</template>

View file

@ -0,0 +1 @@
export { default as Toaster } from "./Sonner.vue";

View file

@ -63,6 +63,7 @@
"tailwindcss-animate": "^1.0.7",
"vue": "^3.5.13",
"vue-router": "^4.5.0",
"vue-sonner": "^1.3.0",
"zod": "^3.23.8"
},
"devDependencies": {

8
utils/urls.ts Normal file
View file

@ -0,0 +1,8 @@
export const wrapUrl = (path: string) => {
return new URL(
path,
identity.value
? `https://${identity.value.instance.domain}`
: window.location.origin,
).toString();
};