Skip to content

Commit

Permalink
Merge pull request #1843 from Infomaniak/auto-advance
Browse files Browse the repository at this point in the history
Auto-advance
  • Loading branch information
KevinBoulongne committed May 22, 2024
2 parents 28fd935 + b2d0e41 commit 88234a5
Show file tree
Hide file tree
Showing 27 changed files with 540 additions and 140 deletions.
150 changes: 86 additions & 64 deletions .idea/navEditor.xml

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions app/src/main/java/com/infomaniak/mail/MatomoMail.kt
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,10 @@ object MatomoMail : MatomoCore {
trackEvent("homeScreenShortcuts", name)
}

fun Fragment.trackAutoAdvanceEvent(name: String) {
trackEvent("settingsAutoAdvance", name)
}

// We need to invert this logical value to keep a coherent value for analytics because actions
// conditions are inverted (ex: if the condition is `message.isSpam`, then we want to unspam)
private fun Boolean.toMailActionValue() = (!this).toFloat()
Expand Down
9 changes: 9 additions & 0 deletions app/src/main/java/com/infomaniak/mail/data/LocalSettings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ class LocalSettings private constructor(context: Context) : SharedValues {
var showPermissionsOnboarding by sharedValue("showPermissionsOnboardingKey", true)
var isSentryTrackingEnabled by sharedValue("isSentryTrackingEnabledKey", true)
var isMatomoTrackingEnabled by sharedValue("isMatomoTrackingEnabledKey", true)
var autoAdvanceMode by sharedValue("autoAdvanceModeKey", AutoAdvanceMode.THREADS_LIST)
var autoAdvanceNaturalThread by sharedValue("autoAdvanceNaturalThreadKey", AutoAdvanceMode.FOLLOWING_THREAD)

fun removeSettings() = sharedPreferences.transaction { clear() }

Expand Down Expand Up @@ -191,6 +193,13 @@ class LocalSettings private constructor(context: Context) : SharedValues {
ASK_ME("false", R.string.settingsOptionAskMe, "askMe"),
}

enum class AutoAdvanceMode(val matomoValue: String, @StringRes val localisedNameRes: Int) {
PREVIOUS_THREAD("previousThread", R.string.settingsAutoAdvancePreviousThreadDescription),
FOLLOWING_THREAD("followingThread", R.string.settingsAutoAdvanceFollowingThreadDescription),
THREADS_LIST("listOfThread", R.string.settingsAutoAdvanceListOfThreadsDescription),
NATURAL_THREAD("naturalThread", R.string.settingsAutoAdvanceNaturalThreadDescription),
}

companion object {

private val TAG = LocalSettings::class.java.simpleName
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,6 @@ import com.infomaniak.mail.ui.newMessage.AiViewModel.Shortcut
import io.realm.kotlin.ext.copyFromRealm
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import okhttp3.MultipartBody
import okhttp3.OkHttpClient
import okhttp3.Request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import com.infomaniak.mail.data.models.thread.Thread
import com.infomaniak.mail.data.models.thread.Thread.ThreadFilter
import com.infomaniak.mail.utils.AccountUtils
import io.realm.kotlin.MutableRealm
import io.realm.kotlin.Realm
import io.realm.kotlin.TypedRealm
import io.realm.kotlin.UpdatePolicy
import io.realm.kotlin.ext.copyFromRealm
Expand Down Expand Up @@ -137,6 +138,13 @@ class MessageController @Inject constructor(private val mailboxContentRealm: Rea
fun getMessagesAsync(messageUid: String): Flow<ResultsChange<Message>> {
return getMessagesQuery(messageUid, mailboxContentRealm()).asFlow()
}

fun getMessageCountInThreadForFolder(threadUid: String, folderId: String, realm: Realm): Long? {
return ThreadController.getThread(threadUid, realm)
?.messages?.query("${Message::folderId.name} == $0", folderId)
?.count()
?.find()
}
//endregion

//region Edit data
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
package com.infomaniak.mail.data.models.signature

import android.content.Context
import com.infomaniak.lib.core.utils.context
import com.infomaniak.mail.R
import com.infomaniak.mail.data.models.draft.Draft
import com.infomaniak.mail.utils.AccountUtils
Expand Down
26 changes: 22 additions & 4 deletions app/src/main/java/com/infomaniak/mail/ui/MainViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ class MainViewModel @Inject constructor(
val reportPhishingTrigger = SingleLiveEvent<Unit>()
val canInstallUpdate = MutableLiveData(false)

val autoAdvanceThreadsUids = MutableLiveData<List<String>>()

val mailboxesLive = mailboxController.getMailboxesAsync(AccountUtils.currentUserId).asLiveData(ioCoroutineContext)

//region Multi selection
Expand Down Expand Up @@ -477,6 +479,8 @@ class MainViewModel @Inject constructor(

deleteThreadOrMessageTrigger.postValue(Unit)
if (apiResponse.isSuccess()) {
if (shouldAutoAdvance(message, threadsUids)) autoAdvanceThreadsUids.postValue(threadsUids)

refreshFoldersAsync(
mailbox = mailbox,
messagesFoldersIds = messages.getFoldersIds(exception = trashId),
Expand Down Expand Up @@ -558,19 +562,21 @@ class MainViewModel @Inject constructor(
//region Move
fun moveThreadsOrMessageTo(
destinationFolderId: String,
threadsUids: Array<String>,
threadsUids: List<String>,
messageUid: String? = null,
) = viewModelScope.launch(ioCoroutineContext) {
val mailbox = currentMailbox.value!!
val destinationFolder = folderController.getFolder(destinationFolderId)!!
val threads = getActionThreads(threadsUids.toList()).ifEmpty { return@launch }
val threads = getActionThreads(threadsUids).ifEmpty { return@launch }
val message = messageUid?.let { messageController.getMessage(it)!! }

val messages = sharedUtils.getMessagesToMove(threads, message)

val apiResponse = ApiRepository.moveMessages(mailbox.uuid, messages.getUids(), destinationFolder.id)

if (apiResponse.isSuccess()) {
if (shouldAutoAdvance(message, threadsUids)) autoAdvanceThreadsUids.postValue(threadsUids)

refreshFoldersAsync(
mailbox = mailbox,
messagesFoldersIds = messages.getFoldersIds(exception = destinationFolder.id),
Expand Down Expand Up @@ -636,6 +642,8 @@ class MainViewModel @Inject constructor(
val apiResponse = ApiRepository.moveMessages(mailbox.uuid, messages.getUids(), destinationFolder.id)

if (apiResponse.isSuccess()) {
if (shouldAutoAdvance(message, threadsUids)) autoAdvanceThreadsUids.postValue(threadsUids)

val messagesFoldersIds = messages.getFoldersIds(exception = destinationFolder.id)
refreshFoldersAsync(
mailbox = mailbox,
Expand Down Expand Up @@ -897,7 +905,7 @@ class MainViewModel @Inject constructor(

fun moveToNewFolder(
name: String,
threadsUids: Array<String>,
threadsUids: List<String>,
messageUid: String?,
) = viewModelScope.launch(ioCoroutineContext) {
val newFolderId = createNewFolderSync(name) ?: return@launch
Expand Down Expand Up @@ -1039,10 +1047,20 @@ class MainViewModel @Inject constructor(
snackbarManager.postValue(appContext.getString(snackbarTitleRes))
}

private fun threadHasOnlyOneMessageLeftInCurrentFolder(threadUid: String): Boolean {
val folderId = currentFolderId ?: return false
return messageController.getMessageCountInThreadForFolder(threadUid, folderId, mailboxContentRealm()) == 1L
}

private fun shouldAutoAdvance(message: Message?, threadsUids: List<String>): Boolean {
val isWorkingWithThread = message == null
return isWorkingWithThread || threadHasOnlyOneMessageLeftInCurrentFolder(threadsUids.first())
}

companion object {
private val TAG: String = MainViewModel::class.java.simpleName
private val DEFAULT_SELECTED_FOLDER = FolderRole.INBOX
private const val REFRESH_DELAY = 2_000L // We add this delay because it doesn't always work if we just use the `etop`.
private const val REFRESH_DELAY = 2_000L // We add this delay because `etop` isn't always big enough.
private const val MAX_REFRESH_DELAY = 6_000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -89,18 +89,16 @@ class ThreadListAdapter @Inject constructor(
private var swipingIsAuthorized: Boolean = true
private var isLoadMoreDisplayed = false

private var onThreadClicked: ((thread: Thread) -> Unit)? = null
private var onFlushClicked: ((dialogTitle: String) -> Unit)? = null
private var onLoadMoreClicked: (() -> Unit)? = null

private var folderRole: FolderRole? = null
private var onSwipeFinished: (() -> Unit)? = null
private var multiSelection: MultiSelectionListener<Thread>? = null
private var isFolderNameVisible: Boolean = false
private var threadListAdapterCallback: ThreadListAdapterCallback? = null

//region Tablet mode
private var openedThreadPosition: Int? = null
private var openedThreadUid: String? = null
var openedThreadPosition: Int? = null
private set
var openedThreadUid: String? = null
private set
//endregion

init {
Expand All @@ -109,20 +107,14 @@ class ThreadListAdapter @Inject constructor(

operator fun invoke(
folderRole: FolderRole?,
onSwipeFinished: (() -> Unit)? = null,
threadListAdapterCallback: ThreadListAdapterCallback,
multiSelection: MultiSelectionListener<Thread>? = null,
isFolderNameVisible: Boolean = false,
onThreadClicked: ((thread: Thread) -> Unit),
onFlushClicked: ((dialogTitle: String) -> Unit)? = null,
onLoadMoreClicked: (() -> Unit)? = null,
) {
this.folderRole = folderRole
this.onSwipeFinished = onSwipeFinished
this.multiSelection = multiSelection
this.isFolderNameVisible = isFolderNameVisible
this.onThreadClicked = onThreadClicked
this.onFlushClicked = onFlushClicked
this.onLoadMoreClicked = onLoadMoreClicked
this.threadListAdapterCallback = threadListAdapterCallback
}

override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
Expand Down Expand Up @@ -295,7 +287,7 @@ class ThreadListAdapter @Inject constructor(
if (multiSelection?.isEnabled == true) {
toggleMultiSelectedThread(thread)
} else {
onThreadClicked?.invoke(thread)
threadListAdapterCallback?.onThreadClicked?.invoke(thread)
// If the Thread is `onlyOneDraft`, we'll directly navigate to the NewMessageActivity.
// It means that we won't go to the ThreadFragment, so there's no need to select anything.
if (thread.uid != openedThreadUid && !thread.isOnlyOneDraft) selectNewThread(position, thread.uid)
Expand All @@ -311,6 +303,20 @@ class ThreadListAdapter @Inject constructor(

if (oldPosition != null && oldPosition < itemCount) notifyItemChanged(oldPosition, NotificationType.SELECTED_STATE)
if (newPosition != null) notifyItemChanged(newPosition, NotificationType.SELECTED_STATE)
if (oldPosition != null && newPosition != null) {
threadListAdapterCallback?.onPositionClickedChanged?.invoke(newPosition, oldPosition)
}
}

fun getNextThread(startingThreadIndex: Int, direction: Int): Pair<Thread, Int>? {
var currentThreadIndex = startingThreadIndex + direction

while (currentThreadIndex >= 0 && currentThreadIndex <= dataSet.lastIndex) {
if (dataSet[currentThreadIndex] is Thread) return dataSet[currentThreadIndex] as Thread to currentThreadIndex
currentThreadIndex += direction
}

return null
}

/**
Expand Down Expand Up @@ -454,14 +460,14 @@ class ThreadListAdapter @Inject constructor(
flushButton.apply {
val buttonText = context.getString(buttonTextId)
text = buttonText
setOnClickListener { onFlushClicked?.invoke(buttonText) }
setOnClickListener { threadListAdapterCallback?.onFlushClicked?.invoke(buttonText) }
}
}

private fun ItemThreadLoadMoreButtonBinding.displayLoadMoreButton() {
loadMoreButton.setOnClickListener {
if (dataSet.last() is Unit) dataSet = dataSet.toMutableList().apply { removeLastOrNull() }
onLoadMoreClicked?.invoke()
threadListAdapterCallback?.onLoadMoreClicked?.invoke()
}
}

Expand Down Expand Up @@ -538,7 +544,7 @@ class ThreadListAdapter @Inject constructor(

override fun onSwipeAnimationFinished(viewHolder: ThreadListViewHolder) {
viewHolder.isSwipedOverHalf = false
onSwipeFinished?.invoke()
threadListAdapterCallback?.onSwipeFinished?.invoke()
unblockOtherSwipes()
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Infomaniak Mail - Android
* Copyright (C) 2024 Infomaniak Network SA
*
* This program 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.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package com.infomaniak.mail.ui.main.folder

import com.infomaniak.mail.data.models.thread.Thread

interface ThreadListAdapterCallback {
var onSwipeFinished: (() -> Unit)?
var onThreadClicked: ((thread: Thread) -> Unit)
var onFlushClicked: ((dialogTitle: String) -> Unit)?
var onLoadMoreClicked: (() -> Unit)?
var onPositionClickedChanged: ((position: Int, previousPosition: Int) -> Unit)?
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,6 @@ import com.infomaniak.mail.MatomoMail.trackMultiSelectionEvent
import com.infomaniak.mail.MatomoMail.trackNewMessageEvent
import com.infomaniak.mail.MatomoMail.trackThreadListEvent
import com.infomaniak.mail.R
import com.infomaniak.mail.data.LocalSettings
import com.infomaniak.mail.data.LocalSettings.SwipeAction
import com.infomaniak.mail.data.LocalSettings.ThreadDensity.COMPACT
import com.infomaniak.mail.data.models.Folder
Expand Down Expand Up @@ -104,9 +103,6 @@ class ThreadListFragment : TwoPaneFragment(), SwipeRefreshLayout.OnRefreshListen

private var isFirstTimeRefreshingThreads = true

@Inject
lateinit var localSettings: LocalSettings

@Inject
lateinit var notificationManagerCompat: NotificationManagerCompat

Expand Down Expand Up @@ -287,39 +283,42 @@ class ThreadListFragment : TwoPaneFragment(), SwipeRefreshLayout.OnRefreshListen
}

private fun setupAdapter() {

threadListAdapter(
folderRole = mainViewModel.currentFolder.value?.role,
onSwipeFinished = { threadListViewModel.isRecoveringFinished.value = true },
threadListAdapterCallback = object : ThreadListAdapterCallback {
override var onSwipeFinished: (() -> Unit)? = { threadListViewModel.isRecoveringFinished.value = true }
override var onThreadClicked: (Thread) -> Unit = ::navigateToThread
override var onFlushClicked: ((dialogTitle: String) -> Unit)? = { dialogTitle ->
val trackerName = when {
isCurrentFolderRole(FolderRole.TRASH) -> "emptyTrash"
isCurrentFolderRole(FolderRole.DRAFT) -> "emptyDraft"
isCurrentFolderRole(FolderRole.SPAM) -> "emptySpam"
else -> null
}

trackerName?.let { trackThreadListEvent(it) }

descriptionDialog.show(
title = dialogTitle,
description = getString(R.string.threadListEmptyFolderAlertDescription),
onPositiveButtonClicked = {
trackThreadListEvent("${trackerName}Confirm")
mainViewModel.flushFolder()
},
)
}
override var onLoadMoreClicked: (() -> Unit)? = {
trackThreadListEvent("loadMore")
mainViewModel.getOnePageOfOldMessages()
}
override var onPositionClickedChanged: ((position: Int, previousPosition: Int) -> Unit)? =
::updateAutoAdvanceNaturalThread
},
multiSelection = object : MultiSelectionListener<Thread> {
override var isEnabled by mainViewModel::isMultiSelectOn
override val selectedItems by mainViewModel::selectedThreads
override val publishSelectedItems = mainViewModel::publishSelectedItems
},
onThreadClicked = ::navigateToThread,
onFlushClicked = { dialogTitle ->
val trackerName = when {
isCurrentFolderRole(FolderRole.TRASH) -> "emptyTrash"
isCurrentFolderRole(FolderRole.DRAFT) -> "emptyDraft"
isCurrentFolderRole(FolderRole.SPAM) -> "emptySpam"
else -> null
}

trackerName?.let { trackThreadListEvent(it) }

descriptionDialog.show(
title = dialogTitle,
description = getString(R.string.threadListEmptyFolderAlertDescription),
onPositiveButtonClicked = {
trackThreadListEvent("${trackerName}Confirm")
mainViewModel.flushFolder()
},
)
},
onLoadMoreClicked = {
trackThreadListEvent("loadMore")
mainViewModel.getOnePageOfOldMessages()
},
)

threadListAdapter.stateRestorationPolicy = StateRestorationPolicy.PREVENT_WHEN_EMPTY
Expand Down
Loading

0 comments on commit 88234a5

Please sign in to comment.