fix: correct timezone display in messages — parse server UTC dates properly
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

Server stores timestamps in UTC without timezone suffix. JavaScript
new Date() treated them as local time, showing times 2h behind.
Added parseUTC() helper that appends 'Z' to naive ISO dates so the
browser correctly converts UTC → user's local timezone.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maciej Pienczyn 2026-04-09 21:31:06 +02:00
parent 898c10921d
commit f0bdfe013b
2 changed files with 27 additions and 18 deletions

View File

@ -84,9 +84,18 @@
d.getFullYear() === y.getFullYear(); d.getFullYear() === y.getFullYear();
} }
// Server stores UTC but isoformat() omits timezone — append Z so JS knows it's UTC
function parseUTC(dateStr) {
if (!dateStr) return null;
if (dateStr.indexOf('Z') === -1 && dateStr.indexOf('+') === -1 && dateStr.indexOf('T') !== -1) {
return new Date(dateStr + 'Z');
}
return new Date(dateStr);
}
function formatTime(dateStr) { function formatTime(dateStr) {
if (!dateStr) return ''; if (!dateStr) return '';
var d = new Date(dateStr); var d = parseUTC(dateStr);
var now = new Date(); var now = new Date();
var diff = (now - d) / 1000; var diff = (now - d) / 1000;
if (diff < 60) return 'teraz'; if (diff < 60) return 'teraz';
@ -98,12 +107,12 @@
function formatMessageTime(dateStr) { function formatMessageTime(dateStr) {
if (!dateStr) return ''; if (!dateStr) return '';
var d = new Date(dateStr); var d = parseUTC(dateStr);
return d.toLocaleTimeString('pl', { hour: '2-digit', minute: '2-digit' }); return d.toLocaleTimeString('pl', { hour: '2-digit', minute: '2-digit' });
} }
function formatDateSeparator(dateStr) { function formatDateSeparator(dateStr) {
var d = new Date(dateStr); var d = parseUTC(dateStr);
if (isToday(d)) return 'Dzisiaj'; if (isToday(d)) return 'Dzisiaj';
if (isYesterday(d)) return 'Wczoraj'; if (isYesterday(d)) return 'Wczoraj';
return d.toLocaleDateString('pl', { day: 'numeric', month: 'long', year: 'numeric' }); return d.toLocaleDateString('pl', { day: 'numeric', month: 'long', year: 'numeric' });
@ -111,7 +120,7 @@
function formatPresence(lastSeen) { function formatPresence(lastSeen) {
if (!lastSeen) return ''; if (!lastSeen) return '';
var d = new Date(lastSeen); var d = parseUTC(lastSeen);
return 'ostatnio: ' + d.toLocaleDateString('pl', { day: '2-digit', month: '2-digit' }) + return 'ostatnio: ' + d.toLocaleDateString('pl', { day: '2-digit', month: '2-digit' }) +
' o ' + d.toLocaleTimeString('pl', { hour: '2-digit', minute: '2-digit' }); ' o ' + d.toLocaleTimeString('pl', { hour: '2-digit', minute: '2-digit' });
} }
@ -329,7 +338,7 @@
state.conversations.sort(function (a, b) { state.conversations.sort(function (a, b) {
var aTime = a.last_message ? a.last_message.created_at : a.updated_at; var aTime = a.last_message ? a.last_message.created_at : a.updated_at;
var bTime = b.last_message ? b.last_message.created_at : b.updated_at; var bTime = b.last_message ? b.last_message.created_at : b.updated_at;
return new Date(bTime || 0) - new Date(aTime || 0); return (parseUTC(bTime) || 0) - (parseUTC(aTime) || 0);
}); });
ConversationList.renderList(); ConversationList.renderList();
@ -479,7 +488,7 @@
messages.forEach(function (msg) { messages.forEach(function (msg) {
// Date separator // Date separator
if (msg.created_at) { if (msg.created_at) {
var msgDate = new Date(msg.created_at).toDateString(); var msgDate = parseUTC(msg.created_at).toDateString();
if (msgDate !== lastDate) { if (msgDate !== lastDate) {
var sep = el('div', 'date-separator'); var sep = el('div', 'date-separator');
sep.textContent = formatDateSeparator(msg.created_at); sep.textContent = formatDateSeparator(msg.created_at);
@ -641,18 +650,18 @@
var readAt = null; var readAt = null;
if (details && details.members) { if (details && details.members) {
var msgTime = new Date(msg.created_at); var msgTime = parseUTC(msg.created_at);
var otherMembers = details.members.filter(function (m) { var otherMembers = details.members.filter(function (m) {
return m.user_id !== window.__CURRENT_USER__.id; return m.user_id !== window.__CURRENT_USER__.id;
}); });
isRead = otherMembers.length > 0 && otherMembers.every(function (m) { isRead = otherMembers.length > 0 && otherMembers.every(function (m) {
return m.last_read_at && new Date(m.last_read_at) >= msgTime; return m.last_read_at && parseUTC(m.last_read_at) >= msgTime;
}); });
if (isRead) { if (isRead) {
// Find earliest read time among others // Find earliest read time among others
var readTimes = otherMembers var readTimes = otherMembers
.filter(function (m) { return m.last_read_at; }) .filter(function (m) { return m.last_read_at; })
.map(function (m) { return new Date(m.last_read_at); }); .map(function (m) { return parseUTC(m.last_read_at); });
if (readTimes.length) { if (readTimes.length) {
readAt = new Date(Math.min.apply(null, readTimes)); readAt = new Date(Math.min.apply(null, readTimes));
} }
@ -670,18 +679,18 @@
// Group: show per-member read status // Group: show per-member read status
// Sort: read (earliest first), then unread (alphabetically) // Sort: read (earliest first), then unread (alphabetically)
otherMembers.sort(function (a, b) { otherMembers.sort(function (a, b) {
var aRead = a.last_read_at && new Date(a.last_read_at) >= msgTime; var aRead = a.last_read_at && parseUTC(a.last_read_at) >= msgTime;
var bRead = b.last_read_at && new Date(b.last_read_at) >= msgTime; var bRead = b.last_read_at && parseUTC(b.last_read_at) >= msgTime;
if (aRead && !bRead) return -1; if (aRead && !bRead) return -1;
if (!aRead && bRead) return 1; if (!aRead && bRead) return 1;
if (aRead && bRead) return new Date(a.last_read_at) - new Date(b.last_read_at); if (aRead && bRead) return parseUTC(a.last_read_at) - parseUTC(b.last_read_at);
return (a.name || '').localeCompare(b.name || '', 'pl'); return (a.name || '').localeCompare(b.name || '', 'pl');
}); });
var lines = []; var lines = [];
otherMembers.forEach(function (m) { otherMembers.forEach(function (m) {
var name = m.name || 'Użytkownik'; var name = m.name || 'Użytkownik';
if (m.last_read_at && new Date(m.last_read_at) >= msgTime) { if (m.last_read_at && parseUTC(m.last_read_at) >= msgTime) {
var d = new Date(m.last_read_at); var d = parseUTC(m.last_read_at);
var dateStr = d.toLocaleDateString('pl', {day:'2-digit', month:'2-digit'}) + ' o ' + d.toLocaleTimeString('pl', {hour:'2-digit', minute:'2-digit'}); var dateStr = d.toLocaleDateString('pl', {day:'2-digit', month:'2-digit'}) + ' o ' + d.toLocaleTimeString('pl', {hour:'2-digit', minute:'2-digit'});
lines.push('<span class="read-status-line read">✓ ' + name + ' — ' + dateStr + '</span>'); lines.push('<span class="read-status-line read">✓ ' + name + ' — ' + dateStr + '</span>');
} else { } else {
@ -778,11 +787,11 @@
if (lastRow) { if (lastRow) {
var lastMsg = state.messages[convId][state.messages[convId].length - 2]; var lastMsg = state.messages[convId][state.messages[convId].length - 2];
if (lastMsg && lastMsg.created_at) { if (lastMsg && lastMsg.created_at) {
lastDate = new Date(lastMsg.created_at).toDateString(); lastDate = parseUTC(lastMsg.created_at).toDateString();
} }
} }
if (msg.created_at) { if (msg.created_at) {
var msgDate = new Date(msg.created_at).toDateString(); var msgDate = parseUTC(msg.created_at).toDateString();
if (msgDate !== lastDate) { if (msgDate !== lastDate) {
var sep = el('div', 'date-separator'); var sep = el('div', 'date-separator');
sep.textContent = formatDateSeparator(msg.created_at); sep.textContent = formatDateSeparator(msg.created_at);
@ -890,7 +899,7 @@
var isMine = msg.sender && msg.sender.id === window.__CURRENT_USER__.id; var isMine = msg.sender && msg.sender.id === window.__CURRENT_USER__.id;
var canEdit = isMine && msg.created_at && var canEdit = isMine && msg.created_at &&
(new Date() - new Date(msg.created_at)) < 24 * 60 * 60 * 1000; (new Date() - parseUTC(msg.created_at)) < 24 * 60 * 60 * 1000;
// Show/hide owner-only buttons // Show/hide owner-only buttons
menu.querySelectorAll('.owner-only').forEach(function (btn) { menu.querySelectorAll('.owner-only').forEach(function (btn) {

View File

@ -326,7 +326,7 @@ window.__CSRF_TOKEN__ = '{{ csrf_token() }}';
// Load conversations.js after data is set // Load conversations.js after data is set
(function() { (function() {
var s = document.createElement('script'); var s = document.createElement('script');
s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=24'; s.src = '{{ url_for("static", filename="js/conversations.js") }}?v=25';
document.body.appendChild(s); document.body.appendChild(s);
})(); })();
{% endblock %} {% endblock %}