Browse Source

Merge pull request #696 from vector-im/feature/cjs/leaving-dms

Show a prompt to reinvite other party in a DM
jonny/proxy
Chris Smith 1 year ago committed by GitHub
parent
commit
f4e17cf12d
  1. 6
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt
  2. 49
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt
  3. 3
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt
  4. 4
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt
  5. 20
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt
  6. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt
  7. 7
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt
  8. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt
  9. 1
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt
  10. 13
      features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt
  11. 171
      features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt
  12. 1
      libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt
  13. 3
      libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt
  14. 1
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt
  15. 41
      libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomMemberFixture.kt
  16. 5
      libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt
  17. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png
  18. BIN
      tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_4,NEXUS_5,1.0,en].png

6
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesEvents.kt

@ -23,5 +23,11 @@ import io.element.android.libraries.matrix.api.core.EventId @@ -23,5 +23,11 @@ import io.element.android.libraries.matrix.api.core.EventId
sealed interface MessagesEvents {
data class HandleAction(val action: TimelineItemAction, val event: TimelineItem.Event) : MessagesEvents
data class ToggleReaction(val emoji: String, val eventId: EventId) : MessagesEvents
data class InviteDialogDismissed(val action: InviteDialogAction) : MessagesEvents
object Dismiss : MessagesEvents
}
enum class InviteDialogAction {
Cancel,
Invite,
}

49
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesPresenter.kt

