302 lines
8.4 KiB
JavaScript
302 lines
8.4 KiB
JavaScript
/**
|
|
* Chat Widget Component
|
|
* Embedded chat for buyer-seller communication
|
|
*/
|
|
|
|
import { t, i18n } from '../i18n.js';
|
|
import { chatService } from '../services/chat.js';
|
|
import { cryptoService } from '../services/crypto.js';
|
|
|
|
class ChatWidget extends HTMLElement {
|
|
static get observedAttributes() {
|
|
return ['listing-id', 'recipient-id', 'recipient-key', 'recipient-name'];
|
|
}
|
|
|
|
constructor() {
|
|
super();
|
|
this.chat = null;
|
|
this.messages = [];
|
|
this.unsubscribe = null;
|
|
}
|
|
|
|
async connectedCallback() {
|
|
await cryptoService.ready;
|
|
|
|
this.listingId = this.getAttribute('listing-id');
|
|
this.recipientId = this.getAttribute('recipient-id');
|
|
this.recipientKey = this.getAttribute('recipient-key');
|
|
this.recipientName = this.getAttribute('recipient-name') || 'Seller';
|
|
|
|
if (this.listingId && this.recipientId && this.recipientKey) {
|
|
this.chat = chatService.getOrCreateChat(
|
|
this.recipientId,
|
|
this.recipientKey,
|
|
this.listingId
|
|
);
|
|
await this.loadMessages();
|
|
}
|
|
|
|
this.render();
|
|
this.setupEventListeners();
|
|
|
|
this.unsubscribe = chatService.subscribe(() => this.refreshMessages());
|
|
this.i18nUnsubscribe = i18n.subscribe(() => this.render());
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
if (this.unsubscribe) this.unsubscribe();
|
|
if (this.i18nUnsubscribe) this.i18nUnsubscribe();
|
|
}
|
|
|
|
async loadMessages() {
|
|
if (!this.chat) return;
|
|
this.messages = await chatService.getMessages(this.chat.id, this.recipientKey);
|
|
}
|
|
|
|
async refreshMessages() {
|
|
await this.loadMessages();
|
|
this.renderMessages();
|
|
this.scrollToBottom();
|
|
}
|
|
|
|
render() {
|
|
this.innerHTML = /* html */`
|
|
<div class="chat-widget">
|
|
<div class="chat-header">
|
|
<span class="chat-recipient">${this.escapeHtml(this.recipientName)}</span>
|
|
<span class="chat-encrypted" title="${t('chat.encrypted')}">🔒</span>
|
|
</div>
|
|
|
|
<div class="chat-messages" id="chat-messages">
|
|
${this.renderMessagesHtml()}
|
|
</div>
|
|
|
|
<form class="chat-input" id="chat-form">
|
|
<input
|
|
type="text"
|
|
id="message-input"
|
|
placeholder="${t('chat.placeholder')}"
|
|
autocomplete="off"
|
|
>
|
|
<button type="submit" class="btn btn-primary">
|
|
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
<line x1="22" y1="2" x2="11" y2="13"></line>
|
|
<polygon points="22 2 15 22 11 13 2 9 22 2"></polygon>
|
|
</svg>
|
|
</button>
|
|
</form>
|
|
</div>
|
|
`;
|
|
|
|
this.setupEventListeners();
|
|
this.scrollToBottom();
|
|
}
|
|
|
|
renderMessagesHtml() {
|
|
if (this.messages.length === 0) {
|
|
return /* html */`
|
|
<div class="chat-empty">
|
|
<p>${t('chat.startConversation')}</p>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
return this.messages.map(msg => /* html */`
|
|
<div class="chat-message ${msg.isOwn ? 'own' : 'other'}">
|
|
<div class="message-bubble">
|
|
<p>${this.escapeHtml(msg.text)}</p>
|
|
<span class="message-time">${this.formatTime(msg.timestamp)}</span>
|
|
</div>
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
renderMessages() {
|
|
const container = this.querySelector('#chat-messages');
|
|
if (container) {
|
|
container.innerHTML = this.renderMessagesHtml();
|
|
}
|
|
}
|
|
|
|
setupEventListeners() {
|
|
const form = this.querySelector('#chat-form');
|
|
form?.addEventListener('submit', (e) => this.handleSubmit(e));
|
|
}
|
|
|
|
async handleSubmit(e) {
|
|
e.preventDefault();
|
|
|
|
const input = this.querySelector('#message-input');
|
|
const text = input?.value.trim();
|
|
|
|
if (!text || !this.chat) return;
|
|
|
|
input.value = '';
|
|
|
|
await chatService.sendMessage(
|
|
this.chat.id,
|
|
this.recipientKey,
|
|
text
|
|
);
|
|
}
|
|
|
|
scrollToBottom() {
|
|
const container = this.querySelector('#chat-messages');
|
|
if (container) {
|
|
container.scrollTop = container.scrollHeight;
|
|
}
|
|
}
|
|
|
|
formatTime(timestamp) {
|
|
const date = new Date(timestamp);
|
|
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
}
|
|
|
|
escapeHtml(text) {
|
|
const div = document.createElement('div');
|
|
div.textContent = text;
|
|
return div.innerHTML;
|
|
}
|
|
}
|
|
|
|
customElements.define('chat-widget', ChatWidget);
|
|
|
|
const style = document.createElement('style');
|
|
style.textContent = /* css */`
|
|
chat-widget {
|
|
display: block;
|
|
}
|
|
|
|
chat-widget .chat-widget {
|
|
display: flex;
|
|
flex-direction: column;
|
|
height: 400px;
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-lg);
|
|
overflow: hidden;
|
|
background: var(--color-bg);
|
|
}
|
|
|
|
chat-widget .chat-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: var(--space-md);
|
|
background: var(--color-bg-secondary);
|
|
border-bottom: 1px solid var(--color-border);
|
|
}
|
|
|
|
chat-widget .chat-recipient {
|
|
font-weight: var(--font-weight-medium);
|
|
color: var(--color-text);
|
|
}
|
|
|
|
chat-widget .chat-encrypted {
|
|
font-size: var(--font-size-sm);
|
|
cursor: help;
|
|
filter: grayscale(1);
|
|
}
|
|
|
|
chat-widget .chat-messages {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: var(--space-md);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--space-sm);
|
|
}
|
|
|
|
chat-widget .chat-empty {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
color: var(--color-text-muted);
|
|
text-align: center;
|
|
}
|
|
|
|
chat-widget .chat-message {
|
|
display: flex;
|
|
max-width: 80%;
|
|
}
|
|
|
|
chat-widget .chat-message.own {
|
|
align-self: flex-end;
|
|
}
|
|
|
|
chat-widget .chat-message.other {
|
|
align-self: flex-start;
|
|
}
|
|
|
|
chat-widget .message-bubble {
|
|
padding: var(--space-sm) var(--space-md);
|
|
border-radius: var(--radius-lg);
|
|
position: relative;
|
|
}
|
|
|
|
chat-widget .chat-message.own .message-bubble {
|
|
background: var(--color-text);
|
|
color: var(--color-bg);
|
|
border-bottom-right-radius: var(--radius-sm);
|
|
}
|
|
|
|
chat-widget .chat-message.other .message-bubble {
|
|
background: var(--color-bg-tertiary);
|
|
color: var(--color-text);
|
|
border-bottom-left-radius: var(--radius-sm);
|
|
}
|
|
|
|
chat-widget .message-bubble p {
|
|
margin: 0;
|
|
word-break: break-word;
|
|
}
|
|
|
|
chat-widget .message-time {
|
|
display: block;
|
|
font-size: var(--font-size-xs);
|
|
opacity: 0.7;
|
|
margin-top: var(--space-xs);
|
|
}
|
|
|
|
chat-widget .chat-input {
|
|
display: flex;
|
|
gap: var(--space-sm);
|
|
padding: var(--space-md);
|
|
border-top: 1px solid var(--color-border);
|
|
background: var(--color-bg-secondary);
|
|
}
|
|
|
|
chat-widget .chat-input input {
|
|
flex: 1;
|
|
padding: var(--space-sm) var(--space-md);
|
|
border: 1px solid var(--color-border);
|
|
border-radius: var(--radius-md);
|
|
background: var(--color-bg);
|
|
color: var(--color-text);
|
|
font-size: var(--font-size-base);
|
|
}
|
|
|
|
chat-widget .chat-input input::placeholder {
|
|
color: var(--color-text-muted);
|
|
}
|
|
|
|
chat-widget .chat-input input:focus {
|
|
outline: none;
|
|
border-color: var(--color-text-muted);
|
|
}
|
|
|
|
chat-widget .chat-input button {
|
|
padding: var(--space-sm);
|
|
border-radius: var(--radius-md);
|
|
background: var(--color-text);
|
|
color: var(--color-bg);
|
|
}
|
|
|
|
chat-widget .chat-input button:hover {
|
|
background: var(--color-text-secondary);
|
|
}
|
|
`;
|
|
document.head.appendChild(style);
|
|
|
|
export { ChatWidget };
|