Fixed chat rooms list sort order + added mentions menu when typing '@' + hide participants in non-group conversation

This commit is contained in:
Sylvain Berfini 2023-11-08 16:42:38 +01:00
parent d895fc6a09
commit dab462de35
8 changed files with 120 additions and 8 deletions

View file

@ -30,6 +30,8 @@ import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
@ -39,6 +41,7 @@ import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.UiThread import androidx.annotation.UiThread
import androidx.core.view.doOnPreDraw import androidx.core.view.doOnPreDraw
import androidx.core.widget.addTextChangedListener
import androidx.databinding.DataBindingUtil import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
@ -70,6 +73,7 @@ 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.LinphoneUtils import org.linphone.utils.LinphoneUtils
import org.linphone.utils.addCharacterAtPosition
import org.linphone.utils.hideKeyboard import org.linphone.utils.hideKeyboard
import org.linphone.utils.setKeyboardInsetListener import org.linphone.utils.setKeyboardInsetListener
import org.linphone.utils.showKeyboard import org.linphone.utils.showKeyboard
@ -114,6 +118,25 @@ class ConversationFragment : GenericFragment() {
} }
} }
private val textObserver = object : TextWatcher {
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
}
override fun afterTextChanged(p0: Editable?) {
viewModel.isParticipantsListOpen.value = false
val split = p0.toString().split(" ")
for (part in split) {
if (part == "@") {
viewModel.isParticipantsListOpen.value = true
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -262,6 +285,14 @@ class ConversationFragment : GenericFragment() {
findNavController().navigate(action) findNavController().navigate(action)
} }
viewModel.participantUsernameToAddEvent.observe(viewLifecycleOwner) {
it.consume { username ->
Log.i("$TAG Adding username [$username] after '@'")
// Also add a space for convenience
binding.sendArea.messageToSend.addCharacterAtPosition("$username ")
}
}
viewModel.searchFilter.observe(viewLifecycleOwner) { filter -> viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
viewModel.applyFilter(filter.trim()) viewModel.applyFilter(filter.trim())
} }
@ -337,9 +368,17 @@ class ConversationFragment : GenericFragment() {
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {
Log.e("$TAG Failed to register data observer to adapter: $e") Log.e("$TAG Failed to register data observer to adapter: $e")
} }
if (viewModel.isGroup.value == true) {
binding.sendArea.messageToSend.addTextChangedListener(textObserver)
}
} }
override fun onPause() { override fun onPause() {
if (viewModel.isGroup.value == true) {
binding.sendArea.messageToSend.removeTextChangedListener(textObserver)
}
try { try {
adapter.unregisterAdapterDataObserver(dataObserver) adapter.unregisterAdapterDataObserver(dataObserver)
} catch (e: IllegalStateException) { } catch (e: IllegalStateException) {

View file

@ -27,14 +27,20 @@ import org.linphone.core.Address
class ParticipantModel @WorkerThread constructor( class ParticipantModel @WorkerThread constructor(
val address: Address, val address: Address,
val isMyselfAdmin: Boolean, val isMyselfAdmin: Boolean = false,
val isParticipantAdmin: Boolean, val isParticipantAdmin: Boolean = false,
private val onClicked: ((model: ParticipantModel) -> Unit)? = null,
private val onMenuClicked: ((view: View, model: ParticipantModel) -> Unit)? = null private val onMenuClicked: ((view: View, model: ParticipantModel) -> Unit)? = null
) { ) {
val sipUri = address.asStringUriOnly() val sipUri = address.asStringUriOnly()
val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(address) val avatarModel = coreContext.contactsManager.getContactAvatarModelForAddress(address)
@UiThread
fun onClicked() {
onClicked?.invoke(this)
}
@UiThread @UiThread
fun openMenu(view: View) { fun openMenu(view: View) {
onMenuClicked?.invoke(view, this) onMenuClicked?.invoke(view, this)

View file

@ -362,10 +362,10 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() {
} else { } else {
for (participant in chatRoom.participants) { for (participant in chatRoom.participants) {
val isParticipantAdmin = if (groupChatRoom) participant.isAdmin else false val isParticipantAdmin = if (groupChatRoom) participant.isAdmin else false
val model = ParticipantModel(participant.address, selfAdmin, isParticipantAdmin) { view, model -> val model = ParticipantModel(participant.address, selfAdmin, isParticipantAdmin, onMenuClicked = { view, model ->
// openMenu // openMenu
showParticipantAdminPopupMenuEvent.postValue(Event(Pair(view, model))) showParticipantAdminPopupMenuEvent.postValue(Event(Pair(view, model)))
} })
friends.add(model.avatarModel.friend) friends.add(model.avatarModel.friend)
participantsList.add(model) participantsList.add(model)
} }
@ -374,6 +374,7 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() {
val avatar = if (groupChatRoom) { val avatar = if (groupChatRoom) {
val fakeFriend = coreContext.core.createFriend() val fakeFriend = coreContext.core.createFriend()
val model = ContactAvatarModel(fakeFriend) val model = ContactAvatarModel(fakeFriend)
model.defaultToConferenceIcon.postValue(true)
model.setPicturesFromFriends(friends) model.setPicturesFromFriends(friends)
model model
} else { } else {

View file

@ -35,6 +35,7 @@ import org.linphone.core.Friend
import org.linphone.core.tools.Log import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.ChatMessageModel import org.linphone.ui.main.chat.model.ChatMessageModel
import org.linphone.ui.main.chat.model.EventLogModel import org.linphone.ui.main.chat.model.EventLogModel
import org.linphone.ui.main.chat.model.ParticipantModel
import org.linphone.ui.main.contacts.model.ContactAvatarModel import org.linphone.ui.main.contacts.model.ContactAvatarModel
import org.linphone.utils.AppUtils import org.linphone.utils.AppUtils
import org.linphone.utils.Event import org.linphone.utils.Event
@ -70,6 +71,14 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
val isEmojiPickerOpen = MutableLiveData<Boolean>() val isEmojiPickerOpen = MutableLiveData<Boolean>()
val isParticipantsListOpen = MutableLiveData<Boolean>()
val participants = MutableLiveData<ArrayList<ParticipantModel>>()
val participantUsernameToAddEvent: MutableLiveData<Event<String>> by lazy {
MutableLiveData<Event<String>>()
}
val isReplying = MutableLiveData<Boolean>() val isReplying = MutableLiveData<Boolean>()
val isReplyingTo = MutableLiveData<String>() val isReplyingTo = MutableLiveData<String>()
@ -181,6 +190,16 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
events.postValue(list) events.postValue(list)
} }
@WorkerThread
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
computeParticipantsList()
}
@WorkerThread
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
computeParticipantsList()
}
} }
init { init {
@ -403,6 +422,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
computeEvents() computeEvents()
chatRoom.markAsRead() chatRoom.markAsRead()
computeParticipantsList()
} }
@WorkerThread @WorkerThread
@ -538,4 +558,24 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
composingLabel.postValue("") composingLabel.postValue("")
} }
} }
@WorkerThread
private fun computeParticipantsList() {
val participantsList = arrayListOf<ParticipantModel>()
for (participant in chatRoom.participants) {
val model = ParticipantModel(participant.address, onClicked = { clicked ->
Log.i("$TAG Clicked on participant [${clicked.sipUri}]")
coreContext.postOnCoreThread {
val username = clicked.address.username
if (!username.isNullOrEmpty()) {
participantUsernameToAddEvent.postValue(Event(username))
}
}
})
participantsList.add(model)
}
participants.postValue(participantsList)
}
} }

