refactor: remove legacy chat service, migrate chat-widget to Directus conversations
This commit is contained in:
@@ -1,45 +1,44 @@
|
||||
/**
|
||||
* Chat Widget Component
|
||||
* Embedded chat for buyer-seller communication
|
||||
* Embedded chat for buyer-seller communication using Directus conversations
|
||||
*/
|
||||
|
||||
import { t, i18n } from '../i18n.js'
|
||||
import { chatService } from '../services/chat.js'
|
||||
import { conversationsService } from '../services/conversations.js'
|
||||
import { cryptoService } from '../services/crypto.js'
|
||||
|
||||
class ChatWidget extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['listing-id', 'recipient-id', 'recipient-key', 'recipient-name']
|
||||
return ['listing-id', 'seller-public-key', 'recipient-name']
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
this.chat = null
|
||||
this.conversation = null
|
||||
this.messages = []
|
||||
this.unsubscribe = null
|
||||
this.loading = true
|
||||
this.error = 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.sellerPublicKey = this.getAttribute('seller-public-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()
|
||||
|
||||
if (this.listingId && this.sellerPublicKey) {
|
||||
await this.initConversation()
|
||||
} else {
|
||||
this.loading = false
|
||||
this.error = 'missing-data'
|
||||
this.render()
|
||||
}
|
||||
|
||||
this.render()
|
||||
this.setupEventListeners()
|
||||
|
||||
this.unsubscribe = chatService.subscribe(() => this.refreshMessages())
|
||||
this.unsubscribe = conversationsService.subscribe(() => this.refreshMessages())
|
||||
this.i18nUnsubscribe = i18n.subscribe(() => this.render())
|
||||
}
|
||||
|
||||
@@ -48,9 +47,28 @@ class ChatWidget extends HTMLElement {
|
||||
if (this.i18nUnsubscribe) this.i18nUnsubscribe()
|
||||
}
|
||||
|
||||
async initConversation() {
|
||||
try {
|
||||
this.conversation = await conversationsService.startOrGetConversation(
|
||||
this.listingId,
|
||||
this.sellerPublicKey
|
||||
)
|
||||
await this.loadMessages()
|
||||
} catch (e) {
|
||||
console.error('Failed to init conversation:', e)
|
||||
this.error = 'init-failed'
|
||||
}
|
||||
this.loading = false
|
||||
this.render()
|
||||
this.setupEventListeners()
|
||||
}
|
||||
|
||||
async loadMessages() {
|
||||
if (!this.chat) return
|
||||
this.messages = await chatService.getMessages(this.chat.id, this.recipientKey)
|
||||
if (!this.conversation) return
|
||||
this.messages = await conversationsService.getMessages(
|
||||
this.conversation.id,
|
||||
this.conversation.otherPublicKey
|
||||
)
|
||||
}
|
||||
|
||||
async refreshMessages() {
|
||||
@@ -60,6 +78,28 @@ class ChatWidget extends HTMLElement {
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.loading) {
|
||||
this.innerHTML = /* html */`
|
||||
<div class="chat-widget">
|
||||
<div class="chat-loading">
|
||||
<span>${t('common.loading')}</span>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
return
|
||||
}
|
||||
|
||||
if (this.error) {
|
||||
this.innerHTML = /* html */`
|
||||
<div class="chat-widget">
|
||||
<div class="chat-error">
|
||||
<p>${t('chat.unavailable')}</p>
|
||||
</div>
|
||||
</div>
|
||||
`
|
||||
return
|
||||
}
|
||||
|
||||
this.innerHTML = /* html */`
|
||||
<div class="chat-widget">
|
||||
<div class="chat-header">
|
||||
@@ -129,15 +169,17 @@ class ChatWidget extends HTMLElement {
|
||||
const input = this.querySelector('#message-input')
|
||||
const text = input?.value.trim()
|
||||
|
||||
if (!text || !this.chat) return
|
||||
if (!text || !this.conversation) return
|
||||
|
||||
input.value = ''
|
||||
|
||||
await chatService.sendMessage(
|
||||
this.chat.id,
|
||||
this.recipientKey,
|
||||
await conversationsService.sendMessage(
|
||||
this.conversation.id,
|
||||
this.conversation.otherPublicKey,
|
||||
text
|
||||
)
|
||||
|
||||
await this.refreshMessages()
|
||||
}
|
||||
|
||||
scrollToBottom() {
|
||||
@@ -177,6 +219,17 @@ style.textContent = /* css */`
|
||||
background: var(--color-bg);
|
||||
}
|
||||
|
||||
chat-widget .chat-loading,
|
||||
chat-widget .chat-error {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-text-muted);
|
||||
text-align: center;
|
||||
padding: var(--space-lg);
|
||||
}
|
||||
|
||||
chat-widget .chat-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@@ -369,8 +369,7 @@ class PageListing extends HTMLElement {
|
||||
<div class="tab-content" id="tab-chat">
|
||||
<chat-widget
|
||||
listing-id="${this.listing?.id || ''}"
|
||||
recipient-id="${this.listing?.user_created || ''}"
|
||||
recipient-key=""
|
||||
seller-public-key="${this.listing?.seller_public_key || ''}"
|
||||
recipient-name="${t('listing.anonymousSeller')}"
|
||||
></chat-widget>
|
||||
</div>
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
export const mockListings = [
|
||||
{
|
||||
id: '1',
|
||||
title: 'iPhone 13 Pro - Sehr guter Zustand',
|
||||
description: 'Verkaufe mein iPhone 13 Pro in sehr gutem Zustand. Das Gerät hat keine Kratzer und funktioniert einwandfrei. Originalverpackung und Ladekabel sind dabei.',
|
||||
price: 699,
|
||||
location: 'Berlin, Mitte',
|
||||
category: 'electronics',
|
||||
subcategory: 'phones',
|
||||
createdAt: '2026-01-27T10:00:00Z',
|
||||
seller: {
|
||||
name: 'Max M.',
|
||||
memberSince: '2023',
|
||||
moneroAddress: '888tNkZrPN6JsEgekjMnABU4TBzc2Dt29EPAvkRxbANsAnjyPbb3iQ1YBRk1UXcdRsiKc9dhwMVgN5S9cQUiyoogDavup3H',
|
||||
publicKey: 'dGVzdC1wdWJsaWMta2V5LW1heC1tLTEyMzQ1Njc4OTA='
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '2',
|
||||
title: 'Vintage Ledersofa 3-Sitzer',
|
||||
description: 'Wunderschönes Vintage-Ledersofa aus den 70er Jahren. Cognacfarben, leichte Patina die dem Stück Charakter verleiht. Sehr bequem.',
|
||||
price: 450,
|
||||
location: 'München, Schwabing',
|
||||
category: 'furniture',
|
||||
subcategory: 'living',
|
||||
createdAt: '2026-01-26T14:30:00Z',
|
||||
seller: {
|
||||
name: 'Anna K.',
|
||||
memberSince: '2024',
|
||||
moneroAddress: '47sghzufGhJJDQEbScMCwVBimTuq6L5JiRixD8VeGbpjCTA12GwZVPWzjmpfLDJNDAWvuNDAWvuNDAWvuNDAWvuN',
|
||||
publicKey: 'dGVzdC1wdWJsaWMta2V5LWFubmEtay0xMjM0NTY3ODkw'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '3',
|
||||
title: 'Canyon Mountainbike 29 Zoll',
|
||||
description: 'Canyon Spectral AL 6.0 in Größe L. Carbon-Rahmen, Shimano XT Schaltung, frisch gewartet. Ideal für Trails und Touren.',
|
||||
price: 1200,
|
||||
location: 'Zürich',
|
||||
category: 'sports',
|
||||
subcategory: 'outdoor',
|
||||
createdAt: '2026-01-25T09:15:00Z',
|
||||
seller: {
|
||||
name: 'Thomas B.',
|
||||
memberSince: '2022',
|
||||
moneroAddress: '44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A',
|
||||
publicKey: 'dGVzdC1wdWJsaWMta2V5LXRob21hcy1iLTEyMzQ1Njc='
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '4',
|
||||
title: 'Gaming PC - RTX 4070, Ryzen 7',
|
||||
description: 'Selbstgebauter Gaming-PC: Ryzen 7 5800X, RTX 4070, 32GB RAM, 1TB NVMe SSD. Perfekt für 1440p Gaming. RGB-Beleuchtung.',
|
||||
price: 1450,
|
||||
location: 'Hamburg',
|
||||
category: 'electronics',
|
||||
subcategory: 'gaming',
|
||||
createdAt: '2026-01-24T16:45:00Z',
|
||||
seller: {
|
||||
name: 'Felix R.',
|
||||
memberSince: '2023',
|
||||
moneroAddress: '48iWMy1PH6VGBJVvHDg9mY7mJ6vBDWVHpGgXEtCGp99kT4Xk5QfN3v7nqMrqGpvU',
|
||||
publicKey: 'dGVzdC1wdWJsaWMta2V5LWZlbGl4LXItMTIzNDU2Nzg='
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '5',
|
||||
title: 'Ikea MALM Schreibtisch weiß',
|
||||
description: 'Ikea MALM Schreibtisch in weiß, 140x65cm. Minimale Gebrauchsspuren, Kabelmanagement integriert. Selbstabholung.',
|
||||
price: 80,
|
||||
location: 'Wien, 1050',
|
||||
category: 'furniture',
|
||||
subcategory: 'office',
|
||||
createdAt: '2026-01-23T11:20:00Z',
|
||||
seller: {
|
||||
name: 'Lisa S.',
|
||||
memberSince: '2025',
|
||||
moneroAddress: '45dEQp8dFKrMXKvFWJFmZKCZhH3ARYMW4MJYM9FJcPuNT5Kek9R',
|
||||
publicKey: 'dGVzdC1wdWJsaWMta2V5LWxpc2Etcy0xMjM0NTY3ODkw'
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '6',
|
||||
title: 'Canada Goose Winterjacke M',
|
||||
description: 'Original Canada Goose Expedition Parka in Schwarz, Größe M. Sehr warm, perfekt für extreme Kälte. NP 1200€.',
|
||||
price: 550,
|
||||
location: 'Köln',
|
||||
category: 'clothing',
|
||||
subcategory: 'men',
|
||||
createdAt: '2026-01-22T08:00:00Z',
|
||||
seller: {
|
||||
name: 'Jan P.',
|
||||
memberSince: '2024',
|
||||
moneroAddress: '42nTNQp8dFKrMXKvFWJFmZKCZhH3ARYMW4MJYM9FJcPuNT5Kek9R',
|
||||
publicKey: 'dGVzdC1wdWJsaWMta2V5LWphbi1wLTEyMzQ1Njc4OTA='
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '7',
|
||||
title: 'Sony A7 III Kamera + 24-70mm',
|
||||
description: 'Sony A7 III Vollformat-Kamera mit Sony 24-70mm f/2.8 GM Objektiv. 15.000 Auslösungen, einwandfreier Zustand.',
|
||||
price: 2200,
|
||||
location: 'Frankfurt',
|
||||
category: 'electronics',
|
||||
subcategory: 'tv_audio',
|
||||
createdAt: '2026-01-21T13:10:00Z',
|
||||
seller: {
|
||||
name: 'Sarah M.',
|
||||
memberSince: '2021',
|
||||
moneroAddress: '46BeWrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy24G3euoGWpzR7T3',
|
||||
publicKey: 'dGVzdC1wdWJsaWMta2V5LXNhcmFoLW0tMTIzNDU2Nzg='
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '8',
|
||||
title: 'Elektrische Gitarre Fender Strat',
|
||||
description: 'Fender Stratocaster Player Series in Sunburst. Ahorn-Hals, 3 Single-Coil Pickups. Inkl. Gigbag.',
|
||||
price: 650,
|
||||
location: 'Stuttgart',
|
||||
category: 'other',
|
||||
subcategory: 'art',
|
||||
createdAt: '2026-01-20T17:30:00Z',
|
||||
seller: {
|
||||
name: 'Michael W.',
|
||||
memberSince: '2022',
|
||||
moneroAddress: '43gFrHpwXmHDpDEUmZBWZfoQpdc6HaERCNmx1pEYL2rAcuwufPN9rXHHtyUA4QVy24G3euoGWpzR7T3',
|
||||
publicKey: 'dGVzdC1wdWJsaWMta2V5LW1pY2hhZWwtdy0xMjM0NTY='
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '9',
|
||||
title: 'Weber Gasgrill Genesis II',
|
||||
description: 'Weber Genesis II E-310 Gasgrill in Schwarz. 3 Brenner, Sear Station, iGrill kompatibel. Wenig benutzt.',
|
||||
price: 480,
|
||||
location: 'Düsseldorf',
|
||||
category: 'garden',
|
||||
subcategory: 'outdoor_living',
|
||||
createdAt: '2026-01-19T10:45:00Z',
|
||||
seller: {
|
||||
name: 'Klaus H.',
|
||||
memberSince: '2023',
|
||||
moneroAddress: '47sghzufGhJJDQEbScMCwVBimTuq6L5JiRixD8VeGbpjCTA12GwZVPWzjmpfLDJNDAWvuNDAWvuNDAWvuNDAWvuN',
|
||||
publicKey: 'dGVzdC1wdWJsaWMta2V5LWtsYXVzLWgtMTIzNDU2Nzg='
|
||||
}
|
||||
},
|
||||
{
|
||||
id: '10',
|
||||
title: 'Adidas Ultra Boost 22 Gr. 43',
|
||||
description: 'Adidas Ultra Boost 22 Laufschuhe in Core Black, Größe 43. Nur wenige Male getragen, wie neu.',
|
||||
price: 95,
|
||||
location: 'Bern',
|
||||
category: 'clothing',
|
||||
subcategory: 'shoes',
|
||||
createdAt: '2026-01-18T15:20:00Z',
|
||||
seller: {
|
||||
name: 'Nina L.',
|
||||
memberSince: '2024',
|
||||
moneroAddress: '44AFFq5kSiGBoZ4NMDwYtN18obc8AemS33DBLWs3H7otXft3XjrpDtQGv7SqSsaBYBb98uNbr2VBBEt7f2wfn3RVGQBEP3A',
|
||||
publicKey: 'dGVzdC1wdWJsaWMta2V5LW5pbmEtbC0xMjM0NTY3ODkw'
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
export function getListingById(id) {
|
||||
return mockListings.find(l => l.id === id) || null
|
||||
}
|
||||
|
||||
export function searchListings(query = '', category = '', subcategory = '') {
|
||||
return mockListings.filter(listing => {
|
||||
const matchesQuery = !query ||
|
||||
listing.title.toLowerCase().includes(query.toLowerCase()) ||
|
||||
listing.description.toLowerCase().includes(query.toLowerCase())
|
||||
const matchesCategory = !category || listing.category === category
|
||||
const matchesSubcategory = !subcategory || listing.subcategory === subcategory
|
||||
return matchesQuery && matchesCategory && matchesSubcategory
|
||||
})
|
||||
}
|
||||
@@ -1,217 +0,0 @@
|
||||
/**
|
||||
* Chat Service - Handles message storage and retrieval
|
||||
* Uses LocalStorage as mock backend until Directus is ready
|
||||
*/
|
||||
|
||||
import { cryptoService } from './crypto.js'
|
||||
|
||||
const CHATS_STORAGE_KEY = 'dgray_chats'
|
||||
const MESSAGES_STORAGE_KEY = 'dgray_messages'
|
||||
|
||||
class ChatService {
|
||||
constructor() {
|
||||
this.subscribers = new Set()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or create a chat between current user and another user
|
||||
* @param {string} recipientId - The other user's ID
|
||||
* @param {string} recipientPublicKey - The other user's public key
|
||||
* @param {string} listingId - The listing this chat is about
|
||||
* @returns {object} - Chat object
|
||||
*/
|
||||
getOrCreateChat(recipientId, recipientPublicKey, listingId) {
|
||||
const chats = this.getAllChats()
|
||||
const myPublicKey = cryptoService.getPublicKey()
|
||||
|
||||
// Find existing chat for this listing + recipient
|
||||
let chat = chats.find(c =>
|
||||
c.listingId === listingId &&
|
||||
c.recipientId === recipientId
|
||||
)
|
||||
|
||||
if (!chat) {
|
||||
chat = {
|
||||
id: this.generateId(),
|
||||
listingId,
|
||||
recipientId,
|
||||
recipientPublicKey,
|
||||
myPublicKey,
|
||||
createdAt: new Date().toISOString(),
|
||||
lastMessageAt: null
|
||||
}
|
||||
chats.push(chat)
|
||||
this.saveChats(chats)
|
||||
}
|
||||
|
||||
return chat
|
||||
}
|
||||
|
||||
getAllChats() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(CHATS_STORAGE_KEY) || '[]')
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
saveChats(chats) {
|
||||
localStorage.setItem(CHATS_STORAGE_KEY, JSON.stringify(chats))
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an encrypted message
|
||||
* @param {string} chatId - Chat ID
|
||||
* @param {string} recipientPublicKey - Recipient's public key
|
||||
* @param {string} plainText - Message content
|
||||
* @returns {object} - The saved message
|
||||
*/
|
||||
async sendMessage(chatId, recipientPublicKey, plainText) {
|
||||
await cryptoService.ready
|
||||
|
||||
const { nonce, ciphertext } = cryptoService.encrypt(plainText, recipientPublicKey)
|
||||
|
||||
const message = {
|
||||
id: this.generateId(),
|
||||
chatId,
|
||||
senderPublicKey: cryptoService.getPublicKey(),
|
||||
nonce,
|
||||
ciphertext,
|
||||
timestamp: new Date().toISOString(),
|
||||
// Store plain text for own messages (we can't decrypt our own box messages)
|
||||
_plainText: plainText
|
||||
}
|
||||
|
||||
const messages = this.getAllMessages()
|
||||
messages.push(message)
|
||||
this.saveMessages(messages)
|
||||
|
||||
// Update chat's lastMessageAt
|
||||
const chats = this.getAllChats()
|
||||
const chat = chats.find(c => c.id === chatId)
|
||||
if (chat) {
|
||||
chat.lastMessageAt = message.timestamp
|
||||
this.saveChats(chats)
|
||||
}
|
||||
|
||||
this.notifySubscribers()
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all messages for a chat, decrypted
|
||||
* @param {string} chatId - Chat ID
|
||||
* @param {string} otherPublicKey - The other party's public key
|
||||
* @returns {Array} - Decrypted messages
|
||||
*/
|
||||
async getMessages(chatId, otherPublicKey) {
|
||||
await cryptoService.ready
|
||||
|
||||
const messages = this.getAllMessages().filter(m => m.chatId === chatId)
|
||||
const myPublicKey = cryptoService.getPublicKey()
|
||||
|
||||
return messages.map(msg => {
|
||||
const isOwn = msg.senderPublicKey === myPublicKey
|
||||
|
||||
let text
|
||||
if (isOwn) {
|
||||
// Use stored plain text for own messages
|
||||
text = msg._plainText || '[Encrypted]'
|
||||
} else {
|
||||
// Decrypt messages from others
|
||||
text = cryptoService.decrypt(msg.ciphertext, msg.nonce, msg.senderPublicKey)
|
||||
if (!text) text = '[Decryption failed]'
|
||||
}
|
||||
|
||||
return {
|
||||
id: msg.id,
|
||||
text,
|
||||
isOwn,
|
||||
timestamp: msg.timestamp
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
getAllMessages() {
|
||||
try {
|
||||
return JSON.parse(localStorage.getItem(MESSAGES_STORAGE_KEY) || '[]')
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
saveMessages(messages) {
|
||||
localStorage.setItem(MESSAGES_STORAGE_KEY, JSON.stringify(messages))
|
||||
}
|
||||
|
||||
generateId() {
|
||||
return 'msg_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9)
|
||||
}
|
||||
|
||||
subscribe(callback) {
|
||||
this.subscribers.add(callback)
|
||||
return () => this.subscribers.delete(callback)
|
||||
}
|
||||
|
||||
notifySubscribers() {
|
||||
this.subscribers.forEach(cb => cb())
|
||||
}
|
||||
|
||||
/**
|
||||
* Simulate receiving a message (for demo purposes)
|
||||
* In production, this would come from Directus/WebSocket
|
||||
*/
|
||||
async simulateIncomingMessage(chatId, senderPublicKey, plainText) {
|
||||
await cryptoService.ready
|
||||
|
||||
const myPublicKey = cryptoService.getPublicKey()
|
||||
const { nonce, ciphertext } = this.encryptForRecipient(plainText, myPublicKey, senderPublicKey)
|
||||
|
||||
const message = {
|
||||
id: this.generateId(),
|
||||
chatId,
|
||||
senderPublicKey,
|
||||
nonce,
|
||||
ciphertext,
|
||||
timestamp: new Date().toISOString()
|
||||
}
|
||||
|
||||
const messages = this.getAllMessages()
|
||||
messages.push(message)
|
||||
this.saveMessages(messages)
|
||||
|
||||
this.notifySubscribers()
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
// Helper for simulation - encrypt as if from another user
|
||||
encryptForRecipient(message, recipientPublicKey, senderSecretKey) {
|
||||
// This is a simplified simulation - in reality the sender would have their own keypair
|
||||
const nacl = window.nacl
|
||||
const naclUtil = window.nacl.util
|
||||
|
||||
const nonce = nacl.randomBytes(nacl.box.nonceLength)
|
||||
const messageUint8 = naclUtil.decodeUTF8(message)
|
||||
|
||||
// For simulation, we create a temporary keypair for the "sender"
|
||||
const senderKeyPair = nacl.box.keyPair.fromSecretKey(
|
||||
naclUtil.decodeBase64(senderSecretKey || naclUtil.encodeBase64(nacl.randomBytes(32)))
|
||||
)
|
||||
|
||||
const encrypted = nacl.box(
|
||||
messageUint8,
|
||||
nonce,
|
||||
naclUtil.decodeBase64(recipientPublicKey),
|
||||
senderKeyPair.secretKey
|
||||
)
|
||||
|
||||
return {
|
||||
nonce: naclUtil.encodeBase64(nonce),
|
||||
ciphertext: naclUtil.encodeBase64(encrypted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const chatService = new ChatService()
|
||||
@@ -5,7 +5,6 @@
|
||||
|
||||
import { directus } from './directus.js'
|
||||
import { cryptoService } from './crypto.js'
|
||||
import { authService } from './auth.js'
|
||||
|
||||
class ConversationsService {
|
||||
constructor() {
|
||||
|
||||
@@ -150,7 +150,8 @@
|
||||
"placeholder": "Nachricht schreiben...",
|
||||
"encrypted": "Ende-zu-Ende verschlüsselt",
|
||||
"startConversation": "Starte eine Unterhaltung mit dem Anbieter.",
|
||||
"send": "Senden"
|
||||
"send": "Senden",
|
||||
"unavailable": "Chat nicht verfügbar"
|
||||
},
|
||||
"create": {
|
||||
"title": "Anzeige erstellen",
|
||||
|
||||
@@ -150,7 +150,8 @@
|
||||
"placeholder": "Write a message...",
|
||||
"encrypted": "End-to-end encrypted",
|
||||
"startConversation": "Start a conversation with the seller.",
|
||||
"send": "Send"
|
||||
"send": "Send",
|
||||
"unavailable": "Chat unavailable"
|
||||
},
|
||||
"create": {
|
||||
"title": "Create Listing",
|
||||
|
||||
@@ -150,7 +150,8 @@
|
||||
"placeholder": "Écrire un message...",
|
||||
"encrypted": "Chiffré de bout en bout",
|
||||
"startConversation": "Démarrez une conversation avec le vendeur.",
|
||||
"send": "Envoyer"
|
||||
"send": "Envoyer",
|
||||
"unavailable": "Chat non disponible"
|
||||
},
|
||||
"create": {
|
||||
"title": "Créer une annonce",
|
||||
|
||||
Reference in New Issue
Block a user