forked from shihaam/textpipe
214 lines
6.0 KiB
Vue
214 lines
6.0 KiB
Vue
<template>
|
|
<div class="app-container">
|
|
<!-- Header -->
|
|
<header class="header">
|
|
<div class="header-left">
|
|
<h1 class="header-title">Textpipe</h1>
|
|
<div class="header-status">
|
|
<span class="status-dot"></span>
|
|
<span>Connected</span>
|
|
</div>
|
|
</div>
|
|
<button class="btn-logout" @click="$emit('logout')">Logout</button>
|
|
</header>
|
|
|
|
<!-- Main Content -->
|
|
<div style="flex: 1; display: flex; flex-direction: column; overflow: hidden;">
|
|
<!-- Conversation View -->
|
|
<template v-if="selectedConversation">
|
|
<MessageThread
|
|
:conversation="selectedConversation"
|
|
:messages="selectedMessages"
|
|
:apiKey="apiKey"
|
|
@back="selectedConversation = null"
|
|
@messageSent="loadMessages"
|
|
/>
|
|
</template>
|
|
|
|
<!-- Conversation List -->
|
|
<template v-else>
|
|
<div class="conversation-list">
|
|
<div v-if="loading" class="loading">
|
|
<div class="spinner"></div>
|
|
</div>
|
|
|
|
<div v-else-if="conversations.length === 0" class="empty-state">
|
|
<div class="empty-state-icon">💬</div>
|
|
<h2 class="empty-state-title">No messages yet</h2>
|
|
<p class="empty-state-text">Start a new conversation</p>
|
|
</div>
|
|
|
|
<div
|
|
v-else
|
|
v-for="conv in conversations"
|
|
:key="conv.address"
|
|
class="conversation-item"
|
|
@click="selectConversation(conv)"
|
|
>
|
|
<div class="conversation-avatar">
|
|
{{ getInitial(conv.address) }}
|
|
</div>
|
|
<div class="conversation-info">
|
|
<div class="conversation-name">{{ conv.address }}</div>
|
|
<div class="conversation-preview">{{ conv.lastMessage }}</div>
|
|
</div>
|
|
<div class="conversation-meta">
|
|
<div class="conversation-time">{{ formatTime(conv.timestamp) }}</div>
|
|
<span v-if="conv.unread" class="conversation-badge">{{ conv.unread }}</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- FAB -->
|
|
<button class="fab" @click="showNewMessage = true">+</button>
|
|
</template>
|
|
</div>
|
|
|
|
<!-- New Message Modal -->
|
|
<div v-if="showNewMessage" class="modal-overlay" @click.self="showNewMessage = false">
|
|
<div class="modal">
|
|
<div class="modal-header">
|
|
<h2 class="modal-title">New Message</h2>
|
|
<button class="btn-close" @click="showNewMessage = false">×</button>
|
|
</div>
|
|
<div class="modal-body">
|
|
<div class="form-group">
|
|
<label>To</label>
|
|
<input
|
|
v-model="newMessageTo"
|
|
type="tel"
|
|
placeholder="Phone number"
|
|
/>
|
|
</div>
|
|
<div class="form-group">
|
|
<label>Message</label>
|
|
<textarea
|
|
v-model="newMessageText"
|
|
placeholder="Type your message..."
|
|
rows="4"
|
|
></textarea>
|
|
</div>
|
|
</div>
|
|
<div class="modal-footer">
|
|
<button
|
|
class="btn btn-primary"
|
|
:disabled="!newMessageTo || !newMessageText || sending"
|
|
@click="sendNewMessage"
|
|
>
|
|
{{ sending ? 'Sending...' : 'Send' }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
|
import { api } from '../api'
|
|
import MessageThread from './MessageThread.vue'
|
|
|
|
const props = defineProps(['apiKey'])
|
|
const emit = defineEmits(['logout'])
|
|
|
|
const messages = ref([])
|
|
const loading = ref(true)
|
|
const selectedConversation = ref(null)
|
|
const showNewMessage = ref(false)
|
|
const newMessageTo = ref('')
|
|
const newMessageText = ref('')
|
|
const sending = ref(false)
|
|
|
|
let pollInterval = null
|
|
|
|
const conversations = computed(() => {
|
|
const grouped = {}
|
|
|
|
messages.value.forEach(msg => {
|
|
const address = msg.address
|
|
if (!grouped[address]) {
|
|
grouped[address] = {
|
|
address,
|
|
messages: [],
|
|
lastMessage: '',
|
|
timestamp: 0,
|
|
unread: 0
|
|
}
|
|
}
|
|
grouped[address].messages.push(msg)
|
|
if (msg.timestamp > grouped[address].timestamp) {
|
|
grouped[address].timestamp = msg.timestamp
|
|
grouped[address].lastMessage = msg.text.substring(0, 50) + (msg.text.length > 50 ? '...' : '')
|
|
}
|
|
})
|
|
|
|
return Object.values(grouped).sort((a, b) => b.timestamp - a.timestamp)
|
|
})
|
|
|
|
const selectedMessages = computed(() => {
|
|
if (!selectedConversation.value) return []
|
|
return messages.value
|
|
.filter(m => m.address === selectedConversation.value.address)
|
|
.sort((a, b) => a.timestamp - b.timestamp)
|
|
})
|
|
|
|
onMounted(() => {
|
|
loadMessages()
|
|
pollInterval = setInterval(loadMessages, 5000)
|
|
})
|
|
|
|
onUnmounted(() => {
|
|
if (pollInterval) clearInterval(pollInterval)
|
|
})
|
|
|
|
const loadMessages = async () => {
|
|
try {
|
|
const data = await api.getMessages(props.apiKey)
|
|
messages.value = data.messages || []
|
|
} catch (e) {
|
|
console.error('Failed to load messages:', e)
|
|
} finally {
|
|
loading.value = false
|
|
}
|
|
}
|
|
|
|
const selectConversation = (conv) => {
|
|
selectedConversation.value = conv
|
|
}
|
|
|
|
const getInitial = (address) => {
|
|
return address.charAt(0).toUpperCase()
|
|
}
|
|
|
|
const formatTime = (timestamp) => {
|
|
const date = new Date(timestamp)
|
|
const now = new Date()
|
|
const diff = now - date
|
|
|
|
if (diff < 86400000) {
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })
|
|
} else if (diff < 604800000) {
|
|
return date.toLocaleDateString([], { weekday: 'short' })
|
|
} else {
|
|
return date.toLocaleDateString([], { month: 'short', day: 'numeric' })
|
|
}
|
|
}
|
|
|
|
const sendNewMessage = async () => {
|
|
if (!newMessageTo.value || !newMessageText.value) return
|
|
|
|
sending.value = true
|
|
try {
|
|
await api.sendMessage(props.apiKey, newMessageTo.value, newMessageText.value)
|
|
showNewMessage.value = false
|
|
newMessageTo.value = ''
|
|
newMessageText.value = ''
|
|
await loadMessages()
|
|
} catch (e) {
|
|
alert('Failed to send message: ' + e.message)
|
|
} finally {
|
|
sending.value = false
|
|
}
|
|
}
|
|
</script>
|