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. -->
{{ m.admin || state.thread?.name || 'you' }} - +
- +
diff --git a/frontend/src/MessagesView.vue b/frontend/src/MessagesView.vue index 5d0d033..fa089a0 100644 --- a/frontend/src/MessagesView.vue +++ b/frontend/src/MessagesView.vue @@ -57,10 +57,8 @@ function preview(t) { return (who + (div.textContent || '')).slice(0, 80) } -// Relative timestamps like elsewhere in analytics; full date on hover. +// Relative timestamps like elsewhere in analytics. const fmtTime = (t) => formatWhen(t) -const fmtFull = (t) => - new Date(t).toLocaleString(undefined, { dateStyle: 'medium', timeStyle: 'short' }) function send(md) { if (thread.value) emit('reply', { id: thread.value.id, text: md }) @@ -84,7 +82,7 @@ function send(md) { {{ t.name || t.id.slice(0, 8) }} {{ preview(t) }} - +
@@ -102,7 +100,7 @@ function send(md) {
{{ m.admin || thread.name || 'visitor' }} - +
diff --git a/pagerite/data.py b/pagerite/data.py index 9757536..1f3fd4b 100644 --- a/pagerite/data.py +++ b/pagerite/data.py @@ -83,8 +83,9 @@ class ContactThread(msgspec.Struct, omit_defaults=True): #: When an admin last looked at the thread; visitor messages newer #: than this are unread in the admin inbox. admin_read: datetime | None = None - #: The visitor's self-entered identity, captured with their first - #: message and fixed afterwards ("" until then). + #: The visitor's self-entered identity (name required, email optional + #: and unvalidated), captured with their first message and fixed + #: afterwards ("" until then). name: str = "" email: str = "" diff --git a/pagerite/messages.py b/pagerite/messages.py index 7700416..67ce6e1 100644 --- a/pagerite/messages.py +++ b/pagerite/messages.py @@ -237,17 +237,15 @@ async def visitor_frame(ws: WebSocket, raw: dict) -> None: if len(text) > MAX_MESSAGE: await ws.send_json({"type": "contact-error", "detail": "message too long"}) return - # The first message must introduce the visitor (name + email); the - # stored identity is fixed from then on. + # The first message must introduce the visitor (name; email optional, + # unvalidated). The stored identity is fixed from then on. thread = data.contact.get(key) name = email = "" if thread is None or not thread.name: name = str(raw.get("name") or "").strip()[:100] email = str(raw.get("email") or "").strip()[:200] - if not name or "@" not in email: - await ws.send_json( - {"type": "contact-error", "detail": "name and email required"} - ) + if not name: + await ws.send_json({"type": "contact-error", "detail": "name required"}) return now = datetime.now(UTC) _append(