- Save recipient edits and sender choices with reply drafts.
- Keep CC and BCC open when an address is cleared.
- Allow SMTP without credentials when authentication is off.
- Use the workspace timezone for reports and campaign dates.
- Show CSAT responses without reloading the conversation.
- Block access to attachments from deleted messages.
- Accept conditional email templates and catch missing or repeated replies.
- Explain why choosing another default email template failed.
- Reload stale routes and keep widget image fallbacks working.
- Keep the launcher usable by keyboard and reset host button styles.
The widget's UI language was either the inbox's fixed language or, on
"auto", the browser's. Host apps with their own language switch had no
way to tell the widget which language their user picked.
- widget.js: an optional `language` setting (LibredeskSettings or
initLibredesk) is passed to the iframe as `lang`, and
`Libredesk.setLanguage(code)` switches it later. A call made while the
iframe is still loading is sent once it reports WIDGET_LOADED.
- main.js: on inboxes set to "auto", a `lang` the host passed wins over
the browser language; a language the admin fixed for the inbox stays.
- App.vue: SET_LANGUAGE loads the strings and switches the i18n locale in
place (help center content reloads in the new language), so an open
conversation is not reset.
- Short or regional tags match a shipped translation: zh -> zh-CN,
de-AT -> de-DE.
Closes#657
This replaces the old "hide theme toggle" checkbox. Admins can also set a separate primary color for dark mode, and the dark logo falls back to the light one when it's empty.
Reduce the inbox form to five grouped tabs and add a responsive live preview.
Add separate light and dark branding with system theme support.
Add launcher size and logo scale controls. Ref #627
Fix realtime message ordering in the agent dashboard. Fixes#626
An article written in another language can now be joined to an existing
article as its translation, or pulled back out into its own group. Also caps
transcript downloads at 1000 messages and fixes the field widths in the
automation rule and action rows.
- handoff form no longer overwrites a JWT contact's name, email or phone.
The CRM resyncs those on every exchange, so the edit reverted anyway.
Visitors still get their submitted values saved.
- search queries dropped the cs and cp table aliases. Any saved view
filtering on conversation_statuses.name returned a 500.
- status and priority never showed as selected in the search filters.
The combobox compares strings, the store gives numbers.
- widget logout left the previous visitor's pre chat draft on screen.
- an auto hidden campaign preview was suppressed forever instead of for
the session.
- switching campaigns in the admin preview did not close the open panel.
The watcher only saw null and non null, so it now keys on campaign id.
- the Help tab blanked out when featured_ids came back as null.
- copilot panel spinner stuck when no conversation was open.
- help center theme toggle was hidden when there were no nav links and
one locale.
- help center dark mode leaked into the article panel inside the widget.
A moon button next to the language picker switches the site between light
and dark. Visitors start in light and the choice is saved in their browser.
Admins get a dark logo field, a checkbox to hide the button, and a light
or dark switch on the customize preview. Colors an admin picks for the
header and footer only apply in light mode.
Widget:
- home background stays pinned while the content scrolls, fade sits on it
- one radius and shadow scale across cards, buttons and inputs
- live search in the help tab with a "no results" state
- attachment-only messages show "Image" in the conversation preview
- launcher and panel use the same shadows as the header
Admin:
- preview column is a fixed width so zoom does not resize it, hidden on small screens
- preview matches the real widget again
- search spinner replaces the magnifier instead of covering the page
Fixes:
- deleting a proactive message did nothing, the dialog cleared the pending
id before the handler read it
- an inbox created through the API could not be saved from the form, it
jumped to the pre-chat tab with no error
Adds livechatWidgetTabs.cy.js covering the proactive message and help
center tabs.
The launcher grows a little on hover and springs back when the pointer
leaves. Also adds a black launcher logo for light backgrounds, softens
the launcher shadow, and expands the widget when help opens on an article.
Move the translation_group_id migration into v2.9.0 and drop the slug backfill. Existing articles each get their own group and admins link translations from the new UI. The public article page now builds its language links from the group instead of the slug, so translations with different slugs no longer 404.
Expand widget conversations with audience-specific forms, AI handoffs, message previews, and help article translations. Keep existing inbox settings compatible and reject invalid live chat configuration.
Geist replaces Instrument Sans and is served from static/ instead of Google
Fonts, so it works on installs with no internet access.
Public help center pages are cached in Redis via fastcache with ETags, and any
admin write clears the group so edits show up on the next load.
Uptime checkers and link validators probe with HEAD, so register
HEAD on all public routes and treat HEAD as a crawler so probes
do not inflate view counts or write search logs.
The custom-domain host lookup hit the DB before any rate limit.
Both the not-found gate and the host-home redirect now pay the
"public" limit first, and the middleware skips double-charging.
DB errors on help center pages rendered the plain unthemed error
page. Reuse the themed notfound template for them: it now takes
code, title, and text, so 404 and 500 both render inside the theme.
Also cap the sitemap at the URL limit, skip re-embedding unchanged
content in the AI embed reconciler, and autofocus the login email.
Frontend:
- the basics schema broke after the locale refine was added, since
.pick() does not exist on a refined schema. Split the base object
schema out and refine only the full form schema.
- creating a collection no longer overwrites the parent picked in the
sheet with the preset parent, so "None" and other choices stick
- the article editor toolbar is inert while the editor is disabled,
since teleporting moved it out of the pointer-events-none wrapper
- stop stealing focus when a parent swaps the editor content
- a11y: label the link/youtube URL inputs and the link list remove
button, and make the help center name cell a real button
Backend:
- reject a custom domain whose hostname another help center already
uses. App-level check in validateCustomDomain, matching the
case-insensitive hostname comparison routing does.
Static:
- docs template: convert the remaining left/right rules to logical
properties and flip the row arrows in RTL
- fix currentcolor casing in article-content.css
Backend:
- log LinkHelpArticleMedia errors on article create/update
- trim a trailing slash off the root URL in PublicURL
- run uniqueCollectionSlug inside the caller's transaction
- update-article sets author_id directly so clearing the author works
- apply the help_centers template CHECK as a named constraint so it
also lands on installs that ran an earlier build of the migration
Frontend:
- new editor tables get the email-safe inline styles by default
- replace deprecated word-wrap/word-break with overflow-wrap
- reject a default locale that is not in the allowed locales
- show an error when the icon picker sprite fails to load
- name the locale remove buttons and keep classic-only header
fields mounted with v-show
Docs template:
- move the collection link out of <summary>, toggle is its own button
- use logical CSS properties so the sidebar nav renders in RTL
- fix text-rendering keyword casing
Article code blocks were plain text with no language. They now use tiptap's
lowlight code block, and the toolbar button turns into a language dropdown
while the cursor is inside a block. 16 languages are registered.
Lowlight only highlights the editor with decorations, which tiptap never
serializes, so the saved HTML would have stayed plain. The content is run
through highlight.js on save, which bakes the hljs spans in. The published
page then needs no JS for colors, only CSS. Reopening an article flattens
the spans back to text, so they do not pile up. Existing articles need one
re-save to pick this up.
The published article page also gets a copy button on every code block,
injected by the shared behavior template. That template now receives the
root context, which it needed for the icons and labels.
Also: the editor toolbar shrink-wraps its buttons instead of stretching the
full column, and each help center row now has an Articles item in its
dropdown, so the article tree is reachable without clicking the name.
public_url is now custom_domain. It only accepts a bare origin, no path or query.
When a help center has a custom domain, its pages live at /{locale} on that domain. Requests on that host fall through to the router's not-found handler, which prepends /hc/{slug} and re-runs the router once, so nothing about the routes changes. All page links come from a new BasePath value, which is empty-prefixed on a custom domain and /hc/{slug} on the app host. Hits on the old /hc/{slug} paths 301 to the custom domain so there is one canonical address per page.
Redirects now send a path-only Location built through fasthttp's URI encoder. ctx.Redirect would turn it into an absolute http:// URL, since nginx forwards to the app over plain HTTP, and a raw path would let /\host in the URL read as another site.
robots.txt on a custom domain also disallows the search pages.
Help centers now have a template setting with two layouts. Classic is
the default: the existing hero banner, large search bar and collection
cards. Docs is new: sticky top bar, collections sidebar on every page,
command palette search (Ctrl+K) with a no-JS fallback form, and an
on-page table of contents.
Backend adds a template column (default 'classic', checked to the two
values), threads it through insert/update queries and the request
validator, and resolves page template names as '<template>-<page>' when
rendering public pages. Docs pages also get the published tree for the
sidebar.
Public templates are restructured so each layout is fully isolated and
a third template is just a new directory plus its CSS file:
web-templates/help/shared/ holds the head, announcement, lang switcher,
icons and shared JS; help/classic/ and help/docs/ each hold their own
layout and five pages. The classic stylesheet is renamed to
help-center-classic.css so the head links help-center-<template>.css
directly. Template parsing now globs the help/ subdirectories too.
The admin create and edit forms get a template picker, and classic-only
styling fields (header background, collection layout, card icons) are
hidden when docs is selected.
Each help center gets an optional public_url. When set, canonical links, hreflang
alternates, og:url, the sitemap and robots.txt point at that host instead of the
app root URL. Stored media URLs are rewritten to root-relative paths so logos,
favicons, header images and article images resolve on whichever host serves the
page. The URL is validated on both the form and the backend, and trailing slashes
are trimmed.
Also fixes issues found in code review:
- the help center template used a locale link as the data root inside the
alternates loop, which broke every public page on a help center with two or
more locales
- creating an article posted the clicked collection id, so changing the
collection in the sheet filed the article in the wrong place
- the collection field's error message was hidden when the locale had no
collections, so Create did nothing with no error shown
- switching the header type or unticking popular articles dropped those theme
values on save, because vee-validate unsets fields that unmount
- closing a new collection sheet with Esc left the parent id set, so the next
collection you edited got re-parented
- a table row with th labels in the first column was treated as a header row
when preparing article content for embeddings
- the img tag regex stopped at a > inside an attribute value and injected
loading/decoding in the middle of an attribute