leonardo hai 1 mes
pai
achega
8ac2818492
Modificáronse 2 ficheiros con 88 adicións e 15 borrados
  1. 34 7
      src/layout/ListaConversas.vue
  2. 54 8
      src/views/conversas/ConversasWhatsappView.vue

+ 34 - 7
src/layout/ListaConversas.vue

@@ -20,7 +20,12 @@ const {
 
 const { editingConversaId, editingTitulo, iniciarEdicao, confirmarEdicao, cancelarEdicao } = useConversaEdicao();
 
-const hoveredConversaId = ref(null);
+const confirmandoExclusaoId = ref(null);
+
+function onExcluir(id) {
+  deleteConversation(id);
+  confirmandoExclusaoId.value = null;
+}
 
 const conversacoesAgrupadas = computed(() => {
   const hoje = new Date().setHours(0, 0, 0, 0);
@@ -66,12 +71,10 @@ async function abrirConversa(id) {
         :key="c.id"
         role="button"
         tabindex="0"
-        class="relative w-full flex items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors overflow-hidden cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
+        class="group relative w-full flex items-center gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors overflow-hidden cursor-pointer focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background"
         :class="String(activeConversationId) === c.id
           ? 'text-gray-900 bg-gray-200/70 dark:text-white dark:bg-white/10'
           : 'text-gray-600 hover:text-gray-900 hover:bg-gray-100/80 dark:text-white/70 dark:hover:text-white dark:hover:bg-white/10'"
-        @mouseenter="hoveredConversaId = c.id"
-        @mouseleave="hoveredConversaId = null"
         @click="editingConversaId !== c.id && abrirConversa(c.id)"
         @keydown.enter.prevent="editingConversaId !== c.id && abrirConversa(c.id)"
         @keydown.space.prevent="editingConversaId !== c.id && abrirConversa(c.id)"
@@ -112,8 +115,7 @@ async function abrirConversa(id) {
         <template v-else>
           <span class="flex-1 min-w-0 truncate leading-snug" :title="c.title">{{ c.title }}</span>
           <div
-            v-show="hoveredConversaId === c.id"
-            class="absolute right-0 top-0 h-full flex items-center gap-0.5 pr-2 pl-16"
+            class="absolute right-0 top-0 h-full flex items-center gap-0.5 pr-2 pl-16 opacity-0 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100 max-[760px]:opacity-100"
             :class="String(activeConversationId) === c.id
               ? 'bg-gradient-to-l from-gray-200 from-60% dark:from-[#131f30] to-transparent'
               : 'bg-gradient-to-l from-gray-100 from-60% dark:from-[#101827] to-transparent'"
@@ -124,6 +126,7 @@ async function abrirConversa(id) {
               type="button"
               class="rounded p-0.5 text-gray-400 hover:text-gray-700 dark:text-white/40 dark:hover:text-white transition-colors"
               title="Renomear"
+              aria-label="Renomear conversa"
               @click.stop="iniciarEdicao(c)"
             >
               <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
@@ -134,13 +137,37 @@ async function abrirConversa(id) {
               type="button"
               class="rounded p-0.5 text-gray-400 hover:text-red-600 dark:text-white/40 dark:hover:text-red-400 transition-colors"
               title="Excluir"
-              @click.stop="deleteConversation(c.id)"
+              aria-label="Excluir conversa"
+              @click.stop="confirmandoExclusaoId = c.id"
             >
               <svg class="h-3.5 w-3.5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                 <path stroke-linecap="round" stroke-linejoin="round" d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0" />
               </svg>
             </button>
           </div>
+
+          <div
+            v-if="confirmandoExclusaoId === c.id"
+            class="absolute inset-0 flex items-center justify-end gap-1.5 rounded-xl bg-gray-200/95 px-3 dark:bg-[#182335]"
+            @click.stop
+            @keydown.stop
+          >
+            <span class="min-w-0 flex-1 truncate text-xs text-gray-600 dark:text-white/60">Excluir esta conversa?</span>
+            <button
+              type="button"
+              class="shrink-0 rounded-lg bg-red-600 px-2.5 py-1 text-xs font-medium text-white"
+              @click.stop="onExcluir(c.id)"
+            >
+              Confirmar
+            </button>
+            <button
+              type="button"
+              class="shrink-0 rounded-lg border border-gray-300 px-2.5 py-1 text-xs font-medium text-gray-700 dark:border-white/20 dark:text-white/70"
+              @click.stop="confirmandoExclusaoId = null"
+            >
+              Cancelar
+            </button>
+          </div>
         </template>
       </div>
       </div>

+ 54 - 8
src/views/conversas/ConversasWhatsappView.vue

@@ -36,6 +36,32 @@ const {
 } = useConversas();
 
 const mediaViewer = ref(null);
+const mensagensEl = ref(null);
+
+const NEAR_BOTTOM_THRESHOLD_PX = 80;
+
+function isNearBottom() {
+  const el = mensagensEl.value;
+  if (!el) return true;
+  return el.scrollHeight - el.scrollTop - el.clientHeight <= NEAR_BOTTOM_THRESHOLD_PX;
+}
+
+watch(
+  () => [mensagens.value.length, mensagensLoading.value],
+  async () => {
+    const el = mensagensEl.value;
+    const shouldStick = !el || isNearBottom();
+    await nextTick();
+    if (!el || !shouldStick) return;
+    el.scrollTop = el.scrollHeight;
+  }
+);
+
+watch(conversaAtivaId, async () => {
+  await nextTick();
+  const el = mensagensEl.value;
+  if (el) el.scrollTop = el.scrollHeight;
+});
 
 function midiaInfo(m) {
   return { tipo: m.TipoMidia, transcricao: null, url: midiaUrls.value[m.Id] };
@@ -79,10 +105,22 @@ const isAdmin = computed(() => String(user.value?.Nivel) === "3");
 const texto = ref("");
 const inputResposta = ref(null);
 
+const RESPOSTA_ALTURA_MAX_PX = 120;
+
+function ajustarAlturaResposta() {
+  const el = inputResposta.value;
+  if (!el) return;
+  el.style.height = "auto";
+  el.style.height = `${Math.min(el.scrollHeight, RESPOSTA_ALTURA_MAX_PX)}px`;
+}
+
 function usarSugestao() {
   if (!sugestao.value) return;
   texto.value = sugestao.value;
-  nextTick(() => inputResposta.value?.focus());
+  nextTick(() => {
+    inputResposta.value?.focus();
+    ajustarAlturaResposta();
+  });
 }
 
 const conversaAtiva = computed(() => conversas.value.find((c) => c.Id === conversaAtivaId.value) ?? null);
@@ -166,11 +204,15 @@ async function enviar() {
   if (!texto.value.trim() || enviando.value) return;
   const valor = texto.value;
   texto.value = "";
+  await nextTick();
+  ajustarAlturaResposta();
   try {
     await enviarResposta(valor);
   } catch (err) {
     texto.value = valor;
     toast.erro(mensagemErroEnvio(err));
+    await nextTick();
+    ajustarAlturaResposta();
   }
 }
 
@@ -257,8 +299,9 @@ onUnmounted(() => {
               <button
                 v-if="isAdmin"
                 type="button"
-                class="absolute right-1 top-1 rounded-lg border border-transparent p-1.5 text-gray-400 opacity-0 transition-opacity hover:border-red-200 hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:border-red-900/50 dark:hover:bg-red-950/30 dark:hover:text-red-400"
+                class="absolute right-1 top-1 rounded-lg border border-transparent p-1.5 text-gray-400 opacity-0 transition-opacity hover:border-red-200 hover:bg-red-50 hover:text-red-600 focus:opacity-100 focus-visible:opacity-100 group-hover:opacity-100 max-[760px]:opacity-100 dark:hover:border-red-900/50 dark:hover:bg-red-950/30 dark:hover:text-red-400"
                 title="Excluir conversa"
+                aria-label="Excluir conversa"
                 @click="confirmandoExclusao = c.Id"
               >
                 <svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
@@ -335,7 +378,7 @@ onUnmounted(() => {
             </BaseBadge>
           </div>
 
-          <div class="conv-scroll min-h-0 flex-1 overflow-y-auto p-4">
+          <div ref="mensagensEl" class="conv-scroll min-h-0 flex-1 overflow-y-auto p-4">
             <div v-if="mensagensLoading" class="text-sm text-gray-500 dark:text-gray-400">Carregando conversa...</div>
             <div v-else-if="mensagensError" class="rounded-lg bg-red-50 px-4 py-3 text-xs text-red-800 dark:bg-red-900/30 dark:text-red-300">
               {{ mensagensError }}
@@ -526,14 +569,17 @@ onUnmounted(() => {
           </div>
           </Transition>
 
-          <form class="flex items-center gap-2 border-t border-gray-200 p-3 dark:border-gray-700" @submit.prevent="enviar">
-            <input
+          <form class="flex items-end gap-2 border-t border-gray-200 p-3 dark:border-gray-700" @submit.prevent="enviar">
+            <textarea
               ref="inputResposta"
               v-model="texto"
-              type="text"
-              placeholder="Digite uma resposta..."
-              class="min-w-0 flex-1 rounded-xl border border-gray-300 bg-background px-3 py-2 text-sm outline-none focus:border-primary dark:border-gray-600"
+              rows="1"
+              placeholder="Digite uma resposta... (Enter envia, Shift+Enter quebra linha)"
+              class="conv-scroll min-w-0 flex-1 resize-none rounded-xl border border-gray-300 bg-background px-3 py-2 text-sm outline-none focus:border-primary dark:border-gray-600"
+              style="max-height: 120px"
               :disabled="enviando"
+              @keydown.enter.exact.prevent="enviar"
+              @input="ajustarAlturaResposta"
             />
             <button
               type="submit"