Skip to content

Commit

Permalink
fix: track unread counts more accurately (#1382)
Browse files Browse the repository at this point in the history
  • Loading branch information
tyleroooo authored Dec 19, 2024
1 parent 82a2b4b commit 963cb30
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 31 deletions.
60 changes: 60 additions & 0 deletions src/hooks/useSeen.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { useEffect, useRef } from 'react';

import { selectRawIndexerHeightData } from '@/abacus-ts/selectors/base';
import { shallowEqual } from 'react-redux';

import { getUserWalletAddress } from '@/state/accountSelectors';
import { setSeenFills, setSeenOpenOrders, setSeenOrderHistory } from '@/state/accountUiMemory';
import { getSelectedNetwork } from '@/state/appSelectors';
import { useAppDispatch, useAppSelector } from '@/state/appTypes';

export function useViewPanel(
market: string | undefined,
kind: 'fills' | 'openOrders' | 'orderHistory'
) {
const networkId = useAppSelector(getSelectedNetwork);
const walletId = useAppSelector(getUserWalletAddress);
const height = useAppSelector(selectRawIndexerHeightData);
const lastSetCore = useRef<any[]>([]);

const dispatch = useAppDispatch();
const actionCreator = (
{
fills: setSeenFills,
openOrders: setSeenOpenOrders,
orderHistory: setSeenOrderHistory,
} as const
)[kind];

const componentWillUnmount = useComponentWillUnmount();

useEffect(() => {
if (height != null && walletId != null) {
// only set once for a given set of configurations
// effectively, view on mount as soon as we load height
const thisCore = [market, actionCreator, networkId, walletId];
if (!shallowEqual(lastSetCore.current, thisCore)) {
lastSetCore.current = thisCore;
dispatch(actionCreator({ scope: { networkId, walletId }, market, height }));
}
}
return () => {
if (componentWillUnmount.current) {
if (height != null && walletId != null) {
dispatch(actionCreator({ scope: { networkId, walletId }, market, height }));
}
}
};
}, [market, actionCreator, networkId, walletId, height, dispatch, componentWillUnmount]);
}

function useComponentWillUnmount() {
const componentWillUnmount = useRef(false);
useEffect(() => {
componentWillUnmount.current = false;
return () => {
componentWillUnmount.current = true;
};
}, []);
return componentWillUnmount;
}
28 changes: 18 additions & 10 deletions src/pages/trade/HorizontalPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ import {
calculateShouldRenderActionsInPositionsTable,
} from '@/state/accountCalculators';
import {
createGetUnseenFillsCount,
createGetUnseenOrdersCount,
getCurrentMarketTradeInfoNumbers,
getHasUnseenFillUpdates,
getHasUnseenOrderUpdates,
getTradeInfoNumbers,
} from '@/state/accountSelectors';
import { useAppSelector } from '@/state/appTypes';
Expand Down Expand Up @@ -69,14 +69,13 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {

const currentMarketId = useAppSelector(getCurrentMarketId);

const { numTotalPositions, numTotalOpenOrders, numTotalUnseenFills } =
useAppSelector(getTradeInfoNumbers, shallowEqual) ?? {};
const { numTotalPositions, numTotalOpenOrders } = useAppSelector(
getTradeInfoNumbers,
shallowEqual
);

const { numOpenOrders, numUnseenFills } =
useAppSelector(getCurrentMarketTradeInfoNumbers, shallowEqual) ?? {};
const { numOpenOrders } = useAppSelector(getCurrentMarketTradeInfoNumbers, shallowEqual);

const hasUnseenOrderUpdates = useAppSelector(getHasUnseenOrderUpdates);
const hasUnseenFillUpdates = useAppSelector(getHasUnseenFillUpdates);
const isAccountViewOnly = useAppSelector(calculateIsAccountViewOnly);
const shouldRenderTriggers = useShouldShowTriggers();
const shouldRenderActions = useParameterizedSelector(
Expand All @@ -85,9 +84,18 @@ export const HorizontalPanel = ({ isOpen = true, setIsOpen }: ElementProps) => {
const isWaitingForOrderToIndex = useAppSelector(getHasUncommittedOrders);
const showCurrentMarket = isTablet || view === PanelView.CurrentMarket;

const fillsTagNumber = shortenNumberForDisplay(
showCurrentMarket ? numUnseenFills : numTotalUnseenFills
const unseenOrders = useParameterizedSelector(
createGetUnseenOrdersCount,
showCurrentMarket ? currentMarketId : undefined
);
const hasUnseenOrderUpdates = unseenOrders > 0;

const numUnseenFills = useParameterizedSelector(
createGetUnseenFillsCount,
showCurrentMarket ? currentMarketId : undefined
);
const hasUnseenFillUpdates = numUnseenFills > 0;
const fillsTagNumber = shortenNumberForDisplay(numUnseenFills);
const ordersTagNumber = shortenNumberForDisplay(
showCurrentMarket ? numOpenOrders : numTotalOpenOrders
);
Expand Down
3 changes: 3 additions & 0 deletions src/state/_store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { runFn } from '@/lib/do';
import { testFlags } from '@/lib/testFlags';

import { accountSlice } from './account';
import { accountUiMemorySlice } from './accountUiMemory';
import { affiliatesSlice } from './affiliates';
import { appSlice } from './app';
import appMiddleware from './appMiddleware';
Expand Down Expand Up @@ -35,6 +36,7 @@ const reducers = {
affiliates: affiliatesSlice.reducer,
app: appSlice.reducer,
appUiConfigs: appUiConfigsSlice.reducer,
accountUiMemory: accountUiMemorySlice.reducer,
assets: assetsSlice.reducer,
configs: configsSlice.reducer,
dialogs: dialogsSlice.reducer,
Expand Down Expand Up @@ -64,6 +66,7 @@ const persistConfig = {
'tradingView',
'wallet',
'appUiConfigs',
'accountUiMemory',
'funkitDeposits',
],
stateReconciler: autoMergeLevel2,
Expand Down
74 changes: 74 additions & 0 deletions src/state/accountSelectors.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { selectRawIndexerHeightData } from '@/abacus-ts/selectors/base';
import { OrderSide } from '@dydxprotocol/v4-client-js';
import BigNumber from 'bignumber.js';
import { groupBy, sum } from 'lodash';
Expand All @@ -18,6 +19,7 @@ import { NUM_PARENT_SUBACCOUNTS, OnboardingState } from '@/constants/account';
import { LEVERAGE_DECIMALS } from '@/constants/numbers';
import { EMPTY_ARR } from '@/constants/objects';

import { mapIfPresent } from '@/lib/do';
import { MustBigNumber } from '@/lib/numbers';
import {
getAverageFillPrice,
Expand All @@ -30,6 +32,8 @@ import {
import { getHydratedPositionData } from '@/lib/positions';

import { type RootState } from './_store';
import { ALL_MARKETS_STRING } from './accountUiMemory';
import { getSelectedNetwork } from './appSelectors';
import { createAppSelector } from './appTypes';
import { getAssets } from './assetsSelectors';
import {
Expand Down Expand Up @@ -687,3 +691,73 @@ export const getUserWalletAddress = (state: RootState) => state.account.wallet?.

export const getUserSubaccountNumber = (state: RootState) =>
state.account.subaccount?.subaccountNumber;

export const getAccountUiMemory = (state: RootState) => state.accountUiMemory;
export const getCurrentAccountMemory = createAppSelector(
[getSelectedNetwork, getUserWalletAddress, getAccountUiMemory],
(networkId, walletId, memory) => memory[walletId ?? '']?.[networkId]
);

export const createGetUnseenOrdersCount = () =>
createAppSelector(
[
getCurrentAccountMemory,
selectRawIndexerHeightData,
getSubaccountOrders,
(state, market: string | undefined) => market,
],
(memory, height, orders, market) => {
if (height == null) {
return 0;
}
const ourOrders =
(market == null ? orders : orders?.filter((o) => o.marketId === market)) ?? EMPTY_ARR;
if (ourOrders.length === 0) {
return 0;
}
if (memory == null) {
return ourOrders.length;
}
const unseen = ourOrders.filter(
(o) =>
(o.updatedAtMilliseconds ?? 0) >
(mapIfPresent(
(memory.seenOpenOrders[o.marketId] ?? memory.seenOpenOrders[ALL_MARKETS_STRING])?.time,
(t) => new Date(t).valueOf()
) ?? 0)
);
return unseen.length;
}
);

export const createGetUnseenFillsCount = () =>
createAppSelector(
[
getCurrentAccountMemory,
selectRawIndexerHeightData,
getSubaccountFills,
(state, market: string | undefined) => market,
],
(memory, height, fills, market) => {
if (height == null) {
return 0;
}
const ourFills =
(market == null ? fills : fills?.filter((o) => o.marketId === market)) ?? EMPTY_ARR;
if (ourFills.length === 0) {
return 0;
}
if (memory == null) {
return ourFills.length;
}
const unseen = ourFills.filter(
(o) =>
o.createdAtMilliseconds >
(mapIfPresent(
(memory.seenFills[o.marketId] ?? memory.seenFills[ALL_MARKETS_STRING])?.time,
(t) => new Date(t).valueOf()
) ?? 0)
);
return unseen.length;
}
);
99 changes: 99 additions & 0 deletions src/state/accountUiMemory.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { HeightResponse } from '@dydxprotocol/v4-client-js';
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';

import { DydxNetwork } from '@/constants/networks';

// NOTE: This app slice is persisted via redux-persist. Changes to this type may require migrations.

export const ALL_MARKETS_STRING = '___ALL_MARKETS___';

// TODO: seen notifications belong in here too
export interface AccountUiMemoryBase {
seenFills: { [marketIdOrAllMarkets: string]: HeightResponse };
seenOpenOrders: { [marketIdOrAllMarkets: string]: HeightResponse };
seenOrderHistory: { [marketIdOrAllMarkets: string]: HeightResponse };
}

type SeenHeightPayload = { height: HeightResponse; market: string | undefined };

export type AccountUiMemoryState = {
[walletId: string]: {
[networkId: string]: AccountUiMemoryBase;
};
};

type AccountScope = {
walletId: string;
networkId: DydxNetwork;
};
type ScopePayload = { scope: AccountScope };

export const initialState: AccountUiMemoryState = {};

function ensureScopePresent(state: AccountUiMemoryState, scope: ScopePayload): AccountUiMemoryBase {
state[scope.scope.walletId] ??= {};
state[scope.scope.walletId]![scope.scope.networkId] ??= {
seenFills: {},
seenOpenOrders: {},
seenOrderHistory: {},
};
return state[scope.scope.walletId]![scope.scope.networkId]!;
}

function setSeen(
state: AccountUiMemoryBase,
key: 'seenFills' | 'seenOpenOrders' | 'seenOrderHistory',
payload: SeenHeightPayload
) {
const maybeAll = state[key][ALL_MARKETS_STRING];
const maybeUs = state[key][payload.market ?? ALL_MARKETS_STRING];
// make sure we are more than existing and more than base
if (
(maybeAll == null || maybeAll.height < payload.height.height) &&
(maybeUs == null || maybeUs.height < payload.height.height)
) {
state[key][payload.market ?? ALL_MARKETS_STRING] = payload.height;
}
// if all markets, remove all smaller
if (payload.market == null) {
Object.keys(state[key]).forEach((marketOrAll) => {
if (marketOrAll === ALL_MARKETS_STRING) {
return;
}
if (state[key][marketOrAll]!.height < payload.height.height) {
delete state[key][marketOrAll];
}
});
}
}

export const accountUiMemorySlice = createSlice({
name: 'accountUiMemory',
initialState,
reducers: {
setSeenFills: (
state: AccountUiMemoryState,
{ payload }: PayloadAction<SeenHeightPayload & ScopePayload>
) => {
const thisState = ensureScopePresent(state, payload);
setSeen(thisState, 'seenFills', payload);
},
setSeenOpenOrders: (
state: AccountUiMemoryState,
{ payload }: PayloadAction<SeenHeightPayload & ScopePayload>
) => {
const thisState = ensureScopePresent(state, payload);
setSeen(thisState, 'seenOpenOrders', payload);
},
setSeenOrderHistory: (
state: AccountUiMemoryState,
{ payload }: PayloadAction<SeenHeightPayload & ScopePayload>
) => {
const thisState = ensureScopePresent(state, payload);
setSeen(thisState, 'seenOrderHistory', payload);
},
},
});

export const { setSeenFills, setSeenOpenOrders, setSeenOrderHistory } =
accountUiMemorySlice.actions;
12 changes: 3 additions & 9 deletions src/views/tables/FillsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { forwardRef, Key, useEffect, useMemo } from 'react';
import { forwardRef, Key, useMemo } from 'react';

import { Nullable } from '@dydxprotocol/v4-abacus';
import { OrderSide } from '@dydxprotocol/v4-client-js';
Expand All @@ -13,6 +13,7 @@ import { STRING_KEYS, type StringGetterFunction } from '@/constants/localization
import { EMPTY_ARR } from '@/constants/objects';

import { useBreakpoints } from '@/hooks/useBreakpoints';
import { useViewPanel } from '@/hooks/useSeen';
import { useStringGetter } from '@/hooks/useStringGetter';

import { tradeViewMixins } from '@/styles/tradeViewMixins';
Expand All @@ -28,7 +29,6 @@ import { TableColumnHeader } from '@/components/Table/TableColumnHeader';
import { PageSize } from '@/components/Table/TablePaginationRow';
import { TagSize } from '@/components/Tag';

import { viewedFills } from '@/state/account';
import { getCurrentMarketFills, getSubaccountFills } from '@/state/accountSelectors';
import { useAppDispatch, useAppSelector } from '@/state/appTypes';
import { getAssets } from '@/state/assetsSelectors';
Expand Down Expand Up @@ -370,13 +370,7 @@ export const FillsTable = forwardRef(
const allPerpetualMarkets = orEmptyRecord(useAppSelector(getPerpetualMarkets, shallowEqual));
const allAssets = orEmptyRecord(useAppSelector(getAssets, shallowEqual));

useEffect(() => {
// marked fills as seen both on mount and dismount (i.e. new fill came in while fills table is being shown)
dispatch(viewedFills(currentMarket));
return () => {
dispatch(viewedFills(currentMarket));
};
}, [currentMarket, dispatch]);
useViewPanel(currentMarket, 'fills');

const symbol = mapIfPresent(currentMarket, (market) =>
mapIfPresent(allPerpetualMarkets[market]?.assetId, (assetId) => allAssets[assetId]?.id)
Expand Down
Loading

0 comments on commit 963cb30

Please sign in to comment.