From 22d4a8baf19654974ac2fda493a78f25d4277185 Mon Sep 17 00:00:00 2001 From: Sylvain Berfini Date: Tue, 14 Mar 2023 17:07:22 +0100 Subject: [PATCH] Added emoji picker in chat --- CHANGELOG.md | 1 + app/build.gradle | 10 +-- .../main/chat/data/ChatMessageData.kt | 6 +- .../viewmodels/ChatMessageSendingViewModel.kt | 10 +++ .../java/org/linphone/core/CorePreferences.kt | 3 + .../main/java/org/linphone/utils/AppUtils.kt | 33 +++++++--- .../org/linphone/utils/DataBindingUtils.kt | 13 ++++ .../main/res/drawable-xhdpi/emoji_default.png | Bin 0 -> 5203 bytes app/src/main/res/drawable/edit.xml | 4 ++ app/src/main/res/drawable/emoji.xml | 19 ++++++ .../res/layout/chat_message_list_cell.xml | 2 +- app/src/main/res/layout/chat_room_sending.xml | 57 ++++++++++++++---- app/src/main/res/values/dimen.xml | 3 + 13 files changed, 135 insertions(+), 26 deletions(-) create mode 100644 app/src/main/res/drawable-xhdpi/emoji_default.png create mode 100644 app/src/main/res/drawable/emoji.xml diff --git a/CHANGELOG.md b/CHANGELOG.md index ea345c667..e4709b815 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ Group changes to describe their impact on the project, as follows: - Showing short term presence for contacts whom publish it + added setting to disable it (enabled by default for sip.linphone.org accounts) - Confirmation dialog before removing account - Attended transfer instead of blind transfer if there is more than 1 call +- Added emoji picker in chat room, and increase size of text if it only contains emojis - Added hidden setting to disable video completely - Added hidden setting to prevent adding / editing / removing native contacts - Added hidden setting to protect settings access using account password diff --git a/app/build.gradle b/app/build.gradle index 61fc0d523..7f076c908 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -194,14 +194,14 @@ android { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation 'androidx.appcompat:appcompat:1.6.1' - implementation 'androidx.core:core-ktx:1.9.0' + implementation 'androidx.core:core-ktx:1.10.0' implementation 'androidx.core:core-splashscreen:1.0.0' - implementation 'androidx.emoji2:emoji2:1.3.0' - implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.0' + implementation 'androidx.emoji2:emoji2:1.4.0-beta01' + implementation 'androidx.emoji2:emoji2-emojipicker:1.4.0-beta01' + implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.1' implementation 'androidx.media:media:1.6.0' implementation "androidx.security:security-crypto-ktx:1.1.0-alpha05" implementation "androidx.window:window:1.0.0" - implementation 'androidx.core:core-ktx:1.9.0' def nav_version = "2.5.3" implementation "androidx.navigation:navigation-fragment-ktx:$nav_version" @@ -219,7 +219,7 @@ dependencies { implementation 'com.google.android.flexbox:flexbox:3.0.0' // https://github.com/coil-kt/coil/blob/main/LICENSE.txt Apache v2.0 - def coil_version = "2.2.2" + def coil_version = "2.3.0" implementation("io.coil-kt:coil:$coil_version") implementation("io.coil-kt:coil-gif:$coil_version") implementation("io.coil-kt:coil-svg:$coil_version") 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 58cfdb409..b0b7c6433 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 @@ -56,6 +56,8 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes val text = MutableLiveData() + val isTextEmoji = MutableLiveData() + val replyData = MutableLiveData() val isDisplayed = MutableLiveData() @@ -188,7 +190,8 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes data.listener = contentListener list.add(data) } else if (content.isText) { - val spannable = Spannable.Factory.getInstance().newSpannable(content.utf8Text?.trim()) + val textContent = content.utf8Text.orEmpty().trim() + val spannable = Spannable.Factory.getInstance().newSpannable(textContent) text.value = PatternClickableSpan() .add( Pattern.compile("(?:)?"), @@ -217,6 +220,7 @@ class ChatMessageData(val chatMessage: ChatMessage) : GenericContactData(chatMes } } ).build(spannable) + isTextEmoji.value = AppUtils.isTextOnlyContainingEmoji(textContent) } else { Log.e("[Chat Message Data] Unexpected content with type: ${content.type}/${content.subtype}") } diff --git a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt index f2f188796..403ee5b5c 100644 --- a/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt +++ b/app/src/main/java/org/linphone/activities/main/chat/viewmodels/ChatMessageSendingViewModel.kt @@ -103,6 +103,10 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() EditorInfo.IME_FLAG_NO_EXTRACT_UI } + val isEmojiPickerOpen = MutableLiveData() + + val isEmojiPickerVisible = MutableLiveData() + private lateinit var recorder: Recorder private var voiceRecordAudioFocusRequest: AudioFocusRequestCompat? = null @@ -136,6 +140,8 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() attachFileEnabled.value = true sendMessageEnabled.value = false + isEmojiPickerOpen.value = false + isEmojiPickerVisible.value = corePreferences.showEmojiPickerButton updateChatRoomReadOnlyState() } @@ -208,6 +214,10 @@ class ChatMessageSendingViewModel(private val chatRoom: ChatRoom) : ViewModel() } } + fun toggleEmojiPicker() { + isEmojiPickerOpen.value = isEmojiPickerOpen.value == false + } + fun sendMessage() { if (!isPlayerClosed()) { stopVoiceRecordPlayer() diff --git a/app/src/main/java/org/linphone/core/CorePreferences.kt b/app/src/main/java/org/linphone/core/CorePreferences.kt index 28de260fb..8d5caea8e 100644 --- a/app/src/main/java/org/linphone/core/CorePreferences.kt +++ b/app/src/main/java/org/linphone/core/CorePreferences.kt @@ -498,6 +498,9 @@ class CorePreferences constructor(private val context: Context) { val allowEndToEndEncryptedChatWithoutPresence: Boolean get() = config.getBool("app", "allow_lime_friend_without_capability", false) + val showEmojiPickerButton: Boolean + get() = config.getBool("app", "show_emoji_picker", true) + // This will prevent UI from showing up, except for the launcher & the foreground service notification val preventInterfaceFromShowingUp: Boolean get() = config.getBool("app", "keep_app_invisible", false) diff --git a/app/src/main/java/org/linphone/utils/AppUtils.kt b/app/src/main/java/org/linphone/utils/AppUtils.kt index 81a538dfa..a8ae1a91c 100644 --- a/app/src/main/java/org/linphone/utils/AppUtils.kt +++ b/app/src/main/java/org/linphone/utils/AppUtils.kt @@ -45,6 +45,18 @@ import org.linphone.core.tools.Log */ class AppUtils { companion object { + var emojiCompat: EmojiCompat? = null + get() = initEmojiCompat() + + private fun initEmojiCompat(): EmojiCompat? { + return try { + EmojiCompat.get() + } catch (ise: IllegalStateException) { + Log.e("[App Utils] Can't get EmojiCompat: $ise") + null + } + } + fun getString(id: Int): String { return coreContext.context.getString(id) } @@ -68,16 +80,10 @@ class AppUtils { var initials = "" var characters = 0 - val emoji = try { - EmojiCompat.get() - } catch (ise: IllegalStateException) { - Log.e("[App Utils] Can't get EmojiCompat: $ise") - null - } - for (i in split.indices) { if (split[i].isNotEmpty()) { try { + val emoji = emojiCompat if (emoji?.hasEmojiGlyph(split[i]) == true) { val glyph = emoji.process(split[i]) if (characters > 0) { // Limit initial to 1 emoji only @@ -100,6 +106,19 @@ class AppUtils { return initials } + fun isTextOnlyContainingEmoji(text: String): Boolean { + val emoji = emojiCompat + emoji ?: return false + + for (split in text.split(" ")) { + // We only check the first and last chars of the split for commodity + if (emoji.getEmojiStart(split, 0) == -1 || emoji.getEmojiEnd(split, split.length - 1) == -1) { + return false + } + } + return true + } + fun pixelsToDp(pixels: Float): Float { return TypedValue.applyDimension( TypedValue.COMPLEX_UNIT_DIP, diff --git a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt index 57f3d0ed3..94e7454ba 100644 --- a/app/src/main/java/org/linphone/utils/DataBindingUtils.kt +++ b/app/src/main/java/org/linphone/utils/DataBindingUtils.kt @@ -34,6 +34,8 @@ import androidx.appcompat.content.res.AppCompatResources import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.Guideline import androidx.databinding.* +import androidx.emoji2.emojipicker.EmojiPickerView +import androidx.emoji2.emojipicker.EmojiViewItem import coil.dispose import coil.load import coil.request.CachePolicy @@ -792,3 +794,14 @@ fun ImageView.setPresenceIcon(presence: ConsolidatedPresence) { } setContentDescription(contentDescription) } + +interface EmojiPickedListener { + fun onEmojiPicked(item: EmojiViewItem) +} + +@BindingAdapter("emojiPickedListener") +fun EmojiPickerView.setEmojiPickedListener(listener: EmojiPickedListener) { + setOnEmojiPickedListener { emoji -> + listener.onEmojiPicked(emoji) + } +} diff --git a/app/src/main/res/drawable-xhdpi/emoji_default.png b/app/src/main/res/drawable-xhdpi/emoji_default.png new file mode 100644 index 0000000000000000000000000000000000000000..5633407379159610438de3ef2428bb2a6e985844 GIT binary patch literal 5203 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 + + + diff --git a/app/src/main/res/drawable/emoji.xml b/app/src/main/res/drawable/emoji.xml new file mode 100644 index 000000000..24c7d6d8d --- /dev/null +++ b/app/src/main/res/drawable/emoji.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + 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 d451c3976..71b7d4b89 100644 --- a/app/src/main/res/layout/chat_message_list_cell.xml +++ b/app/src/main/res/layout/chat_message_list_cell.xml @@ -173,7 +173,7 @@ android:onLongClick="@{contextMenuClickListener}" android:text="@{data.text}" android:textColor="@color/dark_grey_color" - android:textSize="15sp" + 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" android:visibility="@{data.text.length > 0 ? View.VISIBLE : View.GONE}" /> diff --git a/app/src/main/res/layout/chat_room_sending.xml b/app/src/main/res/layout/chat_room_sending.xml index d25972d4e..efc28de06 100644 --- a/app/src/main/res/layout/chat_room_sending.xml +++ b/app/src/main/res/layout/chat_room_sending.xml @@ -110,10 +110,20 @@ + + + app:layout_constraintTop_toBottomOf="@id/emoji_picker" /> + + + + + app:layout_constraintTop_toTopOf="@id/emoji_picker" /> + app:layout_constraintTop_toBottomOf="@id/emoji_picker" /> diff --git a/app/src/main/res/values/dimen.xml b/app/src/main/res/values/dimen.xml index 79054ac01..0f0e3918b 100644 --- a/app/src/main/res/values/dimen.xml +++ b/app/src/main/res/values/dimen.xml @@ -86,4 +86,7 @@ 25dp 1dp 2dp + 290dp + 15sp + 45sp \ No newline at end of file