Skip to content

Commit

Permalink
Merge pull request DroidKaigi#880 from Corvus400/feature/display_snac…
Browse files Browse the repository at this point in the history
…k_bar_compose_multiplatform_timetable_detail_screen

✨ [Compose Multiplatform] When you don't have permission to access the calendar on the session details screen, the snack bar is displayed.
  • Loading branch information
takahirom authored Sep 1, 2024
2 parents 5b287e2 + 8da2bfc commit 40d3585
Show file tree
Hide file tree
Showing 7 changed files with 121 additions and 16 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="permission_required">セッションを予定として追加するには、カレンダーへのアクセス権限が必要です。</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="permission_required">To add a session as a scheduled event, you need access permission to the calendar.</string>
</resources>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.github.droidkaigi.confsched.shared

import conference_app_2024.app_ios_shared.generated.resources.Res

object AppIosSharedRes {
val drawable = Res.drawable
val string = Res.string
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package io.github.droidkaigi.confsched.shared
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.Surface
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
import androidx.compose.material3.windowsizeclass.WindowSizeClass
Expand All @@ -20,15 +21,20 @@ import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.rememberNavController
import co.touchlab.kermit.Logger
import conference_app_2024.app_ios_shared.generated.resources.permission_required
import io.github.droidkaigi.confsched.about.aboutScreen
import io.github.droidkaigi.confsched.about.aboutScreenRoute
import io.github.droidkaigi.confsched.about.navigateAboutScreen
import io.github.droidkaigi.confsched.compose.rememberEventFlow
import io.github.droidkaigi.confsched.contributors.contributorsScreenRoute
import io.github.droidkaigi.confsched.contributors.contributorsScreens
import io.github.droidkaigi.confsched.data.Repositories
import io.github.droidkaigi.confsched.designsystem.theme.KaigiTheme
import io.github.droidkaigi.confsched.designsystem.theme.dotGothic16FontFamily
import io.github.droidkaigi.confsched.droidkaigiui.NavHostWithSharedAxisX
import io.github.droidkaigi.confsched.droidkaigiui.SnackbarMessageEffect
import io.github.droidkaigi.confsched.droidkaigiui.UserMessageStateHolder
import io.github.droidkaigi.confsched.droidkaigiui.compositionlocal.LocalSnackbarHostState
import io.github.droidkaigi.confsched.eventmap.eventMapScreenRoute
import io.github.droidkaigi.confsched.eventmap.eventMapScreens
import io.github.droidkaigi.confsched.eventmap.navigateEventMapScreen
Expand Down Expand Up @@ -76,6 +82,7 @@ import io.github.droidkaigi.confsched.staff.staffScreenRoute
import io.github.droidkaigi.confsched.staff.staffScreens
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
import platform.EventKit.EKEntityType.EKEntityTypeEvent
import platform.EventKit.EKEvent
import platform.EventKit.EKEventStore
Expand All @@ -89,20 +96,21 @@ import platform.UIKit.UIApplication
import platform.UIKit.UIViewController
import platform.darwin.NSObject

private object ExternalNavControllerLink {
var onLicenseScreenRequest: (() -> Unit)? = null
}
data class IosComposeKaigiAppUiState(
val userMessageStateHolder: UserMessageStateHolder,
)

@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@Suppress("UNUSED")
fun kaigiAppController(
repositories: Repositories,
onLicenseScreenRequest: () -> Unit,
): UIViewController = ComposeUIViewController {
ExternalNavControllerLink.onLicenseScreenRequest = onLicenseScreenRequest
val snackbarHostState = remember { SnackbarHostState() }

CompositionLocalProvider(
LocalRepositories provides repositories.map
LocalRepositories provides repositories.map,
LocalSnackbarHostState provides snackbarHostState
) {
val windowSizeClass = calculateWindowSizeClass()

Expand All @@ -121,6 +129,8 @@ fun kaigiAppController(
KaigiApp(
windowSize = windowSizeClass,
fontFamily = fontFamily,
snackbarHostState = snackbarHostState,
onLicenseScreenRequest = onLicenseScreenRequest,
)
}
}
Expand All @@ -129,17 +139,36 @@ fun kaigiAppController(
fun KaigiApp(
windowSize: WindowSizeClass,
fontFamily: FontFamily?,
snackbarHostState: SnackbarHostState,
onLicenseScreenRequest: () -> Unit,
modifier: Modifier = Modifier,
) {
val eventFlow = rememberEventFlow<IosComposeKaigiAppEvent>()
val uiState = iosComposeKaigiAppPresenter(events = eventFlow)

SnackbarMessageEffect(
snackbarHostState = snackbarHostState,
userMessageStateHolder = uiState.userMessageStateHolder,
)

KaigiTheme(
fontFamily = fontFamily,
) {
Surface(
modifier = modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background,
) {
val snackbarMessage = stringResource(AppIosSharedRes.string.permission_required)
KaigiNavHost(
windowSize = windowSize,
onLicenseScreenRequest = onLicenseScreenRequest,
onAccessCalendarIsDenied = {
eventFlow.tryEmit(
IosComposeKaigiAppEvent.ShowRequiresAuthorization(
snackbarMessage = snackbarMessage,
)
)
}
)
}
}
Expand All @@ -148,19 +177,27 @@ fun KaigiApp(
@Composable
private fun KaigiNavHost(
windowSize: WindowSizeClass,
onLicenseScreenRequest: () -> Unit,
onAccessCalendarIsDenied: () -> Unit,
navController: NavHostController = rememberNavController(),
externalNavController: ExternalNavController = rememberExternalNavController()
externalNavController: ExternalNavController = rememberExternalNavController(),
) {
NavHostWithSharedAxisX(navController = navController, startDestination = mainScreenRoute) {
mainScreen(
windowSize = windowSize,
navController = navController,
externalNavController = externalNavController,
onLicenseScreenRequest = onLicenseScreenRequest,
)
sessionScreens(
onNavigationIconClick = navController::popBackStack,
onLinkClick = externalNavController::navigate,
onCalendarRegistrationClick = externalNavController::navigateToCalendarRegistration,
onCalendarRegistrationClick = { timetableItem ->
externalNavController.navigateToCalendarRegistration(
timetableItem = timetableItem,
onAccessCalendarIsDenied = onAccessCalendarIsDenied,
)
},
onShareClick = externalNavController::onShareClick,
onFavoriteListClick = {
navController.navigate(
Expand Down Expand Up @@ -205,6 +242,7 @@ private fun NavGraphBuilder.mainScreen(
windowSize: WindowSizeClass,
navController: NavHostController,
externalNavController: ExternalNavController,
onLicenseScreenRequest: () -> Unit,
) {
mainScreen(
windowSize = windowSize,
Expand Down Expand Up @@ -246,7 +284,7 @@ private fun NavGraphBuilder.mainScreen(
}

AboutItem.Contributors -> navController.navigate(contributorsScreenRoute)
AboutItem.License -> externalNavController.navigateToLicenseScreen()
AboutItem.License -> onLicenseScreenRequest()
AboutItem.Medium -> externalNavController.navigate(
url = "https://medium.com/droidkaigi",
)
Expand Down Expand Up @@ -334,20 +372,18 @@ private class ExternalNavController(
UIApplication.sharedApplication.openURL(nsUrl)
}

fun navigateToLicenseScreen() {
ExternalNavControllerLink.onLicenseScreenRequest?.invoke()
}

/**
* Navigate to Calendar Registration
*/
fun navigateToCalendarRegistration(timetableItem: TimetableItem) {
fun navigateToCalendarRegistration(
timetableItem: TimetableItem,
onAccessCalendarIsDenied: () -> Unit,
) {
val eventStore = EKEventStore()

eventStore.requestAccessToEntityType(EKEntityTypeEvent) { granted, error ->
if (granted.not()) {
// TODO Display a message asking the user to add permissions.
// TODO Otherwise, the privileges will remain permanently denied.
onAccessCalendarIsDenied()
Logger.e("Calendar access was denied by the user.")
return@requestAccessToEntityType
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package io.github.droidkaigi.confsched.shared

import androidx.compose.runtime.Composable
import io.github.droidkaigi.confsched.compose.EventEffect
import io.github.droidkaigi.confsched.compose.EventFlow
import io.github.droidkaigi.confsched.droidkaigiui.providePresenterDefaults
import io.github.droidkaigi.confsched.shared.IosComposeKaigiAppEvent.ShowRequiresAuthorization

sealed interface IosComposeKaigiAppEvent {
val snackbarMessage: String

data class ShowRequiresAuthorization(
override val snackbarMessage: String,
) : IosComposeKaigiAppEvent
}

@Composable
fun iosComposeKaigiAppPresenter(
events: EventFlow<IosComposeKaigiAppEvent>
) : IosComposeKaigiAppUiState = providePresenterDefaults { userMessageStateHolder ->
EventEffect(events) { event ->
when (event) {
is ShowRequiresAuthorization -> {
userMessageStateHolder.showMessage(
message = event.snackbarMessage,
// TODO Add code to transition to the settings screen when the action button is pressed.
// TODO Perhaps UIApplication.openSettingsURLString can be used to achieve this.
actionLabel = null,
)
}
}
}
IosComposeKaigiAppUiState(userMessageStateHolder)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package io.github.droidkaigi.confsched.droidkaigiui.compositionlocal

import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.compositionLocalOf

@Suppress("CompositionLocalAllowlist")
val LocalSnackbarHostState = compositionLocalOf<SnackbarHostState?> {
null
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import io.github.droidkaigi.confsched.droidkaigiui.UserMessageStateHolder
import io.github.droidkaigi.confsched.droidkaigiui.UserMessageStateHolderImpl
import io.github.droidkaigi.confsched.droidkaigiui.compositionlocal.LocalAnimatedVisibilityScope
import io.github.droidkaigi.confsched.droidkaigiui.compositionlocal.LocalSharedTransitionScope
import io.github.droidkaigi.confsched.droidkaigiui.compositionlocal.LocalSnackbarHostState
import io.github.droidkaigi.confsched.model.Lang
import io.github.droidkaigi.confsched.model.TimetableItem
import io.github.droidkaigi.confsched.model.TimetableItem.Session
Expand Down Expand Up @@ -98,7 +99,7 @@ fun TimetableItemDetailScreen(
events = eventFlow,
),
) {
val snackbarHostState = remember { SnackbarHostState() }
val snackbarHostState = snackbarHostSate()
SnackbarMessageEffect(
snackbarHostState = snackbarHostState,
userMessageStateHolder = uiState.userMessageStateHolder,
Expand Down Expand Up @@ -266,6 +267,15 @@ private fun TimetableItemDetailScreen(
}
}

@Composable
fun snackbarHostSate(): SnackbarHostState {
val state = LocalSnackbarHostState.current
if (state != null) {
return state
}
return remember { SnackbarHostState() }
}

@Composable
@Preview
fun TimetableItemDetailScreenPreview() {
Expand Down

0 comments on commit 40d3585

Please sign in to comment.