Show a prompt to reinvite other party in a DM
If you are looking at a DM where the other party has left then when you focus the composer for the first time we'll show a dialog asking if you want to reinvite the other party. Closes #590
This commit is contained in:
parent
2ce614ae97
commit
53861fedb6
18 changed files with 327 additions and 6 deletions
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|||
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
|
|||
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(
|
|||
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(
|
|||
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(
|
|||
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(
|
|||
retrySendMenuState = retryState,
|
||||
hasNetworkConnection = networkConnectionStatus == NetworkStatus.Online,
|
||||
snackbarMessage = snackbarMessage,
|
||||
showReinvitePrompt = showReinvitePrompt,
|
||||
inviteProgress = inviteProgress.value,
|
||||
eventSink = ::handleEvents
|
||||
)
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
val retrySendMenuState: RetrySendMenuState,
|
||||
val hasNetworkConnection: Boolean,
|
||||
val snackbarMessage: SnackbarMessage?,
|
||||
val inviteProgress: Async<Unit>,
|
||||
val showReinvitePrompt: Boolean,
|
||||
val eventSink: (MessagesEvents) -> Unit
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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> {
|
|||
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(
|
|||
),
|
||||
hasNetworkConnection = true,
|
||||
snackbarMessage = null,
|
||||
inviteProgress = Async.Uninitialized,
|
||||
showReinvitePrompt = false,
|
||||
eventSink = {}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
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(
|
|||
return MessageComposerState(
|
||||
text = text.value,
|
||||
isFullScreen = isFullScreen.value,
|
||||
hasFocus = hasFocus.value,
|
||||
mode = composerMode.value,
|
||||
showAttachmentSourcePicker = showAttachmentSourcePicker,
|
||||
attachmentsState = attachmentsState.value,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
state.eventSink(MessageComposerEvents.UpdateText(text))
|
||||
}
|
||||
|
||||
fun onFocusChanged(hasFocus: Boolean) {
|
||||
state.eventSink(MessageComposerEvents.FocusChanged(hasFocus))
|
||||
}
|
||||
|
||||
Box {
|
||||
AttachmentsBottomSheet(state = state)
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue