Skip to content

Adding feed feature files. #581

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

Merged
merged 6 commits into from
Dec 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
junit = { module = "junit:junit", version.ref = "junit" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koin" }
koin-compose-viewmodel = { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koin" }
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
kotlin-serialization = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin" }
Expand Down
2 changes: 2 additions & 0 deletions shared/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ kotlin {
implementation(libs.androidx.lifecycle.viewmodel)
implementation(libs.cketti.codepoints)
implementation(libs.koin.core)
implementation(libs.koin.compose)
implementation(libs.koin.compose.viewmodel)
implementation(libs.kotlinx.datetime)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.ktor.client.core)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.adammcneilly.pocketleague.shared.app.domain.usecases

import com.adammcneilly.pocketleague.shared.app.core.datetime.TimeProvider
import com.adammcneilly.pocketleague.shared.app.core.models.Event
import kotlinx.coroutines.flow.Flow

/**
* Return an observable type of events that are currently happening on today's date.
*/
class GetOngoingEventsUseCase(
private val eventRepository: EventRepository,
private val timeProvider: TimeProvider,
) {
/**
* @see [GetOngoingEventsUseCase]
*/
fun invoke(): Flow<List<Event>> {
val request = EventListRequest.OnDate(
dateUtc = timeProvider.now(),
)

return eventRepository.stream(request)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.adammcneilly.pocketleague.shared.app.domain.usecases

import com.adammcneilly.pocketleague.shared.app.core.datetime.TimeProvider
import com.adammcneilly.pocketleague.shared.app.core.models.Match
import com.adammcneilly.pocketleague.shared.app.feature.feed.usecases.DAYS_PER_WEEK
import kotlinx.coroutines.flow.Flow

/**
* Return an observable type of matches for the past week.
*/
class GetPastWeeksMatchesUseCase(
private val timeProvider: TimeProvider,
private val matchRepository: MatchRepository,
) {
/**
* @see [GetPastWeeksMatchesUseCase].
*/
fun invoke(): Flow<List<Match>> {
val request = MatchListRequest.DateRange(
startDateUTC = timeProvider.daysAgo(DAYS_PER_WEEK),
endDateUTC = timeProvider.now(),
)

return matchRepository.stream(request)
}

companion object {
private const val DAYS_PER_WEEK = 7
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.adammcneilly.pocketleague.shared.app.domain.usecases

import com.adammcneilly.pocketleague.shared.app.core.datetime.TimeProvider
import com.adammcneilly.pocketleague.shared.app.core.models.Event
import kotlinx.coroutines.flow.Flow

/**
* Return an observable type of events that are upcoming from today's date.
*/
class GetUpcomingEventsUseCase(
private val eventRepository: EventRepository,
private val timeProvider: TimeProvider,
) {
/**
* @see [GetUpcomingEventsUseCase]
*/
fun invoke(): Flow<List<Event>> {
val request = EventListRequest.AfterDate(
dateUtc = timeProvider.now(),
)

return eventRepository.stream(request)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
package com.adammcneilly.pocketleague.shared.app.feature.feed

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.adammcneilly.pocketleague.shared.app.core.displaymodels.EventGroupDisplayModel
import com.adammcneilly.pocketleague.shared.app.core.displaymodels.MatchDetailDisplayModel

/**
* The main list of events and matches to show within the feed screen
* that is the landing page when opening the app.
*/
@Composable
fun FeedContent(
recentMatches: List<MatchDetailDisplayModel>,
ongoingEvents: List<EventGroupDisplayModel>,
upcomingEvents: List<EventGroupDisplayModel>,
onMatchClicked: (String) -> Unit,
onEventClicked: (String) -> Unit,
modifier: Modifier = Modifier,
) {
LazyColumn(
modifier = modifier,
contentPadding = PaddingValues(
vertical = PocketLeagueTheme.sizes.screenPadding,
),
verticalArrangement = Arrangement.spacedBy(PocketLeagueTheme.sizes.listItemSpacing),
) {
recentMatchesHeader()

recentMatchesCarousel(recentMatches, onMatchClicked)

happeningNowHeader()

eventGroupList(ongoingEvents, onEventClicked)

upcomingHeader()

eventGroupList(upcomingEvents, onEventClicked)
}
}

private fun LazyListScope.upcomingHeader() {
item {
FeedSectionHeader(
text = "Upcoming",
)
}
}

private fun LazyListScope.eventGroupList(
events: List<EventGroupDisplayModel>,
onEventClicked: (String) -> Unit,
) {
events.forEach { group ->
item {
FeedEventGroup(
group,
onEventClicked,
)
}
}
}

private fun LazyListScope.happeningNowHeader() {
item {
FeedSectionHeader(
text = "Happening Now",
)
}
}

private fun LazyListScope.recentMatchesCarousel(
recentMatches: List<MatchDetailDisplayModel>,
onMatchClicked: (Match.Id) -> Unit,
) {
item {
MatchCarousel(
matches = recentMatches,
contentPadding = PaddingValues(
horizontal = PocketLeagueTheme.sizes.screenPadding,
),
onMatchClicked = onMatchClicked,
)
}
}

private fun LazyListScope.recentMatchesHeader() {
item {
FeedSectionHeader(
text = "Recent Matches",
)
}
}

@Composable
private fun FeedSectionHeader(
text: String,
modifier: Modifier = Modifier,
) {
Text(
text = text,
style = MaterialTheme.typography.headlineSmall,
modifier = modifier
.screenHorizontalPadding(),
)
}

/**
* For a given [displayModel], determine how to render
* that collection for our [FeedContent].
*/
@Composable
private fun FeedEventGroup(
displayModel: EventGroupDisplayModel,
onEventClicked: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val groupModifier = modifier
.screenHorizontalPadding()

when (displayModel) {
is EventGroupDisplayModel.Regionals -> {
EventSummaryListCard(
events = displayModel.events,
onEventClicked = onEventClicked,
modifier = groupModifier,
)
}

is EventGroupDisplayModel.Major -> {
LanEventSummaryCard(
event = displayModel.event,
onEventClicked = onEventClicked,
modifier = groupModifier,
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.adammcneilly.pocketleague.shared.app.feature.feed

import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Modifier
import org.koin.compose.viewmodel.koinViewModel

@Composable
fun FeedScreen(
modifier: Modifier = Modifier,
viewModel: FeedViewModel = koinViewModel(),
) {
val state = viewModel.state.collectAsState()

FeedContent(
recentMatches = state.value.recentMatches,
ongoingEvents = state.value.ongoingEvents,
upcomingEvents = state.value.upcomingEvents,
onMatchClicked = { /*TODO*/ },
onEventClicked = { /*TODO*/ },
modifier = modifier,
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package com.adammcneilly.pocketleague.shared.app.feature.feed

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.adammcneilly.pocketleague.shared.app.core.datetime.DateTimeFormatter
import com.adammcneilly.pocketleague.shared.app.core.datetime.TimeProvider
import com.adammcneilly.pocketleague.shared.app.core.displaymodels.EventGroupDisplayModel
import com.adammcneilly.pocketleague.shared.app.core.displaymodels.EventSummaryDisplayModel
import com.adammcneilly.pocketleague.shared.app.core.displaymodels.MatchDetailDisplayModel
import com.adammcneilly.pocketleague.shared.app.core.locale.LocaleHelper
import com.adammcneilly.pocketleague.shared.app.domain.usecases.GetOngoingEventsUseCase
import com.adammcneilly.pocketleague.shared.app.domain.usecases.GetPastWeeksMatchesUseCase
import com.adammcneilly.pocketleague.shared.app.domain.usecases.GetUpcomingEventsUseCase
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

class FeedViewModel(
private val dateTimeFormatter: DateTimeFormatter,
private val getPastWeeksMatchesUseCase: GetPastWeeksMatchesUseCase,
private val getOngoingEventsUseCase: GetOngoingEventsUseCase,
private val getUpcomingEventsUseCase: GetUpcomingEventsUseCase,
private val localeHelper: LocaleHelper,
private val timeProvider: TimeProvider,
) : ViewModel() {
private val mutableState = MutableStateFlow(FeedViewState.placeholderState())
val state = mutableState.asStateFlow()

init {
observePastWeeksMatches()
observeOngoingEvents()
observeUpcomingEvents()
}

private fun observePastWeeksMatches() {
val matchFlow = getPastWeeksMatchesUseCase
.invoke()
.map { matchList ->
matchList.map { match ->
MatchDetailDisplayModel(
match = match,
dateTimeFormatter = dateTimeFormatter,
timeProvider = timeProvider,
)
}
}

viewModelScope.launch {
matchFlow.collect { matchList ->
mutableState.update { currentState ->
currentState.copy(
recentMatches = matchList,
)
}
}
}
}

private fun observeOngoingEvents() {
val eventFlow = getOngoingEventsUseCase
.invoke()
.map { eventList ->
eventList.map { event ->
EventSummaryDisplayModel(
event = event,
dateTimeFormatter = dateTimeFormatter,
localeHelper = localeHelper,
)
}
}
.map(EventGroupDisplayModel.Companion::mapFromEventList)

viewModelScope.launch {
eventFlow.collect { eventList ->
mutableState.update { currentState ->
currentState.copy(
ongoingEvents = eventList,
)
}
}
}
}

private fun observeUpcomingEvents() {
val eventFlow = getUpcomingEventsUseCase
.invoke()
.map { eventList ->
eventList.map { event ->
EventSummaryDisplayModel(
event = event,
dateTimeFormatter = dateTimeFormatter,
localeHelper = localeHelper,
)
}
}
.map(EventGroupDisplayModel.Companion::mapFromEventList)

viewModelScope.launch {
eventFlow.collect { eventList ->
mutableState.update { currentState ->
currentState.copy(
ongoingEvents = eventList,
)
}
}
}
}
}
Loading
Loading