Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f1838cfdd | ||
|
|
dcb8e6eb25 |
+73
-17
@@ -104,6 +104,16 @@ bool s_show_delete_confirm = false; // "Delete conversation?" confirm overlay (
|
||||
std::string s_delete_cid; // conversation targeted by the delete confirm
|
||||
std::string s_delete_name; // its peer name (for the confirm copy / block-list label)
|
||||
bool s_show_blocked = false; // blocked-conversations manager overlay (unblock)
|
||||
bool s_show_export_confirm = false; // "export as plaintext" confirm overlay
|
||||
std::string s_export_cid; // conversation targeted by the export confirm
|
||||
|
||||
// Export file path for a conversation (the same scheme the inline export used).
|
||||
static std::string chatExportPathFor(const std::string& peerName) {
|
||||
std::string safe;
|
||||
for (char ch : peerName) safe += (std::isalnum(static_cast<unsigned char>(ch)) ? ch : '_');
|
||||
if (safe.empty()) safe = "chat";
|
||||
return util::Platform::getConfigDir() + "/dragonx-chat-" + safe + ".txt";
|
||||
}
|
||||
bool s_show_emoji_picker = false; // emoji picker overlay — fills the conversation-list pane while open
|
||||
char s_emoji_search[48] = ""; // emoji picker keyword filter
|
||||
|
||||
@@ -1241,23 +1251,9 @@ void RenderChatTab(App* app)
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx, by));
|
||||
material::IconButtonStyle a = base; a.tooltip = TR("chat_export_warn");
|
||||
if (material::IconButton("##hdr_export", ICON_MD_FILE_DOWNLOAD, ifont, ImVec2(ib, ib), a)) {
|
||||
std::string content = "DragonX chat export\n";
|
||||
content += sel->peerName + " <" + sel->peerZaddr + ">\n";
|
||||
content += "cid: " + sel->cid + "\n\n";
|
||||
for (const auto& m : store.conversation(sel->cid)) {
|
||||
const bool out = (m.direction == chat::ChatDirection::Outgoing);
|
||||
content += "[" + formatTime(m.timestamp) + "] " +
|
||||
(out ? std::string(TR("chat_you")) : sel->peerName) + ": " + m.body + "\n";
|
||||
}
|
||||
std::string safe;
|
||||
for (char ch : sel->peerName)
|
||||
safe += (std::isalnum(static_cast<unsigned char>(ch)) ? ch : '_');
|
||||
if (safe.empty()) safe = "chat";
|
||||
const std::string path = util::Platform::getConfigDir() + "/dragonx-chat-" + safe + ".txt";
|
||||
if (util::Platform::writeFileAtomically(path, content, /*restrictPermissions=*/true))
|
||||
Notifications::instance().success(std::string(TR("chat_export_done")) + ": " + path);
|
||||
else
|
||||
Notifications::instance().error(TR("chat_export_failed"));
|
||||
// The export writes DECRYPTED plaintext — confirm first instead of writing on one click.
|
||||
s_export_cid = sel->cid;
|
||||
s_show_export_confirm = true;
|
||||
}
|
||||
bx += ib + gap;
|
||||
}
|
||||
@@ -2151,6 +2147,66 @@ void RenderChatTab(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Export-as-plaintext confirm (unencrypted file on disk) ----
|
||||
if (s_show_export_confirm) {
|
||||
const ConvSummary* ex = nullptr;
|
||||
for (const auto& c : convs) if (c.cid == s_export_cid) { ex = &c; break; }
|
||||
if (!ex) {
|
||||
s_show_export_confirm = false;
|
||||
s_export_cid.clear();
|
||||
} else {
|
||||
material::OverlayDialogSpec ov;
|
||||
ov.title = TR("chat_export_confirm_title");
|
||||
ov.p_open = &s_show_export_confirm;
|
||||
ov.style = material::OverlayStyle::BlurFloat;
|
||||
ov.cardWidth = 500.0f;
|
||||
ov.idSuffix = "chatexport";
|
||||
if (material::BeginOverlayDialog(ov)) {
|
||||
const float edp = Layout::dpiScale();
|
||||
const std::string path = chatExportPathFor(ex->peerName);
|
||||
ImGui::PushTextWrapPos(0.0f);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::Warning()));
|
||||
ImGui::TextWrapped("%s", TR("chat_export_confirm_body"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::Spacing();
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(material::OnSurfaceMedium()));
|
||||
ImGui::TextUnformatted(TR("chat_export_confirm_path"));
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::TextWrapped("%s", path.c_str());
|
||||
ImGui::PopTextWrapPos();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
const float egap = ImGui::GetStyle().ItemSpacing.x;
|
||||
float wExp = std::max(110.0f * edp, ImGui::CalcTextSize(TR("chat_export_confirm_btn")).x + 24.0f * edp);
|
||||
float wCan = std::max(90.0f * edp, ImGui::CalcTextSize(TR("chat_cancel")).x + 24.0f * edp);
|
||||
material::BeginOverlayDialogFooter(wExp + wCan + egap, false);
|
||||
if (material::TactileButton(TR("chat_export_confirm_btn"), ImVec2(wExp, 0))) {
|
||||
// Build + write exactly as the old inline export did (format byte-identical).
|
||||
std::string content = "DragonX chat export\n";
|
||||
content += ex->peerName + " <" + ex->peerZaddr + ">\n";
|
||||
content += "cid: " + ex->cid + "\n\n";
|
||||
for (const auto& m : store.conversation(ex->cid)) {
|
||||
const bool out = (m.direction == chat::ChatDirection::Outgoing);
|
||||
content += "[" + formatTime(m.timestamp) + "] " +
|
||||
(out ? std::string(TR("chat_you")) : ex->peerName) + ": " + m.body + "\n";
|
||||
}
|
||||
if (util::Platform::writeFileAtomically(path, content, /*restrictPermissions=*/true))
|
||||
Notifications::instance().success(std::string(TR("chat_export_done")) + ": " + path);
|
||||
else
|
||||
Notifications::instance().error(TR("chat_export_failed"));
|
||||
s_show_export_confirm = false;
|
||||
s_export_cid.clear();
|
||||
}
|
||||
ImGui::SameLine(0, egap);
|
||||
if (material::TactileButton(TR("chat_cancel"), ImVec2(wCan, 0))) {
|
||||
s_show_export_confirm = false;
|
||||
s_export_cid.clear();
|
||||
}
|
||||
material::EndOverlayDialog();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Delete-conversation confirm (revive-on-new-message vs delete & block) ----
|
||||
if (s_show_delete_confirm) {
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
@@ -191,18 +191,18 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
};
|
||||
|
||||
snprintf(valBuf, sizeof(valBuf), "+%.4f", minedToday);
|
||||
snprintf(subBuf2, sizeof(subBuf2), "(%d txn)", minedTodayCount);
|
||||
snprintf(subBuf2, sizeof(subBuf2), TR("mining_txn_count"), minedTodayCount);
|
||||
char todayVal[64], todaySub[64];
|
||||
strncpy(todayVal, valBuf, sizeof(todayVal));
|
||||
strncpy(todaySub, subBuf2, sizeof(todaySub));
|
||||
|
||||
char yesterdayVal[64], yesterdaySub[64];
|
||||
snprintf(yesterdayVal, sizeof(yesterdayVal), "+%.4f", minedYesterday);
|
||||
snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d txn)", minedYesterdayCount);
|
||||
snprintf(yesterdaySub, sizeof(yesterdaySub), TR("mining_txn_count"), minedYesterdayCount);
|
||||
|
||||
char allVal[64], allSub[64];
|
||||
snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime);
|
||||
snprintf(allSub, sizeof(allSub), "(%d txn)", minedAllTimeCount);
|
||||
snprintf(allSub, sizeof(allSub), TR("mining_txn_count"), minedAllTimeCount);
|
||||
|
||||
char estVal[64];
|
||||
if (estActive)
|
||||
@@ -324,6 +324,11 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
|
||||
// -- Mining Address --
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), TR("mining_mining_addr"));
|
||||
{
|
||||
ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, TR("mining_mining_addr"));
|
||||
if (material::IsRectHovered(ImVec2(col3X, cy), ImVec2(col3X + lblSz.x, cy + lblSz.y)))
|
||||
material::Tooltip("%s", TR("mining_addr_explainer"));
|
||||
}
|
||||
std::string mining_address = "";
|
||||
for (const auto& addr : state.addresses) {
|
||||
if (addr.type == "transparent" && !addr.address.empty()) {
|
||||
@@ -351,6 +356,12 @@ void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo&
|
||||
ImVec2(Layout::spacingMd(), 4 * dp))) {
|
||||
Notifications::instance().info(TR("mining_address_copied"));
|
||||
}
|
||||
} else {
|
||||
// No mining (transparent) address yet — explain where rewards go instead of a blank.
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X + valOffX, cy), OnSurfaceMedium(), TR("mining_addr_none"));
|
||||
ImVec2 emSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, TR("mining_addr_none"));
|
||||
if (material::IsRectHovered(ImVec2(col3X + valOffX, cy), ImVec2(col3X + valOffX + emSz.x, cy + emSz.y)))
|
||||
material::Tooltip("%s", TR("mining_addr_none_tip"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -146,6 +146,8 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
||||
ImGui::Dummy(ImVec2(totalW, 0));
|
||||
|
||||
bool payoutBadThisFrame = false; // set when the inline payout error is drawn; reserves a row below
|
||||
|
||||
// Pool URL + Worker inputs inline next to toggle (pool mode only)
|
||||
if (s_pool_mode && soloMiningActive) {
|
||||
// Solo mining is active — show disabled message instead of inputs
|
||||
@@ -427,6 +429,24 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
}
|
||||
}
|
||||
|
||||
// Persistent inline payout error — a hover-only tooltip is missed by anyone who doesn't hover,
|
||||
// so outline the field in red and draw a short reason below it (draw-list only; no layout calls
|
||||
// here, the cursor is hand-positioned). The field height is reserved at the end of the function.
|
||||
{
|
||||
const ImVec2 wMin = ImGui::GetItemRectMin();
|
||||
const ImVec2 wMax = ImGui::GetItemRectMax();
|
||||
const std::string wk(s_pool_worker);
|
||||
if (!wk.empty() && wk != "x" && !util::isValidRecipientAddress(wk)) {
|
||||
payoutBadThisFrame = true;
|
||||
dl->AddRect(wMin, wMax, Error(), 4.0f * dp, 0, 1.5f * dp);
|
||||
const char* msg = util::isTransparentAddress(wk)
|
||||
? TR("mining_payout_needs_shielded_short") : TR("mining_payout_invalid_short");
|
||||
std::string shown = material::TruncateToWidth(msg, capFont, capFont->LegacySize, wrkGroupW);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(wMin.x, wMax.y + 1.0f * dp),
|
||||
ReadableError(), shown.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// --- Worker: Dropdown arrow button ---
|
||||
ImGui::SameLine(0, 0);
|
||||
{
|
||||
@@ -642,7 +662,9 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
||||
// Ensure cursor Y is at toggle bottom regardless of pool input widgets,
|
||||
// so the cards below stay at the same position in both solo and pool modes.
|
||||
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
|
||||
ImGui::Dummy(ImVec2(0, gap * 0.5f));
|
||||
// Reserve a row for the inline payout error so it doesn't overlap the card below (cards shift down
|
||||
// transiently while the error is shown).
|
||||
ImGui::Dummy(ImVec2(0, gap * 0.5f + (payoutBadThisFrame ? capFont->LegacySize + 2.0f * dp : 0.0f)));
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
|
||||
@@ -243,9 +243,9 @@ static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* d
|
||||
{
|
||||
char uptBuf[64];
|
||||
int64_t up = pm.uptime_sec;
|
||||
if (up <= 0) snprintf(uptBuf, sizeof(uptBuf), "N/A");
|
||||
else if (up < 3600) snprintf(uptBuf, sizeof(uptBuf), "%lldm %llds", (long long)(up / 60), (long long)(up % 60));
|
||||
else snprintf(uptBuf, sizeof(uptBuf), "%lldh %lldm", (long long)(up / 3600), (long long)((up % 3600) / 60));
|
||||
if (up <= 0) snprintf(uptBuf, sizeof(uptBuf), "%s", TR("grpc_na"));
|
||||
else if (up < 3600) snprintf(uptBuf, sizeof(uptBuf), TR("mining_uptime_ms"), (long long)(up / 60), (long long)(up % 60));
|
||||
else snprintf(uptBuf, sizeof(uptBuf), TR("mining_uptime_hm"), (long long)(up / 3600), (long long)((up % 3600) / 60));
|
||||
metricRow(TR("mining_uptime"), uptBuf, OnSurface());
|
||||
}
|
||||
// Connection status — replaces the old transient "Connecting" banner, with a timeout so a bad URL /
|
||||
|
||||
@@ -278,6 +278,10 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_export_warn"] = "Saves the decrypted messages as plain text. Store the file securely.";
|
||||
strings_["chat_export_done"] = "Conversation exported";
|
||||
strings_["chat_export_failed"] = "Could not write the export file.";
|
||||
strings_["chat_export_confirm_title"] = "Export chat as plain text?";
|
||||
strings_["chat_export_confirm_body"] = "This saves the decrypted messages as an UNENCRYPTED plain-text file. Anyone who can open the file can read the whole conversation. Store it securely and delete it when you no longer need it.";
|
||||
strings_["chat_export_confirm_path"] = "Will be saved to:";
|
||||
strings_["chat_export_confirm_btn"] = "Export";
|
||||
strings_["chat_len_over"] = "Message too long";
|
||||
strings_["chat_mute"] = "Mute";
|
||||
strings_["chat_unmute"] = "Unmute";
|
||||
@@ -2319,6 +2323,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["send_mining_addr_short"] = "Mining address — can't be paid";
|
||||
strings_["send_mining_addr_blocked"] = "This is a mining (transparent) address. On DragonX these can only receive mining rewards — the network rejects payments to them. Ask the recipient for a shielded address (it starts with zs).";
|
||||
strings_["mining_payout_needs_shielded"] = "Pools pay out with ordinary transactions, which can't reach a mining (transparent, R…) address on DragonX. Use a shielded address (it starts with zs) as your payout address.";
|
||||
strings_["mining_payout_invalid_short"] = "Not a valid DragonX address";
|
||||
strings_["mining_payout_needs_shielded_short"] = "Use a shielded (zs…) address, not a mining address";
|
||||
strings_["new_mining_address"] = "New mining address";
|
||||
strings_["tt_new_mining_address"] = "Creates a transparent (R…) address for solo-mining rewards. It can't receive payments — share a shielded address for those.";
|
||||
strings_["mining_address"] = "Mining address";
|
||||
@@ -2335,6 +2341,8 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["failed_create_mining_address"] = "Failed to create mining address";
|
||||
strings_["new_mining_address_created"] = "New mining address created";
|
||||
strings_["mining_addr_explainer"] = "Receives solo-mining rewards only — the network rejects payments to it. Rewards are shielded automatically once they mature.";
|
||||
strings_["mining_addr_none"] = "None yet";
|
||||
strings_["mining_addr_none_tip"] = "No mining address yet. Create one in the Receive tab (New mining address). Solo-mining rewards are paid to a transparent (R...) mining address, then shielded automatically once they mature.";
|
||||
strings_["receive_use_shielded"] = "Use a shielded address";
|
||||
strings_["receive_not_for_payments"] = "Not for payments";
|
||||
strings_["validate_mining_type"] = "Mining address (transparent)";
|
||||
@@ -2392,6 +2400,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["mining_to_save"] = "to save";
|
||||
strings_["mining_today"] = "Today";
|
||||
strings_["mining_uptime"] = "Uptime";
|
||||
strings_["mining_txn_count"] = "(%d txn)";
|
||||
strings_["mining_uptime_ms"] = "%lldm %llds";
|
||||
strings_["mining_uptime_hm"] = "%lldh %lldm";
|
||||
strings_["mining_yesterday"] = "Yesterday";
|
||||
|
||||
// --- Miner (xmrig) updater ---
|
||||
|
||||
Reference in New Issue
Block a user