fix(desktop-chat): estabiliza janelas e melhora multi-conversas
All checks were successful
CI/CD Web + Desktop / Detect changes (push) Successful in 7s
Quality Checks / Lint, Test and Build (push) Successful in 4m41s
CI/CD Web + Desktop / Deploy Convex functions (push) Has been skipped
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 5m30s

This commit is contained in:
esdrasrenan 2025-12-17 01:44:28 -03:00
parent 380b2e44e9
commit 3f9461a18f
3 changed files with 134 additions and 84 deletions

View file

@ -1000,35 +1000,15 @@ async fn process_chat_update(
}
}
// Se ha multiplas sessoes ativas, usar o hub; senao, abrir janela do chat individual
// SIMPLIFICADO: Removido inner_size() que bloqueava a UI thread
// Se ha multiplas sessoes ativas, usar o hub; senao, abrir janela do chat individual.
//
// Importante (UX): em multiplas sessoes, NAO fecha a janela ativa quando chega mensagem em outra conversa.
// O hub + badge/notificacao sinalizam novas mensagens e o usuario decide quando alternar.
if current_sessions.len() > 1 {
// Multiplas sessoes - usar hub window
// Primeiro, fechar todas as janelas individuais de chat para evitar sobreposicao
for session in &current_sessions {
let label = format!("chat-{}", session.ticket_id);
if let Some(window) = app.get_webview_window(&label) {
let _ = window.close();
}
}
if app.get_webview_window(HUB_WINDOW_LABEL).is_none() {
// Hub nao existe - criar minimizado
let _ = open_hub_window(app);
} else {
// Hub ja existe - mostrar e trazer para frente
if let Some(hub) = app.get_webview_window(HUB_WINDOW_LABEL) {
let _ = hub.show();
let _ = hub.set_focus();
let _ = hub.unminimize();
}
}
let _ = open_hub_window(app);
} else {
// Uma sessao - abrir janela individual
// Fechar o Hub se estiver aberto (nao precisa mais quando ha apenas 1 chat)
if let Some(hub) = app.get_webview_window(HUB_WINDOW_LABEL) {
let _ = hub.close();
}
// Uma sessao - nao precisa de hub
let _ = close_hub_window(app);
// Fallback: se nao conseguimos detectar delta, pega a sessao com mais unread e mais recente.
let session_to_show = if best_delta > 0 {
@ -1041,19 +1021,9 @@ async fn process_chat_update(
})
};
// Mostrar janela de chat (se nao existe, cria minimizada; se existe, traz para frente)
// Mostrar janela de chat (sempre minimizada/nao intrusiva)
if let Some(session) = session_to_show {
let label = format!("chat-{}", session.ticket_id);
if let Some(window) = app.get_webview_window(&label) {
// Janela ja existe - mostrar e trazer para frente
let _ = window.show();
let _ = window.set_focus();
// Garantir que fique visivel mesmo se estava minimizada na taskbar
let _ = window.unminimize();
} else {
// Criar nova janela ja minimizada (menos intrusivo)
let _ = open_chat_window_internal(app, &session.ticket_id, session.ticket_ref, true);
}
let _ = open_chat_window_internal(app, &session.ticket_id, session.ticket_ref, true);
}
}
@ -1087,6 +1057,9 @@ async fn process_chat_update(
// WINDOW MANAGEMENT
// ============================================================================
// Serializa operacoes de janela para evitar race/deadlock no Windows (winit/WebView2).
static WINDOW_OP_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(()));
fn resolve_chat_window_position(
app: &tauri::AppHandle,
window: Option<&tauri::WebviewWindow>,
@ -1128,21 +1101,36 @@ fn resolve_chat_window_position(
}
fn open_chat_window_internal(app: &tauri::AppHandle, ticket_id: &str, ticket_ref: u64, start_minimized: bool) -> Result<(), String> {
let _guard = WINDOW_OP_LOCK.lock();
open_chat_window_with_state(app, ticket_id, ticket_ref, start_minimized)
}
/// Abre janela de chat com estado inicial de minimizacao configuravel
fn open_chat_window_with_state(app: &tauri::AppHandle, ticket_id: &str, ticket_ref: u64, start_minimized: bool) -> Result<(), String> {
let label = format!("chat-{}", ticket_id);
crate::log_info!(
"[WINDOW] open_chat_window: label={} ticket_ref={} start_minimized={}",
label,
ticket_ref,
start_minimized
);
// Verificar se ja existe
if let Some(window) = app.get_webview_window(&label) {
let _ = window.set_ignore_cursor_events(false);
crate::log_info!("[WINDOW] {}: window existe -> show()", label);
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
let _ = window.unminimize();
if !start_minimized {
crate::log_info!("[WINDOW] {}: window existe -> set_focus()", label);
window.set_focus().map_err(|e| e.to_string())?;
}
// Expandir a janela se estiver minimizada (quando clicado na lista)
if !start_minimized {
let _ = set_chat_minimized(app, ticket_id, false);
crate::log_info!("[WINDOW] {}: window existe -> set_chat_minimized(false)", label);
let _ = set_chat_minimized_unlocked(app, ticket_id, false);
}
crate::log_info!("[WINDOW] {}: open_chat_window OK (reuso)", label);
return Ok(());
}
@ -1159,7 +1147,17 @@ fn open_chat_window_with_state(app: &tauri::AppHandle, ticket_id: &str, ticket_r
// Usar query param ao inves de path para compatibilidade com SPA
let url_path = format!("index.html?view=chat&ticketId={}&ticketRef={}", ticket_id, ticket_ref);
WebviewWindowBuilder::new(
crate::log_info!(
"[WINDOW] {}: build() inicio size={}x{} pos=({},{}) url={}",
label,
width,
height,
x,
y,
url_path
);
let window = WebviewWindowBuilder::new(
app,
&label,
WebviewUrl::App(url_path.into()),
@ -1172,16 +1170,24 @@ fn open_chat_window_with_state(app: &tauri::AppHandle, ticket_id: &str, ticket_r
.transparent(true) // Permite fundo transparente
.shadow(false) // Desabilitar sombra para transparencia funcionar corretamente
.resizable(false) // Desabilitar redimensionamento manual
// REMOVIDO: always_on_top(true) causa competicao de Z-order com multiplas janelas
// Mantem o chat acessivel mesmo ao trocar de janela/app (skip_taskbar=true).
.always_on_top(true)
.skip_taskbar(true)
.focused(true)
.focused(!start_minimized)
.visible(true)
.build()
.map_err(|e| e.to_string())?;
crate::log_info!("[WINDOW] {}: build() OK", label);
// IMPORTANTE: Garantir que a janela receba eventos de cursor (evita click-through)
let _ = window.set_ignore_cursor_events(false);
crate::log_info!("[WINDOW] {}: pos-build set_chat_minimized({}) inicio", label, start_minimized);
// Reaplica layout/posicao logo apos criar a janela.
// Isso evita que a primeira abertura apareca no canto superior esquerdo em alguns ambientes.
let _ = set_chat_minimized(app, ticket_id, start_minimized);
let _ = set_chat_minimized_unlocked(app, ticket_id, start_minimized);
crate::log_info!("[WINDOW] {}: pos-build set_chat_minimized({}) fim", label, start_minimized);
crate::log_info!("Janela de chat aberta (minimizada={}): {}", start_minimized, label);
Ok(())
@ -1193,6 +1199,7 @@ pub fn open_chat_window(app: &tauri::AppHandle, ticket_id: &str, ticket_ref: u64
}
pub fn close_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> {
let _guard = WINDOW_OP_LOCK.lock();
let label = format!("chat-{}", ticket_id);
if let Some(window) = app.get_webview_window(&label) {
window.close().map_err(|e| e.to_string())?;
@ -1201,6 +1208,7 @@ pub fn close_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(),
}
pub fn minimize_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(), String> {
let _guard = WINDOW_OP_LOCK.lock();
let label = format!("chat-{}", ticket_id);
if let Some(window) = app.get_webview_window(&label) {
window.hide().map_err(|e| e.to_string())?;
@ -1209,7 +1217,7 @@ pub fn minimize_chat_window(app: &tauri::AppHandle, ticket_id: &str) -> Result<(
}
/// Redimensiona a janela de chat para modo minimizado (chip) ou expandido
pub fn set_chat_minimized(app: &tauri::AppHandle, ticket_id: &str, minimized: bool) -> Result<(), String> {
fn set_chat_minimized_unlocked(app: &tauri::AppHandle, ticket_id: &str, minimized: bool) -> Result<(), String> {
let label = format!("chat-{}", ticket_id);
let window = app.get_webview_window(&label).ok_or("Janela não encontrada")?;
@ -1224,13 +1232,22 @@ pub fn set_chat_minimized(app: &tauri::AppHandle, ticket_id: &str, minimized: bo
let (x, y) = resolve_chat_window_position(app, Some(&window), width, height);
// Aplicar novo tamanho e posicao
crate::log_info!("[WINDOW] {}: set_chat_minimized({}) set_size inicio", label, minimized);
window.set_size(tauri::LogicalSize::new(width, height)).map_err(|e| e.to_string())?;
crate::log_info!("[WINDOW] {}: set_chat_minimized({}) set_size OK", label, minimized);
crate::log_info!("[WINDOW] {}: set_chat_minimized({}) set_position inicio", label, minimized);
window.set_position(tauri::LogicalPosition::new(x, y)).map_err(|e| e.to_string())?;
crate::log_info!("[WINDOW] {}: set_chat_minimized({}) set_position OK", label, minimized);
crate::log_info!("Chat {} -> minimized={}", ticket_id, minimized);
Ok(())
}
pub fn set_chat_minimized(app: &tauri::AppHandle, ticket_id: &str, minimized: bool) -> Result<(), String> {
let _guard = WINDOW_OP_LOCK.lock();
set_chat_minimized_unlocked(app, ticket_id, minimized)
}
// ============================================================================
// HUB WINDOW MANAGEMENT (Lista de todas as sessoes)
// ============================================================================
@ -1238,14 +1255,19 @@ pub fn set_chat_minimized(app: &tauri::AppHandle, ticket_id: &str, minimized: bo
const HUB_WINDOW_LABEL: &str = "chat-hub";
pub fn open_hub_window(app: &tauri::AppHandle) -> Result<(), String> {
let _guard = WINDOW_OP_LOCK.lock();
open_hub_window_with_state(app, true) // Por padrao abre minimizada
}
fn open_hub_window_with_state(app: &tauri::AppHandle, start_minimized: bool) -> Result<(), String> {
// Verificar se ja existe
if let Some(window) = app.get_webview_window(HUB_WINDOW_LABEL) {
let _ = window.set_ignore_cursor_events(false);
window.show().map_err(|e| e.to_string())?;
window.set_focus().map_err(|e| e.to_string())?;
let _ = window.unminimize();
if !start_minimized {
window.set_focus().map_err(|e| e.to_string())?;
}
return Ok(());
}
@ -1275,9 +1297,10 @@ fn open_hub_window_with_state(app: &tauri::AppHandle, start_minimized: bool) ->
.transparent(true)
.shadow(false)
.resizable(false) // Desabilitar redimensionamento manual
// REMOVIDO: always_on_top(true) causa competicao de Z-order com multiplas janelas
// Mantem o hub acessivel mesmo ao trocar de janela/app (skip_taskbar=true).
.always_on_top(true)
.skip_taskbar(true)
.focused(true)
.focused(!start_minimized)
.visible(true)
.build()
.map_err(|e| e.to_string())?;
@ -1285,7 +1308,9 @@ fn open_hub_window_with_state(app: &tauri::AppHandle, start_minimized: bool) ->
// IMPORTANTE: Garantir que a janela receba eventos de cursor (evita click-through)
if let Some(hub) = app.get_webview_window(HUB_WINDOW_LABEL) {
let _ = hub.set_ignore_cursor_events(false);
let _ = hub.set_focus();
if !start_minimized {
let _ = hub.set_focus();
}
}
// REMOVIDO TEMPORARIAMENTE: set_hub_minimized logo apos build pode causar
@ -1297,6 +1322,7 @@ fn open_hub_window_with_state(app: &tauri::AppHandle, start_minimized: bool) ->
}
pub fn close_hub_window(app: &tauri::AppHandle) -> Result<(), String> {
let _guard = WINDOW_OP_LOCK.lock();
if let Some(window) = app.get_webview_window(HUB_WINDOW_LABEL) {
window.close().map_err(|e| e.to_string())?;
}
@ -1304,6 +1330,7 @@ pub fn close_hub_window(app: &tauri::AppHandle) -> Result<(), String> {
}
pub fn set_hub_minimized(app: &tauri::AppHandle, minimized: bool) -> Result<(), String> {
let _guard = WINDOW_OP_LOCK.lock();
let window = app.get_webview_window(HUB_WINDOW_LABEL).ok_or("Hub window não encontrada")?;
let (width, height) = if minimized {
@ -1318,8 +1345,10 @@ pub fn set_hub_minimized(app: &tauri::AppHandle, minimized: bool) -> Result<(),
window.set_size(tauri::LogicalSize::new(width, height)).map_err(|e| e.to_string())?;
window.set_position(tauri::LogicalPosition::new(x, y)).map_err(|e| e.to_string())?;
// Reforcar foco apos resize
let _ = window.set_focus();
// Foco apenas quando expandir (evita roubar foco ao minimizar apos abrir um chat).
if !minimized {
let _ = window.set_focus();
}
crate::log_info!("Hub -> minimized={}, size={}x{}, pos=({},{})", minimized, width, height, x, y);
Ok(())