refactor: integrate current user data across components
- Replaced instances of `authStore` with `currentUser` to streamline user authentication handling. - Updated permission checks and user-related data retrieval to utilize the new `useQuery` for better performance and clarity. - Cleaned up component structures and improved formatting for consistency and readability. - Enhanced error handling and user feedback mechanisms in various components to improve user experience.
This commit is contained in:
@@ -5,7 +5,6 @@
|
||||
import { format } from "date-fns";
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import { onMount, tick } from "svelte";
|
||||
import { authStore } from "$lib/stores/auth.svelte";
|
||||
|
||||
interface Props {
|
||||
conversaId: Id<"conversas">;
|
||||
@@ -14,17 +13,25 @@
|
||||
let { conversaId }: Props = $props();
|
||||
|
||||
const client = useConvexClient();
|
||||
const mensagens = useQuery(api.chat.obterMensagens, { conversaId, limit: 50 });
|
||||
const mensagens = useQuery(api.chat.obterMensagens, {
|
||||
conversaId,
|
||||
limit: 50,
|
||||
});
|
||||
const digitando = useQuery(api.chat.obterDigitando, { conversaId });
|
||||
const isAdmin = useQuery(api.chat.verificarSeEhAdmin, { conversaId });
|
||||
const conversas = useQuery(api.chat.listarConversas, {});
|
||||
// Usuário atual
|
||||
const currentUser = useQuery(api.auth.getCurrentUser, {});
|
||||
|
||||
let messagesContainer: HTMLDivElement;
|
||||
let shouldScrollToBottom = true;
|
||||
let lastMessageCount = 0;
|
||||
let mensagensNotificadas = $state<Set<string>>(new Set());
|
||||
let showNotificationPopup = $state(false);
|
||||
let notificationMessage = $state<{ remetente: string; conteudo: string } | null>(null);
|
||||
let notificationMessage = $state<{
|
||||
remetente: string;
|
||||
conteudo: string;
|
||||
} | null>(null);
|
||||
let notificationTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||
let mensagensCarregadas = $state(false);
|
||||
|
||||
@@ -33,8 +40,8 @@
|
||||
|
||||
// Carregar mensagens já notificadas do localStorage ao montar
|
||||
$effect(() => {
|
||||
if (typeof window !== 'undefined' && !mensagensCarregadas) {
|
||||
const saved = localStorage.getItem('chat-mensagens-notificadas');
|
||||
if (typeof window !== "undefined" && !mensagensCarregadas) {
|
||||
const saved = localStorage.getItem("chat-mensagens-notificadas");
|
||||
if (saved) {
|
||||
try {
|
||||
const ids = JSON.parse(saved) as string[];
|
||||
@@ -44,7 +51,7 @@
|
||||
}
|
||||
}
|
||||
mensagensCarregadas = true;
|
||||
|
||||
|
||||
// Marcar todas as mensagens atuais como já visualizadas (não tocar beep ao abrir)
|
||||
if (mensagens?.data && mensagens.data.length > 0) {
|
||||
mensagens.data.forEach((msg) => {
|
||||
@@ -57,17 +64,20 @@
|
||||
|
||||
// Salvar mensagens notificadas no localStorage
|
||||
function salvarMensagensNotificadas() {
|
||||
if (typeof window !== 'undefined') {
|
||||
if (typeof window !== "undefined") {
|
||||
const ids = Array.from(mensagensNotificadas);
|
||||
// Limitar a 1000 IDs para não encher o localStorage
|
||||
const idsLimitados = ids.slice(-1000);
|
||||
localStorage.setItem('chat-mensagens-notificadas', JSON.stringify(idsLimitados));
|
||||
localStorage.setItem(
|
||||
"chat-mensagens-notificadas",
|
||||
JSON.stringify(idsLimitados),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Atualizar usuarioAtualId sempre que authStore.usuario mudar
|
||||
// Atualizar usuarioAtualId sempre que currentUser mudar
|
||||
$effect(() => {
|
||||
const usuario = authStore.usuario;
|
||||
const usuario = currentUser?.data;
|
||||
if (usuario?._id) {
|
||||
const idStr = String(usuario._id).trim();
|
||||
usuarioAtualId = idStr || null;
|
||||
@@ -80,11 +90,14 @@
|
||||
function tocarSomNotificacao() {
|
||||
try {
|
||||
// Usar AudioContext (requer interação do usuário para iniciar)
|
||||
const AudioContextClass = window.AudioContext || (window as { webkitAudioContext?: typeof AudioContext }).webkitAudioContext;
|
||||
const AudioContextClass =
|
||||
window.AudioContext ||
|
||||
(window as { webkitAudioContext?: typeof AudioContext })
|
||||
.webkitAudioContext;
|
||||
if (!AudioContextClass) return;
|
||||
|
||||
|
||||
let audioContext: AudioContext | null = null;
|
||||
|
||||
|
||||
try {
|
||||
audioContext = new AudioContext();
|
||||
} catch (e) {
|
||||
@@ -92,40 +105,49 @@
|
||||
console.warn("Não foi possível criar AudioContext:", e);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// Resumir contexto se estiver suspenso (necessário após interação do usuário)
|
||||
if (audioContext.state === 'suspended') {
|
||||
audioContext.resume().then(() => {
|
||||
const oscillator = audioContext!.createOscillator();
|
||||
const gainNode = audioContext!.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext!.destination);
|
||||
|
||||
oscillator.frequency.value = 800;
|
||||
oscillator.type = 'sine';
|
||||
|
||||
gainNode.gain.setValueAtTime(0.2, audioContext!.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext!.currentTime + 0.3);
|
||||
|
||||
oscillator.start(audioContext!.currentTime);
|
||||
oscillator.stop(audioContext!.currentTime + 0.3);
|
||||
}).catch(() => {
|
||||
// Ignorar erro se não conseguir resumir
|
||||
});
|
||||
if (audioContext.state === "suspended") {
|
||||
audioContext
|
||||
.resume()
|
||||
.then(() => {
|
||||
const oscillator = audioContext!.createOscillator();
|
||||
const gainNode = audioContext!.createGain();
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext!.destination);
|
||||
|
||||
oscillator.frequency.value = 800;
|
||||
oscillator.type = "sine";
|
||||
|
||||
gainNode.gain.setValueAtTime(0.2, audioContext!.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.01,
|
||||
audioContext!.currentTime + 0.3,
|
||||
);
|
||||
|
||||
oscillator.start(audioContext!.currentTime);
|
||||
oscillator.stop(audioContext!.currentTime + 0.3);
|
||||
})
|
||||
.catch(() => {
|
||||
// Ignorar erro se não conseguir resumir
|
||||
});
|
||||
} else {
|
||||
const oscillator = audioContext.createOscillator();
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
|
||||
oscillator.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
|
||||
oscillator.frequency.value = 800;
|
||||
oscillator.type = 'sine';
|
||||
|
||||
oscillator.type = "sine";
|
||||
|
||||
gainNode.gain.setValueAtTime(0.2, audioContext.currentTime);
|
||||
gainNode.gain.exponentialRampToValueAtTime(0.01, audioContext.currentTime + 0.3);
|
||||
|
||||
gainNode.gain.exponentialRampToValueAtTime(
|
||||
0.01,
|
||||
audioContext.currentTime + 0.3,
|
||||
);
|
||||
|
||||
oscillator.start(audioContext.currentTime);
|
||||
oscillator.stop(audioContext.currentTime + 0.3);
|
||||
}
|
||||
@@ -140,31 +162,39 @@
|
||||
if (mensagens?.data && messagesContainer) {
|
||||
const currentCount = mensagens.data.length;
|
||||
const isNewMessage = currentCount > lastMessageCount;
|
||||
|
||||
|
||||
// Detectar nova mensagem de outro usuário
|
||||
if (isNewMessage && mensagens.data.length > 0 && usuarioAtualId) {
|
||||
const ultimaMensagem = mensagens.data[mensagens.data.length - 1];
|
||||
const mensagemId = String(ultimaMensagem._id);
|
||||
const remetenteIdStr = ultimaMensagem.remetenteId
|
||||
? String(ultimaMensagem.remetenteId).trim()
|
||||
: (ultimaMensagem.remetente?._id ? String(ultimaMensagem.remetente._id).trim() : null);
|
||||
|
||||
const remetenteIdStr = ultimaMensagem.remetenteId
|
||||
? String(ultimaMensagem.remetenteId).trim()
|
||||
: ultimaMensagem.remetente?._id
|
||||
? String(ultimaMensagem.remetente._id).trim()
|
||||
: null;
|
||||
|
||||
// Se é uma nova mensagem de outro usuário (não minha) E ainda não foi notificada
|
||||
if (remetenteIdStr && remetenteIdStr !== usuarioAtualId && !mensagensNotificadas.has(mensagemId)) {
|
||||
if (
|
||||
remetenteIdStr &&
|
||||
remetenteIdStr !== usuarioAtualId &&
|
||||
!mensagensNotificadas.has(mensagemId)
|
||||
) {
|
||||
// Marcar como notificada antes de tocar som (evita duplicação)
|
||||
mensagensNotificadas.add(mensagemId);
|
||||
salvarMensagensNotificadas();
|
||||
|
||||
|
||||
// Tocar som de notificação (apenas uma vez)
|
||||
tocarSomNotificacao();
|
||||
|
||||
|
||||
// Mostrar popup de notificação
|
||||
notificationMessage = {
|
||||
remetente: ultimaMensagem.remetente?.nome || "Usuário",
|
||||
conteudo: ultimaMensagem.conteudo.substring(0, 100) + (ultimaMensagem.conteudo.length > 100 ? "..." : "")
|
||||
conteudo:
|
||||
ultimaMensagem.conteudo.substring(0, 100) +
|
||||
(ultimaMensagem.conteudo.length > 100 ? "..." : ""),
|
||||
};
|
||||
showNotificationPopup = true;
|
||||
|
||||
|
||||
// Ocultar popup após 5 segundos
|
||||
if (notificationTimeout) {
|
||||
clearTimeout(notificationTimeout);
|
||||
@@ -175,7 +205,7 @@
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (isNewMessage || shouldScrollToBottom) {
|
||||
// Usar requestAnimationFrame para garantir que o DOM foi atualizado
|
||||
requestAnimationFrame(() => {
|
||||
@@ -186,7 +216,7 @@
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
lastMessageCount = currentCount;
|
||||
}
|
||||
});
|
||||
@@ -195,9 +225,11 @@
|
||||
$effect(() => {
|
||||
if (mensagens?.data && mensagens.data.length > 0 && usuarioAtualId) {
|
||||
const ultimaMensagem = mensagens.data[mensagens.data.length - 1];
|
||||
const remetenteIdStr = ultimaMensagem.remetenteId
|
||||
? String(ultimaMensagem.remetenteId).trim()
|
||||
: (ultimaMensagem.remetente?._id ? String(ultimaMensagem.remetente._id).trim() : null);
|
||||
const remetenteIdStr = ultimaMensagem.remetenteId
|
||||
? String(ultimaMensagem.remetenteId).trim()
|
||||
: ultimaMensagem.remetente?._id
|
||||
? String(ultimaMensagem.remetente._id).trim()
|
||||
: null;
|
||||
// Só marcar como lida se não for minha mensagem
|
||||
if (remetenteIdStr && remetenteIdStr !== usuarioAtualId) {
|
||||
client.mutation(api.chat.marcarComoLida, {
|
||||
@@ -265,7 +297,9 @@
|
||||
lidaPor?: Id<"usuarios">[]; // IDs dos usuários que leram a mensagem
|
||||
}
|
||||
|
||||
function agruparMensagensPorDia(msgs: Mensagem[]): Record<string, Mensagem[]> {
|
||||
function agruparMensagensPorDia(
|
||||
msgs: Mensagem[],
|
||||
): Record<string, Mensagem[]> {
|
||||
const grupos: Record<string, Mensagem[]> = {};
|
||||
for (const msg of msgs) {
|
||||
const dia = formatarDiaMensagem(msg.enviadaEm);
|
||||
@@ -291,7 +325,9 @@
|
||||
});
|
||||
}
|
||||
|
||||
function getEmojisReacao(mensagem: Mensagem): Array<{ emoji: string; count: number }> {
|
||||
function getEmojisReacao(
|
||||
mensagem: Mensagem,
|
||||
): Array<{ emoji: string; count: number }> {
|
||||
if (!mensagem.reagiuPor || mensagem.reagiuPor.length === 0) return [];
|
||||
|
||||
const emojiMap: Record<string, number> = {};
|
||||
@@ -336,27 +372,33 @@
|
||||
novoConteudoEditado = "";
|
||||
}
|
||||
|
||||
async function deletarMensagem(mensagemId: Id<"mensagens">, isAdminDeleting: boolean = false) {
|
||||
const mensagemTexto = isAdminDeleting
|
||||
async function deletarMensagem(
|
||||
mensagemId: Id<"mensagens">,
|
||||
isAdminDeleting: boolean = false,
|
||||
) {
|
||||
const mensagemTexto = isAdminDeleting
|
||||
? "Tem certeza que deseja deletar esta mensagem como administrador? O remetente será notificado."
|
||||
: "Tem certeza que deseja deletar esta mensagem?";
|
||||
|
||||
|
||||
if (!confirm(mensagemTexto)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (isAdminDeleting) {
|
||||
const resultado = await client.mutation(api.chat.deletarMensagemComoAdmin, {
|
||||
mensagemId,
|
||||
});
|
||||
const resultado = await client.mutation(
|
||||
api.chat.deletarMensagemComoAdmin,
|
||||
{
|
||||
mensagemId,
|
||||
},
|
||||
);
|
||||
if (!resultado.sucesso) {
|
||||
alert(resultado.erro || "Erro ao deletar mensagem");
|
||||
}
|
||||
} else {
|
||||
await client.mutation(api.chat.deletarMensagem, {
|
||||
mensagemId,
|
||||
});
|
||||
await client.mutation(api.chat.deletarMensagem, {
|
||||
mensagemId,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Erro ao deletar mensagem:", error);
|
||||
@@ -393,7 +435,7 @@
|
||||
// Para conversas individuais: verificar se o outro participante leu
|
||||
if (conversa.tipo === "individual") {
|
||||
const outroParticipante = conversa.participantes?.find(
|
||||
(p: any) => String(p) !== usuarioAtualId
|
||||
(p: any) => String(p) !== usuarioAtualId,
|
||||
);
|
||||
if (outroParticipante) {
|
||||
return lidaPorStr.includes(String(outroParticipante));
|
||||
@@ -402,13 +444,16 @@
|
||||
|
||||
// Para grupos/salas: verificar se pelo menos um outro participante leu
|
||||
if (conversa.tipo === "grupo" || conversa.tipo === "sala_reuniao") {
|
||||
const outrosParticipantes = conversa.participantes?.filter(
|
||||
(p: any) => String(p) !== usuarioAtualId && String(p) !== String(mensagem.remetenteId)
|
||||
) || [];
|
||||
const outrosParticipantes =
|
||||
conversa.participantes?.filter(
|
||||
(p: any) =>
|
||||
String(p) !== usuarioAtualId &&
|
||||
String(p) !== String(mensagem.remetenteId),
|
||||
) || [];
|
||||
if (outrosParticipantes.length === 0) return false;
|
||||
// Verificar se pelo menos um outro participante leu
|
||||
return outrosParticipantes.some((p: any) =>
|
||||
lidaPorStr.includes(String(p))
|
||||
lidaPorStr.includes(String(p)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -426,7 +471,9 @@
|
||||
{#each Object.entries(gruposPorDia) as [dia, mensagensDia]}
|
||||
<!-- Separador de dia -->
|
||||
<div class="flex items-center justify-center my-4">
|
||||
<div class="px-3 py-1 rounded-full bg-base-300 text-base-content/70 text-xs">
|
||||
<div
|
||||
class="px-3 py-1 rounded-full bg-base-300 text-base-content/70 text-xs"
|
||||
>
|
||||
{dia}
|
||||
</div>
|
||||
</div>
|
||||
@@ -444,14 +491,17 @@
|
||||
}
|
||||
return null;
|
||||
})()}
|
||||
{@const isMinha = usuarioAtualId && remetenteIdStr && remetenteIdStr === usuarioAtualId}
|
||||
<div class={`flex mb-4 w-full ${isMinha ? "justify-end" : "justify-start"}`}>
|
||||
<div class={`flex flex-col max-w-[75%] ${isMinha ? "items-end" : "items-start"}`}>
|
||||
{@const isMinha =
|
||||
usuarioAtualId && remetenteIdStr && remetenteIdStr === usuarioAtualId}
|
||||
<div
|
||||
class={`flex mb-4 w-full ${isMinha ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<div
|
||||
class={`flex flex-col max-w-[75%] ${isMinha ? "items-end" : "items-start"}`}
|
||||
>
|
||||
<!-- Nome do remetente (sempre exibido, mas discreto para mensagens próprias) -->
|
||||
{#if isMinha}
|
||||
<p class="text-xs text-base-content/40 mb-1 px-3">
|
||||
Você
|
||||
</p>
|
||||
<p class="text-xs text-base-content/40 mb-1 px-3">Você</p>
|
||||
{:else}
|
||||
<p class="text-xs text-base-content/60 mb-1 px-3">
|
||||
{mensagem.remetente?.nome || "Usuário"}
|
||||
@@ -468,13 +518,15 @@
|
||||
>
|
||||
{#if mensagem.mensagemOriginal}
|
||||
<!-- Preview da mensagem respondida -->
|
||||
<div class="mb-2 pl-3 border-l-2 border-base-content/20 opacity-70">
|
||||
<div
|
||||
class="mb-2 pl-3 border-l-2 border-base-content/20 opacity-70"
|
||||
>
|
||||
<p class="text-xs font-medium">
|
||||
{mensagem.mensagemOriginal.remetente?.nome || "Usuário"}
|
||||
</p>
|
||||
<p class="text-xs truncate">
|
||||
{mensagem.mensagemOriginal.deletada
|
||||
? "Mensagem deletada"
|
||||
{mensagem.mensagemOriginal.deletada
|
||||
? "Mensagem deletada"
|
||||
: mensagem.mensagemOriginal.conteudo}
|
||||
</p>
|
||||
</div>
|
||||
@@ -514,12 +566,16 @@
|
||||
{:else if mensagem.tipo === "texto"}
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-start gap-2">
|
||||
<p class="text-sm whitespace-pre-wrap break-words flex-1">{mensagem.conteudo}</p>
|
||||
<p class="text-sm whitespace-pre-wrap break-words flex-1">
|
||||
{mensagem.conteudo}
|
||||
</p>
|
||||
{#if mensagem.editadaEm}
|
||||
<span class="text-xs opacity-50 italic" title="Editado">(editado)</span>
|
||||
<span class="text-xs opacity-50 italic" title="Editado"
|
||||
>(editado)</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
|
||||
<!-- Preview de link -->
|
||||
{#if mensagem.linkPreview}
|
||||
<a
|
||||
@@ -534,19 +590,26 @@
|
||||
alt={mensagem.linkPreview.titulo || "Preview"}
|
||||
class="w-full h-48 object-cover"
|
||||
onerror={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = "none";
|
||||
(e.target as HTMLImageElement).style.display =
|
||||
"none";
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<div class="p-3 bg-base-200">
|
||||
{#if mensagem.linkPreview.site}
|
||||
<p class="text-xs text-base-content/50 mb-1">{mensagem.linkPreview.site}</p>
|
||||
<p class="text-xs text-base-content/50 mb-1">
|
||||
{mensagem.linkPreview.site}
|
||||
</p>
|
||||
{/if}
|
||||
{#if mensagem.linkPreview.titulo}
|
||||
<p class="text-sm font-medium text-base-content mb-1">{mensagem.linkPreview.titulo}</p>
|
||||
<p class="text-sm font-medium text-base-content mb-1">
|
||||
{mensagem.linkPreview.titulo}
|
||||
</p>
|
||||
{/if}
|
||||
{#if mensagem.linkPreview.descricao}
|
||||
<p class="text-xs text-base-content/70 line-clamp-2">{mensagem.linkPreview.descricao}</p>
|
||||
<p class="text-xs text-base-content/70 line-clamp-2">
|
||||
{mensagem.linkPreview.descricao}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</a>
|
||||
@@ -561,7 +624,9 @@
|
||||
/>
|
||||
</div>
|
||||
{#if mensagem.conteudo}
|
||||
<p class="text-sm whitespace-pre-wrap break-words">{mensagem.conteudo}</p>
|
||||
<p class="text-sm whitespace-pre-wrap break-words">
|
||||
{mensagem.conteudo}
|
||||
</p>
|
||||
{/if}
|
||||
{:else if mensagem.tipo === "arquivo"}
|
||||
<a
|
||||
@@ -604,7 +669,8 @@
|
||||
class="text-xs px-2 py-0.5 rounded-full bg-base-300/50 hover:bg-base-300"
|
||||
onclick={() => handleReagir(mensagem._id, reacao.emoji)}
|
||||
>
|
||||
{reacao.emoji} {reacao.count}
|
||||
{reacao.emoji}
|
||||
{reacao.count}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
@@ -622,91 +688,91 @@
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Timestamp e ações -->
|
||||
<div
|
||||
class={`flex items-center gap-2 mt-1 px-3 ${isMinha ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<p class="text-xs text-base-content/50">
|
||||
{formatarDataMensagem(mensagem.enviadaEm)}
|
||||
</p>
|
||||
{#if isMinha && !mensagem.deletada && !mensagem.agendadaPara}
|
||||
<!-- Indicadores de status de envio e leitura -->
|
||||
<div class="flex items-center gap-0.5 ml-1">
|
||||
{#if mensagemFoiLida(mensagem)}
|
||||
<!-- Dois checks azuis para mensagem lida -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5 text-blue-500"
|
||||
style="margin-left: -2px;"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5 text-blue-500"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Um check verde para mensagem enviada -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5 text-green-500"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !mensagem.deletada && !mensagem.agendadaPara}
|
||||
<div class="flex gap-1">
|
||||
{#if isMinha}
|
||||
<!-- Ações para minhas próprias mensagens -->
|
||||
<button
|
||||
class="text-xs text-base-content/50 hover:text-primary transition-colors"
|
||||
onclick={() => editarMensagem(mensagem)}
|
||||
title="Editar mensagem"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
class="text-xs text-base-content/50 hover:text-error transition-colors"
|
||||
onclick={() => deletarMensagem(mensagem._id, false)}
|
||||
title="Deletar mensagem"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
{:else if isAdmin?.data}
|
||||
<!-- Ações para admin deletar mensagens de outros -->
|
||||
<button
|
||||
class="text-xs text-base-content/50 hover:text-error transition-colors"
|
||||
onclick={() => deletarMensagem(mensagem._id, true)}
|
||||
title="Deletar mensagem (como administrador)"
|
||||
>
|
||||
🗑️ Admin
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Timestamp e ações -->
|
||||
<div
|
||||
class={`flex items-center gap-2 mt-1 px-3 ${isMinha ? "justify-end" : "justify-start"}`}
|
||||
>
|
||||
<p class="text-xs text-base-content/50">
|
||||
{formatarDataMensagem(mensagem.enviadaEm)}
|
||||
</p>
|
||||
{#if isMinha && !mensagem.deletada && !mensagem.agendadaPara}
|
||||
<!-- Indicadores de status de envio e leitura -->
|
||||
<div class="flex items-center gap-0.5 ml-1">
|
||||
{#if mensagemFoiLida(mensagem)}
|
||||
<!-- Dois checks azuis para mensagem lida -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5 text-blue-500"
|
||||
style="margin-left: -2px;"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5 text-blue-500"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{:else}
|
||||
<!-- Um check verde para mensagem enviada -->
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="w-3.5 h-3.5 text-green-500"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M19.916 4.626a.75.75 0 0 1 .208 1.04l-9 13.5a.75.75 0 0 1-1.154.114l-6-6a.75.75 0 0 1 1.06-1.06l5.353 5.353 8.493-12.74a.75.75 0 0 1 1.04-.207Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if !mensagem.deletada && !mensagem.agendadaPara}
|
||||
<div class="flex gap-1">
|
||||
{#if isMinha}
|
||||
<!-- Ações para minhas próprias mensagens -->
|
||||
<button
|
||||
class="text-xs text-base-content/50 hover:text-primary transition-colors"
|
||||
onclick={() => editarMensagem(mensagem)}
|
||||
title="Editar mensagem"
|
||||
>
|
||||
✏️
|
||||
</button>
|
||||
<button
|
||||
class="text-xs text-base-content/50 hover:text-error transition-colors"
|
||||
onclick={() => deletarMensagem(mensagem._id, false)}
|
||||
title="Deletar mensagem"
|
||||
>
|
||||
🗑️
|
||||
</button>
|
||||
{:else if isAdmin?.data}
|
||||
<!-- Ações para admin deletar mensagens de outros -->
|
||||
<button
|
||||
class="text-xs text-base-content/50 hover:text-error transition-colors"
|
||||
onclick={() => deletarMensagem(mensagem._id, true)}
|
||||
title="Deletar mensagem (como administrador)"
|
||||
>
|
||||
🗑️ Admin
|
||||
</button>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
@@ -716,7 +782,9 @@
|
||||
{#if digitando?.data && digitando.data.length > 0}
|
||||
<div class="flex items-center gap-2 mb-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="w-2 h-2 rounded-full bg-base-content/50 animate-bounce"></div>
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-base-content/50 animate-bounce"
|
||||
></div>
|
||||
<div
|
||||
class="w-2 h-2 rounded-full bg-base-content/50 animate-bounce"
|
||||
style="animation-delay: 0.1s;"
|
||||
@@ -727,7 +795,8 @@
|
||||
></div>
|
||||
</div>
|
||||
<p class="text-xs text-base-content/60">
|
||||
{digitando.data.map((u: { nome: string }) => u.nome).join(", ")} {digitando.data.length === 1
|
||||
{digitando.data.map((u: { nome: string }) => u.nome).join(", ")}
|
||||
{digitando.data.length === 1
|
||||
? "está digitando"
|
||||
: "estão digitando"}...
|
||||
</p>
|
||||
@@ -756,7 +825,9 @@
|
||||
/>
|
||||
</svg>
|
||||
<p class="text-base-content/70">Nenhuma mensagem ainda</p>
|
||||
<p class="text-sm text-base-content/50 mt-1">Envie a primeira mensagem!</p>
|
||||
<p class="text-sm text-base-content/50 mt-1">
|
||||
Envie a primeira mensagem!
|
||||
</p>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -775,7 +846,9 @@
|
||||
}}
|
||||
>
|
||||
<div class="flex items-start gap-3">
|
||||
<div class="flex-shrink-0 w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center">
|
||||
<div
|
||||
class="shrink-0 w-10 h-10 rounded-full bg-primary/20 flex items-center justify-center"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
@@ -784,16 +857,24 @@
|
||||
stroke="currentColor"
|
||||
class="w-5 h-5 text-primary"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0" />
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M14.857 17.082a23.848 23.848 0 0 0 5.454-1.31A8.967 8.967 0 0 1 18 9.75V9A6 6 0 0 0 6 9v.75a8.967 8.967 0 0 1-2.312 6.022c1.733.64 3.56 1.085 5.455 1.31m5.714 0a24.255 24.255 0 0 1-5.714 0m5.714 0a3 3 0 1 1-5.714 0"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<p class="font-semibold text-base-content text-sm mb-1">Nova mensagem de {notificationMessage.remetente}</p>
|
||||
<p class="text-xs text-base-content/70 line-clamp-2">{notificationMessage.conteudo}</p>
|
||||
<p class="font-semibold text-base-content text-sm mb-1">
|
||||
Nova mensagem de {notificationMessage.remetente}
|
||||
</p>
|
||||
<p class="text-xs text-base-content/70 line-clamp-2">
|
||||
{notificationMessage.conteudo}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="flex-shrink-0 w-6 h-6 rounded-full hover:bg-base-200 flex items-center justify-center transition-colors"
|
||||
class="shrink-0 w-6 h-6 rounded-full hover:bg-base-200 flex items-center justify-center transition-colors"
|
||||
onclick={(e) => {
|
||||
e.stopPropagation();
|
||||
showNotificationPopup = false;
|
||||
@@ -803,11 +884,21 @@
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="w-4 h-4"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M6 18 18 6M6 6l12 12"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user