Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 60 additions & 11 deletions DashWallet/Sources/UI/Explore Dash/UsernameMarketplaceScreen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ final class UsernameMarketplaceViewModel: ObservableObject {
/// `updateSearch` using the SDK's own normalizer (DPNS folds
/// look-alike characters, so a plain lowercase compare would lie).
@Published var registrableQueryLabel: String?
/// Network contest state of a contested-eligible `registrableQueryLabel`,
/// filled best-effort after the search results land. A label with no
/// DPNS document can still be mid-vote — without this the row would
/// claim "Available" for a name already being contested.
@Published var queryContest: UsernameMarketplaceService.ContestPrecheck?

let service = UsernameMarketplaceService()
private var searchTask: Task<Void, Never>?
Expand Down Expand Up @@ -94,11 +99,13 @@ final class UsernameMarketplaceViewModel: ObservableObject {
guard trimmed.count >= 2 else {
searchResults = []
registrableQueryLabel = nil
queryContest = nil
isSearching = false
return
}
searchResults = []
registrableQueryLabel = nil
queryContest = nil
isSearching = true
searchTask = Task { [weak self] in
try? await Task.sleep(nanoseconds: 350_000_000)
Expand All @@ -121,9 +128,28 @@ final class UsernameMarketplaceViewModel: ObservableObject {
errorMessage = UsernameMarketplaceService.userFacingMessage(for: error)
}
isSearching = false
// A contested-eligible unregistered label can already be
// mid-vote (no document exists until the vote resolves) —
// check after the row is showing and refine its subtitle.
// Skipped for own requests: those are answered locally.
if let candidate = registrableQueryLabel,
UsernameMarketplaceService.isContested(candidate),
!hasRequestedContest(for: candidate) {
let state = await service.contestPrecheck(label: candidate)
guard !Task.isCancelled, registrableQueryLabel == candidate else { return }
queryContest = state
}
}
}

/// This identity already has a contested request in for `label` — from
/// the SDK's contested-names cache, or the app's submission bookmark
/// when that cache hasn't synced yet.
func hasRequestedContest(for label: String) -> Bool {
contestedNames.contains { DWContestedNameStatusService.labelsMatch($0, label) }
|| DWContestedNameStatusService.shared.isPendingLabel(label)
}

/// Local read — the wallet's own tracked rows plus the contested
/// labels cache, no network. Vote states for contested labels are
/// filled in best-effort afterward (those are live queries).
Expand Down Expand Up @@ -510,22 +536,48 @@ struct UsernameMarketplaceScreen: View {

private func registerRow(_ label: String) -> some View {
let contested = UsernameMarketplaceService.isContested(label)
// What the row claims must match the contest reality, not just
// document existence: a label mid-vote has no document yet but is
// NOT plainly available. Own requests answer locally; foreign
// contest state comes from the view model's best-effort precheck.
let icon: String
let tint: Color
let subtitle: String
if contested, viewModel.hasRequestedContest(for: label) {
icon = "hourglass"
tint = .dashGolden
subtitle = NSLocalizedString("Requested by you — the network vote is in progress", comment: "Username marketplace: search row for a contested label this identity already requested")
} else if contested, case .activeContest = viewModel.queryContest {
icon = "person.2.fill"
tint = .dashGolden
subtitle = NSLocalizedString("In a network vote — you can join as a contender", comment: "Username marketplace: search row for a contested label with an active vote by others")
} else if contested, viewModel.queryContest == .locked {
icon = "lock.fill"
tint = Color.dash.secondaryText
subtitle = NSLocalizedString("Locked by a network vote — nobody can register it", comment: "Username marketplace: search row for a label a past vote locked")
} else if contested {
icon = "plus.circle.fill"
tint = .dashGolden
subtitle = NSLocalizedString("Available — short names are decided by a network vote", comment: "Username marketplace: unregistered contested-eligible name row")
} else {
icon = "plus.circle.fill"
tint = .dashGreen
subtitle = NSLocalizedString("Available — register it on your identity", comment: "Username marketplace: unregistered name row")
}
return Button {
registerCandidate = RegisterCandidate(label: label)
} label: {
HStack(spacing: 10) {
Image(systemName: "plus.circle.fill")
Image(systemName: icon)
.font(.system(size: 26))
.foregroundColor(.dashGreen)
.foregroundColor(icon == "plus.circle.fill" ? .dashGreen : tint)
Comment on lines +559 to +573

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Use the selected tint for the contested available icon.

Line 573 forces every "plus.circle.fill" icon to .dashGreen. The contested fresh case on Lines 559-561 selects .dashGolden, but the icon renders green while its subtitle renders gold.

Proposed fix
-                    .foregroundColor(icon == "plus.circle.fill" ? .dashGreen : tint)
+                    .foregroundColor(tint)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
icon = "plus.circle.fill"
tint = .dashGolden
subtitle = NSLocalizedString("Available — short names are decided by a network vote", comment: "Username marketplace: unregistered contested-eligible name row")
} else {
icon = "plus.circle.fill"
tint = .dashGreen
subtitle = NSLocalizedString("Available — register it on your identity", comment: "Username marketplace: unregistered name row")
}
return Button {
registerCandidate = RegisterCandidate(label: label)
} label: {
HStack(spacing: 10) {
Image(systemName: "plus.circle.fill")
Image(systemName: icon)
.font(.system(size: 26))
.foregroundColor(.dashGreen)
.foregroundColor(icon == "plus.circle.fill" ? .dashGreen : tint)
icon = "plus.circle.fill"
tint = .dashGolden
subtitle = NSLocalizedString("Available — short names are decided by a network vote", comment: "Username marketplace: unregistered contested-eligible name row")
} else {
icon = "plus.circle.fill"
tint = .dashGreen
subtitle = NSLocalizedString("Available — register it on your identity", comment: "Username marketplace: unregistered name row")
}
return Button {
registerCandidate = RegisterCandidate(label: label)
} label: {
HStack(spacing: 10) {
Image(systemName: icon)
.font(.system(size: 26))
.foregroundColor(tint)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/Sources/UI/Explore` Dash/UsernameMarketplaceScreen.swift around
lines 559 - 573, Update the icon foreground color in the Button label built by
the username marketplace row to use the selected tint directly, so contested
available entries retain .dashGolden while regular available entries remain
.dashGreen.

VStack(alignment: .leading, spacing: 2) {
Text(label)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(.dash.primaryText)
Text(contested
? NSLocalizedString("Available — short names are decided by a network vote", comment: "Username marketplace: unregistered contested-eligible name row")
: NSLocalizedString("Available — register it on your identity", comment: "Username marketplace: unregistered name row"))
Text(subtitle)
.font(.system(size: 11))
.foregroundColor(contested ? .dashGolden : .dashGreen)
.foregroundColor(tint)
}
Spacer()
Image(systemName: "chevron.right")
Expand Down Expand Up @@ -1227,12 +1279,9 @@ private struct RegisterNameSheet: View {
}

/// This identity already has a request in for this label — the vote
/// is in progress and a second submission would just fail. Checks
/// the app bookmark too, in case the SDK cache hasn't synced yet.
/// is in progress and a second submission would just fail.
private var alreadyRequested: Bool {
viewModel.contestedNames.contains {
DWContestedNameStatusService.labelsMatch($0, label)
} || DWContestedNameStatusService.shared.isPendingLabel(label)
viewModel.hasRequestedContest(for: label)
}

/// A DIFFERENT label's contested request is still in the network
Expand Down
9 changes: 9 additions & 0 deletions DashWallet/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,12 @@
/* Username marketplace: section of contested-name requests awaiting the masternode vote */
"In network vote" = "In network vote";

/* Username marketplace: search row for a contested label with an active vote by others */
"In a network vote — you can join as a contender" = "In a network vote — you can join as a contender";

/* Username marketplace: search row for a label a past vote locked */
"Locked by a network vote — nobody can register it" = "Locked by a network vote — nobody can register it";

Comment on lines +755 to +760

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

file -b --mime-encoding DashWallet/en.lproj/Localizable.strings

Repository: dashpay/dashwallet-ios

Length of output: 202


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path

path = Path("DashWallet/en.lproj/Localizable.strings")
data = path.read_bytes()

print(f"path={path}")
print(f"size={len(data)}")
print(f"bom={data[:2].hex()}")
try:
    text = data.decode("utf-16-le")
    print("utf-16le=decodable")
    print(f"nul_ratio={data.count(0) / len(data):.3f}")
    for key in (
        "In a network vote — you can join as a contender",
        "Locked by a network vote — nobody can register it",
    ):
        print(f"{key!r}={key in text}")
except UnicodeDecodeError as exc:
    print(f"utf-16le=not_decodable:{exc}")
PY

printf '\nRelevant repository configuration:\n'
rg -n -i 'BartyCrouch|Localizable\.strings|UTF-16|utf-16' \
  --glob '!DashWallet/**/Localizable.strings' \
  .github . 2>/dev/null | head -80 || true

Repository: dashpay/dashwallet-ios

Length of output: 12569


Convert DashWallet/en.lproj/Localizable.strings to UTF-16LE. Keep it managed by Xcode or BartyCrouch.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@DashWallet/en.lproj/Localizable.strings` around lines 755 - 760, Convert
DashWallet/en.lproj/Localizable.strings to UTF-16LE while preserving all
localized keys and values, and ensure the file remains managed by Xcode or
BartyCrouch.

Source: Coding guidelines

/* Username marketplace: latest ownership change */
"Last transferred" = "Last transferred";

Expand Down Expand Up @@ -2488,6 +2494,9 @@
/* Username marketplace: contested request not yet indexed by Platform */
"Requested — waiting for the network vote" = "Requested — waiting for the network vote";

/* Username marketplace: search row for a contested label this identity already requested */
"Requested by you — the network vote is in progress" = "Requested by you — the network vote is in progress";

/* Asset-lock retry in progress */
"Retrying transfer…" = "Retrying transfer…";

Expand Down
Loading