From dafc75a6b7f65e98e4a4d9a4f9d692150462cde6 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 31 Jul 2024 16:01:29 +0300 Subject: [PATCH 001/237] Added ThreadEntity.| #74 --- .../email/database/FlowCryptRoomDatabase.kt | 2 + .../email/database/entity/ThreadEntity.kt | 58 +++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ThreadEntity.kt diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt index 76087e5572..e4457260e5 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt @@ -40,6 +40,7 @@ import com.flowcrypt.email.database.entity.LabelEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.database.entity.PublicKeyEntity import com.flowcrypt.email.database.entity.RecipientEntity +import com.flowcrypt.email.database.entity.ThreadEntity import com.flowcrypt.email.extensions.kotlin.toInputStream import com.flowcrypt.email.security.KeyStoreCryptoManager import com.flowcrypt.email.security.pgp.PgpKey @@ -70,6 +71,7 @@ import java.util.UUID MessageEntity::class, PublicKeyEntity::class, AccountSettingsEntity::class, + ThreadEntity::class, ], version = FlowCryptRoomDatabase.DB_VERSION ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ThreadEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ThreadEntity.kt new file mode 100644 index 0000000000..c9f9650ae8 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ThreadEntity.kt @@ -0,0 +1,58 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.database.entity + +import android.provider.BaseColumns +import androidx.room.ColumnInfo +import androidx.room.Entity +import androidx.room.ForeignKey +import androidx.room.Index +import androidx.room.PrimaryKey + +/** + * @author Denys Bondarenko + */ +@Entity( + tableName = ThreadEntity.TABLE_NAME, + indices = [ + Index( + name = "account_account_type_in_threads", + value = ["account", "account_type"] + ), + Index( + name = "threadId_account_account_type_in_threads", + value = ["thread_id", "account", "account_type"], + unique = true + ) + ], + foreignKeys = [ + ForeignKey( + entity = AccountEntity::class, + parentColumns = ["email", "account_type"], + childColumns = ["account", "account_type"], + onDelete = ForeignKey.CASCADE + ) + ] +) +data class ThreadEntity( + @PrimaryKey(autoGenerate = true) @ColumnInfo(name = BaseColumns._ID) val id: Long? = null, + val account: String, + @ColumnInfo(name = "account_type", defaultValue = "NULL") val accountType: String? = null, + val folder: String, + @ColumnInfo(name = "thread_id", defaultValue = "NULL") val threadId: String? = null, + @ColumnInfo(name = "history_id", defaultValue = "NULL") val historyId: String? = null, + @ColumnInfo(defaultValue = "NULL") val subject: String? = null, + @ColumnInfo(defaultValue = "NULL") val addresses: String? = null, + @ColumnInfo(name = "date", defaultValue = "NULL") val date: Long? = null, + @ColumnInfo(name = "label_ids", defaultValue = "NULL") val labelIds: String? = null, + @ColumnInfo(name = "messages_count", defaultValue = "0") val messagesCount: Int = 0, + @ColumnInfo(name = "has_attachments", defaultValue = "0") val hasAttachments: Boolean = false, + @ColumnInfo(name = "has_pgp", defaultValue = "0") val hasPgp: Boolean = false, +) { + companion object { + const val TABLE_NAME = "threads" + } +} \ No newline at end of file From 7d9cf77a039a7ce1fbceba65b48aa68c1404a242 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 11:03:19 +0300 Subject: [PATCH 002/237] Reverted back some changes.| #74 --- .../email/database/FlowCryptRoomDatabase.kt | 2 - .../email/database/entity/ThreadEntity.kt | 58 ------------------- 2 files changed, 60 deletions(-) delete mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ThreadEntity.kt diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt index e4457260e5..76087e5572 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt @@ -40,7 +40,6 @@ import com.flowcrypt.email.database.entity.LabelEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.database.entity.PublicKeyEntity import com.flowcrypt.email.database.entity.RecipientEntity -import com.flowcrypt.email.database.entity.ThreadEntity import com.flowcrypt.email.extensions.kotlin.toInputStream import com.flowcrypt.email.security.KeyStoreCryptoManager import com.flowcrypt.email.security.pgp.PgpKey @@ -71,7 +70,6 @@ import java.util.UUID MessageEntity::class, PublicKeyEntity::class, AccountSettingsEntity::class, - ThreadEntity::class, ], version = FlowCryptRoomDatabase.DB_VERSION ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ThreadEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ThreadEntity.kt deleted file mode 100644 index c9f9650ae8..0000000000 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/ThreadEntity.kt +++ /dev/null @@ -1,58 +0,0 @@ -/* - * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: denbond7 - */ - -package com.flowcrypt.email.database.entity - -import android.provider.BaseColumns -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Index -import androidx.room.PrimaryKey - -/** - * @author Denys Bondarenko - */ -@Entity( - tableName = ThreadEntity.TABLE_NAME, - indices = [ - Index( - name = "account_account_type_in_threads", - value = ["account", "account_type"] - ), - Index( - name = "threadId_account_account_type_in_threads", - value = ["thread_id", "account", "account_type"], - unique = true - ) - ], - foreignKeys = [ - ForeignKey( - entity = AccountEntity::class, - parentColumns = ["email", "account_type"], - childColumns = ["account", "account_type"], - onDelete = ForeignKey.CASCADE - ) - ] -) -data class ThreadEntity( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = BaseColumns._ID) val id: Long? = null, - val account: String, - @ColumnInfo(name = "account_type", defaultValue = "NULL") val accountType: String? = null, - val folder: String, - @ColumnInfo(name = "thread_id", defaultValue = "NULL") val threadId: String? = null, - @ColumnInfo(name = "history_id", defaultValue = "NULL") val historyId: String? = null, - @ColumnInfo(defaultValue = "NULL") val subject: String? = null, - @ColumnInfo(defaultValue = "NULL") val addresses: String? = null, - @ColumnInfo(name = "date", defaultValue = "NULL") val date: Long? = null, - @ColumnInfo(name = "label_ids", defaultValue = "NULL") val labelIds: String? = null, - @ColumnInfo(name = "messages_count", defaultValue = "0") val messagesCount: Int = 0, - @ColumnInfo(name = "has_attachments", defaultValue = "0") val hasAttachments: Boolean = false, - @ColumnInfo(name = "has_pgp", defaultValue = "0") val hasPgp: Boolean = false, -) { - companion object { - const val TABLE_NAME = "threads" - } -} \ No newline at end of file From 5d4488add21ea6c974c242369b5cbfe754aa1865 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 11:38:01 +0300 Subject: [PATCH 003/237] wip --- .../api/email/model/IncomingMessageInfo.kt | 4 +- .../api/email/model/OutgoingMessageInfo.kt | 11 +- .../email/database/dao/AttachmentDao.kt | 14 +-- .../email/database/dao/MessageDao.kt | 102 +++++++++--------- .../email/database/entity/AttachmentEntity.kt | 21 ++-- .../email/database/entity/MessageEntity.kt | 92 +++++++++------- .../CreateOutgoingMessageViewModel.kt | 4 +- .../email/jetpack/viewmodel/DraftViewModel.kt | 8 +- .../jetpack/viewmodel/MessagesViewModel.kt | 2 +- .../jetpack/viewmodel/MsgDetailsViewModel.kt | 8 +- .../HandlePasswordProtectedMsgWorker.kt | 2 +- .../fragment/MessageDetailsFragment.kt | 4 +- .../activity/fragment/MessagesListFragment.kt | 9 +- .../email/api/email/EmailUtilTest.kt | 5 +- 14 files changed, 154 insertions(+), 132 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/IncomingMessageInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/IncomingMessageInfo.kt index 404eef3e1c..02ecc5c3cb 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/IncomingMessageInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/IncomingMessageInfo.kt @@ -42,7 +42,7 @@ data class IncomingMessageInfo constructor( fun getReplyTo(): List = msgEntity.replyToAddress fun getReplyToWithoutOwnerAddress(): List = getReplyTo().filter { - !it.address.equals(msgEntity.email, true) + !it.address.equals(msgEntity.account, true) } fun getReceiveDate(): Date = Date(msgEntity.receivedDate ?: 0) @@ -97,7 +97,7 @@ data class IncomingMessageInfo constructor( //remove all addresses that To has ccSet.removeAll(toRecipients.toSet()) //remove aliases as Gmail does and the owner address - val fromAddress = msgEntity.email + val fromAddress = msgEntity.account ccSet.removeAll { internetAddress -> aliases.any { alias -> internetAddress.address.equals(alias, ignoreCase = true) } || fromAddress.equals(internetAddress.address, true) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt index a1cb85c747..419740eef4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt @@ -120,15 +120,16 @@ data class OutgoingMessageInfo( ): MessageEntity { val timestamp = System.currentTimeMillis() return MessageEntity( - email = requireNotNull(account), + account = requireNotNull(account), + accountType = requireNotNull(account),//need to fix it. Don't merge folder = folder, uid = uid, receivedDate = timestamp, sentDate = timestamp, - fromAddress = from.toString(), - replyTo = replyTo, - toAddress = InternetAddress.toString(toRecipients?.toTypedArray()), - ccAddress = InternetAddress.toString(ccRecipients?.toTypedArray()), + fromAddresses = from.toString(), + replyToAddresses = replyTo, + toAddresses = InternetAddress.toString(toRecipients?.toTypedArray()), + ccAddresses = InternetAddress.toString(ccRecipients?.toTypedArray()), subject = subject, flags = flags.toString().uppercase(), hasAttachments = atts?.isNotEmpty() == true || forwardedAtts?.isNotEmpty() == true, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt index 7178f701ce..4240dde6b3 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt @@ -16,28 +16,28 @@ import kotlinx.coroutines.flow.Flow @Dao interface AttachmentDao : BaseDao { - @Query("DELETE FROM attachment WHERE email = :email AND folder = :label") + @Query("DELETE FROM attachment WHERE account = :email AND folder = :label") suspend fun deleteAtt(email: String?, label: String?): Int - @Query("SELECT * FROM attachment WHERE email = :account AND folder = :label AND uid = :uid") + @Query("SELECT * FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") fun getAttachments(account: String, label: String, uid: Long): List - @Query("SELECT * FROM attachment WHERE email = :account AND folder = :label AND uid = :uid") + @Query("SELECT * FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") suspend fun getAttachmentsSuspend( account: String, label: String, uid: Long ): List - @Query("SELECT * FROM attachment WHERE email = :account AND folder = :label AND uid = :uid") + @Query("SELECT * FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") fun getAttachmentsFlow(account: String, label: String, uid: Long): Flow> - @Query("DELETE FROM attachment WHERE email = :account AND folder = :label AND uid = :uid") + @Query("DELETE FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") fun deleteAtt(account: String, label: String, uid: Long): Int - @Query("DELETE FROM attachment WHERE email = :account AND folder = :label AND uid = :uid") + @Query("DELETE FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") suspend fun deleteAttSuspend(account: String, label: String, uid: Long): Int - @Query("DELETE FROM attachment WHERE email = :email") + @Query("DELETE FROM attachment WHERE account = :email") suspend fun deleteByEmailSuspend(email: String?): Int } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt index 862ae46372..f810de1ae4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt @@ -30,25 +30,25 @@ import kotlinx.coroutines.withContext */ @Dao abstract class MessageDao : BaseDao { - @Query("SELECT * FROM messages WHERE email = :account AND folder = :folder AND uid = :uid") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder AND uid = :uid") abstract fun getMsg(account: String?, folder: String?, uid: Long): MessageEntity? - @Query("SELECT * FROM messages WHERE email = :account AND folder = :folder AND uid = :uid") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder AND uid = :uid") abstract suspend fun getMsgSuspend(account: String?, folder: String?, uid: Long): MessageEntity? @Query("SELECT * FROM messages WHERE _id = :id") abstract suspend fun getMsgById(id: Long): MessageEntity? - @Query("SELECT * FROM messages WHERE email = :account AND folder = :folder") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder") abstract fun getMsgsLD(account: String, folder: String): LiveData - @Query("SELECT * FROM messages WHERE email = :account AND folder = :folder") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder") abstract fun getMsgs(account: String, folder: String): List - @Query("SELECT * FROM messages WHERE email = :account AND folder = :folder") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder") abstract suspend fun getMsgsSuspend(account: String, folder: String): List - @Query("SELECT * FROM messages WHERE email = :account AND folder = :folder AND _id IN (:msgsID)") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder AND _id IN (:msgsID)") abstract suspend fun getMsgsByIDSuspend( account: String, folder: String, @@ -60,7 +60,7 @@ abstract class MessageDao : BaseDao { @Query( "SELECT * FROM messages " + - "WHERE email = :account AND folder = :folder AND is_new = 1 ORDER BY :orderBy" + "WHERE account = :account AND folder = :folder AND is_new = 1 ORDER BY :orderBy" ) abstract fun getNewMsgs( account: String, folder: String, @@ -69,7 +69,7 @@ abstract class MessageDao : BaseDao { @Query( "SELECT * FROM messages " + - "WHERE email = :account AND folder = :folder AND is_new = 1 ORDER BY :orderBy" + "WHERE account = :account AND folder = :folder AND is_new = 1 ORDER BY :orderBy" ) abstract suspend fun getNewMsgsSuspend( account: String, folder: String, @@ -78,7 +78,7 @@ abstract class MessageDao : BaseDao { @Query( "SELECT * FROM messages " + - "WHERE email = :account AND folder = :folder AND uid IN (:msgsUID)" + "WHERE account = :account AND folder = :folder AND uid IN (:msgsUID)" ) abstract fun getMsgsByUids( account: String?, @@ -88,7 +88,7 @@ abstract class MessageDao : BaseDao { @Query( "SELECT * FROM messages " + - "WHERE email = :account AND folder = :folder AND uid IN (:msgsUID)" + "WHERE account = :account AND folder = :folder AND uid IN (:msgsUID)" ) abstract suspend fun getMsgsByUidsSuspend( account: String?, @@ -98,7 +98,7 @@ abstract class MessageDao : BaseDao { @Query( "SELECT * FROM messages " + - "WHERE email = :account AND folder = :folder ORDER BY received_date DESC" + "WHERE account = :account AND folder = :folder ORDER BY received_date DESC" ) abstract fun getMessagesDataSourceFactory( account: String, @@ -108,13 +108,13 @@ abstract class MessageDao : BaseDao { @Query( "SELECT * FROM (" + "SELECT * FROM messages " + - "WHERE email = :account AND folder = :folder AND received_date > :date " + + "WHERE account = :account AND folder = :folder AND received_date > :date " + "ORDER BY received_date ASC " + "LIMIT :limit) " + "UNION " + "SELECT * FROM (" + "SELECT * FROM messages " + - "WHERE email = :account AND folder = :folder AND received_date <= :date " + + "WHERE account = :account AND folder = :folder AND received_date <= :date " + "ORDER BY received_date DESC " + "LIMIT :limit) " + "ORDER BY received_date DESC" @@ -126,44 +126,44 @@ abstract class MessageDao : BaseDao { limit: Int ): List - @Query("SELECT * FROM messages WHERE email = :account AND folder = :folder AND uid = :uid") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder AND uid = :uid") abstract fun getMsgLiveData(account: String, folder: String, uid: Long): LiveData @Query("SELECT * FROM messages WHERE _id = :id") abstract fun getMsgLiveDataById(id: Long): LiveData - @Query("DELETE FROM messages WHERE email = :email AND folder = :label") + @Query("DELETE FROM messages WHERE account = :email AND folder = :label") abstract suspend fun delete(email: String?, label: String?): Int - @Query("DELETE FROM messages WHERE email = :email AND folder NOT IN (:labels)") + @Query("DELETE FROM messages WHERE account = :email AND folder NOT IN (:labels)") abstract suspend fun deleteAllExceptRelatedToLabels( email: String?, labels: Collection ): Int - @Query("DELETE FROM messages WHERE email = :email AND folder = :label AND uid IN (:msgsUID)") + @Query("DELETE FROM messages WHERE account = :email AND folder = :label AND uid IN (:msgsUID)") abstract fun delete(email: String?, label: String?, msgsUID: Collection): Int - @Query("DELETE FROM messages WHERE email = :email AND folder = :label AND uid IN (:msgsUID)") + @Query("DELETE FROM messages WHERE account = :email AND folder = :label AND uid IN (:msgsUID)") abstract suspend fun deleteSuspendByUIDs( email: String?, label: String?, msgsUID: Collection ): Int - @Query("SELECT * FROM messages WHERE email = :account AND folder = :label") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :label") abstract fun getOutboxMsgs( account: String?, label: String = JavaEmailConstants.FOLDER_OUTBOX ): List - @Query("SELECT * FROM messages WHERE email = :account AND folder = :label") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :label") abstract fun getOutboxMsgsLD( account: String?, label: String = JavaEmailConstants.FOLDER_OUTBOX ): LiveData> - @Query("SELECT * FROM messages WHERE email = :account AND folder = :label") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :label") abstract suspend fun getOutboxMsgsSuspend( account: String?, label: String = JavaEmailConstants.FOLDER_OUTBOX @@ -176,7 +176,7 @@ abstract class MessageDao : BaseDao { @Query( "SELECT * FROM messages " + - "WHERE email = :account AND folder = :label AND state IN (:msgStates)" + "WHERE account = :account AND folder = :label AND state IN (:msgStates)" ) abstract fun getOutboxMsgsByStates( account: String?, label: String = JavaEmailConstants.FOLDER_OUTBOX, @@ -185,7 +185,7 @@ abstract class MessageDao : BaseDao { @Query( "SELECT * FROM messages " + - "WHERE email = :account AND folder = :label AND state IN (:msgStates)" + "WHERE account = :account AND folder = :label AND state IN (:msgStates)" ) abstract suspend fun getOutboxMsgsByStatesSuspend( account: String?, @@ -194,7 +194,7 @@ abstract class MessageDao : BaseDao { ): List @Query( - "DELETE FROM messages WHERE email = :email " + + "DELETE FROM messages WHERE account = :email " + "AND folder = :label AND uid = :uid AND (state NOT IN (:msgStates) OR state IS NULL)" ) abstract suspend fun deleteOutgoingMsg( @@ -207,7 +207,7 @@ abstract class MessageDao : BaseDao { ): Int @Query( - "SELECT COUNT(*) FROM messages WHERE email = :account " + + "SELECT COUNT(*) FROM messages WHERE account = :account " + "AND folder = :label AND (state IN (:msgStates) OR state IS NULL)" ) abstract suspend fun getFailedOutgoingMessagesCountSuspend( @@ -225,31 +225,31 @@ abstract class MessageDao : BaseDao { ) ): Int - @Query("SELECT COUNT(*) FROM messages WHERE email = :account AND folder = :folder") + @Query("SELECT COUNT(*) FROM messages WHERE account = :account AND folder = :folder") abstract fun count(account: String?, folder: String?): Int - @Query("SELECT COUNT(*) FROM messages WHERE email = :account AND folder = :folder") + @Query("SELECT COUNT(*) FROM messages WHERE account = :account AND folder = :folder") abstract suspend fun countSuspend(account: String?, folder: String?): Int? - @Query("SELECT max(uid) FROM messages WHERE email = :account AND folder = :folder") + @Query("SELECT max(uid) FROM messages WHERE account = :account AND folder = :folder") abstract fun getLastUIDOfMsgForLabel(account: String?, folder: String?): Int @Query( "SELECT * FROM messages " + - "WHERE email = :account AND folder = :folder ORDER BY uid DESC LIMIT 1" + "WHERE account = :account AND folder = :folder ORDER BY uid DESC LIMIT 1" ) abstract suspend fun getNewestMsg(account: String?, folder: String?): MessageEntity? - @Query("SELECT max(uid) FROM messages WHERE email = :account AND folder = :folder") + @Query("SELECT max(uid) FROM messages WHERE account = :account AND folder = :folder") abstract suspend fun getLastUIDOfMsgForLabelSuspend(account: String?, folder: String?): Int? - @Query("SELECT min(uid) FROM messages WHERE email = :account AND folder = :folder") + @Query("SELECT min(uid) FROM messages WHERE account = :account AND folder = :folder") abstract fun getOldestUIDOfMsgForLabel(account: String?, folder: String?): Int - @Query("SELECT min(uid) FROM messages WHERE email = :account AND folder = :folder") + @Query("SELECT min(uid) FROM messages WHERE account = :account AND folder = :folder") abstract suspend fun getOldestUIDOfMsgForLabelSuspend(account: String?, folder: String?): Int? - @Query("SELECT uid FROM messages WHERE email = :account AND folder = :folder") + @Query("SELECT uid FROM messages WHERE account = :account AND folder = :folder") abstract suspend fun getUIDsForLabel(account: String?, folder: String?): List /** @@ -261,7 +261,7 @@ abstract class MessageDao : BaseDao { */ @Query( "SELECT uid FROM messages " + - "WHERE email = :account AND folder = :label AND is_encrypted = -1" + "WHERE account = :account AND folder = :label AND is_encrypted = -1" ) abstract suspend fun getNotCheckedUIDs(account: String?, label: String): List @@ -274,7 +274,7 @@ abstract class MessageDao : BaseDao { */ @Query( "SELECT uid FROM messages " + - "WHERE email = :account AND folder = :label AND flags NOT LIKE '%\\SEEN'" + "WHERE account = :account AND folder = :label AND flags NOT LIKE '%\\SEEN'" ) abstract fun getUIDOfUnseenMsgs(account: String?, label: String): List @@ -289,19 +289,19 @@ abstract class MessageDao : BaseDao { */ @Query( "UPDATE messages SET state=:newValues " + - "WHERE email = :account AND folder = :label AND state = :oldValue" + "WHERE account = :account AND folder = :label AND state = :oldValue" ) abstract fun changeMsgsState(account: String?, label: String?, oldValue: Int, newValues: Int): Int @Query( "UPDATE messages SET state=:newValue " + - "WHERE email = :account AND folder = :label" + "WHERE account = :account AND folder = :label" ) abstract fun changeMsgsState(account: String?, label: String?, newValue: Int? = null): Int @Query( "UPDATE messages SET state=:newValue " + - "WHERE email = :account AND folder = :label" + "WHERE account = :account AND folder = :label" ) abstract suspend fun changeMsgsStateSuspend( account: String?, @@ -311,7 +311,7 @@ abstract class MessageDao : BaseDao { @Query( "UPDATE messages SET state=:newValues " + - "WHERE email = :account AND folder = :label AND state = :oldValue" + "WHERE account = :account AND folder = :label AND state = :oldValue" ) abstract suspend fun changeMsgsStateSuspend( account: String?, label: String?, oldValue: Int, @@ -325,7 +325,7 @@ abstract class MessageDao : BaseDao { */ @Query( "UPDATE messages SET state=2 " + - "WHERE email = :account AND folder = :label AND state =:oldValue" + "WHERE account = :account AND folder = :label AND state =:oldValue" ) abstract fun resetMsgsWithSendingState( account: String?, @@ -340,7 +340,7 @@ abstract class MessageDao : BaseDao { */ @Query( "UPDATE messages SET state=2 " + - "WHERE email = :account AND folder = :label AND state =:oldValue" + "WHERE account = :account AND folder = :label AND state =:oldValue" ) abstract suspend fun resetMsgsWithSendingStateSuspend( account: String?, @@ -348,32 +348,32 @@ abstract class MessageDao : BaseDao { oldValue: Int = MessageState.SENDING.value ): Int - @Query("SELECT uid, flags FROM messages WHERE email = :account AND folder = :label") + @Query("SELECT uid, flags FROM messages WHERE account = :account AND folder = :label") abstract fun getUIDAndFlagsPairs(account: String?, label: String): List - @Query("SELECT uid, flags FROM messages WHERE email = :account AND folder = :label") + @Query("SELECT uid, flags FROM messages WHERE account = :account AND folder = :label") abstract suspend fun getUIDAndFlagsPairsSuspend( account: String?, label: String ): List - @Query("SELECT * FROM messages WHERE email = :account AND state =:stateValue") + @Query("SELECT * FROM messages WHERE account = :account AND state =:stateValue") abstract fun getMsgsWithState(account: String?, stateValue: Int): List - @Query("SELECT * FROM messages WHERE email = :account AND state =:stateValue") + @Query("SELECT * FROM messages WHERE account = :account AND state =:stateValue") abstract suspend fun getMsgsWithStateSuspend( account: String?, stateValue: Int ): List - @Query("SELECT * FROM messages WHERE email = :account AND folder = :label AND state =:stateValue") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :label AND state =:stateValue") abstract fun getMsgsWithState( account: String?, label: String?, stateValue: Int ): List - @Query("SELECT * FROM messages WHERE email = :account AND folder = :label AND state =:stateValue") + @Query("SELECT * FROM messages WHERE account = :account AND folder = :label AND state =:stateValue") abstract suspend fun getMsgsWithStateSuspend( account: String?, label: String?, @@ -382,7 +382,7 @@ abstract class MessageDao : BaseDao { @Query( "UPDATE messages SET is_new = 0 " + - "WHERE email = :account AND folder = :label AND uid IN (:uidList)" + "WHERE account = :account AND folder = :label AND uid IN (:uidList)" ) abstract suspend fun markMsgsAsOld( account: String?, @@ -390,13 +390,13 @@ abstract class MessageDao : BaseDao { uidList: Collection ): Int - @Query("UPDATE messages SET is_new = 0 WHERE email = :account AND folder = :label AND is_new = 1") + @Query("UPDATE messages SET is_new = 0 WHERE account = :account AND folder = :label AND is_new = 1") abstract suspend fun markMsgsAsOld(account: String?, label: String?): Int - @Query("DELETE FROM messages WHERE email = :email") + @Query("DELETE FROM messages WHERE account = :email") abstract suspend fun deleteByEmailSuspend(email: String?): Int - @Query("SELECT COUNT(*) FROM messages WHERE email = :account AND folder = :label") + @Query("SELECT COUNT(*) FROM messages WHERE account = :account AND folder = :label") abstract suspend fun getMsgsCount(account: String, label: String): Int @Transaction diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt index b6bc4c23d5..7e0c8fe0a5 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt @@ -22,22 +22,26 @@ import com.flowcrypt.email.api.email.model.AttachmentInfo tableName = AttachmentEntity.TABLE_NAME, indices = [ Index( - name = "email_uid_folder_path_in_attachment", - value = ["email", "uid", "folder", "path"], + name = "account_account_type_folder_uid_in_attachment", + value = ["account", "account_type", "folder", "uid"] + ), + Index( + name = "account_account_type_folder_uid_path_in_attachment", + value = ["account", "account_type", "folder", "uid", "path"], unique = true ), - Index(name = "email_folder_uid_in_attachment", value = ["email", "folder", "uid"]) ], foreignKeys = [ ForeignKey( - entity = MessageEntity::class, parentColumns = ["email", "folder", "uid"], - childColumns = ["email", "folder", "uid"], onDelete = ForeignKey.CASCADE + entity = MessageEntity::class, parentColumns = ["account", "account_type", "folder", "uid"], + childColumns = ["account", "account_type", "folder", "uid"], onDelete = ForeignKey.CASCADE ) ] ) data class AttachmentEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = BaseColumns._ID) val id: Long?, - val email: String, + val account: String, + @ColumnInfo(name = "account_type") val accountType: String, val folder: String, val uid: Long, val name: String, @@ -57,7 +61,7 @@ data class AttachmentEntity( fun toAttInfo(): AttachmentInfo { return AttachmentInfo.Builder( - email = email, + email = account, folder = folder, uid = uid, name = name, @@ -85,7 +89,8 @@ data class AttachmentEntity( return AttachmentEntity( id = null, - email = email, + account = email, + accountType = "accountType",//need to think here folder = folder, uid = uid, name = name, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index d2d6116ce8..6990c7c031 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -12,6 +12,7 @@ import android.provider.BaseColumns import androidx.preference.PreferenceManager import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.ForeignKey import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey @@ -48,60 +49,73 @@ import java.util.Properties /** * @author Denys Bondarenko */ -//todo-denbond7 need to add ForeignKey on account table @Entity( tableName = MessageEntity.TABLE_NAME, indices = [ - Index(name = "email_in_messages", value = ["email"]), - Index(name = "email_uid_folder_in_messages", value = ["email", "uid", "folder"], unique = true) + Index(name = "account_account_type_in_messages", value = ["account", "account_type"]), + Index(name = "uid_in_messages", value = ["uid"]), + Index( + name = "account_account_type_folder_uid_in_messages", + value = ["account", "account_type", "folder", "uid"], + unique = true + ), + ], + foreignKeys = [ + ForeignKey( + entity = AccountEntity::class, + parentColumns = ["email", "account_type"], + childColumns = ["account", "account_type"], + onDelete = ForeignKey.CASCADE + ) ] ) @Parcelize data class MessageEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = BaseColumns._ID) val id: Long? = null, - val email: String, + val account: String, + @ColumnInfo(name = "account_type") val accountType: String, val folder: String, val uid: Long, @ColumnInfo(name = "received_date", defaultValue = "NULL") val receivedDate: Long? = null, @ColumnInfo(name = "sent_date", defaultValue = "NULL") val sentDate: Long? = null, - @ColumnInfo(name = "from_address", defaultValue = "NULL") val fromAddress: String? = null, - @ColumnInfo(name = "to_address", defaultValue = "NULL") val toAddress: String? = null, - @ColumnInfo(name = "cc_address", defaultValue = "NULL") val ccAddress: String? = null, + @ColumnInfo(name = "from_addresses", defaultValue = "NULL") val fromAddresses: String? = null, + @ColumnInfo(name = "to_addresses", defaultValue = "NULL") val toAddresses: String? = null, + @ColumnInfo(name = "cc_addresses", defaultValue = "NULL") val ccAddresses: String? = null, + @ColumnInfo( + name = "reply_to_addresses", + defaultValue = "NULL" + ) val replyToAddresses: String? = null, @ColumnInfo(defaultValue = "NULL") val subject: String? = null, @ColumnInfo(defaultValue = "NULL") val flags: String? = null, - @ColumnInfo( - name = "is_message_has_attachments", - defaultValue = "0" - ) val hasAttachments: Boolean? = null, - @ColumnInfo(name = "is_encrypted", defaultValue = "-1") val isEncrypted: Boolean? = null, + @ColumnInfo(name = "has_attachments", defaultValue = "0") val hasAttachments: Boolean? = null, @ColumnInfo(name = "is_new", defaultValue = "-1") val isNew: Boolean? = null, @ColumnInfo(defaultValue = "-1") val state: Int? = null, @ColumnInfo(name = "attachments_directory") val attachmentsDirectory: String? = null, - @ColumnInfo(name = "error_msg", defaultValue = "NULL") val errorMsg: String? = null, - @ColumnInfo(name = "reply_to", defaultValue = "NULL") val replyTo: String? = null, + @ColumnInfo(name = "error_message", defaultValue = "NULL") val errorMsg: String? = null, @ColumnInfo(name = "thread_id", defaultValue = "NULL") val threadId: String? = null, @ColumnInfo(name = "history_id", defaultValue = "NULL") val historyId: String? = null, @ColumnInfo(name = "password", defaultValue = "NULL") val password: ByteArray? = null, @ColumnInfo(name = "draft_id", defaultValue = "NULL") val draftId: String? = null, @ColumnInfo(name = "label_ids", defaultValue = "NULL") val labelIds: String? = null, + @ColumnInfo(name = "is_encrypted", defaultValue = "-1") val isEncrypted: Boolean? = null, @ColumnInfo(name = "has_pgp", defaultValue = "0") val hasPgp: Boolean? = null, ) : Parcelable { @IgnoredOnParcel @Ignore - val from: List = fromAddress.asInternetAddresses().asList() + val from: List = fromAddresses.asInternetAddresses().asList() @IgnoredOnParcel @Ignore - val replyToAddress: List = replyTo.asInternetAddresses().asList() + val replyToAddress: List = replyToAddresses.asInternetAddresses().asList() @IgnoredOnParcel @Ignore - val to: List = toAddress.asInternetAddresses().asList() + val to: List = toAddresses.asInternetAddresses().asList() @IgnoredOnParcel @Ignore - val cc: List = ccAddress.asInternetAddresses().asList() + val cc: List = ccAddresses.asInternetAddresses().asList() @IgnoredOnParcel @Ignore @@ -158,23 +172,23 @@ data class MessageEntity( other as MessageEntity if (id != other.id) return false - if (email != other.email) return false + if (account != other.account) return false + if (accountType != other.accountType) return false if (folder != other.folder) return false if (uid != other.uid) return false if (receivedDate != other.receivedDate) return false if (sentDate != other.sentDate) return false - if (fromAddress != other.fromAddress) return false - if (toAddress != other.toAddress) return false - if (ccAddress != other.ccAddress) return false + if (fromAddresses != other.fromAddresses) return false + if (toAddresses != other.toAddresses) return false + if (ccAddresses != other.ccAddresses) return false + if (replyToAddresses != other.replyToAddresses) return false if (subject != other.subject) return false if (flags != other.flags) return false if (hasAttachments != other.hasAttachments) return false - if (isEncrypted != other.isEncrypted) return false if (isNew != other.isNew) return false if (state != other.state) return false if (attachmentsDirectory != other.attachmentsDirectory) return false if (errorMsg != other.errorMsg) return false - if (replyTo != other.replyTo) return false if (threadId != other.threadId) return false if (historyId != other.historyId) return false if (password != null) { @@ -183,34 +197,37 @@ data class MessageEntity( } else if (other.password != null) return false if (draftId != other.draftId) return false if (labelIds != other.labelIds) return false + if (isEncrypted != other.isEncrypted) return false if (hasPgp != other.hasPgp) return false + return true } override fun hashCode(): Int { var result = id?.hashCode() ?: 0 - result = 31 * result + email.hashCode() + result = 31 * result + account.hashCode() + result = 31 * result + accountType.hashCode() result = 31 * result + folder.hashCode() result = 31 * result + uid.hashCode() result = 31 * result + (receivedDate?.hashCode() ?: 0) result = 31 * result + (sentDate?.hashCode() ?: 0) - result = 31 * result + (fromAddress?.hashCode() ?: 0) - result = 31 * result + (toAddress?.hashCode() ?: 0) - result = 31 * result + (ccAddress?.hashCode() ?: 0) + result = 31 * result + (fromAddresses?.hashCode() ?: 0) + result = 31 * result + (toAddresses?.hashCode() ?: 0) + result = 31 * result + (ccAddresses?.hashCode() ?: 0) + result = 31 * result + (replyToAddresses?.hashCode() ?: 0) result = 31 * result + (subject?.hashCode() ?: 0) result = 31 * result + (flags?.hashCode() ?: 0) result = 31 * result + (hasAttachments?.hashCode() ?: 0) - result = 31 * result + (isEncrypted?.hashCode() ?: 0) result = 31 * result + (isNew?.hashCode() ?: 0) result = 31 * result + (state ?: 0) result = 31 * result + (attachmentsDirectory?.hashCode() ?: 0) result = 31 * result + (errorMsg?.hashCode() ?: 0) - result = 31 * result + (replyTo?.hashCode() ?: 0) result = 31 * result + (threadId?.hashCode() ?: 0) result = 31 * result + (historyId?.hashCode() ?: 0) result = 31 * result + (password?.contentHashCode() ?: 0) result = 31 * result + (draftId?.hashCode() ?: 0) result = 31 * result + (labelIds?.hashCode() ?: 0) + result = 31 * result + (isEncrypted?.hashCode() ?: 0) result = 31 * result + (hasPgp?.hashCode() ?: 0) return result } @@ -374,15 +391,17 @@ data class MessageEntity( hasPgp: Boolean? = null, hasAttachments: Boolean? = null ): MessageEntity { - return MessageEntity(email = email, + return MessageEntity( + account = email, + accountType = "accountType",//need to fix it. Don't merge folder = label, uid = uid, receivedDate = msg.receivedDate?.time, sentDate = msg.sentDate?.time, - fromAddress = InternetAddress.toString(msg.from), - replyTo = InternetAddress.toString(msg.replyTo), - toAddress = InternetAddress.toString(msg.getRecipients(Message.RecipientType.TO)), - ccAddress = InternetAddress.toString(msg.getRecipients(Message.RecipientType.CC)), + fromAddresses = InternetAddress.toString(msg.from), + replyToAddresses = InternetAddress.toString(msg.replyTo), + toAddresses = InternetAddress.toString(msg.getRecipients(Message.RecipientType.TO)), + ccAddresses = InternetAddress.toString(msg.getRecipients(Message.RecipientType.CC)), subject = msg.subject, flags = msg.flags.toString().uppercase(), hasAttachments = hasAttachments?.let { hasAttachments } ?: EmailUtil.hasAtt(msg), @@ -404,7 +423,8 @@ data class MessageEntity( flags: List = listOf(MessageFlag.SEEN) ): MessageEntity { return MessageEntity( - email = email, + account = email, + accountType = "accountType",//need to fix it. Don't merge folder = label, uid = uid, sentDate = System.currentTimeMillis(), diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/CreateOutgoingMessageViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/CreateOutgoingMessageViewModel.kt index e6f0cd426b..5220db2e8d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/CreateOutgoingMessageViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/CreateOutgoingMessageViewModel.kt @@ -67,7 +67,7 @@ class CreateOutgoingMessageViewModel( ?: throw IllegalStateException("No active account") val replyTo = outgoingMessageInfo.replyToMessageEntityId?.let { - roomDatabase.msgDao().getMsgById(it)?.replyTo + roomDatabase.msgDao().getMsgById(it)?.replyToAddresses } messageEntity = outgoingMessageInfo.toMessageEntity( folder = JavaEmailConstants.FOLDER_OUTBOX, @@ -117,7 +117,7 @@ class CreateOutgoingMessageViewModel( DownloadForwardedAttachmentsWorker.enqueue(context) } else { val existingMsgEntity = roomDatabase.msgDao().getMsg( - messageEntity.email, messageEntity.folder, messageEntity.uid + messageEntity.account, messageEntity.folder, messageEntity.uid ) ?: throw IllegalStateException("A message is not exist") if (outgoingMessageInfo.encryptionType == MessageEncryptionType.ENCRYPTED && outgoingMessageInfo.isPasswordProtected == true diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt index 24853ea7f9..cd65cf9c44 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt @@ -254,10 +254,10 @@ class DraftViewModel( ) val messageEntityWithoutStateChange = draftMessageEntity.copy( subject = outgoingMessageInfo.subject, - fromAddress = InternetAddress.toString(arrayOf(outgoingMessageInfo.from)), - replyTo = InternetAddress.toString(arrayOf(outgoingMessageInfo.from)), - toAddress = InternetAddress.toString(outgoingMessageInfo.toRecipients?.toTypedArray()), - ccAddress = InternetAddress.toString(outgoingMessageInfo.ccRecipients?.toTypedArray()), + fromAddresses = InternetAddress.toString(arrayOf(outgoingMessageInfo.from)), + replyToAddresses = InternetAddress.toString(arrayOf(outgoingMessageInfo.from)), + toAddresses = InternetAddress.toString(outgoingMessageInfo.toRecipients?.toTypedArray()), + ccAddresses = InternetAddress.toString(outgoingMessageInfo.ccRecipients?.toTypedArray()), sentDate = mimeMessage.sentDate?.time, receivedDate = mimeMessage.sentDate?.time ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index dbd1636b59..c2c41b9797 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -265,7 +265,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application var needUpdateOutboxLabel = false for (entity in entities) { val isMsgDeleted = with(entity) { - roomDatabase.msgDao().deleteOutgoingMsg(email, folder, uid) > 0 + roomDatabase.msgDao().deleteOutgoingMsg(account, folder, uid) > 0 } if (isMsgDeleted) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index 4e7c140289..0e73560660 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -125,7 +125,7 @@ class MsgDetailsViewModel( if (it.isNotEmpty()) { emit( roomDatabase.msgDao().getMsgSuspend( - account = messageEntity.email, + account = messageEntity.account, folder = messageEntity.folder, uid = messageEntity.uid ) @@ -139,7 +139,7 @@ class MsgDetailsViewModel( liveData { emit( roomDatabase.msgDao().getMsgSuspend( - account = messageEntity.email, + account = messageEntity.account, folder = messageEntity.folder, uid = messageEntity.uid ) @@ -278,7 +278,7 @@ class MsgDetailsViewModel( @OptIn(ExperimentalCoroutinesApi::class) private val separatedAttachmentsFlow = roomDatabase.attachmentDao().getAttachmentsFlow( - account = messageEntity.email, + account = messageEntity.account, label = messageEntity.folder, uid = messageEntity.uid ).mapLatest { list -> @@ -486,7 +486,7 @@ class MsgDetailsViewModel( val accountEntity = getActiveAccountSuspend() ?: return@launch if (JavaEmailConstants.FOLDER_OUTBOX.equals(localFolder.fullName, ignoreCase = true)) { - val outgoingMsgCount = roomDatabase.msgDao().getOutboxMsgsSuspend(msgEntity.email).size + val outgoingMsgCount = roomDatabase.msgDao().getOutboxMsgsSuspend(msgEntity.account).size val outboxLabel = roomDatabase.labelDao().getLabelSuspend( account = accountEntity.email, accountType = accountEntity.accountType, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt index e2ff0b9beb..cc289a165d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt @@ -336,7 +336,7 @@ class HandlePasswordProtectedMsgWorker(context: Context, params: WorkerParameter private suspend fun getAttachments(msgEntity: MessageEntity) = roomDatabase.attachmentDao().getAttachmentsSuspend( - account = msgEntity.email, + account = msgEntity.account, label = JavaEmailConstants.FOLDER_OUTBOX, uid = msgEntity.uid ).map { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt index 417ee456bd..9c5e989715 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt @@ -672,7 +672,7 @@ class MessageDetailsFragment : BaseFragment(), Pr private fun showSendersPublicKeyDialog() { showChoosePublicKeyDialogFragment( requestKey = REQUEST_KEY_CHOOSE_PUBLIC_KEY + args.messageEntity.id?.toString(), - email = args.messageEntity.email, + email = args.messageEntity.account, choiceMode = ListView.CHOICE_MODE_SINGLE, titleResourceId = R.plurals.tell_sender_to_update_their_settings ) @@ -981,7 +981,7 @@ class MessageDetailsFragment : BaseFragment(), Pr private fun prepareToText(): String { val stringBuilder = SpannableStringBuilder() val meAddress = args.messageEntity.to.firstOrNull { - it.address.equals(args.messageEntity.email, true) + it.address.equals(args.messageEntity.account, true) } val leftAddresses: List if (meAddress == null) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt index faf00c2fe6..aa0a7d6c12 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt @@ -29,7 +29,6 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.appcompat.widget.SearchView -import androidx.appcompat.widget.SwitchCompat import androidx.core.content.ContextCompat import androidx.core.view.MenuHost import androidx.core.view.MenuProvider @@ -38,7 +37,6 @@ import androidx.fragment.app.viewModels import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.navigation.NavDirections -import androidx.preference.PreferenceManager import androidx.recyclerview.selection.SelectionTracker import androidx.recyclerview.selection.StorageStrategy import androidx.recyclerview.widget.DividerItemDecoration @@ -46,7 +44,6 @@ import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.swiperefreshlayout.widget.SwipeRefreshLayout -import com.flowcrypt.email.Constants import com.flowcrypt.email.NavGraphDirections import com.flowcrypt.email.R import com.flowcrypt.email.api.email.FoldersManager @@ -92,13 +89,11 @@ import com.flowcrypt.email.ui.activity.fragment.base.ListProgressBehaviour import com.flowcrypt.email.ui.activity.fragment.dialog.ChangeGmailLabelsDialogFragmentArgs import com.flowcrypt.email.ui.activity.fragment.dialog.InfoDialogFragment import com.flowcrypt.email.ui.activity.fragment.dialog.TwoWayDialogFragment -import com.flowcrypt.email.ui.activity.fragment.preferences.NotificationsSettingsFragment import com.flowcrypt.email.ui.adapter.MsgsPagedListAdapter import com.flowcrypt.email.ui.adapter.selection.CustomStableIdKeyProvider import com.flowcrypt.email.ui.adapter.selection.MsgItemDetailsLookup import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.OutgoingMessagesManager -import com.flowcrypt.email.util.SharedPreferencesHelper import com.flowcrypt.email.util.exception.CommonConnectionException import com.google.android.gms.auth.UserRecoverableAuthException import com.google.android.material.snackbar.Snackbar @@ -512,14 +507,14 @@ class MessagesListFragment : BaseFragment(), ListPr MessageState.ERROR_PRIVATE_KEY_NOT_FOUND -> { val errorMsg = messageEntity.errorMsg - message = if (errorMsg?.equals(messageEntity.email, ignoreCase = true) == true) { + message = if (errorMsg?.equals(messageEntity.account, ignoreCase = true) == true) { getString( R.string.no_key_available_for_your_email_account, getString(R.string.support_email) ) } else { getString( - R.string.no_key_available_for_your_emails, errorMsg, messageEntity.email, + R.string.no_key_available_for_your_emails, errorMsg, messageEntity.account, getString(R.string.support_email) ) } diff --git a/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/EmailUtilTest.kt b/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/EmailUtilTest.kt index cfbde263ca..3df7cd1eed 100644 --- a/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/EmailUtilTest.kt +++ b/FlowCrypt/src/test/java/com/flowcrypt/email/api/email/EmailUtilTest.kt @@ -101,10 +101,11 @@ class EmailUtilTest { val receivedDate = Instant.now() val incomingMessageInfo = IncomingMessageInfo( msgEntity = MessageEntity( - email = accountEntity.email, + account = accountEntity.email, + accountType = accountEntity.email,//need to fix, Don't merge folder = "INBOX", uid = 123, - fromAddress = InternetAddress.toString(replyToMIME.from), + fromAddresses = InternetAddress.toString(replyToMIME.from), receivedDate = receivedDate.toEpochMilli() ), localFolder = LocalFolder( From a6138218299ea6df59d1d371189c060cccedd614 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 11:42:34 +0300 Subject: [PATCH 004/237] wip --- .../email/database/FlowCryptRoomDatabase.kt | 2 +- .../email/database/dao/AttachmentDao.kt | 8 +- .../email/database/dao/MessageDao.kt | 99 ++++++++++--------- 3 files changed, 55 insertions(+), 54 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt index 76087e5572..032b8e60a0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/FlowCryptRoomDatabase.kt @@ -16,6 +16,7 @@ import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import androidx.sqlite.db.SupportSQLiteDatabase +import com.flowcrypt.account.database.dao.MessageDao import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.database.converters.ClientConfigurationConverter import com.flowcrypt.email.database.converters.LabelListVisibilityConverter @@ -27,7 +28,6 @@ import com.flowcrypt.email.database.dao.ActionQueueDao import com.flowcrypt.email.database.dao.AttachmentDao import com.flowcrypt.email.database.dao.KeysDao import com.flowcrypt.email.database.dao.LabelDao -import com.flowcrypt.email.database.dao.MessageDao import com.flowcrypt.email.database.dao.PubKeyDao import com.flowcrypt.email.database.dao.RecipientDao import com.flowcrypt.email.database.entity.AccountAliasesEntity diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt index 4240dde6b3..fde50f400f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt @@ -16,8 +16,8 @@ import kotlinx.coroutines.flow.Flow @Dao interface AttachmentDao : BaseDao { - @Query("DELETE FROM attachment WHERE account = :email AND folder = :label") - suspend fun deleteAtt(email: String?, label: String?): Int + @Query("DELETE FROM attachment WHERE account = :account AND folder = :label") + suspend fun deleteAtt(account: String?, label: String?): Int @Query("SELECT * FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") fun getAttachments(account: String, label: String, uid: Long): List @@ -38,6 +38,6 @@ interface AttachmentDao : BaseDao { @Query("DELETE FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") suspend fun deleteAttSuspend(account: String, label: String, uid: Long): Int - @Query("DELETE FROM attachment WHERE account = :email") - suspend fun deleteByEmailSuspend(email: String?): Int + @Query("DELETE FROM attachment WHERE account = :account") + suspend fun deleteByEmailSuspend(account: String?): Int } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt index f810de1ae4..1a5660bf3c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt @@ -3,7 +3,7 @@ * Contributors: DenBond7 */ -package com.flowcrypt.email.database.dao +package com.flowcrypt.account.database.dao import android.content.Context import androidx.lifecycle.LiveData @@ -14,6 +14,7 @@ import androidx.room.Transaction import com.flowcrypt.email.api.email.FoldersManager import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.database.MessageState +import com.flowcrypt.email.database.dao.BaseDao import com.flowcrypt.email.database.dao.BaseDao.Companion.doOperationViaSteps import com.flowcrypt.email.database.dao.BaseDao.Companion.doOperationViaStepsSuspend import com.flowcrypt.email.database.dao.BaseDao.Companion.getEntitiesViaStepsSuspend @@ -132,21 +133,21 @@ abstract class MessageDao : BaseDao { @Query("SELECT * FROM messages WHERE _id = :id") abstract fun getMsgLiveDataById(id: Long): LiveData - @Query("DELETE FROM messages WHERE account = :email AND folder = :label") - abstract suspend fun delete(email: String?, label: String?): Int + @Query("DELETE FROM messages WHERE account = :account AND folder = :label") + abstract suspend fun delete(account: String?, label: String?): Int - @Query("DELETE FROM messages WHERE account = :email AND folder NOT IN (:labels)") + @Query("DELETE FROM messages WHERE account = :account AND folder NOT IN (:labels)") abstract suspend fun deleteAllExceptRelatedToLabels( - email: String?, + account: String?, labels: Collection ): Int - @Query("DELETE FROM messages WHERE account = :email AND folder = :label AND uid IN (:msgsUID)") - abstract fun delete(email: String?, label: String?, msgsUID: Collection): Int + @Query("DELETE FROM messages WHERE account = :account AND folder = :label AND uid IN (:msgsUID)") + abstract fun delete(account: String?, label: String?, msgsUID: Collection): Int - @Query("DELETE FROM messages WHERE account = :email AND folder = :label AND uid IN (:msgsUID)") + @Query("DELETE FROM messages WHERE account = :account AND folder = :label AND uid IN (:msgsUID)") abstract suspend fun deleteSuspendByUIDs( - email: String?, + account: String?, label: String?, msgsUID: Collection ): Int @@ -194,11 +195,11 @@ abstract class MessageDao : BaseDao { ): List @Query( - "DELETE FROM messages WHERE account = :email " + + "DELETE FROM messages WHERE account = :account " + "AND folder = :label AND uid = :uid AND (state NOT IN (:msgStates) OR state IS NULL)" ) abstract suspend fun deleteOutgoingMsg( - email: String?, label: String?, uid: Long?, + account: String?, label: String?, uid: Long?, msgStates: Collection = listOf( MessageState.SENDING.value, MessageState.SENT_WITHOUT_LOCAL_COPY.value, @@ -255,7 +256,7 @@ abstract class MessageDao : BaseDao { /** * Get the list of UID of all messages in the database which were not checked to encryption. * - * @param account The user email. + * @param account The user account. * @param label The label name. * @return The list of UID of selected messages in the database for some label. */ @@ -268,7 +269,7 @@ abstract class MessageDao : BaseDao { /** * Get a list of UID and flags of all unseen messages in the database for some label. * - * @param account The user email. + * @param account The user account. * @param label The label name. * @return The list of UID and flags of all unseen messages in the database for some label. */ @@ -281,7 +282,7 @@ abstract class MessageDao : BaseDao { /** * Switch [MessageState] for messages of the given folder of the given account * - * @param account The email that the message linked. + * @param account The account that the message linked. * @param label The folder label. * @param oldValue The old value. * @param newValues The new value. @@ -321,7 +322,7 @@ abstract class MessageDao : BaseDao { /** * Add the messages which have a current state equal [MessageState.SENDING] to the sending queue again. * - * @param account The email that the message linked + * @param account The account that the message linked */ @Query( "UPDATE messages SET state=2 " + @@ -336,7 +337,7 @@ abstract class MessageDao : BaseDao { /** * Add the messages which have a current state equal [MessageState.SENDING] to the sending queue again. * - * @param account The email that the message linked + * @param account The account that the message linked */ @Query( "UPDATE messages SET state=2 " + @@ -393,55 +394,55 @@ abstract class MessageDao : BaseDao { @Query("UPDATE messages SET is_new = 0 WHERE account = :account AND folder = :label AND is_new = 1") abstract suspend fun markMsgsAsOld(account: String?, label: String?): Int - @Query("DELETE FROM messages WHERE account = :email") - abstract suspend fun deleteByEmailSuspend(email: String?): Int + @Query("DELETE FROM messages WHERE account = :account") + abstract suspend fun deleteByEmailSuspend(account: String?): Int @Query("SELECT COUNT(*) FROM messages WHERE account = :account AND folder = :label") abstract suspend fun getMsgsCount(account: String, label: String): Int @Transaction - open fun deleteByUIDs(email: String?, label: String?, msgsUID: Collection) { + open fun deleteByUIDs(account: String?, label: String?, msgsUID: Collection) { doOperationViaSteps(list = ArrayList(msgsUID)) { stepUIDs: Collection -> - delete(email, label, stepUIDs) + delete(account, label, stepUIDs) } } @Transaction - open suspend fun deleteByUIDsSuspend(email: String?, label: String?, msgsUID: Collection) = + open suspend fun deleteByUIDsSuspend(account: String?, label: String?, msgsUID: Collection) = withContext(Dispatchers.IO) { doOperationViaStepsSuspend(list = ArrayList(msgsUID)) { stepUIDs: Collection -> - deleteSuspendByUIDs(email, label, stepUIDs) + deleteSuspendByUIDs(account, label, stepUIDs) } } @Transaction - open fun updateFlags(email: String?, label: String?, flagsMap: Map) { + open fun updateFlags(account: String?, label: String?, flagsMap: Map) { doOperationViaSteps(list = ArrayList(flagsMap.keys)) { stepUIDs: Collection -> - updateFlagsByUIDs(email, label, flagsMap, stepUIDs) + updateFlagsByUIDs(account, label, flagsMap, stepUIDs) } } @Transaction - open suspend fun updateFlagsSuspend(email: String?, label: String?, flagsMap: Map) = + open suspend fun updateFlagsSuspend(account: String?, label: String?, flagsMap: Map) = withContext(Dispatchers.IO) { doOperationViaStepsSuspend(list = ArrayList(flagsMap.keys)) { stepUIDs: Collection -> - updateFlagsByUIDsSuspend(email, label, flagsMap, stepUIDs) + updateFlagsByUIDsSuspend(account, label, flagsMap, stepUIDs) } } /** * Update the message flags in the local database. * - * @param email The email that the message linked. + * @param account The account that the message linked. * @param label The folder label. * @param uid The message UID. * @param flags The message flags. */ @Transaction - open suspend fun updateLocalMsgFlags(email: String?, label: String?, uid: Long, flags: Flags) = + open suspend fun updateLocalMsgFlags(account: String?, label: String?, uid: Long, flags: Flags) = withContext(Dispatchers.IO) { val msgEntity = - getMsgSuspend(account = email, folder = label, uid = uid) ?: return@withContext + getMsgSuspend(account = account, folder = label, uid = uid) ?: return@withContext val modifiedMsgEntity = if (flags.contains(Flags.Flag.SEEN)) { msgEntity.copy(flags = flags.toString().uppercase(), isNew = false) } else { @@ -452,11 +453,11 @@ abstract class MessageDao : BaseDao { @Transaction open suspend fun updateEncryptionStates( - email: String?, + account: String?, label: String?, flagsMap: Map ) = withContext(Dispatchers.IO) { - val msgEntities = getMsgsByUidsSuspend(account = email, folder = label, msgsUID = flagsMap.keys) + val msgEntities = getMsgsByUidsSuspend(account = account, folder = label, msgsUID = flagsMap.keys) val modifiedMsgEntities = ArrayList() for (msgEntity in msgEntities) { @@ -472,65 +473,65 @@ abstract class MessageDao : BaseDao { /** * Get a map of UID and flags of all messages in the database for some label. * - * @param email The user email. + * @param account The user account. * @param label The label name. * @return The map of UID and flags of all messages in the database for some label. */ @Transaction - open fun getMapOfUIDAndMsgFlags(email: String, label: String): Map { - return getUIDAndFlagsPairs(email, label).associate { it.uid to it.flags } + open fun getMapOfUIDAndMsgFlags(account: String, label: String): Map { + return getUIDAndFlagsPairs(account, label).associate { it.uid to it.flags } } /** * Get a map of UID and flags of all messages in the database for some label. * - * @param email The user email. + * @param account The user account. * @param label The label name. * @return The map of UID and flags of all messages in the database for some label. */ @Transaction - open suspend fun getMapOfUIDAndMsgFlagsSuspend(email: String, label: String): Map = + open suspend fun getMapOfUIDAndMsgFlagsSuspend(account: String, label: String): Map = withContext(Dispatchers.IO) { - return@withContext getUIDAndFlagsPairsSuspend(email, label).associate { it.uid to it.flags } + return@withContext getUIDAndFlagsPairsSuspend(account, label).associate { it.uid to it.flags } } /** * Mark messages as old in the local database. * - * @param email The email that the message linked. + * @param account The account that the message linked. * @param label The folder label. * @param uidList The list of the UIDs. */ @Transaction - open suspend fun setOldStatus(email: String?, label: String?, uidList: List) = + open suspend fun setOldStatus(account: String?, label: String?, uidList: List) = withContext(Dispatchers.IO) { doOperationViaStepsSuspend(list = uidList) { stepUIDs: Collection -> - markMsgsAsOld(email, label, stepUIDs) + markMsgsAsOld(account, label, stepUIDs) } } open suspend fun getMsgsByUIDs( - email: String, + account: String, label: String, uidList: List ): List = withContext(Dispatchers.IO) { return@withContext getEntitiesViaStepsSuspend(list = uidList) { stepUIDs: Collection -> - getMsgsByUids(email, label, stepUIDs) + getMsgsByUids(account, label, stepUIDs) } } open suspend fun updateGmailLabels( - email: String?, + account: String?, label: String?, labelsToBeUpdatedMap: Map ) = withContext(Dispatchers.IO) { - if (email == null || label == null) { + if (account == null || label == null) { return@withContext } val messagesToBeUpdated = getMsgsByUIDs( - email = email, + account = account, label = label, uidList = labelsToBeUpdatedMap.keys.toList() ).map { entity -> @@ -555,10 +556,10 @@ abstract class MessageDao : BaseDao { } private suspend fun updateFlagsByUIDsSuspend( - email: String?, label: String?, flagsMap: Map, + account: String?, label: String?, flagsMap: Map, uids: Collection? ): Int = withContext(Dispatchers.IO) { - val msgEntities = getMsgsByUidsSuspend(account = email, folder = label, msgsUID = uids) + val msgEntities = getMsgsByUidsSuspend(account = account, folder = label, msgsUID = uids) val modifiedMsgEntities = ArrayList() for (msgEntity in msgEntities) { @@ -577,10 +578,10 @@ abstract class MessageDao : BaseDao { } private fun updateFlagsByUIDs( - email: String?, label: String?, flagsMap: Map, + account: String?, label: String?, flagsMap: Map, uids: Collection? ): Int { - val msgEntities = getMsgsByUids(account = email, folder = label, msgsUID = uids) + val msgEntities = getMsgsByUids(account = account, folder = label, msgsUID = uids) val modifiedMsgEntities = ArrayList() for (msgEntity in msgEntities) { From 4622ff22ca5772a38dab04e645417861f66d5519 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 11:49:11 +0300 Subject: [PATCH 005/237] wip --- .../api/email/model/OutgoingMessageInfo.kt | 3 +- .../email/database/dao/AttachmentDao.kt | 37 ++++++++++--------- .../email/database/entity/AccountEntity.kt | 6 +-- .../jetpack/viewmodel/MsgDetailsViewModel.kt | 1 + .../DownloadForwardedAttachmentsWorker.kt | 8 ++-- .../HandlePasswordProtectedMsgWorker.kt | 3 +- .../workmanager/MessagesSenderWorker.kt | 20 +++++++--- .../email/ui/activity/MainActivity.kt | 5 --- .../fragment/CreateMessageFragment.kt | 3 +- 9 files changed, 50 insertions(+), 36 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt index 419740eef4..74a0b36231 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/model/OutgoingMessageInfo.kt @@ -25,6 +25,7 @@ import java.util.UUID @Parcelize data class OutgoingMessageInfo( @Expose val account: String? = null, + @Expose val accountType: String? = null, @Expose val subject: String? = null, @Expose val msg: String? = null, @Expose val toRecipients: List? = null, @@ -121,7 +122,7 @@ data class OutgoingMessageInfo( val timestamp = System.currentTimeMillis() return MessageEntity( account = requireNotNull(account), - accountType = requireNotNull(account),//need to fix it. Don't merge + accountType = requireNotNull(accountType), folder = folder, uid = uid, receivedDate = timestamp, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt index fde50f400f..aa42f970b8 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt @@ -16,28 +16,31 @@ import kotlinx.coroutines.flow.Flow @Dao interface AttachmentDao : BaseDao { - @Query("DELETE FROM attachment WHERE account = :account AND folder = :label") - suspend fun deleteAtt(account: String?, label: String?): Int - - @Query("SELECT * FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") - fun getAttachments(account: String, label: String, uid: Long): List - - @Query("SELECT * FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") + @Query( + "SELECT * FROM attachment WHERE account = :account " + + "AND account_type = :accountType AND folder = :label AND uid = :uid" + ) suspend fun getAttachmentsSuspend( account: String, + accountType: String, label: String, uid: Long ): List - @Query("SELECT * FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") - fun getAttachmentsFlow(account: String, label: String, uid: Long): Flow> - - @Query("DELETE FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") - fun deleteAtt(account: String, label: String, uid: Long): Int - - @Query("DELETE FROM attachment WHERE account = :account AND folder = :label AND uid = :uid") - suspend fun deleteAttSuspend(account: String, label: String, uid: Long): Int + @Query( + "SELECT * FROM attachment WHERE account = :account " + + "AND account_type = :accountType AND folder = :label AND uid = :uid" + ) + fun getAttachmentsFlow( + account: String, + accountType: String, + label: String, + uid: Long + ): Flow> - @Query("DELETE FROM attachment WHERE account = :account") - suspend fun deleteByEmailSuspend(account: String?): Int + @Query( + "DELETE FROM attachment WHERE account = :account " + + "AND account_type = :accountType AND folder = :label AND uid = :uid" + ) + suspend fun deleteAttSuspend(account: String, accountType: String, label: String, uid: Long): Int } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt index fea49dc4f9..855f76b1f7 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt @@ -40,7 +40,7 @@ data class AccountEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = BaseColumns._ID) val id: Long? = null, val email: String, - @ColumnInfo(name = "account_type", defaultValue = "NULL") val accountType: String? = null, + @ColumnInfo(name = "account_type", defaultValue = "NULL") val accountType: String, @ColumnInfo(name = "display_name", defaultValue = "NULL") val displayName: String? = null, @ColumnInfo(name = "given_name", defaultValue = "NULL") val givenName: String? = null, @ColumnInfo(name = "family_name", defaultValue = "NULL") val familyName: String? = null, @@ -109,7 +109,7 @@ data class AccountEntity( useStartTlsForSmtp: Boolean = false, ) : this( email = requireNotNull(googleSignInAccount.email).lowercase(), - accountType = googleSignInAccount.account?.type?.lowercase(), + accountType = requireNotNull(googleSignInAccount.account?.type?.lowercase()),//need to fix, don't merge displayName = googleSignInAccount.displayName, givenName = googleSignInAccount.givenName, familyName = googleSignInAccount.familyName, @@ -183,7 +183,7 @@ data class AccountEntity( constructor(email: String) : this( email = email, - accountType = null, + accountType = "null",//need to fix, don't merge displayName = null, givenName = null, familyName = null, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index 0e73560660..1cf9687ae9 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -279,6 +279,7 @@ class MsgDetailsViewModel( @OptIn(ExperimentalCoroutinesApi::class) private val separatedAttachmentsFlow = roomDatabase.attachmentDao().getAttachmentsFlow( account = messageEntity.account, + accountType = messageEntity.accountType, label = messageEntity.folder, uid = messageEntity.uid ).mapLatest { list -> diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt index 843b41fed9..2227b651ec 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt @@ -28,12 +28,12 @@ import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.LogsUtil import com.flowcrypt.email.util.exception.ExceptionUtil import com.google.android.gms.common.util.CollectionUtils -import org.eclipse.angus.mail.imap.IMAPFolder import jakarta.mail.Folder import jakarta.mail.Store import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.apache.commons.io.FileUtils +import org.eclipse.angus.mail.imap.IMAPFolder import java.io.File import java.io.FileOutputStream import java.io.InputStream @@ -123,8 +123,10 @@ class DownloadForwardedAttachmentsWorker(context: Context, params: WorkerParamet val msgAttsDir = File(attCacheDir, msgEntity.attachmentsDirectory!!) try { val atts = roomDatabase.attachmentDao().getAttachmentsSuspend( - account.email, - JavaEmailConstants.FOLDER_OUTBOX, msgEntity.uid + account = account.email, + accountType = account.accountType, + label = JavaEmailConstants.FOLDER_OUTBOX, + uid = msgEntity.uid ).filter { it.isForwarded } if (atts.isEmpty()) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt index cc289a165d..99fe532ecd 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt @@ -32,7 +32,6 @@ import com.flowcrypt.email.util.LogsUtil import com.flowcrypt.email.util.OutgoingMessagesManager import com.flowcrypt.email.util.exception.ExceptionUtil import com.google.gson.GsonBuilder -import org.eclipse.angus.mail.util.MailConnectException import jakarta.activation.DataHandler import jakarta.mail.Address import jakarta.mail.Message @@ -48,6 +47,7 @@ import kotlinx.coroutines.withContext import org.apache.commons.io.FilenameUtils import org.bouncycastle.openpgp.PGPPublicKeyRingCollection import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.eclipse.angus.mail.util.MailConnectException import org.pgpainless.util.Passphrase import java.io.ByteArrayOutputStream import java.io.FileNotFoundException @@ -337,6 +337,7 @@ class HandlePasswordProtectedMsgWorker(context: Context, params: WorkerParameter private suspend fun getAttachments(msgEntity: MessageEntity) = roomDatabase.attachmentDao().getAttachmentsSuspend( account = msgEntity.account, + accountType = msgEntity.accountType, label = JavaEmailConstants.FOLDER_OUTBOX, uid = msgEntity.uid ).map { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt index a8ab77081c..e944921e54 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt @@ -45,8 +45,6 @@ import com.google.android.gms.auth.UserRecoverableAuthException import com.google.android.gms.common.util.CollectionUtils import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException import com.google.api.client.http.FileContent -import org.eclipse.angus.mail.imap.IMAPFolder -import org.eclipse.angus.mail.util.MailConnectException import jakarta.mail.AuthenticationFailedException import jakarta.mail.Flags import jakarta.mail.Folder @@ -58,6 +56,8 @@ import jakarta.mail.internet.MimeMessage import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPFolder +import org.eclipse.angus.mail.util.MailConnectException import java.io.File import java.io.FileNotFoundException import java.io.FileOutputStream @@ -254,7 +254,12 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : delay(2000) val attachments = roomDatabase.attachmentDao() - .getAttachmentsSuspend(email, JavaEmailConstants.FOLDER_OUTBOX, msgEntity.uid) + .getAttachmentsSuspend( + account = email, + accountType = account.accountType, + label = JavaEmailConstants.FOLDER_OUTBOX, + uid = msgEntity.uid + ) val isMsgSent = sendMsg(account, msgEntity, attachments, sess, store) if (!isMsgSent) { @@ -354,8 +359,12 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : } val msgEntity = list.first() try { - val attachments = roomDatabase.attachmentDao() - .getAttachmentsSuspend(email, JavaEmailConstants.FOLDER_OUTBOX, msgEntity.uid) + val attachments = roomDatabase.attachmentDao().getAttachmentsSuspend( + account = email, + accountType = account.accountType, + label = JavaEmailConstants.FOLDER_OUTBOX, + uid = msgEntity.uid + ) val mimeMsg = EmailUtil.createMimeMsg(applicationContext, sess, msgEntity, attachments) @@ -426,6 +435,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : withContext(Dispatchers.IO) { FlowCryptRoomDatabase.getDatabase(applicationContext).attachmentDao().deleteAttSuspend( account = account.email, + accountType = account.accountType, label = JavaEmailConstants.FOLDER_OUTBOX, uid = details.uid ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt index b771f6905c..793e723e95 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/MainActivity.kt @@ -446,11 +446,6 @@ class MainActivity : BaseActivity() { roomDatabase.accountDao().logout(accountEntity) removeAccountFromAccountManager(accountEntity) - //todo-denbond7 Improve this via onDelete = ForeignKey.CASCADE - //remove all info about the given account from the local db - roomDatabase.msgDao().deleteByEmailSuspend(accountEntity.email) - roomDatabase.attachmentDao().deleteByEmailSuspend(accountEntity.email) - val newActiveAccount = roomDatabase.accountDao().getActiveAccountSuspend() if (newActiveAccount == null) { roomDatabase.recipientDao().deleteAll() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt index 1f35e5ce74..3595310c23 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/CreateMessageFragment.kt @@ -518,7 +518,8 @@ class CreateMessageFragment : BaseFragment(), super.onAccountInfoRefreshed(accountEntity) composeMsgViewModel.updateOutgoingMessageInfo( composeMsgViewModel.outgoingMessageInfoStateFlow.value.copy( - account = accountEntity?.email + account = accountEntity?.email, + accountType = accountEntity?.accountType ) ) From 0982660b9ab132db12599a124405a56c7fa59bb1 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 12:12:29 +0300 Subject: [PATCH 006/237] wip --- .../java/com/flowcrypt/email/database/entity/AccountEntity.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt index 855f76b1f7..69f59483f2 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt @@ -40,7 +40,7 @@ data class AccountEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = BaseColumns._ID) val id: Long? = null, val email: String, - @ColumnInfo(name = "account_type", defaultValue = "NULL") val accountType: String, + @ColumnInfo(name = "account_type") val accountType: String, @ColumnInfo(name = "display_name", defaultValue = "NULL") val displayName: String? = null, @ColumnInfo(name = "given_name", defaultValue = "NULL") val givenName: String? = null, @ColumnInfo(name = "family_name", defaultValue = "NULL") val familyName: String? = null, @@ -312,7 +312,7 @@ data class AccountEntity( override fun hashCode(): Int { var result = id?.hashCode() ?: 0 result = 31 * result + email.hashCode() - result = 31 * result + (accountType?.hashCode() ?: 0) + result = 31 * result + accountType.hashCode() result = 31 * result + (displayName?.hashCode() ?: 0) result = 31 * result + (givenName?.hashCode() ?: 0) result = 31 * result + (familyName?.hashCode() ?: 0) From a9a7de29339b6a2e2942aa19d06e9f379bda723a Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 12:16:42 +0300 Subject: [PATCH 007/237] wip --- .../java/com/flowcrypt/email/database/dao/AttachmentDao.kt | 4 ++-- .../workmanager/DownloadForwardedAttachmentsWorker.kt | 2 +- .../jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt | 2 +- .../email/jetpack/workmanager/MessagesSenderWorker.kt | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt index aa42f970b8..7a31d0ddff 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/AttachmentDao.kt @@ -20,7 +20,7 @@ interface AttachmentDao : BaseDao { "SELECT * FROM attachment WHERE account = :account " + "AND account_type = :accountType AND folder = :label AND uid = :uid" ) - suspend fun getAttachmentsSuspend( + suspend fun getAttachments( account: String, accountType: String, label: String, @@ -42,5 +42,5 @@ interface AttachmentDao : BaseDao { "DELETE FROM attachment WHERE account = :account " + "AND account_type = :accountType AND folder = :label AND uid = :uid" ) - suspend fun deleteAttSuspend(account: String, accountType: String, label: String, uid: Long): Int + suspend fun deleteAttachments(account: String, accountType: String, label: String, uid: Long): Int } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt index 2227b651ec..3e89411849 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt @@ -122,7 +122,7 @@ class DownloadForwardedAttachmentsWorker(context: Context, params: WorkerParamet val msgEntity = detailsList[0] val msgAttsDir = File(attCacheDir, msgEntity.attachmentsDirectory!!) try { - val atts = roomDatabase.attachmentDao().getAttachmentsSuspend( + val atts = roomDatabase.attachmentDao().getAttachments( account = account.email, accountType = account.accountType, label = JavaEmailConstants.FOLDER_OUTBOX, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt index 99fe532ecd..93be459e71 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/HandlePasswordProtectedMsgWorker.kt @@ -335,7 +335,7 @@ class HandlePasswordProtectedMsgWorker(context: Context, params: WorkerParameter } private suspend fun getAttachments(msgEntity: MessageEntity) = - roomDatabase.attachmentDao().getAttachmentsSuspend( + roomDatabase.attachmentDao().getAttachments( account = msgEntity.account, accountType = msgEntity.accountType, label = JavaEmailConstants.FOLDER_OUTBOX, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt index e944921e54..c04b7b56fe 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt @@ -254,7 +254,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : delay(2000) val attachments = roomDatabase.attachmentDao() - .getAttachmentsSuspend( + .getAttachments( account = email, accountType = account.accountType, label = JavaEmailConstants.FOLDER_OUTBOX, @@ -359,7 +359,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : } val msgEntity = list.first() try { - val attachments = roomDatabase.attachmentDao().getAttachmentsSuspend( + val attachments = roomDatabase.attachmentDao().getAttachments( account = email, accountType = account.accountType, label = JavaEmailConstants.FOLDER_OUTBOX, @@ -433,7 +433,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : details: MessageEntity ) = withContext(Dispatchers.IO) { - FlowCryptRoomDatabase.getDatabase(applicationContext).attachmentDao().deleteAttSuspend( + FlowCryptRoomDatabase.getDatabase(applicationContext).attachmentDao().deleteAttachments( account = account.email, accountType = account.accountType, label = JavaEmailConstants.FOLDER_OUTBOX, From a28d48ba24f9d0ac575f7193beaaf96d6934c14e Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 12:18:14 +0300 Subject: [PATCH 008/237] wip --- .../flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt index c04b7b56fe..4da8da17c1 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt @@ -433,7 +433,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : details: MessageEntity ) = withContext(Dispatchers.IO) { - FlowCryptRoomDatabase.getDatabase(applicationContext).attachmentDao().deleteAttachments( + roomDatabase.attachmentDao().deleteAttachments( account = account.email, accountType = account.accountType, label = JavaEmailConstants.FOLDER_OUTBOX, From 95bddde8a8cb4039d6e0da954ae4a27a6d6a4455 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 16:44:37 +0300 Subject: [PATCH 009/237] wip --- .../email/database/entity/AccountEntity.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt index 69f59483f2..2e959588ad 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt @@ -14,6 +14,7 @@ import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey import com.flowcrypt.email.R +import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.email.gmail.GmailConstants import com.flowcrypt.email.api.email.model.AuthCredentials @@ -87,9 +88,7 @@ data class AccountEntity( @IgnoredOnParcel @Ignore - val account: Account = Account(this.email.ifEmpty { "unknown" }, - accountType ?: this.email.substring(this.email.indexOf('@') + 1).lowercase() - .ifEmpty { "unknown" }) + val account: Account = Account(this.email.ifEmpty { ACCOUNT_TYPE_UNKNOWN }, accountType) @IgnoredOnParcel @Ignore @@ -109,7 +108,9 @@ data class AccountEntity( useStartTlsForSmtp: Boolean = false, ) : this( email = requireNotNull(googleSignInAccount.email).lowercase(), - accountType = requireNotNull(googleSignInAccount.account?.type?.lowercase()),//need to fix, don't merge + accountType = googleSignInAccount.account?.type?.lowercase() ?: EmailUtil.getDomain( + requireNotNull(googleSignInAccount.email) + ).ifEmpty { ACCOUNT_TYPE_UNKNOWN }, displayName = googleSignInAccount.displayName, givenName = googleSignInAccount.givenName, familyName = googleSignInAccount.familyName, @@ -148,8 +149,7 @@ data class AccountEntity( constructor(authCredentials: AuthCredentials, clientConfiguration: ClientConfiguration? = null) : this( email = authCredentials.email.lowercase(), - accountType = - authCredentials.email.substring(authCredentials.email.indexOf('@') + 1).lowercase(), + accountType = EmailUtil.getDomain(authCredentials.email).ifEmpty { ACCOUNT_TYPE_UNKNOWN }, displayName = authCredentials.displayName, givenName = null, familyName = null, @@ -183,7 +183,7 @@ data class AccountEntity( constructor(email: String) : this( email = email, - accountType = "null",//need to fix, don't merge + accountType = EmailUtil.getDomain(email).ifEmpty { ACCOUNT_TYPE_UNKNOWN }, displayName = null, givenName = null, familyName = null, @@ -350,5 +350,6 @@ data class AccountEntity( const val TABLE_NAME = "accounts" const val ACCOUNT_TYPE_GOOGLE = "com.google" const val ACCOUNT_TYPE_OUTLOOK = "outlook.com" + const val ACCOUNT_TYPE_UNKNOWN = "unknown" } } From 213fc4030984b5710e6a2953cc65a687ebca8a13 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 17:07:14 +0300 Subject: [PATCH 010/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 5 +++-- .../email/database/entity/AttachmentEntity.kt | 4 ++-- .../email/jetpack/viewmodel/LabelsViewModel.kt | 9 +++------ .../email/jetpack/viewmodel/MessagesViewModel.kt | 5 +++-- .../email/jetpack/viewmodel/MsgDetailsViewModel.kt | 8 +++++--- .../service/ProcessingOutgoingMessageInfoHelper.kt | 13 +++++++++++-- 6 files changed, 27 insertions(+), 17 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 4a14f1730a..0e4ff5b15d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -638,11 +638,12 @@ class GmailApiHelper { if (msg.uid in savedMsgUIDsSet) { attachments.addAll(getAttsInfoFromMessagePart(msg.payload).mapNotNull { attachmentInfo -> AttachmentEntity.fromAttInfo( - attachmentInfo.copy( + attachmentInfo = attachmentInfo.copy( email = account.email, folder = localFolder.fullName, uid = msg.uid - ) + ), + accountType = account.accountType ) }) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt index 7e0c8fe0a5..8e0f70a52b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AttachmentEntity.kt @@ -81,7 +81,7 @@ data class AttachmentEntity( companion object { const val TABLE_NAME = "attachment" - fun fromAttInfo(attachmentInfo: AttachmentInfo): AttachmentEntity? { + fun fromAttInfo(attachmentInfo: AttachmentInfo, accountType: String): AttachmentEntity? { with(attachmentInfo) { val email = email ?: return null val folder = folder ?: return null @@ -90,7 +90,7 @@ data class AttachmentEntity( return AttachmentEntity( id = null, account = email, - accountType = "accountType",//need to think here + accountType = accountType, folder = folder, uid = uid, name = name, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LabelsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LabelsViewModel.kt index 6fa9053539..75d301fc83 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LabelsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/LabelsViewModel.kt @@ -17,7 +17,6 @@ import com.flowcrypt.email.api.email.IMAPStoreManager import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.api.retrofit.response.base.Result -import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.LabelEntity import com.flowcrypt.email.jetpack.workmanager.sync.UpdateLabelsWorker import kotlinx.coroutines.launch @@ -32,11 +31,9 @@ class LabelsViewModel(application: Application) : AccountViewModel(application) val foldersManagerLiveData: LiveData = labelsLiveData.switchMap { liveData { - val foldersManager = activeAccountLiveData.value?.let { account -> - FoldersManager.build(account, it) - } ?: FoldersManager(AccountEntity("")) - - emit(foldersManager) + activeAccountLiveData.value?.let { account -> + emit(FoldersManager.build(account, it)) + } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index c2c41b9797..efb2092e97 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -593,11 +593,12 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val uid = remoteFolder.getUID(msg) attachments.addAll(EmailUtil.getAttsInfoFromPart(msg).mapNotNull { attachmentInfo -> AttachmentEntity.fromAttInfo( - attachmentInfo.copy( + attachmentInfo = attachmentInfo.copy( email = account.email, folder = localFolder.fullName, uid = uid - ) + ), + accountType = account.accountType ) }) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index 1cf9687ae9..ee93bd5f0f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -912,7 +912,8 @@ class MsgDetailsViewModel( JavaEmailConstants.FOLDER_SEARCH }, uid = msgUid - ) + ), + accountType = accountEntity.accountType ) } @@ -936,11 +937,12 @@ class MsgDetailsViewModel( val attachments = GmailApiHelper.getAttsInfoFromMessagePart(msg.payload).mapNotNull { attachmentInfo -> AttachmentEntity.fromAttInfo( - attachmentInfo.copy( + attachmentInfo = attachmentInfo.copy( email = accountEntity.email, folder = localFolder.fullName, uid = msg.uid - ) + ), + accountType = accountEntity.accountType ) } FlowCryptRoomDatabase.getDatabase(getApplication()).attachmentDao() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/ProcessingOutgoingMessageInfoHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/ProcessingOutgoingMessageInfoHelper.kt index 808655315e..afe1ab8776 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/ProcessingOutgoingMessageInfoHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/ProcessingOutgoingMessageInfoHelper.kt @@ -14,6 +14,7 @@ import com.flowcrypt.email.api.email.model.AttachmentInfo import com.flowcrypt.email.api.email.model.OutgoingMessageInfo import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.dao.BaseDao +import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.database.entity.RecipientEntity @@ -73,7 +74,12 @@ object ProcessingOutgoingMessageInfoHelper { } } - addAttsToCache(context, outgoingMsgInfo, msgAttsCacheDir) + addAttsToCache( + context = context, + accountEntity = accountEntity, + outgoingMsgInfo = outgoingMsgInfo, + attsCacheDir = msgAttsCacheDir + ) } afterMimeMessageCreatingAction.invoke(mimeMessage) @@ -91,6 +97,7 @@ object ProcessingOutgoingMessageInfoHelper { private fun addAttsToCache( context: Context, + accountEntity: AccountEntity, outgoingMsgInfo: OutgoingMessageInfo, attsCacheDir: File ) { @@ -178,7 +185,9 @@ object ProcessingOutgoingMessageInfoHelper { } } - roomDatabase.attachmentDao().insert(cachedAtts.mapNotNull { AttachmentEntity.fromAttInfo(it) }) + roomDatabase.attachmentDao().insert(cachedAtts.mapNotNull { + AttachmentEntity.fromAttInfo(attachmentInfo = it, accountType = accountEntity.accountType) + }) } /** From 133a7b95e9c4584c7050d7493583f948f8d5d5c2 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 17:17:26 +0300 Subject: [PATCH 011/237] wip --- .../com/flowcrypt/email/database/entity/MessageEntity.kt | 7 ++++--- .../flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt | 3 ++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index 6990c7c031..9dac64071c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -416,15 +416,16 @@ data class MessageEntity( } fun genMsgEntity( - email: String, + account: String, + accountType: String, label: String, uid: Long, info: OutgoingMessageInfo, flags: List = listOf(MessageFlag.SEEN) ): MessageEntity { return MessageEntity( - account = email, - accountType = "accountType",//need to fix it. Don't merge + account = account, + accountType = accountType, folder = label, uid = uid, sentDate = System.currentTimeMillis(), diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt index cd65cf9c44..054ab17ed9 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt @@ -286,7 +286,8 @@ class DraftViewModel( val folderDrafts = foldersManager.folderDrafts ?: throw IllegalStateException("Drafts folder is undefined") val newDraftMessageEntity = MessageEntity.genMsgEntity( - email = accountEntity.email, + account = accountEntity.email, + accountType = accountEntity.accountType, label = folderDrafts.fullName, uid = System.currentTimeMillis(), info = outgoingMessageInfo, From e253b1ed1fab5f48ee124120e8e9a383b348e2d3 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 1 Aug 2024 17:26:06 +0300 Subject: [PATCH 012/237] wip --- .../email/database/entity/MessageEntity.kt | 21 ++++++----- .../jetpack/viewmodel/MessagesViewModel.kt | 36 +++++++++---------- .../workmanager/sync/BaseIdleWorker.kt | 3 +- .../workmanager/sync/InboxIdleSyncWorker.kt | 6 ++-- .../workmanager/sync/SyncDraftsWorker.kt | 3 +- 5 files changed, 36 insertions(+), 33 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index 9dac64071c..5b93c5855c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -238,7 +238,8 @@ data class MessageEntity( fun genMessageEntities( context: Context, - email: String, + account: String, + accountType: String, label: String, folder: IMAPFolder, msgs: Array?, @@ -281,7 +282,8 @@ data class MessageEntity( messageEntities.add( genMsgEntity( - email = email, + account = account, + accountType = accountType, label = label, msg = msg, uid = folder.getUID(msg), @@ -302,7 +304,8 @@ data class MessageEntity( fun genMessageEntities( context: Context, - email: String, + account: String, + accountType:String, label: String, msgsList: List, isNew: Boolean, @@ -344,7 +347,8 @@ data class MessageEntity( val mimeMessage = GmaiAPIMimeMessage(Session.getInstance(Properties()), msg) messageEntities.add( genMsgEntity( - email = email, + account = account, + accountType = accountType, label = label, msg = mimeMessage, uid = msg.uid, @@ -372,7 +376,7 @@ data class MessageEntity( * Prepare the content values for insert to the database. This method must be called in the * non-UI thread. * - * @param email The email that the message linked. + * @param account The email that the message linked. * @param label The folder label. * @param msg The message which will be added to the database. * @param uid The message UID. @@ -382,7 +386,8 @@ data class MessageEntity( * [Message] object */ fun genMsgEntity( - email: String, + account: String, + accountType: String, label: String, msg: Message, uid: Long, @@ -392,8 +397,8 @@ data class MessageEntity( hasAttachments: Boolean? = null ): MessageEntity { return MessageEntity( - account = email, - accountType = "accountType",//need to fix it. Don't merge + account = account, + accountType = accountType, folder = label, uid = uid, receivedDate = msg.receivedDate?.time, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index efb2092e97..067844db62 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -535,7 +535,8 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val isOnlyPgpModeEnabled = account.showOnlyEncrypted ?: false val msgEntities = MessageEntity.genMessageEntities( context = getApplication(), - email = email, + account = email, + accountType = account.accountType, label = folder, msgsList = msgs, isNew = false, @@ -559,14 +560,12 @@ class MessagesViewModel(application: Application) : AccountViewModel(application msgs: Array = emptyArray(), hasPgpAfterAdditionalSearchSet: Set = emptySet() ) = withContext(Dispatchers.IO) { - val email = account.email - val folder = localFolder.fullName - val isOnlyPgpModeEnabled = account.showOnlyEncrypted ?: false val msgEntities = MessageEntity.genMessageEntities( context = getApplication(), - email = email, - label = folder, + account = account.email, + accountType = account.accountType, + label = localFolder.fullName, folder = remoteFolder, msgs = msgs, isNew = false, @@ -785,18 +784,15 @@ class MessagesViewModel(application: Application) : AccountViewModel(application msgs: Array = emptyArray(), hasPgpAfterAdditionalSearchSet: Set = emptySet() ) = withContext(Dispatchers.IO) { - val email = account.email - val isOnlyPgpModeEnabled = account.showOnlyEncrypted ?: false - val searchLabel = JavaEmailConstants.FOLDER_SEARCH - val msgEntities = MessageEntity.genMessageEntities( context = getApplication(), - email = email, - label = searchLabel, + account = account.email, + accountType = account.accountType, + label = JavaEmailConstants.FOLDER_SEARCH, folder = remoteFolder, msgs = msgs, isNew = false, - isOnlyPgpModeEnabled = isOnlyPgpModeEnabled, + isOnlyPgpModeEnabled = account.showOnlyEncrypted ?: false, hasPgpAfterAdditionalSearchSet = hasPgpAfterAdditionalSearchSet ) @@ -809,14 +805,12 @@ class MessagesViewModel(application: Application) : AccountViewModel(application account: AccountEntity, localFolder: LocalFolder, msgs: List ) = withContext(Dispatchers.IO) { - val email = account.email - val label = localFolder.fullName - val isOnlyPgpModeEnabled = account.showOnlyEncrypted ?: false val msgEntities = MessageEntity.genMessageEntities( context = getApplication(), - email = email, - label = label, + account = account.email, + accountType = account.accountType, + label = localFolder.fullName, msgsList = msgs, isNew = false, onlyPgpModeEnabled = isOnlyPgpModeEnabled @@ -973,7 +967,8 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val msgEntities = MessageEntity.genMessageEntities( context = getApplication(), - email = email, + account = email, + accountType = accountEntity.accountType, label = folderName, folder = remoteFolder, msgs = newCandidates, @@ -1040,7 +1035,8 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val msgEntities = MessageEntity.genMessageEntities( context = getApplication(), - email = accountEntity.email, + account = accountEntity.email, + accountType = accountEntity.accountType, label = localFolder.fullName, msgsList = msgs, isNew = isNew, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt index 097a4ef23b..dcbca2e079 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt @@ -91,7 +91,8 @@ abstract class BaseIdleWorker(context: Context, params: WorkerParameters) : val msgEntities = MessageEntity.genMessageEntities( context = applicationContext, - email = accountEntity.email, + account = accountEntity.email, + accountType = accountEntity.accountType, label = localFolder.fullName, folder = remoteFolder, msgs = newMsgs, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt index 68234938d2..23af6814ce 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt @@ -19,13 +19,13 @@ import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasPgp import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.exception.GmailAPIException import com.google.api.services.gmail.model.History -import org.eclipse.angus.mail.imap.IMAPFolder import jakarta.mail.FetchProfile import jakarta.mail.Folder import jakarta.mail.Store import jakarta.mail.UIDFolder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPFolder import java.math.BigInteger import java.net.HttpURLConnection @@ -161,7 +161,6 @@ open class InboxIdleSyncWorker(context: Context, params: WorkerParameters) : newCandidatesMap, updateCandidatesMap, labelsToBeUpdatedMap -> - val email = accountEntity.email processDeletedMsgs(accountEntity, localFolder.fullName, deleteCandidatesUIDs) val newCandidates = newCandidatesMap.values.toList() @@ -180,7 +179,8 @@ open class InboxIdleSyncWorker(context: Context, params: WorkerParameters) : val msgEntities = MessageEntity.genMessageEntities( context = applicationContext, - email = email, + account = accountEntity.email, + accountType = accountEntity.accountType, label = localFolder.fullName, msgsList = msgs, isNew = isNew, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/SyncDraftsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/SyncDraftsWorker.kt index b038099324..9648f4fa9f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/SyncDraftsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/SyncDraftsWorker.kt @@ -62,7 +62,8 @@ class SyncDraftsWorker(context: Context, params: WorkerParameters) : val msgEntities = MessageEntity.genMessageEntities( context = applicationContext, - email = accountEntity.email, + account = accountEntity.email, + accountType = accountEntity.accountType, label = folderDrafts.fullName, msgsList = msgs, isNew = false, From f3360922bdb7b2a86b812b120899423d4d60056a Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 5 Aug 2024 11:40:59 +0300 Subject: [PATCH 013/237] wip --- .../java/com/flowcrypt/email/database/entity/MessageEntity.kt | 3 +++ FlowCrypt/src/main/res/layout/messages_list_item.xml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index 5b93c5855c..c62bffc89a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -99,6 +99,7 @@ data class MessageEntity( @ColumnInfo(name = "label_ids", defaultValue = "NULL") val labelIds: String? = null, @ColumnInfo(name = "is_encrypted", defaultValue = "-1") val isEncrypted: Boolean? = null, @ColumnInfo(name = "has_pgp", defaultValue = "0") val hasPgp: Boolean? = null, + @ColumnInfo(name = "thread_messages_count", defaultValue = "NULL") val threadMessagesCount: Int? = null, ) : Parcelable { @IgnoredOnParcel @@ -199,6 +200,7 @@ data class MessageEntity( if (labelIds != other.labelIds) return false if (isEncrypted != other.isEncrypted) return false if (hasPgp != other.hasPgp) return false + if (threadMessagesCount != other.threadMessagesCount) return false return true } @@ -229,6 +231,7 @@ data class MessageEntity( result = 31 * result + (labelIds?.hashCode() ?: 0) result = 31 * result + (isEncrypted?.hashCode() ?: 0) result = 31 * result + (hasPgp?.hashCode() ?: 0) + result = 31 * result + (threadMessagesCount?.hashCode() ?: 0) return result } diff --git a/FlowCrypt/src/main/res/layout/messages_list_item.xml b/FlowCrypt/src/main/res/layout/messages_list_item.xml index f87679f545..5525cf3f14 100644 --- a/FlowCrypt/src/main/res/layout/messages_list_item.xml +++ b/FlowCrypt/src/main/res/layout/messages_list_item.xml @@ -29,7 +29,7 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="@dimen/default_margin_content_small" - android:ellipsize="end" + android:ellipsize="middle" android:maxLines="1" android:textColor="?attr/itemTitleColor" android:textSize="@dimen/default_text_size_big" From b2d33fb09803d616510b9f78830309a9d502f886 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 5 Aug 2024 15:06:16 +0300 Subject: [PATCH 014/237] Added a base implementation of loading threads.| #74 --- .../email/api/email/gmail/GmailApiHelper.kt | 111 ++++++++++++++++++ .../api/email/gmail/model/GmailThreadInfo.kt | 21 ++++ .../email/database/entity/AccountEntity.kt | 6 +- .../jetpack/viewmodel/MessagesViewModel.kt | 68 +++++++---- 4 files changed, 184 insertions(+), 22 deletions(-) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 0e4ff5b15d..2b10e53be0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -15,6 +15,7 @@ import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.email.gmail.api.GMailRawAttachmentFilterInputStream import com.flowcrypt.email.api.email.gmail.api.GMailRawMIMEMessageFilterInputStream +import com.flowcrypt.email.api.email.gmail.model.GmailThreadInfo import com.flowcrypt.email.api.email.model.AttachmentInfo import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.api.retrofit.response.base.Result @@ -52,6 +53,7 @@ import com.google.api.services.gmail.model.Draft import com.google.api.services.gmail.model.History import com.google.api.services.gmail.model.Label import com.google.api.services.gmail.model.ListMessagesResponse +import com.google.api.services.gmail.model.ListThreadsResponse import com.google.api.services.gmail.model.Message import com.google.api.services.gmail.model.MessagePart import jakarta.mail.Flags @@ -245,6 +247,38 @@ class GmailApiHelper { return@withContext Base64InputStream(GMailRawMIMEMessageFilterInputStream(message.executeAsInputStream())) } + suspend fun loadThreads( + context: Context, + accountEntity: AccountEntity, + localFolder: LocalFolder, + maxResult: Long = COUNT_OF_LOADED_EMAILS_BY_STEP, + fields: List? = null, + nextPageToken: String? = null + ): ListThreadsResponse = withContext(Dispatchers.IO) { + val gmailApiService = generateGmailApiService(context, accountEntity) + val request = gmailApiService + .users() + .threads() + .list(DEFAULT_USER_ID) + .setPageToken(nextPageToken) + .setMaxResults(maxResult) + + fields?.let { fields -> + request.fields = fields.joinToString(separator = ",") + } + + if (!localFolder.isAll) { + request.labelIds = listOf(localFolder.fullName) + } + + if (accountEntity.showOnlyEncrypted == true) { + request.q = + (EmailUtil.genPgpThingsSearchTerm(accountEntity) as? GmailRawSearchTerm)?.pattern + } + + return@withContext request.execute() + } + suspend fun loadMsgsBaseInfo( context: Context, accountEntity: AccountEntity, @@ -305,6 +339,83 @@ class GmailApiHelper { } } + suspend fun loadGmailThreadInfoInParallel( + context: Context, + accountEntity: AccountEntity, + threads: List, + localFolder: LocalFolder, + format: String = MESSAGE_RESPONSE_FORMAT_FULL, + stepValue: Int = 10 + ): List = withContext(Dispatchers.IO) + { + return@withContext useParallel(list = threads, stepValue = stepValue) { list -> + loadGmailThreadInfo(context, accountEntity, list, localFolder, format) + } + } + + suspend fun loadGmailThreadInfo( + context: Context, + accountEntity: AccountEntity, + threads: Collection, + localFolder: LocalFolder, + format: String = MESSAGE_RESPONSE_FORMAT_FULL, + metadataHeaders: List? = null, + fields: List? = FULL_INFO_WITHOUT_DATA + ): List = withContext(Dispatchers.IO) + { + val gmailApiService = generateGmailApiService(context, accountEntity) + val batch = gmailApiService.batch() + + val listResult = mutableListOf() + val isTrash = localFolder.fullName.equals(LABEL_TRASH, true) + + for (thread in threads) { + val request = gmailApiService + .users() + .threads() + .get(DEFAULT_USER_ID, thread.id) + .setFormat(format) + + metadataHeaders?.let { metadataHeaders -> + request.metadataHeaders = metadataHeaders + } + + request.queue( + batch, + object : JsonBatchCallback() { + override fun onSuccess( + t: com.google.api.services.gmail.model.Thread?, + responseHeaders: HttpHeaders? + ) { + t?.let { thread -> + thread.messages?.lastOrNull()?.let { + if (isTrash || it.labelIds?.contains(LABEL_TRASH) != true) { + listResult.add( + GmailThreadInfo( + id = thread.id, + lastMessage = it, + messagesCount = thread.messages?.size ?: 0, + recipients = emptyList(), + subject = it.snippet, + labels = "" + ) + ) + } + } + } ?: throw java.lang.NullPointerException() + } + + override fun onFailure(e: GoogleJsonError?, responseHeaders: HttpHeaders?) { + + } + }) + } + + batch.execute() + + return@withContext listResult + } + suspend fun loadMsgs( context: Context, accountEntity: AccountEntity, messages: Collection, localFolder: LocalFolder, format: String = MESSAGE_RESPONSE_FORMAT_FULL, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt new file mode 100644 index 0000000000..96c9c5821c --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt @@ -0,0 +1,21 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.api.email.gmail.model + +import com.google.api.services.gmail.model.Message +import jakarta.mail.internet.InternetAddress + +/** + * @author Denys Bondarenko + */ +data class GmailThreadInfo( + val id: String, + val lastMessage: Message, + val messagesCount: Int, + val recipients: List, + val subject: String, + val labels: String +) \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt index 2e959588ad..1a4aee8a62 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/AccountEntity.kt @@ -84,6 +84,7 @@ data class AccountEntity( @ColumnInfo(name = "service_pgp_private_key") val servicePgpPrivateKey: ByteArray, @ColumnInfo(name = "signature", defaultValue = "NULL") val signature: String? = null, @ColumnInfo(name = "use_alias_signatures", defaultValue = "0") val useAliasSignatures: Boolean = false, + @ColumnInfo(name = "use_conversation_mode", defaultValue = "0") val useConversationMode: Boolean = false, ) : Parcelable { @IgnoredOnParcel @@ -143,7 +144,8 @@ data class AccountEntity( useCustomerFesUrl = useCustomerFesUrl, servicePgpPassphrase = "", servicePgpPrivateKey = byteArrayOf(), - useAliasSignatures = true + useAliasSignatures = true, + useConversationMode = true ) constructor(authCredentials: AuthCredentials, clientConfiguration: ClientConfiguration? = null) : @@ -305,6 +307,7 @@ data class AccountEntity( if (!servicePgpPrivateKey.contentEquals(other.servicePgpPrivateKey)) return false if (signature != other.signature) return false if (useAliasSignatures != other.useAliasSignatures) return false + if (useConversationMode != other.useConversationMode) return false return true } @@ -343,6 +346,7 @@ data class AccountEntity( result = 31 * result + servicePgpPrivateKey.contentHashCode() result = 31 * result + (signature?.hashCode() ?: 0) result = 31 * result + useAliasSignatures.hashCode() + result = 31 * result + useConversationMode.hashCode() return result } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index 067844db62..7275fd032a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -461,31 +461,57 @@ class MessagesViewModel(application: Application) : AccountViewModel(application resultCode = R.id.progress_id_gmail_list ) ) - val messagesBaseInfo = GmailApiHelper.loadMsgsBaseInfo( - context = getApplication(), - accountEntity = accountEntity, - localFolder = localFolder, - nextPageToken = if (totalItemsCount > 0) labelEntity?.nextPageToken else null - ) - val draftIdsMap = when (messagesBaseInfo) { - is ListDraftsResponse -> messagesBaseInfo.drafts?.associateBy( - { it.message.id }, - { it.id }) ?: emptyMap() + val messages: List + val nextPageToken: String? + val draftIdsMap: Map - else -> emptyMap() - } + if (accountEntity.useConversationMode) { + val threadsResponse = GmailApiHelper.loadThreads( + context = getApplication(), + accountEntity = accountEntity, + localFolder = localFolder, + nextPageToken = if (totalItemsCount > 0) labelEntity?.nextPageToken else null + ) - val messages = when (messagesBaseInfo) { - is ListMessagesResponse -> messagesBaseInfo.messages ?: emptyList() - is ListDraftsResponse -> messagesBaseInfo.drafts?.map { it.message } ?: emptyList() - else -> emptyList() - } + val gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( + context = getApplication(), + accountEntity = accountEntity, + threads = threadsResponse.threads, + format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_MINIMAL, + localFolder = localFolder + ) - val nextPageToken = when (messagesBaseInfo) { - is ListMessagesResponse -> messagesBaseInfo.nextPageToken - is ListDraftsResponse -> messagesBaseInfo.nextPageToken - else -> null + messages = gmailThreadInfoList.map { it.lastMessage } + nextPageToken = threadsResponse.nextPageToken + draftIdsMap = emptyMap()//todo-denbond7 fix me + } else{ + val messagesBaseInfo = GmailApiHelper.loadMsgsBaseInfo( + context = getApplication(), + accountEntity = accountEntity, + localFolder = localFolder, + nextPageToken = if (totalItemsCount > 0) labelEntity?.nextPageToken else null + ) + + draftIdsMap = when (messagesBaseInfo) { + is ListDraftsResponse -> messagesBaseInfo.drafts?.associateBy( + { it.message.id }, + { it.id }) ?: emptyMap() + + else -> emptyMap() + } + + messages = when (messagesBaseInfo) { + is ListMessagesResponse -> messagesBaseInfo.messages ?: emptyList() + is ListDraftsResponse -> messagesBaseInfo.drafts?.map { it.message } ?: emptyList() + else -> emptyList() + } + + nextPageToken = when (messagesBaseInfo) { + is ListMessagesResponse -> messagesBaseInfo.nextPageToken + is ListDraftsResponse -> messagesBaseInfo.nextPageToken + else -> null + } } loadMsgsFromRemoteServerLiveData.postValue( From 43a752f50cfa1fae7a38b80cf1c5e79cde4ca61b Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 5 Aug 2024 16:11:32 +0300 Subject: [PATCH 015/237] Added showing thread messages count + correct labels + all recipients in a thread.| #74 --- .../email/api/email/gmail/GmailApiHelper.kt | 8 ++- .../api/email/gmail/model/GmailThreadInfo.kt | 4 +- .../email/database/entity/MessageEntity.kt | 37 +++++++------ .../api/services/gmail/model/ThreadExt.kt | 41 ++++++++++++++ .../jetpack/viewmodel/MessagesViewModel.kt | 55 +++++++++++++++---- .../email/ui/adapter/MsgsPagedListAdapter.kt | 38 ++++++++++--- .../flowcrypt/email/util/AvatarGenerator.kt | 3 +- 7 files changed, 148 insertions(+), 38 deletions(-) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 2b10e53be0..284ed65746 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -23,6 +23,7 @@ import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getUniqueRecipients import com.flowcrypt.email.extensions.contentId import com.flowcrypt.email.extensions.disposition import com.flowcrypt.email.extensions.isMimeType @@ -118,6 +119,7 @@ class GmailApiHelper { const val MESSAGE_RESPONSE_FORMAT_RAW = "raw" const val MESSAGE_RESPONSE_FORMAT_FULL = "full" const val MESSAGE_RESPONSE_FORMAT_MINIMAL = "minimal" + const val THREAD_RESPONSE_FORMAT_METADATA = "metadata" private val FULL_INFO_WITHOUT_DATA = listOf( "id", @@ -395,9 +397,11 @@ class GmailApiHelper { id = thread.id, lastMessage = it, messagesCount = thread.messages?.size ?: 0, - recipients = emptyList(), + recipients = thread.getUniqueRecipients(), subject = it.snippet, - labels = "" + labels = thread.messages.flatMap { message -> + message.labelIds ?: emptyList() + }.toSortedSet() ) ) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt index 96c9c5821c..9b08b9a7dd 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt @@ -17,5 +17,7 @@ data class GmailThreadInfo( val messagesCount: Int, val recipients: List, val subject: String, - val labels: String + val labels: Set, + val hasAttachments: Boolean = false, + val hasPgpThings: Boolean = false, ) \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index c62bffc89a..2df10f0a66 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -313,7 +313,11 @@ data class MessageEntity( msgsList: List, isNew: Boolean, onlyPgpModeEnabled: Boolean, - draftIdsMap: Map = emptyMap() + draftIdsMap: Map = emptyMap(), + messageModificationAction: ( + message: com.google.api.services.gmail.model.Message, + messageEntity: MessageEntity + ) -> MessageEntity = { _, messageEntity -> messageEntity } ): List { val messageEntities = mutableListOf() val isNotificationDisabled = NotificationsSettingsFragment.NOTIFICATION_LEVEL_NEVER == @@ -348,22 +352,23 @@ data class MessageEntity( } val mimeMessage = GmaiAPIMimeMessage(Session.getInstance(Properties()), msg) + val messageEntityTemplate = genMsgEntity( + account = account, + accountType = accountType, + label = label, + msg = mimeMessage, + uid = msg.uid, + isNew = isNewTemp, + hasPgp = hasPgp, + hasAttachments = GmailApiHelper.getAttsInfoFromMessagePart(msg.payload).isNotEmpty() + ).copy( + threadId = msg.threadId, + historyId = msg.historyId.toString(), + draftId = draftIdsMap[msg.id], + labelIds = msg.labelIds?.joinToString(separator = LABEL_IDS_SEPARATOR) + ) messageEntities.add( - genMsgEntity( - account = account, - accountType = accountType, - label = label, - msg = mimeMessage, - uid = msg.uid, - isNew = isNewTemp, - hasPgp = hasPgp, - hasAttachments = GmailApiHelper.getAttsInfoFromMessagePart(msg.payload).isNotEmpty() - ).copy( - threadId = msg.threadId, - historyId = msg.historyId.toString(), - draftId = draftIdsMap[msg.id], - labelIds = msg.labelIds?.joinToString(separator = LABEL_IDS_SEPARATOR) - ) + messageModificationAction.invoke(msg, messageEntityTemplate) ) } catch (e: MessageRemovedException) { e.printStackTrace() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt new file mode 100644 index 0000000000..87200ac8ba --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt @@ -0,0 +1,41 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.extensions.com.google.api.services.gmail.model + +import com.flowcrypt.email.extensions.kotlin.asInternetAddresses +import com.google.api.services.gmail.model.Thread +import jakarta.mail.internet.InternetAddress + +/** + * @author Denys Bondarenko + */ +fun Thread.getUniqueRecipients(): List { + return mutableListOf().apply { + val filteredHeaders = messages?.flatMap { message -> + message?.payload?.headers?.filter { header -> + //need to check this line and test + header.name in listOf( + "From", + "To" + )//maybe need to add Cc. need to check + } ?: emptyList() + } + + val mapOfUniqueRecipients = mutableMapOf() + filteredHeaders?.forEach { header -> + header.value.asInternetAddresses().forEach { internetAddress -> + val address = internetAddress.address.lowercase() + + if (!mapOfUniqueRecipients.contains(address) + || mapOfUniqueRecipients[address].isNullOrEmpty() + ) { + add(internetAddress) + mapOfUniqueRecipients[address] = internetAddress.personal + } + } + } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index 7275fd032a..03929ffcef 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -23,6 +23,7 @@ import com.flowcrypt.email.api.email.IMAPStoreManager import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.gmail.api.GmaiAPIMimeMessage +import com.flowcrypt.email.api.email.gmail.model.GmailThreadInfo import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.api.email.model.MessageFlag import com.flowcrypt.email.api.retrofit.response.base.Result @@ -32,6 +33,7 @@ import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.LabelEntity import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.database.entity.MessageEntity.Companion.LABEL_IDS_SEPARATOR import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasPgp import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.jetpack.workmanager.EmailAndNameWorker @@ -417,7 +419,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application ) ) if (end < 1) { - handleReceivedMsgs(accountEntity, localFolder, imapFolder) + handleReceivedMessages(accountEntity, localFolder, imapFolder) } else { val msgs: Array = if (isOnlyPgpModeEnabled == true) { foundMsgs.copyOfRange(start - 1, end) @@ -433,7 +435,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application imapFolder.search(EmailUtil.genPgpThingsSearchTerm(accountEntity), msgs) .map { imapFolder.getUID(it) }.toSet() - handleReceivedMsgs( + handleReceivedMessages( account = accountEntity, localFolder = localFolder, remoteFolder = folder, @@ -465,6 +467,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val messages: List val nextPageToken: String? val draftIdsMap: Map + val gmailThreadInfoList: List if (accountEntity.useConversationMode) { val threadsResponse = GmailApiHelper.loadThreads( @@ -474,18 +477,19 @@ class MessagesViewModel(application: Application) : AccountViewModel(application nextPageToken = if (totalItemsCount > 0) labelEntity?.nextPageToken else null ) - val gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( + gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( context = getApplication(), accountEntity = accountEntity, - threads = threadsResponse.threads, - format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_MINIMAL, + threads = threadsResponse.threads ?: emptyList(), + format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_FULL, localFolder = localFolder ) messages = gmailThreadInfoList.map { it.lastMessage } nextPageToken = threadsResponse.nextPageToken draftIdsMap = emptyMap()//todo-denbond7 fix me - } else{ + } else { + gmailThreadInfoList = emptyList() val messagesBaseInfo = GmailApiHelper.loadMsgsBaseInfo( context = getApplication(), accountEntity = accountEntity, @@ -531,7 +535,30 @@ class MessagesViewModel(application: Application) : AccountViewModel(application resultCode = R.id.progress_id_gmail_msgs_info ) ) - handleReceivedMsgs(accountEntity, localFolder, msgs, draftIdsMap) + handleReceivedMessages( + accountEntity, + localFolder, + msgs, + draftIdsMap + ) { message, messageEntity -> + if (accountEntity.useConversationMode) { + val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } + if (thread != null) { + messageEntity.copy( + threadMessagesCount = thread.messagesCount, + labelIds = thread.labels.joinToString(separator = LABEL_IDS_SEPARATOR), + hasAttachments = thread.hasAttachments, + fromAddresses = InternetAddress.toString(thread.recipients.toTypedArray()), + toAddresses = InternetAddress.toString(thread.recipients.toTypedArray()), + hasPgp = thread.hasPgpThings + ) + } else { + messageEntity + } + } else { + messageEntity + } + } } else { loadMsgsFromRemoteServerLiveData.postValue( Result.loading( @@ -549,11 +576,15 @@ class MessagesViewModel(application: Application) : AccountViewModel(application return@withContext Result.success(true) } - private suspend fun handleReceivedMsgs( + private suspend fun handleReceivedMessages( account: AccountEntity, localFolder: LocalFolder, msgs: List, - draftIdsMap: Map = emptyMap() + draftIdsMap: Map = emptyMap(), + messageEntityModificationAction: ( + message: com.google.api.services.gmail.model.Message, + messageEntity: MessageEntity + ) -> MessageEntity ) = withContext(Dispatchers.IO) { val email = account.email val folder = localFolder.fullName @@ -568,7 +599,9 @@ class MessagesViewModel(application: Application) : AccountViewModel(application isNew = false, onlyPgpModeEnabled = isOnlyPgpModeEnabled, draftIdsMap = draftIdsMap - ) + ) { message, messageEntity -> + messageEntityModificationAction.invoke(message, messageEntity) + } roomDatabase.msgDao().insertWithReplaceSuspend(msgEntities) GmailApiHelper.identifyAttachments(msgEntities, msgs, account, localFolder, roomDatabase) @@ -579,7 +612,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application ) } - private suspend fun handleReceivedMsgs( + private suspend fun handleReceivedMessages( account: AccountEntity, localFolder: LocalFolder, remoteFolder: IMAPFolder, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt index d0bb4a968a..bb99c878f9 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt @@ -10,6 +10,7 @@ import android.graphics.Camera import android.graphics.Color import android.graphics.Typeface import android.text.SpannableString +import android.text.SpannableStringBuilder import android.text.Spanned import android.text.TextUtils import android.text.style.AbsoluteSizeSpan @@ -303,16 +304,39 @@ class MsgsPagedListAdapter(private val onMessageClickListener: OnMessageClickLis folderType: FoldersManager.FolderType?, messageEntity: MessageEntity, context: Context - ) = when (folderType) { - FoldersManager.FolderType.SENT -> generateAddresses(messageEntity.to) + ): CharSequence { + val addresses = when (folderType) { + FoldersManager.FolderType.SENT -> generateAddresses(messageEntity.to) - FoldersManager.FolderType.DRAFTS -> generateAddresses(messageEntity.to).ifEmpty { - context.getString(R.string.no_recipients) - } + FoldersManager.FolderType.DRAFTS -> generateAddresses(messageEntity.to).ifEmpty { + context.getString(R.string.no_recipients) + } - FoldersManager.FolderType.OUTBOX -> generateOutboxStatus(context, messageEntity.msgState) + FoldersManager.FolderType.OUTBOX -> generateOutboxStatus(context, messageEntity.msgState) - else -> generateAddresses(messageEntity.from) + else -> generateAddresses(messageEntity.from) + } + + return if ((messageEntity.threadMessagesCount ?: 0) > 1) { + SpannableStringBuilder(addresses).apply { + val spannableStringForThreadMessageCount = SpannableString( + "(${messageEntity.threadMessagesCount})" + ).apply { + val textSize = + context.resources.getDimensionPixelSize(R.dimen.default_text_size_small) + setSpan( + AbsoluteSizeSpan(textSize), + 0, + length, + Spanned.SPAN_INCLUSIVE_INCLUSIVE + ) + } + append(" ") + append(spannableStringForThreadMessageCount) + } + } else { + addresses + } } private fun changeStatusView(messageEntity: MessageEntity) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/AvatarGenerator.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/AvatarGenerator.kt index 23df3f0d44..4a77e7c2a6 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/util/AvatarGenerator.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/AvatarGenerator.kt @@ -59,7 +59,8 @@ object AvatarGenerator { //draw a text textPaint.textSize = fontSize canvas.drawText( - text.substring(0, 2).uppercase(), (bitmapWidth / 2).toFloat(), + text.takeIf { it.length > 1 }?.substring(0, 2)?.uppercase() ?: text, + (bitmapWidth / 2).toFloat(), bitmapHeight / 2 - (textPaint.descent() + textPaint.ascent()) / 2, textPaint ) From 6e466a2cc6aae5f647e96afb0dc47aba4ef2b94e Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 7 Aug 2024 10:28:24 +0300 Subject: [PATCH 016/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 12 ++-- .../api/services/gmail/model/ThreadExt.kt | 30 ++++---- .../email/ui/adapter/MsgsPagedListAdapter.kt | 71 +++++++++++++------ 3 files changed, 73 insertions(+), 40 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 284ed65746..9be650eeed 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -390,15 +390,17 @@ class GmailApiHelper { responseHeaders: HttpHeaders? ) { t?.let { thread -> - thread.messages?.lastOrNull()?.let { - if (isTrash || it.labelIds?.contains(LABEL_TRASH) != true) { + thread.messages?.lastOrNull()?.let { lastMessageInThread -> + if (isTrash || lastMessageInThread.labelIds?.contains(LABEL_TRASH) != true) { listResult.add( GmailThreadInfo( id = thread.id, - lastMessage = it, + lastMessage = lastMessageInThread, messagesCount = thread.messages?.size ?: 0, - recipients = thread.getUniqueRecipients(), - subject = it.snippet, + recipients = thread.getUniqueRecipients(accountEntity.email), + subject = lastMessageInThread.payload?.headers?.firstOrNull { header -> + header.name == "Subject" + }?.value ?: context.getString(R.string.no_subject), labels = thread.messages.flatMap { message -> message.labelIds ?: emptyList() }.toSortedSet() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt index 87200ac8ba..f795cfd3a2 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt @@ -12,30 +12,36 @@ import jakarta.mail.internet.InternetAddress /** * @author Denys Bondarenko */ -fun Thread.getUniqueRecipients(): List { +//todo-denbond7 need to use ordering to show recipients in right order based on the conversation history +fun Thread.getUniqueRecipients(account: String): List { return mutableListOf().apply { val filteredHeaders = messages?.flatMap { message -> - message?.payload?.headers?.filter { header -> - //need to check this line and test - header.name in listOf( - "From", - "To" - )//maybe need to add Cc. need to check - } ?: emptyList() + if (message?.payload?.headers?.any { + it.name in listOf( + "From", + "To", + "Cc" + ) && it.value.contains(account, true) + } == true) { + message.payload?.headers?.filter { header -> + header.name == "From" + } ?: emptyList() + } else emptyList() } - val mapOfUniqueRecipients = mutableMapOf() + val mapOfUniqueRecipients = mutableMapOf() filteredHeaders?.forEach { header -> header.value.asInternetAddresses().forEach { internetAddress -> val address = internetAddress.address.lowercase() if (!mapOfUniqueRecipients.contains(address) - || mapOfUniqueRecipients[address].isNullOrEmpty() + || mapOfUniqueRecipients[address]?.personal.isNullOrEmpty() ) { - add(internetAddress) - mapOfUniqueRecipients[address] = internetAddress.personal + mapOfUniqueRecipients[address] = internetAddress } } } + + addAll(mapOfUniqueRecipients.values) } } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt index bb99c878f9..3952b040b0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt @@ -231,7 +231,7 @@ class MsgsPagedListAdapter(private val onMessageClickListener: OnMessageClickLis } ) - val senderAddress = prepareSenderAddress(folderType, messageEntity, context) + val senderAddress = prepareSenderAddress(context, folderType, messageEntity) binding.textViewSenderAddress.text = senderAddress updateAvatar(senderAddress, folderType, lastDataId == messageEntity.id) @@ -301,20 +301,33 @@ class MsgsPagedListAdapter(private val onMessageClickListener: OnMessageClickLis } private fun prepareSenderAddress( + context: Context, folderType: FoldersManager.FolderType?, - messageEntity: MessageEntity, - context: Context + messageEntity: MessageEntity ): CharSequence { + val accountName = messageEntity.account val addresses = when (folderType) { - FoldersManager.FolderType.SENT -> generateAddresses(messageEntity.to) + FoldersManager.FolderType.SENT -> generateAddresses( + context = context, + accountName = accountName, + internetAddresses = messageEntity.to + ) - FoldersManager.FolderType.DRAFTS -> generateAddresses(messageEntity.to).ifEmpty { + FoldersManager.FolderType.DRAFTS -> generateAddresses( + context = context, + accountName = accountName, + internetAddresses = messageEntity.to + ).ifEmpty { context.getString(R.string.no_recipients) } FoldersManager.FolderType.OUTBOX -> generateOutboxStatus(context, messageEntity.msgState) - else -> generateAddresses(messageEntity.from) + else -> generateAddresses( + context = context, + accountName = accountName, + internetAddresses = messageEntity.from + ) } return if ((messageEntity.threadMessagesCount ?: 0) > 1) { @@ -561,29 +574,41 @@ class MsgsPagedListAdapter(private val onMessageClickListener: OnMessageClickLis return SENDER_NAME_PATTERN.matcher(name).replaceFirst("") } - private fun generateAddresses(internetAddresses: List?): String { + private fun generateAddresses( + context: Context, + accountName: String, + internetAddresses: List? + ): String { if (internetAddresses == null) { - return "null" + return context.getString(R.string.no_recipients) } - val iMax = internetAddresses.size - 1 - if (iMax == -1) { - return "" + val mapOfUniqueInternetAddresses = mutableMapOf() + internetAddresses.forEach { internetAddress -> + val existingInternetAddress = + mapOfUniqueInternetAddresses[internetAddress.address.lowercase()] + if (existingInternetAddress == null || existingInternetAddress.personal?.isEmpty() == true) { + mapOfUniqueInternetAddresses[internetAddress.address.lowercase()] = internetAddress + } } - val stringBuilder = StringBuilder() - var i = 0 - while (true) { - val address = internetAddresses[i] - val displayName = - if (TextUtils.isEmpty(address.personal)) address.address else address.personal - stringBuilder.append(displayName) - if (i == iMax) { - return prepareSenderName(stringBuilder.toString()) + val uniqueRecipients = mapOfUniqueInternetAddresses.values.map { internetAddress -> + if (accountName.equals(internetAddress.address, true)) { + context.getString(R.string.me) + } else { + if (internetAddress.personal.isNullOrEmpty()) { + internetAddress.address + } else { + internetAddress.personal + } } - stringBuilder.append(", ") - i++ - } + }.toSet() + + return prepareSenderName(uniqueRecipients.joinToString { + if (uniqueRecipients.size > 1) { + it.split(" ").firstOrNull() ?: it + } else it + }) } companion object { From 77cc85260497773aff27926cbd876de44095939c Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 12 Aug 2024 14:21:47 +0300 Subject: [PATCH 017/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 17 +++++++++++++---- .../api/services/gmail/model/MessageExt.kt | 14 ++++++++++++++ .../jetpack/viewmodel/MessagesViewModel.kt | 1 + 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 9be650eeed..94e9e2279a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -23,6 +23,8 @@ import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getRecipients +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getSubject import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getUniqueRecipients import com.flowcrypt.email.extensions.contentId import com.flowcrypt.email.extensions.disposition @@ -390,6 +392,15 @@ class GmailApiHelper { responseHeaders: HttpHeaders? ) { t?.let { thread -> + val receiverEmail = accountEntity.email + val subject = thread.messages?.getOrNull(0)?.takeIf { message -> + message.getRecipients("From").any { internetAddress -> + internetAddress.address.equals(receiverEmail, true) + } || (thread.messages?.size?: 0) == 1 + }?.getSubject() + ?: thread.messages?.getOrNull(1)?.getSubject() + ?: context.getString(R.string.no_subject) + thread.messages?.lastOrNull()?.let { lastMessageInThread -> if (isTrash || lastMessageInThread.labelIds?.contains(LABEL_TRASH) != true) { listResult.add( @@ -397,10 +408,8 @@ class GmailApiHelper { id = thread.id, lastMessage = lastMessageInThread, messagesCount = thread.messages?.size ?: 0, - recipients = thread.getUniqueRecipients(accountEntity.email), - subject = lastMessageInThread.payload?.headers?.firstOrNull { header -> - header.name == "Subject" - }?.value ?: context.getString(R.string.no_subject), + recipients = thread.getUniqueRecipients(receiverEmail), + subject = subject, labels = thread.messages.flatMap { message -> message.labelIds ?: emptyList() }.toSortedSet() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt index 719a08af11..37602f0bf8 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt @@ -7,7 +7,9 @@ package com.flowcrypt.email.extensions.com.google.api.services.gmail.model import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.extensions.kotlin.asContentTypeOrNull +import com.flowcrypt.email.extensions.kotlin.asInternetAddresses import com.google.api.services.gmail.model.Message +import jakarta.mail.internet.InternetAddress /** * @author Denys Bondarenko @@ -40,4 +42,16 @@ fun Message.hasPgp(): Boolean { || isOpenPGPMimeSigned || isOpenPGPMimeEncrypted || hasEncryptedParts +} + +fun Message.getRecipients(vararg recipientType: String): List { + return payload?.headers?.firstOrNull { header -> + header.name in recipientType + }?.value?.asInternetAddresses()?.toList() ?: emptyList() +} + +fun Message.getSubject(): String? { + return payload?.headers?.firstOrNull { header -> + header.name == "Subject" + }?.value } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index 03929ffcef..3a8b6b0664 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -545,6 +545,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } if (thread != null) { messageEntity.copy( + subject = thread.subject, threadMessagesCount = thread.messagesCount, labelIds = thread.labels.joinToString(separator = LABEL_IDS_SEPARATOR), hasAttachments = thread.hasAttachments, From 671012bf9a926b1f0a3758bb2cca058c2ab16f0c Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 13 Aug 2024 11:02:23 +0300 Subject: [PATCH 018/237] Added NewMessageDetailsFragment as a temp container for the future changes.| #74 --- .../fragment/NewMessageDetailsFragment.java | 14 ++++++++++++++ .../flowcrypt/email/ui/adapter/FragmentsAdapter.kt | 11 +++++------ .../res/layout/fragment_new_message_details.xml | 10 ++++++++++ FlowCrypt/src/main/res/navigation/nav_graph.xml | 13 +++++++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.java create mode 100644 FlowCrypt/src/main/res/layout/fragment_new_message_details.xml diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.java b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.java new file mode 100644 index 0000000000..d894552f32 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.java @@ -0,0 +1,14 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.ui.activity.fragment; + +import androidx.fragment.app.Fragment; + +/** + * @author Denys Bondarenko + */ +public class NewMessageDetailsFragment extends Fragment { +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt index 5af69ccccd..58568ac4c4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt @@ -11,8 +11,8 @@ import androidx.recyclerview.widget.DiffUtil import androidx.viewpager2.adapter.FragmentStateAdapter import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.entity.MessageEntity -import com.flowcrypt.email.ui.activity.fragment.MessageDetailsFragment -import com.flowcrypt.email.ui.activity.fragment.MessageDetailsFragmentArgs +import com.flowcrypt.email.ui.activity.fragment.NewMessageDetailsFragment +import com.flowcrypt.email.ui.activity.fragment.NewMessageDetailsFragmentArgs /** * @author Denys Bondarenko @@ -45,10 +45,9 @@ class FragmentsAdapter( override fun getItemCount(): Int = asyncListDiffer.currentList.size override fun createFragment(position: Int): Fragment = - MessageDetailsFragment().apply { - arguments = MessageDetailsFragmentArgs( - messageEntity = asyncListDiffer.currentList[position], - localFolder = localFolder, + NewMessageDetailsFragment().apply { + arguments = NewMessageDetailsFragmentArgs( + messageEntityId = requireNotNull(asyncListDiffer.currentList[position].id), isViewPagerMode = true ).toBundle() } diff --git a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml new file mode 100644 index 0000000000..0361973f45 --- /dev/null +++ b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/FlowCrypt/src/main/res/navigation/nav_graph.xml b/FlowCrypt/src/main/res/navigation/nav_graph.xml index dd99423ab2..bb76a5e346 100644 --- a/FlowCrypt/src/main/res/navigation/nav_graph.xml +++ b/FlowCrypt/src/main/res/navigation/nav_graph.xml @@ -326,6 +326,19 @@ android:defaultValue="false" /> + + + + + Date: Tue, 13 Aug 2024 16:49:41 +0300 Subject: [PATCH 019/237] wip --- .../res/layout/item_message_in_thread.xml | 232 ++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 FlowCrypt/src/main/res/layout/item_message_in_thread.xml diff --git a/FlowCrypt/src/main/res/layout/item_message_in_thread.xml b/FlowCrypt/src/main/res/layout/item_message_in_thread.xml new file mode 100644 index 0000000000..d9c36f4cd9 --- /dev/null +++ b/FlowCrypt/src/main/res/layout/item_message_in_thread.xml @@ -0,0 +1,232 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From 058601cf614e505b8350ae507287705fa6a8d2eb Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 14 Aug 2024 10:29:29 +0300 Subject: [PATCH 020/237] Rename .java to .kt --- ...ewMessageDetailsFragment.java => NewMessageDetailsFragment.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/{NewMessageDetailsFragment.java => NewMessageDetailsFragment.kt} (100%) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.java b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt similarity index 100% rename from FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.java rename to FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt From 86521f4fd602a019a313c96a213e02222e48ffa1 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 14 Aug 2024 10:29:35 +0300 Subject: [PATCH 021/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 32 ++- .../email/database/dao/MessageDao.kt | 4 + .../viewmodel/ThreadDetailsViewModel.kt | 117 +++++++++ .../fragment/NewMessageDetailsFragment.kt | 110 ++++++++- .../ui/adapter/MessagesInThreadListAdapter.kt | 101 ++++++++ .../layout/fragment_new_message_details.xml | 92 ++++++- .../res/layout/item_message_in_thread.xml | 227 ++++++++++-------- 7 files changed, 568 insertions(+), 115 deletions(-) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 94e9e2279a..d829860a2b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -353,11 +353,11 @@ class GmailApiHelper { ): List = withContext(Dispatchers.IO) { return@withContext useParallel(list = threads, stepValue = stepValue) { list -> - loadGmailThreadInfo(context, accountEntity, list, localFolder, format) + loadThreadsInfo(context, accountEntity, list, localFolder, format) } } - suspend fun loadGmailThreadInfo( + suspend fun loadThreadsInfo( context: Context, accountEntity: AccountEntity, threads: Collection, @@ -431,6 +431,34 @@ class GmailApiHelper { return@withContext listResult } + suspend fun loadMessagesInThread( + context: Context, + accountEntity: AccountEntity, + threadId: String, + format: String = MESSAGE_RESPONSE_FORMAT_FULL, + metadataHeaders: List? = null, + fields: List? = null + ): List = withContext(Dispatchers.IO) + { + val gmailApiService = generateGmailApiService(context, accountEntity) + + val request = gmailApiService + .users() + .threads() + .get(DEFAULT_USER_ID, threadId) + .setFormat(format) + + metadataHeaders?.let { metadataHeaders -> + request.metadataHeaders = metadataHeaders + } + + fields?.let { fields -> + request.fields = fields.joinToString(separator = ",") + } + + return@withContext request.execute()?.messages ?: emptyList() + } + suspend fun loadMsgs( context: Context, accountEntity: AccountEntity, messages: Collection, localFolder: LocalFolder, format: String = MESSAGE_RESPONSE_FORMAT_FULL, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt index 1a5660bf3c..c2e63866f7 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt @@ -22,6 +22,7 @@ import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.MessageEntity import jakarta.mail.Flags import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.withContext /** @@ -40,6 +41,9 @@ abstract class MessageDao : BaseDao { @Query("SELECT * FROM messages WHERE _id = :id") abstract suspend fun getMsgById(id: Long): MessageEntity? + @Query("SELECT * FROM messages WHERE _id = :id") + abstract fun getMessageByIdFlow(id: Long): Flow + @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder") abstract fun getMsgsLD(account: String, folder: String): LiveData diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt new file mode 100644 index 0000000000..41b95c4b88 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -0,0 +1,117 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.jetpack.viewmodel + +import android.app.Application +import androidx.lifecycle.asFlow +import com.flowcrypt.email.api.email.gmail.GmailApiHelper +import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly +import com.flowcrypt.email.extensions.kotlin.toHex +import com.flowcrypt.email.ui.adapter.GmailApiLabelsListAdapter +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.flow.mapLatest +import kotlinx.coroutines.flow.merge + +/** + * @author Denys Bondarenko + */ +class ThreadDetailsViewModel( + private val messageEntityId: Long, + application: Application +) : AccountViewModel(application) { + + val messageFlow: Flow = roomDatabase.msgDao().getMessageByIdFlow(messageEntityId).distinctUntilChanged() + + val messagesInThreadFlow: Flow> = + merge( + flow { + val initialMessageEntity = roomDatabase.msgDao().getMsgById(messageEntityId) ?: return@flow + val activeAccount = getActiveAccountSuspend() ?: return@flow + if (initialMessageEntity.threadId.isNullOrEmpty() || !activeAccount.isGoogleSignInAccount) { + emit(listOf(initialMessageEntity)) + } else { + try { + val messagesInThread = GmailApiHelper.loadMessagesInThread( + application, + activeAccount, + initialMessageEntity.threadId + ) + + val isOnlyPgpModeEnabled = activeAccount.showOnlyEncrypted ?: false + val messageEntities = MessageEntity.genMessageEntities( + context = getApplication(), + account = activeAccount.email, + accountType = activeAccount.accountType, + label = "INBOX", + msgsList = messagesInThread, + isNew = false, + onlyPgpModeEnabled = isOnlyPgpModeEnabled, + draftIdsMap = emptyMap() + ) + + emit(messageEntities) + } catch (e: Exception) { + e.printStackTraceIfDebugOnly() + emit(listOf(initialMessageEntity)) + } + } + }, + ) + + @OptIn(ExperimentalCoroutinesApi::class) + val messageGmailApiLabelsFlow: Flow> = + merge( + messageFlow.mapLatest { latestMessageEntityRecord -> + val activeAccount = getActiveAccountSuspend() + if (activeAccount?.isGoogleSignInAccount == true) { + val labelEntities = + roomDatabase.labelDao().getLabelsSuspend(activeAccount.email, activeAccount.accountType) + MessageEntity.generateColoredLabels( + latestMessageEntityRecord?.labelIds?.split(MessageEntity.LABEL_IDS_SEPARATOR), + labelEntities + ) + } else { + emptyList() + } + }, + activeAccountLiveData.asFlow().mapLatest { account -> + if (account?.isGoogleSignInAccount == true) { + val labelEntities = + roomDatabase.labelDao().getLabelsSuspend(account.email, account.accountType) + val freshestMessageEntity = roomDatabase.msgDao().getMsgById(messageEntityId) + val cachedLabelIds = + freshestMessageEntity?.labelIds?.split(MessageEntity.LABEL_IDS_SEPARATOR) + try { + val message = GmailApiHelper.loadMsgInfoSuspend( + context = getApplication(), + accountEntity = account, + msgId = messageEntityId.toHex(), + fields = null, + format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_MINIMAL + ) + + val latestLabelIds = message.labelIds + if (cachedLabelIds == null + || !(latestLabelIds.containsAll(cachedLabelIds) + && cachedLabelIds.containsAll(latestLabelIds)) + ) { + freshestMessageEntity?.copy( + labelIds = latestLabelIds.joinToString(MessageEntity.LABEL_IDS_SEPARATOR) + )?.let { roomDatabase.msgDao().updateSuspend(it) } + } + MessageEntity.generateColoredLabels(latestLabelIds, labelEntities) + } catch (e: Exception) { + MessageEntity.generateColoredLabels(cachedLabelIds, labelEntities) + } + } else { + emptyList() + } + }) +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt index d894552f32..74a5b24d4f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt @@ -3,12 +3,114 @@ * Contributors: denbond7 */ -package com.flowcrypt.email.ui.activity.fragment; +package com.flowcrypt.email.ui.activity.fragment -import androidx.fragment.app.Fragment; +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.viewModels +import androidx.lifecycle.ViewModel +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.LinearLayoutManager +import com.flowcrypt.email.R +import com.flowcrypt.email.databinding.FragmentNewMessageDetailsBinding +import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithViewLifecycle +import com.flowcrypt.email.extensions.androidx.fragment.app.toast +import com.flowcrypt.email.extensions.visible +import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory +import com.flowcrypt.email.jetpack.viewmodel.ThreadDetailsViewModel +import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment +import com.flowcrypt.email.ui.adapter.GmailApiLabelsListAdapter +import com.flowcrypt.email.ui.adapter.MessagesInThreadListAdapter +import com.flowcrypt.email.ui.adapter.recyclerview.itemdecoration.MarginItemDecoration +import com.google.android.flexbox.FlexDirection +import com.google.android.flexbox.FlexboxLayoutManager +import com.google.android.flexbox.JustifyContent /** * @author Denys Bondarenko */ -public class NewMessageDetailsFragment extends Fragment { -} +class NewMessageDetailsFragment : BaseFragment() { + override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentNewMessageDetailsBinding.inflate(inflater, container, false) + + private val args by navArgs() + private val threadDetailsViewModel: ThreadDetailsViewModel by viewModels { + object : CustomAndroidViewModelFactory(requireActivity().application) { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return ThreadDetailsViewModel( + args.messageEntityId, requireActivity().application + ) as T + } + } + } + + private val gmailApiLabelsListAdapter = GmailApiLabelsListAdapter( + object : GmailApiLabelsListAdapter.OnLabelClickListener { + override fun onLabelClick(label: GmailApiLabelsListAdapter.Label) { + toast("fix me") + } + }) + + private val messagesInThreadListAdapter = MessagesInThreadListAdapter() + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + initViews() + setupLabelsViewModel() + setupThreadDetailsViewModel() + } + + private fun setupThreadDetailsViewModel() { + launchAndRepeatWithViewLifecycle { + threadDetailsViewModel.messageFlow.collect { + binding?.textViewSubject?.text = it?.subject + } + } + + launchAndRepeatWithViewLifecycle { + threadDetailsViewModel.messagesInThreadFlow.collect { + messagesInThreadListAdapter.submitList(it) + binding?.recyclerViewMessages?.visible() + } + } + + launchAndRepeatWithViewLifecycle { + threadDetailsViewModel.messageGmailApiLabelsFlow.collect { + gmailApiLabelsListAdapter.submitList(it) + } + } + } + + private fun setupLabelsViewModel() { + + } + + private fun initViews() { + binding?.recyclerViewMessages?.apply { + val linearLayoutManager = LinearLayoutManager(context) + layoutManager = linearLayoutManager + addItemDecoration( + DividerItemDecoration(context, linearLayoutManager.orientation) + ) + adapter = messagesInThreadListAdapter + } + + binding?.recyclerViewLabels?.apply { + layoutManager = FlexboxLayoutManager(context).apply { + flexDirection = FlexDirection.ROW + justifyContent = JustifyContent.FLEX_START + } + addItemDecoration( + MarginItemDecoration( + marginRight = resources.getDimensionPixelSize(R.dimen.default_margin_small), + marginTop = resources.getDimensionPixelSize(R.dimen.default_margin_small) + ) + ) + adapter = gmailApiLabelsListAdapter + } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt new file mode 100644 index 0000000000..52b3c1ba45 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -0,0 +1,101 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.ui.adapter + +import android.content.Context +import android.text.SpannableStringBuilder +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.text.toSpannable +import androidx.core.view.isVisible +import androidx.recyclerview.widget.DiffUtil +import androidx.recyclerview.widget.ListAdapter +import androidx.recyclerview.widget.RecyclerView +import com.flowcrypt.email.R +import com.flowcrypt.email.api.email.EmailUtil +import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.databinding.ItemMessageInThreadBinding +import com.flowcrypt.email.extensions.android.widget.useGlideToApplyImageFromSource +import com.flowcrypt.email.extensions.jakarta.mail.internet.personalOrEmail +import com.flowcrypt.email.extensions.visibleOrGone +import com.flowcrypt.email.util.DateTimeUtil +import com.flowcrypt.email.util.graphics.glide.AvatarModelLoader +import jakarta.mail.internet.InternetAddress + +/** + * @author Denys Bondarenko + */ +class MessagesInThreadListAdapter : ListAdapter(DIFF_UTIL_ITEM_CALLBACK) { + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_message_in_thread, parent, false) + ) + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.bindTo(getItem(position)) + } + + inner class ViewHolder(itemView: View) : + RecyclerView.ViewHolder(itemView) { + val binding = ItemMessageInThreadBinding.bind(itemView) + + fun bindTo(item: MessageEntity) { + val context = itemView.context + binding.header.setOnClickListener { + binding.groupCollapsibleContent.visibleOrGone(!binding.groupCollapsibleContent.isVisible) + } + val senderAddress = EmailUtil.getFirstAddressString(item.from) + binding.imageViewAvatar.useGlideToApplyImageFromSource( + source = AvatarModelLoader.SCHEMA_AVATAR + senderAddress + ) + binding.textViewSenderAddress.text = senderAddress + binding.tVTo.text = prepareToText(context, item) + binding.textViewDate.text = + DateTimeUtil.formatSameDayTime(context, item.receivedDate ?: 0) + } + + private fun prepareToText(context: Context, messageEntity: MessageEntity): String { + val stringBuilder = SpannableStringBuilder() + val meAddress = messageEntity.to.firstOrNull { + it.address.equals(messageEntity.account, true) + } + val leftAddresses: List + if (meAddress == null) { + leftAddresses = messageEntity.to + } else { + stringBuilder.append(context.getString(R.string.me)) + leftAddresses = ArrayList(messageEntity.to) - meAddress + if (leftAddresses.isNotEmpty()) { + stringBuilder.append(", ") + } + } + + val to = leftAddresses.foldIndexed(stringBuilder) { index, builder, it -> + builder.append(it.personalOrEmail) + if (index != leftAddresses.size - 1) { + builder.append(",") + } + builder + }.toSpannable() + + return context.getString(R.string.to_receiver, to) + } + } + + companion object { + private val DIFF_UTIL_ITEM_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MessageEntity, newItem: MessageEntity) = + oldItem.uid == newItem.uid + + override fun areContentsTheSame(oldItem: MessageEntity, newItem: MessageEntity) = + oldItem == newItem + } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml index 0361973f45..00b52b6857 100644 --- a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml @@ -1,10 +1,94 @@ - + android:layout_height="match_parent" + android:fillViewport="true"> - \ No newline at end of file + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/FlowCrypt/src/main/res/layout/item_message_in_thread.xml b/FlowCrypt/src/main/res/layout/item_message_in_thread.xml index d9c36f4cd9..227610d703 100644 --- a/FlowCrypt/src/main/res/layout/item_message_in_thread.xml +++ b/FlowCrypt/src/main/res/layout/item_message_in_thread.xml @@ -8,111 +8,121 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginStart="@dimen/default_margin_medium" - android:layout_marginTop="@dimen/default_margin_content" - android:layout_marginEnd="@dimen/default_margin_medium" android:animateLayoutChanges="true"> - - - - - - - - - - - - - + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> + + + + + + + + + + + + + + + @@ -148,7 +158,7 @@ + app:layout_constraintTop_toBottomOf="@+id/headerFooter"> + + From b63b5eea767e1f13660590abf7d226fa6b4681b2 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 15 Aug 2024 10:37:01 +0300 Subject: [PATCH 022/237] wip --- .../fragment/NewMessageDetailsFragment.kt | 79 +++++++++++++++++-- .../ui/adapter/MessagesInThreadListAdapter.kt | 9 ++- .../email/ui/widget/NonLockingScrollView.kt | 3 +- .../layout/fragment_new_message_details.xml | 50 +++++++++--- 4 files changed, 122 insertions(+), 19 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt index 74a5b24d4f..d5ff12fe01 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt @@ -5,37 +5,47 @@ package com.flowcrypt.email.ui.activity.fragment +import android.content.res.ColorStateList import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.navigation.fragment.navArgs -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import com.flowcrypt.email.R import com.flowcrypt.email.databinding.FragmentNewMessageDetailsBinding import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithViewLifecycle import com.flowcrypt.email.extensions.androidx.fragment.app.toast -import com.flowcrypt.email.extensions.visible import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.ThreadDetailsViewModel import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment +import com.flowcrypt.email.ui.activity.fragment.base.ProgressBehaviour import com.flowcrypt.email.ui.adapter.GmailApiLabelsListAdapter import com.flowcrypt.email.ui.adapter.MessagesInThreadListAdapter import com.flowcrypt.email.ui.adapter.recyclerview.itemdecoration.MarginItemDecoration import com.google.android.flexbox.FlexDirection import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.JustifyContent +import com.google.android.material.divider.MaterialDividerItemDecoration /** * @author Denys Bondarenko */ -class NewMessageDetailsFragment : BaseFragment() { +class NewMessageDetailsFragment : BaseFragment(), + ProgressBehaviour { override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentNewMessageDetailsBinding.inflate(inflater, container, false) + override val progressView: View? + get() = binding?.progress?.root + override val contentView: View? + get() = binding?.groupContent + override val statusView: View? + get() = binding?.status?.root + private val args by navArgs() private val threadDetailsViewModel: ThreadDetailsViewModel by viewModels { object : CustomAndroidViewModelFactory(requireActivity().application) { @@ -74,7 +84,25 @@ class NewMessageDetailsFragment : BaseFragment launchAndRepeatWithViewLifecycle { threadDetailsViewModel.messagesInThreadFlow.collect { messagesInThreadListAdapter.submitList(it) - binding?.recyclerViewMessages?.visible() + updateReplyButtons(it.lastOrNull()?.hasPgp == true) + showContent() + + binding?.recyclerViewMessages?.let { recyclerView -> + recyclerView.scrollToPosition(recyclerView.adapter?.itemCount!! - 1) + } + + /*GlobalScope.launch { + withContext(Dispatchers.Main) { + delay(500) + binding?.nonLockingScrollView?.apply { + val lastChild = getChildAt(childCount - 1) + val bottom = lastChild.bottom + paddingBottom + val delta = bottom - (scrollY + height) + scrollBy(0, delta) + fullScroll(View.FOCUS_DOWN) + } + } + }*/ } } @@ -94,7 +122,11 @@ class NewMessageDetailsFragment : BaseFragment val linearLayoutManager = LinearLayoutManager(context) layoutManager = linearLayoutManager addItemDecoration( - DividerItemDecoration(context, linearLayoutManager.orientation) + MaterialDividerItemDecoration( + context, linearLayoutManager.orientation + ).apply { + isLastItemDecorated = false + } ) adapter = messagesInThreadListAdapter } @@ -113,4 +145,41 @@ class NewMessageDetailsFragment : BaseFragment adapter = gmailApiLabelsListAdapter } } + + private fun updateReplyButtons(usePgpMode: Boolean) { + if (binding?.layoutReplyButtons != null) { + val imageViewReply = binding?.layoutReplyButtons?.imageViewReply + val imageViewReplyAll = binding?.layoutReplyButtons?.imageViewReplyAll + val imageViewFwd = binding?.layoutReplyButtons?.imageViewFwd + + val textViewReply = binding?.layoutReplyButtons?.textViewReply + val textViewReplyAll = binding?.layoutReplyButtons?.textViewReplyAll + val textViewFwd = binding?.layoutReplyButtons?.textViewFwd + + val buttonsColorId: Int + + if (usePgpMode) { + buttonsColorId = R.color.colorPrimary + textViewReply?.setText(R.string.reply_encrypted) + textViewReplyAll?.setText(R.string.reply_all_encrypted) + textViewFwd?.setText(R.string.forward_encrypted) + } else { + buttonsColorId = R.color.red + textViewReply?.setText(R.string.reply) + textViewReplyAll?.setText(R.string.reply_all) + textViewFwd?.setText(R.string.forward) + } + + val colorStateList = + ColorStateList.valueOf(ContextCompat.getColor(requireContext(), buttonsColorId)) + + imageViewReply?.imageTintList = colorStateList + imageViewReplyAll?.imageTintList = colorStateList + imageViewFwd?.imageTintList = colorStateList + + /*binding?.layoutReplyButtons?.layoutReplyButton?.setOnClickListener(this) + binding?.layoutReplyButtons?.layoutFwdButton?.setOnClickListener(this) + binding?.layoutReplyButtons?.layoutReplyAllButton?.setOnClickListener(this)*/ + } + } } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt index 52b3c1ba45..ea01129c9e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -39,18 +39,19 @@ class MessagesInThreadListAdapter : ListAdapter - + app:layout_constraintTop_toBottomOf="@+id/view" /> - + + + + + + + \ No newline at end of file From 7616d81258c4546d6e6417f86085d8298fc0d171 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 15 Aug 2024 14:59:38 +0300 Subject: [PATCH 023/237] wip --- .../fragment/NewMessageDetailsFragment.kt | 24 +--- .../ui/adapter/MessagesInThreadListAdapter.kt | 4 +- .../layout/fragment_new_message_details.xml | 127 ++++++++---------- .../fragment_new_message_details_scene.xml | 85 ++++++++++++ 4 files changed, 147 insertions(+), 93 deletions(-) create mode 100644 FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt index d5ff12fe01..a7a86f0d8c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt @@ -42,7 +42,7 @@ class NewMessageDetailsFragment : BaseFragment override val progressView: View? get() = binding?.progress?.root override val contentView: View? - get() = binding?.groupContent + get() = binding?.groupContent?.rootView override val statusView: View? get() = binding?.status?.root @@ -70,7 +70,6 @@ class NewMessageDetailsFragment : BaseFragment override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initViews() - setupLabelsViewModel() setupThreadDetailsViewModel() } @@ -86,23 +85,6 @@ class NewMessageDetailsFragment : BaseFragment messagesInThreadListAdapter.submitList(it) updateReplyButtons(it.lastOrNull()?.hasPgp == true) showContent() - - binding?.recyclerViewMessages?.let { recyclerView -> - recyclerView.scrollToPosition(recyclerView.adapter?.itemCount!! - 1) - } - - /*GlobalScope.launch { - withContext(Dispatchers.Main) { - delay(500) - binding?.nonLockingScrollView?.apply { - val lastChild = getChildAt(childCount - 1) - val bottom = lastChild.bottom + paddingBottom - val delta = bottom - (scrollY + height) - scrollBy(0, delta) - fullScroll(View.FOCUS_DOWN) - } - } - }*/ } } @@ -113,10 +95,6 @@ class NewMessageDetailsFragment : BaseFragment } } - private fun setupLabelsViewModel() { - - } - private fun initViews() { binding?.recyclerViewMessages?.apply { val linearLayoutManager = LinearLayoutManager(context) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt index ea01129c9e..bac34bb7ef 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -61,9 +61,9 @@ class MessagesInThreadListAdapter : ListAdapter - + app:layoutDescription="@xml/fragment_new_message_details_scene"> + + + + + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent"> - - - - - - - + - + - + - + - - \ No newline at end of file + \ No newline at end of file diff --git a/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml b/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml new file mode 100644 index 0000000000..aedca987c8 --- /dev/null +++ b/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 7a13d45dc156ce8f813cbf97c0befdbde994bea1 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 26 Aug 2024 11:10:39 +0300 Subject: [PATCH 024/237] Added the final version of MotionLayout in the message details screen.| #74 --- .../layout/fragment_new_message_details.xml | 2 +- .../fragment_new_message_details_scene.xml | 35 ++++++++----------- 2 files changed, 15 insertions(+), 22 deletions(-) diff --git a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml index 3eeef33403..b8c10c5364 100644 --- a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml @@ -97,7 +97,7 @@ layout="@layout/reply_layout" android:layout_width="match_parent" android:layout_height="wrap_content" - android:layout_marginBottom="8dp" + android:layout_marginBottom="@dimen/default_margin_content_small" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml b/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml index aedca987c8..0473687511 100644 --- a/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml +++ b/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml @@ -1,16 +1,6 @@ @@ -40,18 +30,19 @@ android:id="@id/recyclerViewMessages" android:layout_width="match_parent" android:layout_height="0dp" - motion:layout_constraintBottom_toBottomOf="parent" + motion:layout_constraintBottom_toTopOf="@+id/layoutReplyButtons" + motion:layout_constraintEnd_toEndOf="parent" + motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toBottomOf="@+id/header" /> + android:layout_marginBottom="@dimen/default_margin_content_small" + motion:layout_constraintBottom_toBottomOf="parent" + motion:layout_constraintEnd_toEndOf="parent" + motion:layout_constraintStart_toStartOf="parent" /> @@ -69,14 +60,16 @@ android:id="@id/recyclerViewMessages" android:layout_width="match_parent" android:layout_height="0dp" - motion:layout_constraintBottom_toBottomOf="parent" + motion:layout_constraintBottom_toTopOf="@+id/layoutReplyButtons" + motion:layout_constraintEnd_toEndOf="parent" + motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toBottomOf="@+id/header" /> From fc01f15765b673330b32510330981d6262b93014 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 26 Aug 2024 14:14:02 +0300 Subject: [PATCH 025/237] wip --- .../fragment/MessageDetailsFragment.kt | 8 +- .../fragment/NewMessageDetailsFragment.kt | 30 ++-- .../ViewPagerMessageDetailsFragment.kt | 17 +-- .../layout/fragment_new_message_details.xml | 138 +++++++++-------- .../fragment_view_pager_message_details.xml | 22 --- .../src/main/res/layout/reply_layout.xml | 143 ++++++------------ .../fragment_new_message_details_scene.xml | 15 ++ 7 files changed, 151 insertions(+), 222 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt index 9c5e989715..9bfe097236 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt @@ -490,7 +490,7 @@ class MessageDetailsFragment : BaseFragment(), Pr } override fun onClick(v: View) { - when (v.id) { + /*when (v.id) { R.id.layoutReplyButton -> { startActivity( CreateMessageActivity.generateIntent( @@ -561,7 +561,7 @@ class MessageDetailsFragment : BaseFragment(), Pr ) } } - } + }*/ } override fun onAccountInfoRefreshed(accountEntity: AccountEntity?) { @@ -1181,7 +1181,7 @@ class MessageDetailsFragment : BaseFragment(), Pr * Update the reply buttons layout depending on the [MessageEncryptionType] */ private fun updateReplyButtons() { - if (binding?.layoutReplyButtons != null) { + /*if (binding?.layoutReplyButtons != null) { val imageViewReply = binding?.layoutReplyButtons?.imageViewReply val imageViewReplyAll = binding?.layoutReplyButtons?.imageViewReplyAll val imageViewFwd = binding?.layoutReplyButtons?.imageViewFwd @@ -1217,7 +1217,7 @@ class MessageDetailsFragment : BaseFragment(), Pr binding?.layoutReplyButtons?.root?.visibleOrGone( !args.messageEntity.isOutboxMsg && !args.messageEntity.isDraft ) - } + }*/ } /** diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt index a7a86f0d8c..f5c419fde6 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt @@ -42,7 +42,7 @@ class NewMessageDetailsFragment : BaseFragment override val progressView: View? get() = binding?.progress?.root override val contentView: View? - get() = binding?.groupContent?.rootView + get() = binding?.content override val statusView: View? get() = binding?.status?.root @@ -126,34 +126,30 @@ class NewMessageDetailsFragment : BaseFragment private fun updateReplyButtons(usePgpMode: Boolean) { if (binding?.layoutReplyButtons != null) { - val imageViewReply = binding?.layoutReplyButtons?.imageViewReply - val imageViewReplyAll = binding?.layoutReplyButtons?.imageViewReplyAll - val imageViewFwd = binding?.layoutReplyButtons?.imageViewFwd - - val textViewReply = binding?.layoutReplyButtons?.textViewReply - val textViewReplyAll = binding?.layoutReplyButtons?.textViewReplyAll - val textViewFwd = binding?.layoutReplyButtons?.textViewFwd + val imageViewReply = binding?.layoutReplyButtons?.replyButton + val imageViewReplyAll = binding?.layoutReplyButtons?.replyAllButton + val imageViewFwd = binding?.layoutReplyButtons?.forwardButton val buttonsColorId: Int if (usePgpMode) { buttonsColorId = R.color.colorPrimary - textViewReply?.setText(R.string.reply_encrypted) - textViewReplyAll?.setText(R.string.reply_all_encrypted) - textViewFwd?.setText(R.string.forward_encrypted) + imageViewReply?.setText(R.string.reply_encrypted) + imageViewReplyAll?.setText(R.string.reply_all_encrypted) + imageViewFwd?.setText(R.string.forward_encrypted) } else { buttonsColorId = R.color.red - textViewReply?.setText(R.string.reply) - textViewReplyAll?.setText(R.string.reply_all) - textViewFwd?.setText(R.string.forward) + imageViewReply?.setText(R.string.reply) + imageViewReplyAll?.setText(R.string.reply_all) + imageViewFwd?.setText(R.string.forward) } val colorStateList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), buttonsColorId)) - imageViewReply?.imageTintList = colorStateList - imageViewReplyAll?.imageTintList = colorStateList - imageViewFwd?.imageTintList = colorStateList + imageViewReply?.iconTint = colorStateList + imageViewReplyAll?.iconTint = colorStateList + imageViewFwd?.iconTint = colorStateList /*binding?.layoutReplyButtons?.layoutReplyButton?.setOnClickListener(this) binding?.layoutReplyButtons?.layoutFwdButton?.setOnClickListener(this) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt index 6517e334d5..b3fb6adadb 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt @@ -31,14 +31,12 @@ import com.flowcrypt.email.extensions.observeOnce import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.MessagesViewPagerViewModel import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment -import com.flowcrypt.email.ui.activity.fragment.base.ProgressBehaviour import com.flowcrypt.email.ui.adapter.FragmentsAdapter /** * @author Denys Bondarenko */ -class ViewPagerMessageDetailsFragment : BaseFragment(), - ProgressBehaviour { +class ViewPagerMessageDetailsFragment : BaseFragment() { private val args by navArgs() private val messagesViewPagerViewModel: MessagesViewPagerViewModel by viewModels { @@ -54,13 +52,6 @@ class ViewPagerMessageDetailsFragment : BaseFragment - showContent() - } + ) { _, _ -> } addItemDecoration(DividerItemDecoration(view.context, ORIENTATION_HORIZONTAL)) @@ -120,7 +109,6 @@ class ViewPagerMessageDetailsFragment : BaseFragment { (binding?.viewPager2?.adapter as? FragmentsAdapter)?.submit(it.data ?: emptyList()) - showContent() } else -> { @@ -134,7 +122,6 @@ class ViewPagerMessageDetailsFragment : BaseFragment { (binding?.viewPager2?.adapter as? FragmentsAdapter)?.submit(it.data ?: emptyList()) - showContent() } else -> {} diff --git a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml index b8c10c5364..d76d29b4b2 100644 --- a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml @@ -2,13 +2,11 @@ ~ © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com ~ Contributors: DenBond7 --> - - + android:layout_height="match_parent"> - - + app:layout_constraintTop_toTopOf="parent"> + + + + + + + app:layout_constraintTop_toBottomOf="@+id/header" + app:layout_constraintVertical_bias="0.0" + tools:itemCount="3" + tools:listitem="@layout/item_message_in_thread" /> - - - - - + - + - \ No newline at end of file + diff --git a/FlowCrypt/src/main/res/layout/fragment_view_pager_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_view_pager_message_details.xml index 3346e543bc..bfb8db040a 100644 --- a/FlowCrypt/src/main/res/layout/fragment_view_pager_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_view_pager_message_details.xml @@ -18,27 +18,5 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - - diff --git a/FlowCrypt/src/main/res/layout/reply_layout.xml b/FlowCrypt/src/main/res/layout/reply_layout.xml index e8d8627e45..750a5c1689 100644 --- a/FlowCrypt/src/main/res/layout/reply_layout.xml +++ b/FlowCrypt/src/main/res/layout/reply_layout.xml @@ -6,116 +6,63 @@ - - - + app:layout_constraintTop_toTopOf="parent" /> - - - - - - + app:layout_constraintStart_toEndOf="@+id/replyButton" + app:layout_constraintTop_toTopOf="parent" /> - - - - - - + app:layout_constraintHorizontal_bias="0.5" + app:layout_constraintStart_toEndOf="@+id/replyAllButton" + app:layout_constraintTop_toTopOf="parent" /> - - diff --git a/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml b/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml index 0473687511..fbc5469b9d 100644 --- a/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml +++ b/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml @@ -43,6 +43,21 @@ motion:layout_constraintBottom_toBottomOf="parent" motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintStart_toStartOf="parent" /> + + + From c3516eca8a341e02899ea434bab96e8767b45f4b Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 26 Aug 2024 17:02:22 +0300 Subject: [PATCH 026/237] wip --- .../fragment/NewMessageDetailsFragment.kt | 15 +++++++ .../ui/adapter/MessagesInThreadListAdapter.kt | 17 +++++++- .../res/drawable/ic_question_answer_24.xml | 17 ++++++++ .../layout/fragment_new_message_details.xml | 28 ++++++++++--- .../fragment_new_message_details_scene.xml | 39 ++++++++++--------- 5 files changed, 91 insertions(+), 25 deletions(-) create mode 100644 FlowCrypt/src/main/res/drawable/ic_question_answer_24.xml diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt index f5c419fde6..a5299393ff 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt @@ -19,6 +19,8 @@ import com.flowcrypt.email.R import com.flowcrypt.email.databinding.FragmentNewMessageDetailsBinding import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithViewLifecycle import com.flowcrypt.email.extensions.androidx.fragment.app.toast +import com.flowcrypt.email.extensions.gone +import com.flowcrypt.email.extensions.visible import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.ThreadDetailsViewModel import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment @@ -83,6 +85,14 @@ class NewMessageDetailsFragment : BaseFragment launchAndRepeatWithViewLifecycle { threadDetailsViewModel.messagesInThreadFlow.collect { messagesInThreadListAdapter.submitList(it) + + if (it.size > 1) { + binding?.displayFullConversation?.visible() + binding?.displayFullConversation?.text = "Show full conversation(${it.size - 1} left)" + } else { + binding?.displayFullConversation?.gone() + } + updateReplyButtons(it.lastOrNull()?.hasPgp == true) showContent() } @@ -122,6 +132,11 @@ class NewMessageDetailsFragment : BaseFragment ) adapter = gmailApiLabelsListAdapter } + + binding?.displayFullConversation?.setOnClickListener { v -> + v.gone() + messagesInThreadListAdapter.showAll() + } } private fun updateReplyButtons(usePgpMode: Boolean) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt index bac34bb7ef..8b26a37dea 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -32,6 +32,9 @@ import jakarta.mail.internet.InternetAddress class MessagesInThreadListAdapter : ListAdapter(DIFF_UTIL_ITEM_CALLBACK) { + private val map = mapOf() + private val fullList: MutableList = mutableListOf() + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( LayoutInflater.from(parent.context).inflate(R.layout.item_message_in_thread, parent, false) @@ -42,6 +45,16 @@ class MessagesInThreadListAdapter : ListAdapter?) { + fullList.clear() + fullList.addAll(list ?: emptyList()) + super.submitList(list?.filterIndexed { index, _ -> index == list.size - 1 }) + } + + fun showAll() { + super.submitList(fullList) + } + inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val binding = ItemMessageInThreadBinding.bind(itemView) @@ -61,9 +74,9 @@ class MessagesInThreadListAdapter : ListAdapter + + + + + + diff --git a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml index d76d29b4b2..fe610d96de 100644 --- a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml @@ -1,6 +1,6 @@ + app:layout_constraintTop_toTopOf="parent" + tools:visibility="visible"> + + @@ -109,7 +128,6 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginBottom="@dimen/default_margin_content_small" - android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> diff --git a/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml b/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml index fbc5469b9d..951b4cce18 100644 --- a/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml +++ b/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml @@ -1,6 +1,6 @@ @@ -26,6 +26,15 @@ motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toTopOf="parent" /> + + + motion:layout_constraintTop_toBottomOf="@+id/displayFullConversation" /> - - - @@ -71,6 +65,15 @@ motion:layout_constraintTop_toTopOf="parent" motion:motionProgress="1" /> + + + motion:layout_constraintTop_toBottomOf="@+id/displayFullConversation" /> Date: Mon, 26 Aug 2024 17:26:42 +0300 Subject: [PATCH 027/237] wip --- .../activity/fragment/NewMessageDetailsFragment.kt | 4 ++++ .../ui/adapter/MessagesInThreadListAdapter.kt | 14 +++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt index a5299393ff..029cd70f5f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt @@ -136,6 +136,10 @@ class NewMessageDetailsFragment : BaseFragment binding?.displayFullConversation?.setOnClickListener { v -> v.gone() messagesInThreadListAdapter.showAll() + /*lifecycleScope.launch { + delay(100) + binding?.recyclerViewMessages?.smoothScrollToPosition(0) + }*/ } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt index 8b26a37dea..976d69f353 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -21,6 +21,7 @@ import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.databinding.ItemMessageInThreadBinding import com.flowcrypt.email.extensions.android.widget.useGlideToApplyImageFromSource import com.flowcrypt.email.extensions.jakarta.mail.internet.personalOrEmail +import com.flowcrypt.email.extensions.toast import com.flowcrypt.email.extensions.visibleOrGone import com.flowcrypt.email.util.DateTimeUtil import com.flowcrypt.email.util.graphics.glide.AvatarModelLoader @@ -32,7 +33,7 @@ import jakarta.mail.internet.InternetAddress class MessagesInThreadListAdapter : ListAdapter(DIFF_UTIL_ITEM_CALLBACK) { - private val map = mapOf() + private val collapsedStates = mutableMapOf() private val fullList: MutableList = mutableListOf() override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { @@ -42,6 +43,9 @@ class MessagesInThreadListAdapter : ListAdapter Date: Mon, 26 Aug 2024 17:36:07 +0300 Subject: [PATCH 028/237] wip --- FlowCrypt/src/main/res/layout/fragment_new_message_details.xml | 1 + .../src/main/res/xml/fragment_new_message_details_scene.xml | 1 + 2 files changed, 2 insertions(+) diff --git a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml index fe610d96de..8e28ba1f49 100644 --- a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml @@ -77,6 +77,7 @@ android:layout_height="wrap_content" android:layout_marginStart="@dimen/default_margin_medium" android:layout_marginEnd="@dimen/default_margin_medium" + android:layout_marginBottom="@dimen/default_margin_small" android:nestedScrollingEnabled="false" android:overScrollMode="never" app:layoutManager="com.google.android.flexbox.FlexboxLayoutManager" diff --git a/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml b/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml index 951b4cce18..727c328233 100644 --- a/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml +++ b/FlowCrypt/src/main/res/xml/fragment_new_message_details_scene.xml @@ -33,6 +33,7 @@ motion:layout_constraintEnd_toEndOf="parent" motion:layout_constraintStart_toStartOf="parent" motion:layout_constraintTop_toBottomOf="@+id/header" + android:layout_marginTop="8dp" motion:visibilityMode="ignore" /> Date: Mon, 26 Aug 2024 17:52:59 +0300 Subject: [PATCH 029/237] wip --- .../com/google/api/services/gmail/model/ThreadExt.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt index f795cfd3a2..00da218396 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt @@ -20,7 +20,8 @@ fun Thread.getUniqueRecipients(account: String): List { it.name in listOf( "From", "To", - "Cc" + "Cc", + "Delivered-To" ) && it.value.contains(account, true) } == true) { message.payload?.headers?.filter { header -> From d1fa7d522b7e669501fe0227845623a63185eba3 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 28 Aug 2024 11:20:00 +0300 Subject: [PATCH 030/237] wip --- .../com/flowcrypt/email/database/entity/MessageEntity.kt | 5 ++++- .../email/jetpack/viewmodel/ThreadDetailsViewModel.kt | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index 2df10f0a66..d9664f5576 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.database.entity @@ -100,6 +100,7 @@ data class MessageEntity( @ColumnInfo(name = "is_encrypted", defaultValue = "-1") val isEncrypted: Boolean? = null, @ColumnInfo(name = "has_pgp", defaultValue = "0") val hasPgp: Boolean? = null, @ColumnInfo(name = "thread_messages_count", defaultValue = "NULL") val threadMessagesCount: Int? = null, + @ColumnInfo(name = "snippet", defaultValue = "NULL") val snippet: String? = null, ) : Parcelable { @IgnoredOnParcel @@ -201,6 +202,7 @@ data class MessageEntity( if (isEncrypted != other.isEncrypted) return false if (hasPgp != other.hasPgp) return false if (threadMessagesCount != other.threadMessagesCount) return false + if (snippet != other.snippet) return false return true } @@ -232,6 +234,7 @@ data class MessageEntity( result = 31 * result + (isEncrypted?.hashCode() ?: 0) result = 31 * result + (hasPgp?.hashCode() ?: 0) result = 31 * result + (threadMessagesCount?.hashCode() ?: 0) + result = 31 * result + (snippet?.hashCode() ?: 0) return result } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index 41b95c4b88..56176d99fc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -54,7 +54,9 @@ class ThreadDetailsViewModel( isNew = false, onlyPgpModeEnabled = isOnlyPgpModeEnabled, draftIdsMap = emptyMap() - ) + ) { message, messageEntity -> + messageEntity.copy(snippet = message.snippet) + } emit(messageEntities) } catch (e: Exception) { From c01fcc3056ce0ae61b097c7872ea992f4e5f8e66 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 28 Aug 2024 11:45:24 +0300 Subject: [PATCH 031/237] wip --- ...ailsFragment.kt => GmailThreadFragment.kt} | 4 +- .../fragment/MessageDetailsFragment.kt | 81 ++++++++++++------- .../email/ui/adapter/FragmentsAdapter.kt | 11 +-- .../res/layout/fragment_message_details.xml | 23 +++++- .../src/main/res/navigation/nav_graph.xml | 10 +-- FlowCrypt/src/main/res/values-ru/strings.xml | 10 +++ FlowCrypt/src/main/res/values-uk/strings.xml | 10 +++ FlowCrypt/src/main/res/values/colors.xml | 3 +- FlowCrypt/src/main/res/values/strings.xml | 5 +- 9 files changed, 108 insertions(+), 49 deletions(-) rename FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/{NewMessageDetailsFragment.kt => GmailThreadFragment.kt} (97%) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt similarity index 97% rename from FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt rename to FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt index 029cd70f5f..b024386c84 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/NewMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt @@ -36,7 +36,7 @@ import com.google.android.material.divider.MaterialDividerItemDecoration /** * @author Denys Bondarenko */ -class NewMessageDetailsFragment : BaseFragment(), +class GmailThreadFragment : BaseFragment(), ProgressBehaviour { override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentNewMessageDetailsBinding.inflate(inflater, container, false) @@ -48,7 +48,7 @@ class NewMessageDetailsFragment : BaseFragment override val statusView: View? get() = binding?.status?.root - private val args by navArgs() + private val args by navArgs() private val threadDetailsViewModel: ThreadDetailsViewModel by viewModels { object : CustomAndroidViewModelFactory(requireActivity().application) { @Suppress("UNCHECKED_CAST") diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt index 9bfe097236..e2c3ea1459 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui.activity.fragment @@ -490,8 +490,8 @@ class MessageDetailsFragment : BaseFragment(), Pr } override fun onClick(v: View) { - /*when (v.id) { - R.id.layoutReplyButton -> { + when (v.id) { + R.id.replyButton -> { startActivity( CreateMessageActivity.generateIntent( context, MessageType.REPLY, msgEncryptType, prepareMsgInfoForReply() @@ -499,7 +499,7 @@ class MessageDetailsFragment : BaseFragment(), Pr ) } - R.id.imageButtonReplyAll, R.id.layoutReplyAllButton -> { + R.id.imageButtonReplyAll, R.id.replyAllButton -> { startActivity( CreateMessageActivity.generateIntent( context, MessageType.REPLY_ALL, msgEncryptType, prepareMsgInfoForReply() @@ -513,12 +513,12 @@ class MessageDetailsFragment : BaseFragment(), Pr popup.setOnMenuItemClickListener { when (it.itemId) { R.id.menuActionReply -> { - binding?.layoutReplyButtons?.layoutReplyButton?.let { view -> onClick(view) } + binding?.layoutReplyButtons?.replyButton?.let { view -> onClick(view) } true } R.id.menuActionForward -> { - binding?.layoutReplyButtons?.layoutFwdButton?.let { view -> onClick(view) } + binding?.layoutReplyButtons?.forwardButton?.let { view -> onClick(view) } true } @@ -532,7 +532,7 @@ class MessageDetailsFragment : BaseFragment(), Pr } - R.id.layoutFwdButton -> { + R.id.forwardButton -> { if (attachmentsRecyclerViewAdapter.currentList.none { it.isEmbeddedAndPossiblyEncrypted() }) { @@ -561,7 +561,7 @@ class MessageDetailsFragment : BaseFragment(), Pr ) } } - }*/ + } } override fun onAccountInfoRefreshed(accountEntity: AccountEntity?) { @@ -884,6 +884,29 @@ class MessageDetailsFragment : BaseFragment(), Pr private fun updateViews(messageEntity: MessageEntity) { updateActionBar(messageEntity) + messageEntity.threadMessagesCount?.let { threadMessagesCount -> + if (threadMessagesCount > 1) { + binding?.displayFullConversation?.apply { + visible() + text = resources.getQuantityString( + R.plurals.show_full_conversation, threadMessagesCount, threadMessagesCount + ) + messageEntity.id?.let { + setOnClickListener { + navController?.navigate( + object : NavDirections { + override val actionId = R.id.gmailThreadFragment + override val arguments = GmailThreadFragmentArgs( + messageEntityId = messageEntity.id + ).toBundle() + } + ) + } + } + } + } + } + binding?.imageButtonReplyAll?.visibleOrInvisible( !messageEntity.isOutboxMsg && !messageEntity.isDraft ) @@ -1181,43 +1204,39 @@ class MessageDetailsFragment : BaseFragment(), Pr * Update the reply buttons layout depending on the [MessageEncryptionType] */ private fun updateReplyButtons() { - /*if (binding?.layoutReplyButtons != null) { - val imageViewReply = binding?.layoutReplyButtons?.imageViewReply - val imageViewReplyAll = binding?.layoutReplyButtons?.imageViewReplyAll - val imageViewFwd = binding?.layoutReplyButtons?.imageViewFwd - - val textViewReply = binding?.layoutReplyButtons?.textViewReply - val textViewReplyAll = binding?.layoutReplyButtons?.textViewReplyAll - val textViewFwd = binding?.layoutReplyButtons?.textViewFwd + if (binding?.layoutReplyButtons != null) { + val replyButton = binding?.layoutReplyButtons?.replyButton + val replyAllButton = binding?.layoutReplyButtons?.replyAllButton + val forwardButton = binding?.layoutReplyButtons?.forwardButton val buttonsColorId: Int if (msgEncryptType === MessageEncryptionType.ENCRYPTED) { buttonsColorId = R.color.colorPrimary - textViewReply?.setText(R.string.reply_encrypted) - textViewReplyAll?.setText(R.string.reply_all_encrypted) - textViewFwd?.setText(R.string.forward_encrypted) + replyButton?.setText(R.string.reply_encrypted) + replyAllButton?.setText(R.string.reply_all_encrypted) + forwardButton?.setText(R.string.forward_encrypted) } else { buttonsColorId = R.color.red - textViewReply?.setText(R.string.reply) - textViewReplyAll?.setText(R.string.reply_all) - textViewFwd?.setText(R.string.forward) + replyButton?.setText(R.string.reply) + replyAllButton?.setText(R.string.reply_all) + forwardButton?.setText(R.string.forward) } - val colorStateList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), buttonsColorId)) + val colorStateList = + ColorStateList.valueOf(ContextCompat.getColor(requireContext(), buttonsColorId)) - binding?.imageButtonReplyAll?.imageTintList = colorStateList - imageViewReply?.imageTintList = colorStateList - imageViewReplyAll?.imageTintList = colorStateList - imageViewFwd?.imageTintList = colorStateList + replyButton?.iconTint = colorStateList + replyAllButton?.iconTint = colorStateList + forwardButton?.iconTint = colorStateList - binding?.layoutReplyButtons?.layoutReplyButton?.setOnClickListener(this) - binding?.layoutReplyButtons?.layoutFwdButton?.setOnClickListener(this) - binding?.layoutReplyButtons?.layoutReplyAllButton?.setOnClickListener(this) + replyButton?.setOnClickListener(this) + replyAllButton?.setOnClickListener(this) + forwardButton?.setOnClickListener(this) binding?.layoutReplyButtons?.root?.visibleOrGone( !args.messageEntity.isOutboxMsg && !args.messageEntity.isDraft ) - }*/ + } } /** diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt index 58568ac4c4..5af69ccccd 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt @@ -11,8 +11,8 @@ import androidx.recyclerview.widget.DiffUtil import androidx.viewpager2.adapter.FragmentStateAdapter import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.entity.MessageEntity -import com.flowcrypt.email.ui.activity.fragment.NewMessageDetailsFragment -import com.flowcrypt.email.ui.activity.fragment.NewMessageDetailsFragmentArgs +import com.flowcrypt.email.ui.activity.fragment.MessageDetailsFragment +import com.flowcrypt.email.ui.activity.fragment.MessageDetailsFragmentArgs /** * @author Denys Bondarenko @@ -45,9 +45,10 @@ class FragmentsAdapter( override fun getItemCount(): Int = asyncListDiffer.currentList.size override fun createFragment(position: Int): Fragment = - NewMessageDetailsFragment().apply { - arguments = NewMessageDetailsFragmentArgs( - messageEntityId = requireNotNull(asyncListDiffer.currentList[position].id), + MessageDetailsFragment().apply { + arguments = MessageDetailsFragmentArgs( + messageEntity = asyncListDiffer.currentList[position], + localFolder = localFolder, isViewPagerMode = true ).toBundle() } diff --git a/FlowCrypt/src/main/res/layout/fragment_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_message_details.xml index 504e21c919..3531df416f 100644 --- a/FlowCrypt/src/main/res/layout/fragment_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_message_details.xml @@ -1,6 +1,6 @@ + + + app:layout_constraintTop_toBottomOf="@+id/displayFullConversation" /> - + + Тема Отправить @@ -631,4 +636,9 @@ Пожалуйста, укажите здесь свою подпись Сделайте резервную копию в почтовом ящике Сохранить предоставленные закрытые ключи PGP, защищенные парольной фразой, в качестве резервной копии в папке «Входящие». Это поможет Вам получить доступ к Вашим зашифрованным сообщениям с других устройств (при условии предоставление Вашей парольной фразы). Вы можете безопасно оставить его в своем почтовом ящике или заархивировать.\n\nЕсли Вам не нужна такая резервная копия, отключите эту опцию. + + Показать всю переписку из %1$d сообщения + Показать всю переписку из %1$d сообщений + Показать всю переписку из %1$d сообщений + diff --git a/FlowCrypt/src/main/res/values-uk/strings.xml b/FlowCrypt/src/main/res/values-uk/strings.xml index d57149d9a1..7b89f46502 100644 --- a/FlowCrypt/src/main/res/values-uk/strings.xml +++ b/FlowCrypt/src/main/res/values-uk/strings.xml @@ -1,5 +1,10 @@ + + Тема Відправити @@ -632,4 +637,9 @@ Будь ласка, вкажіть свій підпис тут Зробіть резервну копію в електронній скриньці Зберегти надані секретні ключі PGP, захищені парольною фразою, як резервну копію в папці «Вхідні». Це допоможе отримати Вам доступ до Ваших зашифрованих повідомлень з інших пристроїв (за умови надання Вашої парольної фрази). Ви можете спокійно залишити його у папці "Вхідні" або заархівувати.\n\nЯкщо Вам не потрібна така резервна копія, вимкніть цю опцію. + + Показати повне листування із %1$d повідомлення + Показати повне листування із %1$d повідомленнь + Показати повне листування із %1$d повідомленнь + diff --git a/FlowCrypt/src/main/res/values/colors.xml b/FlowCrypt/src/main/res/values/colors.xml index 8ddbbd9c43..5ecec80a1f 100644 --- a/FlowCrypt/src/main/res/values/colors.xml +++ b/FlowCrypt/src/main/res/values/colors.xml @@ -1,6 +1,6 @@ @@ -13,6 +13,7 @@ #F0F0F0 #C6C6C6 #989898 + #808080 #535353 #A0FFFFFF #C27E23 diff --git a/FlowCrypt/src/main/res/values/strings.xml b/FlowCrypt/src/main/res/values/strings.xml index 012d42e375..0049c0a1a2 100644 --- a/FlowCrypt/src/main/res/values/strings.xml +++ b/FlowCrypt/src/main/res/values/strings.xml @@ -1,6 +1,6 @@ @@ -648,4 +648,7 @@ Please specify your signature here Make a backup in the email box Save the given passphrase-protected PGP private keys as a backup in the inbox. It will help you access your encrypted messages from other devices (along with your pass phrase). You can safely leave it in your inbox or archive it.\n\nIf you don\'t need to have such a backup, please disable this option. + + Show a full conversation of %1$d messages + From a830418d5615fb6c1f2e6406131a065f39416c39 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 28 Aug 2024 18:28:09 +0300 Subject: [PATCH 032/237] wip --- .../email/database/entity/MessageEntity.kt | 3 + .../viewmodel/ThreadDetailsViewModel.kt | 12 +- .../activity/fragment/GmailThreadFragment.kt | 46 +-- .../fragment/MessageDetailsFragment.kt | 3 +- .../ui/adapter/MessagesInThreadListAdapter.kt | 47 +-- .../adapter/MsgDetailsRecyclerViewAdapter.kt | 11 +- .../main/res/drawable/ic_attach_file_16.xml | 17 + .../layout/fragment_new_message_details.xml | 29 +- .../res/layout/item_message_in_thread.xml | 332 ++++++------------ .../src/main/res/navigation/nav_graph.xml | 3 + .../fragment_new_message_details_scene.xml | 23 +- 11 files changed, 195 insertions(+), 331 deletions(-) create mode 100644 FlowCrypt/src/main/res/drawable/ic_attach_file_16.xml diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index d9664f5576..bade2f5b26 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -101,6 +101,7 @@ data class MessageEntity( @ColumnInfo(name = "has_pgp", defaultValue = "0") val hasPgp: Boolean? = null, @ColumnInfo(name = "thread_messages_count", defaultValue = "NULL") val threadMessagesCount: Int? = null, @ColumnInfo(name = "snippet", defaultValue = "NULL") val snippet: String? = null, + @ColumnInfo(name = "is_visible", defaultValue = "1") val isVisible: Boolean = true, ) : Parcelable { @IgnoredOnParcel @@ -203,6 +204,7 @@ data class MessageEntity( if (hasPgp != other.hasPgp) return false if (threadMessagesCount != other.threadMessagesCount) return false if (snippet != other.snippet) return false + if (isVisible != other.isVisible) return false return true } @@ -235,6 +237,7 @@ data class MessageEntity( result = 31 * result + (hasPgp?.hashCode() ?: 0) result = 31 * result + (threadMessagesCount?.hashCode() ?: 0) result = 31 * result + (snippet?.hashCode() ?: 0) + result = 31 * result + isVisible.hashCode() return result } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index 56176d99fc..d15c3685b5 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -8,6 +8,7 @@ package com.flowcrypt.email.jetpack.viewmodel import android.app.Application import androidx.lifecycle.asFlow import com.flowcrypt.email.api.email.gmail.GmailApiHelper +import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly import com.flowcrypt.email.extensions.kotlin.toHex @@ -55,9 +56,18 @@ class ThreadDetailsViewModel( onlyPgpModeEnabled = isOnlyPgpModeEnabled, draftIdsMap = emptyMap() ) { message, messageEntity -> - messageEntity.copy(snippet = message.snippet) + messageEntity.copy(snippet = message.snippet, isVisible = false) } + roomDatabase.msgDao().insertWithReplaceSuspend(messageEntities) + GmailApiHelper.identifyAttachments( + msgEntities = messageEntities, + msgs = messagesInThread, + account = activeAccount, + localFolder = LocalFolder(activeAccount.email, GmailApiHelper.LABEL_INBOX),//fix me + roomDatabase = roomDatabase + ) + emit(messageEntities) } catch (e: Exception) { e.printStackTraceIfDebugOnly() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt index b024386c84..58703e8a18 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt @@ -13,14 +13,18 @@ import android.view.ViewGroup import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel +import androidx.lifecycle.lifecycleScope import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import com.flowcrypt.email.R +import com.flowcrypt.email.api.email.gmail.GmailApiHelper +import com.flowcrypt.email.api.email.model.LocalFolder +import com.flowcrypt.email.database.FlowCryptRoomDatabase +import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.databinding.FragmentNewMessageDetailsBinding import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithViewLifecycle +import com.flowcrypt.email.extensions.androidx.fragment.app.navController import com.flowcrypt.email.extensions.androidx.fragment.app.toast -import com.flowcrypt.email.extensions.gone -import com.flowcrypt.email.extensions.visible import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.ThreadDetailsViewModel import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment @@ -32,6 +36,7 @@ import com.google.android.flexbox.FlexDirection import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.JustifyContent import com.google.android.material.divider.MaterialDividerItemDecoration +import kotlinx.coroutines.launch /** * @author Denys Bondarenko @@ -67,7 +72,25 @@ class GmailThreadFragment : BaseFragment(), } }) - private val messagesInThreadListAdapter = MessagesInThreadListAdapter() + private val messagesInThreadListAdapter = + MessagesInThreadListAdapter(object : MessagesInThreadListAdapter.OnMessageClickListener { + override fun onMessageClick(messageEntity: MessageEntity) { + lifecycleScope.launch { + FlowCryptRoomDatabase.getDatabase(requireContext()).msgDao() + .getMsgSuspend(messageEntity.account, messageEntity.folder, messageEntity.uid)?.let { + navController?.navigate( + GmailThreadFragmentDirections.actionGmailThreadFragmentToMessageDetailsFragment( + messageEntity = it, + localFolder = LocalFolder( + messageEntity.account, + GmailApiHelper.LABEL_INBOX + )//fix me + ) + ) + } + } + } + }) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) @@ -85,14 +108,6 @@ class GmailThreadFragment : BaseFragment(), launchAndRepeatWithViewLifecycle { threadDetailsViewModel.messagesInThreadFlow.collect { messagesInThreadListAdapter.submitList(it) - - if (it.size > 1) { - binding?.displayFullConversation?.visible() - binding?.displayFullConversation?.text = "Show full conversation(${it.size - 1} left)" - } else { - binding?.displayFullConversation?.gone() - } - updateReplyButtons(it.lastOrNull()?.hasPgp == true) showContent() } @@ -132,15 +147,6 @@ class GmailThreadFragment : BaseFragment(), ) adapter = gmailApiLabelsListAdapter } - - binding?.displayFullConversation?.setOnClickListener { v -> - v.gone() - messagesInThreadListAdapter.showAll() - /*lifecycleScope.launch { - delay(100) - binding?.recyclerViewMessages?.smoothScrollToPosition(0) - }*/ - } } private fun updateReplyButtons(usePgpMode: Boolean) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt index e2c3ea1459..856fc38f95 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt @@ -187,7 +187,7 @@ class MessageDetailsFragment : BaseFragment(), Pr } } - val requestPermissionLauncher = + private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean -> if (isGranted) { downloadAttachment() @@ -1226,6 +1226,7 @@ class MessageDetailsFragment : BaseFragment(), Pr val colorStateList = ColorStateList.valueOf(ContextCompat.getColor(requireContext(), buttonsColorId)) + binding?.imageButtonReplyAll?.imageTintList = colorStateList replyButton?.iconTint = colorStateList replyAllButton?.iconTint = colorStateList forwardButton?.iconTint = colorStateList diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt index 976d69f353..03574d5a87 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -11,7 +11,6 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import androidx.core.text.toSpannable -import androidx.core.view.isVisible import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -21,7 +20,6 @@ import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.databinding.ItemMessageInThreadBinding import com.flowcrypt.email.extensions.android.widget.useGlideToApplyImageFromSource import com.flowcrypt.email.extensions.jakarta.mail.internet.personalOrEmail -import com.flowcrypt.email.extensions.toast import com.flowcrypt.email.extensions.visibleOrGone import com.flowcrypt.email.util.DateTimeUtil import com.flowcrypt.email.util.graphics.glide.AvatarModelLoader @@ -30,11 +28,8 @@ import jakarta.mail.internet.InternetAddress /** * @author Denys Bondarenko */ -class MessagesInThreadListAdapter : ListAdapter(DIFF_UTIL_ITEM_CALLBACK) { - - private val collapsedStates = mutableMapOf() - private val fullList: MutableList = mutableListOf() +class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageClickListener) : + ListAdapter(DIFF_UTIL_ITEM_CALLBACK) { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { return ViewHolder( @@ -43,48 +38,30 @@ class MessagesInThreadListAdapter : ListAdapter?) { - fullList.clear() - fullList.addAll(list ?: emptyList()) - super.submitList(list?.filterIndexed { index, _ -> index == list.size - 1 }) + holder.bindTo(getItem(position), onMessageClickListener) } - fun showAll() { - super.submitList(fullList) + interface OnMessageClickListener { + fun onMessageClick(messageEntity: MessageEntity) } inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { val binding = ItemMessageInThreadBinding.bind(itemView) - fun bindTo(item: MessageEntity, isTheLast: Boolean) { + fun bindTo(item: MessageEntity, onMessageClickListener: OnMessageClickListener) { val context = itemView.context - val isCollapsed = collapsedStates[item.uid] - binding.groupCollapsibleContent.visibleOrGone(collapsedStates[item.uid] ?: false) - binding.header.setOnClickListener { - val newState = !binding.groupCollapsibleContent.isVisible - collapsedStates[item.uid] = newState - binding.groupCollapsibleContent.visibleOrGone(newState) - } - + itemView.setOnClickListener { onMessageClickListener.onMessageClick(item) } val senderAddress = EmailUtil.getFirstAddressString(item.from) binding.imageViewAvatar.useGlideToApplyImageFromSource( source = AvatarModelLoader.SCHEMA_AVATAR + senderAddress ) - binding.textViewSenderAddress.text = senderAddress + binding.textViewSnippet.text = item.snippet + binding.textViewSender.text = senderAddress binding.tVTo.text = prepareToText(context, item) - binding.textViewDate.text = - DateTimeUtil.formatSameDayTime(context, item.receivedDate ?: 0) - - if (isTheLast && isCollapsed == null) { - binding.header.callOnClick() - } + binding.textViewDate.text = DateTimeUtil.formatSameDayTime(context, item.receivedDate ?: 0) + binding.viewHasPgp.visibleOrGone(item.hasPgp == true || item.isEncrypted == true) + binding.viewHasAttachments.visibleOrGone(item.hasAttachments == true) } private fun prepareToText(context: Context, messageEntity: MessageEntity): String { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgDetailsRecyclerViewAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgDetailsRecyclerViewAdapter.kt index a3860ae19c..b9f03f0622 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgDetailsRecyclerViewAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgDetailsRecyclerViewAdapter.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui.adapter @@ -8,11 +8,11 @@ package com.flowcrypt.email.ui.adapter import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.flowcrypt.email.R +import com.flowcrypt.email.databinding.ItemMimeHeaderBinding /** * @author Denys Bondarenko @@ -32,12 +32,11 @@ class MsgDetailsRecyclerViewAdapter : } inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { - val tVHeaderName: TextView = itemView.findViewById(R.id.tVHeaderName) - val tVHeaderValue: TextView = itemView.findViewById(R.id.tVHeaderValue) + val binding = ItemMimeHeaderBinding.bind(itemView) fun bindTo(item: Header?) { - tVHeaderName.text = item?.name - tVHeaderValue.text = item?.value + binding.tVHeaderName.text = item?.name + binding.tVHeaderValue.text = item?.value } } diff --git a/FlowCrypt/src/main/res/drawable/ic_attach_file_16.xml b/FlowCrypt/src/main/res/drawable/ic_attach_file_16.xml new file mode 100644 index 0000000000..0a4bd53e04 --- /dev/null +++ b/FlowCrypt/src/main/res/drawable/ic_attach_file_16.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml index 8e28ba1f49..ee5e134729 100644 --- a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml @@ -31,12 +31,11 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - - - @@ -133,6 +112,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" /> - + diff --git a/FlowCrypt/src/main/res/layout/item_message_in_thread.xml b/FlowCrypt/src/main/res/layout/item_message_in_thread.xml index 227610d703..da6b14b3ac 100644 --- a/FlowCrypt/src/main/res/layout/item_message_in_thread.xml +++ b/FlowCrypt/src/main/res/layout/item_message_in_thread.xml @@ -1,6 +1,6 @@ - - + + - - - - - - - - + app:layout_constraintTop_toTopOf="parent" + app:srcCompat="@drawable/ic_account_default_photo" /> - - - - - - + + + - - - - + app:layout_constraintTop_toBottomOf="@+id/textViewDate" + app:tint="@color/gray" + tools:visibility="visible" /> - - - - - - - - - - - - - - + app:layout_constraintStart_toEndOf="@+id/textViewSender" + app:layout_constraintTop_toTopOf="@+id/tVTo" + tools:text="1:05 PM" /> - - - - - - - - - - - + android:layout_marginTop="@dimen/default_margin_content_small" + android:ellipsize="end" + android:maxLines="1" + android:textAlignment="textStart" + app:layout_constrainedWidth="true" + app:layout_constraintHorizontal_chainStyle="packed" + app:layout_constraintStart_toEndOf="@+id/imageViewAvatar" + app:layout_constraintStart_toStartOf="@+id/textViewSender" + app:layout_constraintTop_toBottomOf="@+id/textViewSender" + tools:text="To me" /> + + + diff --git a/FlowCrypt/src/main/res/navigation/nav_graph.xml b/FlowCrypt/src/main/res/navigation/nav_graph.xml index 4c2ebf53ba..4fc9c5da45 100644 --- a/FlowCrypt/src/main/res/navigation/nav_graph.xml +++ b/FlowCrypt/src/main/res/navigation/nav_graph.xml @@ -333,6 +333,9 @@ + - - + motion:layout_constraintTop_toBottomOf="@+id/header" /> - - + motion:layout_constraintTop_toBottomOf="@+id/header" /> Date: Thu, 29 Aug 2024 10:40:37 +0300 Subject: [PATCH 033/237] wip --- .../flowcrypt/email/api/email/EmailUtil.kt | 14 +- .../email/database/dao/MessageDao.kt | 8 +- .../email/database/entity/MessageEntity.kt | 155 ++++++++++++++++++ .../jetpack/viewmodel/MessagesViewModel.kt | 7 +- .../activity/fragment/GmailThreadFragment.kt | 36 ---- .../fragment/MessageDetailsFragment.kt | 30 +--- .../ui/adapter/MessagesInThreadListAdapter.kt | 40 +---- .../email/ui/adapter/MsgsPagedListAdapter.kt | 112 +------------ .../layout/fragment_new_message_details.xml | 91 ++++------ FlowCrypt/src/main/res/values-ru/strings.xml | 1 + FlowCrypt/src/main/res/values-uk/strings.xml | 1 + FlowCrypt/src/main/res/values/strings.xml | 1 + 12 files changed, 222 insertions(+), 274 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt index 0dc3f6f05d..942f53a788 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/EmailUtil.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.api.email @@ -615,11 +615,15 @@ class EmailUtil { } as HashMap } - fun hasEncryptedData(rawMsg: String?) = - rawMsg?.contains("-----BEGIN PGP MESSAGE-----") == true + fun hasEncryptedData(rawMsg: String?): Boolean { + return "^-----BEGIN PGP MESSAGE-----".toRegex(RegexOption.MULTILINE) + .containsMatchIn(rawMsg ?: "") + } - fun hasSignedData(rawMsg: String?) = - rawMsg?.contains("-----BEGIN PGP SIGNED MESSAGE-----") == true + fun hasSignedData(rawMsg: String?): Boolean { + return "^-----BEGIN PGP SIGNED MESSAGE-----".toRegex(RegexOption.MULTILINE) + .containsMatchIn(rawMsg ?: "") + } fun genPgpThingsSearchTerm(account: AccountEntity): SearchTerm { return if (AccountEntity.ACCOUNT_TYPE_GOOGLE.equals(account.accountType, ignoreCase = true)) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt index c2e63866f7..cadee92222 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.account.database.dao @@ -103,7 +103,7 @@ abstract class MessageDao : BaseDao { @Query( "SELECT * FROM messages " + - "WHERE account = :account AND folder = :folder ORDER BY received_date DESC" + "WHERE account = :account AND folder = :folder AND is_visible = 1 ORDER BY received_date DESC" ) abstract fun getMessagesDataSourceFactory( account: String, @@ -113,13 +113,13 @@ abstract class MessageDao : BaseDao { @Query( "SELECT * FROM (" + "SELECT * FROM messages " + - "WHERE account = :account AND folder = :folder AND received_date > :date " + + "WHERE account = :account AND folder = :folder AND is_visible = 1 AND received_date > :date " + "ORDER BY received_date ASC " + "LIMIT :limit) " + "UNION " + "SELECT * FROM (" + "SELECT * FROM messages " + - "WHERE account = :account AND folder = :folder AND received_date <= :date " + + "WHERE account = :account AND folder = :folder AND is_visible = 1 AND received_date <= :date " + "ORDER BY received_date DESC " + "LIMIT :limit) " + "ORDER BY received_date DESC" diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index bade2f5b26..df8116d5db 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -9,6 +9,10 @@ import android.content.ContentValues import android.content.Context import android.os.Parcelable import android.provider.BaseColumns +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.Spanned +import android.text.style.AbsoluteSizeSpan import androidx.preference.PreferenceManager import androidx.room.ColumnInfo import androidx.room.Entity @@ -17,7 +21,9 @@ import androidx.room.Ignore import androidx.room.Index import androidx.room.PrimaryKey import com.flowcrypt.email.Constants +import com.flowcrypt.email.R import com.flowcrypt.email.api.email.EmailUtil +import com.flowcrypt.email.api.email.FoldersManager import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.gmail.api.GmaiAPIMimeMessage @@ -26,12 +32,14 @@ import com.flowcrypt.email.api.email.model.OutgoingMessageInfo import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasPgp import com.flowcrypt.email.extensions.jakarta.mail.hasPgp +import com.flowcrypt.email.extensions.jakarta.mail.internet.personalOrEmail import com.flowcrypt.email.extensions.kotlin.asInternetAddresses import com.flowcrypt.email.extensions.kotlin.capitalize import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.extensions.uid import com.flowcrypt.email.ui.activity.fragment.preferences.NotificationsSettingsFragment import com.flowcrypt.email.ui.adapter.GmailApiLabelsListAdapter +import com.flowcrypt.email.ui.adapter.MsgsPagedListAdapter.MessageViewHolder.Companion.SENDER_NAME_PATTERN import com.flowcrypt.email.util.SharedPreferencesHelper import com.google.android.gms.common.util.CollectionUtils import jakarta.mail.Flags @@ -100,6 +108,10 @@ data class MessageEntity( @ColumnInfo(name = "is_encrypted", defaultValue = "-1") val isEncrypted: Boolean? = null, @ColumnInfo(name = "has_pgp", defaultValue = "0") val hasPgp: Boolean? = null, @ColumnInfo(name = "thread_messages_count", defaultValue = "NULL") val threadMessagesCount: Int? = null, + @ColumnInfo( + name = "thread_recipients_addresses", + defaultValue = "NULL" + ) val threadRecipientsAddresses: String? = null, @ColumnInfo(name = "snippet", defaultValue = "NULL") val snippet: String? = null, @ColumnInfo(name = "is_visible", defaultValue = "1") val isVisible: Boolean = true, ) : Parcelable { @@ -120,6 +132,11 @@ data class MessageEntity( @Ignore val cc: List = ccAddresses.asInternetAddresses().asList() + @IgnoredOnParcel + @Ignore + val threadRecipients: List = + threadRecipientsAddresses.asInternetAddresses().asList() + @IgnoredOnParcel @Ignore val msgState: MessageState = MessageState.generate(state ?: MessageState.NONE.value) @@ -203,6 +220,7 @@ data class MessageEntity( if (isEncrypted != other.isEncrypted) return false if (hasPgp != other.hasPgp) return false if (threadMessagesCount != other.threadMessagesCount) return false + if (threadRecipientsAddresses != other.threadRecipientsAddresses) return false if (snippet != other.snippet) return false if (isVisible != other.isVisible) return false @@ -236,11 +254,148 @@ data class MessageEntity( result = 31 * result + (isEncrypted?.hashCode() ?: 0) result = 31 * result + (hasPgp?.hashCode() ?: 0) result = 31 * result + (threadMessagesCount?.hashCode() ?: 0) + result = 31 * result + (threadRecipientsAddresses?.hashCode() ?: 0) result = 31 * result + (snippet?.hashCode() ?: 0) result = 31 * result + isVisible.hashCode() return result } + fun generateSenderAddress( + context: Context, + folderType: FoldersManager.FolderType? + ): CharSequence { + val accountName = account + val addresses = when (folderType) { + FoldersManager.FolderType.SENT -> generateAddresses( + context = context, + accountName = accountName, + internetAddresses = to + ) + + FoldersManager.FolderType.DRAFTS -> generateAddresses( + context = context, + accountName = accountName, + internetAddresses = to + ).ifEmpty { + context.getString(R.string.no_recipients) + } + + else -> generateAddresses( + context = context, + accountName = accountName, + internetAddresses = if (threadId != null) { + threadRecipients + } else { + from + } + ) + } + + return if ((threadMessagesCount ?: 0) > 1) { + SpannableStringBuilder(addresses).apply { + val spannableStringForThreadMessageCount = SpannableString( + "(${threadMessagesCount})" + ).apply { + val textSize = + context.resources.getDimensionPixelSize(R.dimen.default_text_size_small) + setSpan( + AbsoluteSizeSpan(textSize), + 0, + length, + Spanned.SPAN_INCLUSIVE_INCLUSIVE + ) + } + append(" ") + append(spannableStringForThreadMessageCount) + } + } else { + addresses + } + } + + fun generateToText(context: Context): String { + val allRecipients = to + cc + val meAddress = allRecipients.firstOrNull { + it.address.equals(account, true) + } + val sortedAllRecipients = if (meAddress == null) { + allRecipients + } else { + listOf(meAddress) + (allRecipients - listOf(meAddress).toSet()) + } + val uniqueRecipientsMap = mutableMapOf() + + sortedAllRecipients.forEach { internetAddress -> + val address = internetAddress.address.lowercase() + + if (!uniqueRecipientsMap.contains(address) + || uniqueRecipientsMap[address]?.personal.isNullOrEmpty() + ) { + uniqueRecipientsMap[address] = internetAddress + } + } + + return context.getString( + R.string.to_receiver, + uniqueRecipientsMap.values.joinToString { + if (it.address.equals(account, true)) { + context.getString(R.string.me) + } else { + it.personalOrEmail.split(" ").firstOrNull() ?: it.personalOrEmail + } + }) + } + + private fun generateAddresses( + context: Context, + accountName: String, + internetAddresses: List? + ): String { + if (internetAddresses == null) { + return context.getString(R.string.no_recipients) + } + + val mapOfUniqueInternetAddresses = mutableMapOf() + internetAddresses.forEach { internetAddress -> + val existingInternetAddress = + mapOfUniqueInternetAddresses[internetAddress.address.lowercase()] + if (existingInternetAddress == null || existingInternetAddress.personal?.isEmpty() == true) { + mapOfUniqueInternetAddresses[internetAddress.address.lowercase()] = internetAddress + } + } + + val uniqueRecipients = mapOfUniqueInternetAddresses.values.map { internetAddress -> + if (accountName.equals(internetAddress.address, true)) { + context.getString(R.string.me) + } else { + if (internetAddress.personal.isNullOrEmpty()) { + internetAddress.address + } else { + internetAddress.personal + } + } + }.toSet() + + return generateSenderName(uniqueRecipients.joinToString { + if (uniqueRecipients.size > 1) { + it.split(" ").firstOrNull() ?: it + } else it + }) + } + + /** + * Prepare the sender name. + * + * Remove common mail domains: gmail.com, yahoo.com, live.com, outlook.com + * + * + * @param name An incoming name + * @return A generated sender name. + */ + private fun generateSenderName(name: String): String { + return SENDER_NAME_PATTERN.matcher(name).replaceFirst("") + } + companion object { const val TABLE_NAME = "messages" const val LABEL_IDS_SEPARATOR = " " diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index 3a8b6b0664..88482a5b96 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.viewmodel @@ -549,8 +549,9 @@ class MessagesViewModel(application: Application) : AccountViewModel(application threadMessagesCount = thread.messagesCount, labelIds = thread.labels.joinToString(separator = LABEL_IDS_SEPARATOR), hasAttachments = thread.hasAttachments, - fromAddresses = InternetAddress.toString(thread.recipients.toTypedArray()), - toAddresses = InternetAddress.toString(thread.recipients.toTypedArray()), + threadRecipientsAddresses = InternetAddress.toString( + thread.recipients.toTypedArray() + ), hasPgp = thread.hasPgpThings ) } else { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt index 58703e8a18..dba44e304e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt @@ -5,12 +5,10 @@ package com.flowcrypt.email.ui.activity.fragment -import android.content.res.ColorStateList import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.content.ContextCompat import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope @@ -108,7 +106,6 @@ class GmailThreadFragment : BaseFragment(), launchAndRepeatWithViewLifecycle { threadDetailsViewModel.messagesInThreadFlow.collect { messagesInThreadListAdapter.submitList(it) - updateReplyButtons(it.lastOrNull()?.hasPgp == true) showContent() } } @@ -148,37 +145,4 @@ class GmailThreadFragment : BaseFragment(), adapter = gmailApiLabelsListAdapter } } - - private fun updateReplyButtons(usePgpMode: Boolean) { - if (binding?.layoutReplyButtons != null) { - val imageViewReply = binding?.layoutReplyButtons?.replyButton - val imageViewReplyAll = binding?.layoutReplyButtons?.replyAllButton - val imageViewFwd = binding?.layoutReplyButtons?.forwardButton - - val buttonsColorId: Int - - if (usePgpMode) { - buttonsColorId = R.color.colorPrimary - imageViewReply?.setText(R.string.reply_encrypted) - imageViewReplyAll?.setText(R.string.reply_all_encrypted) - imageViewFwd?.setText(R.string.forward_encrypted) - } else { - buttonsColorId = R.color.red - imageViewReply?.setText(R.string.reply) - imageViewReplyAll?.setText(R.string.reply_all) - imageViewFwd?.setText(R.string.forward) - } - - val colorStateList = - ColorStateList.valueOf(ContextCompat.getColor(requireContext(), buttonsColorId)) - - imageViewReply?.iconTint = colorStateList - imageViewReplyAll?.iconTint = colorStateList - imageViewFwd?.iconTint = colorStateList - - /*binding?.layoutReplyButtons?.layoutReplyButton?.setOnClickListener(this) - binding?.layoutReplyButtons?.layoutFwdButton?.setOnClickListener(this) - binding?.layoutReplyButtons?.layoutReplyAllButton?.setOnClickListener(this)*/ - } - } } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt index 856fc38f95..c21d225cdf 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt @@ -94,7 +94,6 @@ import com.flowcrypt.email.extensions.exceptionMsgWithStack import com.flowcrypt.email.extensions.gone import com.flowcrypt.email.extensions.incrementSafely import com.flowcrypt.email.extensions.jakarta.mail.internet.getFormattedString -import com.flowcrypt.email.extensions.jakarta.mail.internet.personalOrEmail import com.flowcrypt.email.extensions.visible import com.flowcrypt.email.extensions.visibleOrGone import com.flowcrypt.email.extensions.visibleOrInvisible @@ -942,7 +941,7 @@ class MessageDetailsFragment : BaseFragment(), Pr } private fun updateMsgDetails(messageEntity: MessageEntity) { - binding?.tVTo?.text = prepareToText() + binding?.tVTo?.text = messageEntity.generateToText(requireContext()) val headers = mutableListOf().apply { add( @@ -1001,33 +1000,6 @@ class MessageDetailsFragment : BaseFragment(), Pr return DateUtils.formatDateTime(context, dateInMilliseconds, flags) } - private fun prepareToText(): String { - val stringBuilder = SpannableStringBuilder() - val meAddress = args.messageEntity.to.firstOrNull { - it.address.equals(args.messageEntity.account, true) - } - val leftAddresses: List - if (meAddress == null) { - leftAddresses = args.messageEntity.to - } else { - stringBuilder.append(getString(R.string.me)) - leftAddresses = ArrayList(args.messageEntity.to) - meAddress - if (leftAddresses.isNotEmpty()) { - stringBuilder.append(", ") - } - } - - val to = leftAddresses.foldIndexed(stringBuilder) { index, builder, it -> - builder.append(it.personalOrEmail) - if (index != leftAddresses.size - 1) { - builder.append(",") - } - builder - }.toSpannable() - - return getString(R.string.to_receiver, to) - } - private fun formatAddresses(addresses: List) = addresses.foldIndexed(SpannableStringBuilder()) { index, builder, it -> if (index < MAX_ALLOWED_RECIPIENTS_IN_HEADER_VALUE) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt index 03574d5a87..b8762408e6 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -5,12 +5,9 @@ package com.flowcrypt.email.ui.adapter -import android.content.Context -import android.text.SpannableStringBuilder import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.core.text.toSpannable import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -19,11 +16,9 @@ import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.databinding.ItemMessageInThreadBinding import com.flowcrypt.email.extensions.android.widget.useGlideToApplyImageFromSource -import com.flowcrypt.email.extensions.jakarta.mail.internet.personalOrEmail import com.flowcrypt.email.extensions.visibleOrGone import com.flowcrypt.email.util.DateTimeUtil import com.flowcrypt.email.util.graphics.glide.AvatarModelLoader -import jakarta.mail.internet.InternetAddress /** * @author Denys Bondarenko @@ -56,40 +51,17 @@ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageC binding.imageViewAvatar.useGlideToApplyImageFromSource( source = AvatarModelLoader.SCHEMA_AVATAR + senderAddress ) - binding.textViewSnippet.text = item.snippet + binding.textViewSnippet.text = if (item.hasPgp == true) { + context.getString(R.string.preview_is_not_available_for_messages_with_pgp) + } else { + item.snippet + } binding.textViewSender.text = senderAddress - binding.tVTo.text = prepareToText(context, item) + binding.tVTo.text = item.generateToText(context) binding.textViewDate.text = DateTimeUtil.formatSameDayTime(context, item.receivedDate ?: 0) binding.viewHasPgp.visibleOrGone(item.hasPgp == true || item.isEncrypted == true) binding.viewHasAttachments.visibleOrGone(item.hasAttachments == true) } - - private fun prepareToText(context: Context, messageEntity: MessageEntity): String { - val stringBuilder = SpannableStringBuilder() - val meAddress = messageEntity.to.firstOrNull { - it.address.equals(messageEntity.account, true) - } - val leftAddresses: List - if (meAddress == null) { - leftAddresses = messageEntity.to - } else { - stringBuilder.append(context.getString(R.string.me)) - leftAddresses = ArrayList(messageEntity.to) - meAddress - if (leftAddresses.isNotEmpty()) { - stringBuilder.append(", ") - } - } - - val to = leftAddresses.foldIndexed(stringBuilder) { index, builder, it -> - builder.append(it.personalOrEmail) - if (index != leftAddresses.size - 1) { - builder.append(",") - } - builder - }.toSpannable() - - return context.getString(R.string.to_receiver, to) - } } companion object { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt index 3952b040b0..2e55b9f3b2 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui.adapter @@ -10,7 +10,6 @@ import android.graphics.Camera import android.graphics.Color import android.graphics.Typeface import android.text.SpannableString -import android.text.SpannableStringBuilder import android.text.Spanned import android.text.TextUtils import android.text.style.AbsoluteSizeSpan @@ -52,7 +51,6 @@ import com.google.android.flexbox.FlexDirection import com.google.android.flexbox.FlexboxLayoutManager import com.google.android.flexbox.JustifyContent import com.google.android.material.color.MaterialColors -import jakarta.mail.internet.InternetAddress import java.util.regex.Pattern /** @@ -231,7 +229,11 @@ class MsgsPagedListAdapter(private val onMessageClickListener: OnMessageClickLis } ) - val senderAddress = prepareSenderAddress(context, folderType, messageEntity) + val senderAddress = when (folderType) { + FoldersManager.FolderType.OUTBOX -> generateOutboxStatus(context, messageEntity.msgState) + + else -> messageEntity.generateSenderAddress(context, folderType) + } binding.textViewSenderAddress.text = senderAddress updateAvatar(senderAddress, folderType, lastDataId == messageEntity.id) @@ -300,58 +302,6 @@ class MsgsPagedListAdapter(private val onMessageClickListener: OnMessageClickLis lastDataId = messageEntity?.id } - private fun prepareSenderAddress( - context: Context, - folderType: FoldersManager.FolderType?, - messageEntity: MessageEntity - ): CharSequence { - val accountName = messageEntity.account - val addresses = when (folderType) { - FoldersManager.FolderType.SENT -> generateAddresses( - context = context, - accountName = accountName, - internetAddresses = messageEntity.to - ) - - FoldersManager.FolderType.DRAFTS -> generateAddresses( - context = context, - accountName = accountName, - internetAddresses = messageEntity.to - ).ifEmpty { - context.getString(R.string.no_recipients) - } - - FoldersManager.FolderType.OUTBOX -> generateOutboxStatus(context, messageEntity.msgState) - - else -> generateAddresses( - context = context, - accountName = accountName, - internetAddresses = messageEntity.from - ) - } - - return if ((messageEntity.threadMessagesCount ?: 0) > 1) { - SpannableStringBuilder(addresses).apply { - val spannableStringForThreadMessageCount = SpannableString( - "(${messageEntity.threadMessagesCount})" - ).apply { - val textSize = - context.resources.getDimensionPixelSize(R.dimen.default_text_size_small) - setSpan( - AbsoluteSizeSpan(textSize), - 0, - length, - Spanned.SPAN_INCLUSIVE_INCLUSIVE - ) - } - append(" ") - append(spannableStringForThreadMessageCount) - } - } else { - addresses - } - } - private fun changeStatusView(messageEntity: MessageEntity) { when (messageEntity.msgState) { MessageState.PENDING_ARCHIVING -> { @@ -561,56 +511,6 @@ class MsgsPagedListAdapter(private val onMessageClickListener: OnMessageClickLis changeViewsTypeface(Typeface.NORMAL) } - /** - * Prepare the sender name. - * - * * Remove common mail domains: gmail.com, yahoo.com, live.com, outlook.com - * - * - * @param name An incoming name - * @return A generated sender name. - */ - private fun prepareSenderName(name: String): String { - return SENDER_NAME_PATTERN.matcher(name).replaceFirst("") - } - - private fun generateAddresses( - context: Context, - accountName: String, - internetAddresses: List? - ): String { - if (internetAddresses == null) { - return context.getString(R.string.no_recipients) - } - - val mapOfUniqueInternetAddresses = mutableMapOf() - internetAddresses.forEach { internetAddress -> - val existingInternetAddress = - mapOfUniqueInternetAddresses[internetAddress.address.lowercase()] - if (existingInternetAddress == null || existingInternetAddress.personal?.isEmpty() == true) { - mapOfUniqueInternetAddresses[internetAddress.address.lowercase()] = internetAddress - } - } - - val uniqueRecipients = mapOfUniqueInternetAddresses.values.map { internetAddress -> - if (accountName.equals(internetAddress.address, true)) { - context.getString(R.string.me) - } else { - if (internetAddress.personal.isNullOrEmpty()) { - internetAddress.address - } else { - internetAddress.personal - } - } - }.toSet() - - return prepareSenderName(uniqueRecipients.joinToString { - if (uniqueRecipients.size > 1) { - it.split(" ").firstOrNull() ?: it - } else it - }) - } - companion object { /** * [Pattern] which will be used for finding some information in the sender name diff --git a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml index ee5e134729..8bb4a6a1e9 100644 --- a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml @@ -42,76 +42,53 @@ app:layout_constraintTop_toTopOf="parent" tools:visibility="visible"> - - - - - - - + app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginLeft="@dimen/default_margin_medium" + tools:text="[FlowCrypt/flowcrypt-security] Unlimited unauthenticated file storage via password protected attachments (#126)" /> + - - diff --git a/FlowCrypt/src/main/res/values-ru/strings.xml b/FlowCrypt/src/main/res/values-ru/strings.xml index defd970ef1..fab1e349c0 100644 --- a/FlowCrypt/src/main/res/values-ru/strings.xml +++ b/FlowCrypt/src/main/res/values-ru/strings.xml @@ -636,6 +636,7 @@ Пожалуйста, укажите здесь свою подпись Сделайте резервную копию в почтовом ящике Сохранить предоставленные закрытые ключи PGP, защищенные парольной фразой, в качестве резервной копии в папке «Входящие». Это поможет Вам получить доступ к Вашим зашифрованным сообщениям с других устройств (при условии предоставление Вашей парольной фразы). Вы можете безопасно оставить его в своем почтовом ящике или заархивировать.\n\nЕсли Вам не нужна такая резервная копия, отключите эту опцию. + Предварительный просмотр недоступен для сообщений с PGP. Показать всю переписку из %1$d сообщения Показать всю переписку из %1$d сообщений diff --git a/FlowCrypt/src/main/res/values-uk/strings.xml b/FlowCrypt/src/main/res/values-uk/strings.xml index 7b89f46502..a1bbbe3a04 100644 --- a/FlowCrypt/src/main/res/values-uk/strings.xml +++ b/FlowCrypt/src/main/res/values-uk/strings.xml @@ -637,6 +637,7 @@ Будь ласка, вкажіть свій підпис тут Зробіть резервну копію в електронній скриньці Зберегти надані секретні ключі PGP, захищені парольною фразою, як резервну копію в папці «Вхідні». Це допоможе отримати Вам доступ до Ваших зашифрованих повідомлень з інших пристроїв (за умови надання Вашої парольної фрази). Ви можете спокійно залишити його у папці "Вхідні" або заархівувати.\n\nЯкщо Вам не потрібна така резервна копія, вимкніть цю опцію. + Попередній перегляд недоступний для повідомлень з PGP Показати повне листування із %1$d повідомлення Показати повне листування із %1$d повідомленнь diff --git a/FlowCrypt/src/main/res/values/strings.xml b/FlowCrypt/src/main/res/values/strings.xml index 0049c0a1a2..73a204823b 100644 --- a/FlowCrypt/src/main/res/values/strings.xml +++ b/FlowCrypt/src/main/res/values/strings.xml @@ -648,6 +648,7 @@ Please specify your signature here Make a backup in the email box Save the given passphrase-protected PGP private keys as a backup in the inbox. It will help you access your encrypted messages from other devices (along with your pass phrase). You can safely leave it in your inbox or archive it.\n\nIf you don\'t need to have such a backup, please disable this option. + The preview is not available for messages with PGP Show a full conversation of %1$d messages From 4b1b6afff42539232a92a625c887a008fbef1180 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Fri, 30 Aug 2024 09:58:01 +0300 Subject: [PATCH 034/237] wip --- .../email/database/dao/MessageDao.kt | 22 ++++++++ .../viewmodel/MessagesViewPagerViewModel.kt | 30 ++++++++-- .../activity/fragment/GmailThreadFragment.kt | 11 +++- .../ViewPagerMessageDetailsFragment.kt | 1 + .../res/layout/item_message_in_thread.xml | 55 +++++-------------- .../src/main/res/navigation/nav_graph.xml | 8 ++- 6 files changed, 75 insertions(+), 52 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt index cadee92222..d353918dce 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt @@ -131,6 +131,28 @@ abstract class MessageDao : BaseDao { limit: Int ): List + @Query( + "SELECT * FROM (" + + "SELECT * FROM messages " + + "WHERE account = :account AND folder = :folder AND thread_id = :threadId AND received_date > :date " + + "ORDER BY received_date DESC " + + "LIMIT :limit) " + + "UNION " + + "SELECT * FROM (" + + "SELECT * FROM messages " + + "WHERE account = :account AND folder = :folder AND thread_id = :threadId AND received_date <= :date " + + "ORDER BY received_date ASC " + + "LIMIT :limit) " + + "ORDER BY received_date ASC" + ) + abstract suspend fun getMessagesInThreadForViewPager( + account: String, + folder: String, + threadId: String, + date: Long, + limit: Int + ): List + @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder AND uid = :uid") abstract fun getMsgLiveData(account: String, folder: String, uid: Long): LiveData diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewPagerViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewPagerViewModel.kt index f1a16a505e..848e044619 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewPagerViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewPagerViewModel.kt @@ -21,6 +21,7 @@ import com.flowcrypt.email.database.entity.MessageEntity class MessagesViewPagerViewModel( private val initialMessageEntityId: Long, private val localFolder: LocalFolder, + private val isThreadMode: Boolean, application: Application ) : AccountViewModel(application) { val initialLiveData: LiveData>> = @@ -57,21 +58,38 @@ class MessagesViewPagerViewModel( emit(Result.loading()) val activeAccount = getActiveAccountSuspend() if (activeAccount != null) { - emit( + val result = if (isThreadMode) { + Result.success( + roomDatabase.msgDao() + .getMessagesInThreadForViewPager( + account = activeAccount.email, + folder = if (localFolder.searchQuery.isNullOrEmpty()) { + localFolder.fullName + } else { + JavaEmailConstants.FOLDER_SEARCH + }, + threadId = messageEntity.threadId ?: "", + date = messageEntity.receivedDate ?: 0, + limit = PAGE_SIZE / 2 + ) + ) + } else { Result.success( roomDatabase.msgDao() .getMessagesForViewPager( - activeAccount.email, - if (localFolder.searchQuery.isNullOrEmpty()) { + account = activeAccount.email, + folder = if (localFolder.searchQuery.isNullOrEmpty()) { localFolder.fullName } else { JavaEmailConstants.FOLDER_SEARCH }, - messageEntity.receivedDate ?: 0, - PAGE_SIZE / 2 + date = messageEntity.receivedDate ?: 0, + limit = PAGE_SIZE / 2 ) ) - ) + } + + emit(result) } else { emit(Result.success(emptyList())) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt index dba44e304e..7934bf51bb 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt @@ -75,10 +75,15 @@ class GmailThreadFragment : BaseFragment(), override fun onMessageClick(messageEntity: MessageEntity) { lifecycleScope.launch { FlowCryptRoomDatabase.getDatabase(requireContext()).msgDao() - .getMsgSuspend(messageEntity.account, messageEntity.folder, messageEntity.uid)?.let { + .getMsgSuspend( + messageEntity.account, + messageEntity.folder, + messageEntity.uid + )?.id?.let { navController?.navigate( - GmailThreadFragmentDirections.actionGmailThreadFragmentToMessageDetailsFragment( - messageEntity = it, + GmailThreadFragmentDirections.actionGmailThreadFragmentToViewPagerMessageDetailsFragment( + messageEntityId = it, + isThreadMode = true, localFolder = LocalFolder( messageEntity.account, GmailApiHelper.LABEL_INBOX diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt index b3fb6adadb..b75f30c02a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt @@ -46,6 +46,7 @@ class ViewPagerMessageDetailsFragment : BaseFragment - - - - @@ -85,14 +58,15 @@ android:contentDescription="@string/security" android:src="@drawable/ic_security" android:visibility="gone" - app:layout_constraintEnd_toStartOf="@+id/viewHasAttachments" - app:layout_constraintTop_toBottomOf="@+id/textViewDate" + app:layout_constraintBottom_toBottomOf="@+id/tVTo" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintTop_toTopOf="@+id/tVTo" app:tint="@color/colorPrimary" tools:visibility="visible" /> diff --git a/FlowCrypt/src/main/res/navigation/nav_graph.xml b/FlowCrypt/src/main/res/navigation/nav_graph.xml index 4fc9c5da45..f1715a8837 100644 --- a/FlowCrypt/src/main/res/navigation/nav_graph.xml +++ b/FlowCrypt/src/main/res/navigation/nav_graph.xml @@ -305,6 +305,10 @@ + @@ -334,8 +338,8 @@ android:name="messageEntityId" app:argType="long" /> + android:id="@+id/action_gmailThreadFragment_to_viewPagerMessageDetailsFragment" + app:destination="@id/viewPagerMessageDetailsFragment" /> Date: Fri, 30 Aug 2024 11:01:32 +0300 Subject: [PATCH 035/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 115 +++++++++++------- .../email/database/entity/MessageEntity.kt | 2 +- .../api/services/gmail/model/ThreadExt.kt | 6 + .../viewmodel/DownloadAttachmentViewModel.kt | 7 +- .../jetpack/viewmodel/MessagesViewModel.kt | 2 +- .../jetpack/viewmodel/MsgDetailsViewModel.kt | 38 +++--- .../viewmodel/ThreadDetailsViewModel.kt | 2 +- .../DownloadForwardedAttachmentsWorker.kt | 4 +- .../workmanager/sync/UploadDraftsWorker.kt | 2 +- .../AttachmentDownloadManagerService.kt | 6 +- 10 files changed, 116 insertions(+), 68 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index d829860a2b..23a7be4a46 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -1,8 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: - * DenBond7 - * Ivan Pizhenko + * Contributors: denbond7 */ package com.flowcrypt.email.api.email.gmail @@ -25,6 +23,7 @@ import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getRecipients import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getSubject +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getUniqueLabelsSet import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getUniqueRecipients import com.flowcrypt.email.extensions.contentId import com.flowcrypt.email.extensions.disposition @@ -59,6 +58,7 @@ import com.google.api.services.gmail.model.ListMessagesResponse import com.google.api.services.gmail.model.ListThreadsResponse import com.google.api.services.gmail.model.Message import com.google.api.services.gmail.model.MessagePart +import com.google.api.services.gmail.model.Thread import jakarta.mail.Flags import jakarta.mail.MessagingException import jakarta.mail.Part @@ -119,9 +119,9 @@ class GmailApiHelper { private const val COUNT_OF_LOADED_EMAILS_BY_STEP = JavaEmailConstants.COUNT_OF_LOADED_EMAILS_BY_STEP.toLong() const val MESSAGE_RESPONSE_FORMAT_RAW = "raw" - const val MESSAGE_RESPONSE_FORMAT_FULL = "full" - const val MESSAGE_RESPONSE_FORMAT_MINIMAL = "minimal" - const val THREAD_RESPONSE_FORMAT_METADATA = "metadata" + const val RESPONSE_FORMAT_FULL = "full" + const val RESPONSE_FORMAT_MINIMAL = "minimal" + const val RESPONSE_FORMAT_METADATA = "metadata" private val FULL_INFO_WITHOUT_DATA = listOf( "id", @@ -334,7 +334,7 @@ class GmailApiHelper { accountEntity: AccountEntity, messages: List, localFolder: LocalFolder, - format: String = MESSAGE_RESPONSE_FORMAT_FULL, + format: String = RESPONSE_FORMAT_FULL, stepValue: Int = 10 ): List = withContext(Dispatchers.IO) { @@ -346,9 +346,9 @@ class GmailApiHelper { suspend fun loadGmailThreadInfoInParallel( context: Context, accountEntity: AccountEntity, - threads: List, + threads: List, localFolder: LocalFolder, - format: String = MESSAGE_RESPONSE_FORMAT_FULL, + format: String = RESPONSE_FORMAT_FULL, stepValue: Int = 10 ): List = withContext(Dispatchers.IO) { @@ -360,9 +360,9 @@ class GmailApiHelper { suspend fun loadThreadsInfo( context: Context, accountEntity: AccountEntity, - threads: Collection, + threads: Collection, localFolder: LocalFolder, - format: String = MESSAGE_RESPONSE_FORMAT_FULL, + format: String = RESPONSE_FORMAT_FULL, metadataHeaders: List? = null, fields: List? = FULL_INFO_WITHOUT_DATA ): List = withContext(Dispatchers.IO) @@ -386,38 +386,15 @@ class GmailApiHelper { request.queue( batch, - object : JsonBatchCallback() { + object : JsonBatchCallback() { override fun onSuccess( - t: com.google.api.services.gmail.model.Thread?, + t: Thread?, responseHeaders: HttpHeaders? ) { t?.let { thread -> - val receiverEmail = accountEntity.email - val subject = thread.messages?.getOrNull(0)?.takeIf { message -> - message.getRecipients("From").any { internetAddress -> - internetAddress.address.equals(receiverEmail, true) - } || (thread.messages?.size?: 0) == 1 - }?.getSubject() - ?: thread.messages?.getOrNull(1)?.getSubject() - ?: context.getString(R.string.no_subject) - - thread.messages?.lastOrNull()?.let { lastMessageInThread -> - if (isTrash || lastMessageInThread.labelIds?.contains(LABEL_TRASH) != true) { - listResult.add( - GmailThreadInfo( - id = thread.id, - lastMessage = lastMessageInThread, - messagesCount = thread.messages?.size ?: 0, - recipients = thread.getUniqueRecipients(receiverEmail), - subject = subject, - labels = thread.messages.flatMap { message -> - message.labelIds ?: emptyList() - }.toSortedSet() - ) - ) - } - } - } ?: throw java.lang.NullPointerException() + val gmailThreadInfo = extractGmailThreadInfo(accountEntity, thread, context) + listResult.add(gmailThreadInfo) + } } override fun onFailure(e: GoogleJsonError?, responseHeaders: HttpHeaders?) { @@ -431,11 +408,42 @@ class GmailApiHelper { return@withContext listResult } + suspend fun loadThreadInfo( + context: Context, + accountEntity: AccountEntity, + threadId: String, + format: String = RESPONSE_FORMAT_FULL, + metadataHeaders: List? = null, + fields: List? = FULL_INFO_WITHOUT_DATA + ): GmailThreadInfo = withContext(Dispatchers.IO) + { + val gmailApiService = generateGmailApiService(context, accountEntity) + + val request = gmailApiService + .users() + .threads() + .get(DEFAULT_USER_ID, threadId) + .setFormat(format) + + metadataHeaders?.let { metadataHeaders -> + request.metadataHeaders = metadataHeaders + } + + fields?.let { fields -> + request.fields = fields.joinToString(separator = ",") + } + + val thread = request.execute() + val gmailThreadInfo = extractGmailThreadInfo(accountEntity, thread, context) + + return@withContext gmailThreadInfo + } + suspend fun loadMessagesInThread( context: Context, accountEntity: AccountEntity, threadId: String, - format: String = MESSAGE_RESPONSE_FORMAT_FULL, + format: String = RESPONSE_FORMAT_FULL, metadataHeaders: List? = null, fields: List? = null ): List = withContext(Dispatchers.IO) @@ -461,7 +469,7 @@ class GmailApiHelper { suspend fun loadMsgs( context: Context, accountEntity: AccountEntity, messages: Collection, - localFolder: LocalFolder, format: String = MESSAGE_RESPONSE_FORMAT_FULL, + localFolder: LocalFolder, format: String = RESPONSE_FORMAT_FULL, metadataHeaders: List? = null, fields: List? = FULL_INFO_WITHOUT_DATA ): List = withContext(Dispatchers.IO) { @@ -1280,5 +1288,30 @@ class GmailApiHelper { } ?: e } } + + private fun extractGmailThreadInfo( + accountEntity: AccountEntity, + thread: Thread, + context: Context + ): GmailThreadInfo { + val receiverEmail = accountEntity.email + val subject = thread.messages?.getOrNull(0)?.takeIf { message -> + message.getRecipients("From").any { internetAddress -> + internetAddress.address.equals(receiverEmail, true) + } || (thread.messages?.size ?: 0) == 1 + }?.getSubject() + ?: thread.messages?.getOrNull(1)?.getSubject() + ?: context.getString(R.string.no_subject) + + val gmailThreadInfo = GmailThreadInfo( + id = thread.id, + lastMessage = requireNotNull(thread.messages?.last()), + messagesCount = thread.messages?.size ?: 0, + recipients = thread.getUniqueRecipients(receiverEmail), + subject = subject, + labels = thread.getUniqueLabelsSet() + ) + return gmailThreadInfo + } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index df8116d5db..321cae2a3e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -610,7 +610,7 @@ data class MessageEntity( } fun generateColoredLabels( - labelIds: List?, + labelIds: Collection?, labelEntities: List?, skippedLabels: List = emptyList() ): List { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt index 00da218396..055bf2a8d8 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt @@ -45,4 +45,10 @@ fun Thread.getUniqueRecipients(account: String): List { addAll(mapOfUniqueRecipients.values) } +} + +fun Thread.getUniqueLabelsSet(): Set { + return messages?.flatMap { message -> + message.labelIds ?: emptyList() + }?.toSortedSet() ?: emptySet() } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DownloadAttachmentViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DownloadAttachmentViewModel.kt index b6a3fa2664..ac758fb18d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DownloadAttachmentViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DownloadAttachmentViewModel.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.viewmodel @@ -19,7 +19,6 @@ import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.security.SecurityUtils import com.flowcrypt.email.util.coroutines.runners.ControlledRunner import com.flowcrypt.email.util.exception.ExceptionUtil -import org.eclipse.angus.mail.imap.IMAPFolder import jakarta.mail.Folder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow @@ -29,9 +28,9 @@ import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.apache.commons.io.IOUtils +import org.eclipse.angus.mail.imap.IMAPFolder import java.io.ByteArrayOutputStream import java.io.InputStream -import java.lang.IllegalArgumentException /** * @author Denys Bondarenko @@ -99,7 +98,7 @@ class DownloadAttachmentViewModel(val attachmentInfo: AttachmentInfo, applicatio context = context, accountEntity = account, msgId = attachmentInfo.uid.toHex(), - format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_FULL + format = GmailApiHelper.RESPONSE_FORMAT_FULL ) val attPart = GmailApiHelper.getAttPartByPath(msg.payload, neededPath = attachmentInfo.path) ?: throw IllegalStateException(context.getString(R.string.attachment_not_found)) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index 88482a5b96..974eaa7d01 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -481,7 +481,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application context = getApplication(), accountEntity = accountEntity, threads = threadsResponse.threads ?: emptyList(), - format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_FULL, + format = GmailApiHelper.RESPONSE_FORMAT_FULL, localFolder = localFolder ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index ee93bd5f0f..9b2dc9e122 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.viewmodel @@ -57,9 +57,6 @@ import com.flowcrypt.email.util.cache.DiskLruCache import com.flowcrypt.email.util.coroutines.runners.ControlledRunner import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.exception.SyncTaskTerminatedException -import org.eclipse.angus.mail.imap.IMAPBodyPart -import org.eclipse.angus.mail.imap.IMAPFolder -import org.eclipse.angus.mail.imap.IMAPMessage import jakarta.mail.BodyPart import jakarta.mail.FetchProfile import jakarta.mail.Folder @@ -82,6 +79,9 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.isActive import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPBodyPart +import org.eclipse.angus.mail.imap.IMAPFolder +import org.eclipse.angus.mail.imap.IMAPMessage import org.pgpainless.PGPainless import org.pgpainless.key.protection.PasswordBasedSecretKeyRingProtector import org.pgpainless.util.Passphrase @@ -327,15 +327,25 @@ class MsgDetailsViewModel( val cachedLabelIds = freshestMessageEntity?.labelIds?.split(MessageEntity.LABEL_IDS_SEPARATOR) try { - val message = GmailApiHelper.loadMsgInfoSuspend( - context = getApplication(), - accountEntity = account, - msgId = messageEntity.uidAsHEX, - fields = null, - format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_MINIMAL - ) + val latestLabelIds = + if (account.useConversationMode) { + GmailApiHelper.loadThreadInfo( + context = getApplication(), + accountEntity = account, + threadId = freshestMessageEntity?.threadId ?: "", + fields = listOf("messages/labelIds"), + format = GmailApiHelper.RESPONSE_FORMAT_MINIMAL + ).labels + } else { + GmailApiHelper.loadMsgInfoSuspend( + context = getApplication(), + accountEntity = account, + msgId = messageEntity.uidAsHEX, + fields = null, + format = GmailApiHelper.RESPONSE_FORMAT_MINIMAL + ).labelIds + } - val latestLabelIds = message.labelIds if (cachedLabelIds == null || !(latestLabelIds.containsAll(cachedLabelIds) && cachedLabelIds.containsAll(latestLabelIds)) @@ -645,7 +655,7 @@ class MsgDetailsViewModel( accountEntity = accountEntity, msgId = messageEntity.uidAsHEX, fields = null, - format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_FULL + format = GmailApiHelper.RESPONSE_FORMAT_FULL ) msgSize = msgFullInfo.sizeEstimate val originalMsg = GmaiAPIMimeMessage( @@ -932,7 +942,7 @@ class MsgDetailsViewModel( context = getApplication(), accountEntity = accountEntity, msgId = messageEntity.uidAsHEX, - format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_FULL + format = GmailApiHelper.RESPONSE_FORMAT_FULL ) val attachments = GmailApiHelper.getAttsInfoFromMessagePart(msg.payload).mapNotNull { attachmentInfo -> diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index d15c3685b5..360607db7f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -106,7 +106,7 @@ class ThreadDetailsViewModel( accountEntity = account, msgId = messageEntityId.toHex(), fields = null, - format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_MINIMAL + format = GmailApiHelper.RESPONSE_FORMAT_MINIMAL ) val latestLabelIds = message.labelIds diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt index 3e89411849..16b55fc88f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/DownloadForwardedAttachmentsWorker.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.workmanager @@ -185,7 +185,7 @@ class DownloadForwardedAttachmentsWorker(context: Context, params: WorkerParamet context = applicationContext, accountEntity = account, msgId = it, - format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_FULL + format = GmailApiHelper.RESPONSE_FORMAT_FULL ) } ?: return@withContext MessageState.ERROR_ORIGINAL_MESSAGE_MISSING diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt index f8403143cc..7267313e01 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt @@ -78,7 +78,7 @@ class UploadDraftsWorker(context: Context, params: WorkerParameters) : "threadId", "historyId" ), - format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_FULL + format = GmailApiHelper.RESPONSE_FORMAT_FULL ) val freshestMessageEntity = diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt index 3405f4ee9e..452bad766a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/attachment/AttachmentDownloadManagerService.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.service.attachment @@ -45,7 +45,6 @@ import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.LogsUtil import com.flowcrypt.email.util.exception.ExceptionUtil import com.google.android.gms.common.util.CollectionUtils -import org.eclipse.angus.mail.imap.IMAPFolder import jakarta.mail.Folder import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow @@ -56,6 +55,7 @@ import org.apache.commons.io.FileUtils import org.apache.commons.io.FilenameUtils import org.apache.commons.io.IOUtils import org.bouncycastle.openpgp.PGPSecretKeyRingCollection +import org.eclipse.angus.mail.imap.IMAPFolder import java.io.File import java.io.FileInputStream import java.io.InputStream @@ -529,7 +529,7 @@ class AttachmentDownloadManagerService : LifecycleService() { context = context, accountEntity = account, msgId = att.uid.toHex(), - format = GmailApiHelper.MESSAGE_RESPONSE_FORMAT_FULL + format = GmailApiHelper.RESPONSE_FORMAT_FULL ) val attPart = GmailApiHelper.getAttPartByPath(msg.payload, neededPath = att.path) ?: throw IllegalStateException(context.getString(R.string.attachment_not_found)) From 9137b8394683d3afc3483e0daad8a09695a9a798 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Fri, 30 Aug 2024 11:09:43 +0300 Subject: [PATCH 036/237] wip --- .../email/jetpack/viewmodel/MsgDetailsViewModel.kt | 2 +- .../email/jetpack/viewmodel/ThreadDetailsViewModel.kt | 11 ++++------- .../main/res/layout/fragment_new_message_details.xml | 4 +++- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index 9b2dc9e122..c44fda2022 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -333,7 +333,7 @@ class MsgDetailsViewModel( context = getApplication(), accountEntity = account, threadId = freshestMessageEntity?.threadId ?: "", - fields = listOf("messages/labelIds"), + fields = listOf("id", "messages/labelIds"), format = GmailApiHelper.RESPONSE_FORMAT_MINIMAL ).labels } else { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index 360607db7f..ad8dc75781 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -11,7 +11,6 @@ import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly -import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.ui.adapter.GmailApiLabelsListAdapter import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow @@ -101,15 +100,13 @@ class ThreadDetailsViewModel( val cachedLabelIds = freshestMessageEntity?.labelIds?.split(MessageEntity.LABEL_IDS_SEPARATOR) try { - val message = GmailApiHelper.loadMsgInfoSuspend( + val latestLabelIds = GmailApiHelper.loadThreadInfo( context = getApplication(), accountEntity = account, - msgId = messageEntityId.toHex(), - fields = null, + threadId = freshestMessageEntity?.threadId ?: "", + fields = listOf("id", "messages/labelIds"), format = GmailApiHelper.RESPONSE_FORMAT_MINIMAL - ) - - val latestLabelIds = message.labelIds + ).labels if (cachedLabelIds == null || !(latestLabelIds.containsAll(cachedLabelIds) && cachedLabelIds.containsAll(latestLabelIds)) diff --git a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml index 8bb4a6a1e9..1a8961619a 100644 --- a/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_new_message_details.xml @@ -78,13 +78,15 @@ tools:itemCount="4" tools:listitem="@layout/item_label_badge" tools:orientation="horizontal" /> + From d5dc0497a4957f2fd571f35697c983694f746099 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 2 Sep 2024 11:31:09 +0300 Subject: [PATCH 037/237] wip --- .../fragment/MessageDetailsFragment.kt | 9 ++++- .../ViewPagerMessageDetailsFragment.kt | 3 +- .../email/ui/adapter/FragmentsAdapter.kt | 4 +- .../ui/adapter/MessagesInThreadListAdapter.kt | 39 ++++++++++++++++++- .../src/main/res/navigation/nav_graph.xml | 4 ++ 5 files changed, 53 insertions(+), 6 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt index c21d225cdf..e645467072 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt @@ -288,7 +288,7 @@ class MessageDetailsFragment : BaseFragment(), Pr private val gmailApiLabelsListAdapter = GmailApiLabelsListAdapter( object : GmailApiLabelsListAdapter.OnLabelClickListener { override fun onLabelClick(label: GmailApiLabelsListAdapter.Label) { - if (args.localFolder.searchQuery == null) { + if (args.localFolder.searchQuery == null && !args.isThreadMode) { changeGmailLabels() } } @@ -358,6 +358,11 @@ class MessageDetailsFragment : BaseFragment(), Pr override fun onSetupActionBarMenu(menuHost: MenuHost) { super.onSetupActionBarMenu(menuHost) + + if (args.isThreadMode) { + return + } + menuHost.addMenuProvider(object : MenuProvider { override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { if (!args.isViewPagerMode) { @@ -884,7 +889,7 @@ class MessageDetailsFragment : BaseFragment(), Pr updateActionBar(messageEntity) messageEntity.threadMessagesCount?.let { threadMessagesCount -> - if (threadMessagesCount > 1) { + if (threadMessagesCount > 1 && !args.isThreadMode) { binding?.displayFullConversation?.apply { visible() text = resources.getQuantityString( diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt index b75f30c02a..4421ddcb89 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt @@ -66,7 +66,8 @@ class ViewPagerMessageDetailsFragment : BaseFragment } addItemDecoration(DividerItemDecoration(view.context, ORIENTATION_HORIZONTAL)) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt index 5af69ccccd..90b8b3559a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt @@ -21,6 +21,7 @@ class FragmentsAdapter( private val localFolder: LocalFolder, initialList: List, fragment: Fragment, + private val isThreadMode: Boolean, listListener: AsyncListDiffer.ListListener ) : FragmentStateAdapter(fragment) { private val diffUtil = object : DiffUtil.ItemCallback() { @@ -49,7 +50,8 @@ class FragmentsAdapter( arguments = MessageDetailsFragmentArgs( messageEntity = asyncListDiffer.currentList[position], localFolder = localFolder, - isViewPagerMode = true + isViewPagerMode = true, + isThreadMode = isThreadMode ).toBundle() } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt index b8762408e6..fd01f70ccc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -5,6 +5,8 @@ package com.flowcrypt.email.ui.adapter +import android.graphics.Color +import android.graphics.Typeface import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -19,6 +21,7 @@ import com.flowcrypt.email.extensions.android.widget.useGlideToApplyImageFromSou import com.flowcrypt.email.extensions.visibleOrGone import com.flowcrypt.email.util.DateTimeUtil import com.flowcrypt.email.util.graphics.glide.AvatarModelLoader +import com.google.android.material.color.MaterialColors /** * @author Denys Bondarenko @@ -56,11 +59,43 @@ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageC } else { item.snippet } - binding.textViewSender.text = senderAddress binding.tVTo.text = item.generateToText(context) - binding.textViewDate.text = DateTimeUtil.formatSameDayTime(context, item.receivedDate ?: 0) + binding.textViewSender.apply { + text = senderAddress + if (item.isSeen) { + setTypeface(null, Typeface.NORMAL) + setTextColor( + MaterialColors.getColor( + context, + com.google.android.material.R.attr.colorOnSurfaceVariant, + Color.BLACK + ) + ) + } else { + setTypeface(null, Typeface.BOLD) + setTextColor( + MaterialColors.getColor(context, R.attr.itemTitleColor, Color.BLACK) + ) + } + } + binding.textViewSender.text = senderAddress + binding.textViewDate.apply { + text = DateTimeUtil.formatSameDayTime(context, item.receivedDate ?: 0) + if (item.isSeen) { + setTypeface(null, Typeface.NORMAL) + setTextColor( + MaterialColors.getColor(context, R.attr.itemSubTitleColor, Color.BLACK) + ) + } else { + setTypeface(null, Typeface.BOLD) + setTextColor( + MaterialColors.getColor(context, R.attr.itemTitleColor, Color.BLACK) + ) + } + } binding.viewHasPgp.visibleOrGone(item.hasPgp == true || item.isEncrypted == true) binding.viewHasAttachments.visibleOrGone(item.hasAttachments == true) + binding.textViewDate.setTypeface(null, if (item.isSeen) Typeface.NORMAL else Typeface.BOLD) } } diff --git a/FlowCrypt/src/main/res/navigation/nav_graph.xml b/FlowCrypt/src/main/res/navigation/nav_graph.xml index f1715a8837..c75d29227e 100644 --- a/FlowCrypt/src/main/res/navigation/nav_graph.xml +++ b/FlowCrypt/src/main/res/navigation/nav_graph.xml @@ -328,6 +328,10 @@ android:name="isViewPagerMode" app:argType="boolean" android:defaultValue="false" /> + Date: Mon, 2 Sep 2024 17:01:19 +0300 Subject: [PATCH 038/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 2 +- .../jetpack/viewmodel/MessagesViewModel.kt | 146 ++++++++++++++++-- 2 files changed, 135 insertions(+), 13 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 23a7be4a46..e740ba6733 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -1236,7 +1236,7 @@ class GmailApiHelper { .setSelectedAccount(account) } - private fun labelsToImapFlags(labelIds: List): Flags { + fun labelsToImapFlags(labelIds: List): Flags { val flags = Flags() labelIds.forEach { when (it) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index 974eaa7d01..8c17cca41b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -49,6 +49,7 @@ import com.google.api.client.googleapis.json.GoogleJsonResponseException import com.google.api.services.gmail.model.History import com.google.api.services.gmail.model.ListDraftsResponse import com.google.api.services.gmail.model.ListMessagesResponse +import com.google.api.services.gmail.model.Thread import jakarta.mail.FetchProfile import jakarta.mail.Folder import jakarta.mail.Message @@ -1070,13 +1071,29 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val newCandidates = newCandidatesMap.values if (newCandidates.isNotEmpty()) { - val msgs = GmailApiHelper.loadMsgsInParallel( - getApplication(), accountEntity, - newCandidates.toList(), localFolder - ).run { - if (accountEntity.showOnlyEncrypted == true) { - filter { it.hasPgp() } - } else this + val gmailThreadInfoList: List + val msgs: List + if (accountEntity.useConversationMode) { + val uniqueThreadIdList = newCandidates.map { it.threadId }.toSet() + gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( + context = getApplication(), + accountEntity = accountEntity, + threads = uniqueThreadIdList.map { Thread().apply { id = it } }, + format = GmailApiHelper.RESPONSE_FORMAT_FULL, + localFolder = localFolder + ) + + msgs = gmailThreadInfoList.map { it.lastMessage } + } else { + gmailThreadInfoList = emptyList() + msgs = GmailApiHelper.loadMsgsInParallel( + getApplication(), accountEntity, + newCandidates.toList(), localFolder + ).run { + if (accountEntity.showOnlyEncrypted == true) { + filter { it.hasPgp() } + } else this + } } val draftIdsMap = if (localFolder.isDrafts) { @@ -1103,7 +1120,23 @@ class MessagesViewModel(application: Application) : AccountViewModel(application isNew = isNew, onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, draftIdsMap = draftIdsMap - ) + ) { message, messageEntity -> + if (accountEntity.useConversationMode) { + val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } + messageEntity.copy( + subject = thread?.subject, + threadMessagesCount = thread?.messagesCount, + labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), + hasAttachments = thread?.hasAttachments, + threadRecipientsAddresses = InternetAddress.toString( + thread?.recipients?.toTypedArray() + ), + hasPgp = thread?.hasPgpThings + ) + } else { + messageEntity + } + } roomDatabase.msgDao().insertWithReplaceSuspend(msgEntities) GmailApiHelper.identifyAttachments( @@ -1115,11 +1148,100 @@ class MessagesViewModel(application: Application) : AccountViewModel(application ) } - roomDatabase.msgDao() - .updateFlagsSuspend(accountEntity.email, localFolder.fullName, updateCandidatesMap) + if (accountEntity.useConversationMode) { + //this code should be improved. Just for proof of concept + val s = updateCandidatesMap.keys//it's messages ids + val threadIds = roomDatabase.msgDao() + .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = s) + .mapNotNull { it.threadId }.toSet() - roomDatabase.msgDao() - .updateGmailLabels(accountEntity.email, localFolder.fullName, labelsToBeUpdatedMap) + val gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( + context = getApplication(), + accountEntity = accountEntity, + threads = threadIds.map { Thread().apply { id = it } }, + format = GmailApiHelper.RESPONSE_FORMAT_FULL, + localFolder = localFolder + ) + + val msgs = gmailThreadInfoList.map { it.lastMessage } + val map = MessageEntity.genMessageEntities( + context = getApplication(), + account = accountEntity.email, + accountType = accountEntity.accountType, + label = localFolder.fullName, + msgsList = msgs, + isNew = false, + onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, + draftIdsMap = emptyMap() + ) { message, messageEntity -> + val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } + messageEntity.copy( + subject = thread?.subject, + threadMessagesCount = thread?.messagesCount, + labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), + hasAttachments = thread?.hasAttachments, + threadRecipientsAddresses = InternetAddress.toString( + thread?.recipients?.toTypedArray() + ), + hasPgp = thread?.hasPgpThings + ) + }.associateBy({ it.uid }, + { + GmailApiHelper.labelsToImapFlags( + it.labelIds?.split(LABEL_IDS_SEPARATOR) ?: emptyList() + ) + }) + + roomDatabase.msgDao() + .updateFlagsSuspend(accountEntity.email, localFolder.fullName, map) + + + val m = labelsToBeUpdatedMap.keys//it's messages ids + val threadIds2 = roomDatabase.msgDao() + .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = m) + .mapNotNull { it.threadId }.toSet() + + val gmailThreadInfoList2 = GmailApiHelper.loadGmailThreadInfoInParallel( + context = getApplication(), + accountEntity = accountEntity, + threads = threadIds2.map { Thread().apply { id = it } }, + format = GmailApiHelper.RESPONSE_FORMAT_FULL, + localFolder = localFolder + ) + + val msgs2 = gmailThreadInfoList2.map { it.lastMessage } + val map2 = MessageEntity.genMessageEntities( + context = getApplication(), + account = accountEntity.email, + accountType = accountEntity.accountType, + label = localFolder.fullName, + msgsList = msgs2, + isNew = false, + onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, + draftIdsMap = emptyMap() + ) { message, messageEntity -> + val thread = gmailThreadInfoList2.firstOrNull { it.id == message.threadId } + messageEntity.copy( + subject = thread?.subject, + threadMessagesCount = thread?.messagesCount, + labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), + hasAttachments = thread?.hasAttachments, + threadRecipientsAddresses = InternetAddress.toString( + thread?.recipients?.toTypedArray() + ), + hasPgp = thread?.hasPgpThings + ) + }.associateBy({ it.uid }, { it.labelIds ?: "" }) + + roomDatabase.msgDao() + .updateGmailLabels(accountEntity.email, localFolder.fullName, map2) + } else { + roomDatabase.msgDao() + .updateFlagsSuspend(accountEntity.email, localFolder.fullName, updateCandidatesMap) + + roomDatabase.msgDao() + .updateGmailLabels(accountEntity.email, localFolder.fullName, labelsToBeUpdatedMap) + } if (folderType === FoldersManager.FolderType.SENT) { val session = Session.getInstance(Properties()) From b93ebd801d8e3b3564b3599c2802d3137e7f2d0c Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 3 Sep 2024 11:52:06 +0300 Subject: [PATCH 039/237] wip --- .../flowcrypt/email/FlowCryptApplication.kt | 25 +++++++- .../com/flowcrypt/email/extensions/Context.kt | 7 ++- .../workmanager/sync/InboxIdleSyncWorker.kt | 59 +++++++++++++++---- .../activity/fragment/MessagesListFragment.kt | 15 +++-- 4 files changed, 88 insertions(+), 18 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/FlowCryptApplication.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/FlowCryptApplication.kt index 7cb690d966..5100504dc0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/FlowCryptApplication.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/FlowCryptApplication.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email @@ -8,6 +8,9 @@ package com.flowcrypt.email import android.app.Application import android.content.Context import android.util.Log +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ProcessLifecycleOwner import androidx.preference.PreferenceManager import androidx.work.Configuration import androidx.work.ExistingPeriodicWorkPolicy @@ -49,6 +52,9 @@ import java.util.concurrent.TimeUnit * @author Denys Bondarenko */ class FlowCryptApplication : Application(), Configuration.Provider { + + val appForegroundedObserver = AppForegroundedObserver() + override val workManagerConfiguration: Configuration get() = Configuration.Builder() .setMinimumLoggingLevel( @@ -61,6 +67,8 @@ class FlowCryptApplication : Application(), Configuration.Provider { override fun onCreate() { super.onCreate() + ProcessLifecycleOwner.get().lifecycle.addObserver(appForegroundedObserver) + setupGlobalSettingsForJavaMail() setupPGPainless() setupKeysStorage() @@ -259,4 +267,19 @@ class FlowCryptApplication : Application(), Configuration.Provider { } } } + + class AppForegroundedObserver : DefaultLifecycleObserver { + val isAppForegrounded: Boolean + get() { + return isAppForegroundedInternal + } + private var isAppForegroundedInternal = false + override fun onStart(owner: LifecycleOwner) { + isAppForegroundedInternal = false + } + + override fun onStop(owner: LifecycleOwner) { + isAppForegroundedInternal = true + } + } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/Context.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/Context.kt index 258d847c74..995be3f39d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/Context.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/Context.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.extensions @@ -10,6 +10,7 @@ import android.net.ConnectivityManager import android.net.NetworkCapabilities import android.os.Build import android.widget.Toast +import com.flowcrypt.email.FlowCryptApplication /** * @author Denys Bondarenko @@ -36,3 +37,7 @@ fun Context?.hasActiveConnection(): Boolean { } } ?: false } + +fun Context?.isAppForegrounded(): Boolean { + return (this as? FlowCryptApplication)?.appForegroundedObserver?.isAppForegrounded ?: false +} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt index 23af6814ce..6bf3ac31dc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.workmanager.sync @@ -12,17 +12,21 @@ import com.flowcrypt.email.BuildConfig import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.FoldersManager import com.flowcrypt.email.api.email.gmail.GmailApiHelper +import com.flowcrypt.email.api.email.gmail.model.GmailThreadInfo import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.database.entity.MessageEntity.Companion.LABEL_IDS_SEPARATOR import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasPgp -import com.flowcrypt.email.util.GeneralUtil +import com.flowcrypt.email.extensions.isAppForegrounded import com.flowcrypt.email.util.exception.GmailAPIException import com.google.api.services.gmail.model.History +import com.google.api.services.gmail.model.Thread import jakarta.mail.FetchProfile import jakarta.mail.Folder import jakarta.mail.Store import jakarta.mail.UIDFolder +import jakarta.mail.internet.InternetAddress import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.angus.mail.imap.IMAPFolder @@ -165,17 +169,34 @@ open class InboxIdleSyncWorker(context: Context, params: WorkerParameters) : val newCandidates = newCandidatesMap.values.toList() if (newCandidates.isNotEmpty()) { - val msgs = GmailApiHelper.loadMsgsInParallel( - applicationContext, accountEntity, - newCandidates, localFolder - ).run { - if (accountEntity.showOnlyEncrypted == true) { - filter { it.hasPgp() } - } else this + val gmailThreadInfoList: List + val msgs: List + + if (accountEntity.useConversationMode) { + val uniqueThreadIdList = newCandidates.map { it.threadId }.toSet() + gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( + context = applicationContext, + accountEntity = accountEntity, + threads = uniqueThreadIdList.map { Thread().apply { id = it } }, + format = GmailApiHelper.RESPONSE_FORMAT_FULL, + localFolder = localFolder + ) + + msgs = gmailThreadInfoList.map { it.lastMessage } + } else { + gmailThreadInfoList = emptyList() + msgs = GmailApiHelper.loadMsgsInParallel( + applicationContext, accountEntity, + newCandidates.toList(), localFolder + ).run { + if (accountEntity.showOnlyEncrypted == true) { + filter { it.hasPgp() } + } else this + } } val isOnlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false - val isNew = !GeneralUtil.isAppForegrounded() + val isNew = applicationContext.isAppForegrounded() val msgEntities = MessageEntity.genMessageEntities( context = applicationContext, @@ -185,7 +206,23 @@ open class InboxIdleSyncWorker(context: Context, params: WorkerParameters) : msgsList = msgs, isNew = isNew, onlyPgpModeEnabled = isOnlyPgpModeEnabled - ) + ) { message, messageEntity -> + if (accountEntity.useConversationMode) { + val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } + messageEntity.copy( + subject = thread?.subject, + threadMessagesCount = thread?.messagesCount, + labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), + hasAttachments = thread?.hasAttachments, + threadRecipientsAddresses = InternetAddress.toString( + thread?.recipients?.toTypedArray() + ), + hasPgp = thread?.hasPgpThings + ) + } else { + messageEntity + } + } processNewMsgs(accountEntity, localFolder, msgEntities) GmailApiHelper.identifyAttachments( diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt index aa0a7d6c12..1591dda555 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui.activity.fragment @@ -77,13 +77,12 @@ import com.flowcrypt.email.jetpack.workmanager.sync.DeleteDraftsWorker import com.flowcrypt.email.jetpack.workmanager.sync.DeleteMessagesPermanentlyWorker import com.flowcrypt.email.jetpack.workmanager.sync.DeleteMessagesWorker import com.flowcrypt.email.jetpack.workmanager.sync.EmptyTrashWorker +import com.flowcrypt.email.jetpack.workmanager.sync.InboxIdleSyncWorker import com.flowcrypt.email.jetpack.workmanager.sync.MarkAsNotSpamWorker import com.flowcrypt.email.jetpack.workmanager.sync.MovingToInboxWorker import com.flowcrypt.email.jetpack.workmanager.sync.MovingToSpamWorker import com.flowcrypt.email.jetpack.workmanager.sync.UpdateMsgsSeenStateWorker -import com.flowcrypt.email.model.MessageType import com.flowcrypt.email.service.MessagesNotificationManager -import com.flowcrypt.email.ui.activity.CreateMessageActivity import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment import com.flowcrypt.email.ui.activity.fragment.base.ListProgressBehaviour import com.flowcrypt.email.ui.activity.fragment.dialog.ChangeGmailLabelsDialogFragmentArgs @@ -99,6 +98,7 @@ import com.google.android.gms.auth.UserRecoverableAuthException import com.google.android.material.snackbar.Snackbar import com.google.api.client.googleapis.extensions.android.gms.auth.UserRecoverableAuthIOException import jakarta.mail.AuthenticationFailedException +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import me.everything.android.ui.overscroll.IOverScrollDecor import me.everything.android.ui.overscroll.IOverScrollState @@ -603,9 +603,14 @@ class MessagesListFragment : BaseFragment(), ListPr binding?.swipeRefreshLayout?.setOnRefreshListener(this) binding?.floatActionButtonCompose?.setOnClickListener { - startActivity( + lifecycleScope.launch { + delay(10000) + InboxIdleSyncWorker.enqueue(requireContext()) + } + + /*startActivity( CreateMessageActivity.generateIntent(context, MessageType.NEW) - ) + )*/ } } From 806af20e2630ca0dcda9e919e2a7a39361187735 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 3 Sep 2024 14:59:19 +0300 Subject: [PATCH 040/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 96 ----- .../api/email/gmail/GmailHistoryHandler.kt | 358 ++++++++++++++++++ .../jetpack/viewmodel/MessagesViewModel.kt | 293 ++------------ .../workmanager/sync/BaseIdleWorker.kt | 26 +- .../workmanager/sync/InboxIdleSyncWorker.kt | 94 +---- .../com/flowcrypt/email/util/GeneralUtil.kt | 62 ++- 6 files changed, 474 insertions(+), 455 deletions(-) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index e740ba6733..ec86c5943d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -693,102 +693,6 @@ class GmailApiHelper { return@withContext historyList } - suspend fun processHistory( - localFolder: LocalFolder, - historyList: List, - action: suspend ( - deleteCandidatesUIDs: Set, - newCandidatesMap: Map, - updateCandidatesMap: Map, - labelsToBeUpdatedMap: Map - ) -> Unit - ) = withContext(Dispatchers.IO) - { - val deleteCandidatesUIDs = mutableSetOf() - val newCandidatesMap = mutableMapOf() - val updateCandidates = mutableMapOf() - val labelsToBeUpdatedMap = mutableMapOf() - val isDrafts = localFolder.isDrafts - - for (history in historyList) { - history.messagesDeleted?.let { messagesDeleted -> - for (historyMsgDeleted in messagesDeleted) { - newCandidatesMap.remove(historyMsgDeleted.message.uid) - updateCandidates.remove(historyMsgDeleted.message.uid) - deleteCandidatesUIDs.add(historyMsgDeleted.message.uid) - } - } - - history.messagesAdded?.let { messagesAdded -> - for (historyMsgAdded in messagesAdded) { - if (LABEL_DRAFT in (historyMsgAdded.message.labelIds ?: emptyList()) && !isDrafts) { - //skip adding drafts to non-Drafts folder - continue - } - deleteCandidatesUIDs.remove(historyMsgAdded.message.uid) - updateCandidates.remove(historyMsgAdded.message.uid) - newCandidatesMap[historyMsgAdded.message.uid] = historyMsgAdded.message - } - } - - history.labelsRemoved?.let { labelsRemoved -> - for (historyLabelRemoved in labelsRemoved) { - val historyMessageLabelIds = historyLabelRemoved?.message?.labelIds ?: emptyList() - labelsToBeUpdatedMap[historyLabelRemoved.message.uid] = - historyMessageLabelIds.joinToString(MessageEntity.LABEL_IDS_SEPARATOR) - if (localFolder.fullName in (historyLabelRemoved.labelIds ?: emptyList())) { - newCandidatesMap.remove(historyLabelRemoved.message.uid) - updateCandidates.remove(historyLabelRemoved.message.uid) - deleteCandidatesUIDs.add(historyLabelRemoved.message.uid) - continue - } - - if (LABEL_TRASH in (historyLabelRemoved.labelIds ?: emptyList())) { - val message = historyLabelRemoved.message - if (localFolder.fullName in historyMessageLabelIds) { - deleteCandidatesUIDs.remove(message.uid) - updateCandidates.remove(message.uid) - newCandidatesMap[message.uid] = message - continue - } - } - - val existedFlags = labelsToImapFlags(historyMessageLabelIds) - updateCandidates[historyLabelRemoved.message.uid] = existedFlags - } - } - - history.labelsAdded?.let { labelsAdded -> - for (historyLabelAdded in labelsAdded) { - labelsToBeUpdatedMap[historyLabelAdded.message.uid] = historyLabelAdded.message - .labelIds.joinToString(MessageEntity.LABEL_IDS_SEPARATOR) - if (localFolder.fullName in (historyLabelAdded.labelIds ?: emptyList())) { - deleteCandidatesUIDs.remove(historyLabelAdded.message.uid) - updateCandidates.remove(historyLabelAdded.message.uid) - newCandidatesMap[historyLabelAdded.message.uid] = historyLabelAdded.message - continue - } - - if ((historyLabelAdded.labelIds ?: emptyList()).contains(LABEL_TRASH)) { - newCandidatesMap.remove(historyLabelAdded.message.uid) - updateCandidates.remove(historyLabelAdded.message.uid) - deleteCandidatesUIDs.add(historyLabelAdded.message.uid) - continue - } - - val existedFlags = labelsToImapFlags(historyLabelAdded.message.labelIds ?: emptyList()) - updateCandidates[historyLabelAdded.message.uid] = existedFlags - } - } - } - action.invoke( - deleteCandidatesUIDs, - newCandidatesMap, - updateCandidates, - labelsToBeUpdatedMap - ) - } - suspend fun identifyAttachments( msgEntities: List, msgs: List, account: AccountEntity, localFolder: LocalFolder, roomDatabase: FlowCryptRoomDatabase diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt new file mode 100644 index 0000000000..1ee96b4f83 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt @@ -0,0 +1,358 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.api.email.gmail + +import android.content.Context +import com.flowcrypt.email.R +import com.flowcrypt.email.api.email.FoldersManager +import com.flowcrypt.email.api.email.gmail.GmailApiHelper.Companion.LABEL_DRAFT +import com.flowcrypt.email.api.email.gmail.GmailApiHelper.Companion.LABEL_TRASH +import com.flowcrypt.email.api.email.gmail.GmailApiHelper.Companion.labelsToImapFlags +import com.flowcrypt.email.api.email.gmail.api.GmaiAPIMimeMessage +import com.flowcrypt.email.api.email.gmail.model.GmailThreadInfo +import com.flowcrypt.email.api.email.model.LocalFolder +import com.flowcrypt.email.database.FlowCryptRoomDatabase +import com.flowcrypt.email.database.entity.AccountEntity +import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.database.entity.MessageEntity.Companion.LABEL_IDS_SEPARATOR +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasPgp +import com.flowcrypt.email.extensions.kotlin.toHex +import com.flowcrypt.email.extensions.uid +import com.flowcrypt.email.service.MessagesNotificationManager +import com.flowcrypt.email.util.GeneralUtil +import com.google.api.services.gmail.model.History +import com.google.api.services.gmail.model.Message +import com.google.api.services.gmail.model.Thread +import jakarta.mail.Flags +import jakarta.mail.Session +import jakarta.mail.internet.InternetAddress +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext +import java.util.Properties + +/** + * @author Denys Bondarenko + */ +object GmailHistoryHandler { + suspend fun handleHistory( + context: Context, + accountEntity: AccountEntity, + localFolder: LocalFolder, + historyList: List, + action: suspend ( + deleteCandidatesUIDs: Set, + newCandidatesMap: Map, + updateCandidatesMap: Map, + labelsToBeUpdatedMap: Map + ) -> Unit = { _, _, _, _ -> } + ) = withContext(Dispatchers.IO) { + processHistory(localFolder, historyList) { deleteCandidatesUIDs, + newCandidatesMap, + updateCandidatesMap, + labelsToBeUpdatedMap -> + val applicationContext = context.applicationContext + val roomDatabase = FlowCryptRoomDatabase.getDatabase(applicationContext) + + roomDatabase.msgDao() + .deleteByUIDsSuspend(accountEntity.email, localFolder.fullName, deleteCandidatesUIDs) + + val folderType = FoldersManager.getFolderType(localFolder) + if (folderType === FoldersManager.FolderType.INBOX) { + val notificationManager = MessagesNotificationManager(applicationContext) + for (uid in deleteCandidatesUIDs) { + notificationManager.cancel(uid.toHex()) + } + } + + val newCandidates = newCandidatesMap.values + if (newCandidates.isNotEmpty()) { + val gmailThreadInfoList: List + val msgs: List + if (accountEntity.useConversationMode) { + val uniqueThreadIdList = newCandidates.map { it.threadId }.toSet() + gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( + context = applicationContext, + accountEntity = accountEntity, + threads = uniqueThreadIdList.map { Thread().apply { id = it } }, + format = GmailApiHelper.RESPONSE_FORMAT_FULL, + localFolder = localFolder + ) + + msgs = gmailThreadInfoList.map { it.lastMessage } + } else { + gmailThreadInfoList = emptyList() + msgs = GmailApiHelper.loadMsgsInParallel( + applicationContext, accountEntity, + newCandidates.toList(), localFolder + ).run { + if (accountEntity.showOnlyEncrypted == true) { + filter { it.hasPgp() } + } else this + } + } + + val draftIdsMap = if (localFolder.isDrafts) { + val drafts = + GmailApiHelper.loadBaseDraftInfoInParallel(applicationContext, accountEntity, msgs) + val fetchedDraftIdsMap = drafts.associateBy({ it.message.id }, { it.id }) + if (fetchedDraftIdsMap.size != msgs.size) { + throw IllegalStateException( + (applicationContext as Context).getString(R.string.fetching_drafts_info_failed) + ) + } + fetchedDraftIdsMap + } else emptyMap() + + val isNew = + !GeneralUtil.isAppForegrounded() && folderType === FoldersManager.FolderType.INBOX + + val msgEntities = MessageEntity.genMessageEntities( + context = applicationContext, + account = accountEntity.email, + accountType = accountEntity.accountType, + label = localFolder.fullName, + msgsList = msgs, + isNew = isNew, + onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, + draftIdsMap = draftIdsMap + ) { message, messageEntity -> + if (accountEntity.useConversationMode) { + val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } + messageEntity.copy( + subject = thread?.subject, + threadMessagesCount = thread?.messagesCount, + labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), + hasAttachments = thread?.hasAttachments, + threadRecipientsAddresses = InternetAddress.toString( + thread?.recipients?.toTypedArray() + ), + hasPgp = thread?.hasPgpThings + ) + } else { + messageEntity + } + } + + roomDatabase.msgDao().insertWithReplaceSuspend(msgEntities) + GmailApiHelper.identifyAttachments( + msgEntities, + msgs, + accountEntity, + localFolder, + roomDatabase + ) + } + + if (accountEntity.useConversationMode) { + //this code should be improved. Just for proof of concept + val s = updateCandidatesMap.keys//it's messages ids + val threadIds = roomDatabase.msgDao() + .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = s) + .mapNotNull { it.threadId }.toSet() + + val gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( + context = applicationContext, + accountEntity = accountEntity, + threads = threadIds.map { Thread().apply { id = it } }, + format = GmailApiHelper.RESPONSE_FORMAT_FULL, + localFolder = localFolder + ) + + val msgs = gmailThreadInfoList.map { it.lastMessage } + val map = MessageEntity.genMessageEntities( + context = applicationContext, + account = accountEntity.email, + accountType = accountEntity.accountType, + label = localFolder.fullName, + msgsList = msgs, + isNew = false, + onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, + draftIdsMap = emptyMap() + ) { message, messageEntity -> + val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } + messageEntity.copy( + subject = thread?.subject, + threadMessagesCount = thread?.messagesCount, + labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), + hasAttachments = thread?.hasAttachments, + threadRecipientsAddresses = InternetAddress.toString( + thread?.recipients?.toTypedArray() + ), + hasPgp = thread?.hasPgpThings + ) + }.associateBy({ it.uid }, + { + labelsToImapFlags( + it.labelIds?.split(LABEL_IDS_SEPARATOR) ?: emptyList() + ) + }) + + roomDatabase.msgDao() + .updateFlagsSuspend(accountEntity.email, localFolder.fullName, map) + + + val m = labelsToBeUpdatedMap.keys//it's messages ids + val threadIds2 = roomDatabase.msgDao() + .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = m) + .mapNotNull { it.threadId }.toSet() + + val gmailThreadInfoList2 = GmailApiHelper.loadGmailThreadInfoInParallel( + context = applicationContext, + accountEntity = accountEntity, + threads = threadIds2.map { Thread().apply { id = it } }, + format = GmailApiHelper.RESPONSE_FORMAT_FULL, + localFolder = localFolder + ) + + val msgs2 = gmailThreadInfoList2.map { it.lastMessage } + val map2 = MessageEntity.genMessageEntities( + context = applicationContext, + account = accountEntity.email, + accountType = accountEntity.accountType, + label = localFolder.fullName, + msgsList = msgs2, + isNew = false, + onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, + draftIdsMap = emptyMap() + ) { message, messageEntity -> + val thread = gmailThreadInfoList2.firstOrNull { it.id == message.threadId } + messageEntity.copy( + subject = thread?.subject, + threadMessagesCount = thread?.messagesCount, + labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), + hasAttachments = thread?.hasAttachments, + threadRecipientsAddresses = InternetAddress.toString( + thread?.recipients?.toTypedArray() + ), + hasPgp = thread?.hasPgpThings + ) + }.associateBy({ it.uid }, { it.labelIds ?: "" }) + + roomDatabase.msgDao() + .updateGmailLabels(accountEntity.email, localFolder.fullName, map2) + } else { + roomDatabase.msgDao() + .updateFlagsSuspend(accountEntity.email, localFolder.fullName, updateCandidatesMap) + + roomDatabase.msgDao() + .updateGmailLabels(accountEntity.email, localFolder.fullName, labelsToBeUpdatedMap) + } + + if (folderType === FoldersManager.FolderType.SENT) { + val session = Session.getInstance(Properties()) + GeneralUtil.updateLocalContactsIfNeeded( + context = applicationContext, + messages = newCandidates + .filter { it.labelIds?.contains(GmailApiHelper.LABEL_SENT) == true } + .map { GmaiAPIMimeMessage(session, it) }.toTypedArray() + ) + } + + action.invoke( + deleteCandidatesUIDs, + newCandidatesMap, + updateCandidatesMap, + labelsToBeUpdatedMap + ) + } + } + + private suspend fun processHistory( + localFolder: LocalFolder, + historyList: List, + action: suspend ( + deleteCandidatesUIDs: Set, + newCandidatesMap: Map, + updateCandidatesMap: Map, + labelsToBeUpdatedMap: Map + ) -> Unit + ) = withContext(Dispatchers.IO) + { + val deleteCandidatesUIDs = mutableSetOf() + val newCandidatesMap = mutableMapOf() + val updateCandidates = mutableMapOf() + val labelsToBeUpdatedMap = mutableMapOf() + val isDrafts = localFolder.isDrafts + + for (history in historyList) { + history.messagesDeleted?.let { messagesDeleted -> + for (historyMsgDeleted in messagesDeleted) { + newCandidatesMap.remove(historyMsgDeleted.message.uid) + updateCandidates.remove(historyMsgDeleted.message.uid) + deleteCandidatesUIDs.add(historyMsgDeleted.message.uid) + } + } + + history.messagesAdded?.let { messagesAdded -> + for (historyMsgAdded in messagesAdded) { + if (LABEL_DRAFT in (historyMsgAdded.message.labelIds ?: emptyList()) && !isDrafts) { + //skip adding drafts to non-Drafts folder + continue + } + deleteCandidatesUIDs.remove(historyMsgAdded.message.uid) + updateCandidates.remove(historyMsgAdded.message.uid) + newCandidatesMap[historyMsgAdded.message.uid] = historyMsgAdded.message + } + } + + history.labelsRemoved?.let { labelsRemoved -> + for (historyLabelRemoved in labelsRemoved) { + val historyMessageLabelIds = historyLabelRemoved?.message?.labelIds ?: emptyList() + labelsToBeUpdatedMap[historyLabelRemoved.message.uid] = + historyMessageLabelIds.joinToString(LABEL_IDS_SEPARATOR) + if (localFolder.fullName in (historyLabelRemoved.labelIds ?: emptyList())) { + newCandidatesMap.remove(historyLabelRemoved.message.uid) + updateCandidates.remove(historyLabelRemoved.message.uid) + deleteCandidatesUIDs.add(historyLabelRemoved.message.uid) + continue + } + + if (LABEL_TRASH in (historyLabelRemoved.labelIds ?: emptyList())) { + val message = historyLabelRemoved.message + if (localFolder.fullName in historyMessageLabelIds) { + deleteCandidatesUIDs.remove(message.uid) + updateCandidates.remove(message.uid) + newCandidatesMap[message.uid] = message + continue + } + } + + val existedFlags = labelsToImapFlags(historyMessageLabelIds) + updateCandidates[historyLabelRemoved.message.uid] = existedFlags + } + } + + history.labelsAdded?.let { labelsAdded -> + for (historyLabelAdded in labelsAdded) { + labelsToBeUpdatedMap[historyLabelAdded.message.uid] = historyLabelAdded.message + .labelIds.joinToString(LABEL_IDS_SEPARATOR) + if (localFolder.fullName in (historyLabelAdded.labelIds ?: emptyList())) { + deleteCandidatesUIDs.remove(historyLabelAdded.message.uid) + updateCandidates.remove(historyLabelAdded.message.uid) + newCandidatesMap[historyLabelAdded.message.uid] = historyLabelAdded.message + continue + } + + if ((historyLabelAdded.labelIds ?: emptyList()).contains(LABEL_TRASH)) { + newCandidatesMap.remove(historyLabelAdded.message.uid) + updateCandidates.remove(historyLabelAdded.message.uid) + deleteCandidatesUIDs.add(historyLabelAdded.message.uid) + continue + } + + val existedFlags = labelsToImapFlags(historyLabelAdded.message.labelIds ?: emptyList()) + updateCandidates[historyLabelAdded.message.uid] = existedFlags + } + } + } + action.invoke( + deleteCandidatesUIDs, + newCandidatesMap, + updateCandidates, + labelsToBeUpdatedMap + ) + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index 8c17cca41b..d47bb8e5e3 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -6,7 +6,6 @@ package com.flowcrypt.email.jetpack.viewmodel import android.app.Application -import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.liveData @@ -22,6 +21,7 @@ import com.flowcrypt.email.api.email.FoldersManager import com.flowcrypt.email.api.email.IMAPStoreManager import com.flowcrypt.email.api.email.JavaEmailConstants import com.flowcrypt.email.api.email.gmail.GmailApiHelper +import com.flowcrypt.email.api.email.gmail.GmailHistoryHandler import com.flowcrypt.email.api.email.gmail.api.GmaiAPIMimeMessage import com.flowcrypt.email.api.email.gmail.model.GmailThreadInfo import com.flowcrypt.email.api.email.model.LocalFolder @@ -34,9 +34,7 @@ import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.LabelEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.database.entity.MessageEntity.Companion.LABEL_IDS_SEPARATOR -import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasPgp import com.flowcrypt.email.extensions.kotlin.toHex -import com.flowcrypt.email.jetpack.workmanager.EmailAndNameWorker import com.flowcrypt.email.jetpack.workmanager.sync.SyncDraftsWorker import com.flowcrypt.email.jetpack.workmanager.sync.UploadDraftsWorker import com.flowcrypt.email.service.MessagesNotificationManager @@ -46,14 +44,11 @@ import com.flowcrypt.email.util.OutgoingMessagesManager import com.flowcrypt.email.util.coroutines.runners.ControlledRunner import com.flowcrypt.email.util.exception.ExceptionUtil import com.google.api.client.googleapis.json.GoogleJsonResponseException -import com.google.api.services.gmail.model.History import com.google.api.services.gmail.model.ListDraftsResponse import com.google.api.services.gmail.model.ListMessagesResponse -import com.google.api.services.gmail.model.Thread import jakarta.mail.FetchProfile import jakarta.mail.Folder import jakarta.mail.Message -import jakarta.mail.MessagingException import jakarta.mail.Session import jakarta.mail.Store import jakarta.mail.UIDFolder @@ -609,7 +604,9 @@ class MessagesViewModel(application: Application) : AccountViewModel(application roomDatabase.msgDao().insertWithReplaceSuspend(msgEntities) GmailApiHelper.identifyAttachments(msgEntities, msgs, account, localFolder, roomDatabase) val session = Session.getInstance(Properties()) - updateLocalContactsIfNeeded(messages = msgs + GeneralUtil.updateLocalContactsIfNeeded( + context = getApplication(), + messages = msgs .filter { it.labelIds?.contains(GmailApiHelper.LABEL_SENT) == true } .map { GmaiAPIMimeMessage(session, it) }.toTypedArray() ) @@ -638,7 +635,11 @@ class MessagesViewModel(application: Application) : AccountViewModel(application roomDatabase.msgDao().insertWithReplaceSuspend(msgEntities) identifyAttachments(msgEntities, msgs, remoteFolder, account, localFolder, roomDatabase) - updateLocalContactsIfNeeded(remoteFolder, msgs) + GeneralUtil.updateLocalContactsIfNeeded( + context = getApplication(), + imapFolder = remoteFolder, + messages = msgs + ) } private suspend fun identifyAttachments( @@ -672,59 +673,6 @@ class MessagesViewModel(application: Application) : AccountViewModel(application } } - private suspend fun updateLocalContactsIfNeeded( - imapFolder: IMAPFolder? = null, - messages: Array - ) = withContext(Dispatchers.IO) { - try { - val isSentFolder = imapFolder?.attributes?.contains("\\Sent") ?: true - - if (isSentFolder) { - val emailAndNamePairs = mutableListOf>() - for (message in messages) { - emailAndNamePairs.addAll(getEmailAndNamePairs(message)) - } - - EmailAndNameWorker.enqueue(getApplication(), emailAndNamePairs) - } - } catch (e: MessagingException) { - e.printStackTrace() - ExceptionUtil.handleError(e) - } - } - - /** - * Generate a list of [Pair] objects from the input message. - * This information will be retrieved from "to" and "cc" headers. - * - * @param msg The input [jakarta.mail.Message]. - * @return [List] of [Pair] objects, which contains information - * about - * emails and names. - * @throws MessagingException when retrieve information about recipients. - */ - private fun getEmailAndNamePairs(msg: Message): List> { - val pairs = mutableListOf>() - - val addressesTo = msg.getRecipients(Message.RecipientType.TO) - if (addressesTo != null) { - for (address in addressesTo) { - val internetAddress = address as InternetAddress - pairs.add(Pair(internetAddress.address, internetAddress.personal)) - } - } - - val addressesCC = msg.getRecipients(Message.RecipientType.CC) - if (addressesCC != null) { - for (address in addressesCC) { - val internetAddress = address as InternetAddress - pairs.add(Pair(internetAddress.address, internetAddress.personal)) - } - } - - return pairs - } - private suspend fun searchMsgsOnRemoteServerAndStoreLocally( accountEntity: AccountEntity, localFolder: LocalFolder, @@ -860,7 +808,11 @@ class MessagesViewModel(application: Application) : AccountViewModel(application roomDatabase.msgDao().insertWithReplaceSuspend(msgEntities) - updateLocalContactsIfNeeded(remoteFolder, msgs) + GeneralUtil.updateLocalContactsIfNeeded( + context = getApplication(), + imapFolder = remoteFolder, + messages = msgs + ) } private suspend fun handleSearchResults( @@ -881,7 +833,9 @@ class MessagesViewModel(application: Application) : AccountViewModel(application roomDatabase.msgDao().insertWithReplaceSuspend(msgEntities) GmailApiHelper.identifyAttachments(msgEntities, msgs, account, localFolder, roomDatabase) val session = Session.getInstance(Properties()) - updateLocalContactsIfNeeded(messages = msgs + GeneralUtil.updateLocalContactsIfNeeded( + context = getApplication(), + messages = msgs .filter { it.labelIds?.contains(GmailApiHelper.LABEL_SENT) == true } .map { GmaiAPIMimeMessage(session, it) }.toTypedArray() ) @@ -906,7 +860,12 @@ class MessagesViewModel(application: Application) : AccountViewModel(application historyId = labelEntityHistoryId.max(msgEntityHistoryId) ) - handleMsgsFromHistory(accountEntity, localFolder, historyList) + GmailHistoryHandler.handleHistory( + context = getApplication(), + accountEntity = accountEntity, + localFolder = localFolder, + historyList = historyList + ) } catch (e: Exception) { e.printStackTrace() when (e) { @@ -1046,211 +1005,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application .associate { remoteFolder.getUID(it) to it.flags } roomDatabase.msgDao().updateFlagsSuspend(accountEntity.email, folderName, updateCandidates) - updateLocalContactsIfNeeded(remoteFolder, newCandidates) - } - - private suspend fun handleMsgsFromHistory( - accountEntity: AccountEntity, localFolder: LocalFolder, - historyList: List - ) = withContext(Dispatchers.IO) { - - GmailApiHelper.processHistory(localFolder, historyList) { deleteCandidatesUIDs, - newCandidatesMap, - updateCandidatesMap, - labelsToBeUpdatedMap -> - roomDatabase.msgDao() - .deleteByUIDsSuspend(accountEntity.email, localFolder.fullName, deleteCandidatesUIDs) - - val folderType = FoldersManager.getFolderType(localFolder) - if (folderType === FoldersManager.FolderType.INBOX) { - val notificationManager = MessagesNotificationManager(getApplication()) - for (uid in deleteCandidatesUIDs) { - notificationManager.cancel(uid.toHex()) - } - } - - val newCandidates = newCandidatesMap.values - if (newCandidates.isNotEmpty()) { - val gmailThreadInfoList: List - val msgs: List - if (accountEntity.useConversationMode) { - val uniqueThreadIdList = newCandidates.map { it.threadId }.toSet() - gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( - context = getApplication(), - accountEntity = accountEntity, - threads = uniqueThreadIdList.map { Thread().apply { id = it } }, - format = GmailApiHelper.RESPONSE_FORMAT_FULL, - localFolder = localFolder - ) - - msgs = gmailThreadInfoList.map { it.lastMessage } - } else { - gmailThreadInfoList = emptyList() - msgs = GmailApiHelper.loadMsgsInParallel( - getApplication(), accountEntity, - newCandidates.toList(), localFolder - ).run { - if (accountEntity.showOnlyEncrypted == true) { - filter { it.hasPgp() } - } else this - } - } - - val draftIdsMap = if (localFolder.isDrafts) { - val drafts = - GmailApiHelper.loadBaseDraftInfoInParallel(getApplication(), accountEntity, msgs) - val fetchedDraftIdsMap = drafts.associateBy({ it.message.id }, { it.id }) - if (fetchedDraftIdsMap.size != msgs.size) { - throw IllegalStateException( - (getApplication() as Context).getString(R.string.fetching_drafts_info_failed) - ) - } - fetchedDraftIdsMap - } else emptyMap() - - val isNew = - !GeneralUtil.isAppForegrounded() && folderType === FoldersManager.FolderType.INBOX - - val msgEntities = MessageEntity.genMessageEntities( - context = getApplication(), - account = accountEntity.email, - accountType = accountEntity.accountType, - label = localFolder.fullName, - msgsList = msgs, - isNew = isNew, - onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, - draftIdsMap = draftIdsMap - ) { message, messageEntity -> - if (accountEntity.useConversationMode) { - val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } - messageEntity.copy( - subject = thread?.subject, - threadMessagesCount = thread?.messagesCount, - labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), - hasAttachments = thread?.hasAttachments, - threadRecipientsAddresses = InternetAddress.toString( - thread?.recipients?.toTypedArray() - ), - hasPgp = thread?.hasPgpThings - ) - } else { - messageEntity - } - } - - roomDatabase.msgDao().insertWithReplaceSuspend(msgEntities) - GmailApiHelper.identifyAttachments( - msgEntities, - msgs, - accountEntity, - localFolder, - roomDatabase - ) - } - - if (accountEntity.useConversationMode) { - //this code should be improved. Just for proof of concept - val s = updateCandidatesMap.keys//it's messages ids - val threadIds = roomDatabase.msgDao() - .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = s) - .mapNotNull { it.threadId }.toSet() - - val gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( - context = getApplication(), - accountEntity = accountEntity, - threads = threadIds.map { Thread().apply { id = it } }, - format = GmailApiHelper.RESPONSE_FORMAT_FULL, - localFolder = localFolder - ) - - val msgs = gmailThreadInfoList.map { it.lastMessage } - val map = MessageEntity.genMessageEntities( - context = getApplication(), - account = accountEntity.email, - accountType = accountEntity.accountType, - label = localFolder.fullName, - msgsList = msgs, - isNew = false, - onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, - draftIdsMap = emptyMap() - ) { message, messageEntity -> - val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } - messageEntity.copy( - subject = thread?.subject, - threadMessagesCount = thread?.messagesCount, - labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), - hasAttachments = thread?.hasAttachments, - threadRecipientsAddresses = InternetAddress.toString( - thread?.recipients?.toTypedArray() - ), - hasPgp = thread?.hasPgpThings - ) - }.associateBy({ it.uid }, - { - GmailApiHelper.labelsToImapFlags( - it.labelIds?.split(LABEL_IDS_SEPARATOR) ?: emptyList() - ) - }) - - roomDatabase.msgDao() - .updateFlagsSuspend(accountEntity.email, localFolder.fullName, map) - - - val m = labelsToBeUpdatedMap.keys//it's messages ids - val threadIds2 = roomDatabase.msgDao() - .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = m) - .mapNotNull { it.threadId }.toSet() - - val gmailThreadInfoList2 = GmailApiHelper.loadGmailThreadInfoInParallel( - context = getApplication(), - accountEntity = accountEntity, - threads = threadIds2.map { Thread().apply { id = it } }, - format = GmailApiHelper.RESPONSE_FORMAT_FULL, - localFolder = localFolder - ) - - val msgs2 = gmailThreadInfoList2.map { it.lastMessage } - val map2 = MessageEntity.genMessageEntities( - context = getApplication(), - account = accountEntity.email, - accountType = accountEntity.accountType, - label = localFolder.fullName, - msgsList = msgs2, - isNew = false, - onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, - draftIdsMap = emptyMap() - ) { message, messageEntity -> - val thread = gmailThreadInfoList2.firstOrNull { it.id == message.threadId } - messageEntity.copy( - subject = thread?.subject, - threadMessagesCount = thread?.messagesCount, - labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), - hasAttachments = thread?.hasAttachments, - threadRecipientsAddresses = InternetAddress.toString( - thread?.recipients?.toTypedArray() - ), - hasPgp = thread?.hasPgpThings - ) - }.associateBy({ it.uid }, { it.labelIds ?: "" }) - - roomDatabase.msgDao() - .updateGmailLabels(accountEntity.email, localFolder.fullName, map2) - } else { - roomDatabase.msgDao() - .updateFlagsSuspend(accountEntity.email, localFolder.fullName, updateCandidatesMap) - - roomDatabase.msgDao() - .updateGmailLabels(accountEntity.email, localFolder.fullName, labelsToBeUpdatedMap) - } - - if (folderType === FoldersManager.FolderType.SENT) { - val session = Session.getInstance(Properties()) - updateLocalContactsIfNeeded(messages = newCandidates - .filter { it.labelIds?.contains(GmailApiHelper.LABEL_SENT) == true } - .map { GmaiAPIMimeMessage(session, it) }.toTypedArray() - ) - } - } + GeneralUtil.updateLocalContactsIfNeeded(getApplication(), remoteFolder, newCandidates) } private suspend fun clearHistoryIdForLabel(accountEntity: AccountEntity, label: String) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt index dcbca2e079..93f7fa52d3 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.workmanager.sync @@ -11,21 +11,22 @@ import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.extensions.isAppForegrounded import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.service.MessagesNotificationManager import com.flowcrypt.email.util.GeneralUtil -import org.eclipse.angus.mail.imap.IMAPFolder import jakarta.mail.Flags import jakarta.mail.Message import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPFolder /** * @author Denys Bondarenko */ abstract class BaseIdleWorker(context: Context, params: WorkerParameters) : BaseSyncWorker(context, params) { - private val notificationManager = MessagesNotificationManager(applicationContext) + protected val notificationManager = MessagesNotificationManager(applicationContext) protected suspend fun processDeletedMsgs( cachedUIDSet: Set, remoteFolder: IMAPFolder, @@ -42,8 +43,12 @@ abstract class BaseIdleWorker(context: Context, params: WorkerParameters) : ) { roomDatabase.msgDao() .deleteByUIDsSuspend(accountEntity.email, folderFullName, deleteCandidatesUIDs) - if (!GeneralUtil.isAppForegrounded()) { - for (uid in deleteCandidatesUIDs) { + tryToRemoveNotifications(deleteCandidatesUIDs) + } + + protected fun tryToRemoveNotifications(uidList: Collection) { + if (!applicationContext.isAppForegrounded()) { + for (uid in uidList) { notificationManager.cancel(uid.toHex()) } } @@ -67,6 +72,10 @@ abstract class BaseIdleWorker(context: Context, params: WorkerParameters) : ) { roomDatabase.msgDao().updateFlagsSuspend(accountEntity.email, folderFullName, updateCandidates) + removeNotificationForSeenMessages(updateCandidates) + } + + protected fun removeNotificationForSeenMessages(updateCandidates: Map) { if (!GeneralUtil.isAppForegrounded()) { for (item in updateCandidates) { val uid = item.key @@ -112,6 +121,13 @@ abstract class BaseIdleWorker(context: Context, params: WorkerParameters) : ) { roomDatabase.msgDao().insertWithReplaceSuspend(msgEntities) + tryToShowNotificationsForNewMessages(accountEntity, localFolder) + } + + protected suspend fun tryToShowNotificationsForNewMessages( + accountEntity: AccountEntity, + localFolder: LocalFolder + ) { if (!GeneralUtil.isAppForegrounded()) { val detailsList = roomDatabase.msgDao().getNewMsgsSuspend(accountEntity.email, localFolder.fullName) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt index 6bf3ac31dc..5349fde4e3 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt @@ -12,21 +12,15 @@ import com.flowcrypt.email.BuildConfig import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.FoldersManager import com.flowcrypt.email.api.email.gmail.GmailApiHelper -import com.flowcrypt.email.api.email.gmail.model.GmailThreadInfo +import com.flowcrypt.email.api.email.gmail.GmailHistoryHandler import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.entity.AccountEntity -import com.flowcrypt.email.database.entity.MessageEntity -import com.flowcrypt.email.database.entity.MessageEntity.Companion.LABEL_IDS_SEPARATOR -import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasPgp -import com.flowcrypt.email.extensions.isAppForegrounded import com.flowcrypt.email.util.exception.GmailAPIException import com.google.api.services.gmail.model.History -import com.google.api.services.gmail.model.Thread import jakarta.mail.FetchProfile import jakarta.mail.Folder import jakarta.mail.Store import jakarta.mail.UIDFolder -import jakarta.mail.internet.InternetAddress import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.eclipse.angus.mail.imap.IMAPFolder @@ -161,83 +155,15 @@ open class InboxIdleSyncWorker(context: Context, params: WorkerParameters) : accountEntity: AccountEntity, localFolder: LocalFolder, historyList: List ) = withContext(Dispatchers.IO) { - GmailApiHelper.processHistory(localFolder, historyList) { deleteCandidatesUIDs, - newCandidatesMap, - updateCandidatesMap, - labelsToBeUpdatedMap -> - processDeletedMsgs(accountEntity, localFolder.fullName, deleteCandidatesUIDs) - - val newCandidates = newCandidatesMap.values.toList() - if (newCandidates.isNotEmpty()) { - val gmailThreadInfoList: List - val msgs: List - - if (accountEntity.useConversationMode) { - val uniqueThreadIdList = newCandidates.map { it.threadId }.toSet() - gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( - context = applicationContext, - accountEntity = accountEntity, - threads = uniqueThreadIdList.map { Thread().apply { id = it } }, - format = GmailApiHelper.RESPONSE_FORMAT_FULL, - localFolder = localFolder - ) - - msgs = gmailThreadInfoList.map { it.lastMessage } - } else { - gmailThreadInfoList = emptyList() - msgs = GmailApiHelper.loadMsgsInParallel( - applicationContext, accountEntity, - newCandidates.toList(), localFolder - ).run { - if (accountEntity.showOnlyEncrypted == true) { - filter { it.hasPgp() } - } else this - } - } - - val isOnlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false - val isNew = applicationContext.isAppForegrounded() - - val msgEntities = MessageEntity.genMessageEntities( - context = applicationContext, - account = accountEntity.email, - accountType = accountEntity.accountType, - label = localFolder.fullName, - msgsList = msgs, - isNew = isNew, - onlyPgpModeEnabled = isOnlyPgpModeEnabled - ) { message, messageEntity -> - if (accountEntity.useConversationMode) { - val thread = gmailThreadInfoList.firstOrNull { it.id == message.threadId } - messageEntity.copy( - subject = thread?.subject, - threadMessagesCount = thread?.messagesCount, - labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), - hasAttachments = thread?.hasAttachments, - threadRecipientsAddresses = InternetAddress.toString( - thread?.recipients?.toTypedArray() - ), - hasPgp = thread?.hasPgpThings - ) - } else { - messageEntity - } - } - - processNewMsgs(accountEntity, localFolder, msgEntities) - GmailApiHelper.identifyAttachments( - msgEntities, - msgs, - accountEntity, - localFolder, - roomDatabase - ) - } - - processUpdatedMsgs(accountEntity, localFolder.fullName, updateCandidatesMap) - - roomDatabase.msgDao() - .updateGmailLabels(accountEntity.email, localFolder.fullName, labelsToBeUpdatedMap) + GmailHistoryHandler.handleHistory( + applicationContext, + accountEntity, + localFolder, + historyList + ) { deleteCandidatesUIDs, _, updateCandidatesMap, _ -> + tryToRemoveNotifications(deleteCandidatesUIDs) + tryToShowNotificationsForNewMessages(accountEntity, localFolder) + removeNotificationForSeenMessages(updateCandidatesMap) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt index 6ee89f7d4b..285b8cd8f1 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.util @@ -39,17 +39,23 @@ import com.flowcrypt.email.api.retrofit.RetrofitApiServiceInterface import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.extensions.hasActiveConnection +import com.flowcrypt.email.jetpack.workmanager.EmailAndNameWorker import com.flowcrypt.email.model.KeysStorage import com.flowcrypt.email.security.pgp.PgpKey import com.flowcrypt.email.ui.notifications.ErrorNotificationManager import com.flowcrypt.email.util.exception.CommonConnectionException +import com.flowcrypt.email.util.exception.ExceptionUtil import com.flowcrypt.email.util.google.GoogleApiClientHelper import com.google.android.gms.auth.api.signin.GoogleSignIn +import jakarta.mail.Message +import jakarta.mail.MessagingException +import jakarta.mail.internet.InternetAddress import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import okhttp3.OkHttpClient import org.apache.commons.io.IOUtils +import org.eclipse.angus.mail.imap.IMAPFolder import org.jose4j.jwt.consumer.JwtConsumerBuilder import retrofit2.Retrofit import java.io.File @@ -583,5 +589,59 @@ class GeneralUtil { val preamble = context.getString(R.string.no_private_keys_suitable_for_encryption) return "$preamble

$stringBuilder" } + + suspend fun updateLocalContactsIfNeeded( + context: Context, + imapFolder: IMAPFolder? = null, + messages: Array + ) = withContext(Dispatchers.IO) { + try { + val isSentFolder = imapFolder?.attributes?.contains("\\Sent") ?: true + + if (isSentFolder) { + val emailAndNamePairs = mutableListOf>() + for (message in messages) { + emailAndNamePairs.addAll(getEmailAndNamePairs(message)) + } + + EmailAndNameWorker.enqueue(context, emailAndNamePairs) + } + } catch (e: MessagingException) { + e.printStackTrace() + ExceptionUtil.handleError(e) + } + } + + /** + * Generate a list of [Pair] objects from the input message. + * This information will be retrieved from "to" and "cc" headers. + * + * @param msg The input [jakarta.mail.Message]. + * @return [List] of [Pair] objects, which contains information + * about + * emails and names. + * @throws MessagingException when retrieve information about recipients. + */ + private fun getEmailAndNamePairs(msg: Message): List> { + val pairs = mutableListOf>() + + val addressesTo = msg.getRecipients(Message.RecipientType.TO) + if (addressesTo != null) { + for (address in addressesTo) { + val internetAddress = address as InternetAddress + pairs.add(Pair(internetAddress.address, internetAddress.personal)) + } + } + + val addressesCC = msg.getRecipients(Message.RecipientType.CC) + if (addressesCC != null) { + for (address in addressesCC) { + val internetAddress = address as InternetAddress + pairs.add(Pair(internetAddress.address, internetAddress.personal)) + } + } + + return pairs + } } } From c6829c3e2c96010dc8e14c9952dc51143340e87f Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 3 Sep 2024 17:18:58 +0300 Subject: [PATCH 041/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 4 +- .../api/email/gmail/GmailHistoryHandler.kt | 44 ++++++++++++++++--- .../api/email/gmail/model/GmailThreadInfo.kt | 1 + .../email/database/dao/MessageDao.kt | 10 +++++ .../api/services/gmail/model/ThreadExt.kt | 7 +++ 5 files changed, 60 insertions(+), 6 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index ec86c5943d..f9675cc04e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -25,6 +25,7 @@ import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getRec import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getSubject import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getUniqueLabelsSet import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getUniqueRecipients +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasUnreadMessages import com.flowcrypt.email.extensions.contentId import com.flowcrypt.email.extensions.disposition import com.flowcrypt.email.extensions.isMimeType @@ -1213,7 +1214,8 @@ class GmailApiHelper { messagesCount = thread.messages?.size ?: 0, recipients = thread.getUniqueRecipients(receiverEmail), subject = subject, - labels = thread.getUniqueLabelsSet() + labels = thread.getUniqueLabelsSet(), + hasUnreadMessages = thread.hasUnreadMessages() ) return gmailThreadInfo } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt index 1ee96b4f83..0abb007b6a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt @@ -14,11 +14,13 @@ import com.flowcrypt.email.api.email.gmail.GmailApiHelper.Companion.labelsToImap import com.flowcrypt.email.api.email.gmail.api.GmaiAPIMimeMessage import com.flowcrypt.email.api.email.gmail.model.GmailThreadInfo import com.flowcrypt.email.api.email.model.LocalFolder +import com.flowcrypt.email.api.email.model.MessageFlag import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.database.entity.MessageEntity.Companion.LABEL_IDS_SEPARATOR import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasPgp +import com.flowcrypt.email.extensions.isAppForegrounded import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.extensions.uid import com.flowcrypt.email.service.MessagesNotificationManager @@ -71,8 +73,12 @@ object GmailHistoryHandler { if (newCandidates.isNotEmpty()) { val gmailThreadInfoList: List val msgs: List + val existingThreads: List if (accountEntity.useConversationMode) { val uniqueThreadIdList = newCandidates.map { it.threadId }.toSet() + existingThreads = roomDatabase.msgDao() + .getThreadsByID(accountEntity.email, localFolder.fullName, uniqueThreadIdList) + val existingThreadIds = existingThreads.mapNotNull { it.threadId } gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( context = applicationContext, accountEntity = accountEntity, @@ -81,9 +87,11 @@ object GmailHistoryHandler { localFolder = localFolder ) - msgs = gmailThreadInfoList.map { it.lastMessage } + msgs = + gmailThreadInfoList.map { it.lastMessage }.filter { it.threadId !in existingThreadIds } } else { gmailThreadInfoList = emptyList() + existingThreads = emptyList() msgs = GmailApiHelper.loadMsgsInParallel( applicationContext, accountEntity, newCandidates.toList(), localFolder @@ -106,8 +114,7 @@ object GmailHistoryHandler { fetchedDraftIdsMap } else emptyMap() - val isNew = - !GeneralUtil.isAppForegrounded() && folderType === FoldersManager.FolderType.INBOX + val isNew = !context.isAppForegrounded() && folderType == FoldersManager.FolderType.INBOX val msgEntities = MessageEntity.genMessageEntities( context = applicationContext, @@ -144,11 +151,38 @@ object GmailHistoryHandler { localFolder, roomDatabase ) + + if (accountEntity.useConversationMode && existingThreads.isNotEmpty()) { + val threadsToBeUpdated = existingThreads.mapNotNull { threadMessageEntity -> + val thread = gmailThreadInfoList.firstOrNull { it.id == threadMessageEntity.threadId } + if (thread != null) { + threadMessageEntity.copy( + threadMessagesCount = thread.messagesCount, + labelIds = thread.labels.joinToString(separator = LABEL_IDS_SEPARATOR), + hasAttachments = thread.hasAttachments, + threadRecipientsAddresses = InternetAddress.toString( + thread.recipients.toTypedArray() + ), + hasPgp = thread.hasPgpThings, + flags = if (thread.hasUnreadMessages) { + threadMessageEntity.flags?.replace(MessageFlag.SEEN.value, "") + } else { + if (threadMessageEntity.flags?.contains(MessageFlag.SEEN.value) == true) { + threadMessageEntity.flags + } else { + threadMessageEntity.flags.plus("${MessageFlag.SEEN.value} ") + } + } + ) + } else null + } + roomDatabase.msgDao().updateSuspend(threadsToBeUpdated) + } } if (accountEntity.useConversationMode) { //this code should be improved. Just for proof of concept - val s = updateCandidatesMap.keys//it's messages ids + val s = updateCandidatesMap.keys//messages ids val threadIds = roomDatabase.msgDao() .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = s) .mapNotNull { it.threadId }.toSet() @@ -194,7 +228,7 @@ object GmailHistoryHandler { .updateFlagsSuspend(accountEntity.email, localFolder.fullName, map) - val m = labelsToBeUpdatedMap.keys//it's messages ids + val m = labelsToBeUpdatedMap.keys//messages ids val threadIds2 = roomDatabase.msgDao() .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = m) .mapNotNull { it.threadId }.toSet() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt index 9b08b9a7dd..6451f1fab1 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/model/GmailThreadInfo.kt @@ -20,4 +20,5 @@ data class GmailThreadInfo( val labels: Set, val hasAttachments: Boolean = false, val hasPgpThings: Boolean = false, + val hasUnreadMessages: Boolean = false, ) \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt index d353918dce..8f92a436ad 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt @@ -60,6 +60,16 @@ abstract class MessageDao : BaseDao { msgsID: Collection? ): List + @Query( + "SELECT * FROM messages WHERE (account = :account AND folder = :folder AND is_visible = 1 " + + "AND thread_id IN (:threadIdList)) GROUP BY thread_id" + ) + abstract suspend fun getThreadsByID( + account: String, + folder: String, + threadIdList: Collection + ): List + @Query("SELECT * FROM messages WHERE _id IN (:msgsID)") abstract suspend fun getMessagesByIDs(msgsID: Collection): List diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt index 055bf2a8d8..3f7e400af9 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt @@ -5,6 +5,7 @@ package com.flowcrypt.email.extensions.com.google.api.services.gmail.model +import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.extensions.kotlin.asInternetAddresses import com.google.api.services.gmail.model.Thread import jakarta.mail.internet.InternetAddress @@ -51,4 +52,10 @@ fun Thread.getUniqueLabelsSet(): Set { return messages?.flatMap { message -> message.labelIds ?: emptyList() }?.toSortedSet() ?: emptySet() +} + +fun Thread.hasUnreadMessages(): Boolean { + return messages?.any { message -> + message.labelIds?.contains(GmailApiHelper.LABEL_UNREAD) != true + } ?: false } \ No newline at end of file From 5deb709bce1a0b8bab4a51b38933abfeea5b38a9 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 4 Sep 2024 11:30:35 +0300 Subject: [PATCH 042/237] wip --- .../email/api/email/MsgsCacheManager.kt | 6 +++++- .../email/api/email/gmail/GmailHistoryHandler.kt | 16 +++++++++++++++- .../google/api/services/gmail/model/ThreadExt.kt | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/MsgsCacheManager.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/MsgsCacheManager.kt index 846709d2c7..d1e9fb0220 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/MsgsCacheManager.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/MsgsCacheManager.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.api.email @@ -57,6 +57,10 @@ object MsgsCacheManager { } } + fun removeMessage(key: String): Boolean { + return diskLruCache.remove(key) + } + fun getMsgAsByteArray(key: String): ByteArray { return diskLruCache[key]?.getByteArray(0) ?: return byteArrayOf() } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt index 0abb007b6a..91bc658bd7 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt @@ -8,6 +8,7 @@ package com.flowcrypt.email.api.email.gmail import android.content.Context import com.flowcrypt.email.R import com.flowcrypt.email.api.email.FoldersManager +import com.flowcrypt.email.api.email.MsgsCacheManager import com.flowcrypt.email.api.email.gmail.GmailApiHelper.Companion.LABEL_DRAFT import com.flowcrypt.email.api.email.gmail.GmailApiHelper.Companion.LABEL_TRASH import com.flowcrypt.email.api.email.gmail.GmailApiHelper.Companion.labelsToImapFlags @@ -156,7 +157,20 @@ object GmailHistoryHandler { val threadsToBeUpdated = existingThreads.mapNotNull { threadMessageEntity -> val thread = gmailThreadInfoList.firstOrNull { it.id == threadMessageEntity.threadId } if (thread != null) { - threadMessageEntity.copy( + val updatedMessageEntity = MessageEntity.genMessageEntities( + context = applicationContext, + account = accountEntity.email, + accountType = accountEntity.accountType, + label = localFolder.fullName, + msgsList = listOf(thread.lastMessage), + isNew = isNew, + onlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false, + draftIdsMap = draftIdsMap + ).first() + + MsgsCacheManager.removeMessage(threadMessageEntity.id.toString()) + updatedMessageEntity.copy( + id = threadMessageEntity.id, threadMessagesCount = thread.messagesCount, labelIds = thread.labels.joinToString(separator = LABEL_IDS_SEPARATOR), hasAttachments = thread.hasAttachments, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt index 3f7e400af9..39b74c5ad4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt @@ -56,6 +56,6 @@ fun Thread.getUniqueLabelsSet(): Set { fun Thread.hasUnreadMessages(): Boolean { return messages?.any { message -> - message.labelIds?.contains(GmailApiHelper.LABEL_UNREAD) != true + message.labelIds?.contains(GmailApiHelper.LABEL_UNREAD) == true } ?: false } \ No newline at end of file From e5f3e22ae2cac52a67a89218816ddcec475fc28f Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 4 Sep 2024 12:18:00 +0300 Subject: [PATCH 043/237] wip --- .../com/flowcrypt/email/FlowCryptApplication.kt | 4 ++-- .../java/com/flowcrypt/email/extensions/Context.kt | 5 +++++ .../email/jetpack/viewmodel/MessagesViewModel.kt | 7 +++++-- .../jetpack/workmanager/sync/BaseIdleWorker.kt | 7 +++---- .../com/flowcrypt/email/service/IdleService.kt | 8 ++++---- .../java/com/flowcrypt/email/util/GeneralUtil.kt | 14 -------------- 6 files changed, 19 insertions(+), 26 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/FlowCryptApplication.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/FlowCryptApplication.kt index 5100504dc0..51dd049a6f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/FlowCryptApplication.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/FlowCryptApplication.kt @@ -275,11 +275,11 @@ class FlowCryptApplication : Application(), Configuration.Provider { } private var isAppForegroundedInternal = false override fun onStart(owner: LifecycleOwner) { - isAppForegroundedInternal = false + isAppForegroundedInternal = true } override fun onStop(owner: LifecycleOwner) { - isAppForegroundedInternal = true + isAppForegroundedInternal = false } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/Context.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/Context.kt index 995be3f39d..79fd0030cc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/Context.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/Context.kt @@ -38,6 +38,11 @@ fun Context?.hasActiveConnection(): Boolean { } ?: false } +/** + * Check is the app foregrounded or visible. + * + * @return true if the app is foregrounded or visible. + */ fun Context?.isAppForegrounded(): Boolean { return (this as? FlowCryptApplication)?.appForegroundedObserver?.isAppForegrounded ?: false } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index d47bb8e5e3..c2227622ef 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -6,6 +6,7 @@ package com.flowcrypt.email.jetpack.viewmodel import android.app.Application +import android.content.Context import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData import androidx.lifecycle.liveData @@ -34,6 +35,7 @@ import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.LabelEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.database.entity.MessageEntity.Companion.LABEL_IDS_SEPARATOR +import com.flowcrypt.email.extensions.isAppForegrounded import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.jetpack.workmanager.sync.SyncDraftsWorker import com.flowcrypt.email.jetpack.workmanager.sync.UploadDraftsWorker @@ -964,6 +966,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application hasPgpAfterAdditionalSearchSet: Set, updatedMsgs: Array ) = withContext(Dispatchers.IO) { + val context: Context = getApplication() val email = accountEntity.email val folderName = localFolder.fullName @@ -974,7 +977,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application roomDatabase.msgDao().deleteByUIDsSuspend(accountEntity.email, folderName, deleteCandidatesUIDs) val folderType = FoldersManager.getFolderType(localFolder) - if (!GeneralUtil.isAppForegrounded() && folderType === FoldersManager.FolderType.INBOX) { + if (!context.isAppForegrounded() && folderType === FoldersManager.FolderType.INBOX) { val notificationManager = MessagesNotificationManager(getApplication()) for (uid in deleteCandidatesUIDs) { notificationManager.cancel(uid.toHex()) @@ -984,7 +987,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application val newCandidates = EmailUtil.genNewCandidates(msgsUIDs, remoteFolder, newMsgs) val isOnlyPgpModeEnabled = accountEntity.showOnlyEncrypted ?: false - val isNew = !GeneralUtil.isAppForegrounded() && folderType === FoldersManager.FolderType.INBOX + val isNew = !context.isAppForegrounded() && folderType === FoldersManager.FolderType.INBOX val msgEntities = MessageEntity.genMessageEntities( context = getApplication(), diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt index 93f7fa52d3..585ecdc98d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/BaseIdleWorker.kt @@ -14,7 +14,6 @@ import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.extensions.isAppForegrounded import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.service.MessagesNotificationManager -import com.flowcrypt.email.util.GeneralUtil import jakarta.mail.Flags import jakarta.mail.Message import kotlinx.coroutines.Dispatchers @@ -76,7 +75,7 @@ abstract class BaseIdleWorker(context: Context, params: WorkerParameters) : } protected fun removeNotificationForSeenMessages(updateCandidates: Map) { - if (!GeneralUtil.isAppForegrounded()) { + if (!applicationContext.isAppForegrounded()) { for (item in updateCandidates) { val uid = item.key if (item.value.contains(Flags.Flag.SEEN)) { @@ -105,7 +104,7 @@ abstract class BaseIdleWorker(context: Context, params: WorkerParameters) : label = localFolder.fullName, folder = remoteFolder, msgs = newMsgs, - isNew = !GeneralUtil.isAppForegrounded(), + isNew = !applicationContext.isAppForegrounded(), isOnlyPgpModeEnabled = false, hasPgpAfterAdditionalSearchSet = hasPgpAfterAdditionalSearchSet ) @@ -128,7 +127,7 @@ abstract class BaseIdleWorker(context: Context, params: WorkerParameters) : accountEntity: AccountEntity, localFolder: LocalFolder ) { - if (!GeneralUtil.isAppForegrounded()) { + if (!applicationContext.isAppForegrounded()) { val detailsList = roomDatabase.msgDao().getNewMsgsSuspend(accountEntity.email, localFolder.fullName) notificationManager.notify(applicationContext, accountEntity, localFolder, detailsList) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/service/IdleService.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/service/IdleService.kt index 510717e1d7..c8c8e4b6ae 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/service/IdleService.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/service/IdleService.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.service @@ -19,21 +19,21 @@ import androidx.lifecycle.switchMap import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity +import com.flowcrypt.email.extensions.isAppForegrounded import com.flowcrypt.email.extensions.kotlin.toHex import com.flowcrypt.email.jetpack.lifecycle.ConnectionLifecycleObserver import com.flowcrypt.email.jetpack.workmanager.sync.InboxIdleMsgsAddedWorker import com.flowcrypt.email.jetpack.workmanager.sync.InboxIdleMsgsRemovedWorker import com.flowcrypt.email.jetpack.workmanager.sync.InboxIdleSyncWorker -import com.flowcrypt.email.util.GeneralUtil import com.flowcrypt.email.util.LogsUtil import com.flowcrypt.email.util.exception.ExceptionUtil -import org.eclipse.angus.mail.imap.IMAPFolder import jakarta.mail.Flags import jakarta.mail.event.MessageChangedEvent import jakarta.mail.event.MessageCountEvent import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPFolder import java.util.concurrent.Executors import java.util.concurrent.Future import java.util.concurrent.ThreadPoolExecutor @@ -186,7 +186,7 @@ class IdleService : LifecycleService() { msg.flags ) - if (!GeneralUtil.isAppForegrounded()) { + if (!applicationContext.isAppForegrounded()) { if (msg.flags.contains(Flags.Flag.SEEN)) { MessagesNotificationManager(applicationContext).cancel(remoteFolder.getUID(msg).toHex()) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt index 285b8cd8f1..94423cc7c3 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/util/GeneralUtil.kt @@ -5,9 +5,6 @@ package com.flowcrypt.email.util -import android.app.ActivityManager -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND -import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE import android.content.ClipData import android.content.ClipboardManager import android.content.ContentResolver @@ -76,17 +73,6 @@ import java.util.concurrent.TimeUnit */ class GeneralUtil { companion object { - /** - * Check is the app foregrounded or visible. - * - * @return true if the app is foregrounded or visible. - */ - fun isAppForegrounded(): Boolean { - val appProcessInfo = ActivityManager.RunningAppProcessInfo() - ActivityManager.getMyMemoryState(appProcessInfo) - return appProcessInfo.importance == IMPORTANCE_FOREGROUND || appProcessInfo.importance == IMPORTANCE_VISIBLE - } - /** * This method checks is it a debug or uiTests build. * From 834b41916aa901ccaa88fa635453ddd977b9a753 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 4 Sep 2024 16:24:14 +0300 Subject: [PATCH 044/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 31 +++++++++++++ .../workmanager/sync/ArchiveMsgsWorker.kt | 43 ++++++++++++------- 2 files changed, 59 insertions(+), 15 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index f9675cc04e..5148ac431b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -59,6 +59,7 @@ import com.google.api.services.gmail.model.ListMessagesResponse import com.google.api.services.gmail.model.ListThreadsResponse import com.google.api.services.gmail.model.Message import com.google.api.services.gmail.model.MessagePart +import com.google.api.services.gmail.model.ModifyThreadRequest import com.google.api.services.gmail.model.Thread import jakarta.mail.Flags import jakarta.mail.MessagingException @@ -548,6 +549,36 @@ class GmailApiHelper { .execute() } + suspend fun changeLabelsForThreads( + context: Context, + accountEntity: AccountEntity, + threadIdList: Collection, + addLabelIds: List? = null, + removeLabelIds: List? = null + ) = withContext(Dispatchers.IO) { + if (addLabelIds == null && removeLabelIds == null) return@withContext + + val gmailApiService = generateGmailApiService(context, accountEntity) + val batch = gmailApiService.batch() + + for (threadId in threadIdList) { + val request = gmailApiService + .users() + .threads() + .modify(DEFAULT_USER_ID, threadId, ModifyThreadRequest().apply { + this.addLabelIds = addLabelIds + this.removeLabelIds = removeLabelIds + }) + + request.queue(batch, object : JsonBatchCallback() { + override fun onSuccess(t: Thread?, responseHeaders: HttpHeaders?) {} + override fun onFailure(e: GoogleJsonError?, responseHeaders: HttpHeaders?) {} + }) + } + + batch.execute() + } + suspend fun deleteMsgsPermanently( context: Context, accountEntity: AccountEntity, ids: List diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt index 8f2599b1cf..e97c049754 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.workmanager.sync @@ -16,15 +16,15 @@ import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.MessageEntity -import org.eclipse.angus.mail.imap.IMAPFolder import jakarta.mail.Folder import jakarta.mail.Message import jakarta.mail.Store import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPFolder /** - * This task moves marked messages to INBOX folder + * This task moves marked messages to Archive folder * * @author Denys Bondarenko */ @@ -39,7 +39,8 @@ class ArchiveMsgsWorker(context: Context, params: WorkerParameters) : } private suspend fun archive(account: AccountEntity, store: Store) = withContext(Dispatchers.IO) { - archiveInternal(account) { folderName, uidList -> + archiveInternal(account) { folderName, entities -> + val uidList = entities.map { it.uid } val foldersManager = FoldersManager.fromDatabaseSuspend(applicationContext, account) val allMailFolder = foldersManager.folderAll ?: return@archiveInternal @@ -57,21 +58,31 @@ class ArchiveMsgsWorker(context: Context, params: WorkerParameters) : } private suspend fun archive(account: AccountEntity) = withContext(Dispatchers.IO) { - archiveInternal(account) { _, uidList -> + archiveInternal(account) { _, entities -> executeGMailAPICall(applicationContext) { - GmailApiHelper.changeLabels( - context = applicationContext, - accountEntity = account, - ids = uidList.map { java.lang.Long.toHexString(it).lowercase() }, - removeLabelIds = listOf(GmailApiHelper.LABEL_INBOX) - ) + if (account.useConversationMode) { + GmailApiHelper.changeLabelsForThreads( + context = applicationContext, + accountEntity = account, + threadIdList = entities.mapNotNull { it.threadId }, + removeLabelIds = listOf(GmailApiHelper.LABEL_INBOX) + ) + } else { + val uidList = entities.map { it.uid } + GmailApiHelper.changeLabels( + context = applicationContext, + accountEntity = account, + ids = uidList.map { java.lang.Long.toHexString(it).lowercase() }, + removeLabelIds = listOf(GmailApiHelper.LABEL_INBOX) + ) + } } } } private suspend fun archiveInternal( account: AccountEntity, - action: suspend (folderName: String, list: List) -> Unit + action: suspend (folderName: String, entities: List) -> Unit ) = withContext(Dispatchers.IO) { val roomDatabase = FlowCryptRoomDatabase.getDatabase(applicationContext) val foldersManager = FoldersManager.fromDatabaseSuspend(applicationContext, account) @@ -83,8 +94,7 @@ class ArchiveMsgsWorker(context: Context, params: WorkerParameters) : if (candidatesForArchiving.isEmpty()) { break } else { - val uidList = candidatesForArchiving.map { it.uid } - action.invoke(inboxFolder.fullName, uidList) + action.invoke(inboxFolder.fullName, candidatesForArchiving) if (account.isGoogleSignInAccount && account.useAPI) { val messageEntitiesToBeDeleted = candidatesForArchiving.filter { it.folder == JavaEmailConstants.FOLDER_INBOX }.toSet() @@ -98,7 +108,10 @@ class ArchiveMsgsWorker(context: Context, params: WorkerParameters) : messageEntity.copy(state = MessageState.NONE.value, labelIds = labelIds) }) } else { - roomDatabase.msgDao().deleteByUIDsSuspend(account.email, inboxFolder.fullName, uidList) + roomDatabase.msgDao().deleteByUIDsSuspend( + account.email, + inboxFolder.fullName, + candidatesForArchiving.map { it.uid }) } } } From 5d01ebfb5d22b784607b0bca9f60e1805795039d Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 4 Sep 2024 16:57:29 +0300 Subject: [PATCH 045/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 13 ++----------- .../api/services/gmail/model/ThreadExt.kt | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 5148ac431b..db8e30cf6a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -21,8 +21,7 @@ import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.AttachmentEntity import com.flowcrypt.email.database.entity.MessageEntity -import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getRecipients -import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getSubject +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.extractSubject import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getUniqueLabelsSet import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getUniqueRecipients import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasUnreadMessages @@ -1231,20 +1230,12 @@ class GmailApiHelper { context: Context ): GmailThreadInfo { val receiverEmail = accountEntity.email - val subject = thread.messages?.getOrNull(0)?.takeIf { message -> - message.getRecipients("From").any { internetAddress -> - internetAddress.address.equals(receiverEmail, true) - } || (thread.messages?.size ?: 0) == 1 - }?.getSubject() - ?: thread.messages?.getOrNull(1)?.getSubject() - ?: context.getString(R.string.no_subject) - val gmailThreadInfo = GmailThreadInfo( id = thread.id, lastMessage = requireNotNull(thread.messages?.last()), messagesCount = thread.messages?.size ?: 0, recipients = thread.getUniqueRecipients(receiverEmail), - subject = subject, + subject = thread.extractSubject(context, receiverEmail), labels = thread.getUniqueLabelsSet(), hasUnreadMessages = thread.hasUnreadMessages() ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt index 39b74c5ad4..b483efb187 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/ThreadExt.kt @@ -5,6 +5,8 @@ package com.flowcrypt.email.extensions.com.google.api.services.gmail.model +import android.content.Context +import com.flowcrypt.email.R import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.extensions.kotlin.asInternetAddresses import com.google.api.services.gmail.model.Thread @@ -58,4 +60,19 @@ fun Thread.hasUnreadMessages(): Boolean { return messages?.any { message -> message.labelIds?.contains(GmailApiHelper.LABEL_UNREAD) == true } ?: false +} + +fun Thread.extractSubject(context: Context, receiverEmail: String): String { + return messages?.getOrNull(0)?.takeIf { message -> + message.getRecipients("From").any { internetAddress -> + internetAddress.address.equals(receiverEmail, true) + } || (messages?.size ?: 0) == 1 + }?.getSubject() + ?: messages.firstOrNull { message -> + message.getRecipients("From").any { internetAddress -> + internetAddress.address.equals(receiverEmail, true) + } + }?.getSubject() + ?: messages?.getOrNull(0)?.getSubject() + ?: context.getString(R.string.no_subject) } \ No newline at end of file From b80135259b5f3afb2242925c6dd24c9ec51e4ef7 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 4 Sep 2024 17:07:24 +0300 Subject: [PATCH 046/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 28 +++++++++++++++ .../workmanager/sync/DeleteMessagesWorker.kt | 36 ++++++++++++------- 2 files changed, 51 insertions(+), 13 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index db8e30cf6a..91ca36c62f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -645,6 +645,34 @@ class GmailApiHelper { batch.execute() } + suspend fun moveThreadToTrash( + context: Context, + accountEntity: AccountEntity, + ids: List + ) = + withContext(Dispatchers.IO) { + val gmailApiService = generateGmailApiService(context, accountEntity) + val batch = gmailApiService.batch() + + for (id in ids) { + val request = gmailApiService + .users() + .threads() + .trash(DEFAULT_USER_ID, id) + request.queue(batch, object : JsonBatchCallback() { + override fun onSuccess(t: Thread?, responseHeaders: HttpHeaders?) { + //need to think about it + } + + override fun onFailure(e: GoogleJsonError?, responseHeaders: HttpHeaders?) { + //need to think about it + } + }) + } + + batch.execute() + } + suspend fun loadTrashMsgs(context: Context, accountEntity: AccountEntity): List = withContext(Dispatchers.IO) { val gmailApiService = generateGmailApiService(context, accountEntity) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt index 3926a34d78..fa5fa83b30 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.workmanager.sync @@ -15,14 +15,15 @@ import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.database.entity.AccountEntity -import org.eclipse.angus.mail.imap.IMAPFolder -import org.eclipse.angus.mail.imap.IMAPStore +import com.flowcrypt.email.database.entity.MessageEntity import jakarta.mail.Folder import jakarta.mail.Message import jakarta.mail.Store import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPFolder +import org.eclipse.angus.mail.imap.IMAPStore /** @@ -42,14 +43,22 @@ class DeleteMessagesWorker(context: Context, params: WorkerParameters) : } private suspend fun moveMsgsToTrash(account: AccountEntity) = withContext(Dispatchers.IO) { - moveMsgsToTrashInternal(account) { _, uidList -> + moveMsgsToTrashInternal(account) { _, entities -> executeGMailAPICall(applicationContext) { //todo-denbond7 need to improve this logic. We should delete local messages only if we'll // success delete remote messages - GmailApiHelper.moveToTrash( - context = applicationContext, - accountEntity = account, - ids = uidList.map { java.lang.Long.toHexString(it).lowercase() }) + if (account.useConversationMode) { + GmailApiHelper.moveThreadToTrash( + context = applicationContext, + accountEntity = account, + ids = entities.mapNotNull { it.threadId }) + } else { + val uidList = entities.map { it.uid } + GmailApiHelper.moveToTrash( + context = applicationContext, + accountEntity = account, + ids = uidList.map { java.lang.Long.toHexString(it).lowercase() }) + } //need to wait while the Gmail server will update labels delay(2000) } @@ -58,12 +67,13 @@ class DeleteMessagesWorker(context: Context, params: WorkerParameters) : private suspend fun moveMsgsToTrash(account: AccountEntity, store: Store) = withContext(Dispatchers.IO) { - moveMsgsToTrashInternal(account) { folderName, uidList -> + moveMsgsToTrashInternal(account) { folderName, entities -> store.getFolder(folderName).use { folder -> val foldersManager = FoldersManager.fromDatabaseSuspend(applicationContext, account) val trash = foldersManager.folderTrash ?: return@use val remoteDestFolder = store.getFolder(trash.fullName) as IMAPFolder val remoteSrcFolder = (folder as IMAPFolder).apply { open(Folder.READ_WRITE) } + val uidList = entities.map { it.uid } val msgs: List = remoteSrcFolder.getMessagesByUID(uidList.toLongArray()).filterNotNull() if (msgs.isNotEmpty()) { @@ -79,7 +89,7 @@ class DeleteMessagesWorker(context: Context, params: WorkerParameters) : private suspend fun moveMsgsToTrashInternal( account: AccountEntity, - action: suspend (folderName: String, list: List) -> Unit + action: suspend (folderName: String, entities: List) -> Unit ) = withContext(Dispatchers.IO) { val roomDatabase = FlowCryptRoomDatabase.getDatabase(applicationContext) @@ -103,9 +113,9 @@ class DeleteMessagesWorker(context: Context, params: WorkerParameters) : ) { continue } - val uidList = filteredMsgs.map { it.uid } - action.invoke(srcFolder, uidList) - roomDatabase.msgDao().deleteByUIDsSuspend(account.email, srcFolder, uidList) + action.invoke(srcFolder, filteredMsgs) + roomDatabase.msgDao() + .deleteByUIDsSuspend(account.email, srcFolder, filteredMsgs.map { it.uid }) } } } From 89d629bcbc1880de7243a92b20ca67112f4935c3 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 4 Sep 2024 17:19:30 +0300 Subject: [PATCH 047/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 28 +++++++++++++++ .../sync/DeleteMessagesPermanentlyWorker.kt | 34 ++++++++++++------- 2 files changed, 50 insertions(+), 12 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 91ca36c62f..caa3af299b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -594,6 +594,34 @@ class GmailApiHelper { } } + suspend fun deleteThreadsPermanently( + context: Context, accountEntity: AccountEntity, + ids: List + ) { + withContext(Dispatchers.IO) { + val gmailApiService = generateGmailApiService(context, accountEntity) + val batch = gmailApiService.batch() + + for (id in ids) { + val request = gmailApiService + .users() + .threads() + .delete(DEFAULT_USER_ID, id) + request.queue(batch, object : JsonBatchCallback() { + override fun onSuccess(t: Void?, responseHeaders: HttpHeaders?) { + //need to think about it + } + + override fun onFailure(e: GoogleJsonError?, responseHeaders: HttpHeaders?) { + //need to think about it + } + }) + } + + batch.execute() + } + } + suspend fun deleteDrafts( context: Context, accountEntity: AccountEntity, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt index f04ecb008f..a59b071533 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.workmanager.sync @@ -14,13 +14,14 @@ import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.database.entity.AccountEntity -import org.eclipse.angus.mail.imap.IMAPFolder +import com.flowcrypt.email.database.entity.MessageEntity import jakarta.mail.Flags import jakarta.mail.Folder import jakarta.mail.Message import jakarta.mail.Store import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPFolder /** * @author Denys Bondarenko @@ -37,9 +38,10 @@ class DeleteMessagesPermanentlyWorker(context: Context, params: WorkerParameters private suspend fun deleteMsgsPermanently(account: AccountEntity, store: Store) = withContext(Dispatchers.IO) { - deleteMsgsPermanentlyInternal(account) { folderName, uidList -> + deleteMsgsPermanentlyInternal(account) { folderName, entities -> store.getFolder(folderName).use { folder -> val imapFolder = (folder as IMAPFolder).apply { open(Folder.READ_WRITE) } + val uidList = entities.map { it.uid } val msgs: List = imapFolder.getMessagesByUID(uidList.toLongArray()).filterNotNull() if (msgs.isNotEmpty()) { @@ -50,19 +52,27 @@ class DeleteMessagesPermanentlyWorker(context: Context, params: WorkerParameters } private suspend fun deleteMsgsPermanently(account: AccountEntity) = withContext(Dispatchers.IO) { - deleteMsgsPermanentlyInternal(account) { _, uidList -> + deleteMsgsPermanentlyInternal(account) { _, entities -> executeGMailAPICall(applicationContext) { - GmailApiHelper.deleteMsgsPermanently( - context = applicationContext, - accountEntity = account, - ids = uidList.map { java.lang.Long.toHexString(it).lowercase() }) + if (account.useConversationMode) { + GmailApiHelper.deleteThreadsPermanently( + context = applicationContext, + accountEntity = account, + ids = entities.mapNotNull { it.threadId }) + } else { + val uidList = entities.map { it.uid } + GmailApiHelper.deleteMsgsPermanently( + context = applicationContext, + accountEntity = account, + ids = uidList.map { java.lang.Long.toHexString(it).lowercase() }) + } } } } private suspend fun deleteMsgsPermanentlyInternal( account: AccountEntity, - action: suspend (folderName: String, list: List) -> Unit + action: suspend (folderName: String, entities: List) -> Unit ) = withContext(Dispatchers.IO) { val foldersManager = FoldersManager.fromDatabaseSuspend(applicationContext, account) @@ -77,9 +87,9 @@ class DeleteMessagesPermanentlyWorker(context: Context, params: WorkerParameters if (candidatesForDeleting.isEmpty()) { break } else { - val uidList = candidatesForDeleting.map { it.uid } - action.invoke(trash.fullName, uidList) - roomDatabase.msgDao().deleteByUIDsSuspend(account.email, trash.fullName, uidList) + action.invoke(trash.fullName, candidatesForDeleting) + roomDatabase.msgDao() + .deleteByUIDsSuspend(account.email, trash.fullName, candidatesForDeleting.map { it.uid }) } } } From f6462f1586ac359d8c71abb71948ef8cbdc7b169 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 4 Sep 2024 17:26:15 +0300 Subject: [PATCH 048/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 2 +- .../workmanager/sync/EmptyTrashWorker.kt | 27 ++++++++++++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index caa3af299b..576f89a967 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -596,7 +596,7 @@ class GmailApiHelper { suspend fun deleteThreadsPermanently( context: Context, accountEntity: AccountEntity, - ids: List + ids: Collection ) { withContext(Dispatchers.IO) { val gmailApiService = generateGmailApiService(context, accountEntity) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/EmptyTrashWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/EmptyTrashWorker.kt index 94facd91e3..489a312aaf 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/EmptyTrashWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/EmptyTrashWorker.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.workmanager.sync @@ -13,13 +13,13 @@ import com.flowcrypt.email.api.email.FoldersManager import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.database.entity.AccountEntity -import org.eclipse.angus.mail.imap.IMAPFolder import jakarta.mail.Flags import jakarta.mail.Folder import jakarta.mail.Store import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPFolder /** * @author Denys Bondarenko @@ -40,12 +40,25 @@ class EmptyTrashWorker(context: Context, params: WorkerParameters) : .changeMsgsStateSuspend(account.email, folderName, MessageState.PENDING_EMPTY_TRASH.value) try { executeGMailAPICall(applicationContext) { - val msgs = GmailApiHelper.loadTrashMsgs(applicationContext, account) - if (msgs.isNotEmpty()) { - GmailApiHelper.deleteMsgsPermanently(applicationContext, account, msgs.map { it.id }) - //need to wait while the Gmail server will update labels - delay(2000) + val messages = GmailApiHelper.loadTrashMsgs(applicationContext, account) + if (messages.isEmpty()) { + return@executeGMailAPICall + } + + if (account.useConversationMode) { + GmailApiHelper.deleteThreadsPermanently( + applicationContext, + account, + messages.mapNotNull { it.threadId }.toSet() + ) + } else { + GmailApiHelper.deleteMsgsPermanently( + applicationContext, + account, + messages.map { it.id }) } + //need to wait while the Gmail server will update labels + delay(2000) } } catch (e: Exception) { roomDatabase.msgDao().changeMsgsStateSuspend(account.email, folderName) From ff15375e1628e237fed5e9840600bbe1e21cac9c Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 4 Sep 2024 17:29:08 +0300 Subject: [PATCH 049/237] wip --- .../workmanager/sync/InboxIdleSyncWorker.kt | 29 +++++++------------ 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt index 5349fde4e3..2d5b8ff539 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt @@ -13,10 +13,8 @@ import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.api.email.FoldersManager import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.gmail.GmailHistoryHandler -import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.util.exception.GmailAPIException -import com.google.api.services.gmail.model.History import jakarta.mail.FetchProfile import jakarta.mail.Folder import jakarta.mail.Store @@ -129,7 +127,16 @@ open class InboxIdleSyncWorker(context: Context, params: WorkerParameters) : historyId = labelEntityHistoryId.max(msgEntityHistoryId) ) - handleMsgsFromHistory(accountEntity, inboxLocalFolder, historyList) + GmailHistoryHandler.handleHistory( + applicationContext, + accountEntity, + inboxLocalFolder, + historyList + ) { deleteCandidatesUIDs, _, updateCandidatesMap, _ -> + tryToRemoveNotifications(deleteCandidatesUIDs) + tryToShowNotificationsForNewMessages(accountEntity, inboxLocalFolder) + removeNotificationForSeenMessages(updateCandidatesMap) + } } catch (e: Exception) { if (e is GmailAPIException && e.code == HttpURLConnection.HTTP_NOT_FOUND) { /* @@ -151,22 +158,6 @@ open class InboxIdleSyncWorker(context: Context, params: WorkerParameters) : } } - private suspend fun handleMsgsFromHistory( - accountEntity: AccountEntity, localFolder: LocalFolder, - historyList: List - ) = withContext(Dispatchers.IO) { - GmailHistoryHandler.handleHistory( - applicationContext, - accountEntity, - localFolder, - historyList - ) { deleteCandidatesUIDs, _, updateCandidatesMap, _ -> - tryToRemoveNotifications(deleteCandidatesUIDs) - tryToShowNotificationsForNewMessages(accountEntity, localFolder) - removeNotificationForSeenMessages(updateCandidatesMap) - } - } - companion object { const val GROUP_UNIQUE_TAG = BuildConfig.APPLICATION_ID + ".IDLE_SYNC_INBOX" From 905ac8fcb4dde4771ec27e5e3f1d1af7014e9178 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 5 Sep 2024 09:50:31 +0300 Subject: [PATCH 050/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 2 +- .../base/BaseMoveMessagesWorker.kt | 37 +++++++++++++------ .../workmanager/sync/ArchiveMsgsWorker.kt | 2 +- .../sync/DeleteMessagesPermanentlyWorker.kt | 3 +- .../workmanager/sync/DeleteMessagesWorker.kt | 3 +- 5 files changed, 31 insertions(+), 16 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 576f89a967..2e21fa862d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -676,7 +676,7 @@ class GmailApiHelper { suspend fun moveThreadToTrash( context: Context, accountEntity: AccountEntity, - ids: List + ids: Collection ) = withContext(Dispatchers.IO) { val gmailApiService = generateGmailApiService(context, accountEntity) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/base/BaseMoveMessagesWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/base/BaseMoveMessagesWorker.kt index a116c2afba..d32c9c1e1e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/base/BaseMoveMessagesWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/base/BaseMoveMessagesWorker.kt @@ -15,13 +15,13 @@ import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.jetpack.workmanager.sync.BaseSyncWorker -import org.eclipse.angus.mail.imap.IMAPFolder import jakarta.mail.Folder import jakarta.mail.Message import jakarta.mail.Store import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPFolder /** * @author Denys Bondarenko @@ -51,9 +51,10 @@ abstract class BaseMoveMessagesWorker(context: Context, params: WorkerParameters withContext(Dispatchers.IO) { val localDestinationFolder = getDestinationFolderForIMAP(account) ?: return@withContext - moveMessagesInternal(account) { folderName, uidList -> + moveMessagesInternal(account) { folderName, entities -> store.getFolder(folderName).use { folder -> val remoteSrcFolder = (folder as IMAPFolder).apply { open(Folder.READ_WRITE) } + val uidList = entities.map { it.uid } val msgs: List = remoteSrcFolder.getMessagesByUID(uidList.toLongArray()).filterNotNull() val remoteDestFolder = store.getFolder(localDestinationFolder.fullName) as IMAPFolder @@ -66,16 +67,28 @@ abstract class BaseMoveMessagesWorker(context: Context, params: WorkerParameters } private suspend fun moveMessages(account: AccountEntity) = withContext(Dispatchers.IO) { - moveMessagesInternal(account) { folderName, uidList -> + moveMessagesInternal(account) { folderName, entities -> executeGMailAPICall(applicationContext) { val gmailApiLabelsData = getAddAndRemoveLabelIdsForGmailAPI(folderName) - GmailApiHelper.changeLabels( - context = applicationContext, - accountEntity = account, - ids = uidList.map { java.lang.Long.toHexString(it).lowercase() }, - addLabelIds = gmailApiLabelsData.addLabelIds, - removeLabelIds = gmailApiLabelsData.removeLabelIds - ) + + if (account.useConversationMode) { + GmailApiHelper.changeLabelsForThreads( + context = applicationContext, + accountEntity = account, + threadIdList = entities.mapNotNull { it.threadId }.toSet(), + addLabelIds = gmailApiLabelsData.addLabelIds, + removeLabelIds = gmailApiLabelsData.removeLabelIds + ) + } else { + val uidList = entities.map { it.uid } + GmailApiHelper.changeLabels( + context = applicationContext, + accountEntity = account, + ids = uidList.map { java.lang.Long.toHexString(it).lowercase() }, + addLabelIds = gmailApiLabelsData.addLabelIds, + removeLabelIds = gmailApiLabelsData.removeLabelIds + ) + } } delay(2000) @@ -84,7 +97,7 @@ abstract class BaseMoveMessagesWorker(context: Context, params: WorkerParameters private suspend fun moveMessagesInternal( account: AccountEntity, - action: suspend (folderName: String, uidList: List) -> Unit + action: suspend (folderName: String, messageEntities: List) -> Unit ) = withContext(Dispatchers.IO) { val roomDatabase = FlowCryptRoomDatabase.getDatabase(applicationContext) @@ -109,8 +122,8 @@ abstract class BaseMoveMessagesWorker(context: Context, params: WorkerParameters continue } + action.invoke(srcFolder, filteredMessages) val uidList = filteredMessages.map { it.uid } - action.invoke(srcFolder, uidList) val movedMessages = messagesToMove.filter { it.uid in uidList } .map { it.copy(state = MessageState.NONE.value) } onMessagesMovedOnServer(account, srcFolder, movedMessages) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt index e97c049754..a4d1a4333b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt @@ -64,7 +64,7 @@ class ArchiveMsgsWorker(context: Context, params: WorkerParameters) : GmailApiHelper.changeLabelsForThreads( context = applicationContext, accountEntity = account, - threadIdList = entities.mapNotNull { it.threadId }, + threadIdList = entities.mapNotNull { it.threadId }.toSet(), removeLabelIds = listOf(GmailApiHelper.LABEL_INBOX) ) } else { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt index a59b071533..02a7da16db 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt @@ -58,7 +58,8 @@ class DeleteMessagesPermanentlyWorker(context: Context, params: WorkerParameters GmailApiHelper.deleteThreadsPermanently( context = applicationContext, accountEntity = account, - ids = entities.mapNotNull { it.threadId }) + ids = entities.mapNotNull { it.threadId }.toSet() + ) } else { val uidList = entities.map { it.uid } GmailApiHelper.deleteMsgsPermanently( diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt index fa5fa83b30..580434afcb 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt @@ -51,7 +51,8 @@ class DeleteMessagesWorker(context: Context, params: WorkerParameters) : GmailApiHelper.moveThreadToTrash( context = applicationContext, accountEntity = account, - ids = entities.mapNotNull { it.threadId }) + ids = entities.mapNotNull { it.threadId }.toSet() + ) } else { val uidList = entities.map { it.uid } GmailApiHelper.moveToTrash( From f02943bc35182e878ef9966e1a2deec1fbc25e26 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 5 Sep 2024 10:13:19 +0300 Subject: [PATCH 051/237] wip --- .../jetpack/viewmodel/GmailLabelsViewModel.kt | 24 +++++++++++++------ .../workmanager/sync/UploadDraftsWorker.kt | 3 +++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/GmailLabelsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/GmailLabelsViewModel.kt index 121e03effc..d4cd0db157 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/GmailLabelsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/GmailLabelsViewModel.kt @@ -118,13 +118,23 @@ class GmailLabelsViewModel( it.initialState != MaterialCheckBox.STATE_UNCHECKED && it.state == MaterialCheckBox.STATE_UNCHECKED } - GmailApiHelper.changeLabels( - context = getApplication(), - accountEntity = activeAccount, - ids = messageEntities.map { it.uidAsHEX }, - addLabelIds = labelsToBeAdded.map { it.id }, - removeLabelIds = labelsToBeRemoved.map { it.id } - ) + if (activeAccount.useConversationMode) { + GmailApiHelper.changeLabelsForThreads( + context = getApplication(), + accountEntity = activeAccount, + threadIdList = messageEntities.mapNotNull { it.threadId }.toSet(), + addLabelIds = labelsToBeAdded.map { it.id }, + removeLabelIds = labelsToBeRemoved.map { it.id } + ) + } else { + GmailApiHelper.changeLabels( + context = getApplication(), + accountEntity = activeAccount, + ids = messageEntities.map { it.uidAsHEX }, + addLabelIds = labelsToBeAdded.map { it.id }, + removeLabelIds = labelsToBeRemoved.map { it.id } + ) + } //update labels in the local cache val toBeDeletedMessageEntities = mutableListOf() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt index 7267313e01..bac89821f4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt @@ -30,6 +30,9 @@ import java.io.File import java.io.FileFilter import java.util.Properties +/** + * @author Denys Bondarenko + */ class UploadDraftsWorker(context: Context, params: WorkerParameters) : BaseSyncWorker(context, params) { override suspend fun runIMAPAction(accountEntity: AccountEntity, store: Store) { From 939bd20849ee96066b311c0d525c0c48f810d1b4 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 5 Sep 2024 12:45:58 +0300 Subject: [PATCH 052/237] wip --- .../sync/UpdateMsgsSeenStateWorker.kt | 56 +++++++++++++------ 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UpdateMsgsSeenStateWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UpdateMsgsSeenStateWorker.kt index e19462cff4..d3f78ba217 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UpdateMsgsSeenStateWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UpdateMsgsSeenStateWorker.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.workmanager.sync @@ -13,13 +13,14 @@ import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.database.entity.AccountEntity -import org.eclipse.angus.mail.imap.IMAPFolder +import com.flowcrypt.email.database.entity.MessageEntity import jakarta.mail.Flags import jakarta.mail.Folder import jakarta.mail.Message import jakarta.mail.Store import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext +import org.eclipse.angus.mail.imap.IMAPFolder /** * This task mark candidates as read/unread. @@ -43,9 +44,10 @@ class UpdateMsgsSeenStateWorker(context: Context, params: WorkerParameters) : store: Store, state: MessageState ) = withContext(Dispatchers.IO) { - changeMsgsReadStateInternal(account, state) { fullFolderName, uidList -> + changeMsgsReadStateInternal(account, state) { fullFolderName, entities -> store.getFolder(fullFolderName).use { folder -> val imapFolder = (folder as IMAPFolder).apply { open(Folder.READ_WRITE) } + val uidList = entities.map { it.uid } val msgs: List = imapFolder.getMessagesByUID(uidList.toLongArray()).filterNotNull() if (msgs.isNotEmpty()) { imapFolder.setFlags( @@ -60,22 +62,40 @@ class UpdateMsgsSeenStateWorker(context: Context, params: WorkerParameters) : private suspend fun changeMsgsReadState(account: AccountEntity, state: MessageState) = withContext(Dispatchers.IO) { - changeMsgsReadStateInternal(account, state) { _, uidList -> + changeMsgsReadStateInternal(account, state) { _, entities -> executeGMailAPICall(applicationContext) { if (state == MessageState.PENDING_MARK_READ) { - GmailApiHelper.changeLabels( - context = applicationContext, - accountEntity = account, - ids = uidList.map { java.lang.Long.toHexString(it).lowercase() }, - removeLabelIds = listOf(GmailApiHelper.LABEL_UNREAD) - ) + if (account.useConversationMode) { + GmailApiHelper.changeLabelsForThreads( + context = applicationContext, + accountEntity = account, + threadIdList = entities.mapNotNull { it.threadId }.toSet(), + removeLabelIds = listOf(GmailApiHelper.LABEL_UNREAD) + ) + } else { + GmailApiHelper.changeLabels( + context = applicationContext, + accountEntity = account, + ids = entities.map { java.lang.Long.toHexString(it.uid).lowercase() }, + removeLabelIds = listOf(GmailApiHelper.LABEL_UNREAD) + ) + } } else { - GmailApiHelper.changeLabels( - context = applicationContext, - accountEntity = account, - ids = uidList.map { java.lang.Long.toHexString(it).lowercase() }, - addLabelIds = listOf(GmailApiHelper.LABEL_UNREAD) - ) + if (account.useConversationMode) { + GmailApiHelper.changeLabelsForThreads( + context = applicationContext, + accountEntity = account, + threadIdList = entities.mapNotNull { it.threadId }.toSet(), + addLabelIds = listOf(GmailApiHelper.LABEL_UNREAD) + ) + } else { + GmailApiHelper.changeLabels( + context = applicationContext, + accountEntity = account, + ids = entities.map { java.lang.Long.toHexString(it.uid).lowercase() }, + addLabelIds = listOf(GmailApiHelper.LABEL_UNREAD) + ) + } } } } @@ -84,7 +104,7 @@ class UpdateMsgsSeenStateWorker(context: Context, params: WorkerParameters) : private suspend fun changeMsgsReadStateInternal( account: AccountEntity, state: MessageState, - action: suspend (folderName: String, list: List) -> Unit + action: suspend (folderName: String, entities: List) -> Unit ) = withContext(Dispatchers.IO) { val roomDatabase = FlowCryptRoomDatabase.getDatabase(applicationContext) val candidatesForMark = @@ -100,8 +120,8 @@ class UpdateMsgsSeenStateWorker(context: Context, params: WorkerParameters) : continue } + action.invoke(folderName, filteredMsgs) val uidList = filteredMsgs.map { it.uid } - action.invoke(folderName, uidList) val entities = roomDatabase.msgDao().getMsgsByUIDs(account.email, folderName, uidList) .filter { it.msgState == state } .map { it.copy(state = MessageState.NONE.value) } From 3f2defb268287c0e2af7172fb307d379603f500b Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 10 Sep 2024 11:15:33 +0300 Subject: [PATCH 053/237] wip --- .../email/database/entity/MessageEntity.kt | 11 +++++++++- .../api/services/gmail/model/MessageExt.kt | 18 +++++++++++++++++ .../viewmodel/ThreadDetailsViewModel.kt | 20 ++++++++++++++++++- .../ui/adapter/MessagesInThreadListAdapter.kt | 20 ++++++++++++++++--- .../email/ui/adapter/MsgsPagedListAdapter.kt | 2 +- FlowCrypt/src/main/res/values-ru/strings.xml | 1 + FlowCrypt/src/main/res/values-uk/strings.xml | 1 + FlowCrypt/src/main/res/values/strings.xml | 1 + 8 files changed, 68 insertions(+), 6 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index 321cae2a3e..a5e32fe245 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -260,7 +260,7 @@ data class MessageEntity( return result } - fun generateSenderAddress( + fun generateSenderAddresses( context: Context, folderType: FoldersManager.FolderType? ): CharSequence { @@ -346,6 +346,15 @@ data class MessageEntity( }) } + fun generateFromText(context: Context): String { + val fromAddress = from.firstOrNull() ?: return "" + return if (fromAddress.address.equals(account, true)) { + context.getString(R.string.me) + } else { + EmailUtil.getFirstAddressString(from) + } + } + private fun generateAddresses( context: Context, accountName: String, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt index 37602f0bf8..384de3a7d4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/com/google/api/services/gmail/model/MessageExt.kt @@ -6,6 +6,8 @@ package com.flowcrypt.email.extensions.com.google.api.services.gmail.model import com.flowcrypt.email.api.email.EmailUtil +import com.flowcrypt.email.api.email.JavaEmailConstants +import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.extensions.kotlin.asContentTypeOrNull import com.flowcrypt.email.extensions.kotlin.asInternetAddresses import com.google.api.services.gmail.model.Message @@ -54,4 +56,20 @@ fun Message.getSubject(): String? { return payload?.headers?.firstOrNull { header -> header.name == "Subject" }?.value +} + +fun Message.getInReplyTo(): String? { + return payload?.headers?.firstOrNull { header -> + header.name == JavaEmailConstants.HEADER_IN_REPLY_TO + }?.value +} + +fun Message.getMessageId(): String? { + return payload?.headers?.firstOrNull { header -> + header.name == JavaEmailConstants.HEADER_MESSAGE_ID + }?.value +} + +fun Message.isDraft(): Boolean { + return labelIds?.contains(GmailApiHelper.LABEL_DRAFT) ?: false } \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index ad8dc75781..f48d3c6d1e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -10,6 +10,9 @@ import androidx.lifecycle.asFlow import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getInReplyTo +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getMessageId +import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.isDraft import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly import com.flowcrypt.email.ui.adapter.GmailApiLabelsListAdapter import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -42,7 +45,22 @@ class ThreadDetailsViewModel( application, activeAccount, initialMessageEntity.threadId - ) + ).toMutableList().apply { + //put drafts in the right position + val drafts = filter { it.isDraft() } + drafts.forEach { draft -> + val inReplyToValue = draft.getInReplyTo() + val inReplyToMessage = firstOrNull { it.getMessageId() == inReplyToValue } + + if (inReplyToMessage != null) { + val inReplyToMessagePosition = indexOf(inReplyToMessage) + if (inReplyToMessagePosition != -1) { + remove(draft) + add(inReplyToMessagePosition + 1, draft) + } + } + } + } val isOnlyPgpModeEnabled = activeAccount.showOnlyEncrypted ?: false val messageEntities = MessageEntity.genMessageEntities( diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt index fd01f70ccc..3d7c338032 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -7,6 +7,10 @@ package com.flowcrypt.email.ui.adapter import android.graphics.Color import android.graphics.Typeface +import android.text.Spannable +import android.text.SpannableString +import android.text.SpannableStringBuilder +import android.text.style.ForegroundColorSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -14,7 +18,6 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.flowcrypt.email.R -import com.flowcrypt.email.api.email.EmailUtil import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.databinding.ItemMessageInThreadBinding import com.flowcrypt.email.extensions.android.widget.useGlideToApplyImageFromSource @@ -50,7 +53,7 @@ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageC fun bindTo(item: MessageEntity, onMessageClickListener: OnMessageClickListener) { val context = itemView.context itemView.setOnClickListener { onMessageClickListener.onMessageClick(item) } - val senderAddress = EmailUtil.getFirstAddressString(item.from) + val senderAddress = item.generateFromText(context) binding.imageViewAvatar.useGlideToApplyImageFromSource( source = AvatarModelLoader.SCHEMA_AVATAR + senderAddress ) @@ -77,8 +80,19 @@ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageC MaterialColors.getColor(context, R.attr.itemTitleColor, Color.BLACK) ) } + + if (item.isDraft) { + val spannableStringBuilder = SpannableStringBuilder(text) + spannableStringBuilder.append(" ") + val timeSpannable = SpannableString("(${context.getString(R.string.draft)})") + timeSpannable.setSpan( + ForegroundColorSpan(Color.RED), 0, timeSpannable.length, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE + ) + spannableStringBuilder.append(timeSpannable) + text = spannableStringBuilder + } } - binding.textViewSender.text = senderAddress binding.textViewDate.apply { text = DateTimeUtil.formatSameDayTime(context, item.receivedDate ?: 0) if (item.isSeen) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt index 2e55b9f3b2..6aaf0dc829 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MsgsPagedListAdapter.kt @@ -232,7 +232,7 @@ class MsgsPagedListAdapter(private val onMessageClickListener: OnMessageClickLis val senderAddress = when (folderType) { FoldersManager.FolderType.OUTBOX -> generateOutboxStatus(context, messageEntity.msgState) - else -> messageEntity.generateSenderAddress(context, folderType) + else -> messageEntity.generateSenderAddresses(context, folderType) } binding.textViewSenderAddress.text = senderAddress diff --git a/FlowCrypt/src/main/res/values-ru/strings.xml b/FlowCrypt/src/main/res/values-ru/strings.xml index fab1e349c0..e53f6c80e7 100644 --- a/FlowCrypt/src/main/res/values-ru/strings.xml +++ b/FlowCrypt/src/main/res/values-ru/strings.xml @@ -637,6 +637,7 @@ Сделайте резервную копию в почтовом ящике Сохранить предоставленные закрытые ключи PGP, защищенные парольной фразой, в качестве резервной копии в папке «Входящие». Это поможет Вам получить доступ к Вашим зашифрованным сообщениям с других устройств (при условии предоставление Вашей парольной фразы). Вы можете безопасно оставить его в своем почтовом ящике или заархивировать.\n\nЕсли Вам не нужна такая резервная копия, отключите эту опцию. Предварительный просмотр недоступен для сообщений с PGP. + Черновик Показать всю переписку из %1$d сообщения Показать всю переписку из %1$d сообщений diff --git a/FlowCrypt/src/main/res/values-uk/strings.xml b/FlowCrypt/src/main/res/values-uk/strings.xml index a1bbbe3a04..d9b3b72f87 100644 --- a/FlowCrypt/src/main/res/values-uk/strings.xml +++ b/FlowCrypt/src/main/res/values-uk/strings.xml @@ -638,6 +638,7 @@ Зробіть резервну копію в електронній скриньці Зберегти надані секретні ключі PGP, захищені парольною фразою, як резервну копію в папці «Вхідні». Це допоможе отримати Вам доступ до Ваших зашифрованих повідомлень з інших пристроїв (за умови надання Вашої парольної фрази). Ви можете спокійно залишити його у папці "Вхідні" або заархівувати.\n\nЯкщо Вам не потрібна така резервна копія, вимкніть цю опцію. Попередній перегляд недоступний для повідомлень з PGP + Чорновик Показати повне листування із %1$d повідомлення Показати повне листування із %1$d повідомленнь diff --git a/FlowCrypt/src/main/res/values/strings.xml b/FlowCrypt/src/main/res/values/strings.xml index 73a204823b..f25aab73b7 100644 --- a/FlowCrypt/src/main/res/values/strings.xml +++ b/FlowCrypt/src/main/res/values/strings.xml @@ -649,6 +649,7 @@ Make a backup in the email box Save the given passphrase-protected PGP private keys as a backup in the inbox. It will help you access your encrypted messages from other devices (along with your pass phrase). You can safely leave it in your inbox or archive it.\n\nIf you don\'t need to have such a backup, please disable this option. The preview is not available for messages with PGP + Draft Show a full conversation of %1$d messages From 79ef2f092c2f7e7d4c78ca931a62d7f08bdad0a1 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Wed, 11 Sep 2024 16:57:51 +0300 Subject: [PATCH 054/237] wip --- .../email/database/dao/MessageDao.kt | 32 +++++--------- .../viewmodel/MessagesViewPagerViewModel.kt | 29 ++++++------- .../viewmodel/ThreadDetailsViewModel.kt | 43 +++++++++++++++---- .../activity/fragment/GmailThreadFragment.kt | 6 ++- .../ViewPagerMessageDetailsFragment.kt | 4 +- .../res/layout/fragment_message_details.xml | 3 ++ .../src/main/res/navigation/nav_graph.xml | 9 ++-- 7 files changed, 73 insertions(+), 53 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt index 8f92a436ad..ce963147de 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt @@ -141,28 +141,6 @@ abstract class MessageDao : BaseDao { limit: Int ): List - @Query( - "SELECT * FROM (" + - "SELECT * FROM messages " + - "WHERE account = :account AND folder = :folder AND thread_id = :threadId AND received_date > :date " + - "ORDER BY received_date DESC " + - "LIMIT :limit) " + - "UNION " + - "SELECT * FROM (" + - "SELECT * FROM messages " + - "WHERE account = :account AND folder = :folder AND thread_id = :threadId AND received_date <= :date " + - "ORDER BY received_date ASC " + - "LIMIT :limit) " + - "ORDER BY received_date ASC" - ) - abstract suspend fun getMessagesInThreadForViewPager( - account: String, - folder: String, - threadId: String, - date: Long, - limit: Int - ): List - @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder AND uid = :uid") abstract fun getMsgLiveData(account: String, folder: String, uid: Long): LiveData @@ -436,6 +414,16 @@ abstract class MessageDao : BaseDao { @Query("SELECT COUNT(*) FROM messages WHERE account = :account AND folder = :label") abstract suspend fun getMsgsCount(account: String, label: String): Int + @Query("SELECT * FROM messages WHERE account = :account AND folder = :folder AND thread_id = :threadId") + abstract suspend fun getMessagesForGmailThread( + account: String, + folder: String, + threadId: String + ): List + + @Query("DELETE FROM messages WHERE account = :account AND folder = :folder AND thread_id = :threadId AND is_visible = 0") + abstract suspend fun clearCacheForGmailThread(account: String?, folder: String, threadId: String) + @Transaction open fun deleteByUIDs(account: String?, label: String?, msgsUID: Collection) { doOperationViaSteps(list = ArrayList(msgsUID)) { stepUIDs: Collection -> diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewPagerViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewPagerViewModel.kt index 848e044619..434141e235 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewPagerViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewPagerViewModel.kt @@ -20,10 +20,13 @@ import com.flowcrypt.email.database.entity.MessageEntity */ class MessagesViewPagerViewModel( private val initialMessageEntityId: Long, + private val sortedEntityIdListForThread: List? = null, private val localFolder: LocalFolder, - private val isThreadMode: Boolean, application: Application ) : AccountViewModel(application) { + + private val isThreadMode: Boolean = sortedEntityIdListForThread?.isNotEmpty() == true + val initialLiveData: LiveData>> = activeAccountLiveData.switchMap { accountEntity -> liveData { @@ -59,20 +62,16 @@ class MessagesViewPagerViewModel( val activeAccount = getActiveAccountSuspend() if (activeAccount != null) { val result = if (isThreadMode) { - Result.success( - roomDatabase.msgDao() - .getMessagesInThreadForViewPager( - account = activeAccount.email, - folder = if (localFolder.searchQuery.isNullOrEmpty()) { - localFolder.fullName - } else { - JavaEmailConstants.FOLDER_SEARCH - }, - threadId = messageEntity.threadId ?: "", - date = messageEntity.receivedDate ?: 0, - limit = PAGE_SIZE / 2 - ) - ) + val cachedMessages = + roomDatabase.msgDao().getMessagesByIDs(sortedEntityIdListForThread ?: emptyList()) + val sortedCachedMessages = + arrayOfNulls(cachedMessages.size).toMutableList().apply { + cachedMessages.forEach { messageEntity -> + add(sortedEntityIdListForThread?.indexOf(messageEntity.id) ?: 0, messageEntity) + } + }.filterNotNull() + + Result.success(sortedCachedMessages) } else { Result.success( roomDatabase.msgDao() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index f48d3c6d1e..09d785a939 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -62,12 +62,15 @@ class ThreadDetailsViewModel( } } + roomDatabase.msgDao() + .updateSuspend(initialMessageEntity.copy(threadMessagesCount = messagesInThread.size)) + val isOnlyPgpModeEnabled = activeAccount.showOnlyEncrypted ?: false val messageEntities = MessageEntity.genMessageEntities( context = getApplication(), account = activeAccount.email, accountType = activeAccount.accountType, - label = "INBOX", + label = GmailApiHelper.LABEL_INBOX, //fix me msgsList = messagesInThread, isNew = false, onlyPgpModeEnabled = isOnlyPgpModeEnabled, @@ -76,16 +79,38 @@ class ThreadDetailsViewModel( messageEntity.copy(snippet = message.snippet, isVisible = false) } - roomDatabase.msgDao().insertWithReplaceSuspend(messageEntities) - GmailApiHelper.identifyAttachments( - msgEntities = messageEntities, - msgs = messagesInThread, - account = activeAccount, - localFolder = LocalFolder(activeAccount.email, GmailApiHelper.LABEL_INBOX),//fix me - roomDatabase = roomDatabase + roomDatabase.msgDao().clearCacheForGmailThread( + account = activeAccount.email, + folder = GmailApiHelper.LABEL_INBOX, //fix me + threadId = initialMessageEntity.threadId + ) + + messageEntities.filter { + it.uid != initialMessageEntity.uid + }.let { + roomDatabase.msgDao().insertWithReplaceSuspend(it) + GmailApiHelper.identifyAttachments( + msgEntities = it, + msgs = messagesInThread, + account = activeAccount, + localFolder = LocalFolder(activeAccount.email, GmailApiHelper.LABEL_INBOX),//fix me + roomDatabase = roomDatabase + ) + } + + val cachedEntities = roomDatabase.msgDao().getMessagesForGmailThread( + activeAccount.email, + GmailApiHelper.LABEL_INBOX,//fix me + initialMessageEntity.threadId, ) - emit(messageEntities) + val finalList = messageEntities.map { fromServerMessageEntity -> + fromServerMessageEntity.copy(id = cachedEntities.firstOrNull { + it.uid == fromServerMessageEntity.uid + }?.id) + } + + emit(finalList) } catch (e: Exception) { e.printStackTraceIfDebugOnly() emit(listOf(initialMessageEntity)) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt index 7934bf51bb..c9edb0a83c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt @@ -83,7 +83,7 @@ class GmailThreadFragment : BaseFragment(), navController?.navigate( GmailThreadFragmentDirections.actionGmailThreadFragmentToViewPagerMessageDetailsFragment( messageEntityId = it, - isThreadMode = true, + sortedEntityIdListForThread = getSortedEntityIdListForThread(), localFolder = LocalFolder( messageEntity.account, GmailApiHelper.LABEL_INBOX @@ -95,6 +95,10 @@ class GmailThreadFragment : BaseFragment(), } }) + private fun getSortedEntityIdListForThread(): LongArray { + return messagesInThreadListAdapter.currentList.mapNotNull { it.id }.toLongArray() + } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) initViews() diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt index 4421ddcb89..36f6987470 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt @@ -46,7 +46,7 @@ class ViewPagerMessageDetailsFragment : BaseFragment } addItemDecoration(DividerItemDecoration(view.context, ORIENTATION_HORIZONTAL)) diff --git a/FlowCrypt/src/main/res/layout/fragment_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_message_details.xml index 3531df416f..68e3852a8e 100644 --- a/FlowCrypt/src/main/res/layout/fragment_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_message_details.xml @@ -297,6 +297,9 @@ diff --git a/FlowCrypt/src/main/res/navigation/nav_graph.xml b/FlowCrypt/src/main/res/navigation/nav_graph.xml index c75d29227e..2cacf8941f 100644 --- a/FlowCrypt/src/main/res/navigation/nav_graph.xml +++ b/FlowCrypt/src/main/res/navigation/nav_graph.xml @@ -305,13 +305,14 @@ - +
Date: Thu, 12 Sep 2024 11:00:42 +0300 Subject: [PATCH 055/237] wip --- .../email/api/email/gmail/GmailApiHelper.kt | 9 ++++----- .../jetpack/viewmodel/MessagesViewModel.kt | 3 ++- .../jetpack/viewmodel/MsgDetailsViewModel.kt | 2 +- .../viewmodel/ThreadDetailsViewModel.kt | 20 ++++++++----------- 4 files changed, 15 insertions(+), 19 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt index 2e21fa862d..ca166fe7ac 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailApiHelper.kt @@ -237,15 +237,14 @@ class GmailApiHelper { suspend fun getWholeMimeMessageInputStream( context: Context, account: AccountEntity?, - messageEntity: MessageEntity + messageId: String ): InputStream = withContext(Dispatchers.IO) { - val msgId = messageEntity.uidAsHEX val gmailApiService = generateGmailApiService(context, account) val message = gmailApiService .users() .messages() - .get(DEFAULT_USER_ID, msgId) + .get(DEFAULT_USER_ID, messageId) .setFormat(MESSAGE_RESPONSE_FORMAT_RAW) message.fields = "raw" @@ -415,7 +414,7 @@ class GmailApiHelper { threadId: String, format: String = RESPONSE_FORMAT_FULL, metadataHeaders: List? = null, - fields: List? = FULL_INFO_WITHOUT_DATA + fields: List? = null ): GmailThreadInfo = withContext(Dispatchers.IO) { val gmailApiService = generateGmailApiService(context, accountEntity) @@ -1288,7 +1287,7 @@ class GmailApiHelper { val receiverEmail = accountEntity.email val gmailThreadInfo = GmailThreadInfo( id = thread.id, - lastMessage = requireNotNull(thread.messages?.last()), + lastMessage = requireNotNull(thread.messages?.last { !it.labelIds.contains(LABEL_DRAFT) }), messagesCount = thread.messages?.size ?: 0, recipients = thread.getUniqueRecipients(receiverEmail), subject = thread.extractSubject(context, receiverEmail), diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index c2227622ef..e181747b13 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -550,7 +550,8 @@ class MessagesViewModel(application: Application) : AccountViewModel(application threadRecipientsAddresses = InternetAddress.toString( thread.recipients.toTypedArray() ), - hasPgp = thread.hasPgpThings + hasPgp = thread.hasPgpThings, + uid = System.nanoTime() ) } else { messageEntity diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index c44fda2022..8e2d41cb4f 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -674,7 +674,7 @@ class MsgDetailsViewModel( GmailApiHelper.getWholeMimeMessageInputStream( context = getApplication(), account = accountEntity, - messageEntity = messageEntity + messageId = msgFullInfo.id ) ) MsgsCacheManager.storeMsg( diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index 09d785a939..9723702643 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -85,18 +85,14 @@ class ThreadDetailsViewModel( threadId = initialMessageEntity.threadId ) - messageEntities.filter { - it.uid != initialMessageEntity.uid - }.let { - roomDatabase.msgDao().insertWithReplaceSuspend(it) - GmailApiHelper.identifyAttachments( - msgEntities = it, - msgs = messagesInThread, - account = activeAccount, - localFolder = LocalFolder(activeAccount.email, GmailApiHelper.LABEL_INBOX),//fix me - roomDatabase = roomDatabase - ) - } + roomDatabase.msgDao().insertWithReplaceSuspend(messageEntities) + GmailApiHelper.identifyAttachments( + msgEntities = messageEntities, + msgs = messagesInThread, + account = activeAccount, + localFolder = LocalFolder(activeAccount.email, GmailApiHelper.LABEL_INBOX),//fix me + roomDatabase = roomDatabase + ) val cachedEntities = roomDatabase.msgDao().getMessagesForGmailThread( activeAccount.email, From eef341c635b1f9fc87362298333111b30248f90f Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 12 Sep 2024 11:11:16 +0300 Subject: [PATCH 056/237] wip --- .../activity/fragment/GmailThreadFragment.kt | 8 +++++ .../fragment/MessageDetailsFragment.kt | 23 -------------- .../activity/fragment/MessagesListFragment.kt | 30 ++++++++++++++----- .../res/layout/fragment_message_details.xml | 21 +------------ .../src/main/res/navigation/nav_graph.xml | 3 ++ 5 files changed, 34 insertions(+), 51 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt index c9edb0a83c..ee7a10f0a0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt @@ -22,6 +22,7 @@ import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.databinding.FragmentNewMessageDetailsBinding import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithViewLifecycle import com.flowcrypt.email.extensions.androidx.fragment.app.navController +import com.flowcrypt.email.extensions.androidx.fragment.app.supportActionBar import com.flowcrypt.email.extensions.androidx.fragment.app.toast import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.ThreadDetailsViewModel @@ -101,10 +102,17 @@ class GmailThreadFragment : BaseFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + updateActionBar() + initViews() setupThreadDetailsViewModel() } + private fun updateActionBar() { + supportActionBar?.title = null + supportActionBar?.subtitle = null + } + private fun setupThreadDetailsViewModel() { launchAndRepeatWithViewLifecycle { threadDetailsViewModel.messageFlow.collect { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt index e645467072..6cf0d7d7f5 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessageDetailsFragment.kt @@ -888,29 +888,6 @@ class MessageDetailsFragment : BaseFragment(), Pr private fun updateViews(messageEntity: MessageEntity) { updateActionBar(messageEntity) - messageEntity.threadMessagesCount?.let { threadMessagesCount -> - if (threadMessagesCount > 1 && !args.isThreadMode) { - binding?.displayFullConversation?.apply { - visible() - text = resources.getQuantityString( - R.plurals.show_full_conversation, threadMessagesCount, threadMessagesCount - ) - messageEntity.id?.let { - setOnClickListener { - navController?.navigate( - object : NavDirections { - override val actionId = R.id.gmailThreadFragment - override val arguments = GmailThreadFragmentArgs( - messageEntityId = messageEntity.id - ).toBundle() - } - ) - } - } - } - } - } - binding?.imageButtonReplyAll?.visibleOrInvisible( !messageEntity.isOutboxMsg && !messageEntity.isDraft ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt index 1591dda555..2737f6eae7 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt @@ -421,14 +421,28 @@ class MessagesListFragment : BaseFragment(), ListPr ) } else { currentFolder?.let { localFolder -> - navController?.navigateSafe( - currentDestinationId = R.id.messagesListFragment, - directions = MessagesListFragmentDirections - .actionMessagesListFragmentToViewPagerMessageDetailsFragment( - messageEntityId = msgEntity.id ?: -1, - localFolder = localFolder - ) - ) + if (account?.isGoogleSignInAccount == true + && account?.useAPI == true + && account?.useConversationMode == true + ) { + navController?.navigateSafe( + currentDestinationId = R.id.messagesListFragment, + directions = MessagesListFragmentDirections + .actionMessagesListFragmentToGmailThreadFragment( + messageEntityId = msgEntity.id ?: -1, + //localFolder = localFolder + ) + ) + } else { + navController?.navigateSafe( + currentDestinationId = R.id.messagesListFragment, + directions = MessagesListFragmentDirections + .actionMessagesListFragmentToViewPagerMessageDetailsFragment( + messageEntityId = msgEntity.id ?: -1, + localFolder = localFolder + ) + ) + } } } } diff --git a/FlowCrypt/src/main/res/layout/fragment_message_details.xml b/FlowCrypt/src/main/res/layout/fragment_message_details.xml index 68e3852a8e..b85e56046d 100644 --- a/FlowCrypt/src/main/res/layout/fragment_message_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_message_details.xml @@ -122,25 +122,6 @@ tools:listitem="@layout/item_label_badge" tools:orientation="horizontal" /> - - + app:layout_constraintTop_toBottomOf="@+id/recyclerViewLabels" /> + Date: Thu, 12 Sep 2024 13:11:15 +0300 Subject: [PATCH 057/237] wip --- .../api/email/gmail/GmailHistoryHandler.kt | 13 ++++---- .../email/database/entity/MessageEntity.kt | 33 +++++-------------- .../jetpack/viewmodel/MessagesViewModel.kt | 2 +- 3 files changed, 16 insertions(+), 32 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt index 91bc658bd7..b70a3b92cc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt @@ -63,7 +63,7 @@ object GmailHistoryHandler { .deleteByUIDsSuspend(accountEntity.email, localFolder.fullName, deleteCandidatesUIDs) val folderType = FoldersManager.getFolderType(localFolder) - if (folderType === FoldersManager.FolderType.INBOX) { + if (folderType == FoldersManager.FolderType.INBOX) { val notificationManager = MessagesNotificationManager(applicationContext) for (uid in deleteCandidatesUIDs) { notificationManager.cancel(uid.toHex()) @@ -134,7 +134,7 @@ object GmailHistoryHandler { threadMessagesCount = thread?.messagesCount, labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), hasAttachments = thread?.hasAttachments, - threadRecipientsAddresses = InternetAddress.toString( + fromAddresses = InternetAddress.toString( thread?.recipients?.toTypedArray() ), hasPgp = thread?.hasPgpThings @@ -174,7 +174,7 @@ object GmailHistoryHandler { threadMessagesCount = thread.messagesCount, labelIds = thread.labels.joinToString(separator = LABEL_IDS_SEPARATOR), hasAttachments = thread.hasAttachments, - threadRecipientsAddresses = InternetAddress.toString( + fromAddresses = InternetAddress.toString( thread.recipients.toTypedArray() ), hasPgp = thread.hasPgpThings, @@ -226,7 +226,7 @@ object GmailHistoryHandler { threadMessagesCount = thread?.messagesCount, labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), hasAttachments = thread?.hasAttachments, - threadRecipientsAddresses = InternetAddress.toString( + fromAddresses = InternetAddress.toString( thread?.recipients?.toTypedArray() ), hasPgp = thread?.hasPgpThings @@ -241,7 +241,6 @@ object GmailHistoryHandler { roomDatabase.msgDao() .updateFlagsSuspend(accountEntity.email, localFolder.fullName, map) - val m = labelsToBeUpdatedMap.keys//messages ids val threadIds2 = roomDatabase.msgDao() .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = m) @@ -272,7 +271,7 @@ object GmailHistoryHandler { threadMessagesCount = thread?.messagesCount, labelIds = thread?.labels?.joinToString(separator = LABEL_IDS_SEPARATOR), hasAttachments = thread?.hasAttachments, - threadRecipientsAddresses = InternetAddress.toString( + fromAddresses = InternetAddress.toString( thread?.recipients?.toTypedArray() ), hasPgp = thread?.hasPgpThings @@ -289,7 +288,7 @@ object GmailHistoryHandler { .updateGmailLabels(accountEntity.email, localFolder.fullName, labelsToBeUpdatedMap) } - if (folderType === FoldersManager.FolderType.SENT) { + if (folderType == FoldersManager.FolderType.SENT) { val session = Session.getInstance(Properties()) GeneralUtil.updateLocalContactsIfNeeded( context = applicationContext, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index a5e32fe245..060c32eee6 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -100,20 +100,16 @@ data class MessageEntity( @ColumnInfo(defaultValue = "-1") val state: Int? = null, @ColumnInfo(name = "attachments_directory") val attachmentsDirectory: String? = null, @ColumnInfo(name = "error_message", defaultValue = "NULL") val errorMsg: String? = null, + @ColumnInfo(name = "password", defaultValue = "NULL") val password: ByteArray? = null, + @ColumnInfo(name = "is_visible", defaultValue = "1") val isVisible: Boolean = true, @ColumnInfo(name = "thread_id", defaultValue = "NULL") val threadId: String? = null, @ColumnInfo(name = "history_id", defaultValue = "NULL") val historyId: String? = null, - @ColumnInfo(name = "password", defaultValue = "NULL") val password: ByteArray? = null, @ColumnInfo(name = "draft_id", defaultValue = "NULL") val draftId: String? = null, @ColumnInfo(name = "label_ids", defaultValue = "NULL") val labelIds: String? = null, @ColumnInfo(name = "is_encrypted", defaultValue = "-1") val isEncrypted: Boolean? = null, @ColumnInfo(name = "has_pgp", defaultValue = "0") val hasPgp: Boolean? = null, @ColumnInfo(name = "thread_messages_count", defaultValue = "NULL") val threadMessagesCount: Int? = null, - @ColumnInfo( - name = "thread_recipients_addresses", - defaultValue = "NULL" - ) val threadRecipientsAddresses: String? = null, @ColumnInfo(name = "snippet", defaultValue = "NULL") val snippet: String? = null, - @ColumnInfo(name = "is_visible", defaultValue = "1") val isVisible: Boolean = true, ) : Parcelable { @IgnoredOnParcel @@ -132,11 +128,6 @@ data class MessageEntity( @Ignore val cc: List = ccAddresses.asInternetAddresses().asList() - @IgnoredOnParcel - @Ignore - val threadRecipients: List = - threadRecipientsAddresses.asInternetAddresses().asList() - @IgnoredOnParcel @Ignore val msgState: MessageState = MessageState.generate(state ?: MessageState.NONE.value) @@ -209,20 +200,19 @@ data class MessageEntity( if (state != other.state) return false if (attachmentsDirectory != other.attachmentsDirectory) return false if (errorMsg != other.errorMsg) return false - if (threadId != other.threadId) return false - if (historyId != other.historyId) return false if (password != null) { if (other.password == null) return false if (!password.contentEquals(other.password)) return false } else if (other.password != null) return false + if (isVisible != other.isVisible) return false + if (threadId != other.threadId) return false + if (historyId != other.historyId) return false if (draftId != other.draftId) return false if (labelIds != other.labelIds) return false if (isEncrypted != other.isEncrypted) return false if (hasPgp != other.hasPgp) return false if (threadMessagesCount != other.threadMessagesCount) return false - if (threadRecipientsAddresses != other.threadRecipientsAddresses) return false if (snippet != other.snippet) return false - if (isVisible != other.isVisible) return false return true } @@ -246,17 +236,16 @@ data class MessageEntity( result = 31 * result + (state ?: 0) result = 31 * result + (attachmentsDirectory?.hashCode() ?: 0) result = 31 * result + (errorMsg?.hashCode() ?: 0) + result = 31 * result + (password?.contentHashCode() ?: 0) + result = 31 * result + isVisible.hashCode() result = 31 * result + (threadId?.hashCode() ?: 0) result = 31 * result + (historyId?.hashCode() ?: 0) - result = 31 * result + (password?.contentHashCode() ?: 0) result = 31 * result + (draftId?.hashCode() ?: 0) result = 31 * result + (labelIds?.hashCode() ?: 0) result = 31 * result + (isEncrypted?.hashCode() ?: 0) result = 31 * result + (hasPgp?.hashCode() ?: 0) - result = 31 * result + (threadMessagesCount?.hashCode() ?: 0) - result = 31 * result + (threadRecipientsAddresses?.hashCode() ?: 0) + result = 31 * result + (threadMessagesCount ?: 0) result = 31 * result + (snippet?.hashCode() ?: 0) - result = 31 * result + isVisible.hashCode() return result } @@ -283,11 +272,7 @@ data class MessageEntity( else -> generateAddresses( context = context, accountName = accountName, - internetAddresses = if (threadId != null) { - threadRecipients - } else { - from - } + internetAddresses = from ) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index e181747b13..e440aeb0e8 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -547,7 +547,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application threadMessagesCount = thread.messagesCount, labelIds = thread.labels.joinToString(separator = LABEL_IDS_SEPARATOR), hasAttachments = thread.hasAttachments, - threadRecipientsAddresses = InternetAddress.toString( + fromAddresses = InternetAddress.toString( thread.recipients.toTypedArray() ), hasPgp = thread.hasPgpThings, From 9b303da980c3aec26585a14e147cdf699016f77b Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Thu, 12 Sep 2024 18:47:48 +0300 Subject: [PATCH 058/237] wip --- .../com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt index b70a3b92cc..b34636ce13 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt @@ -52,7 +52,7 @@ object GmailHistoryHandler { labelsToBeUpdatedMap: Map ) -> Unit = { _, _, _, _ -> } ) = withContext(Dispatchers.IO) { - processHistory(localFolder, historyList) { deleteCandidatesUIDs, + processHistory(accountEntity, localFolder, historyList) { deleteCandidatesUIDs, newCandidatesMap, updateCandidatesMap, labelsToBeUpdatedMap -> @@ -308,6 +308,7 @@ object GmailHistoryHandler { } private suspend fun processHistory( + accountEntity: AccountEntity, localFolder: LocalFolder, historyList: List, action: suspend ( From d9f6daea66af3fb66b84fba64abebbfe111f7d9c Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Fri, 13 Sep 2024 09:38:58 +0300 Subject: [PATCH 059/237] wip --- .../email/api/email/gmail/GmailHistoryHandler.kt | 12 +++++++----- .../com/flowcrypt/email/database/dao/MessageDao.kt | 14 ++++++++++++++ .../email/database/entity/MessageEntity.kt | 9 +++++++-- .../email/extensions/GmailAPIMessageExt.kt | 5 ++++- .../email/jetpack/viewmodel/DraftViewModel.kt | 4 ++-- .../jetpack/viewmodel/GmailLabelsViewModel.kt | 2 +- .../email/jetpack/viewmodel/MessagesViewModel.kt | 4 ++-- .../email/jetpack/viewmodel/MsgDetailsViewModel.kt | 2 +- .../jetpack/viewmodel/ThreadDetailsViewModel.kt | 10 +++++----- .../jetpack/workmanager/MessagesSenderWorker.kt | 8 ++++---- .../workmanager/base/BaseMoveMessagesWorker.kt | 2 +- .../jetpack/workmanager/sync/ArchiveMsgsWorker.kt | 2 +- .../sync/DeleteMessagesPermanentlyWorker.kt | 2 +- .../workmanager/sync/DeleteMessagesWorker.kt | 2 +- .../workmanager/sync/InboxIdleSyncWorker.kt | 2 +- .../workmanager/sync/UpdateMsgsSeenStateWorker.kt | 4 ++-- .../jetpack/workmanager/sync/UploadDraftsWorker.kt | 5 +++-- 17 files changed, 57 insertions(+), 32 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt index b34636ce13..29c954661d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/api/email/gmail/GmailHistoryHandler.kt @@ -23,6 +23,7 @@ import com.flowcrypt.email.database.entity.MessageEntity.Companion.LABEL_IDS_SEP import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.hasPgp import com.flowcrypt.email.extensions.isAppForegrounded import com.flowcrypt.email.extensions.kotlin.toHex +import com.flowcrypt.email.extensions.threadIdAsLong import com.flowcrypt.email.extensions.uid import com.flowcrypt.email.service.MessagesNotificationManager import com.flowcrypt.email.util.GeneralUtil @@ -88,8 +89,8 @@ object GmailHistoryHandler { localFolder = localFolder ) - msgs = - gmailThreadInfoList.map { it.lastMessage }.filter { it.threadId !in existingThreadIds } + msgs = gmailThreadInfoList.map { it.lastMessage } + .filter { it.threadIdAsLong !in existingThreadIds } } else { gmailThreadInfoList = emptyList() existingThreads = emptyList() @@ -155,7 +156,8 @@ object GmailHistoryHandler { if (accountEntity.useConversationMode && existingThreads.isNotEmpty()) { val threadsToBeUpdated = existingThreads.mapNotNull { threadMessageEntity -> - val thread = gmailThreadInfoList.firstOrNull { it.id == threadMessageEntity.threadId } + val thread = + gmailThreadInfoList.firstOrNull { it.id == threadMessageEntity.threadIdAsHEX } if (thread != null) { val updatedMessageEntity = MessageEntity.genMessageEntities( context = applicationContext, @@ -199,7 +201,7 @@ object GmailHistoryHandler { val s = updateCandidatesMap.keys//messages ids val threadIds = roomDatabase.msgDao() .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = s) - .mapNotNull { it.threadId }.toSet() + .mapNotNull { it.threadIdAsHEX }.toSet() val gmailThreadInfoList = GmailApiHelper.loadGmailThreadInfoInParallel( context = applicationContext, @@ -244,7 +246,7 @@ object GmailHistoryHandler { val m = labelsToBeUpdatedMap.keys//messages ids val threadIds2 = roomDatabase.msgDao() .getMsgsByUidsSuspend(accountEntity.email, localFolder.fullName, msgsUID = m) - .mapNotNull { it.threadId }.toSet() + .mapNotNull { it.threadIdAsHEX }.toSet() val gmailThreadInfoList2 = GmailApiHelper.loadGmailThreadInfoInParallel( context = applicationContext, diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt index ce963147de..99badc86f1 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt @@ -255,6 +255,12 @@ abstract class MessageDao : BaseDao { ) abstract suspend fun getNewestMsg(account: String?, folder: String?): MessageEntity? + @Query( + "SELECT * FROM messages " + + "WHERE account = :account AND folder = :folder ORDER BY thread_id DESC LIMIT 1" + ) + abstract suspend fun getNewestThread(account: String?, folder: String?): MessageEntity? + @Query("SELECT max(uid) FROM messages WHERE account = :account AND folder = :folder") abstract suspend fun getLastUIDOfMsgForLabelSuspend(account: String?, folder: String?): Int? @@ -424,6 +430,14 @@ abstract class MessageDao : BaseDao { @Query("DELETE FROM messages WHERE account = :account AND folder = :folder AND thread_id = :threadId AND is_visible = 0") abstract suspend fun clearCacheForGmailThread(account: String?, folder: String, threadId: String) + suspend fun getNewestMsgOrThread(accountEntity: AccountEntity, folder: String?): MessageEntity? { + return if (accountEntity.isGoogleSignInAccount && accountEntity.useAPI && accountEntity.useConversationMode) { + getNewestThread(accountEntity.email, folder) + } else { + getNewestMsg(accountEntity.email, folder) + } + } + @Transaction open fun deleteByUIDs(account: String?, label: String?, msgsUID: Collection) { doOperationViaSteps(list = ArrayList(msgsUID)) { stepUIDs: Collection -> diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt index 060c32eee6..1ed02ad1e8 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/entity/MessageEntity.kt @@ -36,6 +36,7 @@ import com.flowcrypt.email.extensions.jakarta.mail.internet.personalOrEmail import com.flowcrypt.email.extensions.kotlin.asInternetAddresses import com.flowcrypt.email.extensions.kotlin.capitalize import com.flowcrypt.email.extensions.kotlin.toHex +import com.flowcrypt.email.extensions.threadIdAsLong import com.flowcrypt.email.extensions.uid import com.flowcrypt.email.ui.activity.fragment.preferences.NotificationsSettingsFragment import com.flowcrypt.email.ui.adapter.GmailApiLabelsListAdapter @@ -102,7 +103,7 @@ data class MessageEntity( @ColumnInfo(name = "error_message", defaultValue = "NULL") val errorMsg: String? = null, @ColumnInfo(name = "password", defaultValue = "NULL") val password: ByteArray? = null, @ColumnInfo(name = "is_visible", defaultValue = "1") val isVisible: Boolean = true, - @ColumnInfo(name = "thread_id", defaultValue = "NULL") val threadId: String? = null, + @ColumnInfo(name = "thread_id", defaultValue = "NULL") val threadId: Long? = null, @ColumnInfo(name = "history_id", defaultValue = "NULL") val historyId: String? = null, @ColumnInfo(name = "draft_id", defaultValue = "NULL") val draftId: String? = null, @ColumnInfo(name = "label_ids", defaultValue = "NULL") val labelIds: String? = null, @@ -148,6 +149,10 @@ data class MessageEntity( @Ignore val uidAsHEX: String = uid.toHex() + @IgnoredOnParcel + @Ignore + val threadIdAsHEX = threadId?.toHex() + @IgnoredOnParcel @Ignore val isPasswordProtected = password?.isNotEmpty() ?: false @@ -517,7 +522,7 @@ data class MessageEntity( hasPgp = hasPgp, hasAttachments = GmailApiHelper.getAttsInfoFromMessagePart(msg.payload).isNotEmpty() ).copy( - threadId = msg.threadId, + threadId = msg.threadIdAsLong, historyId = msg.historyId.toString(), draftId = draftIdsMap[msg.id], labelIds = msg.labelIds?.joinToString(separator = LABEL_IDS_SEPARATOR) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/GmailAPIMessageExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/GmailAPIMessageExt.kt index 3fc77b93f0..8592a574cc 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/GmailAPIMessageExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/GmailAPIMessageExt.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.extensions @@ -12,3 +12,6 @@ import com.google.api.services.gmail.model.Message */ val Message.uid: Long get() = id.toLong(radix = 16) + +val Message.threadIdAsLong: Long + get() = id.toLong(radix = 16) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt index 054ab17ed9..959a8a1424 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/DraftViewModel.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.viewmodel @@ -57,7 +57,7 @@ import java.util.concurrent.TimeUnit */ class DraftViewModel( existingDraftMessageEntity: MessageEntity? = null, - private val gmailThreadId: String? = null, + private val gmailThreadId: Long? = null, application: Application ) : AccountViewModel(application) { private var sessionDraftMessageEntity: MessageEntity? = existingDraftMessageEntity diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/GmailLabelsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/GmailLabelsViewModel.kt index d4cd0db157..9b85a4232a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/GmailLabelsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/GmailLabelsViewModel.kt @@ -122,7 +122,7 @@ class GmailLabelsViewModel( GmailApiHelper.changeLabelsForThreads( context = getApplication(), accountEntity = activeAccount, - threadIdList = messageEntities.mapNotNull { it.threadId }.toSet(), + threadIdList = messageEntities.mapNotNull { it.threadIdAsHEX }.toSet(), addLabelIds = labelsToBeAdded.map { it.id }, removeLabelIds = labelsToBeRemoved.map { it.id } ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt index e440aeb0e8..73d1fb523c 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MessagesViewModel.kt @@ -849,7 +849,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application localFolder: LocalFolder ): Result = withContext(Dispatchers.IO) { val newestMsg = - roomDatabase.msgDao().getNewestMsg(account = accountEntity.email, localFolder.fullName) + roomDatabase.msgDao().getNewestMsgOrThread(accountEntity, localFolder.fullName) try { val labelEntity = roomDatabase.labelDao() .getLabelSuspend(accountEntity.email, accountEntity.accountType, localFolder.fullName) @@ -874,7 +874,7 @@ class MessagesViewModel(application: Application) : AccountViewModel(application when (e) { is GoogleJsonResponseException -> { if (localFolder.getFolderType() == FoldersManager.FolderType.INBOX - && e.statusCode == HttpURLConnection.HTTP_NOT_FOUND + && e.details.code == HttpURLConnection.HTTP_NOT_FOUND && e.details.errors.any { it.reason.equals("notFound", true) } ) { //client must perform a full sync diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt index 8e2d41cb4f..97d96b9f7b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/MsgDetailsViewModel.kt @@ -332,7 +332,7 @@ class MsgDetailsViewModel( GmailApiHelper.loadThreadInfo( context = getApplication(), accountEntity = account, - threadId = freshestMessageEntity?.threadId ?: "", + threadId = freshestMessageEntity?.threadIdAsHEX ?: "", fields = listOf("id", "messages/labelIds"), format = GmailApiHelper.RESPONSE_FORMAT_MINIMAL ).labels diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index 9723702643..ff5bca32aa 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -37,14 +37,14 @@ class ThreadDetailsViewModel( flow { val initialMessageEntity = roomDatabase.msgDao().getMsgById(messageEntityId) ?: return@flow val activeAccount = getActiveAccountSuspend() ?: return@flow - if (initialMessageEntity.threadId.isNullOrEmpty() || !activeAccount.isGoogleSignInAccount) { + if (initialMessageEntity.threadIdAsHEX.isNullOrEmpty() || !activeAccount.isGoogleSignInAccount) { emit(listOf(initialMessageEntity)) } else { try { val messagesInThread = GmailApiHelper.loadMessagesInThread( application, activeAccount, - initialMessageEntity.threadId + initialMessageEntity.threadIdAsHEX ).toMutableList().apply { //put drafts in the right position val drafts = filter { it.isDraft() } @@ -82,7 +82,7 @@ class ThreadDetailsViewModel( roomDatabase.msgDao().clearCacheForGmailThread( account = activeAccount.email, folder = GmailApiHelper.LABEL_INBOX, //fix me - threadId = initialMessageEntity.threadId + threadId = initialMessageEntity.threadIdAsHEX ) roomDatabase.msgDao().insertWithReplaceSuspend(messageEntities) @@ -97,7 +97,7 @@ class ThreadDetailsViewModel( val cachedEntities = roomDatabase.msgDao().getMessagesForGmailThread( activeAccount.email, GmailApiHelper.LABEL_INBOX,//fix me - initialMessageEntity.threadId, + initialMessageEntity.threadIdAsHEX, ) val finalList = messageEntities.map { fromServerMessageEntity -> @@ -142,7 +142,7 @@ class ThreadDetailsViewModel( val latestLabelIds = GmailApiHelper.loadThreadInfo( context = getApplication(), accountEntity = account, - threadId = freshestMessageEntity?.threadId ?: "", + threadId = freshestMessageEntity?.threadIdAsHEX ?: "", fields = listOf("id", "messages/labelIds"), format = GmailApiHelper.RESPONSE_FORMAT_MINIMAL ).labels diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt index 4da8da17c1..c4b855b2fd 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.jetpack.workmanager @@ -293,7 +293,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : ExceptionUtil.handleError(e) if (!GeneralUtil.isConnected(applicationContext)) { - msgEntity?.let { + msgEntity.let { if (msgEntity.msgState !== MessageState.SENT) { roomDatabase.msgDao() .updateSuspend(msgEntity.copy(state = MessageState.QUEUED.value)) @@ -325,7 +325,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : } } - msgEntity?.let { + msgEntity.let { roomDatabase.msgDao() .updateSuspend(msgEntity.copy(state = newMsgState.value, errorMsg = e.message)) } @@ -469,7 +469,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : mimeMsg.writeTo(out) } - val threadId = msgEntity.threadId + val threadId = msgEntity.threadIdAsHEX ?: mimeMsg.getHeader(JavaEmailConstants.HEADER_IN_REPLY_TO, null) ?.let { replyMsgId -> GmailApiHelper.executeWithResult { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/base/BaseMoveMessagesWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/base/BaseMoveMessagesWorker.kt index d32c9c1e1e..fc05fd86c2 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/base/BaseMoveMessagesWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/base/BaseMoveMessagesWorker.kt @@ -75,7 +75,7 @@ abstract class BaseMoveMessagesWorker(context: Context, params: WorkerParameters GmailApiHelper.changeLabelsForThreads( context = applicationContext, accountEntity = account, - threadIdList = entities.mapNotNull { it.threadId }.toSet(), + threadIdList = entities.mapNotNull { it.threadIdAsHEX }.toSet(), addLabelIds = gmailApiLabelsData.addLabelIds, removeLabelIds = gmailApiLabelsData.removeLabelIds ) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt index a4d1a4333b..75944c20db 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/ArchiveMsgsWorker.kt @@ -64,7 +64,7 @@ class ArchiveMsgsWorker(context: Context, params: WorkerParameters) : GmailApiHelper.changeLabelsForThreads( context = applicationContext, accountEntity = account, - threadIdList = entities.mapNotNull { it.threadId }.toSet(), + threadIdList = entities.mapNotNull { it.threadIdAsHEX }.toSet(), removeLabelIds = listOf(GmailApiHelper.LABEL_INBOX) ) } else { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt index 02a7da16db..9a7da072ad 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesPermanentlyWorker.kt @@ -58,7 +58,7 @@ class DeleteMessagesPermanentlyWorker(context: Context, params: WorkerParameters GmailApiHelper.deleteThreadsPermanently( context = applicationContext, accountEntity = account, - ids = entities.mapNotNull { it.threadId }.toSet() + ids = entities.mapNotNull { it.threadIdAsHEX }.toSet() ) } else { val uidList = entities.map { it.uid } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt index 580434afcb..3dcc751494 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/DeleteMessagesWorker.kt @@ -51,7 +51,7 @@ class DeleteMessagesWorker(context: Context, params: WorkerParameters) : GmailApiHelper.moveThreadToTrash( context = applicationContext, accountEntity = account, - ids = entities.mapNotNull { it.threadId }.toSet() + ids = entities.mapNotNull { it.threadIdAsHEX }.toSet() ) } else { val uidList = entities.map { it.uid } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt index 2d5b8ff539..ce1e2f5396 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/InboxIdleSyncWorker.kt @@ -110,7 +110,7 @@ open class InboxIdleSyncWorker(context: Context, params: WorkerParameters) : val foldersManager = FoldersManager.fromDatabaseSuspend(applicationContext, accountEntity) val inboxLocalFolder = foldersManager.findInboxFolder() ?: return@withContext val newestMsg = - roomDatabase.msgDao().getNewestMsg(account = accountEntity.email, inboxLocalFolder.fullName) + roomDatabase.msgDao().getNewestMsgOrThread(accountEntity, inboxLocalFolder.fullName) val labelEntity = roomDatabase.labelDao() .getLabelSuspend(accountEntity.email, accountEntity.accountType, inboxLocalFolder.fullName) val labelEntityHistoryId = BigInteger(labelEntity?.historyId ?: "0") diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UpdateMsgsSeenStateWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UpdateMsgsSeenStateWorker.kt index d3f78ba217..eca8464b46 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UpdateMsgsSeenStateWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UpdateMsgsSeenStateWorker.kt @@ -69,7 +69,7 @@ class UpdateMsgsSeenStateWorker(context: Context, params: WorkerParameters) : GmailApiHelper.changeLabelsForThreads( context = applicationContext, accountEntity = account, - threadIdList = entities.mapNotNull { it.threadId }.toSet(), + threadIdList = entities.mapNotNull { it.threadIdAsHEX }.toSet(), removeLabelIds = listOf(GmailApiHelper.LABEL_UNREAD) ) } else { @@ -85,7 +85,7 @@ class UpdateMsgsSeenStateWorker(context: Context, params: WorkerParameters) : GmailApiHelper.changeLabelsForThreads( context = applicationContext, accountEntity = account, - threadIdList = entities.mapNotNull { it.threadId }.toSet(), + threadIdList = entities.mapNotNull { it.threadIdAsHEX }.toSet(), addLabelIds = listOf(GmailApiHelper.LABEL_UNREAD) ) } else { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt index bac89821f4..5b2dee2aab 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/sync/UploadDraftsWorker.kt @@ -13,6 +13,7 @@ import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.database.MessageState import com.flowcrypt.email.database.entity.AccountEntity import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.extensions.threadIdAsLong import com.flowcrypt.email.extensions.uid import com.flowcrypt.email.security.KeyStoreCryptoManager import com.flowcrypt.email.util.CacheManager @@ -69,7 +70,7 @@ class UploadDraftsWorker(context: Context, params: WorkerParameters) : account = account, mimeMessage = mimeMessage, draftId = draftId, - threadId = messageEntity.threadId + threadId = messageEntity.threadIdAsHEX ) val message = GmailApiHelper.loadMsgInfoSuspend( @@ -89,7 +90,7 @@ class UploadDraftsWorker(context: Context, params: WorkerParameters) : val messageEntityWithoutStateChange = messageEntity.copy( uid = message.uid, - threadId = message.threadId, + threadId = message.threadIdAsLong, draftId = draft.id, historyId = message.historyId.toString() ) From 5682b5111c96113ad791b11ac92e08dd9668b552 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Fri, 13 Sep 2024 11:02:05 +0300 Subject: [PATCH 060/237] wip --- .../workmanager/MessagesSenderWorker.kt | 14 ++++------- .../activity/fragment/MessagesListFragment.kt | 4 ++-- ...adFragment.kt => ThreadDetailsFragment.kt} | 24 ++++++++++--------- .../ViewPagerMessageDetailsFragment.kt | 14 +++++++---- ...r.kt => MessageDetailsFragmentsAdapter.kt} | 6 +---- .../src/main/res/navigation/nav_graph.xml | 23 ++++++++++++------ 6 files changed, 46 insertions(+), 39 deletions(-) rename FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/{GmailThreadFragment.kt => ThreadDetailsFragment.kt} (88%) rename FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/{FragmentsAdapter.kt => MessageDetailsFragmentsAdapter.kt} (93%) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt index c4b855b2fd..d901324c95 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt @@ -293,11 +293,9 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : ExceptionUtil.handleError(e) if (!GeneralUtil.isConnected(applicationContext)) { - msgEntity.let { - if (msgEntity.msgState !== MessageState.SENT) { - roomDatabase.msgDao() - .updateSuspend(msgEntity.copy(state = MessageState.QUEUED.value)) - } + if (msgEntity.msgState != MessageState.SENT) { + roomDatabase.msgDao() + .updateSuspend(msgEntity.copy(state = MessageState.QUEUED.value)) } throw e } else { @@ -325,10 +323,8 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : } } - msgEntity.let { - roomDatabase.msgDao() - .updateSuspend(msgEntity.copy(state = newMsgState.value, errorMsg = e.message)) - } + roomDatabase.msgDao() + .updateSuspend(msgEntity.copy(state = newMsgState.value, errorMsg = e.message)) } delay(5000) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt index 2737f6eae7..dd2f65ed56 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/MessagesListFragment.kt @@ -428,9 +428,9 @@ class MessagesListFragment : BaseFragment(), ListPr navController?.navigateSafe( currentDestinationId = R.id.messagesListFragment, directions = MessagesListFragmentDirections - .actionMessagesListFragmentToGmailThreadFragment( + .actionMessagesListFragmentToViewPagerThreadDetailsFragment( messageEntityId = msgEntity.id ?: -1, - //localFolder = localFolder + localFolder = localFolder ) ) } else { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt similarity index 88% rename from FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt rename to FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt index ee7a10f0a0..9067b86a27 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/GmailThreadFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt @@ -24,6 +24,7 @@ import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithV import com.flowcrypt.email.extensions.androidx.fragment.app.navController import com.flowcrypt.email.extensions.androidx.fragment.app.supportActionBar import com.flowcrypt.email.extensions.androidx.fragment.app.toast +import com.flowcrypt.email.extensions.androidx.navigation.navigateSafe import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.ThreadDetailsViewModel import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment @@ -40,7 +41,7 @@ import kotlinx.coroutines.launch /** * @author Denys Bondarenko */ -class GmailThreadFragment : BaseFragment(), +class ThreadDetailsFragment : BaseFragment(), ProgressBehaviour { override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?) = FragmentNewMessageDetailsBinding.inflate(inflater, container, false) @@ -52,7 +53,7 @@ class GmailThreadFragment : BaseFragment(), override val statusView: View? get() = binding?.status?.root - private val args by navArgs() + private val args by navArgs() private val threadDetailsViewModel: ThreadDetailsViewModel by viewModels { object : CustomAndroidViewModelFactory(requireActivity().application) { @Suppress("UNCHECKED_CAST") @@ -81,15 +82,16 @@ class GmailThreadFragment : BaseFragment(), messageEntity.folder, messageEntity.uid )?.id?.let { - navController?.navigate( - GmailThreadFragmentDirections.actionGmailThreadFragmentToViewPagerMessageDetailsFragment( - messageEntityId = it, - sortedEntityIdListForThread = getSortedEntityIdListForThread(), - localFolder = LocalFolder( - messageEntity.account, - GmailApiHelper.LABEL_INBOX - )//fix me - ) + navController?.navigateSafe( + currentDestinationId = R.id.threadDetailsFragment, + directions = MessagesListFragmentDirections + .actionMessagesListFragmentToViewPagerThreadDetailsFragment( + messageEntityId = it, + localFolder = LocalFolder( + messageEntity.account, + GmailApiHelper.LABEL_INBOX + )//fix me + ) ) } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt index 36f6987470..c625a67335 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerMessageDetailsFragment.kt @@ -31,7 +31,7 @@ import com.flowcrypt.email.extensions.observeOnce import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.MessagesViewPagerViewModel import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment -import com.flowcrypt.email.ui.adapter.FragmentsAdapter +import com.flowcrypt.email.ui.adapter.MessageDetailsFragmentsAdapter /** * @author Denys Bondarenko @@ -63,7 +63,7 @@ class ViewPagerMessageDetailsFragment : BaseFragment + (adapter as MessageDetailsFragmentsAdapter).getItem(position)?.let { messageEntity -> messagesViewPagerViewModel.onItemSelected(messageEntity) } } @@ -110,7 +110,9 @@ class ViewPagerMessageDetailsFragment : BaseFragment { - (binding?.viewPager2?.adapter as? FragmentsAdapter)?.submit(it.data ?: emptyList()) + (binding?.viewPager2?.adapter as? MessageDetailsFragmentsAdapter)?.submit( + it.data ?: emptyList() + ) } else -> { @@ -123,7 +125,9 @@ class ViewPagerMessageDetailsFragment : BaseFragment { - (binding?.viewPager2?.adapter as? FragmentsAdapter)?.submit(it.data ?: emptyList()) + (binding?.viewPager2?.adapter as? MessageDetailsFragmentsAdapter)?.submit( + it.data ?: emptyList() + ) } else -> {} diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessageDetailsFragmentsAdapter.kt similarity index 93% rename from FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt rename to FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessageDetailsFragmentsAdapter.kt index 90b8b3559a..c6b097bf6b 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/FragmentsAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessageDetailsFragmentsAdapter.kt @@ -17,7 +17,7 @@ import com.flowcrypt.email.ui.activity.fragment.MessageDetailsFragmentArgs /** * @author Denys Bondarenko */ -class FragmentsAdapter( +class MessageDetailsFragmentsAdapter( private val localFolder: LocalFolder, initialList: List, fragment: Fragment, @@ -70,8 +70,4 @@ class FragmentsAdapter( fun getItem(position: Int): MessageEntity? { return asyncListDiffer.currentList.getOrNull(position) } - - fun getItemPositionById(id: Long): Int { - return asyncListDiffer.currentList.indexOfFirst { item -> item.id == id } - } } \ No newline at end of file diff --git a/FlowCrypt/src/main/res/navigation/nav_graph.xml b/FlowCrypt/src/main/res/navigation/nav_graph.xml index ff195d9b06..419512b3f2 100644 --- a/FlowCrypt/src/main/res/navigation/nav_graph.xml +++ b/FlowCrypt/src/main/res/navigation/nav_graph.xml @@ -315,6 +315,18 @@ app:nullable="true" /> + + + + + - + android:id="@+id/action_messagesListFragment_to_viewPagerThreadDetailsFragment" + app:destination="@id/viewPagerThreadDetailsFragment" /> Date: Fri, 13 Sep 2024 15:17:58 +0300 Subject: [PATCH 061/237] wip --- .../email/extensions/GmailAPIMessageExt.kt | 2 +- .../viewmodel/ThreadDetailsViewModel.kt | 24 ++-- .../viewmodel/ThreadsViewPagerViewModel.kt | 102 +++++++++++++ .../ViewPagerThreadDetailsFragment.kt | 135 ++++++++++++++++++ .../adapter/ThreadDetailsFragmentsAdapter.kt | 73 ++++++++++ .../fragment_view_pager_thread_details.xml | 22 +++ 6 files changed, 346 insertions(+), 12 deletions(-) create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadsViewPagerViewModel.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerThreadDetailsFragment.kt create mode 100644 FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ThreadDetailsFragmentsAdapter.kt create mode 100644 FlowCrypt/src/main/res/layout/fragment_view_pager_thread_details.xml diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/GmailAPIMessageExt.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/GmailAPIMessageExt.kt index 8592a574cc..7e9d8daa4a 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/GmailAPIMessageExt.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/extensions/GmailAPIMessageExt.kt @@ -14,4 +14,4 @@ val Message.uid: Long get() = id.toLong(radix = 16) val Message.threadIdAsLong: Long - get() = id.toLong(radix = 16) + get() = threadId.toLong(radix = 16) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index ff5bca32aa..890a3e5a3d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -26,25 +26,27 @@ import kotlinx.coroutines.flow.merge * @author Denys Bondarenko */ class ThreadDetailsViewModel( - private val messageEntityId: Long, + private val threadMessageEntityId: Long, application: Application ) : AccountViewModel(application) { - val messageFlow: Flow = roomDatabase.msgDao().getMessageByIdFlow(messageEntityId).distinctUntilChanged() + val messageFlow: Flow = + roomDatabase.msgDao().getMessageByIdFlow(threadMessageEntityId).distinctUntilChanged() val messagesInThreadFlow: Flow> = merge( flow { - val initialMessageEntity = roomDatabase.msgDao().getMsgById(messageEntityId) ?: return@flow + val threadMessageEntity = + roomDatabase.msgDao().getMsgById(threadMessageEntityId) ?: return@flow val activeAccount = getActiveAccountSuspend() ?: return@flow - if (initialMessageEntity.threadIdAsHEX.isNullOrEmpty() || !activeAccount.isGoogleSignInAccount) { - emit(listOf(initialMessageEntity)) + if (threadMessageEntity.threadIdAsHEX.isNullOrEmpty() || !activeAccount.isGoogleSignInAccount) { + emit(listOf()) } else { try { val messagesInThread = GmailApiHelper.loadMessagesInThread( application, activeAccount, - initialMessageEntity.threadIdAsHEX + threadMessageEntity.threadIdAsHEX ).toMutableList().apply { //put drafts in the right position val drafts = filter { it.isDraft() } @@ -63,7 +65,7 @@ class ThreadDetailsViewModel( } roomDatabase.msgDao() - .updateSuspend(initialMessageEntity.copy(threadMessagesCount = messagesInThread.size)) + .updateSuspend(threadMessageEntity.copy(threadMessagesCount = messagesInThread.size)) val isOnlyPgpModeEnabled = activeAccount.showOnlyEncrypted ?: false val messageEntities = MessageEntity.genMessageEntities( @@ -82,7 +84,7 @@ class ThreadDetailsViewModel( roomDatabase.msgDao().clearCacheForGmailThread( account = activeAccount.email, folder = GmailApiHelper.LABEL_INBOX, //fix me - threadId = initialMessageEntity.threadIdAsHEX + threadId = threadMessageEntity.threadIdAsHEX ) roomDatabase.msgDao().insertWithReplaceSuspend(messageEntities) @@ -97,7 +99,7 @@ class ThreadDetailsViewModel( val cachedEntities = roomDatabase.msgDao().getMessagesForGmailThread( activeAccount.email, GmailApiHelper.LABEL_INBOX,//fix me - initialMessageEntity.threadIdAsHEX, + threadMessageEntity.threadIdAsHEX, ) val finalList = messageEntities.map { fromServerMessageEntity -> @@ -109,7 +111,7 @@ class ThreadDetailsViewModel( emit(finalList) } catch (e: Exception) { e.printStackTraceIfDebugOnly() - emit(listOf(initialMessageEntity)) + emit(listOf(threadMessageEntity)) } } }, @@ -135,7 +137,7 @@ class ThreadDetailsViewModel( if (account?.isGoogleSignInAccount == true) { val labelEntities = roomDatabase.labelDao().getLabelsSuspend(account.email, account.accountType) - val freshestMessageEntity = roomDatabase.msgDao().getMsgById(messageEntityId) + val freshestMessageEntity = roomDatabase.msgDao().getMsgById(threadMessageEntityId) val cachedLabelIds = freshestMessageEntity?.labelIds?.split(MessageEntity.LABEL_IDS_SEPARATOR) try { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadsViewPagerViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadsViewPagerViewModel.kt new file mode 100644 index 0000000000..0ae0be7d70 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadsViewPagerViewModel.kt @@ -0,0 +1,102 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.jetpack.viewmodel + +import android.app.Application +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.liveData +import androidx.lifecycle.switchMap +import com.flowcrypt.email.api.email.JavaEmailConstants +import com.flowcrypt.email.api.email.model.LocalFolder +import com.flowcrypt.email.api.retrofit.response.base.Result +import com.flowcrypt.email.database.entity.MessageEntity + +/** + * @author Denys Bondarenko + */ +class ThreadsViewPagerViewModel( + private val initialMessageEntityId: Long, + private val localFolder: LocalFolder, + application: Application +) : AccountViewModel(application) { + + val initialLiveData: LiveData>> = + activeAccountLiveData.switchMap { accountEntity -> + liveData { + if (accountEntity != null) { + val middleMessageEntity = + roomDatabase.msgDao().getMsgById(initialMessageEntityId) + + if (middleMessageEntity != null) { + emit( + Result.success(listOf(middleMessageEntity)) + ) + } else { + emit( + Result.exception( + IllegalStateException( + "MessageEntity with id = $initialMessageEntityId not found" + ) + ) + ) + } + } else { + emit(Result.exception(IllegalStateException("account is null"))) + } + } + } + + private val manuallySelectedMessageEntity: MutableLiveData = MutableLiveData() + + val messageEntitiesLiveData: LiveData>> = + manuallySelectedMessageEntity.switchMap { messageEntity -> + liveData { + emit(Result.loading()) + val activeAccount = getActiveAccountSuspend() + if (activeAccount != null) { + val result = Result.success( + roomDatabase.msgDao() + .getMessagesForViewPager( + account = activeAccount.email, + folder = if (localFolder.searchQuery.isNullOrEmpty()) { + localFolder.fullName + } else { + JavaEmailConstants.FOLDER_SEARCH + }, + date = messageEntity.receivedDate ?: 0, + limit = PAGE_SIZE / 2 + ) + ) + + emit(result) + } else { + emit(Result.success(emptyList())) + } + } + } + + fun onItemSelected(messageEntity: MessageEntity) { + if (messageEntitiesLiveData.value?.data == null) { + manuallySelectedMessageEntity.value = messageEntity + return + } + + val position = messageEntitiesLiveData.value?.data?.indexOf(messageEntity) ?: return + val listSize = messageEntitiesLiveData.value?.data?.size + if (listSize == null || listSize == 0) { + return + } + + if (position <= listSize / 4 || position >= (listSize - listSize / 4)) { + manuallySelectedMessageEntity.value = messageEntity + } + } + + companion object { + const val PAGE_SIZE = 30 + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerThreadDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerThreadDetailsFragment.kt new file mode 100644 index 0000000000..233c709897 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ViewPagerThreadDetailsFragment.kt @@ -0,0 +1,135 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.ui.activity.fragment + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Menu +import android.view.MenuInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.core.view.MenuHost +import androidx.core.view.MenuProvider +import androidx.fragment.app.viewModels +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.ViewModel +import androidx.navigation.fragment.navArgs +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.viewpager2.widget.ViewPager2.ORIENTATION_HORIZONTAL +import androidx.viewpager2.widget.ViewPager2.OnPageChangeCallback +import com.flowcrypt.email.R +import com.flowcrypt.email.api.retrofit.response.base.Result +import com.flowcrypt.email.databinding.FragmentViewPagerThreadDetailsBinding +import com.flowcrypt.email.extensions.androidx.fragment.app.navController +import com.flowcrypt.email.extensions.androidx.fragment.app.supportActionBar +import com.flowcrypt.email.extensions.androidx.fragment.app.toast +import com.flowcrypt.email.extensions.observeOnce +import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory +import com.flowcrypt.email.jetpack.viewmodel.ThreadsViewPagerViewModel +import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment +import com.flowcrypt.email.ui.adapter.ThreadDetailsFragmentsAdapter + +/** + * @author Denys Bondarenko + */ +class ViewPagerThreadDetailsFragment : BaseFragment() { + private val args by navArgs() + + private val threadsViewPagerViewModel: ThreadsViewPagerViewModel by viewModels { + object : CustomAndroidViewModelFactory(requireActivity().application) { + @Suppress("UNCHECKED_CAST") + override fun create(modelClass: Class): T { + return ThreadsViewPagerViewModel( + initialMessageEntityId = args.messageEntityId, + localFolder = args.localFolder, + application = requireActivity().application + ) as T + } + } + } + + override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?) = + FragmentViewPagerThreadDetailsBinding.inflate(inflater, container, false) + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + //need to clear action bar titles + supportActionBar?.title = null + supportActionBar?.subtitle = null + + binding?.viewPager2?.apply { + adapter = ThreadDetailsFragmentsAdapter( + localFolder = args.localFolder, + initialList = threadsViewPagerViewModel.messageEntitiesLiveData.value?.data ?: emptyList(), + fragment = this@ViewPagerThreadDetailsFragment + ) { _, _ -> } + + addItemDecoration(DividerItemDecoration(view.context, ORIENTATION_HORIZONTAL)) + + setOffscreenPageLimit(1) + registerOnPageChangeCallback(object : OnPageChangeCallback() { + override fun onPageSelected(position: Int) { + super.onPageSelected(position) + (adapter as ThreadDetailsFragmentsAdapter).getItem(position)?.let { messageEntity -> + threadsViewPagerViewModel.onItemSelected(messageEntity) + } + } + }) + } + + setupThreadsViewPagerViewModel() + } + + override fun onDestroyView() { + binding?.viewPager2?.adapter = null + super.onDestroyView() + } + + /* + * we have to define this code to manage action bar items + * for [ThreadDetailsFragment] automatically + */ + override fun onSetupActionBarMenu(menuHost: MenuHost) { + super.onSetupActionBarMenu(menuHost) + menuHost.addMenuProvider(object : MenuProvider { + override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) { + menuInflater.inflate(R.menu.fragment_message_details, menu) + } + + override fun onMenuItemSelected(menuItem: MenuItem): Boolean = false + }, viewLifecycleOwner, Lifecycle.State.RESUMED) + } + + private fun setupThreadsViewPagerViewModel() { + threadsViewPagerViewModel.initialLiveData.observeOnce(viewLifecycleOwner) { + when (it.status) { + Result.Status.SUCCESS -> { + (binding?.viewPager2?.adapter as? ThreadDetailsFragmentsAdapter)?.submit( + it.data ?: emptyList() + ) + } + + else -> { + toast(R.string.message_not_found) + navController?.navigateUp() + } + } + } + + threadsViewPagerViewModel.messageEntitiesLiveData.observe(viewLifecycleOwner) { + when (it.status) { + Result.Status.SUCCESS -> { + (binding?.viewPager2?.adapter as? ThreadDetailsFragmentsAdapter)?.submit( + it.data ?: emptyList() + ) + } + + else -> {} + } + } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ThreadDetailsFragmentsAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ThreadDetailsFragmentsAdapter.kt new file mode 100644 index 0000000000..6b42474ef0 --- /dev/null +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/ThreadDetailsFragmentsAdapter.kt @@ -0,0 +1,73 @@ +/* + * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com + * Contributors: denbond7 + */ + +package com.flowcrypt.email.ui.adapter + +import androidx.fragment.app.Fragment +import androidx.recyclerview.widget.AsyncListDiffer +import androidx.recyclerview.widget.DiffUtil +import androidx.viewpager2.adapter.FragmentStateAdapter +import com.flowcrypt.email.api.email.model.LocalFolder +import com.flowcrypt.email.database.entity.MessageEntity +import com.flowcrypt.email.ui.activity.fragment.ThreadDetailsFragment +import com.flowcrypt.email.ui.activity.fragment.ThreadDetailsFragmentArgs + +/** + * @author Denys Bondarenko + */ +class ThreadDetailsFragmentsAdapter( + private val localFolder: LocalFolder, + initialList: List, + fragment: Fragment, + listListener: AsyncListDiffer.ListListener +) : FragmentStateAdapter(fragment) { + private val diffUtil = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: MessageEntity, newItem: MessageEntity): + Boolean { + return oldItem.id == newItem.id + } + + override fun areContentsTheSame(oldItem: MessageEntity, newItem: MessageEntity): + Boolean { + return oldItem == newItem + } + } + + private val asyncListDiffer = AsyncListDiffer(this, diffUtil) + + init { + asyncListDiffer.submitList(initialList) + asyncListDiffer.addListListener(listListener) + } + + override fun getItemCount(): Int = asyncListDiffer.currentList.size + + override fun createFragment(position: Int): Fragment = + ThreadDetailsFragment().apply { + arguments = ThreadDetailsFragmentArgs( + messageEntityId = asyncListDiffer.currentList[position].id ?: 0, + ).toBundle() + } + + override fun getItemId(position: Int): Long { + return asyncListDiffer.currentList[position].id ?: 0 + } + + override fun containsItem(itemId: Long): Boolean { + return asyncListDiffer.currentList.any { it.id == itemId } + } + + fun submit(newList: List) { + asyncListDiffer.submitList(newList) + } + + fun getItem(position: Int): MessageEntity? { + return asyncListDiffer.currentList.getOrNull(position) + } + + fun getItemPositionById(id: Long): Int { + return asyncListDiffer.currentList.indexOfFirst { item -> item.id == id } + } +} \ No newline at end of file diff --git a/FlowCrypt/src/main/res/layout/fragment_view_pager_thread_details.xml b/FlowCrypt/src/main/res/layout/fragment_view_pager_thread_details.xml new file mode 100644 index 0000000000..033af95932 --- /dev/null +++ b/FlowCrypt/src/main/res/layout/fragment_view_pager_thread_details.xml @@ -0,0 +1,22 @@ + + + + + + + + From cb0f6c3b3aa8f588c7fa794d4fda4d9f03c0cfc4 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Fri, 13 Sep 2024 15:40:13 +0300 Subject: [PATCH 062/237] wip --- .../flowcrypt/email/database/dao/MessageDao.kt | 2 +- .../jetpack/viewmodel/ThreadDetailsViewModel.kt | 2 +- .../activity/fragment/ThreadDetailsFragment.kt | 16 +++++++++------- FlowCrypt/src/main/res/navigation/nav_graph.xml | 3 +++ 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt index 99badc86f1..90cdee30f1 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/database/dao/MessageDao.kt @@ -424,7 +424,7 @@ abstract class MessageDao : BaseDao { abstract suspend fun getMessagesForGmailThread( account: String, folder: String, - threadId: String + threadId: Long ): List @Query("DELETE FROM messages WHERE account = :account AND folder = :folder AND thread_id = :threadId AND is_visible = 0") diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index 890a3e5a3d..13ca801b46 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -99,7 +99,7 @@ class ThreadDetailsViewModel( val cachedEntities = roomDatabase.msgDao().getMessagesForGmailThread( activeAccount.email, GmailApiHelper.LABEL_INBOX,//fix me - threadMessageEntity.threadIdAsHEX, + threadMessageEntity.threadId ?: 0, ) val finalList = messageEntities.map { fromServerMessageEntity -> diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt index 9067b86a27..b53172b359 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt @@ -12,6 +12,7 @@ import android.view.ViewGroup import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel import androidx.lifecycle.lifecycleScope +import androidx.navigation.NavDirections import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager import com.flowcrypt.email.R @@ -24,7 +25,6 @@ import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithV import com.flowcrypt.email.extensions.androidx.fragment.app.navController import com.flowcrypt.email.extensions.androidx.fragment.app.supportActionBar import com.flowcrypt.email.extensions.androidx.fragment.app.toast -import com.flowcrypt.email.extensions.androidx.navigation.navigateSafe import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.ThreadDetailsViewModel import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment @@ -82,16 +82,18 @@ class ThreadDetailsFragment : BaseFragment(), messageEntity.folder, messageEntity.uid )?.id?.let { - navController?.navigateSafe( - currentDestinationId = R.id.threadDetailsFragment, - directions = MessagesListFragmentDirections - .actionMessagesListFragmentToViewPagerThreadDetailsFragment( + navController?.navigate( + object : NavDirections { + override val actionId = R.id.viewPagerMessageDetailsFragment + override val arguments = ViewPagerMessageDetailsFragmentArgs( messageEntityId = it, localFolder = LocalFolder( messageEntity.account, GmailApiHelper.LABEL_INBOX - )//fix me - ) + ),//fix me, + sortedEntityIdListForThread = getSortedEntityIdListForThread() + ).toBundle() + } ) } } diff --git a/FlowCrypt/src/main/res/navigation/nav_graph.xml b/FlowCrypt/src/main/res/navigation/nav_graph.xml index 419512b3f2..2ac015f71d 100644 --- a/FlowCrypt/src/main/res/navigation/nav_graph.xml +++ b/FlowCrypt/src/main/res/navigation/nav_graph.xml @@ -354,6 +354,9 @@ + Date: Mon, 23 Sep 2024 14:13:17 +0300 Subject: [PATCH 063/237] Last worked version for separate screens.| #74 --- .../email/ui/activity/fragment/ThreadDetailsFragment.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt index b53172b359..0b39d868a7 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt @@ -68,7 +68,7 @@ class ThreadDetailsFragment : BaseFragment(), private val gmailApiLabelsListAdapter = GmailApiLabelsListAdapter( object : GmailApiLabelsListAdapter.OnLabelClickListener { override fun onLabelClick(label: GmailApiLabelsListAdapter.Label) { - toast("fix me") + toast("fix me ") } }) From 606bb0a52eb5926335368156fe2a49a7d7acaeb9 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 23 Sep 2024 14:20:30 +0300 Subject: [PATCH 064/237] wip --- .../jetpack/workmanager/MessagesSenderWorker.kt | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt index d901324c95..7ead9fe4b1 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt @@ -294,8 +294,10 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : if (!GeneralUtil.isConnected(applicationContext)) { if (msgEntity.msgState != MessageState.SENT) { - roomDatabase.msgDao() - .updateSuspend(msgEntity.copy(state = MessageState.QUEUED.value)) + msgEntity.copy(state = MessageState.QUEUED.value)?.let { + roomDatabase.msgDao() + .updateSuspend(it) + } } throw e } else { @@ -323,8 +325,10 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : } } - roomDatabase.msgDao() - .updateSuspend(msgEntity.copy(state = newMsgState.value, errorMsg = e.message)) + msgEntity.copy(state = newMsgState.value, errorMsg = e.message)?.let { + roomDatabase.msgDao() + .updateSuspend(it) + } } delay(5000) From 95bc9fd23a067012739cae2074cbb11caadb5574 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 23 Sep 2024 14:29:19 +0300 Subject: [PATCH 065/237] Added a simple version of thread details fragment.| #74 --- .../workmanager/MessagesSenderWorker.kt | 4 +- .../fragment/ThreadDetailsFragment.kt | 40 +++------------- .../res/layout/fragment_thread_details.xml | 46 +++++++++++++++++++ 3 files changed, 54 insertions(+), 36 deletions(-) create mode 100644 FlowCrypt/src/main/res/layout/fragment_thread_details.xml diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt index 7ead9fe4b1..adaf5c2ffd 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/workmanager/MessagesSenderWorker.kt @@ -294,7 +294,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : if (!GeneralUtil.isConnected(applicationContext)) { if (msgEntity.msgState != MessageState.SENT) { - msgEntity.copy(state = MessageState.QUEUED.value)?.let { + msgEntity.copy(state = MessageState.QUEUED.value).let { roomDatabase.msgDao() .updateSuspend(it) } @@ -325,7 +325,7 @@ class MessagesSenderWorker(context: Context, params: WorkerParameters) : } } - msgEntity.copy(state = newMsgState.value, errorMsg = e.message)?.let { + msgEntity.copy(state = newMsgState.value, errorMsg = e.message).let { roomDatabase.msgDao() .updateSuspend(it) } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt index 0b39d868a7..5e6139735d 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt @@ -20,36 +20,29 @@ import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.model.LocalFolder import com.flowcrypt.email.database.FlowCryptRoomDatabase import com.flowcrypt.email.database.entity.MessageEntity -import com.flowcrypt.email.databinding.FragmentNewMessageDetailsBinding +import com.flowcrypt.email.databinding.FragmentThreadDetailsBinding import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithViewLifecycle import com.flowcrypt.email.extensions.androidx.fragment.app.navController import com.flowcrypt.email.extensions.androidx.fragment.app.supportActionBar -import com.flowcrypt.email.extensions.androidx.fragment.app.toast import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.ThreadDetailsViewModel import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment import com.flowcrypt.email.ui.activity.fragment.base.ProgressBehaviour -import com.flowcrypt.email.ui.adapter.GmailApiLabelsListAdapter import com.flowcrypt.email.ui.adapter.MessagesInThreadListAdapter -import com.flowcrypt.email.ui.adapter.recyclerview.itemdecoration.MarginItemDecoration -import com.google.android.flexbox.FlexDirection -import com.google.android.flexbox.FlexboxLayoutManager -import com.google.android.flexbox.JustifyContent import com.google.android.material.divider.MaterialDividerItemDecoration import kotlinx.coroutines.launch /** * @author Denys Bondarenko */ -class ThreadDetailsFragment : BaseFragment(), - ProgressBehaviour { +class ThreadDetailsFragment : BaseFragment(), ProgressBehaviour { override fun inflateBinding(inflater: LayoutInflater, container: ViewGroup?) = - FragmentNewMessageDetailsBinding.inflate(inflater, container, false) + FragmentThreadDetailsBinding.inflate(inflater, container, false) override val progressView: View? get() = binding?.progress?.root override val contentView: View? - get() = binding?.content + get() = binding?.recyclerViewMessages override val statusView: View? get() = binding?.status?.root @@ -65,13 +58,6 @@ class ThreadDetailsFragment : BaseFragment(), } } - private val gmailApiLabelsListAdapter = GmailApiLabelsListAdapter( - object : GmailApiLabelsListAdapter.OnLabelClickListener { - override fun onLabelClick(label: GmailApiLabelsListAdapter.Label) { - toast("fix me ") - } - }) - private val messagesInThreadListAdapter = MessagesInThreadListAdapter(object : MessagesInThreadListAdapter.OnMessageClickListener { override fun onMessageClick(messageEntity: MessageEntity) { @@ -120,7 +106,7 @@ class ThreadDetailsFragment : BaseFragment(), private fun setupThreadDetailsViewModel() { launchAndRepeatWithViewLifecycle { threadDetailsViewModel.messageFlow.collect { - binding?.textViewSubject?.text = it?.subject + //binding?.textViewSubject?.text = it?.subject } } @@ -133,7 +119,7 @@ class ThreadDetailsFragment : BaseFragment(), launchAndRepeatWithViewLifecycle { threadDetailsViewModel.messageGmailApiLabelsFlow.collect { - gmailApiLabelsListAdapter.submitList(it) + } } } @@ -151,19 +137,5 @@ class ThreadDetailsFragment : BaseFragment(), ) adapter = messagesInThreadListAdapter } - - binding?.recyclerViewLabels?.apply { - layoutManager = FlexboxLayoutManager(context).apply { - flexDirection = FlexDirection.ROW - justifyContent = JustifyContent.FLEX_START - } - addItemDecoration( - MarginItemDecoration( - marginRight = resources.getDimensionPixelSize(R.dimen.default_margin_small), - marginTop = resources.getDimensionPixelSize(R.dimen.default_margin_small) - ) - ) - adapter = gmailApiLabelsListAdapter - } } } \ No newline at end of file diff --git a/FlowCrypt/src/main/res/layout/fragment_thread_details.xml b/FlowCrypt/src/main/res/layout/fragment_thread_details.xml new file mode 100644 index 0000000000..dacd245eb9 --- /dev/null +++ b/FlowCrypt/src/main/res/layout/fragment_thread_details.xml @@ -0,0 +1,46 @@ + + + + + + + + + + From 6fa63fd6f4c9dc9a2c13165f3ddb046492cd708c Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Mon, 23 Sep 2024 15:16:20 +0300 Subject: [PATCH 066/237] Added displaying header.| #74 --- .../fragment/ThreadDetailsFragment.kt | 6 +- .../AutoCompleteResultRecyclerViewAdapter.kt | 3 +- .../ui/adapter/MessagesInThreadListAdapter.kt | 128 ++++++++++++++++-- .../main/res/layout/item_thread_header.xml | 47 +++++++ 4 files changed, 167 insertions(+), 17 deletions(-) create mode 100644 FlowCrypt/src/main/res/layout/item_thread_header.xml diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt index 5e6139735d..6e4f10ada1 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt @@ -112,7 +112,11 @@ class ThreadDetailsFragment : BaseFragment(), Prog launchAndRepeatWithViewLifecycle { threadDetailsViewModel.messagesInThreadFlow.collect { - messagesInThreadListAdapter.submitList(it) + messagesInThreadListAdapter.submitList(it.map { messageEntity -> + MessagesInThreadListAdapter.Message( + messageEntity + ) + }) showContent() } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AutoCompleteResultRecyclerViewAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AutoCompleteResultRecyclerViewAdapter.kt index b4c05e91ca..d5ebe06572 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AutoCompleteResultRecyclerViewAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/AutoCompleteResultRecyclerViewAdapter.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui.adapter @@ -53,7 +53,6 @@ class AutoCompleteResultRecyclerViewAdapter( .inflate(R.layout.recipient_auto_complete_item, parent, false) ) } - } override fun getItemId(position: Int): Long { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt index 3d7c338032..c16473438e 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -14,40 +14,107 @@ import android.text.style.ForegroundColorSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.annotation.IntDef import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.flowcrypt.email.R import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.databinding.ItemMessageInThreadBinding +import com.flowcrypt.email.databinding.ItemThreadHeaderBinding import com.flowcrypt.email.extensions.android.widget.useGlideToApplyImageFromSource +import com.flowcrypt.email.extensions.toast import com.flowcrypt.email.extensions.visibleOrGone +import com.flowcrypt.email.ui.adapter.recyclerview.itemdecoration.MarginItemDecoration import com.flowcrypt.email.util.DateTimeUtil import com.flowcrypt.email.util.graphics.glide.AvatarModelLoader +import com.google.android.flexbox.FlexDirection +import com.google.android.flexbox.FlexboxLayoutManager +import com.google.android.flexbox.JustifyContent import com.google.android.material.color.MaterialColors /** * @author Denys Bondarenko */ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageClickListener) : - ListAdapter(DIFF_UTIL_ITEM_CALLBACK) { + ListAdapter( + DIFF_UTIL_ITEM_CALLBACK + ) { - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder( - LayoutInflater.from(parent.context).inflate(R.layout.item_message_in_thread, parent, false) - ) + override fun getItemViewType(position: Int): Int { + return when (position) { + 0 -> HEADER + else -> MESSAGE + } } - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - holder.bindTo(getItem(position), onMessageClickListener) + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { + return when (viewType) { + HEADER -> HeaderViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_thread_header, parent, false) + ) + + MESSAGE -> MessageViewHolder( + LayoutInflater.from(parent.context).inflate(R.layout.item_message_in_thread, parent, false) + ) + + else -> error("Unreachable") + } + } + + override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { + when (holder.itemViewType) { + HEADER -> { + val header = (getItem(position) as? Header) ?: return + (holder as? HeaderViewHolder)?.bindTo(header) + } + + MESSAGE -> { + val message = (getItem(position) as? Message) ?: return + (holder as? MessageViewHolder)?.bindTo(message.messageEntity, onMessageClickListener) + } + + else -> error("Unreachable") + } } interface OnMessageClickListener { fun onMessageClick(messageEntity: MessageEntity) } - inner class ViewHolder(itemView: View) : - RecyclerView.ViewHolder(itemView) { + abstract inner class BaseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) + + inner class HeaderViewHolder(itemView: View) : BaseViewHolder(itemView) { + val binding = ItemThreadHeaderBinding.bind(itemView) + private val gmailApiLabelsListAdapter = GmailApiLabelsListAdapter( + object : GmailApiLabelsListAdapter.OnLabelClickListener { + override fun onLabelClick(label: GmailApiLabelsListAdapter.Label) { + itemView.context.toast("fix me") + } + }) + + fun bindTo(header: Header) { + binding.textViewSubject.text = header.subject + + binding.recyclerViewLabels.apply { + layoutManager = FlexboxLayoutManager(context).apply { + flexDirection = FlexDirection.ROW + justifyContent = JustifyContent.FLEX_START + } + addItemDecoration( + MarginItemDecoration( + marginRight = resources.getDimensionPixelSize(R.dimen.default_margin_small), + marginTop = resources.getDimensionPixelSize(R.dimen.default_margin_small) + ) + ) + adapter = gmailApiLabelsListAdapter + } + + gmailApiLabelsListAdapter.submitList(header.labels) + } + } + + inner class MessageViewHolder(itemView: View) : BaseViewHolder(itemView) { val binding = ItemMessageInThreadBinding.bind(itemView) fun bindTo(item: MessageEntity, onMessageClickListener: OnMessageClickListener) { @@ -113,13 +180,46 @@ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageC } } + abstract class Item(val type: Int) { + abstract val id: Long + abstract fun areContentsTheSame(other: Any?): Boolean + } + + data class Message(val messageEntity: MessageEntity) : Item(MESSAGE) { + override val id: Long + get() = messageEntity.uid + + override fun areContentsTheSame(other: Any?): Boolean { + return this == other + } + } + + data class Header( + val subject: String? = null, + val labels: List + ) : Item(HEADER) { + override val id: Long + get() = Long.MIN_VALUE + + override fun areContentsTheSame(other: Any?): Boolean { + return this == other + } + } + companion object { - private val DIFF_UTIL_ITEM_CALLBACK = object : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: MessageEntity, newItem: MessageEntity) = - oldItem.uid == newItem.uid + private val DIFF_UTIL_ITEM_CALLBACK = object : DiffUtil.ItemCallback() { + override fun areItemsTheSame(oldItem: Item, newItem: Item) = + oldItem.id == newItem.id - override fun areContentsTheSame(oldItem: MessageEntity, newItem: MessageEntity) = - oldItem == newItem + override fun areContentsTheSame(oldItem: Item, newItem: Item) = + oldItem.areContentsTheSame(newItem) } + + @IntDef(HEADER, MESSAGE) + @Retention(AnnotationRetention.SOURCE) + annotation class Type + + const val HEADER = 0 + const val MESSAGE = 1 } } \ No newline at end of file diff --git a/FlowCrypt/src/main/res/layout/item_thread_header.xml b/FlowCrypt/src/main/res/layout/item_thread_header.xml new file mode 100644 index 0000000000..f0ccf8584c --- /dev/null +++ b/FlowCrypt/src/main/res/layout/item_thread_header.xml @@ -0,0 +1,47 @@ + + + + + + + + + \ No newline at end of file From 7428d5f2531264b6762843d6f9d94a54bd4a9d48 Mon Sep 17 00:00:00 2001 From: DenBond7 Date: Tue, 24 Sep 2024 10:42:34 +0300 Subject: [PATCH 067/237] Added handling errors during loading messages for a thread.| #74 --- .../viewmodel/ThreadDetailsViewModel.kt | 180 +++++++++++------- .../fragment/ThreadDetailsFragment.kt | 83 ++++---- .../fragment/base/ListProgressBehaviour.kt | 6 +- .../fragment/base/ProgressBehaviour.kt | 17 +- .../ui/adapter/MessagesInThreadListAdapter.kt | 28 ++- .../res/layout/fragment_thread_details.xml | 4 +- FlowCrypt/src/main/res/layout/status.xml | 14 +- FlowCrypt/src/main/res/values/ids.xml | 3 +- 8 files changed, 186 insertions(+), 149 deletions(-) diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt index 13ca801b46..bcad2c61f0 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/jetpack/viewmodel/ThreadDetailsViewModel.kt @@ -7,20 +7,27 @@ package com.flowcrypt.email.jetpack.viewmodel import android.app.Application import androidx.lifecycle.asFlow +import androidx.lifecycle.viewModelScope import com.flowcrypt.email.api.email.gmail.GmailApiHelper import com.flowcrypt.email.api.email.model.LocalFolder +import com.flowcrypt.email.api.retrofit.response.base.Result import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getInReplyTo import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.getMessageId import com.flowcrypt.email.extensions.com.google.api.services.gmail.model.isDraft import com.flowcrypt.email.extensions.java.lang.printStackTraceIfDebugOnly import com.flowcrypt.email.ui.adapter.GmailApiLabelsListAdapter +import com.flowcrypt.email.util.coroutines.runners.ControlledRunner import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.mapLatest import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.launch /** * @author Denys Bondarenko @@ -33,89 +40,114 @@ class ThreadDetailsViewModel( val messageFlow: Flow = roomDatabase.msgDao().getMessageByIdFlow(threadMessageEntityId).distinctUntilChanged() - val messagesInThreadFlow: Flow> = + private val controlledRunnerForLoadingMessages = ControlledRunner>>() + private val loadMessagesManuallyMutableStateFlow: MutableStateFlow>> = + MutableStateFlow(Result.none()) + private val loadMessagesManuallyStateFlow: StateFlow>> = + loadMessagesManuallyMutableStateFlow.asStateFlow() + + val messagesInThreadFlow: Flow>> = merge( flow { - val threadMessageEntity = - roomDatabase.msgDao().getMsgById(threadMessageEntityId) ?: return@flow - val activeAccount = getActiveAccountSuspend() ?: return@flow - if (threadMessageEntity.threadIdAsHEX.isNullOrEmpty() || !activeAccount.isGoogleSignInAccount) { - emit(listOf()) - } else { - try { - val messagesInThread = GmailApiHelper.loadMessagesInThread( - application, - activeAccount, - threadMessageEntity.threadIdAsHEX - ).toMutableList().apply { - //put drafts in the right position - val drafts = filter { it.isDraft() } - drafts.forEach { draft -> - val inReplyToValue = draft.getInReplyTo() - val inReplyToMessage = firstOrNull { it.getMessageId() == inReplyToValue } - - if (inReplyToMessage != null) { - val inReplyToMessagePosition = indexOf(inReplyToMessage) - if (inReplyToMessagePosition != -1) { - remove(draft) - add(inReplyToMessagePosition + 1, draft) - } - } + emit(Result.loading()) + emit(loadMessagesInternal()) + }, + loadMessagesManuallyStateFlow + ) + + fun loadMessages() { + viewModelScope.launch { + loadMessagesManuallyMutableStateFlow.value = Result.loading() + loadMessagesManuallyMutableStateFlow.value = + controlledRunnerForLoadingMessages.cancelPreviousThenRun { + return@cancelPreviousThenRun loadMessagesInternal() + } + } + } + + private suspend fun loadMessagesInternal(): Result> { + val threadMessageEntity = + roomDatabase.msgDao().getMsgById(threadMessageEntityId) ?: return Result.exception( + IllegalStateException() + ) + val activeAccount = + getActiveAccountSuspend() ?: return Result.exception(IllegalStateException()) + if (threadMessageEntity.threadIdAsHEX.isNullOrEmpty() || !activeAccount.isGoogleSignInAccount) { + return Result.success(listOf()) + } else { + try { + val messagesInThread = GmailApiHelper.loadMessagesInThread( + getApplication(), + activeAccount, + threadMessageEntity.threadIdAsHEX + ).toMutableList().apply { + //put drafts in the right position + val drafts = filter { it.isDraft() } + drafts.forEach { draft -> + val inReplyToValue = draft.getInReplyTo() + val inReplyToMessage = firstOrNull { it.getMessageId() == inReplyToValue } + + if (inReplyToMessage != null) { + val inReplyToMessagePosition = indexOf(inReplyToMessage) + if (inReplyToMessagePosition != -1) { + remove(draft) + add(inReplyToMessagePosition + 1, draft) } } + } + } - roomDatabase.msgDao() - .updateSuspend(threadMessageEntity.copy(threadMessagesCount = messagesInThread.size)) + roomDatabase.msgDao() + .updateSuspend(threadMessageEntity.copy(threadMessagesCount = messagesInThread.size)) - val isOnlyPgpModeEnabled = activeAccount.showOnlyEncrypted ?: false - val messageEntities = MessageEntity.genMessageEntities( - context = getApplication(), - account = activeAccount.email, - accountType = activeAccount.accountType, - label = GmailApiHelper.LABEL_INBOX, //fix me - msgsList = messagesInThread, - isNew = false, - onlyPgpModeEnabled = isOnlyPgpModeEnabled, - draftIdsMap = emptyMap() - ) { message, messageEntity -> - messageEntity.copy(snippet = message.snippet, isVisible = false) - } + val isOnlyPgpModeEnabled = activeAccount.showOnlyEncrypted ?: false + val messageEntities = MessageEntity.genMessageEntities( + context = getApplication(), + account = activeAccount.email, + accountType = activeAccount.accountType, + label = GmailApiHelper.LABEL_INBOX, //fix me + msgsList = messagesInThread, + isNew = false, + onlyPgpModeEnabled = isOnlyPgpModeEnabled, + draftIdsMap = emptyMap() + ) { message, messageEntity -> + messageEntity.copy(snippet = message.snippet, isVisible = false) + } - roomDatabase.msgDao().clearCacheForGmailThread( - account = activeAccount.email, - folder = GmailApiHelper.LABEL_INBOX, //fix me - threadId = threadMessageEntity.threadIdAsHEX - ) - - roomDatabase.msgDao().insertWithReplaceSuspend(messageEntities) - GmailApiHelper.identifyAttachments( - msgEntities = messageEntities, - msgs = messagesInThread, - account = activeAccount, - localFolder = LocalFolder(activeAccount.email, GmailApiHelper.LABEL_INBOX),//fix me - roomDatabase = roomDatabase - ) - - val cachedEntities = roomDatabase.msgDao().getMessagesForGmailThread( - activeAccount.email, - GmailApiHelper.LABEL_INBOX,//fix me - threadMessageEntity.threadId ?: 0, - ) - - val finalList = messageEntities.map { fromServerMessageEntity -> - fromServerMessageEntity.copy(id = cachedEntities.firstOrNull { - it.uid == fromServerMessageEntity.uid - }?.id) - } + roomDatabase.msgDao().clearCacheForGmailThread( + account = activeAccount.email, + folder = GmailApiHelper.LABEL_INBOX, //fix me + threadId = threadMessageEntity.threadIdAsHEX + ) - emit(finalList) - } catch (e: Exception) { - e.printStackTraceIfDebugOnly() - emit(listOf(threadMessageEntity)) - } + roomDatabase.msgDao().insertWithReplaceSuspend(messageEntities) + GmailApiHelper.identifyAttachments( + msgEntities = messageEntities, + msgs = messagesInThread, + account = activeAccount, + localFolder = LocalFolder(activeAccount.email, GmailApiHelper.LABEL_INBOX),//fix me + roomDatabase = roomDatabase + ) + + val cachedEntities = roomDatabase.msgDao().getMessagesForGmailThread( + activeAccount.email, + GmailApiHelper.LABEL_INBOX,//fix me + threadMessageEntity.threadId ?: 0, + ) + + val finalList = messageEntities.map { fromServerMessageEntity -> + fromServerMessageEntity.copy(id = cachedEntities.firstOrNull { + it.uid == fromServerMessageEntity.uid + }?.id) } - }, - ) + + return Result.success(finalList) + } catch (e: Exception) { + e.printStackTraceIfDebugOnly() + return Result.exception(e) + } + } + } @OptIn(ExperimentalCoroutinesApi::class) val messageGmailApiLabelsFlow: Flow> = diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt index 6e4f10ada1..f253660eac 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/ThreadDetailsFragment.kt @@ -11,26 +11,22 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModel -import androidx.lifecycle.lifecycleScope -import androidx.navigation.NavDirections import androidx.navigation.fragment.navArgs import androidx.recyclerview.widget.LinearLayoutManager -import com.flowcrypt.email.R -import com.flowcrypt.email.api.email.gmail.GmailApiHelper -import com.flowcrypt.email.api.email.model.LocalFolder -import com.flowcrypt.email.database.FlowCryptRoomDatabase +import com.flowcrypt.email.api.retrofit.response.base.Result import com.flowcrypt.email.database.entity.MessageEntity import com.flowcrypt.email.databinding.FragmentThreadDetailsBinding import com.flowcrypt.email.extensions.androidx.fragment.app.launchAndRepeatWithViewLifecycle import com.flowcrypt.email.extensions.androidx.fragment.app.navController import com.flowcrypt.email.extensions.androidx.fragment.app.supportActionBar +import com.flowcrypt.email.extensions.androidx.fragment.app.toast +import com.flowcrypt.email.extensions.exceptionMsg import com.flowcrypt.email.jetpack.lifecycle.CustomAndroidViewModelFactory import com.flowcrypt.email.jetpack.viewmodel.ThreadDetailsViewModel import com.flowcrypt.email.ui.activity.fragment.base.BaseFragment import com.flowcrypt.email.ui.activity.fragment.base.ProgressBehaviour import com.flowcrypt.email.ui.adapter.MessagesInThreadListAdapter import com.google.android.material.divider.MaterialDividerItemDecoration -import kotlinx.coroutines.launch /** * @author Denys Bondarenko @@ -58,38 +54,13 @@ class ThreadDetailsFragment : BaseFragment(), Prog } } - private val messagesInThreadListAdapter = - MessagesInThreadListAdapter(object : MessagesInThreadListAdapter.OnMessageClickListener { + private val messagesInThreadListAdapter = MessagesInThreadListAdapter( + object : MessagesInThreadListAdapter.OnMessageClickListener { override fun onMessageClick(messageEntity: MessageEntity) { - lifecycleScope.launch { - FlowCryptRoomDatabase.getDatabase(requireContext()).msgDao() - .getMsgSuspend( - messageEntity.account, - messageEntity.folder, - messageEntity.uid - )?.id?.let { - navController?.navigate( - object : NavDirections { - override val actionId = R.id.viewPagerMessageDetailsFragment - override val arguments = ViewPagerMessageDetailsFragmentArgs( - messageEntityId = it, - localFolder = LocalFolder( - messageEntity.account, - GmailApiHelper.LABEL_INBOX - ),//fix me, - sortedEntityIdListForThread = getSortedEntityIdListForThread() - ).toBundle() - } - ) - } - } + toast(messageEntity.uidAsHEX) } }) - private fun getSortedEntityIdListForThread(): LongArray { - return messagesInThreadListAdapter.currentList.mapNotNull { it.id }.toLongArray() - } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) updateActionBar() @@ -104,26 +75,36 @@ class ThreadDetailsFragment : BaseFragment(), Prog } private fun setupThreadDetailsViewModel() { - launchAndRepeatWithViewLifecycle { - threadDetailsViewModel.messageFlow.collect { - //binding?.textViewSubject?.text = it?.subject - } - } - launchAndRepeatWithViewLifecycle { threadDetailsViewModel.messagesInThreadFlow.collect { - messagesInThreadListAdapter.submitList(it.map { messageEntity -> - MessagesInThreadListAdapter.Message( - messageEntity - ) - }) - showContent() - } - } + when (it.status) { + Result.Status.LOADING -> { + showProgress() + } - launchAndRepeatWithViewLifecycle { - threadDetailsViewModel.messageGmailApiLabelsFlow.collect { + Result.Status.SUCCESS -> { + val data = it.data + if (data.isNullOrEmpty()) { + navController?.navigateUp() + toast("Fix me") + } else { + messagesInThreadListAdapter.submitList(data.map { messageEntity -> + MessagesInThreadListAdapter.Message( + messageEntity + ) + }) + showContent() + } + } + Result.Status.EXCEPTION -> { + showStatus(it.exceptionMsg) { + threadDetailsViewModel.loadMessages() + } + } + + else -> {} + } } } } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ListProgressBehaviour.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ListProgressBehaviour.kt index a4ad76b785..b2ec1e43f4 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ListProgressBehaviour.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ListProgressBehaviour.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui.activity.fragment.base @@ -29,9 +29,9 @@ interface ListProgressBehaviour : ProgressBehaviour { super.showContent() } - override fun showStatus(msg: String?, resourcesId: Int) { + override fun showStatus(msg: String?, resourcesId: Int, action: (() -> Unit)?) { emptyView?.visibility = View.GONE - super.showStatus(msg, resourcesId) + super.showStatus(msg, resourcesId, action) } fun showEmptyView(msg: String? = null, imageResourcesId: Int = 0) { diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ProgressBehaviour.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ProgressBehaviour.kt index 53a54bc49f..886a88dde8 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ProgressBehaviour.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/activity/fragment/base/ProgressBehaviour.kt @@ -1,6 +1,6 @@ /* * © 2016-present FlowCrypt a.s. Limitations apply. Contact human@flowcrypt.com - * Contributors: DenBond7 + * Contributors: denbond7 */ package com.flowcrypt.email.ui.activity.fragment.base @@ -40,7 +40,11 @@ interface ProgressBehaviour { contentView?.visible() } - fun showStatus(msg: String? = null, resourcesId: Int = R.drawable.ic_warning_red_24dp) { + fun showStatus( + msg: String? = null, + resourcesId: Int = R.drawable.ic_warning_red_24dp, + action: (() -> Unit)? = null + ) { goneProgressView() contentView?.gone() @@ -52,6 +56,15 @@ interface ProgressBehaviour { iVStatusImg?.setImageResource(resourcesId) } + if (action != null) { + statusView?.findViewById(R.id.buttonRetry)?.apply { + visible() + setOnClickListener { + action.invoke() + } + } + } + statusView?.visible() } diff --git a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt index c16473438e..248f59d2a6 100644 --- a/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt +++ b/FlowCrypt/src/main/java/com/flowcrypt/email/ui/adapter/MessagesInThreadListAdapter.kt @@ -14,7 +14,6 @@ import android.text.style.ForegroundColorSpan import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.annotation.IntDef import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView @@ -43,18 +42,18 @@ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageC override fun getItemViewType(position: Int): Int { return when (position) { - 0 -> HEADER - else -> MESSAGE + 0 -> Type.HEADER.id + else -> Type.MESSAGE.id } } override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BaseViewHolder { return when (viewType) { - HEADER -> HeaderViewHolder( + Type.HEADER.id -> HeaderViewHolder( LayoutInflater.from(parent.context).inflate(R.layout.item_thread_header, parent, false) ) - MESSAGE -> MessageViewHolder( + Type.MESSAGE.id -> MessageViewHolder( LayoutInflater.from(parent.context).inflate(R.layout.item_message_in_thread, parent, false) ) @@ -64,12 +63,12 @@ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageC override fun onBindViewHolder(holder: BaseViewHolder, position: Int) { when (holder.itemViewType) { - HEADER -> { + Type.HEADER.id -> { val header = (getItem(position) as? Header) ?: return (holder as? HeaderViewHolder)?.bindTo(header) } - MESSAGE -> { + Type.MESSAGE.id -> { val message = (getItem(position) as? Message) ?: return (holder as? MessageViewHolder)?.bindTo(message.messageEntity, onMessageClickListener) } @@ -180,12 +179,12 @@ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageC } } - abstract class Item(val type: Int) { + abstract class Item(val type: Type) { abstract val id: Long abstract fun areContentsTheSame(other: Any?): Boolean } - data class Message(val messageEntity: MessageEntity) : Item(MESSAGE) { + data class Message(val messageEntity: MessageEntity) : Item(Type.MESSAGE) { override val id: Long get() = messageEntity.uid @@ -197,7 +196,7 @@ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageC data class Header( val subject: String? = null, val labels: List - ) : Item(HEADER) { + ) : Item(Type.HEADER) { override val id: Long get() = Long.MIN_VALUE @@ -215,11 +214,8 @@ class MessagesInThreadListAdapter(private val onMessageClickListener: OnMessageC oldItem.areContentsTheSame(newItem) } - @IntDef(HEADER, MESSAGE) - @Retention(AnnotationRetention.SOURCE) - annotation class Type - - const val HEADER = 0 - const val MESSAGE = 1 + enum class Type(val id: Int) { + HEADER(0), MESSAGE(1) + } } } \ No newline at end of file diff --git a/FlowCrypt/src/main/res/layout/fragment_thread_details.xml b/FlowCrypt/src/main/res/layout/fragment_thread_details.xml index dacd245eb9..b7d8af19dc 100644 --- a/FlowCrypt/src/main/res/layout/fragment_thread_details.xml +++ b/FlowCrypt/src/main/res/layout/fragment_thread_details.xml @@ -36,11 +36,13 @@ android:id="@+id/recyclerViewMessages" android:layout_width="match_parent" android:layout_height="0dp" + android:visibility="gone" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" tools:itemCount="8" - tools:listitem="@layout/item_message_in_thread" /> + tools:listitem="@layout/item_message_in_thread" + tools:visibility="visible"> diff --git a/FlowCrypt/src/main/res/layout/status.xml b/FlowCrypt/src/main/res/layout/status.xml index 7f19c1b10e..4f0dcc0c9f 100644 --- a/FlowCrypt/src/main/res/layout/status.xml +++ b/FlowCrypt/src/main/res/layout/status.xml @@ -1,6 +1,6 @@ + +