mirror of
https://github.com/versia-pub/frontend.git
synced 2025-12-06 08:28:20 +01:00
feat: ✨ Add notifications, improve note design
This commit is contained in:
parent
c586db3669
commit
d32f4d6899
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div :class="['prose block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline prose-a:hover:underline', $style.content]" v-html="content">
|
<div :class="['prose prose-sm block relative dark:prose-invert duration-200 !max-w-full break-words prose-a:no-underline prose-a:hover:underline', $style.content]" v-html="content">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Attachments v-if="attachments.length > 0" :attachments="attachments" />
|
<Attachments v-if="attachments.length > 0" :attachments="attachments" />
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="rounded flex flex-row items-center gap-3">
|
<div class="rounded flex flex-row items-center gap-3">
|
||||||
<NuxtLink :href="url" :class="cn('relative size-14', smallLayout && 'size-6')">
|
<NuxtLink :href="url" :class="cn('relative size-14', smallLayout && 'size-8')">
|
||||||
<Avatar :class="cn('size-14 rounded-md border border-card', smallLayout && 'size-6')">
|
<Avatar :class="cn('size-14 rounded-md border border-card', smallLayout && 'size-8')">
|
||||||
<AvatarImage :src="avatar" alt="" />
|
<AvatarImage :src="avatar" alt="" />
|
||||||
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
|
|
@ -10,7 +10,7 @@
|
||||||
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
||||||
</Avatar>
|
</Avatar>
|
||||||
</NuxtLink>
|
</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')">
|
<div :class="cn('flex flex-col gap-0.5 justify-center flex-1 text-left leading-tight', smallLayout && 'text-sm')">
|
||||||
<span class="truncate font-semibold">{{
|
<span class="truncate font-semibold">{{
|
||||||
displayName
|
displayName
|
||||||
}}</span>
|
}}</span>
|
||||||
|
|
@ -37,6 +37,10 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import type { StatusVisibility } from "@versia/client/types";
|
import type { StatusVisibility } from "@versia/client/types";
|
||||||
|
import type {
|
||||||
|
UseTimeAgoMessages,
|
||||||
|
UseTimeAgoUnitNamesDefault,
|
||||||
|
} from "@vueuse/core";
|
||||||
import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
|
import { AtSign, Globe, Lock, LockOpen } from "lucide-vue-next";
|
||||||
import CopyableText from "./copyable-text.vue";
|
import CopyableText from "./copyable-text.vue";
|
||||||
|
|
||||||
|
|
@ -52,7 +56,22 @@ const { acct, createdAt } = defineProps<{
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const [username, instance] = acct.split("@");
|
const [username, instance] = acct.split("@");
|
||||||
const timeAgo = useTimeAgo(createdAt);
|
const digitRegex = /\d/;
|
||||||
|
const timeAgo = useTimeAgo(createdAt, {
|
||||||
|
messages: {
|
||||||
|
justNow: "now",
|
||||||
|
past: (n) => (n.match(digitRegex) ? `${n}` : n),
|
||||||
|
future: (n) => (n.match(digitRegex) ? `in ${n}` : n),
|
||||||
|
month: (n) => `${n}mo`,
|
||||||
|
year: (n) => `${n}y`,
|
||||||
|
day: (n) => `${n}d`,
|
||||||
|
week: (n) => `${n}w`,
|
||||||
|
hour: (n) => `${n}h`,
|
||||||
|
minute: (n) => `${n}m`,
|
||||||
|
second: (n) => `${n}s`,
|
||||||
|
invalid: "",
|
||||||
|
} as UseTimeAgoMessages<UseTimeAgoUnitNamesDefault>,
|
||||||
|
});
|
||||||
const fullTime = new Intl.DateTimeFormat("en-US", {
|
const fullTime = new Intl.DateTimeFormat("en-US", {
|
||||||
dateStyle: "medium",
|
dateStyle: "medium",
|
||||||
timeStyle: "short",
|
timeStyle: "short",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<Card as="article" class="rounded-none border-0 duration-200 shadow-none max-w-full">
|
<Card as="article" class="rounded-none border-0 duration-200 shadow- max-w-full">
|
||||||
<CardHeader class="pb-4" as="header">
|
<CardHeader class="pb-4" as="header">
|
||||||
<ReblogHeader v-if="note.reblog" :avatar="note.account.avatar" :display-name="note.account.display_name"
|
<ReblogHeader v-if="note.reblog" :avatar="note.account.avatar" :display-name="note.account.display_name"
|
||||||
:url="reblogAccountUrl" />
|
:url="reblogAccountUrl" />
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Content :content="noteToUse.content" :quote="note.quote ?? undefined" :attachments="noteToUse.media_attachments"/>
|
<Content :content="noteToUse.content" :quote="note.quote ?? undefined" :attachments="noteToUse.media_attachments"/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter v-if="!hideActions">
|
<CardFooter v-if="!hideActions" class="p-4 pt-0">
|
||||||
<Actions :reply-count="noteToUse.replies_count" :like-count="noteToUse.favourites_count" :url="url"
|
<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" @edit="useEvent('composer:edit', note)" @reply="useEvent('composer:reply', note)" @quote="useEvent('composer:quote', note)" @delete="useEvent('note:delete', note)" :note-id="noteToUse.id" :liked="noteToUse.favourited ?? false" :reblogged="noteToUse.reblogged ?? false" />
|
: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" @edit="useEvent('composer:edit', note)" @reply="useEvent('composer:reply', note)" @quote="useEvent('composer:quote', note)" @delete="useEvent('note:delete', note)" :note-id="noteToUse.id" :liked="noteToUse.favourited ?? false" :reblogged="noteToUse.reblogged ?? false" />
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
|
|
|
||||||
87
components/notifications/notification.vue
Normal file
87
components/notifications/notification.vue
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<template>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Card>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger :as-child="true">
|
||||||
|
<CardHeader v-if="notification.account" class="flex-row items-center gap-2 p-4">
|
||||||
|
<component :is="icon" class="size-5" />
|
||||||
|
<Avatar class="size-6 rounded-md border border-card">
|
||||||
|
<AvatarImage :src="notification.account.avatar" alt="" />
|
||||||
|
<AvatarFallback class="rounded-lg"> AA </AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
<span class="font-semibold">{{ notification.type === 'mention' ? text.toLowerCase() : notification.account.display_name }}</span>
|
||||||
|
</CardHeader>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{{ text }}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
<CardContent class="p-0">
|
||||||
|
<Note v-if="notification.status" :note="notification.status" :small-layout="true" :hide-actions="true" />
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</TooltipProvider>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import type { Notification } from "@versia/client/types";
|
||||||
|
import {
|
||||||
|
AtSign,
|
||||||
|
Heart,
|
||||||
|
Repeat,
|
||||||
|
User,
|
||||||
|
UserCheck,
|
||||||
|
UserPlus,
|
||||||
|
} from "lucide-vue-next";
|
||||||
|
import { Avatar, AvatarFallback, AvatarImage } from "~/components/ui/avatar";
|
||||||
|
import { Card, CardContent, CardHeader } from "~/components/ui/card";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "~/components/ui/tooltip";
|
||||||
|
import Note from "../notes/note.vue";
|
||||||
|
|
||||||
|
const { notification } = defineProps<{
|
||||||
|
notification: Notification;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
switch (notification.type) {
|
||||||
|
case "mention":
|
||||||
|
return AtSign;
|
||||||
|
case "reblog":
|
||||||
|
return Repeat;
|
||||||
|
case "follow":
|
||||||
|
return UserPlus;
|
||||||
|
case "favourite":
|
||||||
|
return Heart;
|
||||||
|
case "follow_request":
|
||||||
|
return User;
|
||||||
|
case "follow_accept":
|
||||||
|
return UserCheck;
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const text = computed(() => {
|
||||||
|
switch (notification.type) {
|
||||||
|
case "mention":
|
||||||
|
return "Mentioned you";
|
||||||
|
case "reblog":
|
||||||
|
return "Reblogged your note";
|
||||||
|
case "follow":
|
||||||
|
return "Followed you";
|
||||||
|
case "favourite":
|
||||||
|
return "Liked your note";
|
||||||
|
case "follow_request":
|
||||||
|
return "Requested to follow you";
|
||||||
|
case "follow_accept":
|
||||||
|
return "Accepted your follow request";
|
||||||
|
default:
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
@ -54,6 +54,7 @@ import {
|
||||||
SidebarRail,
|
SidebarRail,
|
||||||
SidebarTrigger,
|
SidebarTrigger,
|
||||||
} from "~/components/ui/sidebar";
|
} from "~/components/ui/sidebar";
|
||||||
|
import NotificationsTimeline from "../timelines/notifications.vue";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import ThemeSwitcher from "./theme-switcher.vue";
|
import ThemeSwitcher from "./theme-switcher.vue";
|
||||||
|
|
||||||
|
|
@ -140,7 +141,7 @@ const instance = useInstance();
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent>
|
<SidebarContent>
|
||||||
<SidebarGroup class="group-data-[collapsible=icon]:hidden">
|
<SidebarGroup>
|
||||||
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
<SidebarGroupLabel>Navigation</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem v-for="item in data.other" :key="item.name">
|
<SidebarMenuItem v-for="item in data.other" :key="item.name">
|
||||||
|
|
@ -184,7 +185,7 @@ const instance = useInstance();
|
||||||
</SidebarGroup>
|
</SidebarGroup>
|
||||||
</SidebarContent>
|
</SidebarContent>
|
||||||
<SidebarFooter>
|
<SidebarFooter>
|
||||||
<SidebarMenu>
|
<SidebarMenu class="gap-3">
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<ThemeSwitcher />
|
<ThemeSwitcher />
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
|
|
@ -240,9 +241,9 @@ const instance = useInstance();
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
<SidebarMenuItem>
|
<SidebarMenuItem>
|
||||||
<Button variant="default" size="lg" class="w-full" @click="useEvent('composer:open')">
|
<Button variant="default" size="lg" class="w-full group-data-[collapsible=icon]:px-4" @click="useEvent('composer:open')">
|
||||||
<Pen />
|
<Pen />
|
||||||
Compose
|
<span class="group-data-[collapsible=icon]:hidden">Compose</span>
|
||||||
</Button>
|
</Button>
|
||||||
</SidebarMenuItem>
|
</SidebarMenuItem>
|
||||||
</SidebarMenu>
|
</SidebarMenu>
|
||||||
|
|
@ -274,5 +275,11 @@ const instance = useInstance();
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</SidebarInset>
|
</SidebarInset>
|
||||||
|
<Sidebar variant="inset" collapsible="none" side="right" class="[--sidebar-width:24rem]">
|
||||||
|
<SidebarContent class="p-2 overflow-y-auto">
|
||||||
|
<NotificationsTimeline />
|
||||||
|
</SidebarContent>
|
||||||
|
<SidebarRail />
|
||||||
|
</Sidebar>
|
||||||
</SidebarProvider>
|
</SidebarProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<component :is="itemComponent" :note="item" @update="$emit('update', $event)"
|
<component :is="itemComponent" :note="type === 'status' ? item : undefined" :notification="type === 'notification' ? item : undefined" @update="$emit('update', $event)"
|
||||||
@delete="$emit('delete', item?.id)" />
|
@delete="$emit('delete', item?.id)" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import type { Notification, Status } from "@versia/client/types";
|
import type { Notification, Status } from "@versia/client/types";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import NewNoteItem from "../notes/note.vue";
|
import Note from "../notes/note.vue";
|
||||||
import NotificationItem from "../social-elements/notifications/notif.vue";
|
import NotificationItem from "../notifications/notification.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
item?: Status | Notification;
|
item?: Status | Notification;
|
||||||
|
|
@ -16,7 +16,7 @@ const props = defineProps<{
|
||||||
|
|
||||||
const itemComponent = computed(() => {
|
const itemComponent = computed(() => {
|
||||||
if (props.type === "status") {
|
if (props.type === "status") {
|
||||||
return NewNoteItem;
|
return Note;
|
||||||
}
|
}
|
||||||
if (props.type === "notification") {
|
if (props.type === "notification") {
|
||||||
return NotificationItem;
|
return NotificationItem;
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
<!-- Timeline.vue -->
|
<!-- Timeline.vue -->
|
||||||
<template>
|
<template>
|
||||||
<div class="timeline rounded overflow-hidden">
|
<div class="timeline rounded">
|
||||||
<TransitionGroup name="timeline-item" tag="div" class="timeline-items *:rounded space-y-4 *:border *:border-border/50">
|
<TransitionGroup name="timeline-item" tag="div" class="timeline-items *:rounded space-y-4 *:border *:border-border/50">
|
||||||
<TimelineItem :type="type" v-for="item in items" :key="item.id" :item="item" @update="updateItem"
|
<TimelineItem :type="type" v-for="item in items" :key="item.id" :item="item" @update="updateItem"
|
||||||
@delete="removeItem" />
|
@delete="removeItem" />
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue