Skip to content

Commit 8382546

Browse files
committed
Disable toolbar formatting options when the webview is unfocused
1 parent c9bff4e commit 8382546

8 files changed

+104
-43
lines changed

app/src/main/java/com/infomaniak/mail/ui/main/folder/ThreadListMultiSelection.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ class ThreadListMultiSelection {
141141
private fun displaySelectionToolbar(isMultiSelectOn: Boolean) = with(threadListFragment.binding) {
142142
val autoTransition = AutoTransition()
143143
autoTransition.duration = TOOLBAR_FADE_DURATION
144-
TransitionManager.beginDelayedTransition(formatOptionsLayout, autoTransition)
144+
TransitionManager.beginDelayedTransition(formatOptionsScrollView, autoTransition)
145145

146146
toolbar.isGone = isMultiSelectOn
147147
toolbarSelection.isVisible = isMultiSelectOn

app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageFragment.kt

+38-16
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ import androidx.activity.addCallback
3636
import androidx.appcompat.app.AppCompatActivity
3737
import androidx.appcompat.content.res.AppCompatResources
3838
import androidx.constraintlayout.widget.Group
39+
import androidx.core.view.forEach
3940
import androidx.core.view.isGone
4041
import androidx.core.view.isVisible
4142
import androidx.fragment.app.Fragment
@@ -191,8 +192,11 @@ class NewMessageFragment : Fragment() {
191192
observeUiQuote()
192193
observeShimmering()
193194

194-
editorManager.observeEditorActions()
195-
editorManager.observeEditorStatus()
195+
with(editorManager) {
196+
observeEditorActions()
197+
observeEditorStatus()
198+
}
199+
196200
externalsManager.observeExternals(newMessageViewModel.arrivedFromExistingDraft())
197201

198202
with(aiManager) {
@@ -205,6 +209,7 @@ class NewMessageFragment : Fragment() {
205209
setOnFocusChangedListeners()
206210
observeContacts()
207211
observeCcAndBccVisibility()
212+
observeFocusedElement()
208213
}
209214
}
210215

@@ -351,27 +356,44 @@ class NewMessageFragment : Fragment() {
351356
}
352357

353358
private fun initEditorUi() {
354-
binding.editorWebView.apply {
355-
subscribeToStates(setOf(BOLD, ITALIC, UNDERLINE, STRIKE_THROUGH, UNORDERED_LIST))
359+
binding.editorWebView.subscribeToStates(setOf(BOLD, ITALIC, UNDERLINE, STRIKE_THROUGH, UNORDERED_LIST))
360+
setEditorStyle()
361+
handleEditorPlaceholderVisibility()
356362

357-
enableAlgorithmicDarkening(isEnabled = true)
358-
if (context.isNightModeEnabled()) addCss(context.loadCss(R.raw.custom_dark_mode))
363+
setToolbarEnabledStatus(false)
364+
disableButtonsWhenFocusIsLost()
365+
}
359366

360-
val customColors = listOf(PRIMARY_COLOR_CODE to context.getAttributeColor(RMaterial.attr.colorPrimary))
361-
addCss(context.loadCss(R.raw.style, customColors))
362-
addCss(context.loadCss(R.raw.editor_style, customColors))
367+
private fun setEditorStyle() = with(binding.editorWebView) {
368+
enableAlgorithmicDarkening(isEnabled = true)
369+
if (context.isNightModeEnabled()) addCss(context.loadCss(R.raw.custom_dark_mode))
370+
371+
val customColors = listOf(PRIMARY_COLOR_CODE to context.getAttributeColor(RMaterial.attr.colorPrimary))
372+
addCss(context.loadCss(R.raw.style, customColors))
373+
addCss(context.loadCss(R.raw.editor_style, customColors))
374+
}
363375

364-
val isPlaceholderVisible = combine(
365-
isEmptyFlow.filterNotNull(),
366-
newMessageViewModel.isShimmering,
367-
) { isEditorEmpty, isShimmering -> isEditorEmpty && !isShimmering }
376+
private fun handleEditorPlaceholderVisibility() {
377+
val isPlaceholderVisible = combine(
378+
binding.editorWebView.isEmptyFlow.filterNotNull(),
379+
newMessageViewModel.isShimmering,
380+
) { isEditorEmpty, isShimmering -> isEditorEmpty && !isShimmering }
368381

369-
isPlaceholderVisible
370-
.onEach { isVisible -> binding.newMessagePlaceholder.isVisible = isVisible }
371-
.launchIn(lifecycleScope)
382+
isPlaceholderVisible
383+
.onEach { isVisible -> binding.newMessagePlaceholder.isVisible = isVisible }
384+
.launchIn(lifecycleScope)
385+
}
386+
387+
private fun disableButtonsWhenFocusIsLost() {
388+
newMessageViewModel.isEditorWebViewFocusedLiveData.observe(viewLifecycleOwner) { hasFocus ->
389+
setToolbarEnabledStatus(hasFocus)
372390
}
373391
}
374392

393+
private fun setToolbarEnabledStatus(isEnabled: Boolean) {
394+
binding.formatOptionsLayout.forEach { view -> view.isEnabled = isEnabled }
395+
}
396+
375397
private fun initializeDraft() = with(newMessageViewModel) {
376398
if (initResult.value == null) {
377399
initDraftAndViewModel(intent = requireActivity().intent).observe(viewLifecycleOwner) { draft ->

app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageRecipientFieldsManager.kt

+30-18
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
*/
1818
package com.infomaniak.mail.ui.newMessage
1919

20-
import android.view.View
2120
import androidx.core.view.isGone
2221
import androidx.core.view.isVisible
2322
import com.infomaniak.mail.data.models.correspondent.Recipient
@@ -34,7 +33,7 @@ class NewMessageRecipientFieldsManager @Inject constructor(private val snackbarM
3433
private var _externalsManager: NewMessageExternalsManager? = null
3534
private inline val externalsManager: NewMessageExternalsManager get() = _externalsManager!!
3635

37-
private var lastFieldToTakeFocus: FieldType? = TO
36+
private var lastFieldToTakeFocus: FocusableElement? = FocusableElement.TO
3837

3938
fun initValues(
4039
newMessageViewModel: NewMessageViewModel,
@@ -59,7 +58,7 @@ class NewMessageRecipientFieldsManager @Inject constructor(private val snackbarM
5958
onContactAddedCallback = { newMessageViewModel.addRecipientToField(recipient = it, type = TO) },
6059
onContactRemovedCallback = { recipient -> recipient.removeInViewModelAndUpdateBannerVisibility(TO) },
6160
onCopyContactAddressCallback = { fragment.copyRecipientEmailToClipboard(it, snackbarManager) },
62-
gotFocusCallback = { fieldGotFocus(TO) },
61+
gotFocusCallback = { elementGotFocus(FocusableElement.TO) },
6362
onToggleEverythingCallback = ::openAdvancedFields,
6463
)
6564

@@ -69,7 +68,7 @@ class NewMessageRecipientFieldsManager @Inject constructor(private val snackbarM
6968
onContactAddedCallback = { newMessageViewModel.addRecipientToField(recipient = it, type = CC) },
7069
onContactRemovedCallback = { recipient -> recipient.removeInViewModelAndUpdateBannerVisibility(CC) },
7170
onCopyContactAddressCallback = { fragment.copyRecipientEmailToClipboard(it, snackbarManager) },
72-
gotFocusCallback = { fieldGotFocus(CC) },
71+
gotFocusCallback = { elementGotFocus(FocusableElement.CC) },
7372
)
7473

7574
bccField.initRecipientField(
@@ -78,7 +77,7 @@ class NewMessageRecipientFieldsManager @Inject constructor(private val snackbarM
7877
onContactAddedCallback = { newMessageViewModel.addRecipientToField(recipient = it, type = BCC) },
7978
onContactRemovedCallback = { recipient -> recipient.removeInViewModelAndUpdateBannerVisibility(BCC) },
8079
onCopyContactAddressCallback = { fragment.copyRecipientEmailToClipboard(it, snackbarManager) },
81-
gotFocusCallback = { fieldGotFocus(BCC) },
80+
gotFocusCallback = { elementGotFocus(FocusableElement.BCC) },
8281
)
8382
}
8483

@@ -97,18 +96,24 @@ class NewMessageRecipientFieldsManager @Inject constructor(private val snackbarM
9796
externalsManager.updateBannerVisibility()
9897
}
9998

100-
private fun fieldGotFocus(field: FieldType?) = with(binding) {
101-
if (lastFieldToTakeFocus == field) return
99+
private fun elementGotFocus(element: FocusableElement?) {
100+
newMessageViewModel.focusedElementLiveData.value = element
101+
}
102102

103-
if (field == null && newMessageViewModel.otherRecipientsFieldsAreEmpty.value == true) {
104-
toField.collapseEverything()
105-
} else {
106-
if (field != TO) toField.collapse()
107-
if (field != CC) ccField.collapse()
108-
if (field != BCC) bccField.collapse()
109-
}
103+
fun observeFocusedElement() = with(binding) {
104+
newMessageViewModel.focusedElementLiveData.observe(viewLifecycleOwner) { field ->
105+
if (lastFieldToTakeFocus == field) return@observe
106+
107+
if (field == null && newMessageViewModel.otherRecipientsFieldsAreEmpty.value == true) {
108+
toField.collapseEverything()
109+
} else {
110+
if (field != FocusableElement.TO) toField.collapse()
111+
if (field != FocusableElement.CC) ccField.collapse()
112+
if (field != FocusableElement.BCC) bccField.collapse()
113+
}
110114

111-
lastFieldToTakeFocus = field
115+
lastFieldToTakeFocus = field
116+
}
112117
}
113118

114119
private fun openAdvancedFields(isCollapsed: Boolean) = with(binding) {
@@ -122,9 +127,8 @@ class NewMessageRecipientFieldsManager @Inject constructor(private val snackbarM
122127
}
123128

124129
fun setOnFocusChangedListeners() = with(binding) {
125-
val listener = View.OnFocusChangeListener { _, hasFocus -> if (hasFocus) fieldGotFocus(null) }
126-
subjectTextField.onFocusChangeListener = listener
127-
editorWebView.onFocusChangeListener = listener
130+
subjectTextField.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) elementGotFocus(FocusableElement.OTHER) }
131+
editorWebView.setOnFocusChangeListener { _, hasFocus -> if (hasFocus) elementGotFocus(FocusableElement.BODY) }
128132
}
129133

130134
fun focusBodyField() {
@@ -155,4 +159,12 @@ class NewMessageRecipientFieldsManager @Inject constructor(private val snackbarM
155159
CC,
156160
BCC,
157161
}
162+
163+
enum class FocusableElement {
164+
TO,
165+
CC,
166+
BCC,
167+
BODY,
168+
OTHER,
169+
}
158170
}

app/src/main/java/com/infomaniak/mail/ui/newMessage/NewMessageViewModel.kt

+6-1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import androidx.lifecycle.MutableLiveData
3131
import androidx.lifecycle.SavedStateHandle
3232
import androidx.lifecycle.asLiveData
3333
import androidx.lifecycle.liveData
34+
import androidx.lifecycle.map
3435
import androidx.lifecycle.viewModelScope
3536
import com.infomaniak.lib.core.MatomoCore.TrackerAction
3637
import com.infomaniak.lib.core.utils.SentryLog
@@ -71,6 +72,7 @@ import com.infomaniak.mail.ui.main.SnackbarManager
7172
import com.infomaniak.mail.ui.newMessage.NewMessageActivity.DraftSaveConfiguration
7273
import com.infomaniak.mail.ui.newMessage.NewMessageEditorManager.EditorAction
7374
import com.infomaniak.mail.ui.newMessage.NewMessageRecipientFieldsManager.FieldType
75+
import com.infomaniak.mail.ui.newMessage.NewMessageRecipientFieldsManager.FocusableElement
7476
import com.infomaniak.mail.ui.newMessage.NewMessageViewModel.SignatureScore.*
7577
import com.infomaniak.mail.utils.AccountUtils
7678
import com.infomaniak.mail.utils.ContactUtils.arrangeMergedContacts
@@ -168,7 +170,10 @@ class NewMessageViewModel @Inject constructor(
168170

169171
private var snapshot: DraftSnapshot? = null
170172

171-
var otherRecipientsFieldsAreEmpty = MutableLiveData(true)
173+
val otherRecipientsFieldsAreEmpty = MutableLiveData(true)
174+
val focusedElementLiveData = MutableLiveData<FocusableElement>()
175+
val isEditorWebViewFocusedLiveData = focusedElementLiveData.map { it == FocusableElement.BODY }
176+
172177
var initializeFieldsAsOpen = SingleLiveEvent<Boolean>()
173178
val importAttachmentsLiveData = SingleLiveEvent<List<Uri>>()
174179
val importAttachmentsResult = SingleLiveEvent<ImportationResult>()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?xml version="1.0" encoding="utf-8"?><!--
2+
~ Copyright (C) 2024 Infomaniak Network SA
3+
~ Infomaniak Mail - Android
4+
~
5+
~ This program is free software: you can redistribute it and/or modify
6+
~ it under the terms of the GNU General Public License as published by
7+
~ the Free Software Foundation, either version 3 of the License, or
8+
~ (at your option) any later version.
9+
~
10+
~ This program is distributed in the hope that it will be useful,
11+
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
~ GNU General Public License for more details.
14+
~
15+
~ You should have received a copy of the GNU General Public License
16+
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
-->
18+
<selector xmlns:android="http://schemas.android.com/apk/res/android">
19+
<item android:color="@color/disabledIconColor" android:state_enabled="false" />
20+
<item android:color="@color/iconColor" />
21+
</selector>

app/src/main/res/layout/fragment_new_message.xml

+5-4
Original file line numberDiff line numberDiff line change
@@ -596,13 +596,13 @@
596596
app:icon="@drawable/ic_ai_magic_wand"
597597
app:iconTint="@color/aiColor"
598598
app:layout_constraintBottom_toBottomOf="parent"
599-
app:layout_constraintEnd_toStartOf="@id/formatOptionsLayout"
599+
app:layout_constraintEnd_toStartOf="@id/formatOptionsScrollView"
600600
app:layout_constraintStart_toEndOf="@+id/editorClock"
601601
app:layout_constraintTop_toTopOf="parent"
602602
tools:visibility="visible" />
603603

604604
<HorizontalScrollView
605-
android:id="@+id/formatOptionsLayout"
605+
android:id="@+id/formatOptionsScrollView"
606606
android:layout_width="0dp"
607607
android:layout_height="match_parent"
608608
app:layout_constraintBottom_toBottomOf="parent"
@@ -611,6 +611,7 @@
611611
app:layout_constraintTop_toTopOf="parent">
612612

613613
<LinearLayout
614+
android:id="@+id/formatOptionsLayout"
614615
android:layout_width="wrap_content"
615616
android:layout_height="match_parent"
616617
android:orientation="horizontal">
@@ -660,7 +661,7 @@
660661
android:layout_marginVertical="@dimen/marginStandardSmall"
661662
app:layout_constraintBottom_toBottomOf="parent"
662663
app:layout_constraintEnd_toStartOf="@id/sendButton"
663-
app:layout_constraintStart_toEndOf="@id/formatOptionsLayout"
664+
app:layout_constraintStart_toEndOf="@id/formatOptionsScrollView"
664665
app:layout_constraintTop_toTopOf="parent" />
665666

666667
<com.google.android.material.button.MaterialButton
@@ -680,7 +681,7 @@
680681
android:layout_width="wrap_content"
681682
android:layout_height="wrap_content"
682683
android:visibility="gone"
683-
app:constraint_referenced_ids="formatOptionsLayout,formatOptionsDivider"
684+
app:constraint_referenced_ids="formatOptionsScrollView,formatOptionsDivider"
684685
tools:visibility="visible" />
685686

686687
<androidx.constraintlayout.widget.Group

app/src/main/res/layout/fragment_thread_list.xml

+2-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
android:layout_height="wrap_content">
4242

4343
<FrameLayout
44-
android:id="@+id/formatOptionsLayout"
44+
android:id="@+id/formatOptionsScrollView"
4545
android:layout_width="match_parent"
4646
android:layout_height="wrap_content"
4747
app:layout_constraintEnd_toEndOf="parent"
@@ -135,7 +135,7 @@
135135
app:layout_constraintBottom_toBottomOf="parent"
136136
app:layout_constraintEnd_toStartOf="@id/unreadCountChip"
137137
app:layout_constraintStart_toStartOf="parent"
138-
app:layout_constraintTop_toBottomOf="@id/formatOptionsLayout">
138+
app:layout_constraintTop_toBottomOf="@id/formatOptionsScrollView">
139139

140140
<TextView
141141
android:id="@+id/noNetwork"

app/src/main/res/values/styles.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,7 @@
244244
<style name="EditorFormatButton" parent="Widget.Material3.Button.IconButton">
245245
<item name="android:minWidth">48dp</item>
246246
<item name="backgroundTint">@color/editor_button_background</item>
247-
<item name="iconTint">@color/iconColor</item>
247+
<item name="iconTint">@color/icon_button_disabled</item>
248248
</style>
249249

250250
<style name="QuickBottomActionBarButton" parent="Widget.Material3.Button.TextButton">

0 commit comments

Comments
 (0)