Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a462ee146 | ||
|
|
4bdfae155a | ||
|
|
5778035656 | ||
|
|
0a28f05d53 | ||
|
|
6537c34a38 | ||
|
|
094f116b10 |
@@ -117,7 +117,7 @@ jobs:
|
|||||||
echo "Release: $SERVER/$REPO/releases/tag/$REF_NAME"
|
echo "Release: $SERVER/$REPO/releases/tag/$REF_NAME"
|
||||||
else
|
else
|
||||||
owner=${REPO%%/*}
|
owner=${REPO%%/*}
|
||||||
version="$(echo "$REF_NAME" | tr -c 'A-Za-z0-9._-\n' '-')-${SHA::8}"
|
version="$(printf '%s' "$REF_NAME" | tr -c 'A-Za-z0-9._-' '-')-$(printf '%s' "$SHA" | cut -c1-8)"
|
||||||
for f in *.apk SHA256SUMS; do
|
for f in *.apk SHA256SUMS; do
|
||||||
curl -fsS -X PUT -H "$auth" --upload-file "$f" "$SERVER/api/packages/$owner/generic/musicshare-android/$version/$f"
|
curl -fsS -X PUT -H "$auth" --upload-file "$f" "$SERVER/api/packages/$owner/generic/musicshare-android/$version/$f"
|
||||||
echo "Uploaded $f"
|
echo "Uploaded $f"
|
||||||
|
|||||||
@@ -103,27 +103,15 @@ export async function toNotificationDTOs(rows: Row[]): Promise<NotificationDTO[]
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Newest first, 30 at a time; the cursor is the last one's time. */
|
/** Newest first, 30 at a time; the cursor is the last one's time. */
|
||||||
export async function listNotifications(
|
export async function listNotifications(userId: string, cursor: string | null) {
|
||||||
userId: string,
|
|
||||||
cursor: string | null,
|
|
||||||
actorId?: string // Added optional actorId filter
|
|
||||||
): Promise<{
|
|
||||||
notifications: NotificationDTO[];
|
|
||||||
unread: number;
|
|
||||||
nextCursor: string | null;
|
|
||||||
}> {
|
|
||||||
const n = schema.notifications;
|
const n = schema.notifications;
|
||||||
const before = cursor ? new Date(cursor) : null;
|
const before = cursor ? new Date(cursor) : null;
|
||||||
const query = db
|
const rows = await db
|
||||||
.select()
|
.select()
|
||||||
.from(n)
|
.from(n)
|
||||||
.where(and(
|
.where(and(eq(n.userId, userId), before && !isNaN(before.getTime()) ? lt(n.createdAt, before) : undefined))
|
||||||
eq(n.userId, userId),
|
.orderBy(desc(n.createdAt))
|
||||||
before && !isNaN(before.getTime()) ? lt(n.createdAt, before) : undefined,
|
.limit(31);
|
||||||
actorId ? eq(n.actorId, actorId) : undefined // Apply actor filter if provided
|
|
||||||
));
|
|
||||||
|
|
||||||
const rows = await query.orderBy(desc(n.createdAt)).limit(31);
|
|
||||||
const page = rows.slice(0, 30);
|
const page = rows.slice(0, 30);
|
||||||
return {
|
return {
|
||||||
notifications: await toNotificationDTOs(page),
|
notifications: await toNotificationDTOs(page),
|
||||||
|
|||||||
@@ -8,68 +8,32 @@ import { notificationPath, notificationText, timeAgo, type NotificationListDTO }
|
|||||||
import { Avatar } from '@/components/Avatar';
|
import { Avatar } from '@/components/Avatar';
|
||||||
import { client, useMe } from '@/lib/api';
|
import { client, useMe } from '@/lib/api';
|
||||||
|
|
||||||
const ADMIN_ACTOR_USERNAME = 'seriouscritter';
|
|
||||||
|
|
||||||
/** Reactions, comments, new followers and songs sent to you, newest first. Opening it marks them read. */
|
/** Reactions, comments, new followers and songs sent to you, newest first. Opening it marks them read. */
|
||||||
export default function NotificationsPage() {
|
export default function NotificationsPage() {
|
||||||
const { me, loggedOut } = useMe();
|
const { me, loggedOut } = useMe();
|
||||||
const { mutate } = useSWRConfig();
|
const { mutate } = useSWRConfig();
|
||||||
|
|
||||||
// Fetch all notifications for the user
|
|
||||||
const { data, size, setSize, isLoading } = useSWRInfinite<NotificationListDTO>(
|
const { data, size, setSize, isLoading } = useSWRInfinite<NotificationListDTO>(
|
||||||
(i, prev: NotificationListDTO | null) => (!me || (i > 0 && !prev?.nextCursor) ? null : ['notifications', prev?.nextCursor ?? null]),
|
(i, prev: NotificationListDTO | null) => (!me || (i > 0 && !prev?.nextCursor) ? null : ['notifications', prev?.nextCursor ?? null]),
|
||||||
([, cursor]: [string, string | null]) => client.notifications(cursor),
|
([, cursor]: [string, string | null]) => client.notifications(cursor),
|
||||||
{ revalidateOnFocus: false },
|
{ revalidateOnFocus: false },
|
||||||
);
|
);
|
||||||
|
const items = data?.flatMap((p) => p.notifications) ?? [];
|
||||||
const allItems = data?.flatMap((p) => p.notifications) ?? [];
|
|
||||||
const unread = data?.[0]?.unread ?? 0;
|
const unread = data?.[0]?.unread ?? 0;
|
||||||
|
|
||||||
// Filter messages specifically from seriouscritter
|
|
||||||
const seriousCritterMessages = allItems.filter(n => n.actor?.username === ADMIN_ACTOR_USERNAME);
|
|
||||||
|
|
||||||
// Seen once you've looked: clear the bell (rows stay highlighted until you come back).
|
// Seen once you've looked: clear the bell (rows stay highlighted until you come back).
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (unread) client.markNotificationsRead().then(() => mutate('/notifications'), () => {});
|
if (unread) client.markNotificationsRead().then(() => mutate('/notifications'), () => {});
|
||||||
}, [unread, mutate]);
|
}, [unread, mutate]);
|
||||||
|
|
||||||
if (loggedOut) return <p className="muted"><Link href="/login">Log in</Link> to see your notifications.</p>;
|
if (loggedOut) return <p className="muted"><Link href="/login">Log in</Link> to see your notifications.</p>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="stack">
|
<div className="stack">
|
||||||
<h1>Notifications</h1>
|
<h1>Notifications</h1>
|
||||||
|
{items.length === 0 && !isLoading && (
|
||||||
{/* Dedicated section for seriouscritter's messages */}
|
|
||||||
{seriousCritterMessages.length > 0 && (
|
|
||||||
<div className="admin-section">
|
|
||||||
<h2 className="muted">From {ADMIN_ACTOR_USERNAME}</h2>
|
|
||||||
<ul className="notifications">
|
|
||||||
{seriousCritterMessages.map((n) => {
|
|
||||||
const { title, body } = notificationText(n);
|
|
||||||
return (
|
|
||||||
<li key={n.id} className={n.read ? '' : 'unread'}>
|
|
||||||
<Link href={notificationPath(n)} className="notification">
|
|
||||||
<Avatar user={n.actor} size={40} />
|
|
||||||
<span className="notification-text">
|
|
||||||
<strong>{title}</strong>
|
|
||||||
<span>{body</span >
|
|
||||||
<span className="muted">{timeAgo(n.createdAt)}</span>
|
|
||||||
</span>
|
|
||||||
{n.track?.artworkUrl && <img className="artwork-sm" src={n.track.artworkUrl} alt="" />}
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* General notifications list */}
|
|
||||||
{allItems.length === 0 && !isLoading && (
|
|
||||||
<p className="muted">Nothing yet. Reactions, new followers and songs sent to you show up here.</p>
|
<p className="muted">Nothing yet. Reactions, new followers and songs sent to you show up here.</p>
|
||||||
)}
|
)}
|
||||||
<ul className="notifications">
|
<ul className="notifications">
|
||||||
{allItems.map((n) => {
|
{items.map((n) => {
|
||||||
const { title, body } = notificationText(n);
|
const { title, body } = notificationText(n);
|
||||||
return (
|
return (
|
||||||
<li key={n.id} className={n.read ? '' : 'unread'}>
|
<li key={n.id} className={n.read ? '' : 'unread'}>
|
||||||
@@ -77,7 +41,7 @@ export default function NotificationsPage() {
|
|||||||
<Avatar user={n.actor} size={40} />
|
<Avatar user={n.actor} size={40} />
|
||||||
<span className="notification-text">
|
<span className="notification-text">
|
||||||
<strong>{title}</strong>
|
<strong>{title}</strong>
|
||||||
<span>{body</span >
|
<span>{body}</span>
|
||||||
<span className="muted">{timeAgo(n.createdAt)}</span>
|
<span className="muted">{timeAgo(n.createdAt)}</span>
|
||||||
</span>
|
</span>
|
||||||
{n.track?.artworkUrl && <img className="artwork-sm" src={n.track.artworkUrl} alt="" />}
|
{n.track?.artworkUrl && <img className="artwork-sm" src={n.track.artworkUrl} alt="" />}
|
||||||
@@ -86,12 +50,11 @@ export default function NotificationsPage() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
{data?.at(-1)?.nextCursor && (
|
{data?.at(-1)?.nextCursor && (
|
||||||
<button className="btn" onClick={() => setSize(size + 1)}>
|
<button className="btn" onClick={() => setSize(size + 1)}>
|
||||||
Show more
|
Show more
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div >
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user