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..2b9e7c61f0
--- /dev/null
+++ b/__tests__/components/brain/notifications/priority-alert/NotificationPriorityAlert.test.tsx
@@ -0,0 +1,98 @@
+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", () => ({
+ 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(
+
+ );
+ expect(DropMock).toHaveBeenCalled();
+ 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/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..2bbbb7d94e
--- /dev/null
+++ b/components/brain/notifications/priority-alert/NotificationPriorityAlert.tsx
@@ -0,0 +1,137 @@
+"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 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 (
+
+ );
+ }
+
+ 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: firstDrop.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 (
+
+
+ {headerSection}
+
+
+
+
+ );
+}
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 {