Using RFC 9078 feature from SDK: IM reactions

This commit is contained in:
Sylvain Berfini 2022-11-07 15:00:20 +01:00
parent bfcdf19869
commit 8249f2c3a6
22 changed files with 717 additions and 27 deletions

View file

@ -10,6 +10,11 @@ Group changes to describe their impact on the project, as follows:
Fixed for any bug fixes.
Security to invite users to upgrade in case of vulnerabilities.
## [5.2.0] - Unreleased
### Added
- Chat messages emoji "reactions"
## [5.1.0] - Unreleased
### Added
@ -34,7 +39,20 @@ Group changes to describe their impact on the project, as follows:
### Fixed
- Messages not marked as reply in basic chat room if sending more than 1 content
## [5.0.10] - 2023-01-04
## [5.0.11] - 2023-05-09
### Fixed
- Wrong call displayed when hanging up a call while an incoming one is ringing
- Crash related to call history
- Crash due to wrongly format string
- Add/remove missing listener on FriendLists created after Core has been created
### Changed
- Improved GSM call interruption
- Updated translations
## [5.0.10] - 2023-04-04
### Fixed
- Plain copy of encrypted files (when VFS is enabled) not cleaned
- Avatar display issue if contact's "initials" contains more than 1 emoji or an emoji + a character

View file

