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();
}
// 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) {
if (!dateStr) return '';
var d = new Date(dateStr);
var d = parseUTC(dateStr);
var now = new Date();
var diff = (now - d) / 1000;
if (diff < 60) return 'teraz';
@ -98,12 +107,12 @@
function formatMessageTime(dateStr) {
if (!dateStr) return '';
var d = new Date(dateStr);
var d = parseUTC(dateStr);
return d.toLocaleTimeString('pl', { hour: '2-digit', minute: '2-digit' });
}
function formatDateSeparator(dateStr) {
var d = new Date(dateStr);
var d = parseUTC(dateStr);
if (isToday(d)) return 'Dzisiaj';
if (isYesterday(d)) return 'Wczoraj';
return d.toLocaleDateString('pl', { day: 'numeric', month: 'long', year: 'numeric' });
@ -111,7 +120,7 @@
function formatPresence(lastSeen) {
if (!lastSeen) return '';
var d = new Date(lastSeen);
var d = parseUTC(lastSeen);
return 'ostatnio: ' + d.toLocaleDateString('pl', { day: '2-digit', month: '2-digit' }) +
' o ' + d.toLocaleTimeString('pl', { hour: '2-digit', minute: '2-digit' });
}
@ -329,7 +338,7 @@
state.conversations.sort(function (a, b) {
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;
return new Date(bTime || 0) - new Date(aTime || 0);
return (parseUTC(bTime) || 0) - (parseUTC(aTime) || 0);
});
ConversationList.renderList();
@ -479,7 +488,7 @@
messages.forEach(function (msg) {
// Date separator
if (msg.created_at) {
var msgDate = new Date(msg.created_at).toDateString();
var msgDate = parseUTC(msg.created_at).toDateString();
if (msgDate !== lastDate) {
var sep = el('div', 'date-separator');
sep.textContent = formatDateSeparator(msg.created_at);
@ -641,18 +650,18 @@
var readAt = null;
if (details && details.members) {
var msgTime = new Date(msg.created_at);
var msgTime = parseUTC(msg.created_at);
var otherMembers = details.members.filter(function (m) {
return m.user_id !== window.__CURRENT_USER__.id;
});
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) {
// Find earliest read time among others
var readTimes = otherMembers
.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) {
readAt = new Date(Math.min.apply(null, readTimes));
}
@ -670,18 +679,18 @@
// Group: show per-member read status
// Sort: read (earliest first), then unread (alphabetically)
otherMembers.sort(function (a, b) {
var aRead = a.last_read_at && new Date(a.last_read_at) >= msgTime;
var bRead = b.last_read_at && new Date(b.last_read_at) >= msgTime;
var aRead = a.last_read_at && parseUTC(a.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 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');
});
var lines = [];
otherMembers.forEach(function (m) {
var name = m.name || 'Użytkownik';
if (m.last_read_at && new Date(m.last_read_at) >= msgTime) {
var d = new Date(m.last_read_at);
if (m.last_read_at && parseUTC(m.last_read_at) >= msgTime) {
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'});
lines.push('<span class="read-status-line read">✓ ' + name + ' — ' + dateStr + '</span>');
} else {
@ -778,11 +787,11 @@
if (lastRow) {
var lastMsg = state.messages[convId][state.messages[convId].length - 2];
if (lastMsg && lastMsg.created_at) {
lastDate = new Date(lastMsg.created_at).toDateString();
lastDate = parseUTC(lastMsg.created_at).toDateString();
}
}
if (msg.created_at) {
var msgDate = new Date(msg.created_at).toDateString();
var msgDate = parseUTC(msg.created_at).toDateString();
if (msgDate !== lastDate) {
var sep = el('div', 'date-separator');
sep.textContent = formatDateSeparator(msg.created_at);
@ -890,7 +899,7 @@
var isMine = msg.sender && msg.sender.id === window.__CURRENT_USER__.id;
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
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
(function() {
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);
})();
{% endblock %}