Merge pull request #2642 from mstrofnone/fix/last-seen-formatting

Fix Last Seen formatting for older timestamps ("Last seen Jan 14 ago" → "Last seen on Jan 14, 2026 (3 months ago)")
This commit is contained in:
Vitor Pamplona
2026-04-29 07:53:07 -04:00
committed by GitHub
3 changed files with 116 additions and 2 deletions
@@ -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,
@@ -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,
+28
View File
@@ -2379,6 +2379,34 @@
<string name="relay_settings_lower2">outbox lists</string>
<string name="relay_settings_lower">relay settings</string>
<string name="last_seen">Last seen %1$s ago</string>
<string name="last_seen_on_date">Last seen on %1$s (%2$s ago)</string>
<string name="last_seen_just_now">Last seen just now</string>
<string name="last_seen_never">Never seen</string>
<plurals name="duration_minutes">
<item quantity="one">%1$d minute</item>
<item quantity="other">%1$d minutes</item>
</plurals>
<plurals name="duration_hours">
<item quantity="one">%1$d hour</item>
<item quantity="other">%1$d hours</item>
</plurals>
<plurals name="duration_days">
<item quantity="one">%1$d day</item>
<item quantity="other">%1$d days</item>
</plurals>
<plurals name="duration_weeks">
<item quantity="one">%1$d week</item>
<item quantity="other">%1$d weeks</item>
</plurals>
<plurals name="duration_months">
<item quantity="one">%1$d month</item>
<item quantity="other">%1$d months</item>
</plurals>
<plurals name="duration_years">
<item quantity="one">%1$d year</item>
<item quantity="other">%1$d years</item>
</plurals>
<string name="event_sync_less_than_until">&lt;%1$s</string>
<string name="event_sync_status_connecting">Connecting</string>