Fix/dismissible infinite toasts (#1319)

* fix: make PWA install and OpenCode update toasts dismissible

Both 'Install OpenChamber' and 'OpenCode update available' toasts use
duration: Infinity with no close affordance, so they persist on screen
until the user accepts (install/update) or reloads the tab. For users
who do not want to install the PWA or upgrade right now, this is
intrusive and there is no opt-out.

Add a Dismiss button (sonner cancel action) to both toasts. When the
user dismisses:

- PWA: persist a flag in localStorage so the prompt does not reappear
  on future sessions. Accepting Install still works as before.
- OpenCode update: persist the dismissed version in localStorage. The
  toast will appear again only when a newer version becomes available.

Adds new i18n keys pwa.installPrompt.dismiss and
opencodeUpdate.toast.actions.dismiss across all seven locales (en,
es, ko, pl, pt-BR, uk, zh-CN).

* test: extract toast dedup helpers and cover with 28 unit tests

Lift the dismissal-decision logic out of usePwaInstallPrompt and
OpenCodeUpdateToast into a React-free sibling module so it can be
unit-tested directly. The React surfaces remain sole owners of side
effects (storage writes, toast.info, event listeners); the new module
only answers 'should we show?'.

New module openCodeUpdateDedup.ts exposes four helpers:
- shouldShowPwaInstallToast(input) - three gates: dismissed,
  sessionShown, hasActiveToast.
- shouldShowOpenCodeUpdateToast(input) - empty version, seen set,
  dismissed===version gates; a different dismissed version lets the
  toast resurface for the new release.
- resolveOpenCodeUpdateVersion(detail) - parses CustomEvent payloads
  defensively (null/non-object/non-string -> '').
- resolveOpenCodeUpgradeStatusVersion(status) - parses upgrade status
  payloads (status falsy / available!==true / latestVersion non-string
  -> '').

Consumers now call the helpers and only run the side-effect when the
decision is true. Behaviour is unchanged.

Coverage: 28 tests via bun:test, 33 expects, all pass first try.
This commit is contained in:
Roberto Bertó
2026-05-19 16:57:23 +03:00
committed by GitHub
parent bb87111c49
commit 46bef9b0c7
11 changed files with 395 additions and 22 deletions
+2
View File
@@ -16,6 +16,7 @@ export const dict = {
'common.revealPath.fileManager': 'Open in File Manager',
'pwa.installPrompt.description': 'Install OpenChamber for quicker access',
'pwa.installPrompt.action': 'Install',
'pwa.installPrompt.dismiss': 'Dismiss',
'pwa.installPrompt.started': 'Install started',
'pwa.installPrompt.installed': 'OpenChamber installed',
'layout.mainTab.chat': 'Chat',
@@ -2210,6 +2211,7 @@ export const dict = {
'opencodeUpdate.toast.available.title': 'OpenCode update available',
'opencodeUpdate.toast.available.description': 'Version {version} is ready to install.',
'opencodeUpdate.toast.actions.update': 'Update',
'opencodeUpdate.toast.actions.dismiss': 'Dismiss',
'opencodeUpdate.toast.actions.reload': 'Reload OpenCode',
'opencodeUpdate.toast.upgrading.title': 'Updating OpenCode...',
'opencodeUpdate.toast.upgrading.description': 'Keep OpenChamber open.',
+2
View File
@@ -17,6 +17,7 @@ export const dict: Record<I18nKey, string> = {
"common.revealPath.fileManager": "Abrir en gestor de archivos",
"pwa.installPrompt.description": "Instala OpenChamber para acceder más rápido",
"pwa.installPrompt.action": "Instalar",
"pwa.installPrompt.dismiss": "Descartar",
"pwa.installPrompt.started": "Instalación iniciada",
"pwa.installPrompt.installed": "OpenChamber se instaló",
"layout.mainTab.chat": "Chat",
@@ -2176,6 +2177,7 @@ export const dict: Record<I18nKey, string> = {
"opencodeUpdate.toast.available.title": "Actualización de OpenCode disponible",
"opencodeUpdate.toast.available.description": "La versión {version} está lista para instalar.",
"opencodeUpdate.toast.actions.update": "Actualizar",
"opencodeUpdate.toast.actions.dismiss": "Descartar",
"opencodeUpdate.toast.actions.reload": "Recargar OpenCode",
"opencodeUpdate.toast.upgrading.title": "Actualizando OpenCode...",
"opencodeUpdate.toast.upgrading.description": "Mantén OpenChamber abierto.",
+2
View File
@@ -17,6 +17,7 @@ export const dict: Record<I18nKey, string> = {
'common.revealPath.fileManager': '파일 관리자에서 열기',
'pwa.installPrompt.description': '더 빠르게 접근하려면 OpenChamber를 설치하세요',
'pwa.installPrompt.action': '설치',
'pwa.installPrompt.dismiss': '닫기',
'pwa.installPrompt.started': '설치가 시작되었습니다',
'pwa.installPrompt.installed': 'OpenChamber가 설치되었습니다',
'layout.mainTab.chat': '채팅',
@@ -2210,6 +2211,7 @@ export const dict: Record<I18nKey, string> = {
'opencodeUpdate.toast.available.title': 'OpenCode 업데이트 사용 가능',
'opencodeUpdate.toast.available.description': '버전 {version}을 설치할 수 있습니다.',
'opencodeUpdate.toast.actions.update': '업데이트',
'opencodeUpdate.toast.actions.dismiss': '닫기',
'opencodeUpdate.toast.actions.reload': 'OpenCode 다시 로드',
'opencodeUpdate.toast.upgrading.title': 'OpenCode 업데이트 중...',
'opencodeUpdate.toast.upgrading.description': 'OpenChamber를 열어 두세요.',
+2
View File
@@ -18,6 +18,7 @@ export const dict: Record<I18nKey, string> = {
'common.revealPath.fileManager': 'Otwórz w Menedżerze plików',
'pwa.installPrompt.description': 'Zainstaluj OpenChamber, aby mieć szybszy dostęp',
'pwa.installPrompt.action': 'Zainstaluj',
'pwa.installPrompt.dismiss': 'Odrzuć',
'pwa.installPrompt.started': 'Instalacja rozpoczęta',
'pwa.installPrompt.installed': 'OpenChamber został zainstalowany',
'layout.mainTab.chat': 'Czat',
@@ -2191,6 +2192,7 @@ export const dict: Record<I18nKey, string> = {
'opencodeUpdate.toast.available.title': 'Dostępna aktualizacja OpenCode',
'opencodeUpdate.toast.available.description': 'Wersja {version} jest gotowa do instalacji.',
'opencodeUpdate.toast.actions.update': 'Aktualizuj',
'opencodeUpdate.toast.actions.dismiss': 'Odrzuć',
'opencodeUpdate.toast.actions.reload': 'Przeładuj OpenCode',
'opencodeUpdate.toast.upgrading.title': 'Aktualizowanie OpenCode...',
'opencodeUpdate.toast.upgrading.description': 'Pozostaw OpenChamber otwarte.',
@@ -17,6 +17,7 @@ export const dict: Record<I18nKey, string> = {
"common.revealPath.fileManager": "Abrir no gerenciador de arquivos",
"pwa.installPrompt.description": "Instale o OpenChamber para acessar mais rápido",
"pwa.installPrompt.action": "Instalar",
"pwa.installPrompt.dismiss": "Dispensar",
"pwa.installPrompt.started": "Instalação iniciada",
"pwa.installPrompt.installed": "OpenChamber instalado",
"layout.mainTab.chat": "Chat",
@@ -2176,6 +2177,7 @@ export const dict: Record<I18nKey, string> = {
"opencodeUpdate.toast.available.title": "Atualização do OpenCode disponível",
"opencodeUpdate.toast.available.description": "A versão {version} está pronta para instalar.",
"opencodeUpdate.toast.actions.update": "Atualizar",
"opencodeUpdate.toast.actions.dismiss": "Dispensar",
"opencodeUpdate.toast.actions.reload": "Recarregar OpenCode",
"opencodeUpdate.toast.upgrading.title": "Atualizando OpenCode...",
"opencodeUpdate.toast.upgrading.description": "Mantenha o OpenChamber aberto.",
+2
View File
@@ -17,6 +17,7 @@ export const dict: Record<I18nKey, string> = {
"common.revealPath.fileManager": "Відкрити у файловому менеджері",
"pwa.installPrompt.description": "Установіть OpenChamber для швидшого доступу",
"pwa.installPrompt.action": "Установити",
"pwa.installPrompt.dismiss": "Сховати",
"pwa.installPrompt.started": "Установлення розпочато",
"pwa.installPrompt.installed": "OpenChamber встановлено",
"layout.mainTab.chat": "Чат",
@@ -2176,6 +2177,7 @@ export const dict: Record<I18nKey, string> = {
"opencodeUpdate.toast.available.title": "Доступне оновлення OpenCode",
"opencodeUpdate.toast.available.description": "Версія {version} готова до встановлення.",
"opencodeUpdate.toast.actions.update": "Оновити",
"opencodeUpdate.toast.actions.dismiss": "Сховати",
"opencodeUpdate.toast.actions.reload": "Перезавантажити OpenCode",
"opencodeUpdate.toast.upgrading.title": "Оновлення OpenCode...",
"opencodeUpdate.toast.upgrading.description": "Залиште OpenChamber відкритим.",
@@ -17,6 +17,7 @@ export const dict: Record<I18nKey, string> = {
'common.revealPath.fileManager': '在文件管理器中打开',
'pwa.installPrompt.description': '安装 OpenChamber 以便更快访问',
'pwa.installPrompt.action': '安装',
'pwa.installPrompt.dismiss': '忽略',
'pwa.installPrompt.started': '已开始安装',
'pwa.installPrompt.installed': 'OpenChamber 已安装',
'layout.mainTab.chat': '聊天',
@@ -2176,6 +2177,7 @@ export const dict: Record<I18nKey, string> = {
'opencodeUpdate.toast.available.title': 'OpenCode 有可用更新',
'opencodeUpdate.toast.available.description': '版本 {version} 已可安装。',
'opencodeUpdate.toast.actions.update': '更新',
'opencodeUpdate.toast.actions.dismiss': '忽略',
'opencodeUpdate.toast.actions.reload': '重载 OpenCode',
'opencodeUpdate.toast.upgrading.title': '正在更新 OpenCode...',
'opencodeUpdate.toast.upgrading.description': '请保持 OpenChamber 打开。',