From ef6fef956eb1b71f11947029a6d3032524e2b2a2 Mon Sep 17 00:00:00 2001 From: Moise Lubwimi Date: Wed, 20 Sep 2023 17:24:27 +0100 Subject: [PATCH 1/8] fix: convert credit pills to notifications --- packages/client/src/client/App.tsx | 7 - .../Credit/CreditRfqForm/CreditRfqForm.tsx | 29 ++- .../CreditRfqs/CreditRfqConfirmation.tsx | 159 ----------------- .../App/Credit/CreditRfqs/CreditRfqsCore.tsx | 16 +- .../client/App/LiveRates/LiveRatesCore.tsx | 26 ++- .../client/OpenFin/apps/Launcher/index.tsx | 8 +- .../src/client/notifications.finsemble.ts | 22 ++- .../src/client/notifications.openfin.ts | 168 +++++++++++++----- packages/client/src/client/notifications.ts | 18 +- .../client/src/client/notifications.web.ts | 86 ++++++--- .../client/src/client/notificationsUtils.ts | 20 ++- .../client/src/client/utils/formatNumber.ts | 2 + .../src/services/credit/creditRfqRequests.ts | 32 +++- packages/client/src/workspace/provider.ts | 6 +- 14 files changed, 327 insertions(+), 272 deletions(-) delete mode 100644 packages/client/src/client/App/Credit/CreditRfqs/CreditRfqConfirmation.tsx diff --git a/packages/client/src/client/App.tsx b/packages/client/src/client/App.tsx index 23ef6fa9d3..84b97b941e 100644 --- a/packages/client/src/client/App.tsx +++ b/packages/client/src/client/App.tsx @@ -6,10 +6,6 @@ import { checkTradingGatewayCompatibility } from "@/services/tradingGatewayCompa import { ENVIRONMENT } from "./constants" import { getMainApp } from "./main" -import { - registerCreditAcceptedNotifications, - registerFxNotifications, -} from "./notifications" import { GlobalScrollbarStyle, GlobalStyle, ThemeProvider } from "./theme" const MainApp = getMainApp() @@ -21,9 +17,6 @@ export async function initApp() { checkTradingGatewayCompatibility() - registerFxNotifications() - registerCreditAcceptedNotifications() - const container = document.getElementById("root") const root = createRoot(container as HTMLElement) diff --git a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx index 361a103dcb..7973aed9bf 100644 --- a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx +++ b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx @@ -1,7 +1,11 @@ -import { lazy, Suspense } from "react" +import { lazy, Suspense, useEffect } from "react" import styled from "styled-components" import { Loader } from "@/client/components/Loader" +import { + registerCreatedCreditNotification, + unregisterCreatedCreditNotification, +} from "@/client/notifications" const CreditRfqFormCore = lazy(() => import("./CreditRfqFormCore")) @@ -17,12 +21,21 @@ const loader = ( ) -export const CreditRfqForm = () => ( - - - {loader} - - -) +export const CreditRfqForm = () => { + useEffect(() => { + registerCreatedCreditNotification() + return () => { + unregisterCreatedCreditNotification() + } + }, []) + + return ( + + + {loader} + + + ) +} export default CreditRfqForm diff --git a/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqConfirmation.tsx b/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqConfirmation.tsx deleted file mode 100644 index 6e9ed3c8df..0000000000 --- a/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqConfirmation.tsx +++ /dev/null @@ -1,159 +0,0 @@ -import { bind } from "@react-rxjs/core" -import { createSignal } from "@react-rxjs/utils" -import { FaCheckCircle, FaTimes } from "react-icons/fa" -import { concat, of, race, timer } from "rxjs" -import { - filter, - map, - mapTo, - switchMap, - take, - withLatestFrom, -} from "rxjs/operators" -import styled from "styled-components" - -import { customNumberFormatter } from "@/client/utils" -import { AcceptedQuoteState, Direction } from "@/generated/TradingGateway" -import { - acceptedCreditRfq$, - createdCreditRfq$, - creditInstruments$, - creditQuotes$, - creditRfqsById$, -} from "@/services/credit" - -const ConfirmationPill = styled.div<{ direction: Direction }>` - position: absolute; - left: 50%; - transform: translateX(-50%); - background-color: ${({ theme, direction }) => - theme.colors.spectrum.uniqueCollections[direction].base}; - display: flex; - align-items: center; - padding: 0.5rem 1rem; - border-radius: 2rem; - font-size: 0.8rem; - color: ${({ theme }) => theme.white}; - & > svg { - margin-right: 5px; - } -` - -const IconWrapper = styled.div<{ direction: Direction }>` - display: flex; - justify-content: center; - align-items: center; - margin-left: 5px; - width: 1.5em; - height: 1.5em; - border-radius: 1.5em; - &:hover { - cursor: pointer; - background-color: ${({ theme, direction }) => - theme.colors.spectrum.uniqueCollections[direction].lighter}; - } -` - -// Dismiss Message -const DISMISS_TIMEOUT = 5_000 -const [dismiss$, onDismissMessage] = createSignal() -export { onDismissMessage } - -const [useRfqCreatedConfirmation] = bind( - createdCreditRfq$.pipe( - filter((response) => response !== null), - withLatestFrom(creditInstruments$), - map(([response, creditInstruments]) => ({ - ...response, - request: { - ...response.request, - instrument: - creditInstruments.find( - (instrument) => instrument.id === response.request.instrumentId, - ) ?? null, - }, - })), - switchMap((response) => - concat( - of(response), - race([dismiss$.pipe(take(1)), timer(DISMISS_TIMEOUT)]).pipe( - mapTo(null), - ), - ), - ), - ), - null, -) - -const formatter = customNumberFormatter() - -export const CreditRfqCreatedConfirmation = () => { - const confirmation = useRfqCreatedConfirmation() - - if (!confirmation) { - return null - } - - const { direction, dealerIds, quantity, instrument } = confirmation.request - - return confirmation ? ( - - You have sent an {direction} RFQ for {formatter(quantity)}{" "} - {instrument?.name} to {dealerIds.length} dealers - - - - - ) : null -} - -const [useRfqAcceptedConfirmation] = bind( - acceptedCreditRfq$.pipe( - withLatestFrom(creditQuotes$, creditRfqsById$), - map(([response, quotes, rfqsById]) => { - const quote = quotes.find((quote) => quote.id === response.quoteId) - const rfq = quote && rfqsById[quote.rfqId] - return { - ...response, - quote, - rfq, - dealer: rfq?.dealers.find((dealer) => dealer.id === quote?.dealerId), - instrument: rfq?.instrument, - } - }), - switchMap((response) => - concat( - of(response), - race([dismiss$.pipe(take(1)), timer(DISMISS_TIMEOUT)]).pipe( - mapTo(null), - ), - ), - ), - ), - null, -) - -export const CreditRfqAcceptedConfirmation = () => { - const confirmation = useRfqAcceptedConfirmation() - - if (!confirmation) { - return null - } - - const { rfq, quote, dealer, instrument } = confirmation - - if (!rfq || !quote || !dealer || !instrument) { - return null - } - - return confirmation ? ( - - - You have accepted a quote for {formatter(rfq.quantity)} {instrument.name}{" "} - @${(quote.state as AcceptedQuoteState).payload} from {dealer.name} - - - - - ) : null -} diff --git a/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx b/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx index 03c624aa47..a6324b17d8 100644 --- a/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx +++ b/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx @@ -3,16 +3,12 @@ import { useEffect } from "react" import styled from "styled-components" import { - registerCreditQuoteNotifications, - unregisterCreditQuoteNotifications, + registerCreditNotifications, + unregisterCreditNotifications, } from "@/client/notifications" import { WithChildren } from "@/client/utils/utilityTypes" import { CreditRfqCardGrid } from "./CreditRfqCards" -import { - CreditRfqAcceptedConfirmation, - CreditRfqCreatedConfirmation, -} from "./CreditRfqConfirmation" import { CreditRfqsHeader } from "./CreditRfqsHeader" const CreditRfqsCoreWrapper = styled.div` @@ -23,9 +19,11 @@ const CreditRfqsCoreWrapper = styled.div` ` const CreditRfqsCore = ({ children }: WithChildren) => { useEffect(() => { - registerCreditQuoteNotifications() + registerCreditNotifications() - return unregisterCreditQuoteNotifications + return () => { + unregisterCreditNotifications() + } }, []) return ( @@ -33,8 +31,6 @@ const CreditRfqsCore = ({ children }: WithChildren) => { - - ) diff --git a/packages/client/src/client/App/LiveRates/LiveRatesCore.tsx b/packages/client/src/client/App/LiveRates/LiveRatesCore.tsx index 30a6e44df1..a3c5c0cf6e 100644 --- a/packages/client/src/client/App/LiveRates/LiveRatesCore.tsx +++ b/packages/client/src/client/App/LiveRates/LiveRatesCore.tsx @@ -1,6 +1,11 @@ import { Subscribe } from "@react-rxjs/core" +import { useEffect } from "react" import { merge } from "rxjs" +import { + registerFxNotifications, + unregisterFxNotifications, +} from "@/client/notifications" import { WithChildren } from "@/client/utils/utilityTypes" import { MainHeader, mainHeader$ } from "./MainHeader" @@ -8,11 +13,20 @@ import { Tiles, tiles$ } from "./Tiles" export const liveRates$ = merge(tiles$, mainHeader$) -const LiveRates = ({ children }: WithChildren) => ( - - - - -) +const LiveRates = ({ children }: WithChildren) => { + useEffect(() => { + registerFxNotifications() + return () => { + unregisterFxNotifications() + } + }, []) + + return ( + + + + + ) +} export default LiveRates diff --git a/packages/client/src/client/OpenFin/apps/Launcher/index.tsx b/packages/client/src/client/OpenFin/apps/Launcher/index.tsx index 6b08d4c935..c718fa9d98 100644 --- a/packages/client/src/client/OpenFin/apps/Launcher/index.tsx +++ b/packages/client/src/client/OpenFin/apps/Launcher/index.tsx @@ -5,8 +5,8 @@ import Measure, { ContentRect } from "react-measure" import { Loader } from "@/client/components/Loader" import LogoIcon from "@/client/components/LogoIcon" import { - registerCreditQuoteNotifications, - unregisterCreditQuoteNotifications, + registerCreditNotifications, + unregisterCreditNotifications, } from "@/client/notifications" import { onResetInput, @@ -98,10 +98,10 @@ function Launcher() { useEffect(() => { setOverlay(overlayRef.current) getCurrentWindowBounds().then(setInitialBounds).catch(console.error) - registerCreditQuoteNotifications() + registerCreditNotifications() return () => { - unregisterCreditQuoteNotifications() + unregisterCreditNotifications() } }, []) diff --git a/packages/client/src/client/notifications.finsemble.ts b/packages/client/src/client/notifications.finsemble.ts index a2353302ee..e81449400b 100644 --- a/packages/client/src/client/notifications.finsemble.ts +++ b/packages/client/src/client/notifications.finsemble.ts @@ -1,3 +1,5 @@ +import { Subscription } from "rxjs" + import { ExecutionTrade } from "@/services/executions" import { executions$ } from "@/services/executions/executions" @@ -16,8 +18,10 @@ export function sendFxTradeNotification(executionTrade: ExecutionTrade) { window.FSBL.Clients.NotificationClient.notify([notification]) } +let executionSubscription: Subscription | null = null + export function registerFxNotifications() { - executions$.subscribe({ + executionSubscription = executions$.subscribe({ next: (executionTrade) => { sendFxTradeNotification(executionTrade) }, @@ -30,16 +34,26 @@ export function registerFxNotifications() { }) } +export function unregisterFxNotifications() { + if (executionSubscription) { + executionSubscription.unsubscribe() + } +} + // TODO (4823) implement these for Finsemble when upgrading -export function registerCreditAcceptedNotifications() { +export function registerCreditNotifications() { + // no-op +} + +export function unregisterCreditNotifications() { // no-op } -export function registerCreditQuoteNotifications() { +export function registerCreatedCreditNotification() { // no-op } -export function unregisterCreditQuoteNotifications() { +export function unregisterCreatedCreditNotification() { // no-op } diff --git a/packages/client/src/client/notifications.openfin.ts b/packages/client/src/client/notifications.openfin.ts index fb8ee5efea..0a1a53b860 100644 --- a/packages/client/src/client/notifications.openfin.ts +++ b/packages/client/src/client/notifications.openfin.ts @@ -19,6 +19,8 @@ import { import { Direction } from "@/generated/TradingGateway" import { acceptedRfqWithQuote$, + ConfirmCreatedCreditRfq, + createdCreditConfirmation$, lastQuoteReceived$, PricedQuoteDetails, RfqWithPricedQuote, @@ -27,8 +29,9 @@ import { executions$, ExecutionTrade } from "@/services/executions" import { setCreditRfqCardHighlight } from "./App/Credit/CreditRfqs/CreditRfqCards" import { - processCreditAccepted, processCreditQuote, + processCreditRfqAccepted, + processCreditRfqCreated, processFxExecution, } from "./notificationsUtils" import { constructUrl } from "./utils/constructUrl" @@ -146,7 +149,7 @@ const sendFxTradeNotification = (trade: ExecutionTrade) => { } const sendQuoteAcceptedNotification = ({ rfq, quote }: RfqWithPricedQuote) => { - const { title, tradeDetails } = processCreditAccepted(rfq, quote) + const { title, tradeDetails } = processCreditRfqAccepted(rfq, quote) const notificationOptions: TemplateCustom = { template: "custom", @@ -215,6 +218,46 @@ const sendCreditQuoteNotification = (quote: PricedQuoteDetails) => { create(notificationOptions) } +const sendRFQCreatedConfirmationNotification = ( + rfqRequestConfirmation: ConfirmCreatedCreditRfq, +) => { + const { title, rfqDetails } = processCreditRfqCreated(rfqRequestConfirmation) + + const notificationOptions: TemplateCustom = { + template: "custom", + templateOptions: createNotificationTemplate( + rfqRequestConfirmation.request.direction, + ), + templateData: { + messageTradeDirection: + rfqRequestConfirmation.request.direction.toUpperCase(), + messageTradeDetails: rfqDetails, + }, + + icon: creditIconUrl, + category: "RFQ Created", + title, + indicator: { + text: "new rfq", + color: IndicatorColor.GRAY, + }, + buttons: [ + { + title: "View RFQ", + iconUrl: creditIconUrl, + onClick: { + task: TASK_HIGHLIGHT_CREDIT_RFQ, + payload: { + rfqId: rfqRequestConfirmation.rfqId, + }, + }, + }, + ], + } + + create(notificationOptions) +} + const TOPIC_HIGHLIGHT_FX_BLOTTER = "highlight-fx-blotter" export const TOPIC_HIGHLIGHT_CREDIT_RFQ = "highlight-credit-rfq" export const TOPIC_HIGHLIGHT_CREDIT_BLOTTER = "highlight-credit-blotter" @@ -253,6 +296,8 @@ export const handleHighlightRfqAction = (event: NotificationActionEvent) => { export type NotificationActionHandler = (event: NotificationActionEvent) => void let areFxNotificationsRegistered = false +let executionSubscription: Subscription | null = null + export const registerFxNotifications = ( handler?: NotificationActionHandler, ) => { @@ -270,7 +315,7 @@ export const registerFxNotifications = ( handler || handleHighlightFxBlotterAction, ) - executions$.subscribe({ + executionSubscription = executions$.subscribe({ next: (executionTrade) => { sendFxTradeNotification(executionTrade) }, @@ -284,13 +329,65 @@ export const registerFxNotifications = ( } } -let areCreditQuoteNotificationsRegistered = false -export const registerCreditQuoteNotifications = ( +export const unregisterFxNotifications = () => { + if (executionSubscription) { + areFxNotificationsRegistered = false + executionSubscription.unsubscribe() + } +} + +const registerCreditQuoteNotifications = ( handler?: NotificationActionHandler, ) => { - if (!areCreditQuoteNotificationsRegistered) { - areCreditQuoteNotificationsRegistered = true + fin.InterApplicationBus.subscribe( + { uuid: "*" }, + TOPIC_HIGHLIGHT_CREDIT_RFQ, + (message: { rfqId: number }) => { + setCreditRfqCardHighlight(message.rfqId) + }, + ) + + addEventListener("notification-action", handler || handleHighlightRfqAction) + quotesReceivedSubscription = lastQuoteReceived$.subscribe({ + next: (quote) => { + sendCreditQuoteNotification(quote) + }, + error: (e) => { + console.error(e) + }, + complete: () => { + console.error("credit quote notifications stream completed!?") + }, + }) +} + +let acceptedRfqWithQuoteSubscription: Subscription | null = null +const registerCreditAcceptedNotifications = ( + handler?: NotificationActionHandler, +) => { + fin.InterApplicationBus.subscribe( + { uuid: "*" }, + TOPIC_HIGHLIGHT_CREDIT_BLOTTER, + (message: { tradeId: number }) => { + setCreditTradeRowHighlight(message.tradeId) + }, + ) + + addEventListener( + "notification-action", + handler || handleHighlightCreditBlotterAction, + ) + + acceptedRfqWithQuoteSubscription = acceptedRfqWithQuote$.subscribe((rfq) => { + sendQuoteAcceptedNotification(rfq) + }) +} + +let createdCreditSubscription: Subscription | null = null + +export const registerCreatedCreditNotification = () => { + if (createdCreditSubscription == null) { fin.InterApplicationBus.subscribe( { uuid: "*" }, TOPIC_HIGHLIGHT_CREDIT_RFQ, @@ -299,51 +396,40 @@ export const registerCreditQuoteNotifications = ( }, ) - addEventListener("notification-action", handler || handleHighlightRfqAction) + addEventListener("notification-action", handleHighlightRfqAction) - quotesReceivedSubscription = lastQuoteReceived$.subscribe({ - next: (quote) => { - sendCreditQuoteNotification(quote) - }, - error: (e) => { - console.error(e) + createdCreditSubscription = createdCreditConfirmation$.subscribe( + (rfqRquest) => { + sendRFQCreatedConfirmationNotification(rfqRquest) }, - complete: () => { - console.error("credit quote notifications stream completed!?") - }, - }) + ) } } -let areCreditAcceptedNotificationsRegistered = false -export const registerCreditAcceptedNotifications = ( +export const unregisterCreatedCreditNotification = () => { + if (createdCreditSubscription != null) { + createdCreditSubscription.unsubscribe() + } +} + +let areCreditNotificationsCreated = false +export const registerCreditNotifications = ( handler?: NotificationActionHandler, ) => { - if (!areCreditAcceptedNotificationsRegistered) { - areCreditAcceptedNotificationsRegistered = true - - fin.InterApplicationBus.subscribe( - { uuid: "*" }, - TOPIC_HIGHLIGHT_CREDIT_BLOTTER, - (message: { tradeId: number }) => { - setCreditTradeRowHighlight(message.tradeId) - }, - ) - - addEventListener( - "notification-action", - handler || handleHighlightCreditBlotterAction, - ) - - acceptedRfqWithQuote$.subscribe((rfq) => { - sendQuoteAcceptedNotification(rfq) - }) + if (!areCreditNotificationsCreated) { + registerCreditAcceptedNotifications(handler) + registerCreditQuoteNotifications(handler) + areCreditNotificationsCreated = true } } -export const unregisterCreditQuoteNotifications = () => { +export const unregisterCreditNotifications = () => { if (quotesReceivedSubscription) { - areCreditQuoteNotificationsRegistered = false quotesReceivedSubscription.unsubscribe() } + + if (acceptedRfqWithQuoteSubscription) { + acceptedRfqWithQuoteSubscription.unsubscribe() + } + areCreditNotificationsCreated = false } diff --git a/packages/client/src/client/notifications.ts b/packages/client/src/client/notifications.ts index 9b9aaa13a4..b162ababdf 100644 --- a/packages/client/src/client/notifications.ts +++ b/packages/client/src/client/notifications.ts @@ -2,14 +2,22 @@ export function registerFxNotifications(): Promise { return Promise.reject("Function should be implemented at platform level") } -export function registerCreditQuoteNotifications(): Promise { +export function unregisterFxNotifications(): Promise { return Promise.reject("Function should be implemented at platform level") } -export function unregisterCreditQuoteNotifications() { - new Error("Function should be implemented at platform level") +export function registerCreditNotifications(): Promise { + return Promise.reject("Function should be implemented at platform level") +} + +export function unregisterCreditNotifications(): Promise { + return Promise.reject("Function should be implemented at platform level") } -export function registerCreditAcceptedNotifications() { - // no-op by default; implemented for OpenFin +export function registerCreatedCreditNotification(): Promise { + return Promise.reject("Function should be implemented at platform level") +} + +export function unregisterCreatedCreditNotification(): Promise { + return Promise.reject("Function should be implemented at platform level") } diff --git a/packages/client/src/client/notifications.web.ts b/packages/client/src/client/notifications.web.ts index 6a5a6b446e..411429c85e 100644 --- a/packages/client/src/client/notifications.web.ts +++ b/packages/client/src/client/notifications.web.ts @@ -2,16 +2,18 @@ import { Subscription } from "rxjs" import { acceptedRfqWithQuote$, + ConfirmCreatedCreditRfq, + createdCreditConfirmation$, lastQuoteReceived$, PricedQuoteDetails, - QuoteDetails, RfqWithPricedQuote, } from "@/services/credit" import { executions$, ExecutionTrade } from "@/services/executions" import { - processCreditAccepted, processCreditQuote, + processCreditRfqAccepted, + processCreditRfqCreated, processFxExecution, } from "./notificationsUtils" import { constructUrl } from "./utils/constructUrl" @@ -33,7 +35,7 @@ const sendFxTradeNotification = (trade: ExecutionTrade) => { } const sendQuoteAcceptedNotification = ({ rfq, quote }: RfqWithPricedQuote) => { - const { title, tradeDetails } = processCreditAccepted(rfq, quote) + const { title, tradeDetails } = processCreditRfqAccepted(rfq, quote) const options: NotificationOptions = { body: `${rfq.direction} ${tradeDetails}`, @@ -44,6 +46,17 @@ const sendQuoteAcceptedNotification = ({ rfq, quote }: RfqWithPricedQuote) => { new Notification(title, options) } +const sendCreditCreatedNotification = (rfqCreate: ConfirmCreatedCreditRfq) => { + const { title, rfqDetails } = processCreditRfqCreated(rfqCreate) + const options: NotificationOptions = { + body: `You have sent a ${rfqCreate.request.direction} ${rfqDetails}`, + icon: creditIconUrl, + dir: "ltr", + } + + new Notification(title, options) +} + const sendCreditQuoteNotification = (quote: PricedQuoteDetails) => { const { title, tradeDetails } = processCreditQuote(quote) @@ -76,13 +89,14 @@ const notificationsGranted = () => } } }) +let executionSubscription: Subscription | null = null export async function registerFxNotifications() { try { await notificationsGranted() // send trade executed for this tab only (driven from executeTrade ACK) - executions$.subscribe({ + executionSubscription = executions$.subscribe({ next: (executionTrade) => { sendFxTradeNotification(executionTrade) }, @@ -98,12 +112,19 @@ export async function registerFxNotifications() { } } +export function unregisterFxNotifications() { + if (executionSubscription) { + executionSubscription.unsubscribe() + } +} + let quotesReceivedSubscription: Subscription | null = null +let acceptedRfqWithQuoteSubscription: Subscription | null = null +let createdCreditSubscription: Subscription | null = null -export async function registerCreditQuoteNotifications() { +export async function registerCreditNotifications() { try { await notificationsGranted() - // send quote alerts for every live tab connected to BE (driven from rfq update feed) quotesReceivedSubscription = lastQuoteReceived$.subscribe({ next: (quote) => { @@ -116,26 +137,51 @@ export async function registerCreditQuoteNotifications() { console.error("credit quote notifications stream completed!?") }, }) - } catch (_) { - console.log("Notification permission was not granted.") + + // send accepted quote for this tab only (driven from acceptQuote ACK) + acceptedRfqWithQuoteSubscription = acceptedRfqWithQuote$.subscribe({ + next: (rfqWithQuote) => { + sendQuoteAcceptedNotification(rfqWithQuote) + }, + error: (e) => { + console.error(e) + }, + complete: () => { + console.error("accepted Rfq notifications stream completed!?") + }, + }) + } catch (e) { + console.error(e) } } -export function unregisterCreditQuoteNotifications() { - if (quotesReceivedSubscription) { - quotesReceivedSubscription.unsubscribe() +export function registerCreatedCreditNotification() { + // send created rfq + createdCreditSubscription = createdCreditConfirmation$.subscribe({ + next: (createdRFQRequest) => { + sendCreditCreatedNotification(createdRFQRequest) + }, + error: (e) => { + console.error(e) + }, + complete: () => { + console.error("Created credit notifications stream completed!?") + }, + }) +} + +export function unregisterCreatedCreditNotification() { + if (createdCreditSubscription) { + createdCreditSubscription.unsubscribe() } } -export async function registerCreditAcceptedNotifications() { - try { - await notificationsGranted() +export function unregisterCreditNotifications() { + if (quotesReceivedSubscription) { + quotesReceivedSubscription.unsubscribe() + } - // send accepted quote for this tab only (driven from acceptQuote ACK) - acceptedRfqWithQuote$.subscribe((rfqWithQuote) => - sendQuoteAcceptedNotification(rfqWithQuote), - ) - } catch (e) { - console.error(e) + if (acceptedRfqWithQuoteSubscription) { + acceptedRfqWithQuoteSubscription.unsubscribe() } } diff --git a/packages/client/src/client/notificationsUtils.ts b/packages/client/src/client/notificationsUtils.ts index 6992b801dc..4b76f8b799 100644 --- a/packages/client/src/client/notificationsUtils.ts +++ b/packages/client/src/client/notificationsUtils.ts @@ -1,4 +1,8 @@ -import { PricedQuoteDetails, RfqDetails } from "@/services/credit" +import { + ConfirmCreatedCreditRfq, + PricedQuoteDetails, + RfqDetails, +} from "@/services/credit" import { ExecutionStatus, ExecutionTrade } from "@/services/executions" import { PricedQuoteBody } from "@/services/rfqs/types" @@ -19,7 +23,7 @@ export const processFxExecution = (executionTrade: ExecutionTrade) => { } } -export const processCreditAccepted = ( +export const processCreditRfqAccepted = ( rfq: RfqDetails, quote: PricedQuoteBody, ) => { @@ -40,3 +44,15 @@ export const processCreditQuote = (quote: PricedQuoteDetails) => { )} @ $${formatNumber(quote.state.payload)}`, } } + +export const processCreditRfqCreated = ({ + request, + rfqId, +}: ConfirmCreatedCreditRfq) => { + return { + title: `RFQ Created: RFQ ID ${rfqId}`, + rfqDetails: `RFQ for ${formatNumber(request.quantity)} ${ + request.instrument?.name + } to ${request.dealerIds.length} dealers`, + } +} diff --git a/packages/client/src/client/utils/formatNumber.ts b/packages/client/src/client/utils/formatNumber.ts index d8c1a6d896..8fbeb63f4e 100644 --- a/packages/client/src/client/utils/formatNumber.ts +++ b/packages/client/src/client/utils/formatNumber.ts @@ -228,3 +228,5 @@ const decimalRegExp = new RegExp(DECIMAL_SEPARATOR_REGEXP, "g") */ export const parseQuantity = (rawValue: string): number => Number(rawValue.replace(filterRegExp, "").replace(decimalRegExp, ".")) + +export const adjustUserCreditQuantity = (value: number): number => value * 1000 diff --git a/packages/client/src/services/credit/creditRfqRequests.ts b/packages/client/src/services/credit/creditRfqRequests.ts index 17dc0a5c0f..253671a6bf 100644 --- a/packages/client/src/services/credit/creditRfqRequests.ts +++ b/packages/client/src/services/credit/creditRfqRequests.ts @@ -1,13 +1,14 @@ import { createSignal } from "@react-rxjs/utils" import { filter, map, tap, withLatestFrom } from "rxjs/operators" -import { showRfqInSellSide } from "@/client/utils" +import { adjustUserCreditQuantity, showRfqInSellSide } from "@/client/utils" import { AcceptQuoteRequest, ACK_ACCEPT_QUOTE_RESPONSE, ACK_CREATE_RFQ_RESPONSE, CancelRfqRequest, CreateRfqRequest, + InstrumentBody, PassRequest, QuoteRequest, WorkflowService, @@ -15,6 +16,7 @@ import { import { PricedQuoteBody } from "../rfqs/types" import { adaptiveDealerId$ } from "./creditDealers" +import { creditInstruments$ } from "./creditInstruments" import { creditRfqsById$, RfqDetails } from "./creditRfqs" export interface CreatedCreditRfq { @@ -22,6 +24,14 @@ export interface CreatedCreditRfq { rfqId: number } +type ConfirmRfqRequest = CreateRfqRequest & { + instrument: InstrumentBody | null +} +export interface ConfirmCreatedCreditRfq + extends Omit { + request: ConfirmRfqRequest +} + export const [createdCreditRfq$, setCreatedCreditRfq] = createSignal() @@ -33,7 +43,7 @@ export const createCreditRfq$ = (request: CreateRfqRequest) => { direction, expirySecs, instrumentId, - quantity: quantity * 1000, + quantity: adjustUserCreditQuantity(quantity), }).pipe( tap((response) => { if (response.type === ACK_CREATE_RFQ_RESPONSE) { @@ -43,6 +53,24 @@ export const createCreditRfq$ = (request: CreateRfqRequest) => { ) } +export const createdCreditConfirmation$ = createdCreditRfq$.pipe( + filter((response) => response !== null), + withLatestFrom(creditInstruments$), + map( + ([response, creditInstruments]): ConfirmCreatedCreditRfq => ({ + ...response, + request: { + ...response.request, + quantity: adjustUserCreditQuantity(response.request.quantity), + instrument: + creditInstruments.find( + (instrument) => instrument.id === response.request.instrumentId, + ) ?? null, + }, + }), + ), +) + const sellSideRfqs$ = createdCreditRfq$.pipe( withLatestFrom(adaptiveDealerId$), filter( diff --git a/packages/client/src/workspace/provider.ts b/packages/client/src/workspace/provider.ts index 34ae484a4a..9cec63bc71 100644 --- a/packages/client/src/workspace/provider.ts +++ b/packages/client/src/workspace/provider.ts @@ -1,8 +1,7 @@ import { init as workspacePlatformInit } from "@openfin/workspace-platform" import { - registerCreditAcceptedNotifications, - registerCreditQuoteNotifications, + registerCreditNotifications, registerFxNotifications, } from "@/client/notifications.openfin" import { initConnection } from "@/services/connection" @@ -51,8 +50,7 @@ async function init() { await showHome() registerFxNotifications(handleFxTradeNotification) - registerCreditQuoteNotifications(handleCreditRfqNotification) - registerCreditAcceptedNotifications() + registerCreditNotifications(handleCreditRfqNotification) const sub = registerSimulatedDealerResponses() From 8185e766ea7cea2ed5952d02756827e10b8502d8 Mon Sep 17 00:00:00 2001 From: Alan Greasley Date: Fri, 29 Sep 2023 13:31:26 +0100 Subject: [PATCH 2/8] chore: fx and credit notifications refactor --- .../Credit/CreditRfqForm/CreditRfqForm.tsx | 29 +- .../CreditRfqForm/CreditRfqFormCore.tsx | 11 +- .../App/Credit/CreditRfqs/CreditRfqsCore.tsx | 8 +- .../client/App/LiveRates/LiveRatesCore.tsx | 8 +- .../client/OpenFin/apps/Launcher/index.tsx | 20 +- .../src/client/notifications.finsemble.ts | 26 +- .../src/client/notifications.openfin.ts | 264 ++++++++++-------- packages/client/src/client/notifications.ts | 20 +- .../client/src/client/notifications.web.ts | 129 +++++---- .../client/src/client/notificationsUtils.ts | 38 +-- .../src/workspace/home/notifications.ts | 9 +- packages/client/src/workspace/provider.ts | 16 +- 12 files changed, 326 insertions(+), 252 deletions(-) diff --git a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx index 7973aed9bf..361a103dcb 100644 --- a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx +++ b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx @@ -1,11 +1,7 @@ -import { lazy, Suspense, useEffect } from "react" +import { lazy, Suspense } from "react" import styled from "styled-components" import { Loader } from "@/client/components/Loader" -import { - registerCreatedCreditNotification, - unregisterCreatedCreditNotification, -} from "@/client/notifications" const CreditRfqFormCore = lazy(() => import("./CreditRfqFormCore")) @@ -21,21 +17,12 @@ const loader = ( ) -export const CreditRfqForm = () => { - useEffect(() => { - registerCreatedCreditNotification() - return () => { - unregisterCreatedCreditNotification() - } - }, []) - - return ( - - - {loader} - - - ) -} +export const CreditRfqForm = () => ( + + + {loader} + + +) export default CreditRfqForm diff --git a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx index 3a359629c2..920affe38b 100644 --- a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx +++ b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx @@ -2,6 +2,10 @@ import { Subscribe } from "@react-rxjs/core" import { useEffect } from "react" import styled from "styled-components" +import { + registerCreditRfqCreatedNotifications, + unregisterCreditRfqCreatedNotifications, +} from "@/client/notifications" import { WithChildren } from "@/client/utils/utilityTypes" import { registerSimulatedDealerResponses } from "@/services/credit/creditRfqResponses" @@ -60,9 +64,14 @@ const CreditRfqFooter = styled.footer` const CreditRfqFormCore = ({ children }: WithChildren) => { useEffect(() => { const subscription = registerSimulatedDealerResponses() + registerCreditRfqCreatedNotifications() - return () => subscription.unsubscribe() + return () => { + subscription.unsubscribe() + unregisterCreditRfqCreatedNotifications() + } }, []) + return ( diff --git a/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx b/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx index a6324b17d8..5b466b7b04 100644 --- a/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx +++ b/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx @@ -3,8 +3,8 @@ import { useEffect } from "react" import styled from "styled-components" import { - registerCreditNotifications, - unregisterCreditNotifications, + registerCreditQuoteReceivedNotifications, + unregisterCreditQuoteReceivedNotifications, } from "@/client/notifications" import { WithChildren } from "@/client/utils/utilityTypes" @@ -19,10 +19,10 @@ const CreditRfqsCoreWrapper = styled.div` ` const CreditRfqsCore = ({ children }: WithChildren) => { useEffect(() => { - registerCreditNotifications() + registerCreditQuoteReceivedNotifications() return () => { - unregisterCreditNotifications() + unregisterCreditQuoteReceivedNotifications() } }, []) diff --git a/packages/client/src/client/App/LiveRates/LiveRatesCore.tsx b/packages/client/src/client/App/LiveRates/LiveRatesCore.tsx index a3c5c0cf6e..b40b31c7f2 100644 --- a/packages/client/src/client/App/LiveRates/LiveRatesCore.tsx +++ b/packages/client/src/client/App/LiveRates/LiveRatesCore.tsx @@ -3,8 +3,8 @@ import { useEffect } from "react" import { merge } from "rxjs" import { - registerFxNotifications, - unregisterFxNotifications, + registerFxTradeNotifications, + unregisterFxTradeNotifications, } from "@/client/notifications" import { WithChildren } from "@/client/utils/utilityTypes" @@ -15,9 +15,9 @@ export const liveRates$ = merge(tiles$, mainHeader$) const LiveRates = ({ children }: WithChildren) => { useEffect(() => { - registerFxNotifications() + registerFxTradeNotifications() return () => { - unregisterFxNotifications() + unregisterFxTradeNotifications() } }, []) diff --git a/packages/client/src/client/OpenFin/apps/Launcher/index.tsx b/packages/client/src/client/OpenFin/apps/Launcher/index.tsx index c718fa9d98..3b195a494b 100644 --- a/packages/client/src/client/OpenFin/apps/Launcher/index.tsx +++ b/packages/client/src/client/OpenFin/apps/Launcher/index.tsx @@ -5,8 +5,14 @@ import Measure, { ContentRect } from "react-measure" import { Loader } from "@/client/components/Loader" import LogoIcon from "@/client/components/LogoIcon" import { - registerCreditNotifications, - unregisterCreditNotifications, + registerCreditQuoteAcceptedNotifications, + registerCreditQuoteReceivedNotifications, + registerCreditRfqCreatedNotifications, + registerFxTradeNotifications, + unregisterCreditQuoteAcceptedNotifications, + unregisterCreditQuoteReceivedNotifications, + unregisterCreditRfqCreatedNotifications, + unregisterFxTradeNotifications, } from "@/client/notifications" import { onResetInput, @@ -98,10 +104,16 @@ function Launcher() { useEffect(() => { setOverlay(overlayRef.current) getCurrentWindowBounds().then(setInitialBounds).catch(console.error) - registerCreditNotifications() + registerCreditRfqCreatedNotifications() + registerCreditQuoteReceivedNotifications() + registerCreditQuoteAcceptedNotifications() + registerFxTradeNotifications() return () => { - unregisterCreditNotifications() + unregisterCreditRfqCreatedNotifications() + unregisterCreditQuoteReceivedNotifications() + unregisterCreditQuoteAcceptedNotifications() + unregisterFxTradeNotifications() } }, []) diff --git a/packages/client/src/client/notifications.finsemble.ts b/packages/client/src/client/notifications.finsemble.ts index e81449400b..fe3569388c 100644 --- a/packages/client/src/client/notifications.finsemble.ts +++ b/packages/client/src/client/notifications.finsemble.ts @@ -5,7 +5,7 @@ import { executions$ } from "@/services/executions/executions" import { processFxExecution } from "./notificationsUtils" -export function sendFxTradeNotification(executionTrade: ExecutionTrade) { +function sendFxTradeNotification(executionTrade: ExecutionTrade) { const { title, tradeDetails: tradeCurrencyDetails } = processFxExecution(executionTrade) const body = `${executionTrade.direction} ${tradeCurrencyDetails}` @@ -20,7 +20,7 @@ export function sendFxTradeNotification(executionTrade: ExecutionTrade) { let executionSubscription: Subscription | null = null -export function registerFxNotifications() { +export function registerFxTradeNotifications() { executionSubscription = executions$.subscribe({ next: (executionTrade) => { sendFxTradeNotification(executionTrade) @@ -34,26 +34,36 @@ export function registerFxNotifications() { }) } -export function unregisterFxNotifications() { +export function unregisterFxTradeNotifications() { if (executionSubscription) { executionSubscription.unsubscribe() } } -// TODO (4823) implement these for Finsemble when upgrading +// +// TODO (5580) implement these for Finsemble when adding in Credit apps/views +// -export function registerCreditNotifications() { +export function registerCreditRfqCreatedNotifications() { // no-op } -export function unregisterCreditNotifications() { +export function unregisterCreditRfqCreatedNotifications() { // no-op } -export function registerCreatedCreditNotification() { +export function registerCreditQuoteReceivedNotifications() { // no-op } -export function unregisterCreatedCreditNotification() { +export function unregisterCreditQuoteReceivedNotifications() { + // no-op +} + +export function registerCreditQuoteAcceptedNotifications() { + // no-op +} + +export function unregisterCreditQuoteAcceptedNotifications() { // no-op } diff --git a/packages/client/src/client/notifications.openfin.ts b/packages/client/src/client/notifications.openfin.ts index 0a1a53b860..8ec1785d5a 100644 --- a/packages/client/src/client/notifications.openfin.ts +++ b/packages/client/src/client/notifications.openfin.ts @@ -29,9 +29,9 @@ import { executions$, ExecutionTrade } from "@/services/executions" import { setCreditRfqCardHighlight } from "./App/Credit/CreditRfqs/CreditRfqCards" import { - processCreditQuote, - processCreditRfqAccepted, - processCreditRfqCreated, + processCreditQuoteReceived, + processCreditRfqCreatedConfirmation, + processCreditRfqWithAcceptedQuote, processFxExecution, } from "./notificationsUtils" import { constructUrl } from "./utils/constructUrl" @@ -148,32 +148,39 @@ const sendFxTradeNotification = (trade: ExecutionTrade) => { create(notificationOptions) } -const sendQuoteAcceptedNotification = ({ rfq, quote }: RfqWithPricedQuote) => { - const { title, tradeDetails } = processCreditRfqAccepted(rfq, quote) +const sendCreditRfqCreatedNotification = ( + rfqRequestConfirmation: ConfirmCreatedCreditRfq, +) => { + const { title, rfqDetails } = processCreditRfqCreatedConfirmation( + rfqRequestConfirmation, + ) const notificationOptions: TemplateCustom = { template: "custom", - templateOptions: createNotificationTemplate(rfq.direction), + templateOptions: createNotificationTemplate( + rfqRequestConfirmation.request.direction, + ), templateData: { - messageTradeDirection: rfq.direction.toUpperCase(), - messageTradeDetails: tradeDetails, + messageTradeDirection: + rfqRequestConfirmation.request.direction.toUpperCase(), + messageTradeDetails: rfqDetails, }, - // non-template stuff below + icon: creditIconUrl, - category: "Trade Executed", + category: "RFQ Created", title, indicator: { - text: "accepted", - color: IndicatorColor.GREEN, + text: "new rfq", + color: IndicatorColor.YELLOW, }, buttons: [ { - title: "Highlight trade in blotter", + title: "View RFQ", iconUrl: creditIconUrl, onClick: { - task: TASK_HIGHLIGHT_CREDIT_TRADE, + task: TASK_HIGHLIGHT_CREDIT_RFQ, payload: { - tradeId: rfq.id, + rfqId: rfqRequestConfirmation.rfqId, }, }, }, @@ -183,15 +190,15 @@ const sendQuoteAcceptedNotification = ({ rfq, quote }: RfqWithPricedQuote) => { create(notificationOptions) } -const sendCreditQuoteNotification = (quote: PricedQuoteDetails) => { - const { title, tradeDetails } = processCreditQuote(quote) +const sendCreditQuoteReceivedNotification = (quote: PricedQuoteDetails) => { + const { title, quoteDetails } = processCreditQuoteReceived(quote) const notificationOptions: TemplateCustom = { template: "custom", templateOptions: createNotificationTemplate(quote.direction), templateData: { messageTradeDirection: quote.direction.toUpperCase(), - messageTradeDetails: tradeDetails, + messageTradeDetails: quoteDetails, }, // non-template stuff below icon: creditIconUrl, @@ -218,37 +225,35 @@ const sendCreditQuoteNotification = (quote: PricedQuoteDetails) => { create(notificationOptions) } -const sendRFQCreatedConfirmationNotification = ( - rfqRequestConfirmation: ConfirmCreatedCreditRfq, -) => { - const { title, rfqDetails } = processCreditRfqCreated(rfqRequestConfirmation) +const sendCreditQuoteAcceptedNotification = ({ + rfq, + quote, +}: RfqWithPricedQuote) => { + const { title, tradeDetails } = processCreditRfqWithAcceptedQuote(rfq, quote) const notificationOptions: TemplateCustom = { template: "custom", - templateOptions: createNotificationTemplate( - rfqRequestConfirmation.request.direction, - ), + templateOptions: createNotificationTemplate(rfq.direction), templateData: { - messageTradeDirection: - rfqRequestConfirmation.request.direction.toUpperCase(), - messageTradeDetails: rfqDetails, + messageTradeDirection: rfq.direction.toUpperCase(), + messageTradeDetails: tradeDetails, }, - + // non-template stuff below icon: creditIconUrl, - category: "RFQ Created", + category: "Trade Executed", title, indicator: { - text: "new rfq", - color: IndicatorColor.GRAY, + text: "accepted", + color: IndicatorColor.GREEN, }, buttons: [ { - title: "View RFQ", + title: "Highlight trade in blotter", iconUrl: creditIconUrl, onClick: { - task: TASK_HIGHLIGHT_CREDIT_RFQ, + task: TASK_HIGHLIGHT_CREDIT_TRADE, payload: { - rfqId: rfqRequestConfirmation.rfqId, + tradeId: rfq.id, }, }, }, @@ -259,7 +264,8 @@ const sendRFQCreatedConfirmationNotification = ( } const TOPIC_HIGHLIGHT_FX_BLOTTER = "highlight-fx-blotter" -export const TOPIC_HIGHLIGHT_CREDIT_RFQ = "highlight-credit-rfq" +const TOPIC_HIGHLIGHT_CREDIT_RFQ = "highlight-credit-rfq" +// also used from RFQs tile, to highlight traded RFQ in blotter export const TOPIC_HIGHLIGHT_CREDIT_BLOTTER = "highlight-credit-blotter" export const handleHighlightFxBlotterAction = ( @@ -273,21 +279,19 @@ export const handleHighlightFxBlotterAction = ( } } -let quotesReceivedSubscription: Subscription | null = null - -const handleHighlightCreditBlotterAction = (event: NotificationActionEvent) => { - if (event.result.task === TASK_HIGHLIGHT_CREDIT_TRADE) { +export const handleHighlightRfqAction = (event: NotificationActionEvent) => { + if (event.result.task === TASK_HIGHLIGHT_CREDIT_RFQ) { fin.InterApplicationBus.publish( - TOPIC_HIGHLIGHT_CREDIT_BLOTTER, + TOPIC_HIGHLIGHT_CREDIT_RFQ, event.result.payload, ) } } -export const handleHighlightRfqAction = (event: NotificationActionEvent) => { - if (event.result.task === TASK_HIGHLIGHT_CREDIT_RFQ) { +const handleHighlightCreditBlotterAction = (event: NotificationActionEvent) => { + if (event.result.task === TASK_HIGHLIGHT_CREDIT_TRADE) { fin.InterApplicationBus.publish( - TOPIC_HIGHLIGHT_CREDIT_RFQ, + TOPIC_HIGHLIGHT_CREDIT_BLOTTER, event.result.payload, ) } @@ -295,50 +299,96 @@ export const handleHighlightRfqAction = (event: NotificationActionEvent) => { export type NotificationActionHandler = (event: NotificationActionEvent) => void -let areFxNotificationsRegistered = false +let areFxTradeNotificationsRegistered = false let executionSubscription: Subscription | null = null -export const registerFxNotifications = ( +export const registerFxTradeNotifications = ( + handler?: NotificationActionHandler, +) => { + if (areFxTradeNotificationsRegistered) { + return + } + areFxTradeNotificationsRegistered = true + + fin.InterApplicationBus.subscribe( + { uuid: "*" }, + TOPIC_HIGHLIGHT_FX_BLOTTER, + (message: { tradeId: number }) => setFxTradeRowHighlight(message.tradeId), + ) + + addEventListener( + "notification-action", + handler || handleHighlightFxBlotterAction, + ) + + executionSubscription = executions$.subscribe({ + next: (executionTrade) => { + sendFxTradeNotification(executionTrade) + }, + error: (e) => { + console.error(e) + }, + complete: () => { + console.error("FX trade notifications stream completed!?") + }, + }) +} + +export const unregisterFxTradeNotifications = () => { + if (executionSubscription) { + areFxTradeNotificationsRegistered = false + executionSubscription.unsubscribe() + } +} + +let areCreditRfqCreatedNotificationsRegistered = false +let creditRfqCreatedSubscription: Subscription | null = null + +export const registerCreditRfqCreatedNotifications = ( handler?: NotificationActionHandler, ) => { - if (!areFxNotificationsRegistered) { - areFxNotificationsRegistered = true + if (areCreditRfqCreatedNotificationsRegistered) { + return + } + areCreditRfqCreatedNotificationsRegistered = true + if (creditRfqCreatedSubscription == null) { fin.InterApplicationBus.subscribe( { uuid: "*" }, - TOPIC_HIGHLIGHT_FX_BLOTTER, - (message: { tradeId: number }) => setFxTradeRowHighlight(message.tradeId), + TOPIC_HIGHLIGHT_CREDIT_RFQ, + (message: { rfqId: number }) => { + setCreditRfqCardHighlight(message.rfqId) + }, ) - addEventListener( - "notification-action", - handler || handleHighlightFxBlotterAction, - ) + addEventListener("notification-action", handler || handleHighlightRfqAction) - executionSubscription = executions$.subscribe({ - next: (executionTrade) => { - sendFxTradeNotification(executionTrade) - }, - error: (e) => { - console.error(e) - }, - complete: () => { - console.error("FX trade notifications stream completed!?") + creditRfqCreatedSubscription = createdCreditConfirmation$.subscribe( + (rfqRquest) => { + sendCreditRfqCreatedNotification(rfqRquest) }, - }) + ) } } -export const unregisterFxNotifications = () => { - if (executionSubscription) { - areFxNotificationsRegistered = false - executionSubscription.unsubscribe() +export const unregisterCreditRfqCreatedNotifications = () => { + if (creditRfqCreatedSubscription != null) { + creditRfqCreatedSubscription.unsubscribe() } + areCreditRfqCreatedNotificationsRegistered = false } -const registerCreditQuoteNotifications = ( +let areCreditQuoteReceivedNotificationsRegistered = false +let creditQuoteReceivedSubscription: Subscription | null = null + +export const registerCreditQuoteReceivedNotifications = ( handler?: NotificationActionHandler, ) => { + if (areCreditQuoteReceivedNotificationsRegistered) { + return + } + areCreditQuoteReceivedNotificationsRegistered = true + fin.InterApplicationBus.subscribe( { uuid: "*" }, TOPIC_HIGHLIGHT_CREDIT_RFQ, @@ -349,9 +399,9 @@ const registerCreditQuoteNotifications = ( addEventListener("notification-action", handler || handleHighlightRfqAction) - quotesReceivedSubscription = lastQuoteReceived$.subscribe({ + creditQuoteReceivedSubscription = lastQuoteReceived$.subscribe({ next: (quote) => { - sendCreditQuoteNotification(quote) + sendCreditQuoteReceivedNotification(quote) }, error: (e) => { console.error(e) @@ -362,10 +412,23 @@ const registerCreditQuoteNotifications = ( }) } -let acceptedRfqWithQuoteSubscription: Subscription | null = null -const registerCreditAcceptedNotifications = ( +export const unregisterCreditQuoteReceivedNotifications = () => { + if (creditQuoteReceivedSubscription != null) { + creditQuoteReceivedSubscription.unsubscribe() + } + areCreditQuoteReceivedNotificationsRegistered = false +} + +let areCreditQuoteAcceptedNotificationsRegistered = false +let creditQuoteAcceptedSubscription: Subscription | null = null + +export const registerCreditQuoteAcceptedNotifications = ( handler?: NotificationActionHandler, ) => { + if (areCreditQuoteAcceptedNotificationsRegistered) { + return + } + areCreditQuoteAcceptedNotificationsRegistered = true fin.InterApplicationBus.subscribe( { uuid: "*" }, TOPIC_HIGHLIGHT_CREDIT_BLOTTER, @@ -379,57 +442,14 @@ const registerCreditAcceptedNotifications = ( handler || handleHighlightCreditBlotterAction, ) - acceptedRfqWithQuoteSubscription = acceptedRfqWithQuote$.subscribe((rfq) => { - sendQuoteAcceptedNotification(rfq) + creditQuoteAcceptedSubscription = acceptedRfqWithQuote$.subscribe((rfq) => { + sendCreditQuoteAcceptedNotification(rfq) }) } -let createdCreditSubscription: Subscription | null = null - -export const registerCreatedCreditNotification = () => { - if (createdCreditSubscription == null) { - fin.InterApplicationBus.subscribe( - { uuid: "*" }, - TOPIC_HIGHLIGHT_CREDIT_RFQ, - (message: { rfqId: number }) => { - setCreditRfqCardHighlight(message.rfqId) - }, - ) - - addEventListener("notification-action", handleHighlightRfqAction) - - createdCreditSubscription = createdCreditConfirmation$.subscribe( - (rfqRquest) => { - sendRFQCreatedConfirmationNotification(rfqRquest) - }, - ) - } -} - -export const unregisterCreatedCreditNotification = () => { - if (createdCreditSubscription != null) { - createdCreditSubscription.unsubscribe() - } -} - -let areCreditNotificationsCreated = false -export const registerCreditNotifications = ( - handler?: NotificationActionHandler, -) => { - if (!areCreditNotificationsCreated) { - registerCreditAcceptedNotifications(handler) - registerCreditQuoteNotifications(handler) - areCreditNotificationsCreated = true - } -} - -export const unregisterCreditNotifications = () => { - if (quotesReceivedSubscription) { - quotesReceivedSubscription.unsubscribe() - } - - if (acceptedRfqWithQuoteSubscription) { - acceptedRfqWithQuoteSubscription.unsubscribe() +export const unregisterCreditQuoteAcceptedNotifications = () => { + if (creditQuoteAcceptedSubscription != null) { + creditQuoteAcceptedSubscription.unsubscribe() } - areCreditNotificationsCreated = false + areCreditQuoteAcceptedNotificationsRegistered = false } diff --git a/packages/client/src/client/notifications.ts b/packages/client/src/client/notifications.ts index b162ababdf..b0257ac2b2 100644 --- a/packages/client/src/client/notifications.ts +++ b/packages/client/src/client/notifications.ts @@ -1,23 +1,31 @@ -export function registerFxNotifications(): Promise { +export function registerFxTradeNotifications(): Promise { return Promise.reject("Function should be implemented at platform level") } -export function unregisterFxNotifications(): Promise { +export function unregisterFxTradeNotifications(): Promise { return Promise.reject("Function should be implemented at platform level") } -export function registerCreditNotifications(): Promise { +export function registerCreditRfqCreatedNotifications(): Promise { return Promise.reject("Function should be implemented at platform level") } -export function unregisterCreditNotifications(): Promise { +export function unregisterCreditRfqCreatedNotifications(): Promise { return Promise.reject("Function should be implemented at platform level") } -export function registerCreatedCreditNotification(): Promise { +export function registerCreditQuoteReceivedNotifications(): Promise { return Promise.reject("Function should be implemented at platform level") } -export function unregisterCreatedCreditNotification(): Promise { +export function unregisterCreditQuoteReceivedNotifications(): Promise { + return Promise.reject("Function should be implemented at platform level") +} + +export function registerCreditQuoteAcceptedNotifications(): Promise { + return Promise.reject("Function should be implemented at platform level") +} + +export function unregisterCreditQuoteAcceptedNotifications(): Promise { return Promise.reject("Function should be implemented at platform level") } diff --git a/packages/client/src/client/notifications.web.ts b/packages/client/src/client/notifications.web.ts index 411429c85e..f335f03b9d 100644 --- a/packages/client/src/client/notifications.web.ts +++ b/packages/client/src/client/notifications.web.ts @@ -11,9 +11,9 @@ import { import { executions$, ExecutionTrade } from "@/services/executions" import { - processCreditQuote, - processCreditRfqAccepted, - processCreditRfqCreated, + processCreditQuoteReceived, + processCreditRfqCreatedConfirmation, + processCreditRfqWithAcceptedQuote, processFxExecution, } from "./notificationsUtils" import { constructUrl } from "./utils/constructUrl" @@ -34,11 +34,12 @@ const sendFxTradeNotification = (trade: ExecutionTrade) => { new Notification(title, options) } -const sendQuoteAcceptedNotification = ({ rfq, quote }: RfqWithPricedQuote) => { - const { title, tradeDetails } = processCreditRfqAccepted(rfq, quote) - +const sendCreditRfqCreatedNotification = ( + rfqCreate: ConfirmCreatedCreditRfq, +) => { + const { title, rfqDetails } = processCreditRfqCreatedConfirmation(rfqCreate) const options: NotificationOptions = { - body: `${rfq.direction} ${tradeDetails}`, + body: `You have sent a ${rfqCreate.request.direction} ${rfqDetails}`, icon: creditIconUrl, dir: "ltr", } @@ -46,10 +47,11 @@ const sendQuoteAcceptedNotification = ({ rfq, quote }: RfqWithPricedQuote) => { new Notification(title, options) } -const sendCreditCreatedNotification = (rfqCreate: ConfirmCreatedCreditRfq) => { - const { title, rfqDetails } = processCreditRfqCreated(rfqCreate) +const sendCreditQuoteReceivedNotification = (quote: PricedQuoteDetails) => { + const { title, quoteDetails } = processCreditQuoteReceived(quote) + const options: NotificationOptions = { - body: `You have sent a ${rfqCreate.request.direction} ${rfqDetails}`, + body: `${quote.direction} ${quoteDetails}`, icon: creditIconUrl, dir: "ltr", } @@ -57,11 +59,14 @@ const sendCreditCreatedNotification = (rfqCreate: ConfirmCreatedCreditRfq) => { new Notification(title, options) } -const sendCreditQuoteNotification = (quote: PricedQuoteDetails) => { - const { title, tradeDetails } = processCreditQuote(quote) +const sendCreditQuoteAcceptedNotification = ({ + rfq, + quote, +}: RfqWithPricedQuote) => { + const { title, tradeDetails } = processCreditRfqWithAcceptedQuote(rfq, quote) const options: NotificationOptions = { - body: `${quote.direction} ${tradeDetails}`, + body: `${rfq.direction} ${tradeDetails}`, icon: creditIconUrl, dir: "ltr", } @@ -89,9 +94,10 @@ const notificationsGranted = () => } } }) + let executionSubscription: Subscription | null = null -export async function registerFxNotifications() { +export async function registerFxTradeNotifications() { try { await notificationsGranted() @@ -104,7 +110,7 @@ export async function registerFxNotifications() { console.error(e) }, complete: () => { - console.error("notifications stream completed!?") + console.error("FX notifications execution stream completed!?") }, }) } catch (_) { @@ -112,42 +118,80 @@ export async function registerFxNotifications() { } } -export function unregisterFxNotifications() { +export function unregisterFxTradeNotifications() { if (executionSubscription) { executionSubscription.unsubscribe() } } -let quotesReceivedSubscription: Subscription | null = null -let acceptedRfqWithQuoteSubscription: Subscription | null = null -let createdCreditSubscription: Subscription | null = null +let creditRfqCreatedSubscription: Subscription | null = null + +export function registerCreditRfqCreatedNotifications() { + // send rfq created alerts for this tab only (driven from credit createRfq ACK) + creditRfqCreatedSubscription = createdCreditConfirmation$.subscribe({ + next: (createdRFQRequest) => { + sendCreditRfqCreatedNotification(createdRFQRequest) + }, + error: (e) => { + console.error(e) + }, + complete: () => { + console.error("Credit notifications RFQ created stream completed!?") + }, + }) +} + +export function unregisterCreditRfqCreatedNotifications() { + if (creditRfqCreatedSubscription) { + creditRfqCreatedSubscription.unsubscribe() + } +} + +let creditQuoteReceivedSubscription: Subscription | null = null -export async function registerCreditNotifications() { +export async function registerCreditQuoteReceivedNotifications() { try { await notificationsGranted() + // send quote alerts for every live tab connected to BE (driven from rfq update feed) - quotesReceivedSubscription = lastQuoteReceived$.subscribe({ + creditQuoteReceivedSubscription = lastQuoteReceived$.subscribe({ next: (quote) => { - sendCreditQuoteNotification(quote) + sendCreditQuoteReceivedNotification(quote) }, error: (e) => { console.error(e) }, complete: () => { - console.error("credit quote notifications stream completed!?") + console.error("credit quote received notifications stream completed!?") }, }) + } catch (e) { + console.error(e) + } +} + +export function unregisterCreditQuoteReceivedNotifications() { + if (creditQuoteReceivedSubscription) { + creditQuoteReceivedSubscription.unsubscribe() + } +} + +let creditQuoteAcceptedSubscription: Subscription | null = null - // send accepted quote for this tab only (driven from acceptQuote ACK) - acceptedRfqWithQuoteSubscription = acceptedRfqWithQuote$.subscribe({ +export async function registerCreditQuoteAcceptedNotifications() { + try { + await notificationsGranted() + + // send accepted quote alerts for this tab only (driven from credit accept ACK) + creditQuoteAcceptedSubscription = acceptedRfqWithQuote$.subscribe({ next: (rfqWithQuote) => { - sendQuoteAcceptedNotification(rfqWithQuote) + sendCreditQuoteAcceptedNotification(rfqWithQuote) }, error: (e) => { console.error(e) }, complete: () => { - console.error("accepted Rfq notifications stream completed!?") + console.error("credit quote accepted notifications stream completed!?") }, }) } catch (e) { @@ -155,33 +199,8 @@ export async function registerCreditNotifications() { } } -export function registerCreatedCreditNotification() { - // send created rfq - createdCreditSubscription = createdCreditConfirmation$.subscribe({ - next: (createdRFQRequest) => { - sendCreditCreatedNotification(createdRFQRequest) - }, - error: (e) => { - console.error(e) - }, - complete: () => { - console.error("Created credit notifications stream completed!?") - }, - }) -} - -export function unregisterCreatedCreditNotification() { - if (createdCreditSubscription) { - createdCreditSubscription.unsubscribe() - } -} - -export function unregisterCreditNotifications() { - if (quotesReceivedSubscription) { - quotesReceivedSubscription.unsubscribe() - } - - if (acceptedRfqWithQuoteSubscription) { - acceptedRfqWithQuoteSubscription.unsubscribe() +export function unregisterCreditQuoteAcceptedNotifications() { + if (creditQuoteAcceptedSubscription) { + creditQuoteAcceptedSubscription.unsubscribe() } } diff --git a/packages/client/src/client/notificationsUtils.ts b/packages/client/src/client/notificationsUtils.ts index 4b76f8b799..e05baa9b0a 100644 --- a/packages/client/src/client/notificationsUtils.ts +++ b/packages/client/src/client/notificationsUtils.ts @@ -23,36 +23,36 @@ export const processFxExecution = (executionTrade: ExecutionTrade) => { } } -export const processCreditRfqAccepted = ( - rfq: RfqDetails, - quote: PricedQuoteBody, -) => { - const dealer = rfq.dealers.find((dealer) => dealer.id === quote.dealerId) +export const processCreditRfqCreatedConfirmation = ({ + request, + rfqId, +}: ConfirmCreatedCreditRfq) => { return { - title: `Quote Accepted: RFQ ID ${quote.rfqId} from ${dealer?.name}`, - tradeDetails: `${rfq.instrument?.name} ${formatNumber( - rfq.quantity, - )} @ $${formatNumber(quote.state.payload)}`, + title: `RFQ Created: RFQ ID ${rfqId}`, + rfqDetails: `RFQ for ${formatNumber(request.quantity)} ${ + request.instrument?.name + } to ${request.dealerIds.length} dealers`, } } -export const processCreditQuote = (quote: PricedQuoteDetails) => { +export const processCreditQuoteReceived = (quote: PricedQuoteDetails) => { return { title: `Quote Received: RFQ ID ${quote.rfqId} from ${quote.dealer?.name}`, - tradeDetails: `${quote.instrument?.name} ${formatNumber( + quoteDetails: `${quote.instrument?.name} ${formatNumber( quote.quantity, )} @ $${formatNumber(quote.state.payload)}`, } } -export const processCreditRfqCreated = ({ - request, - rfqId, -}: ConfirmCreatedCreditRfq) => { +export const processCreditRfqWithAcceptedQuote = ( + rfq: RfqDetails, + quote: PricedQuoteBody, +) => { + const dealer = rfq.dealers.find((dealer) => dealer.id === quote.dealerId) return { - title: `RFQ Created: RFQ ID ${rfqId}`, - rfqDetails: `RFQ for ${formatNumber(request.quantity)} ${ - request.instrument?.name - } to ${request.dealerIds.length} dealers`, + title: `Quote Accepted: RFQ ID ${quote.rfqId} from ${dealer?.name}`, + tradeDetails: `${rfq.instrument?.name} ${formatNumber( + rfq.quantity, + )} @ $${formatNumber(quote.state.payload)}`, } } diff --git a/packages/client/src/workspace/home/notifications.ts b/packages/client/src/workspace/home/notifications.ts index 2860456e48..1351c23fbe 100644 --- a/packages/client/src/workspace/home/notifications.ts +++ b/packages/client/src/workspace/home/notifications.ts @@ -12,8 +12,10 @@ import { RT_PLATFORM_UUID_PREFIX } from "@/client/OpenFin/utils/window" import { VITE_RT_URL } from "../constants" +// TODO what about TASK_HIGHLIGHT_CREDIT_TRADE + let rfqsView: View | null = null -export const handleCreditRfqNotification = async ( +export const handleCreditViewRfqNotificationEvents = async ( event: NotificationActionEvent, ) => { if (event.result.task === TASK_HIGHLIGHT_CREDIT_RFQ) { @@ -24,6 +26,9 @@ export const handleCreditRfqNotification = async ( )?.isRunning //if credit is already open, highlight the rfq + + // TODO what if it is not open .. just give up? + if (isCreditOpen) { handleHighlightRfqAction(event) } else if (!rfqsView) { @@ -46,7 +51,7 @@ export const handleCreditRfqNotification = async ( } let blotterView: View | null = null -export const handleFxTradeNotification = async ( +export const handleFxHighlightTradeNotificationEvents = async ( event: NotificationActionEvent, ) => { if (event.result.task === TASK_HIGHLIGHT_FX_TRADE) { diff --git a/packages/client/src/workspace/provider.ts b/packages/client/src/workspace/provider.ts index 9cec63bc71..070508f8fe 100644 --- a/packages/client/src/workspace/provider.ts +++ b/packages/client/src/workspace/provider.ts @@ -1,8 +1,9 @@ import { init as workspacePlatformInit } from "@openfin/workspace-platform" import { - registerCreditNotifications, - registerFxNotifications, + registerCreditQuoteReceivedNotifications, + registerCreditRfqCreatedNotifications, + registerFxTradeNotifications, } from "@/client/notifications.openfin" import { initConnection } from "@/services/connection" import { registerSimulatedDealerResponses } from "@/services/credit/creditRfqResponses" @@ -12,8 +13,8 @@ import { BASE_URL } from "./constants" import { deregisterdock, dockCustomActions, registerDock } from "./dock" import { deregisterHome, registerHome, showHome } from "./home" import { - handleCreditRfqNotification, - handleFxTradeNotification, + handleCreditViewRfqNotificationEvents, + handleFxHighlightTradeNotificationEvents, } from "./home/notifications" import { deregisterStore, registerStore } from "./store" @@ -49,8 +50,11 @@ async function init() { await registerDock() await showHome() - registerFxNotifications(handleFxTradeNotification) - registerCreditNotifications(handleCreditRfqNotification) + registerFxTradeNotifications(handleFxHighlightTradeNotificationEvents) + registerCreditRfqCreatedNotifications(handleCreditViewRfqNotificationEvents) + registerCreditQuoteReceivedNotifications( + handleCreditViewRfqNotificationEvents, + ) const sub = registerSimulatedDealerResponses() From 88fca7d166e73f337fe83dac020fcf87f2c3bf5d Mon Sep 17 00:00:00 2001 From: Alan Greasley Date: Tue, 3 Oct 2023 15:10:30 +0100 Subject: [PATCH 3/8] chore: minimise dup notifications, except OF Credit + Launcher quotes --- .../Credit/CreditRfqForm/CreditRfqForm.tsx | 32 ++++++++--- .../CreditRfqForm/CreditRfqFormCore.tsx | 7 --- .../App/Credit/CreditRfqs/CreditRfqsCore.tsx | 6 ++ .../src/client/App/Trades/CreditTrades.tsx | 32 ++++++++--- .../src/client/notifications.openfin.ts | 55 ++++++++++-------- .../client/src/client/notifications.web.ts | 56 ++++++++++++++----- 6 files changed, 129 insertions(+), 59 deletions(-) diff --git a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx index 361a103dcb..f6ebe3d8d7 100644 --- a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx +++ b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx @@ -1,7 +1,11 @@ -import { lazy, Suspense } from "react" +import { lazy, Suspense, useEffect } from "react" import styled from "styled-components" import { Loader } from "@/client/components/Loader" +import { + registerCreditRfqCreatedNotifications, + unregisterCreditRfqCreatedNotifications, +} from "@/client/notifications" const CreditRfqFormCore = lazy(() => import("./CreditRfqFormCore")) @@ -17,12 +21,24 @@ const loader = ( ) -export const CreditRfqForm = () => ( - - - {loader} - - -) +export const CreditRfqForm = () => { + // TODO (5569) - required, otherwise Web will not populate instrument-related columns + // Instrument data is not ready when the RFQ updates are processed, without this (must be indirect subscription) + // (should be in lazy-loaded Core module, like other registrations) + useEffect(() => { + registerCreditRfqCreatedNotifications() + return () => { + unregisterCreditRfqCreatedNotifications() + } + }, []) + + return ( + + + {loader} + + + ) +} export default CreditRfqForm diff --git a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx index 920affe38b..9913cc705d 100644 --- a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx +++ b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx @@ -2,10 +2,6 @@ import { Subscribe } from "@react-rxjs/core" import { useEffect } from "react" import styled from "styled-components" -import { - registerCreditRfqCreatedNotifications, - unregisterCreditRfqCreatedNotifications, -} from "@/client/notifications" import { WithChildren } from "@/client/utils/utilityTypes" import { registerSimulatedDealerResponses } from "@/services/credit/creditRfqResponses" @@ -64,11 +60,8 @@ const CreditRfqFooter = styled.footer` const CreditRfqFormCore = ({ children }: WithChildren) => { useEffect(() => { const subscription = registerSimulatedDealerResponses() - registerCreditRfqCreatedNotifications() - return () => { subscription.unsubscribe() - unregisterCreditRfqCreatedNotifications() } }, []) diff --git a/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx b/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx index 5b466b7b04..a4a7d7ac21 100644 --- a/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx +++ b/packages/client/src/client/App/Credit/CreditRfqs/CreditRfqsCore.tsx @@ -3,7 +3,9 @@ import { useEffect } from "react" import styled from "styled-components" import { + registerCreditQuoteAcceptedNotifications, registerCreditQuoteReceivedNotifications, + unregisterCreditQuoteAcceptedNotifications, unregisterCreditQuoteReceivedNotifications, } from "@/client/notifications" import { WithChildren } from "@/client/utils/utilityTypes" @@ -20,9 +22,13 @@ const CreditRfqsCoreWrapper = styled.div` const CreditRfqsCore = ({ children }: WithChildren) => { useEffect(() => { registerCreditQuoteReceivedNotifications() + // the most logical place for accepted notifications, + // for all cases except the NLP-based nested RFQ ticket + registerCreditQuoteAcceptedNotifications() return () => { unregisterCreditQuoteReceivedNotifications() + unregisterCreditQuoteAcceptedNotifications() } }, []) diff --git a/packages/client/src/client/App/Trades/CreditTrades.tsx b/packages/client/src/client/App/Trades/CreditTrades.tsx index 66d098a7a5..06b35704b7 100644 --- a/packages/client/src/client/App/Trades/CreditTrades.tsx +++ b/packages/client/src/client/App/Trades/CreditTrades.tsx @@ -1,7 +1,11 @@ -import { lazy, Suspense } from "react" +import { lazy, Suspense, useEffect } from "react" import styled from "styled-components" import { Loader } from "@/client/components/Loader" +import { + registerCreditQuoteAcceptedNotifications, + unregisterCreditQuoteAcceptedNotifications, +} from "@/client/notifications" const TradesCore = lazy(() => import("./CoreCreditTrades")) @@ -12,12 +16,24 @@ const TradesWrapper = styled.article` background: ${({ theme }) => theme.core.darkBackground}; ` -export const CreditTrades = () => ( - - }> - - - -) +export const CreditTrades = () => { + // TODO (5569) - for now, we need to register for RFQ "accepted" here + // .. otherwise, in OpenFin, there is no past data in the Credit Blotter + // only processes endStateOfTheWorld rfq update + useEffect(() => { + registerCreditQuoteAcceptedNotifications() + return () => { + unregisterCreditQuoteAcceptedNotifications() + } + }, []) + + return ( + + }> + + + + ) +} export default CreditTrades diff --git a/packages/client/src/client/notifications.openfin.ts b/packages/client/src/client/notifications.openfin.ts index 8ec1785d5a..a139395065 100644 --- a/packages/client/src/client/notifications.openfin.ts +++ b/packages/client/src/client/notifications.openfin.ts @@ -1,6 +1,6 @@ import * as CSS from "csstype" import { - addEventListener, + addEventListener as addOpenFinNotificationsEventListener, ContainerTemplateFragment, create, CustomTemplateOptions, @@ -316,7 +316,7 @@ export const registerFxTradeNotifications = ( (message: { tradeId: number }) => setFxTradeRowHighlight(message.tradeId), ) - addEventListener( + addOpenFinNotificationsEventListener( "notification-action", handler || handleHighlightFxBlotterAction, ) @@ -336,9 +336,10 @@ export const registerFxTradeNotifications = ( export const unregisterFxTradeNotifications = () => { if (executionSubscription) { - areFxTradeNotificationsRegistered = false executionSubscription.unsubscribe() + executionSubscription = null } + areFxTradeNotificationsRegistered = false } let areCreditRfqCreatedNotificationsRegistered = false @@ -352,28 +353,30 @@ export const registerCreditRfqCreatedNotifications = ( } areCreditRfqCreatedNotificationsRegistered = true - if (creditRfqCreatedSubscription == null) { - fin.InterApplicationBus.subscribe( - { uuid: "*" }, - TOPIC_HIGHLIGHT_CREDIT_RFQ, - (message: { rfqId: number }) => { - setCreditRfqCardHighlight(message.rfqId) - }, - ) + fin.InterApplicationBus.subscribe( + { uuid: "*" }, + TOPIC_HIGHLIGHT_CREDIT_RFQ, + (message: { rfqId: number }) => { + setCreditRfqCardHighlight(message.rfqId) + }, + ) - addEventListener("notification-action", handler || handleHighlightRfqAction) + addOpenFinNotificationsEventListener( + "notification-action", + handler || handleHighlightRfqAction, + ) - creditRfqCreatedSubscription = createdCreditConfirmation$.subscribe( - (rfqRquest) => { - sendCreditRfqCreatedNotification(rfqRquest) - }, - ) - } + creditRfqCreatedSubscription = createdCreditConfirmation$.subscribe( + (rfqRquest) => { + sendCreditRfqCreatedNotification(rfqRquest) + }, + ) } export const unregisterCreditRfqCreatedNotifications = () => { - if (creditRfqCreatedSubscription != null) { + if (creditRfqCreatedSubscription) { creditRfqCreatedSubscription.unsubscribe() + creditRfqCreatedSubscription = null } areCreditRfqCreatedNotificationsRegistered = false } @@ -397,7 +400,10 @@ export const registerCreditQuoteReceivedNotifications = ( }, ) - addEventListener("notification-action", handler || handleHighlightRfqAction) + addOpenFinNotificationsEventListener( + "notification-action", + handler || handleHighlightRfqAction, + ) creditQuoteReceivedSubscription = lastQuoteReceived$.subscribe({ next: (quote) => { @@ -413,8 +419,9 @@ export const registerCreditQuoteReceivedNotifications = ( } export const unregisterCreditQuoteReceivedNotifications = () => { - if (creditQuoteReceivedSubscription != null) { + if (creditQuoteReceivedSubscription) { creditQuoteReceivedSubscription.unsubscribe() + creditQuoteReceivedSubscription = null } areCreditQuoteReceivedNotificationsRegistered = false } @@ -429,6 +436,7 @@ export const registerCreditQuoteAcceptedNotifications = ( return } areCreditQuoteAcceptedNotificationsRegistered = true + fin.InterApplicationBus.subscribe( { uuid: "*" }, TOPIC_HIGHLIGHT_CREDIT_BLOTTER, @@ -437,7 +445,7 @@ export const registerCreditQuoteAcceptedNotifications = ( }, ) - addEventListener( + addOpenFinNotificationsEventListener( "notification-action", handler || handleHighlightCreditBlotterAction, ) @@ -448,8 +456,9 @@ export const registerCreditQuoteAcceptedNotifications = ( } export const unregisterCreditQuoteAcceptedNotifications = () => { - if (creditQuoteAcceptedSubscription != null) { + if (creditQuoteAcceptedSubscription) { creditQuoteAcceptedSubscription.unsubscribe() + creditQuoteAcceptedSubscription = null } areCreditQuoteAcceptedNotificationsRegistered = false } diff --git a/packages/client/src/client/notifications.web.ts b/packages/client/src/client/notifications.web.ts index f335f03b9d..5da6c6392f 100644 --- a/packages/client/src/client/notifications.web.ts +++ b/packages/client/src/client/notifications.web.ts @@ -95,12 +95,20 @@ const notificationsGranted = () => } }) +// NOTE: All the guard code complication below is due to: +// a) the delay of the notificationsGranted promise - in development mode effects, unreg happens before reg +// b) other calls of the same registration in the same context causes duplicate subs + let executionSubscription: Subscription | null = null export async function registerFxTradeNotifications() { try { await notificationsGranted() + if (executionSubscription) { + return + } + // send trade executed for this tab only (driven from executeTrade ACK) executionSubscription = executions$.subscribe({ next: (executionTrade) => { @@ -121,29 +129,41 @@ export async function registerFxTradeNotifications() { export function unregisterFxTradeNotifications() { if (executionSubscription) { executionSubscription.unsubscribe() + executionSubscription = null } } let creditRfqCreatedSubscription: Subscription | null = null -export function registerCreditRfqCreatedNotifications() { - // send rfq created alerts for this tab only (driven from credit createRfq ACK) - creditRfqCreatedSubscription = createdCreditConfirmation$.subscribe({ - next: (createdRFQRequest) => { - sendCreditRfqCreatedNotification(createdRFQRequest) - }, - error: (e) => { - console.error(e) - }, - complete: () => { - console.error("Credit notifications RFQ created stream completed!?") - }, - }) +export async function registerCreditRfqCreatedNotifications() { + try { + await notificationsGranted() + + if (creditRfqCreatedSubscription) { + return + } + + // send rfq created alerts for this tab only (driven from credit createRfq ACK) + creditRfqCreatedSubscription = createdCreditConfirmation$.subscribe({ + next: (createdRFQRequest) => { + sendCreditRfqCreatedNotification(createdRFQRequest) + }, + error: (e) => { + console.error(e) + }, + complete: () => { + console.error("Credit notifications RFQ created stream completed!?") + }, + }) + } catch (e) { + console.error(e) + } } export function unregisterCreditRfqCreatedNotifications() { if (creditRfqCreatedSubscription) { creditRfqCreatedSubscription.unsubscribe() + creditRfqCreatedSubscription = null } } @@ -153,6 +173,10 @@ export async function registerCreditQuoteReceivedNotifications() { try { await notificationsGranted() + if (creditQuoteReceivedSubscription) { + return + } + // send quote alerts for every live tab connected to BE (driven from rfq update feed) creditQuoteReceivedSubscription = lastQuoteReceived$.subscribe({ next: (quote) => { @@ -173,6 +197,7 @@ export async function registerCreditQuoteReceivedNotifications() { export function unregisterCreditQuoteReceivedNotifications() { if (creditQuoteReceivedSubscription) { creditQuoteReceivedSubscription.unsubscribe() + creditQuoteReceivedSubscription = null } } @@ -182,6 +207,10 @@ export async function registerCreditQuoteAcceptedNotifications() { try { await notificationsGranted() + if (creditQuoteAcceptedSubscription) { + return + } + // send accepted quote alerts for this tab only (driven from credit accept ACK) creditQuoteAcceptedSubscription = acceptedRfqWithQuote$.subscribe({ next: (rfqWithQuote) => { @@ -202,5 +231,6 @@ export async function registerCreditQuoteAcceptedNotifications() { export function unregisterCreditQuoteAcceptedNotifications() { if (creditQuoteAcceptedSubscription) { creditQuoteAcceptedSubscription.unsubscribe() + creditQuoteAcceptedSubscription = null } } From b18a3a347642eebaf5370372dd4818e496bf241d Mon Sep 17 00:00:00 2001 From: Alan Greasley Date: Wed, 4 Oct 2023 10:35:30 +0100 Subject: [PATCH 4/8] chore: move credit created notification reg to lazy Core module and fix streams --- packages/client/package.json | 2 +- .../Credit/CreditRfqForm/CreditRfqForm.tsx | 32 +++++-------------- .../CreditRfqForm/CreditRfqFormCore.tsx | 6 ++++ .../src/client/App/Trades/CoreFxTrades.tsx | 13 +++++++- .../src/client/App/Trades/CreditTrades.tsx | 32 +++++-------------- .../src/services/credit/creditDealers.ts | 4 +-- .../src/services/credit/creditInstruments.ts | 4 +-- .../client/src/services/credit/creditRfqs.ts | 1 - 8 files changed, 37 insertions(+), 57 deletions(-) diff --git a/packages/client/package.json b/packages/client/package.json index 8fbb61b593..79e47055a7 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -57,7 +57,7 @@ "workspace:dev": "cross-env TARGET=workspace vite --config vite-ws.config.ts", "workspace:build": "cross-env TARGET=workspace vite build --config vite-ws.config.ts", "workspace:serve": "cross-env TARGET=workspace vite serve --config vite-ws.config.ts", - "workspace:run": "cross-env-shell \"wait-on -l http://localhost:2017/config/workspace.json && openfin -l http://localhost:2017/config/workspace.json\"", + "workspace:run": "cross-env-shell \"wait-on -l http://localhost:2017/config/workspace.json && openfin -l -c http://localhost:2017/config/workspace.json\"", "workspace:start": "concurrently \"npm:openfin:dev\" \"npm:workspace:dev\" \"npm:workspace:run\"", "finsemble:dev": "cross-env TARGET=finsemble vite", "finsemble:build": "cross-env TARGET=finsemble vite build", diff --git a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx index f6ebe3d8d7..361a103dcb 100644 --- a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx +++ b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqForm.tsx @@ -1,11 +1,7 @@ -import { lazy, Suspense, useEffect } from "react" +import { lazy, Suspense } from "react" import styled from "styled-components" import { Loader } from "@/client/components/Loader" -import { - registerCreditRfqCreatedNotifications, - unregisterCreditRfqCreatedNotifications, -} from "@/client/notifications" const CreditRfqFormCore = lazy(() => import("./CreditRfqFormCore")) @@ -21,24 +17,12 @@ const loader = ( ) -export const CreditRfqForm = () => { - // TODO (5569) - required, otherwise Web will not populate instrument-related columns - // Instrument data is not ready when the RFQ updates are processed, without this (must be indirect subscription) - // (should be in lazy-loaded Core module, like other registrations) - useEffect(() => { - registerCreditRfqCreatedNotifications() - return () => { - unregisterCreditRfqCreatedNotifications() - } - }, []) - - return ( - - - {loader} - - - ) -} +export const CreditRfqForm = () => ( + + + {loader} + + +) export default CreditRfqForm diff --git a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx index 9913cc705d..9e8128dbd9 100644 --- a/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx +++ b/packages/client/src/client/App/Credit/CreditRfqForm/CreditRfqFormCore.tsx @@ -2,6 +2,10 @@ import { Subscribe } from "@react-rxjs/core" import { useEffect } from "react" import styled from "styled-components" +import { + registerCreditRfqCreatedNotifications, + unregisterCreditRfqCreatedNotifications, +} from "@/client/notifications" import { WithChildren } from "@/client/utils/utilityTypes" import { registerSimulatedDealerResponses } from "@/services/credit/creditRfqResponses" @@ -60,8 +64,10 @@ const CreditRfqFooter = styled.footer` const CreditRfqFormCore = ({ children }: WithChildren) => { useEffect(() => { const subscription = registerSimulatedDealerResponses() + registerCreditRfqCreatedNotifications() return () => { subscription.unsubscribe() + unregisterCreditRfqCreatedNotifications() } }, []) diff --git a/packages/client/src/client/App/Trades/CoreFxTrades.tsx b/packages/client/src/client/App/Trades/CoreFxTrades.tsx index a5a1330ddb..9008f05a9d 100644 --- a/packages/client/src/client/App/Trades/CoreFxTrades.tsx +++ b/packages/client/src/client/App/Trades/CoreFxTrades.tsx @@ -1,6 +1,10 @@ import { broadcast } from "@finos/fdc3" -import { useCallback } from "react" +import { useCallback, useEffect } from "react" +import { + registerFxTradeNotifications, + unregisterFxTradeNotifications, +} from "@/client/notifications" import { FxTrade, trades$ } from "@/services/trades" import { TradesGrid } from "./TradesGrid" @@ -8,6 +12,13 @@ import { useFxTradeRowHighlight } from "./TradesState" import { fxColDef, fxColFields } from "./TradesState/colConfig" const FxTrades = () => { + useEffect(() => { + registerFxTradeNotifications() + return () => { + unregisterFxTradeNotifications() + } + }, []) + const highlightedRow = useFxTradeRowHighlight() const tryBroadcastContext = useCallback((trade: FxTrade) => { diff --git a/packages/client/src/client/App/Trades/CreditTrades.tsx b/packages/client/src/client/App/Trades/CreditTrades.tsx index 06b35704b7..66d098a7a5 100644 --- a/packages/client/src/client/App/Trades/CreditTrades.tsx +++ b/packages/client/src/client/App/Trades/CreditTrades.tsx @@ -1,11 +1,7 @@ -import { lazy, Suspense, useEffect } from "react" +import { lazy, Suspense } from "react" import styled from "styled-components" import { Loader } from "@/client/components/Loader" -import { - registerCreditQuoteAcceptedNotifications, - unregisterCreditQuoteAcceptedNotifications, -} from "@/client/notifications" const TradesCore = lazy(() => import("./CoreCreditTrades")) @@ -16,24 +12,12 @@ const TradesWrapper = styled.article` background: ${({ theme }) => theme.core.darkBackground}; ` -export const CreditTrades = () => { - // TODO (5569) - for now, we need to register for RFQ "accepted" here - // .. otherwise, in OpenFin, there is no past data in the Credit Blotter - // only processes endStateOfTheWorld rfq update - useEffect(() => { - registerCreditQuoteAcceptedNotifications() - return () => { - unregisterCreditQuoteAcceptedNotifications() - } - }, []) - - return ( - - }> - - - - ) -} +export const CreditTrades = () => ( + + }> + + + +) export default CreditTrades diff --git a/packages/client/src/services/credit/creditDealers.ts b/packages/client/src/services/credit/creditDealers.ts index cdac2a5b4a..1a45885e8a 100644 --- a/packages/client/src/services/credit/creditDealers.ts +++ b/packages/client/src/services/credit/creditDealers.ts @@ -1,4 +1,4 @@ -import { bind, shareLatest } from "@react-rxjs/core" +import { bind } from "@react-rxjs/core" import { map, scan } from "rxjs/operators" import { @@ -29,9 +29,7 @@ export const [useCreditDealers, creditDealers$] = bind( return acc } }, []), - shareLatest(), ), - [], ) export const [useCreditDealerById, creditDealerById$] = bind( diff --git a/packages/client/src/services/credit/creditInstruments.ts b/packages/client/src/services/credit/creditInstruments.ts index 652c9ec08a..82e2818726 100644 --- a/packages/client/src/services/credit/creditInstruments.ts +++ b/packages/client/src/services/credit/creditInstruments.ts @@ -1,6 +1,5 @@ import { bind, shareLatest } from "@react-rxjs/core" -import { of } from "rxjs" -import { map, scan, tap } from "rxjs/operators" +import { map, scan } from "rxjs/operators" import { ADDED_INSTRUMENT_UPDATE, @@ -71,7 +70,6 @@ export const [useCreditInstruments, creditInstruments$] = bind( creditInstrumentsByCusip$.pipe( map((creditInstrumentsByCusip) => Object.values(creditInstrumentsByCusip)), ), - [], ) export const [useCreditInstrumentById, creditInstrumentById$] = bind( diff --git a/packages/client/src/services/credit/creditRfqs.ts b/packages/client/src/services/credit/creditRfqs.ts index e21e418e62..7262ddae16 100644 --- a/packages/client/src/services/credit/creditRfqs.ts +++ b/packages/client/src/services/credit/creditRfqs.ts @@ -61,7 +61,6 @@ export interface PricedQuoteDetails extends Omit { export const creditRfqUpdates$ = WorkflowService.subscribe().pipe( withConnection(), - shareLatest(), ) export const creditRfqsById$ = creditRfqUpdates$.pipe( From 3d74029ead434e803eb7b465632512debc48d366 Mon Sep 17 00:00:00 2001 From: Alan Greasley Date: Wed, 4 Oct 2023 22:40:37 +0100 Subject: [PATCH 5/8] chore: add workspace credit accepted notification action to open blotter view --- packages/client/README.md | 16 ++ .../src/client/notifications.openfin.ts | 7 +- .../src/workspace/config/workspace.json | 3 +- .../src/workspace/home/notifications.ts | 154 +++++++++++++----- packages/client/src/workspace/provider.ts | 11 +- 5 files changed, 142 insertions(+), 49 deletions(-) diff --git a/packages/client/README.md b/packages/client/README.md index d5ba72436c..50a333cd67 100644 --- a/packages/client/README.md +++ b/packages/client/README.md @@ -98,6 +98,22 @@ As a shortcut, to run the server and client in one command, use npm run openfin:start: ``` +To debug OpenFin windows more easily (using Chromium devtools), check the relevant manifest for the appropriate port in e.g. + +``` + "arguments": "--remote-debugging-port=9092" +``` + +navigate to [chrome://inspect/#devices](chrome://inspect/#devices) in a Chrome tab + +add the address in the dialog you get when you click "Configure ..." + +``` +http://localhost: +``` + +and any running OpenFin windows should be displayed, with Inspect links etc. + ### NLP in the OpenFin Launcher and Workspace (below) The OpenFin Launcher and Workspace Home UI have a search command line interface powered by [DialogFlow](https://cloud.google.com/dialogflow), where you can enter commands like diff --git a/packages/client/src/client/notifications.openfin.ts b/packages/client/src/client/notifications.openfin.ts index a139395065..a07fde64d0 100644 --- a/packages/client/src/client/notifications.openfin.ts +++ b/packages/client/src/client/notifications.openfin.ts @@ -281,6 +281,8 @@ export const handleHighlightFxBlotterAction = ( export const handleHighlightRfqAction = (event: NotificationActionEvent) => { if (event.result.task === TASK_HIGHLIGHT_CREDIT_RFQ) { + console.warn("hightlight RFQ", event.result.payload) + fin.InterApplicationBus.publish( TOPIC_HIGHLIGHT_CREDIT_RFQ, event.result.payload, @@ -288,8 +290,11 @@ export const handleHighlightRfqAction = (event: NotificationActionEvent) => { } } -const handleHighlightCreditBlotterAction = (event: NotificationActionEvent) => { +export const handleHighlightCreditBlotterAction = ( + event: NotificationActionEvent, +) => { if (event.result.task === TASK_HIGHLIGHT_CREDIT_TRADE) { + console.warn("hightlight Credit Trade", event.result.payload) fin.InterApplicationBus.publish( TOPIC_HIGHLIGHT_CREDIT_BLOTTER, event.result.payload, diff --git a/packages/client/src/workspace/config/workspace.json b/packages/client/src/workspace/config/workspace.json index 230381dc25..1b2796292c 100644 --- a/packages/client/src/workspace/config/workspace.json +++ b/packages/client/src/workspace/config/workspace.json @@ -2,7 +2,8 @@ "devtools_port": 9090, "licenseKey": "WSb63491c7-090f-4e38-8088-62675824c24b", "runtime": { - "version": "" + "version": "", + "arguments": "--remote-debugging-port=9092" }, "platform": { "uuid": "adaptive-workspace-provider-", diff --git a/packages/client/src/workspace/home/notifications.ts b/packages/client/src/workspace/home/notifications.ts index 1351c23fbe..5ebb718408 100644 --- a/packages/client/src/workspace/home/notifications.ts +++ b/packages/client/src/workspace/home/notifications.ts @@ -3,80 +3,150 @@ import { getCurrentSync } from "@openfin/workspace-platform" import { NotificationActionEvent } from "openfin-notifications" import { + handleHighlightCreditBlotterAction, handleHighlightFxBlotterAction, handleHighlightRfqAction, TASK_HIGHLIGHT_CREDIT_RFQ, + TASK_HIGHLIGHT_CREDIT_TRADE, TASK_HIGHLIGHT_FX_TRADE, } from "@/client/notifications.openfin" import { RT_PLATFORM_UUID_PREFIX } from "@/client/OpenFin/utils/window" import { VITE_RT_URL } from "../constants" -// TODO what about TASK_HIGHLIGHT_CREDIT_TRADE +let openingCreditRfqsView = false +let creditRfqsView: View | null = null +let openingCreditBlotterView = false +let creditBlotterView: View | null = null -let rfqsView: View | null = null -export const handleCreditViewRfqNotificationEvents = async ( +export const handleCreditNotificationEvents = async ( event: NotificationActionEvent, ) => { - if (event.result.task === TASK_HIGHLIGHT_CREDIT_RFQ) { - const apps = await fin.System.getAllApplications() + const apps = await fin.System.getAllApplications() + const creditApp = apps.find((app) => + app.uuid.includes(`${RT_PLATFORM_UUID_PREFIX}credit`), + ) + const isRTCreditAppOpen = creditApp?.isRunning - const isCreditOpen = apps.find((app) => - app.uuid.includes(`${RT_PLATFORM_UUID_PREFIX}credit`), - )?.isRunning + if (event.result.task === TASK_HIGHLIGHT_CREDIT_RFQ) { + // if RT Credit app is already open, highlight the rfq + if (isRTCreditAppOpen) { + console.debug("Credit app is running, highlight RFQ", creditApp.uuid) + const creditAppWindow = await fin.Application.wrapSync({ + uuid: creditApp.uuid, + }).getWindow() + creditAppWindow.bringToFront() + handleHighlightRfqAction(event) + return + } - //if credit is already open, highlight the rfq + // (or just focus it if we opened one earlier and it is still there) + if (creditRfqsView) { + console.debug( + "credit rfqs view is running, highlight RFQ", + creditRfqsView.identity.uuid, + ) + creditRfqsView.focus() + handleHighlightRfqAction(event) + return + } - // TODO what if it is not open .. just give up? + // otherwise open a new Credit RFQs view, + // and block out any other handlers from doing the same + if (!openingCreditRfqsView) { + openingCreditRfqsView = true - if (isCreditOpen) { - handleHighlightRfqAction(event) - } else if (!rfqsView) { - //else open an rfqs view const platform = getCurrentSync() - - rfqsView = await platform.createView({ + creditRfqsView = await platform.createView({ url: `${VITE_RT_URL}/credit-rfqs`, bounds: { width: 320, height: 180, top: 0, left: 0 }, }) - rfqsView.on("destroyed", () => { - rfqsView?.removeAllListeners() - rfqsView = null + creditRfqsView.on("destroyed", () => { + creditRfqsView?.removeAllListeners() + creditRfqsView = null }) - } else { - rfqsView.focus() + openingCreditRfqsView = false } } -} -let blotterView: View | null = null -export const handleFxHighlightTradeNotificationEvents = async ( - event: NotificationActionEvent, -) => { - if (event.result.task === TASK_HIGHLIGHT_FX_TRADE) { - const apps = await fin.System.getAllApplications() + if (event.result.task === TASK_HIGHLIGHT_CREDIT_TRADE) { + // if RT Credit app is already open, highlight the trade + if (isRTCreditAppOpen) { + handleHighlightCreditBlotterAction(event) + return + } - const isFxOpen = apps.find((app) => - app.uuid.includes(`${RT_PLATFORM_UUID_PREFIX}fx`), - )?.isRunning + // (or just focus it if we opened one earlier and it is still there) + if (creditBlotterView) { + creditBlotterView.focus() + handleHighlightCreditBlotterAction(event) + return + } - if (isFxOpen) { - handleHighlightFxBlotterAction(event) - } else if (!blotterView) { - const platform = getCurrentSync() + // otherwise open a new Credit RFQs view, + // and block out any other handlers from doing the same + if (!openingCreditBlotterView) { + openingCreditBlotterView = true - blotterView = await platform.createView({ - url: `${VITE_RT_URL}/fx-blotter`, + const platform = getCurrentSync() + creditBlotterView = await platform.createView({ + url: `${VITE_RT_URL}/credit-blotter`, bounds: { width: 320, height: 180, top: 0, left: 0 }, }) - blotterView.on("destroyed", () => { - blotterView?.removeAllListeners() - blotterView = null + creditBlotterView.on("destroyed", () => { + creditBlotterView?.removeAllListeners() + creditBlotterView = null }) - } else { - blotterView.focus() + openingCreditBlotterView = false } } } + +let blotterView: View | null = null +export const handleFxHighlightTradeNotificationEvents = async ( + event: NotificationActionEvent, +) => { + if (event.result.task !== TASK_HIGHLIGHT_FX_TRADE) { + return + } + + const apps = await fin.System.getAllApplications() + const fxApp = apps.find((app) => + app.uuid.includes(`${RT_PLATFORM_UUID_PREFIX}fx`), + ) + const isFxOpen = fxApp?.isRunning + + if (isFxOpen) { + console.debug("FX app is running, highlight trade in blotter", fxApp.uuid) + const fxAppWindow = await fin.Application.wrapSync({ + uuid: fxApp.uuid, + }).getWindow() + fxAppWindow.bringToFront() + handleHighlightFxBlotterAction(event) + return + } + + if (blotterView) { + console.debug( + "blotter view is running, highlight trade in blotter", + blotterView.identity.uuid, + ) + blotterView.focus() + handleHighlightFxBlotterAction(event) + return + } + + const platform = getCurrentSync() + + blotterView = await platform.createView({ + url: `${VITE_RT_URL}/fx-blotter`, + bounds: { width: 320, height: 180, top: 0, left: 0 }, + }) + + blotterView.on("destroyed", () => { + blotterView?.removeAllListeners() + blotterView = null + }) +} diff --git a/packages/client/src/workspace/provider.ts b/packages/client/src/workspace/provider.ts index 070508f8fe..3ecd25782a 100644 --- a/packages/client/src/workspace/provider.ts +++ b/packages/client/src/workspace/provider.ts @@ -1,6 +1,7 @@ import { init as workspacePlatformInit } from "@openfin/workspace-platform" import { + registerCreditQuoteAcceptedNotifications, registerCreditQuoteReceivedNotifications, registerCreditRfqCreatedNotifications, registerFxTradeNotifications, @@ -13,7 +14,7 @@ import { BASE_URL } from "./constants" import { deregisterdock, dockCustomActions, registerDock } from "./dock" import { deregisterHome, registerHome, showHome } from "./home" import { - handleCreditViewRfqNotificationEvents, + handleCreditNotificationEvents, handleFxHighlightTradeNotificationEvents, } from "./home/notifications" import { deregisterStore, registerStore } from "./store" @@ -51,10 +52,9 @@ async function init() { await showHome() registerFxTradeNotifications(handleFxHighlightTradeNotificationEvents) - registerCreditRfqCreatedNotifications(handleCreditViewRfqNotificationEvents) - registerCreditQuoteReceivedNotifications( - handleCreditViewRfqNotificationEvents, - ) + registerCreditRfqCreatedNotifications(handleCreditNotificationEvents) + registerCreditQuoteReceivedNotifications(handleCreditNotificationEvents) + registerCreditQuoteAcceptedNotifications(handleCreditNotificationEvents) const sub = registerSimulatedDealerResponses() @@ -62,6 +62,7 @@ async function init() { const providerWindow = fin.Window.getCurrentSync() providerWindow.once("close-requested", async () => { + // this runs _after_ the user clicks on confirm in the, well, confirmation dialog await deregisterHome() await deregisterStore() await deregisterdock() From cbec2cda9fff69a67a12e3c245f771aa267090f1 Mon Sep 17 00:00:00 2001 From: Alan Greasley Date: Mon, 9 Oct 2023 14:30:08 +0100 Subject: [PATCH 6/8] chore: fix blotter highlight from notification --- .../src/client/App/Trades/CoreCreditTrades.tsx | 13 +++++++++++++ packages/client/src/client/notifications.openfin.ts | 3 --- packages/client/src/workspace/dock.ts | 2 +- packages/client/src/workspace/home/notifications.ts | 2 +- packages/client/src/workspace/provider.ts | 12 ++++++------ 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/packages/client/src/client/App/Trades/CoreCreditTrades.tsx b/packages/client/src/client/App/Trades/CoreCreditTrades.tsx index fca14ea870..1f90370dad 100644 --- a/packages/client/src/client/App/Trades/CoreCreditTrades.tsx +++ b/packages/client/src/client/App/Trades/CoreCreditTrades.tsx @@ -1,3 +1,9 @@ +import { useEffect } from "react" + +import { + registerCreditQuoteAcceptedNotifications, + unregisterCreditQuoteAcceptedNotifications, +} from "@/client/notifications" import { creditTrades$ } from "@/services/trades" import { TradesGrid } from "./TradesGrid" @@ -6,6 +12,13 @@ import { creditColDef, creditColFields } from "./TradesState/colConfig" const CreditTrades = () => { const highlightedRow = useCreditTradeRowHighlight() + useEffect(() => { + registerCreditQuoteAcceptedNotifications() + + return () => { + unregisterCreditQuoteAcceptedNotifications() + } + }, []) return ( { if (event.result.task === TASK_HIGHLIGHT_CREDIT_RFQ) { - console.warn("hightlight RFQ", event.result.payload) - fin.InterApplicationBus.publish( TOPIC_HIGHLIGHT_CREDIT_RFQ, event.result.payload, @@ -294,7 +292,6 @@ export const handleHighlightCreditBlotterAction = ( event: NotificationActionEvent, ) => { if (event.result.task === TASK_HIGHLIGHT_CREDIT_TRADE) { - console.warn("hightlight Credit Trade", event.result.payload) fin.InterApplicationBus.publish( TOPIC_HIGHLIGHT_CREDIT_BLOTTER, event.result.payload, diff --git a/packages/client/src/workspace/dock.ts b/packages/client/src/workspace/dock.ts index 8199cd8e89..de969a9d68 100644 --- a/packages/client/src/workspace/dock.ts +++ b/packages/client/src/workspace/dock.ts @@ -50,7 +50,7 @@ export const registerDock = () => { return Dock.show() } -export async function deregisterdock() { +export async function deregisterDock() { return Dock.deregister() } diff --git a/packages/client/src/workspace/home/notifications.ts b/packages/client/src/workspace/home/notifications.ts index 5ebb718408..543bb5404d 100644 --- a/packages/client/src/workspace/home/notifications.ts +++ b/packages/client/src/workspace/home/notifications.ts @@ -105,7 +105,7 @@ export const handleCreditNotificationEvents = async ( } let blotterView: View | null = null -export const handleFxHighlightTradeNotificationEvents = async ( +export const handleFxNotificationEvents = async ( event: NotificationActionEvent, ) => { if (event.result.task !== TASK_HIGHLIGHT_FX_TRADE) { diff --git a/packages/client/src/workspace/provider.ts b/packages/client/src/workspace/provider.ts index 3ecd25782a..894803da9d 100644 --- a/packages/client/src/workspace/provider.ts +++ b/packages/client/src/workspace/provider.ts @@ -11,11 +11,11 @@ import { registerSimulatedDealerResponses } from "@/services/credit/creditRfqRes import { customActions, overrideCallback } from "./browser" import { BASE_URL } from "./constants" -import { deregisterdock, dockCustomActions, registerDock } from "./dock" +import { deregisterDock, dockCustomActions, registerDock } from "./dock" import { deregisterHome, registerHome, showHome } from "./home" import { handleCreditNotificationEvents, - handleFxHighlightTradeNotificationEvents, + handleFxNotificationEvents, } from "./home/notifications" import { deregisterStore, registerStore } from "./store" @@ -51,12 +51,12 @@ async function init() { await registerDock() await showHome() - registerFxTradeNotifications(handleFxHighlightTradeNotificationEvents) + registerFxTradeNotifications(handleFxNotificationEvents) registerCreditRfqCreatedNotifications(handleCreditNotificationEvents) registerCreditQuoteReceivedNotifications(handleCreditNotificationEvents) registerCreditQuoteAcceptedNotifications(handleCreditNotificationEvents) - const sub = registerSimulatedDealerResponses() + const simulatedDealersSubscription = registerSimulatedDealerResponses() await initConnection() @@ -65,8 +65,8 @@ async function init() { // this runs _after_ the user clicks on confirm in the, well, confirmation dialog await deregisterHome() await deregisterStore() - await deregisterdock() - sub.unsubscribe() + await deregisterDock() + simulatedDealersSubscription.unsubscribe() fin.Platform.getCurrentSync().quit() }) } From e31e1f7e21899d2a68ee69dc9813b3edc6820999 Mon Sep 17 00:00:00 2001 From: Alan Greasley Date: Tue, 10 Oct 2023 10:44:51 +0100 Subject: [PATCH 7/8] fix: OF WS credit sell-side popup on RFQ create --- .../limitChecker/limitChecker.openfin.ts | 8 ++- packages/client/vite-ws.config.ts | 71 +++++++++++++++++++ 2 files changed, 76 insertions(+), 3 deletions(-) diff --git a/packages/client/src/services/limitChecker/limitChecker.openfin.ts b/packages/client/src/services/limitChecker/limitChecker.openfin.ts index b00bae876a..5c4012bd0b 100644 --- a/packages/client/src/services/limitChecker/limitChecker.openfin.ts +++ b/packages/client/src/services/limitChecker/limitChecker.openfin.ts @@ -4,9 +4,11 @@ import { map, switchMap, take, tap } from "rxjs/operators" import { CheckLimitStreamGenerator } from "./types" -window.fdc3.addContextListener("limit-checker-status", (context) => { - if (context.id) limitCheckSubscription$.next(context.id.isAlive) -}) +if (window.fdc3) { + window.fdc3.addContextListener("limit-checker-status", (context) => { + if (context.id) limitCheckSubscription$.next(context.id.isAlive) + }) +} const limitCheckSubscription$ = new BehaviorSubject("false") diff --git a/packages/client/vite-ws.config.ts b/packages/client/vite-ws.config.ts index 2c691689c5..fd9ba2896a 100644 --- a/packages/client/vite-ws.config.ts +++ b/packages/client/vite-ws.config.ts @@ -1,3 +1,5 @@ +import { existsSync, readdirSync } from "fs" +import path from "path" import { defineConfig, loadEnv, Plugin } from "vite" import { createHtmlPlugin } from "vite-plugin-html" import { TransformOption, viteStaticCopy } from "vite-plugin-static-copy" @@ -12,6 +14,74 @@ function getBaseUrl(dev: boolean) { : `${process.env.DOMAIN || ""}${process.env.URL_PATH || ""}` || "" } +// Replace files with . if they exist +// Note - resolveId source and importer args are different between dev and build +// Some more investigation and work should be done to improve this when possible +function targetBuildPlugin(dev: boolean, target: string): Plugin { + return { + name: "targetBuildPlugin", + enforce: "pre", + resolveId: function (source, importer) { + if (dev) { + const extension = source.split(".")[1] + if (extension !== "ts" && extension !== "tsx") return null + + const file = path.parse(source) + const files = readdirSync("." + file.dir) + + // Only continue if we can find a .. file + if (!files.includes(`${file.name}.${target}.${extension}`)) return null + + const mockPath = `${file.dir}/${file.name}.${target}.${extension}` + return this.resolve(mockPath, importer) + } else { + if ( + !importer || + importer.includes("node_modules") || + source === "./main" + ) { + return null + } + + if (!source.startsWith(".") && !source.startsWith("/")) { + return null + } + + const sourcePath = path.parse(source) + const importerPath = path.parse(importer.replace(/\\/g, "/")) + + // If imported file starts with /src we can not append it to importer dir + // so we need to strip the path by the rootPrefix first + const aliasedSourceRootPrefix = "/src" + const baseCandidatePath = + sourcePath.dir.startsWith(aliasedSourceRootPrefix) && + importerPath.dir.includes(aliasedSourceRootPrefix) + ? `${importerPath.dir.split(aliasedSourceRootPrefix)[0]}/` + : importerPath.dir + const targetFileName = `${sourcePath.name}.${target.toLowerCase()}` + + const candidatePath = path.join( + baseCandidatePath, + sourcePath.dir, + targetFileName, + ) + + // Source doesn't have file extension, so try all extensions + const candidateTs = `${candidatePath}.ts` + if (existsSync(candidateTs)) { + console.log(`candidate is ${candidateTs}\n`) + return candidateTs + } + const candidateTsx = `${candidatePath}.tsx` + if (existsSync(candidateTsx)) { + console.log(`candidate is ${candidateTsx}\n`) + return candidateTsx + } + } + }, + } +} + const copyOpenfinPlugin = ( isDev: boolean, env: string, @@ -86,6 +156,7 @@ const setConfig = ({ mode }) => { const isDev = mode === "development" const baseUrl = getBaseUrl(isDev) const plugins = [ + targetBuildPlugin(isDev, "openfin"), copyOpenfinPlugin(isDev, env, process.env.VITE_RA_URL!), injectScriptIntoHtml(), ] From 74bd5085075ee4481dfab308726e4d8eb69b1386 Mon Sep 17 00:00:00 2001 From: Alan Greasley Date: Tue, 10 Oct 2023 14:30:30 +0100 Subject: [PATCH 8/8] fix: OF credit quote registration, de-duping quotes on multiple reg --- .../src/client/notifications.openfin.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/client/src/client/notifications.openfin.ts b/packages/client/src/client/notifications.openfin.ts index 63cb4cf7e1..1dceef9b4d 100644 --- a/packages/client/src/client/notifications.openfin.ts +++ b/packages/client/src/client/notifications.openfin.ts @@ -1,3 +1,4 @@ +import { EntityType, Me } from "@openfin/core/src/OpenFin" import * as CSS from "csstype" import { addEventListener as addOpenFinNotificationsEventListener, @@ -386,7 +387,7 @@ export const unregisterCreditRfqCreatedNotifications = () => { let areCreditQuoteReceivedNotificationsRegistered = false let creditQuoteReceivedSubscription: Subscription | null = null -export const registerCreditQuoteReceivedNotifications = ( +export const registerCreditQuoteReceivedNotifications = async ( handler?: NotificationActionHandler, ) => { if (areCreditQuoteReceivedNotificationsRegistered) { @@ -407,6 +408,22 @@ export const registerCreditQuoteReceivedNotifications = ( handler || handleHighlightRfqAction, ) + // we do not want to duplicate the subscription to the common quotes feed + // as this will duplicate notifications + // .. bit of TS type torture .. otherwise we cannot cleanly query the isView + const isView = (fin.View.me as Me).isView + if (isView) { + const allApps = await fin.System.getAllApplications() + const parentLauncherPlatform = allApps.find( + (app) => + app.uuid.startsWith("adaptive-workspace-provider-local") || + app.uuid.startsWith("reactive-launcher"), + ) + if (parentLauncherPlatform) { + return + } + } + creditQuoteReceivedSubscription = lastQuoteReceived$.subscribe({ next: (quote) => { sendCreditQuoteReceivedNotification(quote)