big milestone 2!!!

This commit is contained in:
padreug 2025-02-11 14:53:37 +01:00
parent ac906ca6c9
commit 231658b980
8 changed files with 167 additions and 297 deletions

View file

@ -1,19 +1,20 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { ref, computed, nextTick, onMounted, watch } from 'vue'
import { useNostrStore } from '@/stores/nostr'
import { npubToHex } from '@/lib/nostr'
import type { DirectMessage } from '@/types/nostr'
import { Card, CardContent, CardFooter, CardHeader } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { ScrollArea } from '@/components/ui/scroll-area'
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar'
import { Send, AlertCircle } from 'lucide-vue-next'
import ChatBox from './ChatBox.vue'
const nostrStore = useNostrStore()
const input = ref('')
const isSending = ref(false)
const error = ref('')
const messagesEndRef = ref<HTMLDivElement | null>(null)
const SUPPORT_NPUB = import.meta.env.VITE_SUPPORT_NPUB
if (!SUPPORT_NPUB) {
@ -22,6 +23,47 @@ if (!SUPPORT_NPUB) {
const inputLength = computed(() => input.value.trim().length)
// Group messages by sender and time
interface MessageGroup {
sent: boolean
messages: DirectMessage[]
timestamp: number
}
const groupedMessages = computed<MessageGroup[]>(() => {
const groups: MessageGroup[] = []
let currentGroup: MessageGroup | null = null
// Sort messages by timestamp first
const sortedMessages = [...nostrStore.currentMessages].sort((a, b) => a.created_at - b.created_at)
for (const message of sortedMessages) {
// Start a new group if:
// 1. No current group
// 2. Different sender than last message
// 3. More than 2 minutes since last message
if (!currentGroup ||
currentGroup.sent !== message.sent ||
message.created_at - currentGroup.messages[currentGroup.messages.length - 1].created_at > 120) {
currentGroup = {
sent: message.sent,
messages: [],
timestamp: message.created_at
}
groups.push(currentGroup)
}
currentGroup.messages.push(message)
}
return groups
})
// Scroll to bottom when new messages arrive
watch(() => nostrStore.currentMessages.length, () => {
nextTick(() => {
scrollToBottom()
})
})
onMounted(async () => {
try {
if (!SUPPORT_NPUB) return
@ -29,12 +71,19 @@ onMounted(async () => {
const supportPubkeyHex = npubToHex(SUPPORT_NPUB)
nostrStore.activeChat = supportPubkeyHex
await nostrStore.subscribeToMessages()
scrollToBottom()
} catch (err) {
console.error('Failed to initialize support chat:', err)
error.value = 'Failed to connect to support. Please try again later.'
}
})
function scrollToBottom() {
if (messagesEndRef.value) {
messagesEndRef.value.scrollIntoView({ behavior: 'smooth' })
}
}
const sendMessage = async (event: Event) => {
event.preventDefault()
if (inputLength.value === 0 || !nostrStore.activeChat || isSending.value) return
@ -51,6 +100,27 @@ const sendMessage = async (event: Event) => {
isSending.value = false
}
}
const formatTime = (timestamp: number) => {
return new Date(timestamp * 1000).toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit'
})
}
const formatDate = (timestamp: number) => {
const date = new Date(timestamp * 1000)
const today = new Date()
const yesterday = new Date(today)
yesterday.setDate(yesterday.getDate() - 1)
if (date.toDateString() === today.toDateString()) {
return 'Today'
} else if (date.toDateString() === yesterday.toDateString()) {
return 'Yesterday'
}
return date.toLocaleDateString()
}
</script>
<template>
@ -79,7 +149,42 @@ const sendMessage = async (event: Event) => {
</div>
<!-- Chat Messages -->
<ChatBox />
<div class="flex flex-col space-y-4">
<template v-for="(group, groupIndex) in groupedMessages" :key="groupIndex">
<!-- Date separator -->
<div v-if="groupIndex === 0 || formatDate(group.timestamp) !== formatDate(groupedMessages[groupIndex - 1].timestamp)"
class="flex justify-center my-4">
<div class="px-3 py-1 rounded-full bg-muted text-xs text-muted-foreground">
{{ formatDate(group.timestamp) }}
</div>
</div>
<!-- Message group -->
<div :class="[
'flex flex-col gap-2 animate-in fade-in-50 slide-in-from-bottom-5',
group.sent ? 'items-end' : 'items-start'
]">
<div v-for="(message, messageIndex) in group.messages" :key="message.id"
class="group flex flex-col space-y-0.5">
<div :class="[
'px-3 py-2 rounded-2xl max-w-[85%] break-words text-sm',
group.sent
? 'bg-primary text-primary-foreground'
: 'bg-muted',
// Rounded corners based on position in group
messageIndex === 0 && (group.sent ? 'rounded-tr-sm' : 'rounded-tl-sm'),
messageIndex === group.messages.length - 1 && (group.sent ? 'rounded-br-sm' : 'rounded-bl-sm')
]">
{{ message.content }}
</div>
<span class="px-2 text-[10px] text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity">
{{ formatTime(message.created_at) }}
</span>
</div>
</div>
</template>
<div ref="messagesEndRef" />
</div>
</div>
</ScrollArea>
</CardContent>
@ -108,17 +213,26 @@ const sendMessage = async (event: Event) => {
<style scoped>
.animate-in {
animation: animate-in 0.2s ease-out;
animation-duration: 0.2s;
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
animation-fill-mode: forwards;
}
@keyframes animate-in {
from {
opacity: 0;
transform: translateY(10px);
}
to {
opacity: 1;
transform: translateY(0);
}
.fade-in-50 {
animation-name: fade-in-50;
}
.slide-in-from-bottom-5 {
animation-name: slide-in-from-bottom-5;
}
@keyframes fade-in-50 {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-in-from-bottom-5 {
from { transform: translateY(5px); }
to { transform: translateY(0); }
}
</style>