Merge branch 'feat/2604_option-to-toggle-webview-light-or-dark-mode-in-action-sheet' into 'develop'

Add an action for switching between light and dark mode in the message details web view

See merge request android/mail/proton-mail-android!954
This commit is contained in:
Stefanija Boshkovska 2022-02-14 12:36:03 +00:00
commit 01b143f6ad
33 changed files with 1033 additions and 105 deletions

View File

@ -28,7 +28,6 @@ import android.content.Intent;
import android.content.SharedPreferences;
import android.content.pm.PackageManager;
import android.content.res.ColorStateList;
import android.content.res.Configuration;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.PorterDuff;
@ -261,9 +260,6 @@ public class ComposeMessageActivity
@Inject
RenderDimensionsProvider renderDimensionsProvider;
@Inject
SetUpWebViewDarkModeHandlingIfSupported setUpWebViewDarkModeHandlingIfSupported;
String composerInstanceId;
Menu menu;
@ -405,7 +401,12 @@ public class ComposeMessageActivity
webSettings.setBuiltInZoomControls(true);
webSettings.setPluginState(WebSettings.PluginState.OFF);
setUpWebViewDarkModeHandlingIfSupported.invoke(this, quotedMessageWebView);
composeMessageViewModel.setUpWebViewDarkMode(
this,
mUserManager.requireCurrentUserId(),
quotedMessageWebView,
composeMessageViewModel.getDraftId()
);
binding.composerQuotedMessageContainer.addView(quotedMessageWebView);
}
@ -1617,7 +1618,7 @@ public class ComposeMessageActivity
if (!respondInline) {
String css = AppUtil.readTxt(this, R.raw.css_reset_with_custom_props);
String darkCss = "";
if ((getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) {
if (composeMessageViewModel.isAppInDarkMode(this)) {
darkCss = AppUtil.readTxt(this, R.raw.css_reset_dark_mode_only);
}
Transformer viewportTransformer = new ViewportTransformer(renderDimensionsProvider.getRenderWidth(this), css, darkCss);
@ -1661,7 +1662,7 @@ public class ComposeMessageActivity
String content = composeMessageViewModel.getMessageDataResult().getContent();
String css = AppUtil.readTxt(this, R.raw.css_reset_with_custom_props);
String darkCss = "";
if ((getResources().getConfiguration().uiMode & Configuration.UI_MODE_NIGHT_MASK) == Configuration.UI_MODE_NIGHT_YES) {
if (composeMessageViewModel.isAppInDarkMode(this)) {
darkCss = AppUtil.readTxt(this, R.raw.css_reset_dark_mode_only);
}
Transformer contentTransformer = new ViewportTransformer(renderDimensionsProvider.getRenderWidth(this), css, darkCss);

View File

@ -69,6 +69,7 @@ import ch.protonmail.android.views.messageDetails.MessageDetailsHeaderView
import ch.protonmail.android.views.messageDetails.ReplyActionsView
import kotlinx.android.synthetic.main.layout_message_details.view.*
import kotlinx.android.synthetic.main.layout_message_details_body.view.*
import kotlinx.coroutines.runBlocking
import org.apache.http.protocol.HTTP
import timber.log.Timber
@ -322,6 +323,10 @@ internal class MessageDetailsAdapter(
val messageBodyProgress = itemView.messageWebViewContainer
.findViewById<ProgressBar>(R.id.item_message_body_progress_view_id) ?: return
message.messageId?.let {
setUpWebViewDarkModeBlocking(webView, it)
}
messageBodyProgress.isVisible = listItem.messageFormattedHtml.isNullOrEmpty()
displayRemoteContentButton.isVisible = false
loadEmbeddedImagesButton.isVisible = listItem.showLoadEmbeddedImagesButton
@ -427,8 +432,6 @@ internal class MessageDetailsAdapter(
openInProtonCalenderView: View,
editDraftButton: Button
) {
val item = visibleItems[position]
loadEmbeddedImagesContainer.setOnClickListener { view ->
view.visibility = View.GONE
// Once images were loaded for one message, we automatically load them for all the others, so:
@ -458,6 +461,7 @@ internal class MessageDetailsAdapter(
}
openInProtonCalenderView.setOnClickListener {
val item = visibleItems[position]
onOpenInProtonCalendarClicked(item.message)
}
@ -561,6 +565,18 @@ internal class MessageDetailsAdapter(
expandLastNonDraftItem()
}
fun reloadMessage(messageId: String) {
val item = visibleItems.find { it.message.messageId == messageId && it.itemType == TYPE_ITEM }
val itemIndex = visibleItems.indexOf(item)
// Set message formatted html to null, in order to trigger loading
// and switch to light/dark mode in the web view
val newItem = item?.apply { messageFormattedHtml = null }
newItem?.let {
visibleItems[itemIndex] = newItem
notifyItemChanged(itemIndex)
}
}
private fun expandLastNonDraftItem() {
val lastNonDraftHeaderIndex = visibleItems.indexOfLast {
!it.message.isDraft() && it.itemType == TYPE_HEADER
@ -613,8 +629,6 @@ internal class MessageDetailsAdapter(
}
private fun configureWebView(webView: WebView, pmWebViewClient: PMWebViewClient) {
setUpWebViewDarkModeHandlingIfSupported(context, webView)
webView.isScrollbarFadingEnabled = false
webView.isVerticalScrollBarEnabled = false
webView.isHorizontalScrollBarEnabled = false
@ -742,4 +756,7 @@ internal class MessageDetailsAdapter(
private fun isAndroidAPILevelLowerThan26() = Build.VERSION.SDK_INT < Build.VERSION_CODES.O
private fun setUpWebViewDarkModeBlocking(webView: WebView, messageId: String) = runBlocking {
setUpWebViewDarkModeHandlingIfSupported(context, userManager.requireCurrentUserId(), webView, messageId)
}
}

View File

@ -76,6 +76,7 @@ import ch.protonmail.android.mailbox.domain.usecase.MoveMessagesToFolder
import ch.protonmail.android.mailbox.presentation.ConversationModeEnabled
import ch.protonmail.android.repository.MessageRepository
import ch.protonmail.android.ui.model.LabelChipUiModel
import ch.protonmail.android.usecase.IsAppInDarkMode
import ch.protonmail.android.usecase.VerifyConnection
import ch.protonmail.android.usecase.delete.DeleteMessage
import ch.protonmail.android.usecase.fetch.FetchVerificationKeys
@ -89,6 +90,7 @@ import ch.protonmail.android.utils.HTMLTransformer.DefaultTransformer
import ch.protonmail.android.utils.HTMLTransformer.ViewportTransformer
import ch.protonmail.android.utils.UiUtil
import ch.protonmail.android.utils.crypto.KeyInformation
import ch.protonmail.android.details.domain.usecase.GetViewInDarkModeMessagePreference
import ch.protonmail.android.viewmodel.ConnectivityBaseViewModel
import com.birbit.android.jobqueue.JobManager
import dagger.hilt.android.lifecycle.HiltViewModel
@ -128,6 +130,8 @@ import javax.inject.Inject
@Suppress("LongParameterList") // Every new parameter adds a new issue and breaks the build
@HiltViewModel
internal class MessageDetailsViewModel @Inject constructor(
private val isAppInDarkMode: IsAppInDarkMode,
private val getViewInDarkModeMessagePreference: GetViewInDarkModeMessagePreference,
private val messageDetailsRepository: MessageDetailsRepository,
private val messageRepository: MessageRepository,
private val userManager: UserManager,
@ -191,6 +195,7 @@ internal class MessageDetailsViewModel @Inject constructor(
private val _messageDetailsError: MutableLiveData<Event<String>> = MutableLiveData()
private val _showPermissionMissingDialog: MutableLiveData<Unit> = MutableLiveData()
private val _conversationUiFlow = MutableSharedFlow<ConversationUiModel>(replay = 1)
private val _reloadMessageFlow = MutableSharedFlow<String>(replay = 1)
val conversationUiModel: SharedFlow<ConversationUiModel>
get() = _conversationUiFlow
@ -213,6 +218,9 @@ internal class MessageDetailsViewModel @Inject constructor(
val messageRenderedWithImages: LiveData<Message>
get() = _messageRenderedWithImages
val reloadMessageFlow: SharedFlow<String>
get() = _reloadMessageFlow
private var areImagesDisplayed: Boolean = false
private var visibleToTheUser = true
@ -898,4 +906,14 @@ internal class MessageDetailsViewModel @Inject constructor(
)
}
}
fun isAppInDarkMode(context: Context) = isAppInDarkMode.invoke(context)
fun isWebViewInDarkModeBlocking(context: Context, messageId: String) = runBlocking {
getViewInDarkModeMessagePreference(context, userManager.requireCurrentUserId(), messageId)
}
fun reloadMessage(messageId: String) {
_reloadMessageFlow.tryEmit(messageId)
}
}

View File

@ -26,6 +26,7 @@ import ch.protonmail.android.data.local.ContactDatabase
import ch.protonmail.android.data.local.CounterDatabase
import ch.protonmail.android.data.local.MessageDao
import ch.protonmail.android.data.local.MessageDatabase
import ch.protonmail.android.data.local.MessagePreferenceDao
import ch.protonmail.android.data.local.NotificationDao
import ch.protonmail.android.data.local.NotificationDatabase
import ch.protonmail.android.data.local.PendingActionDao
@ -79,4 +80,6 @@ class DatabaseProvider @Inject constructor(
fun providePendingActionDao(userId: UserId): PendingActionDao =
PendingActionDatabase.getInstance(context, userId).getDao()
fun provideMessagePreferenceDao(userId: UserId): MessagePreferenceDao =
MessageDatabase.getInstance(context, userId).getMessagePreferenceDao()
}

View File

@ -19,9 +19,11 @@
package ch.protonmail.android.compose
import android.annotation.SuppressLint
import android.content.Context
import android.graphics.Color
import android.text.Spanned
import android.text.TextUtils
import android.webkit.WebView
import androidx.core.net.MailTo
import androidx.lifecycle.LiveData
import androidx.lifecycle.MediatorLiveData
@ -57,6 +59,7 @@ import ch.protonmail.android.events.Status
import ch.protonmail.android.feature.account.allLoggedInBlocking
import ch.protonmail.android.jobs.contacts.GetSendPreferenceJob
import ch.protonmail.android.ui.view.DaysHoursPair
import ch.protonmail.android.usecase.IsAppInDarkMode
import ch.protonmail.android.usecase.VerifyConnection
import ch.protonmail.android.usecase.compose.SaveDraft
import ch.protonmail.android.usecase.compose.SaveDraftResult
@ -69,6 +72,7 @@ import ch.protonmail.android.utils.MailToData
import ch.protonmail.android.utils.MessageUtils
import ch.protonmail.android.utils.UiUtil
import ch.protonmail.android.utils.resources.StringResourceResolver
import ch.protonmail.android.utils.webview.SetUpWebViewDarkModeHandlingIfSupported
import ch.protonmail.android.viewmodel.ConnectivityBaseViewModel
import com.squareup.otto.Subscribe
import dagger.hilt.android.lifecycle.HiltViewModel
@ -107,6 +111,7 @@ const val GREATER_THAN = "&gt;"
@HiltViewModel
class ComposeMessageViewModel @Inject constructor(
private val isAppInDarkMode: IsAppInDarkMode,
private val composeMessageRepository: ComposeMessageRepository,
private val userManager: UserManager,
accountManager: AccountManager,
@ -120,7 +125,8 @@ class ComposeMessageViewModel @Inject constructor(
verifyConnection: VerifyConnection,
networkConfigurator: NetworkConfigurator,
private val htmlToSpanned: HtmlToSpanned,
private val addExpirationTimeToMessage: AddExpirationTimeToMessage
private val addExpirationTimeToMessage: AddExpirationTimeToMessage,
private val setUpWebViewDarkModeHandlingIfSupported: SetUpWebViewDarkModeHandlingIfSupported
) : ConnectivityBaseViewModel(verifyConnection, networkConfigurator) {
// region events data
@ -1312,4 +1318,17 @@ class ComposeMessageViewModel @Inject constructor(
Timber.v("Parsed mailto: $dataString to $mailToData")
return mailToData
}
fun setUpWebViewDarkMode(context: Context, userId: UserId, webView: WebView, draftId: String) {
viewModelScope.launch {
setUpWebViewDarkModeHandlingIfSupported(
context,
userId,
webView,
draftId
)
}
}
fun isAppInDarkMode(context: Context) = isAppInDarkMode.invoke(context)
}

View File

@ -25,6 +25,7 @@ import ch.protonmail.android.data.ProtonMailConverters
import ch.protonmail.android.data.local.model.Attachment
import ch.protonmail.android.data.local.model.AttachmentTypesConverter
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.data.local.model.MessagePreferenceEntity
import ch.protonmail.android.data.local.model.MessagesTypesConverter
import ch.protonmail.android.mailbox.data.local.ConversationDao
import ch.protonmail.android.mailbox.data.local.ConversationTypesConverter
@ -38,9 +39,10 @@ import me.proton.core.data.room.db.CommonConverters
Attachment::class,
ConversationDatabaseModel::class,
Message::class,
UnreadCounterEntity::class,
MessagePreferenceEntity::class,
UnreadCounterEntity::class
],
version = 14
version = 15
)
@TypeConverters(
value = [
@ -57,8 +59,9 @@ internal abstract class MessageDatabase : RoomDatabase() {
fun getDao(): MessageDao =
getMessageDao()
abstract fun getMessageDao(): MessageDao
abstract fun getConversationDao(): ConversationDao
abstract fun getMessageDao(): MessageDao
abstract fun getMessagePreferenceDao(): MessagePreferenceDao
abstract fun getUnreadCounterDao(): UnreadCounterDao
companion object Factory : DatabaseFactory<MessageDatabase>(

View File

@ -0,0 +1,45 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.data.local
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import ch.protonmail.android.data.local.model.MessagePreferenceEntity
import ch.protonmail.android.data.local.model.MessagePreferenceEntity.Companion.COLUMN_MESSAGE_ID
import ch.protonmail.android.data.local.model.MessagePreferenceEntity.Companion.TABLE_MESSAGE_PREFERENCE
import me.proton.core.data.room.db.BaseDao
@Dao
abstract class MessagePreferenceDao : BaseDao<MessagePreferenceEntity>() {
@Insert(onConflict = OnConflictStrategy.REPLACE)
abstract suspend fun saveMessagePreference(messagePreferenceEntity: MessagePreferenceEntity): Long
@Query(
"""
SELECT *
FROM $TABLE_MESSAGE_PREFERENCE
WHERE $COLUMN_MESSAGE_ID = :messageId
"""
)
abstract suspend fun findMessagePreference(messageId: String): MessagePreferenceEntity?
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.data.local.model
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import ch.protonmail.android.data.local.model.MessagePreferenceEntity.Companion.TABLE_MESSAGE_PREFERENCE
@Entity(
tableName = TABLE_MESSAGE_PREFERENCE
)
data class MessagePreferenceEntity(
@PrimaryKey(autoGenerate = true)
@ColumnInfo(name = COLUMN_ID)
val id: Long = 0,
@ColumnInfo(name = COLUMN_MESSAGE_ID)
val messageId: String,
@ColumnInfo(name = COLUMN_VIEW_IN_DARK_MODE)
val viewInDarkMode: Boolean,
) {
companion object {
const val TABLE_MESSAGE_PREFERENCE = "message_preference"
const val COLUMN_ID = "ID"
const val COLUMN_MESSAGE_ID = "message_id"
const val COLUMN_VIEW_IN_DARK_MODE = "view_in_dark_mode"
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (c) 2022 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.details.domain.usecase
import android.content.Context
import androidx.webkit.WebViewFeature
import ch.protonmail.android.repository.MessageRepository
import ch.protonmail.android.usecase.IsAppInDarkMode
import me.proton.core.domain.entity.UserId
import javax.inject.Inject
/**
* A use case that checks whether a given message should be viewed in dark mode or not
*/
class GetViewInDarkModeMessagePreference @Inject constructor(
private val messageRepository: MessageRepository,
private val isAppInDarkMode: IsAppInDarkMode
) {
suspend operator fun invoke(
context: Context,
userId: UserId,
messageId: String
): Boolean {
return if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
messageRepository.getViewInDarkModeMessagePreference(userId, messageId)
?: isAppInDarkMode(context)
} else false
}
}

View File

@ -0,0 +1,40 @@
/*
* Copyright (c) 2022 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.details.domain.usecase
import ch.protonmail.android.repository.MessageRepository
import me.proton.core.domain.entity.UserId
import javax.inject.Inject
/**
* A use case that saves the view in dark mode message preference
*/
class SetViewInDarkModeMessagePreference @Inject constructor(
private val messageRepository: MessageRepository,
) {
suspend operator fun invoke(
userId: UserId,
messageId: String,
viewInDarkMode: Boolean
) {
messageRepository.saveViewInDarkModeMessagePreference(userId, messageId, viewInDarkMode)
}
}

View File

@ -66,7 +66,7 @@ class MessageBodyLoader @Inject constructor(
fetchedMessage,
renderDimensionsProvider.getRenderWidth(fragmentActivity),
messageBodyCssProvider.getMessageBodyCss(),
messageBodyCssProvider.getMessageBodyDarkModeCss(),
messageBodyCssProvider.getMessageBodyDarkModeCss(userManager.requireCurrentUserId(), fetchedMessage.messageId!!),
getStringResource(R.string.request_timeout)
)
fetchedMessage.decryptedHTML = messageBody

View File

@ -25,7 +25,6 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.res.Configuration
import android.net.Uri
import android.os.Bundle
import android.view.ContextMenu
@ -85,6 +84,7 @@ import kotlinx.android.synthetic.main.activity_message_details.*
import kotlinx.android.synthetic.main.layout_message_details_activity_toolbar.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.onEach
import me.proton.core.domain.entity.UserId
import me.proton.core.util.kotlin.EMPTY_STRING
import timber.log.Timber
@ -196,6 +196,12 @@ internal class MessageDetailsActivity : BaseStoragePermissionActivity() {
viewModel.handleStarUnStar(messageOrConversationId, isChecked)
}
viewModel.reloadMessageFlow
.onEach { messageId ->
messageExpandableAdapter.reloadMessage(messageId)
}
.launchIn(lifecycleScope)
lifecycle.addObserver(viewModel)
}
@ -251,13 +257,13 @@ internal class MessageDetailsActivity : BaseStoragePermissionActivity() {
val showDecryptionError = messageBodyState is MessageBodyState.Error.DecryptionError
val loadedMessage = messageBodyState.message
val messageId = loadedMessage.messageId ?: return@mapLatest
val parsedBody = viewModel.formatMessageHtmlBody(
loadedMessage,
renderDimensionsProvider.getRenderWidth(this),
AppUtil.readTxt(this, R.raw.css_reset_with_custom_props),
if (
resources.configuration.uiMode and
Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
if (viewModel.isAppInDarkMode(this) &&
viewModel.isWebViewInDarkModeBlocking(this, messageId)
) {
AppUtil.readTxt(this, R.raw.css_reset_dark_mode_only)
} else {
@ -266,7 +272,6 @@ internal class MessageDetailsActivity : BaseStoragePermissionActivity() {
this.getString(R.string.request_timeout)
)
val messageId = loadedMessage.messageId ?: return@mapLatest
val showLoadEmbeddedImagesButton = handleEmbeddedImagesLoading(loadedMessage)
messageExpandableAdapter.showMessageDetails(
parsedBody,

View File

@ -19,8 +19,6 @@
package ch.protonmail.android.navigation.presentation
import android.content.Intent
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
import android.content.res.Configuration.UI_MODE_NIGHT_YES
import android.os.Build
import android.os.Bundle
import android.view.View
@ -284,7 +282,7 @@ internal abstract class NavigationActivity : BaseActivity() {
override fun onResume() {
super.onResume()
if (SHOULD_DRAW_DRAWER_BEHIND_SYSTEM_BARS)
if (resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES) {
if (navigationViewModel.isAppInDarkMode(this)) {
setDarkStatusBar()
} else {
setLightStatusBar()
@ -360,7 +358,7 @@ internal abstract class NavigationActivity : BaseActivity() {
override fun onDrawerClosed(drawerView: View) {
super.onDrawerClosed(drawerView)
if (SHOULD_DRAW_DRAWER_BEHIND_SYSTEM_BARS)
if (resources.configuration.uiMode and UI_MODE_NIGHT_MASK == UI_MODE_NIGHT_YES) {
if (navigationViewModel.isAppInDarkMode(this@NavigationActivity)) {
setDarkStatusBar()
} else {
setLightStatusBar()

View File

@ -19,11 +19,13 @@
package ch.protonmail.android.navigation.presentation
import android.content.Context
import androidx.lifecycle.ViewModel
import ch.protonmail.android.R
import ch.protonmail.android.core.Constants
import ch.protonmail.android.feature.account.AccountStateManager
import ch.protonmail.android.prefs.SecureSharedPreferences
import ch.protonmail.android.usecase.IsAppInDarkMode
import ch.protonmail.android.utils.notifier.UserNotifier
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.withContext
@ -34,6 +36,7 @@ import javax.inject.Inject
@HiltViewModel
internal class NavigationViewModel @Inject constructor(
private val isAppInDarkMode: IsAppInDarkMode,
private val secureSharedPreferencesFactory: SecureSharedPreferences.Factory,
private val accountStateManager: AccountStateManager,
private val userNotifier: UserNotifier,
@ -51,4 +54,6 @@ internal class NavigationViewModel @Inject constructor(
true
}
}
fun isAppInDarkMode(context: Context) = isAppInDarkMode.invoke(context)
}

View File

@ -31,6 +31,7 @@ import ch.protonmail.android.data.ProtonStore
import ch.protonmail.android.data.local.CounterDao
import ch.protonmail.android.data.local.MessageDao
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.data.local.model.MessagePreferenceEntity
import ch.protonmail.android.domain.LoadMoreFlow
import ch.protonmail.android.jobs.PostReadJob
import ch.protonmail.android.jobs.PostStarJob
@ -387,6 +388,19 @@ class MessageRepository @Inject constructor(
messageDao.deleteMessagesByIds(messageIds)
}
suspend fun saveViewInDarkModeMessagePreference(userId: UserId, messageId: String, viewInDarkMode: Boolean) {
val messagePreferenceDao = databaseProvider.provideMessagePreferenceDao(userId)
val messagePreference = messagePreferenceDao.findMessagePreference(messageId)
?.copy(viewInDarkMode = viewInDarkMode)
?: MessagePreferenceEntity(messageId = messageId, viewInDarkMode = viewInDarkMode)
messagePreferenceDao.saveMessagePreference(messagePreference)
}
suspend fun getViewInDarkModeMessagePreference(userId: UserId, messageId: String): Boolean? {
val messagePreferenceDao = databaseProvider.provideMessagePreferenceDao(userId)
return messagePreferenceDao.findMessagePreference(messageId)?.viewInDarkMode
}
private suspend fun Message.saveBodyToFileIfNeeded() {
withContext(dispatcherProvider.Io) {
messageBody = messageBody?.let {

View File

@ -35,6 +35,7 @@ import androidx.lifecycle.lifecycleScope
import ch.protonmail.android.R
import ch.protonmail.android.activities.messageDetails.EXTRA_VIEW_HEADERS
import ch.protonmail.android.activities.messageDetails.MessageViewHeadersActivity
import ch.protonmail.android.activities.messageDetails.viewmodel.MessageDetailsViewModel
import ch.protonmail.android.core.Constants
import ch.protonmail.android.databinding.FragmentMessageActionSheetBinding
import ch.protonmail.android.databinding.LayoutMessageDetailsActionsSheetButtonsBinding
@ -63,6 +64,7 @@ class MessageActionSheet : BottomSheetDialogFragment() {
private var actionSheetHeader: ActionSheetHeader? = null
private val viewModel: MessageActionSheetViewModel by viewModels()
private val mailboxViewModel: MailboxViewModel by activityViewModels()
private val messageDetailsViewModel: MessageDetailsViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
@ -89,13 +91,18 @@ class MessageActionSheet : BottomSheetDialogFragment() {
.onEach { updateViewState(it, binding) }
.launchIn(lifecycleScope)
viewModel.setupViewState(messageIds, messageLocation, mailboxLabelId, actionsTarget)
viewModel.setupViewState(
requireContext(),
messageIds,
messageLocation,
mailboxLabelId,
actionsTarget
)
setupHeaderBindings(binding.actionSheetHeaderDetailsActions, arguments)
setupReplyActionsBindings(
binding.includeLayoutActionSheetButtons, actionsTarget, messageIds, doesConversationHaveMoreThanOneMessage
)
setupManageSectionBindings(binding, viewModel, actionsTarget, messageIds, messageLocation, mailboxLabelId)
setupMoreSectionBindings(binding, actionsTarget, messageIds)
actionSheetHeader = binding.actionSheetHeaderDetailsActions
@ -157,6 +164,7 @@ class MessageActionSheet : BottomSheetDialogFragment() {
) {
when (state) {
is MessageActionSheetState.Data -> {
setupManageSectionBindings(binding, state.manageSectionState)
setupMoveSectionState(binding, state.moveSectionState)
}
MessageActionSheetState.Initial -> {
@ -219,66 +227,66 @@ class MessageActionSheet : BottomSheetDialogFragment() {
private fun setupManageSectionBindings(
binding: FragmentMessageActionSheetBinding,
viewModel: MessageActionSheetViewModel,
actionsTarget: ActionSheetTarget,
messageIds: List<String>,
messageLocation: Constants.MessageLocationType,
mailboxLabelId: String
state: MessageActionSheetState.ManageSectionState
) {
with(binding) {
val isStarred = arguments?.getBoolean(EXTRA_ARG_IS_STARED) ?: false
textViewDetailsActionsUnstar.apply {
isVisible = actionsTarget in arrayOf(
ActionSheetTarget.MAILBOX_ITEMS_IN_MAILBOX_SCREEN,
ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
) || isStarred
isVisible = state.showUnstarAction
setOnClickListener {
viewModel.unStarMessage(
messageIds,
messageLocation,
actionsTarget == ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
state.mailboxItemIds,
state.messageLocation,
state.actionsTarget == ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
)
}
}
textViewDetailsActionsStar.apply {
isVisible = actionsTarget in arrayOf(
ActionSheetTarget.MAILBOX_ITEMS_IN_MAILBOX_SCREEN,
ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
) || !isStarred
isVisible = state.showStarAction
setOnClickListener {
viewModel.starMessage(
messageIds,
messageLocation,
actionsTarget == ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
state.mailboxItemIds,
state.messageLocation,
state.actionsTarget == ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
)
}
}
textViewDetailsActionsMarkRead.apply {
isVisible = actionsTarget == ActionSheetTarget.MAILBOX_ITEMS_IN_MAILBOX_SCREEN
isVisible = state.showMarkReadAction
setOnClickListener {
viewModel.markRead(
messageIds,
messageLocation,
mailboxLabelId,
actionsTarget == ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
state.mailboxItemIds,
state.messageLocation,
state.currentLocationId,
state.actionsTarget == ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
)
}
}
textViewDetailsActionsMarkUnread.setOnClickListener {
viewModel.markUnread(
messageIds,
messageLocation,
mailboxLabelId,
actionsTarget == ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
state.mailboxItemIds,
state.messageLocation,
state.currentLocationId,
state.actionsTarget == ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
)
}
textViewDetailsActionsLabelAs.setOnClickListener {
viewModel.showLabelsManager(messageIds, messageLocation, mailboxLabelId)
viewModel.showLabelsManager(state.mailboxItemIds, state.messageLocation, state.currentLocationId)
dismiss()
}
detailsActionsViewInLightModeTextView.apply {
isVisible = state.showViewInLightModeAction
setOnClickListener {
viewModel.viewInLightMode(state.mailboxItemIds.first())
}
}
detailsActionsViewInDarkModeTextView.apply {
isVisible = state.showViewInDarkModeAction
setOnClickListener {
viewModel.viewInDarkMode(state.mailboxItemIds.first())
}
}
}
}
@ -415,6 +423,10 @@ class MessageActionSheet : BottomSheetDialogFragment() {
}
is MessageActionSheetAction.CouldNotCompleteActionError ->
showCouldNotCompleteActionError()
is MessageActionSheetAction.ViewMessageInLightDarkMode -> {
messageDetailsViewModel.reloadMessage(sheetAction.messageId)
handleDismissBehavior(false)
}
else -> Timber.v("unhandled action $sheetAction")
}
}
@ -473,7 +485,7 @@ class MessageActionSheet : BottomSheetDialogFragment() {
private const val EXTRA_ARG_MAILBOX_LABEL_ID = "arg_mailbox_label_id"
private const val EXTRA_ARG_TITLE = "arg_message_details_actions_title"
private const val EXTRA_ARG_SUBTITLE = "arg_message_details_actions_sub_title"
private const val EXTRA_ARG_IS_STARED = "arg_extra_is_stared"
const val EXTRA_ARG_IS_STARRED = "arg_extra_is_stared"
private const val EXTRA_ARG_CONVERSATION_HAS_MORE_THAN_ONE_MESSAGE =
"arg_conversation_has_more_than_one_message"
private const val HEADER_SLIDE_THRESHOLD = 0.8f
@ -505,7 +517,7 @@ class MessageActionSheet : BottomSheetDialogFragment() {
EXTRA_ARG_MESSAGE_IDS to messagesIds.toTypedArray(),
EXTRA_ARG_TITLE to title,
EXTRA_ARG_SUBTITLE to subTitle,
EXTRA_ARG_IS_STARED to isStarred,
EXTRA_ARG_IS_STARRED to isStarred,
EXTRA_ARG_CURRENT_FOLDER_LOCATION_ID to currentFolderLocationId,
EXTRA_ARG_MAILBOX_LABEL_ID to mailboxLabelId,
EXTRA_ARG_ACTION_TARGET to actionSheetTarget,

View File

@ -49,5 +49,9 @@ sealed class MessageActionSheetAction {
val areMailboxItemsMovedFromLocation: Boolean
) : MessageActionSheetAction()
data class ViewMessageInLightDarkMode(
val messageId: String
) : MessageActionSheetAction()
object CouldNotCompleteActionError : MessageActionSheetAction()
}

View File

@ -26,9 +26,22 @@ sealed class MessageActionSheetState {
object Initial : MessageActionSheetState()
data class Data(
val manageSectionState: ManageSectionState,
val moveSectionState: MoveSectionState
) : MessageActionSheetState()
data class ManageSectionState(
val mailboxItemIds: List<String>,
val messageLocation: Constants.MessageLocationType,
val currentLocationId: String,
val actionsTarget: ActionSheetTarget,
val showStarAction: Boolean,
val showUnstarAction: Boolean,
val showMarkReadAction: Boolean,
val showViewInLightModeAction: Boolean,
val showViewInDarkModeAction: Boolean
)
data class MoveSectionState(
val mailboxItemIds: List<String>,
val messageLocation: Constants.MessageLocationType,

View File

@ -19,6 +19,7 @@
package ch.protonmail.android.ui.actionsheet
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
@ -33,9 +34,12 @@ import ch.protonmail.android.mailbox.domain.model.ConversationsActionResult
import ch.protonmail.android.mailbox.presentation.ConversationModeEnabled
import ch.protonmail.android.repository.MessageRepository
import ch.protonmail.android.ui.actionsheet.model.ActionSheetTarget
import ch.protonmail.android.usecase.IsAppInDarkMode
import ch.protonmail.android.usecase.delete.DeleteMessage
import ch.protonmail.android.usecase.message.ChangeMessagesReadStatus
import ch.protonmail.android.usecase.message.ChangeMessagesStarredStatus
import ch.protonmail.android.details.domain.usecase.GetViewInDarkModeMessagePreference
import ch.protonmail.android.details.domain.usecase.SetViewInDarkModeMessagePreference
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
@ -60,6 +64,9 @@ internal class MessageActionSheetViewModel @Inject constructor(
private val changeMessagesStarredStatus: ChangeMessagesStarredStatus,
private val changeConversationsStarredStatus: ChangeConversationsStarredStatus,
private val conversationModeEnabled: ConversationModeEnabled,
private val isAppInDarkMode: IsAppInDarkMode,
private val getViewInDarkModeMessagePreference: GetViewInDarkModeMessagePreference,
private val setViewInDarkModeMessagePreference: SetViewInDarkModeMessagePreference,
private val accountManager: AccountManager
) : ViewModel() {
@ -73,13 +80,28 @@ internal class MessageActionSheetViewModel @Inject constructor(
get() = actionsMutableFlow
fun setupViewState(
context: Context,
messageIds: List<String>,
messageLocation: Constants.MessageLocationType,
mailboxLocationId: String,
actionsTarget: ActionSheetTarget
) {
val moveSectionState = computeMoveSectionState(actionsTarget, messageLocation, mailboxLocationId, messageIds)
mutableStateFlow.value = MessageActionSheetState.Data(moveSectionState)
viewModelScope.launch {
val manageSectionState = computeManageSectionState(
context,
messageLocation,
mailboxLocationId,
messageIds,
actionsTarget
)
val moveSectionState = computeMoveSectionState(
actionsTarget,
messageLocation,
mailboxLocationId,
messageIds
)
mutableStateFlow.value = MessageActionSheetState.Data(manageSectionState, moveSectionState)
}
}
fun showLabelsManager(
@ -339,6 +361,30 @@ internal class MessageActionSheetViewModel @Inject constructor(
}
}
fun viewInLightMode(messageId: String) {
viewModelScope.launch {
accountManager.getPrimaryUserId().first()?.let {
setViewInDarkModeMessagePreference(it, messageId, viewInDarkMode = false)
}
}.invokeOnCompletion {
actionsMutableFlow.value = MessageActionSheetAction.ViewMessageInLightDarkMode(
messageId = messageId
)
}
}
fun viewInDarkMode(messageId: String) {
viewModelScope.launch {
accountManager.getPrimaryUserId().first()?.let {
setViewInDarkModeMessagePreference(it, messageId, viewInDarkMode = true)
}
}.invokeOnCompletion {
actionsMutableFlow.value = MessageActionSheetAction.ViewMessageInLightDarkMode(
messageId = messageId
)
}
}
private fun moveMessagesToFolderAndDismiss(
ids: List<String>,
newFolderLocationId: Constants.MessageLocationType,
@ -416,6 +462,59 @@ internal class MessageActionSheetViewModel @Inject constructor(
MessageActionSheet.EXTRA_ARG_ACTION_TARGET
) ?: ActionSheetTarget.MESSAGE_ITEM_IN_DETAIL_SCREEN
private fun isAppInDarkMode(context: Context) = isAppInDarkMode.invoke(context)
private suspend fun isWebViewInDarkMode(context: Context, messageId: String): Boolean {
accountManager.getPrimaryUserId().first()?.let {
return getViewInDarkModeMessagePreference(context, it, messageId)
}
return false
}
private suspend fun computeManageSectionState(
context: Context,
messageLocation: Constants.MessageLocationType,
mailboxLocationId: String,
messageIds: List<String>,
actionsTarget: ActionSheetTarget
): MessageActionSheetState.ManageSectionState {
val isStarred = savedStateHandle.get<Boolean>(MessageActionSheet.EXTRA_ARG_IS_STARRED) ?: false
val showStarAction = actionsTarget in arrayOf(
ActionSheetTarget.MAILBOX_ITEMS_IN_MAILBOX_SCREEN,
ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
) || !isStarred
val showUnstarAction = actionsTarget in arrayOf(
ActionSheetTarget.MAILBOX_ITEMS_IN_MAILBOX_SCREEN,
ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
) || isStarred
val showMarkReadAction = actionsTarget == ActionSheetTarget.MAILBOX_ITEMS_IN_MAILBOX_SCREEN
val showViewInLightModeAction = actionsTarget in arrayOf(
ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN,
ActionSheetTarget.MESSAGE_ITEM_IN_DETAIL_SCREEN
) && isAppInDarkMode(context) && isWebViewInDarkMode(context, messageIds.first())
val showViewInDarkModeAction = actionsTarget in arrayOf(
ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN,
ActionSheetTarget.MESSAGE_ITEM_IN_DETAIL_SCREEN
) && isAppInDarkMode(context) && !isWebViewInDarkMode(context, messageIds.first())
return MessageActionSheetState.ManageSectionState(
messageIds,
messageLocation,
mailboxLocationId,
actionsTarget,
showStarAction,
showUnstarAction,
showMarkReadAction,
showViewInLightModeAction,
showViewInDarkModeAction
)
}
private fun computeMoveSectionState(
actionsTarget: ActionSheetTarget,
messageLocation: Constants.MessageLocationType,

View File

@ -0,0 +1,33 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.usecase
import android.content.Context
import android.content.res.Configuration
import javax.inject.Inject
/**
* A use case that checks whether the app is in dark mode or not
*/
class IsAppInDarkMode @Inject constructor() {
operator fun invoke(context: Context): Boolean =
context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
}

View File

@ -20,26 +20,26 @@
package ch.protonmail.android.utils.css
import android.content.Context
import android.content.res.Configuration
import ch.protonmail.android.R
import ch.protonmail.android.usecase.IsAppInDarkMode
import ch.protonmail.android.utils.AppUtil
import ch.protonmail.android.details.domain.usecase.GetViewInDarkModeMessagePreference
import me.proton.core.domain.entity.UserId
import me.proton.core.util.kotlin.EMPTY_STRING
import javax.inject.Inject
class MessageBodyCssProvider @Inject constructor(
private val context: Context
private val context: Context,
private val isAppInDarkMode: IsAppInDarkMode,
private val getViewInDarkModeMessagePreference: GetViewInDarkModeMessagePreference
) {
fun getMessageBodyCss(): String {
return AppUtil.readTxt(context, R.raw.css_reset_with_custom_props)
}
fun getMessageBodyCss(): String = AppUtil.readTxt(context, R.raw.css_reset_with_custom_props)
fun getMessageBodyDarkModeCss(): String {
return if (context.resources.configuration.uiMode
and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
suspend fun getMessageBodyDarkModeCss(userId: UserId, messageId: String): String =
if (isAppInDarkMode(context) && getViewInDarkModeMessagePreference(context, userId, messageId)) {
AppUtil.readTxt(context, R.raw.css_reset_dark_mode_only)
} else {
EMPTY_STRING
}
}
}

View File

@ -24,15 +24,25 @@ import android.content.res.Configuration
import android.webkit.WebView
import androidx.webkit.WebSettingsCompat
import androidx.webkit.WebViewFeature
import ch.protonmail.android.details.domain.usecase.GetViewInDarkModeMessagePreference
import me.proton.core.domain.entity.UserId
import javax.inject.Inject
class SetUpWebViewDarkModeHandlingIfSupported @Inject constructor() {
class SetUpWebViewDarkModeHandlingIfSupported @Inject constructor(
private val getViewInDarkModeMessagePreference: GetViewInDarkModeMessagePreference
) {
operator fun invoke(context: Context, webView: WebView) {
suspend operator fun invoke(context: Context, userId: UserId, webView: WebView, messageId: String) {
if (WebViewFeature.isFeatureSupported(WebViewFeature.FORCE_DARK)) {
val viewInDarkModeMessagePreference = getViewInDarkModeMessagePreference(context, userId, messageId)
when (context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
Configuration.UI_MODE_NIGHT_YES -> {
WebSettingsCompat.setForceDark(webView.settings, WebSettingsCompat.FORCE_DARK_ON)
if (viewInDarkModeMessagePreference) {
WebSettingsCompat.setForceDark(webView.settings, WebSettingsCompat.FORCE_DARK_ON)
} else {
WebSettingsCompat.setForceDark(webView.settings, WebSettingsCompat.FORCE_DARK_OFF)
}
}
Configuration.UI_MODE_NIGHT_NO, Configuration.UI_MODE_NIGHT_UNDEFINED -> {
WebSettingsCompat.setForceDark(webView.settings, WebSettingsCompat.FORCE_DARK_OFF)

View File

@ -0,0 +1,29 @@
<!--
~ Copyright (c) 2020 Proton Technologies AG
~
~ This file is part of ProtonMail.
~
~ ProtonMail 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.
~
~ ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M11.1487,4C11.1454,4 11.142,4 11.1387,4C8.8955,4 7.0771,5.7909 7.0771,8C7.0771,10.2091 8.8955,12 11.1387,12C11.142,12 11.1454,12 11.1487,12C11.8158,11.9984 12.4452,11.8384 13,11.5562C12.5811,12.1159 12.0658,12.6014 11.4783,12.9889C10.51,13.6275 9.3454,14 8.0925,14C4.7277,14 2,11.3137 2,8C2,4.6863 4.7277,2 8.0925,2C9.3454,2 10.51,2.3725 11.4783,3.0111C12.0658,3.3986 12.5811,3.8841 13,4.4438C12.4452,4.1616 11.8158,4.0016 11.1487,4ZM9.6212,3.229C9.1389,3.0803 8.6255,3 8.0925,3C5.2655,3 3,5.2529 3,8C3,10.7471 5.2655,13 8.0925,13C8.6255,13 9.1389,12.9198 9.6212,12.771C7.574,12.1357 6.0771,10.2488 6.0771,8C6.0771,5.7512 7.574,3.8643 9.6212,3.229Z"
android:fillColor="@color/icon_norm"
android:fillType="evenOdd"/>
</vector>

View File

@ -0,0 +1,29 @@
<!--
~ Copyright (c) 2020 Proton Technologies AG
~
~ This file is part of ProtonMail.
~
~ ProtonMail 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.
~
~ ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M7.5,1H8.5V3H7.5V1ZM7.5,15V13H8.5V15H7.5ZM13.3033,3.4038L12.5962,2.6967L11.182,4.1109L11.8891,4.818L13.3033,3.4038ZM4.1109,11.182L4.818,11.8891L3.4038,13.3033L2.6967,12.5962L4.1109,11.182ZM12.5962,13.3033L13.3033,12.5962L11.8891,11.182L11.182,11.8891L12.5962,13.3033ZM4.818,4.1109L4.1109,4.818L2.6967,3.4038L3.4038,2.6967L4.818,4.1109ZM3,8.5V7.5H1V8.5H3ZM13,7.5H15V8.5H13V7.5ZM11,8C11,9.6568 9.6568,11 8,11C6.3432,11 5,9.6568 5,8C5,6.3432 6.3432,5 8,5C9.6568,5 11,6.3432 11,8ZM12,8C12,10.2091 10.2091,12 8,12C5.7909,12 4,10.2091 4,8C4,5.7909 5.7909,4 8,4C10.2091,4 12,5.7909 12,8Z"
android:fillColor="@color/icon_norm"
android:fillType="evenOdd"/>
</vector>

View File

@ -104,6 +104,26 @@
android:text="@string/label_as"
app:drawableStartCompat="@drawable/ic_label" />
<TextView
android:id="@+id/details_actions_view_in_light_mode_text_view"
style="@style/ListStandardItem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/action_sheet_view_in_light_mode"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_sun"
tools:visibility="visible" />
<TextView
android:id="@+id/details_actions_view_in_dark_mode_text_view"
style="@style/ListStandardItem"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/action_sheet_view_in_dark_mode"
android:visibility="gone"
app:drawableStartCompat="@drawable/ic_moon"
tools:visibility="visible" />
<View style="@style/ViewSeparatorHorizontal" />
<TextView

View File

@ -918,6 +918,8 @@
<string name="manage">Manage</string>
<string name="message_from">Message from %s</string>
<string name="not_spam_move_to_inbox">Not spam (Move to Inbox)</string>
<string name="action_sheet_view_in_light_mode">View in Light mode</string>
<string name="action_sheet_view_in_dark_mode">View in Dark mode</string>
<plurals name="x_messages_count">
<item quantity="one">%d message</item>
<item quantity="other">%d messages</item>

View File

@ -61,6 +61,7 @@ import ch.protonmail.android.mailbox.presentation.ConversationModeEnabled
import ch.protonmail.android.repository.MessageRepository
import ch.protonmail.android.testAndroid.lifecycle.testObserver
import ch.protonmail.android.ui.model.LabelChipUiModel
import ch.protonmail.android.usecase.IsAppInDarkMode
import ch.protonmail.android.usecase.VerifyConnection
import ch.protonmail.android.usecase.delete.DeleteMessage
import ch.protonmail.android.usecase.fetch.FetchVerificationKeys
@ -68,6 +69,7 @@ import ch.protonmail.android.usecase.message.ChangeMessagesReadStatus
import ch.protonmail.android.usecase.message.ChangeMessagesStarredStatus
import ch.protonmail.android.util.ProtonCalendarUtil
import ch.protonmail.android.utils.DownloadUtils
import ch.protonmail.android.details.domain.usecase.GetViewInDarkModeMessagePreference
import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
@ -111,6 +113,10 @@ private const val MESSAGE_SENDER_EMAIL_ADDRESS = "sender@protonmail.com"
class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
private val isAppInDarkMode: IsAppInDarkMode = mockk()
private val getViewInDarkModeMessagePreference: GetViewInDarkModeMessagePreference = mockk()
private val changeMessagesReadStatus: ChangeMessagesReadStatus = mockk()
private val changeConversationsReadStatus: ChangeConversationsReadStatus = mockk(relaxed = true)
@ -207,6 +213,8 @@ class MessageDetailsViewModelTest : ArchTest, CoroutinesTest {
mockkStatic(Color::class)
every { Color.parseColor(any()) } returns testColorInt
viewModel = MessageDetailsViewModel(
isAppInDarkMode = isAppInDarkMode,
getViewInDarkModeMessagePreference = getViewInDarkModeMessagePreference,
messageDetailsRepository = messageDetailsRepository,
messageRepository = messageRepository,
userManager = userManager,

View File

@ -19,7 +19,6 @@
package ch.protonmail.android.compose
import androidx.work.WorkManager
import ch.protonmail.android.R
import ch.protonmail.android.activities.messageDetails.repository.MessageDetailsRepository
import ch.protonmail.android.api.NetworkConfigurator
@ -31,6 +30,7 @@ import ch.protonmail.android.core.UserManager
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.testAndroid.lifecycle.testObserver
import ch.protonmail.android.testAndroid.rx.TrampolineScheduler
import ch.protonmail.android.usecase.IsAppInDarkMode
import ch.protonmail.android.usecase.VerifyConnection
import ch.protonmail.android.usecase.compose.SaveDraft
import ch.protonmail.android.usecase.compose.SaveDraftResult
@ -38,6 +38,7 @@ import ch.protonmail.android.usecase.delete.DeleteMessage
import ch.protonmail.android.usecase.fetch.FetchPublicKeys
import ch.protonmail.android.utils.UiUtil
import ch.protonmail.android.utils.resources.StringResourceResolver
import ch.protonmail.android.utils.webview.SetUpWebViewDarkModeHandlingIfSupported
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@ -62,6 +63,8 @@ class ComposeMessageViewModelTest : ArchTest, CoroutinesTest {
@get:Rule
val trampolineSchedulerRule = TrampolineScheduler()
private val isAppInDarkMode: IsAppInDarkMode = mockk()
private val stringResourceResolver: StringResourceResolver = mockk(relaxed = true)
private val composeMessageRepository: ComposeMessageRepository = mockk(relaxed = true)
@ -89,13 +92,14 @@ class ComposeMessageViewModelTest : ArchTest, CoroutinesTest {
private val verifyConnection: VerifyConnection = mockk()
private val workManager: WorkManager = mockk(relaxed = true)
private val htmlToSpanned: HtmlToSpanned = mockk(relaxed = true)
private val addExpirationTimeToMessage: AddExpirationTimeToMessage = mockk()
private val setUpWebViewDarkModeHandlingIfSupported: SetUpWebViewDarkModeHandlingIfSupported = mockk()
private val viewModel = ComposeMessageViewModel(
isAppInDarkMode = isAppInDarkMode,
composeMessageRepository = composeMessageRepository,
userManager = userManager,
accountManager = accountManager,
@ -109,7 +113,8 @@ class ComposeMessageViewModelTest : ArchTest, CoroutinesTest {
verifyConnection = verifyConnection,
networkConfigurator = networkConfigurator,
htmlToSpanned = htmlToSpanned,
addExpirationTimeToMessage = addExpirationTimeToMessage
addExpirationTimeToMessage = addExpirationTimeToMessage,
setUpWebViewDarkModeHandlingIfSupported = setUpWebViewDarkModeHandlingIfSupported
)
@BeforeTest

View File

@ -0,0 +1,58 @@
/*
* Copyright (c) 2020 Proton Technologies AG
*
* This file is part of ProtonMail.
*
* ProtonMail 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.
*
* ProtonMail 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 ProtonMail. If not, see https://www.gnu.org/licenses/.
*/
package ch.protonmail.android.details.domain.usecase
import ch.protonmail.android.repository.MessageRepository
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import kotlinx.coroutines.test.runBlockingTest
import me.proton.core.domain.entity.UserId
import kotlin.test.Test
/**
* Tests the behaviour of [SetViewInDarkModeMessagePreference]
*/
class SetViewInDarkModeMessagePreferenceTest {
private val messageRepository: MessageRepository = mockk {
coEvery { saveViewInDarkModeMessagePreference(any(), any(), any()) } just runs
}
private val setViewInDarkModeMessagePreference = SetViewInDarkModeMessagePreference(
messageRepository
)
private val testUserId = UserId("testUserId")
private val testMessageId = "messageId"
@Test
fun `should call repository method for saving the view in dark mode message preference`() = runBlockingTest {
// when
setViewInDarkModeMessagePreference(testUserId, testMessageId, viewInDarkMode = true)
// then
coVerify {
messageRepository.saveViewInDarkModeMessagePreference(testUserId, testMessageId, viewInDarkMode = true)
}
}
}

View File

@ -24,8 +24,6 @@ import ch.protonmail.android.R
import ch.protonmail.android.core.UserManager
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.details.domain.MessageBodyDecryptor
import ch.protonmail.android.details.domain.MessageBodyParser
import ch.protonmail.android.details.domain.model.MessageBodyParts
import ch.protonmail.android.details.presentation.mapper.MessageToMessageDetailsListItemMapper
import ch.protonmail.android.repository.MessageRepository
import ch.protonmail.android.testdata.KeyInformationTestData
@ -57,7 +55,7 @@ class MessageBodyLoaderTest {
}
private val messageBodyCssProviderMock = mockk<MessageBodyCssProvider> {
every { getMessageBodyCss() } returns TestData.MESSAGE_BODY_CSS
every { getMessageBodyDarkModeCss() } returns TestData.MESSAGE_BODY_DARK_MODE_CSS
coEvery { getMessageBodyDarkModeCss(any(), any()) } returns TestData.MESSAGE_BODY_DARK_MODE_CSS
}
private val getStringResourceMock = mockk<StringResourceResolver> {
every { this@mockk.invoke(R.string.request_timeout) } returns TestData.DEFAULT_ERROR_MESSAGE

View File

@ -25,6 +25,7 @@ import ch.protonmail.android.core.Constants
import ch.protonmail.android.feature.account.AccountStateManager
import ch.protonmail.android.prefs.SecureSharedPreferences
import ch.protonmail.android.testdata.UserIdTestData
import ch.protonmail.android.usecase.IsAppInDarkMode
import ch.protonmail.android.utils.notifier.UserNotifier
import io.mockk.called
import io.mockk.every
@ -41,6 +42,8 @@ import kotlin.test.assertTrue
class NavigationViewModelTest : ArchTest, CoroutinesTest {
private val isAppInDarkMode: IsAppInDarkMode = mockk()
private val sharedPrefsMock = mockk<SharedPreferences>()
private val sharedPreferencesFactoryMock = mockk<SecureSharedPreferences.Factory> {
every { userPreferences(UserIdTestData.userId) } returns sharedPrefsMock
@ -52,6 +55,7 @@ class NavigationViewModelTest : ArchTest, CoroutinesTest {
every { showError(R.string.logged_out_description) } just runs
}
private val navigationViewModel = NavigationViewModel(
isAppInDarkMode,
sharedPreferencesFactoryMock,
accountStateManagerMock,
userNotifierMock,

View File

@ -30,7 +30,9 @@ import ch.protonmail.android.core.NetworkConnectivityManager
import ch.protonmail.android.core.UserManager
import ch.protonmail.android.data.local.CounterDao
import ch.protonmail.android.data.local.MessageDao
import ch.protonmail.android.data.local.MessagePreferenceDao
import ch.protonmail.android.data.local.model.Message
import ch.protonmail.android.data.local.model.MessagePreferenceEntity
import ch.protonmail.android.domain.entity.user.User
import ch.protonmail.android.labels.domain.LabelRepository
import ch.protonmail.android.labels.domain.model.Label
@ -93,10 +95,15 @@ class MessageRepositoryTest {
coEvery { insertUnreadLocation(any()) } just runs
}
private val messagePreferenceDao: MessagePreferenceDao = mockk {
coEvery { saveMessagePreference(any()) } returns 123
}
private val databaseProvider: DatabaseProvider = mockk {
every { provideMessageDao(any()) } returns messageDao
every { provideUnreadCounterDao(any()) } returns unreadCounterDao
every { provideCounterDao(any()) } returns counterDao
every { provideMessageDao(any()) } returns messageDao
every { provideMessagePreferenceDao(any()) } returns messagePreferenceDao
every { provideUnreadCounterDao(any()) } returns unreadCounterDao
}
private val messageBodyFileManager: MessageBodyFileManager = mockk()
@ -832,6 +839,37 @@ class MessageRepositoryTest {
assertEquals(expectedLabelIds, savedMessageCaptor.captured.allLabelIDs)
}
@Test
fun `should save message preference if it does not exist in DB when saving view in dark mode preference`() = runBlockingTest {
// given
val expectedResult = buildMessagePreference(viewInDarkMode = true)
coEvery { messagePreferenceDao.findMessagePreference(messageId) } returns null
// when
messageRepository.saveViewInDarkModeMessagePreference(testUserId, messageId, viewInDarkMode = true)
// then
val result = slot<MessagePreferenceEntity>()
coVerify { messagePreferenceDao.saveMessagePreference(capture(result)) }
assertEquals(expectedResult, result.captured)
}
@Test
fun `should update message preference if it already exists in DB when saving view in dark mode preference`() = runBlockingTest {
// given
val messagePreference = buildMessagePreference(viewInDarkMode = true)
val expectedResult = buildMessagePreference(viewInDarkMode = false)
coEvery { messagePreferenceDao.findMessagePreference(messageId) } returns messagePreference
// when
messageRepository.saveViewInDarkModeMessagePreference(testUserId, messageId, viewInDarkMode = false)
// then
val result = slot<MessagePreferenceEntity>()
coVerify { messagePreferenceDao.saveMessagePreference(capture(result)) }
assertEquals(expectedResult, result.captured)
}
private fun setupUnreadCounterDaoToSimulateReplace() {
val counters = MutableStateFlow(emptyList<UnreadCounterEntity>())
@ -872,4 +910,14 @@ class MessageRepositoryTest {
path = path,
parentId = parentId
)
private fun buildMessagePreference(
id: Long = 0,
messageId: String = this.messageId,
viewInDarkMode: Boolean = true
) = MessagePreferenceEntity(
id = id,
messageId = messageId,
viewInDarkMode = viewInDarkMode
)
}

View File

@ -19,6 +19,7 @@
package ch.protonmail.android.ui.dialog
import android.content.Context
import androidx.lifecycle.SavedStateHandle
import ch.protonmail.android.core.Constants
import ch.protonmail.android.data.local.model.Message
@ -31,13 +32,17 @@ import ch.protonmail.android.mailbox.domain.MoveConversationsToFolder
import ch.protonmail.android.mailbox.domain.model.ConversationsActionResult
import ch.protonmail.android.mailbox.presentation.ConversationModeEnabled
import ch.protonmail.android.repository.MessageRepository
import ch.protonmail.android.ui.actionsheet.MessageActionSheet.Companion.EXTRA_ARG_IS_STARRED
import ch.protonmail.android.ui.actionsheet.MessageActionSheetAction
import ch.protonmail.android.ui.actionsheet.MessageActionSheetState
import ch.protonmail.android.ui.actionsheet.MessageActionSheetViewModel
import ch.protonmail.android.ui.actionsheet.model.ActionSheetTarget
import ch.protonmail.android.usecase.IsAppInDarkMode
import ch.protonmail.android.usecase.delete.DeleteMessage
import ch.protonmail.android.usecase.message.ChangeMessagesReadStatus
import ch.protonmail.android.usecase.message.ChangeMessagesStarredStatus
import ch.protonmail.android.details.domain.usecase.GetViewInDarkModeMessagePreference
import ch.protonmail.android.details.domain.usecase.SetViewInDarkModeMessagePreference
import io.mockk.Called
import io.mockk.MockKAnnotations
import io.mockk.Runs
@ -47,6 +52,7 @@ import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.just
import io.mockk.mockk
import io.mockk.runs
import io.mockk.verify
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runBlockingTest
@ -90,12 +96,21 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
@MockK
private lateinit var deleteConversations: DeleteConversations
private val isAppInDarkMode: IsAppInDarkMode = mockk()
private val getViewInDarkModeMessagePreference: GetViewInDarkModeMessagePreference = mockk()
@MockK
private lateinit var setViewInDarkModeMessagePreference: SetViewInDarkModeMessagePreference
@MockK
private lateinit var accountManager: AccountManager
@MockK
private lateinit var savedStateHandle: SavedStateHandle
private val context: Context = mockk()
private lateinit var viewModel: MessageActionSheetViewModel
private val testUserId = UserId("testUser")
@ -116,6 +131,9 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
changeMessagesStarredStatus,
changeConversationsStarredStatus,
conversationModeEnabled,
isAppInDarkMode,
getViewInDarkModeMessagePreference,
setViewInDarkModeMessagePreference,
accountManager
)
}
@ -611,14 +629,51 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
)
}
@Test
fun `verify the use case for saving message preference is called when view in light mode action is clicked`() = runBlockingTest {
// given
val messageId = "messageId"
val expectedActionsFlowValue = MessageActionSheetAction.ViewMessageInLightDarkMode(messageId)
coEvery { setViewInDarkModeMessagePreference(testUserId, messageId, viewInDarkMode = false) } just runs
// when
viewModel.viewInLightMode(messageId)
// then
coVerify { setViewInDarkModeMessagePreference(testUserId, messageId, viewInDarkMode = false) }
assertEquals(expectedActionsFlowValue, viewModel.actionsFlow.value)
}
@Test
fun `verify the use case for saving message preference is called when view in dark mode action is clicked`() = runBlockingTest {
// given
val messageId = "messageId"
val expectedActionsFlowValue = MessageActionSheetAction.ViewMessageInLightDarkMode(messageId)
coEvery { setViewInDarkModeMessagePreference(testUserId, messageId, viewInDarkMode = true) } just runs
// when
viewModel.viewInDarkMode(messageId)
// then
coVerify { setViewInDarkModeMessagePreference(testUserId, messageId, viewInDarkMode = true) }
assertEquals(expectedActionsFlowValue, viewModel.actionsFlow.value)
}
@Test
fun verifySetupViewStateReturnsMoveSectionStateWithShowMoveToInboxActionTrueWhenActionTargetIsMessageAndMessageLocationIsTrash() {
val messageIds = listOf("messageId7")
val currentFolder = Constants.MessageLocationType.TRASH
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -632,7 +687,7 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = false,
showDeleteAction = true
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
@ -640,9 +695,16 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
val messageIds = listOf("messageId7")
val currentFolder = Constants.MessageLocationType.SENT
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -656,7 +718,7 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = false,
showDeleteAction = true
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
@ -664,9 +726,16 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
val messageIds = listOf("messageId8")
val currentFolder = Constants.MessageLocationType.INBOX
val actionsTarget = ActionSheetTarget.CONVERSATION_ITEM_IN_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -680,7 +749,7 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = true,
showDeleteAction = false
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
@ -688,9 +757,16 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
val messageIds = listOf("messageId8")
val currentFolder = Constants.MessageLocationType.ARCHIVE
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -704,7 +780,7 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = true,
showDeleteAction = false
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
@ -712,9 +788,16 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
val messageIds = listOf("messageId10")
val currentFolder = Constants.MessageLocationType.TRASH
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -728,7 +811,7 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = false,
showDeleteAction = true
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
@ -736,9 +819,16 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
val messageIds = listOf("messageId11")
val currentFolder = Constants.MessageLocationType.LABEL
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -752,7 +842,7 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = true,
showDeleteAction = false
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
@ -760,9 +850,16 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
val messageIds = listOf("messageId12")
val currentFolder = Constants.MessageLocationType.SPAM
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -776,7 +873,7 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = false,
showDeleteAction = true
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
@ -784,9 +881,16 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
val messageIds = listOf("messageId13")
val currentFolder = Constants.MessageLocationType.INBOX
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -800,7 +904,7 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = true,
showDeleteAction = false
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
@ -808,9 +912,16 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
val messageIds = listOf("messageId14")
val currentFolder = Constants.MessageLocationType.DRAFT
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -824,7 +935,7 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = false,
showDeleteAction = true
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
@ -832,9 +943,16 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
val messageIds = listOf("messageId15")
val currentFolder = Constants.MessageLocationType.DRAFT
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -848,7 +966,7 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = false,
showDeleteAction = true
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
@ -856,9 +974,16 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
val messageIds = listOf("messageId16")
val currentFolder = Constants.MessageLocationType.ARCHIVE
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_WITHIN_CONVERSATION_DETAIL_SCREEN
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
viewModel.setupViewState(
messageIds, currentFolder, currentFolder.messageLocationTypeValue.toString(), actionsTarget
context,
messageIds,
currentFolder,
currentFolder.messageLocationTypeValue.toString(),
actionsTarget
)
val expected = MessageActionSheetState.MoveSectionState(
@ -872,7 +997,176 @@ class MessageActionSheetViewModelTest : ArchTest, CoroutinesTest {
showMoveToSpamAction = true,
showDeleteAction = false
)
assertEquals(MessageActionSheetState.Data(expected), viewModel.stateFlow.value)
assertEquals(expected, (viewModel.stateFlow.value as MessageActionSheetState.Data).moveSectionState)
}
@Test
fun `verify star, unstar and mark read actions are visible when action sheet is summoned from mailbox`() {
// given
val messageIds = listOf("messageId")
val currentFolder = Constants.MessageLocationType.INBOX
val actionsTarget = ActionSheetTarget.MAILBOX_ITEMS_IN_MAILBOX_SCREEN
val expectedResult = MessageActionSheetState.ManageSectionState(
messageIds,
currentFolder,
currentFolder.asLabelId(),
actionsTarget,
showStarAction = true,
showUnstarAction = true,
showMarkReadAction = true,
showViewInLightModeAction = false,
showViewInDarkModeAction = false
)
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
// when
viewModel.setupViewState(
context,
messageIds,
currentFolder,
currentFolder.asLabelId(),
actionsTarget
)
// then
assertEquals(expectedResult, (viewModel.stateFlow.value as MessageActionSheetState.Data).manageSectionState)
}
@Test
fun `verify star action is visible when message is not starred in details screen`() {
// given
val messageIds = listOf("messageId")
val currentFolder = Constants.MessageLocationType.INBOX
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_IN_DETAIL_SCREEN
val expectedResult = MessageActionSheetState.ManageSectionState(
messageIds,
currentFolder,
currentFolder.asLabelId(),
actionsTarget,
showStarAction = true,
showUnstarAction = false,
showMarkReadAction = false,
showViewInLightModeAction = false,
showViewInDarkModeAction = false
)
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
// when
viewModel.setupViewState(
context,
messageIds,
currentFolder,
currentFolder.asLabelId(),
actionsTarget
)
// then
assertEquals(expectedResult, (viewModel.stateFlow.value as MessageActionSheetState.Data).manageSectionState)
}
@Test
fun `verify unstar action is visible when message is starred in details screen`() {
// given
val messageIds = listOf("messageId")
val currentFolder = Constants.MessageLocationType.INBOX
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_IN_DETAIL_SCREEN
val expectedResult = MessageActionSheetState.ManageSectionState(
messageIds,
currentFolder,
currentFolder.asLabelId(),
actionsTarget,
showStarAction = false,
showUnstarAction = true,
showMarkReadAction = false,
showViewInLightModeAction = false,
showViewInDarkModeAction = false
)
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns true
every { isAppInDarkMode(any()) } returns false
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
// when
viewModel.setupViewState(
context,
messageIds,
currentFolder,
currentFolder.asLabelId(),
actionsTarget
)
// then
assertEquals(expectedResult, (viewModel.stateFlow.value as MessageActionSheetState.Data).manageSectionState)
}
@Test
fun `verify view in dark mode action is visible when app is in dark mode and web view is in light mode`() {
// given
val messageIds = listOf("messageId")
val currentFolder = Constants.MessageLocationType.INBOX
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_IN_DETAIL_SCREEN
val expectedResult = MessageActionSheetState.ManageSectionState(
messageIds,
currentFolder,
currentFolder.asLabelId(),
actionsTarget,
showStarAction = true,
showUnstarAction = false,
showMarkReadAction = false,
showViewInLightModeAction = false,
showViewInDarkModeAction = true
)
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns true
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns false
// when
viewModel.setupViewState(
context,
messageIds,
currentFolder,
currentFolder.asLabelId(),
actionsTarget
)
// then
assertEquals(expectedResult, (viewModel.stateFlow.value as MessageActionSheetState.Data).manageSectionState)
}
@Test
fun `verify view in light mode action is visible when app and web view are in dark mode`() {
// given
val messageIds = listOf("messageId")
val currentFolder = Constants.MessageLocationType.INBOX
val actionsTarget = ActionSheetTarget.MESSAGE_ITEM_IN_DETAIL_SCREEN
val expectedResult = MessageActionSheetState.ManageSectionState(
messageIds,
currentFolder,
currentFolder.asLabelId(),
actionsTarget,
showStarAction = true,
showUnstarAction = false,
showMarkReadAction = false,
showViewInLightModeAction = true,
showViewInDarkModeAction = false
)
every { savedStateHandle.get<Boolean>(EXTRA_ARG_IS_STARRED) } returns false
every { isAppInDarkMode(any()) } returns true
coEvery { getViewInDarkModeMessagePreference(any(), any(), any()) } returns true
// when
viewModel.setupViewState(
context,
messageIds,
currentFolder,
currentFolder.asLabelId(),
actionsTarget
)
// then
assertEquals(expectedResult, (viewModel.stateFlow.value as MessageActionSheetState.Data).manageSectionState)
}
}