@ -27,6 +27,7 @@ import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.PopupWindow
import android.widget.TextView
import androidx.databinding.DataBindingUtil
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.MutableLiveData
@ -108,6 +109,10 @@ class ChatMessagesListAdapter(
MutableLiveData<Event<ChatMessage>>()
}
val showReactionsListEvent: MutableLiveData<Event<ChatMessage>> by lazy {
MutableLiveData<Event<ChatMessage>>()
}
val errorEvent: MutableLiveData<Event<Int>> by lazy {
MutableLiveData<Event<Int>>()
}
@ -153,6 +158,10 @@ class ChatMessagesListAdapter(
callConferenceEvent.value = Event(Pair(address, subject))
}
override fun onShowReactionsList(chatMessage: ChatMessage) {
showReactionsListEvent.value = Event(chatMessage)
}
override fun onError(messageId: Int) {
errorEvent.value = Event(messageId)
}
@ -363,7 +372,7 @@ class ChatMessagesListAdapter(
)
val itemSize = AppUtils.getDimension(R.dimen.chat_message_popup_item_height).toInt()
var totalSize = itemSize * 7
var totalSize = itemSize * 8
if (chatMessage.chatRoom.hasCapability(
ChatRoom.Capabilities.OneToOne.toInt()
)
@ -410,6 +419,13 @@ class ChatMessagesListAdapter(
// Elevation is for showing a shadow around the popup
popupWindow.elevation = 20f
popupView.setEmojiClickListener {
val emoji = it as? TextView
if (emoji != null) {
reactToMessage(emoji.text.toString())
popupWindow.dismiss()
}
}
popupView.setResendClickListener {
resendMessage()
popupWindow.dismiss()
@ -448,6 +464,17 @@ class ChatMessagesListAdapter(
}
}
private fun reactToMessage(reaction: String) {
val chatMessage = binding.data?.chatMessage
if (chatMessage != null) {
Log.i(
"[Chat Message Data] Reacting to message [$chatMessage] with [$reaction] emoji"
)
val reactionMessage = chatMessage.createReaction(reaction)
reactionMessage.send()
}
}
private fun resendMessage() {
val chatMessage = binding.data?.chatMessage
if (chatMessage != null) {

View file

@ -521,5 +521,7 @@ interface OnContentClickedListener {
fun onCallConference(address: String, subject: String?)
fun onShowReactionsList(chatMessage: ChatMessage)
fun onError(messageId: Int)
}

View file

@ -30,6 +30,7 @@ import org.linphone.contact.ContactsUpdatedListenerStub
import org.linphone.contact.GenericContactData
import org.linphone.core.ChatMessage
import org.linphone.core.ChatMessageListenerStub
import org.linphone.core.ChatMessageReaction
import org.linphone.core.tools.Log
import org.linphone.utils.AppUtils
import org.linphone.utils.Event
@ -71,6 +72,8 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
MutableLiveData<Event<Boolean>>()
}
val reactions = MutableLiveData<ArrayList<String>>()
var hasPreviousMessage = false
var hasNextMessage = false
@ -85,6 +88,13 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
override fun onEphemeralMessageTimerStarted(message: ChatMessage) {
updateEphemeralTimer()
}
override fun onNewMessageReaction(message: ChatMessage, reaction: ChatMessageReaction) {
Log.i(
"[Chat Message Data] New reaction to display [${reaction.body}] from [${reaction.fromAddress.asStringUriOnly()}]"
)
updateReactionsList()
}
}
private val contactsListener = object : ContactsUpdatedListenerStub() {
@ -122,6 +132,8 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
if (contact.value == null) {
coreContext.contactsManager.addListener(contactsListener)
}
updateReactionsList()
}
override fun destroy() {
@ -179,6 +191,10 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
}
}
fun showReactionsList() {
contentListener?.onShowReactionsList(chatMessage)
}
private fun updateChatMessageState(state: ChatMessage.State) {
sendInProgress.value = when (state) {
ChatMessage.State.InProgress, ChatMessage.State.FileTransferInProgress, ChatMessage.State.FileTransferDone -> true
@ -265,6 +281,23 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes
contents.value = list
}
fun updateReactionsList() {
val reactionsList = arrayListOf<String>()
val allReactions = chatMessage.reactions
if (allReactions.isNotEmpty()) {
for (reaction in allReactions) {
val body = reaction.body
if (!reactionsList.contains(body)) {
reactionsList.add(body)
}
}
reactionsList.add(allReactions.size.toString())
}
reactions.value = reactionsList
}
private fun updateEphemeralTimer() {
if (chatMessage.isEphemeral) {
if (chatMessage.ephemeralExpireTime == 0L) {

View file

@ -0,0 +1,35 @@
/*
* Copyright (c) 2010-2022 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.activities.main.chat.data
import androidx.lifecycle.MutableLiveData
import org.linphone.contact.GenericContactData
import org.linphone.core.ChatMessageReaction
class ChatMessageReactionData(
chatMessageReaction: ChatMessageReaction
) : GenericContactData(chatMessageReaction.fromAddress) {
val reaction = MutableLiveData<String>()
init {
reaction.value = chatMessageReaction.body
}
}

View file

@ -0,0 +1,89 @@
/*
* Copyright (c) 2010-2022 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.activities.main.chat.data
import androidx.lifecycle.MutableLiveData
import org.linphone.core.ChatMessage
import org.linphone.core.ChatMessageListenerStub
import org.linphone.core.ChatMessageReaction
import org.linphone.core.tools.Log
class ChatMessageReactionsListData(private val chatMessage: ChatMessage) {
val reactions = MutableLiveData<ArrayList<ChatMessageReaction>>()
val filteredReactions = MutableLiveData<ArrayList<ChatMessageReactionData>>()
val reactionsMap = HashMap<String, Int>()
val listener = object : ChatMessageListenerStub() {
override fun onNewMessageReaction(message: ChatMessage, reaction: ChatMessageReaction) {
val address = reaction.fromAddress
Log.i(
"[Chat Message Reactions List] Reaction received [${reaction.body}] from [${address.asStringUriOnly()}]"
)
updateReactionsList(message)
}
}
private var filter = ""
init {
chatMessage.addListener(listener)
updateReactionsList(chatMessage)
}
fun onDestroy() {
chatMessage.removeListener(listener)
}
fun updateFilteredReactions(newFilter: String) {
filter = newFilter
filteredReactions.value.orEmpty().forEach(ChatMessageReactionData::destroy)
val reactionsList = arrayListOf<ChatMessageReactionData>()
for (reaction in reactions.value.orEmpty()) {
if (filter.isEmpty() || filter == reaction.body) {
val data = ChatMessageReactionData(reaction)
reactionsList.add(data)
}
}
filteredReactions.value = reactionsList
}
private fun updateReactionsList(chatMessage: ChatMessage) {
reactionsMap.clear()
val reactionsList = arrayListOf<ChatMessageReaction>()
for (reaction in chatMessage.reactions) {
val body = reaction.body
val count = if (reactionsMap.containsKey(body)) {
reactionsMap[body] ?: 1
} else {
1
}
reactionsMap[body] = count
reactionsList.add(reaction)
}
reactions.value = reactionsList
updateFilteredReactions(filter)
}
}

View file

@ -0,0 +1,97 @@
/*
* Copyright (c) 2010-2022 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.activities.main.chat.fragments
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.tabs.TabLayout
import org.linphone.R
import org.linphone.activities.main.chat.data.ChatMessageReactionsListData
import org.linphone.core.ChatMessage
import org.linphone.databinding.ChatMessageReactionsListDialogBinding
import org.linphone.utils.AppUtils
class ChatMessageReactionsListDialogFragment(
private val chatMessage: ChatMessage
) : BottomSheetDialogFragment() {
companion object {
const val TAG = "ChatMessageReactionsListDialogFragment"
}
private lateinit var data: ChatMessageReactionsListData
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val binding = ChatMessageReactionsListDialogBinding.inflate(layoutInflater)
binding.lifecycleOwner = viewLifecycleOwner
data = ChatMessageReactionsListData(chatMessage)
binding.data = data
data.reactions.observe(viewLifecycleOwner) {
binding.tabs.removeAllTabs()
binding.tabs.addTab(
binding.tabs.newTab().setText(
AppUtils.getStringWithPlural(
R.plurals.chat_message_reactions_count,
it.orEmpty().size
)
).setId(0)
)
var index = 1
data.reactionsMap.forEach { (key, value) ->
binding.tabs.addTab(
binding.tabs.newTab().setText("$key $value").setId(index).setTag(key)
)
index += 1
}
}
binding.tabs.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab) {
if (tab.id == 0) {
data.updateFilteredReactions("")
} else {
data.updateFilteredReactions(tab.tag.toString())
}
}
override fun onTabUnselected(tab: TabLayout.Tab) {
}
override fun onTabReselected(tab: TabLayout.Tab) {
}
})
return binding.root
}
override fun onDestroy() {
data.onDestroy()
super.onDestroy()
}
}

View file

@ -631,6 +631,18 @@ class DetailChatRoomFragment : MasterFragment<ChatRoomDetailFragmentBinding, Cha
}
}
adapter.showReactionsListEvent.observe(
viewLifecycleOwner
) {
it.consume { message ->
val modalBottomSheet = ChatMessageReactionsListDialogFragment(message)
modalBottomSheet.show(
parentFragmentManager,
ChatMessageReactionsListDialogFragment.TAG
)
}
}
adapter.errorEvent.observe(
viewLifecycleOwner
) {

View file

@ -217,6 +217,16 @@ class ContactsManager(private val context: Context) {
return null
}
@Synchronized
fun isAddressMyself(address: Address): Boolean {
for (friend in localFriends) {
if (friend.address?.weakEqual(address) == true) {
return true
}
}
return false
}
@Synchronized
fun addListener(listener: ContactsUpdatedListener) {
contactsUpdatedListeners.add(listener)

View file

@ -189,6 +189,70 @@ class NotificationsManager(private val context: Context) {
}
}
override fun onNewMessageReaction(
core: Core,
chatRoom: ChatRoom,
message: ChatMessage,
reaction: ChatMessageReaction
) {
val address = reaction.fromAddress
val defaultAccountAddress = core.defaultAccount?.params?.identityAddress
// Do not notify our own reactions, it won't be done anyway since the chat room is very likely to be currently displayed
if (defaultAccountAddress != null && defaultAccountAddress.weakEqual(address)) return
Log.i(
"[Notifications Manager] Reaction received [${reaction.body}] from [${address.asStringUriOnly()}] for chat message [$message]"
)
if (corePreferences.disableChat) return
if (corePreferences.preventInterfaceFromShowingUp) {
Log.w("[Notifications Manager] We were asked to not show the chat notifications")
return
}
if (currentlyDisplayedChatRoomAddress == chatRoom.peerAddress.asStringUriOnly()) {
Log.i(
"[Notifications Manager] Chat room is currently displayed, do not notify received reaction"
)
// Mark as read is now done in the DetailChatRoomFragment
return
}
val id = LinphoneUtils.getChatRoomId(chatRoom.localAddress, chatRoom.peerAddress)
val mute = corePreferences.chatRoomMuted(id)
if (mute) {
Log.i("[Notifications Manager] Chat room $id has been muted")
return
}
if (coreContext.contactsManager.isAddressMyself(address)) {
Log.i(
"[Notifications Manager] Reaction has been sent by ourselves, do not notify it"
)
return
}
if (corePreferences.chatRoomShortcuts) {
if (ShortcutsHelper.isShortcutToChatRoomAlreadyCreated(context, chatRoom)) {
Log.i("[Notifications Manager] Chat room shortcut already exists")
} else {
Log.i(
"[Notifications Manager] Ensure chat room shortcut exists for bubble notification"
)
ShortcutsHelper.createShortcutsToChatRooms(context)
}
}
val notifiable = createChatReactionNotifiable(chatRoom, reaction.body, address, message)
if (notifiable.messages.isNotEmpty()) {
displayChatNotifiable(chatRoom, notifiable)
} else {
Log.e(
"[Notifications Manager] Notifiable is empty but we should have displayed the reaction!"
)
}
}
override fun onChatRoomRead(core: Core, chatRoom: ChatRoom) {
val address = chatRoom.peerAddress.asStringUriOnly()
val notifiable = chatNotificationsMap[address]
@ -798,6 +862,37 @@ class NotificationsManager(private val context: Context) {
notifiable.isGroup = true
notifiable.groupTitle = room.subject
}
return notifiable
}
private fun createChatReactionNotifiable(
room: ChatRoom,
reaction: String,
from: Address,
message: ChatMessage
): Notifiable {
val notifiable = getNotifiableForRoom(room)
val friend = coreContext.contactsManager.findContactByAddress(from)
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, friend?.getThumbnailUri())
val displayName = friend?.name ?: LinphoneUtils.getDisplayName(from)
val originalMessage = LinphoneUtils.getTextDescribingMessage(message)
val text = AppUtils.getString(R.string.chat_message_reaction_received).format(
displayName,
reaction,
originalMessage
)
val notifiableMessage = NotifiableMessage(
text,
friend,
displayName,
message.time,
senderAvatar = roundPicture,
isOutgoing = false
)
notifiable.messages.add(notifiableMessage)
return notifiable
}
@ -812,6 +907,13 @@ class NotificationsManager(private val context: Context) {
notifiable.remoteAddress = room.peerAddress.asStringUriOnly()
chatNotificationsMap[address] = notifiable
if (room.hasCapability(ChatRoom.Capabilities.OneToOne.toInt())) {
notifiable.isGroup = false
} else {
notifiable.isGroup = true
notifiable.groupTitle = room.subject
}
}
return notifiable
}
@ -819,26 +921,7 @@ class NotificationsManager(private val context: Context) {
private fun getNotifiableMessage(message: ChatMessage, friend: Friend?): NotifiableMessage {
val roundPicture = ImageUtils.getRoundBitmapFromUri(context, friend?.getThumbnailUri())
val displayName = friend?.name ?: LinphoneUtils.getDisplayName(message.fromAddress)
var text = ""
val firstContent = message.contents.firstOrNull()
text = if (firstContent?.isIcalendar == true) {
AppUtils.getString(R.string.conference_invitation_received_notification)
} else if (firstContent?.isVoiceRecording == true) {
AppUtils.getString(R.string.chat_message_voice_recording_received_notification)
} else {
message.contents.find { content -> content.isText }?.utf8Text ?: ""
}
if (text.isEmpty()) {
for (content in message.contents) {
if (text.isNotEmpty()) {
text += ", "
}
text += content.name
}
}
val text = LinphoneUtils.getTextDescribingMessage(message)
val notifiableMessage = NotifiableMessage(
text,
friend,

View file

@ -39,6 +39,7 @@ import androidx.core.view.doOnLayout
import androidx.databinding.*
import androidx.emoji2.emojipicker.EmojiPickerView
import androidx.emoji2.emojipicker.EmojiViewItem
import androidx.lifecycle.LifecycleOwner
import coil.dispose
import coil.load
import coil.request.CachePolicy
@ -50,7 +51,6 @@ import org.linphone.BR
import org.linphone.LinphoneApplication.Companion.coreContext
import org.linphone.LinphoneApplication.Companion.corePreferences
import org.linphone.R
import org.linphone.activities.GenericActivity
import org.linphone.activities.main.settings.SettingListener
import org.linphone.activities.voip.data.ConferenceParticipantDeviceData
import org.linphone.activities.voip.views.ScrollDotsView
@ -258,7 +258,9 @@ fun setListener(view: SeekBar, lambda: (Any) -> Unit) {
fun setInflatedViewStubLifecycleOwner(view: View, enable: Boolean) {
val binding = DataBindingUtil.bind<ViewDataBinding>(view)
// This is a bit hacky...
binding?.lifecycleOwner = view.context as GenericActivity
if (view.context is LifecycleOwner) {
binding?.lifecycleOwner = view.context as? LifecycleOwner
}
}
@BindingAdapter("entries")
@ -296,7 +298,9 @@ private fun <T> setEntries(
binding.setVariable(BR.parent, parent)
// This is a bit hacky...
binding.lifecycleOwner = viewGroup.context as GenericActivity
if (viewGroup.context is LifecycleOwner) {
binding.lifecycleOwner = viewGroup.context as? LifecycleOwner
}
viewGroup.addView(binding.root)
}

View file

@ -309,5 +309,32 @@ class LinphoneUtils {
return null
}
fun getTextDescribingMessage(message: ChatMessage): String {
// If message contains text, then use that
var text = message.contents.find { content -> content.isText }?.utf8Text ?: ""
if (text.isEmpty()) {
val firstContent = message.contents.firstOrNull()
if (firstContent?.isIcalendar == true) {
text = AppUtils.getString(
R.string.conference_invitation_notification_short_desc
)
} else if (firstContent?.isVoiceRecording == true) {
text = AppUtils.getString(
R.string.chat_message_voice_recording_notification_short_desc
)
} else {
for (content in message.contents) {
if (text.isNotEmpty()) {
text += ", "
}
text += content.name
}
}
}
return text
}
}
}

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android" android:shape="rectangle">
<corners android:radius="5dp"/>
<size android:height="5dp" android:width="30dp"/>
<solid android:color="@color/voip_light_gray"/>
</shape>

View file

@ -89,6 +89,7 @@
<LinearLayout
android:id="@+id/background"
android:background="@drawable/chat_bubble_outgoing_full"
backgroundImage="@{data.backgroundRes, default=@drawable/chat_bubble_outgoing_full}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
@ -171,7 +172,7 @@
android:longClickable="true"
android:onClick="@{clickListener}"
android:onLongClick="@{contextMenuClickListener}"
android:text="@{data.text}"
android:text="@{data.text, default=`Lorem Ipsum: dolor sit amet`}"
android:textColor="@color/dark_grey_color"
android:textSize="@{data.isTextEmoji ? @dimen/chat_message_emoji_font_size : @dimen/chat_message_text_font_size, default=@dimen/chat_message_text_font_size}"
android:textStyle="normal"
@ -196,6 +197,25 @@
</LinearLayout>
<LinearLayout
android:onClick="@{() -> data.showReactionsList()}"
android:id="@+id/reactions"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="@drawable/chat_bubble_outgoing_full"
backgroundImage="@{data.backgroundRes, default=@drawable/chat_bubble_outgoing_full}"
android:padding="2dp"
android:layout_marginTop="-10dp"
android:layout_marginStart="5dp"
android:layout_marginEnd="5dp"
android:layout_below="@id/background"
android:layout_alignLeft="@{data.chatMessage.outgoing || selectionListViewModel.isEditionEnabled ? 0 : @id/background}"
android:layout_alignRight="@{data.chatMessage.outgoing || selectionListViewModel.isEditionEnabled ? @id/background : 0}"
android:orientation="horizontal"
android:gravity="center_vertical"
entries="@{data.reactions}"
layout="@{@layout/chat_message_reaction}" />
<TextView
android:id="@+id/time"
android:layout_width="wrap_content"
@ -206,7 +226,7 @@
android:layout_marginTop="7dp"
android:fontFamily="sans-serif"
android:lineSpacingExtra="0sp"
android:text="@{data.chatMessage.outgoing ? data.time : data.time + ` - ` + (data.contact.name ?? data.displayName)}"
android:text="@{data.chatMessage.outgoing ? data.time : data.time + ` - ` + (data.contact.name ?? data.displayName), default=`07/11 13:19 - John Doe`}"
android:textColor="@color/chat_bubble_text_color"
android:textSize="13sp"
android:textStyle="normal"

View file

@ -4,6 +4,9 @@
<data>
<import type="android.view.View" />
<variable
name="emojiClickListener"
type="View.OnClickListener" />
<variable
name="resendClickListener"
type="View.OnClickListener" />
@ -51,6 +54,77 @@
android:orientation="vertical"
android:background="?attr/backgroundColor">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="@dimen/chat_message_popup_item_height">
<TextView
style="@style/chat_message_emoji_reaction_font"
android:onClick="@{emojiClickListener}"
android:id="@+id/love"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@string/emoji_love"
app:layout_constraintEnd_toStartOf="@id/thumbs_up"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
style="@style/chat_message_emoji_reaction_font"
android:onClick="@{emojiClickListener}"
android:id="@+id/thumbs_up"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@string/emoji_thumbs_up"
app:layout_constraintEnd_toStartOf="@id/laughing"
app:layout_constraintStart_toEndOf="@id/love"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
style="@style/chat_message_emoji_reaction_font"
android:onClick="@{emojiClickListener}"
android:id="@+id/laughing"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@string/emoji_laughing"
app:layout_constraintEnd_toStartOf="@id/surprised"
app:layout_constraintStart_toEndOf="@id/thumbs_up"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
style="@style/chat_message_emoji_reaction_font"
android:onClick="@{emojiClickListener}"
android:id="@+id/surprised"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@string/emoji_surprised"
app:layout_constraintEnd_toStartOf="@id/tear"
app:layout_constraintStart_toEndOf="@id/laughing"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
<TextView
style="@style/chat_message_emoji_reaction_font"
android:onClick="@{emojiClickListener}"
android:id="@+id/tear"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="10dp"
android:text="@string/emoji_tear"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/surprised"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
<TextView
android:layout_width="match_parent"
android:layout_height="@dimen/chat_message_popup_item_height"

View file

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<variable
name="data"
type="String" />
</data>
<TextView
android:textSize="@{data.matches(`\\d+`) ? @dimen/chat_message_emoji_reactions_count_font_size : @dimen/chat_message_emoji_reaction_font_size, default=@dimen/chat_message_emoji_reaction_font_size}"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:text="@{data, default=@string/emoji_love}"/>
</layout>

View file

@ -0,0 +1,53 @@
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<import type="android.view.View" />
<variable
name="data"
type="org.linphone.activities.main.chat.data.ChatMessageReactionData" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="10dp">
<ImageView
coilContact="@{data}"
android:id="@+id/avatar"
android:layout_width="@dimen/contact_avatar_size"
android:layout_height="@dimen/contact_avatar_size"
android:background="@drawable/generated_avatar_bg"
android:contentDescription="@null"
android:src="@drawable/voip_single_contact_avatar"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
style="@style/contact_name_list_cell_font"
android:id="@+id/contact"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:text="@{data.contact.name ?? data.displayName}"
app:layout_constraintStart_toEndOf="@id/avatar"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
<TextView
style="@style/chat_message_emoji_reaction_font"
android:id="@+id/reaction"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:text="@{data.reaction}"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"/>
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<layout
xmlns:android="http://schemas.android.com/apk/res/android">
<data>
<import type="android.view.View" />
<variable
name="data"
type="org.linphone.activities.main.chat.data.ChatMessageReactionsListData" />
</data>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:background="?attr/backgroundColor">
<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:layout_marginTop="10dp"
android:layout_marginBottom="10dp"
android:src="@drawable/shape_dialog_handle" />
<com.google.android.material.tabs.TabLayout
android:id="@+id/tabs"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginStart="2dp"
android:layout_marginEnd="2dp"
android:layout_marginBottom="10dp"
android:background="?dividerColor" />
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
entries="@{data.filteredReactions}"
layout="@{@layout/chat_message_reactions_list_cell}" />
</ScrollView>
</LinearLayout>
</layout>

View file

@ -785,4 +785,9 @@
<string name="assistant_alternative_way_create_account">Pour créer un compte avec votre email :</string>
<string name="assistant_no_push_warning">Votre périphérique ne semble pas supporter les notifications \'push\'.\n\nVous ne pourrez donc pas créer des comptes dans l\'application mais vous pouvez toujours le faire sur notre site internet :</string>
<string name="assistant_create_email_account_not_validated">Votre compte n\'est pas activé, veuillez cliquer sur le lien que vous avez reçu par courriel</string>
<string name="chat_message_reaction_received">%s a réagi par %s à : %s</string>
<string name="chat_message_one_reaction">%s réaction</string>
<string name="chat_message_many_reactions">%s réactions</string>
<string name="conference_invitation_notification_short_desc">invitation à une conférence</string>
<string name="chat_message_voice_recording_notification_short_desc">message vocal</string>
</resources>

View file

@ -89,4 +89,6 @@
<dimen name="chat_room_emoji_picker_height">290dp</dimen>
<dimen name="chat_message_text_font_size">15sp</dimen>
<dimen name="chat_message_emoji_font_size">45sp</dimen>
<dimen name="chat_message_emoji_reaction_font_size">20sp</dimen>
<dimen name="chat_message_emoji_reactions_count_font_size">14sp</dimen>
</resources>

View file

@ -234,6 +234,7 @@
<string name="chat_message_abort_removal">Abort</string>
<string name="chat_message_download_already_in_progress">Please wait for first download to finish before starting a new one</string>
<string name="chat_message_voice_recording_received_notification">You have received a voice message</string>
<string name="chat_message_voice_recording_notification_short_desc">voice message</string>
<string name="chat_message_voice_recording">Voice message</string>
<string name="chat_room_presence_online">Online</string>
<string name="chat_room_presence_last_seen_online_today">Online today at</string>
@ -242,6 +243,20 @@
<string name="chat_room_presence_away">Away</string>
<string name="chat_room_presence_do_not_disturb">Do not disturb</string>
<!-- Chat reactions -->
<string name="chat_message_reaction_received">%s has reacted by %s to: %s</string>
<string name="emoji_love" translatable="false">❤️</string>
<string name="emoji_thumbs_up" translatable="false">👍</string>
<string name="emoji_laughing" translatable="false">😂</string>
<string name="emoji_surprised" translatable="false">😮</string>
<string name="emoji_tear" translatable="false">😢</string>
<plurals name="chat_message_reactions_count" translatable="false">
<item quantity="one">@string/chat_message_one_reaction</item>
<item quantity="other">@string/chat_message_many_reactions</item>
</plurals>
<string name="chat_message_one_reaction">%s reaction</string>
<string name="chat_message_many_reactions">%s reactions</string>
<!-- Recordings -->
<string name="recordings_empty_list">No recordings</string>
<string name="recordings_export">Export recording using…</string>
@ -308,6 +323,7 @@
<string name="conference_info_removed">Meeting info has been deleted</string>
<string name="conference_empty">You are currently alone in this group call</string>
<string name="conference_invitation_received_notification">You have been invited to a meeting</string>
<string name="conference_invitation_notification_short_desc">meeting invitation</string>
<string name="conference_invitation">Meeting invitation</string>
<string name="conference_low_bandwidth">Low bandwidth detected, disabling video</string>
<string name="conference_first_to_join">You\'re the first to join the group call</string>

View file

@ -353,6 +353,10 @@
<item name="android:fontFamily">sans-serif</item>
</style>
<style name="chat_message_emoji_reaction_font">
<item name="android:textSize">@dimen/chat_message_emoji_reaction_font_size</item>
</style>
<!-- Dialog related -->
<style name="dialog_title_font" parent="@android:style/TextAppearance.Medium">