contact messages: site settings fields, localized strings, enabled toggle
Data gains contact_enabled/contact_label/contact_email/contact_subject.
The label and subject localize via the chunk-keyed trans store
(i18n.site_string), ride the page asset config as pagerite:contact-*
(absent when disabled), and default the {contact:} button label; a
per-button {contact: Label} arg still wins. Disabled messaging renders
no buttons and rejects visitor sends; admins keep the inbox.
This commit is contained in:
1 parent
1796942892
commit
2adafd215b
5 files changed
+97
-7
No files matched your search
+3
-1
@@ -553,7 +553,9 @@ async def editor_ws(ws: WebSocket) -> None:
|
||||
# shows real cards, not the literal tag. No translation
|
||||
# context: the preview has no lang of its own, so cards
|
||||
# render in their originals.
|
||||
directives: dict = {"contact": views._contact_tag}
|
||||
directives: dict = {
|
||||
"contact": lambda args, _env: views._contact_tag(args, _env, data)
|
||||
}
|
||||
if node is not None and has_cards_tag:
|
||||
directives["cards"] = (
|
||||
lambda args, _env, node=node, path=path: views._cards_tag(
|
||||
|
||||
@@ -210,6 +210,21 @@ class Data(msgspec.Struct):
|
||||
#: Visitor contact threads (docs/messages.md), keyed by the blake3
|
||||
#: hash of the visitor's session token (never the token itself).
|
||||
contact: dict[bytes, ContactThread] = {}
|
||||
#: Messaging on/off: when False, {contact:} buttons do not render,
|
||||
#: visitors get no corner ✉️ icon and sends are rejected (admins keep
|
||||
#: the inbox for the existing threads).
|
||||
contact_enabled: bool = True
|
||||
#: Site-wide default label for the {contact:} button and the visitor
|
||||
#: dialog title (a per-button {contact: Label} arg wins). Empty = the
|
||||
#: built-in "Contact us". Translated per language in Data.trans, keyed
|
||||
#: by chunk hash like titles (i18n.site_string).
|
||||
contact_label: str = ""
|
||||
#: Contact email shown as a mailto link in the visitor dialog (with
|
||||
#: the subject prefilled). Empty = no link.
|
||||
contact_email: str = ""
|
||||
#: Subject for the mailto link; empty = the (localized) label.
|
||||
#: Translated like contact_label.
|
||||
contact_subject: str = ""
|
||||
|
||||
|
||||
def node_markdown(data: Data, node: Node) -> str | None:
|
||||
|
||||
@@ -407,6 +407,36 @@ def set_title_translation(data: Data, node: Node, lang: str, title: str) -> bool
|
||||
return True
|
||||
|
||||
|
||||
def site_string(data: Data, lang: str, text: str) -> str:
|
||||
"""A site-level settings string (not article content — the contact
|
||||
label/subject) in ``lang``: its translation from the trans store,
|
||||
chunk-keyed like titles (the translator service picks these up via
|
||||
pending_items), falling back to the original. The site default
|
||||
language always gets the original."""
|
||||
if not text or not lang or lang == primary_lang(data.menu, ""):
|
||||
return text
|
||||
return data.trans.get(chunk_key(text), {}).get(lang) or text
|
||||
|
||||
|
||||
def set_string_translation(data: Data, lang: str, original: str, text: str) -> bool:
|
||||
"""Record (or drop) a site-string translation (i18n.site_string): a
|
||||
fragment in ``Data.trans`` keyed by the original's chunk hash — the
|
||||
same storage machine translations use, overriding them. Empty or the
|
||||
original's text drops the override. Returns True when anything changed.
|
||||
Pure data ops — the caller wraps in a transaction and invalidates."""
|
||||
key = chunk_key(original)
|
||||
current = data.trans.get(key, {}).get(lang)
|
||||
if not text or text == original:
|
||||
if current is None:
|
||||
return False
|
||||
del data.trans[key][lang]
|
||||
return True
|
||||
if current == text:
|
||||
return False
|
||||
data.trans.setdefault(key, {})[lang] = text
|
||||
return True
|
||||
|
||||
|
||||
def clear_translations(data: Data) -> None:
|
||||
"""Drop all machine translations (``Data.trans``) and rebuild the
|
||||
availability index (``node.langs``) from the surviving user overrides —
|
||||
|
||||
@@ -242,6 +242,11 @@ 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
|
||||
if not data.contact_enabled:
|
||||
# Messaging off in the site settings: sends are rejected (the UI
|
||||
# is already hidden); existing threads and admin replies stay.
|
||||
await ws.send_json({"type": "contact-error", "detail": "messaging is disabled"})
|
||||
return
|
||||
# The first message must introduce the visitor (name; email optional,
|
||||
# unvalidated). The stored identity is fixed from then on.
|
||||
thread = data.contact.get(key)
|
||||
|
||||
+44
-6
@@ -337,6 +337,7 @@ def _layout(
|
||||
canonical: str = "",
|
||||
alternates: list[tuple[str, str]] = (),
|
||||
markdown_url: str = "",
|
||||
contact: dict[str, str] | None = None,
|
||||
) -> Template:
|
||||
"""Page layout template with standard assets and ES-module scripts.
|
||||
|
||||
@@ -369,6 +370,9 @@ def _layout(
|
||||
``alternates`` ((hreflang, href) pairs) are the page's language URLs
|
||||
(see docs/localization.md), emitted right after the viewport and before
|
||||
the social tags: canonical first, then the hreflang alternates.
|
||||
|
||||
``contact`` carries the localized contact-messaging strings
|
||||
(_contact_config; None = messaging disabled) into the asset config.
|
||||
"""
|
||||
doc = Document(
|
||||
E.Title, lang=lang, dir="rtl" if lang in i18n.RTL_LANGUAGES else "ltr"
|
||||
@@ -428,6 +432,15 @@ def _layout(
|
||||
config["pagerite:contact-css"] = ",".join(contact_css)
|
||||
if editor_css:
|
||||
config["pagerite:editor-css"] = editor_css
|
||||
# Localized contact-messaging strings (label = the {contact:} buttons'
|
||||
# default and the visitor dialog's title, mailto address + subject).
|
||||
# Absent entirely when messaging is disabled: pagerite.js then shows
|
||||
# the corner ✉️ icon to admins only.
|
||||
if contact is not None:
|
||||
config["pagerite:contact-label"] = contact["label"]
|
||||
config["pagerite:contact-subject"] = contact["subject"]
|
||||
if contact["email"]:
|
||||
config["pagerite:contact-email"] = contact["email"]
|
||||
if vite_url:
|
||||
for key, value in config.items():
|
||||
doc.meta(name=key, content=value)
|
||||
@@ -904,7 +917,14 @@ def page_content(
|
||||
# A {cards} tag places the card rows itself (possibly several);
|
||||
# without one the children are appended after the content as before.
|
||||
has_cards_tag = _CARDS_TAG_RE.search(content) is not None
|
||||
directives = {"contact": _contact_tag}
|
||||
# Messaging disabled: {contact:} tags render nothing at all.
|
||||
directives = {
|
||||
"contact": (
|
||||
(lambda args, _env: _contact_tag(args, _env, data, lang))
|
||||
if data.contact_enabled
|
||||
else (lambda _args, _env: "")
|
||||
)
|
||||
}
|
||||
if has_cards_tag:
|
||||
directives["cards"] = lambda args, _env: _cards_tag(
|
||||
menu, data, node, path, args, translation, link_lang, lang
|
||||
@@ -986,15 +1006,31 @@ def _cards(
|
||||
_CARDS_TAG_RE = re.compile(r"^\{cards(?::[^{}\n]*)?\}[ \t]*$", re.MULTILINE)
|
||||
|
||||
|
||||
def _contact_tag(args: str, _env: dict) -> str:
|
||||
def _contact_config(data: Data, lang: str) -> dict[str, str]:
|
||||
"""The localized contact-messaging strings for the page's asset config
|
||||
(pagerite:contact-*): the {contact:} buttons' default label doubling as
|
||||
the visitor dialog's title, the mailto address and its subject (the
|
||||
label when no separate subject is configured). Translations come from
|
||||
the chunk-keyed trans store (i18n.site_string)."""
|
||||
label = i18n.site_string(data, lang, data.contact_label) or "Contact us"
|
||||
return {
|
||||
"label": label,
|
||||
"email": data.contact_email,
|
||||
"subject": i18n.site_string(data, lang, data.contact_subject) or label,
|
||||
}
|
||||
|
||||
|
||||
def _contact_tag(args: str, _env: dict, data: Data | None = None, lang: str = "") -> str:
|
||||
"""Expand a ``{contact: Label}`` directive: the button that opens the
|
||||
visitor contact panel (docs/messages.md). The args are the button
|
||||
label ("Contact us" when empty). pagerite.js binds the click and
|
||||
lazy-loads the panel bundle; in the editor preview it stays inert."""
|
||||
visitor contact panel (docs/messages.md). The args are a per-button
|
||||
label overriding the configured site label (Data.contact_label,
|
||||
localized here). pagerite.js binds the click and lazy-loads the panel
|
||||
bundle; in the editor preview it stays inert."""
|
||||
return str(
|
||||
E.button(
|
||||
E.span("✉️", class_="icon"),
|
||||
args.strip() or "Contact us",
|
||||
args.strip()
|
||||
or (_contact_config(data, lang)["label"] if data else "Contact us"),
|
||||
type="button",
|
||||
class_="contact-trigger",
|
||||
)
|
||||
@@ -1420,6 +1456,7 @@ def render_page(
|
||||
alternates,
|
||||
# The page's Markdown counterpart (pages.show_page's .md mode).
|
||||
f"/{path}.md",
|
||||
contact=_contact_config(data, lang) if data.contact_enabled else None,
|
||||
)(
|
||||
Title=f"{title} – {brand}" if brand else title,
|
||||
Brand=_brand_link(brand, brand_html, link_lang),
|
||||
@@ -1483,6 +1520,7 @@ def render_category(
|
||||
favicon,
|
||||
lang=lang,
|
||||
alternates=alternates,
|
||||
contact=_contact_config(data, lang) if data.contact_enabled else None,
|
||||
)(
|
||||
Title=f"{title} – {brand}" if brand else title,
|
||||
Brand=_brand_link(brand, brand_html, link_lang),
|
||||
|
||||
Reference in new issue
Block a user