v3: added localization, use global player for uploaded audio/video files
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Link } from "react-router";
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { Trans } from "@lingui/react/macro";
|
||||
|
||||
import { API_URL, NOTIFICATIONS_PAGE_SIZE } from "../config/api.ts";
|
||||
import { useAuth } from "../hooks/useAuth.ts";
|
||||
@@ -110,62 +112,62 @@ function notificationContent(n: Notification): React.ReactNode {
|
||||
case "user_followed": {
|
||||
const d = data as UserFollowedData;
|
||||
return (
|
||||
<>
|
||||
<Trans>
|
||||
<strong>{d.followerUsername}</strong>
|
||||
{" started following you"}
|
||||
</>
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
case "playlist_followed": {
|
||||
const d = data as PlaylistFollowedData;
|
||||
return (
|
||||
<>
|
||||
<Trans>
|
||||
<strong>{d.followerUsername}</strong>
|
||||
{" followed your playlist "}
|
||||
<strong>{d.playlistTitle}</strong>
|
||||
</>
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
case "user_dump_posted": {
|
||||
const d = data as UserDumpPostedData;
|
||||
return (
|
||||
<>
|
||||
<Trans>
|
||||
<strong>{d.dumperUsername}</strong>
|
||||
{" posted "}
|
||||
<strong>{d.dumpTitle}</strong>
|
||||
</>
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
case "playlist_dump_added": {
|
||||
const d = data as PlaylistDumpAddedData;
|
||||
return (
|
||||
<>
|
||||
<Trans>
|
||||
<strong>{d.dumpTitle}</strong>
|
||||
{" was added to "}
|
||||
<strong>{d.playlistTitle}</strong>
|
||||
</>
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
case "dump_upvoted": {
|
||||
const d = data as DumpUpvotedData;
|
||||
return (
|
||||
<>
|
||||
<Trans>
|
||||
<strong>{d.voterUsername}</strong>
|
||||
{" upvoted "}
|
||||
<strong>{d.dumpTitle}</strong>
|
||||
</>
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
case "user_mentioned": {
|
||||
const d = data as UserMentionedData;
|
||||
const where = d.contextTitle ||
|
||||
(d.contextType === "comment" ? "a comment" : "a post");
|
||||
(d.contextType === "comment" ? t`a comment` : t`a post`);
|
||||
return (
|
||||
<>
|
||||
<Trans>
|
||||
<strong>{d.mentionerUsername}</strong>
|
||||
{" mentioned you in "}
|
||||
<strong>{where}</strong>
|
||||
</>
|
||||
</Trans>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -173,13 +175,13 @@ function notificationContent(n: Notification): React.ReactNode {
|
||||
|
||||
function timeAgo(date: Date): string {
|
||||
const secs = Math.floor((Date.now() - date.getTime()) / 1000);
|
||||
if (secs < 60) return "just now";
|
||||
if (secs < 60) return t`just now`;
|
||||
const mins = Math.floor(secs / 60);
|
||||
if (mins < 60) return `${mins}m ago`;
|
||||
if (mins < 60) return t`${mins}m ago`;
|
||||
const hrs = Math.floor(mins / 60);
|
||||
if (hrs < 24) return `${hrs}h ago`;
|
||||
if (hrs < 24) return t`${hrs}h ago`;
|
||||
const days = Math.floor(hrs / 24);
|
||||
if (days < 7) return `${days}d ago`;
|
||||
if (days < 7) return t`${days}d ago`;
|
||||
return date.toLocaleDateString(undefined, { month: "short", day: "numeric" });
|
||||
}
|
||||
|
||||
@@ -215,8 +217,6 @@ export function Notifications() {
|
||||
const [state, setState] = useState<State>({ status: "loading" });
|
||||
|
||||
useEffect(() => {
|
||||
// 1. Fetch with original read state so unread items are highlighted
|
||||
// 2. Only after displaying, mark all read on the server
|
||||
authFetch(
|
||||
`${API_URL}/api/notifications?page=1&limit=${NOTIFICATIONS_PAGE_SIZE}`,
|
||||
)
|
||||
@@ -231,7 +231,6 @@ export function Notifications() {
|
||||
page: 1,
|
||||
loadingMore: false,
|
||||
});
|
||||
// Mark read server-side after we've shown the unread state
|
||||
return authFetch(`${API_URL}/api/notifications/read-all`, {
|
||||
method: "POST",
|
||||
});
|
||||
@@ -251,17 +250,19 @@ export function Notifications() {
|
||||
setState({ status: "error", error: err.message });
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
}, [authFetch, clearUnreadNotifications]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!lastNotification) return;
|
||||
const [prevLastNotification, setPrevLastNotification] = useState(
|
||||
lastNotification,
|
||||
);
|
||||
if (prevLastNotification !== lastNotification && lastNotification !== null) {
|
||||
setPrevLastNotification(lastNotification);
|
||||
setState((s) => {
|
||||
if (s.status !== "loaded") return s;
|
||||
if (s.items.some((n) => n.id === lastNotification.id)) return s;
|
||||
// Keep as unread so it gets highlighted when it arrives
|
||||
return { ...s, items: [lastNotification, ...s.items] };
|
||||
});
|
||||
}, [lastNotification]);
|
||||
}
|
||||
|
||||
const loadMore = () => {
|
||||
if (state.status !== "loaded" || !state.hasMore || state.loadingMore) {
|
||||
@@ -304,27 +305,31 @@ export function Notifications() {
|
||||
<div className="notifications-header">
|
||||
<h1 className="notifications-title">
|
||||
<span className="notifications-title-bell">🔔</span>
|
||||
Notifications
|
||||
<Trans>Notifications</Trans>
|
||||
</h1>
|
||||
{state.status === "loaded" && totalUnread > 0 && (
|
||||
<span className="notifications-unread-pill">
|
||||
{totalUnread} new
|
||||
{totalUnread} <Trans>new</Trans>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{state.status === "loading" && <p className="page-loading">Loading…</p>}
|
||||
{state.status === "loading" && (
|
||||
<p className="page-loading"><Trans>Loading…</Trans></p>
|
||||
)}
|
||||
{state.status === "error" && (
|
||||
<ErrorCard title="Failed to load" message={state.error} />
|
||||
<ErrorCard title={t`Failed to load`} message={state.error} />
|
||||
)}
|
||||
|
||||
{state.status === "loaded" && state.items.length === 0 && (
|
||||
<div className="notifications-empty">
|
||||
<span className="notifications-empty-icon">🔕</span>
|
||||
<p>Nothing here yet.</p>
|
||||
<p><Trans>Nothing here yet.</Trans></p>
|
||||
<p className="notifications-empty-hint">
|
||||
You'll be notified when someone follows your playlists, upvotes
|
||||
your dumps, or posts new content.
|
||||
<Trans>
|
||||
You'll be notified when someone follows your playlists, upvotes
|
||||
your dumps, or posts new content.
|
||||
</Trans>
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -332,7 +337,9 @@ export function Notifications() {
|
||||
{state.status === "loaded" && state.items.length > 0 &&
|
||||
groupByDate(state.items).map(({ label, items }) => (
|
||||
<section key={label} className="notif-group">
|
||||
<h2 className="notif-group-label">{label}</h2>
|
||||
<h2 className="notif-group-label">
|
||||
{label === "Today" ? t`Today` : label === "Yesterday" ? t`Yesterday` : t`Earlier`}
|
||||
</h2>
|
||||
<ul className="notification-list">
|
||||
{items.map((n) => (
|
||||
<li
|
||||
@@ -376,7 +383,7 @@ export function Notifications() {
|
||||
onClick={loadMore}
|
||||
disabled={state.loadingMore}
|
||||
>
|
||||
{state.loadingMore ? "Loading…" : "Load more"}
|
||||
{state.loadingMore ? <Trans>Loading…</Trans> : <Trans>Load more</Trans>}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user