Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@
<string name="session_app_clinic_apply">Apply for App-Clinic</string>

<string name="talk_details">Talk Details</string>
<string name="talks">Talks</string>
<string name="about_this_talk">About this talk</string>
<plurals name="speaker_section">
<item quantity="one">Speaker</item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ val viewModelModule = module {
viewModelOf(::SpeakerListViewModel)
viewModelOf(::SponsorsViewModel)
viewModelOf(::VenueViewModel)
viewModel { (speakerId: String) -> SpeakerDetailsViewModel(speakerId, get()) }
viewModel { (speakerId: String) -> SpeakerDetailsViewModel(speakerId, get(), get()) }
viewModelOf(::AgendaViewModel)
viewModel { (sessionId: String) ->
SessionDetailViewModel(sessionId, get(), get(), get(), get(), get(), get(), get())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ private fun AVANavDisplay(
SpeakerDetailsRoute(
speakerDetailsViewModel = koinViewModel(key = key.speakerId) { parametersOf(key.speakerId) },
onBackClick = { navigator.goBack() },
onSessionClick = { sessionId -> navigator.navigateToSessionDetail(sessionId) },
sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = LocalNavAnimatedContentScope.current,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,35 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.androidmakers.ui.model.Lce
import com.androidmakers.ui.model.toLce
import fr.androidmakers.domain.model.Session
import fr.androidmakers.domain.model.SocialsItem
import fr.androidmakers.domain.model.Speaker
import fr.androidmakers.domain.repo.SessionsRepository
import fr.androidmakers.domain.repo.SpeakersRepository
import fr.androidmakers.domain.utils.UrlOpener
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.stateIn

class SpeakerDetailsViewModel(
speakerId: String,
speakersRepository: SpeakersRepository,
sessionsRepository: SessionsRepository,
) : ViewModel() {

val uiState: StateFlow<Lce<SpeakerDetailsUiState>> = speakersRepository
.getSpeaker(speakerId).map { result ->
result.map {
SpeakerDetailsUiState(
speaker = it
)
}.toLce()
}
val uiState: StateFlow<Lce<SpeakerDetailsUiState>> = combine(
speakersRepository.getSpeaker(speakerId),
sessionsRepository.getSessions(refresh = false),
) { speakerResult, sessionsResult ->
speakerResult.map { speaker ->
SpeakerDetailsUiState(
speaker = speaker,
sessions = sessionsResult.getOrDefault(emptyList())
.filter { session -> speakerId in session.speakers }
)
}.toLce()
Comment on lines +28 to +34
Copy link

Copilot AI Apr 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sessionsResult.getOrDefault(emptyList()) swallows session-loading failures and makes them indistinguishable from a real “speaker has no talks” case (the talks section will simply be hidden). Consider propagating the failure into the Lce (or representing sessions as their own Lce in SpeakerDetailsUiState) so the UI can differentiate between “no talks” vs “failed to load talks” and potentially offer retry.

Suggested change
speakerResult.map { speaker ->
SpeakerDetailsUiState(
speaker = speaker,
sessions = sessionsResult.getOrDefault(emptyList())
.filter { session -> speakerId in session.speakers }
)
}.toLce()
speakerResult.fold(
onSuccess = { speaker ->
sessionsResult.map { sessions ->
SpeakerDetailsUiState(
speaker = speaker,
sessions = sessions.filter { session -> speakerId in session.speakers }
)
}
},
onFailure = { throwable -> Result.failure(throwable) }
).toLce()

Copilot uses AI. Check for mistakes.
}
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5_000),
Expand All @@ -38,5 +45,6 @@ class SpeakerDetailsViewModel(
}

data class SpeakerDetailsUiState(
val speaker: Speaker
val speaker: Speaker,
val sessions: List<Session> = emptyList(),
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ import androidx.compose.animation.SharedTransitionScope
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ElevatedCard
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand All @@ -30,6 +32,7 @@ import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.Hyphens
import androidx.compose.ui.text.style.LineBreak
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
Expand All @@ -39,11 +42,16 @@ import com.androidmakers.ui.common.SocialButtons
import com.androidmakers.ui.common.toUrlOpener
import com.androidmakers.ui.model.Lce
import com.androidmakers.ui.theme.LocalIsNeobrutalism
import com.androidmakers.ui.theme.neoBrutalElevation
import fr.androidmakers.domain.model.Session
import fr.androidmakers.domain.model.SocialsItem
import fr.androidmakers.domain.model.Speaker
import fr.paug.androidmakers.ui.Res
import fr.paug.androidmakers.ui.ic_arrow_back
import fr.paug.androidmakers.ui.back
import fr.paug.androidmakers.ui.speakers
import fr.paug.androidmakers.ui.talks
import kotlinx.datetime.LocalDateTime
import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource

Expand All @@ -52,6 +60,7 @@ import org.jetbrains.compose.resources.stringResource
fun SpeakerDetailsRoute(
speakerDetailsViewModel: SpeakerDetailsViewModel,
onBackClick: () -> Unit,
onSessionClick: (sessionId: String) -> Unit = {},
sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null,
) {
Expand All @@ -68,6 +77,7 @@ fun SpeakerDetailsRoute(
uiState = state.content,
onSocialItemClick = { speakerDetailsViewModel.openSpeakerLink(urlOpener, it) },
onBackClick = onBackClick,
onSessionClick = onSessionClick,
sharedTransitionScope = sharedTransitionScope,
animatedVisibilityScope = animatedVisibilityScope,
)
Expand All @@ -81,6 +91,7 @@ fun SpeakerDetailsScreen(
uiState: SpeakerDetailsUiState,
onSocialItemClick: (SocialsItem) -> Unit,
onBackClick: () -> Unit,
onSessionClick: (sessionId: String) -> Unit = {},
sharedTransitionScope: SharedTransitionScope? = null,
animatedVisibilityScope: AnimatedVisibilityScope? = null,
) {
Expand Down Expand Up @@ -162,6 +173,105 @@ fun SpeakerDetailsScreen(
speaker = speaker,
onClickOnItem = onSocialItemClick
)

if (uiState.sessions.isNotEmpty()) {
SpeakerTalksSection(
sessions = uiState.sessions,
onSessionClick = onSessionClick,
)
}
}
}
}

@Composable
private fun SpeakerTalksSection(
sessions: List<Session>,
onSessionClick: (sessionId: String) -> Unit,
) {
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
Text(
text = stringResource(Res.string.talks),
style = MaterialTheme.typography.titleMedium,
)
sessions.forEach { session ->
ElevatedCard(
modifier = Modifier.fillMaxWidth().neoBrutalElevation(),
onClick = { onSessionClick(session.id) },
shape = MaterialTheme.shapes.large,
) {
Text(
modifier = Modifier.padding(16.dp),
text = session.title,
style = MaterialTheme.typography.bodyLarge,
)
}
}
}
}

// region Previews

private val previewSpeaker = Speaker(
id = "speaker-1",
name = "Ada Lovelace",
company = "Babbage & Co.",
bio = "Mathematician and writer, chiefly known for her work on Charles Babbage's proposed mechanical general-purpose computer, the Analytical Engine.",
photoUrl = null,
socials = listOf(
SocialsItem(name = "Twitter", url = "https://twitter.com/ada"),
),
)

private val previewSessions = listOf(
Session(
id = "session-1",
title = "The First Algorithm: A Deep Dive into Analytical Engine Programming",
speakers = listOf("speaker-1"),
startsAt = LocalDateTime(2026, 4, 9, 9, 0),
endsAt = LocalDateTime(2026, 4, 9, 9, 45),
roomId = "room-1",
isServiceSession = false,
type = "talk",
),
Session(
id = "session-2",
title = "Beyond Numbers: Mathematical Imagination in the 19th Century",
speakers = listOf("speaker-1"),
startsAt = LocalDateTime(2026, 4, 10, 11, 0),
endsAt = LocalDateTime(2026, 4, 10, 11, 45),
roomId = "room-2",
isServiceSession = false,
type = "talk",
),
)

@Preview
@Composable
private fun SpeakerDetailsScreenPreview() {
SpeakerDetailsScreen(
uiState = SpeakerDetailsUiState(
speaker = previewSpeaker,
sessions = previewSessions,
),
onSocialItemClick = {},
onBackClick = {},
onSessionClick = {},
)
}

@Preview
@Composable
private fun SpeakerDetailsScreenNoTalksPreview() {
SpeakerDetailsScreen(
uiState = SpeakerDetailsUiState(
speaker = previewSpeaker,
sessions = emptyList(),
),
onSocialItemClick = {},
onBackClick = {},
onSessionClick = {},
)
}

// endregion