mirror of
https://codeberg.org/timelimit/timelimit-android.git
synced 2025-10-03 09:49:25 +02:00
Refactor task review yes handling
This commit is contained in:
parent
cb4beed825
commit
fb864723ef
9 changed files with 85 additions and 76 deletions
|
@ -34,6 +34,9 @@ abstract class UserDao {
|
|||
@Query("SELECT * from user WHERE id = :userId AND type = \"child\"")
|
||||
abstract fun getChildUserByIdLive(userId: String): LiveData<User?>
|
||||
|
||||
@Query("SELECT * from user WHERE id = :userId AND type = \"child\"")
|
||||
abstract suspend fun getChildUserByIdCoroutine(userId: String): User?
|
||||
|
||||
@Query("SELECT * from user WHERE id = :userId AND type = \"parent\"")
|
||||
abstract fun getParentUserByIdLive(userId: String): LiveData<User?>
|
||||
|
||||
|
|
|
@ -15,13 +15,17 @@
|
|||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import androidx.lifecycle.asFlow
|
||||
import io.timelimit.android.livedata.ignoreUnchanged
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.livedata.or
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class FullVersionLogic(logic: AppLogic) {
|
||||
private val hasFullVersion = logic.database.config().getFullVersionUntilAsync().map { it != 0L }.ignoreUnchanged()
|
||||
val isLocalMode = logic.database.config().getDeviceAuthTokenAsync().map { it == "" }
|
||||
|
||||
val shouldProvideFullVersionFunctions = hasFullVersion.or(isLocalMode)
|
||||
|
||||
suspend fun shouldProvideFullVersionFunctions() = shouldProvideFullVersionFunctions.asFlow().first()
|
||||
}
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
*/
|
||||
package io.timelimit.android.logic
|
||||
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.data.Database
|
||||
import io.timelimit.android.livedata.liveDataFromNonNullValue
|
||||
import io.timelimit.android.livedata.map
|
||||
|
@ -26,6 +28,10 @@ class ServerApiLevelLogic(logic: AppLogic) {
|
|||
ServerApiLevelInfo.Offline
|
||||
else
|
||||
ServerApiLevelInfo.Online(serverLevel = database.config().getServerApiLevelSync())
|
||||
|
||||
private suspend fun getCoroutine(database: Database): ServerApiLevelInfo = Threads.database.executeAndWait {
|
||||
getSync(database)
|
||||
}
|
||||
}
|
||||
|
||||
private val database = logic.database
|
||||
|
@ -38,6 +44,8 @@ class ServerApiLevelLogic(logic: AppLogic) {
|
|||
ServerApiLevelInfo.Online(serverLevel = apiLevel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCoroutine() = getCoroutine(database)
|
||||
}
|
||||
|
||||
sealed class ServerApiLevelInfo {
|
||||
|
|
|
@ -63,6 +63,7 @@ import io.timelimit.android.ui.manage.device.add.AddDeviceFragment
|
|||
import io.timelimit.android.ui.model.*
|
||||
import io.timelimit.android.ui.overview.overview.CanNotAddDevicesInLocalModeDialogFragment
|
||||
import io.timelimit.android.ui.payment.ActivityPurchaseModel
|
||||
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||
import io.timelimit.android.ui.util.SyncStatusModel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
@ -141,6 +142,7 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
|||
ActivityCommand.ShowAddDeviceFragment -> AddDeviceFragment().show(supportFragmentManager)
|
||||
ActivityCommand.ShowCanNotAddDevicesInLocalModeDialogFragment -> CanNotAddDevicesInLocalModeDialogFragment().show(supportFragmentManager)
|
||||
ActivityCommand.ShowAuthenticationScreen -> showAuthenticationScreen()
|
||||
ActivityCommand.ShowMissingPremiumDialog -> RequiresPurchaseDialogFragment().show(supportFragmentManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -276,7 +278,11 @@ class MainActivity : AppCompatActivity(), ActivityViewModelHolder, U2fManager.De
|
|||
.padding(paddingValues)
|
||||
)
|
||||
},
|
||||
showAuthenticationDialog = showAuthenticationDialog
|
||||
showAuthenticationDialog = showAuthenticationDialog,
|
||||
snackbarHostState = when (screen) {
|
||||
is ScreenWithSnackbar -> screen.snackbarHostState
|
||||
else -> null
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,6 +34,7 @@ fun ScreenScaffold(
|
|||
screen: Screen?,
|
||||
title: String,
|
||||
subtitle: String?,
|
||||
snackbarHostState: SnackbarHostState?,
|
||||
content: @Composable (PaddingValues) -> Unit,
|
||||
executeCommand: (UpdateStateCommand) -> Unit,
|
||||
showAuthenticationDialog: (() -> Unit)?
|
||||
|
@ -106,6 +107,7 @@ fun ScreenScaffold(
|
|||
}
|
||||
}
|
||||
},
|
||||
snackbarHost = { SnackbarHost(snackbarHostState ?: it) },
|
||||
content = content
|
||||
)
|
||||
}
|
|
@ -19,4 +19,5 @@ sealed class ActivityCommand {
|
|||
object ShowCanNotAddDevicesInLocalModeDialogFragment: ActivityCommand()
|
||||
object ShowAddDeviceFragment: ActivityCommand()
|
||||
object ShowAuthenticationScreen: ActivityCommand()
|
||||
object ShowMissingPremiumDialog: ActivityCommand()
|
||||
}
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package io.timelimit.android.ui.model
|
||||
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.outlined.Info
|
||||
import io.timelimit.android.R
|
||||
|
@ -34,7 +35,8 @@ sealed class Screen(
|
|||
|
||||
class OverviewScreen(
|
||||
state: State,
|
||||
val content: OverviewHandling.OverviewScreen
|
||||
val content: OverviewHandling.OverviewScreen,
|
||||
override val snackbarHostState: SnackbarHostState
|
||||
): Screen(
|
||||
state,
|
||||
listOf(Menu.Icon(
|
||||
|
@ -46,7 +48,10 @@ sealed class Screen(
|
|||
R.string.main_tab_uninstall,
|
||||
UpdateStateCommand.Overview.Uninstall
|
||||
))
|
||||
), ScreenWithAuthenticationFab
|
||||
), ScreenWithAuthenticationFab, ScreenWithSnackbar
|
||||
}
|
||||
|
||||
interface ScreenWithAuthenticationFab
|
||||
interface ScreenWithSnackbar {
|
||||
val snackbarHostState: SnackbarHostState
|
||||
}
|
|
@ -15,20 +15,23 @@
|
|||
*/
|
||||
package io.timelimit.android.ui.model.main
|
||||
|
||||
import androidx.compose.material.SnackbarHostState
|
||||
import androidx.lifecycle.asFlow
|
||||
import io.timelimit.android.BuildConfig
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.async.Threads
|
||||
import io.timelimit.android.coroutines.executeAndWait
|
||||
import io.timelimit.android.data.extensions.getTimezone
|
||||
import io.timelimit.android.data.model.Device
|
||||
import io.timelimit.android.data.model.HintsToShow
|
||||
import io.timelimit.android.data.model.UserType
|
||||
import io.timelimit.android.data.model.derived.FullChildTask
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.extensions.tryWithLock
|
||||
import io.timelimit.android.extensions.whileTrue
|
||||
import io.timelimit.android.integration.platform.RuntimePermissionStatus
|
||||
import io.timelimit.android.livedata.map
|
||||
import io.timelimit.android.logic.AppLogic
|
||||
import io.timelimit.android.logic.ServerApiLevelInfo
|
||||
import io.timelimit.android.sync.actions.ReviewChildTaskAction
|
||||
import io.timelimit.android.sync.actions.apply.ApplyActionUtil
|
||||
import io.timelimit.android.ui.model.ActivityCommand
|
||||
|
@ -51,7 +54,8 @@ object OverviewHandling {
|
|||
authentication: AuthenticationModelApi,
|
||||
stateLive: MutableStateFlow<State>
|
||||
): Flow<Screen> {
|
||||
val actions: Actions = getActions(logic, scope, activityCommand, authentication, stateLive)
|
||||
val snackbarHostState = SnackbarHostState()
|
||||
val actions: Actions = getActions(logic, scope, activityCommand, authentication, stateLive, snackbarHostState)
|
||||
val overviewStateLive: Flow<OverviewState> = stateLive.transform { if (it is State.Overview) emit(it.state) }
|
||||
val overviewState2Live: Flow<State.Overview> = stateLive.transform { if (it is State.Overview) emit(it) }
|
||||
val overviewScreenLive: Flow<OverviewScreen> = getScreen(logic, actions, overviewStateLive)
|
||||
|
@ -59,7 +63,7 @@ object OverviewHandling {
|
|||
|
||||
return hasMatchingStateLive.whileTrue {
|
||||
overviewState2Live.combine(overviewScreenLive) { state, overviewScreen ->
|
||||
Screen.OverviewScreen(state, overviewScreen)
|
||||
Screen.OverviewScreen(state, overviewScreen, snackbarHostState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -69,20 +73,31 @@ object OverviewHandling {
|
|||
scope: CoroutineScope,
|
||||
activityCommand: SendChannel<ActivityCommand>,
|
||||
authentication: AuthenticationModelApi,
|
||||
stateLive: MutableStateFlow<State>
|
||||
stateLive: MutableStateFlow<State>,
|
||||
snackbarHostState: SnackbarHostState
|
||||
): Actions {
|
||||
val lock = Mutex()
|
||||
|
||||
fun launch(action: suspend () -> Unit) {
|
||||
scope.launch {
|
||||
try {
|
||||
action()
|
||||
} catch (ex: Exception) {
|
||||
snackbarHostState.showSnackbar(logic.context.getString(R.string.error_general))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return Actions(
|
||||
hideIntro = {
|
||||
scope.launch {
|
||||
launch {
|
||||
Threads.database.executeAndWait {
|
||||
logic.database.config().setHintsShownSync(HintsToShow.OVERVIEW_INTRODUCTION)
|
||||
}
|
||||
}
|
||||
},
|
||||
addDevice = {
|
||||
scope.launch {
|
||||
launch {
|
||||
lock.tryWithLock {
|
||||
val isLocalMode = Threads.database.executeAndWait {
|
||||
logic.database.config().getDeviceAuthTokenSync().isEmpty()
|
||||
|
@ -94,7 +109,7 @@ object OverviewHandling {
|
|||
}
|
||||
},
|
||||
skipTaskReview = { task ->
|
||||
scope.launch {
|
||||
launch {
|
||||
lock.tryWithLock {
|
||||
if (authentication.doParentAuthentication() != null) {
|
||||
stateLive.update { oldState ->
|
||||
|
@ -110,8 +125,7 @@ object OverviewHandling {
|
|||
}
|
||||
},
|
||||
reviewReject = { task ->
|
||||
// TODO: add error handler to scope
|
||||
scope.launch {
|
||||
launch {
|
||||
lock.tryWithLock {
|
||||
authentication.doParentAuthentication()?.let { parent ->
|
||||
ApplyActionUtil.applyParentAction(
|
||||
|
@ -128,9 +142,30 @@ object OverviewHandling {
|
|||
}
|
||||
}
|
||||
},
|
||||
reviewAccept = {
|
||||
scope.launch {
|
||||
TODO()
|
||||
reviewAccept = { task ->
|
||||
launch {
|
||||
val parent = authentication.authenticatedParentOnly.first()
|
||||
val hasPremium = logic.fullVersion.shouldProvideFullVersionFunctions()
|
||||
|
||||
if (parent == null) authentication.doParentAuthentication()
|
||||
else if (!hasPremium) activityCommand.send(ActivityCommand.ShowMissingPremiumDialog)
|
||||
else {
|
||||
val serverApiLevel = logic.serverApiLevelLogic.getCoroutine()
|
||||
val child = logic.database.user().getChildUserByIdCoroutine(task.task.childId)
|
||||
val time = logic.timeApi.getCurrentTimeInMillis()
|
||||
val day = DateInTimezone.newInstance(time, child.getTimezone()).dayOfEpoch
|
||||
|
||||
ApplyActionUtil.applyParentAction(
|
||||
ReviewChildTaskAction(
|
||||
taskId = task.task.childTask.taskId,
|
||||
ok = true,
|
||||
time = time,
|
||||
day = if (serverApiLevel.hasLevelOrIsOffline(2)) day else null
|
||||
),
|
||||
parent.authentication,
|
||||
logic
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
@ -274,36 +309,12 @@ object OverviewHandling {
|
|||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private fun getTaskToReview(logic: AppLogic, hiddenTaskIdsLive: Flow<Set<String>>): Flow<TaskToReview?> {
|
||||
val pendingTasksLive = logic.database.childTasks().getPendingTasksFlow()
|
||||
val serverApiLevelLive = logic.serverApiLevelLogic.infoLive.asFlow()
|
||||
val hasPremiumLive = logic.fullVersion.shouldProvideFullVersionFunctions.asFlow()
|
||||
|
||||
val taskWithChildLive = pendingTasksLive.combine(hiddenTaskIdsLive) { pendingTasks, hiddenTaskIds ->
|
||||
private fun getTaskToReview(logic: AppLogic, hiddenTaskIdsLive: Flow<Set<String>>): Flow<TaskToReview?> =
|
||||
logic.database.childTasks().getPendingTasksFlow().combine(hiddenTaskIdsLive) { pendingTasks, hiddenTaskIds ->
|
||||
pendingTasks
|
||||
.filterNot { hiddenTaskIds.contains(it.childTask.taskId) }
|
||||
.firstOrNull()
|
||||
}.transformLatest {
|
||||
if (it != null) {
|
||||
emitAll(logic.database.user().getChildUserByIdLive(it.childId).asFlow().map { childInfo ->
|
||||
if (childInfo == null) null
|
||||
else Pair(it, childInfo.timeZone)
|
||||
})
|
||||
} else emit(null)
|
||||
}
|
||||
|
||||
return combine(
|
||||
serverApiLevelLive, hasPremiumLive, taskWithChildLive
|
||||
) { serverApiLevel, hasPremium, taskWithChild ->
|
||||
if (taskWithChild == null) null
|
||||
else TaskToReview(
|
||||
task = taskWithChild.first,
|
||||
childTimezone = TimeZone.getTimeZone(taskWithChild.second),
|
||||
serverApiLevel = serverApiLevel,
|
||||
hasPremium = hasPremium
|
||||
)
|
||||
}
|
||||
?.let { TaskToReview(it) }
|
||||
}
|
||||
|
||||
data class OverviewState(
|
||||
|
@ -346,12 +357,7 @@ object OverviewHandling {
|
|||
val showServerMessage: String?,
|
||||
val showIntro: Boolean
|
||||
)
|
||||
data class TaskToReview(
|
||||
val task: FullChildTask,
|
||||
val hasPremium: Boolean,
|
||||
val childTimezone: TimeZone,
|
||||
val serverApiLevel: ServerApiLevelInfo
|
||||
)
|
||||
data class TaskToReview(val task: FullChildTask)
|
||||
data class UserItem(
|
||||
val id: String,
|
||||
val name: String,
|
||||
|
|
|
@ -29,12 +29,8 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.timelimit.android.R
|
||||
import io.timelimit.android.date.DateInTimezone
|
||||
import io.timelimit.android.sync.actions.ReviewChildTaskAction
|
||||
import io.timelimit.android.ui.MainActivity
|
||||
import io.timelimit.android.ui.model.UpdateStateCommand
|
||||
import io.timelimit.android.ui.model.main.OverviewHandling
|
||||
import io.timelimit.android.ui.payment.RequiresPurchaseDialogFragment
|
||||
import io.timelimit.android.ui.util.DateUtil
|
||||
import io.timelimit.android.util.TimeTextUtil
|
||||
|
||||
|
@ -45,9 +41,6 @@ fun OverviewScreen(
|
|||
executeCommand: (UpdateStateCommand) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
// TODO: implement this without dependency on MainActivity
|
||||
val activity = LocalContext.current as MainActivity
|
||||
|
||||
LazyColumn (
|
||||
contentPadding = PaddingValues(0.dp, 8.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
|
@ -186,9 +179,6 @@ fun OverviewScreen(
|
|||
}
|
||||
|
||||
Row {
|
||||
val auth = activity.getActivityViewModel()
|
||||
val logic = auth.logic
|
||||
|
||||
TextButton(onClick = {
|
||||
screen.actions.skipTaskReview(screen.taskToReview)
|
||||
}) {
|
||||
|
@ -203,23 +193,7 @@ fun OverviewScreen(
|
|||
|
||||
Spacer(Modifier.width(8.dp))
|
||||
|
||||
OutlinedButton(onClick = {
|
||||
if (activity.getActivityViewModel().isParentAuthenticated()) {
|
||||
if (screen.taskToReview.hasPremium) {
|
||||
val time = logic.timeApi.getCurrentTimeInMillis()
|
||||
val day = DateInTimezone.newInstance(time, screen.taskToReview.childTimezone).dayOfEpoch
|
||||
|
||||
auth.tryDispatchParentAction(
|
||||
ReviewChildTaskAction(
|
||||
taskId = screen.taskToReview.task.childTask.taskId,
|
||||
ok = true,
|
||||
time = time,
|
||||
day = if (screen.taskToReview.serverApiLevel.hasLevelOrIsOffline(2)) day else null
|
||||
)
|
||||
)
|
||||
} else RequiresPurchaseDialogFragment().show(activity.supportFragmentManager)
|
||||
} else activity.showAuthenticationScreen()
|
||||
}) {
|
||||
OutlinedButton(onClick = { screen.actions.reviewAccept(screen.taskToReview) }) {
|
||||
Text(stringResource(R.string.generic_yes))
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue