|
@@ -36,6 +36,32 @@ const {
|
|
|
} = useConversas();
|
|
} = useConversas();
|
|
|
|
|
|
|
|
const mediaViewer = ref(null);
|
|
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) {
|
|
function midiaInfo(m) {
|
|
|
return { tipo: m.TipoMidia, transcricao: null, url: midiaUrls.value[m.Id] };
|
|
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 texto = ref("");
|
|
|
const inputResposta = ref(null);
|
|
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() {
|
|
function usarSugestao() {
|
|
|
if (!sugestao.value) return;
|
|
if (!sugestao.value) return;
|
|
|
texto.value = sugestao.value;
|
|
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);
|
|
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;
|
|
if (!texto.value.trim() || enviando.value) return;
|
|
|
const valor = texto.value;
|
|
const valor = texto.value;
|
|
|
texto.value = "";
|
|
texto.value = "";
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ ajustarAlturaResposta();
|
|
|
try {
|
|
try {
|
|
|
await enviarResposta(valor);
|
|
await enviarResposta(valor);
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
texto.value = valor;
|
|
texto.value = valor;
|
|
|
toast.erro(mensagemErroEnvio(err));
|
|
toast.erro(mensagemErroEnvio(err));
|
|
|
|
|
+ await nextTick();
|
|
|
|
|
+ ajustarAlturaResposta();
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -257,8 +299,9 @@ onUnmounted(() => {
|
|
|
<button
|
|
<button
|
|
|
v-if="isAdmin"
|
|
v-if="isAdmin"
|
|
|
type="button"
|
|
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"
|
|
title="Excluir conversa"
|
|
|
|
|
+ aria-label="Excluir conversa"
|
|
|
@click="confirmandoExclusao = c.Id"
|
|
@click="confirmandoExclusao = c.Id"
|
|
|
>
|
|
>
|
|
|
<svg class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8">
|
|
<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>
|
|
</BaseBadge>
|
|
|
</div>
|
|
</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-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">
|
|
<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 }}
|
|
{{ mensagensError }}
|
|
@@ -526,14 +569,17 @@ onUnmounted(() => {
|
|
|
</div>
|
|
</div>
|
|
|
</Transition>
|
|
</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"
|
|
ref="inputResposta"
|
|
|
v-model="texto"
|
|
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"
|
|
:disabled="enviando"
|
|
|
|
|
+ @keydown.enter.exact.prevent="enviar"
|
|
|
|
|
+ @input="ajustarAlturaResposta"
|
|
|
/>
|
|
/>
|
|
|
<button
|
|
<button
|
|
|
type="submit"
|
|
type="submit"
|