From 1df6504aaca352db4407d15a61c56a5140075be6 Mon Sep 17 00:00:00 2001 From: M Date: Wed, 29 Apr 2026 19:11:09 +1000 Subject: [PATCH] Fix Last Seen formatting for older timestamps The profile 'Last seen' line was rendered by feeding timeAgo() output into the 'Last seen %1$s ago' template. timeAgo() returns a bare absolute date (e.g. 'Jan 14' or 'Jul 27, 2024') for anything older than a month, so the result was nonsensical: - 'Last seen Jan 14 ago' - 'Last seen Jul 27, 2024 ago' Replace the call with a new lastSeenSentence() helper that always returns a self-contained, grammatical sentence: - 'Last seen 5 minutes ago' - 'Last seen 2 hours ago' - 'Last seen 3 days ago' - 'Last seen 2 weeks ago' - 'Last seen on Jul 27, 2024 (9 months ago)' - 'Last seen on Jan 14, 2024 (1 year ago)' Anything older than a week now also shows the absolute date alongside a coarse relative duration, so users can see exactly when the activity happened without giving up the human-readable 'X ago' framing. Adds plural-aware duration_minutes/hours/days/weeks/months/years resources plus last_seen_on_date / last_seen_just_now / last_seen_never helper strings. The existing 'last_seen' string keeps its placeholder shape so existing translations continue to work for the recent-duration case. --- .../amethyst/ui/note/TimeAgoFormatter.kt | 86 +++++++++++++++++++ .../profile/header/DrawAdditionalInfo.kt | 4 +- amethyst/src/main/res/values/strings.xml | 28 ++++++ 3 files changed, 116 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt index 5f7ce1a3e..c3986ac97 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/note/TimeAgoFormatter.kt @@ -241,6 +241,92 @@ fun dateFormatter( } } +/** + * Builds the full "Last seen ..." sentence shown on profiles and similar UI. + * + * Unlike [timeAgo], which can return a bare absolute date string (e.g. "Jan 14") + * for older timestamps, this function always returns a self-contained, grammatical + * description such as: + * + * - "Last seen 5 minutes ago" + * - "Last seen 2 hours ago" + * - "Last seen 3 days ago" + * - "Last seen 2 weeks ago" + * - "Last seen on Jul 27, 2024 (9 months ago)" + * - "Last seen on Jan 14, 2024 (1 year ago)" + * + * The duration component uses sensible units (seconds/minutes/hours/days/weeks/months/years) + * and pluralizes via Android plural resources. For anything older than a week we also + * include the absolute date so users see exactly when the activity happened. + */ +fun lastSeenSentence( + time: Long?, + context: Context, +): String { + if (time == null) return "" + if (time == 0L) return stringRes(context, R.string.last_seen_never) + + val nowSec = TimeUtils.now() + val diff = nowSec - time + + // Negative drift (clock skew, future timestamp) — treat as "just now". + if (diff < TimeUtils.ONE_MINUTE) { + return stringRes(context, R.string.last_seen_just_now) + } + + val resources = context.resources + + // Recent: render purely as a relative duration. + if (diff < TimeUtils.ONE_WEEK) { + val durationText = + when { + diff < TimeUtils.ONE_HOUR -> { + val n = (diff / TimeUtils.ONE_MINUTE).toInt() + resources.getQuantityString(R.plurals.duration_minutes, n, n) + } + + diff < TimeUtils.ONE_DAY -> { + val n = (diff / TimeUtils.ONE_HOUR).toInt() + resources.getQuantityString(R.plurals.duration_hours, n, n) + } + + else -> { + val n = (diff / TimeUtils.ONE_DAY).toInt() + resources.getQuantityString(R.plurals.duration_days, n, n) + } + } + return stringRes(context, R.string.last_seen, durationText) + } + + // Older than a week: include absolute date plus a coarse relative duration. + val durationText = + when { + diff < TimeUtils.ONE_MONTH -> { + val n = (diff / TimeUtils.ONE_WEEK).toInt() + resources.getQuantityString(R.plurals.duration_weeks, n, n) + } + + diff < TimeUtils.ONE_YEAR -> { + val n = (diff / TimeUtils.ONE_MONTH).toInt().coerceAtLeast(1) + resources.getQuantityString(R.plurals.duration_months, n, n) + } + + else -> { + val n = (diff / TimeUtils.ONE_YEAR).toInt().coerceAtLeast(1) + resources.getQuantityString(R.plurals.duration_years, n, n) + } + } + + if (locale != Locale.getDefault()) { + locale = Locale.getDefault() + yearFormatter = SimpleDateFormat(YEAR_DATE_FORMAT, locale) + monthFormatter = SimpleDateFormat(MONTH_DATE_FORMAT, locale) + } + val dateText = yearFormatter.format(time * 1000) + + return stringRes(context, R.string.last_seen_on_date, dateText, durationText) +} + fun timeAgoShort( mills: Long?, stringForNow: String, diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt index cf01ffc03..8a382fb66 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/header/DrawAdditionalInfo.kt @@ -66,7 +66,7 @@ import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.DrawPlayName import com.vitorpamplona.amethyst.ui.note.LoadAddressableNote import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol -import com.vitorpamplona.amethyst.ui.note.timeAgo +import com.vitorpamplona.amethyst.ui.note.lastSeenSentence import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel import com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.header.apps.DisplayAppRecommendations @@ -312,7 +312,7 @@ fun DisplayLastSeen( lastSeen?.let { timestamp -> val context = LocalContext.current Text( - text = stringRes(R.string.last_seen, timeAgo(timestamp, context, prefix = "", seconds = R.string.seconds)), + text = lastSeenSentence(timestamp, context), color = MaterialTheme.colorScheme.placeholderText, maxLines = 1, overflow = TextOverflow.Ellipsis, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 53cb1277c..83f6351c3 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -2379,6 +2379,34 @@ outbox lists relay settings Last seen %1$s ago + Last seen on %1$s (%2$s ago) + Last seen just now + Never seen + + + %1$d minute + %1$d minutes + + + %1$d hour + %1$d hours + + + %1$d day + %1$d days + + + %1$d week + %1$d weeks + + + %1$d month + %1$d months + + + %1$d year + %1$d years + <%1$s Connecting