feat(contacts): show online status in user list (#131)
This commit is contained in:
parent
c16c9fff97
commit
00b28d2f58
5 changed files with 66 additions and 4 deletions
|
|
@ -11,7 +11,6 @@ import androidx.compose.foundation.lazy.items
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Person
|
||||
import androidx.compose.material3.Badge
|
||||
import androidx.compose.material3.BadgedBox
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.CircularProgressIndicator
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
|
|
@ -83,8 +82,10 @@ internal fun UserListScreen(
|
|||
|
||||
@Composable
|
||||
private fun UserListItem(user: UserListItemDto, unreadCount: Int, onClick: () -> Unit) {
|
||||
val statusText = formatOnlineStatus(user.lastSeen)
|
||||
ListItem(
|
||||
headlineContent = { Text(user.username) },
|
||||
supportingContent = statusText?.let { { Text(it, style = MaterialTheme.typography.bodySmall) } },
|
||||
leadingContent = {
|
||||
Icon(imageVector = Icons.Outlined.Person, contentDescription = null)
|
||||
},
|
||||
|
|
@ -97,3 +98,23 @@ private fun UserListItem(user: UserListItemDto, unreadCount: Int, onClick: () ->
|
|||
)
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
private fun formatOnlineStatus(lastSeen: Long?): String? {
|
||||
if (lastSeen == null) return null
|
||||
val now = System.currentTimeMillis()
|
||||
if (now - lastSeen < ONLINE_THRESHOLD_MS) return "Verbunden"
|
||||
val dt = java.util.Date(lastSeen)
|
||||
val cal = java.util.Calendar.getInstance().apply { time = dt }
|
||||
val todayCal = java.util.Calendar.getInstance()
|
||||
val isToday = cal.get(java.util.Calendar.YEAR) == todayCal.get(java.util.Calendar.YEAR) &&
|
||||
cal.get(java.util.Calendar.DAY_OF_YEAR) == todayCal.get(java.util.Calendar.DAY_OF_YEAR)
|
||||
val timeFmt = java.text.SimpleDateFormat("HH:mm", java.util.Locale.GERMAN)
|
||||
return if (isToday) {
|
||||
"zuletzt online um ${timeFmt.format(dt)}"
|
||||
} else {
|
||||
val dateFmt = java.text.SimpleDateFormat("dd.MM.", java.util.Locale.GERMAN)
|
||||
"zuletzt online am ${dateFmt.format(dt)} um ${timeFmt.format(dt)}"
|
||||
}
|
||||
}
|
||||
|
||||
private const val ONLINE_THRESHOLD_MS = 60_000L
|
||||
|
|
|
|||
|
|
@ -5,10 +5,13 @@ import androidx.lifecycle.viewModelScope
|
|||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import de.bollwerk.app.domain.repository.MessageRepository
|
||||
import de.bollwerk.shared.model.UserListItemDto
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -29,8 +32,30 @@ internal class UserListViewModel @Inject constructor(
|
|||
val unreadCounts: StateFlow<Map<String, Int>> = messageRepository.getUnreadCountsBySender()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
||||
|
||||
private var pollingJob: Job? = null
|
||||
|
||||
init {
|
||||
loadUsers()
|
||||
startPolling()
|
||||
}
|
||||
|
||||
private fun startPolling() {
|
||||
pollingJob = viewModelScope.launch {
|
||||
while (isActive) {
|
||||
delay(POLL_INTERVAL_MS)
|
||||
refreshStatuses()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun refreshStatuses() {
|
||||
val result = messageRepository.fetchUsers()
|
||||
if (result.isSuccess) {
|
||||
val fresh = result.getOrDefault(emptyList())
|
||||
_uiState.value = _uiState.value.copy(users = fresh)
|
||||
} else {
|
||||
android.util.Log.w(TAG, "Online-Status-Polling fehlgeschlagen: ${result.exceptionOrNull()?.message}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadUsers() {
|
||||
|
|
@ -49,4 +74,9 @@ internal class UserListViewModel @Inject constructor(
|
|||
}
|
||||
|
||||
fun retry() = loadUsers()
|
||||
|
||||
private companion object {
|
||||
const val POLL_INTERVAL_MS = 30_000L
|
||||
const val TAG = "UserListViewModel"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,9 +25,14 @@ internal fun Route.userRoutes(userRepository: UserRepository, webSocketManager:
|
|||
HttpStatusCode.Unauthorized,
|
||||
ErrorResponse(status = 401, message = "Unauthorized")
|
||||
)
|
||||
val now = System.currentTimeMillis()
|
||||
val users = userRepository.listAll()
|
||||
.filter { it.id != principal.userId }
|
||||
.map { UserListItemDto(id = it.id, username = it.username) }
|
||||
.map { user ->
|
||||
val online = webSocketManager.isOnline(user.id)
|
||||
val lastSeen = if (online) now else webSocketManager.getLastSeen(user.id)
|
||||
UserListItemDto(id = user.id, username = user.username, lastSeen = lastSeen)
|
||||
}
|
||||
call.respond(HttpStatusCode.OK, users)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,17 +13,22 @@ import java.util.concurrent.CopyOnWriteArraySet
|
|||
internal class WebSocketManager {
|
||||
|
||||
private val sessions = ConcurrentHashMap<String, CopyOnWriteArraySet<WebSocketSession>>()
|
||||
private val lastSeenMap = ConcurrentHashMap<String, Long>()
|
||||
|
||||
fun addSession(userId: String, session: WebSocketSession) {
|
||||
sessions.getOrPut(userId) { CopyOnWriteArraySet() }.add(session)
|
||||
sessions.computeIfAbsent(userId) { CopyOnWriteArraySet() }.add(session)
|
||||
lastSeenMap[userId] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
fun removeSession(userId: String, session: WebSocketSession) {
|
||||
sessions[userId]?.remove(session)
|
||||
lastSeenMap[userId] = System.currentTimeMillis()
|
||||
}
|
||||
|
||||
fun isOnline(userId: String): Boolean = sessions[userId]?.isNotEmpty() == true
|
||||
|
||||
fun getLastSeen(userId: String): Long? = lastSeenMap[userId]
|
||||
|
||||
suspend fun notifyInventoryUpdated(userId: String, itemId: String) {
|
||||
val payload = buildJsonObject {
|
||||
put("type", "inventoryUpdated")
|
||||
|
|
|
|||
|
|
@ -5,5 +5,6 @@ import kotlinx.serialization.Serializable
|
|||
@Serializable
|
||||
data class UserListItemDto(
|
||||
val id: String,
|
||||
val username: String
|
||||
val username: String,
|
||||
val lastSeen: Long? = null
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue