diff --git a/app/ui/legacy/build.gradle b/app/ui/legacy/build.gradle
index 2e4f957a751dc694f73182a050c3db2b26f0ea6f..36ef6c0a2ea05c65004a57786fd245d9d2d807dd 100644
--- a/app/ui/legacy/build.gradle
+++ b/app/ui/legacy/build.gradle
@@ -42,6 +42,10 @@ dependencies {
implementation libs.fastadapter.extensions.drag
implementation libs.fastadapter.extensions.utils
implementation libs.circleimageview
+ implementation libs.moshi
+ implementation libs.moshi.kotlin
+ implementation libs.gson
+ implementation libs.androidx.datastore
api libs.appauth
implementation libs.commons.io
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/EmailCache.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/EmailCache.kt
new file mode 100644
index 0000000000000000000000000000000000000000..f1ff13198cf139101d592e32d523d107db10d0db
--- /dev/null
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/EmailCache.kt
@@ -0,0 +1,122 @@
+/*
+ * Copyright MURENA SAS 2023
+ * 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 .
+ */
+
+package com.fsck.k9.ui.messagelist
+
+import android.content.Context
+import androidx.datastore.preferences.core.edit
+import androidx.datastore.preferences.core.stringPreferencesKey
+import androidx.datastore.preferences.preferencesDataStore
+import com.fsck.k9.Account
+import com.fsck.k9.controller.MessageReference
+import com.google.gson.Gson
+import com.google.gson.TypeAdapter
+import com.google.gson.reflect.TypeToken
+import com.google.gson.stream.JsonReader
+import com.google.gson.stream.JsonToken
+import com.google.gson.stream.JsonWriter
+import java.io.IOException
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.runBlocking
+import timber.log.Timber
+
+class EmailCache constructor(private val context: Context, private val gson: Gson) {
+ companion object {
+ private const val MAX_CACHE_SIZE = 20
+ private const val preferenceDataStoreName = "emailCache"
+ private val Context.emailCacheDataStore by preferencesDataStore(preferenceDataStoreName)
+ }
+
+ private val MAIL_LIST_KEY = stringPreferencesKey("mail_list")
+ private var isCacheShown = false
+
+ suspend fun getCachedMails(): List? {
+ if (isCacheShown) {
+ return null
+ }
+
+ return runBlocking {
+ fetchCachedMail()
+ }
+ }
+
+ suspend fun saveLatestMails(mailList: List) {
+ if (isCacheShown) return
+
+ isCacheShown = true
+ val cachedMailsWithLatest = getLatestMails(mailList)
+
+ val listType = object : TypeToken>() {}.type
+ val mailListJson = gson.toJson(cachedMailsWithLatest, listType)
+ context.emailCacheDataStore.edit {
+ it[MAIL_LIST_KEY] = mailListJson
+ }
+ Timber.d("Saved latest mails in the cache")
+ }
+
+ suspend fun deleteMail(messages: List) {
+ isCacheShown = false
+ val cachedMessages = fetchCachedMail()?.toMutableList()
+ cachedMessages?.let {
+ messages.forEach { messageRef ->
+ cachedMessages.removeIf { it.messageUid == messageRef.uid }
+ saveLatestMails(cachedMessages)
+ }
+ }
+ }
+
+ private suspend fun fetchCachedMail(): List? {
+ val listType = object : TypeToken>() {}.type
+ val mailListJson = context.emailCacheDataStore.data.map { it[MAIL_LIST_KEY] }.firstOrNull()
+ Timber.d("Cached email data: $mailListJson")
+ return gson.fromJson(mailListJson, listType)
+ }
+
+ private suspend fun getLatestMails(mailList: List): MutableList {
+ var cachedMailsWithLatest = mutableListOf()
+ cachedMailsWithLatest.addAll(mailList)
+ fetchCachedMail()?.let {
+ cachedMailsWithLatest.addAll(it)
+ }
+
+ cachedMailsWithLatest.sortedByDescending { it.messageDate }
+ val lastIndex = if (cachedMailsWithLatest.size < MAX_CACHE_SIZE) cachedMailsWithLatest.size else MAX_CACHE_SIZE
+ cachedMailsWithLatest = cachedMailsWithLatest.subList(0, lastIndex)
+ return cachedMailsWithLatest
+ }
+}
+
+internal class CharSequenceTypeAdapter : TypeAdapter() {
+ @Throws(IOException::class)
+ override fun write(writer: JsonWriter, value: CharSequence?) {
+ if (value == null) {
+ writer.nullValue()
+ } else {
+ writer.value(value.toString())
+ }
+ }
+
+ @Throws(IOException::class)
+ override fun read(reader: JsonReader): CharSequence? {
+ return if (reader.peek() === JsonToken.NULL) {
+ reader.skipValue()
+ null
+ } else {
+ reader.nextString()
+ }
+ }
+}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt
index 487fcea1c558a811a2624ec127971fd8e2286f7e..95ffa93e130313d661c279be2903f96fc7437b28 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt
@@ -1,5 +1,9 @@
package com.fsck.k9.ui.messagelist
+import com.google.gson.GsonBuilder
+import com.squareup.moshi.Moshi
+import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
+import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
@@ -14,8 +18,11 @@ val messageListUiModule = module {
messageHelper = get()
)
}
+ single { Moshi.Builder().add(KotlinJsonAdapterFactory()) .build() }
+ single { GsonBuilder().registerTypeAdapter(CharSequence::class.java, CharSequenceTypeAdapter()).create() }
+ single { EmailCache(androidContext(), get()) }
factory {
- MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get())
+ MessageListLiveDataFactory(messageListLoader = get(), preferences = get(), messageListRepository = get(), emailCache = get())
}
single { SortTypeToastProvider() }
}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
index 9f5261fdf161239d7faea05c43f273afe4d9b0c2..1979715e582efb65f3fd532518fcb1562dfb19b4 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListFragment.kt
@@ -22,6 +22,7 @@ import androidx.core.view.setPadding
import androidx.fragment.app.Fragment
import androidx.recyclerview.widget.DefaultItemAnimator
import androidx.lifecycle.Observer
+import androidx.lifecycle.lifecycleScope
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import androidx.recyclerview.widget.RecyclerView.SCROLL_STATE_IDLE
@@ -64,6 +65,8 @@ import com.google.android.material.floatingactionbutton.ExtendedFloatingActionBu
import com.google.android.material.snackbar.BaseTransientBottomBar.BaseCallback
import com.google.android.material.snackbar.Snackbar
import java.util.concurrent.Future
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.launch
import net.jcip.annotations.GuardedBy
import org.koin.android.ext.android.inject
import org.koin.androidx.viewmodel.ext.android.sharedViewModel
@@ -88,6 +91,7 @@ class MessageListFragment :
private val messagingController: MessagingController by inject()
private val preferences: Preferences by inject()
private val clock: Clock by inject()
+ private val emailCache: EmailCache by inject()
private val handler = MessageListHandler(this)
private val activityListener = MessageListActivityListener()
@@ -805,6 +809,9 @@ class MessageListFragment :
}
private fun onDeleteConfirmed(messages: List) {
+ lifecycleScope.launch {
+ emailCache.deleteMail(messages)
+ }
if (showingThreadedList) {
messagingController.deleteThreads(messages)
} else {
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveData.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveData.kt
index bf757521d24ff89ef52ac88bf86691e44c068281..e8e517a5bed00ac88e7fd288ea7bcbe8f5727f44 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveData.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveData.kt
@@ -5,6 +5,7 @@ import com.fsck.k9.Preferences
import com.fsck.k9.mailstore.MessageListChangedListener
import com.fsck.k9.mailstore.MessageListRepository
import com.fsck.k9.search.getAccountUuids
+import com.fsck.k9.search.getAccounts
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -15,7 +16,8 @@ class MessageListLiveData(
private val preferences: Preferences,
private val messageListRepository: MessageListRepository,
private val coroutineScope: CoroutineScope,
- val config: MessageListConfig
+ val config: MessageListConfig,
+ val emailCache: EmailCache
) : LiveData() {
private val messageListChangedListener = MessageListChangedListener {
@@ -24,10 +26,15 @@ class MessageListLiveData(
private fun loadMessageListAsync() {
coroutineScope.launch(Dispatchers.Main) {
+ emailCache.getCachedMails()?.let {
+ value = MessageListInfo(it, true)
+ }
+
val messageList = withContext(Dispatchers.IO) {
messageListLoader.getMessageList(config)
}
value = messageList
+ emailCache.saveLatestMails(messageList.messageListItems)
}
}
diff --git a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveDataFactory.kt b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveDataFactory.kt
index af995910f10bc08f7b2885f774e2102603fe7ce7..236f679c117c690e0c2b67d31f6eaa17b9b60128 100644
--- a/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveDataFactory.kt
+++ b/app/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLiveDataFactory.kt
@@ -7,9 +7,10 @@ import kotlinx.coroutines.CoroutineScope
class MessageListLiveDataFactory(
private val messageListLoader: MessageListLoader,
private val preferences: Preferences,
- private val messageListRepository: MessageListRepository
+ private val messageListRepository: MessageListRepository,
+ private val emailCache: EmailCache
) {
fun create(coroutineScope: CoroutineScope, config: MessageListConfig): MessageListLiveData {
- return MessageListLiveData(messageListLoader, preferences, messageListRepository, coroutineScope, config)
+ return MessageListLiveData(messageListLoader, preferences, messageListRepository, coroutineScope, config, emailCache)
}
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 123d4e8720dc8be00e68ba8fbb327ebe0abffdf6..c2c24ab364853baf0fc65f3ebdb1e8bead285ebb 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -29,6 +29,7 @@ okhttp = "4.10.0"
glide = "4.14.2"
moshi = "1.14.0"
mockito = "5.0.0"
+datastore = "1.0.0"
[plugins]
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
@@ -70,6 +71,7 @@ androidx-preference = { module = "androidx.preference:preference", version.ref =
androidx-swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
androidx-test-core = "androidx.test:core:1.5.0"
android-material = "com.google.android.material:material:1.7.0"
+androidx-datastore = { module = "androidx.datastore:datastore-preferences", version.ref = "datastore" }
fastadapter = { module = "com.mikepenz:fastadapter", version.ref = "fastAdapter" }
fastadapter-extensions-drag = { module = "com.mikepenz:fastadapter-extensions-drag", version.ref = "fastAdapter" }
fastadapter-extensions-utils = { module = "com.mikepenz:fastadapter-extensions-utils", version.ref = "fastAdapter" }
@@ -79,7 +81,9 @@ preferencex-datetimepicker = { module = "com.takisoft.preferencex:preferencex-da
preferencex-colorpicker = { module = "com.takisoft.preferencex:preferencex-colorpicker", version.ref = "preferencesFix" }
okio = "com.squareup.okio:okio:3.3.0"
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
+moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshi" }
moshi-kotlin-codegen = { module = "com.squareup.moshi:moshi-kotlin-codegen", version.ref = "moshi" }
+gson = "com.google.code.gson:gson:2.8.9"
timber = "com.jakewharton.timber:timber:5.0.1"
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koinCore" }
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koinAndroid" }