Merge pull request #302 from ouchadam/feature-leave-room
[Feature] Leave room
This commit is contained in:
commit
449f1bf4c2
|
@ -67,7 +67,7 @@ internal fun MessengerScreen(
|
||||||
) {
|
) {
|
||||||
val state = viewModel.current
|
val state = viewModel.current
|
||||||
|
|
||||||
viewModel.ObserveEvents(galleryLauncher)
|
viewModel.ObserveEvents(galleryLauncher, navigator)
|
||||||
LifecycleEffect(
|
LifecycleEffect(
|
||||||
onStart = { viewModel.dispatch(ComponentLifecycle.Visible) },
|
onStart = { viewModel.dispatch(ComponentLifecycle.Visible) },
|
||||||
onStop = { viewModel.dispatch(ComponentLifecycle.Gone) }
|
onStop = { viewModel.dispatch(ComponentLifecycle.Gone) }
|
||||||
|
@ -85,6 +85,30 @@ internal fun MessengerScreen(
|
||||||
onImageClick = { viewModel.dispatch(ComposerStateChange.ImagePreview.Show(it)) }
|
onImageClick = { viewModel.dispatch(ComposerStateChange.ImagePreview.Show(it)) }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
when (val dialog = state.dialogState) {
|
||||||
|
null -> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
|
||||||
|
is DialogState.PositiveNegative -> {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = { viewModel.dispatch(ScreenAction.LeaveRoomConfirmation.Deny) },
|
||||||
|
confirmButton = {
|
||||||
|
Button(onClick = { viewModel.dispatch(ScreenAction.LeaveRoomConfirmation.Confirm) }) {
|
||||||
|
Text("Leave room")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
Button(onClick = { viewModel.dispatch(ScreenAction.LeaveRoomConfirmation.Deny) }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = { Text(dialog.title) },
|
||||||
|
text = { Text(dialog.subtitle) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = {
|
Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = {
|
||||||
state.roomState.takeIfContent()?.let {
|
state.roomState.takeIfContent()?.let {
|
||||||
|
@ -98,8 +122,10 @@ internal fun MessengerScreen(
|
||||||
viewModel.dispatch(ScreenAction.Notifications.Mute)
|
viewModel.dispatch(ScreenAction.Notifications.Mute)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
DropdownMenuItem(text = { Text("Leave room", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = {
|
||||||
|
viewModel.dispatch(ScreenAction.LeaveRoom)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -207,7 +233,7 @@ private fun ZoomableImage(viewerState: ViewerState) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun MessengerState.ObserveEvents(galleryLauncher: ActivityResultLauncher<ImageGalleryActivityPayload>) {
|
private fun MessengerState.ObserveEvents(galleryLauncher: ActivityResultLauncher<ImageGalleryActivityPayload>, navigator: Navigator) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
StartObserving {
|
StartObserving {
|
||||||
this@ObserveEvents.events.launch {
|
this@ObserveEvents.events.launch {
|
||||||
|
@ -221,6 +247,8 @@ private fun MessengerState.ObserveEvents(galleryLauncher: ActivityResultLauncher
|
||||||
is MessengerEvent.Toast -> {
|
is MessengerEvent.Toast -> {
|
||||||
Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
|
Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MessengerEvent.OnLeftRoom -> navigator.navigate.upToHome()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,19 @@ sealed interface ScreenAction : Action {
|
||||||
data class CopyToClipboard(val model: BubbleModel) : ScreenAction
|
data class CopyToClipboard(val model: BubbleModel) : ScreenAction
|
||||||
object SendMessage : ScreenAction
|
object SendMessage : ScreenAction
|
||||||
object OpenGalleryPicker : ScreenAction
|
object OpenGalleryPicker : ScreenAction
|
||||||
|
object LeaveRoom : ScreenAction
|
||||||
|
|
||||||
sealed interface Notifications : ScreenAction {
|
sealed interface Notifications : ScreenAction {
|
||||||
object Mute : Notifications
|
object Mute : Notifications
|
||||||
object Unmute : Notifications
|
object Unmute : Notifications
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sealed interface LeaveRoomConfirmation : ScreenAction {
|
||||||
|
object Confirm : LeaveRoomConfirmation
|
||||||
|
object Deny : LeaveRoomConfirmation
|
||||||
|
}
|
||||||
|
|
||||||
|
data class UpdateDialogState(val dialogState: DialogState?): ScreenAction
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ComponentLifecycle : Action {
|
sealed interface ComponentLifecycle : Action {
|
||||||
|
|
|
@ -19,6 +19,7 @@ import app.dapk.st.navigator.MessageAttachment
|
||||||
import app.dapk.state.*
|
import app.dapk.state.*
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
internal fun messengerReducer(
|
internal fun messengerReducer(
|
||||||
jobBag: JobBag,
|
jobBag: JobBag,
|
||||||
|
@ -36,6 +37,7 @@ internal fun messengerReducer(
|
||||||
roomState = Lce.Loading(),
|
roomState = Lce.Loading(),
|
||||||
composerState = initialComposerState(initialAttachments),
|
composerState = initialComposerState(initialAttachments),
|
||||||
viewerState = null,
|
viewerState = null,
|
||||||
|
dialogState = null,
|
||||||
),
|
),
|
||||||
|
|
||||||
async(ComponentLifecycle::class) { action ->
|
async(ComponentLifecycle::class) { action ->
|
||||||
|
@ -159,9 +161,43 @@ internal fun messengerReducer(
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
change(ScreenAction.UpdateDialogState::class) { action, state ->
|
||||||
|
state.copy(dialogState = action.dialogState)
|
||||||
|
},
|
||||||
|
|
||||||
|
rewrite(ScreenAction.LeaveRoom::class) {
|
||||||
|
ScreenAction.UpdateDialogState(
|
||||||
|
DialogState.PositiveNegative(
|
||||||
|
title = "Leave room",
|
||||||
|
subtitle = "Are you sure you want you leave the room? If the room is private you will need to be invited again to rejoin.",
|
||||||
|
negativeAction = ScreenAction.LeaveRoomConfirmation.Deny,
|
||||||
|
positiveAction = ScreenAction.LeaveRoomConfirmation.Confirm,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
async(ScreenAction.LeaveRoomConfirmation::class) { action ->
|
||||||
|
dispatch(ScreenAction.UpdateDialogState(dialogState = null))
|
||||||
|
|
||||||
|
when (action) {
|
||||||
|
ScreenAction.LeaveRoomConfirmation.Confirm -> {
|
||||||
|
runCatching { chatEngine.rejectRoom(getState().roomId) }.fold(
|
||||||
|
onSuccess = { eventEmitter.invoke(MessengerEvent.OnLeftRoom) },
|
||||||
|
onFailure = { eventEmitter.invoke(MessengerEvent.Toast("Failed to leave room")) },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ScreenAction.LeaveRoomConfirmation.Deny -> {
|
||||||
|
// do nothing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun <A : Action, S> rewrite(klass: KClass<A>, mapper: (A) -> Action) = async<A, S>(klass) { action -> dispatch(mapper(action)) }
|
||||||
|
|
||||||
private suspend fun ChatEngine.sendTextMessage(content: MessengerPageState, composerState: ComposerState.Text) {
|
private suspend fun ChatEngine.sendTextMessage(content: MessengerPageState, composerState: ComposerState.Text) {
|
||||||
val roomState = content.roomState
|
val roomState = content.roomState
|
||||||
val message = SendMessage.TextMessage(
|
val message = SendMessage.TextMessage(
|
||||||
|
|
|
@ -1,12 +1,13 @@
|
||||||
package app.dapk.st.messenger.state
|
package app.dapk.st.messenger.state
|
||||||
|
|
||||||
import app.dapk.st.core.Lce
|
import app.dapk.st.core.Lce
|
||||||
import app.dapk.st.state.State
|
|
||||||
import app.dapk.st.design.components.BubbleModel
|
import app.dapk.st.design.components.BubbleModel
|
||||||
import app.dapk.st.engine.MessengerPageState
|
import app.dapk.st.engine.MessengerPageState
|
||||||
import app.dapk.st.engine.RoomEvent
|
import app.dapk.st.engine.RoomEvent
|
||||||
import app.dapk.st.matrix.common.RoomId
|
import app.dapk.st.matrix.common.RoomId
|
||||||
import app.dapk.st.navigator.MessageAttachment
|
import app.dapk.st.navigator.MessageAttachment
|
||||||
|
import app.dapk.st.state.State
|
||||||
|
import app.dapk.state.Action
|
||||||
|
|
||||||
typealias MessengerState = State<MessengerScreenState, MessengerEvent>
|
typealias MessengerState = State<MessengerScreenState, MessengerEvent>
|
||||||
|
|
||||||
|
@ -15,15 +16,26 @@ data class MessengerScreenState(
|
||||||
val roomState: Lce<MessengerPageState>,
|
val roomState: Lce<MessengerPageState>,
|
||||||
val composerState: ComposerState,
|
val composerState: ComposerState,
|
||||||
val viewerState: ViewerState?,
|
val viewerState: ViewerState?,
|
||||||
|
val dialogState: DialogState?,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ViewerState(
|
data class ViewerState(
|
||||||
val event: BubbleModel.Image,
|
val event: BubbleModel.Image,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
sealed interface DialogState {
|
||||||
|
data class PositiveNegative(
|
||||||
|
val title: String,
|
||||||
|
val subtitle: String,
|
||||||
|
val positiveAction: Action,
|
||||||
|
val negativeAction: Action,
|
||||||
|
) : DialogState
|
||||||
|
}
|
||||||
|
|
||||||
sealed interface MessengerEvent {
|
sealed interface MessengerEvent {
|
||||||
object SelectImageAttachment : MessengerEvent
|
object SelectImageAttachment : MessengerEvent
|
||||||
data class Toast(val message: String) : MessengerEvent
|
data class Toast(val message: String) : MessengerEvent
|
||||||
|
object OnLeftRoom : MessengerEvent
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface ComposerState {
|
sealed interface ComposerState {
|
||||||
|
|
|
@ -15,14 +15,12 @@ import fake.FakeChatEngine
|
||||||
import fake.FakeJobBag
|
import fake.FakeJobBag
|
||||||
import fake.FakeMessageOptionsStore
|
import fake.FakeMessageOptionsStore
|
||||||
import fixture.*
|
import fixture.*
|
||||||
|
import io.mockk.coEvery
|
||||||
import io.mockk.every
|
import io.mockk.every
|
||||||
import io.mockk.mockk
|
import io.mockk.mockk
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import test.ReducerTestScope
|
import test.*
|
||||||
import test.delegateReturn
|
|
||||||
import test.expect
|
|
||||||
import test.testReducer
|
|
||||||
|
|
||||||
private const val READ_RECEIPTS_ARE_DISABLED = true
|
private const val READ_RECEIPTS_ARE_DISABLED = true
|
||||||
private val A_ROOM_ID = aRoomId("messenger state room id")
|
private val A_ROOM_ID = aRoomId("messenger state room id")
|
||||||
|
@ -37,11 +35,16 @@ private val AN_IMAGE_BUBBLE = BubbleModel.Image(
|
||||||
mockk(),
|
mockk(),
|
||||||
BubbleModel.Event("author-id", "author-name", edited = false, time = "10:27")
|
BubbleModel.Event("author-id", "author-name", edited = false, time = "10:27")
|
||||||
)
|
)
|
||||||
|
|
||||||
private val A_TEXT_BUBBLE = BubbleModel.Text(
|
private val A_TEXT_BUBBLE = BubbleModel.Text(
|
||||||
content = RichText(listOf(RichText.Part.Normal(A_MESSAGE_CONTENT))),
|
content = RichText(listOf(RichText.Part.Normal(A_MESSAGE_CONTENT))),
|
||||||
BubbleModel.Event("author-id", "author-name", edited = false, time = "10:27")
|
BubbleModel.Event("author-id", "author-name", edited = false, time = "10:27")
|
||||||
)
|
)
|
||||||
|
private val A_DIALOG_STATE = DialogState.PositiveNegative(
|
||||||
|
"a title",
|
||||||
|
"a subtitle",
|
||||||
|
positiveAction = ScreenAction.LeaveRoomConfirmation.Confirm,
|
||||||
|
negativeAction = ScreenAction.LeaveRoomConfirmation.Deny,
|
||||||
|
)
|
||||||
|
|
||||||
class MessengerReducerTest {
|
class MessengerReducerTest {
|
||||||
|
|
||||||
|
@ -72,6 +75,7 @@ class MessengerReducerTest {
|
||||||
roomState = Lce.Loading(),
|
roomState = Lce.Loading(),
|
||||||
composerState = ComposerState.Text(value = "", reply = null),
|
composerState = ComposerState.Text(value = "", reply = null),
|
||||||
viewerState = null,
|
viewerState = null,
|
||||||
|
dialogState = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -84,6 +88,7 @@ class MessengerReducerTest {
|
||||||
roomState = Lce.Loading(),
|
roomState = Lce.Loading(),
|
||||||
composerState = ComposerState.Text(value = "", reply = null),
|
composerState = ComposerState.Text(value = "", reply = null),
|
||||||
viewerState = null,
|
viewerState = null,
|
||||||
|
dialogState = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -96,6 +101,7 @@ class MessengerReducerTest {
|
||||||
roomState = Lce.Loading(),
|
roomState = Lce.Loading(),
|
||||||
composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null),
|
composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null),
|
||||||
viewerState = null,
|
viewerState = null,
|
||||||
|
dialogState = null,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -221,6 +227,60 @@ class MessengerReducerTest {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when LeaveRoom, then updates dialog state with leave room confirmation`() = runReducerTest {
|
||||||
|
reduce(ScreenAction.LeaveRoom)
|
||||||
|
|
||||||
|
assertOnlyDispatches(
|
||||||
|
ScreenAction.UpdateDialogState(
|
||||||
|
DialogState.PositiveNegative(
|
||||||
|
title = "Leave room",
|
||||||
|
subtitle = "Are you sure you want you leave the room? If the room is private you will need to be invited again to rejoin.",
|
||||||
|
negativeAction = ScreenAction.LeaveRoomConfirmation.Deny,
|
||||||
|
positiveAction = ScreenAction.LeaveRoomConfirmation.Confirm,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when UpdateDialogState, then updates dialog state`() = runReducerTest {
|
||||||
|
reduce(ScreenAction.UpdateDialogState(dialogState = A_DIALOG_STATE))
|
||||||
|
|
||||||
|
assertOnlyStateChange { it.copy(dialogState = A_DIALOG_STATE) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given can leave room, when LeaveConfirmation Confirm, then removes dialog and rejects room and emits OnLeftRoom`() = runReducerTest {
|
||||||
|
fakeChatEngine.expect { it.rejectRoom(A_ROOM_ID) }
|
||||||
|
|
||||||
|
reduce(ScreenAction.LeaveRoomConfirmation.Confirm)
|
||||||
|
|
||||||
|
assertDispatches(ScreenAction.UpdateDialogState(dialogState = null))
|
||||||
|
assertEvents(MessengerEvent.OnLeftRoom)
|
||||||
|
assertNoStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `given leave room fails, when LeaveConfirmation Confirm, then removes dialog and emits toast`() = runReducerTest {
|
||||||
|
fakeChatEngine.expectError(error = RuntimeException("an error")) { fakeChatEngine.rejectRoom(A_ROOM_ID) }
|
||||||
|
|
||||||
|
reduce(ScreenAction.LeaveRoomConfirmation.Confirm)
|
||||||
|
|
||||||
|
assertDispatches(ScreenAction.UpdateDialogState(dialogState = null))
|
||||||
|
assertEvents(MessengerEvent.Toast("Failed to leave room"))
|
||||||
|
assertNoStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `when LeaveConfirmation Deny, then removes dialog and does nothing`() = runReducerTest {
|
||||||
|
reduce(ScreenAction.LeaveRoomConfirmation.Deny)
|
||||||
|
|
||||||
|
assertDispatches(ScreenAction.UpdateDialogState(dialogState = null))
|
||||||
|
assertNoEvents()
|
||||||
|
assertNoStateChange()
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun `when OpenGalleryPicker, then emits event`() = runReducerTest {
|
fun `when OpenGalleryPicker, then emits event`() = runReducerTest {
|
||||||
reduce(ScreenAction.OpenGalleryPicker)
|
reduce(ScreenAction.OpenGalleryPicker)
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit d596949ac2b923b02da55ddd78e2e26dc46af82a
|
Subproject commit 337a65b27b9911205e52a87c075be4bbf70a557d
|
Loading…
Reference in New Issue