Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Leave room #302

Merged
merged 2 commits into from
Jan 7, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ internal fun MessengerScreen(
) {
val state = viewModel.current

viewModel.ObserveEvents(galleryLauncher)
viewModel.ObserveEvents(galleryLauncher, navigator)
LifecycleEffect(
onStart = { viewModel.dispatch(ComponentLifecycle.Visible) },
onStop = { viewModel.dispatch(ComponentLifecycle.Gone) }
Expand All @@ -85,6 +85,30 @@ internal fun MessengerScreen(
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 {
Toolbar(onNavigate = { navigator.navigate.upToHome() }, roomTitle, actions = {
state.roomState.takeIfContent()?.let {
Expand All @@ -98,8 +122,10 @@ internal fun MessengerScreen(
viewModel.dispatch(ScreenAction.Notifications.Mute)
})
}
DropdownMenuItem(text = { Text("Leave room", color = MaterialTheme.colorScheme.onSecondaryContainer) }, onClick = {
viewModel.dispatch(ScreenAction.LeaveRoom)
})
}

}
})

Expand Down Expand Up @@ -207,7 +233,7 @@ private fun ZoomableImage(viewerState: ViewerState) {
}

@Composable
private fun MessengerState.ObserveEvents(galleryLauncher: ActivityResultLauncher<ImageGalleryActivityPayload>) {
private fun MessengerState.ObserveEvents(galleryLauncher: ActivityResultLauncher<ImageGalleryActivityPayload>, navigator: Navigator) {
val context = LocalContext.current
StartObserving {
this@ObserveEvents.events.launch {
Expand All @@ -221,6 +247,8 @@ private fun MessengerState.ObserveEvents(galleryLauncher: ActivityResultLauncher
is MessengerEvent.Toast -> {
Toast.makeText(context, it.message, Toast.LENGTH_SHORT).show()
}

MessengerEvent.OnLeftRoom -> navigator.navigate.upToHome()
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@ sealed interface ScreenAction : Action {
data class CopyToClipboard(val model: BubbleModel) : ScreenAction
object SendMessage : ScreenAction
object OpenGalleryPicker : ScreenAction
object LeaveRoom : ScreenAction

sealed interface Notifications : ScreenAction {
object Mute : 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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import app.dapk.st.navigator.MessageAttachment
import app.dapk.state.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlin.reflect.KClass

internal fun messengerReducer(
jobBag: JobBag,
Expand All @@ -36,6 +37,7 @@ internal fun messengerReducer(
roomState = Lce.Loading(),
composerState = initialComposerState(initialAttachments),
viewerState = null,
dialogState = null,
),

async(ComponentLifecycle::class) { action ->
Expand Down Expand Up @@ -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) {
val roomState = content.roomState
val message = SendMessage.TextMessage(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package app.dapk.st.messenger.state

import app.dapk.st.core.Lce
import app.dapk.st.state.State
import app.dapk.st.design.components.BubbleModel
import app.dapk.st.engine.MessengerPageState
import app.dapk.st.engine.RoomEvent
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.navigator.MessageAttachment
import app.dapk.st.state.State
import app.dapk.state.Action

typealias MessengerState = State<MessengerScreenState, MessengerEvent>

Expand All @@ -15,15 +16,26 @@ data class MessengerScreenState(
val roomState: Lce<MessengerPageState>,
val composerState: ComposerState,
val viewerState: ViewerState?,
val dialogState: DialogState?,
)

data class ViewerState(
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 {
object SelectImageAttachment : MessengerEvent
data class Toast(val message: String) : MessengerEvent
object OnLeftRoom : MessengerEvent
}

sealed interface ComposerState {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,12 @@ import fake.FakeChatEngine
import fake.FakeJobBag
import fake.FakeMessageOptionsStore
import fixture.*
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf
import org.junit.Test
import test.ReducerTestScope
import test.delegateReturn
import test.expect
import test.testReducer
import test.*

private const val READ_RECEIPTS_ARE_DISABLED = true
private val A_ROOM_ID = aRoomId("messenger state room id")
Expand All @@ -37,11 +35,16 @@ private val AN_IMAGE_BUBBLE = BubbleModel.Image(
mockk(),
BubbleModel.Event("author-id", "author-name", edited = false, time = "10:27")
)

private val A_TEXT_BUBBLE = BubbleModel.Text(
content = RichText(listOf(RichText.Part.Normal(A_MESSAGE_CONTENT))),
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 {

Expand Down Expand Up @@ -72,6 +75,7 @@ class MessengerReducerTest {
roomState = Lce.Loading(),
composerState = ComposerState.Text(value = "", reply = null),
viewerState = null,
dialogState = null,
)
)
}
Expand All @@ -84,6 +88,7 @@ class MessengerReducerTest {
roomState = Lce.Loading(),
composerState = ComposerState.Text(value = "", reply = null),
viewerState = null,
dialogState = null,
)
)
}
Expand All @@ -96,6 +101,7 @@ class MessengerReducerTest {
roomState = Lce.Loading(),
composerState = ComposerState.Attachments(listOf(A_MESSAGE_ATTACHMENT), reply = null),
viewerState = null,
dialogState = null,
)
)
}
Expand Down Expand Up @@ -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
fun `when OpenGalleryPicker, then emits event`() = runReducerTest {
reduce(ScreenAction.OpenGalleryPicker)
Expand Down