FEATURE: added pols to frontend

This commit is contained in:
Your Name 2026-07-22 14:52:55 +03:00
parent c0391f8e69
commit b5982063c4
6 changed files with 83 additions and 78 deletions

BIN
chat.db

Binary file not shown.

View File

@ -523,7 +523,6 @@
let pollInterval = null; let pollInterval = null;
let selectedGifUrl = null; let selectedGifUrl = null;
let votedPolls = new Set(); let votedPolls = new Set();
let pollsData = [];
function loadVotedPolls() { function loadVotedPolls() {
try { try {
@ -711,7 +710,7 @@
if (data.success) { if (data.success) {
showToast('Poll created!', 'success'); showToast('Poll created!', 'success');
pollModal.classList.remove('open'); pollModal.classList.remove('open');
loadPolls(); loadMessages();
} else { } else {
showToast(data.error || 'Failed to create poll', 'error'); showToast(data.error || 'Failed to create poll', 'error');
} }
@ -745,27 +744,34 @@
renderChannels(); renderChannels();
messagesEl.innerHTML = '<div class="empty-state">Loading...</div>'; messagesEl.innerHTML = '<div class="empty-state">Loading...</div>';
loadMessages(); loadMessages();
loadPolls();
} }
async function loadPolls() { async function loadMessages() {
try { try {
const r = await fetch(`/api/polls?channel=${currentChannel}`); const r = await fetch(`/api/messages?channel=${currentChannel}&limit=50`);
const data = await r.json(); const data = await r.json();
if (data.success) pollsData = data.polls || []; if (data.success) {
else pollsData = []; window._lastMessages = data.messages || [];
renderMessages(window._lastMessages);
}
else showToast('Failed to load messages', 'error');
} catch (e) { } catch (e) {
pollsData = []; showToast('Network error', 'error');
} }
} }
function renderPolls() { function renderMessages(msgs) {
if (!pollsData.length) return ''; if (msgs.length === 0) {
messagesEl.innerHTML = '<div class="empty-state">💬 No messages yet. Be the first!</div>';
return;
}
let html = ''; let html = '';
pollsData.forEach(poll => { msgs.forEach(msg => {
if (msg.poll) {
const poll = msg.poll;
const isClosed = poll.status === 'closed'; const isClosed = poll.status === 'closed';
const isExpired = poll.expires_at && Date.now() > poll.expires_at; const isExpired = poll.expires_at && Date.now() > poll.expires_at;
const hasVoted = votedPolls.has(poll.id) || poll.options.some(o => o.votes > 0 && false); const hasVoted = votedPolls.has(poll.id);
const total = poll.total_votes || 0; const total = poll.total_votes || 0;
let optionsHtml = ''; let optionsHtml = '';
poll.options.forEach(opt => { poll.options.forEach(opt => {
@ -785,8 +791,7 @@
</div>`; </div>`;
} }
}); });
const statusLabel = isClosed || poll.status === 'closed' ? 'Closed' : const statusLabel = isClosed ? 'Closed' : isExpired ? 'Expired' : 'Active';
isExpired ? 'Expired' : 'Active';
const statusClass = isClosed || isExpired ? 'closed' : 'active'; const statusClass = isClosed || isExpired ? 'closed' : 'active';
const creatorLabel = poll.is_anonymous ? 'Anonymous' : escapeHtml(poll.creator_ip); const creatorLabel = poll.is_anonymous ? 'Anonymous' : escapeHtml(poll.creator_ip);
html += `<div class="poll-card" data-poll-id="${poll.id}"> html += `<div class="poll-card" data-poll-id="${poll.id}">
@ -794,32 +799,7 @@
<div class="poll-meta">by ${creatorLabel} · ${total} vote${total !== 1 ? 's' : ''} · <span class="poll-status ${statusClass}">${statusLabel}</span></div> <div class="poll-meta">by ${creatorLabel} · ${total} vote${total !== 1 ? 's' : ''} · <span class="poll-status ${statusClass}">${statusLabel}</span></div>
<div class="poll-options">${optionsHtml}</div> <div class="poll-options">${optionsHtml}</div>
</div>`; </div>`;
}); } else {
return html;
}
async function loadMessages() {
try {
const r = await fetch(`/api/messages?channel=${currentChannel}&limit=50`);
const data = await r.json();
if (data.success) {
window._lastMessages = data.messages || [];
renderMessages(window._lastMessages);
}
else showToast('Failed to load messages', 'error');
} catch (e) {
showToast('Network error', 'error');
}
}
function renderMessages(msgs) {
const pollsHtml = renderPolls();
if (msgs.length === 0 && !pollsHtml) {
messagesEl.innerHTML = '<div class="empty-state">💬 No messages yet. Be the first!</div>';
return;
}
let html = pollsHtml;
msgs.forEach(msg => {
const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' }); const time = new Date(msg.timestamp).toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
let contentHtml = escapeHtml(msg.content || ''); let contentHtml = escapeHtml(msg.content || '');
let gifToShow = msg.gif_url || null; let gifToShow = msg.gif_url || null;
@ -835,6 +815,7 @@
<div class="msg-content">${contentHtml}</div> <div class="msg-content">${contentHtml}</div>
<div class="msg-time">${time}</div> <div class="msg-time">${time}</div>
</div>`; </div>`;
}
}); });
messagesEl.innerHTML = html; messagesEl.innerHTML = html;
attachPollHandlers(); attachPollHandlers();
@ -859,10 +840,10 @@
votedPolls.add(pollId); votedPolls.add(pollId);
saveVotedPolls(); saveVotedPolls();
showToast('Vote recorded!', 'success'); showToast('Vote recorded!', 'success');
loadPolls().then(() => renderMessages(window._lastMessages || [])); loadMessages();
} else { } else {
showToast(data.error || 'Failed to vote', 'error'); showToast(data.error || 'Failed to vote', 'error');
loadPolls().then(() => renderMessages(window._lastMessages || [])); loadMessages();
} }
} catch (e) { } catch (e) {
showToast('Network error', 'error'); showToast('Network error', 'error');
@ -904,17 +885,13 @@
loadVotedPolls(); loadVotedPolls();
renderChannels(); renderChannels();
loadMessages(); loadMessages();
loadPolls();
sendBtn.addEventListener('click', sendMessage); sendBtn.addEventListener('click', sendMessage);
inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); }); inputEl.addEventListener('keydown', e => { if (e.key === 'Enter') sendMessage(); });
// Auto-poll every 3 seconds // Auto-refresh every 3 seconds
setInterval(() => { setInterval(() => {
loadMessages(); loadMessages();
loadPolls().then(() => {
if (window._lastMessages) renderMessages(window._lastMessages);
});
}, 3000); }, 3000);
</script> </script>
</body> </body>

