forked from shihaam/textpipe
basic frontend added
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user