fix(desktop-chat): estabiliza janelas e melhora multi-conversas
All checks were successful
All checks were successful
This commit is contained in:
parent
380b2e44e9
commit
3f9461a18f
3 changed files with 134 additions and 84 deletions
|
|
@ -1000,35 +1000,15 @@ async fn process_chat_update(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Se ha multiplas sessoes ativas, usar o hub; senao, abrir janela do chat individual
|
// Se ha multiplas sessoes ativas, usar o hub; senao, abrir janela do chat individual.
|
||||||
// SIMPLIFICADO: Removido inner_size() que bloqueava a UI thread
|
//
|
||||||
|
// 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 {
|
if current_sessions.len() > 1 {
|
||||||
// Multiplas sessoes - usar hub window
|
let _ = open_hub_window(app);
|
||||||
// Primeiro, fechar todas as janelas individuais de chat para evitar sobreposicao
|
|
||||||
for session in ¤t_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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Uma sessao - abrir janela individual
|
// Uma sessao - nao precisa de hub
|
||||||
// Fechar o Hub se estiver aberto (nao precisa mais quando ha apenas 1 chat)
|
let _ = close_hub_window(app);
|
||||||
if let Some(hub) = app.get_webview_window(HUB_WINDOW_LABEL) {
|
|
||||||
let _ = hub.close();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fallback: se nao conseguimos detectar delta, pega a sessao com mais unread e mais recente.
|
// Fallback: se nao conseguimos detectar delta, pega a sessao com mais unread e mais recente.
|
||||||
let session_to_show = if best_delta > 0 {
|
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 {
|
if let Some(session) = session_to_show {
|
||||||
let label = format!("chat-{}", session.ticket_id);
|
let _ = open_chat_window_internal(app, &session.ticket_id, session.ticket_ref, true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1087,6 +1057,9 @@ async fn process_chat_update(
|
||||||
// WINDOW MANAGEMENT
|
// 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(
|
fn resolve_chat_window_position(
|
||||||
app: &tauri::AppHandle,
|
app: &tauri::AppHandle,
|
||||||
window: Option<&tauri::WebviewWindow>,
|
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> {
|
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)
|
open_chat_window_with_state(app, ticket_id, ticket_ref, start_minimized)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Abre janela de chat com estado inicial de minimizacao configuravel
|
/// 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> {
|
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);
|
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
|
// Verificar se ja existe
|
||||||
if let Some(window) = app.get_webview_window(&label) {
|
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.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)
|
// Expandir a janela se estiver minimizada (quando clicado na lista)
|
||||||
if !start_minimized {
|
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(());
|
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
|
// 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);
|
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,
|
app,
|
||||||
&label,
|
&label,
|
||||||
WebviewUrl::App(url_path.into()),
|
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
|
.transparent(true) // Permite fundo transparente
|
||||||
.shadow(false) // Desabilitar sombra para transparencia funcionar corretamente
|
.shadow(false) // Desabilitar sombra para transparencia funcionar corretamente
|
||||||
.resizable(false) // Desabilitar redimensionamento manual
|
.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)
|
.skip_taskbar(true)
|
||||||
.focused(true)
|
.focused(!start_minimized)
|
||||||
.visible(true)
|
.visible(true)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| e.to_string())?;
|
.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.
|
// Reaplica layout/posicao logo apos criar a janela.
|
||||||
// Isso evita que a primeira abertura apareca no canto superior esquerdo em alguns ambientes.
|
// 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);
|
crate::log_info!("Janela de chat aberta (minimizada={}): {}", start_minimized, label);
|
||||||
Ok(())
|
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> {
|
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);
|
let label = format!("chat-{}", ticket_id);
|
||||||
if let Some(window) = app.get_webview_window(&label) {
|
if let Some(window) = app.get_webview_window(&label) {
|
||||||
window.close().map_err(|e| e.to_string())?;
|
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> {
|
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);
|
let label = format!("chat-{}", ticket_id);
|
||||||
if let Some(window) = app.get_webview_window(&label) {
|
if let Some(window) = app.get_webview_window(&label) {
|
||||||
window.hide().map_err(|e| e.to_string())?;
|
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
|
/// 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 label = format!("chat-{}", ticket_id);
|
||||||
let window = app.get_webview_window(&label).ok_or("Janela não encontrada")?;
|
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);
|
let (x, y) = resolve_chat_window_position(app, Some(&window), width, height);
|
||||||
|
|
||||||
// Aplicar novo tamanho e posicao
|
// 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())?;
|
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())?;
|
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);
|
crate::log_info!("Chat {} -> minimized={}", ticket_id, minimized);
|
||||||
Ok(())
|
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)
|
// 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";
|
const HUB_WINDOW_LABEL: &str = "chat-hub";
|
||||||
|
|
||||||
pub fn open_hub_window(app: &tauri::AppHandle) -> Result<(), String> {
|
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
|
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> {
|
fn open_hub_window_with_state(app: &tauri::AppHandle, start_minimized: bool) -> Result<(), String> {
|
||||||
// Verificar se ja existe
|
// Verificar se ja existe
|
||||||
if let Some(window) = app.get_webview_window(HUB_WINDOW_LABEL) {
|
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.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(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1275,9 +1297,10 @@ fn open_hub_window_with_state(app: &tauri::AppHandle, start_minimized: bool) ->
|
||||||
.transparent(true)
|
.transparent(true)
|
||||||
.shadow(false)
|
.shadow(false)
|
||||||
.resizable(false) // Desabilitar redimensionamento manual
|
.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)
|
.skip_taskbar(true)
|
||||||
.focused(true)
|
.focused(!start_minimized)
|
||||||
.visible(true)
|
.visible(true)
|
||||||
.build()
|
.build()
|
||||||
.map_err(|e| e.to_string())?;
|
.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)
|
// IMPORTANTE: Garantir que a janela receba eventos de cursor (evita click-through)
|
||||||
if let Some(hub) = app.get_webview_window(HUB_WINDOW_LABEL) {
|
if let Some(hub) = app.get_webview_window(HUB_WINDOW_LABEL) {
|
||||||
let _ = hub.set_ignore_cursor_events(false);
|
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
|
// 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> {
|
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) {
|
if let Some(window) = app.get_webview_window(HUB_WINDOW_LABEL) {
|
||||||
window.close().map_err(|e| e.to_string())?;
|
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> {
|
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 window = app.get_webview_window(HUB_WINDOW_LABEL).ok_or("Hub window não encontrada")?;
|
||||||
|
|
||||||
let (width, height) = if minimized {
|
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_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())?;
|
window.set_position(tauri::LogicalPosition::new(x, y)).map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
// Reforcar foco apos resize
|
// Foco apenas quando expandir (evita roubar foco ao minimizar apos abrir um chat).
|
||||||
let _ = window.set_focus();
|
if !minimized {
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
|
||||||
crate::log_info!("Hub -> minimized={}, size={}x{}, pos=({},{})", minimized, width, height, x, y);
|
crate::log_info!("Hub -> minimized={}, size={}x{}, pos=({},{})", minimized, width, height, x, y);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -39,19 +39,11 @@ export function ChatHubWidget() {
|
||||||
return () => window.removeEventListener("resize", handler)
|
return () => window.removeEventListener("resize", handler)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
// DEBUG: Detectar se a janela esta em modo click-through
|
|
||||||
useEffect(() => {
|
|
||||||
const onDown = (e: PointerEvent) => console.log("POINTER DOWN HUB", e.target)
|
|
||||||
window.addEventListener("pointerdown", onDown)
|
|
||||||
return () => window.removeEventListener("pointerdown", onDown)
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const handleSelectSession = async (ticketId: string, ticketRef: number) => {
|
const handleSelectSession = async (ticketId: string, ticketRef: number) => {
|
||||||
console.log("handleSelectSession CALLED", { ticketId, ticketRef })
|
|
||||||
try {
|
try {
|
||||||
// Tauri 2.x auto-converts snake_case (Rust) to camelCase (JS)
|
// Tauri 2.x auto-converts snake_case (Rust) to camelCase (JS)
|
||||||
const result = await invoke("open_chat_window", { ticketId, ticketRef })
|
await invoke("open_chat_window", { ticketId, ticketRef })
|
||||||
console.log("open_chat_window SUCCESS", result)
|
await invoke("close_hub_window")
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("open_chat_window FAILED:", err)
|
console.error("open_chat_window FAILED:", err)
|
||||||
}
|
}
|
||||||
|
|
@ -67,10 +59,8 @@ export function ChatHubWidget() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleExpand = async () => {
|
const handleExpand = async () => {
|
||||||
console.log("handleExpand CALLED")
|
|
||||||
try {
|
try {
|
||||||
const result = await invoke("set_hub_minimized", { minimized: false })
|
await invoke("set_hub_minimized", { minimized: false })
|
||||||
console.log("set_hub_minimized SUCCESS", result)
|
|
||||||
setTimeout(() => setIsMinimized(false), 100)
|
setTimeout(() => setIsMinimized(false), 100)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("set_hub_minimized FAILED:", err)
|
console.error("set_hub_minimized FAILED:", err)
|
||||||
|
|
@ -127,13 +117,9 @@ export function ChatHubWidget() {
|
||||||
<div className="pointer-events-none flex h-full w-full items-end justify-end bg-transparent pr-3">
|
<div className="pointer-events-none flex h-full w-full items-end justify-end bg-transparent pr-3">
|
||||||
<button
|
<button
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
console.log("EXPAND BUTTON CLICKED")
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
handleExpand()
|
handleExpand()
|
||||||
}}
|
}}
|
||||||
onPointerDown={(e) => console.log("EXPAND POINTER DOWN", e.target)}
|
|
||||||
onMouseDown={(e) => console.log("EXPAND MOUSE DOWN", e.target)}
|
|
||||||
onMouseUp={(e) => console.log("EXPAND MOUSE UP", e.target)}
|
|
||||||
className="pointer-events-auto relative flex items-center gap-2 rounded-full bg-black px-4 py-2 text-white shadow-lg hover:bg-black/90"
|
className="pointer-events-auto relative flex items-center gap-2 rounded-full bg-black px-4 py-2 text-white shadow-lg hover:bg-black/90"
|
||||||
>
|
>
|
||||||
<MessageCircle className="size-4" />
|
<MessageCircle className="size-4" />
|
||||||
|
|
@ -175,14 +161,14 @@ export function ChatHubWidget() {
|
||||||
<button
|
<button
|
||||||
onClick={handleMinimize}
|
onClick={handleMinimize}
|
||||||
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
||||||
title="Minimizar"
|
aria-label="Minimizar lista de chats"
|
||||||
>
|
>
|
||||||
<Minimize2 className="size-4" />
|
<Minimize2 className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
||||||
title="Fechar"
|
aria-label="Fechar lista de chats"
|
||||||
>
|
>
|
||||||
<X className="size-4" />
|
<X className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -213,7 +199,6 @@ function SessionItem({
|
||||||
onClick: () => void
|
onClick: () => void
|
||||||
}) {
|
}) {
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
console.log("SESSION ITEM CLICKED", session.ticketRef)
|
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
onClick()
|
onClick()
|
||||||
}
|
}
|
||||||
|
|
@ -221,7 +206,6 @@ function SessionItem({
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
onClick={handleClick}
|
onClick={handleClick}
|
||||||
onPointerDown={() => console.log("SESSION POINTER DOWN", session.ticketRef)}
|
|
||||||
className="flex w-full items-center gap-3 rounded-xl p-3 text-left transition hover:bg-slate-50"
|
className="flex w-full items-center gap-3 rounded-xl p-3 text-left transition hover:bg-slate-50"
|
||||||
>
|
>
|
||||||
{/* Avatar */}
|
{/* Avatar */}
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
||||||
import { open as openDialog } from "@tauri-apps/plugin-dialog"
|
import { open as openDialog } from "@tauri-apps/plugin-dialog"
|
||||||
import { openUrl as openExternal } from "@tauri-apps/plugin-opener"
|
import { openUrl as openExternal } from "@tauri-apps/plugin-opener"
|
||||||
import { invoke } from "@tauri-apps/api/core"
|
import { invoke } from "@tauri-apps/api/core"
|
||||||
import { Send, X, Loader2, MessageCircle, Paperclip, FileText, Image as ImageIcon, File, User, ChevronUp, Minimize2, Eye, Download, Check } from "lucide-react"
|
import { Send, X, Loader2, MessageCircle, Paperclip, FileText, Image as ImageIcon, File, User, ChevronUp, Minimize2, Eye, Download, Check, MessagesSquare } from "lucide-react"
|
||||||
import type { Id } from "@convex/_generated/dataModel"
|
import type { Id } from "@convex/_generated/dataModel"
|
||||||
import { useMachineMessages, usePostMachineMessage, useMarkMachineMessagesRead, type MachineMessage } from "./useConvexMachineQueries"
|
import { useMachineMessages, useMachineSessions, usePostMachineMessage, useMarkMachineMessagesRead, type MachineMessage } from "./useConvexMachineQueries"
|
||||||
import { useConvexMachine } from "./ConvexMachineProvider"
|
import { useConvexMachine } from "./ConvexMachineProvider"
|
||||||
|
|
||||||
const MAX_MESSAGES_IN_MEMORY = 200
|
const MAX_MESSAGES_IN_MEMORY = 200
|
||||||
|
|
@ -178,7 +178,7 @@ function MessageAttachment({
|
||||||
<button
|
<button
|
||||||
onClick={handleView}
|
onClick={handleView}
|
||||||
className="flex size-7 items-center justify-center rounded-full bg-white/20 hover:bg-white/30"
|
className="flex size-7 items-center justify-center rounded-full bg-white/20 hover:bg-white/30"
|
||||||
title="Visualizar"
|
aria-label="Visualizar anexo"
|
||||||
>
|
>
|
||||||
<Eye className="size-4 text-white" />
|
<Eye className="size-4 text-white" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -186,7 +186,7 @@ function MessageAttachment({
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={downloading}
|
disabled={downloading}
|
||||||
className="flex size-7 items-center justify-center rounded-full bg-white/20 hover:bg-white/30 disabled:opacity-60"
|
className="flex size-7 items-center justify-center rounded-full bg-white/20 hover:bg-white/30 disabled:opacity-60"
|
||||||
title="Baixar"
|
aria-label="Baixar anexo"
|
||||||
>
|
>
|
||||||
{downloading ? (
|
{downloading ? (
|
||||||
<Loader2 className="size-4 animate-spin text-white" />
|
<Loader2 className="size-4 animate-spin text-white" />
|
||||||
|
|
@ -204,7 +204,11 @@ function MessageAttachment({
|
||||||
return (
|
return (
|
||||||
<div className={`flex items-center gap-2 rounded-lg p-2 text-xs ${isAgent ? "bg-white/10" : "bg-slate-100"}`}>
|
<div className={`flex items-center gap-2 rounded-lg p-2 text-xs ${isAgent ? "bg-white/10" : "bg-slate-100"}`}>
|
||||||
{getFileIcon(attachment.name)}
|
{getFileIcon(attachment.name)}
|
||||||
<button onClick={handleView} className="flex-1 truncate text-left hover:underline" title="Visualizar">
|
<button
|
||||||
|
onClick={handleView}
|
||||||
|
className="flex-1 truncate text-left hover:underline"
|
||||||
|
aria-label={`Visualizar anexo ${attachment.name}`}
|
||||||
|
>
|
||||||
{attachment.name}
|
{attachment.name}
|
||||||
</button>
|
</button>
|
||||||
{sizeLabel && <span className="text-xs opacity-60">({sizeLabel})</span>}
|
{sizeLabel && <span className="text-xs opacity-60">({sizeLabel})</span>}
|
||||||
|
|
@ -212,7 +216,7 @@ function MessageAttachment({
|
||||||
<button
|
<button
|
||||||
onClick={handleView}
|
onClick={handleView}
|
||||||
className={`flex size-7 items-center justify-center rounded-md ${isAgent ? "hover:bg-white/10" : "hover:bg-slate-200"}`}
|
className={`flex size-7 items-center justify-center rounded-md ${isAgent ? "hover:bg-white/10" : "hover:bg-slate-200"}`}
|
||||||
title="Visualizar"
|
aria-label="Visualizar anexo"
|
||||||
>
|
>
|
||||||
<Eye className="size-4" />
|
<Eye className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -220,7 +224,7 @@ function MessageAttachment({
|
||||||
onClick={handleDownload}
|
onClick={handleDownload}
|
||||||
disabled={downloading}
|
disabled={downloading}
|
||||||
className={`flex size-7 items-center justify-center rounded-md disabled:opacity-60 ${isAgent ? "hover:bg-white/10" : "hover:bg-slate-200"}`}
|
className={`flex size-7 items-center justify-center rounded-md disabled:opacity-60 ${isAgent ? "hover:bg-white/10" : "hover:bg-slate-200"}`}
|
||||||
title="Baixar"
|
aria-label="Baixar anexo"
|
||||||
>
|
>
|
||||||
{downloading ? (
|
{downloading ? (
|
||||||
<Loader2 className="size-4 animate-spin" />
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
|
@ -250,6 +254,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
|
|
||||||
// Convex hooks
|
// Convex hooks
|
||||||
const { apiBaseUrl, machineToken } = useConvexMachine()
|
const { apiBaseUrl, machineToken } = useConvexMachine()
|
||||||
|
const { sessions: machineSessions = [] } = useMachineSessions()
|
||||||
const { messages: convexMessages, hasSession, unreadCount, isLoading } = useMachineMessages(
|
const { messages: convexMessages, hasSession, unreadCount, isLoading } = useMachineMessages(
|
||||||
ticketId as Id<"tickets">,
|
ticketId as Id<"tickets">,
|
||||||
{ limit: MAX_MESSAGES_IN_MEMORY }
|
{ limit: MAX_MESSAGES_IN_MEMORY }
|
||||||
|
|
@ -276,6 +281,22 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
const unreadAgentMessageIds = useMemo(() => getUnreadAgentMessageIds(messages, unreadCount), [messages, unreadCount])
|
const unreadAgentMessageIds = useMemo(() => getUnreadAgentMessageIds(messages, unreadCount), [messages, unreadCount])
|
||||||
const firstUnreadAgentMessageId = unreadAgentMessageIds[0] ?? null
|
const firstUnreadAgentMessageId = unreadAgentMessageIds[0] ?? null
|
||||||
|
|
||||||
|
const otherUnreadCount = useMemo(() => {
|
||||||
|
if (machineSessions.length <= 1) return 0
|
||||||
|
return machineSessions.reduce((sum, session) => {
|
||||||
|
return sum + (session.ticketId === ticketId ? 0 : session.unreadCount)
|
||||||
|
}, 0)
|
||||||
|
}, [machineSessions, ticketId])
|
||||||
|
|
||||||
|
const handleOpenHub = useCallback(async () => {
|
||||||
|
try {
|
||||||
|
await invoke("open_hub_window")
|
||||||
|
await invoke("set_hub_minimized", { minimized: false })
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Erro ao abrir hub:", err)
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
const updateIsAtBottom = useCallback(() => {
|
const updateIsAtBottom = useCallback(() => {
|
||||||
const el = messagesContainerRef.current
|
const el = messagesContainerRef.current
|
||||||
if (!el) return
|
if (!el) return
|
||||||
|
|
@ -562,7 +583,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className="ml-1 rounded-full p-1 text-slate-600 hover:bg-slate-300/60"
|
className="ml-1 rounded-full p-1 text-slate-600 hover:bg-slate-300/60"
|
||||||
title="Fechar"
|
aria-label="Fechar chat"
|
||||||
>
|
>
|
||||||
<X className="size-4" />
|
<X className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -621,17 +642,31 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
|
{machineSessions.length > 1 && (
|
||||||
|
<button
|
||||||
|
onClick={handleOpenHub}
|
||||||
|
className="relative rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
||||||
|
aria-label="Abrir lista de chats"
|
||||||
|
>
|
||||||
|
<MessagesSquare className="size-4" />
|
||||||
|
{otherUnreadCount > 0 && (
|
||||||
|
<span className="absolute -right-1 -top-1 flex size-5 items-center justify-center rounded-full bg-red-500 text-[10px] font-bold text-white">
|
||||||
|
{otherUnreadCount > 9 ? "9+" : otherUnreadCount}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
onClick={handleMinimize}
|
onClick={handleMinimize}
|
||||||
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
||||||
title="Minimizar"
|
aria-label="Minimizar chat"
|
||||||
>
|
>
|
||||||
<Minimize2 className="size-4" />
|
<Minimize2 className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleClose}
|
onClick={handleClose}
|
||||||
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
||||||
title="Fechar"
|
aria-label="Fechar chat"
|
||||||
>
|
>
|
||||||
<X className="size-4" />
|
<X className="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -772,6 +807,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
<button
|
<button
|
||||||
onClick={() => handleRemoveAttachment(att.storageId)}
|
onClick={() => handleRemoveAttachment(att.storageId)}
|
||||||
className="ml-1 rounded p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-600"
|
className="ml-1 rounded p-0.5 text-slate-400 hover:bg-slate-200 hover:text-slate-600"
|
||||||
|
aria-label={`Remover anexo ${att.name}`}
|
||||||
>
|
>
|
||||||
<X className="size-3" />
|
<X className="size-3" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -792,7 +828,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
onClick={handleAttach}
|
onClick={handleAttach}
|
||||||
disabled={isUploading || isSending}
|
disabled={isUploading || isSending}
|
||||||
className="flex size-9 items-center justify-center rounded-lg text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 disabled:opacity-50"
|
className="flex size-9 items-center justify-center rounded-lg text-slate-500 transition hover:bg-slate-100 hover:text-slate-700 disabled:opacity-50"
|
||||||
title="Anexar arquivo"
|
aria-label="Anexar arquivo"
|
||||||
>
|
>
|
||||||
{isUploading ? (
|
{isUploading ? (
|
||||||
<Loader2 className="size-4 animate-spin" />
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
|
@ -804,6 +840,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
||||||
onClick={handleSend}
|
onClick={handleSend}
|
||||||
disabled={(!inputValue.trim() && pendingAttachments.length === 0) || isSending}
|
disabled={(!inputValue.trim() && pendingAttachments.length === 0) || isSending}
|
||||||
className="flex size-9 items-center justify-center rounded-lg bg-black text-white transition hover:bg-black/90 disabled:opacity-50"
|
className="flex size-9 items-center justify-center rounded-lg bg-black text-white transition hover:bg-black/90 disabled:opacity-50"
|
||||||
|
aria-label="Enviar mensagem"
|
||||||
>
|
>
|
||||||
{isSending ? (
|
{isSending ? (
|
||||||
<Loader2 className="size-4 animate-spin" />
|
<Loader2 className="size-4 animate-spin" />
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue