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.net.Uri
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
@ -39,6 +41,7 @@ import androidx.activity.result.PickVisualMediaRequest
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.UiThread
import androidx.core.view.doOnPreDraw
import androidx.core.widget.addTextChangedListener
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
@ -70,6 +73,7 @@ import org.linphone.ui.main.fragment.GenericFragment
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
import org.linphone.utils.LinphoneUtils
import org.linphone.utils.addCharacterAtPosition
import org.linphone.utils.hideKeyboard
import org.linphone.utils.setKeyboardInsetListener
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?) {
super.onCreate(savedInstanceState)
@ -262,6 +285,14 @@ class ConversationFragment : GenericFragment() {
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.applyFilter(filter.trim())
}
@ -337,9 +368,17 @@ class ConversationFragment : GenericFragment() {
} catch (e: IllegalStateException) {
Log.e("$TAG Failed to register data observer to adapter: $e")
}
if (viewModel.isGroup.value == true) {
binding.sendArea.messageToSend.addTextChangedListener(textObserver)
}
}
override fun onPause() {
if (viewModel.isGroup.value == true) {
binding.sendArea.messageToSend.removeTextChangedListener(textObserver)
}
try {
adapter.unregisterAdapterDataObserver(dataObserver)
} catch (e: IllegalStateException) {

View file

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

View file

@ -362,10 +362,10 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() {
} else {
for (participant in chatRoom.participants) {
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
showParticipantAdminPopupMenuEvent.postValue(Event(Pair(view, model)))
}
})
friends.add(model.avatarModel.friend)
participantsList.add(model)
}
@ -374,6 +374,7 @@ class ConversationInfoViewModel @UiThread constructor() : ViewModel() {
val avatar = if (groupChatRoom) {
val fakeFriend = coreContext.core.createFriend()
val model = ContactAvatarModel(fakeFriend)
model.defaultToConferenceIcon.postValue(true)
model.setPicturesFromFriends(friends)
model
} else {

View file

@ -35,6 +35,7 @@ import org.linphone.core.Friend
import org.linphone.core.tools.Log
import org.linphone.ui.main.chat.model.ChatMessageModel
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.utils.AppUtils
import org.linphone.utils.Event
@ -70,6 +71,14 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
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 isReplyingTo = MutableLiveData<String>()
@ -181,6 +190,16 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
events.postValue(list)
}
@WorkerThread
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
computeParticipantsList()
}
@WorkerThread
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
computeParticipantsList()
}
}
init {
@ -403,6 +422,7 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
computeEvents()
chatRoom.markAsRead()
computeParticipantsList()
}
@WorkerThread
@ -538,4 +558,24 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
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")
val sortedList = arrayListOf<ConversationModel>()
sortedList.addAll(conversations.value.orEmpty())
sortedList.sortBy {
sortedList.sortByDescending {
it.lastUpdateTime.value
}
conversations.postValue(sortedList)

View file

@ -223,10 +223,31 @@
android:textColor="@color/gray_main2_400"
android:visibility="@{viewModel.composingLabel.length() == 0 ? View.GONE : View.VISIBLE}"
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_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>
<include

View file

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

View file

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