Browse Source

Add emoji reacts support (#889)

* feat: Add emoji reacts support

* Remove message multi-selection

* Add emoji reaction model

* Add emoji reaction panel

* Blur reacts panel background

* Show emoji keyboard

* Add emoji sprites

* Update reaction proto

* Emoji database updates

* Emoji database refactor

* Emoji reaction persistence

* Optimize reactions retrieval

* Fix emoji group query

* Display emojis

* Fix emoji persistence

* Cleanup

* Persistence refactor

* Add reactions bottom sheet

* Cleanup

* Ui tweaks

* React with any emoji

* Show emoji react notifications

* Remove reaction

* Show reactions modal on long press

* Click to react (+1) with an emoji

* Click to react with an emoji

* Enable emoji expand/collapse

* fix: some compile issues from merge conflicts

* fix: compile issues merging quote and media message UI

* fix: xml IDs and adding in legacy is selected for future inclusion

* Fix view constraints

* Fix merge issue

* Add message selection option in conversation context menu

* Add sogs emoji integration

* Handle sogs emoji reactions

* Enable sending/deleting sogs emojis

* fix: improve the visible message layout

* fix: add file IDs to request parameters for message send (#940)

* Fix open group polling from seqno instead of last hash (#939)

* fix: reset seqno to get recent messages from open groups

* build: upgrade build numbers

* fix: actually run the migration

* Using StringBuilder to construct request url

* Fix reaction filter

* fix: is_mms added in second projection query

* Update default emojis

* fix: include legacy and new open groups in server ID tracking (#941)

* feat: add hidden moderator and admin roles, separated as they may be used independently in future (#942)

* Cleanup

* Fix view constraints

* Add reactions capability check

* Fix reactions alignment

* Ui fixes

* Display reactions list

* feat: add formatted count strings

* fix: account for negatives and add tests

* Migrate old official open group locations for polling and adding (#932)

* feat: adding in first part of open group migrations and tests for migration logic / helpers

* feat: test code and migration logic for open groups in the case of no conflicts

* feat: add in extra test cases and refactor code for migrator

* refactor: migrate open group join URLs and references to server in adding new open groups to catch legacy and re-write it

* refactor: joining open groups using OpenGroupUrlParser.kt now

* fix: add in compile issues for renamed OpenGroupApi.kt from OpenGroupV2

* fix: prevent duplicates of http/https for new open group DNS and prevent adding new groups based on public key

* fix: room and server swapped parameters

* fix: replace default server for config messages

* fix: actually using public key to de-dupe didn't work for rooms

* build: bump version code and name

* Display reactions list on open groups for moderators

* Ui tweaks

* Ui tweaks for moderation

* Refactor

* fix: compile issue

* fix: de-duping joined queries in the get X from cursor

* Restore import

* fix: colouring the reaction overlay scrubber

* fix: highlight colour, show reaction count if 1 or above

* Cleanup

* fix: light mode accent

* fix: light / dark mode themeing in reactions dialog fragment

* Emoji notification blinded id check

* fix: show reaction list correctly and pass isUserModerator to bind methods

* fix: remove unnecessary places for the moderator

* fix: X button for removing own react not showing up properly

* feat: add clear all header view

* fix: migrate the clear all to the correct location

* fix: use display instead of base

* Truncate emoji sender ids

* feat: add notify thread function in thread db

* Notify threads on reaction received

* fix: design fixes for the reaction list

* fix: emoji reactions bottom sheet dialog UI designs

* feat: add unsupported emoji reaction

* fix: crash and doing vector properly

* Fix reaction database queries

* Fix background open group adder job

* Show new open group reactions

* Fetch a maximum of 5 reactors

* Handle open group reactions polling conflicts

* Add count to user reaction

* Show number of additional reactors

* fix: unreads set same as the unread query

* fix: design changes

* fix: update dependency to improve flexboxlayout behaviour, design consistencies

* Add select message icon and update long press menu items order and wording

* Fix crash on reactors dialog

* fix: colours and backgrounds to match designs

* fix: add header in recipient item

* fix: margins

* fix: alignments and layout issues for emoji reactions view

* feat: add overflow previews and logic for overflow

* Dim action bar

* Add emoji search

* Search index fix

* Set count for 1:1 and closed group reactions when inserting in local database

* Use on screen toolbar to allow overlaying

* Show/hide scroll to bottom button

* feat: add extended properties so it doesn't collapse on re-bind

* Cleanup

* feat: prevent keeping extended on rebinding if we get a new message ID

* fix: long press works on devices now, fix release lint issue and crash for emoji search DBs from emoji builds

* Display message timestamp

* Fix modal items alignment

* fix: sort order and emoji count in compareTo

* Scale down really large messages to fit

* Prevent closed group crash

* Fix reaction author

Co-authored-by: charles <charles@oxen.io>
Co-authored-by: jubb <hjubb@users.noreply.github.com>
pull/958/head
ceokot 3 months ago committed by GitHub
parent
commit
16ca97d2d3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 9
      app/build.gradle
  2. 2
      app/src/main/AndroidManifest.xml
  3. BIN
      app/src/main/assets/emoji/Activity.webp
  4. BIN
      app/src/main/assets/emoji/Flags_0.webp
  5. BIN
      app/src/main/assets/emoji/Flags_1.webp
  6. BIN
      app/src/main/assets/emoji/Foods.webp
  7. BIN
      app/src/main/assets/emoji/Nature.webp
  8. BIN
      app/src/main/assets/emoji/Objects.webp
  9. BIN
      app/src/main/assets/emoji/People_0.webp
  10. BIN
      app/src/main/assets/emoji/People_1.webp
  11. BIN
      app/src/main/assets/emoji/People_2.webp
  12. BIN
      app/src/main/assets/emoji/People_3.webp
  13. BIN
      app/src/main/assets/emoji/People_4.webp
  14. BIN
      app/src/main/assets/emoji/People_5.webp
  15. BIN
      app/src/main/assets/emoji/People_6.webp
  16. BIN
      app/src/main/assets/emoji/People_7.webp
  17. BIN
      app/src/main/assets/emoji/People_8.webp
  18. BIN
      app/src/main/assets/emoji/People_9.webp
  19. BIN
      app/src/main/assets/emoji/Places.webp
  20. BIN
      app/src/main/assets/emoji/Symbols.webp
  21. 1
      app/src/main/assets/emoji/emoji_data.json
  22. 1
      app/src/main/assets/emoji/emoji_search_index.json
  23. 28
      app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java
  24. 17
      app/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java
  25. 50
      app/src/main/java/org/thoughtcrime/securesms/animation/ResizeAnimation.java
  26. 17
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/CompositeEmojiPageModel.java
  27. 26
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emoji.java
  28. 27
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java
  29. 9
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEventListener.java
  30. 8
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java
  31. 46
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiImageView.java
  32. 53
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiItemDecoration.kt
  33. 7
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiKeyboardProvider.java
  34. 5
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java
  35. 177
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java
  36. 262
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageViewGridAdapter.java
  37. 73
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPages.java
  38. 227
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java
  39. 44
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java
  40. 13
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java
  41. 54
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java
  42. 4
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiVariationSelectorPopup.java
  43. 38
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java
  44. 38
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java
  45. 31
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiDrawInfo.java
  46. 5
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiDrawInfo.kt
  47. 10
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java
  48. 6
      app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiTree.java
  49. 12
      app/src/main/java/org/thoughtcrime/securesms/components/menu/ActionItem.kt
  50. 94
      app/src/main/java/org/thoughtcrime/securesms/components/menu/ContextMenuList.kt
  51. 337
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt
  52. 33
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt
  53. 58
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationContextMenu.kt
  54. 94
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionDelegate.kt
  55. 882
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java
  56. 30
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt
  57. 73
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DimensionUnit.java
  58. 34
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/NoCrossfadeChangeDefaultAnimator.java
  59. 17
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/SelectedConversationModel.kt
  60. 381
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.java
  61. 380
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ViewUtil.java
  62. 98
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/WindowUtil.java
  63. 2
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt
  64. 10
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt
  65. 24
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuItemHelper.kt
  66. 343
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.java
  67. 115
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt
  68. 47
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt
  69. 15
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageViewDelegate.kt
  70. 7
      app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt
  71. 6
      app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java
  72. 101
      app/src/main/java/org/thoughtcrime/securesms/database/EmojiSearchDatabase.kt
  73. 11
      app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java
  74. 47
      app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt
  75. 8
      app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt
  76. 44
      app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java
  77. 41
      app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt
  78. 2
      app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java
  79. 56
      app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java
  80. 258
      app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt
  81. 2
      app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java
  82. 49
      app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java
  83. 62
      app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt
  84. 86
      app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java
  85. 35
      app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java
  86. 28
      app/src/main/java/org/thoughtcrime/securesms/database/model/EmojiSearchData.java
  87. 27
      app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java
  88. 34
      app/src/main/java/org/thoughtcrime/securesms/database/model/MessageId.kt
  89. 9
      app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
  90. 4
      app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java
  91. 13
      app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java
  92. 14
      app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.kt
  93. 4
      app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java
  94. 2
      app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt
  95. 8
      app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt
  96. 47
      app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt
  97. 138
      app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiJsonParser.kt
  98. 16
      app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPage.kt
  99. 85
      app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPageCache.kt
  100. 161
      app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt
  101. Some files were not shown because too many files have changed in this diff Show More

9
app/build.gradle

@ -90,10 +90,7 @@ dependencies {
exclude group: 'com.squareup.okhttp', module: 'okhttp-urlconnection'
}
implementation 'com.annimon:stream:1.1.8'
implementation ('com.takisoft.fix:colorpicker:0.9.1') {
exclude group: 'com.android.support', module: 'appcompat-v7'
exclude group: 'com.android.support', module: 'recyclerview-v7'
}
implementation 'com.takisoft.fix:colorpicker:1.0.1'
implementation 'com.codewaves.stickyheadergrid:stickyheadergrid:0.9.4'
implementation 'com.github.dmytrodanylyk.circular-progress-button:library:1.1.3-S2'
implementation 'org.signal:android-database-sqlcipher:3.5.9-S3'
@ -159,8 +156,8 @@ dependencies {
testImplementation 'org.robolectric:shadows-multidex:4.4'
}
def canonicalVersionCode = 292
def canonicalVersionName = "1.14.0"
def canonicalVersionCode = 294
def canonicalVersionName = "1.14.2"
def postFixSize = 10
def abiPostFix = ['armeabi-v7a' : 1,

2
app/src/main/AndroidManifest.xml

@ -221,7 +221,7 @@
android:name="org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2"
android:screenOrientation="portrait"
android:parentActivityName="org.thoughtcrime.securesms.home.HomeActivity"
android:theme="@style/Theme.Session.DayNight.FlatActionBar">
android:theme="@style/Theme.Session.DayNight.NoActionBar">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value="org.thoughtcrime.securesms.home.HomeActivity" />

BIN
app/src/main/assets/emoji/Activity.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

BIN
app/src/main/assets/emoji/Flags_0.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
app/src/main/assets/emoji/Flags_1.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
app/src/main/assets/emoji/Foods.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

BIN
app/src/main/assets/emoji/Nature.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 166 KiB

BIN
app/src/main/assets/emoji/Objects.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 238 KiB

BIN
app/src/main/assets/emoji/People_0.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

BIN
app/src/main/assets/emoji/People_1.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

BIN
app/src/main/assets/emoji/People_2.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
app/src/main/assets/emoji/People_3.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

BIN
app/src/main/assets/emoji/People_4.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

BIN
app/src/main/assets/emoji/People_5.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

BIN
app/src/main/assets/emoji/People_6.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

BIN
app/src/main/assets/emoji/People_7.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

BIN
app/src/main/assets/emoji/People_8.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 KiB

BIN
app/src/main/assets/emoji/People_9.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

BIN
app/src/main/assets/emoji/Places.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

BIN
app/src/main/assets/emoji/Symbols.webp

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

1
app/src/main/assets/emoji/emoji_data.json

File diff suppressed because one or more lines are too long

1
app/src/main/assets/emoji/emoji_search_index.json

File diff suppressed because one or more lines are too long

28
app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java

@ -47,17 +47,22 @@ import org.session.libsession.utilities.Util;
import org.session.libsession.utilities.WindowDebouncer;
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
import org.session.libsession.utilities.dynamiclanguage.LocaleParser;
import org.session.libsignal.utilities.JsonUtil;
import org.session.libsignal.utilities.Log;
import org.session.libsignal.utilities.ThreadUtils;
import org.signal.aesgcmprovider.AesGcmProvider;
import org.thoughtcrime.securesms.components.TypingStatusSender;
import org.thoughtcrime.securesms.crypto.KeyPairUtilities;
import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.JobDatabase;
import org.thoughtcrime.securesms.database.LokiAPIDatabase;
import org.thoughtcrime.securesms.database.Storage;
import org.thoughtcrime.securesms.database.model.EmojiSearchData;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.dependencies.DatabaseModule;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.groups.OpenGroupManager;
import org.thoughtcrime.securesms.groups.OpenGroupMigrator;
import org.thoughtcrime.securesms.home.HomeActivity;
import org.thoughtcrime.securesms.jobmanager.JobManager;
import org.thoughtcrime.securesms.jobmanager.impl.JsonDataSerializer;
@ -89,12 +94,16 @@ import org.webrtc.voiceengine.WebRtcAudioManager;
import org.webrtc.voiceengine.WebRtcAudioUtils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.security.Security;
import java.util.Arrays;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.Timer;
import java.util.concurrent.Executors;
import javax.inject.Inject;
@ -191,6 +200,9 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
storage,
messageDataProvider,
()-> KeyPairUtilities.INSTANCE.getUserED25519KeyPair(this));
// migrate session open group data
OpenGroupMigrator.migrate(getDatabaseComponent());
// end migration
callMessageProcessor = new CallMessageProcessor(this, textSecurePreferences, ProcessLifecycleOwner.get().getLifecycle(), storage);
Log.i(TAG, "onCreate()");
startKovenant();
@ -220,6 +232,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
initializeWebRtc();
initializeBlobProvider();
resubmitProfilePictureIfNeeded();
loadEmojiSearchIndexIfNeeded();
EmojiSource.refresh();
}
@Override
@ -489,6 +503,20 @@ public class ApplicationContext extends Application implements DefaultLifecycleO
});
}
private void loadEmojiSearchIndexIfNeeded() {
Executors.newSingleThreadExecutor().execute(() -> {
EmojiSearchDatabase emojiSearchDb = getDatabaseComponent().emojiSearchDatabase();
if (emojiSearchDb.query("face", 1).isEmpty()) {
try (InputStream inputStream = getAssets().open("emoji/emoji_search_index.json")) {
List<EmojiSearchData> searchIndex = Arrays.asList(JsonUtil.fromJson(inputStream, EmojiSearchData[].class));
emojiSearchDb.setSearchIndex(searchIndex);
} catch (IOException e) {
Log.e("Loki", "Failed to load emoji search index");
}
}
});
}
public void clearAllData(boolean isMigratingToV2KeyPair) {
String token = TextSecurePreferences.getFCMToken(this);
if (token != null && !token.isEmpty()) {

17
app/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java

@ -0,0 +1,17 @@
package org.thoughtcrime.securesms.animation;
import android.animation.Animator;
public abstract class AnimationCompleteListener implements Animator.AnimatorListener {
@Override
public final void onAnimationStart(Animator animation) {}
@Override
public abstract void onAnimationEnd(Animator animation);
@Override
public final void onAnimationCancel(Animator animation) {}
@Override
public final void onAnimationRepeat(Animator animation) {}
}

50
app/src/main/java/org/thoughtcrime/securesms/animation/ResizeAnimation.java

@ -0,0 +1,50 @@
package org.thoughtcrime.securesms.animation;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.Transformation;
import androidx.annotation.NonNull;
public class ResizeAnimation extends Animation {
private final View target;
private final int targetWidthPx;
private final int targetHeightPx;
private int startWidth;
private int startHeight;
public ResizeAnimation(@NonNull View target, int targetWidthPx, int targetHeightPx) {
this.target = target;
this.targetWidthPx = targetWidthPx;
this.targetHeightPx = targetHeightPx;
}
@Override
protected void applyTransformation(float interpolatedTime, Transformation t) {
int newWidth = (int) (startWidth + (targetWidthPx - startWidth) * interpolatedTime);
int newHeight = (int) (startHeight + (targetHeightPx - startHeight) * interpolatedTime);
ViewGroup.LayoutParams params = target.getLayoutParams();
params.width = newWidth;
params.height = newHeight;
target.setLayoutParams(params);
}
@Override
public void initialize(int width, int height, int parentWidth, int parentHeight) {
super.initialize(width, height, parentWidth, parentHeight);
this.startWidth = width;
this.startHeight = height;
}
@Override
public boolean willChangeBounds() {
return true;
}
}

17
app/src/main/java/org/thoughtcrime/securesms/components/emoji/CompositeEmojiPageModel.java

@ -1,21 +1,30 @@
package org.thoughtcrime.securesms.components.emoji;
import android.net.Uri;
import androidx.annotation.AttrRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.conversation.v2.Util;
import java.util.LinkedList;
import java.util.List;
public class CompositeEmojiPageModel implements EmojiPageModel {
@AttrRes private final int iconAttr;
@NonNull private final EmojiPageModel[] models;
@AttrRes private final int iconAttr;
@NonNull private final List<EmojiPageModel> models;
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull EmojiPageModel... models) {
public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull List<EmojiPageModel> models) {
this.iconAttr = iconAttr;
this.models = models;
}
@Override
public String getKey() {
return Util.hasItems(models) ? models.get(0).getKey() : "";
}
public int getIconAttr() {
return iconAttr;
}
@ -44,7 +53,7 @@ public class CompositeEmojiPageModel implements EmojiPageModel {
}
@Override
public @Nullable String getSprite() {
public @Nullable Uri getSpriteUri() {
return null;
}

26
app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emoji.java

@ -1,14 +1,27 @@
package org.thoughtcrime.securesms.components.emoji;
import androidx.annotation.Nullable;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
public class Emoji {
private final List<String> variations;
private final List<String> rawVariations;
public Emoji(String... variations) {
this.variations = Arrays.asList(variations);
this(Arrays.asList(variations), Collections.emptyList());
}
public Emoji(List<String> variations) {
this(variations, Collections.emptyList());
}
public Emoji(List<String> variations, List<String> rawVariations) {
this.variations = variations;
this.rawVariations = rawVariations;
}
public String getValue() {
@ -18,4 +31,15 @@ public class Emoji {
public List<String> getVariations() {
return variations;
}
public boolean hasMultipleVariations() {
return variations.size() > 1;
}
public @Nullable String getRawVariation(int variationIndex) {
if (rawVariations != null && variationIndex >= 0 && variationIndex < rawVariations.size()) {
return rawVariations.get(variationIndex);
}
return null;
}
}

27
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java

@ -1,20 +1,23 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.text.InputFilter;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatEditText;
import android.text.InputFilter;
import android.util.AttributeSet;
import network.loki.messenger.R;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.components.emoji.EmojiProvider.EmojiDrawable;
import org.session.libsession.utilities.TextSecurePreferences;
import network.loki.messenger.R;
public class EmojiEditText extends AppCompatEditText {
private static final String TAG = EmojiEditText.class.getSimpleName();
private static final String TAG = Log.tag(EmojiEditText.class);
public EmojiEditText(Context context) {
this(context, null);
@ -26,8 +29,14 @@ public class EmojiEditText extends AppCompatEditText {
public EmojiEditText(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
if (!TextSecurePreferences.isSystemEmojiPreferred(getContext())) {
setFilters(appendEmojiFilter(this.getFilters()));
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiTextView, 0, 0);
boolean forceCustom = a.getBoolean(R.styleable.EmojiTextView_emoji_forceCustom, false);
boolean jumboEmoji = a.getBoolean(R.styleable.EmojiTextView_emoji_forceJumbo, false);
a.recycle();
if (!isInEditMode() && forceCustom) {
setFilters(appendEmojiFilter(this.getFilters(), jumboEmoji));
}
}
@ -45,7 +54,7 @@ public class EmojiEditText extends AppCompatEditText {
else super.invalidateDrawable(drawable);
}
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters) {
private InputFilter[] appendEmojiFilter(@Nullable InputFilter[] originalFilters, boolean jumboEmoji) {
InputFilter[] result;
if (originalFilters != null) {
@ -55,7 +64,7 @@ public class EmojiEditText extends AppCompatEditText {
result = new InputFilter[1];
}
result[0] = new EmojiFilter(this);
result[0] = new EmojiFilter(this, jumboEmoji);
return result;
}

9
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEventListener.java

@ -0,0 +1,9 @@
package org.thoughtcrime.securesms.components.emoji;
import android.view.KeyEvent;
public interface EmojiEventListener {
void onEmojiSelected(String emoji);
void onKeyEvent(KeyEvent keyEvent);
}

8
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java

@ -8,9 +8,11 @@ import android.widget.TextView;
public class EmojiFilter implements InputFilter {
private TextView view;
private boolean jumboEmoji;
public EmojiFilter(TextView view) {
this.view = view;
public EmojiFilter(TextView view, boolean jumboEmoji) {
this.view = view;
this.jumboEmoji = jumboEmoji;
}
@Override
@ -19,7 +21,7 @@ public class EmojiFilter implements InputFilter {
char[] v = new char[end - start];
TextUtils.getChars(source, start, end, v, 0);
Spannable emojified = EmojiProvider.getInstance(view.getContext()).emojify(new String(v), view);
Spannable emojified = EmojiProvider.emojify(new String(v), view, jumboEmoji);
if (source instanceof Spanned && emojified != null) {
TextUtils.copySpansFrom((Spanned) source, start, end, null, emojified, 0);

46
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiImageView.java

@ -0,0 +1,46 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.AppCompatImageView;
import network.loki.messenger.R;
public class EmojiImageView extends AppCompatImageView {
private final boolean forceJumboEmoji;
public EmojiImageView(Context context) {
this(context, null);
}
public EmojiImageView(Context context, AttributeSet attrs) {
this(context, attrs, 0);
}
public EmojiImageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
TypedArray a = context.getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiImageView, 0, 0);
forceJumboEmoji = a.getBoolean(R.styleable.EmojiImageView_forceJumbo, false);
a.recycle();
}
public void setImageEmoji(CharSequence emoji) {
if (isInEditMode()) {
setImageResource(R.drawable.ic_emoji);
} else {
Drawable emojiDrawable = EmojiProvider.getEmojiDrawable(getContext(), emoji);
if (emojiDrawable == null) {
// fallback
setImageResource(R.drawable.ic_outline_disabled_by_default_24);
} else {
setImageDrawable(emojiDrawable);
}
}
}
}

53
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiItemDecoration.kt

@ -0,0 +1,53 @@
package org.thoughtcrime.securesms.components.emoji
import android.graphics.Canvas
import android.graphics.Rect
import android.graphics.drawable.Drawable
import android.view.View
import androidx.appcompat.widget.AppCompatTextView
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiModel
import org.thoughtcrime.securesms.conversation.v2.ViewUtil
import org.thoughtcrime.securesms.util.InsetItemDecoration
private val EDGE_LENGTH: Int = ViewUtil.dpToPx(6)
private val HORIZONTAL_INSET: Int = ViewUtil.dpToPx(6)
private val EMOJI_VERTICAL_INSET: Int = ViewUtil.dpToPx(5)
private val HEADER_VERTICAL_INSET: Int = ViewUtil.dpToPx(8)
/**
* Use super class to add insets to the emojis and use the [onDrawOver] to draw the variation
* hint if the emoji has more than one variation.
*/
class EmojiItemDecoration(private val allowVariations: Boolean, private val variationsDrawable: Drawable) : InsetItemDecoration(SetInset()) {
override fun onDrawOver(canvas: Canvas, parent: RecyclerView, state: RecyclerView.State) {
super.onDrawOver(canvas, parent, state)
val adapter: EmojiPageViewGridAdapter? = parent.adapter as? EmojiPageViewGridAdapter
if (allowVariations && adapter != null) {
for (i in 0 until parent.childCount) {
val child: View = parent.getChildAt(i)
val position: Int = parent.getChildAdapterPosition(child)
if (position >= 0 && position <= adapter.itemCount) {
val model = adapter.currentList[position]
if (model is EmojiModel && model.emoji.hasMultipleVariations()) {
variationsDrawable.setBounds(child.right, child.bottom - EDGE_LENGTH, child.right + EDGE_LENGTH, child.bottom)
variationsDrawable.draw(canvas)
}
}
}
}
}
private class SetInset : InsetItemDecoration.SetInset() {
override fun setInset(outRect: Rect, view: View, parent: RecyclerView) {
val isHeader = view.javaClass == AppCompatTextView::class.java
outRect.left = HORIZONTAL_INSET
outRect.right = HORIZONTAL_INSET
outRect.top = if (isHeader) HEADER_VERTICAL_INSET else EMOJI_VERTICAL_INSET
outRect.bottom = if (isHeader) 0 else EMOJI_VERTICAL_INSET
}
}
}

7
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiKeyboardProvider.java

@ -136,8 +136,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener);
page.setModel(pages.get(position));
EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener, false);
container.addView(page);
return page;
}
@ -160,8 +159,4 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
}
}
public interface EmojiEventListener {
void onEmojiSelected(String emoji);
void onKeyEvent(KeyEvent keyEvent);
}
}

5
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java

@ -1,12 +1,15 @@
package org.thoughtcrime.securesms.components.emoji;
import android.net.Uri;
import androidx.annotation.Nullable;
import java.util.List;
public interface EmojiPageModel {
String getKey();
int getIconAttr();
List<String> getEmoji();
List<Emoji> getDisplayEmoji();
boolean hasSpriteMap();
String getSprite();
@Nullable Uri getSpriteUri();
boolean isDynamic();
}

177
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java

@ -1,60 +1,136 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
import androidx.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.LinearSmoothScroller;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.widget.FrameLayout;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiHeader;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.EmojiNoResultsModel;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
import org.thoughtcrime.securesms.conversation.v2.ViewUtil;
import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.DrawableUtil;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
import network.loki.messenger.R;
import java.util.List;
import java.util.Optional;
public class EmojiPageView extends FrameLayout implements VariationSelectorListener {
private static final String TAG = EmojiPageView.class.getSimpleName();
import network.loki.messenger.R;
private EmojiPageModel model;
private EmojiPageViewGridAdapter adapter;
private RecyclerView recyclerView;
private GridLayoutManager layoutManager;
public class EmojiPageView extends RecyclerView implements VariationSelectorListener {
private AdapterFactory adapterFactory;
private LinearLayoutManager layoutManager;
private RecyclerView.OnItemTouchListener scrollDisabler;
private VariationSelectorListener variationSelectorListener;
private EmojiVariationSelectorPopup popup;
public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
}
public EmojiPageView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
}
public EmojiPageView(@NonNull Context context,
@NonNull EmojiKeyboardProvider.EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener)
@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations)
{
super(context);
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
initialize(emojiSelectionListener, variationSelectorListener, allowVariations);
}
public EmojiPageView(@NonNull Context context,
@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations,
@NonNull LinearLayoutManager layoutManager,
@LayoutRes int displayEmojiLayoutResId,
@LayoutRes int displayEmoticonLayoutResId)
{
super(context);
initialize(emojiSelectionListener, variationSelectorListener, allowVariations, layoutManager, displayEmojiLayoutResId, displayEmoticonLayoutResId);
}
public void initialize(@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations)
{
initialize(emojiSelectionListener, variationSelectorListener, allowVariations, new GridLayoutManager(getContext(), 8), R.layout.emoji_display_item_grid, R.layout.emoji_text_display_item_grid);
}
public void initialize(@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations,
@NonNull LinearLayoutManager layoutManager,
@LayoutRes int displayEmojiLayoutResId,
@LayoutRes int displayEmoticonLayoutResId)
{
this.variationSelectorListener = variationSelectorListener;
recyclerView = view.findViewById(R.id.emoji);
layoutManager = new GridLayoutManager(context, 8);
scrollDisabler = new ScrollDisabler();
popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener);
adapter = new EmojiPageViewGridAdapter(EmojiProvider.getInstance(context),
popup,
emojiSelectionListener,
this);
this.layoutManager = layoutManager;
this.scrollDisabler = new ScrollDisabler();
this.popup = new EmojiVariationSelectorPopup(getContext(), emojiSelectionListener);
this.adapterFactory = () -> new EmojiPageViewGridAdapter(popup,
emojiSelectionListener,
this,
allowVariations,
displayEmojiLayoutResId,
displayEmoticonLayoutResId);
if (this.layoutManager instanceof GridLayoutManager) {
GridLayoutManager gridLayout = (GridLayoutManager) this.layoutManager;
gridLayout.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
@Override
public int getSpanSize(int position) {
if (getAdapter() != null) {
Optional<MappingModel<?>> model = getAdapter().getModel(position);
if (model.isPresent() && (model.get() instanceof EmojiHeader || model.get() instanceof EmojiNoResultsModel)) {
return gridLayout.getSpanCount();
}
}
return 1;
}
});
}
setLayoutManager(layoutManager);
Drawable drawable = DrawableUtil.tint(ContextUtil.requireDrawable(getContext(), R.drawable.triangle_bottom_right_corner), ContextCompat.getColor(getContext(), R.color.signal_button_secondary_text_disabled));
addItemDecoration(new EmojiItemDecoration(allowVariations, drawable));
}
public void presentForEmojiKeyboard() {
setPadding(getPaddingLeft(),
getPaddingTop(),
getPaddingRight(),
getPaddingBottom() + ViewUtil.dpToPx(56));
recyclerView.setLayoutManager(layoutManager);
recyclerView.setAdapter(adapter);
setClipToPadding(false);
}
public void onSelected() {
if (model.isDynamic() && adapter != null) {
adapter.notifyDataSetChanged();
if (getAdapter() != null) {
getAdapter().notifyDataSetChanged();
}
}
public void setModel(EmojiPageModel model) {
this.model = model;
adapter.setEmoji(model.getDisplayEmoji());
public void setList(@NonNull List<MappingModel<?>> list, @Nullable Runnable commitCallback) {
EmojiPageViewGridAdapter adapter = adapterFactory.create();
setAdapter(adapter);
adapter.submitList(list, commitCallback);
}
@Override
@ -66,16 +142,21 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
layoutManager.setSpanCount(Math.max(w / idealWidth, 1));
if (layoutManager instanceof GridLayoutManager) {
int viewWidth = w - getPaddingStart() - getPaddingEnd();
int idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
int spanCount = Math.max(viewWidth / idealWidth, 1);
((GridLayoutManager) layoutManager).setSpanCount(spanCount);
}
}
@Override
public void onVariationSelectorStateChanged(boolean open) {
if (open) {
recyclerView.addOnItemTouchListener(scrollDisabler);
addOnItemTouchListener(scrollDisabler);
} else {
post(() -> recyclerView.removeOnItemTouchListener(scrollDisabler));
post(() -> removeOnItemTouchListener(scrollDisabler));
}
if (variationSelectorListener != null) {
@ -83,6 +164,32 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
}
}
public void setRecyclerNestedScrollingEnabled(boolean enabled) {
setNestedScrollingEnabled(enabled);
}
public void smoothScrollToPositionTop(int position) {
int currentPosition = layoutManager.findFirstCompletelyVisibleItemPosition();
boolean shortTrip = Math.abs(currentPosition - position) < 475;
if (shortTrip) {
RecyclerView.SmoothScroller smoothScroller = new LinearSmoothScroller(getContext()) {
@Override
protected int getVerticalSnapPreference() {
return LinearSmoothScroller.SNAP_TO_START;
}
};
smoothScroller.setTargetPosition(position);
layoutManager.startSmoothScroll(smoothScroller);
} else {
layoutManager.scrollToPositionWithOffset(position, 0);
}
}
public @Nullable EmojiPageViewGridAdapter getAdapter() {
return (EmojiPageViewGridAdapter) super.getAdapter();
}
private static class ScrollDisabler implements RecyclerView.OnItemTouchListener {
@Override
public boolean onInterceptTouchEvent(@NonNull RecyclerView recyclerView, @NonNull MotionEvent motionEvent) {
@ -95,4 +202,8 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
@Override
public void onRequestDisallowInterceptTouchEvent(boolean b) { }
}
private interface AdapterFactory {
EmojiPageViewGridAdapter create();
}
}

262
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageViewGridAdapter.java

@ -1,113 +1,237 @@
package org.thoughtcrime.securesms.components.emoji;
import android.graphics.drawable.Drawable;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.PopupWindow;
import android.widget.TextView;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
import java.util.ArrayList;
import java.util.List;
import org.thoughtcrime.securesms.util.adapter.mapping.LayoutFactory;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingAdapter;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingModel;
import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder;
import network.loki.messenger.R;
public class EmojiPageViewGridAdapter extends RecyclerView.Adapter<EmojiPageViewGridAdapter.EmojiViewHolder> implements PopupWindow.OnDismissListener {
public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWindow.OnDismissListener {
private final List<Emoji> emojiList;
private final EmojiProvider emojiProvider;
private final EmojiVariationSelectorPopup popup;
private final VariationSelectorListener variationSelectorListener;
private final EmojiEventListener emojiEventListener;
private final VariationSelectorListener variationSelectorListener;
public EmojiPageViewGridAdapter(@NonNull EmojiProvider emojiProvider,
@NonNull EmojiVariationSelectorPopup popup,
public EmojiPageViewGridAdapter(@NonNull EmojiVariationSelectorPopup popup,
@NonNull EmojiEventListener emojiEventListener,
@NonNull VariationSelectorListener variationSelectorListener)
@NonNull VariationSelectorListener variationSelectorListener,
boolean allowVariations,
@LayoutRes int displayEmojiLayoutResId,
@LayoutRes int displayEmoticonLayoutResId)
{
this.emojiList = new ArrayList<>();
this.emojiProvider = emojiProvider;
this.popup = popup;
this.emojiEventListener = emojiEventListener;
this.variationSelectorListener = variationSelectorListener;
popup.setOnDismissListener(this);
registerFactory(EmojiHeader.class, new LayoutFactory<>(EmojiHeaderViewHolder::new, R.layout.emoji_grid_header));
registerFactory(EmojiModel.class, new LayoutFactory<>(v -> new EmojiViewHolder(v, emojiEventListener, variationSelectorListener, popup, allowVariations), displayEmojiLayoutResId));
registerFactory(EmojiTextModel.class, new LayoutFactory<>(v -> new EmojiTextViewHolder(v, emojiEventListener), displayEmoticonLayoutResId));
registerFactory(EmojiNoResultsModel.class, new LayoutFactory<>(MappingViewHolder.SimpleViewHolder::new, R.layout.emoji_grid_no_results));
}
@NonNull
@Override
public EmojiViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
return new EmojiViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.emoji_display_item, viewGroup, false));
public void onDismiss() {
variationSelectorListener.onVariationSelectorStateChanged(false);
}
@Override
public void onBindViewHolder(@NonNull EmojiViewHolder viewHolder, int i) {
Emoji emoji = emojiList.get(i);
public static class EmojiHeader implements MappingModel<EmojiHeader>, HasKey {
Drawable drawable = emojiProvider.getEmojiDrawable(emoji.getValue());
private final String key;
private final int title;
if (drawable != null) {
viewHolder.textView.setVisibility(View.GONE);
viewHolder.imageView.setVisibility(View.VISIBLE);
public EmojiHeader(@NonNull String key, int title) {
this.key = key;
this.title = title;
}
viewHolder.imageView.setImageDrawable(drawable);
} else {
viewHolder.textView.setVisibility(View.VISIBLE);
viewHolder.imageView.setVisibility(View.GONE);
@Override
public @NonNull String getKey() {
return key;
}
viewHolder.textView.setEmoji(emoji.getValue());
@Override
public boolean areItemsTheSame(@NonNull EmojiHeader newItem) {
return title == newItem.title;
}
viewHolder.itemView.setOnClickListener(v -> {
emojiEventListener.onEmojiSelected(emoji.getValue());
});
@Override
public boolean areContentsTheSame(@NonNull EmojiHeader newItem) {
return areItemsTheSame(newItem);
}
}
if (emoji.getVariations().size() > 1) {
viewHolder.itemView.setOnLongClickListener(v -> {
popup.dismiss();
popup.setVariations(emoji.getVariations());
popup.showAsDropDown(viewHolder.itemView, 0, -(2 * viewHolder.itemView.getHeight()));
variationSelectorListener.onVariationSelectorStateChanged(true);
return true;
});
viewHolder.hintCorner.setVisibility(View.VISIBLE);
} else {
viewHolder.itemView.setOnLongClickListener(null);
viewHolder.hintCorner.setVisibility(View.GONE);
static class EmojiHeaderViewHolder extends MappingViewHolder<EmojiHeader> {
private final TextView title;
public EmojiHeaderViewHolder(@NonNull View itemView) {
super(itemView);
title = findViewById(R.id.emoji_grid_header_title);
}
@Override
public void bind(@NonNull EmojiHeader model) {
title.setText(model.title);
}
}
@Override
public int getItemCount() {
return emojiList.size();
public static class EmojiModel implements MappingModel<EmojiModel>, HasKey {
private final String key;
private final Emoji emoji;
public EmojiModel(@NonNull String key, @NonNull Emoji emoji) {
this.key = key;
this.emoji = emoji;
}
@Override
public @NonNull String getKey() {
return key;
}
public @NonNull Emoji getEmoji() {
return emoji;
}
@Override
public boolean areItemsTheSame(@NonNull EmojiModel newItem) {
return newItem.emoji.getValue().equals(emoji.getValue());
}
@Override
public boolean areContentsTheSame(@NonNull EmojiModel newItem) {
return areItemsTheSame(newItem);
}
}
public void setEmoji(@NonNull List<Emoji> emojiList) {
this.emojiList.clear();
this.emojiList.addAll(emojiList);
notifyDataSetChanged();
static class EmojiViewHolder extends MappingViewHolder<EmojiModel> {
private final EmojiVariationSelectorPopup popup;
private final VariationSelectorListener variationSelectorListener;
private final EmojiEventListener emojiEventListener;
private final boolean allowVariations;
private final ImageView imageView;
public EmojiViewHolder(@NonNull View itemView,
@NonNull EmojiEventListener emojiEventListener,
@NonNull VariationSelectorListener variationSelectorListener,
@NonNull EmojiVariationSelectorPopup popup,
boolean allowVariations)
{
super(itemView);
this.popup = popup;
this.variationSelectorListener = variationSelectorListener;
this.emojiEventListener = emojiEventListener;
this.allowVariations = allowVariations;
this.imageView = itemView.findViewById(R.id.emoji_image);
}
@Override
public void bind(@NonNull EmojiModel model) {
final Drawable drawable = EmojiProvider.getEmojiDrawable(imageView.getContext(), model.emoji.getValue());
if (drawable != null) {
imageView.setVisibility(View.VISIBLE);
imageView.setImageDrawable(drawable);
}
itemView.setOnClickListener(v -> {
emojiEventListener.onEmojiSelected(model.emoji.getValue());
});
if (allowVariations && model.emoji.hasMultipleVariations()) {
itemView.setOnLongClickListener(v -> {
popup.dismiss();
popup.setVariations(model.emoji.getVariations());
popup.showAsDropDown(itemView, 0, -(2 * itemView.getHeight()));
variationSelectorListener.onVariationSelectorStateChanged(true);
return true;
});
} else {
itemView.setOnLongClickListener(null);
}
}
}
@Override
public void onDismiss() {
variationSelectorListener.onVariationSelectorStateChanged(false);
public static class EmojiTextModel implements MappingModel<EmojiTextModel>, HasKey {
private final String key;
private final Emoji emoji;
public EmojiTextModel(@NonNull String key, @NonNull Emoji emoji) {
this.key = key;
this.emoji = emoji;
}
@Override
public @NonNull String getKey() {
return key;
}
public @NonNull Emoji getEmoji() {
return emoji;
}
@Override
public boolean areItemsTheSame(@NonNull EmojiTextModel newItem) {
return newItem.emoji.getValue().equals(emoji.getValue());
}
@Override
public boolean areContentsTheSame(@NonNull EmojiTextModel newItem) {
return areItemsTheSame(newItem);
}
}
static class EmojiViewHolder extends RecyclerView.ViewHolder {
static class EmojiTextViewHolder extends MappingViewHolder<EmojiTextModel> {
private final ImageView imageView;
private final AsciiEmojiView textView;
private final ImageView hintCorner;
private final EmojiEventListener emojiEventListener;
private final AsciiEmojiView textView;
public EmojiViewHolder(@NonNull View itemView) {
public EmojiTextViewHolder(@NonNull View itemView,
@NonNull EmojiEventListener emojiEventListener)
{
super(itemView);
this.imageView = itemView.findViewById(R.id.emoji_image);
this.textView = itemView.findViewById(R.id.emoji_text);
this.hintCorner = itemView.findViewById(R.id.emoji_variation_hint);
this.emojiEventListener = emojiEventListener;
this.textView = itemView.findViewById(R.id.emoji_text);
}
@Override
public void bind(@NonNull EmojiTextModel model) {
textView.setEmoji(model.emoji.getValue());
itemView.setOnClickListener(v -> {
emojiEventListener.onEmojiSelected(model.emoji.getValue());
});
}
}
public static class EmojiNoResultsModel implements MappingModel<EmojiNoResultsModel> {
@Override
public boolean areItemsTheSame(@NonNull EmojiNoResultsModel newItem) {
return true;
}
@Override
public boolean areContentsTheSame(@NonNull EmojiNoResultsModel newItem) {
return true;
}
}
public interface HasKey {
@NonNull String getKey();
}
public interface VariationSelectorListener {

73
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPages.java

File diff suppressed because one or more lines are too long

227
app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.components.emoji;
import android.annotation.TargetApi;
import static org.session.libsession.utilities.Util.runOnMain;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
@ -9,145 +10,156 @@ import android.graphics.Paint;
import android.graphics.PixelFormat;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.os.Build.VERSION;
import android.os.Build.VERSION_CODES;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.widget.TextView;
import network.loki.messenger.R;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiPageBitmap;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiTree;
import org.session.libsignal.utilities.Log;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import org.session.libsession.utilities.FutureTaskListener;
import org.session.libsession.utilities.Util;
import org.session.libsignal.utilities.Pair;
import org.session.libsignal.utilities.Log;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo;
import org.thoughtcrime.securesms.components.emoji.parsing.EmojiParser;
import org.thoughtcrime.securesms.emoji.EmojiPageCache;
import org.thoughtcrime.securesms.emoji.EmojiSource;
import org.thoughtcrime.securesms.util.Util;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicBoolean;
class EmojiProvider {
private static final String TAG = EmojiProvider.class.getSimpleName();
private static volatile EmojiProvider instance = null;
private static final Paint paint = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
public class EmojiProvider {
private final EmojiTree emojiTree = new EmojiTree();
private static final String TAG = Log.tag(EmojiProvider.class);
private static final Paint PAINT = new Paint(Paint.FILTER_BITMAP_FLAG | Paint.ANTI_ALIAS_FLAG);
private static final int EMOJI_RAW_HEIGHT = 64;
private static final int EMOJI_RAW_WIDTH = 64;
private static final int EMOJI_VERT_PAD = 0;
private static final int EMOJI_PER_ROW = 32;
private final float decodeScale;
private final float verticalPad;
public static EmojiProvider getInstance(Context context) {
if (instance == null) {
synchronized (EmojiProvider.class) {
if (instance == null) {
instance = new EmojiProvider(context);
}
}
}
return instance;
}
private EmojiProvider(Context context) {
this.decodeScale = Math.min(1f, context.getResources().getDimension(R.dimen.emoji_drawer_size) / EMOJI_RAW_HEIGHT);
this.verticalPad = EMOJI_VERT_PAD * this.decodeScale;
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
if (page.hasSpriteMap()) {
EmojiPageBitmap pageBitmap = new EmojiPageBitmap(context, page, decodeScale);