Skip to content

Commit

Permalink
Use kotlinx.datetime for time handling
Browse files Browse the repository at this point in the history
  • Loading branch information
zsmb13 committed Feb 11, 2025
1 parent f94c932 commit d077a3a
Show file tree
Hide file tree
Showing 13 changed files with 187 additions and 247 deletions.
29 changes: 18 additions & 11 deletions backend/src/main/kotlin/org/jetbrains/kotlinconf/backend/Api.kt
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
package org.jetbrains.kotlinconf.backend

import io.ktor.http.*
import io.ktor.server.application.*
import io.ktor.server.auth.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.util.date.*
import org.jetbrains.kotlinconf.*
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.ApplicationCall
import io.ktor.server.auth.principal
import io.ktor.server.request.receive
import io.ktor.server.response.respond
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.routing.post
import io.ktor.server.routing.route
import io.ktor.util.date.GMTDate
import kotlinx.datetime.toInstant
import org.jetbrains.kotlinconf.EVENT_TIME_ZONE
import org.jetbrains.kotlinconf.FeedbackInfo
import org.jetbrains.kotlinconf.VoteInfo
import org.jetbrains.kotlinconf.Votes
import java.time.*
import java.time.Clock
import java.time.LocalDateTime

internal fun Route.api(
store: Store,
Expand Down Expand Up @@ -64,8 +71,8 @@ private fun Route.apiVote(

val nowTime = now()

val startVotesAt = session.startsAt
val votingPeriodStarted = nowTime >= startVotesAt.timestamp
val startVotesAt = session.startsAt.toInstant(EVENT_TIME_ZONE)
val votingPeriodStarted = nowTime >= startVotesAt.toEpochMilliseconds()

if (!votingPeriodStarted) {
return@post call.respond(comeBackLater)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.jetbrains.kotlinconf.backend

import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.jetbrains.kotlinconf.GMTDateSerializable
import org.jetbrains.kotlinconf.SessionId
import org.jetbrains.kotlinconf.SpeakerId

Expand All @@ -24,8 +24,8 @@ data class SessionData(
val speakers: List<SpeakerId>,
@SerialName("description")
var descriptionText: String? = "",
val startsAt: GMTDateSerializable?,
val endsAt: GMTDateSerializable?,
val startsAt: LocalDateTime?,
val endsAt: LocalDateTime?,
val title: String,
val roomId: Int?,
val questionAnswers: List<QuestionAnswerData> = emptyList(),
Expand Down
46 changes: 10 additions & 36 deletions shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/APIClient.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,11 @@ import io.ktor.http.isSuccess
import io.ktor.http.path
import io.ktor.http.takeFrom
import io.ktor.serialization.kotlinx.json.json
import io.ktor.util.date.GMTDate
import io.ktor.util.date.Month
import io.ktor.utils.io.core.Closeable
import kotlinx.datetime.Instant
import kotlinx.datetime.LocalDateTime
import org.jetbrains.kotlinconf.utils.appLogger

val HTTP_CLIENT = HttpClient()

/**
* Adapter to handle backend API and manage auth information.
*/
Expand All @@ -38,7 +36,7 @@ class APIClient(
) : Closeable {
var userId: String? = null

private val client = HTTP_CLIENT.config {
private val client = HttpClient {
install(ContentNegotiation) {
json()
}
Expand Down Expand Up @@ -135,12 +133,9 @@ class APIClient(
}.body<Votes>().votes
}

/**
* Get server time.
*/
suspend fun getServerTime(): GMTDate = client.get {
suspend fun getServerTime(): Instant = client.get {
apiUrl("time")
}.bodyAsText().let { response -> GMTDate(response.toLong()) }
}.bodyAsText().let { response -> Instant.fromEpochMilliseconds(response.toLong()) }

// TODO real api call https://github.com/JetBrains/kotlinconf-app/issues/268
suspend fun getNews(): List<NewsItem> = EXAMPLE_NEWS_ITEMS
Expand Down Expand Up @@ -171,56 +166,35 @@ private val EXAMPLE_NEWS_ITEMS = listOf(
id = "0",
title = "Kotlin 1.9 Released",
content = "**Exciting news for Kotlin developers!** The latest version of Kotlin brings significant improvements and new features to enhance your development experience.\n\nSome highlights include:\n- *K2 compiler* improvements for faster compilation\n- Enhanced type inference system\n- New stdlib functions\n\nCheck out the detailed release notes at [kotlinlang.org](https://kotlinlang.org) and start exploring these amazing features today! The Kotlin team has been working hard to make this release even more **powerful** and *developer-friendly*.",
date = GMTDate(
year = 2024,
month = Month.APRIL,
dayOfMonth = 23,
hours = 10,
minutes = 24,
seconds = 2
),
date = LocalDateTime.parse("2024-04-23T10:24:02"),
photoUrl = "https://picsum.photos/1800/900"
),
NewsItem(
id = "1",
title = "KotlinConf 2024 Announced",
content = "Get ready for the most anticipated Kotlin event of the year! **KotlinConf 2024** brings together developers from around the world for an unforgettable experience.\n\n*What to expect:*\n- Inspiring keynotes from Kotlin leaders\n- In-depth technical sessions\n- Hands-on workshops\n- Networking opportunities\n\nDon't miss the chance to meet fellow Kotlin enthusiasts and learn from industry experts. Early bird tickets are now available at [kotlinconf.com/2024](https://kotlinconf.com/2024).\n\n**Pro tip:** Check out the *conference app* to plan your schedule and connect with other attendees!",
date = GMTDate(year = 2024, month = Month.MAY, dayOfMonth = 22, hours = 10, minutes = 24, seconds = 2),
date = LocalDateTime.parse("2024-05-22T10:24:02"),
photoUrl = null
),
NewsItem(
id = "2",
title = "Jetpack Compose Updates",
content = "The world of **Jetpack Compose** continues to evolve with exciting new features for both Android and Desktop development!\n\n*Latest improvements include:*\n- Enhanced performance optimizations\n- New material design components\n- Improved animation APIs\n- Better desktop window management\n\nRead the comprehensive guide on the [Android Developers Blog](https://android-developers.googleblog.com) and explore the [Compose Multiplatform documentation](https://www.jetbrains.com/compose-multiplatform/).\n\n**Did you know?** You can now easily share up to *90% of your UI code* between Android and Desktop applications using Compose Multiplatform!",
date = GMTDate(
year = 2024,
month = Month.JANUARY,
dayOfMonth = 22,
hours = 10,
minutes = 24,
seconds = 2
),
date = LocalDateTime.parse("2024-01-22T10:24:02"),
photoUrl = null
),
NewsItem(
id = "3",
title = "New Kotlin Multiplatform Features",
content = "**Kotlin Multiplatform** technology reaches new heights with groundbreaking features and improvements!\n\n*Key highlights of the latest release:*\n- Simplified project setup and configuration\n- Enhanced iOS integration with new Kotlin/Native features\n- Improved dependency management\n- Extended WebAssembly support\n\nStart building your next cross-platform project with [KMP](https://kotlinlang.org/docs/multiplatform.html) today!\n\n**Success Story:** *Philips* recently shared how they achieved a **75% code sharing rate** across platforms using Kotlin Multiplatform. Read their detailed case study on the [Kotlin Blog](https://blog.jetbrains.com/kotlin/).\n\nExplore the [official documentation](https://kotlinlang.org/docs/multiplatform-get-started.html) to learn more about these exciting developments!",
date = GMTDate(year = 2024, month = Month.MAY, dayOfMonth = 23, hours = 10, minutes = 24, seconds = 2),
date = LocalDateTime.parse("2024-05-23T10:24:02"),
photoUrl = "https://picsum.photos/1800/900"
),
NewsItem(
id = "4",
title = "Kotlin Community Highlights",
content = "The **Kotlin community** continues to innovate and inspire! Let's celebrate some remarkable community contributions.\n\n*Featured Projects:*\n- **Ktor 2.0**: A powerful framework for building asynchronous servers and clients\n- *Kotlin Native Bridge*: Seamless integration between Kotlin and native platforms\n- **KMP-NativeCoroutines**: Simplified concurrency for multiplatform projects\n\nJoin the community on [Kotlin Slack](https://kotlinlang.slack.com) with over *100,000 members* and share your own projects!\n\n**Want to contribute?** Check out the [Kotlin Contributing Guidelines](https://github.com/JetBrains/kotlin) and help shape the future of Kotlin. The community has already contributed more than *500 patches* to the latest release!\n\nExplore more community projects on [Kotlin Weekly](https://kotlinweekly.net) and get inspired for your next project.",
date = GMTDate(
year = 2024,
month = Month.APRIL,
dayOfMonth = 20,
hours = 10,
minutes = 24,
seconds = 2
),
date = LocalDateTime.parse("2024-04-20T10:24:02"),
photoUrl = "https://picsum.photos/1800/900"
)
)
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.jetbrains.kotlinconf

import io.ktor.util.date.GMTDate
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
Expand All @@ -14,28 +13,11 @@ import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.toInstant
import org.jetbrains.kotlinconf.storage.ApplicationStorage
import org.jetbrains.kotlinconf.utils.time

val UNKNOWN_SESSION_CARD: SessionCardView = SessionCardView(
id = SessionId("unknown"),
title = "unknown",
speakerLine = "unknown",
locationLine = "unknown",
startsAt = GMTDate.START,
endsAt = GMTDate.START,
speakerIds = emptyList(),
isFavorite = false,
description = "unknown",
vote = null,
tags = emptyList(),
startsInMinutes = null,
state = SessionState.Upcoming,
)

val UNKNOWN_SPEAKER: Speaker = Speaker(
SpeakerId("unknown"), "unknown", "unknown", "unknown", ""
)
import org.jetbrains.kotlinconf.utils.DateTimeFormatting
import kotlin.time.Duration.Companion.minutes

class ConferenceService(
private val client: APIClient,
Expand Down Expand Up @@ -162,18 +144,14 @@ class ConferenceService(
suspend fun sendFeedback(sessionId: SessionId, feedbackValue: String): Boolean =
client.sendFeedback(sessionId, feedbackValue)

fun speakerById(id: SpeakerId): Speaker = speakers.value[id] ?: UNKNOWN_SPEAKER
fun speakerById(id: SpeakerId): Speaker? = speakers.value[id]

fun sessionById(id: SessionId): SessionCardView =
sessionCards.value.find { it.id == id } ?: UNKNOWN_SESSION_CARD

fun sessionByIdFlow(id: SessionId): Flow<SessionCardView> =
sessionCards
.map { sessions -> sessions.find { it.id == id } ?: UNKNOWN_SESSION_CARD }
fun sessionByIdFlow(id: SessionId): Flow<SessionCardView?> =
sessionCards.map { sessions -> sessions.find { it.id == id } }

fun speakersBySessionId(id: SessionId): Flow<List<Speaker>> =
sessionByIdFlow(id).map { session ->
session.speakerIds.map { speakerId -> speakerById(speakerId) }
session?.speakerIds?.mapNotNull { speakerId -> speakerById(speakerId) } ?: emptyList()
}

fun sessionsForSpeaker(id: SpeakerId): List<SessionCardView> =
Expand Down Expand Up @@ -204,34 +182,26 @@ class ConferenceService(
val notificationsAllowed = storage.getNotificationsAllowed().first()
if (!notificationsAllowed) return@launch

val startTimestamp = session.startsAt.timestamp
val reminderTimestamp = startTimestamp - 5 * 60 * 1000
val nowTimestamp = timeProvider.now().timestamp
val delay = reminderTimestamp - nowTimestamp
val voteTimeStamp = session.endsAt.timestamp

when {
delay >= 0 -> {
notificationManager.schedule(delay, session.title, "Starts in 5 minutes.")
}
val start = session.startsAt.toInstant(EVENT_TIME_ZONE)
val end = session.endsAt.toInstant(EVENT_TIME_ZONE)
val now = timeProvider.now().toInstant(EVENT_TIME_ZONE)

nowTimestamp in reminderTimestamp..<startTimestamp -> {
notificationManager.schedule(0, session.title, "The session is about to start.")
}
val reminderTime = start - 5.minutes

nowTimestamp in startTimestamp..<voteTimeStamp -> {
notificationManager.schedule(0, session.title, "Hurry up! The session has already started!")
}
// Notifications for session start
val startsLater = now < reminderTime
val startsSoon = now in reminderTime..<start
val isLive = now in start..<end
when {
startsLater -> notificationManager.schedule((reminderTime - now).inWholeMilliseconds, session.title, "Starts in 5 minutes.")
startsSoon -> notificationManager.schedule(0, session.title, "The session is about to start.")
isLive -> notificationManager.schedule(0, session.title, "Hurry up! The session has already started!")
}

if (nowTimestamp > voteTimeStamp) return@launch

val voteDelay = voteTimeStamp - nowTimestamp
notificationManager.schedule(
voteDelay,
"${session.title} finished",
"How was the talk?"
)
// Notifications for session end
if (end > now) {
notificationManager.schedule((end - now).inWholeMilliseconds, "${session.title} finished", "How was the talk?")
}
}
}

Expand All @@ -247,7 +217,7 @@ class ConferenceService(

private fun mapNewsItemToDisplayItem(
item: NewsItem,
now: GMTDate,
now: LocalDateTime,
): NewsDisplayItem {
return NewsDisplayItem(
id = item.id,
Expand All @@ -258,13 +228,12 @@ class ConferenceService(
)
}

private fun GMTDate.toNewsDisplayTime(now: GMTDate): String {
return if (year == now.year && dayOfYear == now.dayOfYear) {
return time()
} else if (year == now.year) {
"${month.value} $dayOfMonth"
} else {
"${month.value} $dayOfMonth, $year"
private fun LocalDateTime.toNewsDisplayTime(now: LocalDateTime): String {
val isToday = year == now.year && dayOfYear == now.dayOfYear
return when {
isToday -> DateTimeFormatting.time(this)
year == now.year -> DateTimeFormatting.date(this)
else -> DateTimeFormatting.dateWithYear(this)
}
}

Expand Down
17 changes: 6 additions & 11 deletions shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/Model.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
package org.jetbrains.kotlinconf

import io.ktor.util.date.*
import kotlinx.serialization.*
import org.jetbrains.kotlinconf.utils.*
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.Serializable
import kotlin.jvm.JvmInline

typealias GMTDateSerializable = @Serializable(GMTDateSerializer::class) GMTDate

@Serializable
@JvmInline
value class SpeakerId(val id: String) {
Expand Down Expand Up @@ -46,12 +43,10 @@ class Session(
val description: String,
val speakerIds: List<SpeakerId>,
val location: String,
val startsAt: GMTDateSerializable,
val endsAt: GMTDateSerializable,
val startsAt: LocalDateTime,
val endsAt: LocalDateTime,
val tags: List<String>? = null
) {
val timeLine get() = startsAt.time() + " - " + endsAt.time()
}
)

@Serializable
class VoteInfo(
Expand Down Expand Up @@ -92,7 +87,7 @@ enum class Theme {
class NewsItem(
val id: String,
val photoUrl: String?,
val date: GMTDateSerializable,
val date: LocalDateTime,
val title: String,
val content: String,
)
Expand Down
Loading

0 comments on commit d077a3a

Please sign in to comment.