feat(chat): polish thread header, emoji picker, and pane gutter
Tier A + B chat-tab visual pass: - Widen the list<->thread gutter (1px -> 10px) for breathing room. - Redesign the thread header: avatar + name on the left with a right-aligned frameless icon toolbar (add-contact/export/mute/hide, each with a tooltip); the name is clipped to the toolbar's left edge so a long contact label can't overrun the icons. - Replace the "Copy Full Address" button with a click-to-copy shortened address (copies the full z-addr), preceded by a key-verify lock whose tooltip shows a comparable identity-key fingerprint. - Show a "Waiting for reply" chip in the header when the peer's identity key isn't known yet. - Emoji picker: frameless grid with tight cells (glyphs fill the cell) and leftover width spread into the column gaps so it stays edge-to-edge. - Larger 30px composer emoji toggle that lights up while the picker is open; footer height + byte-counter centering adjusted to match. New i18n keys (chat_copy_address_tip / chat_verify_key / chat_awaiting_key) added additively to all 8 languages; CJK subset font rebuilt for the new zh glyph. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -131,6 +131,19 @@ std::string initialOf(const std::string& name) {
|
||||
return name.substr(i, std::min(len, name.size() - i));
|
||||
}
|
||||
|
||||
// Render a peer identity key (hex) as a groupable fingerprint for the verify tooltip: uppercased,
|
||||
// spaced every 4 hex chars, wrapped every 16 (8 bytes) so two users can read it off line by line.
|
||||
std::string keyFingerprint(const std::string& hex) {
|
||||
std::string out;
|
||||
out.reserve(hex.size() + hex.size() / 4);
|
||||
for (std::size_t i = 0; i < hex.size(); ++i) {
|
||||
const unsigned char c = static_cast<unsigned char>(hex[i]);
|
||||
out += static_cast<char>(std::toupper(c));
|
||||
if ((i % 4) == 3 && i + 1 < hex.size()) out += ((i % 16) == 15) ? '\n' : ' ';
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
struct ConvSummary {
|
||||
std::string cid;
|
||||
std::string peerZaddr;
|
||||
@@ -255,22 +268,34 @@ void renderEmojiPickerOverlay(char* buf, std::size_t bufSize) {
|
||||
|
||||
const std::string q = s_emoji_search;
|
||||
ImGui::BeginChild("##emojigrid", ImVec2(0, 0), false);
|
||||
ImGui::PushFont(material::Type().subtitle1()); // larger + emoji-capable
|
||||
const float cell = 32.0f * Layout::dpiScale();
|
||||
const float step = cell + ImGui::GetStyle().ItemSpacing.x;
|
||||
const int perRow = std::max(1, static_cast<int>(ImGui::GetContentRegionAvail().x / step));
|
||||
// Frameless cells tiled to the full pane width. The cell is kept tight (so the emoji fills it
|
||||
// instead of floating in a big square); the leftover width is spread into the column gaps so the
|
||||
// grid still spans edge-to-edge (Q12 polish — "scale to container width", denser variant).
|
||||
ImFont* emojiFont = material::Type().subtitle1(); // emoji-capable + larger than body
|
||||
const float dp = Layout::dpiScale();
|
||||
const float cell = 30.0f * dp; // tight square around the glyph
|
||||
const float minGap = 5.0f * dp;
|
||||
const float availW = ImGui::GetContentRegionAvail().x;
|
||||
const int perRow = std::max(1, static_cast<int>((availW + minGap) / (cell + minGap)));
|
||||
const float spacing = perRow > 1
|
||||
? std::clamp((availW - perRow * cell) / (perRow - 1), minGap, minGap * 3.0f)
|
||||
: minGap;
|
||||
material::IconButtonStyle es;
|
||||
es.hoverBg = material::WithAlpha(material::OnSurface(), 34);
|
||||
es.bgRounding = 8.0f * dp;
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(spacing, spacing));
|
||||
int shown = 0;
|
||||
for (const auto& e : kEmoji) {
|
||||
if (!q.empty() && !containsCI(e.keywords, q)) continue;
|
||||
ImGui::PushID(shown);
|
||||
if (ImGui::Button(e.glyph, ImVec2(cell, cell))) {
|
||||
if (material::IconButton("##em", e.glyph, emojiFont, ImVec2(cell, cell), es)) {
|
||||
const std::size_t cur = std::strlen(buf), add = std::strlen(e.glyph);
|
||||
if (cur + add < bufSize) { std::memcpy(buf + cur, e.glyph, add); buf[cur + add] = '\0'; }
|
||||
}
|
||||
ImGui::PopID();
|
||||
if (++shown % perRow != 0) ImGui::SameLine();
|
||||
}
|
||||
ImGui::PopFont();
|
||||
ImGui::PopStyleVar();
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
@@ -461,7 +486,7 @@ void RenderChatTab(App* app)
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleColor(); // list ChildBg tint (V6)
|
||||
|
||||
ImGui::SameLine(0.0f, 1.0f * Layout::dpiScale()); // tight single-line seam between the panes
|
||||
ImGui::SameLine(0.0f, 10.0f * Layout::dpiScale()); // breathing gutter between the list and thread panes
|
||||
|
||||
// ---- Right: selected conversation thread ----
|
||||
ImGui::BeginChild("##ChatThread", ImVec2(0, avail.y), true);
|
||||
@@ -471,91 +496,183 @@ void RenderChatTab(App* app)
|
||||
|
||||
if (sel) {
|
||||
app->markChatConversationSeen(sel->cid, sel->lastTs); // viewing the thread clears its unread (Q1)
|
||||
// Header: peer avatar + name (V7).
|
||||
// ── Header (V7 / Tier A+B): avatar + name on the left, a right-aligned icon toolbar,
|
||||
// then a click-to-copy address with a key-verify lock (or a "waiting for reply" chip).
|
||||
const float hdpi = Layout::dpiScale();
|
||||
ImGui::PushFont(material::Type().subtitle1());
|
||||
const float nameH = ImGui::GetTextLineHeight();
|
||||
const float ib = nameH + 8.0f * hdpi; // icon-button square == header row height
|
||||
const float gap = 3.0f * hdpi;
|
||||
const float rowH = ib;
|
||||
const float avR = nameH * 0.62f;
|
||||
const ImVec2 hp = ImGui::GetCursorScreenPos();
|
||||
const float rightX = hp.x + ImGui::GetContentRegionAvail().x;
|
||||
const float textX = hp.x + 2.0f * avR + 10.0f * hdpi;
|
||||
ImDrawList* hdl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 avC(hp.x + avR, hp.y + rowH * 0.5f); // avatar centered in the row
|
||||
hdl->AddCircleFilled(avC, avR, avatarColor(sel->cid), 24);
|
||||
{
|
||||
const float nameH = ImGui::GetTextLineHeight();
|
||||
const float avR = nameH * 0.6f;
|
||||
const ImVec2 hp = ImGui::GetCursorScreenPos();
|
||||
ImDrawList* hdl = ImGui::GetWindowDrawList();
|
||||
const ImVec2 avC(hp.x + avR, hp.y + nameH * 0.5f);
|
||||
hdl->AddCircleFilled(avC, avR, avatarColor(sel->cid), 24);
|
||||
const std::string init = initialOf(sel->peerName);
|
||||
const ImVec2 isz = ImGui::CalcTextSize(init.c_str());
|
||||
hdl->AddText(ImVec2(avC.x - isz.x * 0.5f, avC.y - isz.y * 0.5f),
|
||||
IM_COL32(255, 255, 255, 235), init.c_str());
|
||||
ImGui::SetCursorScreenPos(ImVec2(hp.x + 2.0f * avR + 8.0f * Layout::dpiScale(), hp.y));
|
||||
}
|
||||
const bool hasAddr = !sel->peerZaddr.empty();
|
||||
const bool known = hasAddr && book.findByAddress(sel->peerZaddr) >= 0;
|
||||
const bool muted = app->settings() && app->settings()->isChatMuted(sel->cid);
|
||||
|
||||
// The toolbar's left edge is known up front (from the button count), so the name can be
|
||||
// clipped to it — a long contact label can't overrun the icons (mirrors the list-pane clip).
|
||||
const int nBtns = 3 + ((hasAddr && !known) ? 1 : 0);
|
||||
const float toolbarLeft = rightX - (nBtns * ib + (nBtns - 1) * gap);
|
||||
ImGui::SetCursorScreenPos(ImVec2(textX, hp.y + (rowH - nameH) * 0.5f)); // name centered in row
|
||||
ImGui::PushClipRect(ImVec2(textX, hp.y), ImVec2(toolbarLeft - gap, hp.y + rowH), true);
|
||||
ImGui::TextUnformatted(sel->peerName.c_str());
|
||||
ImGui::PopClipRect();
|
||||
ImGui::PopFont();
|
||||
if (!sel->peerZaddr.empty()) {
|
||||
ImGui::PushFont(metaFont);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::OnSurfaceMedium());
|
||||
ImGui::TextUnformatted(shorten(sel->peerZaddr, 20, 12).c_str());
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
// Header actions: copy the peer z-address (Q3), and add them to contacts when unknown (Q2).
|
||||
if (ImGui::SmallButton(TR("copy_address"))) ImGui::SetClipboardText(sel->peerZaddr.c_str());
|
||||
if (book.findByAddress(sel->peerZaddr) < 0) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(TR("chat_add_contact"))) {
|
||||
|
||||
// Right-aligned icon toolbar: [add contact?] · export · mute · hide. Each is a frameless
|
||||
// IconButton with a hover pill + tooltip (the tooltip carries the wording the old text buttons had).
|
||||
{
|
||||
ImFont* ifont = material::Type().iconMed();
|
||||
material::IconButtonStyle base;
|
||||
base.color = material::OnSurfaceMedium();
|
||||
base.hoverColor = material::OnSurface();
|
||||
base.hoverBg = material::WithAlpha(material::OnSurface(), 30);
|
||||
base.bgRounding = 7.0f * hdpi;
|
||||
|
||||
float bx = toolbarLeft;
|
||||
const float by = hp.y;
|
||||
|
||||
// Add to contacts — only when the peer isn't in the address book yet (Q2).
|
||||
if (hasAddr && !known) {
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx, by));
|
||||
material::IconButtonStyle a = base; a.tooltip = TR("chat_add_contact");
|
||||
if (material::IconButton("##hdr_add", ICON_MD_PERSON_ADD, ifont, ImVec2(ib, ib), a)) {
|
||||
data::AddressBookEntry e(sel->peerName, sel->peerZaddr); // label = shortened addr; rename in Contacts
|
||||
if (book.addEntry(e)) Notifications::instance().success(TR("chat_contact_added"));
|
||||
else Notifications::instance().error(TR("address_book_exists"));
|
||||
}
|
||||
bx += ib + gap;
|
||||
}
|
||||
// Export the decrypted conversation to a plain-text file (Q11). Written with restricted
|
||||
// permissions; the tooltip warns it's plaintext.
|
||||
ImGui::SameLine();
|
||||
const bool exportClicked = ImGui::SmallButton(TR("chat_export"));
|
||||
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("chat_export_warn"));
|
||||
if (exportClicked) {
|
||||
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";
|
||||
// Export the decrypted conversation to a plain-text file (Q11); the tooltip warns it's plaintext.
|
||||
{
|
||||
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"));
|
||||
}
|
||||
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"));
|
||||
bx += ib + gap;
|
||||
}
|
||||
// Mute toggle — muted conversations don't badge or toast (Q10). Persisted to settings.
|
||||
ImGui::SameLine();
|
||||
const bool muted = app->settings() && app->settings()->isChatMuted(sel->cid);
|
||||
if (ImGui::SmallButton(muted ? TR("chat_unmute") : TR("chat_mute")) && app->settings()) {
|
||||
app->settings()->setChatMuted(sel->cid, !muted);
|
||||
app->settings()->save();
|
||||
{
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx, by));
|
||||
material::IconButtonStyle a = base;
|
||||
a.tooltip = muted ? TR("chat_unmute") : TR("chat_mute");
|
||||
if (muted) { a.color = material::Primary(); a.hoverColor = material::Primary(); }
|
||||
if (material::IconButton("##hdr_mute",
|
||||
muted ? ICON_MD_NOTIFICATIONS_OFF : ICON_MD_NOTIFICATIONS,
|
||||
ifont, ImVec2(ib, ib), a) && app->settings()) {
|
||||
app->settings()->setChatMuted(sel->cid, !muted);
|
||||
app->settings()->save();
|
||||
}
|
||||
bx += ib + gap;
|
||||
}
|
||||
// Hide / Unhide. Hiding drops it from the list (messages stay in the encrypted store);
|
||||
// Unhide (shown only for a hidden conversation while "Show hidden" is on) restores it.
|
||||
ImGui::SameLine();
|
||||
if (ImGui::SmallButton(sel->hidden ? TR("chat_unhide") : TR("chat_hide")) && app->settings()) {
|
||||
if (sel->hidden) {
|
||||
app->settings()->setChatHidden(sel->cid, false);
|
||||
app->settings()->save();
|
||||
} else {
|
||||
app->markChatConversationSeen(sel->cid, sel->lastTs); // don't leave a phantom unread
|
||||
app->settings()->setChatHidden(sel->cid, true);
|
||||
app->settings()->save();
|
||||
s_selected_cid.clear();
|
||||
Notifications::instance().info(TR("chat_hidden_toast"));
|
||||
{
|
||||
ImGui::SetCursorScreenPos(ImVec2(bx, by));
|
||||
material::IconButtonStyle a = base;
|
||||
a.tooltip = sel->hidden ? TR("chat_unhide") : TR("chat_hide");
|
||||
if (material::IconButton("##hdr_hide",
|
||||
sel->hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF,
|
||||
ifont, ImVec2(ib, ib), a) && app->settings()) {
|
||||
if (sel->hidden) {
|
||||
app->settings()->setChatHidden(sel->cid, false);
|
||||
app->settings()->save();
|
||||
} else {
|
||||
app->markChatConversationSeen(sel->cid, sel->lastTs); // don't leave a phantom unread
|
||||
app->settings()->setChatHidden(sel->cid, true);
|
||||
app->settings()->save();
|
||||
s_selected_cid.clear();
|
||||
Notifications::instance().info(TR("chat_hidden_toast"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Row 2: a key-verify lock (Tier B) + click-to-copy address (Q3), or a "waiting for reply" chip.
|
||||
ImGui::SetCursorScreenPos(ImVec2(textX, hp.y + rowH + 3.0f * hdpi));
|
||||
if (hasAddr) {
|
||||
// Lock glyph → we hold the peer's identity key; the tooltip shows a comparable fingerprint.
|
||||
if (!sel->peerPubKey.empty()) {
|
||||
ImGui::PushFont(material::Type().iconSmall());
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, material::WithAlpha(material::Primary(), 220));
|
||||
ImGui::TextUnformatted(ICON_MD_LOCK);
|
||||
ImGui::PopStyleColor();
|
||||
ImGui::PopFont();
|
||||
if (ImGui::IsItemHovered())
|
||||
material::Tooltip("%s\n%s", TR("chat_verify_key"), keyFingerprint(sel->peerPubKey).c_str());
|
||||
ImGui::SameLine(0.0f, 5.0f * hdpi);
|
||||
}
|
||||
// Click-to-copy shortened address (replaces the old "Copy Full Address" button).
|
||||
ImGui::PushFont(metaFont);
|
||||
const std::string sa = shorten(sel->peerZaddr, 20, 12);
|
||||
const ImVec2 tp = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 tsz = ImGui::CalcTextSize(sa.c_str());
|
||||
const bool addrClicked = ImGui::InvisibleButton("##hdr_copyaddr", tsz);
|
||||
const bool addrHov = ImGui::IsItemHovered();
|
||||
ImGui::GetWindowDrawList()->AddText(
|
||||
tp, addrHov ? material::OnSurface() : material::OnSurfaceMedium(), sa.c_str());
|
||||
if (addrHov) {
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
material::Tooltip("%s", TR("chat_copy_address_tip"));
|
||||
}
|
||||
if (addrClicked) {
|
||||
ImGui::SetClipboardText(sel->peerZaddr.c_str());
|
||||
Notifications::instance().success(TR("copied"));
|
||||
}
|
||||
// Waiting-for-reply chip when the peer's identity key isn't known yet (Tier B).
|
||||
if (sel->peerPubKey.empty()) {
|
||||
ImGui::SameLine(0.0f, 8.0f * hdpi);
|
||||
const char* wl = TR("chat_awaiting_key");
|
||||
const ImVec2 wsz = ImGui::CalcTextSize(wl);
|
||||
const float px = 7.0f * hdpi, py = 2.0f * hdpi;
|
||||
const ImVec2 cp = ImGui::GetCursorScreenPos();
|
||||
const ImVec2 pmax(cp.x + wsz.x + 2.0f * px, cp.y + wsz.y + 2.0f * py);
|
||||
ImDrawList* cdl = ImGui::GetWindowDrawList();
|
||||
cdl->AddRectFilled(cp, pmax, material::WithAlpha(material::Primary(), 38),
|
||||
(wsz.y + 2.0f * py) * 0.5f);
|
||||
cdl->AddText(ImVec2(cp.x + px, cp.y + py),
|
||||
material::WithAlpha(material::Primary(), 230), wl);
|
||||
ImGui::Dummy(ImVec2(wsz.x + 2.0f * px, wsz.y + 2.0f * py));
|
||||
}
|
||||
ImGui::PopFont();
|
||||
} else {
|
||||
ImGui::Dummy(ImVec2(0.0f, metaSz)); // keep the header height stable when there's no address
|
||||
}
|
||||
ImGui::Separator();
|
||||
|
||||
const float dp = Layout::dpiScale();
|
||||
const float composerBoxH = ImGui::GetTextLineHeight() * 2.6f; // multi-line composer (Q6)
|
||||
const float footerH = composerBoxH + ImGui::GetTextLineHeight() + 20.0f * dp;
|
||||
// Footer = composer box + the emoji-toggle/counter row (30dp tall button) + padding.
|
||||
const float footerH = composerBoxH + 30.0f * dp + 20.0f * dp;
|
||||
ImGui::BeginChild("##ChatMessages", ImVec2(0, ImGui::GetContentRegionAvail().y - footerH), false);
|
||||
bool atBottom = true;
|
||||
const ImVec2 msgWinMin = ImGui::GetWindowPos();
|
||||
@@ -697,15 +814,26 @@ void RenderChatTab(App* app)
|
||||
ImGui::EndDisabled();
|
||||
ImGui::PopStyleColor(3);
|
||||
// Emoji picker button (left) + byte counter (right) on one row. The picker opens as an
|
||||
// overlay over the conversation-list pane (rendered there), not a floating window.
|
||||
ImGui::PushFont(nameFont); // Body2 has emoji merged, so the glyph renders
|
||||
const bool emojiClicked = ImGui::SmallButton(u8"🙂");
|
||||
ImGui::PopFont();
|
||||
if (emojiClicked) { s_show_emoji_picker = !s_show_emoji_picker; s_emoji_search[0] = '\0'; }
|
||||
// overlay over the conversation-list pane (rendered there), not a floating window. The
|
||||
// toggle is a larger frameless button (Tier A) that lights up while the picker is open.
|
||||
const float emojiBtn = 30.0f * dp;
|
||||
{
|
||||
material::IconButtonStyle es;
|
||||
es.hoverBg = material::WithAlpha(material::OnSurface(), 30);
|
||||
es.bgRounding = 8.0f * dp;
|
||||
if (s_show_emoji_picker) es.restBg = material::WithAlpha(material::Primary(), 46);
|
||||
if (material::IconButton("##emojitoggle", u8"🙂", material::Type().subtitle1(),
|
||||
ImVec2(emojiBtn, emojiBtn), es)) {
|
||||
s_show_emoji_picker = !s_show_emoji_picker;
|
||||
s_emoji_search[0] = '\0';
|
||||
}
|
||||
}
|
||||
ImGui::SameLine();
|
||||
// Byte counter (Error tint over cap) with the over-cap label.
|
||||
// Byte counter (Error tint over cap) with the over-cap label — vertically centered on the button.
|
||||
ImGui::PushFont(metaFont);
|
||||
ImGui::PushStyleColor(ImGuiCol_Text, overCap ? material::Error() : material::OnSurfaceMedium());
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() +
|
||||
std::max(0.0f, (emojiBtn - ImGui::GetTextLineHeight()) * 0.5f));
|
||||
if (overCap) { ImGui::TextUnformatted(TR("chat_len_over")); ImGui::SameLine(); }
|
||||
const std::string counter = std::to_string(used) + " / " + std::to_string(kBodyMaxBytes);
|
||||
const float cw = metaFont->CalcTextSizeA(metaSz, FLT_MAX, 0.0f, counter.c_str()).x;
|
||||
|
||||
@@ -260,6 +260,9 @@ void I18n::loadBuiltinEnglish()
|
||||
strings_["chat_hidden_toast"] = "Conversation hidden — a new message brings it back";
|
||||
strings_["chat_pick_contact"] = "Choose from contacts\xE2\x80\xA6";
|
||||
strings_["chat_no_z_contacts"] = "No shielded-address contacts yet";
|
||||
strings_["chat_copy_address_tip"] = "Click to copy address";
|
||||
strings_["chat_verify_key"] = "Identity key \xE2\x80\x94 compare to verify";
|
||||
strings_["chat_awaiting_key"] = "Waiting for reply";
|
||||
// Seed-phrase backup (full-node)
|
||||
strings_["seed_backup_button"] = "Seed phrase";
|
||||
strings_["tt_seed_backup"] = "Show and back up your wallet's 24-word recovery seed phrase";
|
||||
|
||||
Reference in New Issue
Block a user