187 lines
6.1 KiB
JavaScript
187 lines
6.1 KiB
JavaScript
window.BackOneAddons = window.BackOneAddons || {};
|
|
|
|
window.BackOneAddons['secure-chat'] = function secureChatAddon() {
|
|
const messagesByPeer = {};
|
|
let selectedPeer = null;
|
|
let root = null;
|
|
let unsubChat = null;
|
|
|
|
function escapeHtml(s) {
|
|
return String(s).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
|
|
function clearSessionMessages() {
|
|
Object.keys(messagesByPeer).forEach(k => delete messagesByPeer[k]);
|
|
}
|
|
|
|
function getThread(peerId) {
|
|
if (!messagesByPeer[peerId]) messagesByPeer[peerId] = [];
|
|
return messagesByPeer[peerId];
|
|
}
|
|
|
|
function formatTime(ts) {
|
|
if (!ts) return '';
|
|
const d = new Date(ts);
|
|
return d.toLocaleString(undefined, { hour: '2-digit', minute: '2-digit', month: 'short', day: 'numeric' });
|
|
}
|
|
|
|
function networkLabel() {
|
|
const n = BackOneNetworkSession.getActiveNetwork();
|
|
return n ? (n.name || n.id) : BackOneNetworkSession.getNetworkId();
|
|
}
|
|
|
|
function renderHeader() {
|
|
const header = root?.querySelector('#secure-chat-header');
|
|
if (!header) return;
|
|
const net = networkLabel();
|
|
if (!selectedPeer) {
|
|
header.textContent = net ? `Select a contact on ${net}` : 'Join a network in the header bar';
|
|
return;
|
|
}
|
|
header.innerHTML = `<strong>${escapeHtml(selectedPeer.displayName || selectedPeer.peerId)}</strong> <small>${escapeHtml(selectedPeer.peerId)}${net ? ' · ' + escapeHtml(net) : ''}</small>`;
|
|
}
|
|
|
|
function selfName() {
|
|
const addr = (BackOneNetworkSession.getCoreStatus().address || '').toLowerCase();
|
|
return addr ? addr.slice(0, 10) + '…' : 'me';
|
|
}
|
|
|
|
return {
|
|
mount(container) {
|
|
root = container;
|
|
clearSessionMessages();
|
|
container.innerHTML = `
|
|
<div class="comm-layout comm-split-layout">
|
|
<aside class="comm-sidebar">
|
|
<div class="comm-sidebar-head">
|
|
<h2>Secure Chat</h2>
|
|
<p class="comm-sidebar-sub">Contacts · session only</p>
|
|
</div>
|
|
<div class="comm-sidebar-body">
|
|
<div id="secure-chat-peer-picker"></div>
|
|
</div>
|
|
</aside>
|
|
<div class="comm-main">
|
|
<div class="chat-thread-panel">
|
|
<div class="chat-thread-header" id="secure-chat-header">Select a contact</div>
|
|
<div id="secure-chat-thread" class="chat-messages"></div>
|
|
<div class="chat-compose">
|
|
<textarea id="secure-chat-input" rows="2" placeholder="Type a secure message" disabled></textarea>
|
|
<div class="chat-compose-row">
|
|
<button type="button" id="secure-chat-send" class="btn-primary" disabled>Send</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
const thread = container.querySelector('#secure-chat-thread');
|
|
const input = container.querySelector('#secure-chat-input');
|
|
const sendBtn = container.querySelector('#secure-chat-send');
|
|
|
|
const render = () => {
|
|
if (!selectedPeer) {
|
|
thread.innerHTML = '<p class="chat-empty">Choose a contact from the list</p>';
|
|
return;
|
|
}
|
|
const messages = getThread(selectedPeer.peerId);
|
|
if (!messages.length) {
|
|
thread.innerHTML = '<p class="chat-empty">No messages yet — start the conversation</p>';
|
|
return;
|
|
}
|
|
thread.innerHTML = messages.map(m => `
|
|
<div class="chat-message">
|
|
<div class="chat-message-meta">
|
|
<span class="chat-message-sender">${escapeHtml(m.senderName || m.sender || 'me')}</span>
|
|
<span class="chat-message-time">${escapeHtml(formatTime(m.at))}</span>
|
|
</div>
|
|
${m.body ? `<div class="chat-message-body">${escapeHtml(m.body)}</div>` : ''}
|
|
</div>`).join('');
|
|
thread.scrollTop = thread.scrollHeight;
|
|
};
|
|
|
|
function updateInputs() {
|
|
const networkId = BackOneNetworkSession.getNetworkId();
|
|
const ok = !!networkId && !!selectedPeer;
|
|
input.disabled = !ok;
|
|
sendBtn.disabled = !ok;
|
|
renderHeader();
|
|
}
|
|
|
|
if (unsubChat) unsubChat();
|
|
unsubChat = BackOneWebHubClient.subscribe('chat_message', (msg) => {
|
|
const networkId = BackOneNetworkSession.getNetworkId();
|
|
if (!networkId || (msg.networkId && msg.networkId.toLowerCase() !== networkId)) return;
|
|
const senderId = (msg.senderId || '').toLowerCase();
|
|
const peerId = selectedPeer?.peerId?.toLowerCase();
|
|
if (!peerId || senderId !== peerId) {
|
|
if (senderId) {
|
|
getThread(senderId).push({
|
|
sender: senderId,
|
|
senderName: msg.senderName || senderId,
|
|
body: msg.body || '',
|
|
at: msg.at || Date.now()
|
|
});
|
|
}
|
|
return;
|
|
}
|
|
getThread(peerId).push({
|
|
sender: senderId,
|
|
senderName: msg.senderName || senderId,
|
|
body: msg.body || '',
|
|
at: msg.at || Date.now()
|
|
});
|
|
render();
|
|
});
|
|
|
|
BackOnePeerPicker(container.querySelector('#secure-chat-peer-picker'), {
|
|
layout: 'sidebar',
|
|
showActionButton: false,
|
|
onSelect(peer) {
|
|
selectedPeer = peer;
|
|
updateInputs();
|
|
render();
|
|
},
|
|
onNetworkChange() {
|
|
selectedPeer = null;
|
|
clearSessionMessages();
|
|
updateInputs();
|
|
render();
|
|
}
|
|
});
|
|
|
|
BackOneNetworkSession.subscribe(() => {
|
|
updateInputs();
|
|
render();
|
|
});
|
|
|
|
sendBtn.onclick = () => {
|
|
const networkId = BackOneNetworkSession.getNetworkId();
|
|
if (!selectedPeer || !networkId) return;
|
|
const body = input.value.trim();
|
|
if (!body) return;
|
|
const now = Date.now();
|
|
const name = selfName();
|
|
BackOneWebHubClient.send({
|
|
type: 'chat_message',
|
|
networkId,
|
|
targetId: selectedPeer.peerId,
|
|
senderName: name,
|
|
body
|
|
});
|
|
getThread(selectedPeer.peerId).push({
|
|
sender: 'me',
|
|
senderName: name,
|
|
body,
|
|
at: now
|
|
});
|
|
input.value = '';
|
|
render();
|
|
};
|
|
|
|
updateInputs();
|
|
render();
|
|
}
|
|
};
|
|
};
|