diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc330195..5ed65195d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt index 9a8bbea1c..9f5f39200 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/adapters/ChatMessagesListAdapter.kt @@ -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>() } + val showReactionsListEvent: MutableLiveData> by lazy { + MutableLiveData>() + } + val errorEvent: MutableLiveData> by lazy { MutableLiveData>() } @@ -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) { diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt index d97232dc0..03c9eacd7 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageContentData.kt @@ -521,5 +521,7 @@ interface OnContentClickedListener { fun onCallConference(address: String, subject: String?) + fun onShowReactionsList(chatMessage: ChatMessage) + fun onError(messageId: Int) } diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt index 555a9afe7..150856853 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageData.kt @@ -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>() } + val reactions = MutableLiveData>() + 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() + 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) { diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionData.kt new file mode 100644 index 000000000..c73b423d8 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionData.kt @@ -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 . + */ +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() + + init { + reaction.value = chatMessageReaction.body + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionsListData.kt b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionsListData.kt new file mode 100644 index 000000000..83a147b2e --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/data/ChatMessageReactionsListData.kt @@ -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 . + */ +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>() + + val filteredReactions = MutableLiveData>() + + val reactionsMap = HashMap() + + 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() + 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() + 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) + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatMessageReactionsListDialogFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatMessageReactionsListDialogFragment.kt new file mode 100644 index 000000000..8d08e8c26 --- /dev/null +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/ChatMessageReactionsListDialogFragment.kt @@ -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 . + */ +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() + } +} diff --git a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt index 2283d648e..2520d288e 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/fragments/DetailChatRoomFragment.kt @@ -631,6 +631,18 @@ class DetailChatRoomFragment : MasterFragment + val modalBottomSheet = ChatMessageReactionsListDialogFragment(message) + modalBottomSheet.show( + parentFragmentManager, + ChatMessageReactionsListDialogFragment.TAG + ) + } + } + adapter.errorEvent.observe( viewLifecycleOwner ) { diff --git a/app/src/main/java/org/linphone/contact/ContactsManager.kt b/app/src/main/java/org/linphone/contact/ContactsManager.kt index 623cc1be9..7892a3f8f 100644 --- a/app/src/main/java/org/linphone/contact/ContactsManager.kt +++ b/app/src/main/java/org/linphone/contact/ContactsManager.kt @@ -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) diff --git a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt index b301d63ec..c5d6f4594 100644 --- a/app/src/main/java/org/linphone/notifications/NotificationsManager.kt +++ b/app/src/main/java/org/linphone/notifications/NotificationsManager.kt @@ -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, diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 7fffb7b22..b4f72f1c2 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -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(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 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) } diff --git a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt index a2d848c27..8258581fb 100644 --- a/app/src/main/java/org/linphone/utils/LinphoneUtils.kt +++ b/app/src/main/java/org/linphone/utils/LinphoneUtils.kt @@ -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 + } } } diff --git a/app/src/main/res/drawable/shape_dialog_handle.xml b/app/src/main/res/drawable/shape_dialog_handle.xml new file mode 100644 index 000000000..030e5bcf1 --- /dev/null +++ b/app/src/main/res/drawable/shape_dialog_handle.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_message_list_cell.xml b/app/src/main/res/layout/chat_message_list_cell.xml index 71b7d4b89..61fa9c16f 100644 --- a/app/src/main/res/layout/chat_message_list_cell.xml +++ b/app/src/main/res/layout/chat_message_list_cell.xml @@ -89,6 +89,7 @@ + + + @@ -51,6 +54,77 @@ android:orientation="vertical" android:background="?attr/backgroundColor"> + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_message_reactions_list_cell.xml b/app/src/main/res/layout/chat_message_reactions_list_cell.xml new file mode 100644 index 000000000..e17e672ce --- /dev/null +++ b/app/src/main/res/layout/chat_message_reactions_list_cell.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/chat_message_reactions_list_dialog.xml b/app/src/main/res/layout/chat_message_reactions_list_dialog.xml new file mode 100644 index 000000000..eba884dbe --- /dev/null +++ b/app/src/main/res/layout/chat_message_reactions_list_dialog.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 4ca897fd9..2478ffe60 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -785,4 +785,9 @@ Pour créer un compte avec votre email : 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 : Votre compte n\'est pas activé, veuillez cliquer sur le lien que vous avez reçu par courriel + %s a réagi par %s à : %s + %s réaction + %s réactions + invitation à une conférence + message vocal \ No newline at end of file diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 0f0e3918b..7c7b7fa34 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -89,4 +89,6 @@ 290dp 15sp 45sp + 20sp + 14sp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cc6f60741..a2376c008 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -234,6 +234,7 @@ Abort Please wait for first download to finish before starting a new one You have received a voice message + voice message Voice message Online Online today at @@ -242,6 +243,20 @@ Away Do not disturb + + %s has reacted by %s to: %s + ❤️ + 👍 + 😂 + 😮 + 😢 + + @string/chat_message_one_reaction + @string/chat_message_many_reactions + + %s reaction + %s reactions + No recordings Export recording using… @@ -308,6 +323,7 @@ Meeting info has been deleted You are currently alone in this group call You have been invited to a meeting + meeting invitation Meeting invitation Low bandwidth detected, disabling video You\'re the first to join the group call diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index e78f1af08..bafbcbfdf 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -353,6 +353,10 @@ sans-serif + +