diff --git a/docs/messages.md b/docs/messages.md
index 4ee4058..a029c15 100644
--- a/docs/messages.md
+++ b/docs/messages.md
@@ -52,8 +52,9 @@ no extra connection). Frames **to** the server:
- `{token, msg, pow, fr, name?, email?}` — append a message; `msg` is the
Markdown-subset source, `pow` the solved proof-of-work (below), `fr` the
page path it was sent from (stored for context). The **first** message
- of a thread must carry `name` and `email` introducing the visitor; the
- identity is stored on the thread and fixed from then on.
+ of a thread must carry `name` (and optionally `email`, unvalidated)
+ introducing the visitor; the identity is stored on the thread and fixed
+ from then on.
Frames **from** the server: `{type: "thread", id, created, unread, name,
email, messages: [{id, t, admin, html, page}]}` — a full thread snapshot
@@ -145,7 +146,8 @@ server restart (the pending set is RAM-only) still linger — no GC.
`Data.contact: dict[bytes, ContactThread]` in the kanta database, keyed by
the 12-byte blake3 hash of the session token (never the token itself). A
thread is `created`, `admin_read`, the visitor's self-entered
-`name`/`email` (required with the first message, fixed afterwards) and
+`name`/`email` (name required, email optional; captured with the first
+message, fixed afterwards) and
`messages: dict[str, ContactMessage]` keyed by a sortable timestamp-random
id (dict-keyed, so a save's database diff touches only the appended
message). Each message stores `t`, the Markdown source `md`, its strict
diff --git a/frontend/src/ContactPanel.vue b/frontend/src/ContactPanel.vue
index fbae0f5..7bcb020 100644
--- a/frontend/src/ContactPanel.vue
+++ b/frontend/src/ContactPanel.vue
@@ -23,25 +23,21 @@ async function scrollDown() {
watch(() => props.state.thread?.messages?.length, scrollDown)
watch(() => props.state.open, (open) => { if (open) scrollDown() })
-// Relative timestamps like in analytics; the full date is on hover.
+// Relative timestamps like in analytics.
const fmtTime = (t) => formatWhen(t)
-const fmtFull = (t) =>
- new Date(t).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' })
const NAME_KEY = 'pagerite-contact-name'
const EMAIL_KEY = 'pagerite-contact-email'
-// Name and email are asked with the first message; from then on the
-// thread's stored identity labels the visitor's messages.
+// Name (and optionally email) are asked with the first message; from
+// then on the thread's stored identity labels the visitor's messages.
const intro = reactive({
name: localStorage.getItem(NAME_KEY) || '',
email: localStorage.getItem(EMAIL_KEY) || '',
})
const showIntro = computed(() => !props.state.thread?.name)
-// Sending stays disabled until both intro fields look filled.
-const introInvalid = computed(
- () => showIntro.value && (!intro.name.trim() || !intro.email.trim().includes('@')),
-)
+// Sending stays disabled until the name field is filled.
+const introInvalid = computed(() => showIntro.value && !intro.name.trim())
function onSend(md) {
if (showIntro.value) {
@@ -78,14 +74,14 @@ function onSend(md) {
output only, never user markup. -->