Skip to content

Commit

Permalink
Handle buttons toggling in Composer on body draft changes
Browse files Browse the repository at this point in the history
MAILANDR-2397
  • Loading branch information
nick0602 committed Dec 17, 2024
1 parent 4eaa13c commit 44e2994
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@ import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Divider
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
Expand All @@ -55,19 +54,20 @@ fun ComposerBottomBar(
isMessageExpirationTimeSet: Boolean,
onSetMessagePasswordClick: (MessageId, SenderEmail) -> Unit,
onSetExpirationTimeClick: () -> Unit,
enableInteractions: Boolean,
modifier: Modifier = Modifier
) {
Column(modifier = modifier.fillMaxWidth()) {
Divider(color = ProtonTheme.colors.separatorNorm, thickness = MailDimens.SeparatorHeight)
HorizontalDivider(color = ProtonTheme.colors.separatorNorm, thickness = MailDimens.SeparatorHeight)
Row(
modifier = Modifier
.fillMaxWidth()
.height(MailDimens.ExtraLargeSpacing)
.padding(horizontal = ProtonDimens.ExtraSmallSpacing),
verticalAlignment = Alignment.CenterVertically
) {
AddPasswordButton(draftId, senderEmail, isMessagePasswordSet, onSetMessagePasswordClick)
SetExpirationButton(isMessageExpirationTimeSet, onSetExpirationTimeClick)
AddPasswordButton(draftId, senderEmail, isMessagePasswordSet, enableInteractions, onSetMessagePasswordClick)
SetExpirationButton(isMessageExpirationTimeSet, enableInteractions, onSetExpirationTimeClick)
}
}
}
Expand All @@ -77,23 +77,30 @@ private fun AddPasswordButton(
draftId: MessageId,
senderEmail: SenderEmail,
isMessagePasswordSet: Boolean,
isEnabled: Boolean,
onSetMessagePasswordClick: (MessageId, SenderEmail) -> Unit
) {
BottomBarButton(
iconRes = R.drawable.ic_proton_lock,
contentDescriptionRes = R.string.composer_button_add_password,
shouldShowCheckmark = isMessagePasswordSet,
onClick = { onSetMessagePasswordClick(draftId, senderEmail) }
onClick = { onSetMessagePasswordClick(draftId, senderEmail) },
isEnabled = isEnabled
)
}

