From d750b281ead50805be89e82a78ba64c7cc9c99a0 Mon Sep 17 00:00:00 2001 From: mike-dydx Date: Tue, 2 Jul 2024 08:45:41 -0400 Subject: [PATCH 1/6] add notional value to position card --- .../dydxPortfolioPositionsViewPresenter.swift | 2 + .../Sections/dydxPortfolioPositionsView.swift | 172 +++++++++--------- 2 files changed, 92 insertions(+), 82 deletions(-) diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift index e3428ffab..234136b5e 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift @@ -119,7 +119,9 @@ class dydxPortfolioPositionsViewPresenter: HostedViewPresenter Void)? = nil, onMarginEditAction: (() -> Void)? = nil) { self.size = size + self.notionalValue = notionalValue self.token = token self.sideText = sideText self.leverage = leverage @@ -51,6 +53,7 @@ public class dydxPortfolioPositionItemViewModel: PlatformViewModel { } @Published public var size: String? + @Published public var notionalValue: String? @Published public var token: TokenTextViewModel? @Published public var sideText = SideTextViewModel() @Published public var leverage: String? @@ -69,6 +72,7 @@ public class dydxPortfolioPositionItemViewModel: PlatformViewModel { public static var previewValue: dydxPortfolioPositionItemViewModel { let item = dydxPortfolioPositionItemViewModel( size: "299", + notionalValue: "$420.69", token: .previewValue, sideText: .previewValue, leverage: "0.01x", @@ -94,13 +98,12 @@ public class dydxPortfolioPositionItemViewModel: PlatformViewModel { } return AnyView( - VStack { + VStack(spacing: 20) { self.createTopView(parentStyle: style) self.createBottomView(parentStyle: style) } - .frame(height: 120) - .padding(.vertical, 12) - .padding(.horizontal, 8) + .padding(.vertical, 16) + .padding(.horizontal, 20) .themeGradient(background: .layer3, gradientType: self.gradientType) .cornerRadius(16) .onTapGesture { [weak self] in @@ -112,89 +115,90 @@ public class dydxPortfolioPositionItemViewModel: PlatformViewModel { } private func createTopView(parentStyle: ThemeStyle) -> some View { - let icon = self.createLogo(parentStyle: parentStyle) - let main = self.createMain(parentStyle: parentStyle) - - return PlatformTableViewCellViewModel(logo: icon.wrappedViewModel, - main: main.wrappedViewModel) - .createView(parentStyle: parentStyle) + HStack(spacing: 0) { + createLogo(parentStyle: parentStyle) + Spacer(minLength: 8) + createTopRowStats(parentStyle: parentStyle) + } } private func createBottomView(parentStyle: ThemeStyle) -> some View { - GeometryReader { geo in - HStack(alignment: .top) { + SingleAxisGeometryReader(axis: .horizontal, alignment: .center) { width in + let numElements: CGFloat = 3.0 + let spacing: CGFloat = 8 + let elementWidth = max(0, (width - (numElements - 1) * spacing) / numElements) + return HStack(alignment: .top, spacing: 8) { VStack(alignment: .leading, spacing: 4) { - Text(DataLocalizer.localize(path: "APP.GENERAL.INDEX_ENTRY")) - .themeFont(fontSize: .smaller) - .themeColor(foreground: .textTertiary) - - Text(self.indexPrice ?? "") - .themeFont(fontSize: .small) - .themeColor(foreground: .textPrimary) - .minimumScaleFactor(0.5) - - Text(self.entryPrice ?? "") - .themeFont(fontSize: .smaller) - .themeColor(foreground: .textTertiary) - .minimumScaleFactor(0.5) + Group { + Text(DataLocalizer.localize(path: "APP.GENERAL.INDEX_ENTRY")) + .themeFont(fontSize: .smaller) + .themeColor(foreground: .textTertiary) + + Text(self.indexPrice ?? "") + .themeFont(fontSize: .small) + .themeColor(foreground: .textPrimary) + + Text(self.entryPrice ?? "") + .themeFont(fontSize: .smaller) + .themeColor(foreground: .textTertiary) + } + .lineLimit(1) + .minimumScaleFactor(0.5) + .frame(width: elementWidth, alignment: .leading) } - .leftAligned() - .frame(width: geo.size.width / 3) VStack(alignment: .leading, spacing: 4) { - Text(DataLocalizer.localize(path: "APP.GENERAL.PROFIT_AND_LOSS")) - .themeFont(fontSize: .smaller) - .themeColor(foreground: .textTertiary) - - self.unrealizedPnl?.createView(parentStyle: parentStyle.themeFont(fontType: .number, fontSize: .small)) - Text(self.unrealizedPnlPercent) - .themeFont(fontSize: .smaller) - .themeColor(foreground: .textTertiary) + Group { + Text(DataLocalizer.localize(path: "APP.GENERAL.PROFIT_AND_LOSS")) + .themeFont(fontSize: .smaller) + .themeColor(foreground: .textTertiary) + + self.unrealizedPnl?.createView(parentStyle: parentStyle.themeFont(fontType: .number, fontSize: .small)) + Text(self.unrealizedPnlPercent) + .themeFont(fontSize: .smaller) + .themeColor(foreground: .textTertiary) + } + .frame(width: elementWidth, alignment: .leading) } - .leftAligned() - .frame(width: geo.size.width / 3) VStack(alignment: .leading, spacing: 4) { - Text(DataLocalizer.localize(path: "APP.GENERAL.MARGIN")) - .themeFont(fontSize: .smaller) - .themeColor(foreground: .textTertiary) - - HStack { - VStack(alignment: .leading, spacing: 4) { - Text(self.marginValue) - .themeFont(fontSize: .small) - .themeColor(foreground: .textPrimary) - .minimumScaleFactor(0.5) - - Text(self.marginMode) - .themeFont(fontSize: .smaller) - .themeColor(foreground: .textTertiary) - .minimumScaleFactor(0.5) - } - - Spacer() - - if self.isMarginAdjustable { + Group { + Text(DataLocalizer.localize(path: "APP.GENERAL.MARGIN")) + .themeFont(fontSize: .smaller) + .themeColor(foreground: .textTertiary) + + HStack(spacing: 8) { + VStack(alignment: .leading, spacing: 4) { + Text(self.marginValue) + .themeFont(fontSize: .small) + .themeColor(foreground: .textPrimary) + + Text(self.marginMode) + .themeFont(fontSize: .smaller) + .themeColor(foreground: .textTertiary) + } - let buttonContent = PlatformIconViewModel(type: .asset(name: "icon_edit", bundle: Bundle.dydxView), - size: CGSize(width: 20, height: 20), - templateColor: .textSecondary) - PlatformButtonViewModel(content: buttonContent, - type: PlatformButtonType.iconType) { [weak self] in - self?.handler?.onMarginEditAction?() + if self.isMarginAdjustable { + + let buttonContent = PlatformIconViewModel(type: .asset(name: "icon_edit", bundle: Bundle.dydxView), + size: CGSize(width: 20, height: 20), + templateColor: .textSecondary) + PlatformButtonViewModel(content: buttonContent, + type: PlatformButtonType.iconType) { [weak self] in + self?.handler?.onMarginEditAction?() + } + .createView(parentStyle: parentStyle) + .frame(width: 32, height: 32) + .themeColor(background: .layer6) + .border(borderWidth: 1, cornerRadius: 7, borderColor: ThemeColor.SemanticColor.layer7.color) } - .createView(parentStyle: parentStyle) - .frame(width: 32, height: 32) - .themeColor(background: .layer6) - .border(borderWidth: 1, cornerRadius: 7, borderColor: ThemeColor.SemanticColor.layer7.color) } } + .frame(width: elementWidth, alignment: .leading) } - .leftAligned() - .frame(width: geo.size.width / 3) } + } - .padding(.horizontal, 16) } private func createLogo( parentStyle: ThemeStyle) -> some View { @@ -206,16 +210,22 @@ public class dydxPortfolioPositionItemViewModel: PlatformViewModel { } } - private func createMain(parentStyle: ThemeStyle) -> some View { - VStack(alignment: .leading, spacing: 0) { - HStack(spacing: 2) { - Text(size ?? "") - .themeFont(fontType: .number, fontSize: .small) - - token?.createView(parentStyle: parentStyle.themeFont(fontSize: .smallest)) + private func createTopRowStats(parentStyle: ThemeStyle) -> some View { + HStack(alignment: .top, spacing: 0) { + VStack(alignment: .leading, spacing: 2) { + HStack(spacing: 4) { + Text(size ?? "") + .themeFont(fontType: .base, fontSize: .small) + .themeColor(foreground: .textPrimary) + token?.createView(parentStyle: parentStyle.themeFont(fontSize: .smallest)) + } + Text(notionalValue ?? "") + .themeFont(fontSize: .smaller) + .themeColor(foreground: .textTertiary) } - HStack(spacing: 2) { + Spacer(minLength: 8) + HStack(alignment: .top, spacing: 2) { sideText .createView(parentStyle: parentStyle.themeFont(fontSize: .smaller)) Text("@") @@ -223,11 +233,10 @@ public class dydxPortfolioPositionItemViewModel: PlatformViewModel { .themeColor(foreground: .textTertiary) Text(leverage ?? "") - .themeFont(fontType: .number, fontSize: .smaller) + .themeFont(fontType: .base, fontSize: .smaller) + .themeColor(foreground: .textPrimary) } } - .leftAligned() - .minimumScaleFactor(0.5) } } @@ -282,7 +291,6 @@ public class dydxPortfolioPositionsViewModel: PlatformViewModel { .borderAndClip(style: .circle, borderColor: .borderDefault) Spacer() } - .padding(.horizontal, 16) .frame(width: UIScreen.main.bounds.width - 32) .themeFont(fontSize: .small) .themeColor(foreground: .textTertiary) From 45cea7900eceb9393b08561a1a591e3bb6847359 Mon Sep 17 00:00:00 2001 From: mike-dydx Date: Tue, 2 Jul 2024 09:31:21 -0400 Subject: [PATCH 2/6] remove null checks to display empty state --- .../Receipt/dydxTradeReceiptPresenter.swift | 29 +++++++++---------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Receipt/dydxTradeReceiptPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Receipt/dydxTradeReceiptPresenter.swift index ae54ac04b..b2255dc02 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Receipt/dydxTradeReceiptPresenter.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Receipt/dydxTradeReceiptPresenter.swift @@ -43,17 +43,16 @@ final class dydxTradeReceiptPresenter: dydxReceiptPresenter { AbacusStateManager.shared.state.selectedSubaccountPositions, AbacusStateManager.shared.state.marketMap) .sink { [weak self] input, positions, marketMap in - if let tradeSummary = input.summary, - let marketId = input.marketId, - let market = marketMap[marketId], - let position = positions.first(where: { $0.id == marketId }) { - self?.updateExpectedPrice(tradeSummary: tradeSummary, market: market) - self?.updateLiquidationPrice(position: position, market: market) - self?.updatePositionMargin(position: position) - self?.updatePositionLeverage(position: position) - self?.updateTradingFee(tradeSummary: tradeSummary) - self?.updateTradingRewards(tradeSummary: tradeSummary) - } + let tradeSummary = input.summary + let marketId = input.marketId + let market = marketMap[marketId ?? ""] + let position = positions.first(where: { $0.id == marketId }) + self?.updateExpectedPrice(tradeSummary: tradeSummary, market: market) + self?.updateLiquidationPrice(position: position, market: market) + self?.updatePositionMargin(position: position) + self?.updatePositionLeverage(position: position) + self?.updateTradingFee(tradeSummary: tradeSummary) + self?.updateTradingRewards(tradeSummary: tradeSummary) } .store(in: &subscriptions) @@ -85,16 +84,16 @@ final class dydxTradeReceiptPresenter: dydxReceiptPresenter { } } - private func updateExpectedPrice(tradeSummary: TradeInputSummary?, market: PerpetualMarket) { - let value = dydxFormatter.shared.dollar(number: tradeSummary?.price?.doubleValue, digits: market.configs?.displayTickSizeDecimals?.intValue ?? 0) + private func updateExpectedPrice(tradeSummary: TradeInputSummary?, market: PerpetualMarket?) { + let value = dydxFormatter.shared.dollar(number: tradeSummary?.price?.doubleValue, digits: market?.configs?.displayTickSizeDecimals?.intValue ?? 2) expectedPriceViewModel.title = DataLocalizer.localize(path: "APP.TRADE.EXPECTED_PRICE") expectedPriceViewModel.value = value } - private func updateLiquidationPrice(position: SubaccountPosition?, market: PerpetualMarket) { + private func updateLiquidationPrice(position: SubaccountPosition?, market: PerpetualMarket?) { let title = DataLocalizer.localize(path: "APP.TRADE.LIQUIDATION_PRICE_SHORT") let unit = AmountTextModel.Unit.dollar - let tickSize = market.configs?.displayTickSizeDecimals?.intValue.asNsNumber + let tickSize = market?.configs?.displayTickSizeDecimals?.intValue.asNsNumber ?? 2 liquidationPriceViewModel.title = title liquidationPriceViewModel.value = createAmountChangeViewModel(title: title, tradeState: position?.liquidationPrice, tickSize: tickSize, unit: unit) } From 3500ff015e5c3e4ec6b92be78d5f0e404584a94b Mon Sep 17 00:00:00 2001 From: mike-dydx Date: Tue, 2 Jul 2024 10:51:45 -0400 Subject: [PATCH 3/6] cache displayed alerts to not display again --- .../Workers/dydxAlertsWorker.swift | 41 +++++++++++++++---- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/GlobalWorkers/Workers/dydxAlertsWorker.swift b/dydx/dydxPresenters/dydxPresenters/_v4/GlobalWorkers/Workers/dydxAlertsWorker.swift index 7faa90e94..44252b1bf 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/GlobalWorkers/Workers/dydxAlertsWorker.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/GlobalWorkers/Workers/dydxAlertsWorker.swift @@ -32,14 +32,30 @@ extension Abacus.NotificationType { } final class dydxAlertsWorker: BaseWorker { - private var handledAlertHashes = Set() + static let userDefaultsKey: String = "dydxAlertsWorker" + + // do not set directly, use `markAlertAsHandled` instead + private var handledAlertIds = { + if let handledAlertIds = UserDefaults.standard.array(forKey: dydxAlertsWorker.userDefaultsKey) as? [String] { + return Set(handledAlertIds) + } else { + return Set() + } + }() + + private func markAlertAsHandled(_ alert: Abacus.Notification) { + if handledAlertIds.insert(alert.betterId).inserted { + var handledAlerts = Array(handledAlertIds) + handledAlerts.append(alert.betterId) + UserDefaults.standard.set(handledAlerts, forKey: dydxAlertsWorker.userDefaultsKey) + } + } override func start() { super.start() AbacusStateManager.shared.state.alerts .removeDuplicates() - .receive(on: RunLoop.main) .sink { [weak self] alerts in self?.updateAlerts(alerts: alerts) } @@ -49,7 +65,7 @@ final class dydxAlertsWorker: BaseWorker { private func updateAlerts(alerts: [Abacus.Notification]) { alerts // don't display an alert which has already been handled - .filter { !handledAlertHashes.contains($0.hashValue) } + .filter { !handledAlertIds.contains($0.betterId) } // display alerts in chronological order they were received .sorted { $0.updateTimeInMilliseconds < $1.updateTimeInMilliseconds } .forEach { alert in @@ -58,13 +74,22 @@ final class dydxAlertsWorker: BaseWorker { Router.shared?.navigate(to: RoutingRequest(path: link!), animated: true, completion: nil) }] : nil if SettingsStore.shared?.shouldDisplayInAppNotifications != false { - ErrorInfo.shared?.info(title: alert.title, - message: alert.text, - type: alert.type.infoType, - error: nil, time: nil, actions: actions) + DispatchQueue.main.async { + ErrorInfo.shared?.info(title: alert.title, + message: alert.text, + type: alert.type.infoType, + error: nil, time: nil, actions: actions) + } } // add to alert ids set to avoid double handling - handledAlertHashes.insert(alert.hashValue) + markAlertAsHandled(alert) } } } + +private extension Abacus.Notification { + // id is just the fill id which is not guaranteed unique + var betterId: String { + "\(self.id)\(self.updateTimeInMilliseconds)\(self.title)" + } +} From 1018befdb6c14f5e5be70ddddcbae9f9aebb4ea9 Mon Sep 17 00:00:00 2001 From: mike-dydx Date: Tue, 2 Jul 2024 11:18:41 -0400 Subject: [PATCH 4/6] display liq/oracle price --- .../dydxPortfolioPositionsViewPresenter.swift | 6 +++++- .../Sections/dydxPortfolioPositionsView.swift | 11 ++++++----- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift index 234136b5e..ef276ef3b 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/Portfolio/Components/dydxPortfolioPositionsViewPresenter.swift @@ -138,7 +138,11 @@ class dydxPortfolioPositionsViewPresenter: HostedViewPresenter Date: Tue, 2 Jul 2024 13:20:29 -0400 Subject: [PATCH 5/6] Revert "cache displayed alerts to not display again" This reverts commit 3500ff015e5c3e4ec6b92be78d5f0e404584a94b. --- .../Workers/dydxAlertsWorker.swift | 41 ++++--------------- 1 file changed, 8 insertions(+), 33 deletions(-) diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/GlobalWorkers/Workers/dydxAlertsWorker.swift b/dydx/dydxPresenters/dydxPresenters/_v4/GlobalWorkers/Workers/dydxAlertsWorker.swift index 44252b1bf..7faa90e94 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/GlobalWorkers/Workers/dydxAlertsWorker.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/GlobalWorkers/Workers/dydxAlertsWorker.swift @@ -32,30 +32,14 @@ extension Abacus.NotificationType { } final class dydxAlertsWorker: BaseWorker { - static let userDefaultsKey: String = "dydxAlertsWorker" - - // do not set directly, use `markAlertAsHandled` instead - private var handledAlertIds = { - if let handledAlertIds = UserDefaults.standard.array(forKey: dydxAlertsWorker.userDefaultsKey) as? [String] { - return Set(handledAlertIds) - } else { - return Set() - } - }() - - private func markAlertAsHandled(_ alert: Abacus.Notification) { - if handledAlertIds.insert(alert.betterId).inserted { - var handledAlerts = Array(handledAlertIds) - handledAlerts.append(alert.betterId) - UserDefaults.standard.set(handledAlerts, forKey: dydxAlertsWorker.userDefaultsKey) - } - } + private var handledAlertHashes = Set() override func start() { super.start() AbacusStateManager.shared.state.alerts .removeDuplicates() + .receive(on: RunLoop.main) .sink { [weak self] alerts in self?.updateAlerts(alerts: alerts) } @@ -65,7 +49,7 @@ final class dydxAlertsWorker: BaseWorker { private func updateAlerts(alerts: [Abacus.Notification]) { alerts // don't display an alert which has already been handled - .filter { !handledAlertIds.contains($0.betterId) } + .filter { !handledAlertHashes.contains($0.hashValue) } // display alerts in chronological order they were received .sorted { $0.updateTimeInMilliseconds < $1.updateTimeInMilliseconds } .forEach { alert in @@ -74,22 +58,13 @@ final class dydxAlertsWorker: BaseWorker { Router.shared?.navigate(to: RoutingRequest(path: link!), animated: true, completion: nil) }] : nil if SettingsStore.shared?.shouldDisplayInAppNotifications != false { - DispatchQueue.main.async { - ErrorInfo.shared?.info(title: alert.title, - message: alert.text, - type: alert.type.infoType, - error: nil, time: nil, actions: actions) - } + ErrorInfo.shared?.info(title: alert.title, + message: alert.text, + type: alert.type.infoType, + error: nil, time: nil, actions: actions) } // add to alert ids set to avoid double handling - markAlertAsHandled(alert) + handledAlertHashes.insert(alert.hashValue) } } } - -private extension Abacus.Notification { - // id is just the fill id which is not guaranteed unique - var betterId: String { - "\(self.id)\(self.updateTimeInMilliseconds)\(self.title)" - } -} From ba3fd6ca4bffc71d425d4bb79fc1300d881fd39b Mon Sep 17 00:00:00 2001 From: mike-dydx Date: Tue, 2 Jul 2024 14:01:59 -0400 Subject: [PATCH 6/6] use notional instead of value total --- .../MarketInfo/Components/dydxMarketPositionViewPresenter.swift | 2 +- .../Components/dydxPortfolioPositionsViewPresenter.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketPositionViewPresenter.swift b/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketPositionViewPresenter.swift index 59f758413..f52ef3e95 100644 --- a/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketPositionViewPresenter.swift +++ b/dydx/dydxPresenters/dydxPresenters/_v4/MarketInfo/Components/dydxMarketPositionViewPresenter.swift @@ -123,7 +123,7 @@ class dydxMarketPositionViewPresenter: HostedViewPresenter