Display activity date in local time (using the time zone that is stored with the activity), not in UTC (#4)

* Display timestamps using the timezone that is stored at the activity (fix 'new Date()' invocation)

* Display timestamps using the timezone that is stored at the activity (relative date in timeline views)

* Use correct timezone for auto-generated activity title

---------

Co-authored-by: Niklas Deutschmann <sonstharmlos@noreply.codeberg.org>
This commit is contained in:
Niklas 2026-04-27 22:01:08 +02:00 committed by GitHub
parent 5df4da86a5
commit 102d515b42
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 49 additions and 11 deletions

View file

@ -386,7 +386,8 @@ public class ActivityFileService {
activityTitle = parsedData.getTitle(); activityTitle = parsedData.getTitle();
} else { } else {
// Generate title if not provided // Generate title if not provided
activityTitle = ActivityFormatter.generateActivityTitle(parsedData.getStartTime(), parsedData.getActivityType()); activityTitle = ActivityFormatter.generateActivityTitle(parsedData.getStartTime(), parsedData.getTimezone(),
parsedData.getActivityType());
} }
// Default to PUBLIC if visibility not specified // Default to PUBLIC if visibility not specified

View file

@ -24,7 +24,6 @@ import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
@ -257,7 +256,7 @@ public class FitFileService {
private String generateTitle(ParsedActivityData parsedData) { private String generateTitle(ParsedActivityData parsedData) {
return ActivityFormatter.generateActivityTitle( return ActivityFormatter.generateActivityTitle(
parsedData.getStartTime(), parsedData.getStartTime(),
parsedData.getActivityType() parsedData.getTimezone(), parsedData.getActivityType()
); );
} }

View file

@ -1,13 +1,14 @@
package net.javahippie.fitpub.util; package net.javahippie.fitpub.util;
import lombok.extern.slf4j.Slf4j;
import net.javahippie.fitpub.model.entity.Activity; import net.javahippie.fitpub.model.entity.Activity;
import java.time.LocalDateTime; import java.time.*;
import java.time.LocalTime;
/** /**
* Utility class for formatting activity-related data for display. * Utility class for formatting activity-related data for display.
*/ */
@Slf4j
public class ActivityFormatter { public class ActivityFormatter {
/** /**
@ -47,21 +48,25 @@ public class ActivityFormatter {
* Generates a default activity title based on the time of day and activity type. * Generates a default activity title based on the time of day and activity type.
* Format: "[Time of Day] [Activity Type]" (e.g., "Morning Run", "Evening Ride") * Format: "[Time of Day] [Activity Type]" (e.g., "Morning Run", "Evening Ride")
* *
* @param startedAt the activity start time * @param startedAt the activity start time
* @param timezone the timezone ID of the activity
* @param activityType the activity type * @param activityType the activity type
* @return generated title * @return generated title
*/ */
public static String generateActivityTitle(LocalDateTime startedAt, Activity.ActivityType activityType) { public static String generateActivityTitle(LocalDateTime startedAt, String timezone, Activity.ActivityType activityType) {
if (startedAt == null || activityType == null) { if (startedAt == null || activityType == null) {
return "Activity"; return "Activity";
} }
String timeOfDay = getTimeOfDay(startedAt.toLocalTime()); LocalDateTime startedAtLocal = getUtcDateTimeInZone(startedAt, timezone);
String timeOfDay = getTimeOfDay(startedAtLocal.toLocalTime());
String formattedType = formatActivityType(activityType); String formattedType = formatActivityType(activityType);
return timeOfDay + " " + formattedType; return timeOfDay + " " + formattedType;
} }
/** /**
* Determines the time of day based on the hour. * Determines the time of day based on the hour.
* *
@ -81,4 +86,25 @@ public class ActivityFormatter {
return "Night"; return "Night";
} }
} }
/**
* Attempts to convert the given LocalDateTime (which is assumed to be UTC) into a LocalDateTime in the given
* timezone
*
* @param utcDateTime The original date and time (UTC)
* @param timezone A timezone ID
* @return The original date and time adjusted to the timezone, if the zone ID could be parsed. The original date
* and time otherwise
*
*/
private static LocalDateTime getUtcDateTimeInZone(LocalDateTime utcDateTime, String timezone) {
try {
return utcDateTime.atZone(ZoneOffset.UTC)
.withZoneSameInstant(ZoneId.of(timezone))
.toLocalDateTime();
} catch (DateTimeException e) {
log.warn("Invalid time zone ID: {}", timezone);
return utcDateTime;
}
}
} }

View file

@ -434,7 +434,7 @@ function formatDateTimeWithTimezone(timestamp, timezone, options = {}) {
// Parse the timestamp - backend sends LocalDateTime without 'Z' // Parse the timestamp - backend sends LocalDateTime without 'Z'
// We need to interpret it in the specified timezone // We need to interpret it in the specified timezone
const date = new Date(timestamp); const date = new Date(ensureUTC(timestamp));
// Default options for date/time display // Default options for date/time display
const defaultOptions = { const defaultOptions = {
@ -473,6 +473,17 @@ function formatDateWithTimezone(timestamp, timezone) {
}); });
} }
/**
* Ensures that a timestamp will be interpreted as UTC by new Date()
* See https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date (Date time string format)
*
* @param {string} timestamp - ISO timestamp or LocalDateTime string
* @returns {string} The input string, but with a trailing 'Z'
*/
function ensureUTC(timestamp) {
return timestamp.endsWith('Z') ? timestamp : timestamp + 'Z';
}
// Make functions available globally for inline scripts // Make functions available globally for inline scripts
window.FitPub = { window.FitPub = {
createActivityMap, createActivityMap,
@ -482,5 +493,6 @@ window.FitPub = {
formatDistance, formatDistance,
formatPace, formatPace,
formatDateTimeWithTimezone, formatDateTimeWithTimezone,
formatDateWithTimezone formatDateWithTimezone,
ensureUTC
}; };

View file

@ -727,7 +727,7 @@ const FitPubTimeline = {
* @returns {string} Time ago string * @returns {string} Time ago string
*/ */
formatTimeAgo: function(timestamp) { formatTimeAgo: function(timestamp) {
const date = new Date(timestamp); const date = new Date(FitPub.ensureUTC(timestamp));
const now = new Date(); const now = new Date();
const secondsAgo = Math.floor((now - date) / 1000); const secondsAgo = Math.floor((now - date) / 1000);