Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -202,6 +211,7 @@ tasks.withType(Test).configureEach {

tasks.withType(KotlinCompile).configureEach {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_11)
jvmTarget.set(JvmTarget.JVM_17)
}
}

16 changes: 8 additions & 8 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -52,14 +52,14 @@
android:name=".ui.ParticipantListActivity"
android:theme="@style/AppTheme.NoActionBar" />

<service
android:name=".services.FCMListenerService"
android:exported="false">
<intent-filter>
<action android:name="com.google.firebase.INSTANCE_ID_EVENT" />
<action android:name="com.google.firebase.MESSAGING_EVENT" />
</intent-filter>
</service>
<!-- <service-->
<!-- android:name=".services.FCMListenerService"-->
<!-- android:exported="false">-->
<!-- <intent-filter>-->
<!-- <action android:name="com.google.firebase.INSTANCE_ID_EVENT" />-->
<!-- <action android:name="com.google.firebase.MESSAGING_EVENT" />-->
<!-- </intent-filter>-->
<!-- </service>-->

<meta-data
android:name="com.google.firebase.messaging.default_notification_icon"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,16 @@ import com.twilio.conversations.app.common.enums.DownloadState.NOT_STARTED
import com.twilio.conversations.app.common.enums.Reaction
import com.twilio.conversations.app.common.enums.SendStatus
import com.twilio.conversations.app.data.models.MessageListViewItem
import com.twilio.conversations.app.data.models.MessageMediaViewItem
import com.twilio.conversations.app.databinding.RowMessageItemIncomingBinding
import com.twilio.conversations.app.databinding.RowMessageItemOutgoingBinding
import com.twilio.conversations.app.databinding.RowMessageMediaItemBinding
import com.twilio.conversations.app.databinding.ViewReactionItemBinding
import timber.log.Timber

class MessageListAdapter(
private val onDisplaySendError: (message: MessageListViewItem) -> 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
Expand Down Expand Up @@ -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
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*
Expand All @@ -49,28 +50,23 @@ 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 ?: "",
this.messageIndex,
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<MediaDataItem>? = null): MessageListViewItem {
return MessageListViewItem(
this.sid,
this.uuid,
Expand All @@ -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
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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")

Expand Down
Original file line number Diff line number Diff line change
@@ -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<MediaDataItem>)

@Query("SELECT * FROM media_table WHERE messageSid = :messageSid")
fun getAllMediaByMessageSid(messageSid: String): List<MediaDataItem>?

@Query("SELECT * FROM media_table WHERE messageUuid = :messageUuid")
fun getAllMediaByMessageUuid(messageUuid: String): List<MediaDataItem>?

@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)
}
Loading