From 7f5baab5df12ba3aeb26384af3ec47ac553d20fb Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Tue, 17 Mar 2026 13:58:07 -0400 Subject: [PATCH] Adds last seen to the user profile --- .../amethyst/model/LocalCache.kt | 2 +- .../datasource/FilterUserProfileLastSeen.kt | 44 +++++++++++++++++++ .../UserProfileMetadataFilterSubAssembler.kt | 1 + .../profile/header/DrawAdditionalInfo.kt | 36 +++++++++++++++ amethyst/src/main/res/values/strings.xml | 1 + 5 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLastSeen.kt diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt index 923e7a91b..cac8b32c9 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/model/LocalCache.kt @@ -325,7 +325,7 @@ object LocalCache : ILocalCache, ICacheProvider { newFilter.init() - observables.put(newFilter, newFilter) + observables[newFilter] = newFilter awaitClose { observables.remove(newFilter) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLastSeen.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLastSeen.kt new file mode 100644 index 000000000..c37e0acc4 --- /dev/null +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/FilterUserProfileLastSeen.kt @@ -0,0 +1,44 @@ +/* + * Copyright (c) 2025 Vitor Pamplona + * + * Permission is hereby granted, free of charge, to any person obtaining a copy of + * this software and associated documentation files (the "Software"), to deal in + * the Software without restriction, including without limitation the rights to use, + * copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the + * Software, and to permit persons to whom the Software is furnished to do so, + * subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS + * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR + * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN + * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION + * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +package com.vitorpamplona.amethyst.ui.screen.loggedIn.profile.datasource + +import com.vitorpamplona.amethyst.service.relays.SincePerRelayMap +import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter +import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl + +fun filterUserProfileLastSeen( + users: Map>, + since: SincePerRelayMap?, +): List { + if (users.isEmpty()) return emptyList() + + return users.map { + RelayBasedFilter( + relay = it.key, + filter = + Filter( + authors = it.value.sorted(), + since = since?.get(it.key)?.time, + ), + ) + } +} diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt index 591010081..e6d21c933 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/ui/screen/loggedIn/profile/datasource/UserProfileMetadataFilterSubAssembler.kt @@ -52,6 +52,7 @@ class UserProfileMetadataFilterSubAssembler( return listOfNotNull( filterUserProfileMetadata(userPerRelay, since), filterUserProfileLists(userPerRelay, since), + filterUserProfileLastSeen(userPerRelay, since), ).flatten() } 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 202a59ebc..d88b0fb95 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 @@ -45,6 +45,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.ClipEntry import androidx.compose.ui.platform.LocalClipboard +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight @@ -65,6 +66,7 @@ import com.vitorpamplona.amethyst.ui.navigation.navs.INav import com.vitorpamplona.amethyst.ui.navigation.routes.Route import com.vitorpamplona.amethyst.ui.note.DrawPlayName import com.vitorpamplona.amethyst.ui.note.ObserveAndRenderNIP05VerifiedSymbol +import com.vitorpamplona.amethyst.ui.note.timeAgo import com.vitorpamplona.amethyst.ui.note.toShortDisplay import com.vitorpamplona.amethyst.ui.painterRes import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel @@ -78,11 +80,16 @@ import com.vitorpamplona.amethyst.ui.theme.SpacedBy3dp import com.vitorpamplona.amethyst.ui.theme.SpacedBy5dp import com.vitorpamplona.amethyst.ui.theme.StdHorzSpacer import com.vitorpamplona.amethyst.ui.theme.placeholderText +import com.vitorpamplona.quartz.nip01Core.core.Event +import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip39ExtIdentities.GitHubIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.IdentityClaimTag import com.vitorpamplona.quartz.nip39ExtIdentities.MastodonIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TelegramIdentity import com.vitorpamplona.quartz.nip39ExtIdentities.TwitterIdentity +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch private const val IDENTITY_ICON_CACHE_KEY = 0 @@ -200,6 +207,8 @@ fun DrawAdditionalInfo( } } + DisplayLastSeen(baseUser, accountViewModel) + DisplayNip05ProfileStatus(baseUser, accountViewModel) val lud16 = @@ -281,6 +290,33 @@ fun DrawAdditionalInfo( } } +@Composable +fun DisplayLastSeen( + user: User, + accountViewModel: AccountViewModel, +) { + val lastSeenFlow = + remember { + accountViewModel.account.cache + .observeLatestEvent(Filter(authors = listOf(user.pubkeyHex))) + .map { + it?.createdAt + }.flowOn(Dispatchers.IO) + } + + val lastSeen by lastSeenFlow.collectAsStateWithLifecycle(null) + + lastSeen?.let { timestamp -> + val context = LocalContext.current + Text( + text = stringRes(R.string.last_seen, timeAgo(timestamp, context)), + color = MaterialTheme.colorScheme.placeholderText, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + ) + } +} + @Composable fun DisplayNip05ProfileStatus( user: User, diff --git a/amethyst/src/main/res/values/strings.xml b/amethyst/src/main/res/values/strings.xml index 8671d48d2..c87d1195d 100644 --- a/amethyst/src/main/res/values/strings.xml +++ b/amethyst/src/main/res/values/strings.xml @@ -1816,4 +1816,5 @@ DMs profiles relay settings + Last seen %1$s ago