Added message forward + a few small improvements

This commit is contained in:
Sylvain Berfini 2023-12-13 13:34:52 +01:00
parent 678949aff2
commit d52c12606f
8 changed files with 115 additions and 51 deletions

View file

@ -278,6 +278,14 @@ class ConversationFragment : SlidingPaneChildFragment() {
}
} else {
sendMessageViewModel.configureChatRoom(viewModel.chatRoom)
// Wait for chat room to be ready before trying to forward a message in it
sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner) {
it.consume { toForward ->
Log.i("$TAG Found message to forward")
sendMessageViewModel.forwardMessage(toForward)
}
}
}
}
}
@ -297,7 +305,7 @@ class ConversationFragment : SlidingPaneChildFragment() {
viewModel.scrollToBottomEvent.observe(viewLifecycleOwner) {
it.consume {
binding.eventsList.scrollToPosition(adapter.itemCount - 1)
scrollToBottom()
}
}
@ -516,7 +524,7 @@ class ConversationFragment : SlidingPaneChildFragment() {
sendMessageViewModel.isEmojiPickerOpen.value = false
// Scroll to bottom when keyboard is opened so latest message is visible
binding.eventsList.scrollToPosition(adapter.itemCount - 1)
scrollToBottom()
}
}
}
@ -533,7 +541,7 @@ class ConversationFragment : SlidingPaneChildFragment() {
if (viewModel.scrollingPosition != SCROLLING_POSITION_NOT_SET) {
binding.eventsList.scrollToPosition(viewModel.scrollingPosition)
} else {
binding.eventsList.scrollToPosition(adapter.itemCount - 1)
scrollToBottom()
}
try {
@ -573,34 +581,40 @@ class ConversationFragment : SlidingPaneChildFragment() {
currentChatMessageModelForBottomSheet = null
}
private fun scrollToFirstUnreadMessageOrBottom(smooth: Boolean): Boolean {
if (adapter.itemCount > 0) {
val recyclerView = binding.eventsList
private fun scrollToBottom() {
if (adapter.itemCount == 0) return
// Scroll to first unread message if any, unless we are already on it
val firstUnreadMessagePosition = adapter.getFirstUnreadMessagePosition()
val currentPosition = (recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
val indexToScrollTo = if (firstUnreadMessagePosition != -1 && firstUnreadMessagePosition != currentPosition) {
firstUnreadMessagePosition
} else {
adapter.itemCount - 1
}
Log.i(
"$TAG Scrolling to position $indexToScrollTo, first unread message is at $firstUnreadMessagePosition"
)
if (smooth) {
recyclerView.smoothScrollToPosition(indexToScrollTo)
} else {
recyclerView.scrollToPosition(indexToScrollTo)
}
if (firstUnreadMessagePosition == 0) {
// Return true only if all unread messages don't fit in the recyclerview height
return recyclerView.computeVerticalScrollRange() > recyclerView.height
lifecycleScope.launch {
withContext(Dispatchers.IO) {
delay(100)
withContext(Dispatchers.Main) {
binding.eventsList.scrollToPosition(adapter.itemCount - 1)
}
}
}
return false
}
private fun scrollToFirstUnreadMessageOrBottom(smooth: Boolean) {
if (adapter.itemCount == 0) return
val recyclerView = binding.eventsList
// Scroll to first unread message if any, unless we are already on it
val firstUnreadMessagePosition = adapter.getFirstUnreadMessagePosition()
val currentPosition = (recyclerView.layoutManager as LinearLayoutManager).findFirstCompletelyVisibleItemPosition()
val indexToScrollTo = if (firstUnreadMessagePosition != -1 && firstUnreadMessagePosition != currentPosition) {
firstUnreadMessagePosition
} else {
adapter.itemCount - 1
}
Log.i(
"$TAG Scrolling to position $indexToScrollTo, first unread message is at $firstUnreadMessagePosition"
)
if (smooth) {
recyclerView.smoothScrollToPosition(indexToScrollTo)
} else {
recyclerView.scrollToPosition(indexToScrollTo)
}
}
private fun showChatMessageLongPressMenu(chatMessageModel: MessageModel) {
@ -648,6 +662,16 @@ class ConversationFragment : SlidingPaneChildFragment() {
dialog.dismiss()
}
layout.setForwardClickListener {
Log.i("$TAG Forwarding message, going back to conversations list")
// Remove observer before setting the message to forward
// as we don't want to forward it in this chat room
sharedViewModel.messageToForwardEvent.removeObservers(viewLifecycleOwner)
sharedViewModel.messageToForwardEvent.postValue(Event(chatMessageModel))
dialog.dismiss()
goBack()
}
layout.setReplyClickListener {
Log.i("$TAG Updating sending area to reply to selected message")
sendMessageViewModel.replyToMessage(chatMessageModel)

View file

@ -273,6 +273,39 @@ class ConversationsListFragment : AbstractTopBarFragment() {
}
}
sharedViewModel.messageToForwardEvent.observe(viewLifecycleOwner) { event ->
if (!event.consumed()) {
// Do not consume it yet
val message = getString(R.string.toast_message_waiting_to_be_forwarded)
val icon = R.drawable.forward
(requireActivity() as MainActivity).showGreenToast(message, icon)
Log.i("$TAG Found a message waiting to be forwarded")
}
}
sharedViewModel.filesToShareFromIntent.observe(viewLifecycleOwner) { filesToShare ->
val count = filesToShare.size
if (count > 0) {
val message = AppUtils.getStringWithPlural(
R.plurals.toast_files_waiting_to_be_shared,
count,
filesToShare.size.toString()
)
val icon = R.drawable.file
(requireActivity() as MainActivity).showGreenToast(message, icon)
Log.i("$TAG Found [$count] files waiting to be shared")
}
}
sharedViewModel.textToShareFromIntent.observe(viewLifecycleOwner) { textToShare ->
if (textToShare.isNotEmpty()) {
val message = getString(R.string.toast_text_waiting_to_be_shared)
val icon = R.drawable.file
(requireActivity() as MainActivity).showGreenToast(message, icon)
Log.i("$TAG Found text waiting to be shared")
}
}
// TopBarFragment related
setViewModelAndTitle(
@ -311,19 +344,6 @@ class ConversationsListFragment : AbstractTopBarFragment() {
Log.e("$TAG Failed to unregister data observer to adapter: $e")
}
val filesToShare = sharedViewModel.filesToShareFromIntent.value.orEmpty()
if (filesToShare.isNotEmpty()) {
val count = filesToShare.size
val message = AppUtils.getStringWithPlural(
R.plurals.toast_files_waiting_to_be_shared,
count,
filesToShare.size.toString()
)
val icon = R.drawable.file
(requireActivity() as MainActivity).showGreenToast(message, icon)
Log.i("$TAG Found [$count] files waiting to be shared")
}
// Scroll to top when fragment is resumed
binding.conversationsList.scrollToPosition(0)
}

View file

@ -348,6 +348,18 @@ class SendMessageInConversationViewModel @UiThread constructor() : ViewModel() {
}
}
@UiThread
fun forwardMessage(toForward: MessageModel) {
coreContext.postOnCoreThread {
if (::chatRoom.isInitialized) {
val messageToForward = toForward.chatMessage
val forwardedMessage = chatRoom.createForwardMessage(messageToForward)
Log.i("$TAG Sending forwarded message")
forwardedMessage.send()
}
}
}
@UiThread
fun startVoiceMessageRecording() {
if (ActivityCompat.checkSelfPermission(

View file

@ -24,6 +24,7 @@ import androidx.annotation.UiThread
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import org.linphone.core.ChatRoom
import org.linphone.ui.main.chat.model.MessageModel
import org.linphone.utils.Event
class SharedMainViewModel @UiThread constructor() : ViewModel() {
@ -102,6 +103,8 @@ class SharedMainViewModel @UiThread constructor() : ViewModel() {
val filesToShareFromIntent = MutableLiveData<ArrayList<String>>()
val messageToForwardEvent = MutableLiveData<Event<MessageModel>>()
var displayedChatRoom: ChatRoom? = null // Prevents the need to go look for the chat room
val showConversationEvent: MutableLiveData<Event<Pair<String, String>>> by lazy {
MutableLiveData<Event<Pair<String, String>>>()

View file

@ -37,4 +37,9 @@ open class Event<out T> @AnyThread constructor(private val content: T) {
handleContent(content)
}
}
@UiThread
fun consumed(): Boolean {
return handled.get()
}
}

View file

@ -82,7 +82,7 @@
android:id="@+id/reply_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginStart="5dp"
android:text="@{model.replyTo, default=`John Doe`}"
android:textSize="12sp"
android:textColor="?attr/color_main2_500"
@ -135,7 +135,7 @@
android:id="@+id/forward_label"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginStart="5dp"
android:text="@string/message_forwarded_label"
android:textSize="12sp"
android:textColor="?attr/color_main2_500"
@ -187,11 +187,10 @@
android:onClick="@{() -> model.firstImageClicked()}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxHeight="@dimen/chat_bubble_big_image_max_size"
android:layout_height="@dimen/chat_bubble_big_image_max_size"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:visibility="@{model.filesList.size() == 1 &amp;&amp; model.firstImagePath.length() >= 0 ? View.VISIBLE : View.GONE}"
android:visibility="@{model.filesList.size() == 1 &amp;&amp; model.firstImagePath.length() >= 0 ? View.VISIBLE : View.GONE, default=gone}"
coilBubble="@{model.firstImagePath}"/>
<ViewStub

View file

@ -58,7 +58,7 @@
android:id="@+id/reply_icon"
android:layout_width="@dimen/small_icon_size"
android:layout_height="@dimen/small_icon_size"
android:layout_marginEnd="8dp"
android:layout_marginEnd="5dp"
android:src="@drawable/reply"
app:layout_constraintEnd_toStartOf="@id/reply_name"
app:layout_constraintTop_toTopOf="@id/reply_name"
@ -112,7 +112,7 @@
android:id="@+id/forward_icon"
android:layout_width="@dimen/small_icon_size"
android:layout_height="@dimen/small_icon_size"
android:layout_marginEnd="8dp"
android:layout_marginEnd="5dp"
android:src="@drawable/forward"
android:visibility="@{model.isForward ? View.VISIBLE : View.GONE, default=gone}"
app:layout_constraintEnd_toStartOf="@id/forward_label"
@ -150,11 +150,10 @@
android:onClick="@{() -> model.firstImageClicked()}"
android:onLongClick="@{onLongClickListener}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxHeight="@dimen/chat_bubble_big_image_max_size"
android:layout_height="@dimen/chat_bubble_big_image_max_size"
android:adjustViewBounds="true"
android:scaleType="fitCenter"
android:visibility="@{model.filesList.size() == 1 &amp;&amp; model.firstImagePath.length() >= 0 ? View.VISIBLE : View.GONE}"
android:visibility="@{model.filesList.size() == 1 &amp;&amp; model.firstImagePath.length() >= 0 ? View.VISIBLE : View.GONE, default=gone}"
coilBubble="@{model.firstImagePath}"/>
<ViewStub

View file

@ -160,6 +160,8 @@
<item quantity="one">%s file waiting to be shared</item>
<item quantity="other">%s files waiting to be shared</item>
</plurals>
<string name="toast_text_waiting_to_be_shared">Text is waiting to be shared</string>
<string name="toast_message_waiting_to_be_forwarded">A message is waiting to be forwarded</string>
<string name="assistant_account_login">Login</string>
<string name="assistant_scan_qr_code">Scan QR code</string>