diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt index fcf2fcfb9..83b2fc241 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ChatMessageModel.kt @@ -24,7 +24,6 @@ import android.text.SpannableStringBuilder import android.text.Spanned import androidx.annotation.UiThread import androidx.annotation.WorkerThread -import androidx.core.text.set import androidx.lifecycle.MutableLiveData import java.util.regex.Pattern import org.linphone.LinphoneApplication.Companion.coreContext @@ -32,6 +31,7 @@ import org.linphone.core.Address import org.linphone.core.ChatMessage import org.linphone.core.ChatMessageListenerStub import org.linphone.core.ChatMessageReaction +import org.linphone.core.Content import org.linphone.core.tools.Log import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.utils.Event @@ -65,6 +65,8 @@ class ChatMessageModel @WorkerThread constructor( val text = MutableLiveData() + val bigImagePath = MutableLiveData() + val timestamp = chatMessage.time val time = TimestampUtils.toString(timestamp) @@ -103,88 +105,40 @@ class ChatMessageModel @WorkerThread constructor( statusIcon.postValue(LinphoneUtils.getChatIconResId(chatMessage.state)) updateReactionsList() - var textFound = false - for (content in chatMessage.contents) { + var displayableContentFound = false + val contents = chatMessage.contents + for (content in contents) { if (content.isText) { - val textContent = content.utf8Text.orEmpty().trim() - val spannableBuilder = SpannableStringBuilder(textContent) - - // Check for mentions - val chatRoom = chatMessage.chatRoom - val matcher = Pattern.compile(MENTION_REGEXP).matcher(textContent) - while (matcher.find()) { - val start = matcher.start() - val end = matcher.end() - val source = textContent.subSequence(start + 1, end) // +1 to remove @ - Log.i("$TAG Found mention [$source]") - - // Find address matching username - val address = if (chatRoom.localAddress.username == source) { - Log.i("$TAG mention found in local address") - coreContext.core.accountList.find { - it.params.identityAddress?.username == source - }?.params?.identityAddress - } else if (chatRoom.peerAddress.username == source) { - Log.i("$TAG mention found in peer address") - chatRoom.peerAddress - } else { - Log.i("$TAG looking for mention in participants") - chatRoom.participants.find { - it.address.username == source - }?.address - } - // Find display name for address - if (address != null) { - val displayName = coreContext.contactsManager.findDisplayName(address) + computeTextContent(content) + displayableContentFound = true + } else { + if (content.isFile) { + val path = content.filePath ?: "" + if (path.isNotEmpty()) { Log.i( - "$TAG Using display name [$displayName] instead of username [$source]" + "$TAG Found file ready to be displayed [$path] with MIME [${content.type}/${content.subtype}] for message [${chatMessage.messageId}]" ) - spannableBuilder.replace(start, end, "@$displayName") - val span = PatternClickableSpan.StyledClickableSpan( - object : - SpannableClickedListener { - override fun onSpanClicked(text: String) { - Log.i("$TAG Clicked on [$text] span") - } + when (content.type) { + "image", "video" -> { + bigImagePath.postValue(path) + displayableContentFound = true } - ) - spannableBuilder.setSpan( - span, - start, - start + displayName.length + 1, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE - ) + "audio" -> { + } + else -> { + } + } + } else { + Log.i("$TAG Content path is empty : have to download it first") + // TODO: download it } + } else { + Log.i("$TAG Content is not a File") } - - // Add clickable span for SIP URIs - text.postValue( - PatternClickableSpan() - .add( - Pattern.compile( - SIP_URI_REGEXP - ), - object : SpannableClickedListener { - @UiThread - override fun onSpanClicked(text: String) { - coreContext.postOnCoreThread { - Log.i("$TAG Clicked on SIP URI: $text") - val address = coreContext.core.interpretUrl(text) - if (address != null) { - coreContext.startCall(address) - } else { - Log.w("$TAG Failed to parse [$text] as SIP URI") - } - } - } - } - ) - .build(spannableBuilder) - ) - textFound = true } } - if (!textFound) { + + if (!displayableContentFound) { // Temporary workaround to prevent empty bubbles val describe = LinphoneUtils.getTextDescribingMessage(chatMessage) val spannable = Spannable.Factory.getInstance().newSpannable(describe) text.postValue(spannable) @@ -230,4 +184,82 @@ class ChatMessageModel @WorkerThread constructor( Log.i("$TAG Reactions for message [$id] are [$reactionsList]") reactions.postValue(reactionsList) } + + @WorkerThread + private fun computeTextContent(content: Content) { + val textContent = content.utf8Text.orEmpty().trim() + Log.i("$TAG Found text content [$textContent] for message [${chatMessage.messageId}]") + val spannableBuilder = SpannableStringBuilder(textContent) + + // Check for mentions + val chatRoom = chatMessage.chatRoom + val matcher = Pattern.compile(MENTION_REGEXP).matcher(textContent) + while (matcher.find()) { + val start = matcher.start() + val end = matcher.end() + val source = textContent.subSequence(start + 1, end) // +1 to remove @ + Log.i("$TAG Found mention [$source]") + + // Find address matching username + val address = if (chatRoom.localAddress.username == source) { + coreContext.core.accountList.find { + it.params.identityAddress?.username == source + }?.params?.identityAddress + } else if (chatRoom.peerAddress.username == source) { + chatRoom.peerAddress + } else { + chatRoom.participants.find { + it.address.username == source + }?.address + } + // Find display name for address + if (address != null) { + val displayName = coreContext.contactsManager.findDisplayName(address) + Log.i( + "$TAG Using display name [$displayName] instead of username [$source]" + ) + spannableBuilder.replace(start, end, "@$displayName") + val span = PatternClickableSpan.StyledClickableSpan( + object : + SpannableClickedListener { + override fun onSpanClicked(text: String) { + Log.i("$TAG Clicked on [$text] span") + // TODO + } + } + ) + spannableBuilder.setSpan( + span, + start, + start + displayName.length + 1, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE + ) + } + } + + // Add clickable span for SIP URIs + text.postValue( + PatternClickableSpan() + .add( + Pattern.compile( + SIP_URI_REGEXP + ), + object : SpannableClickedListener { + @UiThread + override fun onSpanClicked(text: String) { + coreContext.postOnCoreThread { + Log.i("$TAG Clicked on SIP URI: $text") + val address = coreContext.core.interpretUrl(text) + if (address != null) { + coreContext.startCall(address) + } else { + Log.w("$TAG Failed to parse [$text] as SIP URI") + } + } + } + } + ) + .build(spannableBuilder) + ) + } } diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 0bee54b12..ab506af7c 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -51,6 +51,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope import coil.dispose import coil.load +import coil.request.videoFrameMillis import com.google.android.material.imageview.ShapeableImageView import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.coroutineScope @@ -205,6 +206,40 @@ fun AppCompatTextView.setColor(@ColorRes color: Int) { setTextColor(AppUtils.getColor(color)) } +@UiThread +@BindingAdapter("coilBubble") +fun ImageView.loadImageForChatBubble(file: String?) { + if (!file.isNullOrEmpty()) { + if (FileUtils.isExtensionVideo(file)) { + load(file) { + videoFrameMillis(0) + listener( + onError = { _, result -> + Log.e( + "[Data Binding] [Coil] Error getting preview picture from video? [$file]: ${result.throwable}" + ) + visibility = View.GONE + }, + onSuccess = { _, _ -> + // TODO: Display "play" button above video preview + } + ) + } + } else { + load(file) { + listener( + onError = { _, result -> + Log.e( + "[Data Binding] [Coil] Error getting picture from file [$file]: ${result.throwable}" + ) + visibility = View.GONE + } + ) + } + } + } +} + @UiThread @BindingAdapter("coil") fun ShapeableImageView.loadCircleFileWithCoil(file: String?) { diff --git a/app/src/main/java/org/linphone/utils/FileUtils.kt b/app/src/main/java/org/linphone/utils/FileUtils.kt index d6f8a30f4..87546bd31 100644 --- a/app/src/main/java/org/linphone/utils/FileUtils.kt +++ b/app/src/main/java/org/linphone/utils/FileUtils.kt @@ -253,6 +253,13 @@ class FileUtils { return name } + @AnyThread + fun isExtensionVideo(path: String): Boolean { + val extension = getExtensionFromFileName(path) + val type = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension) + return getMimeType(type) == MimeType.Video + } + @AnyThread private fun getExtensionFromFileName(fileName: String): String { var extension = MimeTypeMap.getFileExtensionFromUrl(fileName) diff --git a/app/src/main/res/layout/chat_bubble_content.xml b/app/src/main/res/layout/chat_bubble_content.xml new file mode 100644 index 000000000..77882c0de --- /dev/null +++ b/app/src/main/res/layout/chat_bubble_content.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_bubble_incoming.xml b/app/src/main/res/layout/chat_bubble_incoming.xml index bdc18a523..25794d7cb 100644 --- a/app/src/main/res/layout/chat_bubble_incoming.xml +++ b/app/src/main/res/layout/chat_bubble_incoming.xml @@ -108,7 +108,7 @@ android:layout_height="wrap_content" app:barrierDirection="end" app:barrierMargin="10dp" - app:constraint_referenced_ids="delivery_status, text_message" /> + app:constraint_referenced_ids="delivery_status, contents" /> - + app:layout_constraintEnd_toEndOf="parent" /> + app:layout_constraintTop_toBottomOf="@id/contents" + app:layout_constraintStart_toStartOf="@id/contents"/> + app:constraint_referenced_ids="date_time, contents" /> - + app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 45b6493ba..da273c91d 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -66,6 +66,7 @@ 12dp 5dp 30sp + 200dp 290dp 425dp