v3: added content slugs, fixed real-time updates in client, added @mentions across the app, added new file selector and drop zone
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
|
||||
import { API_URL } from "../config/api.ts";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
import { ErrorCard } from "../components/ErrorCard.tsx";
|
||||
import { Tooltip } from "../components/Tooltip.tsx";
|
||||
import { useWS } from "../hooks/useWS.ts";
|
||||
import type {
|
||||
DumpUpvotedData,
|
||||
@@ -15,6 +16,7 @@ import type {
|
||||
RawNotification,
|
||||
UserDumpPostedData,
|
||||
UserFollowedData,
|
||||
UserMentionedData,
|
||||
} from "../model.ts";
|
||||
import { deserializeNotification } from "../model.ts";
|
||||
import { PageShell } from "../components/PageShell.tsx";
|
||||
@@ -33,7 +35,7 @@ type State =
|
||||
loadingMore: boolean;
|
||||
};
|
||||
|
||||
type NotifIconKind = "upvote" | "follow" | "dump" | "playlist";
|
||||
type NotifIconKind = "upvote" | "follow" | "dump" | "playlist" | "mention";
|
||||
|
||||
function notifIconKind(type: Notification["type"]): NotifIconKind {
|
||||
switch (type) {
|
||||
@@ -47,16 +49,31 @@ function notifIconKind(type: Notification["type"]): NotifIconKind {
|
||||
return "dump";
|
||||
case "playlist_dump_added":
|
||||
return "playlist";
|
||||
case "user_mentioned":
|
||||
return "mention";
|
||||
}
|
||||
}
|
||||
|
||||
const UpvoteSvg = () => (
|
||||
<svg viewBox="0 0 10 10" width="11" height="11" fill="currentColor">
|
||||
<polygon points="5,1 9,9 1,9" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
const FollowSvg = () => (
|
||||
<svg viewBox="0 0 10 10" width="10" height="10" fill="currentColor">
|
||||
<polygon points="2,1 9,5 2,9" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
function NotifIcon({ type }: { type: Notification["type"] }) {
|
||||
const kind = notifIconKind(type);
|
||||
const glyphs: Record<NotifIconKind, string> = {
|
||||
upvote: "▲",
|
||||
follow: "►",
|
||||
const glyphs: Record<NotifIconKind, React.ReactNode> = {
|
||||
upvote: <UpvoteSvg />,
|
||||
follow: <FollowSvg />,
|
||||
dump: "🚚",
|
||||
playlist: "📜",
|
||||
mention: "@",
|
||||
};
|
||||
return (
|
||||
<span className={`notif-icon notif-icon--${kind}`}>
|
||||
@@ -65,78 +82,56 @@ function NotifIcon({ type }: { type: Notification["type"] }) {
|
||||
);
|
||||
}
|
||||
|
||||
function notificationLink(n: Notification): string {
|
||||
const data = n.data as NotificationData;
|
||||
switch (n.type) {
|
||||
case "user_followed":
|
||||
return `/users/${(data as UserFollowedData).followerUsername}`;
|
||||
case "playlist_followed":
|
||||
return `/playlists/${(data as PlaylistFollowedData).playlistId}`;
|
||||
case "user_dump_posted":
|
||||
return `/dumps/${(data as UserDumpPostedData).dumpId}`;
|
||||
case "playlist_dump_added":
|
||||
return `/dumps/${(data as PlaylistDumpAddedData).dumpId}`;
|
||||
case "dump_upvoted":
|
||||
return `/dumps/${(data as DumpUpvotedData).dumpId}`;
|
||||
case "user_mentioned": {
|
||||
const d = data as UserMentionedData;
|
||||
if (d.contextType === "comment") return `/dumps/${d.dumpId}#comment-${d.contextId}`;
|
||||
if (d.contextType === "dump") return `/dumps/${d.contextId}`;
|
||||
return `/playlists/${d.contextId}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function notificationContent(n: Notification): React.ReactNode {
|
||||
const data = n.data as NotificationData;
|
||||
switch (n.type) {
|
||||
case "user_followed": {
|
||||
const d = data as UserFollowedData;
|
||||
return (
|
||||
<>
|
||||
<Link to={`/users/${d.followerUsername}`} className="notif-link">
|
||||
{d.followerUsername}
|
||||
</Link>
|
||||
{" started following you"}
|
||||
</>
|
||||
);
|
||||
return <><strong>{d.followerUsername}</strong>{" started following you"}</>;
|
||||
}
|
||||
case "playlist_followed": {
|
||||
const d = data as PlaylistFollowedData;
|
||||
return (
|
||||
<>
|
||||
<Link to={`/users/${d.followerUsername}`} className="notif-link">
|
||||
{d.followerUsername}
|
||||
</Link>
|
||||
{" followed your playlist "}
|
||||
<Link to={`/playlists/${d.playlistId}`} className="notif-link">
|
||||
{d.playlistTitle}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
return <><strong>{d.followerUsername}</strong>{" followed your playlist "}<strong>{d.playlistTitle}</strong></>;
|
||||
}
|
||||
case "user_dump_posted": {
|
||||
const d = data as UserDumpPostedData;
|
||||
return (
|
||||
<>
|
||||
<Link to={`/users/${d.dumperUsername}`} className="notif-link">
|
||||
{d.dumperUsername}
|
||||
</Link>
|
||||
{" posted "}
|
||||
<Link to={`/dumps/${d.dumpId}`} className="notif-link">
|
||||
{d.dumpTitle}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
return <><strong>{d.dumperUsername}</strong>{" posted "}<strong>{d.dumpTitle}</strong></>;
|
||||
}
|
||||
case "playlist_dump_added": {
|
||||
const d = data as PlaylistDumpAddedData;
|
||||
return (
|
||||
<>
|
||||
<Link to={`/dumps/${d.dumpId}`} className="notif-link">
|
||||
{d.dumpTitle}
|
||||
</Link>
|
||||
{" was added to "}
|
||||
<Link to={`/playlists/${d.playlistId}`} className="notif-link">
|
||||
{d.playlistTitle}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
return <><strong>{d.dumpTitle}</strong>{" was added to "}<strong>{d.playlistTitle}</strong></>;
|
||||
}
|
||||
case "dump_upvoted": {
|
||||
const d = data as DumpUpvotedData;
|
||||
return (
|
||||
<>
|
||||
<Link to={`/users/${d.voterUsername}`} className="notif-link">
|
||||
{d.voterUsername}
|
||||
</Link>
|
||||
{" upvoted "}
|
||||
<Link to={`/dumps/${d.dumpId}`} className="notif-link">
|
||||
{d.dumpTitle}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
return <><strong>{d.voterUsername}</strong>{" upvoted "}<strong>{d.dumpTitle}</strong></>;
|
||||
}
|
||||
case "user_mentioned": {
|
||||
const d = data as UserMentionedData;
|
||||
const where = d.contextTitle || (d.contextType === "comment" ? "a comment" : "a post");
|
||||
return <><strong>{d.mentionerUsername}</strong>{" mentioned you in "}<strong>{where}</strong></>;
|
||||
}
|
||||
default:
|
||||
return "New notification";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -308,21 +303,28 @@ export function Notifications() {
|
||||
!n.read ? " notification-item--unread" : ""
|
||||
}`}
|
||||
>
|
||||
<NotifIcon type={n.type} />
|
||||
<div className="notification-body">
|
||||
<span className="notification-content">
|
||||
{notificationContent(n)}
|
||||
</span>
|
||||
<time
|
||||
className="notification-time"
|
||||
dateTime={n.createdAt.toISOString()}
|
||||
>
|
||||
{timeAgo(n.createdAt)}
|
||||
</time>
|
||||
</div>
|
||||
{!n.read && (
|
||||
<span className="notif-dot" aria-hidden="true" />
|
||||
)}
|
||||
<Link
|
||||
to={notificationLink(n)}
|
||||
className="notification-item-link"
|
||||
>
|
||||
<NotifIcon type={n.type} />
|
||||
<div className="notification-body">
|
||||
<span className="notification-content">
|
||||
{notificationContent(n)}
|
||||
</span>
|
||||
<Tooltip text={n.createdAt.toLocaleString()}>
|
||||
<time
|
||||
className="notification-time"
|
||||
dateTime={n.createdAt.toISOString()}
|
||||
>
|
||||
{timeAgo(n.createdAt)}
|
||||
</time>
|
||||
</Tooltip>
|
||||
</div>
|
||||
{!n.read && (
|
||||
<span className="notif-dot" aria-hidden="true" />
|
||||
)}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
Reference in New Issue
Block a user