From 870a9d0d7d1f0ac13650873bff763c0b4d681d4c Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 12 Nov 2025 13:14:35 +0200 Subject: [PATCH 1/4] Priority Notifications Signed-off-by: prxt6529 --- .../brain/notifications/NotificationItem.tsx | 19 ++- components/brain/notifications/index.tsx | 7 +- .../NotificationPriorityAlert.tsx | 120 ++++++++++++++++++ generated/models/ApiNotificationCause.ts | 3 +- openapi.yaml | 1 + types/feed.types.ts | 13 +- 6 files changed, 154 insertions(+), 9 deletions(-) create mode 100644 components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx diff --git a/components/brain/notifications/NotificationItem.tsx b/components/brain/notifications/NotificationItem.tsx index b4e55602b6..5c392fdd18 100644 --- a/components/brain/notifications/NotificationItem.tsx +++ b/components/brain/notifications/NotificationItem.tsx @@ -1,16 +1,17 @@ -import { memo } from "react"; +import { DropInteractionParams } from "@/components/waves/drops/Drop"; import { ApiNotificationCause } from "@/generated/models/ApiNotificationCause"; import { assertUnreachable } from "@/helpers/AllowlistToolHelpers"; import { ExtendedDrop } from "@/helpers/waves/drop.helpers"; -import { TypedNotification } from "@/types/feed.types"; import { ActiveDropState } from "@/types/dropInteractionTypes"; -import { DropInteractionParams } from "@/components/waves/drops/Drop"; +import { TypedNotification } from "@/types/feed.types"; +import { memo } from "react"; +import NotificationAllDrops from "./all-drops/NotificationAllDrops"; import NotificationDropQuoted from "./drop-quoted/NotificationDropQuoted"; import NotificationDropReplied from "./drop-replied/NotificationDropReplied"; import NotificationIdentityMentioned from "./identity-mentioned/NotificationIdentityMentioned"; import NotificationIdentitySubscribed from "./identity-subscribed/NotificationIdentitySubscribed"; +import NotificationPriorityAlert from "./priority-alert/NotificationPriorityAlert"; import NotificationWaveCreated from "./wave-created/NotificationWaveCreated"; -import NotificationAllDrops from "./all-drops/NotificationAllDrops"; import type { JSX } from "react"; import NotificationDropReacted from "./drop-reacted/NotificationDropReacted"; @@ -85,6 +86,16 @@ function NotificationItemComponent({ onDropContentClick={onDropContentClick} /> ); + case ApiNotificationCause.PriorityAlert: + return ( + + ); default: assertUnreachable(notification); return
; diff --git a/components/brain/notifications/index.tsx b/components/brain/notifications/index.tsx index 59b9da781d..006d17bef3 100644 --- a/components/brain/notifications/index.tsx +++ b/components/brain/notifications/index.tsx @@ -1,8 +1,8 @@ "use client"; -import { useMemo } from "react"; import { ApiNotificationCause } from "@/generated/models/ApiNotificationCause"; import type { ActiveDropState } from "@/types/dropInteractionTypes"; +import { useMemo } from "react"; import NotificationsCauseFilter from "./NotificationsCauseFilter"; import { useNotificationsController } from "./hooks/useNotificationsController"; import { useNotificationsScroll } from "./hooks/useNotificationsScroll"; @@ -22,11 +22,12 @@ const NOTIFICATION_CAUSE_PRIORITY: Record = { [ApiNotificationCause.DropReacted]: 5, [ApiNotificationCause.WaveCreated]: 6, [ApiNotificationCause.AllDrops]: 7, + [ApiNotificationCause.PriorityAlert]: 8, }; const compareNotificationCause = ( firstCause: ApiNotificationCause, - secondCause: ApiNotificationCause, + secondCause: ApiNotificationCause ): number => NOTIFICATION_CAUSE_PRIORITY[firstCause] - NOTIFICATION_CAUSE_PRIORITY[secondCause]; @@ -52,7 +53,7 @@ export default function Notifications({ activeFilter?.cause ? [...activeFilter.cause].sort(compareNotificationCause).join("|") : "notifications-filter-all", - [activeFilter], + [activeFilter] ); const { scrollContainerRef, handleScroll } = useNotificationsScroll({ diff --git a/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx b/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx new file mode 100644 index 0000000000..19a76d74ba --- /dev/null +++ b/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx @@ -0,0 +1,120 @@ +"use client"; + +import Drop, { + DropInteractionParams, + DropLocation, +} from "@/components/waves/drops/Drop"; +import { ApiDrop } from "@/generated/models/ApiDrop"; +import { getTimeAgoShort } from "@/helpers/Helpers"; +import { getScaledImageUri, ImageScale } from "@/helpers/image.helpers"; +import { getWaveRoute } from "@/helpers/navigation.helpers"; +import { DropSize, ExtendedDrop } from "@/helpers/waves/drop.helpers"; +import useDeviceInfo from "@/hooks/useDeviceInfo"; +import { ActiveDropState } from "@/types/dropInteractionTypes"; +import { INotificationPriorityAlert } from "@/types/feed.types"; +import Link from "next/link"; +import { useRouter } from "next/navigation"; + +export default function NotificationPriorityAlert({ + notification, + activeDrop, + onReply, + onQuote, + onDropContentClick, +}: { + readonly notification: INotificationPriorityAlert; + readonly activeDrop: ActiveDropState | null; + readonly onReply: (param: DropInteractionParams) => void; + readonly onQuote: (param: DropInteractionParams) => void; + readonly onDropContentClick?: (drop: ExtendedDrop) => void; +}) { + const router = useRouter(); + const { isApp } = useDeviceInfo(); + const baseWave = notification.related_drops[0].wave as any; + const isDirectMessage = + baseWave?.chat?.scope?.group?.is_direct_message ?? false; + + const onReplyClick = (serialNo: number) => { + router.push( + getWaveRoute({ + waveId: notification.related_drops[0].wave.id, + serialNo, + isDirectMessage, + isApp, + }) + ); + }; + + const onQuoteClick = (quote: ApiDrop) => { + const quoteWave = quote.wave as any; + const quoteIsDm = + quoteWave?.chat?.scope?.group?.is_direct_message ?? isDirectMessage; + + router.push( + getWaveRoute({ + waveId: quote.wave.id, + serialNo: quote.serial_no, + isDirectMessage: quoteIsDm, + isApp, + }) + ); + }; + + return ( +
+
+
+
+ {notification.related_identity.pfp ? ( + # + ) : ( +
+ )} +
+ + + {notification.related_identity.handle} + {" "} + sent a priority alert 🚨{" "} + + + • + {" "} + {getTimeAgoShort(notification.created_at)} + + +
+ + +
+
+ ); +} diff --git a/generated/models/ApiNotificationCause.ts b/generated/models/ApiNotificationCause.ts index 5ed871dcfd..32a621400e 100644 --- a/generated/models/ApiNotificationCause.ts +++ b/generated/models/ApiNotificationCause.ts @@ -20,5 +20,6 @@ export enum ApiNotificationCause { DropVoted = 'DROP_VOTED', DropReacted = 'DROP_REACTED', WaveCreated = 'WAVE_CREATED', - AllDrops = 'ALL_DROPS' + AllDrops = 'ALL_DROPS', + PriorityAlert = 'PRIORITY_ALERT' } diff --git a/openapi.yaml b/openapi.yaml index 2021f45a79..11f54f8c01 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -5563,6 +5563,7 @@ components: - DROP_REACTED - WAVE_CREATED - ALL_DROPS + - PRIORITY_ALERT ApiNotificationsResponse: type: object required: diff --git a/types/feed.types.ts b/types/feed.types.ts index 365fbb1ed0..041d6b0297 100644 --- a/types/feed.types.ts +++ b/types/feed.types.ts @@ -132,6 +132,16 @@ export type INotificationAllDrops = { }; }; +export type INotificationPriorityAlert = { + readonly id: number; + readonly cause: ApiNotificationCause.PriorityAlert; + readonly created_at: number; + readonly read_at: number | null; + readonly related_identity: ApiProfileMin; + readonly related_drops: Array; + readonly additional_context: any; +}; + export type TypedNotification = | INotificationIdentitySubscribed | INotificationIdentityMentioned @@ -140,7 +150,8 @@ export type TypedNotification = | INotificationDropQuoted | INotificationDropReplied | INotificationWaveCreated - | INotificationAllDrops; + | INotificationAllDrops + | INotificationPriorityAlert; export interface TypedNotificationsResponse extends Omit { From b557b938b2d2ececf4ab2c799989827f5943d7ba Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 12 Nov 2025 13:31:51 +0200 Subject: [PATCH 2/4] WIP Signed-off-by: prxt6529 --- .../item/NotificationItem.test.tsx | 6 ++ .../NotificationPriorityAlert.test.tsx | 99 +++++++++++++++++++ .../NotificationPriorityAlert.tsx | 88 ++++++++++++----- 3 files changed, 171 insertions(+), 22 deletions(-) create mode 100644 __tests__/components/brain/notifications/priority-alert/NotificationPriorityAlert.test.tsx diff --git a/__tests__/components/brain/notifications/item/NotificationItem.test.tsx b/__tests__/components/brain/notifications/item/NotificationItem.test.tsx index 90cdf4a329..12104112e3 100644 --- a/__tests__/components/brain/notifications/item/NotificationItem.test.tsx +++ b/__tests__/components/brain/notifications/item/NotificationItem.test.tsx @@ -5,6 +5,7 @@ import { ApiNotificationCause } from '@/generated/models/ApiNotificationCause'; jest.mock('@/components/brain/notifications/drop-quoted/NotificationDropQuoted', () => ({ __esModule: true, default: () =>
})); jest.mock('@/components/brain/notifications/drop-replied/NotificationDropReplied', () => ({ __esModule: true, default: () =>
})); +jest.mock('@/components/brain/notifications/priority-alert/NotificationPriorityAlert', () => ({ __esModule: true, default: () =>
})); describe('NotificationItem', () => { const base = { id: '1' } as any; @@ -17,4 +18,9 @@ describe('NotificationItem', () => { render(); expect(screen.getByTestId('replied')).toBeInTheDocument(); }); + + it('renders priority alert component', () => { + render(); + expect(screen.getByTestId('priority-alert')).toBeInTheDocument(); + }); }); diff --git a/__tests__/components/brain/notifications/priority-alert/NotificationPriorityAlert.test.tsx b/__tests__/components/brain/notifications/priority-alert/NotificationPriorityAlert.test.tsx new file mode 100644 index 0000000000..e15e2275ab --- /dev/null +++ b/__tests__/components/brain/notifications/priority-alert/NotificationPriorityAlert.test.tsx @@ -0,0 +1,99 @@ +import { render, screen } from "@testing-library/react"; +import React from "react"; +import NotificationPriorityAlert from "@/components/brain/notifications/priority-alert/NotificationPriorityAlert"; +import { useRouter } from "next/navigation"; + +jest.mock("next/navigation", () => ({ + useRouter: jest.fn(() => ({ push: jest.fn() })), + useSearchParams: jest.fn(), + usePathname: jest.fn(), +})); + +const DropMock = jest.fn(() =>
); +jest.mock("@/components/waves/drops/Drop", () => ({ + __esModule: true, + default: (props: any) => { + DropMock(props); + return
; + }, + DropLocation: { + MY_STREAM: "MY_STREAM", + WAVE: "WAVE", + }, +})); + +jest.mock("@/hooks/useDeviceInfo", () => ({ + __esModule: true, + default: jest.fn(() => ({ isApp: false })), +})); + +const baseNotification: any = { + id: 1, + cause: "PRIORITY_ALERT" as any, + related_identity: { handle: "alice", pfp: null }, + related_drops: [ + { + id: "d", + wave: { id: "w" }, + author: { handle: "alice" }, + serial_no: 1, + parts: [], + metadata: [], + }, + ], + additional_context: {}, + created_at: 1, + read_at: null, +}; + +describe("NotificationPriorityAlert", () => { + it("renders priority alert notification with drop", () => { + render( + + ); + expect(screen.getByText("alice")).toBeInTheDocument(); + expect(screen.getByText(/sent a priority alert/)).toBeInTheDocument(); + expect(screen.getByTestId("drop")).toBeInTheDocument(); + }); + + it("renders notification header when related_drops is empty", () => { + const notificationWithoutDrops = { + ...baseNotification, + related_drops: [], + }; + render( + + ); + expect(screen.getByText("alice")).toBeInTheDocument(); + expect(screen.getByText(/sent a priority alert/)).toBeInTheDocument(); + expect(screen.queryByTestId("drop")).not.toBeInTheDocument(); + }); + + it("uses router in reply and quote handlers", () => { + render( + + ); + const props = DropMock.mock.calls[0][0]; + props.onReplyClick(5); + props.onQuoteClick({ wave: { id: "w" }, serial_no: 6 } as any); + const router = (useRouter as jest.Mock).mock.results[0].value; + expect(router.push).toHaveBeenCalledWith("/waves?wave=w&serialNo=5/"); + expect(router.push).toHaveBeenCalledWith("/waves?wave=w&serialNo=6/"); + }); +}); + diff --git a/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx b/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx index 19a76d74ba..a84b1be711 100644 --- a/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx +++ b/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx @@ -30,14 +30,56 @@ export default function NotificationPriorityAlert({ }) { const router = useRouter(); const { isApp } = useDeviceInfo(); - const baseWave = notification.related_drops[0].wave as any; + + if (!notification.related_drops || notification.related_drops.length === 0) { + return ( +
+
+
+
+ {notification.related_identity.pfp ? ( + # + ) : ( +
+ )} +
+ + + {notification.related_identity.handle} + {" "} + sent a priority alert 🚨{" "} + + + • + {" "} + {getTimeAgoShort(notification.created_at)} + + +
+
+
+ ); + } + + const baseWave = notification.related_drops[0]?.wave as any; const isDirectMessage = baseWave?.chat?.scope?.group?.is_direct_message ?? false; const onReplyClick = (serialNo: number) => { + const firstDrop = notification.related_drops[0]; + if (!firstDrop?.wave?.id) return; router.push( getWaveRoute({ - waveId: notification.related_drops[0].wave.id, + waveId: firstDrop.wave.id, serialNo, isDirectMessage, isApp, @@ -94,26 +136,28 @@ export default function NotificationPriorityAlert({
- + {notification.related_drops[0] && ( + + )}
); From 18d1fce0fa3fd9a0dde44adf368536cadae6c15c Mon Sep 17 00:00:00 2001 From: prxt6529 Date: Wed, 12 Nov 2025 13:46:56 +0200 Subject: [PATCH 3/4] WIP Signed-off-by: prxt6529 --- .../NotificationPriorityAlert.tsx | 95 +++++++------------ 1 file changed, 35 insertions(+), 60 deletions(-) diff --git a/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx b/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx index a84b1be711..7b6ef6aa8d 100644 --- a/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx +++ b/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx @@ -31,40 +31,44 @@ export default function NotificationPriorityAlert({ const router = useRouter(); const { isApp } = useDeviceInfo(); + const headerSection = ( +
+
+ {notification.related_identity.pfp ? ( + # + ) : ( +
+ )} +
+ + + {notification.related_identity.handle} + {" "} + sent a priority alert 🚨{" "} + + + • + {" "} + {getTimeAgoShort(notification.created_at)} + + +
+ ); + if (!notification.related_drops || notification.related_drops.length === 0) { return (
-
-
- {notification.related_identity.pfp ? ( - # - ) : ( -
- )} -
- - - {notification.related_identity.handle} - {" "} - sent a priority alert 🚨{" "} - - - • - {" "} - {getTimeAgoShort(notification.created_at)} - - -
+ {headerSection}
); @@ -105,36 +109,7 @@ export default function NotificationPriorityAlert({ return (
-
-
- {notification.related_identity.pfp ? ( - # - ) : ( -
- )} -
- - - {notification.related_identity.handle} - {" "} - sent a priority alert 🚨{" "} - - - • - {" "} - {getTimeAgoShort(notification.created_at)} - - -
+ {headerSection} {notification.related_drops[0] && ( Date: Wed, 12 Nov 2025 13:55:53 +0200 Subject: [PATCH 4/4] WIP Signed-off-by: prxt6529 --- .../NotificationPriorityAlert.test.tsx | 5 +-- .../NotificationPriorityAlert.tsx | 42 +++++++++---------- 2 files changed, 22 insertions(+), 25 deletions(-) diff --git a/__tests__/components/brain/notifications/priority-alert/NotificationPriorityAlert.test.tsx b/__tests__/components/brain/notifications/priority-alert/NotificationPriorityAlert.test.tsx index e15e2275ab..2b9e7c61f0 100644 --- a/__tests__/components/brain/notifications/priority-alert/NotificationPriorityAlert.test.tsx +++ b/__tests__/components/brain/notifications/priority-alert/NotificationPriorityAlert.test.tsx @@ -1,6 +1,5 @@ -import { render, screen } from "@testing-library/react"; -import React from "react"; import NotificationPriorityAlert from "@/components/brain/notifications/priority-alert/NotificationPriorityAlert"; +import { render, screen } from "@testing-library/react"; import { useRouter } from "next/navigation"; jest.mock("next/navigation", () => ({ @@ -88,6 +87,7 @@ describe("NotificationPriorityAlert", () => { onQuote={jest.fn()} /> ); + expect(DropMock).toHaveBeenCalled(); const props = DropMock.mock.calls[0][0]; props.onReplyClick(5); props.onQuoteClick({ wave: { id: "w" }, serial_no: 6 } as any); @@ -96,4 +96,3 @@ describe("NotificationPriorityAlert", () => { expect(router.push).toHaveBeenCalledWith("/waves?wave=w&serialNo=6/"); }); }); - diff --git a/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx b/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx index 7b6ef6aa8d..2bbbb7d94e 100644 --- a/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx +++ b/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx @@ -111,28 +111,26 @@ export default function NotificationPriorityAlert({
{headerSection} - {notification.related_drops[0] && ( - - )} +
);