Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto-advance #1843

Merged
merged 29 commits into from
May 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
300c95c
Design settings auto advance
NicolasBourdin88 Apr 24, 2024
14ff3a1
Add autoAdvanceSettings back-end logic
NicolasBourdin88 Apr 29, 2024
5eaaf06
Add autoAdvanceSettings back-end logic on delete or archive action
NicolasBourdin88 May 1, 2024
72b65b1
Reformat code
NicolasBourdin88 May 2, 2024
8730ac1
Fix snackbar bug
NicolasBourdin88 May 2, 2024
93e1336
Add matomo track event
NicolasBourdin88 May 3, 2024
86a9418
Modify observe logic on archive and delete and animation on tablet mode
NicolasBourdin88 May 3, 2024
6d9072f
Add move action to auto advance
NicolasBourdin88 May 6, 2024
f8d1f18
Simplify auto advance
NicolasBourdin88 May 6, 2024
8f4fa90
Apply suggestion from code review
NicolasBourdin88 May 8, 2024
9f065d6
Modify last action mode logic
NicolasBourdin88 May 8, 2024
eab0a16
Correction String
NicolasBourdin88 May 8, 2024
da65525
Regroup all call in same place
NicolasBourdin88 May 8, 2024
bb16481
Apply suggestion from code review
NicolasBourdin88 May 8, 2024
322ec57
Better place auto advance logic
NicolasBourdin88 May 10, 2024
04e47f8
Apply suggestion from code review
NicolasBourdin88 May 10, 2024
3f47987
directly call navigate to thread method instead of adapters callback …
NicolasBourdin88 May 13, 2024
82ac114
Change Array type to List so we don't have to repeat .toList() call
NicolasBourdin88 May 13, 2024
246cbb5
Apply suggestion from code review
NicolasBourdin88 May 13, 2024
979c347
Correct es String
NicolasBourdin88 May 13, 2024
8b95b46
Change logic emplacement to natural advance mode
NicolasBourdin88 May 13, 2024
33aa8c0
Apply suggestion from code review
NicolasBourdin88 May 14, 2024
070fea0
Add ThreadListAdapterCallback
NicolasBourdin88 May 14, 2024
4cd8825
Apply suggestion from code review
NicolasBourdin88 May 15, 2024
9c5091c
Format code
KevinBoulongne May 21, 2024
4318844
Simplify `getNextThreadToOpenByPosition()` code
KevinBoulongne May 21, 2024
096fb57
Use existing `openedThreadPosition` instead of adding new `previousTh…
KevinBoulongne May 21, 2024
22a3318
Correction english translation
NicolasBourdin88 May 21, 2024
b2d0e41
Format code
KevinBoulongne May 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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),
NicolasBourdin88 marked this conversation as resolved.
Show resolved Hide resolved
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
NicolasBourdin88 marked this conversation as resolved.
Show resolved Hide resolved
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
Loading