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 ( +
+
+ {headerSection} +
+
+ ); + } + + 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 {