mirror of
https://gitlab.linphone.org/BC/public/linphone-android.git
synced 2026-01-17 11:28:06 +00:00
Split ConversationViewModel in two
This commit is contained in:
parent
8f33f1f0c9
commit
3071c079ba
8 changed files with 365 additions and 276 deletions
|
|
@ -70,6 +70,7 @@ import org.linphone.ui.main.chat.model.ChatMessageReactionsModel
|
|||
import org.linphone.ui.main.chat.view.RichEditText
|
||||
import org.linphone.ui.main.chat.viewmodel.ConversationViewModel
|
||||
import org.linphone.ui.main.chat.viewmodel.ConversationViewModel.Companion.SCROLLING_POSITION_NOT_SET
|
||||
import org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel
|
||||
import org.linphone.ui.main.fragment.GenericFragment
|
||||
import org.linphone.utils.AppUtils
|
||||
import org.linphone.utils.Event
|
||||
|
|
@ -90,6 +91,8 @@ class ConversationFragment : GenericFragment() {
|
|||
|
||||
private lateinit var viewModel: ConversationViewModel
|
||||
|
||||
private lateinit var sendMessageViewModel: SendMessageInConversationViewModel
|
||||
|
||||
private lateinit var adapter: ConversationEventAdapter
|
||||
|
||||
private lateinit var bottomSheetAdapter: ChatMessageBottomSheetAdapter
|
||||
|
|
@ -107,7 +110,7 @@ class ConversationFragment : GenericFragment() {
|
|||
Log.i("$TAG Picked file [$uri] matching path is [$path]")
|
||||
if (path != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
viewModel.addAttachment(path)
|
||||
sendMessageViewModel.addAttachment(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -139,12 +142,13 @@ class ConversationFragment : GenericFragment() {
|
|||
}
|
||||
|
||||
override fun afterTextChanged(p0: Editable?) {
|
||||
viewModel.isParticipantsListOpen.value = false
|
||||
sendMessageViewModel.isParticipantsListOpen.value = false
|
||||
|
||||
val split = p0.toString().split(" ")
|
||||
for (part in split) {
|
||||
if (part == "@") {
|
||||
viewModel.isParticipantsListOpen.value = true
|
||||
Log.i("$TAG '@' found, opening participants list")
|
||||
sendMessageViewModel.isParticipantsListOpen.value = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -182,7 +186,10 @@ class ConversationFragment : GenericFragment() {
|
|||
binding.lifecycleOwner = viewLifecycleOwner
|
||||
|
||||
viewModel = ViewModelProvider(this)[ConversationViewModel::class.java]
|
||||
sendMessageViewModel = ViewModelProvider(this)[SendMessageInConversationViewModel::class.java]
|
||||
|
||||
binding.viewModel = viewModel
|
||||
binding.sendMessageViewModel = sendMessageViewModel
|
||||
|
||||
binding.setBackClickListener {
|
||||
goBack()
|
||||
|
|
@ -212,12 +219,13 @@ class ConversationFragment : GenericFragment() {
|
|||
goBack()
|
||||
// TODO: show toast
|
||||
}
|
||||
} else {
|
||||
sendMessageViewModel.configureChatRoom(viewModel.chatRoom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.events.observe(viewLifecycleOwner) { items ->
|
||||
val currentCount = adapter.itemCount
|
||||
adapter.submitList(items)
|
||||
Log.i("$TAG Events (messages) list updated with [${items.size}] items")
|
||||
|
||||
|
|
@ -298,13 +306,13 @@ class ConversationFragment : GenericFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
viewModel.emojiToAddEvent.observe(viewLifecycleOwner) {
|
||||
sendMessageViewModel.emojiToAddEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { emoji ->
|
||||
binding.sendArea.messageToSend.addCharacterAtPosition(emoji)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.participantUsernameToAddEvent.observe(viewLifecycleOwner) {
|
||||
sendMessageViewModel.participantUsernameToAddEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { username ->
|
||||
Log.i("$TAG Adding username [$username] after '@'")
|
||||
// Also add a space for convenience
|
||||
|
|
@ -312,16 +320,16 @@ class ConversationFragment : GenericFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
|
||||
viewModel.applyFilter(filter.trim())
|
||||
}
|
||||
|
||||
viewModel.requestKeyboardHidingEvent.observe(viewLifecycleOwner) {
|
||||
sendMessageViewModel.requestKeyboardHidingEvent.observe(viewLifecycleOwner) {
|
||||
it.consume {
|
||||
binding.search.hideKeyboard()
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.searchFilter.observe(viewLifecycleOwner) { filter ->
|
||||
viewModel.applyFilter(filter.trim())
|
||||
}
|
||||
|
||||
viewModel.focusSearchBarEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { show ->
|
||||
if (show) {
|
||||
|
|
@ -346,6 +354,13 @@ class ConversationFragment : GenericFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
viewModel.isGroup.observe(viewLifecycleOwner) { group ->
|
||||
if (group) {
|
||||
Log.i("$TAG Adding text observer to chat message sending area")
|
||||
binding.sendArea.messageToSend.addTextChangedListener(textObserver)
|
||||
}
|
||||
}
|
||||
|
||||
viewModel.conferenceToJoinEvent.observe(viewLifecycleOwner) {
|
||||
it.consume { conferenceUri ->
|
||||
Log.i("$TAG Requesting to go to waiting room for conference URI [$conferenceUri]")
|
||||
|
|
@ -378,7 +393,7 @@ class ConversationFragment : GenericFragment() {
|
|||
Log.i("$TAG Rich content URI [$uri] matching path is [$path]")
|
||||
if (path != null) {
|
||||
withContext(Dispatchers.Main) {
|
||||
viewModel.addAttachment(path)
|
||||
sendMessageViewModel.addAttachment(path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -390,7 +405,7 @@ class ConversationFragment : GenericFragment() {
|
|||
if (files.isNotEmpty()) {
|
||||
Log.i("$TAG Found [${files.size}] files to share from intent")
|
||||
for (path in files) {
|
||||
viewModel.addAttachment(path)
|
||||
sendMessageViewModel.addAttachment(path)
|
||||
}
|
||||
|
||||
sharedViewModel.filesToShareFromIntent.value = arrayListOf()
|
||||
|
|
@ -401,13 +416,13 @@ class ConversationFragment : GenericFragment() {
|
|||
RichEditText.RichEditTextSendListener {
|
||||
override fun onControlEnterPressedAndReleased() {
|
||||
Log.i("$TAG Detected left control + enter key presses, sending message")
|
||||
viewModel.sendMessage()
|
||||
sendMessageViewModel.sendMessage()
|
||||
}
|
||||
})
|
||||
|
||||
binding.root.setKeyboardInsetListener { keyboardVisible ->
|
||||
if (keyboardVisible) {
|
||||
viewModel.isEmojiPickerOpen.value = false
|
||||
sendMessageViewModel.isEmojiPickerOpen.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -430,10 +445,6 @@ 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() {
|
||||
|
|
@ -497,7 +508,7 @@ class ConversationFragment : GenericFragment() {
|
|||
|
||||
layout.setReplyClickListener {
|
||||
Log.i("$TAG Updating sending area to reply to selected message")
|
||||
viewModel.replyToMessage(chatMessageModel)
|
||||
sendMessageViewModel.replyToMessage(chatMessageModel)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -23,12 +23,9 @@ import androidx.annotation.UiThread
|
|||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.R
|
||||
import org.linphone.core.Address
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomListenerStub
|
||||
import org.linphone.core.EventLog
|
||||
|
|
@ -37,12 +34,9 @@ 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.FileModel
|
||||
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
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class ConversationViewModel @UiThread constructor() : ViewModel() {
|
||||
|
|
@ -67,30 +61,10 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
|
|||
|
||||
val composingLabel = MutableLiveData<String>()
|
||||
|
||||
val textToSend = MutableLiveData<String>()
|
||||
|
||||
val searchBarVisible = MutableLiveData<Boolean>()
|
||||
|
||||
val searchFilter = MutableLiveData<String>()
|
||||
|
||||
val isEmojiPickerOpen = MutableLiveData<Boolean>()
|
||||
|
||||
val isParticipantsListOpen = MutableLiveData<Boolean>()
|
||||
|
||||
val participants = MutableLiveData<ArrayList<ParticipantModel>>()
|
||||
|
||||
val isFileAttachmentsListOpen = MutableLiveData<Boolean>()
|
||||
|
||||
val attachments = MutableLiveData<ArrayList<FileModel>>()
|
||||
|
||||
val isReplying = MutableLiveData<Boolean>()
|
||||
|
||||
val isReplyingTo = MutableLiveData<String>()
|
||||
|
||||
val isReplyingToMessage = MutableLiveData<String>()
|
||||
|
||||
val voiceRecordingInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
var scrollingPosition: Int = SCROLLING_POSITION_NOT_SET
|
||||
|
||||
val requestKeyboardHidingEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
|
|
@ -113,20 +87,10 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
|
|||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val emojiToAddEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val participantUsernameToAddEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val chatRoomFoundEvent = MutableLiveData<Event<Boolean>>()
|
||||
|
||||
lateinit var chatRoom: ChatRoom
|
||||
|
||||
private var chatMessageToReplyTo: ChatMessage? = null
|
||||
|
||||
private val chatRoomListener = object : ChatRoomListenerStub() {
|
||||
@WorkerThread
|
||||
override fun onChatMessageSending(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
|
|
@ -204,32 +168,15 @@ 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 {
|
||||
searchBarVisible.value = false
|
||||
isEmojiPickerOpen.value = false
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
viewModelScope.launch {
|
||||
for (file in attachments.value.orEmpty()) {
|
||||
file.deleteFile()
|
||||
}
|
||||
}
|
||||
|
||||
coreContext.postOnCoreThread {
|
||||
if (::chatRoom.isInitialized) {
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
|
|
@ -319,96 +266,6 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun toggleEmojiPickerVisibility() {
|
||||
isEmojiPickerOpen.value = isEmojiPickerOpen.value == false
|
||||
if (isEmojiPickerOpen.value == true) {
|
||||
requestKeyboardHidingEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun insertEmoji(emoji: String) {
|
||||
emojiToAddEvent.value = Event(emoji)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun replyToMessage(model: ChatMessageModel) {
|
||||
coreContext.postOnCoreThread {
|
||||
val message = model.chatMessage
|
||||
Log.i("$TAG Pending reply to chat message [${message.messageId}]")
|
||||
chatMessageToReplyTo = message
|
||||
isReplyingTo.postValue(model.avatarModel.friend.name)
|
||||
isReplyingToMessage.postValue(LinphoneUtils.getTextDescribingMessage(message))
|
||||
isReplying.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun cancelReply() {
|
||||
Log.i("$TAG Cancelling reply")
|
||||
isReplying.value = false
|
||||
chatMessageToReplyTo = null
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun sendMessage() {
|
||||
coreContext.postOnCoreThread {
|
||||
val messageToReplyTo = chatMessageToReplyTo
|
||||
val message = if (messageToReplyTo != null) {
|
||||
Log.i("$TAG Sending message as reply to [${messageToReplyTo.messageId}]")
|
||||
chatRoom.createReplyMessage(messageToReplyTo)
|
||||
} else {
|
||||
chatRoom.createEmptyMessage()
|
||||
}
|
||||
|
||||
val toSend = textToSend.value.orEmpty().trim()
|
||||
if (toSend.isNotEmpty()) {
|
||||
message.addUtf8TextContent(toSend)
|
||||
}
|
||||
|
||||
for (attachment in attachments.value.orEmpty()) {
|
||||
val content = Factory.instance().createContent()
|
||||
|
||||
content.type = when (attachment.mimeType) {
|
||||
FileUtils.MimeType.Image -> "image"
|
||||
FileUtils.MimeType.Audio -> "audio"
|
||||
FileUtils.MimeType.Video -> "video"
|
||||
FileUtils.MimeType.Pdf -> "application"
|
||||
FileUtils.MimeType.PlainText -> "text"
|
||||
else -> "file"
|
||||
}
|
||||
content.subtype = if (attachment.mimeType == FileUtils.MimeType.PlainText) {
|
||||
"plain"
|
||||
} else {
|
||||
FileUtils.getExtensionFromFileName(attachment.fileName)
|
||||
}
|
||||
content.name = attachment.fileName
|
||||
content.filePath = attachment.file // Let the file body handler take care of the upload
|
||||
|
||||
message.addFileContent(content)
|
||||
}
|
||||
|
||||
if (message.contents.isNotEmpty()) {
|
||||
Log.i("$TAG Sending message")
|
||||
message.send()
|
||||
}
|
||||
|
||||
Log.i("$TAG Message sent, re-setting defaults")
|
||||
textToSend.postValue("")
|
||||
isReplying.postValue(false)
|
||||
isFileAttachmentsListOpen.postValue(false)
|
||||
isParticipantsListOpen.postValue(false)
|
||||
isEmojiPickerOpen.postValue(false)
|
||||
|
||||
// Warning: do not delete files
|
||||
val attachmentsList = arrayListOf<FileModel>()
|
||||
attachments.postValue(attachmentsList)
|
||||
|
||||
chatMessageToReplyTo = null
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun deleteChatMessage(chatMessageModel: ChatMessageModel) {
|
||||
coreContext.postOnCoreThread {
|
||||
|
|
@ -428,85 +285,6 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
|
|||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun closeParticipantsList() {
|
||||
isParticipantsListOpen.value = false
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun closeFileAttachmentsList() {
|
||||
viewModelScope.launch {
|
||||
for (file in attachments.value.orEmpty()) {
|
||||
file.deleteFile()
|
||||
}
|
||||
}
|
||||
val list = arrayListOf<FileModel>()
|
||||
attachments.value = list
|
||||
|
||||
isFileAttachmentsListOpen.value = false
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun addAttachment(file: String) {
|
||||
val list = arrayListOf<FileModel>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
val model = FileModel(file) { file ->
|
||||
removeAttachment(file)
|
||||
}
|
||||
list.add(model)
|
||||
attachments.value = list
|
||||
|
||||
if (list.isNotEmpty()) {
|
||||
isFileAttachmentsListOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun removeAttachment(file: String, delete: Boolean = true) {
|
||||
val list = arrayListOf<FileModel>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
val found = list.find {
|
||||
it.file == file
|
||||
}
|
||||
if (found != null) {
|
||||
if (delete) {
|
||||
viewModelScope.launch {
|
||||
found.deleteFile()
|
||||
}
|
||||
}
|
||||
list.remove(found)
|
||||
} else {
|
||||
Log.w("$TAG Failed to find file attachment matching [$file]")
|
||||
}
|
||||
attachments.value = list
|
||||
|
||||
if (list.isEmpty()) {
|
||||
isFileAttachmentsListOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun startVoiceMessageRecording() {
|
||||
voiceRecordingInProgress.value = true
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun stopVoiceMessageRecording() {
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun cancelVoiceMessageRecording() {
|
||||
voiceRecordingInProgress.value = false
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun playVoiceMessageRecording() {
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun pauseVoiceMessageRecording() {
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun configureChatRoom() {
|
||||
scrollingPosition = SCROLLING_POSITION_NOT_SET
|
||||
|
|
@ -551,7 +329,6 @@ class ConversationViewModel @UiThread constructor() : ViewModel() {
|
|||
|
||||
computeEvents()
|
||||
chatRoom.markAsRead()
|
||||
computeParticipantsList()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
|
@ -686,24 +463,4 @@ 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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,312 @@
|
|||
/*
|
||||
* Copyright (c) 2010-2023 Belledonne Communications SARL.
|
||||
*
|
||||
* This file is part of linphone-android
|
||||
* (see https://www.linphone.org).
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.linphone.ui.main.chat.viewmodel
|
||||
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.linphone.LinphoneApplication.Companion.coreContext
|
||||
import org.linphone.core.ChatMessage
|
||||
import org.linphone.core.ChatRoom
|
||||
import org.linphone.core.ChatRoomListenerStub
|
||||
import org.linphone.core.EventLog
|
||||
import org.linphone.core.Factory
|
||||
import org.linphone.core.tools.Log
|
||||
import org.linphone.ui.main.chat.model.ChatMessageModel
|
||||
import org.linphone.ui.main.chat.model.FileModel
|
||||
import org.linphone.ui.main.chat.model.ParticipantModel
|
||||
import org.linphone.utils.Event
|
||||
import org.linphone.utils.FileUtils
|
||||
import org.linphone.utils.LinphoneUtils
|
||||
|
||||
class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() {
|
||||
companion object {
|
||||
private const val TAG = "[Send Message In Conversation ViewModel]"
|
||||
}
|
||||
|
||||
val textToSend = MutableLiveData<String>()
|
||||
|
||||
val isEmojiPickerOpen = MutableLiveData<Boolean>()
|
||||
|
||||
val isParticipantsListOpen = MutableLiveData<Boolean>()
|
||||
|
||||
val participants = MutableLiveData<ArrayList<ParticipantModel>>()
|
||||
|
||||
val isFileAttachmentsListOpen = MutableLiveData<Boolean>()
|
||||
|
||||
val attachments = MutableLiveData<ArrayList<FileModel>>()
|
||||
|
||||
val isReplying = MutableLiveData<Boolean>()
|
||||
|
||||
val isReplyingTo = MutableLiveData<String>()
|
||||
|
||||
val isReplyingToMessage = MutableLiveData<String>()
|
||||
|
||||
val voiceRecordingInProgress = MutableLiveData<Boolean>()
|
||||
|
||||
val requestKeyboardHidingEvent: MutableLiveData<Event<Boolean>> by lazy {
|
||||
MutableLiveData<Event<Boolean>>()
|
||||
}
|
||||
|
||||
val emojiToAddEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
val participantUsernameToAddEvent: MutableLiveData<Event<String>> by lazy {
|
||||
MutableLiveData<Event<String>>()
|
||||
}
|
||||
|
||||
lateinit var chatRoom: ChatRoom
|
||||
|
||||
private var chatMessageToReplyTo: ChatMessage? = null
|
||||
|
||||
private val chatRoomListener = object : ChatRoomListenerStub() {
|
||||
@WorkerThread
|
||||
override fun onParticipantAdded(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
computeParticipantsList()
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
override fun onParticipantRemoved(chatRoom: ChatRoom, eventLog: EventLog) {
|
||||
computeParticipantsList()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
isEmojiPickerOpen.value = false
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
viewModelScope.launch {
|
||||
for (file in attachments.value.orEmpty()) {
|
||||
file.deleteFile()
|
||||
}
|
||||
}
|
||||
|
||||
coreContext.postOnCoreThread {
|
||||
if (::chatRoom.isInitialized) {
|
||||
chatRoom.removeListener(chatRoomListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun configureChatRoom(room: ChatRoom) {
|
||||
chatRoom = room
|
||||
coreContext.postOnCoreThread {
|
||||
chatRoom.addListener(chatRoomListener)
|
||||
computeParticipantsList()
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun toggleEmojiPickerVisibility() {
|
||||
isEmojiPickerOpen.value = isEmojiPickerOpen.value == false
|
||||
if (isEmojiPickerOpen.value == true) {
|
||||
requestKeyboardHidingEvent.value = Event(true)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun insertEmoji(emoji: String) {
|
||||
emojiToAddEvent.value = Event(emoji)
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun replyToMessage(model: ChatMessageModel) {
|
||||
coreContext.postOnCoreThread {
|
||||
val message = model.chatMessage
|
||||
Log.i("$TAG Pending reply to chat message [${message.messageId}]")
|
||||
chatMessageToReplyTo = message
|
||||
isReplyingTo.postValue(model.avatarModel.friend.name)
|
||||
isReplyingToMessage.postValue(LinphoneUtils.getTextDescribingMessage(message))
|
||||
isReplying.postValue(true)
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun cancelReply() {
|
||||
Log.i("$TAG Cancelling reply")
|
||||
isReplying.value = false
|
||||
chatMessageToReplyTo = null
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun sendMessage() {
|
||||
coreContext.postOnCoreThread {
|
||||
val messageToReplyTo = chatMessageToReplyTo
|
||||
val message = if (messageToReplyTo != null) {
|
||||
Log.i("$TAG Sending message as reply to [${messageToReplyTo.messageId}]")
|
||||
chatRoom.createReplyMessage(messageToReplyTo)
|
||||
} else {
|
||||
chatRoom.createEmptyMessage()
|
||||
}
|
||||
|
||||
val toSend = textToSend.value.orEmpty().trim()
|
||||
if (toSend.isNotEmpty()) {
|
||||
message.addUtf8TextContent(toSend)
|
||||
}
|
||||
|
||||
for (attachment in attachments.value.orEmpty()) {
|
||||
val content = Factory.instance().createContent()
|
||||
|
||||
content.type = when (attachment.mimeType) {
|
||||
FileUtils.MimeType.Image -> "image"
|
||||
FileUtils.MimeType.Audio -> "audio"
|
||||
FileUtils.MimeType.Video -> "video"
|
||||
FileUtils.MimeType.Pdf -> "application"
|
||||
FileUtils.MimeType.PlainText -> "text"
|
||||
else -> "file"
|
||||
}
|
||||
content.subtype = if (attachment.mimeType == FileUtils.MimeType.PlainText) {
|
||||
"plain"
|
||||
} else {
|
||||
FileUtils.getExtensionFromFileName(attachment.fileName)
|
||||
}
|
||||
content.name = attachment.fileName
|
||||
content.filePath = attachment.file // Let the file body handler take care of the upload
|
||||
|
||||
message.addFileContent(content)
|
||||
}
|
||||
|
||||
if (message.contents.isNotEmpty()) {
|
||||
Log.i("$TAG Sending message")
|
||||
message.send()
|
||||
}
|
||||
|
||||
Log.i("$TAG Message sent, re-setting defaults")
|
||||
textToSend.postValue("")
|
||||
isReplying.postValue(false)
|
||||
isFileAttachmentsListOpen.postValue(false)
|
||||
isParticipantsListOpen.postValue(false)
|
||||
isEmojiPickerOpen.postValue(false)
|
||||
|
||||
// Warning: do not delete files
|
||||
val attachmentsList = arrayListOf<FileModel>()
|
||||
attachments.postValue(attachmentsList)
|
||||
|
||||
chatMessageToReplyTo = null
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun closeParticipantsList() {
|
||||
isParticipantsListOpen.value = false
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun closeFileAttachmentsList() {
|
||||
viewModelScope.launch {
|
||||
for (file in attachments.value.orEmpty()) {
|
||||
file.deleteFile()
|
||||
}
|
||||
}
|
||||
val list = arrayListOf<FileModel>()
|
||||
attachments.value = list
|
||||
|
||||
isFileAttachmentsListOpen.value = false
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun addAttachment(file: String) {
|
||||
val list = arrayListOf<FileModel>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
val model = FileModel(file) { file ->
|
||||
removeAttachment(file)
|
||||
}
|
||||
list.add(model)
|
||||
attachments.value = list
|
||||
|
||||
if (list.isNotEmpty()) {
|
||||
isFileAttachmentsListOpen.value = true
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun removeAttachment(file: String, delete: Boolean = true) {
|
||||
val list = arrayListOf<FileModel>()
|
||||
list.addAll(attachments.value.orEmpty())
|
||||
val found = list.find {
|
||||
it.file == file
|
||||
}
|
||||
if (found != null) {
|
||||
if (delete) {
|
||||
viewModelScope.launch {
|
||||
found.deleteFile()
|
||||
}
|
||||
}
|
||||
list.remove(found)
|
||||
} else {
|
||||
Log.w("$TAG Failed to find file attachment matching [$file]")
|
||||
}
|
||||
attachments.value = list
|
||||
|
||||
if (list.isEmpty()) {
|
||||
isFileAttachmentsListOpen.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun startVoiceMessageRecording() {
|
||||
voiceRecordingInProgress.value = true
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun stopVoiceMessageRecording() {
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun cancelVoiceMessageRecording() {
|
||||
voiceRecordingInProgress.value = false
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun playVoiceMessageRecording() {
|
||||
}
|
||||
|
||||
@UiThread
|
||||
fun pauseVoiceMessageRecording() {
|
||||
}
|
||||
|
||||
@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)
|
||||
}
|
||||
}
|
||||
|
|
@ -6,7 +6,7 @@
|
|||
<import type="android.view.View" />
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.ui.main.chat.viewmodel.ConversationViewModel" />
|
||||
type="org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
|
|
|||
|
|
@ -24,6 +24,9 @@
|
|||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.ui.main.chat.viewmodel.ConversationViewModel" />
|
||||
<variable
|
||||
name="sendMessageViewModel"
|
||||
type="org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
|
|
@ -231,7 +234,7 @@
|
|||
<include
|
||||
android:id="@+id/send_area"
|
||||
openFilePickerClickListener="@{openFilePickerClickListener}"
|
||||
viewModel="@{viewModel}"
|
||||
viewModel="@{sendMessageViewModel}"
|
||||
android:visibility="@{viewModel.isReadOnly ? View.GONE : View.VISIBLE}"
|
||||
layout="@layout/chat_conversation_send_area_bottom_sheet"/>
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<import type="android.view.View" />
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.ui.main.chat.viewmodel.ConversationViewModel" />
|
||||
type="org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<import type="android.view.View" />
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.ui.main.chat.viewmodel.ConversationViewModel" />
|
||||
type="org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@
|
|||
type="View.OnClickListener" />
|
||||
<variable
|
||||
name="viewModel"
|
||||
type="org.linphone.ui.main.chat.viewmodel.ConversationViewModel" />
|
||||
type="org.linphone.ui.main.chat.viewmodel.SendMessageInConversationViewModel" />
|
||||
</data>
|
||||
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
|
|
@ -20,6 +20,20 @@
|
|||
app:layout_behavior="com.google.android.material.bottomsheet.BottomSheetBehavior">
|
||||
<!-- Keep behavior to have it at the bottom -->
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/voice_recording"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{viewModel.voiceRecordingInProgress ? View.VISIBLE : View.GONE, default=gone}"
|
||||
app:constraint_referenced_ids="cancel_voice_message, voice_record_progress, stop_recording, voice_recording_length" />
|
||||
|
||||
<androidx.constraintlayout.widget.Group
|
||||
android:id="@+id/standard_messages"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="@{viewModel.voiceRecordingInProgress ? View.INVISIBLE : View.VISIBLE}"
|
||||
app:constraint_referenced_ids="emoji_picker_toggle, attach_file, message_area_background, message_to_send" />
|
||||
|
||||
<include
|
||||
android:id="@+id/reply_area"
|
||||
layout="@layout/chat_conversation_reply_area"
|
||||
|
|
@ -69,7 +83,6 @@
|
|||
android:onClick="@{() -> viewModel.toggleEmojiPickerVisibility()}"
|
||||
android:padding="8dp"
|
||||
android:src="@{viewModel.isEmojiPickerOpen ? @drawable/x : @drawable/smiley, default=@drawable/smiley}"
|
||||
android:visibility="@{viewModel.voiceRecordingInProgress ? View.GONE : View.VISIBLE}"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/message_area_background"
|
||||
|
|
@ -84,7 +97,6 @@
|
|||
android:padding="8dp"
|
||||
android:src="@drawable/x"
|
||||
android:background="@drawable/circle_white_button_background"
|
||||
android:visibility="@{viewModel.voiceRecordingInProgress ? View.VISIBLE : View.GONE, default=gone}"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/message_area_background"
|
||||
|
|
@ -99,7 +111,6 @@
|
|||
android:onClick="@{openFilePickerClickListener}"
|
||||
android:padding="8dp"
|
||||
android:src="@drawable/paperclip"
|
||||
android:visibility="@{viewModel.voiceRecordingInProgress ? View.GONE : View.VISIBLE}"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_area_background"
|
||||
app:layout_constraintEnd_toStartOf="@id/message_area_background"
|
||||
app:layout_constraintStart_toEndOf="@id/emoji_picker_toggle"
|
||||
|
|
@ -112,7 +123,6 @@
|
|||
android:layout_height="0dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:src="@drawable/edit_text_background"
|
||||
android:visibility="@{viewModel.voiceRecordingInProgress ? View.INVISIBLE : View.VISIBLE}"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_to_send"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/attach_file"
|
||||
|
|
@ -136,7 +146,6 @@
|
|||
android:text="@={viewModel.textToSend}"
|
||||
android:textColorHint="@color/gray_main2_400"
|
||||
android:textSize="14sp"
|
||||
android:visibility="@{viewModel.voiceRecordingInProgress ? View.INVISIBLE : View.VISIBLE}"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@id/send_barrier"
|
||||
app:layout_constraintStart_toStartOf="@id/message_area_background"
|
||||
|
|
@ -157,7 +166,6 @@
|
|||
android:layout_marginStart="20dp"
|
||||
android:layout_marginEnd="20dp"
|
||||
android:progressDrawable="@drawable/voice_recording_gradient_progress"
|
||||
android:visibility="@{viewModel.voiceRecordingInProgress ? View.VISIBLE : View.GONE, default=gone}"
|
||||
tools:progress="60"
|
||||
app:layout_constraintBottom_toBottomOf="@id/message_to_send"
|
||||
app:layout_constraintEnd_toStartOf="@id/send_message"
|
||||
|
|
@ -173,7 +181,6 @@
|
|||
android:padding="8dp"
|
||||
android:src="@drawable/stop_fill"
|
||||
android:background="@drawable/circle_white_button_background"
|
||||
android:visibility="@{viewModel.voiceRecordingInProgress ? View.VISIBLE : View.GONE, default=gone}"
|
||||
app:layout_constraintBottom_toBottomOf="@id/voice_record_progress"
|
||||
app:layout_constraintStart_toStartOf="@id/voice_record_progress"
|
||||
app:layout_constraintTop_toTopOf="@id/voice_record_progress"
|
||||
|
|
@ -193,7 +200,6 @@
|
|||
android:textSize="14sp"
|
||||
android:textColor="@color/gray_main2_600"
|
||||
android:background="@drawable/shape_squircle_white_r50_background"
|
||||
android:visibility="@{viewModel.voiceRecordingInProgress ? View.VISIBLE : View.GONE, default=gone}"
|
||||
app:layout_constraintBottom_toBottomOf="@id/voice_record_progress"
|
||||
app:layout_constraintEnd_toEndOf="@id/voice_record_progress"
|
||||
app:layout_constraintTop_toTopOf="@id/voice_record_progress"/>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue