feat: name an unknown sender from the reader to enable replies
This commit is contained in:
parent
025963371f
commit
fd2d9298f8
2 changed files with 72 additions and 15 deletions
|
|
@ -174,25 +174,79 @@ async function openMessage(id) {
|
|||
: `from: ${opened.from}\nkey: ${b32encode(opened.sender)}\nid: ${row.id}\ndate: ${fmtTime(opened.time)}`;
|
||||
view.querySelector(".fields").textContent = fields.map(([k, v]) => `${k}: ${v}`).join("\n");
|
||||
view.querySelector("pre").textContent = opened.body;
|
||||
if (folder === "inbox" && !opened.knownSender) {
|
||||
const note = document.createElement("p");
|
||||
note.className = "placeholder";
|
||||
note.textContent = "The wire carries no sender name, only this signed key. " +
|
||||
"Import the sender's smol:// address in settings to name future mail.";
|
||||
view.append(note);
|
||||
}
|
||||
if (folder === "inbox") {
|
||||
const reply = document.createElement("button");
|
||||
reply.textContent = "reply";
|
||||
reply.onclick = () => openCompose({
|
||||
to: store.addressForKey(opened.sender) ?? "",
|
||||
subject: opened.subject ? (opened.subject.startsWith("Re:") ? opened.subject : `Re: ${opened.subject}`) : "",
|
||||
replyTo: row.id,
|
||||
});
|
||||
view.append(reply);
|
||||
if (opened.knownSender) {
|
||||
view.append(replyButton(opened, row));
|
||||
} else {
|
||||
// The wire gives only a signed key; a reply needs the sender's address,
|
||||
// which the user supplies once and we bind to that exact key.
|
||||
const note = document.createElement("p");
|
||||
note.className = "placeholder";
|
||||
note.textContent = "The wire carries no sender name, only this signed key. " +
|
||||
"To reply, give the sender's address — it is checked against this key:";
|
||||
const form = document.createElement("form");
|
||||
form.className = "name-sender";
|
||||
const input = document.createElement("input");
|
||||
input.placeholder = "alice@example.org or smol://…";
|
||||
input.spellcheck = false;
|
||||
const name = document.createElement("button");
|
||||
name.textContent = "name sender & reply";
|
||||
form.append(input, name);
|
||||
form.onsubmit = async (event) => {
|
||||
event.preventDefault();
|
||||
name.disabled = true;
|
||||
try {
|
||||
await nameSender(input.value, opened.sender);
|
||||
openMessage(row.id); // the sender is named now; the reply button appears
|
||||
openCompose({
|
||||
to: store.addressForKey(opened.sender),
|
||||
subject: opened.subject ? (opened.subject.startsWith("Re:") ? opened.subject : `Re: ${opened.subject}`) : "",
|
||||
replyTo: row.id,
|
||||
});
|
||||
} catch (err) {
|
||||
status(err.message, true);
|
||||
} finally {
|
||||
name.disabled = false;
|
||||
}
|
||||
};
|
||||
view.append(note, form);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function replyButton(opened, row) {
|
||||
const reply = document.createElement("button");
|
||||
reply.textContent = "reply";
|
||||
reply.onclick = () => openCompose({
|
||||
to: store.addressForKey(opened.sender) ?? "",
|
||||
subject: opened.subject ? (opened.subject.startsWith("Re:") ? opened.subject : `Re: ${opened.subject}`) : "",
|
||||
replyTo: row.id,
|
||||
});
|
||||
return reply;
|
||||
}
|
||||
|
||||
// Bind a user-supplied address to the key that signed a message. A smol://
|
||||
// address carries its own key (verified); a short address is resolved and the
|
||||
// result kept on first use. Anything that binds a different key is refused.
|
||||
async function nameSender(text, senderKey) {
|
||||
const addr = parseAddress(text.trim());
|
||||
let key = addr.identity, verified = true;
|
||||
if (!key) {
|
||||
const opened = await connect(addr, { requirePin: false });
|
||||
try {
|
||||
({ identity: key } = await resolveOp(opened.session, addr.user));
|
||||
} finally {
|
||||
opened.session.stream.close();
|
||||
}
|
||||
verified = false; // trust on first use, as with any RESOLVE
|
||||
}
|
||||
if (!timingSafeEqual(key, senderKey))
|
||||
throw new SmolError("that address carries a different key than this message's sender");
|
||||
store.saveContact(addr.short, senderKey, verified);
|
||||
status(`${addr.short} saved for this sender${verified ? " (verified key)" : ""}`);
|
||||
renderFolder();
|
||||
}
|
||||
|
||||
// --- compose and send -----------------------------------------------------------
|
||||
|
||||
const compose = $("compose");
|
||||
|
|
|
|||
|
|
@ -102,6 +102,9 @@ input, textarea {
|
|||
.verified { color: var(--accent); font-size: 12px; }
|
||||
.warn { color: var(--warn); }
|
||||
|
||||
.name-sender { display: flex; gap: 8px; margin: 12px 0 0; }
|
||||
.name-sender input { flex: 1; }
|
||||
|
||||
dialog {
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue