shared/src/commonMain/kotlin/org/jetbrains/kotlinconf/navigation/KotlinConfNavHost.kt (239 lines of code) (raw):
package org.jetbrains.kotlinconf.navigation
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.saveable.rememberSerializable
import androidx.compose.ui.platform.LocalUriHandler
import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator
import androidx.navigation3.runtime.EntryProviderScope
import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
import androidx.navigation3.ui.NavDisplay
import androidx.savedstate.compose.serialization.serializers.SnapshotStateListSerializer
import kotlinx.coroutines.channels.Channel
import org.jetbrains.kotlinconf.LocalFlags
import org.jetbrains.kotlinconf.LocalNotificationId
import org.jetbrains.kotlinconf.SessionId
import org.jetbrains.kotlinconf.URLs
import org.jetbrains.kotlinconf.screens.AboutAppScreen
import org.jetbrains.kotlinconf.screens.AboutConference
import org.jetbrains.kotlinconf.screens.AppPrivacyNotice
import org.jetbrains.kotlinconf.screens.AppPrivacyNoticePrompt
import org.jetbrains.kotlinconf.screens.AppTermsOfUse
import org.jetbrains.kotlinconf.screens.CodeOfConduct
import org.jetbrains.kotlinconf.screens.DeveloperMenuScreen
import org.jetbrains.kotlinconf.screens.LicensesScreen
import org.jetbrains.kotlinconf.screens.MainScreen
import org.jetbrains.kotlinconf.screens.NestedMapScreen
import org.jetbrains.kotlinconf.screens.PartnerDetailScreen
import org.jetbrains.kotlinconf.screens.PartnersScreen
import org.jetbrains.kotlinconf.screens.SessionScreen
import org.jetbrains.kotlinconf.screens.SettingsScreen
import org.jetbrains.kotlinconf.screens.SingleLicenseScreen
import org.jetbrains.kotlinconf.screens.SpeakerDetailScreen
import org.jetbrains.kotlinconf.screens.StartNotificationsScreen
import org.jetbrains.kotlinconf.screens.VisitorPrivacyNotice
import org.jetbrains.kotlinconf.screens.VisitorTermsOfUse
import org.jetbrains.kotlinconf.utils.getStoreUrl
fun navigateByLocalNotificationId(notificationId: String) {
LocalNotificationId.parse(notificationId)?.let {
when (it.type) {
LocalNotificationId.Type.SessionStart, LocalNotificationId.Type.SessionEnd ->
navigateToSession(SessionId(it.id))
}
}
}
fun navigateToSession(sessionId: SessionId) {
notificationNavRequests.trySend(SessionScreen(sessionId))
}
private val notificationNavRequests = Channel<AppRoute>(capacity = 1)
@Composable
private fun NotificationHandler(backStack: MutableList<AppRoute>) {
LaunchedEffect(Unit) {
while (true) {
backStack.add(notificationNavRequests.receive())
}
}
}
@Composable
internal fun KotlinConfNavHost(isOnboardingComplete: Boolean) {
val backstack: MutableList<AppRoute> =
rememberSerializable(serializer = SnapshotStateListSerializer()) {
val startDestination = if (isOnboardingComplete) MainScreen else StartPrivacyNoticeScreen
mutableStateListOf(startDestination)
}
// TODO Integrate with browser navigation here https://github.com/JetBrains/kotlinconf-app/issues/557
NotificationHandler(backstack)
val onBack = {
if (backstack.size > 1) backstack.removeLastOrNull()
}
NavDisplay(
backStack = backstack,
onBack = onBack,
entryProvider = entryProvider {
screens(backstack, onBack)
},
entryDecorators = listOf(
rememberSaveableStateHolderNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator(),
),
)
}
private fun EntryProviderScope<AppRoute>.screens(
backStack: MutableList<AppRoute>,
onBack: () -> Unit,
) {
entry<StartPrivacyNoticeScreen> {
val skipNotifications = LocalFlags.current.supportsNotifications.not()
AppPrivacyNoticePrompt(
onRejectNotice = {
if (skipNotifications) {
backStack.clear()
backStack.add(MainScreen)
} else {
backStack.add(StartNotificationsScreen)
}
},
onAcceptNotice = {
if (skipNotifications) {
backStack.clear()
backStack.add(MainScreen)
} else {
backStack.add(StartNotificationsScreen)
}
},
onAppTermsOfUse = { backStack.add(AppTermsOfUseScreen) },
confirmationRequired = false,
)
}
entry<StartNotificationsScreen> {
StartNotificationsScreen(
onDone = {
backStack.clear()
backStack.add(MainScreen)
}
)
}
entry<MainScreen> {
MainScreen(onNavigate = { backStack.add(it) })
}
entry<SpeakerDetailScreen> {
SpeakerDetailScreen(
speakerId = it.speakerId,
onBack = onBack,
onSession = { backStack.add(SessionScreen(it)) },
)
}
entry<SessionScreen> {
val urlHandler = LocalUriHandler.current
SessionScreen(
sessionId = it.sessionId,
openedForFeedback = it.openedForFeedback,
onBack = onBack,
onPrivacyNoticeNeeded = { backStack.add(AppPrivacyNoticePrompt) },
onSpeaker = { speakerId -> backStack.add(SpeakerDetailScreen(speakerId)) },
onWatchVideo = { videoUrl -> urlHandler.openUri(videoUrl) },
onNavigateToMap = { roomName ->
backStack.add(NestedMapScreen(roomName))
},
)
}
entry<AboutAppScreen> {
val uriHandler = LocalUriHandler.current
AboutAppScreen(
onBack = onBack,
onGitHubRepo = { uriHandler.openUri(URLs.GITHUB_REPO) },
onRateApp = { getStoreUrl()?.let { uriHandler.openUri(it) } },
onPrivacyNotice = { backStack.add(AppPrivacyNoticeScreen) },
onTermsOfUse = { backStack.add(AppTermsOfUseScreen) },
onLicenses = { backStack.add(LicensesScreen) },
onJunie = { uriHandler.openUri(URLs.JUNIE_LANDING_PAGE) },
onDeveloperMenu = { backStack.add(DeveloperMenuScreen) },
)
}
entry<LicensesScreen> {
LicensesScreen(
onLicenseClick = { licenseName, licenseText ->
backStack.add(SingleLicenseScreen(licenseName, licenseText))
},
onBack = onBack,
)
}
entry<SingleLicenseScreen> {
SingleLicenseScreen(
licenseName = it.licenseName,
licenseContent = it.licenseText,
onBack = onBack,
)
}
entry<AboutConferenceScreen> {
val urlHandler = LocalUriHandler.current
AboutConference(
onPrivacyNotice = { backStack.add(VisitorPrivacyNoticeScreen) },
onGeneralTerms = { backStack.add(TermsOfUseScreen) },
onWebsiteLink = { urlHandler.openUri(URLs.KOTLINCONF_HOMEPAGE) },
onBack = onBack,
onSpeaker = { speakerId -> backStack.add(SpeakerDetailScreen(speakerId)) },
)
}
entry<CodeOfConductScreen> {
CodeOfConduct(onBack = onBack)
}
entry<SettingsScreen> {
SettingsScreen(onBack = onBack)
}
entry<VisitorPrivacyNoticeScreen> {
VisitorPrivacyNotice(onBack = onBack)
}
entry<AppPrivacyNoticeScreen> {
AppPrivacyNotice(
onBack = onBack,
onAppTermsOfUse = { backStack.add(AppTermsOfUseScreen) },
)
}
entry<TermsOfUseScreen> {
VisitorTermsOfUse(
onBack = onBack,
onCodeOfConduct = { backStack.add(CodeOfConductScreen) },
onVisitorPrivacyNotice = { backStack.add(VisitorPrivacyNoticeScreen) },
)
}
entry<AppTermsOfUseScreen> {
AppTermsOfUse(
onBack = onBack,
onAppPrivacyNotice = {
backStack.add(AppPrivacyNoticeScreen)
},
)
}
entry<PartnersScreen> {
PartnersScreen(
onBack = onBack,
onPartnerDetail = { partnerId ->
backStack.add(PartnerDetailScreen(partnerId))
}
)
}
entry<PartnerDetailScreen> {
PartnerDetailScreen(
partnerId = it.partnerId,
onBack = onBack,
)
}
entry<AppPrivacyNoticePrompt> {
AppPrivacyNoticePrompt(
onRejectNotice = onBack,
onAcceptNotice = onBack,
onAppTermsOfUse = { backStack.add(AppTermsOfUseScreen) },
confirmationRequired = true,
)
}
entry<DeveloperMenuScreen> {
DeveloperMenuScreen(onBack = onBack)
}
entry<NestedMapScreen> {
NestedMapScreen(
roomName = it.roomName,
onBack = onBack,
)
}
}