@Composable
private fun SetExpirationButton(isMessageExpirationTimeSet: Boolean, onSetExpirationTimeClick: () -> Unit) {
private fun SetExpirationButton(
isMessageExpirationTimeSet: Boolean,
isEnabled: Boolean,
onSetExpirationTimeClick: () -> Unit
) {
BottomBarButton(
iconRes = R.drawable.ic_proton_hourglass,
contentDescriptionRes = R.string.composer_button_set_expiration,
shouldShowCheckmark = isMessageExpirationTimeSet,
onClick = onSetExpirationTimeClick
onClick = onSetExpirationTimeClick,
isEnabled = isEnabled
)
}

Expand All @@ -102,18 +109,17 @@ private fun BottomBarButton(
@DrawableRes iconRes: Int,
@StringRes contentDescriptionRes: Int,
shouldShowCheckmark: Boolean,
onClick: () -> Unit
onClick: () -> Unit,
isEnabled: Boolean
) {
Box {
IconButton(
EnabledStateIconButton(
icon = painterResource(id = iconRes),
isEnabled = isEnabled,
contentDescription = stringResource(id = contentDescriptionRes),
onClick = onClick
) {
Icon(
painter = painterResource(id = iconRes),
contentDescription = stringResource(id = contentDescriptionRes),
tint = ProtonTheme.colors.iconNorm
)
}
)

if (shouldShowCheckmark) {
Box(
modifier = Modifier
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,10 @@ fun ComposerScreen(actions: ComposerScreen.Actions, viewModel: ComposerViewModel
val context = LocalContext.current
val view = LocalView.current
val keyboardController = LocalSoftwareKeyboardController.current

val state by viewModel.state.collectAsStateWithLifecycle()
val isUpdatingBodyState by viewModel.isBodyUpdating.collectAsStateWithLifecycle()

var recipientsOpen by rememberSaveable { mutableStateOf(false) }
var focusedField by rememberSaveable {
mutableStateOf(if (state.fields.to.isEmpty()) FocusedFieldType.TO else FocusedFieldType.BODY)
Expand Down Expand Up @@ -186,7 +189,8 @@ fun ComposerScreen(actions: ComposerScreen.Actions, viewModel: ComposerViewModel
onSendMessageComposerClick = {
viewModel.submit(ComposerAction.OnSendMessage)
},
isSendMessageButtonEnabled = state.isSubmittable
isSendMessageButtonEnabled = state.isSubmittable && !isUpdatingBodyState,
enableSecondaryButtonsInteraction = !isUpdatingBodyState
)
},
bottomBar = {
Expand All @@ -199,7 +203,8 @@ fun ComposerScreen(actions: ComposerScreen.Actions, viewModel: ComposerViewModel
onSetExpirationTimeClick = {
bottomSheetType.value = BottomSheetType.SetExpirationTime
viewModel.submit(ComposerAction.OnSetExpirationTimeRequested)
}
},
enableInteractions = !isUpdatingBodyState
)
},
snackbarHost = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,11 @@ import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
Expand All @@ -54,42 +53,37 @@ internal fun ComposerTopBar(
onAddAttachmentsClick: () -> Unit,
onCloseComposerClick: () -> Unit,
onSendMessageComposerClick: () -> Unit,
isSendMessageButtonEnabled: Boolean
isSendMessageButtonEnabled: Boolean,
enableSecondaryButtonsInteraction: Boolean
) {
ProtonTopAppBar(
modifier = Modifier.testTag(ComposerTestTags.TopAppBar),
title = {},
navigationIcon = {
IconButton(
modifier = Modifier.testTag(ComposerTestTags.CloseButton),
onClick = onCloseComposerClick
) {
Icon(
imageVector = Icons.Filled.Close,
tint = ProtonTheme.colors.iconNorm,
contentDescription = stringResource(R.string.close_composer_content_description)
)
}
EnabledStateIconButton(
icon = rememberVectorPainter(Icons.Filled.Close),
isEnabled = enableSecondaryButtonsInteraction,
contentDescription = stringResource(R.string.close_composer_content_description),
onClick = onCloseComposerClick,
modifier = Modifier.testTag(ComposerTestTags.CloseButton)
)
},
actions = {
AttachmentsButton(attachmentsCount = attachmentsCount, onClick = onAddAttachmentsClick)
IconButton(
AttachmentsButton(
attachmentsCount = attachmentsCount,
onClick = onAddAttachmentsClick,
isEnabled = enableSecondaryButtonsInteraction
)

EnabledStateIconButton(
icon = painterResource(id = R.drawable.ic_proton_paper_plane),
isEnabled = isSendMessageButtonEnabled,
contentDescription = stringResource(R.string.send_message_content_description),
onClick = onSendMessageComposerClick,
modifier = Modifier
.testTag(ComposerTestTags.SendButton)
.thenIf(!isSendMessageButtonEnabled) { semantics { disabled() } },
onClick = onSendMessageComposerClick,
enabled = isSendMessageButtonEnabled
) {
Icon(
painter = painterResource(id = R.drawable.ic_proton_paper_plane),
tint = if (isSendMessageButtonEnabled) {
ProtonTheme.colors.iconNorm
} else {
ProtonTheme.colors.iconDisabled
},
contentDescription = stringResource(R.string.send_message_content_description)
)
}
.thenIf(!isSendMessageButtonEnabled) { semantics { disabled() } }
)
}
)
}
Expand All @@ -98,6 +92,7 @@ internal fun ComposerTopBar(
private fun AttachmentsButton(
attachmentsCount: Int,
onClick: () -> Unit,
isEnabled: Boolean,
modifier: Modifier = Modifier
) {
Row(
Expand All @@ -108,16 +103,14 @@ private fun AttachmentsButton(
if (attachmentsCount > 0) {
AttachmentsNumber(attachmentsCount)
}
IconButton(

EnabledStateIconButton(
icon = painterResource(id = R.drawable.ic_proton_paper_clip),
isEnabled = isEnabled,
contentDescription = stringResource(id = R.string.composer_add_attachments_content_description),
modifier = Modifier.testTag(ComposerTestTags.AttachmentsButton),
onClick = onClick
) {
Icon(
painter = painterResource(id = R.drawable.ic_proton_paper_clip),
contentDescription = stringResource(id = R.string.composer_add_attachments_content_description),
tint = ProtonTheme.colors.iconNorm
)
}
)
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* Copyright (c) 2022 Proton Technologies AG
* This file is part of Proton Technologies AG and Proton Mail.
*
* Proton Mail is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Proton Mail is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Proton Mail. If not, see <https://www.gnu.org/licenses/>.
*/

package ch.protonmail.android.mailcomposer.presentation.ui

import androidx.compose.animation.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.semantics.disabled
import androidx.compose.ui.semantics.semantics
import ch.protonmail.android.uicomponents.chips.thenIf
import kotlinx.coroutines.launch
import me.proton.core.compose.theme.ProtonTheme

@Composable
fun EnabledStateIconButton(
icon: Painter,
isEnabled: Boolean,
contentDescription: String,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val enabledColor = ProtonTheme.colors.iconNorm
val disabledColor = ProtonTheme.colors.iconDisabled

val animatedColor = remember { Animatable(if (isEnabled) enabledColor else disabledColor) }
val scope = rememberCoroutineScope()

LaunchedEffect(isEnabled) {
val targetColor = if (isEnabled) enabledColor else disabledColor

// Animate to the target color without abrupt interruptions
if (animatedColor.targetValue != targetColor) {
scope.launch {
animatedColor.animateTo(
targetValue = targetColor,
animationSpec = tween(durationMillis = 500)
)
}
}
}

IconButton(
modifier = modifier
.thenIf(!isEnabled) { semantics { disabled() } },
onClick = onClick,
enabled = isEnabled
) {
Icon(
painter = icon,
tint = animatedColor.value,
contentDescription = contentDescription
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,7 @@ class ComposerViewModel @Inject constructor(
)
)
val state: StateFlow<ComposerDraftState> = mutableState
val isBodyUpdating = MutableStateFlow(false)

private val composerActionsChannel = Channel<ComposerAction>(Channel.BUFFERED)

Expand Down Expand Up @@ -401,6 +402,7 @@ class ComposerViewModel @Inject constructor(
when (action) {
is ComposerAction.AttachmentsAdded -> onAttachmentsAdded(action)
is ComposerAction.DraftBodyChanged -> onDraftBodyChanged(action)

is ComposerAction.SenderChanged -> emitNewStateFor(onSenderChanged(action))
is ComposerAction.SubjectChanged -> emitNewStateFor(onSubjectChanged(action))
is ComposerAction.ChangeSenderRequested -> emitNewStateFor(onChangeSender())
Expand Down Expand Up @@ -650,10 +652,15 @@ class ComposerViewModel @Inject constructor(
)

private suspend fun onDraftBodyChanged(action: ComposerAction.DraftBodyChanged) {
isBodyUpdating.value = true

emitNewStateFor(ComposerAction.DraftBodyChanged(action.draftBody))

// Do not store the draft if the body is exactly the same as signature + footer.
if (isBodyEmptyOrEqualsToSignatureAndFooter(action.draftBody)) return
if (isBodyEmptyOrEqualsToSignatureAndFooter(action.draftBody)) {
isBodyUpdating.value = false
return
}

storeDraftWithBody(
primaryUserId(),
Expand All @@ -662,6 +669,8 @@ class ComposerViewModel @Inject constructor(
currentDraftQuotedHtmlBody(),
currentSenderEmail()
).onLeft { emitNewStateFor(ComposerEvent.ErrorStoringDraftBody) }

isBodyUpdating.value = false
}

private suspend fun injectAddressSignature(senderEmail: SenderEmail, previousSenderEmail: SenderEmail? = null) {
Expand Down

0 comments on commit 44e2994

Please sign in to comment.