Compare commits
2 commits
2bdc5ae882
...
3f9461a18f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3f9461a18f | ||
|
|
380b2e44e9 |
4 changed files with 178 additions and 122 deletions
|
|
@ -189,48 +189,38 @@ jobs:
|
|||
chown -R 1000:1000 /target
|
||||
echo "Permissoes do build corrigidas"
|
||||
|
||||
- name: Publish build to stable APP_DIR directory
|
||||
- name: Atualizar symlink do APP_DIR estavel (deploy atomico)
|
||||
run: |
|
||||
set -e
|
||||
DEST="$HOME/apps/sistema"
|
||||
PARENT_DIR="$HOME/apps"
|
||||
set -euo pipefail
|
||||
ROOT="$HOME/apps"
|
||||
STABLE_LINK="$ROOT/sistema.current"
|
||||
|
||||
# SOLUCAO DEFINITIVA: Limpar completamente o destino usando Docker (root)
|
||||
# Isso evita erros de permissao de arquivos criados por Docker em deploys anteriores
|
||||
if [ -d "$DEST" ]; then
|
||||
echo "Limpando diretorio destino: $DEST"
|
||||
# Preservar apenas o .env (configuracoes de producao)
|
||||
if [ -f "$DEST/.env" ]; then
|
||||
docker run --rm -v "$DEST":/src -v /tmp:/backup alpine:3 \
|
||||
cp /src/.env /backup/.env.backup
|
||||
echo ".env salvo em /tmp/.env.backup"
|
||||
fi
|
||||
# Remover o diretorio COMPLETAMENTE usando Docker Alpine como root
|
||||
docker run --rm -v "$PARENT_DIR":/parent alpine:3 \
|
||||
rm -rf /parent/sistema
|
||||
echo "Diretorio removido"
|
||||
mkdir -p "$ROOT"
|
||||
|
||||
# Sanidade: se esses arquivos nao existirem, o container vai falhar no boot.
|
||||
test -f "$EFFECTIVE_APP_DIR/scripts/start-web.sh" || { echo "ERROR: scripts/start-web.sh nao encontrado em $EFFECTIVE_APP_DIR" >&2; exit 1; }
|
||||
test -f "$EFFECTIVE_APP_DIR/stack.yml" || { echo "ERROR: stack.yml nao encontrado em $EFFECTIVE_APP_DIR" >&2; exit 1; }
|
||||
test -d "$EFFECTIVE_APP_DIR/node_modules" || { echo "ERROR: node_modules nao encontrado em $EFFECTIVE_APP_DIR (necessario para next start)" >&2; exit 1; }
|
||||
test -d "$EFFECTIVE_APP_DIR/.next" || { echo "ERROR: .next nao encontrado em $EFFECTIVE_APP_DIR (build nao gerado)" >&2; exit 1; }
|
||||
|
||||
PREV=""
|
||||
if [ -L "$STABLE_LINK" ]; then
|
||||
PREV="$(readlink -f "$STABLE_LINK" || true)"
|
||||
fi
|
||||
echo "PREV_APP_DIR=$PREV" >> "$GITHUB_ENV"
|
||||
|
||||
ln -sfn "$EFFECTIVE_APP_DIR" "$STABLE_LINK"
|
||||
|
||||
# Compat: mantem $HOME/apps/sistema como symlink quando possivel (nao mexe se for pasta).
|
||||
if [ -L "$ROOT/sistema" ] || [ ! -e "$ROOT/sistema" ]; then
|
||||
ln -sfn "$STABLE_LINK" "$ROOT/sistema"
|
||||
fi
|
||||
|
||||
# Recriar o diretorio (sera criado com permissoes do usuario runner)
|
||||
mkdir -p "$DEST"
|
||||
|
||||
# Restaurar .env antes do rsync
|
||||
if [ -f /tmp/.env.backup ]; then
|
||||
cp /tmp/.env.backup "$DEST/.env"
|
||||
rm /tmp/.env.backup
|
||||
echo ".env restaurado"
|
||||
fi
|
||||
|
||||
# Copiar build completo (sem conflitos de permissao agora)
|
||||
rsync -a --no-owner --no-group \
|
||||
--exclude '.pnpm-store' --exclude '.pnpm-store/**' \
|
||||
"$EFFECTIVE_APP_DIR"/ "$DEST"/
|
||||
|
||||
echo "Published build to: $DEST"
|
||||
echo "APP_DIR estavel -> $(readlink -f "$STABLE_LINK")"
|
||||
|
||||
- name: Swarm deploy (stack.yml)
|
||||
run: |
|
||||
APP_DIR_STABLE="$HOME/apps/sistema"
|
||||
APP_DIR_STABLE="$HOME/apps/sistema.current"
|
||||
if [ ! -d "$APP_DIR_STABLE" ]; then
|
||||
echo "ERROR: Stable APP_DIR does not exist: $APP_DIR_STABLE" >&2; exit 1
|
||||
fi
|
||||
|
|
@ -261,8 +251,24 @@ jobs:
|
|||
fi
|
||||
sleep 10
|
||||
done
|
||||
echo "AVISO: Timeout aguardando servicos. Status atual:"
|
||||
docker service ls --filter "label=com.docker.stack.namespace=sistema"
|
||||
echo "ERRO: Timeout aguardando servicos. Status atual:"
|
||||
docker service ls --filter "label=com.docker.stack.namespace=sistema" || true
|
||||
docker service ps sistema_web --no-trunc || true
|
||||
docker service logs sistema_web --since 5m --raw 2>/dev/null | tail -n 200 || true
|
||||
|
||||
if [ -n "${PREV_APP_DIR:-}" ]; then
|
||||
echo "Rollback: revertendo APP_DIR estavel para: $PREV_APP_DIR"
|
||||
ln -sfn "$PREV_APP_DIR" "$HOME/apps/sistema.current"
|
||||
cd "$HOME/apps/sistema.current"
|
||||
set -o allexport
|
||||
if [ -f .env ]; then
|
||||
. ./.env
|
||||
fi
|
||||
set +o allexport
|
||||
APP_DIR="$HOME/apps/sistema.current" RELEASE_SHA=${{ github.sha }} docker stack deploy --with-registry-auth -c stack.yml sistema || true
|
||||
fi
|
||||
|
||||
exit 1
|
||||
|
||||
- name: Cleanup old build workdirs (keep last 2)
|
||||
run: |
|
||||
|
|
@ -270,7 +276,7 @@ jobs:
|
|||
ROOT="$HOME/apps"
|
||||
KEEP=2
|
||||
PATTERN='web.build.*'
|
||||
ACTIVE="$HOME/apps/sistema"
|
||||
ACTIVE="$(readlink -f "$HOME/apps/sistema.current" 2>/dev/null || true)"
|
||||
echo "Scanning $ROOT for old $PATTERN dirs"
|
||||
LIST=$(find "$ROOT" -maxdepth 1 -type d -name "$PATTERN" | sort -r || true)
|
||||
echo "$LIST" | sed -n "1,${KEEP}p" | sed 's/^/Keeping: /' || true
|
||||
|
|
|
|||
|
|
@ -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 ¤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();
|
||||
}
|
||||
}
|
||||
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(())
|
||||
|
|
|
|||
|
|
@ -39,19 +39,11 @@ export function ChatHubWidget() {
|
|||
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) => {
|
||||
console.log("handleSelectSession CALLED", { ticketId, ticketRef })
|
||||
try {
|
||||
// Tauri 2.x auto-converts snake_case (Rust) to camelCase (JS)
|
||||
const result = await invoke("open_chat_window", { ticketId, ticketRef })
|
||||
console.log("open_chat_window SUCCESS", result)
|
||||
await invoke("open_chat_window", { ticketId, ticketRef })
|
||||
await invoke("close_hub_window")
|
||||
} catch (err) {
|
||||
console.error("open_chat_window FAILED:", err)
|
||||
}
|
||||
|
|
@ -67,10 +59,8 @@ export function ChatHubWidget() {
|
|||
}
|
||||
|
||||
const handleExpand = async () => {
|
||||
console.log("handleExpand CALLED")
|
||||
try {
|
||||
const result = await invoke("set_hub_minimized", { minimized: false })
|
||||
console.log("set_hub_minimized SUCCESS", result)
|
||||
await invoke("set_hub_minimized", { minimized: false })
|
||||
setTimeout(() => setIsMinimized(false), 100)
|
||||
} catch (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">
|
||||
<button
|
||||
onClick={(e) => {
|
||||
console.log("EXPAND BUTTON CLICKED")
|
||||
e.stopPropagation()
|
||||
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"
|
||||
>
|
||||
<MessageCircle className="size-4" />
|
||||
|
|
@ -175,14 +161,14 @@ export function ChatHubWidget() {
|
|||
<button
|
||||
onClick={handleMinimize}
|
||||
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" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
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" />
|
||||
</button>
|
||||
|
|
@ -213,7 +199,6 @@ function SessionItem({
|
|||
onClick: () => void
|
||||
}) {
|
||||
const handleClick = (e: React.MouseEvent) => {
|
||||
console.log("SESSION ITEM CLICKED", session.ticketRef)
|
||||
e.stopPropagation()
|
||||
onClick()
|
||||
}
|
||||
|
|
@ -221,7 +206,6 @@ function SessionItem({
|
|||
return (
|
||||
<button
|
||||
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"
|
||||
>
|
||||
{/* Avatar */}
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
|
|||
import { open as openDialog } from "@tauri-apps/plugin-dialog"
|
||||
import { openUrl as openExternal } from "@tauri-apps/plugin-opener"
|
||||
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 { useMachineMessages, usePostMachineMessage, useMarkMachineMessagesRead, type MachineMessage } from "./useConvexMachineQueries"
|
||||
import { useMachineMessages, useMachineSessions, usePostMachineMessage, useMarkMachineMessagesRead, type MachineMessage } from "./useConvexMachineQueries"
|
||||
import { useConvexMachine } from "./ConvexMachineProvider"
|
||||
|
||||
const MAX_MESSAGES_IN_MEMORY = 200
|
||||
|
|
@ -178,7 +178,7 @@ function MessageAttachment({
|
|||
<button
|
||||
onClick={handleView}
|
||||
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" />
|
||||
</button>
|
||||
|
|
@ -186,7 +186,7 @@ function MessageAttachment({
|
|||
onClick={handleDownload}
|
||||
disabled={downloading}
|
||||
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 ? (
|
||||
<Loader2 className="size-4 animate-spin text-white" />
|
||||
|
|
@ -204,7 +204,11 @@ function MessageAttachment({
|
|||
return (
|
||||
<div className={`flex items-center gap-2 rounded-lg p-2 text-xs ${isAgent ? "bg-white/10" : "bg-slate-100"}`}>
|
||||
{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}
|
||||
</button>
|
||||
{sizeLabel && <span className="text-xs opacity-60">({sizeLabel})</span>}
|
||||
|
|
@ -212,7 +216,7 @@ function MessageAttachment({
|
|||
<button
|
||||
onClick={handleView}
|
||||
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" />
|
||||
</button>
|
||||
|
|
@ -220,7 +224,7 @@ function MessageAttachment({
|
|||
onClick={handleDownload}
|
||||
disabled={downloading}
|
||||
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 ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
|
|
@ -250,6 +254,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
|||
|
||||
// Convex hooks
|
||||
const { apiBaseUrl, machineToken } = useConvexMachine()
|
||||
const { sessions: machineSessions = [] } = useMachineSessions()
|
||||
const { messages: convexMessages, hasSession, unreadCount, isLoading } = useMachineMessages(
|
||||
ticketId as Id<"tickets">,
|
||||
{ limit: MAX_MESSAGES_IN_MEMORY }
|
||||
|
|
@ -276,6 +281,22 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
|||
const unreadAgentMessageIds = useMemo(() => getUnreadAgentMessageIds(messages, unreadCount), [messages, unreadCount])
|
||||
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 el = messagesContainerRef.current
|
||||
if (!el) return
|
||||
|
|
@ -562,7 +583,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
|||
<button
|
||||
onClick={handleClose}
|
||||
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" />
|
||||
</button>
|
||||
|
|
@ -621,17 +642,31 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
|||
</div>
|
||||
</div>
|
||||
<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
|
||||
onClick={handleMinimize}
|
||||
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
||||
title="Minimizar"
|
||||
aria-label="Minimizar chat"
|
||||
>
|
||||
<Minimize2 className="size-4" />
|
||||
</button>
|
||||
<button
|
||||
onClick={handleClose}
|
||||
className="rounded-md p-1.5 text-slate-500 hover:bg-slate-100"
|
||||
title="Fechar"
|
||||
aria-label="Fechar chat"
|
||||
>
|
||||
<X className="size-4" />
|
||||
</button>
|
||||
|
|
@ -772,6 +807,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
|||
<button
|
||||
onClick={() => handleRemoveAttachment(att.storageId)}
|
||||
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" />
|
||||
</button>
|
||||
|
|
@ -792,7 +828,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
|||
onClick={handleAttach}
|
||||
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"
|
||||
title="Anexar arquivo"
|
||||
aria-label="Anexar arquivo"
|
||||
>
|
||||
{isUploading ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
|
|
@ -804,6 +840,7 @@ export function ChatWidget({ ticketId, ticketRef }: ChatWidgetProps) {
|
|||
onClick={handleSend}
|
||||
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"
|
||||
aria-label="Enviar mensagem"
|
||||
>
|
||||
{isSending ? (
|
||||
<Loader2 className="size-4 animate-spin" />
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue