feat(messages): auto-refresh group chat with 5-second polling
Some checks are pending
NordaBiz Tests / Unit & Integration Tests (push) Waiting to run
NordaBiz Tests / E2E Tests (Playwright) (push) Blocked by required conditions
NordaBiz Tests / Smoke Tests (Production) (push) Blocked by required conditions
NordaBiz Tests / Send Failure Notification (push) Blocked by required conditions

New API endpoint /api/grupa/<id>/nowe returns messages after given ID.
Group view polls every 5 seconds and appends new messages without reload.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-03-20 12:22:30 +01:00
parent b587e28cea
commit cda97c18bb
3 changed files with 146 additions and 0 deletions

View File

@ -274,6 +274,7 @@ def register_blueprints(app):
'group_remove_member': 'messages.group_remove_member',
'group_change_role': 'messages.group_change_role',
'group_edit': 'messages.group_edit',
'group_poll_messages': 'messages.group_poll_messages',
})
logger.info("Created messages endpoint aliases")
except ImportError as e:

View File

@ -407,6 +407,75 @@ def group_send(group_id):
db.close()
@bp.route('/api/grupa/<int:group_id>/nowe', methods=['GET'])
@limiter.exempt
@login_required
@member_required
def group_poll_messages(group_id):
"""API: Pobierz nowe wiadomości po danym ID (polling)"""
after_id = request.args.get('after', 0, type=int)
db = SessionLocal()
try:
group, membership = _check_group_access(db, group_id, current_user.id)
if not group:
return jsonify({'messages': []})
new_msgs = db.query(GroupMessage).options(
joinedload(GroupMessage.sender)
).filter(
GroupMessage.group_id == group_id,
GroupMessage.id > after_id
).order_by(GroupMessage.created_at.asc()).all()
# Update read timestamp
if new_msgs:
membership.last_read_at = datetime.now()
db.commit()
# Build read receipts for new messages
members = db.query(MessageGroupMember).options(
joinedload(MessageGroupMember.user)
).filter(
MessageGroupMember.group_id == group_id,
MessageGroupMember.user_id != current_user.id
).all()
read_receipts = {}
for m in members:
if not m.last_read_at:
continue
for msg in reversed(new_msgs):
if msg.created_at <= m.last_read_at:
if msg.id not in read_receipts:
read_receipts[msg.id] = []
read_receipts[msg.id].append({
'name': m.user.name or m.user.email.split('@')[0],
'avatar_url': ('/static/' + m.user.avatar_path) if m.user.avatar_path else None,
'initial': (m.user.name or m.user.email)[0].upper()
})
break
result = []
for msg in new_msgs:
sender = msg.sender
result.append({
'id': msg.id,
'sender_id': msg.sender_id,
'sender_name': sender.name or sender.email.split('@')[0] if sender else 'Ktoś',
'sender_avatar': ('/static/' + sender.avatar_path) if sender and sender.avatar_path else None,
'sender_initial': (sender.name or sender.email)[0].upper() if sender else '?',
'is_me': msg.sender_id == current_user.id,
'content': msg.content,
'time': msg.created_at.strftime('%d.%m.%Y %H:%M'),
'read_by': read_receipts.get(msg.id, [])
})
return jsonify({'messages': result})
finally:
db.close()
# ============================================================
# GROUP MANAGEMENT ROUTES
# ============================================================

View File

@ -604,4 +604,80 @@
btn.textContent = 'Wysylanie...';
});
})();
/* Auto-refresh: poll for new messages every 5 seconds */
(function() {
var section = document.getElementById('messages-section');
if (!section) return;
var allMsgs = section.querySelectorAll('.group-message');
var lastId = 0;
if (allMsgs.length > 0) {
// Extract max message ID from data attribute or count
lastId = {{ messages[-1].id if messages else 0 }};
}
function createMessageHtml(msg) {
var avatarHtml;
if (msg.sender_avatar) {
avatarHtml = '<a href="/osoba/' + msg.sender_id + '" style="text-decoration:none;flex-shrink:0;">' +
'<img src="' + msg.sender_avatar + '" class="msg-avatar" alt="">' +
'</a>';
} else {
avatarHtml = '<a href="/osoba/' + msg.sender_id + '" style="text-decoration:none;flex-shrink:0;">' +
'<div class="msg-initial' + (msg.is_me ? ' is-me' : '') + '">' + msg.sender_initial + '</div>' +
'</a>';
}
var senderHtml = msg.is_me ? 'Ty' :
'<a href="/osoba/' + msg.sender_id + '" style="color:inherit;text-decoration:none;" onmouseover="this.style.textDecoration=\'underline\'" onmouseout="this.style.textDecoration=\'none\'">' + msg.sender_name + '</a>';
var receiptsHtml = '';
if (msg.read_by && msg.read_by.length > 0) {
receiptsHtml = '<div class="msg-read-receipts">';
msg.read_by.forEach(function(r) {
if (r.avatar_url) {
receiptsHtml += '<div class="read-receipt" title="' + r.name + '"><img src="' + r.avatar_url + '" alt=""></div>';
} else {
receiptsHtml += '<div class="read-receipt" title="' + r.name + '"><span>' + r.initial + '</span></div>';
}
});
receiptsHtml += '</div>';
}
return '<div class="group-message" data-msg-id="' + msg.id + '">' +
avatarHtml +
'<div class="msg-bubble">' +
'<div class="msg-header">' +
'<span class="msg-sender">' + senderHtml + '</span>' +
'<span class="msg-time">' + msg.time + '</span>' +
'</div>' +
'<div class="msg-content">' + msg.content + '</div>' +
'</div>' +
receiptsHtml +
'</div>';
}
function pollMessages() {
fetch('/api/grupa/' + {{ group.id }} + '/nowe?after=' + lastId)
.then(function(r) { return r.json(); })
.then(function(data) {
if (data.messages && data.messages.length > 0) {
// Remove old read receipts (they may have moved)
section.querySelectorAll('.msg-read-receipts').forEach(function(el) { el.remove(); });
data.messages.forEach(function(msg) {
section.insertAdjacentHTML('beforeend', createMessageHtml(msg));
lastId = msg.id;
});
// Scroll to bottom
section.scrollTop = section.scrollHeight;
}
})
.catch(function() { /* silent */ });
}
setInterval(pollMessages, 5000);
})();
{% endblock %}