Backend (profiles.py / main.py): - Fix bot startup crash: create active user before start_chat - Fix address-clobbering bug on restart (UserContactLink vs CreatedConnLink) - Add user profile type alongside bots - Channels: create groups with observer links, classify via acceptMemberRole - Chat rooms: get_chat_history + send_to_chat, history/messages/send routes UI: - Chat room view with message bubbles and live polling - Convert top nav to collapsible left sidebar (mobile-friendly off-canvas) - Three-way Users/Bots split; clickable cards; copy-address buttons + links - Add Matrix theme alongside Original Light/Dark - File upload sidebar link; site footer on all pages incl. login - Bot-type descriptions on the Bots page; QR caption on profiles Remove orphaned index.html (superseded by list.html). Ignore downloaded libs/, exploration DBs, and local ai.sh. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
155 lines
5.6 KiB
HTML
155 lines
5.6 KiB
HTML
{% extends "base.html" %}
|
|
{% block title %}{{ chat_name }} — SimpleX Manager{% endblock %}
|
|
|
|
{% block head %}
|
|
<style>
|
|
.chat-wrap {
|
|
display: flex; flex-direction: column;
|
|
height: calc(100vh - 140px); min-height: 400px;
|
|
background: var(--card); border-radius: 10px; box-shadow: var(--shadow);
|
|
overflow: hidden;
|
|
}
|
|
.chat-head {
|
|
display: flex; align-items: center; justify-content: space-between;
|
|
padding: 14px 18px; border-bottom: 1px solid var(--border);
|
|
}
|
|
.chat-head .title { font-weight: 700; font-size: 16px; }
|
|
|
|
.chat-log {
|
|
flex: 1; overflow-y: auto; padding: 18px;
|
|
display: flex; flex-direction: column; gap: 8px;
|
|
}
|
|
.bubble {
|
|
max-width: 72%; padding: 8px 12px; border-radius: 14px;
|
|
font-size: 14px; line-height: 1.4; word-wrap: break-word; white-space: pre-wrap;
|
|
}
|
|
.bubble .who { font-size: 11px; font-weight: 700; opacity: 0.7; margin-bottom: 2px; }
|
|
.bubble .ts { font-size: 10px; opacity: 0.55; margin-top: 3px; text-align: right; }
|
|
.bubble.in { align-self: flex-start; background: var(--bg); border: 1px solid var(--border); }
|
|
.bubble.out { align-self: flex-end; background: var(--accent); color: var(--btn-light-text); }
|
|
.bubble.deleted { font-style: italic; opacity: 0.5; }
|
|
|
|
.chat-compose {
|
|
display: flex; gap: 8px; padding: 12px; border-top: 1px solid var(--border);
|
|
}
|
|
.chat-compose textarea { resize: none; height: 42px; }
|
|
.chat-empty { text-align: center; color: var(--muted); margin: auto; font-size: 14px; }
|
|
.chat-banner { padding: 8px 18px; font-size: 12px; color: var(--muted);
|
|
background: var(--bg); border-bottom: 1px solid var(--border); }
|
|
</style>
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="flex gap-8" style="margin-bottom:16px;">
|
|
<a href="/profile/{{ profile.id }}" class="muted" style="text-decoration:none;">← {{ profile.name }}</a>
|
|
<span class="muted">/</span>
|
|
<strong>{{ chat_name }}</strong>
|
|
<span class="tag">{{ 'channel' if is_channel else chat_type }}</span>
|
|
</div>
|
|
|
|
<div class="chat-wrap">
|
|
<div class="chat-head">
|
|
<span class="title">{{ chat_name }}</span>
|
|
<button class="btn btn-ghost" style="padding:4px 12px;font-size:12px;"
|
|
onclick="loadMessages(true)">↻ Refresh</button>
|
|
</div>
|
|
|
|
{% if is_channel %}
|
|
<div class="chat-banner">📢 Channel — messages you send here broadcast to all subscribers.</div>
|
|
{% endif %}
|
|
|
|
<div class="chat-log" id="chat-log">
|
|
{% if not running %}
|
|
<div class="chat-empty">Profile is stopped. Start it to load messages.</div>
|
|
{% else %}
|
|
<div class="chat-empty" id="chat-loading">Loading messages…</div>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<div class="chat-compose">
|
|
<textarea id="msg-input" placeholder="{{ 'Broadcast a message…' if is_channel else 'Type a message…' }}"
|
|
{% if not running %}disabled{% endif %}
|
|
onkeydown="if(event.key==='Enter'&&!event.shiftKey){event.preventDefault();sendMsg();}"></textarea>
|
|
<button class="btn btn-primary" onclick="sendMsg()" {% if not running %}disabled{% endif %}>Send</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const PROFILE_ID = {{ profile.id }};
|
|
const CHAT_TYPE = '{{ chat_type }}';
|
|
const CHAT_ID = {{ chat_id }};
|
|
const RUNNING = {{ 'true' if running else 'false' }};
|
|
const _token = () => document.cookie.match(/(?:^|;\s*)token=([^;]+)/)?.[1] || '';
|
|
|
|
let lastIds = ''; // signature of rendered messages, to skip needless re-renders
|
|
|
|
function fmtTs(iso) {
|
|
if (!iso) return '';
|
|
const d = new Date(iso);
|
|
if (isNaN(d)) return '';
|
|
return d.toLocaleString([], {month:'short', day:'numeric', hour:'2-digit', minute:'2-digit'});
|
|
}
|
|
|
|
function render(messages) {
|
|
const log = document.getElementById('chat-log');
|
|
const sig = messages.map(m => m.id).join(',');
|
|
if (sig === lastIds) return; // nothing new
|
|
const atBottom = log.scrollHeight - log.scrollTop - log.clientHeight < 60;
|
|
lastIds = sig;
|
|
|
|
if (!messages.length) {
|
|
log.innerHTML = '<div class="chat-empty">No messages yet.</div>';
|
|
return;
|
|
}
|
|
log.innerHTML = messages.map(m => {
|
|
const cls = 'bubble ' + (m.outgoing ? 'out' : 'in') + (m.deleted ? ' deleted' : '');
|
|
const who = (!m.outgoing && m.sender) ? `<div class="who">${escapeHtml(m.sender)}</div>` : '';
|
|
const txt = m.deleted ? '(deleted)' : escapeHtml(m.text || '');
|
|
return `<div class="${cls}">${who}${txt}<div class="ts">${fmtTs(m.ts)}</div></div>`;
|
|
}).join('');
|
|
if (atBottom) log.scrollTop = log.scrollHeight;
|
|
}
|
|
|
|
function escapeHtml(s) {
|
|
return s.replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c]));
|
|
}
|
|
|
|
async function loadMessages(force) {
|
|
if (!RUNNING) return;
|
|
try {
|
|
const resp = await fetch(`/api/profiles/${PROFILE_ID}/chat/${CHAT_TYPE}/${CHAT_ID}/messages?count=80`, {
|
|
headers: {'X-Token': _token()},
|
|
});
|
|
if (!resp.ok) return;
|
|
const data = await resp.json();
|
|
if (force) lastIds = '';
|
|
render(data.messages || []);
|
|
} catch(e) {}
|
|
}
|
|
|
|
async function sendMsg() {
|
|
const input = document.getElementById('msg-input');
|
|
const text = input.value.trim();
|
|
if (!text) return;
|
|
input.value = '';
|
|
const resp = await fetch(`/api/profiles/${PROFILE_ID}/chat/${CHAT_TYPE}/${CHAT_ID}/send`, {
|
|
method: 'POST',
|
|
headers: {'Content-Type': 'application/json', 'X-Token': _token()},
|
|
body: JSON.stringify({text}),
|
|
});
|
|
const data = await resp.json();
|
|
if (!data.ok) {
|
|
input.value = text; // restore on failure
|
|
alert('Failed to send');
|
|
return;
|
|
}
|
|
setTimeout(() => loadMessages(true), 250); // reflect the sent message quickly
|
|
}
|
|
|
|
if (RUNNING) {
|
|
loadMessages(true);
|
|
setInterval(loadMessages, 3000); // live updates via polling
|
|
}
|
|
</script>
|
|
{% endblock %}
|