View file

@ -171,7 +171,7 @@ class ConversationsListViewModel @UiThread constructor() : AbstractTopBarViewMod
Log.i("$TAG Re-ordering chat rooms") Log.i("$TAG Re-ordering chat rooms")
val sortedList = arrayListOf<ConversationModel>() val sortedList = arrayListOf<ConversationModel>()
sortedList.addAll(conversations.value.orEmpty()) sortedList.addAll(conversations.value.orEmpty())
sortedList.sortBy { sortedList.sortByDescending {
it.lastUpdateTime.value it.lastUpdateTime.value
} }
conversations.postValue(sortedList) conversations.postValue(sortedList)

View file

@ -223,10 +223,31 @@
android:textColor="@color/gray_main2_400" android:textColor="@color/gray_main2_400"
android:visibility="@{viewModel.composingLabel.length() == 0 ? View.GONE : View.VISIBLE}" android:visibility="@{viewModel.composingLabel.length() == 0 ? View.GONE : View.VISIBLE}"
app:layout_constraintTop_toBottomOf="@id/events_list" app:layout_constraintTop_toBottomOf="@id/events_list"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toTopOf="@id/participants"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" /> app:layout_constraintEnd_toEndOf="parent" />
<androidx.core.widget.NestedScrollView
android:id="@+id/participants"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:background="@drawable/shape_squircle_gray_100_background"
android:visibility="@{viewModel.isParticipantsListOpen ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintHeight_max="300dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintBottom_toBottomOf="parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:orientation="vertical"
entries="@{viewModel.participants}"
layout="@{@layout/chat_participant_list_cell}"/>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<include <include

View file

@ -204,6 +204,7 @@
android:text="@string/conversation_info_participants_list_title" android:text="@string/conversation_info_participants_list_title"
android:drawableEnd="@{viewModel.expandParticipants ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}" android:drawableEnd="@{viewModel.expandParticipants ? @drawable/caret_up : @drawable/caret_down, default=@drawable/caret_up}"
android:drawableTint="@color/gray_main2_600" android:drawableTint="@color/gray_main2_600"
android:visibility="@{viewModel.isGroup ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/mute_label"/> app:layout_constraintTop_toBottomOf="@id/mute_label"/>
@ -215,6 +216,7 @@
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:src="@drawable/shape_squircle_white_background" android:src="@drawable/shape_squircle_white_background"
android:visibility="@{viewModel.isGroup ? View.VISIBLE : View.GONE}"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@id/participants" app:layout_constraintTop_toTopOf="@id/participants"
@ -222,13 +224,14 @@
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/participants" android:id="@+id/participants"
android:visibility="@{viewModel.expandParticipants ? View.VISIBLE : View.GONE}" android:visibility="@{viewModel.expandParticipants &amp;&amp; viewModel.isGroup ? View.VISIBLE : View.GONE}"
android:layout_width="0dp" android:layout_width="0dp"
android:layout_height="0dp" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="16dp" android:layout_marginEnd="16dp"
android:layout_marginTop="8dp" android:layout_marginTop="8dp"
android:layout_marginBottom="16dp" android:layout_marginBottom="16dp"
app:layout_constraintHeight_max="300dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/participants_label" app:layout_constraintTop_toBottomOf="@id/participants_label"
@ -240,6 +243,7 @@
android:onClick="@{addParticipantsClickListener}" android:onClick="@{addParticipantsClickListener}"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:background="@drawable/tertiary_button_background" android:background="@drawable/tertiary_button_background"
android:paddingStart="16dp" android:paddingStart="16dp"
android:paddingEnd="16dp" android:paddingEnd="16dp"

View file

@ -13,6 +13,7 @@
</data> </data>
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:onClick="@{() -> model.onClicked()}"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="@drawable/primary_cell_background" android:background="@drawable/primary_cell_background"