Use EventSink lambda in state instead of Flow in Presenter
This commit is contained in:
parent
56e54bb172
commit
1a0c9df1da
43 changed files with 277 additions and 490 deletions
|
|
@ -25,8 +25,8 @@ import com.bumble.appyx.navmodel.backstack.operation.pop
|
||||||
import com.bumble.appyx.navmodel.backstack.operation.push
|
import com.bumble.appyx.navmodel.backstack.operation.push
|
||||||
import io.element.android.x.architecture.createNode
|
import io.element.android.x.architecture.createNode
|
||||||
import io.element.android.x.architecture.presenterConnector
|
import io.element.android.x.architecture.presenterConnector
|
||||||
|
import io.element.android.x.core.compose.OnLifecycleEvent
|
||||||
import io.element.android.x.core.di.DaggerComponentOwner
|
import io.element.android.x.core.di.DaggerComponentOwner
|
||||||
import io.element.android.x.core.screenshot.ImageResult
|
|
||||||
import io.element.android.x.di.SessionComponentsOwner
|
import io.element.android.x.di.SessionComponentsOwner
|
||||||
import io.element.android.x.features.rageshake.bugreport.BugReportNode
|
import io.element.android.x.features.rageshake.bugreport.BugReportNode
|
||||||
import io.element.android.x.matrix.Matrix
|
import io.element.android.x.matrix.Matrix
|
||||||
|
|
@ -77,22 +77,6 @@ class RootFlowNode(
|
||||||
|
|
||||||
private val presenterConnector = presenterConnector(rootPresenter)
|
private val presenterConnector = presenterConnector(rootPresenter)
|
||||||
|
|
||||||
init {
|
|
||||||
Timber.v("Init")
|
|
||||||
lifecycle.subscribe(
|
|
||||||
onCreate = { Timber.v("OnCreate") },
|
|
||||||
onResume = {
|
|
||||||
Timber.v("OnResume")
|
|
||||||
presenterConnector.emitEvent(RootEvents.StartRageshakeDetection)
|
|
||||||
},
|
|
||||||
onPause = {
|
|
||||||
Timber.v("OnPause")
|
|
||||||
presenterConnector.emitEvent(RootEvents.StopRageshakeDetection)
|
|
||||||
},
|
|
||||||
onDestroy = { Timber.v("OnDestroy") }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
matrix.isLoggedIn()
|
matrix.isLoggedIn()
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
|
|
@ -114,47 +98,21 @@ class RootFlowNode(
|
||||||
.launchIn(lifecycleScope)
|
.launchIn(lifecycleScope)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun hideShowkaseButton() {
|
|
||||||
presenterConnector.emitEvent(RootEvents.HideShowkaseButton)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onOpenBugReport() {
|
private fun onOpenBugReport() {
|
||||||
presenterConnector.emitEvent(RootEvents.ResetAppHasCrashed)
|
|
||||||
backstack.push(NavTarget.BugReport)
|
backstack.push(NavTarget.BugReport)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onCrashDetectedDismissed() {
|
|
||||||
presenterConnector.emitEvent(RootEvents.ResetAllCrashData)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDismissRageshake() {
|
|
||||||
presenterConnector.emitEvent(RootEvents.DismissRageshake)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDisableRageshake() {
|
|
||||||
presenterConnector.emitEvent(RootEvents.DisableRageshake)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state by presenterConnector.stateFlow.collectAsState()
|
val state by presenterConnector.stateFlow.collectAsState()
|
||||||
RootView(
|
RootView(
|
||||||
state = state,
|
state = state,
|
||||||
onHideShowkaseClicked = this::hideShowkaseButton,
|
|
||||||
onOpenBugReport = this::onOpenBugReport,
|
onOpenBugReport = this::onOpenBugReport,
|
||||||
onCrashDetectedDismissed = this::onCrashDetectedDismissed,
|
|
||||||
onDisableRageshake = this::onDisableRageshake,
|
|
||||||
onDismissRageshake = this::onDismissRageshake,
|
|
||||||
onScreenshotTaken = this::onScreenshotTaken
|
|
||||||
) {
|
) {
|
||||||
Children(navModel = backstack)
|
Children(navModel = backstack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onScreenshotTaken(imageResult: ImageResult) {
|
|
||||||
presenterConnector.emitEvent(RootEvents.ProcessScreenshot(imageResult))
|
|
||||||
}
|
|
||||||
|
|
||||||
private val bugReportNodeCallback = object : BugReportNode.Callback {
|
private val bugReportNodeCallback = object : BugReportNode.Callback {
|
||||||
override fun onBugReportSent() {
|
override fun onBugReportSent() {
|
||||||
backstack.pop()
|
backstack.pop()
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,5 @@
|
||||||
package io.element.android.x.root
|
package io.element.android.x.root
|
||||||
|
|
||||||
import io.element.android.x.core.screenshot.ImageResult
|
|
||||||
|
|
||||||
sealed interface RootEvents {
|
sealed interface RootEvents {
|
||||||
data class ProcessScreenshot(val imageResult: ImageResult) : RootEvents
|
object HideShowkaseButton : RootEvents
|
||||||
object HideShowkaseButton: RootEvents
|
|
||||||
object ResetAllCrashData : RootEvents
|
|
||||||
object ResetAppHasCrashed: RootEvents
|
|
||||||
object DisableRageshake: RootEvents
|
|
||||||
object DismissRageshake: RootEvents
|
|
||||||
object StartRageshakeDetection: RootEvents
|
|
||||||
object StopRageshakeDetection: RootEvents
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,45 @@
|
||||||
package io.element.android.x.root
|
package io.element.android.x.root
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import io.element.android.x.architecture.Presenter
|
import io.element.android.x.architecture.Presenter
|
||||||
import io.element.android.x.architecture.SharedFlowHolder
|
|
||||||
import io.element.android.x.features.rageshake.bugreport.BugReportEvents
|
|
||||||
import io.element.android.x.features.rageshake.bugreport.BugReportPresenter
|
import io.element.android.x.features.rageshake.bugreport.BugReportPresenter
|
||||||
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents
|
|
||||||
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionPresenter
|
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionPresenter
|
||||||
import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents
|
|
||||||
import io.element.android.x.features.rageshake.detection.RageshakeDetectionPresenter
|
import io.element.android.x.features.rageshake.detection.RageshakeDetectionPresenter
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RootPresenter @Inject constructor(
|
class RootPresenter @Inject constructor(
|
||||||
private val bugReportPresenter: BugReportPresenter,
|
private val bugReportPresenter: BugReportPresenter,
|
||||||
private val crashDetectionPresenter: CrashDetectionPresenter,
|
private val crashDetectionPresenter: CrashDetectionPresenter,
|
||||||
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter,
|
private val rageshakeDetectionPresenter: RageshakeDetectionPresenter,
|
||||||
) : Presenter<RootState, RootEvents> {
|
) : Presenter<RootState> {
|
||||||
|
|
||||||
private val rageshakeDetectionEventsFlow = SharedFlowHolder<RageshakeDetectionEvents>()
|
|
||||||
private val bugReporterEventsFlow = SharedFlowHolder<BugReportEvents>()
|
|
||||||
private val crashDetectionEventsFlow = SharedFlowHolder<CrashDetectionEvents>()
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(events: Flow<RootEvents>): RootState {
|
override fun present(): RootState {
|
||||||
val isBugReportVisible = rememberSaveable {
|
val isBugReportVisible = rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
val isShowkaseButtonVisible = rememberSaveable {
|
val isShowkaseButtonVisible = rememberSaveable {
|
||||||
mutableStateOf(true)
|
mutableStateOf(true)
|
||||||
}
|
}
|
||||||
val rageshakeDetectionState = rageshakeDetectionPresenter.present(events = rageshakeDetectionEventsFlow.asSharedFlow())
|
val rageshakeDetectionState = rageshakeDetectionPresenter.present()
|
||||||
val crashDetectionState = crashDetectionPresenter.present(events = crashDetectionEventsFlow.asSharedFlow())
|
val crashDetectionState = crashDetectionPresenter.present()
|
||||||
val bugReportState = bugReportPresenter.present(events = bugReporterEventsFlow.asSharedFlow())
|
val bugReportState = bugReportPresenter.present()
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
fun handleEvent(event: RootEvents) {
|
||||||
events.collect { event ->
|
when (event) {
|
||||||
when (event) {
|
RootEvents.HideShowkaseButton -> isShowkaseButtonVisible.value = false
|
||||||
RootEvents.HideShowkaseButton -> isShowkaseButtonVisible.value = false
|
|
||||||
RootEvents.ResetAllCrashData -> crashDetectionEventsFlow.emit(CrashDetectionEvents.ResetAllCrashData)
|
|
||||||
RootEvents.ResetAppHasCrashed -> crashDetectionEventsFlow.emit(CrashDetectionEvents.ResetAppHasCrashed)
|
|
||||||
RootEvents.DisableRageshake -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.Disable)
|
|
||||||
RootEvents.DismissRageshake -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.Dismiss)
|
|
||||||
RootEvents.StartRageshakeDetection -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.StartDetection)
|
|
||||||
RootEvents.StopRageshakeDetection -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.StopDetection)
|
|
||||||
is RootEvents.ProcessScreenshot -> rageshakeDetectionEventsFlow.emit(RageshakeDetectionEvents.ProcessScreenshot(event.imageResult))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return RootState(
|
return RootState(
|
||||||
isBugReportVisible = isBugReportVisible.value,
|
isBugReportVisible = isBugReportVisible.value,
|
||||||
isShowkaseButtonVisible = isShowkaseButtonVisible.value,
|
isShowkaseButtonVisible = isShowkaseButtonVisible.value,
|
||||||
rageshakeDetectionState = rageshakeDetectionState,
|
rageshakeDetectionState = rageshakeDetectionState,
|
||||||
crashDetectionState = crashDetectionState,
|
crashDetectionState = crashDetectionState,
|
||||||
bugReportState = bugReportState
|
bugReportState = bugReportState,
|
||||||
|
eventSink = ::handleEvent
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,5 +11,6 @@ data class RootState(
|
||||||
val isShowkaseButtonVisible: Boolean,
|
val isShowkaseButtonVisible: Boolean,
|
||||||
val rageshakeDetectionState: RageshakeDetectionState,
|
val rageshakeDetectionState: RageshakeDetectionState,
|
||||||
val crashDetectionState: CrashDetectionState,
|
val crashDetectionState: CrashDetectionState,
|
||||||
val bugReportState: BugReportState
|
val bugReportState: BugReportState,
|
||||||
|
val eventSink: (RootEvents) -> Unit
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,9 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.airbnb.android.showkase.models.Showkase
|
import com.airbnb.android.showkase.models.Showkase
|
||||||
import io.element.android.x.component.ShowkaseButton
|
import io.element.android.x.component.ShowkaseButton
|
||||||
import io.element.android.x.core.screenshot.ImageResult
|
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionEvents
|
||||||
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView
|
import io.element.android.x.features.rageshake.crash.ui.CrashDetectionView
|
||||||
|
import io.element.android.x.features.rageshake.detection.RageshakeDetectionEvents
|
||||||
import io.element.android.x.features.rageshake.detection.RageshakeDetectionView
|
import io.element.android.x.features.rageshake.detection.RageshakeDetectionView
|
||||||
import io.element.android.x.getBrowserIntent
|
import io.element.android.x.getBrowserIntent
|
||||||
|
|
||||||
|
|
@ -19,12 +20,7 @@ import io.element.android.x.getBrowserIntent
|
||||||
fun RootView(
|
fun RootView(
|
||||||
state: RootState,
|
state: RootState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onHideShowkaseClicked: () -> Unit = { },
|
|
||||||
onOpenBugReport: () -> Unit = {},
|
onOpenBugReport: () -> Unit = {},
|
||||||
onCrashDetectedDismissed: () -> Unit = {},
|
|
||||||
onDisableRageshake: () -> Unit = {},
|
|
||||||
onDismissRageshake: () -> Unit = {},
|
|
||||||
onScreenshotTaken: (ImageResult) -> Unit = {},
|
|
||||||
children: @Composable BoxScope.() -> Unit,
|
children: @Composable BoxScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
Box(
|
Box(
|
||||||
|
|
@ -33,23 +29,27 @@ fun RootView(
|
||||||
contentAlignment = Alignment.TopCenter,
|
contentAlignment = Alignment.TopCenter,
|
||||||
) {
|
) {
|
||||||
children()
|
children()
|
||||||
|
val eventSink = state.eventSink
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
fun onOpenBugReport() {
|
||||||
|
state.crashDetectionState.eventSink(CrashDetectionEvents.ResetAppHasCrashed)
|
||||||
|
state.rageshakeDetectionState.eventSink(RageshakeDetectionEvents.Dismiss)
|
||||||
|
onOpenBugReport.invoke()
|
||||||
|
}
|
||||||
|
|
||||||
ShowkaseButton(
|
ShowkaseButton(
|
||||||
isVisible = state.isShowkaseButtonVisible,
|
isVisible = state.isShowkaseButtonVisible,
|
||||||
onCloseClicked = onHideShowkaseClicked,
|
onCloseClicked = { eventSink(RootEvents.HideShowkaseButton) },
|
||||||
onClick = { ContextCompat.startActivity(context, Showkase.getBrowserIntent(context), null) }
|
onClick = { ContextCompat.startActivity(context, Showkase.getBrowserIntent(context), null) }
|
||||||
)
|
)
|
||||||
RageshakeDetectionView(
|
RageshakeDetectionView(
|
||||||
state = state.rageshakeDetectionState,
|
state = state.rageshakeDetectionState,
|
||||||
onOpenBugReport = onOpenBugReport,
|
onOpenBugReport = ::onOpenBugReport,
|
||||||
onDisableClicked = onDisableRageshake,
|
|
||||||
onNoClicked = onDismissRageshake,
|
|
||||||
onScreenshotTaken = onScreenshotTaken
|
|
||||||
)
|
)
|
||||||
CrashDetectionView(
|
CrashDetectionView(
|
||||||
state = state.crashDetectionState,
|
state = state.crashDetectionState,
|
||||||
onOpenBugReport = onOpenBugReport,
|
onOpenBugReport = ::onOpenBugReport,
|
||||||
onPopupDismissed = onCrashDetectedDismissed
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,14 +22,6 @@ class ChangeServerNode @AssistedInject constructor(
|
||||||
|
|
||||||
private val presenterConnector = presenterConnector(presenter)
|
private val presenterConnector = presenterConnector(presenter)
|
||||||
|
|
||||||
private fun onChangeServer(server: String) {
|
|
||||||
presenterConnector.emitEvent(ChangeServerEvents.SetServer(server))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSubmit() {
|
|
||||||
presenterConnector.emitEvent(ChangeServerEvents.Submit)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSuccess() {
|
private fun onSuccess() {
|
||||||
navigateUp()
|
navigateUp()
|
||||||
}
|
}
|
||||||
|
|
@ -39,8 +31,6 @@ class ChangeServerNode @AssistedInject constructor(
|
||||||
val state by presenterConnector.stateFlow.collectAsState()
|
val state by presenterConnector.stateFlow.collectAsState()
|
||||||
ChangeServerView(
|
ChangeServerView(
|
||||||
state = state,
|
state = state,
|
||||||
onChangeServer = this::onChangeServer,
|
|
||||||
onChangeServerSubmit = this::onSubmit,
|
|
||||||
onChangeServerSuccess = this::onSuccess,
|
onChangeServerSuccess = this::onSuccess,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,42 @@
|
||||||
package io.element.android.x.features.login.changeserver
|
package io.element.android.x.features.login.changeserver
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import io.element.android.x.architecture.Async
|
import io.element.android.x.architecture.Async
|
||||||
import io.element.android.x.architecture.Presenter
|
import io.element.android.x.architecture.Presenter
|
||||||
import io.element.android.x.architecture.execute
|
import io.element.android.x.architecture.execute
|
||||||
import io.element.android.x.matrix.Matrix
|
import io.element.android.x.matrix.Matrix
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class ChangeServerPresenter @Inject constructor(private val matrix: Matrix) : Presenter<ChangeServerState, ChangeServerEvents> {
|
class ChangeServerPresenter @Inject constructor(private val matrix: Matrix) : Presenter<ChangeServerState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(events: Flow<ChangeServerEvents>): ChangeServerState {
|
override fun present(): ChangeServerState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val homeserver = rememberSaveable {
|
val homeserver = rememberSaveable {
|
||||||
mutableStateOf(matrix.getHomeserverOrDefault())
|
mutableStateOf(matrix.getHomeserverOrDefault())
|
||||||
}
|
}
|
||||||
val changeServerAction: MutableState<Async<Unit>> = remember {
|
val changeServerAction: MutableState<Async<Unit>> = remember {
|
||||||
mutableStateOf(Async.Uninitialized)
|
mutableStateOf(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
events.collect { event ->
|
fun handleEvents(event: ChangeServerEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
is ChangeServerEvents.SetServer -> homeserver.value = event.server
|
is ChangeServerEvents.SetServer -> homeserver.value = event.server
|
||||||
ChangeServerEvents.Submit -> submit(homeserver.value, changeServerAction)
|
ChangeServerEvents.Submit -> localCoroutineScope.submit(homeserver.value, changeServerAction)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ChangeServerState(
|
return ChangeServerState(
|
||||||
homeserver = homeserver.value,
|
homeserver = homeserver.value,
|
||||||
changeServerAction = changeServerAction.value
|
changeServerAction = changeServerAction.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import io.element.android.x.architecture.Async
|
||||||
data class ChangeServerState(
|
data class ChangeServerState(
|
||||||
val homeserver: String = "",
|
val homeserver: String = "",
|
||||||
val changeServerAction: Async<Unit> = Async.Uninitialized,
|
val changeServerAction: Async<Unit> = Async.Uninitialized,
|
||||||
|
val eventSink: (ChangeServerEvents) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Async.Loading
|
val submitEnabled = homeserver.isNotEmpty() && changeServerAction !is Async.Loading
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -64,14 +64,13 @@ import io.element.android.x.features.login.error.changeServerError
|
||||||
fun ChangeServerView(
|
fun ChangeServerView(
|
||||||
state: ChangeServerState,
|
state: ChangeServerState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onChangeServer: (String) -> Unit = {},
|
|
||||||
onChangeServerSubmit: () -> Unit = {},
|
|
||||||
onChangeServerSuccess: () -> Unit = {},
|
onChangeServerSuccess: () -> Unit = {},
|
||||||
) {
|
) {
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
) {
|
) {
|
||||||
|
val eventSink = state.eventSink
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -135,7 +134,7 @@ fun ChangeServerView(
|
||||||
.padding(top = 200.dp),
|
.padding(top = 200.dp),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
homeserverFieldState = it
|
homeserverFieldState = it
|
||||||
onChangeServer(it)
|
eventSink(ChangeServerEvents.SetServer(it))
|
||||||
},
|
},
|
||||||
label = {
|
label = {
|
||||||
Text(text = "Server")
|
Text(text = "Server")
|
||||||
|
|
@ -146,7 +145,7 @@ fun ChangeServerView(
|
||||||
imeAction = ImeAction.Done,
|
imeAction = ImeAction.Done,
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onDone = { onChangeServerSubmit() }
|
onDone = { eventSink(ChangeServerEvents.Submit) }
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if (state.changeServerAction is Async.Failure) {
|
if (state.changeServerAction is Async.Failure) {
|
||||||
|
|
@ -161,7 +160,7 @@ fun ChangeServerView(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
Button(
|
Button(
|
||||||
onClick = onChangeServerSubmit,
|
onClick = { eventSink(ChangeServerEvents.Submit) },
|
||||||
enabled = state.submitEnabled,
|
enabled = state.submitEnabled,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import com.bumble.appyx.core.lifecycle.subscribe
|
import com.bumble.appyx.core.lifecycle.subscribe
|
||||||
import com.bumble.appyx.core.modality.BuildContext
|
import com.bumble.appyx.core.modality.BuildContext
|
||||||
import com.bumble.appyx.core.node.Node
|
import com.bumble.appyx.core.node.Node
|
||||||
|
|
@ -13,6 +14,7 @@ import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import io.element.android.x.anvilannotations.ContributesNode
|
import io.element.android.x.anvilannotations.ContributesNode
|
||||||
import io.element.android.x.architecture.presenterConnector
|
import io.element.android.x.architecture.presenterConnector
|
||||||
|
import io.element.android.x.core.compose.OnLifecycleEvent
|
||||||
import io.element.android.x.di.AppScope
|
import io.element.android.x.di.AppScope
|
||||||
|
|
||||||
@ContributesNode(AppScope::class)
|
@ContributesNode(AppScope::class)
|
||||||
|
|
@ -24,12 +26,6 @@ class LoginRootNode @AssistedInject constructor(
|
||||||
|
|
||||||
private val presenterConnector = presenterConnector(presenter)
|
private val presenterConnector = presenterConnector(presenter)
|
||||||
|
|
||||||
init {
|
|
||||||
lifecycle.subscribe(
|
|
||||||
onResume = { presenterConnector.emitEvent(LoginRootEvents.RefreshHomeServer) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onChangeHomeServer()
|
fun onChangeHomeServer()
|
||||||
}
|
}
|
||||||
|
|
@ -38,27 +34,18 @@ class LoginRootNode @AssistedInject constructor(
|
||||||
plugins<Callback>().forEach { it.onChangeHomeServer() }
|
plugins<Callback>().forEach { it.onChangeHomeServer() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onLoginChanged(login: String) {
|
|
||||||
presenterConnector.emitEvent(LoginRootEvents.SetLogin(login))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onPasswordChanged(password: String) {
|
|
||||||
presenterConnector.emitEvent(LoginRootEvents.SetPassword(password))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSubmit() {
|
|
||||||
presenterConnector.emitEvent(LoginRootEvents.Submit)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun View(modifier: Modifier) {
|
override fun View(modifier: Modifier) {
|
||||||
val state by presenterConnector.stateFlow.collectAsState()
|
val state by presenterConnector.stateFlow.collectAsState()
|
||||||
|
OnLifecycleEvent { _, event ->
|
||||||
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_RESUME -> state.eventSink(LoginRootEvents.RefreshHomeServer)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
LoginRootScreen(
|
LoginRootScreen(
|
||||||
state = state,
|
state = state,
|
||||||
onChangeServer = this::onChangeHomeServer,
|
onChangeServer = this::onChangeHomeServer,
|
||||||
onLoginChanged = this::onLoginChanged,
|
|
||||||
onPasswordChanged = this::onPasswordChanged,
|
|
||||||
onSubmitClicked = this::onSubmit
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import io.element.android.x.architecture.Presenter
|
import io.element.android.x.architecture.Presenter
|
||||||
import io.element.android.x.matrix.Matrix
|
import io.element.android.x.matrix.Matrix
|
||||||
|
|
@ -13,10 +14,11 @@ import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Presenter<LoginRootState, LoginRootEvents> {
|
class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Presenter<LoginRootState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(events: Flow<LoginRootEvents>): LoginRootState {
|
override fun present(): LoginRootState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val homeserver = rememberSaveable {
|
val homeserver = rememberSaveable {
|
||||||
mutableStateOf(matrix.getHomeserverOrDefault())
|
mutableStateOf(matrix.getHomeserverOrDefault())
|
||||||
}
|
}
|
||||||
|
|
@ -27,24 +29,24 @@ class LoginRootPresenter @Inject constructor(private val matrix: Matrix) : Prese
|
||||||
mutableStateOf(LoginFormState.Default)
|
mutableStateOf(LoginFormState.Default)
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
fun handleEvents(event: LoginRootEvents){
|
||||||
events.collect { event ->
|
when (event) {
|
||||||
when (event) {
|
LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver)
|
||||||
LoginRootEvents.RefreshHomeServer -> refreshHomeServer(homeserver)
|
is LoginRootEvents.SetLogin -> updateFormState(formState) {
|
||||||
is LoginRootEvents.SetLogin -> updateFormState(formState) {
|
copy(login = event.login)
|
||||||
copy(login = event.login)
|
|
||||||
}
|
|
||||||
is LoginRootEvents.SetPassword -> updateFormState(formState) {
|
|
||||||
copy(password = event.password)
|
|
||||||
}
|
|
||||||
LoginRootEvents.Submit -> submit(homeserver.value, formState.value, loggedInState)
|
|
||||||
}
|
}
|
||||||
|
is LoginRootEvents.SetPassword -> updateFormState(formState) {
|
||||||
|
copy(password = event.password)
|
||||||
|
}
|
||||||
|
LoginRootEvents.Submit -> localCoroutineScope.submit(homeserver.value, formState.value, loggedInState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return LoginRootState(
|
return LoginRootState(
|
||||||
homeserver = homeserver.value,
|
homeserver = homeserver.value,
|
||||||
loggedInState = loggedInState.value,
|
loggedInState = loggedInState.value,
|
||||||
formState = formState.value
|
formState = formState.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -68,11 +68,9 @@ fun LoginRootScreen(
|
||||||
state: LoginRootState,
|
state: LoginRootState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onChangeServer: () -> Unit = {},
|
onChangeServer: () -> Unit = {},
|
||||||
onLoginChanged: (String) -> Unit = {},
|
|
||||||
onPasswordChanged: (String) -> Unit = {},
|
|
||||||
onSubmitClicked: () -> Unit = {},
|
|
||||||
onLoginWithSuccess: (SessionId) -> Unit = {},
|
onLoginWithSuccess: (SessionId) -> Unit = {},
|
||||||
) {
|
) {
|
||||||
|
val eventSink = state.eventSink
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
color = MaterialTheme.colorScheme.background,
|
color = MaterialTheme.colorScheme.background,
|
||||||
|
|
@ -144,7 +142,7 @@ fun LoginRootScreen(
|
||||||
},
|
},
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
loginFieldState = it
|
loginFieldState = it
|
||||||
onLoginChanged(it)
|
eventSink(LoginRootEvents.SetLogin(it))
|
||||||
},
|
},
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
keyboardType = KeyboardType.Email,
|
keyboardType = KeyboardType.Email,
|
||||||
|
|
@ -163,7 +161,7 @@ fun LoginRootScreen(
|
||||||
.padding(top = 24.dp),
|
.padding(top = 24.dp),
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
passwordFieldState = it
|
passwordFieldState = it
|
||||||
onPasswordChanged(it)
|
eventSink(LoginRootEvents.SetPassword(it))
|
||||||
},
|
},
|
||||||
label = {
|
label = {
|
||||||
Text(text = "Password")
|
Text(text = "Password")
|
||||||
|
|
@ -185,7 +183,7 @@ fun LoginRootScreen(
|
||||||
imeAction = ImeAction.Done,
|
imeAction = ImeAction.Done,
|
||||||
),
|
),
|
||||||
keyboardActions = KeyboardActions(
|
keyboardActions = KeyboardActions(
|
||||||
onDone = { onSubmitClicked() }
|
onDone = { eventSink(LoginRootEvents.Submit) }
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
if (state.loggedInState is LoggedInState.ErrorLoggingIn) {
|
if (state.loggedInState is LoggedInState.ErrorLoggingIn) {
|
||||||
|
|
@ -199,7 +197,7 @@ fun LoginRootScreen(
|
||||||
}
|
}
|
||||||
// Submit
|
// Submit
|
||||||
Button(
|
Button(
|
||||||
onClick = onSubmitClicked,
|
onClick = { eventSink(LoginRootEvents.Submit) },
|
||||||
enabled = state.submitEnabled,
|
enabled = state.submitEnabled,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ data class LoginRootState(
|
||||||
val homeserver: String = "",
|
val homeserver: String = "",
|
||||||
val loggedInState: LoggedInState = LoggedInState.NotLoggedIn,
|
val loggedInState: LoggedInState = LoggedInState.NotLoggedIn,
|
||||||
val formState: LoginFormState = LoginFormState.Default,
|
val formState: LoginFormState = LoginFormState.Default,
|
||||||
|
val eventSink: (LoginRootEvents) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val submitEnabled =
|
val submitEnabled =
|
||||||
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn
|
formState.login.isNotEmpty() && formState.password.isNotEmpty() && loggedInState != LoggedInState.LoggingIn
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,36 @@
|
||||||
package io.element.android.x.features.logout
|
package io.element.android.x.features.logout
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import io.element.android.x.architecture.Async
|
import io.element.android.x.architecture.Async
|
||||||
import io.element.android.x.architecture.Presenter
|
import io.element.android.x.architecture.Presenter
|
||||||
import io.element.android.x.architecture.execute
|
import io.element.android.x.architecture.execute
|
||||||
import io.element.android.x.matrix.MatrixClient
|
import io.element.android.x.matrix.MatrixClient
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : Presenter<LogoutPreferenceState, LogoutPreferenceEvents> {
|
class LogoutPreferencePresenter @Inject constructor(private val matrixClient: MatrixClient) : Presenter<LogoutPreferenceState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(events: Flow<LogoutPreferenceEvents>): LogoutPreferenceState {
|
override fun present(): LogoutPreferenceState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val logoutAction: MutableState<Async<Unit>> = remember {
|
val logoutAction: MutableState<Async<Unit>> = remember {
|
||||||
mutableStateOf(Async.Uninitialized)
|
mutableStateOf(Async.Uninitialized)
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
events.collect { event ->
|
fun handleEvents(event: LogoutPreferenceEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
LogoutPreferenceEvents.Logout -> logout(logoutAction)
|
LogoutPreferenceEvents.Logout -> localCoroutineScope.logout(logoutAction)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return LogoutPreferenceState(
|
return LogoutPreferenceState(
|
||||||
logoutAction = logoutAction.value
|
logoutAction = logoutAction.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ package io.element.android.x.features.logout
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Logout
|
import androidx.compose.material.icons.filled.Logout
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -34,14 +35,15 @@ import io.element.android.x.element.resources.R as ElementR
|
||||||
@Composable
|
@Composable
|
||||||
fun LogoutPreferenceView(
|
fun LogoutPreferenceView(
|
||||||
state: LogoutPreferenceState,
|
state: LogoutPreferenceState,
|
||||||
onLogoutClicked: () -> Unit = {},
|
onSuccessLogout: () -> Unit = {}
|
||||||
onSuccessLogout: () -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
|
val eventSink = state.eventSink
|
||||||
if (state.logoutAction is Async.Success) {
|
if (state.logoutAction is Async.Success) {
|
||||||
onSuccessLogout()
|
LaunchedEffect(state.logoutAction) {
|
||||||
|
onSuccessLogout()
|
||||||
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val openDialog = remember { mutableStateOf(false) }
|
val openDialog = remember { mutableStateOf(false) }
|
||||||
|
|
||||||
LogoutPreferenceContent(
|
LogoutPreferenceContent(
|
||||||
|
|
@ -61,7 +63,7 @@ fun LogoutPreferenceView(
|
||||||
},
|
},
|
||||||
onSubmitClicked = {
|
onSubmitClicked = {
|
||||||
openDialog.value = false
|
openDialog.value = false
|
||||||
onLogoutClicked()
|
eventSink(LogoutPreferenceEvents.Logout)
|
||||||
},
|
},
|
||||||
onDismiss = {
|
onDismiss = {
|
||||||
openDialog.value = false
|
openDialog.value = false
|
||||||
|
|
|
||||||
|
|
@ -20,4 +20,5 @@ import io.element.android.x.architecture.Async
|
||||||
|
|
||||||
data class LogoutPreferenceState(
|
data class LogoutPreferenceState(
|
||||||
val logoutAction: Async<Unit> = Async.Uninitialized,
|
val logoutAction: Async<Unit> = Async.Uninitialized,
|
||||||
|
val eventSink: (LogoutPreferenceEvents) -> Unit = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.x.features.onboarding
|
|
||||||
|
|
||||||
import com.airbnb.mvrx.MavericksViewModel
|
|
||||||
|
|
||||||
class OnBoardingViewModel(initialState: OnBoardingViewState) :
|
|
||||||
MavericksViewModel<OnBoardingViewState>(initialState) {
|
|
||||||
|
|
||||||
fun onPageChanged(page: Int) {
|
|
||||||
setState {
|
|
||||||
copy(
|
|
||||||
currentPage = page,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
/*
|
|
||||||
* Copyright (c) 2022 New Vector Ltd
|
|
||||||
*
|
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
* you may not use this file except in compliance with the License.
|
|
||||||
* You may obtain a copy of the License at
|
|
||||||
*
|
|
||||||
* http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
*
|
|
||||||
* Unless required by applicable law or agreed to in writing, software
|
|
||||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
* See the License for the specific language governing permissions and
|
|
||||||
* limitations under the License.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package io.element.android.x.features.onboarding
|
|
||||||
|
|
||||||
import com.airbnb.mvrx.MavericksState
|
|
||||||
|
|
||||||
data class OnBoardingViewState(
|
|
||||||
val currentPage: Int = 0,
|
|
||||||
) : MavericksState
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
package io.element.android.x.features.preferences.root
|
|
||||||
|
|
||||||
sealed interface PreferencesRootEvents {
|
|
||||||
object Logout : PreferencesRootEvents
|
|
||||||
data class SetRageshakeSensitivity(val sensitivity: Float) : PreferencesRootEvents
|
|
||||||
data class SetRageshakeEnabled(val enabled: Boolean) : PreferencesRootEvents
|
|
||||||
}
|
|
||||||
|
|
@ -21,24 +21,12 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
private val presenter: PreferencesRootPresenter,
|
private val presenter: PreferencesRootPresenter,
|
||||||
) : Node(buildContext, plugins = plugins) {
|
) : Node(buildContext, plugins = plugins) {
|
||||||
|
|
||||||
public interface Callback : Plugin {
|
interface Callback : Plugin {
|
||||||
fun onOpenBugReport()
|
fun onOpenBugReport()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val presenterConnector = presenterConnector(presenter)
|
private val presenterConnector = presenterConnector(presenter)
|
||||||
|
|
||||||
private fun onLogoutClicked() {
|
|
||||||
presenterConnector.emitEvent(PreferencesRootEvents.Logout)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onRageshakeEnabledChanged(isEnabled: Boolean) {
|
|
||||||
presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeEnabled(isEnabled))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onRageshakeSensitivityChanged(sensitivity: Float) {
|
|
||||||
presenterConnector.emitEvent(PreferencesRootEvents.SetRageshakeSensitivity(sensitivity))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onOpenBugReport() {
|
private fun onOpenBugReport() {
|
||||||
plugins<Callback>().forEach { it.onOpenBugReport() }
|
plugins<Callback>().forEach { it.onOpenBugReport() }
|
||||||
}
|
}
|
||||||
|
|
@ -48,10 +36,7 @@ class PreferencesRootNode @AssistedInject constructor(
|
||||||
val state by presenterConnector.stateFlow.collectAsState()
|
val state by presenterConnector.stateFlow.collectAsState()
|
||||||
PreferencesRootView(
|
PreferencesRootView(
|
||||||
state = state,
|
state = state,
|
||||||
onLogoutClicked = this::onLogoutClicked,
|
|
||||||
onBackPressed = this::navigateUp,
|
onBackPressed = this::navigateUp,
|
||||||
onRageshakeEnabledChanged = this::onRageshakeEnabledChanged,
|
|
||||||
onRageshakeSensitivityChanged = this::onRageshakeSensitivityChanged,
|
|
||||||
onOpenRageShake = this::onOpenBugReport
|
onOpenRageShake = this::onOpenBugReport
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,42 +1,26 @@
|
||||||
package io.element.android.x.features.preferences.root
|
package io.element.android.x.features.preferences.root
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import io.element.android.x.architecture.Async
|
import io.element.android.x.architecture.Async
|
||||||
import io.element.android.x.architecture.Presenter
|
import io.element.android.x.architecture.Presenter
|
||||||
import io.element.android.x.architecture.SharedFlowHolder
|
|
||||||
import io.element.android.x.features.logout.LogoutPreferenceEvents
|
|
||||||
import io.element.android.x.features.logout.LogoutPreferencePresenter
|
import io.element.android.x.features.logout.LogoutPreferencePresenter
|
||||||
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents
|
|
||||||
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class PreferencesRootPresenter @Inject constructor(
|
class PreferencesRootPresenter @Inject constructor(
|
||||||
private val logoutPresenter: LogoutPreferencePresenter,
|
private val logoutPresenter: LogoutPreferencePresenter,
|
||||||
private val rageshakePresenter: RageshakePreferencesPresenter,
|
private val rageshakePresenter: RageshakePreferencesPresenter,
|
||||||
) : Presenter<PreferencesRootState, PreferencesRootEvents> {
|
) : Presenter<PreferencesRootState> {
|
||||||
|
|
||||||
private val logoutEventsFlow = SharedFlowHolder<LogoutPreferenceEvents>()
|
|
||||||
private val rageshakeEventsFlow = SharedFlowHolder<RageshakePreferencesEvents>()
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(events: Flow<PreferencesRootEvents>): PreferencesRootState {
|
override fun present(): PreferencesRootState {
|
||||||
val logoutState = logoutPresenter.present(events = logoutEventsFlow.asSharedFlow())
|
val logoutState = logoutPresenter.present()
|
||||||
val rageshakeState = rageshakePresenter.present(events = rageshakeEventsFlow.asSharedFlow())
|
val rageshakeState = rageshakePresenter.present()
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
events.collect { event ->
|
|
||||||
when (event) {
|
|
||||||
PreferencesRootEvents.Logout -> logoutEventsFlow.emit(LogoutPreferenceEvents.Logout)
|
|
||||||
is PreferencesRootEvents.SetRageshakeEnabled -> rageshakeEventsFlow.emit(RageshakePreferencesEvents.SetIsEnabled(event.enabled))
|
|
||||||
is PreferencesRootEvents.SetRageshakeSensitivity -> rageshakeEventsFlow.emit(RageshakePreferencesEvents.SetSensitivity(event.sensitivity))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return PreferencesRootState(
|
return PreferencesRootState(
|
||||||
logoutState = logoutState,
|
logoutState = logoutState,
|
||||||
rageshakeState = rageshakeState,
|
rageshakeState = rageshakeState,
|
||||||
myUser = Async.Uninitialized
|
myUser = Async.Uninitialized,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import io.element.android.x.element.resources.R
|
||||||
import io.element.android.x.features.logout.LogoutPreferenceState
|
import io.element.android.x.features.logout.LogoutPreferenceState
|
||||||
import io.element.android.x.features.logout.LogoutPreferenceView
|
import io.element.android.x.features.logout.LogoutPreferenceView
|
||||||
import io.element.android.x.features.preferences.user.UserPreferences
|
import io.element.android.x.features.preferences.user.UserPreferences
|
||||||
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents
|
||||||
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesState
|
||||||
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesView
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesView
|
||||||
|
|
||||||
|
|
@ -18,10 +19,7 @@ fun PreferencesRootView(
|
||||||
state: PreferencesRootState,
|
state: PreferencesRootState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onBackPressed: () -> Unit = {},
|
onBackPressed: () -> Unit = {},
|
||||||
onLogoutClicked: () -> Unit = {},
|
|
||||||
onOpenRageShake: () -> Unit = {},
|
onOpenRageShake: () -> Unit = {},
|
||||||
onRageshakeEnabledChanged: (Boolean) -> Unit = {},
|
|
||||||
onRageshakeSensitivityChanged: (Float) -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
// TODO Hierarchy!
|
// TODO Hierarchy!
|
||||||
// Include pref from other modules
|
// Include pref from other modules
|
||||||
|
|
@ -34,12 +32,9 @@ fun PreferencesRootView(
|
||||||
RageshakePreferencesView(
|
RageshakePreferencesView(
|
||||||
state = state.rageshakeState,
|
state = state.rageshakeState,
|
||||||
onOpenRageshake = onOpenRageShake,
|
onOpenRageshake = onOpenRageShake,
|
||||||
onSensitivityChanged = onRageshakeSensitivityChanged,
|
|
||||||
onIsEnabledChanged = onRageshakeEnabledChanged,
|
|
||||||
)
|
)
|
||||||
LogoutPreferenceView(
|
LogoutPreferenceView(
|
||||||
state = state.logoutState,
|
state = state.logoutState,
|
||||||
onLogoutClicked = onLogoutClicked,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,43 +33,12 @@ class BugReportNode @AssistedInject constructor(
|
||||||
BugReportView(
|
BugReportView(
|
||||||
state = state,
|
state = state,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onDescriptionChanged = this::onDescriptionChanged,
|
|
||||||
onSetSendLog = this::onSetSendLog,
|
|
||||||
onSetSendCrashLog = this::onSetSendCrashLog,
|
|
||||||
onSetCanContact = this::onSetCanContact,
|
|
||||||
onSetSendScreenshot = this::onSetSendScreenshot,
|
|
||||||
onSubmit = this::onSubmit,
|
|
||||||
onDone = this::onDone
|
onDone = this::onDone
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onDone() {
|
private fun onDone() {
|
||||||
presenterConnector.emitEvent(BugReportEvents.ResetAll)
|
|
||||||
plugins<Callback>().forEach { it.onBugReportSent() }
|
plugins<Callback>().forEach { it.onBugReportSent() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onSubmit() {
|
|
||||||
presenterConnector.emitEvent(BugReportEvents.SendBugReport)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSetSendLog(sendLog: Boolean) {
|
|
||||||
presenterConnector.emitEvent(BugReportEvents.SetSendLog(sendLog))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSetSendCrashLog(sendCrashLog: Boolean) {
|
|
||||||
presenterConnector.emitEvent(BugReportEvents.SetSendCrashLog(sendCrashLog))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSetSendScreenshot(sendScreenshot: Boolean) {
|
|
||||||
presenterConnector.emitEvent(BugReportEvents.SetSendScreenshot(sendScreenshot))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onSetCanContact(canContact: Boolean) {
|
|
||||||
presenterConnector.emitEvent(BugReportEvents.SetCanContact(canContact))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDescriptionChanged(description: String) {
|
|
||||||
presenterConnector.emitEvent(BugReportEvents.SetDescription(description))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,8 @@
|
||||||
package io.element.android.x.features.rageshake.bugreport
|
package io.element.android.x.features.rageshake.bugreport
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
|
@ -18,7 +16,6 @@ import io.element.android.x.features.rageshake.reporter.BugReporter
|
||||||
import io.element.android.x.features.rageshake.reporter.ReportType
|
import io.element.android.x.features.rageshake.reporter.ReportType
|
||||||
import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder
|
import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
|
@ -27,7 +24,7 @@ class BugReportPresenter @Inject constructor(
|
||||||
private val crashDataStore: CrashDataStore,
|
private val crashDataStore: CrashDataStore,
|
||||||
private val screenshotHolder: ScreenshotHolder,
|
private val screenshotHolder: ScreenshotHolder,
|
||||||
private val appCoroutineScope: CoroutineScope,
|
private val appCoroutineScope: CoroutineScope,
|
||||||
) : Presenter<BugReportState, BugReportEvents> {
|
) : Presenter<BugReportState> {
|
||||||
|
|
||||||
private class BugReporterUploadListener(
|
private class BugReporterUploadListener(
|
||||||
private val sendingProgress: MutableState<Float>,
|
private val sendingProgress: MutableState<Float>,
|
||||||
|
|
@ -56,7 +53,7 @@ class BugReportPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(events: Flow<BugReportEvents>): BugReportState {
|
override fun present(): BugReportState {
|
||||||
val screenshotUri = rememberSaveable {
|
val screenshotUri = rememberSaveable {
|
||||||
mutableStateOf(
|
mutableStateOf(
|
||||||
screenshotHolder.getFile()?.toUri()?.toString()
|
screenshotHolder.getFile()?.toUri()?.toString()
|
||||||
|
|
@ -76,58 +73,54 @@ class BugReportPresenter @Inject constructor(
|
||||||
mutableStateOf(BugReportFormState.Default)
|
mutableStateOf(BugReportFormState.Default)
|
||||||
}
|
}
|
||||||
val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction)
|
val uploadListener = BugReporterUploadListener(sendingProgress, sendingAction)
|
||||||
val state by remember {
|
|
||||||
derivedStateOf {
|
fun handleEvents(event: BugReportEvents) {
|
||||||
BugReportState(
|
when (event) {
|
||||||
hasCrashLogs = crashInfo.isNotEmpty(),
|
BugReportEvents.SendBugReport -> appCoroutineScope.sendBugReport(formState.value, crashInfo.isNotEmpty(), uploadListener)
|
||||||
sendingProgress = sendingProgress.value,
|
BugReportEvents.ResetAll -> appCoroutineScope.resetAll()
|
||||||
sending = sendingAction.value,
|
is BugReportEvents.SetDescription -> updateFormState(formState) {
|
||||||
formState = formState.value,
|
copy(description = event.description)
|
||||||
screenshotUri = screenshotUri.value
|
}
|
||||||
)
|
is BugReportEvents.SetCanContact -> updateFormState(formState) {
|
||||||
}
|
copy(canContact = event.canContact)
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
is BugReportEvents.SetSendCrashLog -> updateFormState(formState) {
|
||||||
events.collect { event ->
|
copy(sendCrashLogs = event.sendCrashlog)
|
||||||
when (event) {
|
}
|
||||||
BugReportEvents.SendBugReport -> appCoroutineScope.sendBugReport(state, uploadListener)
|
is BugReportEvents.SetSendLog -> updateFormState(formState) {
|
||||||
BugReportEvents.ResetAll -> appCoroutineScope.resetAll()
|
copy(sendLogs = event.sendLog)
|
||||||
is BugReportEvents.SetDescription -> updateFormState(formState) {
|
}
|
||||||
copy(description = event.description)
|
is BugReportEvents.SetSendScreenshot -> updateFormState(formState) {
|
||||||
}
|
copy(sendScreenshot = event.sendScreenshot)
|
||||||
is BugReportEvents.SetCanContact -> updateFormState(formState) {
|
|
||||||
copy(canContact = event.canContact)
|
|
||||||
}
|
|
||||||
is BugReportEvents.SetSendCrashLog -> updateFormState(formState) {
|
|
||||||
copy(sendCrashLogs = event.sendCrashlog)
|
|
||||||
}
|
|
||||||
is BugReportEvents.SetSendLog -> updateFormState(formState) {
|
|
||||||
copy(sendLogs = event.sendLog)
|
|
||||||
}
|
|
||||||
is BugReportEvents.SetSendScreenshot -> updateFormState(formState) {
|
|
||||||
copy(sendScreenshot = event.sendScreenshot)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return state
|
|
||||||
|
return BugReportState(
|
||||||
|
hasCrashLogs = crashInfo.isNotEmpty(),
|
||||||
|
sendingProgress = sendingProgress.value,
|
||||||
|
sending = sendingAction.value,
|
||||||
|
formState = formState.value,
|
||||||
|
screenshotUri = screenshotUri.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateFormState(formState: MutableState<BugReportFormState>, operation: BugReportFormState.() -> BugReportFormState) {
|
private fun updateFormState(formState: MutableState<BugReportFormState>, operation: BugReportFormState.() -> BugReportFormState) {
|
||||||
formState.value = operation(formState.value)
|
formState.value = operation(formState.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.sendBugReport(state: BugReportState, listener: BugReporter.IMXBugReportListener) = launch {
|
private fun CoroutineScope.sendBugReport(formState: BugReportFormState, hasCrashLogs: Boolean, listener: BugReporter.IMXBugReportListener) = launch {
|
||||||
bugReporter.sendBugReport(
|
bugReporter.sendBugReport(
|
||||||
coroutineScope = this,
|
coroutineScope = this,
|
||||||
reportType = ReportType.BUG_REPORT,
|
reportType = ReportType.BUG_REPORT,
|
||||||
withDevicesLogs = state.formState.sendLogs,
|
withDevicesLogs = formState.sendLogs,
|
||||||
withCrashLogs = state.hasCrashLogs && state.formState.sendCrashLogs,
|
withCrashLogs = hasCrashLogs && formState.sendCrashLogs,
|
||||||
withKeyRequestHistory = false,
|
withKeyRequestHistory = false,
|
||||||
withScreenshot = state.formState.sendScreenshot,
|
withScreenshot = formState.sendScreenshot,
|
||||||
theBugDescription = state.formState.description,
|
theBugDescription = formState.description,
|
||||||
serverVersion = "",
|
serverVersion = "",
|
||||||
canContact = state.formState.canContact,
|
canContact = formState.canContact,
|
||||||
customFields = emptyMap(),
|
customFields = emptyMap(),
|
||||||
listener = listener
|
listener = listener
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ data class BugReportState(
|
||||||
val screenshotUri: String? = null,
|
val screenshotUri: String? = null,
|
||||||
val sendingProgress: Float = 0F,
|
val sendingProgress: Float = 0F,
|
||||||
val sending: Async<Unit> = Async.Uninitialized,
|
val sending: Async<Unit> = Async.Uninitialized,
|
||||||
|
val eventSink: (BugReportEvents) -> Unit = {}
|
||||||
) {
|
) {
|
||||||
val submitEnabled =
|
val submitEnabled =
|
||||||
formState.description.length > 10 && sending !is Async.Loading
|
formState.description.length > 10 && sending !is Async.Loading
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
* limitations under the License.
|
* limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
@file:OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
|
|
||||||
package io.element.android.x.features.rageshake.bugreport
|
package io.element.android.x.features.rageshake.bugreport
|
||||||
|
|
||||||
|
|
@ -36,6 +35,7 @@ import androidx.compose.material3.OutlinedTextField
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
|
@ -59,22 +59,21 @@ import io.element.android.x.designsystem.components.LabelledCheckbox
|
||||||
import io.element.android.x.designsystem.components.dialogs.ErrorDialog
|
import io.element.android.x.designsystem.components.dialogs.ErrorDialog
|
||||||
import io.element.android.x.element.resources.R as ElementR
|
import io.element.android.x.element.resources.R as ElementR
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun BugReportView(
|
fun BugReportView(
|
||||||
state: BugReportState,
|
state: BugReportState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onDescriptionChanged: (String) -> Unit = {},
|
|
||||||
onSetSendLog: (Boolean) -> Unit = {},
|
|
||||||
onSetSendCrashLog: (Boolean) -> Unit = {},
|
|
||||||
onSetCanContact: (Boolean) -> Unit = {},
|
|
||||||
onSetSendScreenshot: (Boolean) -> Unit = {},
|
|
||||||
onSubmit: () -> Unit = {},
|
|
||||||
onFailureDialogClosed: () -> Unit = { },
|
|
||||||
onDone: () -> Unit = { },
|
onDone: () -> Unit = { },
|
||||||
) {
|
) {
|
||||||
LogCompositions(tag = "Rageshake", msg = "Root")
|
LogCompositions(tag = "Rageshake", msg = "Root")
|
||||||
|
val eventSink = state.eventSink
|
||||||
if (state.sending is Async.Success) {
|
if (state.sending is Async.Success) {
|
||||||
onDone()
|
LaunchedEffect(state.sending) {
|
||||||
|
eventSink(BugReportEvents.ResetAll)
|
||||||
|
onDone()
|
||||||
|
}
|
||||||
|
return
|
||||||
}
|
}
|
||||||
Surface(
|
Surface(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
|
|
@ -132,7 +131,7 @@ fun BugReportView(
|
||||||
},
|
},
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
descriptionFieldState = it
|
descriptionFieldState = it
|
||||||
onDescriptionChanged(it)
|
eventSink(BugReportEvents.SetDescription(it))
|
||||||
},
|
},
|
||||||
keyboardOptions = KeyboardOptions(
|
keyboardOptions = KeyboardOptions(
|
||||||
keyboardType = KeyboardType.Text,
|
keyboardType = KeyboardType.Text,
|
||||||
|
|
@ -143,28 +142,28 @@ fun BugReportView(
|
||||||
}
|
}
|
||||||
LabelledCheckbox(
|
LabelledCheckbox(
|
||||||
checked = state.formState.sendLogs,
|
checked = state.formState.sendLogs,
|
||||||
onCheckedChange = onSetSendLog,
|
onCheckedChange = { eventSink(BugReportEvents.SetSendLog(it)) },
|
||||||
enabled = isFormEnabled,
|
enabled = isFormEnabled,
|
||||||
text = stringResource(id = ElementR.string.send_bug_report_include_logs)
|
text = stringResource(id = ElementR.string.send_bug_report_include_logs)
|
||||||
)
|
)
|
||||||
if (state.hasCrashLogs) {
|
if (state.hasCrashLogs) {
|
||||||
LabelledCheckbox(
|
LabelledCheckbox(
|
||||||
checked = state.formState.sendCrashLogs,
|
checked = state.formState.sendCrashLogs,
|
||||||
onCheckedChange = onSetSendCrashLog,
|
onCheckedChange = { eventSink(BugReportEvents.SetSendCrashLog(it)) },
|
||||||
enabled = isFormEnabled,
|
enabled = isFormEnabled,
|
||||||
text = stringResource(id = ElementR.string.send_bug_report_include_crash_logs)
|
text = stringResource(id = ElementR.string.send_bug_report_include_crash_logs)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LabelledCheckbox(
|
LabelledCheckbox(
|
||||||
checked = state.formState.canContact,
|
checked = state.formState.canContact,
|
||||||
onCheckedChange = onSetCanContact,
|
onCheckedChange = { eventSink(BugReportEvents.SetCanContact(it)) },
|
||||||
enabled = isFormEnabled,
|
enabled = isFormEnabled,
|
||||||
text = stringResource(id = ElementR.string.you_may_contact_me)
|
text = stringResource(id = ElementR.string.you_may_contact_me)
|
||||||
)
|
)
|
||||||
if (state.screenshotUri != null) {
|
if (state.screenshotUri != null) {
|
||||||
LabelledCheckbox(
|
LabelledCheckbox(
|
||||||
checked = state.formState.sendScreenshot,
|
checked = state.formState.sendScreenshot,
|
||||||
onCheckedChange = onSetSendScreenshot,
|
onCheckedChange = { eventSink(BugReportEvents.SetSendScreenshot(it)) },
|
||||||
enabled = isFormEnabled,
|
enabled = isFormEnabled,
|
||||||
text = stringResource(id = ElementR.string.send_bug_report_include_screenshot)
|
text = stringResource(id = ElementR.string.send_bug_report_include_screenshot)
|
||||||
)
|
)
|
||||||
|
|
@ -187,7 +186,7 @@ fun BugReportView(
|
||||||
}
|
}
|
||||||
// Submit
|
// Submit
|
||||||
Button(
|
Button(
|
||||||
onClick = onSubmit,
|
onClick = { eventSink(BugReportEvents.SendBugReport) },
|
||||||
enabled = state.submitEnabled,
|
enabled = state.submitEnabled,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
|
|
@ -197,7 +196,6 @@ fun BugReportView(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
when (state.sending) {
|
when (state.sending) {
|
||||||
Async.Uninitialized -> Unit
|
|
||||||
is Async.Loading -> {
|
is Async.Loading -> {
|
||||||
CircularProgressIndicator(
|
CircularProgressIndicator(
|
||||||
progress = state.sendingProgress,
|
progress = state.sendingProgress,
|
||||||
|
|
@ -206,9 +204,8 @@ fun BugReportView(
|
||||||
}
|
}
|
||||||
is Async.Failure -> ErrorDialog(
|
is Async.Failure -> ErrorDialog(
|
||||||
content = state.sending.error.toString(),
|
content = state.sending.error.toString(),
|
||||||
onDismiss = onFailureDialogClosed,
|
|
||||||
)
|
)
|
||||||
is Async.Success -> onDone()
|
else -> Unit
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,31 @@
|
||||||
package io.element.android.x.features.rageshake.crash.ui
|
package io.element.android.x.features.rageshake.crash.ui
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import io.element.android.x.architecture.Presenter
|
import io.element.android.x.architecture.Presenter
|
||||||
import io.element.android.x.features.rageshake.crash.CrashDataStore
|
import io.element.android.x.features.rageshake.crash.CrashDataStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class CrashDetectionPresenter @Inject constructor(private val crashDataStore: CrashDataStore) : Presenter<CrashDetectionState, CrashDetectionEvents> {
|
class CrashDetectionPresenter @Inject constructor(private val crashDataStore: CrashDataStore) : Presenter<CrashDetectionState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(events: Flow<CrashDetectionEvents>): CrashDetectionState {
|
override fun present(): CrashDetectionState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val crashDetected = crashDataStore.appHasCrashed().collectAsState(initial = false)
|
val crashDetected = crashDataStore.appHasCrashed().collectAsState(initial = false)
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
events.collect { event ->
|
fun handleEvents(event: CrashDetectionEvents) {
|
||||||
when (event) {
|
when (event) {
|
||||||
CrashDetectionEvents.ResetAllCrashData -> resetAll()
|
CrashDetectionEvents.ResetAllCrashData -> localCoroutineScope.resetAll()
|
||||||
CrashDetectionEvents.ResetAppHasCrashed -> resetAppHasCrashed()
|
CrashDetectionEvents.ResetAppHasCrashed -> localCoroutineScope.resetAppHasCrashed()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return CrashDetectionState(
|
return CrashDetectionState(
|
||||||
crashDetected = crashDetected.value
|
crashDetected = crashDetected.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,15 +28,19 @@ import io.element.android.x.element.resources.R as ElementR
|
||||||
fun CrashDetectionView(
|
fun CrashDetectionView(
|
||||||
state: CrashDetectionState,
|
state: CrashDetectionState,
|
||||||
onOpenBugReport: () -> Unit = { },
|
onOpenBugReport: () -> Unit = { },
|
||||||
onPopupDismissed: () -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
LogCompositions(tag = "Crash", msg = "CrashDetectionScreen")
|
LogCompositions(tag = "Crash", msg = "CrashDetectionScreen")
|
||||||
|
|
||||||
|
fun onPopupDismissed(){
|
||||||
|
state.eventSink(CrashDetectionEvents.ResetAllCrashData)
|
||||||
|
}
|
||||||
|
|
||||||
if (state.crashDetected) {
|
if (state.crashDetected) {
|
||||||
CrashDetectionContent(
|
CrashDetectionContent(
|
||||||
state,
|
state,
|
||||||
onYesClicked = onOpenBugReport,
|
onYesClicked = onOpenBugReport,
|
||||||
onNoClicked = onPopupDismissed,
|
onNoClicked = ::onPopupDismissed,
|
||||||
onDismiss = onPopupDismissed,
|
onDismiss = ::onPopupDismissed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -18,4 +18,5 @@ package io.element.android.x.features.rageshake.crash.ui
|
||||||
|
|
||||||
data class CrashDetectionState(
|
data class CrashDetectionState(
|
||||||
val crashDetected: Boolean = false,
|
val crashDetected: Boolean = false,
|
||||||
|
val eventSink: (CrashDetectionEvents) -> Unit = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,19 +3,17 @@ package io.element.android.x.features.rageshake.detection
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.derivedStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import io.element.android.x.architecture.Presenter
|
import io.element.android.x.architecture.Presenter
|
||||||
import io.element.android.x.architecture.SharedFlowHolder
|
|
||||||
import io.element.android.x.core.screenshot.ImageResult
|
import io.element.android.x.core.screenshot.ImageResult
|
||||||
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesEvents
|
||||||
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter
|
import io.element.android.x.features.rageshake.preferences.RageshakePreferencesPresenter
|
||||||
import io.element.android.x.features.rageshake.rageshake.RageShake
|
import io.element.android.x.features.rageshake.rageshake.RageShake
|
||||||
import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder
|
import io.element.android.x.features.rageshake.screenshot.ScreenshotHolder
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
@ -24,13 +22,12 @@ class RageshakeDetectionPresenter @Inject constructor(
|
||||||
private val screenshotHolder: ScreenshotHolder,
|
private val screenshotHolder: ScreenshotHolder,
|
||||||
private val rageShake: RageShake,
|
private val rageShake: RageShake,
|
||||||
private val preferencesPresenter: RageshakePreferencesPresenter,
|
private val preferencesPresenter: RageshakePreferencesPresenter,
|
||||||
) : Presenter<RageshakeDetectionState, RageshakeDetectionEvents> {
|
) : Presenter<RageshakeDetectionState> {
|
||||||
|
|
||||||
private val preferencesEventsFlow = SharedFlowHolder<RageshakePreferencesEvents>()
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(events: Flow<RageshakeDetectionEvents>): RageshakeDetectionState {
|
override fun present(): RageshakeDetectionState {
|
||||||
val preferencesState = preferencesPresenter.present(events = preferencesEventsFlow.asSharedFlow())
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
|
val preferencesState = preferencesPresenter.present()
|
||||||
val isStarted = rememberSaveable {
|
val isStarted = rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
@ -40,33 +37,38 @@ class RageshakeDetectionPresenter @Inject constructor(
|
||||||
val showDialog = rememberSaveable {
|
val showDialog = rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun handleEvents(event: RageshakeDetectionEvents) {
|
||||||
|
when (event) {
|
||||||
|
RageshakeDetectionEvents.Disable -> {
|
||||||
|
preferencesState.eventSink(RageshakePreferencesEvents.SetIsEnabled(false))
|
||||||
|
showDialog.value = false
|
||||||
|
}
|
||||||
|
RageshakeDetectionEvents.StartDetection -> isStarted.value = true
|
||||||
|
RageshakeDetectionEvents.StopDetection -> isStarted.value = false
|
||||||
|
is RageshakeDetectionEvents.ProcessScreenshot -> localCoroutineScope.processScreenshot(takeScreenshot, showDialog, event.imageResult)
|
||||||
|
RageshakeDetectionEvents.Dismiss -> showDialog.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val state = remember(preferencesState, isStarted.value, takeScreenshot.value, showDialog.value) {
|
val state = remember(preferencesState, isStarted.value, takeScreenshot.value, showDialog.value) {
|
||||||
RageshakeDetectionState(
|
RageshakeDetectionState(
|
||||||
isStarted = isStarted.value,
|
isStarted = isStarted.value,
|
||||||
takeScreenshot = takeScreenshot.value,
|
takeScreenshot = takeScreenshot.value,
|
||||||
showDialog = showDialog.value,
|
showDialog = showDialog.value,
|
||||||
preferenceState = preferencesState
|
preferenceState = preferencesState,
|
||||||
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
events.collect { event ->
|
|
||||||
when (event) {
|
|
||||||
RageshakeDetectionEvents.Disable -> preferencesEventsFlow.emit(RageshakePreferencesEvents.SetIsEnabled(false))
|
|
||||||
RageshakeDetectionEvents.StartDetection -> isStarted.value = true
|
|
||||||
RageshakeDetectionEvents.StopDetection -> isStarted.value = false
|
|
||||||
is RageshakeDetectionEvents.ProcessScreenshot -> processScreenshot(takeScreenshot, showDialog, event.imageResult)
|
|
||||||
RageshakeDetectionEvents.Dismiss -> showDialog.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(preferencesState.sensitivity) {
|
LaunchedEffect(preferencesState.sensitivity) {
|
||||||
rageShake.setSensitivity(preferencesState.sensitivity)
|
rageShake.setSensitivity(preferencesState.sensitivity)
|
||||||
}
|
}
|
||||||
val shouldStart = preferencesState.isEnabled &&
|
val shouldStart = preferencesState.isEnabled &&
|
||||||
preferencesState.isSupported &&
|
preferencesState.isSupported &&
|
||||||
isStarted.value &&
|
isStarted.value &&
|
||||||
!takeScreenshot.value &&
|
!takeScreenshot.value &&
|
||||||
!showDialog.value
|
!showDialog.value
|
||||||
|
|
||||||
LaunchedEffect(shouldStart) {
|
LaunchedEffect(shouldStart) {
|
||||||
handleRageShake(shouldStart, state, takeScreenshot)
|
handleRageShake(shouldStart, state, takeScreenshot)
|
||||||
|
|
|
||||||
|
|
@ -24,5 +24,6 @@ data class RageshakeDetectionState(
|
||||||
val takeScreenshot: Boolean = false,
|
val takeScreenshot: Boolean = false,
|
||||||
val showDialog: Boolean = false,
|
val showDialog: Boolean = false,
|
||||||
val isStarted: Boolean = false,
|
val isStarted: Boolean = false,
|
||||||
val preferenceState: RageshakePreferencesState = RageshakePreferencesState()
|
val preferenceState: RageshakePreferencesState = RageshakePreferencesState(),
|
||||||
|
val eventSink: (RageshakeDetectionEvents) -> Unit = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,9 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalView
|
import androidx.compose.ui.platform.LocalView
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.lifecycle.Lifecycle
|
||||||
import io.element.android.x.core.compose.LogCompositions
|
import io.element.android.x.core.compose.LogCompositions
|
||||||
|
import io.element.android.x.core.compose.OnLifecycleEvent
|
||||||
import io.element.android.x.core.hardware.vibrate
|
import io.element.android.x.core.hardware.vibrate
|
||||||
import io.element.android.x.core.screenshot.ImageResult
|
import io.element.android.x.core.screenshot.ImageResult
|
||||||
import io.element.android.x.core.screenshot.screenshot
|
import io.element.android.x.core.screenshot.screenshot
|
||||||
|
|
@ -34,24 +36,28 @@ import io.element.android.x.element.resources.R as ElementR
|
||||||
fun RageshakeDetectionView(
|
fun RageshakeDetectionView(
|
||||||
state: RageshakeDetectionState,
|
state: RageshakeDetectionState,
|
||||||
onOpenBugReport: () -> Unit = { },
|
onOpenBugReport: () -> Unit = { },
|
||||||
onScreenshotTaken: (ImageResult) -> Unit = {},
|
|
||||||
onDisableClicked: () -> Unit = {},
|
|
||||||
onNoClicked: () -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen")
|
LogCompositions(tag = "Rageshake", msg = "RageshakeDetectionScreen")
|
||||||
|
val eventSink = state.eventSink
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
|
OnLifecycleEvent { _, event ->
|
||||||
|
when (event) {
|
||||||
|
Lifecycle.Event.ON_RESUME -> eventSink(RageshakeDetectionEvents.StartDetection)
|
||||||
|
Lifecycle.Event.ON_PAUSE -> eventSink(RageshakeDetectionEvents.StopDetection)
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
when {
|
when {
|
||||||
state.takeScreenshot -> TakeScreenshot(
|
state.takeScreenshot -> TakeScreenshot(
|
||||||
onScreenshotTaken = onScreenshotTaken
|
onScreenshotTaken = { eventSink(RageshakeDetectionEvents.ProcessScreenshot(it)) }
|
||||||
)
|
)
|
||||||
state.showDialog -> {
|
state.showDialog -> {
|
||||||
LaunchedEffect(key1 = "RS_diag") {
|
LaunchedEffect(Unit) {
|
||||||
context.vibrate()
|
context.vibrate()
|
||||||
}
|
}
|
||||||
RageshakeDialogContent(
|
RageshakeDialogContent(
|
||||||
state,
|
onNoClicked = { eventSink(RageshakeDetectionEvents.Dismiss) },
|
||||||
onNoClicked = onNoClicked,
|
onDisableClicked = { eventSink(RageshakeDetectionEvents.Disable) },
|
||||||
onDisableClicked = onDisableClicked,
|
|
||||||
onYesClicked = onOpenBugReport
|
onYesClicked = onOpenBugReport
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -72,7 +78,6 @@ private fun TakeScreenshot(
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RageshakeDialogContent(
|
fun RageshakeDialogContent(
|
||||||
state: RageshakeDetectionState,
|
|
||||||
onNoClicked: () -> Unit = { },
|
onNoClicked: () -> Unit = { },
|
||||||
onDisableClicked: () -> Unit = { },
|
onDisableClicked: () -> Unit = { },
|
||||||
onYesClicked: () -> Unit = { },
|
onYesClicked: () -> Unit = { },
|
||||||
|
|
@ -83,6 +88,7 @@ fun RageshakeDialogContent(
|
||||||
thirdButtonText = stringResource(id = ElementR.string.action_disable),
|
thirdButtonText = stringResource(id = ElementR.string.action_disable),
|
||||||
submitText = stringResource(id = ElementR.string.yes),
|
submitText = stringResource(id = ElementR.string.yes),
|
||||||
cancelText = stringResource(id = ElementR.string.no),
|
cancelText = stringResource(id = ElementR.string.no),
|
||||||
|
onCancelClicked = onNoClicked,
|
||||||
onThirdButtonClicked = onDisableClicked,
|
onThirdButtonClicked = onDisableClicked,
|
||||||
onSubmitClicked = onYesClicked,
|
onSubmitClicked = onYesClicked,
|
||||||
onDismiss = onNoClicked,
|
onDismiss = onNoClicked,
|
||||||
|
|
@ -93,8 +99,6 @@ fun RageshakeDialogContent(
|
||||||
@Composable
|
@Composable
|
||||||
fun RageshakeDialogContentPreview() {
|
fun RageshakeDialogContentPreview() {
|
||||||
ElementXTheme {
|
ElementXTheme {
|
||||||
RageshakeDialogContent(
|
RageshakeDialogContent()
|
||||||
state = RageshakeDetectionState()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,26 @@
|
||||||
package io.element.android.x.features.rageshake.preferences
|
package io.element.android.x.features.rageshake.preferences
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.MutableState
|
import androidx.compose.runtime.MutableState
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import io.element.android.x.architecture.Presenter
|
import io.element.android.x.architecture.Presenter
|
||||||
import io.element.android.x.features.rageshake.rageshake.RageShake
|
import io.element.android.x.features.rageshake.rageshake.RageShake
|
||||||
import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore
|
import io.element.android.x.features.rageshake.rageshake.RageshakeDataStore
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class RageshakePreferencesPresenter @Inject constructor(
|
class RageshakePreferencesPresenter @Inject constructor(
|
||||||
private val rageshake: RageShake,
|
private val rageshake: RageShake,
|
||||||
private val rageshakeDataStore: RageshakeDataStore,
|
private val rageshakeDataStore: RageshakeDataStore,
|
||||||
) : Presenter<RageshakePreferencesState, RageshakePreferencesEvents> {
|
) : Presenter<RageshakePreferencesState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(events: Flow<RageshakePreferencesEvents>): RageshakePreferencesState {
|
override fun present(): RageshakePreferencesState {
|
||||||
|
val localCoroutineScope = rememberCoroutineScope()
|
||||||
val isSupported: MutableState<Boolean> = rememberSaveable {
|
val isSupported: MutableState<Boolean> = rememberSaveable {
|
||||||
mutableStateOf(rageshake.isAvailable())
|
mutableStateOf(rageshake.isAvailable())
|
||||||
}
|
}
|
||||||
|
|
@ -32,19 +32,18 @@ class RageshakePreferencesPresenter @Inject constructor(
|
||||||
.sensitivity()
|
.sensitivity()
|
||||||
.collectAsState(initial = 0f)
|
.collectAsState(initial = 0f)
|
||||||
|
|
||||||
LaunchedEffect(Unit) {
|
fun handleEvents(event: RageshakePreferencesEvents) {
|
||||||
events.collect { event ->
|
when (event) {
|
||||||
when (event) {
|
is RageshakePreferencesEvents.SetIsEnabled -> localCoroutineScope.setIsEnabled(event.isEnabled)
|
||||||
is RageshakePreferencesEvents.SetIsEnabled -> setIsEnabled(event.isEnabled)
|
is RageshakePreferencesEvents.SetSensitivity -> localCoroutineScope.setSensitivity(event.sensitivity)
|
||||||
is RageshakePreferencesEvents.SetSensitivity -> setSensitivity(event.sensitivity)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return RageshakePreferencesState(
|
return RageshakePreferencesState(
|
||||||
isEnabled = isEnabled.value,
|
isEnabled = isEnabled.value,
|
||||||
isSupported = isSupported.value,
|
isSupported = isSupported.value,
|
||||||
sensitivity = sensitivity.value
|
sensitivity = sensitivity.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -4,4 +4,5 @@ data class RageshakePreferencesState(
|
||||||
val isEnabled: Boolean = false,
|
val isEnabled: Boolean = false,
|
||||||
val isSupported: Boolean = true,
|
val isSupported: Boolean = true,
|
||||||
val sensitivity: Float = 0.3f,
|
val sensitivity: Float = 0.3f,
|
||||||
|
val eventSink: (RageshakePreferencesEvents) -> Unit = {},
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -34,9 +34,16 @@ fun RageshakePreferencesView(
|
||||||
state: RageshakePreferencesState,
|
state: RageshakePreferencesState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onOpenRageshake: () -> Unit = {},
|
onOpenRageshake: () -> Unit = {},
|
||||||
onIsEnabledChanged: (Boolean) -> Unit = {},
|
|
||||||
onSensitivityChanged: (Float) -> Unit = {}
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun onSensitivityChanged(sensitivity: Float){
|
||||||
|
state.eventSink(RageshakePreferencesEvents.SetSensitivity(sensitivity = sensitivity))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onEnabledChanged(isEnabled: Boolean){
|
||||||
|
state.eventSink(RageshakePreferencesEvents.SetIsEnabled(isEnabled = isEnabled))
|
||||||
|
}
|
||||||
|
|
||||||
Column(modifier = modifier) {
|
Column(modifier = modifier) {
|
||||||
PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) {
|
PreferenceCategory(title = stringResource(id = ElementR.string.send_bug_report)) {
|
||||||
PreferenceText(
|
PreferenceText(
|
||||||
|
|
@ -45,12 +52,13 @@ fun RageshakePreferencesView(
|
||||||
onClick = onOpenRageshake
|
onClick = onOpenRageshake
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) {
|
PreferenceCategory(title = stringResource(id = ElementR.string.settings_rageshake)) {
|
||||||
if (state.isSupported) {
|
if (state.isSupported) {
|
||||||
PreferenceSwitch(
|
PreferenceSwitch(
|
||||||
title = stringResource(id = ElementR.string.send_bug_report_rage_shake),
|
title = stringResource(id = ElementR.string.send_bug_report_rage_shake),
|
||||||
isChecked = state.isEnabled,
|
isChecked = state.isEnabled,
|
||||||
onCheckedChange = onIsEnabledChanged
|
onCheckedChange = ::onEnabledChanged
|
||||||
)
|
)
|
||||||
PreferenceSlide(
|
PreferenceSlide(
|
||||||
title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold),
|
title = stringResource(id = ElementR.string.settings_rageshake_detection_threshold),
|
||||||
|
|
@ -58,7 +66,7 @@ fun RageshakePreferencesView(
|
||||||
value = state.sensitivity,
|
value = state.sensitivity,
|
||||||
enabled = state.isEnabled,
|
enabled = state.isEnabled,
|
||||||
steps = 3 /* 5 possible values - steps are in ]0, 1[ */,
|
steps = 3 /* 5 possible values - steps are in ]0, 1[ */,
|
||||||
onValueChange = onSensitivityChanged
|
onValueChange = ::onSensitivityChanged
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
PreferenceText(title = "Rageshaking is not supported by your device")
|
PreferenceText(title = "Rageshaking is not supported by your device")
|
||||||
|
|
|
||||||
|
|
@ -30,18 +30,6 @@ class RoomListNode @AssistedInject constructor(
|
||||||
|
|
||||||
private val connector = presenterConnector(presenter)
|
private val connector = presenterConnector(presenter)
|
||||||
|
|
||||||
private fun updateFilter(filter: String) {
|
|
||||||
connector.emitEvent(RoomListEvents.UpdateFilter(filter))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateVisibleRange(range: IntRange) {
|
|
||||||
connector.emitEvent((RoomListEvents.UpdateVisibleRange(range)))
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun logout() {
|
|
||||||
connector.emitEvent(RoomListEvents.Logout)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onRoomClicked(roomId: RoomId) {
|
private fun onRoomClicked(roomId: RoomId) {
|
||||||
plugins<Callback>().forEach { it.onRoomClicked(roomId) }
|
plugins<Callback>().forEach { it.onRoomClicked(roomId) }
|
||||||
}
|
}
|
||||||
|
|
@ -56,8 +44,6 @@ class RoomListNode @AssistedInject constructor(
|
||||||
RoomListView(
|
RoomListView(
|
||||||
state = state,
|
state = state,
|
||||||
onRoomClicked = this::onRoomClicked,
|
onRoomClicked = this::onRoomClicked,
|
||||||
onFilterChanged = this::updateFilter,
|
|
||||||
onScrollOver = this::updateVisibleRange,
|
|
||||||
onOpenSettings = this::onOpenSettings
|
onOpenSettings = this::onOpenSettings
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,10 @@ import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import io.element.android.x.architecture.Presenter
|
||||||
import io.element.android.x.core.coroutine.parallelMap
|
import io.element.android.x.core.coroutine.parallelMap
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
import io.element.android.x.designsystem.components.avatar.AvatarSize
|
||||||
|
|
@ -19,13 +21,12 @@ import io.element.android.x.features.roomlist.model.RoomListState
|
||||||
import io.element.android.x.matrix.MatrixClient
|
import io.element.android.x.matrix.MatrixClient
|
||||||
import io.element.android.x.matrix.media.MediaResolver
|
import io.element.android.x.matrix.media.MediaResolver
|
||||||
import io.element.android.x.matrix.room.RoomSummary
|
import io.element.android.x.matrix.room.RoomSummary
|
||||||
import io.element.android.x.architecture.Presenter
|
|
||||||
import io.element.android.x.matrix.ui.model.MatrixUser
|
import io.element.android.x.matrix.ui.model.MatrixUser
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.launch
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
private const val extendedRangeSize = 40
|
private const val extendedRangeSize = 40
|
||||||
|
|
@ -33,10 +34,10 @@ private const val extendedRangeSize = 40
|
||||||
class RoomListPresenter @Inject constructor(
|
class RoomListPresenter @Inject constructor(
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
private val lastMessageFormatter: LastMessageFormatter,
|
private val lastMessageFormatter: LastMessageFormatter,
|
||||||
) : Presenter<RoomListState, RoomListEvents> {
|
) : Presenter<RoomListState> {
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun present(events: Flow<RoomListEvents>): RoomListState {
|
override fun present(): RoomListState {
|
||||||
val matrixUser: MutableState<MatrixUser?> = remember {
|
val matrixUser: MutableState<MatrixUser?> = remember {
|
||||||
mutableStateOf(null)
|
mutableStateOf(null)
|
||||||
}
|
}
|
||||||
|
|
@ -52,14 +53,15 @@ class RoomListPresenter @Inject constructor(
|
||||||
}
|
}
|
||||||
LaunchedEffect(Unit) {
|
LaunchedEffect(Unit) {
|
||||||
initialLoad(matrixUser)
|
initialLoad(matrixUser)
|
||||||
events.collect { event ->
|
}
|
||||||
when (event) {
|
|
||||||
RoomListEvents.Logout -> logout(isLoginOut)
|
fun handleEvents(event: RoomListEvents) {
|
||||||
is RoomListEvents.UpdateFilter -> filter = event.newFilter
|
when (event) {
|
||||||
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
|
is RoomListEvents.UpdateFilter -> filter = event.newFilter
|
||||||
}
|
is RoomListEvents.UpdateVisibleRange -> updateVisibleRange(event.range)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(roomSummaries, filter) {
|
LaunchedEffect(roomSummaries, filter) {
|
||||||
filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter)
|
filteredRoomSummaries.value = updateFilteredRoomSummaries(roomSummaries, filter)
|
||||||
}
|
}
|
||||||
|
|
@ -67,7 +69,8 @@ class RoomListPresenter @Inject constructor(
|
||||||
matrixUser = matrixUser.value,
|
matrixUser = matrixUser.value,
|
||||||
roomList = filteredRoomSummaries.value,
|
roomList = filteredRoomSummaries.value,
|
||||||
filter = filter,
|
filter = filter,
|
||||||
isLoginOut = isLoginOut.value
|
isLoginOut = isLoginOut.value,
|
||||||
|
eventSink = ::handleEvents
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -83,7 +86,7 @@ class RoomListPresenter @Inject constructor(
|
||||||
}.toImmutableList()
|
}.toImmutableList()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun initialLoad(matrixUser: MutableState<MatrixUser?>) {
|
private fun CoroutineScope.initialLoad(matrixUser: MutableState<MatrixUser?>) = launch {
|
||||||
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
|
val userAvatarUrl = client.loadUserAvatarURLString().getOrNull()
|
||||||
val userDisplayName = client.loadUserDisplayName().getOrNull()
|
val userDisplayName = client.loadUserDisplayName().getOrNull()
|
||||||
val avatarData =
|
val avatarData =
|
||||||
|
|
@ -100,13 +103,6 @@ class RoomListPresenter @Inject constructor(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun logout(isLoginOut: MutableState<Boolean>) {
|
|
||||||
isLoginOut.value = true
|
|
||||||
delay(2000)
|
|
||||||
client.logout()
|
|
||||||
isLoginOut.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateVisibleRange(range: IntRange) {
|
private fun updateVisibleRange(range: IntRange) {
|
||||||
if (range.isEmpty()) return
|
if (range.isEmpty()) return
|
||||||
val midExtendedRangeSize = extendedRangeSize / 2
|
val midExtendedRangeSize = extendedRangeSize / 2
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ import io.element.android.x.designsystem.ElementXTheme
|
||||||
import io.element.android.x.designsystem.components.avatar.AvatarData
|
import io.element.android.x.designsystem.components.avatar.AvatarData
|
||||||
import io.element.android.x.features.roomlist.components.RoomListTopBar
|
import io.element.android.x.features.roomlist.components.RoomListTopBar
|
||||||
import io.element.android.x.features.roomlist.components.RoomSummaryRow
|
import io.element.android.x.features.roomlist.components.RoomSummaryRow
|
||||||
|
import io.element.android.x.features.roomlist.model.RoomListEvents
|
||||||
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
|
import io.element.android.x.features.roomlist.model.RoomListRoomSummary
|
||||||
import io.element.android.x.features.roomlist.model.RoomListState
|
import io.element.android.x.features.roomlist.model.RoomListState
|
||||||
import io.element.android.x.features.roomlist.model.stubbedRoomSummaries
|
import io.element.android.x.features.roomlist.model.stubbedRoomSummaries
|
||||||
|
|
@ -54,19 +55,26 @@ fun RoomListView(
|
||||||
state: RoomListState,
|
state: RoomListState,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onRoomClicked: (RoomId) -> Unit = {},
|
onRoomClicked: (RoomId) -> Unit = {},
|
||||||
onFilterChanged: (String) -> Unit = {},
|
|
||||||
onOpenSettings: () -> Unit = {},
|
onOpenSettings: () -> Unit = {},
|
||||||
onScrollOver: (IntRange) -> Unit = {},
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
fun onFilterChanged(filter: String){
|
||||||
|
state.eventSink(RoomListEvents.UpdateFilter(filter))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onVisibleRangedChanged(range: IntRange){
|
||||||
|
state.eventSink(RoomListEvents.UpdateVisibleRange(range))
|
||||||
|
}
|
||||||
|
|
||||||
RoomListView(
|
RoomListView(
|
||||||
roomSummaries = state.roomList,
|
roomSummaries = state.roomList,
|
||||||
matrixUser = state.matrixUser,
|
matrixUser = state.matrixUser,
|
||||||
filter = state.filter,
|
filter = state.filter,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onRoomClicked = onRoomClicked,
|
onRoomClicked = onRoomClicked,
|
||||||
onFilterChanged = onFilterChanged,
|
onFilterChanged = ::onFilterChanged,
|
||||||
onOpenSettings = onOpenSettings,
|
onOpenSettings = onOpenSettings,
|
||||||
onScrollOver = onScrollOver,
|
onScrollOver = ::onVisibleRangedChanged,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
package io.element.android.x.features.roomlist.model
|
package io.element.android.x.features.roomlist.model
|
||||||
|
|
||||||
sealed interface RoomListEvents {
|
sealed interface RoomListEvents {
|
||||||
object Logout : RoomListEvents
|
|
||||||
data class UpdateFilter(val newFilter: String) : RoomListEvents
|
data class UpdateFilter(val newFilter: String) : RoomListEvents
|
||||||
data class UpdateVisibleRange(val range: IntRange): RoomListEvents
|
data class UpdateVisibleRange(val range: IntRange): RoomListEvents
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,5 @@ data class RoomListState(
|
||||||
val roomList: ImmutableList<RoomListRoomSummary>,
|
val roomList: ImmutableList<RoomListRoomSummary>,
|
||||||
val filter: String,
|
val filter: String,
|
||||||
val isLoginOut: Boolean,
|
val isLoginOut: Boolean,
|
||||||
|
val eventSink: (RoomListEvents) -> Unit = {}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ package io.element.android.x.architecture
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
interface Presenter<State, Event> {
|
interface Presenter<State> {
|
||||||
@Composable
|
@Composable
|
||||||
fun present(events: Flow<Event>): State
|
fun present(): State
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,19 +8,14 @@ import app.cash.molecule.launchMolecule
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
|
||||||
inline fun <reified State, reified Event> LifecycleOwner.presenterConnector(presenter: Presenter<State, Event>): LifecyclePresenterConnector<State, Event> =
|
inline fun <reified State> LifecycleOwner.presenterConnector(presenter: Presenter<State>): LifecyclePresenterConnector<State> =
|
||||||
LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter)
|
LifecyclePresenterConnector(lifecycleOwner = this, presenter = presenter)
|
||||||
|
|
||||||
class LifecyclePresenterConnector<State, Event>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State, Event>) {
|
class LifecyclePresenterConnector<State>(lifecycleOwner: LifecycleOwner, presenter: Presenter<State>) {
|
||||||
|
|
||||||
private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
|
private val moleculeScope = CoroutineScope(lifecycleOwner.lifecycleScope.coroutineContext + AndroidUiDispatcher.Main)
|
||||||
private val eventFlow = SharedFlowHolder<Event>()
|
|
||||||
|
|
||||||
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
|
val stateFlow: StateFlow<State> = moleculeScope.launchMolecule(RecompositionClock.Immediate) {
|
||||||
presenter.present(events = eventFlow.asSharedFlow())
|
presenter.present()
|
||||||
}
|
|
||||||
|
|
||||||
fun emitEvent(event: Event) {
|
|
||||||
eventFlow.emit(event)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,4 +9,6 @@ class SharedFlowHolder<Data>(capacity: Int = 64) {
|
||||||
fun asSharedFlow() = mutableFlow.asSharedFlow()
|
fun asSharedFlow() = mutableFlow.asSharedFlow()
|
||||||
|
|
||||||
fun emit(data: Data) = mutableFlow.tryEmit(data)
|
fun emit(data: Data) = mutableFlow.tryEmit(data)
|
||||||
|
|
||||||
|
suspend fun awaitEmit(data: Data) = mutableFlow.emit(data)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue