diff --git a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt index a020ae625..bd65cf033 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/fragment/ConversationFragment.kt @@ -115,19 +115,19 @@ class ConversationFragment : GenericFragment() { val layoutManager = LinearLayoutManager(requireContext()) binding.eventsList.layoutManager = layoutManager - viewModel.events.observe(viewLifecycleOwner) { + viewModel.events.observe(viewLifecycleOwner) { items -> val currentCount = adapter.itemCount - adapter.submitList(it) - Log.i("$TAG Events (messages) list updated with [${it.size}] items") - - if (currentCount < it.size) { - binding.eventsList.scrollToPosition(it.size - 1) - } + adapter.submitList(items) + Log.i("$TAG Events (messages) list updated with [${items.size}] items") (view.parent as? ViewGroup)?.doOnPreDraw { startPostponedEnterTransition() sharedViewModel.openSlidingPaneEvent.value = Event(true) } + + if (currentCount < items.size) { + binding.eventsList.scrollToPosition(items.size - 1) + } } val emojisBottomSheetBehavior = BottomSheetBehavior.from(binding.emojiPicker) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt index 3b9fa4c3a..c297d9634 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationViewModel.kt @@ -24,6 +24,7 @@ import androidx.annotation.WorkerThread import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import org.linphone.LinphoneApplication.Companion.coreContext +import org.linphone.R import org.linphone.core.Address import org.linphone.core.ChatRoom import org.linphone.core.ChatRoomListenerStub @@ -34,7 +35,9 @@ import org.linphone.core.tools.Log import org.linphone.ui.main.chat.model.EventLogModel import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.contacts.model.GroupAvatarModel +import org.linphone.utils.AppUtils import org.linphone.utils.Event +import org.linphone.utils.LinphoneUtils class ConversationViewModel @UiThread constructor() : ViewModel() { companion object { @@ -55,6 +58,8 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { val isReadOnly = MutableLiveData() + val composingLabel = MutableLiveData() + val textToSend = MutableLiveData() val chatRoomFoundEvent = MutableLiveData>() @@ -64,6 +69,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { private val avatarsMap = hashMapOf() private val chatRoomListener = object : ChatRoomListenerStub() { + @WorkerThread override fun onChatMessageSending(chatRoom: ChatRoom, eventLog: EventLog) { val message = eventLog.chatMessage Log.i("$TAG Chat message [$message] is being sent") @@ -77,10 +83,45 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { events.postValue(list) } + @WorkerThread override fun onChatMessageSent(chatRoom: ChatRoom, eventLog: EventLog) { val message = eventLog.chatMessage Log.i("$TAG Chat message [$message] has been sent") } + + @WorkerThread + override fun onIsComposingReceived( + chatRoom: ChatRoom, + remoteAddress: Address, + isComposing: Boolean + ) { + Log.i( + "$TAG Remote [${remoteAddress.asStringUriOnly()}] is ${if (isComposing) "composing" else "no longer composing"}" + ) + computeComposingLabel() + } + + @WorkerThread + override fun onChatMessagesReceived(chatRoom: ChatRoom, eventLogs: Array) { + Log.i("$TAG Received [${eventLogs.size}] new message(s)") + chatRoom.markAsRead() + computeComposingLabel() + + val list = arrayListOf() + list.addAll(events.value.orEmpty()) + + for (eventLog in eventLogs) { + val address = if (eventLog.type == EventLog.Type.ConferenceChatMessage) { + eventLog.chatMessage?.fromAddress + } else { + eventLog.participantAddress + } + val avatarModel = getAvatarModelForAddress(address) + list.add(EventLogModel(eventLog, avatarModel)) + } + + events.postValue(list) + } } override fun onCleared() { @@ -147,6 +188,8 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { @WorkerThread private fun configureChatRoom() { + computeComposingLabel() + isGroup.postValue( !chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt()) && chatRoom.hasCapability( ChatRoom.Capabilities.Conference.toInt() @@ -202,20 +245,47 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { return ContactAvatarModel(fakeFriend) } - val key = address.asStringUriOnly() + val clone = address.clone() + clone.clean() + val key = clone.asStringUriOnly() + val foundInMap = if (avatarsMap.keys.contains(key)) avatarsMap[key] else null if (foundInMap != null) return foundInMap - val friend = coreContext.contactsManager.findContactByAddress(address) + val friend = coreContext.contactsManager.findContactByAddress(clone) val avatar = if (friend != null) { ContactAvatarModel(friend) } else { val fakeFriend = coreContext.core.createFriend() - fakeFriend.address = address + fakeFriend.address = clone ContactAvatarModel(fakeFriend) } - avatarsMap[address.asStringUriOnly()] = avatar + avatarsMap[key] = avatar return avatar } + + @WorkerThread + private fun computeComposingLabel() { + var composingFriends = arrayListOf() + var label = "" + for (address in chatRoom.composingAddresses) { + val avatar = getAvatarModelForAddress(address) + val name = avatar.name.value ?: LinphoneUtils.getDisplayName(address) + composingFriends.add(name) + label += "$name, " + } + if (composingFriends.size > 0) { + label = label.dropLast(2) + + val format = AppUtils.getStringWithPlural( + R.plurals.conversation_composing_label, + composingFriends.size, + label + ) + composingLabel.postValue(format) + } else { + composingLabel.postValue("") + } + } } diff --git a/app/src/main/java/org/linphone/utils/AppUtils.kt b/app/src/main/java/org/linphone/utils/AppUtils.kt index b4ca6279c..a46473345 100644 --- a/app/src/main/java/org/linphone/utils/AppUtils.kt +++ b/app/src/main/java/org/linphone/utils/AppUtils.kt @@ -36,6 +36,7 @@ import androidx.annotation.ColorRes import androidx.annotation.DimenRes import androidx.annotation.DrawableRes import androidx.annotation.MainThread +import androidx.annotation.PluralsRes import androidx.annotation.StringRes import androidx.annotation.UiThread import androidx.core.content.ContextCompat @@ -85,6 +86,11 @@ class AppUtils { return coreContext.context.getString(id, args) } + @AnyThread + fun getStringWithPlural(@PluralsRes id: Int, count: Int, value: String): String { + return coreContext.context.resources.getQuantityString(id, count, value) + } + @AnyThread @ColorInt fun getColor(@ColorRes colorId: Int): Int { return ContextCompat.getColor( diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 9be41448a..b1770b330 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -228,12 +228,6 @@ fun AvatarView.loadAccountAvatar(account: AccountModel?) { loadImage( data = uri, onStart = { - // Use initials as placeholder - val initials = account.initials.value.orEmpty() - if (initials != "+") { - avatarInitials = initials - } - if (account.showTrust.value == true) { avatarBorderColor = resources.getColor(R.color.blue_info_500, context.theme) @@ -243,9 +237,12 @@ fun AvatarView.loadAccountAvatar(account: AccountModel?) { avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt() } }, - onSuccess = { _, _ -> - // If loading is successful, remove initials otherwise image won't be visible - avatarInitials = "" + onError = { _, _ -> + // Use initials as fallback + val initials = account.initials.value.orEmpty() + if (initials != "+") { + avatarInitials = initials + } } ) } @@ -253,12 +250,6 @@ fun AvatarView.loadAccountAvatar(account: AccountModel?) { loadImage( data = account.avatar.value, onStart = { - // Use initials as placeholder - val initials = account.initials.value.orEmpty() - if (initials != "+") { - avatarInitials = initials - } - if (account.showTrust.value == true) { avatarBorderColor = resources.getColor(R.color.blue_info_500, context.theme) avatarBorderWidth = AppUtils.getDimension(R.dimen.avatar_trust_border_width).toInt() @@ -266,9 +257,12 @@ fun AvatarView.loadAccountAvatar(account: AccountModel?) { avatarBorderWidth = AppUtils.getDimension(R.dimen.zero).toInt() } }, - onSuccess = { _, _ -> - // If loading is successful, remove initials otherwise image won't be visible - avatarInitials = "" + onError = { _, _ -> + // Use initials as fallback + val initials = account.initials.value.orEmpty() + if (initials != "+") { + avatarInitials = initials + } } ) } @@ -285,12 +279,6 @@ fun AvatarView.loadContactAvatar(contact: ContactAvatarModel?) { loadImage( data = uri, onStart = { - // Use initials as placeholder - val initials = contact.initials - if (initials != "+") { - avatarInitials = initials - } - when (contact.trust.value) { ChatRoom.SecurityLevel.Unsafe -> { avatarBorderColor = @@ -309,12 +297,13 @@ fun AvatarView.loadContactAvatar(contact: ContactAvatarModel?) { } } }, - onSuccess = { _, _ -> - // If loading is successful, remove initials otherwise image won't be visible - avatarInitials = "" - }, onError = { _, result -> Log.e("[Contact Avatar Model] Can't load data: ${result.throwable}") + // Use initials as fallback + val initials = contact.initials + if (initials != "+") { + avatarInitials = initials + } } ) } diff --git a/app/src/main/res/layout/chat_conversation_fragment.xml b/app/src/main/res/layout/chat_conversation_fragment.xml index df957da97..ed26f416c 100644 --- a/app/src/main/res/layout/chat_conversation_fragment.xml +++ b/app/src/main/res/layout/chat_conversation_fragment.xml @@ -154,7 +154,7 @@ android:id="@+id/events_list" android:layout_width="match_parent" android:layout_height="0dp" - android:layout_marginBottom="16dp" + android:layout_marginBottom="5dp" app:layout_constraintTop_toBottomOf="@id/title" app:layout_constraintBottom_toTopOf="@id/composing" /> @@ -164,10 +164,11 @@ android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginStart="10dp" - android:layout_marginBottom="12dp" + android:layout_marginBottom="5dp" + android:text="@{viewModel.composingLabel, default=`John Doe is composing...`}" android:textSize="12sp" android:textColor="@color/gray_main2_400" - android:visibility="gone" + android:visibility="@{viewModel.composingLabel.length() == 0 ? View.GONE : View.VISIBLE}" app:layout_constraintTop_toBottomOf="@id/events_list" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7a60275f8..c84be9c9f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -330,6 +330,10 @@ Create a group conversation No contact for the moment… Say something… + + %s is composing… + %s are composing… + No meeting for the moment… New meeting