@ -21,11 +21,13 @@ import androidx.compose.runtime.Composable @@ -21,11 +21,13 @@ 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
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -52,7 +54,9 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum @@ -52,7 +54,9 @@ import io.element.android.features.messages.impl.utils.messagesummary.MessageSum
import io.element.android.features.networkmonitor.api.NetworkMonitor
import io.element.android.features.networkmonitor.api.NetworkStatus
import io.element.android.libraries.androidutils.clipboard.ClipboardHelper
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.architecture.Presenter
import io.element.android.libraries.architecture.runCatchingUpdatingState
import io.element.android.libraries.core.coroutine.CoroutineDispatchers
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -61,6 +65,7 @@ import io.element.android.libraries.designsystem.utils.SnackbarMessage @@ -61,6 +65,7 @@ import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.designsystem.utils.collectSnackbarMessageAsState
import io.element.android.libraries.matrix.api.core.EventId
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailInfo
import io.element.android.libraries.matrix.ui.components.AttachmentThumbnailType
@ -108,6 +113,22 @@ class MessagesPresenter @AssistedInject constructor( @@ -108,6 +113,22 @@ class MessagesPresenter @AssistedInject constructor(
mutableStateOf(null)
}
var hasDismissedInviteDialog by rememberSaveable {
mutableStateOf(false)
}
val inviteProgress = remember { mutableStateOf<Async<Unit>>(Async.Uninitialized) }
val showReinvitePrompt by remember(
hasDismissedInviteDialog,
composerState.hasFocus,
syncUpdateFlow,
) {
derivedStateOf {
!hasDismissedInviteDialog && composerState.hasFocus && room.isDirect && room.activeMemberCount == 1L
}
}
val networkConnectionStatus by networkMonitor.connectivity.collectAsState()
val snackbarMessage by snackbarDispatcher.collectSnackbarMessageAsState()
@ -125,6 +146,7 @@ class MessagesPresenter @AssistedInject constructor( @@ -125,6 +146,7 @@ class MessagesPresenter @AssistedInject constructor(
LaunchedEffect(composerState.mode.relatedEventId) {
timelineState.eventSink(TimelineEvents.SetHighlightedEvent(composerState.mode.relatedEventId))
}
fun handleEvents(event: MessagesEvents) {
when (event) {
is MessagesEvents.HandleAction -> {
@ -133,9 +155,17 @@ class MessagesPresenter @AssistedInject constructor( @@ -133,9 +155,17 @@ class MessagesPresenter @AssistedInject constructor(
is MessagesEvents.ToggleReaction -> {
localCoroutineScope.toggleReaction(event.emoji, event.eventId)
}
is MessagesEvents.InviteDialogDismissed -> {
hasDismissedInviteDialog = true
if (event.action == InviteDialogAction.Invite) {
localCoroutineScope.reinviteOtherUser(inviteProgress)
}
}
is MessagesEvents.Dismiss -> actionListState.eventSink(ActionListEvents.Clear)
}
}
return MessagesState(
roomId = room.roomId,
roomName = roomName.value,
@ -148,6 +178,8 @@ class MessagesPresenter @AssistedInject constructor( @@ -148,6 +178,8 @@ class MessagesPresenter @AssistedInject constructor(
retrySendMenuState = retryState,
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
snackbarMessage = snackbarMessage,
showReinvitePrompt = showReinvitePrompt,
inviteProgress = inviteProgress.value,
eventSink = ::handleEvents
)
}
@ -176,8 +208,21 @@ class MessagesPresenter @AssistedInject constructor( @@ -176,8 +208,21 @@ class MessagesPresenter @AssistedInject constructor(
.onFailure { Timber.e(it) }
}
private fun notImplementedYet() {
Timber.v("NotImplementedYet")
private fun CoroutineScope.reinviteOtherUser(inviteProgress: MutableState<Async<Unit>>) = launch(dispatchers.io) {
suspend {
room.updateMembers()
val memberList = when (val memberState = room.membersStateFlow.value) {
is MatrixRoomMembersState.Ready -> memberState.roomMembers
is MatrixRoomMembersState.Error -> memberState.prevRoomMembers.orEmpty()
else -> emptyList()
}
val member = memberList.first { it.userId != room.sessionId }
room.inviteUserById(member.userId).onFailure { t ->
Timber.e(t, "Failed to reinvite DM partner")
}.getOrThrow()
}.runCatchingUpdatingState(inviteProgress)
}
private suspend fun handleActionRedact(event: TimelineItem.Event) {

3
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesState.kt

@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer @@ -22,6 +22,7 @@ import io.element.android.features.messages.impl.messagecomposer.MessageComposer
import io.element.android.features.messages.impl.timeline.TimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.utils.SnackbarMessage
import io.element.android.libraries.matrix.api.core.RoomId
@ -39,5 +40,7 @@ data class MessagesState( @@ -39,5 +40,7 @@ data class MessagesState(
val retrySendMenuState: RetrySendMenuState,
val hasNetworkConnection: Boolean,
val snackbarMessage: SnackbarMessage?,
val inviteProgress: Async<Unit>,
val showReinvitePrompt: Boolean,
val eventSink: (MessagesEvents) -> Unit
)

4
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesStateProvider.kt

@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineState @@ -24,6 +24,7 @@ import io.element.android.features.messages.impl.timeline.aTimelineState
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionState
import io.element.android.features.messages.impl.timeline.components.retrysendmenu.RetrySendMenuState
import io.element.android.features.messages.impl.timeline.model.event.aTimelineItemTextContent
import io.element.android.libraries.architecture.Async
import io.element.android.libraries.core.data.StableCharSequence
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.avatar.AvatarSize
@ -37,6 +38,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> { @@ -37,6 +38,7 @@ open class MessagesStateProvider : PreviewParameterProvider<MessagesState> {
aMessagesState().copy(hasNetworkConnection = false),
aMessagesState().copy(composerState = aMessageComposerState().copy(showAttachmentSourcePicker = true)),
aMessagesState().copy(userHasPermissionToSendMessage = false),
aMessagesState().copy(showReinvitePrompt = true),
)
}
@ -64,5 +66,7 @@ fun aMessagesState() = MessagesState( @@ -64,5 +66,7 @@ fun aMessagesState() = MessagesState(
),
hasNetworkConnection = true,
snackbarMessage = null,
inviteProgress = Async.Uninitialized,
showReinvitePrompt = false,
eventSink = {}
)

20
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/MessagesView.kt

@ -68,6 +68,7 @@ import io.element.android.libraries.designsystem.components.ProgressDialog @@ -68,6 +68,7 @@ import io.element.android.libraries.designsystem.components.ProgressDialog
import io.element.android.libraries.designsystem.components.avatar.Avatar
import io.element.android.libraries.designsystem.components.avatar.AvatarData
import io.element.android.libraries.designsystem.components.button.BackButton
import io.element.android.libraries.designsystem.components.dialogs.ConfirmationDialog
import io.element.android.libraries.designsystem.preview.ElementPreviewDark
import io.element.android.libraries.designsystem.preview.ElementPreviewLight
import io.element.android.libraries.designsystem.theme.components.Scaffold
@ -184,6 +185,25 @@ fun MessagesView( @@ -184,6 +185,25 @@ fun MessagesView(
RetrySendMessageMenu(
state = state.retrySendMenuState
)
ReinviteDialog(
state = state
)
}
@Composable
fun ReinviteDialog(state: MessagesState) {
if (state.showReinvitePrompt) {
ConfirmationDialog(
title = stringResource(id = R.string.screen_room_invite_again_alert_title),
content = stringResource(id = R.string.screen_room_invite_again_alert_message),
cancelText = stringResource(id = CommonStrings.action_cancel),
submitText = stringResource(id = CommonStrings.action_invite),
emphasizeSubmitButton = true,
onSubmitClicked = { state.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite)) },
onDismiss = { state.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel)) }
)
}
}
@Composable

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerEvents.kt

@ -22,6 +22,7 @@ import io.element.android.libraries.textcomposer.MessageComposerMode @@ -22,6 +22,7 @@ import io.element.android.libraries.textcomposer.MessageComposerMode
@Immutable
sealed interface MessageComposerEvents {
object ToggleFullScreenState : MessageComposerEvents
data class FocusChanged(val hasFocus: Boolean) : MessageComposerEvents
data class SendMessage(val message: String) : MessageComposerEvents
object CloseSpecialMode : MessageComposerEvents
data class SetMode(val composerMode: MessageComposerMode) : MessageComposerEvents

7
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerPresenter.kt

@ -89,6 +89,9 @@ class MessageComposerPresenter @Inject constructor( @@ -89,6 +89,9 @@ class MessageComposerPresenter @Inject constructor(
val isFullScreen = rememberSaveable {
mutableStateOf(false)
}
val hasFocus = remember {
mutableStateOf(false)
}
val text: MutableState<StableCharSequence> = remember {
mutableStateOf(StableCharSequence(""))
}
@ -115,6 +118,9 @@ class MessageComposerPresenter @Inject constructor( @@ -115,6 +118,9 @@ class MessageComposerPresenter @Inject constructor(
fun handleEvents(event: MessageComposerEvents) {
when (event) {
MessageComposerEvents.ToggleFullScreenState -> isFullScreen.value = !isFullScreen.value
is MessageComposerEvents.FocusChanged -> hasFocus.value = event.hasFocus
is MessageComposerEvents.UpdateText -> text.value = event.text.toStableCharSequence()
MessageComposerEvents.CloseSpecialMode -> {
text.value = "".toStableCharSequence()
@ -158,6 +164,7 @@ class MessageComposerPresenter @Inject constructor( @@ -158,6 +164,7 @@ class MessageComposerPresenter @Inject constructor(
return MessageComposerState(
text = text.value,
isFullScreen = isFullScreen.value,
hasFocus = hasFocus.value,
mode = composerMode.value,
showAttachmentSourcePicker = showAttachmentSourcePicker,
attachmentsState = attachmentsState.value,

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerState.kt

@ -26,6 +26,7 @@ import kotlinx.collections.immutable.ImmutableList @@ -26,6 +26,7 @@ import kotlinx.collections.immutable.ImmutableList
data class MessageComposerState(
val text: StableCharSequence?,
val isFullScreen: Boolean,
val hasFocus: Boolean,
val mode: MessageComposerMode,
val showAttachmentSourcePicker: Boolean,
val attachmentsState: AttachmentsState,

1
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerStateProvider.kt

@ -30,6 +30,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider<MessageCompos @@ -30,6 +30,7 @@ open class MessageComposerStateProvider : PreviewParameterProvider<MessageCompos
fun aMessageComposerState() = MessageComposerState(
text = StableCharSequence(""),
isFullScreen = false,
hasFocus = false,
mode = MessageComposerMode.Normal(content = ""),
showAttachmentSourcePicker = false,
attachmentsState = AttachmentsState.None,

13
features/messages/impl/src/main/kotlin/io/element/android/features/messages/impl/messagecomposer/MessageComposerView.kt

@ -38,6 +38,10 @@ fun MessageComposerView( @@ -38,6 +38,10 @@ fun MessageComposerView(
state.eventSink(MessageComposerEvents.SendMessage(message))
}
fun onAddAttachment() {
state.eventSink(MessageComposerEvents.AddAttachment)
}
fun onCloseSpecialMode() {
state.eventSink(MessageComposerEvents.CloseSpecialMode)
}
@ -46,6 +50,10 @@ fun MessageComposerView( @@ -46,6 +50,10 @@ fun MessageComposerView(
state.eventSink(MessageComposerEvents.UpdateText(text))
}
fun onFocusChanged(hasFocus: Boolean) {
state.eventSink(MessageComposerEvents.FocusChanged(hasFocus))
}
Box {
AttachmentsBottomSheet(state = state)
@ -54,9 +62,8 @@ fun MessageComposerView( @@ -54,9 +62,8 @@ fun MessageComposerView(
composerMode = state.mode,
onResetComposerMode = ::onCloseSpecialMode,
onComposerTextChange = ::onComposerTextChange,
onAddAttachment = {
state.eventSink(MessageComposerEvents.AddAttachment)
},
onAddAttachment = ::onAddAttachment,
onFocusChanged = ::onFocusChanged,
composerCanSendMessage = state.isSendButtonVisible,
composerText = state.text?.charSequence?.toString(),
modifier = modifier

171
features/messages/impl/src/test/kotlin/io/element/android/features/messages/MessagesPresenterTest.kt

@ -24,11 +24,13 @@ import com.google.common.truth.Truth.assertThat @@ -24,11 +24,13 @@ import com.google.common.truth.Truth.assertThat
import io.element.android.features.analytics.test.FakeAnalyticsService
import io.element.android.features.messages.fixtures.aMessageEvent
import io.element.android.features.messages.fixtures.aTimelineItemsFactory
import io.element.android.features.messages.impl.InviteDialogAction
import io.element.android.features.messages.impl.MessagesEvents
import io.element.android.features.messages.impl.MessagesPresenter
import io.element.android.features.messages.impl.actionlist.ActionListPresenter
import io.element.android.features.messages.impl.actionlist.ActionListState
import io.element.android.features.messages.impl.actionlist.model.TimelineItemAction
import io.element.android.features.messages.impl.messagecomposer.MessageComposerEvents
import io.element.android.features.messages.impl.messagecomposer.MessageComposerPresenter
import io.element.android.features.messages.impl.timeline.TimelinePresenter
import io.element.android.features.messages.impl.timeline.components.customreaction.CustomReactionPresenter
@ -49,11 +51,16 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher @@ -49,11 +51,16 @@ import io.element.android.libraries.designsystem.utils.SnackbarDispatcher
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
import io.element.android.libraries.matrix.api.media.MediaSource
import io.element.android.libraries.matrix.api.room.MatrixRoom
import io.element.android.libraries.matrix.api.room.MatrixRoomMembersState
import io.element.android.libraries.matrix.api.room.MessageEventType
import io.element.android.libraries.matrix.api.room.RoomMembershipState
import io.element.android.libraries.matrix.test.AN_AVATAR_URL
import io.element.android.libraries.matrix.test.AN_EVENT_ID
import io.element.android.libraries.matrix.test.A_ROOM_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID
import io.element.android.libraries.matrix.test.A_SESSION_ID_2
import io.element.android.libraries.matrix.test.room.FakeMatrixRoom
import io.element.android.libraries.matrix.test.room.aRoomMember
import io.element.android.libraries.mediapickers.test.FakePickerProvider
import io.element.android.libraries.mediaupload.api.MediaSender
import io.element.android.libraries.mediaupload.test.FakeMediaPreProcessor
@ -349,6 +356,170 @@ class MessagesPresenterTest { @@ -349,6 +356,170 @@ class MessagesPresenterTest {
}
}
@Test
fun `present - shows prompt to reinvite users in DM`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID, isDirect = true, activeMemberCount = 1L)
val presenter = createMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(3)
val initialState = awaitItem()
// Initially the composer doesn't have focus, so we don't show the alert
assertThat(initialState.showReinvitePrompt).isFalse()
// When the input field is focused we show the alert
initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true))
val focusedState = awaitItem()
assertThat(focusedState.showReinvitePrompt).isTrue()
// If it's dismissed then we stop showing the alert
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Cancel))
val dismissedState = awaitItem()
assertThat(dismissedState.showReinvitePrompt).isFalse()
}
}
@Test
fun `present - doesn't show reinvite prompt in non-direct room`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID, isDirect = false, activeMemberCount = 1L)
val presenter = createMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(3)
val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse()
initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true))
val focusedState = awaitItem()
assertThat(focusedState.showReinvitePrompt).isFalse()
}
}
@Test
fun `present - doesn't show reinvite prompt if other party is present`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID, isDirect = true, activeMemberCount = 2L)
val presenter = createMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
skipItems(3)
val initialState = awaitItem()
assertThat(initialState.showReinvitePrompt).isFalse()
initialState.composerState.eventSink(MessageComposerEvents.FocusChanged(true))
val focusedState = awaitItem()
assertThat(focusedState.showReinvitePrompt).isFalse()
}
}
@Test
fun `present - handle reinviting other user when memberlist is ready`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID)
room.givenRoomMembersState(
MatrixRoomMembersState.Ready(
listOf(
aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN),
aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE),
)
)
)
val presenter = createMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(3)
val loadingState = awaitItem()
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
val newState = awaitItem()
assertThat(newState.inviteProgress.isSuccess()).isTrue()
assertThat(room.invitedUserId).isEqualTo(A_SESSION_ID_2)
}
}
@Test
fun `present - handle reinviting other user when memberlist is error`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID)
room.givenRoomMembersState(
MatrixRoomMembersState.Error(
failure = Throwable(),
prevRoomMembers = listOf(
aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN),
aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE),
)
)
)
val presenter = createMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(3)
val loadingState = awaitItem()
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
val newState = awaitItem()
assertThat(newState.inviteProgress.isSuccess()).isTrue()
assertThat(room.invitedUserId).isEqualTo(A_SESSION_ID_2)
}
}
@Test
fun `present - handle reinviting other user when memberlist is not ready`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID)
room.givenRoomMembersState(MatrixRoomMembersState.Unknown)
val presenter = createMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(3)
val loadingState = awaitItem()
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
val newState = awaitItem()
assertThat(newState.inviteProgress.isFailure()).isTrue()
}
}
@Test
fun `present - handle reinviting other user when inviting fails`() = runTest {
val room = FakeMatrixRoom(sessionId = A_SESSION_ID)
room.givenRoomMembersState(
MatrixRoomMembersState.Ready(
listOf(
aRoomMember(userId = A_SESSION_ID, membership = RoomMembershipState.JOIN),
aRoomMember(userId = A_SESSION_ID_2, membership = RoomMembershipState.LEAVE),
)
)
)
room.givenInviteUserResult(Result.failure(Throwable("Oops!")))
val presenter = createMessagePresenter(matrixRoom = room)
moleculeFlow(RecompositionClock.Immediate) {
presenter.present()
}.test {
val initialState = awaitItem()
initialState.eventSink(MessagesEvents.InviteDialogDismissed(InviteDialogAction.Invite))
skipItems(3)
val loadingState = awaitItem()
assertThat(loadingState.inviteProgress.isLoading()).isTrue()
val newState = awaitItem()
assertThat(newState.inviteProgress.isFailure()).isTrue()
}
}
@Test
fun `present - permission to post`() = runTest {
val matrixRoom = FakeMatrixRoom()

1
libraries/matrix/api/src/main/kotlin/io/element/android/libraries/matrix/api/room/MatrixRoom.kt

@ -44,6 +44,7 @@ interface MatrixRoom : Closeable { @@ -44,6 +44,7 @@ interface MatrixRoom : Closeable {
val isEncrypted: Boolean
val isDirect: Boolean
val isPublic: Boolean
val activeMemberCount: Long
val joinedMemberCount: Long
/**

3
libraries/matrix/impl/src/main/kotlin/io/element/android/libraries/matrix/impl/room/RustMatrixRoom.kt

@ -168,6 +168,9 @@ class RustMatrixRoom( @@ -168,6 +168,9 @@ class RustMatrixRoom(
override val joinedMemberCount: Long
get() = innerRoom.joinedMembersCount().toLong()
override val activeMemberCount: Long
get() = innerRoom.activeMembersCount().toLong()
override suspend fun updateMembers(): Result<Unit> = withContext(coroutineDispatchers.io) {
val currentState = _membersStateFlow.value
val currentMembers = currentState.roomMembers()

1
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/FakeMatrixRoom.kt

@ -52,6 +52,7 @@ class FakeMatrixRoom( @@ -52,6 +52,7 @@ class FakeMatrixRoom(
override val isPublic: Boolean = true,
override val isDirect: Boolean = false,
override val joinedMemberCount: Long = 123L,
override val activeMemberCount: Long = 234L,
private val matrixTimeline: MatrixTimeline = FakeMatrixTimeline(),
) : MatrixRoom {

41
libraries/matrix/test/src/main/kotlin/io/element/android/libraries/matrix/test/room/RoomMemberFixture.kt

@ -0,0 +1,41 @@ @@ -0,0 +1,41 @@
/*
* Copyright (c) 2023 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.libraries.matrix.test.room
import io.element.android.libraries.matrix.api.core.UserId
import io.element.android.libraries.matrix.api.room.RoomMember
import io.element.android.libraries.matrix.api.room.RoomMembershipState
fun aRoomMember(
userId: UserId = UserId("@alice:server.org"),
displayName: String? = null,
avatarUrl: String? = null,
membership: RoomMembershipState = RoomMembershipState.JOIN,
isNameAmbiguous: Boolean = false,
powerLevel: Long = 0L,
normalizedPowerLevel: Long = 0L,
isIgnored: Boolean = false,
) = RoomMember(
userId = userId,
displayName = displayName,
avatarUrl = avatarUrl,
membership = membership,
isNameAmbiguous = isNameAmbiguous,
powerLevel = powerLevel,
normalizedPowerLevel = normalizedPowerLevel,
isIgnored = isIgnored,
)

5
libraries/textcomposer/src/main/kotlin/io/element/android/libraries/textcomposer/TextComposer.kt

@ -53,6 +53,7 @@ import androidx.compose.ui.Modifier @@ -53,6 +53,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.SolidColor
@ -93,6 +94,7 @@ fun TextComposer( @@ -93,6 +94,7 @@ fun TextComposer(
onResetComposerMode: () -> Unit = {},
onComposerTextChange: (CharSequence) -> Unit = {},
onAddAttachment: () -> Unit = {},
onFocusChanged: (Boolean) -> Unit = {},
) {
val text = composerText.orEmpty()
Row(
@ -129,7 +131,8 @@ fun TextComposer( @@ -129,7 +131,8 @@ fun TextComposer(
modifier = Modifier
.fillMaxWidth()
.heightIn(min = minHeight)
.focusRequester(focusRequester),
.focusRequester(focusRequester)
.onFocusEvent { onFocusChanged(it.hasFocus) },
value = text,
onValueChange = { onComposerTextChange(it) },
onTextLayout = {

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewDarkPreview_0_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.

BIN
tests/uitests/src/test/snapshots/images/io.element.android.tests.uitests_ScreenshotTest_preview_tests[io.element.android.features.messages.impl_null_DefaultGroup_MessagesViewLightPreview_0_null_4,NEXUS_5,1.0,en].png (Stored with Git LFS)

Binary file not shown.
Loading…
Cancel
Save