193 lines
7.3 KiB
Kotlin
193 lines
7.3 KiB
Kotlin
package app.closer.ui.activity
|
|
|
|
import androidx.compose.foundation.layout.Arrangement
|
|
import androidx.compose.foundation.layout.Box
|
|
import androidx.compose.foundation.layout.Column
|
|
import androidx.compose.foundation.layout.fillMaxSize
|
|
import androidx.compose.foundation.layout.fillMaxWidth
|
|
import androidx.compose.foundation.layout.padding
|
|
import androidx.compose.foundation.Image
|
|
import androidx.compose.foundation.clickable
|
|
import androidx.compose.foundation.layout.size
|
|
import androidx.compose.foundation.lazy.LazyColumn
|
|
import androidx.compose.foundation.lazy.items
|
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
import androidx.compose.material3.MaterialTheme
|
|
import androidx.compose.material3.Text
|
|
import androidx.compose.runtime.Composable
|
|
import androidx.compose.runtime.LaunchedEffect
|
|
import androidx.compose.runtime.collectAsState
|
|
import androidx.compose.runtime.getValue
|
|
import androidx.compose.ui.Alignment
|
|
import androidx.compose.ui.Modifier
|
|
import androidx.compose.ui.text.font.FontWeight
|
|
import androidx.compose.ui.text.style.TextAlign
|
|
import androidx.compose.ui.unit.dp
|
|
import androidx.hilt.navigation.compose.hiltViewModel
|
|
import androidx.lifecycle.ViewModel
|
|
import androidx.lifecycle.viewModelScope
|
|
import app.closer.domain.model.ActivityItem
|
|
import app.closer.core.navigation.AppRoute
|
|
import app.closer.data.remote.FirestoreActivityDataSource
|
|
import androidx.compose.ui.res.painterResource
|
|
import app.closer.R
|
|
import app.closer.ui.components.BrandIllustration
|
|
import app.closer.domain.repository.AuthRepository
|
|
import app.closer.ui.components.CloserCard
|
|
import app.closer.ui.components.CloserHeartLoader
|
|
import app.closer.ui.settings.SettingsSubpage
|
|
import app.closer.ui.theme.CloserPalette
|
|
import app.closer.ui.theme.closerCardColor
|
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
import kotlinx.coroutines.flow.MutableStateFlow
|
|
import kotlinx.coroutines.flow.StateFlow
|
|
import kotlinx.coroutines.flow.asStateFlow
|
|
import kotlinx.coroutines.flow.update
|
|
import kotlinx.coroutines.launch
|
|
import java.text.DateFormat
|
|
import java.util.Date
|
|
import javax.inject.Inject
|
|
|
|
data class ActivityUiState(
|
|
val isLoading: Boolean = true,
|
|
val items: List<ActivityItem> = emptyList()
|
|
)
|
|
|
|
@HiltViewModel
|
|
class ActivityViewModel @Inject constructor(
|
|
private val authRepository: AuthRepository,
|
|
private val activityDataSource: FirestoreActivityDataSource
|
|
) : ViewModel() {
|
|
|
|
private val _uiState = MutableStateFlow(ActivityUiState())
|
|
val uiState: StateFlow<ActivityUiState> = _uiState.asStateFlow()
|
|
|
|
init {
|
|
val uid = authRepository.currentUserId
|
|
if (uid == null) {
|
|
_uiState.update { it.copy(isLoading = false) }
|
|
} else {
|
|
viewModelScope.launch {
|
|
activityDataSource.observeActivity(uid).collect { items ->
|
|
_uiState.update { it.copy(isLoading = false, items = items) }
|
|
}
|
|
}
|
|
// Opening the feed clears the unread badge — best effort.
|
|
viewModelScope.launch { runCatching { activityDataSource.markAllRead(uid) } }
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
fun ActivityScreen(
|
|
onNavigate: (String) -> Unit = {},
|
|
viewModel: ActivityViewModel = hiltViewModel()
|
|
) {
|
|
val state by viewModel.uiState.collectAsState()
|
|
|
|
SettingsSubpage(title = "Together", onBack = { onNavigate("back") }) { padding ->
|
|
if (state.isLoading) {
|
|
Box(modifier = Modifier.fillMaxSize().padding(padding), contentAlignment = Alignment.Center) {
|
|
CloserHeartLoader()
|
|
}
|
|
} else if (state.items.isEmpty()) {
|
|
ActivityEmptyState(modifier = Modifier.padding(padding))
|
|
} else {
|
|
LazyColumn(
|
|
modifier = Modifier
|
|
.fillMaxSize()
|
|
.padding(padding)
|
|
.padding(horizontal = 20.dp),
|
|
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
) {
|
|
items(state.items, key = { it.id }) { item ->
|
|
val route = routeForActivityType(item.type)
|
|
ActivityRow(item, onClick = route?.let { { onNavigate(it) } })
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Maps an activity item's type to the relevant hub to open on tap. Affection/reminder items
|
|
* (thinking_of_you, gentle_reminder, streak, reengagement, …) carry no deeper target, so they
|
|
* return null and the row stays non-tappable.
|
|
*/
|
|
private fun routeForActivityType(type: String): String? {
|
|
val t = type.lowercase()
|
|
return when {
|
|
"message" in t || "chat" in t -> AppRoute.MESSAGES
|
|
"game" in t -> AppRoute.PLAY
|
|
"capsule" in t -> AppRoute.MEMORY_LANE
|
|
"challenge" in t -> AppRoute.CONNECTION_CHALLENGES
|
|
"date" in t -> AppRoute.DATE_MATCHES
|
|
"answer" in t || "reveal" in t || "question" in t || "daily" in t -> AppRoute.DAILY_QUESTION
|
|
else -> null
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ActivityRow(item: ActivityItem, onClick: (() -> Unit)? = null) {
|
|
CloserCard(
|
|
modifier = Modifier
|
|
.fillMaxWidth()
|
|
.let { if (onClick != null) it.clickable(onClick = onClick) else it },
|
|
containerColor = if (item.read) closerCardColor() else CloserPalette.PurpleSoft
|
|
) {
|
|
Column(modifier = Modifier.padding(16.dp)) {
|
|
Text(
|
|
text = item.title.ifBlank { "Shared moment" },
|
|
style = MaterialTheme.typography.titleSmall,
|
|
fontWeight = FontWeight.SemiBold,
|
|
color = MaterialTheme.colorScheme.onSurface
|
|
)
|
|
if (item.body.isNotBlank()) {
|
|
Text(
|
|
text = item.body,
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
}
|
|
if (item.createdAt > 0L) {
|
|
Text(
|
|
text = DateFormat.getDateInstance(DateFormat.MEDIUM).format(Date(item.createdAt)),
|
|
style = MaterialTheme.typography.labelSmall,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
|
)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Composable
|
|
private fun ActivityEmptyState(modifier: Modifier = Modifier) {
|
|
Box(modifier = modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
|
|
Column(
|
|
modifier = Modifier.padding(32.dp),
|
|
horizontalAlignment = Alignment.CenterHorizontally
|
|
) {
|
|
BrandIllustration(
|
|
res = R.drawable.illustration_together_empty,
|
|
contentDescription = null,
|
|
modifier = Modifier.size(180.dp)
|
|
)
|
|
Text(
|
|
text = "Your story, together",
|
|
style = MaterialTheme.typography.titleMedium,
|
|
fontWeight = FontWeight.SemiBold,
|
|
color = MaterialTheme.colorScheme.onSurface,
|
|
textAlign = TextAlign.Center,
|
|
modifier = Modifier.padding(top = 16.dp)
|
|
)
|
|
Text(
|
|
text = "Every answer you reveal, every game you play, every little milestone — they'll gather here, just for the two of you.",
|
|
style = MaterialTheme.typography.bodyMedium,
|
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
textAlign = TextAlign.Center,
|
|
modifier = Modifier.padding(top = 6.dp)
|
|
)
|
|
}
|
|
}
|
|
}
|