diff --git a/app/build.gradle b/app/build.gradle index bac4966..dd18302 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,12 +66,12 @@ android { compileOptions { coreLibraryDesugaringEnabled true - sourceCompatibility JavaVersion.VERSION_11 - targetCompatibility JavaVersion.VERSION_11 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_11.toString() + jvmTarget = JavaVersion.VERSION_17.toString() freeCompilerArgs += [ "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ObsoleteCoroutinesApi", @@ -105,9 +105,18 @@ android { } } +repositories { + google() + mavenCentral() + flatDir { + dirs 'aars' + } +} + dependencies { // Twilio - implementation "com.twilio:conversations-android-with-symbols:6.0.3" +// implementation files('aars/convo-android-release.aar') + implementation "com.twilio:conversations-android-with-symbols:6.1.1" // or without symbols: // implementation "com.twilio:conversations-android:6.0.3" @@ -202,6 +211,7 @@ tasks.withType(Test).configureEach { tasks.withType(KotlinCompile).configureEach { compilerOptions { - jvmTarget.set(JvmTarget.JVM_11) + jvmTarget.set(JvmTarget.JVM_17) } } + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 499a496..a4ffb85 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -52,14 +52,14 @@ android:name=".ui.ParticipantListActivity" android:theme="@style/AppTheme.NoActionBar" /> - - - - - - + + + + + + + + Unit, - private val onDownloadMedia: (message: MessageListViewItem) -> Unit, + private val onDownloadMedia: (message: MessageListViewItem, media: MessageMediaViewItem) -> Unit, private val onOpenMedia: (location: Uri, mimeType: String) -> Unit, private val onItemLongClick: (messageIndex: Long) -> Unit, private val onReactionClicked: (messageIndex: Long) -> Unit @@ -63,52 +65,6 @@ class MessageListAdapter( val binding = holder.binding val context = binding.root.context - val mediaSize = Formatter.formatShortFileSize(context, message.mediaSize ?: 0) - val mediaUploadedBytes = Formatter.formatShortFileSize(context, message.mediaUploadedBytes ?: 0) - val mediaDownloadedBytes = Formatter.formatShortFileSize(context, message.mediaDownloadedBytes ?: 0) - - val attachmentInfoText = when { - message.sendStatus == SendStatus.ERROR -> context.getString(R.string.err_failed_to_upload_media) - - message.mediaUploading -> context.getString(R.string.attachment_uploading, mediaUploadedBytes) - - message.mediaUploadUri != null || - message.mediaDownloadState == COMPLETED -> context.getString(R.string.attachment_tap_to_open) - - message.mediaDownloadState == NOT_STARTED -> mediaSize - - message.mediaDownloadState == DOWNLOADING -> context.getString( - R.string.attachment_downloading, - mediaDownloadedBytes - ) - - message.mediaDownloadState == ERROR -> context.getString(R.string.err_failed_to_download_media) - - else -> error("Never happens") - } - - val attachmentInfoColor = when { - message.sendStatus == SendStatus.ERROR || - message.mediaDownloadState == ERROR -> ContextCompat.getColor(context, R.color.colorAccent) - - message.mediaUploading -> ContextCompat.getColor(context, R.color.text_subtitle) - - message.mediaUploadUri != null || - message.mediaDownloadState == COMPLETED -> ContextCompat.getColor(context, R.color.colorPrimary) - - else -> ContextCompat.getColor(context, R.color.text_subtitle) - } - - val attachmentOnClickListener = View.OnClickListener { - if (message.mediaDownloadState == COMPLETED && message.mediaUri != null) { - onOpenMedia(message.mediaUri, message.mediaType!!) - } else if (message.mediaUploadUri != null) { - onOpenMedia(message.mediaUploadUri, message.mediaType!!) - } else if (message.mediaDownloadState != DOWNLOADING) { - onDownloadMedia(message) - } - } - val longClickListener = View.OnLongClickListener { onItemLongClick(message.index) return@OnLongClickListener true @@ -125,25 +81,111 @@ class MessageListAdapter( when (binding) { is RowMessageItemIncomingBinding -> { binding.message = message - addReactions(binding.messageReactionHolder, message) - binding.attachmentInfo.text = attachmentInfoText - binding.attachmentInfo.setTextColor(attachmentInfoColor) - binding.attachmentBackground.setOnClickListener(attachmentOnClickListener) - binding.attachmentBackground.setOnLongClickListener(longClickListener) +// addReactions(binding.messageReactionHolder, message) + updateAttachments(binding.attachmentsContainer, message) + binding.attachmentsContainer.setOnLongClickListener(longClickListener) } is RowMessageItemOutgoingBinding -> { binding.message = message - addReactions(binding.messageReactionHolder, message) - binding.attachmentInfo.text = attachmentInfoText - binding.attachmentInfo.setTextColor(attachmentInfoColor) - binding.attachmentBackground.setOnClickListener(attachmentOnClickListener) - binding.attachmentBackground.setOnLongClickListener(longClickListener) +// addReactions(binding.messageReactionHolder, message) + updateAttachments(binding.attachmentsContainer, message) + binding.attachmentsContainer.setOnLongClickListener(longClickListener) } else -> error("Unknown binding type: $binding") } } + private fun updateAttachments( + attachmentsContainer: android.widget.LinearLayout, + message: MessageListViewItem + ) { + attachmentsContainer.removeAllViews() + val context = attachmentsContainer.context + + if (message.mediaData.isEmpty()) { + attachmentsContainer.visibility = android.view.View.GONE + return + } + attachmentsContainer.visibility = android.view.View.VISIBLE + + message.mediaData.forEach { mediaItem -> + // Inflate a new layout for each attachment + val attachmentBinding = RowMessageMediaItemBinding.inflate( + LayoutInflater.from(context), + attachmentsContainer, + false // Attach manually below + ) + + // Determine text and color for this specific media item + val mediaSize = mediaItem.mediaSize?.let { Formatter.formatShortFileSize(context, it) } + val mediaUploadedBytes = + Formatter.formatShortFileSize(context, mediaItem.mediaUploadedBytes ?: 0) + val mediaDownloadedBytes = + Formatter.formatShortFileSize(context, mediaItem.mediaDownloadedBytes ?: 0) + + attachmentBinding.attachmentFileName.text = mediaItem.mediaFileName ?: "Attachment" + + val attachmentInfoText = when { + message.sendStatus == SendStatus.ERROR -> context.getString(R.string.err_failed_to_upload_media) + mediaItem.mediaUploading -> context.getString( + R.string.attachment_uploading, + mediaUploadedBytes + ) + + mediaItem.mediaUploadUri != null || mediaItem.mediaDownloadState == COMPLETED -> context.getString( + R.string.attachment_tap_to_open + ) + + mediaItem.mediaDownloadState == NOT_STARTED -> mediaSize + mediaItem.mediaDownloadState == DOWNLOADING -> context.getString( + R.string.attachment_downloading, + mediaDownloadedBytes + ) + + mediaItem.mediaDownloadState == ERROR -> context.getString(R.string.err_failed_to_download_media) + else -> "" + } + + val attachmentInfoColor = when { + message.sendStatus == SendStatus.ERROR || mediaItem.mediaDownloadState == ERROR -> + ContextCompat.getColor(context, R.color.colorAccent) + + mediaItem.mediaUploading || mediaItem.mediaDownloadState == DOWNLOADING -> + ContextCompat.getColor(context, R.color.text_subtitle) + + mediaItem.mediaUploadUri != null || mediaItem.mediaDownloadState == COMPLETED -> + ContextCompat.getColor(context, R.color.colorPrimary) + + else -> ContextCompat.getColor(context, R.color.text_subtitle) + } + + attachmentBinding.attachmentInfo.text = attachmentInfoText + attachmentBinding.attachmentInfo.setTextColor(attachmentInfoColor) + + // Set click listener for this specific attachment + attachmentBinding.root.setOnClickListener { + when { + mediaItem.mediaDownloadState == COMPLETED && mediaItem.mediaUri != null -> + mediaItem.mediaType?.let { it1 -> onOpenMedia(mediaItem.mediaUri, it1) } + + mediaItem.mediaUploadUri != null -> + mediaItem.mediaType?.let { it1 -> + onOpenMedia(mediaItem.mediaUploadUri, + it1 + ) + } + + mediaItem.mediaDownloadState != DOWNLOADING && !mediaItem.mediaUploading -> + onDownloadMedia(message, mediaItem) // Pass the specific media item + } + } + + // Add the newly created attachment view to the container + attachmentsContainer.addView(attachmentBinding.root) + } + } + private fun addReactions(rootView: LinearLayout, message: MessageListViewItem) { rootView.setOnClickListener { onReactionClicked(message.index) } rootView.removeAllViews() diff --git a/app/src/main/java/com/twilio/conversations/app/common/DataConverter.kt b/app/src/main/java/com/twilio/conversations/app/common/DataConverter.kt index 2316bff..cf8d8c4 100644 --- a/app/src/main/java/com/twilio/conversations/app/common/DataConverter.kt +++ b/app/src/main/java/com/twilio/conversations/app/common/DataConverter.kt @@ -23,6 +23,7 @@ import com.twilio.conversations.app.common.extensions.asMessageCount import com.twilio.conversations.app.common.extensions.asMessageDateString import com.twilio.conversations.app.common.extensions.firstMedia import com.twilio.conversations.app.data.localCache.entity.ConversationDataItem +import com.twilio.conversations.app.data.localCache.entity.MediaDataItem import com.twilio.conversations.app.data.localCache.entity.MessageDataItem import com.twilio.conversations.app.data.localCache.entity.ParticipantDataItem import com.twilio.conversations.app.data.models.* @@ -49,12 +50,11 @@ fun Conversation.toConversationDataItem(): ConversationDataItem { } fun Message.toMessageDataItem(currentUserIdentity: String = participant.identity, uuid: String = ""): MessageDataItem { - val media = firstMedia // @todo: support multiple media return MessageDataItem( this.sid, this.conversationSid, this.participantSid, - if (media != null) MessageType.MEDIA.value else MessageType.TEXT.value, + if (this.attachedMedia.isNotEmpty()) MessageType.MEDIA.value else MessageType.TEXT.value, this.author, this.dateCreatedAsDate.time, this.body ?: "", @@ -62,15 +62,11 @@ fun Message.toMessageDataItem(currentUserIdentity: String = participant.identity this.attributes.toString(), if (this.author == currentUserIdentity) Direction.OUTGOING.value else Direction.INCOMING.value, if (this.author == currentUserIdentity) SendStatus.SENT.value else SendStatus.UNDEFINED.value, - uuid, - media?.sid, - media?.filename, - media?.contentType, - media?.size + uuid ) } -fun MessageDataItem.toMessageListViewItem(authorChanged: Boolean): MessageListViewItem { +fun MessageDataItem.toMessageListViewItem(authorChanged: Boolean, mediaList: List? = null): MessageListViewItem { return MessageListViewItem( this.sid, this.uuid, @@ -84,17 +80,20 @@ fun MessageDataItem.toMessageListViewItem(authorChanged: Boolean): MessageListVi sendStatusIcon = SendStatus.fromInt(this.sendStatus).asLastMesageStatusIcon(), getReactions(attributes).asReactionList(), MessageType.fromInt(this.type), - this.mediaSid, - this.mediaFileName, - this.mediaType, - this.mediaSize, - this.mediaUri?.toUri(), - this.mediaDownloadId, - this.mediaDownloadedBytes, - DownloadState.fromInt(this.mediaDownloadState), - this.mediaUploading, - this.mediaUploadedBytes, - this.mediaUploadUri?.toUri(), + mediaData= mediaList?.map { media -> MessageMediaViewItem( + media.mediaSid, + media.mediaFileName, + media.mediaType, + media.mediaSize, + media.mediaUri?.toUri(), + media.mediaDownloadId, + media.mediaDownloadedBytes, + DownloadState.fromInt(media.mediaDownloadState), + media.mediaUploading, + media.mediaUploadedBytes, + media.mediaUploadUri?.toUri() + ) + } ?: emptyList(), this.errorCode ) } diff --git a/app/src/main/java/com/twilio/conversations/app/data/localCache/LocalCacheProvider.kt b/app/src/main/java/com/twilio/conversations/app/data/localCache/LocalCacheProvider.kt index 6178858..3c78d46 100644 --- a/app/src/main/java/com/twilio/conversations/app/data/localCache/LocalCacheProvider.kt +++ b/app/src/main/java/com/twilio/conversations/app/data/localCache/LocalCacheProvider.kt @@ -5,13 +5,15 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import com.twilio.conversations.app.data.localCache.dao.ConversationsDao +import com.twilio.conversations.app.data.localCache.dao.MediaDao import com.twilio.conversations.app.data.localCache.dao.MessagesDao import com.twilio.conversations.app.data.localCache.dao.ParticipantsDao import com.twilio.conversations.app.data.localCache.entity.ConversationDataItem +import com.twilio.conversations.app.data.localCache.entity.MediaDataItem import com.twilio.conversations.app.data.localCache.entity.MessageDataItem import com.twilio.conversations.app.data.localCache.entity.ParticipantDataItem -@Database(entities = [ConversationDataItem::class, MessageDataItem::class, ParticipantDataItem::class], version = 1, exportSchema = false) +@Database(entities = [ConversationDataItem::class, MessageDataItem::class, ParticipantDataItem::class, MediaDataItem::class], version = 1, exportSchema = false) abstract class LocalCacheProvider : RoomDatabase() { abstract fun conversationsDao(): ConversationsDao @@ -20,6 +22,8 @@ abstract class LocalCacheProvider : RoomDatabase() { abstract fun participantsDao(): ParticipantsDao + abstract fun mediaDao(): MediaDao + companion object { val INSTANCE get() = _instance ?: error("call LocalCacheProvider.createInstance() first") diff --git a/app/src/main/java/com/twilio/conversations/app/data/localCache/dao/MediaDao.kt b/app/src/main/java/com/twilio/conversations/app/data/localCache/dao/MediaDao.kt new file mode 100644 index 0000000..51873db --- /dev/null +++ b/app/src/main/java/com/twilio/conversations/app/data/localCache/dao/MediaDao.kt @@ -0,0 +1,38 @@ +package com.twilio.conversations.app.data.localCache.dao + +import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy +import androidx.room.Query +import com.twilio.conversations.app.data.localCache.entity.MediaDataItem + + +@Dao +interface MediaDao { + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(mediaList: List) + + @Query("SELECT * FROM media_table WHERE messageSid = :messageSid") + fun getAllMediaByMessageSid(messageSid: String): List? + + @Query("SELECT * FROM media_table WHERE messageUuid = :messageUuid") + fun getAllMediaByMessageUuid(messageUuid: String): List? + + @Query("UPDATE media_table SET mediaDownloadState = :downloadState WHERE mediaSid = :mediaSid") + fun updateMediaDownloadState(mediaSid: String, downloadState: Int) + + @Query("UPDATE media_table SET mediaDownloadedBytes = :downloadedBytes WHERE mediaSid = :mediaSid") + fun updateMediaDownloadedBytes(mediaSid: String, downloadedBytes: Long) + + @Query("UPDATE media_table SET mediaUri = :location WHERE mediaSid = :mediaSid") + fun updateMediaDownloadLocation(mediaSid: String, location: String) + + @Query("UPDATE media_table SET mediaDownloadId = :downloadId WHERE mediaSid = :mediaSid") + fun updateMediaDownloadId(mediaSid: String, downloadId: Long) + + @Query("UPDATE media_table SET mediaUploading = :downloading WHERE mediaSid = :mediaSid") + fun updateMediaUploadStatus(mediaSid: String, downloading: Boolean) + + @Query("UPDATE media_table SET mediaUploadedBytes = :downloadedBytes WHERE mediaSid = :mediaSid") + fun updateMediaUploadedBytes(mediaSid: String, downloadedBytes: Long) +} \ No newline at end of file diff --git a/app/src/main/java/com/twilio/conversations/app/data/localCache/dao/MessagesDao.kt b/app/src/main/java/com/twilio/conversations/app/data/localCache/dao/MessagesDao.kt index b3c4db1..2e15e92 100644 --- a/app/src/main/java/com/twilio/conversations/app/data/localCache/dao/MessagesDao.kt +++ b/app/src/main/java/com/twilio/conversations/app/data/localCache/dao/MessagesDao.kt @@ -41,13 +41,13 @@ interface MessagesDao { fun updateMessageStatus(uuid: String, sendStatus: Int, errorCode: Int) // Update single Message - @Query("UPDATE message_table SET sid = :sid, sendStatus = :sendStatus, `index` = :index, mediaSize = :mediaSize WHERE uuid = :uuid") - fun updateByUuid(sid: String, uuid: String, sendStatus: Int, index: Long, mediaSize: Long?) + @Query("UPDATE message_table SET sid = :sid, sendStatus = :sendStatus, `index` = :index WHERE uuid = :uuid") + fun updateByUuid(sid: String, uuid: String, sendStatus: Int, index: Long) @Transaction fun updateByUuidOrInsert(message: MessageDataItem) { if (message.uuid.isNotEmpty() && getMessageByUuid(message.uuid) != null) { - updateByUuid(message.sid, message.uuid, message.sendStatus, message.index, message.mediaSize) + updateByUuid(message.sid, message.uuid, message.sendStatus, message.index) } else { insertOrReplace(message) } @@ -55,23 +55,4 @@ interface MessagesDao { @Delete fun delete(message: MessageDataItem) - - @Query("UPDATE message_table SET mediaDownloadState = :downloadState WHERE sid = :messageSid") - fun updateMediaDownloadState(messageSid: String, downloadState: Int) - - @Query("UPDATE message_table SET mediaDownloadedBytes = :downloadedBytes WHERE sid = :messageSid") - fun updateMediaDownloadedBytes(messageSid: String, downloadedBytes: Long) - - @Query("UPDATE message_table SET mediaUri = :location WHERE sid = :messageSid") - fun updateMediaDownloadLocation(messageSid: String, location: String) - - @Query("UPDATE message_table SET mediaDownloadId = :downloadId WHERE sid = :messageSid") - fun updateMediaDownloadId(messageSid: String, downloadId: Long) - - @Query("UPDATE message_table SET mediaUploading = :downloading WHERE uuid = :uuid") - fun updateMediaUploadStatus(uuid: String, downloading: Boolean) - - @Query("UPDATE message_table SET mediaUploadedBytes = :downloadedBytes WHERE uuid = :uuid") - fun updateMediaUploadedBytes(uuid: String, downloadedBytes: Long) - } diff --git a/app/src/main/java/com/twilio/conversations/app/data/localCache/entity/MediaDataItem.kt b/app/src/main/java/com/twilio/conversations/app/data/localCache/entity/MediaDataItem.kt new file mode 100644 index 0000000..a176a1d --- /dev/null +++ b/app/src/main/java/com/twilio/conversations/app/data/localCache/entity/MediaDataItem.kt @@ -0,0 +1,60 @@ +package com.twilio.conversations.app.data.localCache.entity + +import androidx.room.Ignore +import java.io.InputStream +import androidx.room.Entity; + +@Entity(tableName = "media_table", primaryKeys = ["mediaSid"]) +data class MediaDataItem( + val messageSid: String? = null, + val mediaSid: String, + val mediaFileName: String? = null, + val mediaType: String? = null, + val mediaSize: Long? = null, + val mediaUri: String? = null, + val mediaDownloadId: Long? = null, + val mediaDownloadedBytes: Long? = null, + val mediaDownloadState: Int = 0, + val mediaUploading: Boolean = false, + val mediaUploadedBytes: Long? = null, + val mediaUploadUri: String? = null, + val messageUuid: String +) { + @Ignore + @Transient + var inputStream: InputStream? = null + + constructor( + messageSid: String, + mediaSid: String, + mediaFileName: String? = null, + mediaType: String? = null, + mediaSize: Long? = null, + mediaUri: String? = null, + mediaDownloadId: Long? = null, + mediaDownloadedBytes: Long? = null, + mediaDownloadState: Int = 0, + mediaUploading: Boolean = false, + mediaUploadedBytes: Long? = null, + mediaUploadUri: String? = null, + inputStream: InputStream, + messageUuid: String + ) : + this( + messageSid, + mediaSid, + mediaFileName, + mediaType, + mediaSize, + mediaUri, + mediaDownloadId, + mediaDownloadedBytes, + mediaDownloadState, + mediaUploading, + mediaUploadedBytes, + mediaUploadUri, + messageUuid + ) { + this.inputStream = inputStream + } +} diff --git a/app/src/main/java/com/twilio/conversations/app/data/localCache/entity/MessageDataItem.kt b/app/src/main/java/com/twilio/conversations/app/data/localCache/entity/MessageDataItem.kt index f070aae..e682b1e 100644 --- a/app/src/main/java/com/twilio/conversations/app/data/localCache/entity/MessageDataItem.kt +++ b/app/src/main/java/com/twilio/conversations/app/data/localCache/entity/MessageDataItem.kt @@ -1,7 +1,5 @@ package com.twilio.conversations.app.data.localCache.entity - import androidx.room.Entity -import androidx.room.PrimaryKey @Entity(tableName = "message_table", primaryKeys = ["sid", "uuid"]) data class MessageDataItem( @@ -17,16 +15,7 @@ data class MessageDataItem( val direction: Int, val sendStatus: Int, val uuid: String, - val mediaSid: String? = null, - val mediaFileName: String? = null, - val mediaType: String? = null, - val mediaSize: Long? = null, - val mediaUri: String? = null, - val mediaDownloadId: Long? = null, - val mediaDownloadedBytes: Long? = null, - val mediaDownloadState: Int = 0, - val mediaUploading: Boolean = false, - val mediaUploadedBytes: Long? = null, - val mediaUploadUri: String? = null, val errorCode: Int = 0 ) + + diff --git a/app/src/main/java/com/twilio/conversations/app/data/localCache/entity/MessageWithMedia.kt b/app/src/main/java/com/twilio/conversations/app/data/localCache/entity/MessageWithMedia.kt new file mode 100644 index 0000000..fba0f8c --- /dev/null +++ b/app/src/main/java/com/twilio/conversations/app/data/localCache/entity/MessageWithMedia.kt @@ -0,0 +1,13 @@ +package com.twilio.conversations.app.data.localCache.entity + +import androidx.room.Embedded +import androidx.room.Relation + +data class MessageWithMedia( + @Embedded val message: MessageDataItem, + @Relation( + parentColumn = "uuid", + entityColumn = "messageUuid" + ) + val media: List? +) \ No newline at end of file diff --git a/app/src/main/java/com/twilio/conversations/app/data/models/MessageListViewItem.kt b/app/src/main/java/com/twilio/conversations/app/data/models/MessageListViewItem.kt index 0849e66..e9c0ff0 100644 --- a/app/src/main/java/com/twilio/conversations/app/data/models/MessageListViewItem.kt +++ b/app/src/main/java/com/twilio/conversations/app/data/models/MessageListViewItem.kt @@ -4,10 +4,23 @@ import android.net.Uri import com.twilio.conversations.app.common.enums.Direction import com.twilio.conversations.app.common.enums.DownloadState import com.twilio.conversations.app.common.enums.MessageType -import com.twilio.conversations.app.common.enums.Reaction import com.twilio.conversations.app.common.enums.Reactions import com.twilio.conversations.app.common.enums.SendStatus +data class MessageMediaViewItem( + val mediaSid: String?, + val mediaFileName: String?, + val mediaType: String?, + val mediaSize: Long?, + val mediaUri: Uri?, + val mediaDownloadId: Long?, + val mediaDownloadedBytes: Long?, + val mediaDownloadState: DownloadState, + val mediaUploading: Boolean, + val mediaUploadedBytes: Long?, + val mediaUploadUri: Uri? +) + data class MessageListViewItem( val sid: String, val uuid: String, @@ -21,16 +34,8 @@ data class MessageListViewItem( val sendStatusIcon: Int, val reactions: Reactions, val type: MessageType, - val mediaSid: String?, - val mediaFileName: String?, - val mediaType: String?, - val mediaSize: Long?, - val mediaUri: Uri?, - val mediaDownloadId: Long?, - val mediaDownloadedBytes: Long?, - val mediaDownloadState: DownloadState, - val mediaUploading: Boolean, - val mediaUploadedBytes: Long?, - val mediaUploadUri: Uri?, + val mediaData: List = emptyList(), val errorCode: Int -) +) { + fun hasMedia() = this.mediaData.isNotEmpty() +} diff --git a/app/src/main/java/com/twilio/conversations/app/manager/MessageListManager.kt b/app/src/main/java/com/twilio/conversations/app/manager/MessageListManager.kt index 799537d..38cd8d0 100644 --- a/app/src/main/java/com/twilio/conversations/app/manager/MessageListManager.kt +++ b/app/src/main/java/com/twilio/conversations/app/manager/MessageListManager.kt @@ -14,6 +14,7 @@ import com.twilio.conversations.app.common.extensions.firstMedia import com.twilio.conversations.app.common.extensions.removeMessage import com.twilio.conversations.app.common.toMessageDataItem import com.twilio.conversations.app.data.ConversationsClientWrapper +import com.twilio.conversations.app.data.localCache.entity.MediaDataItem import com.twilio.conversations.app.data.localCache.entity.MessageDataItem import com.twilio.conversations.app.data.models.ReactionAttributes import com.twilio.conversations.app.repository.ConversationsRepository @@ -30,14 +31,25 @@ import timber.log.Timber import java.io.InputStream import java.util.* +data class MediaInput ( + val uri: String, + val inputStream: InputStream, + val fileName: String?, + val mimeType: String? +) + interface MessageListManager { suspend fun sendTextMessage(text: String, uuid: String) suspend fun retrySendTextMessage(messageUuid: String) - suspend fun sendMediaMessage( - uri: String, - inputStream: InputStream, - fileName: String?, - mimeType: String?, +// suspend fun sendMediaMessage( +// uri: String, +// inputStream: InputStream, +// fileName: String?, +// mimeType: String?, +// messageUuid: String +// ) + suspend fun sendMultipleMediaMessage( + items: List, messageUuid: String ) suspend fun retrySendMediaMessage(inputStream: InputStream, messageUuid: String) @@ -110,73 +122,89 @@ class MessageListManagerImpl( conversationsRepository.updateMessageByUuid(sentMessage) } - override suspend fun sendMediaMessage( - uri: String, - inputStream: InputStream, - fileName: String?, - mimeType: String?, - messageUuid: String - ) { + override suspend fun sendMultipleMediaMessage(items: List, messageUuid: String) { val identity = conversationsClient.getConversationsClient().myIdentity val conversation = conversationsClient.getConversationsClient().getConversation(conversationSid) val participantSid = conversation.getParticipantByIdentity(identity).sid val attributes = Attributes(messageUuid) - val message = MessageDataItem( - "", - conversationSid, - participantSid, - MessageType.MEDIA.value, - identity, - Date().time, - null, - -1, - attributes.toString(), - Direction.OUTGOING.value, - SendStatus.SENDING.value, - messageUuid, - mediaFileName = fileName, - mediaUploadUri = uri, - mediaType = mimeType - ) - conversationsRepository.insertMessage(message) - val sentMessage = conversation.sendMessage { - this.attributes = attributes - addMedia( - inputStream, - mimeType ?: "", - fileName, - createMediaUploadListener(uri, messageUuid) + val newMessage = + MessageDataItem( + "", + conversationSid, + participantSid, + MessageType.MEDIA.value, + identity, + Date().time, + null, + -1, + attributes.toString(), + Direction.OUTGOING.value, + SendStatus.SENDING.value, + messageUuid ) - }.toMessageDataItem(identity, messageUuid) - conversationsRepository.updateMessageByUuid(sentMessage) + conversationsRepository.insertMessage(newMessage) + + val mediaList = items.filterNotNull().map { + MediaDataItem( + "", + "", + mediaFileName = it.fileName, + mediaUploadUri = it.uri, + mediaType = it.mimeType, + inputStream = it.inputStream, + messageUuid = messageUuid + ) + } + + conversationsRepository.insertMedia(mediaList) + + val message = conversation.sendMessage { + mediaList.forEach { + this.attributes = attributes + it.inputStream?.let { it1 -> + addMedia( + it1, + it.mediaType ?: "", + it.mediaFileName, + createMediaUploadListener(it.mediaUploadUri!!, messageUuid) + ) + } + } + + } + + conversationsRepository.updateMessageByUuid(message.toMessageDataItem(identity, messageUuid)) } override suspend fun retrySendMediaMessage( inputStream: InputStream, messageUuid: String ) { - val message = withContext(dispatchers.io()) { conversationsRepository.getMessageByUuid(messageUuid) } ?: return - if (message.sendStatus == SendStatus.SENDING.value) return - if (message.mediaUploadUri == null) { - Timber.w("Missing mediaUploadUri in retrySendMediaMessage: $message") + val messageWithMedia = withContext(dispatchers.io()) { conversationsRepository.getMessageMediaByMessageUuid(messageUuid) } ?: return + if (messageWithMedia.message.sendStatus == SendStatus.SENDING.value) return + if (messageWithMedia.media?.any{ it.mediaUploadUri == null } == true) { + Timber.w("Missing mediaUploadUri in retrySendMediaMessage: ${messageWithMedia.message}") return } - conversationsRepository.updateMessageByUuid(message.copy(sendStatus = SendStatus.SENDING.value)) + conversationsRepository.updateMessageByUuid(messageWithMedia.message.copy(sendStatus = SendStatus.SENDING.value)) val identity = conversationsClient.getConversationsClient().myIdentity val conversation = conversationsClient.getConversationsClient().getConversation(conversationSid) val sentMessage = conversation.sendMessage { this.attributes = Attributes(messageUuid) - addMedia( - inputStream, - message.mediaType ?: "", - message.mediaFileName, - createMediaUploadListener(message.mediaUploadUri, messageUuid) - ) - }.toMessageDataItem(identity, message.uuid) + messageWithMedia.media?.forEach { + addMedia( + inputStream, + it.mediaType ?: "", + it.mediaFileName, + createMediaUploadListener(it.mediaUploadUri!!, messageUuid) + ) + } + + }.toMessageDataItem(identity, messageWithMedia.message.uuid) conversationsRepository.updateMessageByUuid(sentMessage) } @@ -228,12 +256,15 @@ class MessageListManagerImpl( downloadedLocation: String? ) { val message = conversationsClient.getConversationsClient().getConversation(conversationSid).getMessageByIndex(index) - conversationsRepository.updateMessageMediaDownloadStatus( - messageSid = message.sid, - downloadedBytes = downloadedBytes, - downloadLocation = downloadedLocation, - downloadState = downloadState.value - ) + // @todo fix this logic + message.attachedMedia.forEach { + conversationsRepository.updateMessageMediaDownloadStatus( + mediaSid = it.sid, + downloadedBytes = downloadedBytes, + downloadLocation = downloadedLocation, + downloadState = downloadState.value + ) + } } override suspend fun setReactions(index: Long, reactions: Reactions) { @@ -267,7 +298,10 @@ class MessageListManagerImpl( override suspend fun setMessageMediaDownloadId(messageIndex: Long, id: Long) { val message = conversationsClient.getConversationsClient().getConversation(conversationSid).getMessageByIndex(messageIndex) - conversationsRepository.updateMessageMediaDownloadStatus(messageSid = message.sid, downloadId = id) + message.attachedMedia.forEach { + conversationsRepository.updateMessageMediaDownloadStatus(it.sid, downloadId = id) + + } } override suspend fun removeMessage(messageIndex: Long) { diff --git a/app/src/main/java/com/twilio/conversations/app/repository/ConversationsRepository.kt b/app/src/main/java/com/twilio/conversations/app/repository/ConversationsRepository.kt index 6d9d501..96f19e4 100644 --- a/app/src/main/java/com/twilio/conversations/app/repository/ConversationsRepository.kt +++ b/app/src/main/java/com/twilio/conversations/app/repository/ConversationsRepository.kt @@ -22,7 +22,9 @@ import com.twilio.conversations.app.common.toMessageDataItem import com.twilio.conversations.app.data.ConversationsClientWrapper import com.twilio.conversations.app.data.localCache.LocalCacheProvider import com.twilio.conversations.app.data.localCache.entity.ConversationDataItem +import com.twilio.conversations.app.data.localCache.entity.MediaDataItem import com.twilio.conversations.app.data.localCache.entity.MessageDataItem +import com.twilio.conversations.app.data.localCache.entity.MessageWithMedia import com.twilio.conversations.app.data.localCache.entity.ParticipantDataItem import com.twilio.conversations.app.data.models.MessageListViewItem import com.twilio.conversations.app.data.models.RepositoryRequestStatus @@ -64,23 +66,25 @@ interface ConversationsRepository { fun getConversation(conversationSid: String): Flow> fun getSelfUser(): Flow fun getMessageByUuid(messageUuid: String): MessageDataItem? + fun getMessageMediaByMessageUuid(messageUuid: String): MessageWithMedia? // Interim solution till paging v3.0 is available as an alpha version. // It has support for converting PagedList types fun getMessages(conversationSid: String, pageSize: Int): Flow>> fun insertMessage(message: MessageDataItem) + fun insertMedia(mediaList: List) fun updateMessageByUuid(message: MessageDataItem) fun updateMessageStatus(messageUuid: String, sendStatus: Int, errorCode: Int) fun getTypingParticipants(conversationSid: String): Flow> fun getConversationParticipants(conversationSid: String): Flow>> fun updateMessageMediaDownloadStatus( - messageSid: String, + mediaSid: String, downloadId: Long? = null, downloadLocation: String? = null, downloadState: Int? = null, downloadedBytes: Long? = null ) fun updateMessageMediaUploadStatus( - messageUuid: String, + mediaSid: String, uploading: Boolean? = null, uploadedBytes: Long? = null ) @@ -184,6 +188,11 @@ class ConversationsRepositoryImpl( } override fun getMessageByUuid(messageUuid: String) = localCache.messagesDao().getMessageByUuid(messageUuid) + override fun getMessageMediaByMessageUuid(messageUuid: String): MessageWithMedia? { + val media = localCache.mediaDao().getAllMediaByMessageUuid(messageUuid) + val message = localCache.messagesDao().getMessageByUuid(messageUuid) + return message?.let { MessageWithMedia(it, media) } + } override fun getMessages(conversationSid: String, pageSize: Int): Flow>> { Timber.v("getMessages($conversationSid, $pageSize)") @@ -243,6 +252,12 @@ class ConversationsRepositoryImpl( } } + override fun insertMedia(mediaList: List) { + launch { + localCache.mediaDao().insert(mediaList) + } + } + override fun updateMessageByUuid(message: MessageDataItem) { launch { localCache.messagesDao().updateByUuidOrInsert(message) @@ -270,7 +285,7 @@ class ConversationsRepositoryImpl( } override fun updateMessageMediaDownloadStatus( - messageSid: String, + mediaSid: String, downloadId: Long?, downloadLocation: String?, downloadState: Int?, @@ -278,31 +293,31 @@ class ConversationsRepositoryImpl( ) { launch { if (downloadId != null) { - localCache.messagesDao().updateMediaDownloadId(messageSid, downloadId) + localCache.mediaDao().updateMediaDownloadId(mediaSid, downloadId) } if (downloadLocation != null) { - localCache.messagesDao().updateMediaDownloadLocation(messageSid, downloadLocation) + localCache.mediaDao().updateMediaDownloadLocation(mediaSid, downloadLocation) } if (downloadState != null) { - localCache.messagesDao().updateMediaDownloadState(messageSid, downloadState) + localCache.mediaDao().updateMediaDownloadState(mediaSid, downloadState) } if (downloadedBytes != null) { - localCache.messagesDao().updateMediaDownloadedBytes(messageSid, downloadedBytes) + localCache.mediaDao().updateMediaDownloadedBytes(mediaSid, downloadedBytes) } } } override fun updateMessageMediaUploadStatus( - messageUuid: String, + mediaSid: String, uploading: Boolean?, uploadedBytes: Long? ) { launch { if (uploading != null) { - localCache.messagesDao().updateMediaUploadStatus(messageUuid, uploading) + localCache.mediaDao().updateMediaUploadStatus(mediaSid, uploading) } if (uploadedBytes != null) { - localCache.messagesDao().updateMediaUploadedBytes(messageUuid, uploadedBytes) + localCache.mediaDao().updateMediaUploadedBytes(mediaSid, uploadedBytes) } } } diff --git a/app/src/main/java/com/twilio/conversations/app/ui/MessageListActivity.kt b/app/src/main/java/com/twilio/conversations/app/ui/MessageListActivity.kt index cab6555..8bd55d5 100644 --- a/app/src/main/java/com/twilio/conversations/app/ui/MessageListActivity.kt +++ b/app/src/main/java/com/twilio/conversations/app/ui/MessageListActivity.kt @@ -67,9 +67,12 @@ class MessageListActivity : BaseActivity() { Timber.d("Display send error clicked: ${message.uuid}") showSendErrorDialog(message) }, - onDownloadMedia = { message -> + onDownloadMedia = { message, media -> Timber.d("Download clicked: $message") - messageListViewModel.startMessageMediaDownload(message.index, message.mediaFileName) + media.mediaSid?.let { + messageListViewModel.startMessageMediaDownload(message.index, + it, media.mediaFileName) + } }, onOpenMedia = { uri, mimeType -> Timber.d("Open clicked") @@ -207,13 +210,16 @@ class MessageListActivity : BaseActivity() { if (message.type == MessageType.TEXT) { messageListViewModel.resendTextMessage(message.uuid) } else if (message.type == MessageType.MEDIA) { - val fileInputStream = message.mediaUploadUri?.let { contentResolver.openInputStream(it) } - if (fileInputStream != null) { - messageListViewModel.resendMediaMessage(fileInputStream, message.uuid) - } else { - Timber.w("Could not get input stream for file reading: ${message.mediaUploadUri}") - showToast(R.string.err_failed_to_resend_media) + message.mediaData?.forEach { media -> + val fileInputStream = media.mediaUploadUri?.let { contentResolver.openInputStream(it) } + if (fileInputStream != null) { + messageListViewModel.resendMediaMessage(fileInputStream, message.uuid) + } else { + Timber.w("Could not get input stream for file reading: ${media.mediaUploadUri}") + showToast(R.string.err_failed_to_resend_media) + } } + } } diff --git a/app/src/main/java/com/twilio/conversations/app/ui/dialogs/AttachFileDialog.kt b/app/src/main/java/com/twilio/conversations/app/ui/dialogs/AttachFileDialog.kt index 381315b..43abef6 100644 --- a/app/src/main/java/com/twilio/conversations/app/ui/dialogs/AttachFileDialog.kt +++ b/app/src/main/java/com/twilio/conversations/app/ui/dialogs/AttachFileDialog.kt @@ -8,7 +8,7 @@ import android.provider.OpenableColumns import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import androidx.activity.result.contract.ActivityResultContracts.OpenDocument +import androidx.activity.result.contract.ActivityResultContracts.OpenMultipleDocuments import androidx.activity.result.contract.ActivityResultContracts.TakePicture import androidx.core.content.FileProvider import com.twilio.conversations.app.common.enums.ConversationsError @@ -18,6 +18,7 @@ import com.twilio.conversations.app.common.extensions.lazyActivityViewModel import com.twilio.conversations.app.common.extensions.parcelable import com.twilio.conversations.app.common.injector import com.twilio.conversations.app.databinding.DialogAttachFileBinding +import com.twilio.conversations.app.manager.MediaInput import timber.log.Timber import java.io.File import java.text.SimpleDateFormat @@ -39,8 +40,10 @@ class AttachFileDialog : BaseBottomSheetDialogFragment() { dismiss() } - private val openDocument = registerForActivityResult(OpenDocument()) { uri: Uri? -> - uri?.let { sendMediaMessage(it) } + private val openMultipleDocuments = registerForActivityResult(OpenMultipleDocuments()) { uriList: List? -> + if (uriList != null) { + sendMultipleMediaMessage(uriList) + } dismiss() } @@ -66,7 +69,7 @@ class AttachFileDialog : BaseBottomSheetDialogFragment() { } binding.fileManager.setOnClickListener { - openDocument.launch(arrayOf("*/*")) + openMultipleDocuments.launch(arrayOf("*/*")) } } @@ -92,13 +95,39 @@ class AttachFileDialog : BaseBottomSheetDialogFragment() { val type = contentResolver.getType(uri) val name = contentResolver.getString(uri, OpenableColumns.DISPLAY_NAME) if (inputStream != null) { - messageListViewModel.sendMediaMessage(uri.toString(), inputStream, name, type) + messageListViewModel.sendMultipleMediaMessage(listOf(MediaInput(uri.toString(), inputStream, name, type))) } else { messageListViewModel.onMessageError.value = ConversationsError.MESSAGE_SEND_FAILED Timber.w("Could not get input stream for file reading: $uri") } } + fun sendMultipleMediaMessage(uriList: List) { + var failed = false + val contentResolver = requireContext().contentResolver + val mediaInput = uriList.map { uri -> + val inputStream = contentResolver.openInputStream(uri) + val type = contentResolver.getType(uri) + val name = contentResolver.getString(uri, OpenableColumns.DISPLAY_NAME) + if (inputStream == null) { + failed = true; + Timber.w("Could not get input stream for file reading: $uri") + null + } else { + MediaInput(uri.toString(), inputStream, name, type) + } + } + if (failed) { + messageListViewModel.onMessageError.value = ConversationsError.MESSAGE_SEND_FAILED + + } + + messageListViewModel.sendMultipleMediaMessage(mediaInput) + + + + } + companion object { diff --git a/app/src/main/java/com/twilio/conversations/app/ui/dialogs/MessageActionsDialog.kt b/app/src/main/java/com/twilio/conversations/app/ui/dialogs/MessageActionsDialog.kt index 27abbf3..b12aa51 100644 --- a/app/src/main/java/com/twilio/conversations/app/ui/dialogs/MessageActionsDialog.kt +++ b/app/src/main/java/com/twilio/conversations/app/ui/dialogs/MessageActionsDialog.kt @@ -67,15 +67,15 @@ class MessageActionsDialog : BaseBottomSheetDialogFragment() { private fun shareMessage(message: MessageListViewItem) { val intent = Intent(Intent.ACTION_SEND) - if (message.type == MEDIA) { - intent.type = message.mediaType - val uri = message.mediaUploadUri ?: message.mediaUri ?: return - intent.putExtra(Intent.EXTRA_STREAM, uri) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - } else { - intent.type = "text/plain" - intent.putExtra(Intent.EXTRA_TEXT, message.body) - } +// if (message.type == MEDIA) { +// intent.type = message.mediaType +// val uri = message.mediaUploadUri ?: message.mediaUri ?: return +// intent.putExtra(Intent.EXTRA_STREAM, uri) +// intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) +// } else { +// intent.type = "text/plain" +// intent.putExtra(Intent.EXTRA_TEXT, message.body) +// } startActivity(Intent.createChooser(intent, null)) } diff --git a/app/src/main/java/com/twilio/conversations/app/viewModel/MessageListViewModel.kt b/app/src/main/java/com/twilio/conversations/app/viewModel/MessageListViewModel.kt index 0d3fd9d..4d8d303 100644 --- a/app/src/main/java/com/twilio/conversations/app/viewModel/MessageListViewModel.kt +++ b/app/src/main/java/com/twilio/conversations/app/viewModel/MessageListViewModel.kt @@ -34,6 +34,7 @@ import com.twilio.conversations.app.common.extensions.queryById import com.twilio.conversations.app.data.localCache.entity.ParticipantDataItem import com.twilio.conversations.app.data.models.MessageListViewItem import com.twilio.conversations.app.data.models.RepositoryRequestStatus +import com.twilio.conversations.app.manager.MediaInput import com.twilio.conversations.app.manager.MessageListManager import com.twilio.conversations.app.repository.ConversationsRepository import com.twilio.util.TwilioException @@ -89,9 +90,11 @@ class MessageListViewModel( private val messagesObserver: Observer> = Observer { list -> list.forEach { message -> - if (message?.mediaDownloadState == DownloadState.DOWNLOADING && message.mediaDownloadId != null) { - if (updateMessageMediaDownloadState(message.index, message.mediaDownloadId)) { - observeMessageMediaDownload(message.index, message.mediaDownloadId) + message.mediaData.forEach { media -> + if (media.mediaDownloadState == DownloadState.DOWNLOADING && media.mediaDownloadId != null) { + if (updateMessageMediaDownloadState(message.index, media.mediaDownloadId)) { + observeMessageMediaDownload(message.index, media.mediaDownloadId) + } } } } @@ -144,11 +147,25 @@ class MessageListViewModel( } } - fun sendMediaMessage(uri: String, inputStream: InputStream, fileName: String?, mimeType: String?) = +// fun sendMediaMessage(uri: String, inputStream: InputStream, fileName: String?, mimeType: String?) = +// viewModelScope.launch { +// val messageUuid = UUID.randomUUID().toString() +// try { +// messageListManager.sendMediaMessage(uri, inputStream, fileName, mimeType, messageUuid) +// onMessageSent.call() +// Timber.d("Media message sent: $messageUuid") +// } catch (e: TwilioException) { +// Timber.d("Media message send error: ${e.errorInfo.status}:${e.errorInfo.code} ${e.errorInfo.message}") +// messageListManager.updateMessageStatus(messageUuid, SendStatus.ERROR, e.errorInfo.code) +// onMessageError.value = ConversationsError.MESSAGE_SEND_FAILED +// } +// } + + fun sendMultipleMediaMessage(items: List) = viewModelScope.launch { val messageUuid = UUID.randomUUID().toString() try { - messageListManager.sendMediaMessage(uri, inputStream, fileName, mimeType, messageUuid) + messageListManager.sendMultipleMediaMessage(items, messageUuid) onMessageSent.call() Timber.d("Media message sent: $messageUuid") } catch (e: TwilioException) { @@ -227,7 +244,7 @@ class MessageListViewModel( ) } - fun startMessageMediaDownload(messageIndex: Long, fileName: String?) = viewModelScope.launch { + fun startMessageMediaDownload(messageIndex: Long, mediaSid: String, fileName: String?) = viewModelScope.launch { Timber.d("Start file download for message index $messageIndex") updateMessageMediaDownloadStatus(messageIndex, DownloadState.DOWNLOADING) diff --git a/app/src/main/res/layout/row_message_item_incoming.xml b/app/src/main/res/layout/row_message_item_incoming.xml index f17c764..4742796 100644 --- a/app/src/main/res/layout/row_message_item_incoming.xml +++ b/app/src/main/res/layout/row_message_item_incoming.xml @@ -12,6 +12,8 @@ + + @@ -22,168 +24,117 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_marginHorizontal="16dp" - android:focusableInTouchMode="false" - tools:background="@color/white"> + android:focusableInTouchMode="false"> - + tools:visibility="gone" /> + app:layout_constraintTop_toBottomOf="@id/message_header" + tools:visibility="visible" /> + app:layout_constraintStart_toStartOf="@+id/message_background_barrier" + app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginStart="8dp" /> + + + - - - - - - - - - - - - - + android:orientation="vertical" + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@id/message_date" + app:layout_constraintEnd_toEndOf="@id/message_body" + app:layout_constraintStart_toStartOf="@id/message_body" + app:layout_constraintTop_toBottomOf="@id/message_body" + tools:visibility="gone" /> + + + + + @@ -217,15 +180,11 @@ android:id="@+id/attachment_group" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:visibility="@{message.type == MessageType.MEDIA ? View.VISIBLE : View.GONE}" - app:constraint_referenced_ids=" - attachment_info, - attachment_icon, - attachment_file_name, - attachment_background, - attachment_footer" + app:constraint_referenced_ids="attachments_container" tools:visibility="visible" /> + + diff --git a/app/src/main/res/layout/row_message_item_outgoing.xml b/app/src/main/res/layout/row_message_item_outgoing.xml index 21c8942..f5104fb 100644 --- a/app/src/main/res/layout/row_message_item_outgoing.xml +++ b/app/src/main/res/layout/row_message_item_outgoing.xml @@ -50,12 +50,14 @@ android:id="@+id/message_background" android:layout_width="0dp" android:layout_height="0dp" - android:layout_marginTop="4dp" + android:layout_marginStart="16dp" android:background="@drawable/bg_message_outgoing" app:layout_constraintBottom_toBottomOf="@+id/message_footer" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="@+id/message_background_barrier" - app:layout_constraintTop_toTopOf="@+id/message_avatar" /> + app:layout_constraintTop_toTopOf="parent" + app:layout_goneMarginStart="8dp" /> + + attachments_container" /> - - - - - - - - - - - - - @@ -225,12 +151,12 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="4dp" - android:visibility="@{message.sendStatus == SendStatus.ERROR ? View.VISIBLE : View.GONE}" android:text="@string/message_send_error" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintHorizontal_bias="1" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toBottomOf="@+id/message_background" /> + android:visibility="@{message.sendStatus == SendStatus.ERROR ? View.VISIBLE : View.GONE}" + app:layout_constraintEnd_toEndOf="@+id/message_background" + app:layout_constraintTop_toBottomOf="@+id/message_background" + tools:visibility="visible" /> + + app:constraint_referenced_ids="attachments_container" + tools:visibility="visible" /> + + diff --git a/app/src/main/res/layout/row_message_media_item.xml b/app/src/main/res/layout/row_message_media_item.xml new file mode 100644 index 0000000..13e9eab --- /dev/null +++ b/app/src/main/res/layout/row_message_media_item.xml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build.gradle b/build.gradle index 028849c..6dbd767 100644 --- a/build.gradle +++ b/build.gradle @@ -1,10 +1,10 @@ wrapper { - gradleVersion = '8.1.1' + gradleVersion = '8.5' distributionType = Wrapper.DistributionType.ALL } buildscript { - ext.kotlin_version = '1.9.20' + ext.kotlin_version = '1.9.21' ext.kotlinx_coroutines_version = '1.7.3' ext.mockk_version = '1.12.2' ext.paging_version = '2.1.2' @@ -16,7 +16,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:8.1.4' + classpath 'com.android.tools.build:gradle:8.1.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.4.0' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.9.9' @@ -27,9 +27,13 @@ allprojects { repositories { google() mavenCentral() + flatDir { + dirs 'aars' + } } } task clean(type: Delete) { delete rootProject.buildDir } + diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 8707e8b..a595206 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.1.1-all.zip -networkTimeout=10000 +distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists