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