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
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:
parent
b587e28cea
commit
cda97c18bb
@ -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:
|
||||
|
||||
@ -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
|
||||
# ============================================================
|
||||
|
||||
@ -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 %}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user