Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

implement Speakers screen #243

Merged
merged 7 commits into from
Feb 13, 2025
Merged
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
1 change: 1 addition & 0 deletions shared/src/commonMain/composeResources/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -118,4 +118,5 @@ We will process your data in accordance with the App Privacy Policy. You can adj

<string name="news_feed_title">News</string>

<string name="speakers_title">Speakers</string>
</resources>
2 changes: 2 additions & 0 deletions shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.jetbrains.kotlinconf.screens.PrivacyPolicyViewModel
import org.jetbrains.kotlinconf.screens.ScheduleViewModel
import org.jetbrains.kotlinconf.screens.SessionViewModel
import org.jetbrains.kotlinconf.screens.SettingsViewModel
import org.jetbrains.kotlinconf.screens.SpeakersViewModel
import org.jetbrains.kotlinconf.screens.StartNotificationsViewModel
import org.jetbrains.kotlinconf.storage.ApplicationStorage
import org.jetbrains.kotlinconf.storage.MultiplatformSettingsStorage
Expand Down Expand Up @@ -80,6 +81,7 @@ private fun koinConfiguration(context: ApplicationContext) = koinConfiguration {
viewModelOf(::NewsListViewModel)
viewModelOf(::StartNotificationsViewModel)
viewModelOf(::NewsDetailViewModel)
viewModelOf(::SpeakersViewModel)
}

modules(appModule, viewModelModule)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import org.jetbrains.kotlinconf.SpeakerId
import org.jetbrains.kotlinconf.ui.components.DayHeader
import org.jetbrains.kotlinconf.ui.components.Divider
import org.jetbrains.kotlinconf.ui.components.PageMenuItem
import org.jetbrains.kotlinconf.ui.components.SpeakerCard
import org.jetbrains.kotlinconf.ui.components.StyledText
import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme

