Added search in conversation top bar

This commit is contained in:
Sylvain Berfini 2023-10-16 16:11:18 +02:00
parent a2355e3225
commit f9d2e04609
15 changed files with 250 additions and 81 deletions

View file

@ -152,7 +152,9 @@ class ConversationEventAdapter(
return if (!oldItem.isEvent && !newItem.isEvent) { return if (!oldItem.isEvent && !newItem.isEvent) {
val oldData = (oldItem.model as ChatMessageModel) val oldData = (oldItem.model as ChatMessageModel)
val newData = (newItem.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 { } else {
oldItem.notifyId == newItem.notifyId oldItem.notifyId == newItem.notifyId
} }
@ -162,8 +164,8 @@ class ConversationEventAdapter(
return if (oldItem.isEvent && newItem.isEvent) { return if (oldItem.isEvent && newItem.isEvent) {
true true
} else if (!oldItem.isEvent && !newItem.isEvent) { } else if (!oldItem.isEvent && !newItem.isEvent) {
val oldModel = (newItem.model as ChatMessageModel) val oldModel = (oldItem.model as ChatMessageModel)
val newModel = newItem.model val newModel = (newItem.model as ChatMessageModel)
oldModel.statusIcon.value == newModel.statusIcon.value oldModel.statusIcon.value == newModel.statusIcon.value
} else { } else {
false false

View file

@ -51,6 +51,8 @@ import org.linphone.ui.main.chat.viewmodel.ConversationViewModel
import org.linphone.ui.main.fragment.GenericFragment import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.Event import org.linphone.utils.Event
import org.linphone.utils.hideKeyboard
import org.linphone.utils.showKeyboard
@UiThread @UiThread
class ConversationFragment : GenericFragment() { class ConversationFragment : GenericFragment() {
@ -176,6 +178,21 @@ class ConversationFragment : GenericFragment() {
) )
findNavController().navigate(action) 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) { private fun showChatMessageLongPressMenu(chatMessageModel: ChatMessageModel) {

View file

@ -53,7 +53,9 @@ class ChatMessageModel @WorkerThread constructor(
val text = LinphoneUtils.getTextDescribingMessage(chatMessage) 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 val chatRoomIsReadOnly = chatMessage.chatRoom.isReadOnly

View file

@ -22,6 +22,5 @@ package org.linphone.ui.main.chat.model
import org.linphone.core.Friend import org.linphone.core.Friend
import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.contacts.model.ContactAvatarModel
class ParticipantModel(friend: Friend, val isMyselfAdmin: Boolean, val isParticipantAdmin: Boolean) : ContactAvatarModel( class ParticipantModel(friend: Friend, val isMyselfAdmin: Boolean, val isParticipantAdmin: Boolean) :
friend ContactAvatarModel(friend)
)

View file

@ -70,17 +70,17 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() {
private val chatRoomListener = object : ChatRoomListenerStub() { private val chatRoomListener = object : ChatRoomListenerStub() {
@WorkerThread @WorkerThread
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) { override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
computeParticipantsList(isGroup.value == true) computeParticipantsList()
} }
@WorkerThread @WorkerThread
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) { override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
computeParticipantsList(isGroup.value == true) computeParticipantsList()
} }
@WorkerThread @WorkerThread
override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) { override fun onParticipantAdminStatusChanged(chatRoom: ChatRoom, eventLog: EventLog) {
computeParticipantsList(isGroup.value == true) computeParticipantsList()
} }
@WorkerThread @WorkerThread
@ -183,8 +183,7 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() {
isMyselfAdmin.postValue(chatRoom.me?.isAdmin) isMyselfAdmin.postValue(chatRoom.me?.isAdmin)
val isGroupChatRoom = !chatRoom.hasCapability(ChatRoom.Capabilities.OneToOne.toInt()) && val isGroupChatRoom = isChatRoomAGroup()
chatRoom.hasCapability(ChatRoom.Capabilities.Conference.toInt())
isGroup.postValue(isGroupChatRoom) isGroup.postValue(isGroupChatRoom)
val empty = chatRoom.hasCapability(ChatRoom.Capabilities.Conference.toInt()) && chatRoom.participants.isEmpty() val empty = chatRoom.hasCapability(ChatRoom.Capabilities.Conference.toInt()) && chatRoom.participants.isEmpty()
@ -196,14 +195,16 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() {
subject.postValue(chatRoom.subject) subject.postValue(chatRoom.subject)
computeParticipantsList(isGroupChatRoom) computeParticipantsList()
} }
@WorkerThread @WorkerThread
private fun computeParticipantsList(isGroupChatRoom: Boolean) { private fun computeParticipantsList() {
avatarModel.value?.destroy() avatarModel.value?.destroy()
avatarsMap.values.forEach(ParticipantModel::destroy) avatarsMap.values.forEach(ParticipantModel::destroy)
val groupChatRoom = isChatRoomAGroup()
val friends = arrayListOf<Friend>() val friends = arrayListOf<Friend>()
val participantsList = arrayListOf<ParticipantModel>() val participantsList = arrayListOf<ParticipantModel>()
if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) { if (chatRoom.hasCapability(ChatRoom.Capabilities.Basic.toInt())) {
@ -214,14 +215,14 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() {
for (participant in chatRoom.participants) { for (participant in chatRoom.participants) {
val model = getParticipantModelForAddress( val model = getParticipantModelForAddress(
participant.address, participant.address,
if (isGroup.value == true) participant.isAdmin else false if (groupChatRoom) participant.isAdmin else false
) )
friends.add(model.friend) friends.add(model.friend)
participantsList.add(model) participantsList.add(model)
} }
} }
val avatar = if (isGroupChatRoom) { val avatar = if (groupChatRoom) {
val fakeFriend = coreContext.core.createFriend() val fakeFriend = coreContext.core.createFriend()
ContactAvatarModel(fakeFriend) ContactAvatarModel(fakeFriend)
} else { } else {
@ -260,4 +261,13 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() {
avatarsMap[key] = avatar avatarsMap[key] = avatar
return avatar return avatar
} }
@WorkerThread
private fun isChatRoomAGroup(): Boolean {
return if (::chatRoom.isInitialized) {
LinphoneUtils.isChatRoomAGroup(chatRoom)
} else {
false
}
}
} }

View file

@ -62,10 +62,20 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
val textToSend = MutableLiveData<String>() val textToSend = MutableLiveData<String>()
val searchBarVisible = MutableLiveData<Boolean>()
val searchFilter = MutableLiveData<String>()
val focusSearchBarEvent: MutableLiveData<Event<Boolean>> by lazy {
MutableLiveData<Event<Boolean>>()
}
val chatRoomFoundEvent = MutableLiveData<Event<Boolean>>() val chatRoomFoundEvent = MutableLiveData<Event<Boolean>>()
private lateinit var chatRoom: ChatRoom private lateinit var chatRoom: ChatRoom
private var currentFilter = ""
private val avatarsMap = hashMapOf<String, ContactAvatarModel>() private val avatarsMap = hashMapOf<String, ContactAvatarModel>()
private val chatRoomListener = object : ChatRoomListenerStub() { private val chatRoomListener = object : ChatRoomListenerStub() {
@ -84,7 +94,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
} else { } else {
false false
} }
list.add(EventLogModel(eventLog, avatarModel, isGroup.value == true, group, true)) list.add(EventLogModel(eventLog, avatarModel, isChatRoomAGroup(), group, true))
events.postValue(list) events.postValue(list)
} }
@ -118,7 +128,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
val newList = getEventsListFromHistory( val newList = getEventsListFromHistory(
eventLogs, eventLogs,
isGroupChatRoom = isGroup.value == true searchFilter.value.orEmpty().trim()
) )
list.addAll(newList) list.addAll(newList)
@ -128,6 +138,10 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
} }
} }
init {
searchBarVisible.value = false
}
override fun onCleared() { override fun onCleared() {
super.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 @UiThread
fun findChatRoom(localSipUri: String, remoteSipUri: String) { fun findChatRoom(localSipUri: String, remoteSipUri: String) {
coreContext.postOnCoreThread { core -> coreContext.postOnCoreThread { core ->
@ -174,6 +206,13 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
} }
} }
@UiThread
fun applyFilter(filter: String) {
coreContext.postOnCoreThread {
computeEvents(filter)
}
}
@UiThread @UiThread
fun sendMessage() { fun sendMessage() {
coreContext.postOnCoreThread { core -> coreContext.postOnCoreThread { core ->
@ -215,10 +254,6 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
private fun configureChatRoom() { private fun configureChatRoom() {
computeComposingLabel() 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 empty = chatRoom.hasCapability(ChatRoom.Capabilities.Conference.toInt()) && chatRoom.participants.isEmpty()
val readOnly = chatRoom.isReadOnly || empty val readOnly = chatRoom.isReadOnly || empty
isReadOnly.postValue(readOnly) isReadOnly.postValue(readOnly)
@ -226,6 +261,9 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
Log.w("$TAG Chat room with subject [${chatRoom.subject}] is read only!") Log.w("$TAG Chat room with subject [${chatRoom.subject}] is read only!")
} }
val group = isChatRoomAGroup()
isGroup.postValue(group)
subject.postValue(chatRoom.subject) subject.postValue(chatRoom.subject)
val friends = arrayListOf<Friend>() val friends = arrayListOf<Friend>()
@ -243,7 +281,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
firstParticipant?.address ?: chatRoom.peerAddress firstParticipant?.address ?: chatRoom.peerAddress
} }
val avatar = if (isGroupChatRoom) { val avatar = if (group) {
val fakeFriend = coreContext.core.createFriend() val fakeFriend = coreContext.core.createFriend()
ContactAvatarModel(fakeFriend) ContactAvatarModel(fakeFriend)
} else { } else {
@ -252,17 +290,22 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
avatar.setPicturesFromFriends(friends) avatar.setPicturesFromFriends(friends)
avatarModel.postValue(avatar) avatarModel.postValue(avatar)
val history = chatRoom.getHistoryEvents(0) computeEvents()
val eventsList = getEventsListFromHistory(history, isGroupChatRoom)
events.postValue(eventsList)
chatRoom.markAsRead() 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 @WorkerThread
private fun processGroupedEvents( private fun processGroupedEvents(
groupedEventLogs: ArrayList<EventLog>, groupedEventLogs: ArrayList<EventLog>
isGroupChatRoom: Boolean
): ArrayList<EventLogModel> { ): ArrayList<EventLogModel> {
val eventsList = arrayListOf<EventLogModel>() val eventsList = arrayListOf<EventLogModel>()
@ -273,7 +316,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
val model = EventLogModel( val model = EventLogModel(
groupedEvent, groupedEvent,
avatar, avatar,
isGroupChatRoom, isChatRoomAGroup(),
index > 0, index > 0,
index == groupedEventLogs.size - 1 index == groupedEventLogs.size - 1
) )
@ -286,10 +329,26 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
} }
@WorkerThread @WorkerThread
private fun getEventsListFromHistory(history: Array<EventLog>, isGroupChatRoom: Boolean): ArrayList<EventLogModel> { private fun getEventsListFromHistory(history: Array<EventLog>, filter: String = ""): ArrayList<EventLogModel> {
val eventsList = arrayListOf<EventLogModel>() val eventsList = arrayListOf<EventLogModel>()
val groupedEventLogs = arrayListOf<EventLog>() val groupedEventLogs = arrayListOf<EventLog>()
for (event in history) { 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()) { if (groupedEventLogs.isEmpty()) {
groupedEventLogs.add(event) groupedEventLogs.add(event)
continue continue
@ -299,7 +358,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
val groupEvents = shouldWeGroupTwoEvents(event, previousGroupEvent) val groupEvents = shouldWeGroupTwoEvents(event, previousGroupEvent)
if (!groupEvents) { if (!groupEvents) {
eventsList.addAll(processGroupedEvents(groupedEventLogs, isGroupChatRoom)) eventsList.addAll(processGroupedEvents(groupedEventLogs))
groupedEventLogs.clear() groupedEventLogs.clear()
} }
@ -307,7 +366,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
} }
if (groupedEventLogs.isNotEmpty()) { if (groupedEventLogs.isNotEmpty()) {
eventsList.addAll(processGroupedEvents(groupedEventLogs, isGroupChatRoom)) eventsList.addAll(processGroupedEvents(groupedEventLogs))
groupedEventLogs.clear() groupedEventLogs.clear()
} }
@ -380,4 +439,13 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
composingLabel.postValue("") composingLabel.postValue("")
} }
} }
@WorkerThread
private fun isChatRoomAGroup(): Boolean {
return if (::chatRoom.isInitialized) {
LinphoneUtils.isChatRoomAGroup(chatRoom)
} else {
false
}
}
} }

View file

@ -199,6 +199,12 @@ class LinphoneUtils {
return "${localSipUri.asStringUriOnly()}~${remoteSipUri.asStringUriOnly()}" 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 @WorkerThread
fun getRecordingFilePathForAddress(address: Address): String { fun getRecordingFilePathForAddress(address: Address): String {
val displayName = getDisplayName(address) val displayName = getDisplayName(address)

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:topRightRadius="16dp" android:bottomRightRadius="16dp" android:bottomLeftRadius="16dp" /> <corners android:topRightRadius="16dp" android:bottomRightRadius="16dp" android:bottomLeftRadius="16dp" />
<solid android:color="@color/gray_100"/> <solid android:color="@color/white"/>
</shape> </shape>

View file

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle"> <shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="16dp" /> <corners android:radius="16dp" />
<solid android:color="@color/gray_100"/> <solid android:color="@color/white"/>
</shape> </shape>

View file

@ -64,12 +64,12 @@
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/> app:layout_constraintBottom_toBottomOf="parent"/>
<org.linphone.ui.main.chat.view.ChatBubbleTextView <androidx.appcompat.widget.AppCompatTextView
style="@style/default_text_style" style="@style/default_text_style"
android:id="@+id/text_message" android:id="@+id/text_message"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="26dp"
android:layout_marginTop="12dp" android:layout_marginTop="12dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:paddingBottom="@{model.groupedWithNextOne ? @dimen/chat_bubble_text_padding_with_status : @dimen/chat_bubble_text_padding_with_bubble, default=@dimen/chat_bubble_text_padding_with_status}" android:paddingBottom="@{model.groupedWithNextOne ? @dimen/chat_bubble_text_padding_with_status : @dimen/chat_bubble_text_padding_with_bubble, default=@dimen/chat_bubble_text_padding_with_status}"
@ -104,7 +104,6 @@
android:layout_width="@dimen/small_icon_size" android:layout_width="@dimen/small_icon_size"
android:layout_height="@dimen/small_icon_size" android:layout_height="@dimen/small_icon_size"
android:layout_marginStart="5dp" android:layout_marginStart="5dp"
android:layout_marginTop="2dp"
android:src="@{model.statusIcon, default=@drawable/checks}" android:src="@{model.statusIcon, default=@drawable/checks}"
android:visibility="@{model.isGroupedWithNextOne ? View.VISIBLE : View.GONE}" android:visibility="@{model.isGroupedWithNextOne ? View.VISIBLE : View.GONE}"
app:layout_constraintTop_toTopOf="@id/date_time" app:layout_constraintTop_toTopOf="@id/date_time"

View file

@ -36,6 +36,25 @@
android:layout_marginBottom="80dp" android:layout_marginBottom="80dp"
android:background="@color/white"> android:background="@color/white">
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="avatar, title, search_toggle, info"
android:visibility="@{viewModel.searchBarVisible ? View.GONE : View.VISIBLE}" />
<androidx.constraintlayout.widget.Group
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:constraint_referenced_ids="cancel_search, search, clear_field"
android:visibility="@{viewModel.searchBarVisible ? View.VISIBLE : View.GONE, default=gone}" />
<androidx.constraintlayout.widget.Barrier
android:id="@+id/top_bar_barrier"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:barrierDirection="bottom"
app:constraint_referenced_ids="title, search" />
<ImageView <ImageView
android:id="@+id/back" android:id="@+id/back"
android:layout_width="wrap_content" android:layout_width="wrap_content"
@ -43,7 +62,7 @@
android:padding="15dp" android:padding="15dp"
android:adjustViewBounds="true" android:adjustViewBounds="true"
android:onClick="@{backClickListener}" android:onClick="@{backClickListener}"
android:visibility="@{viewModel.showBackButton ? View.VISIBLE : View.GONE}" android:visibility="@{viewModel.showBackButton &amp;&amp; !viewModel.searchBarVisible ? View.VISIBLE : View.GONE}"
android:src="@drawable/caret_left" android:src="@drawable/caret_left"
app:tint="@color/orange_main_500" app:tint="@color/orange_main_500"
app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintBottom_toBottomOf="@id/title"
@ -68,7 +87,7 @@
android:layout_width="@dimen/avatar_presence_badge_size" android:layout_width="@dimen/avatar_presence_badge_size"
android:layout_height="@dimen/avatar_presence_badge_size" android:layout_height="@dimen/avatar_presence_badge_size"
android:src="@{viewModel.avatarModel.trust == SecurityLevel.Safe ? @drawable/trusted : @drawable/not_trusted, default=@drawable/trusted}" android:src="@{viewModel.avatarModel.trust == SecurityLevel.Safe ? @drawable/trusted : @drawable/not_trusted, default=@drawable/trusted}"
android:visibility="@{viewModel.avatarModel.trust == SecurityLevel.Safe || viewModel.avatarModel.trust == SecurityLevel.Unsafe ? View.VISIBLE : View.GONE}" android:visibility="@{!viewModel.searchBarVisible &amp;&amp; (viewModel.avatarModel.trust == SecurityLevel.Safe || viewModel.avatarModel.trust == SecurityLevel.Unsafe) ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="@id/avatar" app:layout_constraintStart_toStartOf="@id/avatar"
app:layout_constraintBottom_toBottomOf="@id/avatar"/> app:layout_constraintBottom_toBottomOf="@id/avatar"/>
@ -78,14 +97,14 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height" android:layout_height="@dimen/top_bar_height"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="5dp"
android:maxLines="1" android:maxLines="1"
android:ellipsize="end" android:ellipsize="end"
android:text="@{viewModel.isGroup ? viewModel.subject : viewModel.avatarModel.name, default=`John Doe`}" android:text="@{viewModel.isGroup ? viewModel.subject : viewModel.avatarModel.name, default=`John Doe`}"
android:textSize="16sp" android:textSize="16sp"
android:textColor="@color/gray_main2_600" android:textColor="@color/gray_main2_600"
android:gravity="center_vertical" 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_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent"/> app:layout_constraintTop_toTopOf="parent"/>
@ -99,38 +118,82 @@
android:src="@drawable/info" android:src="@drawable/info"
app:layout_constraintTop_toTopOf="@id/title" app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toEndOf="parent"/> app:layout_constraintEnd_toEndOf="parent"
app:tint="@color/gray_main2_500"/>
<ImageView <ImageView
android:id="@+id/video_call" android:id="@+id/search_toggle"
android:onClick="@{startVideoCallClickListener}" android:onClick="@{() -> viewModel.openSearchBar()}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="0dp"
android:padding="15dp" android:padding="15dp"
android:adjustViewBounds="true" android:src="@drawable/magnifying_glass"
android:src="@drawable/video_camera"
app:layout_constraintTop_toTopOf="@id/title"
app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toStartOf="@id/info" /> app:layout_constraintEnd_toStartOf="@id/info"
app:layout_constraintTop_toTopOf="@id/title"
app:tint="@color/gray_main2_500" />
<ImageView <ImageView
android:id="@+id/call" android:id="@+id/cancel_search"
android:onClick="@{startCallClickListener}" android:onClick="@{() -> viewModel.closeSearchBar()}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="0dp"
android:padding="15dp" android:padding="15dp"
android:adjustViewBounds="true" android:src="@drawable/caret_left"
android:src="@drawable/phone" app:layout_constraintBottom_toBottomOf="@id/search"
app:layout_constraintTop_toTopOf="@id/title" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintTop_toTopOf="@id/search"
app:layout_constraintEnd_toStartOf="@id/video_call"/> app:tint="@color/gray_main2_500" />
<com.google.android.material.textfield.TextInputLayout
style="?attr/textInputFilledStyle"
android:id="@+id/search"
android:layout_width="0dp"
android:layout_height="@dimen/top_bar_height"
android:gravity="center_vertical"
android:textColorHint="@color/gray_main2_400"
app:hintEnabled="false"
app:hintAnimationEnabled="false"
app:hintTextColor="@color/gray_main2_400"
app:boxStrokeWidth="0dp"
app:boxStrokeWidthFocused="0dp"
app:layout_constraintEnd_toStartOf="@id/clear_field"
app:layout_constraintStart_toEndOf="@id/cancel_search"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:layout_width="match_parent"
android:layout_height="match_parent"
android:textCursorDrawable="@null"
android:textSize="16sp"
android:inputType="text"
android:paddingVertical="1dp"
android:text="@={viewModel.searchFilter}"
android:background="@android:color/transparent" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/clear_field"
android:onClick="@{() -> viewModel.clearFilter()}"
android:enabled="@{viewModel.searchFilter.length() > 0}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="6dp"
android:layout_marginEnd="9dp"
android:src="@drawable/x"
app:layout_constraintBottom_toBottomOf="@id/search"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/search"
app:tint="@color/gray_main2_500" />
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/events_list" android:id="@+id/events_list"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="0dp" android:layout_height="0dp"
android:layout_marginBottom="5dp" android:paddingBottom="5dp"
app:layout_constraintTop_toBottomOf="@id/title" android:background="@color/gray_100"
app:layout_constraintTop_toBottomOf="@id/top_bar_barrier"
app:layout_constraintBottom_toTopOf="@id/composing" /> app:layout_constraintBottom_toTopOf="@id/composing" />
<androidx.appcompat.widget.AppCompatTextView <androidx.appcompat.widget.AppCompatTextView
@ -139,7 +202,8 @@
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginBottom="5dp" android:paddingBottom="5dp"
android:background="@color/gray_100"
android:text="@{viewModel.composingLabel, default=`John Doe is composing...`}" android:text="@{viewModel.composingLabel, default=`John Doe is composing...`}"
android:textSize="12sp" android:textSize="12sp"
android:textColor="@color/gray_main2_400" android:textColor="@color/gray_main2_400"

View file

@ -55,11 +55,11 @@
android:id="@+id/thumbs_up" android:id="@+id/thumbs_up"
android:onClick="@{() -> model.sendReaction(@string/emoji_thumbs_up)}" android:onClick="@{() -> model.sendReaction(@string/emoji_thumbs_up)}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:text="@string/emoji_thumbs_up" android:text="@string/emoji_thumbs_up"
android:textSize="30sp" android:textSize="@dimen/chat_bubble_long_press_emoji_reaction_size"
app:layout_constraintHorizontal_chainStyle="spread_inside" app:layout_constraintHorizontal_chainStyle="spread_inside"
app:layout_constraintTop_toTopOf="@id/emojis_background" app:layout_constraintTop_toTopOf="@id/emojis_background"
app:layout_constraintBottom_toBottomOf="@id/emojis_background" app:layout_constraintBottom_toBottomOf="@id/emojis_background"
@ -71,12 +71,13 @@
android:id="@+id/love" android:id="@+id/love"
android:onClick="@{() -> model.sendReaction(@string/emoji_love)}" android:onClick="@{() -> model.sendReaction(@string/emoji_love)}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:text="@string/emoji_love" android:text="@string/emoji_love"
android:textSize="30sp" android:textSize="@dimen/chat_bubble_long_press_emoji_reaction_size"
app:layout_constraintTop_toTopOf="@id/thumbs_up" app:layout_constraintTop_toTopOf="@id/thumbs_up"
app:layout_constraintBottom_toBottomOf="@id/emojis_background"
app:layout_constraintStart_toEndOf="@id/thumbs_up" app:layout_constraintStart_toEndOf="@id/thumbs_up"
app:layout_constraintEnd_toStartOf="@id/laughing"/> app:layout_constraintEnd_toStartOf="@id/laughing"/>
@ -85,12 +86,13 @@
android:id="@+id/laughing" android:id="@+id/laughing"
android:onClick="@{() -> model.sendReaction(@string/emoji_laughing)}" android:onClick="@{() -> model.sendReaction(@string/emoji_laughing)}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:text="@string/emoji_laughing" 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_constraintTop_toTopOf="@id/thumbs_up"
app:layout_constraintBottom_toBottomOf="@id/emojis_background"
app:layout_constraintStart_toEndOf="@id/love" app:layout_constraintStart_toEndOf="@id/love"
app:layout_constraintEnd_toStartOf="@id/surprised"/> app:layout_constraintEnd_toStartOf="@id/surprised"/>
@ -99,12 +101,13 @@
android:id="@+id/surprised" android:id="@+id/surprised"
android:onClick="@{() -> model.sendReaction(@string/emoji_surprised)}" android:onClick="@{() -> model.sendReaction(@string/emoji_surprised)}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:text="@string/emoji_surprised" 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_constraintTop_toTopOf="@id/thumbs_up"
app:layout_constraintBottom_toBottomOf="@id/emojis_background"
app:layout_constraintStart_toEndOf="@id/laughing" app:layout_constraintStart_toEndOf="@id/laughing"
app:layout_constraintEnd_toStartOf="@id/tear"/> app:layout_constraintEnd_toStartOf="@id/tear"/>
@ -113,12 +116,13 @@
android:id="@+id/tear" android:id="@+id/tear"
android:onClick="@{() -> model.sendReaction(@string/emoji_tear)}" android:onClick="@{() -> model.sendReaction(@string/emoji_tear)}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="0dp"
android:layout_marginStart="10dp" android:layout_marginStart="10dp"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
android:text="@string/emoji_tear" 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_constraintTop_toTopOf="@id/thumbs_up"
app:layout_constraintBottom_toBottomOf="@id/emojis_background"
app:layout_constraintStart_toEndOf="@id/surprised" app:layout_constraintStart_toEndOf="@id/surprised"
app:layout_constraintEnd_toStartOf="@id/plus"/> app:layout_constraintEnd_toStartOf="@id/plus"/>
@ -133,7 +137,7 @@
app:layout_constraintStart_toEndOf="@id/tear" app:layout_constraintStart_toEndOf="@id/tear"
app:layout_constraintEnd_toEndOf="@id/emojis_background" app:layout_constraintEnd_toEndOf="@id/emojis_background"
app:layout_constraintTop_toTopOf="@id/thumbs_up" app:layout_constraintTop_toTopOf="@id/thumbs_up"
app:layout_constraintBottom_toBottomOf="@id/thumbs_up" /> app:layout_constraintBottom_toBottomOf="@id/emojis_background" />
<include <include
android:id="@+id/bubble" android:id="@+id/bubble"

View file

@ -74,9 +74,8 @@
android:id="@+id/search_toggle" android:id="@+id/search_toggle"
android:onClick="@{() -> viewModel.openSearchBar()}" android:onClick="@{() -> viewModel.openSearchBar()}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="0dp"
android:padding="6dp" android:padding="15dp"
android:layout_marginEnd="9dp"
android:src="@drawable/magnifying_glass" android:src="@drawable/magnifying_glass"
app:layout_constraintBottom_toBottomOf="@id/title" app:layout_constraintBottom_toBottomOf="@id/title"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
@ -87,9 +86,8 @@
android:id="@+id/cancel_search" android:id="@+id/cancel_search"
android:onClick="@{() -> viewModel.closeSearchBar()}" android:onClick="@{() -> viewModel.closeSearchBar()}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="0dp"
android:padding="10dp" android:padding="15dp"
android:layout_marginStart="5dp"
android:src="@drawable/caret_left" android:src="@drawable/caret_left"
app:layout_constraintBottom_toBottomOf="@id/search" app:layout_constraintBottom_toBottomOf="@id/search"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
@ -130,9 +128,8 @@
android:onClick="@{() -> viewModel.clearFilter()}" android:onClick="@{() -> viewModel.clearFilter()}"
android:enabled="@{viewModel.searchFilter.length() > 0}" android:enabled="@{viewModel.searchFilter.length() > 0}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="0dp"
android:padding="6dp" android:padding="15dp"
android:layout_marginEnd="9dp"
android:src="@drawable/x" android:src="@drawable/x"
app:layout_constraintBottom_toBottomOf="@id/search" app:layout_constraintBottom_toBottomOf="@id/search"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"

View file

@ -5,7 +5,7 @@
<dimen name="call_all_actions_menu_height">235dp</dimen> <!-- sum of above two --> <dimen name="call_all_actions_menu_height">235dp</dimen> <!-- sum of above two -->
<dimen name="landscape_nav_bar_width">75dp</dimen> <dimen name="landscape_nav_bar_width">75dp</dimen>
<dimen name="sliding_pane_left_fragment_width">325dp</dimen> <dimen name="sliding_pane_left_fragment_width">425dp</dimen>
<!-- This value is the result of the above two added together --> <!-- This value is the result of the above two added together -->
<dimen name="sliding_pane_left_fragment_with_nav_width">400dp</dimen> <dimen name="sliding_pane_left_fragment_with_nav_width">500dp</dimen>
</resources> </resources>

View file

@ -65,4 +65,5 @@
<dimen name="chat_bubble_long_press_menu_bubble_offset">110dp</dimen> <dimen name="chat_bubble_long_press_menu_bubble_offset">110dp</dimen>
<dimen name="chat_bubble_text_padding_with_bubble">12dp</dimen> <dimen name="chat_bubble_text_padding_with_bubble">12dp</dimen>
<dimen name="chat_bubble_text_padding_with_status">5dp</dimen> <dimen name="chat_bubble_text_padding_with_status">5dp</dimen>
<dimen name="chat_bubble_long_press_emoji_reaction_size">30sp</dimen>
</resources> </resources>