diff --git a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt index 93bf866ba..0ea3bc097 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/adapter/ConversationEventAdapter.kt @@ -152,7 +152,9 @@ class ConversationEventAdapter( return if (!oldItem.isEvent && !newItem.isEvent) { val oldData = (oldItem.model as ChatMessageModel) val newData = (newItem.model as ChatMessageModel) - oldData.time == newData.time && oldData.isOutgoing == newData.isOutgoing + oldData.id == newData.id && + oldData.timestamp == newData.timestamp && + oldData.isOutgoing == newData.isOutgoing } else { oldItem.notifyId == newItem.notifyId } @@ -162,8 +164,8 @@ class ConversationEventAdapter( return if (oldItem.isEvent && newItem.isEvent) { true } else if (!oldItem.isEvent && !newItem.isEvent) { - val oldModel = (newItem.model as ChatMessageModel) - val newModel = newItem.model + val oldModel = (oldItem.model as ChatMessageModel) + val newModel = (newItem.model as ChatMessageModel) oldModel.statusIcon.value == newModel.statusIcon.value } else { false 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 5df4f2060..f08b2e9ad 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 @@ -51,6 +51,8 @@ import org.linphone.ui.main.chat.viewmodel.ConversationViewModel import org.linphone.ui.main.fragment.GenericFragment import org.linphone.utils.AppUtils import org.linphone.utils.Event +import org.linphone.utils.hideKeyboard +import org.linphone.utils.showKeyboard @UiThread class ConversationFragment : GenericFragment() { @@ -176,6 +178,21 @@ class ConversationFragment : GenericFragment() { ) findNavController().navigate(action) } + + viewModel.searchFilter.observe(viewLifecycleOwner) { filter -> + viewModel.applyFilter(filter.trim()) + } + + viewModel.focusSearchBarEvent.observe(viewLifecycleOwner) { + it.consume { show -> + if (show) { + // To automatically open keyboard + binding.search.showKeyboard() + } else { + binding.search.hideKeyboard() + } + } + } } private fun showChatMessageLongPressMenu(chatMessageModel: ChatMessageModel) { 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 e295ab11b..34b4b1b49 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 @@ -53,7 +53,9 @@ class ChatMessageModel @WorkerThread constructor( val text = LinphoneUtils.getTextDescribingMessage(chatMessage) - val time = TimestampUtils.toString(chatMessage.time) + val timestamp = chatMessage.time + + val time = TimestampUtils.toString(timestamp) val chatRoomIsReadOnly = chatMessage.chatRoom.isReadOnly diff --git a/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt b/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt index 4a0a37975..876be7714 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/model/ParticipantModel.kt @@ -22,6 +22,5 @@ package org.linphone.ui.main.chat.model import org.linphone.core.Friend import org.linphone.ui.main.contacts.model.ContactAvatarModel -class ParticipantModel(friend: Friend, val isMyselfAdmin: Boolean, val isParticipantAdmin: Boolean) : ContactAvatarModel( - friend -) +class ParticipantModel(friend: Friend, val isMyselfAdmin: Boolean, val isParticipantAdmin: Boolean) : + ContactAvatarModel(friend) diff --git a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt index 6274da003..1c6a501ac 100644 --- a/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt +++ b/app/src/main/java/org/linphone/ui/main/chat/viewmodel/ConversationInfoViewModel.kt @@ -70,17 +70,17 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() { private val chatRoomListener = object : ChatRoomListenerStub() { @WorkerThread override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) { - computeParticipantsList(isGroup.value == true) + computeParticipantsList() } @WorkerThread override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) { - computeParticipantsList(isGroup.value == true) + computeParticipantsList() } @WorkerThread override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) { - computeParticipantsList(isGroup.value == true) + computeParticipantsList() } @WorkerThread @@ -183,8 +183,7 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() { isMyselfAdmin.postValue(chatRoom.me?.isAdmin) - val isGroupChatRoom = !chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt()) && - chatRoom.hasCapability(ChatRoom.Capabilities.Conference.toInt()) + val isGroupChatRoom = isChatRoomAGroup() isGroup.postValue(isGroupChatRoom) val empty = chatRoom.hasCapability(ChatRoom.Capabilities.Conference.toInt()) && chatRoom.participants.isEmpty() @@ -196,14 +195,16 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() { subject.postValue(chatRoom.subject) - computeParticipantsList(isGroupChatRoom) + computeParticipantsList() } @WorkerThread - private fun computeParticipantsList(isGroupChatRoom: Boolean) { + private fun computeParticipantsList() { avatarModel.value?.destroy() avatarsMap.values.forEach(ParticipantModel::destroy) + val groupChatRoom = isChatRoomAGroup() + val friends = arrayListOf() val participantsList = arrayListOf() if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) { @@ -214,14 +215,14 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() { for (participant in chatRoom.participants) { val model = getParticipantModelForAddress( participant.address, - if (isGroup.value == true) participant.isAdmin else false + if (groupChatRoom) participant.isAdmin else false ) friends.add(model.friend) participantsList.add(model) } } - val avatar = if (isGroupChatRoom) { + val avatar = if (groupChatRoom) { val fakeFriend = coreContext.core.createFriend() ContactAvatarModel(fakeFriend) } else { @@ -260,4 +261,13 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() { avatarsMap[key] = avatar return avatar } + + @WorkerThread + private fun isChatRoomAGroup(): Boolean { + return if (::chatRoom.isInitialized) { + LinphoneUtils.isChatRoomAGroup(chatRoom) + } else { + false + } + } } 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 1a57c81c0..ef50c262a 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 @@ -62,10 +62,20 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { val textToSend = MutableLiveData() + val searchBarVisible = MutableLiveData() + + val searchFilter = MutableLiveData() + + val focusSearchBarEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val chatRoomFoundEvent = MutableLiveData>() private lateinit var chatRoom: ChatRoom + private var currentFilter = "" + private val avatarsMap = hashMapOf() private val chatRoomListener = object : ChatRoomListenerStub() { @@ -84,7 +94,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { } else { false } - list.add(EventLogModel(eventLog, avatarModel, isGroup.value == true, group, true)) + list.add(EventLogModel(eventLog, avatarModel, isChatRoomAGroup(), group, true)) events.postValue(list) } @@ -118,7 +128,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { val newList = getEventsListFromHistory( eventLogs, - isGroupChatRoom = isGroup.value == true + searchFilter.value.orEmpty().trim() ) list.addAll(newList) @@ -128,6 +138,10 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { } } + init { + searchBarVisible.value = false + } + override fun onCleared() { super.onCleared() @@ -139,6 +153,24 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { } } + @UiThread + fun openSearchBar() { + searchBarVisible.value = true + focusSearchBarEvent.value = Event(true) + } + + @UiThread + fun closeSearchBar() { + clearFilter() + searchBarVisible.value = false + focusSearchBarEvent.value = Event(false) + } + + @UiThread + fun clearFilter() { + searchFilter.value = "" + } + @UiThread fun findChatRoom(localSipUri: String, remoteSipUri: String) { coreContext.postOnCoreThread { core -> @@ -174,6 +206,13 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { } } + @UiThread + fun applyFilter(filter: String) { + coreContext.postOnCoreThread { + computeEvents(filter) + } + } + @UiThread fun sendMessage() { coreContext.postOnCoreThread { core -> @@ -215,10 +254,6 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { private fun configureChatRoom() { computeComposingLabel() - val isGroupChatRoom = !chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt()) && - chatRoom.hasCapability(ChatRoom.Capabilities.Conference.toInt()) - isGroup.postValue(isGroupChatRoom) - val empty = chatRoom.hasCapability(ChatRoom.Capabilities.Conference.toInt()) && chatRoom.participants.isEmpty() val readOnly = chatRoom.isReadOnly || empty isReadOnly.postValue(readOnly) @@ -226,6 +261,9 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { Log.w("$TAG Chat room with subject [${chatRoom.subject}] is read only!") } + val group = isChatRoomAGroup() + isGroup.postValue(group) + subject.postValue(chatRoom.subject) val friends = arrayListOf() @@ -243,7 +281,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { firstParticipant?.address ?: chatRoom.peerAddress } - val avatar = if (isGroupChatRoom) { + val avatar = if (group) { val fakeFriend = coreContext.core.createFriend() ContactAvatarModel(fakeFriend) } else { @@ -252,17 +290,22 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { avatar.setPicturesFromFriends(friends) avatarModel.postValue(avatar) - val history = chatRoom.getHistoryEvents(0) - val eventsList = getEventsListFromHistory(history, isGroupChatRoom) - - events.postValue(eventsList) + computeEvents() chatRoom.markAsRead() } + @WorkerThread + private fun computeEvents(filter: String = "") { + events.value.orEmpty().forEach(EventLogModel::destroy) + + val history = chatRoom.getHistoryEvents(0) + val eventsList = getEventsListFromHistory(history, filter) + events.postValue(eventsList) + } + @WorkerThread private fun processGroupedEvents( - groupedEventLogs: ArrayList, - isGroupChatRoom: Boolean + groupedEventLogs: ArrayList ): ArrayList { val eventsList = arrayListOf() @@ -273,7 +316,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { val model = EventLogModel( groupedEvent, avatar, - isGroupChatRoom, + isChatRoomAGroup(), index > 0, index == groupedEventLogs.size - 1 ) @@ -286,10 +329,26 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { } @WorkerThread - private fun getEventsListFromHistory(history: Array, isGroupChatRoom: Boolean): ArrayList { + private fun getEventsListFromHistory(history: Array, filter: String = ""): ArrayList { val eventsList = arrayListOf() val groupedEventLogs = arrayListOf() for (event in history) { + // TODO: let the SDK do it + if (event.type == EventLog.Type.ConferenceChatMessage) { + val message = event.chatMessage ?: continue + val fromAddress = message.fromAddress + val model = getAvatarModelForAddress(fromAddress) + if ( + !model.name.value.orEmpty().contains(filter, ignoreCase = true) && + !fromAddress.asStringUriOnly().contains(filter, ignoreCase = true) && + !message.utf8Text.orEmpty().contains(filter, ignoreCase = true) + ) { + continue + } + } else { + continue + } + if (groupedEventLogs.isEmpty()) { groupedEventLogs.add(event) continue @@ -299,7 +358,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { val groupEvents = shouldWeGroupTwoEvents(event, previousGroupEvent) if (!groupEvents) { - eventsList.addAll(processGroupedEvents(groupedEventLogs, isGroupChatRoom)) + eventsList.addAll(processGroupedEvents(groupedEventLogs)) groupedEventLogs.clear() } @@ -307,7 +366,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { } if (groupedEventLogs.isNotEmpty()) { - eventsList.addAll(processGroupedEvents(groupedEventLogs, isGroupChatRoom)) + eventsList.addAll(processGroupedEvents(groupedEventLogs)) groupedEventLogs.clear() } @@ -380,4 +439,13 @@ class ConversationViewModel @UiThread constructor() : ViewModel() { composingLabel.postValue("") } } + + @WorkerThread + private fun isChatRoomAGroup(): Boolean { + return if (::chatRoom.isInitialized) { + LinphoneUtils.isChatRoomAGroup(chatRoom) + } else { + false + } + } } diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index 914ee3d69..eaf7cdd7e 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -199,6 +199,12 @@ class LinphoneUtils { return "${localSipUri.asStringUriOnly()}~${remoteSipUri.asStringUriOnly()}" } + @WorkerThread + fun isChatRoomAGroup(chatRoom: ChatRoom): Boolean { + return !chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt()) && + chatRoom.hasCapability(ChatRoom.Capabilities.Conference.toInt()) + } + @WorkerThread fun getRecordingFilePathForAddress(address: Address): String { val displayName = getDisplayName(address) diff --git a/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml b/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml index e82f7df3c..0dd826c00 100644 --- a/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml +++ b/app/src/main/res/drawable/shape_chat_bubble_incoming_first.xml @@ -1,5 +1,5 @@ - + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml b/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml index df3a98fea..18bb74022 100644 --- a/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml +++ b/app/src/main/res/drawable/shape_chat_bubble_incoming_full.xml @@ -1,5 +1,5 @@ - + \ 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 0ab7cac46..d41d00e07 100644 --- a/app/src/main/res/layout/chat_bubble_incoming.xml +++ b/app/src/main/res/layout/chat_bubble_incoming.xml @@ -64,12 +64,12 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent"/> - + + + + + + @@ -78,14 +97,14 @@ android:layout_width="0dp" android:layout_height="@dimen/top_bar_height" android:layout_marginStart="10dp" - android:layout_marginEnd="10dp" + android:layout_marginEnd="5dp" android:maxLines="1" android:ellipsize="end" android:text="@{viewModel.isGroup ? viewModel.subject : viewModel.avatarModel.name, default=`John Doe`}" android:textSize="16sp" android:textColor="@color/gray_main2_600" android:gravity="center_vertical" - app:layout_constraintEnd_toStartOf="@id/call" + app:layout_constraintEnd_toStartOf="@id/search_toggle" app:layout_constraintStart_toEndOf="@id/avatar" app:layout_constraintTop_toTopOf="parent"/> @@ -99,38 +118,82 @@ android:src="@drawable/info" app:layout_constraintTop_toTopOf="@id/title" app:layout_constraintBottom_toBottomOf="@id/title" - app:layout_constraintEnd_toEndOf="parent"/> + app:layout_constraintEnd_toEndOf="parent" + app:tint="@color/gray_main2_500"/> + app:layout_constraintEnd_toStartOf="@id/info" + app:layout_constraintTop_toTopOf="@id/title" + app:tint="@color/gray_main2_500" /> + android:src="@drawable/caret_left" + app:layout_constraintBottom_toBottomOf="@id/search" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@id/search" + app:tint="@color/gray_main2_500" /> + + + + + + + + @@ -85,12 +86,13 @@ android:id="@+id/laughing" android:onClick="@{() -> model.sendReaction(@string/emoji_laughing)}" android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_height="0dp" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" android:text="@string/emoji_laughing" - android:textSize="30sp" + android:textSize="@dimen/chat_bubble_long_press_emoji_reaction_size" app:layout_constraintTop_toTopOf="@id/thumbs_up" + app:layout_constraintBottom_toBottomOf="@id/emojis_background" app:layout_constraintStart_toEndOf="@id/love" app:layout_constraintEnd_toStartOf="@id/surprised"/> @@ -99,12 +101,13 @@ android:id="@+id/surprised" android:onClick="@{() -> model.sendReaction(@string/emoji_surprised)}" android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_height="0dp" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" android:text="@string/emoji_surprised" - android:textSize="30sp" + android:textSize="@dimen/chat_bubble_long_press_emoji_reaction_size" app:layout_constraintTop_toTopOf="@id/thumbs_up" + app:layout_constraintBottom_toBottomOf="@id/emojis_background" app:layout_constraintStart_toEndOf="@id/laughing" app:layout_constraintEnd_toStartOf="@id/tear"/> @@ -113,12 +116,13 @@ android:id="@+id/tear" android:onClick="@{() -> model.sendReaction(@string/emoji_tear)}" android:layout_width="wrap_content" - android:layout_height="wrap_content" + android:layout_height="0dp" android:layout_marginStart="10dp" android:layout_marginEnd="10dp" android:text="@string/emoji_tear" - android:textSize="30sp" + android:textSize="@dimen/chat_bubble_long_press_emoji_reaction_size" app:layout_constraintTop_toTopOf="@id/thumbs_up" + app:layout_constraintBottom_toBottomOf="@id/emojis_background" app:layout_constraintStart_toEndOf="@id/surprised" app:layout_constraintEnd_toStartOf="@id/plus"/> @@ -133,7 +137,7 @@ app:layout_constraintStart_toEndOf="@id/tear" app:layout_constraintEnd_toEndOf="@id/emojis_background" app:layout_constraintTop_toTopOf="@id/thumbs_up" - app:layout_constraintBottom_toBottomOf="@id/thumbs_up" /> + app:layout_constraintBottom_toBottomOf="@id/emojis_background" /> 235dp 75dp - 325dp + 425dp - 400dp + 500dp \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 1703b643a..0591a9e2d 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -65,4 +65,5 @@ 110dp 12dp 5dp + 30sp \ No newline at end of file