Expand Down Expand Up @@ -195,7 +196,7 @@ private fun Event(

for (speaker in speakers) {
key(speaker.id) {
org.jetbrains.kotlinconf.ui.components.Speaker(speaker.name, speaker.description, speaker.photoUrl, modifier = Modifier.padding(horizontal = 12.dp))
SpeakerCard(speaker.name, speaker.description, speaker.photoUrl, modifier = Modifier.padding(horizontal = 12.dp))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import org.jetbrains.kotlinconf.navigation.PartnersScreen
import org.jetbrains.kotlinconf.navigation.PrivacyPolicyScreen
import org.jetbrains.kotlinconf.navigation.ScheduleScreen
import org.jetbrains.kotlinconf.navigation.SessionScreen
import org.jetbrains.kotlinconf.navigation.SpeakerDetailsScreen
import org.jetbrains.kotlinconf.navigation.SpeakersScreen
import org.jetbrains.kotlinconf.ui.components.Divider
import org.jetbrains.kotlinconf.ui.components.MainNavDestination
Expand Down Expand Up @@ -79,8 +80,8 @@ fun MainScreen(
)
}
composable<SpeakersScreen> {
Speakers(
onBack = rootNavController::popBackStack
SpeakersScreen(
onSpeaker = { rootNavController.navigate(SpeakerDetailsScreen(it)) }
)
}
composable<ScheduleScreen> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ import org.jetbrains.kotlinconf.ui.components.FeedbackForm
import org.jetbrains.kotlinconf.ui.components.KodeeIconLarge
import org.jetbrains.kotlinconf.ui.components.MainHeaderTitleBar
import org.jetbrains.kotlinconf.ui.components.PageTitle
import org.jetbrains.kotlinconf.ui.components.Speaker
import org.jetbrains.kotlinconf.ui.components.SpeakerCard
import org.jetbrains.kotlinconf.ui.components.StyledText
import org.jetbrains.kotlinconf.ui.components.TopMenuButton
import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme
Expand Down Expand Up @@ -125,7 +125,7 @@ fun SessionScreen(
}

speakers.forEach { speaker ->
Speaker(
SpeakerCard(
name = speaker.name,
title = speaker.position,
photoUrl = speaker.photoUrl,
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package org.jetbrains.kotlinconf.screens

import androidx.compose.foundation.clickable
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.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.unit.dp
import kotlinconfapp.shared.generated.resources.Res
import kotlinconfapp.shared.generated.resources.speakers_title
import kotlinconfapp.ui_components.generated.resources.main_header_search_hint
import kotlinconfapp.ui_components.generated.resources.search_24
import org.jetbrains.compose.resources.stringResource
import org.jetbrains.kotlinconf.SpeakerId
import org.jetbrains.kotlinconf.ui.components.Divider
import org.jetbrains.kotlinconf.ui.components.MainHeaderContainer
import org.jetbrains.kotlinconf.ui.components.MainHeaderContainerState
import org.jetbrains.kotlinconf.ui.components.MainHeaderSearchBar
import org.jetbrains.kotlinconf.ui.components.MainHeaderTitleBar
import org.jetbrains.kotlinconf.ui.components.SpeakerCard
import org.jetbrains.kotlinconf.ui.components.TopMenuButton
import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme
import org.koin.compose.viewmodel.koinViewModel
import kotlinconfapp.ui_components.generated.resources.Res as UiRes

@Composable
fun SpeakersScreen(
onSpeaker: (SpeakerId) -> Unit,
viewModel: SpeakersViewModel = koinViewModel()
) {
var searchState by rememberSaveable { mutableStateOf(MainHeaderContainerState.Title) }
var searchText by rememberSaveable { mutableStateOf("") }
val speakers by viewModel.speakers.collectAsState()

LaunchedEffect(searchText) {
viewModel.setSearchText(searchText)
}

Column(Modifier.fillMaxSize()) {
MainHeaderContainer(
state = searchState,
titleContent = {
MainHeaderTitleBar(
title = stringResource(Res.string.speakers_title),
endContent = {
TopMenuButton(
icon = UiRes.drawable.search_24,
onClick = { searchState = MainHeaderContainerState.Search },
contentDescription = stringResource(UiRes.string.main_header_search_hint)
)
}
)
},
searchContent = {
MainHeaderSearchBar(
searchValue = searchText,
onSearchValueChange = { searchText = it },
onClose = {
searchState = MainHeaderContainerState.Title
searchText = ""
},
onClear = { searchText = "" }
)
}
)

Divider(1.dp, KotlinConfTheme.colors.strokePale)

LazyColumn(Modifier.fillMaxSize()) {
items(speakers) { speaker ->
SpeakerCard(
name = speaker.name,
nameHighlights = speaker.nameHighlights,
title = speaker.position,
titleHighlights = speaker.titleHighlights,
photoUrl = speaker.photoUrl,
modifier = Modifier
.fillMaxWidth()
.padding(12.dp)
.clip(RoundedCornerShape(8.dp))
.clickable { onSpeaker(speaker.id) }
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.jetbrains.kotlinconf.screens

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.stateIn
import org.jetbrains.kotlinconf.ConferenceService
import org.jetbrains.kotlinconf.Speaker
import org.jetbrains.kotlinconf.SpeakerId
import org.jetbrains.kotlinconf.utils.containsDiacritics
import org.jetbrains.kotlinconf.utils.removeDiacritics

data class SpeakerWithHighlights(
val speaker: Speaker,
val nameHighlights: List<IntRange>,
val titleHighlights: List<IntRange>,
) {
val id: SpeakerId get() = speaker.id
val name: String get() = speaker.name
val position: String get() = speaker.position
val photoUrl: String get() = speaker.photoUrl
}

class SpeakersViewModel(
service: ConferenceService,
) : ViewModel() {
private var searchText = MutableStateFlow("")

fun setSearchText(searchText: String) {
this.searchText.value = searchText
}

val speakers = combine(service.speakers, searchText) { speakers, searchText ->
if (searchText.isBlank()) {
speakers.all.map { SpeakerWithHighlights(it, emptyList(), emptyList()) }
} else {
speakers.all.mapNotNull {
// Look for exact matches if diacritics are present, ignore all diacritics otherwise
val diacriticsSearch = searchText.containsDiacritics()
val targetName = if (diacriticsSearch) it.name else it.name.removeDiacritics()
val targetPosition = if (diacriticsSearch) it.position else it.position.removeDiacritics()
val searchPattern = searchText.toRegex(RegexOption.IGNORE_CASE)

val nameMatches = searchPattern.findAll(targetName).map { it.range }.toList()
val titleMatches = searchPattern.findAll(targetPosition).map { it.range }.toList()

if (nameMatches.isNotEmpty() || titleMatches.isNotEmpty()) {
SpeakerWithHighlights(it, nameMatches, titleMatches)
} else {
null
}
}
}
}
.flowOn(Dispatchers.Default)
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ fun GalleryApp() {
SectionTitlePreview()
ServiceEventsPreview()
SettingsItemPreview()
SpeakerPreview()
SpeakerCardPreview()
SwitcherItemPreview()
SwitcherPreview()
TalkCardPreview()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
Expand All @@ -16,7 +15,6 @@ import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import kotlinconfapp.ui_components.generated.resources.Res
import kotlinconfapp.ui_components.generated.resources.kodee_emotion_negative
import kotlinconfapp.ui_components.generated.resources.kodee_emotion_neutral
import kotlinconfapp.ui_components.generated.resources.kodee_emotion_positive
import org.jetbrains.compose.resources.painterResource
Expand All @@ -25,11 +23,13 @@ import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme
import org.jetbrains.kotlinconf.ui.theme.PreviewHelper

@Composable
fun Speaker(
fun SpeakerCard(
name: String,
title: String,
photoUrl: String,
modifier: Modifier = Modifier,
nameHighlights: List<IntRange> = emptyList(),
titleHighlights: List<IntRange> = emptyList(),
) {
Row(
modifier = modifier,
Expand All @@ -49,13 +49,13 @@ fun Speaker(
)
Column {
StyledText(
text = name,
text = buildHighlightedString(name, nameHighlights),
style = KotlinConfTheme.typography.h3,
color = KotlinConfTheme.colors.primaryText,
)
Spacer(modifier = Modifier.size(6.dp))
StyledText(
text = title,
text = buildHighlightedString(title, titleHighlights),
style = KotlinConfTheme.typography.text2,
color = KotlinConfTheme.colors.secondaryText,
)
Expand All @@ -65,16 +65,18 @@ fun Speaker(

@Preview
@Composable
internal fun SpeakerPreview() {
internal fun SpeakerCardPreview() {
PreviewHelper {
Speaker(
SpeakerCard(
name = "John Doe",
title = "Whatever Role Name at That Company",
photoUrl = "https://example.com/not-an-image.jpg",
)
Speaker(
SpeakerCard(
name = "John Doe",
nameHighlights = listOf(0..3), // Highlight "John"
title = "Whatever Role Name at That Company",
titleHighlights = listOf(9..12), // Highlight "Role"
photoUrl = "https://sessionize.com/image/2e2f-0o0o0-XGxKBoqZvxxQxosrZHQHTT.png?download=sebastian-aigner.png",
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ import org.jetbrains.kotlinconf.ui.theme.KotlinConfTheme
import org.jetbrains.kotlinconf.ui.theme.PreviewHelper

@Composable
private fun buildHighlightedString(
internal fun buildHighlightedString(
text: String,
highlights: List<IntRange>,
): AnnotatedString = buildAnnotatedString {
Expand Down