feat(desktop): in-app prompt to restart for an update
OpenSSF Scorecard / Scorecard analysis (push) Waiting to run
CI / Build & test (Node 20) (push) Canceled after 0s
CI / Build & test (Node 24) (push) Canceled after 0s
CI / Build & test (push) Canceled after 0s
CodeQL / Analyze (javascript-typescript) (push) Canceled after 0s
Security / Secret scan (gitleaks) (push) Canceled after 0s
Security / Dependency scan (OSV-Scanner) (push) Canceled after 0s
Security / IaC/config scan (Trivy) (push) Canceled after 0s
Security / License compliance scan (Trivy) (push) Canceled after 0s

The main process already showed a system notification when an update finished
downloading, and nothing in the renderer listened — so the only cue vanished on
its own and was missed by anyone not watching the desktop at that moment.

A prompt now stays in the window until it is acted on or dismissed, with the
restart wired to the install-update channel the preload already exposed.

Dismissing is 'not now', not 'never': a later version reopens the prompt, since
the version it refers to has changed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-01 18:31:59 -03:00
co-authored by Claude Opus 5
parent 7f08384372
commit 71d2109b2f
4 changed files with 67 additions and 0 deletions
@@ -1,4 +1,5 @@
import React, { useEffect } from 'react';
import { UpdateBanner } from '../ui/UpdateBanner';
import { useExpressionStore } from '../../stores/expressionStore';
import { useParams, useNavigate } from 'react-router-dom';
import { SpaceSidebar } from './SpaceSidebar';
@@ -428,6 +429,7 @@ export function AppLayout() {
(e.g. immediately after Join, or after popping voice-full back to
the root). */}
<SoundController />
<UpdateBanner />
<GlobalAudioRenderer />
<NotificationController />
<UpdateToast />
@@ -0,0 +1,53 @@
import { useEffect, useState } from 'react';
import { isElectron } from '../../platform/platform';
import { useT } from '../../i18n';
/**
* Aviso de que uma atualização já baixou e só falta reiniciar.
*
* O processo principal já mostrava uma notificação do sistema, que some sozinha
* e passa despercebida se a pessoa não estiver olhando. Este aviso fica na
* janela até ser atendido ou dispensado.
*/
export function UpdateBanner() {
const t = useT();
const [version, setVersion] = useState<string | null>(null);
const [dismissed, setDismissed] = useState(false);
useEffect(() => {
if (!isElectron() || !window.backspace?.onUpdateDownloaded) return;
window.backspace.onUpdateDownloaded((info) => {
setVersion(info.version);
// Uma atualização nova reabre o aviso mesmo se a anterior foi dispensada:
// dispensar significa "agora não", não "nunca mais".
setDismissed(false);
});
}, []);
if (!version || dismissed) return null;
return (
<div className="fixed bottom-4 right-4 z-[400] max-w-[320px] glass rounded-xl shadow-xl p-3.5 animate-slide-up">
<div className="text-[13px] font-semibold text-txt-primary">{t('update.ready')}</div>
<p className="text-[12px] text-txt-secondary mt-0.5">
{t('update.readyVersion', { version })}
</p>
<div className="flex gap-2 mt-3">
<button
type="button"
onClick={() => window.backspace?.installUpdate?.()}
className="px-3 py-1.5 rounded-md text-[12px] font-medium bg-accent-primary text-white hover:brightness-110"
>
{t('update.restart')}
</button>
<button
type="button"
onClick={() => setDismissed(true)}
className="px-3 py-1.5 rounded-md text-[12px] font-medium bg-interactive-muted text-txt-secondary hover:text-txt-primary"
>
{t('update.later')}
</button>
</div>
</div>
);
}
+6
View File
@@ -56,6 +56,12 @@ export const en = {
'accountMenu.copyId': 'Copy User ID',
'accountMenu.copied': 'Copied',
// Desktop update
'update.ready': 'Update ready',
'update.readyVersion': 'Version {version} is ready to install.',
'update.restart': 'Restart to update',
'update.later': 'Later',
// Custom emojis and stickers
'expressions.title': 'Emojis & Stickers',
'expressions.emojis': 'Emojis',
+6
View File
@@ -55,6 +55,12 @@ export const ptBR: Partial<Dictionary> = {
'accountMenu.copyId': 'Copiar ID do usuário',
'accountMenu.copied': 'Copiado',
// Atualização do app
'update.ready': 'Atualização pronta',
'update.readyVersion': 'A versão {version} está pronta para instalar.',
'update.restart': 'Reiniciar para atualizar',
'update.later': 'Depois',
// Emojis e figurinhas
'expressions.title': 'Emojis e figurinhas',
'expressions.emojis': 'Emojis',