Skip to content

Commit

Permalink
refactor: move notification types into content field (#2773)
Browse files Browse the repository at this point in the history
* cleanup: match on type `DbNotification` `convert_from_db_notification`

* fix: use `create` from superstruct for coersions

* cleanup/refactor: reduce notification type to shared fields with content property

* fix:test(ex): update tests to handle new notification structure

* fix(ts): update code to handle new notification structure

* fix:test(ts): update code to handle new notification structure
  • Loading branch information
firestack authored Sep 18, 2024
1 parent 310c727 commit 348e331
Show file tree
Hide file tree
Showing 32 changed files with 1,089 additions and 718 deletions.
30 changes: 17 additions & 13 deletions assets/src/components/modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import SavePresetModal from "./inputModals/savePresetModal"
import DeletePresetModal from "./inputModals/deletePresetModal"
import OverwritePresetModal from "./inputModals/overwritePresetModal"
import { usePanelStateFromStateDispatchContext } from "../hooks/usePanelState"
import { isBlockWaiverNotification, NotificationType } from "../realtime"

const Modal = (): ReactElement | null => {
const { connectionStatus } = useContext(SocketContext)
Expand All @@ -24,24 +25,27 @@ const Modal = (): ReactElement | null => {
return <DisconnectedModal />
}

if (selectedNotification && selectedVehicleOrGhost === null) {
return <InactiveNotificationModal notification={selectedNotification} />
}

if (
selectedNotification &&
selectedNotification.reason == "chelsea_st_bridge_raised"
isBlockWaiverNotification(selectedNotification) &&
selectedVehicleOrGhost === null
) {
return (
<ChelseaRaisedNotificationModal notification={selectedNotification} />
)
return <InactiveNotificationModal notification={selectedNotification} />
}

if (
selectedNotification &&
selectedNotification.reason == "chelsea_st_bridge_lowered"
) {
return <ChelseaLoweredNotificationModal />
if (selectedNotification?.content.$type === NotificationType.BridgeMovement) {
switch (selectedNotification.content.status) {
case "lowered": {
return <ChelseaLoweredNotificationModal />
}
case "raised": {
return (
<ChelseaRaisedNotificationModal
notification={selectedNotification.content}
/>
)
}
}
}

if (selectedNotification && selectedVehicleOrGhost === undefined) {
Expand Down
129 changes: 87 additions & 42 deletions assets/src/components/notificationCard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import React, { ReactElement } from "react"
import { useRoute, useRoutes } from "../contexts/routesContext"
import { Notification, NotificationReason } from "../realtime"
import { isChelseaBridgeNotification } from "../util/notifications"
import {
BlockWaiverNotification,
Notification,
BlockWaiverReason,
NotificationType,
isBlockWaiverNotification,
} from "../realtime"
import { Route } from "../schedule"
import { formattedTime } from "../util/dateTime"
import { CardBody, CardProperties, CardReadable } from "./card"
Expand All @@ -20,13 +25,19 @@ export const NotificationCard = ({
hideLatestNotification?: () => void
noFocusOrHover?: boolean
}): ReactElement<HTMLElement> => {
const routes = useRoutes(notification.routeIds)
const routeAtCreation = useRoute(notification.routeIdAtCreation)
const routes = useRoutes(
isBlockWaiverNotification(notification) ? notification.content.routeIds : []
)
const routeAtCreation = useRoute(
isBlockWaiverNotification(notification)
? notification.content.routeIdAtCreation
: null
)
const isUnread = notification.state === "unread"
return (
<CardReadable
currentTime={currentTime}
title={<>{title(notification.reason)}</>}
title={<>{title(notification)}</>}
style="kiwi"
isActive={isUnread}
openCallback={() => {
Expand All @@ -36,7 +47,7 @@ export const NotificationCard = ({
hideLatestNotification()
}

if (isChelseaBridgeNotification(notification.reason)) {
if (notification.content.$type === NotificationType.BridgeMovement) {
fullStoryEvent("User clicked Chelsea Bridge Notification", {})
}
}}
Expand All @@ -45,40 +56,55 @@ export const NotificationCard = ({
noFocusOrHover={noFocusOrHover}
>
<CardBody>{description(notification, routes, routeAtCreation)}</CardBody>
<CardProperties
properties={[
{
label: "Run",
value:
notification.runIds.length > 0
? notification.runIds.join(", ")
: null,
},
{
label: "Operator",
value:
notification.operatorName !== null &&
notification.operatorId !== null
? `${notification.operatorName} #${notification.operatorId}`
: null,
sensitive: true,
},
]}
/>
{isBlockWaiverNotification(notification) && (
<CardProperties
properties={[
{
label: "Run",
value:
notification.content.runIds.length > 0
? notification.content.runIds.join(", ")
: null,
},
{
label: "Operator",
value:
notification.content.operatorName !== null &&
notification.content.operatorId !== null
? `${notification.content.operatorName} #${notification.content.operatorId}`
: null,
sensitive: true,
},
]}
/>
)}
</CardReadable>
)
}

export const title = (reason: NotificationReason): string => {
export const title = (notification: Notification) => {
switch (notification.content.$type) {
case NotificationType.BlockWaiver: {
return blockWaiverNotificationTitle(notification.content.reason)
}
case NotificationType.BridgeMovement: {
switch (notification.content.status) {
case "lowered":
return "Chelsea St Bridge Lowered"
case "raised":
return "Chelsea St Bridge Raised"
}
}
}
}
export const blockWaiverNotificationTitle = (
reason: BlockWaiverReason
): string => {
switch (reason) {
case "manpower":
return "No Operator"
case "diverted":
return "Diversion"
case "chelsea_st_bridge_raised":
return "Chelsea St Bridge Raised"
case "chelsea_st_bridge_lowered":
return "Chelsea St Bridge Lowered"
default:
return reason
.replace("_", " ")
Expand All @@ -87,16 +113,16 @@ export const title = (reason: NotificationReason): string => {
.join(" ")
}
}

const description = (
notification: Notification,
const blockWaiverDescription = (
notification: BlockWaiverNotification,
routes: Route[],
routeAtCreation: Route | null
): string => {
const routeNames = routes.map((route) => route.name).join(", ")
const routeNameAtCreation = routeAtCreation
? routeAtCreation.name
: notification.routeIdAtCreation

switch (notification.reason) {
case "manpower":
return `OCC reported that an operator is not available on the ${routeNames}.`
Expand All @@ -120,16 +146,35 @@ const description = (
return `OCC created a dispatcher note due to traffic on the ${
routeNameAtCreation || routeNames
}.`
case "chelsea_st_bridge_raised":
/* eslint-disable @typescript-eslint/no-non-null-assertion */
return `OCC reported that the Chelsea St bridge will be raised until ${formattedTime(
notification.endTime!
)}.`
/* eslint-enable @typescript-eslint/no-non-null-assertion */
case "chelsea_st_bridge_lowered":
return "OCC reported that the Chelsea St bridge has been lowered."
case "other":
default:
return `OCC created a dispatcher note for the ${routeNames}.`
}
}

const description = (
notification: Notification,
routes: Route[],
routeAtCreation: Route | null
): string => {
switch (notification.content.$type) {
case NotificationType.BlockWaiver: {
return blockWaiverDescription(
notification.content,
routes,
routeAtCreation
)
}
case NotificationType.BridgeMovement: {
switch (notification.content.status) {
case "raised":
return `OCC reported that the Chelsea St bridge will be raised until ${formattedTime(
notification.content.loweringTime
)}.`

case "lowered":
return "OCC reported that the Chelsea St bridge has been lowered."
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import React from "react"
import { Notification } from "../../realtime"
import { BridgeRaisedNotification } from "../../realtime"
import { formattedTime } from "../../util/dateTime"
import BasicNotificationModal from "./basicNotificationModal"

const ChelseaRaisedNotificationModal = ({
notification,
}: {
notification: Notification
notification: BridgeRaisedNotification
}) => {
const contentString = (endDate: Date | null): string => {
if (endDate)
Expand All @@ -21,7 +21,7 @@ const ChelseaRaisedNotificationModal = ({
return (
<BasicNotificationModal
title="Chelsea St Bridge Raised"
body={contentString(notification.endTime)}
body={contentString(notification.loweringTime)}
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from "react"
import Loading from "../loading"
import { useMinischeduleRuns } from "../../hooks/useMinischedule"
import { Activity, Run, Piece, Trip } from "../../minischedule"
import { Notification, RunId } from "../../realtime"
import { BlockWaiverNotification, Notification, RunId } from "../../realtime"
import { now, serviceDaySeconds } from "../../util/dateTime"
import { title } from "../notificationCard"
import BasicNotificationModal from "./basicNotificationModal"
Expand All @@ -12,10 +12,10 @@ type RunScheduleRelationship = "current" | "break" | "past"
const InactiveNotificationModal = ({
notification,
}: {
notification: Notification
notification: Notification<BlockWaiverNotification>
}) => {
const runs: (Run | null)[] | undefined = useMinischeduleRuns(
notification.tripIds
notification.content.tripIds
)

if (runs !== undefined) {
Expand All @@ -31,8 +31,8 @@ const InactiveNotificationModal = ({

return (
<BasicNotificationModal
title={title(notification.reason) + " NOTIFICATION"}
body={bodyCopy(notification, uniqueRuns)}
title={title(notification) + " NOTIFICATION"}
body={bodyCopy(notification.content, uniqueRuns)}
/>
)
}
Expand Down Expand Up @@ -104,7 +104,10 @@ const runScheduleRelationshipForRuns = (
return ["current", null]
}

const bodyCopy = (notification: Notification, runs: (Run | null)[]): string => {
const bodyCopy = (
notification: BlockWaiverNotification,
runs: (Run | null)[]
): string => {
if (notification.runIds.length === 0) {
return "Sorry, there's no additional information that we can provide you about this notification."
}
Expand All @@ -123,7 +126,9 @@ const bodyCopy = (notification: Notification, runs: (Run | null)[]): string => {
}
}

const pastNotificationBodyCopy = (notification: Notification): string => {
const pastNotificationBodyCopy = (
notification: BlockWaiverNotification
): string => {
if (notification.runIds.length === 1) {
return `Sorry, we can't show you details for run ${notification.runIds[0]} because nobody is logged into it.`
}
Expand All @@ -133,7 +138,9 @@ const pastNotificationBodyCopy = (notification: Notification): string => {
)} because nobody is logged into them.`
}

const futureNotificationBodyCopy = (notification: Notification): string => {
const futureNotificationBodyCopy = (
notification: BlockWaiverNotification
): string => {
if (notification.runIds.length === 1) {
return `Run ${notification.runIds[0]} is upcoming and not yet active in Skate. Please check back later to see details for this run.`
}
Expand Down
10 changes: 8 additions & 2 deletions assets/src/contexts/notificationsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ import useNotificationsReducer, {
} from "../hooks/useNotificationsReducer"
import useSocket from "../hooks/useSocket"
import useVehicleForNotification from "../hooks/useVehicleForNotification"
import { NotificationId, NotificationState } from "../realtime"
import {
isBlockWaiverNotification,
NotificationId,
NotificationState,
} from "../realtime"
import { selectVehicleFromNotification } from "../state/pagePanelState"
import { StateDispatchContext } from "./stateDispatchContext"

Expand Down Expand Up @@ -75,7 +79,9 @@ export const NotificationsProvider = ({

const { socket } = useSocket()
const vehicleForNotification = useVehicleForNotification(
selectedNotification,
selectedNotification && isBlockWaiverNotification(selectedNotification)
? selectedNotification
: undefined,
socket
)

Expand Down
5 changes: 2 additions & 3 deletions assets/src/hooks/useChannel.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Channel, Socket } from "phoenix"
import { useCallback, useEffect, useState } from "react"
import { assert, Struct, StructError } from "superstruct"
import { create, Struct, StructError } from "superstruct"
import { reload } from "../models/browser"
import * as Sentry from "@sentry/react"

Expand Down Expand Up @@ -122,8 +122,7 @@ export const useCheckedTwoWayChannel = <T, U, V>({
const onOk = useCallback(
({ data: data }: { data: unknown }) => {
try {
assert(data, dataStruct)
setState(parser(data))
setState(parser(create(data, dataStruct)))
} catch (error) {
if (error instanceof StructError) {
Sentry.captureException(error)
Expand Down
8 changes: 5 additions & 3 deletions assets/src/hooks/useNotificationsReducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import { putNotificationReadState } from "../api"
import { otherNotificationReadState } from "../contexts/notificationsContext"
import { SocketContext } from "../contexts/socketContext"
import { tagManagerEvent } from "../helpers/googleTagManager"
import { Notification } from "../realtime"
import { isChelseaBridgeNotification } from "../util/notifications"
import { Notification, NotificationType } from "../realtime"
import {
InitialNotifications,
NewNotification,
Expand Down Expand Up @@ -229,7 +228,10 @@ export const useNotificationsReducer = (

if (latestMessage.type === "new") {
tagManagerEvent("notification_delivered")
if (isChelseaBridgeNotification(latestMessage.payload.reason)) {
if (
latestMessage.payload.content.$type ===
NotificationType.BridgeMovement
) {
fullStoryEvent("User was Delivered a Chelsea Bridge Notification", {})
} else {
fullStoryEvent("User was Delivered a Notification", {})
Expand Down
Loading

0 comments on commit 348e331

Please sign in to comment.