View File

@ -22,6 +22,7 @@ class Messages(Base):
ip: Mapped[str] = mapped_column(String(64), nullable=False) ip: Mapped[str] = mapped_column(String(64), nullable=False)
content: Mapped[str] = mapped_column(Text, nullable=False) content: Mapped[str] = mapped_column(Text, nullable=False)
gif_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True, default=None) gif_url: Mapped[Optional[str]] = mapped_column(Text, nullable=True, default=None)
poll_id: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, default=None)
timestamp: Mapped[int] = mapped_column(Integer, nullable=False) timestamp: Mapped[int] = mapped_column(Integer, nullable=False)
class Poll(Base): class Poll(Base):

View File

@ -18,6 +18,16 @@ class DBfrontend:
self.engine = make_engine(db_cfg) self.engine = make_engine(db_cfg)
self.Session = sessionmaker(bind=self.engine) self.Session = sessionmaker(bind=self.engine)
self.cipher = self._get_cipher(secret, salt) self.cipher = self._get_cipher(secret, salt)
self._migrate()
def _migrate(self):
with self.Session() as session:
from sqlalchemy import inspect, text
inspector = inspect(session.connection())
columns = [c["name"] for c in inspector.get_columns("messages")]
if "poll_id" not in columns:
session.execute(text("ALTER TABLE messages ADD COLUMN poll_id INTEGER"))
session.commit()
@staticmethod @staticmethod
def _get_cipher(secret: bytes, salt: bytes) -> Fernet: def _get_cipher(secret: bytes, salt: bytes) -> Fernet:
@ -34,13 +44,14 @@ class DBfrontend:
except Exception: except Exception:
return "[Decryption failed]" return "[Decryption failed]"
def add_message(self, channel: str, ip: str, content: str, gif_url: Optional[str] = None) -> int: def add_message(self, channel: str, ip: str, content: str, gif_url: Optional[str] = None, poll_id: Optional[int] = None) -> int:
with self.Session() as session: with self.Session() as session:
msg = Messages( msg = Messages(
channel=channel, channel=channel,
ip=ip, ip=ip,
content=self._encrypt(content), content=self._encrypt(content),
gif_url=gif_url, gif_url=gif_url,
poll_id=poll_id,
timestamp=int(time.time() * 1000), timestamp=int(time.time() * 1000),
) )
session.add(msg) session.add(msg)
@ -57,6 +68,11 @@ class DBfrontend:
) )
result = [] result = []
for msg in reversed(q.all()): for msg in reversed(q.all()):
poll_data = None
if msg.poll_id:
poll = session.get(Poll, msg.poll_id)
if poll and poll.status != "deleted":
poll_data = self._serialize_poll(session, poll)
result.append( result.append(
{ {
"id": msg.id, "id": msg.id,
@ -64,6 +80,8 @@ class DBfrontend:
"ip": msg.ip, "ip": msg.ip,
"content": self._decrypt(msg.content), "content": self._decrypt(msg.content),
"gif_url": msg.gif_url, "gif_url": msg.gif_url,
"poll_id": msg.poll_id,
"poll": poll_data,
"timestamp": msg.timestamp, "timestamp": msg.timestamp,
} }
) )
@ -106,6 +124,15 @@ class DBfrontend:
opt = PollOption(poll_id=poll.id, label=self._encrypt(label), position=i) opt = PollOption(poll_id=poll.id, label=self._encrypt(label), position=i)
session.add(opt) session.add(opt)
msg = Messages(
channel=channel,
ip=creator_ip,
content=self._encrypt(question),
poll_id=poll.id,
timestamp=int(time.time() * 1000),
)
session.add(msg)
session.commit() session.commit()
return poll.id return poll.id