From a123976646e59a1df07968c39cc16e61882ca290 Mon Sep 17 00:00:00 2001 From: jaredvu Date: Wed, 6 Sep 2023 13:53:38 -0700 Subject: [PATCH] notification adjustments --- package.json | 2 +- pnpm-lock.yaml | 8 +- src/components/ComboboxMenu.tsx | 4 - src/constants/localization.ts | 2 + src/constants/notifications.ts | 28 ++++--- src/hooks/useNotificationTypes.tsx | 77 +++++++++---------- src/hooks/useNotifications.tsx | 39 ++++------ src/lib/renderSvgToDataUrl.ts | 6 +- .../DetailsDialog/OrderDetailsDialog.tsx | 17 ++-- src/views/menus/NotificationsMenu.tsx | 16 ++-- 10 files changed, 103 insertions(+), 96 deletions(-) diff --git a/package.json b/package.json index 31fe544..893d7bc 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "@cosmjs/proto-signing": "^0.31.0", "@cosmjs/stargate": "^0.31.0", "@cosmjs/tendermint-rpc": "^0.31.0", - "@dydxprotocol/abacus": "^0.4.7", + "@dydxprotocol/abacus": "^0.4.8", "@dydxprotocol/v4-client-js": "^0.32.0", "@dydxprotocol/v4-localization": "^0.0.25", "@ethersproject/providers": "^5.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6cc1d9f..be83a15 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -27,8 +27,8 @@ dependencies: specifier: ^0.31.0 version: 0.31.0 '@dydxprotocol/abacus': - specifier: ^0.4.7 - version: 0.4.7 + specifier: ^0.4.8 + version: 0.4.8 '@dydxprotocol/v4-client-js': specifier: ^0.32.0 version: 0.32.0 @@ -1145,8 +1145,8 @@ packages: resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} dev: true - /@dydxprotocol/abacus@0.4.7: - resolution: {integrity: sha512-3c7Dx5H+KeXZYDpAOJs6fqxSNerKqNSD4tuMNn5F7RXAPWUOU8ADQHAFPnrP1PgTxPjfZBZQzLIziKpyXww4NQ==} + /@dydxprotocol/abacus@0.4.8: + resolution: {integrity: sha512-hX31m+a5Bwbs4dr5raPDtnflwUzEu/9BIt+LOlQgt1YOViuIse8dozgpgCHvqvA8MttqY3QGPalx5iRB8j8vtw==} dev: false /@dydxprotocol/dydxjs@0.3.0: diff --git a/src/components/ComboboxMenu.tsx b/src/components/ComboboxMenu.tsx index e8fa87d..d986610 100644 --- a/src/components/ComboboxMenu.tsx +++ b/src/components/ComboboxMenu.tsx @@ -80,8 +80,6 @@ export const ComboboxMenu = ( ( void; + isNew?: boolean; + }) => void; lastUpdated: number; }) => void; @@ -79,15 +85,15 @@ export type Notifications = Record>; /** Notification display data derived from app state at runtime. */ export type NotificationDisplayData = { - icon?: React.ReactNode; + icon?: React.ReactElement; title?: string; - description?: React.ReactNode; + description?: ReactNode; - customContent?: React.ReactNode; + customContent?: ReactNode; - customMenuContent?: React.ReactNode; + customMenuContent?: ReactNode; actionDescription?: string; diff --git a/src/hooks/useNotificationTypes.tsx b/src/hooks/useNotificationTypes.tsx index 9eacc54..5cf8a6c 100644 --- a/src/hooks/useNotificationTypes.tsx +++ b/src/hooks/useNotificationTypes.tsx @@ -1,27 +1,27 @@ import { useCallback, useEffect, useMemo } from 'react'; import { useSelector, shallowEqual, useDispatch } from 'react-redux'; import { groupBy } from 'lodash'; +import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js'; import { AbacusOrderStatus, ORDER_SIDES, ORDER_STATUS_STRINGS } from '@/constants/abacus'; import { DialogTypes } from '@/constants/dialogs'; import { STRING_KEYS } from '@/constants/localization'; import { type NotificationTypeConfig, NotificationType } from '@/constants/notifications'; -import { ORDER_SIDE_STRINGS, TRADE_TYPE_STRINGS, TradeTypes } from '@/constants/trade'; +import { ORDER_SIDE_STRINGS } from '@/constants/trade'; import { useLocalNotifications } from '@/hooks/useLocalNotifications'; +import { AssetIcon } from '@/components/AssetIcon'; import { Icon, IconName } from '@/components/Icon'; import { Output, OutputType } from '@/components/Output'; +import { OrderStatusIcon } from '@/views/OrderStatusIcon'; import { TransferStatusToast } from '@/views/TransferStatus'; +import { TransferStatusSteps } from '@/views/TransferStatusSteps'; import { getSubaccountFills, getSubaccountOrders } from '@/state/accountSelectors'; import { openDialog } from '@/state/dialogs'; -import { OrderStatusIcon } from '@/views/OrderStatusIcon'; - import { useStringGetter } from './useStringGetter'; -import { TransferStatusSteps } from '@/views/TransferStatusSteps'; -import { TESTNET_CHAIN_ID } from '@dydxprotocol/v4-client-js'; export const notificationTypes = [ { @@ -58,34 +58,33 @@ export const notificationTypes = [ } : undefined); - if (order) - trigger( - order.id, - { - icon: ( - - ), - title: `${stringGetter({ - key: TRADE_TYPE_STRINGS[order.type.rawValue as TradeTypes].tradeTypeKey, - })} ${ - order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0 - ? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED }) - : stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] }) - }`, - description: `${stringGetter({ - key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]], - })} ${order.size} ${order.marketId} @ $${order.price}`, - actionDescription: 'View Order', - actionAltText: 'View this order in the Orders tab or the Notifications menu.', - toastSensitivity: - order.status === AbacusOrderStatus.pending ? 'foreground' : 'background', - toastDuration: 5000, - }, - [order.status.name, order.size], - !order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated - ); + if (order) console.log(order); + trigger({ + id: order.id, + displayData: { + icon: , //, + title: `${stringGetter({ + key: order.resources.typeStringKey ?? '', + })} ${ + order.status === AbacusOrderStatus.open && (order?.totalFilled ?? 0) > 0 + ? stringGetter({ key: STRING_KEYS.PARTIALLY_FILLED }) + : stringGetter({ key: ORDER_STATUS_STRINGS[order.status.name] }) + }`, + + description: `${stringGetter({ + key: ORDER_SIDE_STRINGS[ORDER_SIDES[order.side.name]], + })} ${order.size} ${order.marketId} @ $${order.price}`, + actionDescription: 'View Order', + actionAltText: 'View this order in the Orders tab or the Notifications menu.', + toastSensitivity: + order.status === AbacusOrderStatus.pending ? 'foreground' : 'background', + toastDuration: Infinity, + }, + updateKey: [order.status.name, order.size], + isNew: !order.createdAtMilliseconds || order.createdAtMilliseconds > lastUpdated, + }); } - }, [orderIds]); + }, [orderIds, stringGetter]); }, useNotificationAction: () => { @@ -120,9 +119,9 @@ export const notificationTypes = [ const finished = Boolean(status) && status?.squidTransactionStatus !== 'ongoing'; const type = toChainId === TESTNET_CHAIN_ID ? 'deposit' : 'withdraw'; - trigger( - txHash, - { + trigger({ + id: txHash, + displayData: { icon: , title: stringGetter({ key: getTitleStringKey(type, finished) }), // TODO: confirm with design what the description should be @@ -142,10 +141,10 @@ export const notificationTypes = [ customMenuContent: !finished && , toastSensitivity: 'foreground', }, - [] - ); + updateKey: [], + }); } - }, [transferNotifications]); + }, [transferNotifications, stringGetter]); }, - }, + } as NotificationTypeConfig, ] satisfies NotificationTypeConfig[]; diff --git a/src/hooks/useNotifications.tsx b/src/hooks/useNotifications.tsx index 6c97009..aff09b5 100644 --- a/src/hooks/useNotifications.tsx +++ b/src/hooks/useNotifications.tsx @@ -92,7 +92,7 @@ const useNotificationsContext = () => { for (const { type, useTrigger } of notificationTypes) useTrigger({ trigger: useCallback( - (id, displayData, updateKey, isNew = true) => { + ({ id, displayData, updateKey, isNew = true }) => { const key = getKey({ type, id }); const notification = notifications[key]; @@ -182,28 +182,23 @@ const useNotificationsContext = () => { const iconUrl = displayData.icon && (await renderSvgToDataUrl(displayData.icon).catch(() => undefined)); - const pushNotification = new globalThis.Notification(displayData.title, { - renotify: true, - tag: getKey(notification), - data: notification, - body: displayData.description, - icon: iconUrl ?? '/favicon.svg', - badge: iconUrl ?? '/favicon.svg', - image: iconUrl ?? '/favicon.svg', - vibrate: displayData.toastSensitivity === 'foreground', - requireInteraction: displayData.toastDuration === Infinity, - // actions: [ - // { - // action: displayData.actionDescription, - // title: displayData.actionDescription, - // } - // ].slice(0, globalThis.Notification.maxActions), - }); + if (displayData.title) { + const pushNotification = new globalThis.Notification(displayData.title, { + renotify: true, + tag: getKey(notification), + data: notification, + body: displayData.description ?? '', + icon: (iconUrl as string) ?? '/favicon.svg', + badge: (iconUrl as string) ?? '/favicon.svg', + image: (iconUrl as string) ?? '/favicon.svg', + requireInteraction: displayData.toastDuration === Infinity, + }); - pushNotification.addEventListener('click', () => { - onNotificationAction(notification); - markSeen(notification); - }); + pushNotification.addEventListener('click', () => { + onNotificationAction(notification); + markSeen(notification); + }); + } } setPushNotificationsLastUpdated(Date.now()); diff --git a/src/lib/renderSvgToDataUrl.ts b/src/lib/renderSvgToDataUrl.ts index 13ee329..2df8e55 100644 --- a/src/lib/renderSvgToDataUrl.ts +++ b/src/lib/renderSvgToDataUrl.ts @@ -21,8 +21,10 @@ const applyComputedStyles = (html: string) => { const toDataUrl = (bytes: string, type = 'image/svg+xml') => new Promise((resolve, reject) => { Object.assign(new FileReader(), { - onload: (e) => resolve(e.target.result), - onerror: (e) => reject(e.target.error), + onload: (e: { + target: { result: string | ArrayBuffer | PromiseLike | null }; + }) => resolve(e.target.result), + onerror: (e: { target: { error: Error } }) => reject(e.target.error), }).readAsDataURL(new File([bytes], '', { type })); }); diff --git a/src/views/dialogs/DetailsDialog/OrderDetailsDialog.tsx b/src/views/dialogs/DetailsDialog/OrderDetailsDialog.tsx index ce09538..9294f5e 100644 --- a/src/views/dialogs/DetailsDialog/OrderDetailsDialog.tsx +++ b/src/views/dialogs/DetailsDialog/OrderDetailsDialog.tsx @@ -7,7 +7,7 @@ import { layoutMixins } from '@/styles/layoutMixins'; import { AbacusOrderStatus, AbacusOrderTypes, type Nullable } from '@/constants/abacus'; import { ButtonAction } from '@/constants/buttons'; -import { STRING_KEYS } from '@/constants/localization'; +import { STRING_KEYS, StringKey } from '@/constants/localization'; import { AssetIcon } from '@/components/AssetIcon'; import { Button } from '@/components/Button'; @@ -38,6 +38,9 @@ type ElementProps = { setIsOpen: (open: boolean) => void; }; +const isStringKey = (key: Nullable): key is StringKey => + key != null && STRING_KEYS.hasOwnProperty(key); + export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { const stringGetter = useStringGetter(); const dispatch = useDispatch(); @@ -47,7 +50,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { const { cancelOrder } = useSubaccount(); const { - asset = {}, + asset, cancelReason, createdAtMilliseconds, expiresAtMilliseconds, @@ -57,7 +60,7 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { price, reduceOnly, totalFilled, - resources = {}, + resources, size, status, stepSizeDecimals, @@ -109,9 +112,9 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { - {statusStringKey + {isStringKey(statusStringKey) ? stringGetter({ key: statusStringKey }) - : resources.statusStringKey + : isStringKey(resources.statusStringKey) ? stringGetter({ key: resources.statusStringKey }) : undefined} @@ -121,7 +124,9 @@ export const OrderDetailsDialog = ({ orderId, setIsOpen }: ElementProps) => { { key: 'cancel-reason', label: stringGetter({ key: STRING_KEYS.CANCEL_REASON }), - value: cancelReason ? stringGetter({ key: STRING_KEYS[cancelReason] }) : undefined, + value: isStringKey(cancelReason) + ? stringGetter({ key: STRING_KEYS[cancelReason] }) + : undefined, }, { key: 'amount', diff --git a/src/views/menus/NotificationsMenu.tsx b/src/views/menus/NotificationsMenu.tsx index 348f7d4..2dcdbc7 100644 --- a/src/views/menus/NotificationsMenu.tsx +++ b/src/views/menus/NotificationsMenu.tsx @@ -1,10 +1,12 @@ import React, { useMemo } from 'react'; - import { groupBy } from 'lodash'; +import styled from 'styled-components'; +import { STRING_KEYS } from '@/constants/localization'; import { type Notification, NotificationStatus } from '@/constants/notifications'; - +import { useStringGetter } from '@/hooks'; import { useNotifications } from '@/hooks/useNotifications'; +import { CloseIcon } from '@/icons'; import { Button } from '@/components/Button'; import { ButtonAction, ButtonSize } from '@/constants/buttons'; @@ -13,9 +15,7 @@ import { DialogPlacement } from '@/components/Dialog'; import { Output, OutputType } from '@/components/Output'; import { IconButton } from '@/components/IconButton'; import { Toolbar } from '@/components/Toolbar'; -import { CloseIcon } from '@/icons'; -import styled from 'styled-components'; import { layoutMixins } from '@/styles/layoutMixins'; type ElementProps = { @@ -27,6 +27,8 @@ export const NotificationsMenu = ({ slotTrigger, placement = DialogPlacement.Sidebar, }: ElementProps) => { + const stringGetter = useStringGetter(); + const { notifications, getDisplayData, @@ -69,7 +71,7 @@ export const NotificationsMenu = ({ .map(([status, notifications]) => ({ group: status, groupLabel: { - [NotificationStatus.Triggered]: 'New', + [NotificationStatus.Triggered]: stringGetter({ key: STRING_KEYS.NEW }), // [NotificationStatus.Updated]: 'Updates', [NotificationStatus.Seen]: 'Seen', [NotificationStatus.Cleared]: 'Archived', @@ -135,7 +137,7 @@ export const NotificationsMenu = ({ isOpen={isMenuOpen || placement === DialogPlacement.Inline} setIsOpen={setIsMenuOpen} items={items} - title="Notifications" + title={stringGetter({ key: STRING_KEYS.NOTIFICATIONS })} slotTrigger={ <$TriggerContainer> {slotTrigger} @@ -168,7 +170,7 @@ export const NotificationsMenu = ({ ), }} > - Clear All + {stringGetter({ key: STRING_KEYS.CLEAR })} }