diff --git a/app/build.gradle b/app/build.gradle index f3ef54b1e3..741120f232 100644 --- a/app/build.gradle +++ b/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, diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4195bf2e02..ab7879967d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/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"> diff --git a/app/src/main/assets/emoji/Activity.webp b/app/src/main/assets/emoji/Activity.webp new file mode 100644 index 0000000000..4db973ff49 Binary files /dev/null and b/app/src/main/assets/emoji/Activity.webp differ diff --git a/app/src/main/assets/emoji/Flags_0.webp b/app/src/main/assets/emoji/Flags_0.webp new file mode 100644 index 0000000000..b170104812 Binary files /dev/null and b/app/src/main/assets/emoji/Flags_0.webp differ diff --git a/app/src/main/assets/emoji/Flags_1.webp b/app/src/main/assets/emoji/Flags_1.webp new file mode 100644 index 0000000000..f8e73daecd Binary files /dev/null and b/app/src/main/assets/emoji/Flags_1.webp differ diff --git a/app/src/main/assets/emoji/Foods.webp b/app/src/main/assets/emoji/Foods.webp new file mode 100644 index 0000000000..72199546dd Binary files /dev/null and b/app/src/main/assets/emoji/Foods.webp differ diff --git a/app/src/main/assets/emoji/Nature.webp b/app/src/main/assets/emoji/Nature.webp new file mode 100644 index 0000000000..e2a07a6162 Binary files /dev/null and b/app/src/main/assets/emoji/Nature.webp differ diff --git a/app/src/main/assets/emoji/Objects.webp b/app/src/main/assets/emoji/Objects.webp new file mode 100644 index 0000000000..0b264e16e5 Binary files /dev/null and b/app/src/main/assets/emoji/Objects.webp differ diff --git a/app/src/main/assets/emoji/People_0.webp b/app/src/main/assets/emoji/People_0.webp new file mode 100644 index 0000000000..d684f3fcac Binary files /dev/null and b/app/src/main/assets/emoji/People_0.webp differ diff --git a/app/src/main/assets/emoji/People_1.webp b/app/src/main/assets/emoji/People_1.webp new file mode 100644 index 0000000000..655bfcc512 Binary files /dev/null and b/app/src/main/assets/emoji/People_1.webp differ diff --git a/app/src/main/assets/emoji/People_2.webp b/app/src/main/assets/emoji/People_2.webp new file mode 100644 index 0000000000..546179fbbf Binary files /dev/null and b/app/src/main/assets/emoji/People_2.webp differ diff --git a/app/src/main/assets/emoji/People_3.webp b/app/src/main/assets/emoji/People_3.webp new file mode 100644 index 0000000000..0fc68ab1a8 Binary files /dev/null and b/app/src/main/assets/emoji/People_3.webp differ diff --git a/app/src/main/assets/emoji/People_4.webp b/app/src/main/assets/emoji/People_4.webp new file mode 100644 index 0000000000..2ee56dd2e9 Binary files /dev/null and b/app/src/main/assets/emoji/People_4.webp differ diff --git a/app/src/main/assets/emoji/People_5.webp b/app/src/main/assets/emoji/People_5.webp new file mode 100644 index 0000000000..4810e2fcbb Binary files /dev/null and b/app/src/main/assets/emoji/People_5.webp differ diff --git a/app/src/main/assets/emoji/People_6.webp b/app/src/main/assets/emoji/People_6.webp new file mode 100644 index 0000000000..2343cfb87b Binary files /dev/null and b/app/src/main/assets/emoji/People_6.webp differ diff --git a/app/src/main/assets/emoji/People_7.webp b/app/src/main/assets/emoji/People_7.webp new file mode 100644 index 0000000000..28a70ae90a Binary files /dev/null and b/app/src/main/assets/emoji/People_7.webp differ diff --git a/app/src/main/assets/emoji/People_8.webp b/app/src/main/assets/emoji/People_8.webp new file mode 100644 index 0000000000..ea2cdada5c Binary files /dev/null and b/app/src/main/assets/emoji/People_8.webp differ diff --git a/app/src/main/assets/emoji/People_9.webp b/app/src/main/assets/emoji/People_9.webp new file mode 100644 index 0000000000..fb3333f2b4 Binary files /dev/null and b/app/src/main/assets/emoji/People_9.webp differ diff --git a/app/src/main/assets/emoji/Places.webp b/app/src/main/assets/emoji/Places.webp new file mode 100644 index 0000000000..5dc763b2b6 Binary files /dev/null and b/app/src/main/assets/emoji/Places.webp differ diff --git a/app/src/main/assets/emoji/Symbols.webp b/app/src/main/assets/emoji/Symbols.webp new file mode 100644 index 0000000000..091017a19b Binary files /dev/null and b/app/src/main/assets/emoji/Symbols.webp differ diff --git a/app/src/main/assets/emoji/emoji_data.json b/app/src/main/assets/emoji/emoji_data.json new file mode 100644 index 0000000000..62059254eb --- /dev/null +++ b/app/src/main/assets/emoji/emoji_data.json @@ -0,0 +1 @@ +{"emoji":{"People_0":[["d83dde00"],["d83dde03"],["d83dde04"],["d83dde01"],["d83dde06"],["d83dde05"],["d83edd23"],["d83dde02"],["d83dde42"],["d83dde43"],["d83edee0"],["d83dde09"],["d83dde0a"],["d83dde07"],["d83edd70"],["d83dde0d"],["d83edd29"],["d83dde18"],["d83dde17"],["263afe0f"],["d83dde1a"],["d83dde19"],["d83edd72"],["d83dde0b"],["d83dde1b"],["d83dde1c"],["d83edd2a"],["d83dde1d"],["d83edd11"],["d83edd17"],["d83edd2d"],["d83edee2"],["d83edee3"],["d83edd2b"],["d83edd14"],["d83edee1"],["d83edd10"],["d83edd28"],["d83dde10"],["d83dde11"],["d83dde36"],["d83edee5"],["d83dde36200dd83cdf2bfe0f"],["d83dde0f"],["d83dde12"],["d83dde44"],["d83dde2c"],["d83dde2e200dd83ddca8"],["d83edd25"],["d83dde0c"],["d83dde14"],["d83dde2a"],["d83edd24"],["d83dde34"],["d83dde37"],["d83edd12"],["d83edd15"],["d83edd22"],["d83edd2e"],["d83edd27"],["d83edd75"],["d83edd76"],["d83edd74"],["d83dde35"],["d83dde35200dd83ddcab"],["d83edd2f"],["d83edd20"],["d83edd73"],["d83edd78"],["d83dde0e"],["d83edd13"],["d83eddd0"],["d83dde15"],["d83edee4"],["d83dde1f"],["d83dde41"],["2639fe0f"],["d83dde2e"],["d83dde2f"],["d83dde32"],["d83dde33"],["d83edd7a"],["d83edd79"],["d83dde26"],["d83dde27"],["d83dde28"],["d83dde30"],["d83dde25"],["d83dde22"],["d83dde2d"],["d83dde31"],["d83dde16"],["d83dde23"],["d83dde1e"],["d83dde13"],["d83dde29"],["d83dde2b"],["d83edd71"],["d83dde24"],["d83dde21"],["d83dde20"],["d83edd2c"],["d83dde08"],["d83ddc7f"],["d83ddc80"],["2620fe0f"],["d83ddca9"],["d83edd21"],["d83ddc79"],["d83ddc7a"],["d83ddc7b"],["d83ddc7d"],["d83ddc7e"],["d83edd16"],["d83dde3a"],["d83dde38"],["d83dde39"],["d83dde3b"],["d83dde3c"],["d83dde3d"],["d83dde40"],["d83dde3f"],["d83dde3e"],["d83dde48"],["d83dde49"],["d83dde4a"],["d83ddc8b"],["d83ddc8c"],["d83ddc98"],["d83ddc9d"],["d83ddc96"],["d83ddc97"],["d83ddc93"],["d83ddc9e"],["d83ddc95"],["d83ddc9f"],["2763fe0f"],["d83ddc94"],["2764fe0f200dd83ddd25"],["2764fe0f200dd83ede79"],["2764fe0f"],["d83edde1"],["d83ddc9b"],["d83ddc9a"],["d83ddc99"],["d83ddc9c"],["d83edd0e"],["d83ddda4"],["d83edd0d"],["d83ddcaf"],["d83ddca2"],["d83ddca5"],["d83ddcab"],["d83ddca6"],["d83ddca8"],["d83ddd73fe0f"],["d83ddca3"],["d83ddcac"],["d83ddc41fe0f200dd83ddde8fe0f"],["d83ddde8fe0f"],["d83dddeffe0f"],["d83ddcad"],["d83ddca4"],["d83ddc4b","d83ddc4bd83cdffb","d83ddc4bd83cdffc","d83ddc4bd83cdffd","d83ddc4bd83cdffe","d83ddc4bd83cdfff"],["d83edd1a","d83edd1ad83cdffb","d83edd1ad83cdffc","d83edd1ad83cdffd","d83edd1ad83cdffe","d83edd1ad83cdfff"],["d83ddd90fe0f","d83ddd90d83cdffb","d83ddd90d83cdffc","d83ddd90d83cdffd","d83ddd90d83cdffe","d83ddd90d83cdfff"],["270b","270bd83cdffb","270bd83cdffc","270bd83cdffd","270bd83cdffe","270bd83cdfff"],["d83ddd96","d83ddd96d83cdffb","d83ddd96d83cdffc","d83ddd96d83cdffd","d83ddd96d83cdffe","d83ddd96d83cdfff"],["d83edef1","d83edef1d83cdffb","d83edef1d83cdffc","d83edef1d83cdffd","d83edef1d83cdffe","d83edef1d83cdfff"],["d83edef2","d83edef2d83cdffb","d83edef2d83cdffc","d83edef2d83cdffd","d83edef2d83cdffe","d83edef2d83cdfff"],["d83edef3","d83edef3d83cdffb","d83edef3d83cdffc","d83edef3d83cdffd","d83edef3d83cdffe","d83edef3d83cdfff"],["d83edef4","d83edef4d83cdffb","d83edef4d83cdffc","d83edef4d83cdffd","d83edef4d83cdffe","d83edef4d83cdfff"],["d83ddc4c","d83ddc4cd83cdffb","d83ddc4cd83cdffc","d83ddc4cd83cdffd","d83ddc4cd83cdffe","d83ddc4cd83cdfff"],["d83edd0c","d83edd0cd83cdffb","d83edd0cd83cdffc","d83edd0cd83cdffd","d83edd0cd83cdffe","d83edd0cd83cdfff"],["d83edd0f","d83edd0fd83cdffb","d83edd0fd83cdffc","d83edd0fd83cdffd","d83edd0fd83cdffe","d83edd0fd83cdfff"],["270cfe0f","270cd83cdffb","270cd83cdffc","270cd83cdffd","270cd83cdffe","270cd83cdfff"],["d83edd1e","d83edd1ed83cdffb","d83edd1ed83cdffc","d83edd1ed83cdffd","d83edd1ed83cdffe","d83edd1ed83cdfff"],["d83edef0","d83edef0d83cdffb","d83edef0d83cdffc","d83edef0d83cdffd","d83edef0d83cdffe","d83edef0d83cdfff"]],"People_1":[["d83edd1f","d83edd1fd83cdffb","d83edd1fd83cdffc","d83edd1fd83cdffd","d83edd1fd83cdffe","d83edd1fd83cdfff"],["d83edd18","d83edd18d83cdffb","d83edd18d83cdffc","d83edd18d83cdffd","d83edd18d83cdffe","d83edd18d83cdfff"],["d83edd19","d83edd19d83cdffb","d83edd19d83cdffc","d83edd19d83cdffd","d83edd19d83cdffe","d83edd19d83cdfff"],["d83ddc48","d83ddc48d83cdffb","d83ddc48d83cdffc","d83ddc48d83cdffd","d83ddc48d83cdffe","d83ddc48d83cdfff"],["d83ddc49","d83ddc49d83cdffb","d83ddc49d83cdffc","d83ddc49d83cdffd","d83ddc49d83cdffe","d83ddc49d83cdfff"],["d83ddc46","d83ddc46d83cdffb","d83ddc46d83cdffc","d83ddc46d83cdffd","d83ddc46d83cdffe","d83ddc46d83cdfff"],["d83ddd95","d83ddd95d83cdffb","d83ddd95d83cdffc","d83ddd95d83cdffd","d83ddd95d83cdffe","d83ddd95d83cdfff"],["d83ddc47","d83ddc47d83cdffb","d83ddc47d83cdffc","d83ddc47d83cdffd","d83ddc47d83cdffe","d83ddc47d83cdfff"],["261dfe0f","261dd83cdffb","261dd83cdffc","261dd83cdffd","261dd83cdffe","261dd83cdfff"],["d83edef5","d83edef5d83cdffb","d83edef5d83cdffc","d83edef5d83cdffd","d83edef5d83cdffe","d83edef5d83cdfff"],["d83ddc4d","d83ddc4dd83cdffb","d83ddc4dd83cdffc","d83ddc4dd83cdffd","d83ddc4dd83cdffe","d83ddc4dd83cdfff"],["d83ddc4e","d83ddc4ed83cdffb","d83ddc4ed83cdffc","d83ddc4ed83cdffd","d83ddc4ed83cdffe","d83ddc4ed83cdfff"],["270a","270ad83cdffb","270ad83cdffc","270ad83cdffd","270ad83cdffe","270ad83cdfff"],["d83ddc4a","d83ddc4ad83cdffb","d83ddc4ad83cdffc","d83ddc4ad83cdffd","d83ddc4ad83cdffe","d83ddc4ad83cdfff"],["d83edd1b","d83edd1bd83cdffb","d83edd1bd83cdffc","d83edd1bd83cdffd","d83edd1bd83cdffe","d83edd1bd83cdfff"],["d83edd1c","d83edd1cd83cdffb","d83edd1cd83cdffc","d83edd1cd83cdffd","d83edd1cd83cdffe","d83edd1cd83cdfff"],["d83ddc4f","d83ddc4fd83cdffb","d83ddc4fd83cdffc","d83ddc4fd83cdffd","d83ddc4fd83cdffe","d83ddc4fd83cdfff"],["d83dde4c","d83dde4cd83cdffb","d83dde4cd83cdffc","d83dde4cd83cdffd","d83dde4cd83cdffe","d83dde4cd83cdfff"],["d83edef6","d83edef6d83cdffb","d83edef6d83cdffc","d83edef6d83cdffd","d83edef6d83cdffe","d83edef6d83cdfff"],["d83ddc50","d83ddc50d83cdffb","d83ddc50d83cdffc","d83ddc50d83cdffd","d83ddc50d83cdffe","d83ddc50d83cdfff"],["d83edd32","d83edd32d83cdffb","d83edd32d83cdffc","d83edd32d83cdffd","d83edd32d83cdffe","d83edd32d83cdfff"],["d83edd1d","d83edd1dd83cdffb","d83edd1dd83cdffc","d83edd1dd83cdffd","d83edd1dd83cdffe","d83edd1dd83cdfff","d83edef1d83cdffb200dd83edef2d83cdffc","d83edef1d83cdffb200dd83edef2d83cdffd","d83edef1d83cdffb200dd83edef2d83cdffe","d83edef1d83cdffb200dd83edef2d83cdfff","d83edef1d83cdffc200dd83edef2d83cdffb","d83edef1d83cdffc200dd83edef2d83cdffd","d83edef1d83cdffc200dd83edef2d83cdffe","d83edef1d83cdffc200dd83edef2d83cdfff","d83edef1d83cdffd200dd83edef2d83cdffb","d83edef1d83cdffd200dd83edef2d83cdffc","d83edef1d83cdffd200dd83edef2d83cdffe","d83edef1d83cdffd200dd83edef2d83cdfff","d83edef1d83cdffe200dd83edef2d83cdffb","d83edef1d83cdffe200dd83edef2d83cdffc","d83edef1d83cdffe200dd83edef2d83cdffd","d83edef1d83cdffe200dd83edef2d83cdfff","d83edef1d83cdfff200dd83edef2d83cdffb","d83edef1d83cdfff200dd83edef2d83cdffc","d83edef1d83cdfff200dd83edef2d83cdffd","d83edef1d83cdfff200dd83edef2d83cdffe"],["d83dde4f","d83dde4fd83cdffb","d83dde4fd83cdffc","d83dde4fd83cdffd","d83dde4fd83cdffe","d83dde4fd83cdfff"],["270dfe0f","270dd83cdffb","270dd83cdffc","270dd83cdffd","270dd83cdffe","270dd83cdfff"],["d83ddc85","d83ddc85d83cdffb","d83ddc85d83cdffc","d83ddc85d83cdffd","d83ddc85d83cdffe","d83ddc85d83cdfff"],["d83edd33","d83edd33d83cdffb","d83edd33d83cdffc","d83edd33d83cdffd","d83edd33d83cdffe","d83edd33d83cdfff"],["d83ddcaa","d83ddcaad83cdffb","d83ddcaad83cdffc","d83ddcaad83cdffd","d83ddcaad83cdffe","d83ddcaad83cdfff"],["d83eddbe"],["d83eddbf"],["d83eddb5","d83eddb5d83cdffb","d83eddb5d83cdffc","d83eddb5d83cdffd","d83eddb5d83cdffe","d83eddb5d83cdfff"],["d83eddb6","d83eddb6d83cdffb","d83eddb6d83cdffc","d83eddb6d83cdffd","d83eddb6d83cdffe","d83eddb6d83cdfff"],["d83ddc42","d83ddc42d83cdffb","d83ddc42d83cdffc","d83ddc42d83cdffd","d83ddc42d83cdffe","d83ddc42d83cdfff"],["d83eddbb","d83eddbbd83cdffb","d83eddbbd83cdffc","d83eddbbd83cdffd","d83eddbbd83cdffe","d83eddbbd83cdfff"],["d83ddc43","d83ddc43d83cdffb","d83ddc43d83cdffc","d83ddc43d83cdffd","d83ddc43d83cdffe","d83ddc43d83cdfff"],["d83edde0"],["d83edec0"],["d83edec1"],["d83eddb7"],["d83eddb4"],["d83ddc40"],["d83ddc41fe0f"],["d83ddc45"],["d83ddc44"],["d83edee6"],["d83ddc76","d83ddc76d83cdffb","d83ddc76d83cdffc","d83ddc76d83cdffd","d83ddc76d83cdffe","d83ddc76d83cdfff"],["d83eddd2","d83eddd2d83cdffb","d83eddd2d83cdffc","d83eddd2d83cdffd","d83eddd2d83cdffe","d83eddd2d83cdfff"],["d83ddc66","d83ddc66d83cdffb","d83ddc66d83cdffc","d83ddc66d83cdffd","d83ddc66d83cdffe","d83ddc66d83cdfff"],["d83ddc67","d83ddc67d83cdffb","d83ddc67d83cdffc","d83ddc67d83cdffd","d83ddc67d83cdffe","d83ddc67d83cdfff"],["d83eddd1","d83eddd1d83cdffb","d83eddd1d83cdffc","d83eddd1d83cdffd","d83eddd1d83cdffe","d83eddd1d83cdfff"]],"People_2":[["d83ddc71","d83ddc71d83cdffb","d83ddc71d83cdffc","d83ddc71d83cdffd","d83ddc71d83cdffe","d83ddc71d83cdfff"],["d83ddc68","d83ddc68d83cdffb","d83ddc68d83cdffc","d83ddc68d83cdffd","d83ddc68d83cdffe","d83ddc68d83cdfff"],["d83eddd4","d83eddd4d83cdffb","d83eddd4d83cdffc","d83eddd4d83cdffd","d83eddd4d83cdffe","d83eddd4d83cdfff"],["d83eddd4200d2642fe0f","d83eddd4d83cdffb200d2642fe0f","d83eddd4d83cdffc200d2642fe0f","d83eddd4d83cdffd200d2642fe0f","d83eddd4d83cdffe200d2642fe0f","d83eddd4d83cdfff200d2642fe0f"],["d83eddd4200d2640fe0f","d83eddd4d83cdffb200d2640fe0f","d83eddd4d83cdffc200d2640fe0f","d83eddd4d83cdffd200d2640fe0f","d83eddd4d83cdffe200d2640fe0f","d83eddd4d83cdfff200d2640fe0f"],["d83ddc68200dd83eddb0","d83ddc68d83cdffb200dd83eddb0","d83ddc68d83cdffc200dd83eddb0","d83ddc68d83cdffd200dd83eddb0","d83ddc68d83cdffe200dd83eddb0","d83ddc68d83cdfff200dd83eddb0"],["d83ddc68200dd83eddb1","d83ddc68d83cdffb200dd83eddb1","d83ddc68d83cdffc200dd83eddb1","d83ddc68d83cdffd200dd83eddb1","d83ddc68d83cdffe200dd83eddb1","d83ddc68d83cdfff200dd83eddb1"],["d83ddc68200dd83eddb3","d83ddc68d83cdffb200dd83eddb3","d83ddc68d83cdffc200dd83eddb3","d83ddc68d83cdffd200dd83eddb3","d83ddc68d83cdffe200dd83eddb3","d83ddc68d83cdfff200dd83eddb3"],["d83ddc68200dd83eddb2","d83ddc68d83cdffb200dd83eddb2","d83ddc68d83cdffc200dd83eddb2","d83ddc68d83cdffd200dd83eddb2","d83ddc68d83cdffe200dd83eddb2","d83ddc68d83cdfff200dd83eddb2"],["d83ddc69","d83ddc69d83cdffb","d83ddc69d83cdffc","d83ddc69d83cdffd","d83ddc69d83cdffe","d83ddc69d83cdfff"],["d83ddc69200dd83eddb0","d83ddc69d83cdffb200dd83eddb0","d83ddc69d83cdffc200dd83eddb0","d83ddc69d83cdffd200dd83eddb0","d83ddc69d83cdffe200dd83eddb0","d83ddc69d83cdfff200dd83eddb0"],["d83eddd1200dd83eddb0","d83eddd1d83cdffb200dd83eddb0","d83eddd1d83cdffc200dd83eddb0","d83eddd1d83cdffd200dd83eddb0","d83eddd1d83cdffe200dd83eddb0","d83eddd1d83cdfff200dd83eddb0"],["d83ddc69200dd83eddb1","d83ddc69d83cdffb200dd83eddb1","d83ddc69d83cdffc200dd83eddb1","d83ddc69d83cdffd200dd83eddb1","d83ddc69d83cdffe200dd83eddb1","d83ddc69d83cdfff200dd83eddb1"],["d83eddd1200dd83eddb1","d83eddd1d83cdffb200dd83eddb1","d83eddd1d83cdffc200dd83eddb1","d83eddd1d83cdffd200dd83eddb1","d83eddd1d83cdffe200dd83eddb1","d83eddd1d83cdfff200dd83eddb1"],["d83ddc69200dd83eddb3","d83ddc69d83cdffb200dd83eddb3","d83ddc69d83cdffc200dd83eddb3","d83ddc69d83cdffd200dd83eddb3","d83ddc69d83cdffe200dd83eddb3","d83ddc69d83cdfff200dd83eddb3"],["d83eddd1200dd83eddb3","d83eddd1d83cdffb200dd83eddb3","d83eddd1d83cdffc200dd83eddb3","d83eddd1d83cdffd200dd83eddb3","d83eddd1d83cdffe200dd83eddb3","d83eddd1d83cdfff200dd83eddb3"],["d83ddc69200dd83eddb2","d83ddc69d83cdffb200dd83eddb2","d83ddc69d83cdffc200dd83eddb2","d83ddc69d83cdffd200dd83eddb2","d83ddc69d83cdffe200dd83eddb2","d83ddc69d83cdfff200dd83eddb2"],["d83eddd1200dd83eddb2","d83eddd1d83cdffb200dd83eddb2","d83eddd1d83cdffc200dd83eddb2","d83eddd1d83cdffd200dd83eddb2","d83eddd1d83cdffe200dd83eddb2","d83eddd1d83cdfff200dd83eddb2"],["d83ddc71200d2640fe0f","d83ddc71d83cdffb200d2640fe0f","d83ddc71d83cdffc200d2640fe0f","d83ddc71d83cdffd200d2640fe0f","d83ddc71d83cdffe200d2640fe0f","d83ddc71d83cdfff200d2640fe0f"],["d83ddc71200d2642fe0f","d83ddc71d83cdffb200d2642fe0f","d83ddc71d83cdffc200d2642fe0f","d83ddc71d83cdffd200d2642fe0f","d83ddc71d83cdffe200d2642fe0f","d83ddc71d83cdfff200d2642fe0f"],["d83eddd3","d83eddd3d83cdffb","d83eddd3d83cdffc","d83eddd3d83cdffd","d83eddd3d83cdffe","d83eddd3d83cdfff"],["d83ddc74","d83ddc74d83cdffb","d83ddc74d83cdffc","d83ddc74d83cdffd","d83ddc74d83cdffe","d83ddc74d83cdfff"],["d83ddc75","d83ddc75d83cdffb","d83ddc75d83cdffc","d83ddc75d83cdffd","d83ddc75d83cdffe","d83ddc75d83cdfff"],["d83dde4d","d83dde4dd83cdffb","d83dde4dd83cdffc","d83dde4dd83cdffd","d83dde4dd83cdffe","d83dde4dd83cdfff"],["d83dde4d200d2642fe0f","d83dde4dd83cdffb200d2642fe0f","d83dde4dd83cdffc200d2642fe0f","d83dde4dd83cdffd200d2642fe0f","d83dde4dd83cdffe200d2642fe0f","d83dde4dd83cdfff200d2642fe0f"],["d83dde4d200d2640fe0f","d83dde4dd83cdffb200d2640fe0f","d83dde4dd83cdffc200d2640fe0f","d83dde4dd83cdffd200d2640fe0f","d83dde4dd83cdffe200d2640fe0f","d83dde4dd83cdfff200d2640fe0f"],["d83dde4e","d83dde4ed83cdffb","d83dde4ed83cdffc","d83dde4ed83cdffd","d83dde4ed83cdffe","d83dde4ed83cdfff"],["d83dde4e200d2642fe0f","d83dde4ed83cdffb200d2642fe0f","d83dde4ed83cdffc200d2642fe0f","d83dde4ed83cdffd200d2642fe0f","d83dde4ed83cdffe200d2642fe0f","d83dde4ed83cdfff200d2642fe0f"],["d83dde4e200d2640fe0f","d83dde4ed83cdffb200d2640fe0f","d83dde4ed83cdffc200d2640fe0f","d83dde4ed83cdffd200d2640fe0f","d83dde4ed83cdffe200d2640fe0f","d83dde4ed83cdfff200d2640fe0f"],["d83dde45","d83dde45d83cdffb","d83dde45d83cdffc","d83dde45d83cdffd","d83dde45d83cdffe","d83dde45d83cdfff"],["d83dde45200d2642fe0f","d83dde45d83cdffb200d2642fe0f","d83dde45d83cdffc200d2642fe0f","d83dde45d83cdffd200d2642fe0f","d83dde45d83cdffe200d2642fe0f","d83dde45d83cdfff200d2642fe0f"],["d83dde45200d2640fe0f","d83dde45d83cdffb200d2640fe0f","d83dde45d83cdffc200d2640fe0f","d83dde45d83cdffd200d2640fe0f","d83dde45d83cdffe200d2640fe0f","d83dde45d83cdfff200d2640fe0f"],["d83dde46","d83dde46d83cdffb","d83dde46d83cdffc","d83dde46d83cdffd","d83dde46d83cdffe","d83dde46d83cdfff"],["d83dde46200d2642fe0f","d83dde46d83cdffb200d2642fe0f","d83dde46d83cdffc200d2642fe0f","d83dde46d83cdffd200d2642fe0f","d83dde46d83cdffe200d2642fe0f","d83dde46d83cdfff200d2642fe0f"],["d83dde46200d2640fe0f","d83dde46d83cdffb200d2640fe0f","d83dde46d83cdffc200d2640fe0f","d83dde46d83cdffd200d2640fe0f","d83dde46d83cdffe200d2640fe0f","d83dde46d83cdfff200d2640fe0f"],["d83ddc81","d83ddc81d83cdffb","d83ddc81d83cdffc","d83ddc81d83cdffd","d83ddc81d83cdffe","d83ddc81d83cdfff"],["d83ddc81200d2642fe0f","d83ddc81d83cdffb200d2642fe0f","d83ddc81d83cdffc200d2642fe0f","d83ddc81d83cdffd200d2642fe0f","d83ddc81d83cdffe200d2642fe0f","d83ddc81d83cdfff200d2642fe0f"],["d83ddc81200d2640fe0f","d83ddc81d83cdffb200d2640fe0f","d83ddc81d83cdffc200d2640fe0f","d83ddc81d83cdffd200d2640fe0f","d83ddc81d83cdffe200d2640fe0f","d83ddc81d83cdfff200d2640fe0f"],["d83dde4b","d83dde4bd83cdffb","d83dde4bd83cdffc","d83dde4bd83cdffd","d83dde4bd83cdffe","d83dde4bd83cdfff"],["d83dde4b200d2642fe0f","d83dde4bd83cdffb200d2642fe0f","d83dde4bd83cdffc200d2642fe0f","d83dde4bd83cdffd200d2642fe0f","d83dde4bd83cdffe200d2642fe0f","d83dde4bd83cdfff200d2642fe0f"],["d83dde4b200d2640fe0f","d83dde4bd83cdffb200d2640fe0f","d83dde4bd83cdffc200d2640fe0f","d83dde4bd83cdffd200d2640fe0f","d83dde4bd83cdffe200d2640fe0f","d83dde4bd83cdfff200d2640fe0f"],["d83eddcf","d83eddcfd83cdffb","d83eddcfd83cdffc","d83eddcfd83cdffd","d83eddcfd83cdffe","d83eddcfd83cdfff"]],"People_3":[["d83eddcf200d2642fe0f","d83eddcfd83cdffb200d2642fe0f","d83eddcfd83cdffc200d2642fe0f","d83eddcfd83cdffd200d2642fe0f","d83eddcfd83cdffe200d2642fe0f","d83eddcfd83cdfff200d2642fe0f"],["d83eddcf200d2640fe0f","d83eddcfd83cdffb200d2640fe0f","d83eddcfd83cdffc200d2640fe0f","d83eddcfd83cdffd200d2640fe0f","d83eddcfd83cdffe200d2640fe0f","d83eddcfd83cdfff200d2640fe0f"],["d83dde47","d83dde47d83cdffb","d83dde47d83cdffc","d83dde47d83cdffd","d83dde47d83cdffe","d83dde47d83cdfff"],["d83dde47200d2642fe0f","d83dde47d83cdffb200d2642fe0f","d83dde47d83cdffc200d2642fe0f","d83dde47d83cdffd200d2642fe0f","d83dde47d83cdffe200d2642fe0f","d83dde47d83cdfff200d2642fe0f"],["d83dde47200d2640fe0f","d83dde47d83cdffb200d2640fe0f","d83dde47d83cdffc200d2640fe0f","d83dde47d83cdffd200d2640fe0f","d83dde47d83cdffe200d2640fe0f","d83dde47d83cdfff200d2640fe0f"],["d83edd26","d83edd26d83cdffb","d83edd26d83cdffc","d83edd26d83cdffd","d83edd26d83cdffe","d83edd26d83cdfff"],["d83edd26200d2642fe0f","d83edd26d83cdffb200d2642fe0f","d83edd26d83cdffc200d2642fe0f","d83edd26d83cdffd200d2642fe0f","d83edd26d83cdffe200d2642fe0f","d83edd26d83cdfff200d2642fe0f"],["d83edd26200d2640fe0f","d83edd26d83cdffb200d2640fe0f","d83edd26d83cdffc200d2640fe0f","d83edd26d83cdffd200d2640fe0f","d83edd26d83cdffe200d2640fe0f","d83edd26d83cdfff200d2640fe0f"],["d83edd37","d83edd37d83cdffb","d83edd37d83cdffc","d83edd37d83cdffd","d83edd37d83cdffe","d83edd37d83cdfff"],["d83edd37200d2642fe0f","d83edd37d83cdffb200d2642fe0f","d83edd37d83cdffc200d2642fe0f","d83edd37d83cdffd200d2642fe0f","d83edd37d83cdffe200d2642fe0f","d83edd37d83cdfff200d2642fe0f"],["d83edd37200d2640fe0f","d83edd37d83cdffb200d2640fe0f","d83edd37d83cdffc200d2640fe0f","d83edd37d83cdffd200d2640fe0f","d83edd37d83cdffe200d2640fe0f","d83edd37d83cdfff200d2640fe0f"],["d83eddd1200d2695fe0f","d83eddd1d83cdffb200d2695fe0f","d83eddd1d83cdffc200d2695fe0f","d83eddd1d83cdffd200d2695fe0f","d83eddd1d83cdffe200d2695fe0f","d83eddd1d83cdfff200d2695fe0f"],["d83ddc68200d2695fe0f","d83ddc68d83cdffb200d2695fe0f","d83ddc68d83cdffc200d2695fe0f","d83ddc68d83cdffd200d2695fe0f","d83ddc68d83cdffe200d2695fe0f","d83ddc68d83cdfff200d2695fe0f"],["d83ddc69200d2695fe0f","d83ddc69d83cdffb200d2695fe0f","d83ddc69d83cdffc200d2695fe0f","d83ddc69d83cdffd200d2695fe0f","d83ddc69d83cdffe200d2695fe0f","d83ddc69d83cdfff200d2695fe0f"],["d83eddd1200dd83cdf93","d83eddd1d83cdffb200dd83cdf93","d83eddd1d83cdffc200dd83cdf93","d83eddd1d83cdffd200dd83cdf93","d83eddd1d83cdffe200dd83cdf93","d83eddd1d83cdfff200dd83cdf93"],["d83ddc68200dd83cdf93","d83ddc68d83cdffb200dd83cdf93","d83ddc68d83cdffc200dd83cdf93","d83ddc68d83cdffd200dd83cdf93","d83ddc68d83cdffe200dd83cdf93","d83ddc68d83cdfff200dd83cdf93"],["d83ddc69200dd83cdf93","d83ddc69d83cdffb200dd83cdf93","d83ddc69d83cdffc200dd83cdf93","d83ddc69d83cdffd200dd83cdf93","d83ddc69d83cdffe200dd83cdf93","d83ddc69d83cdfff200dd83cdf93"],["d83eddd1200dd83cdfeb","d83eddd1d83cdffb200dd83cdfeb","d83eddd1d83cdffc200dd83cdfeb","d83eddd1d83cdffd200dd83cdfeb","d83eddd1d83cdffe200dd83cdfeb","d83eddd1d83cdfff200dd83cdfeb"],["d83ddc68200dd83cdfeb","d83ddc68d83cdffb200dd83cdfeb","d83ddc68d83cdffc200dd83cdfeb","d83ddc68d83cdffd200dd83cdfeb","d83ddc68d83cdffe200dd83cdfeb","d83ddc68d83cdfff200dd83cdfeb"],["d83ddc69200dd83cdfeb","d83ddc69d83cdffb200dd83cdfeb","d83ddc69d83cdffc200dd83cdfeb","d83ddc69d83cdffd200dd83cdfeb","d83ddc69d83cdffe200dd83cdfeb","d83ddc69d83cdfff200dd83cdfeb"],["d83eddd1200d2696fe0f","d83eddd1d83cdffb200d2696fe0f","d83eddd1d83cdffc200d2696fe0f","d83eddd1d83cdffd200d2696fe0f","d83eddd1d83cdffe200d2696fe0f","d83eddd1d83cdfff200d2696fe0f"],["d83ddc68200d2696fe0f","d83ddc68d83cdffb200d2696fe0f","d83ddc68d83cdffc200d2696fe0f","d83ddc68d83cdffd200d2696fe0f","d83ddc68d83cdffe200d2696fe0f","d83ddc68d83cdfff200d2696fe0f"],["d83ddc69200d2696fe0f","d83ddc69d83cdffb200d2696fe0f","d83ddc69d83cdffc200d2696fe0f","d83ddc69d83cdffd200d2696fe0f","d83ddc69d83cdffe200d2696fe0f","d83ddc69d83cdfff200d2696fe0f"],["d83eddd1200dd83cdf3e","d83eddd1d83cdffb200dd83cdf3e","d83eddd1d83cdffc200dd83cdf3e","d83eddd1d83cdffd200dd83cdf3e","d83eddd1d83cdffe200dd83cdf3e","d83eddd1d83cdfff200dd83cdf3e"],["d83ddc68200dd83cdf3e","d83ddc68d83cdffb200dd83cdf3e","d83ddc68d83cdffc200dd83cdf3e","d83ddc68d83cdffd200dd83cdf3e","d83ddc68d83cdffe200dd83cdf3e","d83ddc68d83cdfff200dd83cdf3e"],["d83ddc69200dd83cdf3e","d83ddc69d83cdffb200dd83cdf3e","d83ddc69d83cdffc200dd83cdf3e","d83ddc69d83cdffd200dd83cdf3e","d83ddc69d83cdffe200dd83cdf3e","d83ddc69d83cdfff200dd83cdf3e"],["d83eddd1200dd83cdf73","d83eddd1d83cdffb200dd83cdf73","d83eddd1d83cdffc200dd83cdf73","d83eddd1d83cdffd200dd83cdf73","d83eddd1d83cdffe200dd83cdf73","d83eddd1d83cdfff200dd83cdf73"],["d83ddc68200dd83cdf73","d83ddc68d83cdffb200dd83cdf73","d83ddc68d83cdffc200dd83cdf73","d83ddc68d83cdffd200dd83cdf73","d83ddc68d83cdffe200dd83cdf73","d83ddc68d83cdfff200dd83cdf73"],["d83ddc69200dd83cdf73","d83ddc69d83cdffb200dd83cdf73","d83ddc69d83cdffc200dd83cdf73","d83ddc69d83cdffd200dd83cdf73","d83ddc69d83cdffe200dd83cdf73","d83ddc69d83cdfff200dd83cdf73"],["d83eddd1200dd83ddd27","d83eddd1d83cdffb200dd83ddd27","d83eddd1d83cdffc200dd83ddd27","d83eddd1d83cdffd200dd83ddd27","d83eddd1d83cdffe200dd83ddd27","d83eddd1d83cdfff200dd83ddd27"],["d83ddc68200dd83ddd27","d83ddc68d83cdffb200dd83ddd27","d83ddc68d83cdffc200dd83ddd27","d83ddc68d83cdffd200dd83ddd27","d83ddc68d83cdffe200dd83ddd27","d83ddc68d83cdfff200dd83ddd27"],["d83ddc69200dd83ddd27","d83ddc69d83cdffb200dd83ddd27","d83ddc69d83cdffc200dd83ddd27","d83ddc69d83cdffd200dd83ddd27","d83ddc69d83cdffe200dd83ddd27","d83ddc69d83cdfff200dd83ddd27"],["d83eddd1200dd83cdfed","d83eddd1d83cdffb200dd83cdfed","d83eddd1d83cdffc200dd83cdfed","d83eddd1d83cdffd200dd83cdfed","d83eddd1d83cdffe200dd83cdfed","d83eddd1d83cdfff200dd83cdfed"],["d83ddc68200dd83cdfed","d83ddc68d83cdffb200dd83cdfed","d83ddc68d83cdffc200dd83cdfed","d83ddc68d83cdffd200dd83cdfed","d83ddc68d83cdffe200dd83cdfed","d83ddc68d83cdfff200dd83cdfed"],["d83ddc69200dd83cdfed","d83ddc69d83cdffb200dd83cdfed","d83ddc69d83cdffc200dd83cdfed","d83ddc69d83cdffd200dd83cdfed","d83ddc69d83cdffe200dd83cdfed","d83ddc69d83cdfff200dd83cdfed"],["d83eddd1200dd83ddcbc","d83eddd1d83cdffb200dd83ddcbc","d83eddd1d83cdffc200dd83ddcbc","d83eddd1d83cdffd200dd83ddcbc","d83eddd1d83cdffe200dd83ddcbc","d83eddd1d83cdfff200dd83ddcbc"],["d83ddc68200dd83ddcbc","d83ddc68d83cdffb200dd83ddcbc","d83ddc68d83cdffc200dd83ddcbc","d83ddc68d83cdffd200dd83ddcbc","d83ddc68d83cdffe200dd83ddcbc","d83ddc68d83cdfff200dd83ddcbc"],["d83ddc69200dd83ddcbc","d83ddc69d83cdffb200dd83ddcbc","d83ddc69d83cdffc200dd83ddcbc","d83ddc69d83cdffd200dd83ddcbc","d83ddc69d83cdffe200dd83ddcbc","d83ddc69d83cdfff200dd83ddcbc"],["d83eddd1200dd83ddd2c","d83eddd1d83cdffb200dd83ddd2c","d83eddd1d83cdffc200dd83ddd2c","d83eddd1d83cdffd200dd83ddd2c","d83eddd1d83cdffe200dd83ddd2c","d83eddd1d83cdfff200dd83ddd2c"],["d83ddc68200dd83ddd2c","d83ddc68d83cdffb200dd83ddd2c","d83ddc68d83cdffc200dd83ddd2c","d83ddc68d83cdffd200dd83ddd2c","d83ddc68d83cdffe200dd83ddd2c","d83ddc68d83cdfff200dd83ddd2c"],["d83ddc69200dd83ddd2c","d83ddc69d83cdffb200dd83ddd2c","d83ddc69d83cdffc200dd83ddd2c","d83ddc69d83cdffd200dd83ddd2c","d83ddc69d83cdffe200dd83ddd2c","d83ddc69d83cdfff200dd83ddd2c"],["d83eddd1200dd83ddcbb","d83eddd1d83cdffb200dd83ddcbb","d83eddd1d83cdffc200dd83ddcbb","d83eddd1d83cdffd200dd83ddcbb","d83eddd1d83cdffe200dd83ddcbb","d83eddd1d83cdfff200dd83ddcbb"]],"People_4":[["d83ddc68200dd83ddcbb","d83ddc68d83cdffb200dd83ddcbb","d83ddc68d83cdffc200dd83ddcbb","d83ddc68d83cdffd200dd83ddcbb","d83ddc68d83cdffe200dd83ddcbb","d83ddc68d83cdfff200dd83ddcbb"],["d83ddc69200dd83ddcbb","d83ddc69d83cdffb200dd83ddcbb","d83ddc69d83cdffc200dd83ddcbb","d83ddc69d83cdffd200dd83ddcbb","d83ddc69d83cdffe200dd83ddcbb","d83ddc69d83cdfff200dd83ddcbb"],["d83eddd1200dd83cdfa4","d83eddd1d83cdffb200dd83cdfa4","d83eddd1d83cdffc200dd83cdfa4","d83eddd1d83cdffd200dd83cdfa4","d83eddd1d83cdffe200dd83cdfa4","d83eddd1d83cdfff200dd83cdfa4"],["d83ddc68200dd83cdfa4","d83ddc68d83cdffb200dd83cdfa4","d83ddc68d83cdffc200dd83cdfa4","d83ddc68d83cdffd200dd83cdfa4","d83ddc68d83cdffe200dd83cdfa4","d83ddc68d83cdfff200dd83cdfa4"],["d83ddc69200dd83cdfa4","d83ddc69d83cdffb200dd83cdfa4","d83ddc69d83cdffc200dd83cdfa4","d83ddc69d83cdffd200dd83cdfa4","d83ddc69d83cdffe200dd83cdfa4","d83ddc69d83cdfff200dd83cdfa4"],["d83eddd1200dd83cdfa8","d83eddd1d83cdffb200dd83cdfa8","d83eddd1d83cdffc200dd83cdfa8","d83eddd1d83cdffd200dd83cdfa8","d83eddd1d83cdffe200dd83cdfa8","d83eddd1d83cdfff200dd83cdfa8"],["d83ddc68200dd83cdfa8","d83ddc68d83cdffb200dd83cdfa8","d83ddc68d83cdffc200dd83cdfa8","d83ddc68d83cdffd200dd83cdfa8","d83ddc68d83cdffe200dd83cdfa8","d83ddc68d83cdfff200dd83cdfa8"],["d83ddc69200dd83cdfa8","d83ddc69d83cdffb200dd83cdfa8","d83ddc69d83cdffc200dd83cdfa8","d83ddc69d83cdffd200dd83cdfa8","d83ddc69d83cdffe200dd83cdfa8","d83ddc69d83cdfff200dd83cdfa8"],["d83eddd1200d2708fe0f","d83eddd1d83cdffb200d2708fe0f","d83eddd1d83cdffc200d2708fe0f","d83eddd1d83cdffd200d2708fe0f","d83eddd1d83cdffe200d2708fe0f","d83eddd1d83cdfff200d2708fe0f"],["d83ddc68200d2708fe0f","d83ddc68d83cdffb200d2708fe0f","d83ddc68d83cdffc200d2708fe0f","d83ddc68d83cdffd200d2708fe0f","d83ddc68d83cdffe200d2708fe0f","d83ddc68d83cdfff200d2708fe0f"],["d83ddc69200d2708fe0f","d83ddc69d83cdffb200d2708fe0f","d83ddc69d83cdffc200d2708fe0f","d83ddc69d83cdffd200d2708fe0f","d83ddc69d83cdffe200d2708fe0f","d83ddc69d83cdfff200d2708fe0f"],["d83eddd1200dd83dde80","d83eddd1d83cdffb200dd83dde80","d83eddd1d83cdffc200dd83dde80","d83eddd1d83cdffd200dd83dde80","d83eddd1d83cdffe200dd83dde80","d83eddd1d83cdfff200dd83dde80"],["d83ddc68200dd83dde80","d83ddc68d83cdffb200dd83dde80","d83ddc68d83cdffc200dd83dde80","d83ddc68d83cdffd200dd83dde80","d83ddc68d83cdffe200dd83dde80","d83ddc68d83cdfff200dd83dde80"],["d83ddc69200dd83dde80","d83ddc69d83cdffb200dd83dde80","d83ddc69d83cdffc200dd83dde80","d83ddc69d83cdffd200dd83dde80","d83ddc69d83cdffe200dd83dde80","d83ddc69d83cdfff200dd83dde80"],["d83eddd1200dd83dde92","d83eddd1d83cdffb200dd83dde92","d83eddd1d83cdffc200dd83dde92","d83eddd1d83cdffd200dd83dde92","d83eddd1d83cdffe200dd83dde92","d83eddd1d83cdfff200dd83dde92"],["d83ddc68200dd83dde92","d83ddc68d83cdffb200dd83dde92","d83ddc68d83cdffc200dd83dde92","d83ddc68d83cdffd200dd83dde92","d83ddc68d83cdffe200dd83dde92","d83ddc68d83cdfff200dd83dde92"],["d83ddc69200dd83dde92","d83ddc69d83cdffb200dd83dde92","d83ddc69d83cdffc200dd83dde92","d83ddc69d83cdffd200dd83dde92","d83ddc69d83cdffe200dd83dde92","d83ddc69d83cdfff200dd83dde92"],["d83ddc6e","d83ddc6ed83cdffb","d83ddc6ed83cdffc","d83ddc6ed83cdffd","d83ddc6ed83cdffe","d83ddc6ed83cdfff"],["d83ddc6e200d2642fe0f","d83ddc6ed83cdffb200d2642fe0f","d83ddc6ed83cdffc200d2642fe0f","d83ddc6ed83cdffd200d2642fe0f","d83ddc6ed83cdffe200d2642fe0f","d83ddc6ed83cdfff200d2642fe0f"],["d83ddc6e200d2640fe0f","d83ddc6ed83cdffb200d2640fe0f","d83ddc6ed83cdffc200d2640fe0f","d83ddc6ed83cdffd200d2640fe0f","d83ddc6ed83cdffe200d2640fe0f","d83ddc6ed83cdfff200d2640fe0f"],["d83ddd75fe0f","d83ddd75d83cdffb","d83ddd75d83cdffc","d83ddd75d83cdffd","d83ddd75d83cdffe","d83ddd75d83cdfff"],["d83ddd75fe0f200d2642fe0f","d83ddd75d83cdffb200d2642fe0f","d83ddd75d83cdffc200d2642fe0f","d83ddd75d83cdffd200d2642fe0f","d83ddd75d83cdffe200d2642fe0f","d83ddd75d83cdfff200d2642fe0f"],["d83ddd75fe0f200d2640fe0f","d83ddd75d83cdffb200d2640fe0f","d83ddd75d83cdffc200d2640fe0f","d83ddd75d83cdffd200d2640fe0f","d83ddd75d83cdffe200d2640fe0f","d83ddd75d83cdfff200d2640fe0f"],["d83ddc82","d83ddc82d83cdffb","d83ddc82d83cdffc","d83ddc82d83cdffd","d83ddc82d83cdffe","d83ddc82d83cdfff"],["d83ddc82200d2642fe0f","d83ddc82d83cdffb200d2642fe0f","d83ddc82d83cdffc200d2642fe0f","d83ddc82d83cdffd200d2642fe0f","d83ddc82d83cdffe200d2642fe0f","d83ddc82d83cdfff200d2642fe0f"],["d83ddc82200d2640fe0f","d83ddc82d83cdffb200d2640fe0f","d83ddc82d83cdffc200d2640fe0f","d83ddc82d83cdffd200d2640fe0f","d83ddc82d83cdffe200d2640fe0f","d83ddc82d83cdfff200d2640fe0f"],["d83edd77","d83edd77d83cdffb","d83edd77d83cdffc","d83edd77d83cdffd","d83edd77d83cdffe","d83edd77d83cdfff"],["d83ddc77","d83ddc77d83cdffb","d83ddc77d83cdffc","d83ddc77d83cdffd","d83ddc77d83cdffe","d83ddc77d83cdfff"],["d83ddc77200d2642fe0f","d83ddc77d83cdffb200d2642fe0f","d83ddc77d83cdffc200d2642fe0f","d83ddc77d83cdffd200d2642fe0f","d83ddc77d83cdffe200d2642fe0f","d83ddc77d83cdfff200d2642fe0f"],["d83ddc77200d2640fe0f","d83ddc77d83cdffb200d2640fe0f","d83ddc77d83cdffc200d2640fe0f","d83ddc77d83cdffd200d2640fe0f","d83ddc77d83cdffe200d2640fe0f","d83ddc77d83cdfff200d2640fe0f"],["d83edec5","d83edec5d83cdffb","d83edec5d83cdffc","d83edec5d83cdffd","d83edec5d83cdffe","d83edec5d83cdfff"],["d83edd34","d83edd34d83cdffb","d83edd34d83cdffc","d83edd34d83cdffd","d83edd34d83cdffe","d83edd34d83cdfff"],["d83ddc78","d83ddc78d83cdffb","d83ddc78d83cdffc","d83ddc78d83cdffd","d83ddc78d83cdffe","d83ddc78d83cdfff"],["d83ddc73","d83ddc73d83cdffb","d83ddc73d83cdffc","d83ddc73d83cdffd","d83ddc73d83cdffe","d83ddc73d83cdfff"],["d83ddc73200d2642fe0f","d83ddc73d83cdffb200d2642fe0f","d83ddc73d83cdffc200d2642fe0f","d83ddc73d83cdffd200d2642fe0f","d83ddc73d83cdffe200d2642fe0f","d83ddc73d83cdfff200d2642fe0f"],["d83ddc73200d2640fe0f","d83ddc73d83cdffb200d2640fe0f","d83ddc73d83cdffc200d2640fe0f","d83ddc73d83cdffd200d2640fe0f","d83ddc73d83cdffe200d2640fe0f","d83ddc73d83cdfff200d2640fe0f"],["d83ddc72","d83ddc72d83cdffb","d83ddc72d83cdffc","d83ddc72d83cdffd","d83ddc72d83cdffe","d83ddc72d83cdfff"],["d83eddd5","d83eddd5d83cdffb","d83eddd5d83cdffc","d83eddd5d83cdffd","d83eddd5d83cdffe","d83eddd5d83cdfff"],["d83edd35","d83edd35d83cdffb","d83edd35d83cdffc","d83edd35d83cdffd","d83edd35d83cdffe","d83edd35d83cdfff"],["d83edd35200d2642fe0f","d83edd35d83cdffb200d2642fe0f","d83edd35d83cdffc200d2642fe0f","d83edd35d83cdffd200d2642fe0f","d83edd35d83cdffe200d2642fe0f","d83edd35d83cdfff200d2642fe0f"],["d83edd35200d2640fe0f","d83edd35d83cdffb200d2640fe0f","d83edd35d83cdffc200d2640fe0f","d83edd35d83cdffd200d2640fe0f","d83edd35d83cdffe200d2640fe0f","d83edd35d83cdfff200d2640fe0f"],["d83ddc70","d83ddc70d83cdffb","d83ddc70d83cdffc","d83ddc70d83cdffd","d83ddc70d83cdffe","d83ddc70d83cdfff"]],"People_5":[["d83ddc70200d2642fe0f","d83ddc70d83cdffb200d2642fe0f","d83ddc70d83cdffc200d2642fe0f","d83ddc70d83cdffd200d2642fe0f","d83ddc70d83cdffe200d2642fe0f","d83ddc70d83cdfff200d2642fe0f"],["d83ddc70200d2640fe0f","d83ddc70d83cdffb200d2640fe0f","d83ddc70d83cdffc200d2640fe0f","d83ddc70d83cdffd200d2640fe0f","d83ddc70d83cdffe200d2640fe0f","d83ddc70d83cdfff200d2640fe0f"],["d83edd30","d83edd30d83cdffb","d83edd30d83cdffc","d83edd30d83cdffd","d83edd30d83cdffe","d83edd30d83cdfff"],["d83edec3","d83edec3d83cdffb","d83edec3d83cdffc","d83edec3d83cdffd","d83edec3d83cdffe","d83edec3d83cdfff"],["d83edec4","d83edec4d83cdffb","d83edec4d83cdffc","d83edec4d83cdffd","d83edec4d83cdffe","d83edec4d83cdfff"],["d83edd31","d83edd31d83cdffb","d83edd31d83cdffc","d83edd31d83cdffd","d83edd31d83cdffe","d83edd31d83cdfff"],["d83ddc69200dd83cdf7c","d83ddc69d83cdffb200dd83cdf7c","d83ddc69d83cdffc200dd83cdf7c","d83ddc69d83cdffd200dd83cdf7c","d83ddc69d83cdffe200dd83cdf7c","d83ddc69d83cdfff200dd83cdf7c"],["d83ddc68200dd83cdf7c","d83ddc68d83cdffb200dd83cdf7c","d83ddc68d83cdffc200dd83cdf7c","d83ddc68d83cdffd200dd83cdf7c","d83ddc68d83cdffe200dd83cdf7c","d83ddc68d83cdfff200dd83cdf7c"],["d83eddd1200dd83cdf7c","d83eddd1d83cdffb200dd83cdf7c","d83eddd1d83cdffc200dd83cdf7c","d83eddd1d83cdffd200dd83cdf7c","d83eddd1d83cdffe200dd83cdf7c","d83eddd1d83cdfff200dd83cdf7c"],["d83ddc7c","d83ddc7cd83cdffb","d83ddc7cd83cdffc","d83ddc7cd83cdffd","d83ddc7cd83cdffe","d83ddc7cd83cdfff"],["d83cdf85","d83cdf85d83cdffb","d83cdf85d83cdffc","d83cdf85d83cdffd","d83cdf85d83cdffe","d83cdf85d83cdfff"],["d83edd36","d83edd36d83cdffb","d83edd36d83cdffc","d83edd36d83cdffd","d83edd36d83cdffe","d83edd36d83cdfff"],["d83eddd1200dd83cdf84","d83eddd1d83cdffb200dd83cdf84","d83eddd1d83cdffc200dd83cdf84","d83eddd1d83cdffd200dd83cdf84","d83eddd1d83cdffe200dd83cdf84","d83eddd1d83cdfff200dd83cdf84"],["d83eddb8","d83eddb8d83cdffb","d83eddb8d83cdffc","d83eddb8d83cdffd","d83eddb8d83cdffe","d83eddb8d83cdfff"],["d83eddb8200d2642fe0f","d83eddb8d83cdffb200d2642fe0f","d83eddb8d83cdffc200d2642fe0f","d83eddb8d83cdffd200d2642fe0f","d83eddb8d83cdffe200d2642fe0f","d83eddb8d83cdfff200d2642fe0f"],["d83eddb8200d2640fe0f","d83eddb8d83cdffb200d2640fe0f","d83eddb8d83cdffc200d2640fe0f","d83eddb8d83cdffd200d2640fe0f","d83eddb8d83cdffe200d2640fe0f","d83eddb8d83cdfff200d2640fe0f"],["d83eddb9","d83eddb9d83cdffb","d83eddb9d83cdffc","d83eddb9d83cdffd","d83eddb9d83cdffe","d83eddb9d83cdfff"],["d83eddb9200d2642fe0f","d83eddb9d83cdffb200d2642fe0f","d83eddb9d83cdffc200d2642fe0f","d83eddb9d83cdffd200d2642fe0f","d83eddb9d83cdffe200d2642fe0f","d83eddb9d83cdfff200d2642fe0f"],["d83eddb9200d2640fe0f","d83eddb9d83cdffb200d2640fe0f","d83eddb9d83cdffc200d2640fe0f","d83eddb9d83cdffd200d2640fe0f","d83eddb9d83cdffe200d2640fe0f","d83eddb9d83cdfff200d2640fe0f"],["d83eddd9","d83eddd9d83cdffb","d83eddd9d83cdffc","d83eddd9d83cdffd","d83eddd9d83cdffe","d83eddd9d83cdfff"],["d83eddd9200d2642fe0f","d83eddd9d83cdffb200d2642fe0f","d83eddd9d83cdffc200d2642fe0f","d83eddd9d83cdffd200d2642fe0f","d83eddd9d83cdffe200d2642fe0f","d83eddd9d83cdfff200d2642fe0f"],["d83eddd9200d2640fe0f","d83eddd9d83cdffb200d2640fe0f","d83eddd9d83cdffc200d2640fe0f","d83eddd9d83cdffd200d2640fe0f","d83eddd9d83cdffe200d2640fe0f","d83eddd9d83cdfff200d2640fe0f"],["d83eddda","d83edddad83cdffb","d83edddad83cdffc","d83edddad83cdffd","d83edddad83cdffe","d83edddad83cdfff"],["d83eddda200d2642fe0f","d83edddad83cdffb200d2642fe0f","d83edddad83cdffc200d2642fe0f","d83edddad83cdffd200d2642fe0f","d83edddad83cdffe200d2642fe0f","d83edddad83cdfff200d2642fe0f"],["d83eddda200d2640fe0f","d83edddad83cdffb200d2640fe0f","d83edddad83cdffc200d2640fe0f","d83edddad83cdffd200d2640fe0f","d83edddad83cdffe200d2640fe0f","d83edddad83cdfff200d2640fe0f"],["d83edddb","d83edddbd83cdffb","d83edddbd83cdffc","d83edddbd83cdffd","d83edddbd83cdffe","d83edddbd83cdfff"],["d83edddb200d2642fe0f","d83edddbd83cdffb200d2642fe0f","d83edddbd83cdffc200d2642fe0f","d83edddbd83cdffd200d2642fe0f","d83edddbd83cdffe200d2642fe0f","d83edddbd83cdfff200d2642fe0f"],["d83edddb200d2640fe0f","d83edddbd83cdffb200d2640fe0f","d83edddbd83cdffc200d2640fe0f","d83edddbd83cdffd200d2640fe0f","d83edddbd83cdffe200d2640fe0f","d83edddbd83cdfff200d2640fe0f"],["d83edddc","d83edddcd83cdffb","d83edddcd83cdffc","d83edddcd83cdffd","d83edddcd83cdffe","d83edddcd83cdfff"],["d83edddc200d2642fe0f","d83edddcd83cdffb200d2642fe0f","d83edddcd83cdffc200d2642fe0f","d83edddcd83cdffd200d2642fe0f","d83edddcd83cdffe200d2642fe0f","d83edddcd83cdfff200d2642fe0f"],["d83edddc200d2640fe0f","d83edddcd83cdffb200d2640fe0f","d83edddcd83cdffc200d2640fe0f","d83edddcd83cdffd200d2640fe0f","d83edddcd83cdffe200d2640fe0f","d83edddcd83cdfff200d2640fe0f"],["d83edddd","d83eddddd83cdffb","d83eddddd83cdffc","d83eddddd83cdffd","d83eddddd83cdffe","d83eddddd83cdfff"],["d83edddd200d2642fe0f","d83eddddd83cdffb200d2642fe0f","d83eddddd83cdffc200d2642fe0f","d83eddddd83cdffd200d2642fe0f","d83eddddd83cdffe200d2642fe0f","d83eddddd83cdfff200d2642fe0f"],["d83edddd200d2640fe0f","d83eddddd83cdffb200d2640fe0f","d83eddddd83cdffc200d2640fe0f","d83eddddd83cdffd200d2640fe0f","d83eddddd83cdffe200d2640fe0f","d83eddddd83cdfff200d2640fe0f"],["d83eddde"],["d83eddde200d2642fe0f"],["d83eddde200d2640fe0f"],["d83edddf"],["d83edddf200d2642fe0f"],["d83edddf200d2640fe0f"],["d83eddcc"],["d83ddc86","d83ddc86d83cdffb","d83ddc86d83cdffc","d83ddc86d83cdffd","d83ddc86d83cdffe","d83ddc86d83cdfff"],["d83ddc86200d2642fe0f","d83ddc86d83cdffb200d2642fe0f","d83ddc86d83cdffc200d2642fe0f","d83ddc86d83cdffd200d2642fe0f","d83ddc86d83cdffe200d2642fe0f","d83ddc86d83cdfff200d2642fe0f"],["d83ddc86200d2640fe0f","d83ddc86d83cdffb200d2640fe0f","d83ddc86d83cdffc200d2640fe0f","d83ddc86d83cdffd200d2640fe0f","d83ddc86d83cdffe200d2640fe0f","d83ddc86d83cdfff200d2640fe0f"],["d83ddc87","d83ddc87d83cdffb","d83ddc87d83cdffc","d83ddc87d83cdffd","d83ddc87d83cdffe","d83ddc87d83cdfff"],["d83ddc87200d2642fe0f","d83ddc87d83cdffb200d2642fe0f","d83ddc87d83cdffc200d2642fe0f","d83ddc87d83cdffd200d2642fe0f","d83ddc87d83cdffe200d2642fe0f","d83ddc87d83cdfff200d2642fe0f"],["d83ddc87200d2640fe0f","d83ddc87d83cdffb200d2640fe0f","d83ddc87d83cdffc200d2640fe0f","d83ddc87d83cdffd200d2640fe0f","d83ddc87d83cdffe200d2640fe0f","d83ddc87d83cdfff200d2640fe0f"],["d83ddeb6","d83ddeb6d83cdffb","d83ddeb6d83cdffc","d83ddeb6d83cdffd","d83ddeb6d83cdffe","d83ddeb6d83cdfff"]],"People_6":[["d83ddeb6200d2642fe0f","d83ddeb6d83cdffb200d2642fe0f","d83ddeb6d83cdffc200d2642fe0f","d83ddeb6d83cdffd200d2642fe0f","d83ddeb6d83cdffe200d2642fe0f","d83ddeb6d83cdfff200d2642fe0f"],["d83ddeb6200d2640fe0f","d83ddeb6d83cdffb200d2640fe0f","d83ddeb6d83cdffc200d2640fe0f","d83ddeb6d83cdffd200d2640fe0f","d83ddeb6d83cdffe200d2640fe0f","d83ddeb6d83cdfff200d2640fe0f"],["d83eddcd","d83eddcdd83cdffb","d83eddcdd83cdffc","d83eddcdd83cdffd","d83eddcdd83cdffe","d83eddcdd83cdfff"],["d83eddcd200d2642fe0f","d83eddcdd83cdffb200d2642fe0f","d83eddcdd83cdffc200d2642fe0f","d83eddcdd83cdffd200d2642fe0f","d83eddcdd83cdffe200d2642fe0f","d83eddcdd83cdfff200d2642fe0f"],["d83eddcd200d2640fe0f","d83eddcdd83cdffb200d2640fe0f","d83eddcdd83cdffc200d2640fe0f","d83eddcdd83cdffd200d2640fe0f","d83eddcdd83cdffe200d2640fe0f","d83eddcdd83cdfff200d2640fe0f"],["d83eddce","d83eddced83cdffb","d83eddced83cdffc","d83eddced83cdffd","d83eddced83cdffe","d83eddced83cdfff"],["d83eddce200d2642fe0f","d83eddced83cdffb200d2642fe0f","d83eddced83cdffc200d2642fe0f","d83eddced83cdffd200d2642fe0f","d83eddced83cdffe200d2642fe0f","d83eddced83cdfff200d2642fe0f"],["d83eddce200d2640fe0f","d83eddced83cdffb200d2640fe0f","d83eddced83cdffc200d2640fe0f","d83eddced83cdffd200d2640fe0f","d83eddced83cdffe200d2640fe0f","d83eddced83cdfff200d2640fe0f"],["d83eddd1200dd83eddaf","d83eddd1d83cdffb200dd83eddaf","d83eddd1d83cdffc200dd83eddaf","d83eddd1d83cdffd200dd83eddaf","d83eddd1d83cdffe200dd83eddaf","d83eddd1d83cdfff200dd83eddaf"],["d83ddc68200dd83eddaf","d83ddc68d83cdffb200dd83eddaf","d83ddc68d83cdffc200dd83eddaf","d83ddc68d83cdffd200dd83eddaf","d83ddc68d83cdffe200dd83eddaf","d83ddc68d83cdfff200dd83eddaf"],["d83ddc69200dd83eddaf","d83ddc69d83cdffb200dd83eddaf","d83ddc69d83cdffc200dd83eddaf","d83ddc69d83cdffd200dd83eddaf","d83ddc69d83cdffe200dd83eddaf","d83ddc69d83cdfff200dd83eddaf"],["d83eddd1200dd83eddbc","d83eddd1d83cdffb200dd83eddbc","d83eddd1d83cdffc200dd83eddbc","d83eddd1d83cdffd200dd83eddbc","d83eddd1d83cdffe200dd83eddbc","d83eddd1d83cdfff200dd83eddbc"],["d83ddc68200dd83eddbc","d83ddc68d83cdffb200dd83eddbc","d83ddc68d83cdffc200dd83eddbc","d83ddc68d83cdffd200dd83eddbc","d83ddc68d83cdffe200dd83eddbc","d83ddc68d83cdfff200dd83eddbc"],["d83ddc69200dd83eddbc","d83ddc69d83cdffb200dd83eddbc","d83ddc69d83cdffc200dd83eddbc","d83ddc69d83cdffd200dd83eddbc","d83ddc69d83cdffe200dd83eddbc","d83ddc69d83cdfff200dd83eddbc"],["d83eddd1200dd83eddbd","d83eddd1d83cdffb200dd83eddbd","d83eddd1d83cdffc200dd83eddbd","d83eddd1d83cdffd200dd83eddbd","d83eddd1d83cdffe200dd83eddbd","d83eddd1d83cdfff200dd83eddbd"],["d83ddc68200dd83eddbd","d83ddc68d83cdffb200dd83eddbd","d83ddc68d83cdffc200dd83eddbd","d83ddc68d83cdffd200dd83eddbd","d83ddc68d83cdffe200dd83eddbd","d83ddc68d83cdfff200dd83eddbd"],["d83ddc69200dd83eddbd","d83ddc69d83cdffb200dd83eddbd","d83ddc69d83cdffc200dd83eddbd","d83ddc69d83cdffd200dd83eddbd","d83ddc69d83cdffe200dd83eddbd","d83ddc69d83cdfff200dd83eddbd"],["d83cdfc3","d83cdfc3d83cdffb","d83cdfc3d83cdffc","d83cdfc3d83cdffd","d83cdfc3d83cdffe","d83cdfc3d83cdfff"],["d83cdfc3200d2642fe0f","d83cdfc3d83cdffb200d2642fe0f","d83cdfc3d83cdffc200d2642fe0f","d83cdfc3d83cdffd200d2642fe0f","d83cdfc3d83cdffe200d2642fe0f","d83cdfc3d83cdfff200d2642fe0f"],["d83cdfc3200d2640fe0f","d83cdfc3d83cdffb200d2640fe0f","d83cdfc3d83cdffc200d2640fe0f","d83cdfc3d83cdffd200d2640fe0f","d83cdfc3d83cdffe200d2640fe0f","d83cdfc3d83cdfff200d2640fe0f"],["d83ddc83","d83ddc83d83cdffb","d83ddc83d83cdffc","d83ddc83d83cdffd","d83ddc83d83cdffe","d83ddc83d83cdfff"],["d83ddd7a","d83ddd7ad83cdffb","d83ddd7ad83cdffc","d83ddd7ad83cdffd","d83ddd7ad83cdffe","d83ddd7ad83cdfff"],["d83ddd74fe0f","d83ddd74d83cdffb","d83ddd74d83cdffc","d83ddd74d83cdffd","d83ddd74d83cdffe","d83ddd74d83cdfff"],["d83ddc6f"],["d83ddc6f200d2642fe0f"],["d83ddc6f200d2640fe0f"],["d83eddd6","d83eddd6d83cdffb","d83eddd6d83cdffc","d83eddd6d83cdffd","d83eddd6d83cdffe","d83eddd6d83cdfff"],["d83eddd6200d2642fe0f","d83eddd6d83cdffb200d2642fe0f","d83eddd6d83cdffc200d2642fe0f","d83eddd6d83cdffd200d2642fe0f","d83eddd6d83cdffe200d2642fe0f","d83eddd6d83cdfff200d2642fe0f"],["d83eddd6200d2640fe0f","d83eddd6d83cdffb200d2640fe0f","d83eddd6d83cdffc200d2640fe0f","d83eddd6d83cdffd200d2640fe0f","d83eddd6d83cdffe200d2640fe0f","d83eddd6d83cdfff200d2640fe0f"],["d83eddd7","d83eddd7d83cdffb","d83eddd7d83cdffc","d83eddd7d83cdffd","d83eddd7d83cdffe","d83eddd7d83cdfff"],["d83eddd7200d2642fe0f","d83eddd7d83cdffb200d2642fe0f","d83eddd7d83cdffc200d2642fe0f","d83eddd7d83cdffd200d2642fe0f","d83eddd7d83cdffe200d2642fe0f","d83eddd7d83cdfff200d2642fe0f"],["d83eddd7200d2640fe0f","d83eddd7d83cdffb200d2640fe0f","d83eddd7d83cdffc200d2640fe0f","d83eddd7d83cdffd200d2640fe0f","d83eddd7d83cdffe200d2640fe0f","d83eddd7d83cdfff200d2640fe0f"],["d83edd3a"],["d83cdfc7","d83cdfc7d83cdffb","d83cdfc7d83cdffc","d83cdfc7d83cdffd","d83cdfc7d83cdffe","d83cdfc7d83cdfff"],["26f7fe0f"],["d83cdfc2","d83cdfc2d83cdffb","d83cdfc2d83cdffc","d83cdfc2d83cdffd","d83cdfc2d83cdffe","d83cdfc2d83cdfff"],["d83cdfccfe0f","d83cdfccd83cdffb","d83cdfccd83cdffc","d83cdfccd83cdffd","d83cdfccd83cdffe","d83cdfccd83cdfff"],["d83cdfccfe0f200d2642fe0f","d83cdfccd83cdffb200d2642fe0f","d83cdfccd83cdffc200d2642fe0f","d83cdfccd83cdffd200d2642fe0f","d83cdfccd83cdffe200d2642fe0f","d83cdfccd83cdfff200d2642fe0f"],["d83cdfccfe0f200d2640fe0f","d83cdfccd83cdffb200d2640fe0f","d83cdfccd83cdffc200d2640fe0f","d83cdfccd83cdffd200d2640fe0f","d83cdfccd83cdffe200d2640fe0f","d83cdfccd83cdfff200d2640fe0f"],["d83cdfc4","d83cdfc4d83cdffb","d83cdfc4d83cdffc","d83cdfc4d83cdffd","d83cdfc4d83cdffe","d83cdfc4d83cdfff"],["d83cdfc4200d2642fe0f","d83cdfc4d83cdffb200d2642fe0f","d83cdfc4d83cdffc200d2642fe0f","d83cdfc4d83cdffd200d2642fe0f","d83cdfc4d83cdffe200d2642fe0f","d83cdfc4d83cdfff200d2642fe0f"],["d83cdfc4200d2640fe0f","d83cdfc4d83cdffb200d2640fe0f","d83cdfc4d83cdffc200d2640fe0f","d83cdfc4d83cdffd200d2640fe0f","d83cdfc4d83cdffe200d2640fe0f","d83cdfc4d83cdfff200d2640fe0f"],["d83ddea3","d83ddea3d83cdffb","d83ddea3d83cdffc","d83ddea3d83cdffd","d83ddea3d83cdffe","d83ddea3d83cdfff"],["d83ddea3200d2642fe0f","d83ddea3d83cdffb200d2642fe0f","d83ddea3d83cdffc200d2642fe0f","d83ddea3d83cdffd200d2642fe0f","d83ddea3d83cdffe200d2642fe0f","d83ddea3d83cdfff200d2642fe0f"],["d83ddea3200d2640fe0f","d83ddea3d83cdffb200d2640fe0f","d83ddea3d83cdffc200d2640fe0f","d83ddea3d83cdffd200d2640fe0f","d83ddea3d83cdffe200d2640fe0f","d83ddea3d83cdfff200d2640fe0f"],["d83cdfca","d83cdfcad83cdffb","d83cdfcad83cdffc","d83cdfcad83cdffd","d83cdfcad83cdffe","d83cdfcad83cdfff"]],"People_7":[["d83cdfca200d2642fe0f","d83cdfcad83cdffb200d2642fe0f","d83cdfcad83cdffc200d2642fe0f","d83cdfcad83cdffd200d2642fe0f","d83cdfcad83cdffe200d2642fe0f","d83cdfcad83cdfff200d2642fe0f"],["d83cdfca200d2640fe0f","d83cdfcad83cdffb200d2640fe0f","d83cdfcad83cdffc200d2640fe0f","d83cdfcad83cdffd200d2640fe0f","d83cdfcad83cdffe200d2640fe0f","d83cdfcad83cdfff200d2640fe0f"],["26f9fe0f","26f9d83cdffb","26f9d83cdffc","26f9d83cdffd","26f9d83cdffe","26f9d83cdfff"],["26f9fe0f200d2642fe0f","26f9d83cdffb200d2642fe0f","26f9d83cdffc200d2642fe0f","26f9d83cdffd200d2642fe0f","26f9d83cdffe200d2642fe0f","26f9d83cdfff200d2642fe0f"],["26f9fe0f200d2640fe0f","26f9d83cdffb200d2640fe0f","26f9d83cdffc200d2640fe0f","26f9d83cdffd200d2640fe0f","26f9d83cdffe200d2640fe0f","26f9d83cdfff200d2640fe0f"],["d83cdfcbfe0f","d83cdfcbd83cdffb","d83cdfcbd83cdffc","d83cdfcbd83cdffd","d83cdfcbd83cdffe","d83cdfcbd83cdfff"],["d83cdfcbfe0f200d2642fe0f","d83cdfcbd83cdffb200d2642fe0f","d83cdfcbd83cdffc200d2642fe0f","d83cdfcbd83cdffd200d2642fe0f","d83cdfcbd83cdffe200d2642fe0f","d83cdfcbd83cdfff200d2642fe0f"],["d83cdfcbfe0f200d2640fe0f","d83cdfcbd83cdffb200d2640fe0f","d83cdfcbd83cdffc200d2640fe0f","d83cdfcbd83cdffd200d2640fe0f","d83cdfcbd83cdffe200d2640fe0f","d83cdfcbd83cdfff200d2640fe0f"],["d83ddeb4","d83ddeb4d83cdffb","d83ddeb4d83cdffc","d83ddeb4d83cdffd","d83ddeb4d83cdffe","d83ddeb4d83cdfff"],["d83ddeb4200d2642fe0f","d83ddeb4d83cdffb200d2642fe0f","d83ddeb4d83cdffc200d2642fe0f","d83ddeb4d83cdffd200d2642fe0f","d83ddeb4d83cdffe200d2642fe0f","d83ddeb4d83cdfff200d2642fe0f"],["d83ddeb4200d2640fe0f","d83ddeb4d83cdffb200d2640fe0f","d83ddeb4d83cdffc200d2640fe0f","d83ddeb4d83cdffd200d2640fe0f","d83ddeb4d83cdffe200d2640fe0f","d83ddeb4d83cdfff200d2640fe0f"],["d83ddeb5","d83ddeb5d83cdffb","d83ddeb5d83cdffc","d83ddeb5d83cdffd","d83ddeb5d83cdffe","d83ddeb5d83cdfff"],["d83ddeb5200d2642fe0f","d83ddeb5d83cdffb200d2642fe0f","d83ddeb5d83cdffc200d2642fe0f","d83ddeb5d83cdffd200d2642fe0f","d83ddeb5d83cdffe200d2642fe0f","d83ddeb5d83cdfff200d2642fe0f"],["d83ddeb5200d2640fe0f","d83ddeb5d83cdffb200d2640fe0f","d83ddeb5d83cdffc200d2640fe0f","d83ddeb5d83cdffd200d2640fe0f","d83ddeb5d83cdffe200d2640fe0f","d83ddeb5d83cdfff200d2640fe0f"],["d83edd38","d83edd38d83cdffb","d83edd38d83cdffc","d83edd38d83cdffd","d83edd38d83cdffe","d83edd38d83cdfff"],["d83edd38200d2642fe0f","d83edd38d83cdffb200d2642fe0f","d83edd38d83cdffc200d2642fe0f","d83edd38d83cdffd200d2642fe0f","d83edd38d83cdffe200d2642fe0f","d83edd38d83cdfff200d2642fe0f"],["d83edd38200d2640fe0f","d83edd38d83cdffb200d2640fe0f","d83edd38d83cdffc200d2640fe0f","d83edd38d83cdffd200d2640fe0f","d83edd38d83cdffe200d2640fe0f","d83edd38d83cdfff200d2640fe0f"],["d83edd3c"],["d83edd3c200d2642fe0f"],["d83edd3c200d2640fe0f"],["d83edd3d","d83edd3dd83cdffb","d83edd3dd83cdffc","d83edd3dd83cdffd","d83edd3dd83cdffe","d83edd3dd83cdfff"],["d83edd3d200d2642fe0f","d83edd3dd83cdffb200d2642fe0f","d83edd3dd83cdffc200d2642fe0f","d83edd3dd83cdffd200d2642fe0f","d83edd3dd83cdffe200d2642fe0f","d83edd3dd83cdfff200d2642fe0f"],["d83edd3d200d2640fe0f","d83edd3dd83cdffb200d2640fe0f","d83edd3dd83cdffc200d2640fe0f","d83edd3dd83cdffd200d2640fe0f","d83edd3dd83cdffe200d2640fe0f","d83edd3dd83cdfff200d2640fe0f"],["d83edd3e","d83edd3ed83cdffb","d83edd3ed83cdffc","d83edd3ed83cdffd","d83edd3ed83cdffe","d83edd3ed83cdfff"],["d83edd3e200d2642fe0f","d83edd3ed83cdffb200d2642fe0f","d83edd3ed83cdffc200d2642fe0f","d83edd3ed83cdffd200d2642fe0f","d83edd3ed83cdffe200d2642fe0f","d83edd3ed83cdfff200d2642fe0f"],["d83edd3e200d2640fe0f","d83edd3ed83cdffb200d2640fe0f","d83edd3ed83cdffc200d2640fe0f","d83edd3ed83cdffd200d2640fe0f","d83edd3ed83cdffe200d2640fe0f","d83edd3ed83cdfff200d2640fe0f"],["d83edd39","d83edd39d83cdffb","d83edd39d83cdffc","d83edd39d83cdffd","d83edd39d83cdffe","d83edd39d83cdfff"],["d83edd39200d2642fe0f","d83edd39d83cdffb200d2642fe0f","d83edd39d83cdffc200d2642fe0f","d83edd39d83cdffd200d2642fe0f","d83edd39d83cdffe200d2642fe0f","d83edd39d83cdfff200d2642fe0f"],["d83edd39200d2640fe0f","d83edd39d83cdffb200d2640fe0f","d83edd39d83cdffc200d2640fe0f","d83edd39d83cdffd200d2640fe0f","d83edd39d83cdffe200d2640fe0f","d83edd39d83cdfff200d2640fe0f"],["d83eddd8","d83eddd8d83cdffb","d83eddd8d83cdffc","d83eddd8d83cdffd","d83eddd8d83cdffe","d83eddd8d83cdfff"],["d83eddd8200d2642fe0f","d83eddd8d83cdffb200d2642fe0f","d83eddd8d83cdffc200d2642fe0f","d83eddd8d83cdffd200d2642fe0f","d83eddd8d83cdffe200d2642fe0f","d83eddd8d83cdfff200d2642fe0f"],["d83eddd8200d2640fe0f","d83eddd8d83cdffb200d2640fe0f","d83eddd8d83cdffc200d2640fe0f","d83eddd8d83cdffd200d2640fe0f","d83eddd8d83cdffe200d2640fe0f","d83eddd8d83cdfff200d2640fe0f"],["d83ddec0","d83ddec0d83cdffb","d83ddec0d83cdffc","d83ddec0d83cdffd","d83ddec0d83cdffe","d83ddec0d83cdfff"],["d83ddecc","d83ddeccd83cdffb","d83ddeccd83cdffc","d83ddeccd83cdffd","d83ddeccd83cdffe","d83ddeccd83cdfff"],["d83eddd1200dd83edd1d200dd83eddd1","d83eddd1d83cdffb200dd83edd1d200dd83eddd1d83cdffb","d83eddd1d83cdffb200dd83edd1d200dd83eddd1d83cdffc","d83eddd1d83cdffb200dd83edd1d200dd83eddd1d83cdffd","d83eddd1d83cdffb200dd83edd1d200dd83eddd1d83cdffe","d83eddd1d83cdffb200dd83edd1d200dd83eddd1d83cdfff","d83eddd1d83cdffc200dd83edd1d200dd83eddd1d83cdffb","d83eddd1d83cdffc200dd83edd1d200dd83eddd1d83cdffc","d83eddd1d83cdffc200dd83edd1d200dd83eddd1d83cdffd","d83eddd1d83cdffc200dd83edd1d200dd83eddd1d83cdffe","d83eddd1d83cdffc200dd83edd1d200dd83eddd1d83cdfff","d83eddd1d83cdffd200dd83edd1d200dd83eddd1d83cdffb","d83eddd1d83cdffd200dd83edd1d200dd83eddd1d83cdffc","d83eddd1d83cdffd200dd83edd1d200dd83eddd1d83cdffd","d83eddd1d83cdffd200dd83edd1d200dd83eddd1d83cdffe","d83eddd1d83cdffd200dd83edd1d200dd83eddd1d83cdfff","d83eddd1d83cdffe200dd83edd1d200dd83eddd1d83cdffb","d83eddd1d83cdffe200dd83edd1d200dd83eddd1d83cdffc","d83eddd1d83cdffe200dd83edd1d200dd83eddd1d83cdffd","d83eddd1d83cdffe200dd83edd1d200dd83eddd1d83cdffe","d83eddd1d83cdffe200dd83edd1d200dd83eddd1d83cdfff","d83eddd1d83cdfff200dd83edd1d200dd83eddd1d83cdffb","d83eddd1d83cdfff200dd83edd1d200dd83eddd1d83cdffc","d83eddd1d83cdfff200dd83edd1d200dd83eddd1d83cdffd","d83eddd1d83cdfff200dd83edd1d200dd83eddd1d83cdffe","d83eddd1d83cdfff200dd83edd1d200dd83eddd1d83cdfff"],["d83ddc6d","d83ddc6dd83cdffb","d83ddc6dd83cdffc","d83ddc6dd83cdffd","d83ddc6dd83cdffe","d83ddc6dd83cdfff","d83ddc69d83cdffb200dd83edd1d200dd83ddc69d83cdffc","d83ddc69d83cdffb200dd83edd1d200dd83ddc69d83cdffd","d83ddc69d83cdffb200dd83edd1d200dd83ddc69d83cdffe","d83ddc69d83cdffb200dd83edd1d200dd83ddc69d83cdfff","d83ddc69d83cdffc200dd83edd1d200dd83ddc69d83cdffb","d83ddc69d83cdffc200dd83edd1d200dd83ddc69d83cdffd","d83ddc69d83cdffc200dd83edd1d200dd83ddc69d83cdffe","d83ddc69d83cdffc200dd83edd1d200dd83ddc69d83cdfff","d83ddc69d83cdffd200dd83edd1d200dd83ddc69d83cdffb","d83ddc69d83cdffd200dd83edd1d200dd83ddc69d83cdffc","d83ddc69d83cdffd200dd83edd1d200dd83ddc69d83cdffe","d83ddc69d83cdffd200dd83edd1d200dd83ddc69d83cdfff","d83ddc69d83cdffe200dd83edd1d200dd83ddc69d83cdffb","d83ddc69d83cdffe200dd83edd1d200dd83ddc69d83cdffc","d83ddc69d83cdffe200dd83edd1d200dd83ddc69d83cdffd","d83ddc69d83cdffe200dd83edd1d200dd83ddc69d83cdfff","d83ddc69d83cdfff200dd83edd1d200dd83ddc69d83cdffb","d83ddc69d83cdfff200dd83edd1d200dd83ddc69d83cdffc","d83ddc69d83cdfff200dd83edd1d200dd83ddc69d83cdffd","d83ddc69d83cdfff200dd83edd1d200dd83ddc69d83cdffe"]],"People_8":[["d83ddc6b","d83ddc6bd83cdffb","d83ddc6bd83cdffc","d83ddc6bd83cdffd","d83ddc6bd83cdffe","d83ddc6bd83cdfff","d83ddc69d83cdffb200dd83edd1d200dd83ddc68d83cdffc","d83ddc69d83cdffb200dd83edd1d200dd83ddc68d83cdffd","d83ddc69d83cdffb200dd83edd1d200dd83ddc68d83cdffe","d83ddc69d83cdffb200dd83edd1d200dd83ddc68d83cdfff","d83ddc69d83cdffc200dd83edd1d200dd83ddc68d83cdffb","d83ddc69d83cdffc200dd83edd1d200dd83ddc68d83cdffd","d83ddc69d83cdffc200dd83edd1d200dd83ddc68d83cdffe","d83ddc69d83cdffc200dd83edd1d200dd83ddc68d83cdfff","d83ddc69d83cdffd200dd83edd1d200dd83ddc68d83cdffb","d83ddc69d83cdffd200dd83edd1d200dd83ddc68d83cdffc","d83ddc69d83cdffd200dd83edd1d200dd83ddc68d83cdffe","d83ddc69d83cdffd200dd83edd1d200dd83ddc68d83cdfff","d83ddc69d83cdffe200dd83edd1d200dd83ddc68d83cdffb","d83ddc69d83cdffe200dd83edd1d200dd83ddc68d83cdffc","d83ddc69d83cdffe200dd83edd1d200dd83ddc68d83cdffd","d83ddc69d83cdffe200dd83edd1d200dd83ddc68d83cdfff","d83ddc69d83cdfff200dd83edd1d200dd83ddc68d83cdffb","d83ddc69d83cdfff200dd83edd1d200dd83ddc68d83cdffc","d83ddc69d83cdfff200dd83edd1d200dd83ddc68d83cdffd","d83ddc69d83cdfff200dd83edd1d200dd83ddc68d83cdffe"],["d83ddc6c","d83ddc6cd83cdffb","d83ddc6cd83cdffc","d83ddc6cd83cdffd","d83ddc6cd83cdffe","d83ddc6cd83cdfff","d83ddc68d83cdffb200dd83edd1d200dd83ddc68d83cdffc","d83ddc68d83cdffb200dd83edd1d200dd83ddc68d83cdffd","d83ddc68d83cdffb200dd83edd1d200dd83ddc68d83cdffe","d83ddc68d83cdffb200dd83edd1d200dd83ddc68d83cdfff","d83ddc68d83cdffc200dd83edd1d200dd83ddc68d83cdffb","d83ddc68d83cdffc200dd83edd1d200dd83ddc68d83cdffd","d83ddc68d83cdffc200dd83edd1d200dd83ddc68d83cdffe","d83ddc68d83cdffc200dd83edd1d200dd83ddc68d83cdfff","d83ddc68d83cdffd200dd83edd1d200dd83ddc68d83cdffb","d83ddc68d83cdffd200dd83edd1d200dd83ddc68d83cdffc","d83ddc68d83cdffd200dd83edd1d200dd83ddc68d83cdffe","d83ddc68d83cdffd200dd83edd1d200dd83ddc68d83cdfff","d83ddc68d83cdffe200dd83edd1d200dd83ddc68d83cdffb","d83ddc68d83cdffe200dd83edd1d200dd83ddc68d83cdffc","d83ddc68d83cdffe200dd83edd1d200dd83ddc68d83cdffd","d83ddc68d83cdffe200dd83edd1d200dd83ddc68d83cdfff","d83ddc68d83cdfff200dd83edd1d200dd83ddc68d83cdffb","d83ddc68d83cdfff200dd83edd1d200dd83ddc68d83cdffc","d83ddc68d83cdfff200dd83edd1d200dd83ddc68d83cdffd","d83ddc68d83cdfff200dd83edd1d200dd83ddc68d83cdffe"],["d83ddc8f","d83ddc8fd83cdffb","d83ddc8fd83cdffc","d83ddc8fd83cdffd","d83ddc8fd83cdffe","d83ddc8fd83cdfff","d83eddd1d83cdffb200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffc","d83eddd1d83cdffb200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffd","d83eddd1d83cdffb200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffe","d83eddd1d83cdffb200d2764fe0f200dd83ddc8b200dd83eddd1d83cdfff","d83eddd1d83cdffc200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffb","d83eddd1d83cdffc200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffd","d83eddd1d83cdffc200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffe","d83eddd1d83cdffc200d2764fe0f200dd83ddc8b200dd83eddd1d83cdfff","d83eddd1d83cdffd200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffb","d83eddd1d83cdffd200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffc","d83eddd1d83cdffd200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffe","d83eddd1d83cdffd200d2764fe0f200dd83ddc8b200dd83eddd1d83cdfff","d83eddd1d83cdffe200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffb","d83eddd1d83cdffe200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffc","d83eddd1d83cdffe200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffd","d83eddd1d83cdffe200d2764fe0f200dd83ddc8b200dd83eddd1d83cdfff","d83eddd1d83cdfff200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffb","d83eddd1d83cdfff200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffc","d83eddd1d83cdfff200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffd","d83eddd1d83cdfff200d2764fe0f200dd83ddc8b200dd83eddd1d83cdffe"],["d83ddc69200d2764fe0f200dd83ddc8b200dd83ddc68","d83ddc69d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffb","d83ddc69d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffc","d83ddc69d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffd","d83ddc69d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffe","d83ddc69d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc68d83cdfff","d83ddc69d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffb","d83ddc69d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffc","d83ddc69d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffd","d83ddc69d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffe","d83ddc69d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc68d83cdfff","d83ddc69d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffb","d83ddc69d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffc","d83ddc69d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffd","d83ddc69d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffe","d83ddc69d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc68d83cdfff","d83ddc69d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffb","d83ddc69d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffc","d83ddc69d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffd","d83ddc69d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffe","d83ddc69d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc68d83cdfff","d83ddc69d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffb","d83ddc69d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffc","d83ddc69d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffd","d83ddc69d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffe","d83ddc69d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc68d83cdfff"],["d83ddc68200d2764fe0f200dd83ddc8b200dd83ddc68","d83ddc68d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffb","d83ddc68d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffc","d83ddc68d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffd","d83ddc68d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffe","d83ddc68d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc68d83cdfff","d83ddc68d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffb","d83ddc68d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffc","d83ddc68d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffd","d83ddc68d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffe","d83ddc68d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc68d83cdfff","d83ddc68d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffb","d83ddc68d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffc","d83ddc68d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffd","d83ddc68d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffe","d83ddc68d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc68d83cdfff","d83ddc68d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffb","d83ddc68d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffc","d83ddc68d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffd","d83ddc68d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffe","d83ddc68d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc68d83cdfff","d83ddc68d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffb","d83ddc68d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffc","d83ddc68d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffd","d83ddc68d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc68d83cdffe","d83ddc68d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc68d83cdfff"],["d83ddc69200d2764fe0f200dd83ddc8b200dd83ddc69","d83ddc69d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffb","d83ddc69d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffc","d83ddc69d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffd","d83ddc69d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffe","d83ddc69d83cdffb200d2764fe0f200dd83ddc8b200dd83ddc69d83cdfff","d83ddc69d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffb","d83ddc69d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffc","d83ddc69d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffd","d83ddc69d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffe","d83ddc69d83cdffc200d2764fe0f200dd83ddc8b200dd83ddc69d83cdfff","d83ddc69d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffb","d83ddc69d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffc","d83ddc69d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffd","d83ddc69d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffe","d83ddc69d83cdffd200d2764fe0f200dd83ddc8b200dd83ddc69d83cdfff","d83ddc69d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffb","d83ddc69d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffc","d83ddc69d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffd","d83ddc69d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffe","d83ddc69d83cdffe200d2764fe0f200dd83ddc8b200dd83ddc69d83cdfff","d83ddc69d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffb","d83ddc69d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffc","d83ddc69d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffd","d83ddc69d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc69d83cdffe","d83ddc69d83cdfff200d2764fe0f200dd83ddc8b200dd83ddc69d83cdfff"]],"People_9":[["d83ddc91","d83ddc91d83cdffb","d83ddc91d83cdffc","d83ddc91d83cdffd","d83ddc91d83cdffe","d83ddc91d83cdfff","d83eddd1d83cdffb200d2764fe0f200dd83eddd1d83cdffc","d83eddd1d83cdffb200d2764fe0f200dd83eddd1d83cdffd","d83eddd1d83cdffb200d2764fe0f200dd83eddd1d83cdffe","d83eddd1d83cdffb200d2764fe0f200dd83eddd1d83cdfff","d83eddd1d83cdffc200d2764fe0f200dd83eddd1d83cdffb","d83eddd1d83cdffc200d2764fe0f200dd83eddd1d83cdffd","d83eddd1d83cdffc200d2764fe0f200dd83eddd1d83cdffe","d83eddd1d83cdffc200d2764fe0f200dd83eddd1d83cdfff","d83eddd1d83cdffd200d2764fe0f200dd83eddd1d83cdffb","d83eddd1d83cdffd200d2764fe0f200dd83eddd1d83cdffc","d83eddd1d83cdffd200d2764fe0f200dd83eddd1d83cdffe","d83eddd1d83cdffd200d2764fe0f200dd83eddd1d83cdfff","d83eddd1d83cdffe200d2764fe0f200dd83eddd1d83cdffb","d83eddd1d83cdffe200d2764fe0f200dd83eddd1d83cdffc","d83eddd1d83cdffe200d2764fe0f200dd83eddd1d83cdffd","d83eddd1d83cdffe200d2764fe0f200dd83eddd1d83cdfff","d83eddd1d83cdfff200d2764fe0f200dd83eddd1d83cdffb","d83eddd1d83cdfff200d2764fe0f200dd83eddd1d83cdffc","d83eddd1d83cdfff200d2764fe0f200dd83eddd1d83cdffd","d83eddd1d83cdfff200d2764fe0f200dd83eddd1d83cdffe"],["d83ddc69200d2764fe0f200dd83ddc68","d83ddc69d83cdffb200d2764fe0f200dd83ddc68d83cdffb","d83ddc69d83cdffb200d2764fe0f200dd83ddc68d83cdffc","d83ddc69d83cdffb200d2764fe0f200dd83ddc68d83cdffd","d83ddc69d83cdffb200d2764fe0f200dd83ddc68d83cdffe","d83ddc69d83cdffb200d2764fe0f200dd83ddc68d83cdfff","d83ddc69d83cdffc200d2764fe0f200dd83ddc68d83cdffb","d83ddc69d83cdffc200d2764fe0f200dd83ddc68d83cdffc","d83ddc69d83cdffc200d2764fe0f200dd83ddc68d83cdffd","d83ddc69d83cdffc200d2764fe0f200dd83ddc68d83cdffe","d83ddc69d83cdffc200d2764fe0f200dd83ddc68d83cdfff","d83ddc69d83cdffd200d2764fe0f200dd83ddc68d83cdffb","d83ddc69d83cdffd200d2764fe0f200dd83ddc68d83cdffc","d83ddc69d83cdffd200d2764fe0f200dd83ddc68d83cdffd","d83ddc69d83cdffd200d2764fe0f200dd83ddc68d83cdffe","d83ddc69d83cdffd200d2764fe0f200dd83ddc68d83cdfff","d83ddc69d83cdffe200d2764fe0f200dd83ddc68d83cdffb","d83ddc69d83cdffe200d2764fe0f200dd83ddc68d83cdffc","d83ddc69d83cdffe200d2764fe0f200dd83ddc68d83cdffd","d83ddc69d83cdffe200d2764fe0f200dd83ddc68d83cdffe","d83ddc69d83cdffe200d2764fe0f200dd83ddc68d83cdfff","d83ddc69d83cdfff200d2764fe0f200dd83ddc68d83cdffb","d83ddc69d83cdfff200d2764fe0f200dd83ddc68d83cdffc","d83ddc69d83cdfff200d2764fe0f200dd83ddc68d83cdffd","d83ddc69d83cdfff200d2764fe0f200dd83ddc68d83cdffe","d83ddc69d83cdfff200d2764fe0f200dd83ddc68d83cdfff"],["d83ddc68200d2764fe0f200dd83ddc68","d83ddc68d83cdffb200d2764fe0f200dd83ddc68d83cdffb","d83ddc68d83cdffb200d2764fe0f200dd83ddc68d83cdffc","d83ddc68d83cdffb200d2764fe0f200dd83ddc68d83cdffd","d83ddc68d83cdffb200d2764fe0f200dd83ddc68d83cdffe","d83ddc68d83cdffb200d2764fe0f200dd83ddc68d83cdfff","d83ddc68d83cdffc200d2764fe0f200dd83ddc68d83cdffb","d83ddc68d83cdffc200d2764fe0f200dd83ddc68d83cdffc","d83ddc68d83cdffc200d2764fe0f200dd83ddc68d83cdffd","d83ddc68d83cdffc200d2764fe0f200dd83ddc68d83cdffe","d83ddc68d83cdffc200d2764fe0f200dd83ddc68d83cdfff","d83ddc68d83cdffd200d2764fe0f200dd83ddc68d83cdffb","d83ddc68d83cdffd200d2764fe0f200dd83ddc68d83cdffc","d83ddc68d83cdffd200d2764fe0f200dd83ddc68d83cdffd","d83ddc68d83cdffd200d2764fe0f200dd83ddc68d83cdffe","d83ddc68d83cdffd200d2764fe0f200dd83ddc68d83cdfff","d83ddc68d83cdffe200d2764fe0f200dd83ddc68d83cdffb","d83ddc68d83cdffe200d2764fe0f200dd83ddc68d83cdffc","d83ddc68d83cdffe200d2764fe0f200dd83ddc68d83cdffd","d83ddc68d83cdffe200d2764fe0f200dd83ddc68d83cdffe","d83ddc68d83cdffe200d2764fe0f200dd83ddc68d83cdfff","d83ddc68d83cdfff200d2764fe0f200dd83ddc68d83cdffb","d83ddc68d83cdfff200d2764fe0f200dd83ddc68d83cdffc","d83ddc68d83cdfff200d2764fe0f200dd83ddc68d83cdffd","d83ddc68d83cdfff200d2764fe0f200dd83ddc68d83cdffe","d83ddc68d83cdfff200d2764fe0f200dd83ddc68d83cdfff"],["d83ddc69200d2764fe0f200dd83ddc69","d83ddc69d83cdffb200d2764fe0f200dd83ddc69d83cdffb","d83ddc69d83cdffb200d2764fe0f200dd83ddc69d83cdffc","d83ddc69d83cdffb200d2764fe0f200dd83ddc69d83cdffd","d83ddc69d83cdffb200d2764fe0f200dd83ddc69d83cdffe","d83ddc69d83cdffb200d2764fe0f200dd83ddc69d83cdfff","d83ddc69d83cdffc200d2764fe0f200dd83ddc69d83cdffb","d83ddc69d83cdffc200d2764fe0f200dd83ddc69d83cdffc","d83ddc69d83cdffc200d2764fe0f200dd83ddc69d83cdffd","d83ddc69d83cdffc200d2764fe0f200dd83ddc69d83cdffe","d83ddc69d83cdffc200d2764fe0f200dd83ddc69d83cdfff","d83ddc69d83cdffd200d2764fe0f200dd83ddc69d83cdffb","d83ddc69d83cdffd200d2764fe0f200dd83ddc69d83cdffc","d83ddc69d83cdffd200d2764fe0f200dd83ddc69d83cdffd","d83ddc69d83cdffd200d2764fe0f200dd83ddc69d83cdffe","d83ddc69d83cdffd200d2764fe0f200dd83ddc69d83cdfff","d83ddc69d83cdffe200d2764fe0f200dd83ddc69d83cdffb","d83ddc69d83cdffe200d2764fe0f200dd83ddc69d83cdffc","d83ddc69d83cdffe200d2764fe0f200dd83ddc69d83cdffd","d83ddc69d83cdffe200d2764fe0f200dd83ddc69d83cdffe","d83ddc69d83cdffe200d2764fe0f200dd83ddc69d83cdfff","d83ddc69d83cdfff200d2764fe0f200dd83ddc69d83cdffb","d83ddc69d83cdfff200d2764fe0f200dd83ddc69d83cdffc","d83ddc69d83cdfff200d2764fe0f200dd83ddc69d83cdffd","d83ddc69d83cdfff200d2764fe0f200dd83ddc69d83cdffe","d83ddc69d83cdfff200d2764fe0f200dd83ddc69d83cdfff"],["d83ddc6a"],["d83ddc68200dd83ddc69200dd83ddc66"],["d83ddc68200dd83ddc69200dd83ddc67"],["d83ddc68200dd83ddc69200dd83ddc67200dd83ddc66"],["d83ddc68200dd83ddc69200dd83ddc66200dd83ddc66"],["d83ddc68200dd83ddc69200dd83ddc67200dd83ddc67"],["d83ddc68200dd83ddc68200dd83ddc66"],["d83ddc68200dd83ddc68200dd83ddc67"],["d83ddc68200dd83ddc68200dd83ddc67200dd83ddc66"],["d83ddc68200dd83ddc68200dd83ddc66200dd83ddc66"],["d83ddc68200dd83ddc68200dd83ddc67200dd83ddc67"],["d83ddc69200dd83ddc69200dd83ddc66"],["d83ddc69200dd83ddc69200dd83ddc67"],["d83ddc69200dd83ddc69200dd83ddc67200dd83ddc66"],["d83ddc69200dd83ddc69200dd83ddc66200dd83ddc66"],["d83ddc69200dd83ddc69200dd83ddc67200dd83ddc67"],["d83ddc68200dd83ddc66"],["d83ddc68200dd83ddc66200dd83ddc66"],["d83ddc68200dd83ddc67"],["d83ddc68200dd83ddc67200dd83ddc66"],["d83ddc68200dd83ddc67200dd83ddc67"],["d83ddc69200dd83ddc66"],["d83ddc69200dd83ddc66200dd83ddc66"],["d83ddc69200dd83ddc67"],["d83ddc69200dd83ddc67200dd83ddc66"],["d83ddc69200dd83ddc67200dd83ddc67"],["d83ddde3fe0f"],["d83ddc64"],["d83ddc65"],["d83edec2"],["d83ddc63"]],"Nature":[["d83ddc35"],["d83ddc12"],["d83edd8d"],["d83edda7"],["d83ddc36"],["d83ddc15"],["d83eddae"],["d83ddc15200dd83eddba"],["d83ddc29"],["d83ddc3a"],["d83edd8a"],["d83edd9d"],["d83ddc31"],["d83ddc08"],["d83ddc08200d2b1b"],["d83edd81"],["d83ddc2f"],["d83ddc05"],["d83ddc06"],["d83ddc34"],["d83ddc0e"],["d83edd84"],["d83edd93"],["d83edd8c"],["d83eddac"],["d83ddc2e"],["d83ddc02"],["d83ddc03"],["d83ddc04"],["d83ddc37"],["d83ddc16"],["d83ddc17"],["d83ddc3d"],["d83ddc0f"],["d83ddc11"],["d83ddc10"],["d83ddc2a"],["d83ddc2b"],["d83edd99"],["d83edd92"],["d83ddc18"],["d83edda3"],["d83edd8f"],["d83edd9b"],["d83ddc2d"],["d83ddc01"],["d83ddc00"],["d83ddc39"],["d83ddc30"],["d83ddc07"],["d83ddc3ffe0f"],["d83eddab"],["d83edd94"],["d83edd87"],["d83ddc3b"],["d83ddc3b200d2744fe0f"],["d83ddc28"],["d83ddc3c"],["d83edda5"],["d83edda6"],["d83edda8"],["d83edd98"],["d83edda1"],["d83ddc3e"],["d83edd83"],["d83ddc14"],["d83ddc13"],["d83ddc23"],["d83ddc24"],["d83ddc25"],["d83ddc26"],["d83ddc27"],["d83ddd4afe0f"],["d83edd85"],["d83edd86"],["d83edda2"],["d83edd89"],["d83edda4"],["d83edeb6"],["d83edda9"],["d83edd9a"],["d83edd9c"],["d83ddc38"],["d83ddc0a"],["d83ddc22"],["d83edd8e"],["d83ddc0d"],["d83ddc32"],["d83ddc09"],["d83edd95"],["d83edd96"],["d83ddc33"],["d83ddc0b"],["d83ddc2c"],["d83eddad"],["d83ddc1f"],["d83ddc20"],["d83ddc21"],["d83edd88"],["d83ddc19"],["d83ddc1a"],["d83edeb8"],["d83ddc0c"],["d83edd8b"],["d83ddc1b"],["d83ddc1c"],["d83ddc1d"],["d83edeb2"],["d83ddc1e"],["d83edd97"],["d83edeb3"],["d83ddd77fe0f"],["d83ddd78fe0f"],["d83edd82"],["d83edd9f"],["d83edeb0"],["d83edeb1"],["d83edda0"],["d83ddc90"],["d83cdf38"],["d83ddcae"],["d83edeb7"],["d83cdff5fe0f"],["d83cdf39"],["d83edd40"],["d83cdf3a"],["d83cdf3b"],["d83cdf3c"],["d83cdf37"],["d83cdf31"],["d83edeb4"],["d83cdf32"],["d83cdf33"],["d83cdf34"],["d83cdf35"],["d83cdf3e"],["d83cdf3f"],["2618fe0f"],["d83cdf40"],["d83cdf41"],["d83cdf42"],["d83cdf43"],["d83edeb9"],["d83edeba"]],"Foods":[["d83cdf47"],["d83cdf48"],["d83cdf49"],["d83cdf4a"],["d83cdf4b"],["d83cdf4c"],["d83cdf4d"],["d83edd6d"],["d83cdf4e"],["d83cdf4f"],["d83cdf50"],["d83cdf51"],["d83cdf52"],["d83cdf53"],["d83eded0"],["d83edd5d"],["d83cdf45"],["d83eded2"],["d83edd65"],["d83edd51"],["d83cdf46"],["d83edd54"],["d83edd55"],["d83cdf3d"],["d83cdf36fe0f"],["d83eded1"],["d83edd52"],["d83edd6c"],["d83edd66"],["d83eddc4"],["d83eddc5"],["d83cdf44"],["d83edd5c"],["d83eded8"],["d83cdf30"],["d83cdf5e"],["d83edd50"],["d83edd56"],["d83eded3"],["d83edd68"],["d83edd6f"],["d83edd5e"],["d83eddc7"],["d83eddc0"],["d83cdf56"],["d83cdf57"],["d83edd69"],["d83edd53"],["d83cdf54"],["d83cdf5f"],["d83cdf55"],["d83cdf2d"],["d83edd6a"],["d83cdf2e"],["d83cdf2f"],["d83eded4"],["d83edd59"],["d83eddc6"],["d83edd5a"],["d83cdf73"],["d83edd58"],["d83cdf72"],["d83eded5"],["d83edd63"],["d83edd57"],["d83cdf7f"],["d83eddc8"],["d83eddc2"],["d83edd6b"],["d83cdf71"],["d83cdf58"],["d83cdf59"],["d83cdf5a"],["d83cdf5b"],["d83cdf5c"],["d83cdf5d"],["d83cdf60"],["d83cdf62"],["d83cdf63"],["d83cdf64"],["d83cdf65"],["d83edd6e"],["d83cdf61"],["d83edd5f"],["d83edd60"],["d83edd61"],["d83edd80"],["d83edd9e"],["d83edd90"],["d83edd91"],["d83eddaa"],["d83cdf66"],["d83cdf67"],["d83cdf68"],["d83cdf69"],["d83cdf6a"],["d83cdf82"],["d83cdf70"],["d83eddc1"],["d83edd67"],["d83cdf6b"],["d83cdf6c"],["d83cdf6d"],["d83cdf6e"],["d83cdf6f"],["d83cdf7c"],["d83edd5b"],["2615"],["d83eded6"],["d83cdf75"],["d83cdf76"],["d83cdf7e"],["d83cdf77"],["d83cdf78"],["d83cdf79"],["d83cdf7a"],["d83cdf7b"],["d83edd42"],["d83edd43"],["d83eded7"],["d83edd64"],["d83eddcb"],["d83eddc3"],["d83eddc9"],["d83eddca"],["d83edd62"],["d83cdf7dfe0f"],["d83cdf74"],["d83edd44"],["d83ddd2a"],["d83eded9"],["d83cdffa"]],"Places":[["d83cdf0d"],["d83cdf0e"],["d83cdf0f"],["d83cdf10"],["d83dddfafe0f"],["d83dddfe"],["d83edded"],["d83cdfd4fe0f"],["26f0fe0f"],["d83cdf0b"],["d83dddfb"],["d83cdfd5fe0f"],["d83cdfd6fe0f"],["d83cdfdcfe0f"],["d83cdfddfe0f"],["d83cdfdefe0f"],["d83cdfdffe0f"],["d83cdfdbfe0f"],["d83cdfd7fe0f"],["d83eddf1"],["d83edea8"],["d83edeb5"],["d83dded6"],["d83cdfd8fe0f"],["d83cdfdafe0f"],["d83cdfe0"],["d83cdfe1"],["d83cdfe2"],["d83cdfe3"],["d83cdfe4"],["d83cdfe5"],["d83cdfe6"],["d83cdfe8"],["d83cdfe9"],["d83cdfea"],["d83cdfeb"],["d83cdfec"],["d83cdfed"],["d83cdfef"],["d83cdff0"],["d83ddc92"],["d83dddfc"],["d83dddfd"],["26ea"],["d83ddd4c"],["d83dded5"],["d83ddd4d"],["26e9fe0f"],["d83ddd4b"],["26f2"],["26fa"],["d83cdf01"],["d83cdf03"],["d83cdfd9fe0f"],["d83cdf04"],["d83cdf05"],["d83cdf06"],["d83cdf07"],["d83cdf09"],["2668fe0f"],["d83cdfa0"],["d83ddedd"],["d83cdfa1"],["d83cdfa2"],["d83ddc88"],["d83cdfaa"],["d83dde82"],["d83dde83"],["d83dde84"],["d83dde85"],["d83dde86"],["d83dde87"],["d83dde88"],["d83dde89"],["d83dde8a"],["d83dde9d"],["d83dde9e"],["d83dde8b"],["d83dde8c"],["d83dde8d"],["d83dde8e"],["d83dde90"],["d83dde91"],["d83dde92"],["d83dde93"],["d83dde94"],["d83dde95"],["d83dde96"],["d83dde97"],["d83dde98"],["d83dde99"],["d83ddefb"],["d83dde9a"],["d83dde9b"],["d83dde9c"],["d83cdfcefe0f"],["d83cdfcdfe0f"],["d83ddef5"],["d83eddbd"],["d83eddbc"],["d83ddefa"],["d83ddeb2"],["d83ddef4"],["d83ddef9"],["d83ddefc"],["d83dde8f"],["d83ddee3fe0f"],["d83ddee4fe0f"],["d83ddee2fe0f"],["26fd"],["d83ddede"],["d83ddea8"],["d83ddea5"],["d83ddea6"],["d83dded1"],["d83ddea7"],["2693"],["d83ddedf"],["26f5"],["d83ddef6"],["d83ddea4"],["d83ddef3fe0f"],["26f4fe0f"],["d83ddee5fe0f"],["d83ddea2"],["2708fe0f"],["d83ddee9fe0f"],["d83ddeeb"],["d83ddeec"],["d83ede82"],["d83ddcba"],["d83dde81"],["d83dde9f"],["d83ddea0"],["d83ddea1"],["d83ddef0fe0f"],["d83dde80"],["d83ddef8"],["d83ddecefe0f"],["d83eddf3"],["231b"],["23f3"],["231a"],["23f0"],["23f1fe0f"],["23f2fe0f"],["d83ddd70fe0f"],["d83ddd5b"],["d83ddd67"],["d83ddd50"],["d83ddd5c"],["d83ddd51"],["d83ddd5d"],["d83ddd52"],["d83ddd5e"],["d83ddd53"],["d83ddd5f"],["d83ddd54"],["d83ddd60"],["d83ddd55"],["d83ddd61"],["d83ddd56"],["d83ddd62"],["d83ddd57"],["d83ddd63"],["d83ddd58"],["d83ddd64"],["d83ddd59"],["d83ddd65"],["d83ddd5a"],["d83ddd66"],["d83cdf11"],["d83cdf12"],["d83cdf13"],["d83cdf14"],["d83cdf15"],["d83cdf16"],["d83cdf17"],["d83cdf18"],["d83cdf19"],["d83cdf1a"],["d83cdf1b"],["d83cdf1c"],["d83cdf21fe0f"],["2600fe0f"],["d83cdf1d"],["d83cdf1e"],["d83ede90"],["2b50"],["d83cdf1f"],["d83cdf20"],["d83cdf0c"],["2601fe0f"],["26c5"],["26c8fe0f"],["d83cdf24fe0f"],["d83cdf25fe0f"],["d83cdf26fe0f"],["d83cdf27fe0f"],["d83cdf28fe0f"],["d83cdf29fe0f"],["d83cdf2afe0f"],["d83cdf2bfe0f"],["d83cdf2cfe0f"],["d83cdf00"],["d83cdf08"],["d83cdf02"],["2602fe0f"],["2614"],["26f1fe0f"],["26a1"],["2744fe0f"],["2603fe0f"],["26c4"],["2604fe0f"],["d83ddd25"],["d83ddca7"],["d83cdf0a"]],"Activity":[["d83cdf83"],["d83cdf84"],["d83cdf86"],["d83cdf87"],["d83edde8"],["2728"],["d83cdf88"],["d83cdf89"],["d83cdf8a"],["d83cdf8b"],["d83cdf8d"],["d83cdf8e"],["d83cdf8f"],["d83cdf90"],["d83cdf91"],["d83edde7"],["d83cdf80"],["d83cdf81"],["d83cdf97fe0f"],["d83cdf9ffe0f"],["d83cdfab"],["d83cdf96fe0f"],["d83cdfc6"],["d83cdfc5"],["d83edd47"],["d83edd48"],["d83edd49"],["26bd"],["26be"],["d83edd4e"],["d83cdfc0"],["d83cdfd0"],["d83cdfc8"],["d83cdfc9"],["d83cdfbe"],["d83edd4f"],["d83cdfb3"],["d83cdfcf"],["d83cdfd1"],["d83cdfd2"],["d83edd4d"],["d83cdfd3"],["d83cdff8"],["d83edd4a"],["d83edd4b"],["d83edd45"],["26f3"],["26f8fe0f"],["d83cdfa3"],["d83edd3f"],["d83cdfbd"],["d83cdfbf"],["d83ddef7"],["d83edd4c"],["d83cdfaf"],["d83ede80"],["d83ede81"],["d83cdfb1"],["d83ddd2e"],["d83ede84"],["d83eddff"],["d83edeac"],["d83cdfae"],["d83ddd79fe0f"],["d83cdfb0"],["d83cdfb2"],["d83edde9"],["d83eddf8"],["d83ede85"],["d83edea9"],["d83ede86"],["2660fe0f"],["2665fe0f"],["2666fe0f"],["2663fe0f"],["265ffe0f"],["d83cdccf"],["d83cdc04"],["d83cdfb4"],["d83cdfad"],["d83dddbcfe0f"],["d83cdfa8"],["d83eddf5"],["d83edea1"],["d83eddf6"],["d83edea2"]],"Objects":[["d83ddc53"],["d83ddd76fe0f"],["d83edd7d"],["d83edd7c"],["d83eddba"],["d83ddc54"],["d83ddc55"],["d83ddc56"],["d83edde3"],["d83edde4"],["d83edde5"],["d83edde6"],["d83ddc57"],["d83ddc58"],["d83edd7b"],["d83ede71"],["d83ede72"],["d83ede73"],["d83ddc59"],["d83ddc5a"],["d83ddc5b"],["d83ddc5c"],["d83ddc5d"],["d83ddecdfe0f"],["d83cdf92"],["d83ede74"],["d83ddc5e"],["d83ddc5f"],["d83edd7e"],["d83edd7f"],["d83ddc60"],["d83ddc61"],["d83ede70"],["d83ddc62"],["d83ddc51"],["d83ddc52"],["d83cdfa9"],["d83cdf93"],["d83edde2"],["d83ede96"],["26d1fe0f"],["d83ddcff"],["d83ddc84"],["d83ddc8d"],["d83ddc8e"],["d83ddd07"],["d83ddd08"],["d83ddd09"],["d83ddd0a"],["d83ddce2"],["d83ddce3"],["d83ddcef"],["d83ddd14"],["d83ddd15"],["d83cdfbc"],["d83cdfb5"],["d83cdfb6"],["d83cdf99fe0f"],["d83cdf9afe0f"],["d83cdf9bfe0f"],["d83cdfa4"],["d83cdfa7"],["d83ddcfb"],["d83cdfb7"],["d83ede97"],["d83cdfb8"],["d83cdfb9"],["d83cdfba"],["d83cdfbb"],["d83ede95"],["d83edd41"],["d83ede98"],["d83ddcf1"],["d83ddcf2"],["260efe0f"],["d83ddcde"],["d83ddcdf"],["d83ddce0"],["d83ddd0b"],["d83edeab"],["d83ddd0c"],["d83ddcbb"],["d83ddda5fe0f"],["d83ddda8fe0f"],["2328fe0f"],["d83dddb1fe0f"],["d83dddb2fe0f"],["d83ddcbd"],["d83ddcbe"],["d83ddcbf"],["d83ddcc0"],["d83eddee"],["d83cdfa5"],["d83cdf9efe0f"],["d83ddcfdfe0f"],["d83cdfac"],["d83ddcfa"],["d83ddcf7"],["d83ddcf8"],["d83ddcf9"],["d83ddcfc"],["d83ddd0d"],["d83ddd0e"],["d83ddd6ffe0f"],["d83ddca1"],["d83ddd26"],["d83cdfee"],["d83ede94"],["d83ddcd4"],["d83ddcd5"],["d83ddcd6"],["d83ddcd7"],["d83ddcd8"],["d83ddcd9"],["d83ddcda"],["d83ddcd3"],["d83ddcd2"],["d83ddcc3"],["d83ddcdc"],["d83ddcc4"],["d83ddcf0"],["d83ddddefe0f"],["d83ddcd1"],["d83ddd16"],["d83cdff7fe0f"],["d83ddcb0"],["d83ede99"],["d83ddcb4"],["d83ddcb5"],["d83ddcb6"],["d83ddcb7"],["d83ddcb8"],["d83ddcb3"],["d83eddfe"],["d83ddcb9"],["2709fe0f"],["d83ddce7"],["d83ddce8"],["d83ddce9"],["d83ddce4"],["d83ddce5"],["d83ddce6"],["d83ddceb"],["d83ddcea"],["d83ddcec"],["d83ddced"],["d83ddcee"],["d83dddf3fe0f"],["270ffe0f"],["2712fe0f"],["d83ddd8bfe0f"],["d83ddd8afe0f"],["d83ddd8cfe0f"],["d83ddd8dfe0f"],["d83ddcdd"],["d83ddcbc"],["d83ddcc1"],["d83ddcc2"],["d83dddc2fe0f"],["d83ddcc5"],["d83ddcc6"],["d83dddd2fe0f"],["d83dddd3fe0f"],["d83ddcc7"],["d83ddcc8"],["d83ddcc9"],["d83ddcca"],["d83ddccb"],["d83ddccc"],["d83ddccd"],["d83ddcce"],["d83ddd87fe0f"],["d83ddccf"],["d83ddcd0"],["2702fe0f"],["d83dddc3fe0f"],["d83dddc4fe0f"],["d83dddd1fe0f"],["d83ddd12"],["d83ddd13"],["d83ddd0f"],["d83ddd10"],["d83ddd11"],["d83dddddfe0f"],["d83ddd28"],["d83ede93"],["26cffe0f"],["2692fe0f"],["d83ddee0fe0f"],["d83ddde1fe0f"],["2694fe0f"],["d83ddd2b"],["d83ede83"],["d83cdff9"],["d83ddee1fe0f"],["d83ede9a"],["d83ddd27"],["d83ede9b"],["d83ddd29"],["2699fe0f"],["d83ddddcfe0f"],["2696fe0f"],["d83eddaf"],["d83ddd17"],["26d3fe0f"],["d83ede9d"],["d83eddf0"],["d83eddf2"],["d83ede9c"],["2697fe0f"],["d83eddea"],["d83eddeb"],["d83eddec"],["d83ddd2c"],["d83ddd2d"],["d83ddce1"],["d83ddc89"],["d83ede78"],["d83ddc8a"],["d83ede79"],["d83ede7c"],["d83ede7a"],["d83ede7b"],["d83ddeaa"],["d83dded7"],["d83ede9e"],["d83ede9f"],["d83ddecffe0f"],["d83ddecbfe0f"],["d83ede91"],["d83ddebd"],["d83edea0"],["d83ddebf"],["d83ddec1"],["d83edea4"],["d83ede92"],["d83eddf4"],["d83eddf7"],["d83eddf9"],["d83eddfa"],["d83eddfb"],["d83edea3"],["d83eddfc"],["d83edee7"],["d83edea5"],["d83eddfd"],["d83eddef"],["d83dded2"],["d83ddeac"],["26b0fe0f"],["d83edea6"],["26b1fe0f"],["d83dddff"],["d83edea7"],["d83edeaa"]],"Symbols":[["d83cdfe7"],["d83ddeae"],["d83ddeb0"],["267f"],["d83ddeb9"],["d83ddeba"],["d83ddebb"],["d83ddebc"],["d83ddebe"],["d83ddec2"],["d83ddec3"],["d83ddec4"],["d83ddec5"],["26a0fe0f"],["d83ddeb8"],["26d4"],["d83ddeab"],["d83ddeb3"],["d83ddead"],["d83ddeaf"],["d83ddeb1"],["d83ddeb7"],["d83ddcf5"],["d83ddd1e"],["2622fe0f"],["2623fe0f"],["2b06fe0f"],["2197fe0f"],["27a1fe0f"],["2198fe0f"],["2b07fe0f"],["2199fe0f"],["2b05fe0f"],["2196fe0f"],["2195fe0f"],["2194fe0f"],["21a9fe0f"],["21aafe0f"],["2934fe0f"],["2935fe0f"],["d83ddd03"],["d83ddd04"],["d83ddd19"],["d83ddd1a"],["d83ddd1b"],["d83ddd1c"],["d83ddd1d"],["d83dded0"],["269bfe0f"],["d83ddd49fe0f"],["2721fe0f"],["2638fe0f"],["262ffe0f"],["271dfe0f"],["2626fe0f"],["262afe0f"],["262efe0f"],["d83ddd4e"],["d83ddd2f"],["2648"],["2649"],["264a"],["264b"],["264c"],["264d"],["264e"],["264f"],["2650"],["2651"],["2652"],["2653"],["26ce"],["d83ddd00"],["d83ddd01"],["d83ddd02"],["25b6fe0f"],["23e9"],["23edfe0f"],["23effe0f"],["25c0fe0f"],["23ea"],["23eefe0f"],["d83ddd3c"],["23eb"],["d83ddd3d"],["23ec"],["23f8fe0f"],["23f9fe0f"],["23fafe0f"],["23cffe0f"],["d83cdfa6"],["d83ddd05"],["d83ddd06"],["d83ddcf6"],["d83ddcf3"],["d83ddcf4"],["26a7fe0f"],["2716fe0f"],["2795"],["2796"],["2797"],["d83ddff0"],["267efe0f"],["203cfe0f"],["2049fe0f"],["2753"],["2754"],["2755"],["2757"],["3030fe0f"],["d83ddcb1"],["d83ddcb2"],["267bfe0f"],["269cfe0f"],["d83ddd31"],["d83ddcdb"],["d83ddd30"],["2b55"],["2705"],["2611fe0f"],["2714fe0f"],["274c"],["274e"],["27b0"],["27bf"],["303dfe0f"],["2733fe0f"],["2734fe0f"],["2747fe0f"],["00a9fe0f"],["00aefe0f"],["2122fe0f"],["0023fe0f20e3"],["002afe0f20e3"],["0030fe0f20e3"],["0031fe0f20e3"],["0032fe0f20e3"],["0033fe0f20e3"],["0034fe0f20e3"],["0035fe0f20e3"],["0036fe0f20e3"],["0037fe0f20e3"],["0038fe0f20e3"],["0039fe0f20e3"],["d83ddd1f"],["d83ddd20"],["d83ddd21"],["d83ddd22"],["d83ddd23"],["d83ddd24"],["d83cdd70fe0f"],["d83cdd8e"],["d83cdd71fe0f"],["d83cdd91"],["d83cdd92"],["d83cdd93"],["2139fe0f"],["d83cdd94"],["24c2fe0f"],["d83cdd95"],["d83cdd96"],["d83cdd7efe0f"],["d83cdd97"],["d83cdd7ffe0f"],["d83cdd98"],["d83cdd99"],["d83cdd9a"],["d83cde01"],["d83cde02fe0f"],["d83cde37fe0f"],["d83cde36"],["d83cde2f"],["d83cde50"],["d83cde39"],["d83cde1a"],["d83cde32"],["d83cde51"],["d83cde38"],["d83cde34"],["d83cde33"],["3297fe0f"],["3299fe0f"],["d83cde3a"],["d83cde35"],["d83ddd34"],["d83ddfe0"],["d83ddfe1"],["d83ddfe2"],["d83ddd35"],["d83ddfe3"],["d83ddfe4"],["26ab"],["26aa"],["d83ddfe5"],["d83ddfe7"],["d83ddfe8"],["d83ddfe9"],["d83ddfe6"],["d83ddfea"],["d83ddfeb"],["2b1b"],["2b1c"],["25fcfe0f"],["25fbfe0f"],["25fe"],["25fd"],["25aafe0f"],["25abfe0f"],["d83ddd36"],["d83ddd37"],["d83ddd38"],["d83ddd39"],["d83ddd3a"],["d83ddd3b"],["d83ddca0"],["d83ddd18"],["d83ddd33"],["d83ddd32"]],"Flags_0":[["d83cdfc1"],["d83ddea9"],["d83cdf8c"],["d83cdff4"],["d83cdff3fe0f"],["d83cdff3fe0f200dd83cdf08"],["d83cdff3fe0f200d26a7fe0f"],["d83cdff4200d2620fe0f"],["d83cdde6d83cdde8"],["d83cdde6d83cdde9"],["d83cdde6d83cddea"],["d83cdde6d83cddeb"],["d83cdde6d83cddec"],["d83cdde6d83cddee"],["d83cdde6d83cddf1"],["d83cdde6d83cddf2"],["d83cdde6d83cddf4"],["d83cdde6d83cddf6"],["d83cdde6d83cddf7"],["d83cdde6d83cddf8"],["d83cdde6d83cddf9"],["d83cdde6d83cddfa"],["d83cdde6d83cddfc"],["d83cdde6d83cddfd"],["d83cdde6d83cddff"],["d83cdde7d83cdde6"],["d83cdde7d83cdde7"],["d83cdde7d83cdde9"],["d83cdde7d83cddea"],["d83cdde7d83cddeb"],["d83cdde7d83cddec"],["d83cdde7d83cdded"],["d83cdde7d83cddee"],["d83cdde7d83cddef"],["d83cdde7d83cddf1"],["d83cdde7d83cddf2"],["d83cdde7d83cddf3"],["d83cdde7d83cddf4"],["d83cdde7d83cddf6"],["d83cdde7d83cddf7"],["d83cdde7d83cddf8"],["d83cdde7d83cddf9"],["d83cdde7d83cddfb"],["d83cdde7d83cddfc"],["d83cdde7d83cddfe"],["d83cdde7d83cddff"],["d83cdde8d83cdde6"],["d83cdde8d83cdde8"],["d83cdde8d83cdde9"],["d83cdde8d83cddeb"],["d83cdde8d83cddec"],["d83cdde8d83cdded"],["d83cdde8d83cddee"],["d83cdde8d83cddf0"],["d83cdde8d83cddf1"],["d83cdde8d83cddf2"],["d83cdde8d83cddf3"],["d83cdde8d83cddf4"],["d83cdde8d83cddf5"],["d83cdde8d83cddf7"],["d83cdde8d83cddfa"],["d83cdde8d83cddfb"],["d83cdde8d83cddfc"],["d83cdde8d83cddfd"],["d83cdde8d83cddfe"],["d83cdde8d83cddff"],["d83cdde9d83cddea"],["d83cdde9d83cddec"],["d83cdde9d83cddef"],["d83cdde9d83cddf0"],["d83cdde9d83cddf2"],["d83cdde9d83cddf4"],["d83cdde9d83cddff"],["d83cddead83cdde6"],["d83cddead83cdde8"],["d83cddead83cddea"],["d83cddead83cddec"],["d83cddead83cdded"],["d83cddead83cddf7"],["d83cddead83cddf8"],["d83cddead83cddf9"],["d83cddead83cddfa"],["d83cddebd83cddee"],["d83cddebd83cddef"],["d83cddebd83cddf0"],["d83cddebd83cddf2"],["d83cddebd83cddf4"],["d83cddebd83cddf7"],["d83cddecd83cdde6"],["d83cddecd83cdde7"],["d83cddecd83cdde9"],["d83cddecd83cddea"],["d83cddecd83cddeb"],["d83cddecd83cddec"],["d83cddecd83cdded"],["d83cddecd83cddee"],["d83cddecd83cddf1"],["d83cddecd83cddf2"],["d83cddecd83cddf3"],["d83cddecd83cddf5"],["d83cddecd83cddf6"],["d83cddecd83cddf7"],["d83cddecd83cddf8"],["d83cddecd83cddf9"],["d83cddecd83cddfa"],["d83cddecd83cddfc"],["d83cddecd83cddfe"],["d83cddedd83cddf0"],["d83cddedd83cddf2"],["d83cddedd83cddf3"],["d83cddedd83cddf7"],["d83cddedd83cddf9"],["d83cddedd83cddfa"],["d83cddeed83cdde8"],["d83cddeed83cdde9"],["d83cddeed83cddea"],["d83cddeed83cddf1"],["d83cddeed83cddf2"],["d83cddeed83cddf3"],["d83cddeed83cddf4"],["d83cddeed83cddf6"],["d83cddeed83cddf7"],["d83cddeed83cddf8"],["d83cddeed83cddf9"],["d83cddefd83cddea"],["d83cddefd83cddf2"],["d83cddefd83cddf4"],["d83cddefd83cddf5"],["d83cddf0d83cddea"],["d83cddf0d83cddec"],["d83cddf0d83cdded"],["d83cddf0d83cddee"],["d83cddf0d83cddf2"],["d83cddf0d83cddf3"],["d83cddf0d83cddf5"]],"Flags_1":[["d83cddf0d83cddf7"],["d83cddf0d83cddfc"],["d83cddf0d83cddfe"],["d83cddf0d83cddff"],["d83cddf1d83cdde6"],["d83cddf1d83cdde7"],["d83cddf1d83cdde8"],["d83cddf1d83cddee"],["d83cddf1d83cddf0"],["d83cddf1d83cddf7"],["d83cddf1d83cddf8"],["d83cddf1d83cddf9"],["d83cddf1d83cddfa"],["d83cddf1d83cddfb"],["d83cddf1d83cddfe"],["d83cddf2d83cdde6"],["d83cddf2d83cdde8"],["d83cddf2d83cdde9"],["d83cddf2d83cddea"],["d83cddf2d83cddeb"],["d83cddf2d83cddec"],["d83cddf2d83cdded"],["d83cddf2d83cddf0"],["d83cddf2d83cddf1"],["d83cddf2d83cddf2"],["d83cddf2d83cddf3"],["d83cddf2d83cddf4"],["d83cddf2d83cddf5"],["d83cddf2d83cddf6"],["d83cddf2d83cddf7"],["d83cddf2d83cddf8"],["d83cddf2d83cddf9"],["d83cddf2d83cddfa"],["d83cddf2d83cddfb"],["d83cddf2d83cddfc"],["d83cddf2d83cddfd"],["d83cddf2d83cddfe"],["d83cddf2d83cddff"],["d83cddf3d83cdde6"],["d83cddf3d83cdde8"],["d83cddf3d83cddea"],["d83cddf3d83cddeb"],["d83cddf3d83cddec"],["d83cddf3d83cddee"],["d83cddf3d83cddf1"],["d83cddf3d83cddf4"],["d83cddf3d83cddf5"],["d83cddf3d83cddf7"],["d83cddf3d83cddfa"],["d83cddf3d83cddff"],["d83cddf4d83cddf2"],["d83cddf5d83cdde6"],["d83cddf5d83cddea"],["d83cddf5d83cddeb"],["d83cddf5d83cddec"],["d83cddf5d83cdded"],["d83cddf5d83cddf0"],["d83cddf5d83cddf1"],["d83cddf5d83cddf2"],["d83cddf5d83cddf3"],["d83cddf5d83cddf7"],["d83cddf5d83cddf8"],["d83cddf5d83cddf9"],["d83cddf5d83cddfc"],["d83cddf5d83cddfe"],["d83cddf6d83cdde6"],["d83cddf7d83cddea"],["d83cddf7d83cddf4"],["d83cddf7d83cddf8"],["d83cddf7d83cddfa"],["d83cddf7d83cddfc"],["d83cddf8d83cdde6"],["d83cddf8d83cdde7"],["d83cddf8d83cdde8"],["d83cddf8d83cdde9"],["d83cddf8d83cddea"],["d83cddf8d83cddec"],["d83cddf8d83cdded"],["d83cddf8d83cddee"],["d83cddf8d83cddef"],["d83cddf8d83cddf0"],["d83cddf8d83cddf1"],["d83cddf8d83cddf2"],["d83cddf8d83cddf3"],["d83cddf8d83cddf4"],["d83cddf8d83cddf7"],["d83cddf8d83cddf8"],["d83cddf8d83cddf9"],["d83cddf8d83cddfb"],["d83cddf8d83cddfd"],["d83cddf8d83cddfe"],["d83cddf8d83cddff"],["d83cddf9d83cdde6"],["d83cddf9d83cdde8"],["d83cddf9d83cdde9"],["d83cddf9d83cddeb"],["d83cddf9d83cddec"],["d83cddf9d83cdded"],["d83cddf9d83cddef"],["d83cddf9d83cddf0"],["d83cddf9d83cddf1"],["d83cddf9d83cddf2"],["d83cddf9d83cddf3"],["d83cddf9d83cddf4"],["d83cddf9d83cddf7"],["d83cddf9d83cddf9"],["d83cddf9d83cddfb"],["d83cddf9d83cddfc"],["d83cddf9d83cddff"],["d83cddfad83cdde6"],["d83cddfad83cddec"],["d83cddfad83cddf2"],["d83cddfad83cddf3"],["d83cddfad83cddf8"],["d83cddfad83cddfe"],["d83cddfad83cddff"],["d83cddfbd83cdde6"],["d83cddfbd83cdde8"],["d83cddfbd83cddea"],["d83cddfbd83cddec"],["d83cddfbd83cddee"],["d83cddfbd83cddf3"],["d83cddfbd83cddfa"],["d83cddfcd83cddeb"],["d83cddfcd83cddf8"],["d83cddfdd83cddf0"],["d83cddfed83cddea"],["d83cddfed83cddf9"],["d83cddffd83cdde6"],["d83cddffd83cddf2"],["d83cddffd83cddfc"],["d83cdff4db40dc67db40dc62db40dc65db40dc6edb40dc67db40dc7f"],["d83cdff4db40dc67db40dc62db40dc73db40dc63db40dc74db40dc7f"],["d83cdff4db40dc67db40dc62db40dc77db40dc6cdb40dc73db40dc7f"]]},"obsolete":[],"metrics":{"raw_width":66,"raw_height":66,"per_row":16},"densities":["xhdpi"],"format":"webp"} \ No newline at end of file diff --git a/app/src/main/assets/emoji/emoji_search_index.json b/app/src/main/assets/emoji/emoji_search_index.json new file mode 100644 index 0000000000..966f21be32 --- /dev/null +++ b/app/src/main/assets/emoji/emoji_search_index.json @@ -0,0 +1 @@ +[{"emoji":"😀","tags":["face","grin","smile","grinning face","smiling","happy"]},{"emoji":"😃","tags":["face","grinning face with big eyes","open","mouth","smile","smiling","happy"]},{"emoji":"😄","tags":["eye","face","mouth","grinning face with smiling eyes","open","smile","smiling","happy"]},{"emoji":"😁","tags":["beaming face with smiling eyes","eye","grin","face","smile","smiling","happy"]},{"emoji":"😆","tags":["face","grinning squinting face","mouth","laugh","satisfied","smile","smiling","squint","happy"]},{"emoji":"😅","tags":["face","open","grinning face with sweat","smile","sweat","smiling","laugh","funny"]},{"emoji":"🤣","tags":["face","floor","rofl","laugh","rolling","rolling on the floor laughing","rotfl","funny","cry"]},{"emoji":"😂","tags":["face","face with tears of joy","laugh","joy","tear","funny","cry"]},{"emoji":"🙂","tags":["face","slightly smiling face","smiling","smile"]},{"emoji":"🙃","tags":["face","upside-down","sarcasm","silly","sarcastic","uh oh"]},{"emoji":"😉","tags":["face","wink","winking face"]},{"emoji":"😊","tags":["blush","eye","smile","face","smiling face with smiling eyes"]},{"emoji":"😇","tags":["angel","face","halo","fantasy","innocent","smiling face with halo"]},{"emoji":"🥰","tags":["adore","crush","in love","hearts","smiling face with hearts"]},{"emoji":"😍","tags":["eye","face","smile","love","smiling face with heart-eyes","hearts"]},{"emoji":"🤩","tags":["eyes","face","star","grinning","star-struck"]},{"emoji":"😘","tags":["face","face blowing a kiss","flirt","kiss","kiss with heart"]},{"emoji":"😗","tags":["face","kiss","pout","kissing face"]},{"emoji":"☺️","tags":["face","slightly smiling face","smiling","smile"]},{"emoji":"😚","tags":["closed","eye","kiss","face","kissing face with closed eyes","kiss squinty"]},{"emoji":"😙","tags":["eye","face","kissing face with smiling eyes","kiss","smile"]},{"emoji":"🥲","tags":["grateful","proud","smiling","relieved","smiling face with tear","tear","touched"]},{"emoji":"😋","tags":["delicious","face","savouring","face savoring food","smile","yum"]},{"emoji":"😛","tags":["face","face with tongue","tongue"]},{"emoji":"😜","tags":["eye","face","tongue","joke","wink","winking face with tongue"]},{"emoji":"🤪","tags":["eye","goofy","small","large","zany face","silly"]},{"emoji":"😝","tags":["eye","face","squinting face with tongue","horrible","taste","tongue"]},{"emoji":"🤑","tags":["face","money","mouth","money-mouth face"]},{"emoji":"🤗","tags":["face","hug","hugging"]},{"emoji":"🤭","tags":["face with hand over mouth","whoops","shush","shh"]},{"emoji":"🤫","tags":["quiet","shush","shh","shushing face"]},{"emoji":"🤔","tags":["face","thinking"]},{"emoji":"🤐","tags":["face","mouth","zipper-mouth face","zipper","shh"]},{"emoji":"🤨","tags":["distrust","face with raised eyebrow","unsure","skeptic"]},{"emoji":"😐","tags":["deadpan","face","neutral","meh"]},{"emoji":"😑","tags":["expressionless","face","meh","inexpressive","unexpressive"]},{"emoji":"😶","tags":["face","face without mouth","quiet","mouth","silent"]},{"emoji":"😶‍🌫️","tags":["absentminded","face in clouds","head in clouds","face in the fog"]},{"emoji":"😏","tags":["face","smirk","smirking face"]},{"emoji":"😒","tags":["face","unamused","unhappy"]},{"emoji":"🙄","tags":["eyeroll","eyes","face with rolling eyes","face","rolling"]},{"emoji":"😬","tags":["face","grimace","grimacing face"]},{"emoji":"😮‍💨","tags":["exhale","face exhaling","groan","gasp","relief","whisper","whistle"]},{"emoji":"🤥","tags":["face","lie","pinocchio","lying face"]},{"emoji":"😌","tags":["face","relieved"]},{"emoji":"😔","tags":["dejected","face","pensive"]},{"emoji":"😪","tags":["face","sleep","snore","sleepy face"]},{"emoji":"🤤","tags":["drooling","face"]},{"emoji":"😴","tags":["face","sleep","zzz","sleeping face","tired"]},{"emoji":"😷","tags":["cold","doctor","face with medical mask","face","mask","sick"]},{"emoji":"🤒","tags":["face","face with thermometer","sick","ill","thermometer"]},{"emoji":"🤕","tags":["bandage","face","hurt","face with head-bandage","injury"]},{"emoji":"🤢","tags":["face","nauseated","sick","vomit","throw up"]},{"emoji":"🤮","tags":["face vomiting","puke","vomit","sick","throw up"]},{"emoji":"🤧","tags":["face","gesundheit","sneezing face","sneeze","sick"]},{"emoji":"🥵","tags":["feverish","heat stroke","hot face","hot","red-faced","sweating"]},{"emoji":"🥶","tags":["blue-faced","cold","freezing","cold face","frostbite","icicles"]},{"emoji":"🥴","tags":["dizzy","intoxicated","uneven eyes","tipsy","wavy mouth","woozy face","drunk"]},{"emoji":"😵","tags":["dead","face","knocked-out face","knocked out"]},{"emoji":"😵‍💫","tags":["dizzy","face with spiral eyes","spiral","hypnotized","trouble","whoa"]},{"emoji":"🤯","tags":["exploding head","mind blown","shocked"]},{"emoji":"🤠","tags":["cowboy","cowgirl","hat","face"]},{"emoji":"🥳","tags":["celebration","hat","party","horn","partying face"]},{"emoji":"🥸","tags":["disguise","disguised face","glasses","face","incognito","nose"]},{"emoji":"😎","tags":["bright","cool","smiling face with sunglasses","face","sun","sunglasses"]},{"emoji":"🤓","tags":["face","geek","glasses","nerd","smart"]},{"emoji":"🧐","tags":["face with monocle","stuffy"]},{"emoji":"😕","tags":["confused","face","meh"]},{"emoji":"😟","tags":["face","worried","sad"]},{"emoji":"🙁","tags":["face","frown","sad","slightly frowning face"]},{"emoji":"☹️","tags":[]},{"emoji":"😮","tags":["face","face with open mouth","open","mouth","sympathy","wow"]},{"emoji":"😯","tags":["face","hushed","surprised","stunned"]},{"emoji":"😲","tags":["astonished","face","totally","shocked","wow"]},{"emoji":"😳","tags":["dazed","face","flushed"]},{"emoji":"🥺","tags":["begging","mercy","puppy eyes","pleading face","cute"]},{"emoji":"😦","tags":["face","frown","mouth","frowning face with open mouth","open"]},{"emoji":"😧","tags":["anguished","face"]},{"emoji":"😨","tags":["face","fear","scared","fearful"]},{"emoji":"😰","tags":["anxious face with sweat","blue","face","cold","rushed","sweat"]},{"emoji":"😥","tags":["disappointed","face","sad but relieved face","relieved","whew"]},{"emoji":"😢","tags":["cry","crying face","sad","face","tear"]},{"emoji":"😭","tags":["cry","face","sad","loudly crying face","sob","tear"]},{"emoji":"😱","tags":["face","face screaming in fear","munch","fear","scared","scream"]},{"emoji":"😖","tags":["confounded","face"]},{"emoji":"😣","tags":["face","persevere","persevering face"]},{"emoji":"😞","tags":["disappointed","face"]},{"emoji":"😓","tags":["cold","downcast face with sweat","sweat","face"]},{"emoji":"😩","tags":["face","tired","despair","weary"]},{"emoji":"😫","tags":["face","tired","despair"]},{"emoji":"🥱","tags":["bored","tired","yawning face","yawn"]},{"emoji":"😤","tags":["face","face with steam from nose","won","triumph","angry"]},{"emoji":"😡","tags":["angry","face","pouting","mad","rage","red"]},{"emoji":"😠","tags":["anger","angry","mad","face"]},{"emoji":"🤬","tags":["face with symbols on mouth","swearing"]},{"emoji":"😈","tags":["face","fairy tale","horns","fantasy","smile","smiling face with horns"]},{"emoji":"👿","tags":["angry face with horns","demon","face","devil","fantasy","imp"]},{"emoji":"💀","tags":["death","face","monster","fairy tale","skull","dead","funny"]},{"emoji":"☠️","tags":[]},{"emoji":"💩","tags":["dung","face","pile of poo","monster","poo","poop"]},{"emoji":"🤡","tags":["clown","face","silly"]},{"emoji":"👹","tags":["creature","face","fantasy","fairy tale","monster","ogre","devil","demon"]},{"emoji":"👺","tags":["creature","face","fantasy","fairy tale","goblin","monster","devil","demon"]},{"emoji":"👻","tags":["creature","face","fantasy","fairy tale","ghost","monster","boo","spooky"]},{"emoji":"👽","tags":["alien","creature","face","extraterrestrial","fantasy","ufo"]},{"emoji":"👾","tags":["alien","creature","face","extraterrestrial","monster","ufo"]},{"emoji":"🤖","tags":["face","monster","robot"]},{"emoji":"😺","tags":["cat","face","mouth","grinning","open","smile"]},{"emoji":"😸","tags":["cat","eye","grin","face","grinning cat with smiling eyes","smile"]},{"emoji":"😹","tags":["cat","cat with tears of joy","joy","face","tear"]},{"emoji":"😻","tags":["cat","eye","heart eyes","face","love","smile","smiling cat with heart-eyes"]},{"emoji":"😼","tags":["cat","cat with wry smile","ironic","face","smile","wry"]},{"emoji":"😽","tags":["cat","eye","kiss","face","kissing cat"]},{"emoji":"🙀","tags":["cat","face","surprised","oh","weary"]},{"emoji":"😿","tags":["cat","cry","face","crying cat","sad","tear"]},{"emoji":"😾","tags":["cat","face","pouting"]},{"emoji":"🙈","tags":["evil","face","monkey","forbidden","see","see-no-evil monkey"]},{"emoji":"🙉","tags":["evil","face","hear","forbidden","hear-no-evil monkey","monkey"]},{"emoji":"🙊","tags":["evil","face","monkey","forbidden","speak","speak-no-evil monkey"]},{"emoji":"💋","tags":["kiss","kiss mark","lips"]},{"emoji":"💌","tags":["heart letter","letter","mail","love"]},{"emoji":"💘","tags":["arrow","cupid","heart with arrow"]},{"emoji":"💝","tags":["heart with ribbon","ribbon","valentine"]},{"emoji":"💖","tags":["excited","sparkle","sparkling heart"]},{"emoji":"💗","tags":["excited","growing heart","nervous","growing","pulse"]},{"emoji":"💓","tags":["beating","beating heart","pulsating","heartbeat"]},{"emoji":"💞","tags":["revolving","revolving hearts"]},{"emoji":"💕","tags":["love","two hearts"]},{"emoji":"💟","tags":["heart decoration"]},{"emoji":"❣️","tags":["heart exclamation","heart drop","exclamation"]},{"emoji":"💔","tags":["break","broken heart","heart break"]},{"emoji":"❤️‍🔥","tags":["burn","burning heart","heart on fire","love","lust","sacred heart"]},{"emoji":"❤️‍🩹","tags":["healthier","mending heart","heart mending","mending","recovering","recuperating","well","improving"]},{"emoji":"❤️","tags":[]},{"emoji":"🧡","tags":["orange","orange heart","heart orange"]},{"emoji":"💛","tags":["yellow","yellow heart","heart yellow"]},{"emoji":"💚","tags":["green","green heart","heart green"]},{"emoji":"💙","tags":["blue","blue heart","heart blue"]},{"emoji":"💜","tags":["purple","purple heart","heart purple"]},{"emoji":"🤎","tags":["brown","brownheart","heart brown"]},{"emoji":"🖤","tags":["black","black heart","heart black","wicked","evil"]},{"emoji":"🤍","tags":["white","white heart","heart white"]},{"emoji":"💯","tags":["100","full","hundred points","hundred","score"]},{"emoji":"💢","tags":["anger symbol","angry","mad","comic"]},{"emoji":"💥","tags":["boom","collision","comic"]},{"emoji":"💫","tags":["comic","dizzy","star"]},{"emoji":"💦","tags":["comic","splashing","sweat droplets","sweat"]},{"emoji":"💨","tags":["comic","dash","running","dashing away"]},{"emoji":"🕳️","tags":["hole"]},{"emoji":"💣","tags":["bomb","comic"]},{"emoji":"💬","tags":["balloon","bubble","dialog","comic","speech","typing"]},{"emoji":"🗨️","tags":[]},{"emoji":"🗯️","tags":["angry","balloon","mad","bubble","right anger bubble","danger"]},{"emoji":"💭","tags":["balloon","bubble","thought","comic"]},{"emoji":"💤","tags":["comic","sleep","zzz"]},{"emoji":"👋","tags":["hand","wave","waving","hi","hey","hello","bye","goodbye"]},{"emoji":"🤚","tags":["backhand","raised","raised back of hand"]},{"emoji":"🖐️","tags":["finger","hand","splayed","hand with fingers splayed"]},{"emoji":"✋","tags":["hand","high 5","raised hand","high five","hifive"]},{"emoji":"🖖","tags":["finger","hand","vulcan","spock","vulcan salute"]},{"emoji":"👌","tags":["hand","OK"]},{"emoji":"🤌","tags":["fingers","hand gesture","pinched","interrogation","sarcastic"]},{"emoji":"🤏","tags":["pinching hand","small amount","little"]},{"emoji":"✌️","tags":["hand","v","victory"]},{"emoji":"🤞","tags":["cross","crossed fingers","hand","finger","luck"]},{"emoji":"🤟","tags":["hand","ILY","love-you gesture"]},{"emoji":"🤘","tags":["finger","hand","rock-on","horns","sign of the horns","metal"]},{"emoji":"🤙","tags":["call","call me hand","hand","shaka sign","right on","thanks"]},{"emoji":"👈","tags":["backhand","backhand index pointing left","hand","finger","index","point"]},{"emoji":"👉","tags":["backhand","backhand index pointing right","hand","finger","index","point"]},{"emoji":"👆","tags":["backhand","backhand index pointing up","hand","finger","point","up"]},{"emoji":"🖕","tags":["finger","hand","middle finger"]},{"emoji":"👇","tags":["backhand","backhand index pointing down","finger","down","hand","point"]},{"emoji":"☝️","tags":["finger","hand","index pointing up","index","point","up"]},{"emoji":"👍","tags":["+1","hand","thumbs up","thumb","up"]},{"emoji":"👎","tags":["-1","down","thumb","hand","thumbs down"]},{"emoji":"✊","tags":["clenched","fist","punch","hand","raised fist"]},{"emoji":"👊","tags":["clenched","fist","oncoming fist","hand","punch"]},{"emoji":"🤛","tags":["fist","left-facing fist","bump","leftwards"]},{"emoji":"🤜","tags":["fist","right-facing fist","bump","rightwards"]},{"emoji":"👏","tags":["clap","clapping hands","hand"]},{"emoji":"🙌","tags":["celebration","gesture","hooray","hand","raised","raising hands"]},{"emoji":"👐","tags":["hand","open","open hands"]},{"emoji":"🤲","tags":["palms up together","prayer"]},{"emoji":"🤝","tags":["agreement","hand","meeting","handshake","shake"]},{"emoji":"🙏","tags":["ask","folded hands","high 5","hand","high five","please","pray","thanks"]},{"emoji":"✍️","tags":["hand","write","writing hand"]},{"emoji":"💅","tags":["care","cosmetics","nail","manicure","polish"]},{"emoji":"🤳","tags":["camera","phone","selfie"]},{"emoji":"💪","tags":["biceps","comic","flexed biceps","flex","muscle","strong"]},{"emoji":"🦾","tags":["accessibility","mechanical arm","metal","prosthetic","strong"]},{"emoji":"🦿","tags":["accessibility","mechanical leg","metal","prosthetic"]},{"emoji":"🦵","tags":["kick","leg","limb"]},{"emoji":"🦶","tags":["foot","kick","stomp"]},{"emoji":"👂","tags":["body","ear"]},{"emoji":"🦻","tags":["accessibility","ear with hearing aid","hard of hearing"]},{"emoji":"👃","tags":["body","nose"]},{"emoji":"🧠","tags":["brain","intelligent"]},{"emoji":"🫀","tags":["anatomical","cardiology","organ","heart","pulse"]},{"emoji":"🫁","tags":["breath","exhalation","lungs","inhalation","organ","respiration"]},{"emoji":"🦷","tags":["dentist","tooth"]},{"emoji":"🦴","tags":["bone","skeleton"]},{"emoji":"👀","tags":["eye","eyes","look","stare"]},{"emoji":"👁️","tags":["body","eye","stare"]},{"emoji":"👅","tags":["body","tongue"]},{"emoji":"👄","tags":["lips","mouth"]},{"emoji":"👶","tags":["baby","young"]},{"emoji":"🧒","tags":["child","gender-neutral","young","unspecified gender"]},{"emoji":"👦","tags":["boy","young"]},{"emoji":"👧","tags":["girl","Virgo","zodiac","young"]},{"emoji":"🧑","tags":["adult","gender-neutral","unspecified gender","person"]},{"emoji":"👱","tags":["blond","blond-haired person","person: blond hair","hair"]},{"emoji":"👨","tags":["adult","man","male"]},{"emoji":"🧔","tags":["beard","person","person: beard"]},{"emoji":"🧔‍♂️","tags":["beard","man","male","man: beard"]},{"emoji":"👱‍♂️","tags":["blond","blond-haired man","man","hair","man: blond hair","male"]},{"emoji":"👩","tags":["adult","woman","female"]},{"emoji":"🧔‍♀️","tags":["beard","woman","female","woman: beard"]},{"emoji":"👱‍♀️","tags":["blond-haired woman","blonde","woman","hair","woman: blond hair","female"]},{"emoji":"🧓","tags":["adult","gender-neutral","older person","old","unspecified gender"]},{"emoji":"👴","tags":["adult","man","male","old"]},{"emoji":"👵","tags":["adult","old","female","woman"]},{"emoji":"🙍","tags":["frown","gesture","male","person frowning"]},{"emoji":"🙍‍♂️","tags":["frowning","gesture","male","man"]},{"emoji":"🙍‍♀️","tags":["frowning","gesture","female","woman"]},{"emoji":"🙎","tags":["gesture","person pouting","pouting"]},{"emoji":"🙎‍♂️","tags":["gesture","man","male","pouting"]},{"emoji":"🙎‍♀️","tags":["gesture","pouting","female","woman"]},{"emoji":"🙅","tags":["forbidden","gesture","person gesturing NO","hand","prohibited","X","no"]},{"emoji":"🙅‍♂️","tags":["forbidden","gesture","man","hand","man gesturing NO","prohibited","X","no"]},{"emoji":"🙅‍♀️","tags":["forbidden","gesture","prohibited","hand","woman","woman gesturing NO","X","no"]},{"emoji":"🙆","tags":["gesture","hand","person gesturing OK","OK"]},{"emoji":"🙆‍♂️","tags":["gesture","hand","man gesturing OK","man","OK","male"]},{"emoji":"🙆‍♀️","tags":["gesture","hand","woman","OK","woman gesturing OK","female"]},{"emoji":"💁","tags":["hand","help","person tipping hand","information","sassy","tipping"]},{"emoji":"💁‍♂️","tags":["man","man tipping hand","tipping hand","sassy","male"]},{"emoji":"💁‍♀️","tags":["sassy","tipping hand","woman tipping hand","woman","female"]},{"emoji":"🙋","tags":["gesture","hand","person raising hand","raised","hi","hey","hello"]},{"emoji":"🙋‍♂️","tags":["gesture","man","raising hand","man raising hand","male","hi","hey","hello"]},{"emoji":"🙋‍♀️","tags":["gesture","raising hand","woman raising hand","woman","female","hi","hey","hello"]},{"emoji":"🧏","tags":["accessibility","deaf","ear","deaf person","hear"]},{"emoji":"🧏‍♂️","tags":["deaf","man","male","ear","hear"]},{"emoji":"🧏‍♀️","tags":["deaf","woman","female","ear","hear"]},{"emoji":"🙇","tags":["apology","bow","person bowing","gesture","sorry"]},{"emoji":"🙇‍♂️","tags":["apology","bowing","gesture","favor","man","sorry","male"]},{"emoji":"🙇‍♀️","tags":["apology","bowing","gesture","favor","sorry","woman","female"]},{"emoji":"🤦","tags":["disbelief","exasperation","palm","face","person facepalming"]},{"emoji":"🤦‍♂️","tags":["disbelief","exasperation","man","facepalm","man facepalming","male"]},{"emoji":"🤦‍♀️","tags":["disbelief","exasperation","woman","facepalm","woman facepalming","female"]},{"emoji":"🤷","tags":["doubt","ignorance","person shrugging","indifference","shrug"]},{"emoji":"🤷‍♂️","tags":["doubt","ignorance","man","indifference","man shrugging","shrug","male","idk","don't know"]},{"emoji":"🤷‍♀️","tags":["doubt","ignorance","shrug","indifference","woman","woman shrugging","female","idk","don't know"]},{"emoji":"🧑‍⚕️","tags":["doctor","health worker","nurse","healthcare","therapist"]},{"emoji":"👨‍⚕️","tags":["doctor","healthcare","man health worker","man","nurse","therapist","male"]},{"emoji":"👩‍⚕️","tags":["doctor","healthcare","therapist","nurse","woman","woman health worker","female"]},{"emoji":"🧑‍🎓","tags":["graduate","student"]},{"emoji":"👨‍🎓","tags":["graduate","man","male","student"]},{"emoji":"👩‍🎓","tags":["graduate","student","female","woman"]},{"emoji":"🧑‍🏫","tags":["instructor","professor","teacher"]},{"emoji":"👨‍🏫","tags":["instructor","man","teacher","professor","male"]},{"emoji":"👩‍🏫","tags":["instructor","professor","woman","teacher","female"]},{"emoji":"🧑‍⚖️","tags":["judge","justice","scales","law","court"]},{"emoji":"👨‍⚖️","tags":["judge","justice","scales","man","male","law","court"]},{"emoji":"👩‍⚖️","tags":["judge","justice","woman","scales","female","law","court"]},{"emoji":"🧑‍🌾","tags":["farmer","gardener","rancher"]},{"emoji":"👨‍🌾","tags":["farmer","gardener","rancher","man","male"]},{"emoji":"👩‍🌾","tags":["farmer","gardener","woman","rancher","female"]},{"emoji":"🧑‍🍳","tags":["chef","cook"]},{"emoji":"👨‍🍳","tags":["chef","cook","man","male"]},{"emoji":"👩‍🍳","tags":["chef","cook","woman","female"]},{"emoji":"🧑‍🔧","tags":["electrician","mechanic","tradesperson","plumber"]},{"emoji":"👨‍🔧","tags":["electrician","man","plumber","mechanic","tradesperson","male"]},{"emoji":"👩‍🔧","tags":["electrician","mechanic","tradesperson","plumber","woman","female"]},{"emoji":"🧑‍🏭","tags":["assembly","factory","worker","industrial"]},{"emoji":"👨‍🏭","tags":["assembly","factory","man","industrial","worker","male"]},{"emoji":"👩‍🏭","tags":["assembly","factory","woman","industrial","worker","female"]},{"emoji":"🧑‍💼","tags":["architect","business","office worker","manager","white-collar"]},{"emoji":"👨‍💼","tags":["architect","business","man office worker","man","manager","white-collar","male"]},{"emoji":"👩‍💼","tags":["architect","business","white-collar","manager","woman","woman office worker","female"]},{"emoji":"🧑‍🔬","tags":["biologist","chemist","physicist","engineer","scientist"]},{"emoji":"👨‍🔬","tags":["biologist","chemist","man","engineer","physicist","scientist","male"]},{"emoji":"👩‍🔬","tags":["biologist","chemist","physicist","engineer","scientist","woman","female"]},{"emoji":"🧑‍💻","tags":["coder","developer","software","inventor","technologist"]},{"emoji":"👨‍💻","tags":["coder","developer","man","inventor","software","technologist","male"]},{"emoji":"👩‍💻","tags":["coder","developer","software","inventor","technologist","woman","female"]},{"emoji":"🧑‍🎤","tags":["actor","entertainer","singer","rock","star"]},{"emoji":"👨‍🎤","tags":["actor","entertainer","rock","man","singer","star","male"]},{"emoji":"👩‍🎤","tags":["actor","entertainer","singer","rock","star","woman","female"]},{"emoji":"🧑‍🎨","tags":["artist","palette"]},{"emoji":"👨‍🎨","tags":["artist","man","male","palette"]},{"emoji":"👩‍🎨","tags":["artist","palette","female","woman"]},{"emoji":"🧑‍✈️","tags":["pilot"]},{"emoji":"👨‍✈️","tags":["pilot","male","man"]},{"emoji":"👩‍✈️","tags":["pilot","female","woman"]},{"emoji":"🧑‍🚀","tags":["astronaut","rocket"]},{"emoji":"👨‍🚀","tags":["astronaut","man","male","rocket"]},{"emoji":"👩‍🚀","tags":["astronaut","rocket","female","woman"]},{"emoji":"🧑‍🚒","tags":["firefighter","firetruck"]},{"emoji":"👨‍🚒","tags":["firefighter","firetruck","male","man"]},{"emoji":"👩‍🚒","tags":["firefighter","firetruck","female","woman"]},{"emoji":"👮","tags":["cop","officer","police"]},{"emoji":"👮‍♂️","tags":["cop","man","police","officer","male"]},{"emoji":"👮‍♀️","tags":["cop","officer","woman","police","female"]},{"emoji":"🕵️","tags":["detective","sleuth","spy"]},{"emoji":"💂","tags":["guard"]},{"emoji":"💂‍♂️","tags":["guard","man","male"]},{"emoji":"💂‍♀️","tags":["guard","woman","female"]},{"emoji":"🥷","tags":["fighter","hidden","stealth","ninja"]},{"emoji":"👷","tags":["construction","hat","worker"]},{"emoji":"👷‍♂️","tags":["construction","man","male","worker"]},{"emoji":"👷‍♀️","tags":["construction","woman","female","worker"]},{"emoji":"🤴","tags":["prince","king","royal"]},{"emoji":"👸","tags":["fairy tale","fantasy","princess","queen","royal"]},{"emoji":"👳","tags":["person wearing turban","turban"]},{"emoji":"👳‍♂️","tags":["man","man wearing turban","male","turban"]},{"emoji":"👳‍♀️","tags":["turban","woman","female","woman wearing turban"]},{"emoji":"👲","tags":["cap","gua pi mao","person","hat","person with skullcap","skullcap"]},{"emoji":"🧕","tags":["headscarf","hijab","tichel","mantilla","woman with headscarf"]},{"emoji":"🤵","tags":["groom","person","tuxedo","person in tuxedo","wedding"]},{"emoji":"🤵‍♂️","tags":["man","man in tuxedo","male","tuxedo","groom","wedding"]},{"emoji":"🤵‍♀️","tags":["tuxedo","woman","female","woman in tuxedo","groom","wedding"]},{"emoji":"👰","tags":["bride","person","veil","person with veil","wedding"]},{"emoji":"👰‍♂️","tags":["man","man with veil","male","veil","bride"]},{"emoji":"👰‍♀️","tags":["veil","woman","female","woman with veil","bride"]},{"emoji":"🤰","tags":["pregnant","woman","female","baby"]},{"emoji":"🤱","tags":["baby","breast","nursing","breast-feeding"]},{"emoji":"👩‍🍼","tags":["baby","feeding","woman","nursing","female"]},{"emoji":"👨‍🍼","tags":["baby","feeding","nursing","man","male"]},{"emoji":"🧑‍🍼","tags":["baby","feeding","person","nursing"]},{"emoji":"👼","tags":["angel","baby","fairy tale","face","fantasy"]},{"emoji":"🎅","tags":["celebration","Christmas","father","claus","santa","Santa Claus","male"]},{"emoji":"🤶","tags":["celebration","Christmas","mother","claus","Mrs.","Mrs. Claus","female"]},{"emoji":"🧑‍🎄","tags":["Claus, christmas","mx claus"]},{"emoji":"🦸","tags":["good","hero","superhero","heroine","superpower"]},{"emoji":"🦸‍♂️","tags":["good","hero","man superhero","man","superpower","male"]},{"emoji":"🦸‍♀️","tags":["good","hero","superpower","heroine","woman","woman superhero","female"]},{"emoji":"🦹","tags":["criminal","evil","supervillain","superpower","villain"]},{"emoji":"🦹‍♂️","tags":["criminal","evil","man supervillain","man","superpower","villain","male"]},{"emoji":"🦹‍♀️","tags":["criminal","evil","villain","superpower","woman","woman supervillain","female"]},{"emoji":"🧙","tags":["mage","sorcerer","witch","sorceress","wizard"]},{"emoji":"🧙‍♂️","tags":["man mage","sorcerer","male","wizard"]},{"emoji":"🧙‍♀️","tags":["sorceress","witch","female","woman mage","wizard"]},{"emoji":"🧚","tags":["fairy","Oberon","Titania","Puck"]},{"emoji":"🧚‍♂️","tags":["man fairy","Oberon","male","Puck","fairy"]},{"emoji":"🧚‍♀️","tags":["Titania","woman fairy","female","fairy"]},{"emoji":"🧛","tags":["Dracula","undead","vampire"]},{"emoji":"🧛‍♂️","tags":["Dracula","man vampire","male","undead"]},{"emoji":"🧛‍♀️","tags":["undead","woman vampire","female","Dracula"]},{"emoji":"🧜","tags":["mermaid","merman","merwoman","merperson"]},{"emoji":"🧜‍♂️","tags":["merman","Triton","male"]},{"emoji":"🧜‍♀️","tags":["mermaid","merwoman","female"]},{"emoji":"🧝","tags":["elf","magical"]},{"emoji":"🧝‍♂️","tags":["magical","man elf","male"]},{"emoji":"🧝‍♀️","tags":["magical","woman elf","female"]},{"emoji":"🧞","tags":["djinn","genie"]},{"emoji":"🧞‍♂️","tags":["djinn","man genie","male"]},{"emoji":"🧞‍♀️","tags":["djinn","woman genie","female"]},{"emoji":"🧟","tags":["undead","walking dead","zombie"]},{"emoji":"🧟‍♂️","tags":["man zombie","undead","male","walking dead"]},{"emoji":"🧟‍♀️","tags":["undead","walking dead","female","woman zombie"]},{"emoji":"💆","tags":["face","massage","salon","person getting massage"]},{"emoji":"💆‍♂️","tags":["face","man","massage","man getting massage","male"]},{"emoji":"💆‍♀️","tags":["face","massage","woman getting massage","woman","female"]},{"emoji":"💇","tags":["barber","beauty","parlor","haircut","person getting haircut"]},{"emoji":"💇‍♂️","tags":["haircut","man","male","man getting haircut"]},{"emoji":"💇‍♀️","tags":["haircut","woman","female","woman getting haircut"]},{"emoji":"🚶","tags":["hike","person walking","walking","walk"]},{"emoji":"🚶‍♂️","tags":["hike","man","walk","man walking","male"]},{"emoji":"🚶‍♀️","tags":["hike","walk","woman walking","woman","female"]},{"emoji":"🧍","tags":["person standing","stand","standing"]},{"emoji":"🧍‍♂️","tags":["man","standing","male"]},{"emoji":"🧍‍♀️","tags":["standing","woman","female"]},{"emoji":"🧎","tags":["kneel","kneeling","person kneeling"]},{"emoji":"🧎‍♂️","tags":["kneeling","man","male"]},{"emoji":"🧎‍♀️","tags":["kneeling","woman","female"]},{"emoji":"🧑‍🦯","tags":["accessibility","blind","person with white cane"]},{"emoji":"👨‍🦯","tags":["accessibility","blind","man with white cane","man","male"]},{"emoji":"👩‍🦯","tags":["accessibility","blind","woman with white cane","woman","female"]},{"emoji":"🧑‍🦼","tags":["accessibility","person in motorized wheelchair","wheelchair"]},{"emoji":"👨‍🦼","tags":["accessibility","man","wheelchair","man in motorized wheelchair","male"]},{"emoji":"👩‍🦼","tags":["accessibility","wheelchair","woman in motorized wheelchair","woman","female"]},{"emoji":"🧑‍🦽","tags":["accessibility","person in manual wheelchair","wheelchair"]},{"emoji":"👨‍🦽","tags":["accessibility","man","wheelchair","man in manual wheelchair","male"]},{"emoji":"👩‍🦽","tags":["accessibility","wheelchair","woman in manual wheelchair","woman","female"]},{"emoji":"🏃","tags":["marathon","person running","running"]},{"emoji":"🏃‍♂️","tags":["man","marathon","running","racing","male"]},{"emoji":"🏃‍♀️","tags":["marathon","racing","woman","running","female"]},{"emoji":"💃","tags":["dance","dancing","female","woman"]},{"emoji":"🕺","tags":["dance","dancing","male","man"]},{"emoji":"🕴️","tags":["business","person","suit","person in suit levitating"]},{"emoji":"👯","tags":["bunny ear","dancer","people with bunny ears","partying"]},{"emoji":"👯‍♂️","tags":["bunny ear","dancer","men with bunny ears","men","partying"]},{"emoji":"👯‍♀️","tags":["bunny ear","dancer","women","partying","women with bunny ears"]},{"emoji":"🧖","tags":["person in steamy room","sauna","steam room","spa"]},{"emoji":"🧖‍♂️","tags":["man in steamy room","sauna","male","steam room","spa"]},{"emoji":"🧖‍♀️","tags":["sauna","steam room","female","woman in steamy room","spa"]},{"emoji":"🧗","tags":["climber","person climbing"]},{"emoji":"🧗‍♂️","tags":["climber","man climbing","male"]},{"emoji":"🧗‍♀️","tags":["climber","woman climbing","female"]},{"emoji":"🤺","tags":["fencer","fencing","sword","person fencing"]},{"emoji":"🏇","tags":["horse","jockey","racing","racehorse"]},{"emoji":"⛷️","tags":["ski","skier","snow"]},{"emoji":"🏂","tags":["ski","snow","snowboarder","snowboard"]},{"emoji":"🏌️","tags":["ball","golf","person golfing"]},{"emoji":"🏄","tags":["person surfing","surfing"]},{"emoji":"🏄‍♂️","tags":["man","surfing","male"]},{"emoji":"🏄‍♀️","tags":["surfing","woman","female"]},{"emoji":"🚣","tags":["boat","person rowing boat","rowboat"]},{"emoji":"🚣‍♂️","tags":["boat","man","rowboat","man rowing boat","male"]},{"emoji":"🚣‍♀️","tags":["boat","rowboat","woman rowing boat","woman","female"]},{"emoji":"🏊","tags":["person swimming","swim","swimmer"]},{"emoji":"🏊‍♂️","tags":["man","man swimming","male","swim","swimmer"]},{"emoji":"🏊‍♀️","tags":["swim","woman","female","woman swimming","swimmer"]},{"emoji":"⛹️","tags":["ball","person bouncing ball"]},{"emoji":"🏋️","tags":["lifter","person lifting weights","weight"]},{"emoji":"🚴","tags":["bicycle","biking","person biking","cyclist"]},{"emoji":"🚴‍♂️","tags":["bicycle","biking","man","cyclist","male"]},{"emoji":"🚴‍♀️","tags":["bicycle","biking","woman","cyclist","female"]},{"emoji":"🚵","tags":["bicycle","bicyclist","cyclist","bike","mountain","person mountain biking"]},{"emoji":"🚵‍♂️","tags":["bicycle","bike","man","cyclist","man mountain biking","mountain","male"]},{"emoji":"🚵‍♀️","tags":["bicycle","bike","cyclist","biking","mountain","woman","female"]},{"emoji":"🤸","tags":["cartwheel","gymnastics","person cartwheeling"]},{"emoji":"🤸‍♂️","tags":["cartwheel","gymnastics","man cartwheeling","man","male"]},{"emoji":"🤸‍♀️","tags":["cartwheel","gymnastics","woman cartwheeling","woman","female"]},{"emoji":"🤼","tags":["people wrestling","wrestle","wrestler"]},{"emoji":"🤼‍♂️","tags":["men","men wrestling","wrestle"]},{"emoji":"🤼‍♀️","tags":["women","women wrestling","wrestle"]},{"emoji":"🤽","tags":["person playing water polo","polo","water"]},{"emoji":"🤽‍♂️","tags":["man","man playing water polo","male","water polo"]},{"emoji":"🤽‍♀️","tags":["water polo","woman","female","woman playing water polo"]},{"emoji":"🤾","tags":["ball","handball","person playing handball"]},{"emoji":"🤾‍♂️","tags":["handball","man","male","man playing handball"]},{"emoji":"🤾‍♀️","tags":["handball","woman","female","woman playing handball"]},{"emoji":"🤹","tags":["balance","juggle","person juggling","multitask","skill"]},{"emoji":"🤹‍♂️","tags":["juggling","man","male","multitask"]},{"emoji":"🤹‍♀️","tags":["juggling","multitask","female","woman"]},{"emoji":"🧘","tags":["meditation","person in lotus position","yoga","meditate"]},{"emoji":"🧘‍♂️","tags":["man in lotus position","meditation","male","yoga","meditate"]},{"emoji":"🧘‍♀️","tags":["meditation","woman in lotus position","female","yoga","meditate"]},{"emoji":"🛀","tags":["bath","bathtub","person taking bath","bathing"]},{"emoji":"🛌","tags":["hotel","person in bed","sleep","bed","sleeping"]},{"emoji":"🧑‍🤝‍🧑","tags":["couple","hand","holding hands","hold","people holding hands","person"]},{"emoji":"👭","tags":["couple","hand","women","holding hands","women holding hands","female"]},{"emoji":"👫","tags":["couple","hand","holding hands","hold","man","woman","woman and man holding hands","female","male"]},{"emoji":"👬","tags":["couple","Gemini","man","holding hands","men","men holding hands","twins","zodiac","male"]},{"emoji":"💏","tags":["couple","kiss"]},{"emoji":"💑","tags":["couple","couple with heart","love"]},{"emoji":"👪","tags":["family"]},{"emoji":"🗣️","tags":["face","head","speak","silhouette","speaking","shadow","shout"]},{"emoji":"👤","tags":["bust","bust in silhouette","silhouette","shadow"]},{"emoji":"👥","tags":["bust","busts in silhouette","silhouette","shadow"]},{"emoji":"🫂","tags":["goodbye","hello","people hugging","hug","thanks","shadow"]},{"emoji":"👣","tags":["clothing","footprint","print","footprints"]},{"emoji":"🐵","tags":["face","monkey"]},{"emoji":"🐒","tags":["monkey"]},{"emoji":"🦍","tags":["gorilla"]},{"emoji":"🦧","tags":["ape","orangutan"]},{"emoji":"🐶","tags":["dog","face","pet","puppy"]},{"emoji":"🐕","tags":["dog","pet","puppy"]},{"emoji":"🦮","tags":["accessibility","blind","guide dog","guide","dog","assistance","service"]},{"emoji":"🐕‍🦺","tags":["accessibility","assistance","service","dog"]},{"emoji":"🐩","tags":["dog","poodle"]},{"emoji":"🐺","tags":["face","wolf"]},{"emoji":"🦊","tags":["face","fox"]},{"emoji":"🦝","tags":["curious","raccoon","sly"]},{"emoji":"🐱","tags":["cat","face","pet","kitten","kitty"]},{"emoji":"🐈","tags":["cat","pet","kitten","kitty"]},{"emoji":"🐈‍⬛","tags":["black","cat","unlucky","kitty","kitten"]},{"emoji":"🦁","tags":["face","Leo","zodiac","lion"]},{"emoji":"🐯","tags":["face","tiger"]},{"emoji":"🐅","tags":["tiger"]},{"emoji":"🐆","tags":["leopard","cheetah","jaguar","panther"]},{"emoji":"🐴","tags":["face","horse"]},{"emoji":"🐎","tags":["equestrian","horse","racing","racehorse"]},{"emoji":"🦄","tags":["face","unicorn"]},{"emoji":"🦓","tags":["stripe","zebra"]},{"emoji":"🦌","tags":["deer","elk","moose","antelope","reindeer"]},{"emoji":"🦬","tags":["bison","buffalo","wisent","herd"]},{"emoji":"🐮","tags":["cow","face","moo"]},{"emoji":"🐂","tags":["bull","ox","zodiac","Taurus","cow"]},{"emoji":"🐃","tags":["buffalo","water"]},{"emoji":"🐄","tags":["cow","moo"]},{"emoji":"🐷","tags":["face","pig","piggy"]},{"emoji":"🐖","tags":["pig","sow","piggy"]},{"emoji":"🐗","tags":["boar","pig","piggy"]},{"emoji":"🐽","tags":["face","nose","pig","piggy"]},{"emoji":"🐏","tags":["Aries","male","sheep","ram","zodiac"]},{"emoji":"🐑","tags":["ewe","female","sheep"]},{"emoji":"🐐","tags":["Capricorn","goat","zodiac"]},{"emoji":"🐪","tags":["camel","dromedary","hump","desert"]},{"emoji":"🐫","tags":["bactrian","camel","two-hump camel","hump","desert"]},{"emoji":"🦙","tags":["alpaca","guanaco","vicuña","llama","wool"]},{"emoji":"🦒","tags":["giraffe","spots"]},{"emoji":"🐘","tags":["elephant"]},{"emoji":"🦣","tags":["extinction","large","tusk","mammoth","woolly"]},{"emoji":"🦏","tags":["rhinoceros","rhino"]},{"emoji":"🦛","tags":["hippo","hippopotamus"]},{"emoji":"🐭","tags":["face","mouse"]},{"emoji":"🐁","tags":["mouse"]},{"emoji":"🐀","tags":["rat"]},{"emoji":"🐹","tags":["face","hamster","pet","gerbil","chinchilla","guinea pig"]},{"emoji":"🐰","tags":["bunny","face","rabbit","pet"]},{"emoji":"🐇","tags":["bunny","pet","rabbit"]},{"emoji":"🐿️","tags":["chipmunk","squirrel","nuts","acorn"]},{"emoji":"🦫","tags":["beaver","dam"]},{"emoji":"🦔","tags":["hedgehog","spiny"]},{"emoji":"🦇","tags":["bat","vampire"]},{"emoji":"🐻","tags":["bear","face"]},{"emoji":"🐻‍❄️","tags":["arctic","bear","white","polar bear"]},{"emoji":"🐨","tags":["bear","koala"]},{"emoji":"🐼","tags":["face","panda","bear"]},{"emoji":"🦥","tags":["lazy","sloth","slow"]},{"emoji":"🦦","tags":["fishing","otter","playful"]},{"emoji":"🦨","tags":["skunk","stink"]},{"emoji":"🦘","tags":["Australia","joey","kangaroo","jump","marsupial"]},{"emoji":"🦡","tags":["badger","honey badger","pester"]},{"emoji":"🐾","tags":["feet","paw","print","paw prints"]},{"emoji":"🦃","tags":["bird","turkey"]},{"emoji":"🐔","tags":["bird","chicken","hen"]},{"emoji":"🐓","tags":["bird","rooster","hen","chicken"]},{"emoji":"🐣","tags":["baby","bird","hatching","chick"]},{"emoji":"🐤","tags":["baby","bird","chick"]},{"emoji":"🐥","tags":["baby","bird","front-facing baby chick","chick"]},{"emoji":"🐦","tags":["bird"]},{"emoji":"🐧","tags":["bird","penguin"]},{"emoji":"🕊️","tags":["bird","dove","peace","fly"]},{"emoji":"🦅","tags":["bird","eagle"]},{"emoji":"🦆","tags":["bird","duck"]},{"emoji":"🦢","tags":["bird","cygnet","ugly duckling","swan"]},{"emoji":"🦉","tags":["bird","owl","wise"]},{"emoji":"🦤","tags":["dodo","extinction","Mauritius","large"]},{"emoji":"🪶","tags":["bird","feather","light","flight","plumage"]},{"emoji":"🦩","tags":["flamboyant","flamingo","tropical"]},{"emoji":"🦚","tags":["bird","ostentatious","peahen","peacock","proud"]},{"emoji":"🦜","tags":["bird","parrot","talk","pirate"]},{"emoji":"🐸","tags":["face","frog"]},{"emoji":"🐊","tags":["crocodile"]},{"emoji":"🐢","tags":["terrapin","tortoise","turtle"]},{"emoji":"🦎","tags":["lizard","reptile"]},{"emoji":"🐍","tags":["bearer","Ophiuchus","snake","serpent","zodiac"]},{"emoji":"🐲","tags":["dragon","face","fairy tale"]},{"emoji":"🐉","tags":["dragon","fairy tale"]},{"emoji":"🦕","tags":["brachiosaurus","brontosaurus","sauropod","diplodocus","dinosaur"]},{"emoji":"🦖","tags":["T-Rex","Tyrannosaurus Rex","dinosaur"]},{"emoji":"🐳","tags":["face","spouting","whale"]},{"emoji":"🐋","tags":["whale"]},{"emoji":"🐬","tags":["dolphin","flipper"]},{"emoji":"🦭","tags":["sea lion","seal"]},{"emoji":"🐟","tags":["fish","Pisces","zodiac"]},{"emoji":"🐠","tags":["fish","tropical"]},{"emoji":"🐡","tags":["blowfish","fish"]},{"emoji":"🦈","tags":["fish","shark"]},{"emoji":"🐙","tags":["octopus"]},{"emoji":"🐚","tags":["shell","spiral"]},{"emoji":"🐌","tags":["snail"]},{"emoji":"🦋","tags":["butterfly","insect","pretty"]},{"emoji":"🐛","tags":["bug","insect"]},{"emoji":"🐜","tags":["ant","insect"]},{"emoji":"🐝","tags":["bee","honeybee","insect"]},{"emoji":"🪲","tags":["beetle","bug","insect"]},{"emoji":"🐞","tags":["beetle","insect","ladybird","lady beetle","ladybug"]},{"emoji":"🦗","tags":["cricket","grasshopper"]},{"emoji":"🪳","tags":["cockroach","insect","roach","pest"]},{"emoji":"🕷️","tags":["insect","spider"]},{"emoji":"🕸️","tags":["spider","web"]},{"emoji":"🦂","tags":["scorpio","Scorpio","zodiac","scorpion"]},{"emoji":"🦟","tags":["disease","fever","mosquito","malaria","pest","virus"]},{"emoji":"🪰","tags":["disease","fly","pest","maggot","rotting"]},{"emoji":"🪱","tags":["annelid","earthworm","worm","parasite"]},{"emoji":"🦠","tags":["amoeba","bacteria","virus","microbe"]},{"emoji":"💐","tags":["bouquet","flower"]},{"emoji":"🌸","tags":["blossom","cherry","flower"]},{"emoji":"💮","tags":["flower","white flower"]},{"emoji":"🏵️","tags":["plant","rosette"]},{"emoji":"🌹","tags":["flower","rose"]},{"emoji":"🥀","tags":["flower","wilted"]},{"emoji":"🌺","tags":["flower","hibiscus"]},{"emoji":"🌻","tags":["flower","sun","sunflower"]},{"emoji":"🌼","tags":["blossom","flower"]},{"emoji":"🌷","tags":["flower","tulip"]},{"emoji":"🌱","tags":["seedling","young"]},{"emoji":"🪴","tags":["boring","grow","nurturing","house","plant","potted plant","useless"]},{"emoji":"🌲","tags":["evergreen tree","tree"]},{"emoji":"🌳","tags":["deciduous","shedding","tree"]},{"emoji":"🌴","tags":["palm","tree"]},{"emoji":"🌵","tags":["cactus","plant"]},{"emoji":"🌾","tags":["ear","grain","sheaf of rice","rice"]},{"emoji":"🌿","tags":["herb","leaf"]},{"emoji":"☘️","tags":["clover","shamrock","three-leaf clover","three","leaf"]},{"emoji":"🍀","tags":["4","clover","four-leaf clover","four","leaf"]},{"emoji":"🍁","tags":["falling","leaf","maple"]},{"emoji":"🍂","tags":["fallen leaf","falling","leaf"]},{"emoji":"🍃","tags":["blow","flutter","leaf fluttering in wind","leaf","wind"]},{"emoji":"🍇","tags":["fruit","grape","grapes"]},{"emoji":"🍈","tags":["fruit","melon"]},{"emoji":"🍉","tags":["fruit","watermelon"]},{"emoji":"🍊","tags":["fruit","orange","tangerine"]},{"emoji":"🍋","tags":["citrus","fruit","lemon"]},{"emoji":"🍌","tags":["banana","fruit"]},{"emoji":"🍍","tags":["fruit","pineapple"]},{"emoji":"🥭","tags":["fruit","mango","tropical"]},{"emoji":"🍎","tags":["apple","fruit","red"]},{"emoji":"🍏","tags":["apple","fruit","green"]},{"emoji":"🍐","tags":["fruit","pear"]},{"emoji":"🍑","tags":["fruit","peach"]},{"emoji":"🍒","tags":["berries","cherries","fruit","cherry","red"]},{"emoji":"🍓","tags":["berry","fruit","strawberry"]},{"emoji":"🫐","tags":["berry","bilberry","blueberries","blue","blueberry"]},{"emoji":"🥝","tags":["food","fruit","kiwi"]},{"emoji":"🍅","tags":["fruit","tomato","vegetable"]},{"emoji":"🫒","tags":["food","olive"]},{"emoji":"🥥","tags":["coconut","palm","piña colada"]},{"emoji":"🥑","tags":["avocado","food","fruit"]},{"emoji":"🍆","tags":["aubergine","eggplant","vegetable"]},{"emoji":"🥔","tags":["food","potato","vegetable"]},{"emoji":"🥕","tags":["carrot","food","vegetable"]},{"emoji":"🌽","tags":["corn","ear","maize","ear of corn","maze"]},{"emoji":"🌶️","tags":["hot","pepper"]},{"emoji":"🫑","tags":["bell pepper","capsicum","vegetable","pepper"]},{"emoji":"🥒","tags":["cucumber","food","vegetable","pickle"]},{"emoji":"🥬","tags":["bok choy","cabbage","leafy green","kale","lettuce"]},{"emoji":"🥦","tags":["broccoli","wild cabbage"]},{"emoji":"🧄","tags":["flavoring","garlic"]},{"emoji":"🧅","tags":["flavoring","onion"]},{"emoji":"🍄","tags":["mushroom","toadstool"]},{"emoji":"🥜","tags":["food","nut","peanuts","peanut","vegetable"]},{"emoji":"🌰","tags":["chestnut","plant"]},{"emoji":"🍞","tags":["bread","loaf"]},{"emoji":"🥐","tags":["bread","breakfast","food","croissant","french","roll"]},{"emoji":"🥖","tags":["baguette","bread","french","food"]},{"emoji":"🫓","tags":["arepa","flatbread","naan","lavash","pita"]},{"emoji":"🥨","tags":["pretzel","twisted"]},{"emoji":"🥯","tags":["bagel","bakery","schmear","breakfast"]},{"emoji":"🥞","tags":["breakfast","crêpe","hotcake","food","pancake","pancakes"]},{"emoji":"🧇","tags":["breakfast","indecisive","waffle","iron"]},{"emoji":"🧀","tags":["cheese","cheese wedge"]},{"emoji":"🍖","tags":["bone","meat","meat on bone"]},{"emoji":"🍗","tags":["bone","chicken","leg","drumstick","poultry"]},{"emoji":"🥩","tags":["chop","cut of meat","porkchop","lambchop","steak"]},{"emoji":"🥓","tags":["bacon","breakfast","meat","food"]},{"emoji":"🍔","tags":["burger","hamburger"]},{"emoji":"🍟","tags":["french","fries"]},{"emoji":"🍕","tags":["cheese","pizza","slice"]},{"emoji":"🌭","tags":["frankfurter","hot dog","sausage","hotdog"]},{"emoji":"🥪","tags":["bread","sandwich"]},{"emoji":"🌮","tags":["mexican","taco"]},{"emoji":"🌯","tags":["burrito","mexican","wrap"]},{"emoji":"🫔","tags":["mexican","tamale","wrapped"]},{"emoji":"🥙","tags":["falafel","flatbread","gyro","food","kebab","stuffed","pita"]},{"emoji":"🧆","tags":["chickpea","falafel","meatball"]},{"emoji":"🥚","tags":["breakfast","egg","food"]},{"emoji":"🍳","tags":["breakfast","cooking","frying","egg","pan"]},{"emoji":"🥘","tags":["casserole","food","pan","paella","shallow","shallow pan of food"]},{"emoji":"🍲","tags":["pot","pot of food","stew"]},{"emoji":"🫕","tags":["cheese","chocolate","melted","fondue","pot","Swiss"]},{"emoji":"🥣","tags":["bowl with spoon","breakfast","congee","cereal"]},{"emoji":"🥗","tags":["food","green","salad"]},{"emoji":"🍿","tags":["popcorn"]},{"emoji":"🧈","tags":["butter","dairy"]},{"emoji":"🧂","tags":["condiment","salt","shaker"]},{"emoji":"🥫","tags":["can","canned food"]},{"emoji":"🍱","tags":["bento","box"]},{"emoji":"🍘","tags":["cracker","rice"]},{"emoji":"🍙","tags":["ball","Japanese","rice"]},{"emoji":"🍚","tags":["cooked","rice"]},{"emoji":"🍛","tags":["curry","rice"]},{"emoji":"🍜","tags":["bowl","noodle","steaming","ramen"]},{"emoji":"🍝","tags":["pasta","spaghetti"]},{"emoji":"🍠","tags":["potato","roasted","sweet"]},{"emoji":"🍢","tags":["kebab","oden","skewer","seafood","stick"]},{"emoji":"🍣","tags":["sushi"]},{"emoji":"🍤","tags":["fried","prawn","tempura","shrimp"]},{"emoji":"🍥","tags":["cake","fish","pastry","fish cake with swirl","swirl"]},{"emoji":"🥮","tags":["autumn","festival","yuèbǐng","moon cake"]},{"emoji":"🍡","tags":["dango","dessert","skewer","Japanese","stick","sweet"]},{"emoji":"🥟","tags":["dumpling","empanada","jiaozi","gyōza","pierogi","potsticker"]},{"emoji":"🥠","tags":["fortune cookie","prophecy"]},{"emoji":"🥡","tags":["oyster pail","takeout box"]},{"emoji":"🦀","tags":["Cancer","crab","zodiac"]},{"emoji":"🦞","tags":["bisque","claws","seafood","lobster"]},{"emoji":"🦐","tags":["food","shellfish","small","shrimp"]},{"emoji":"🦑","tags":["food","molusc","squid"]},{"emoji":"🦪","tags":["diving","oyster","pearl"]},{"emoji":"🍦","tags":["cream","dessert","icecream","ice","soft","sweet"]},{"emoji":"🍧","tags":["dessert","ice","sweet","shaved"]},{"emoji":"🍨","tags":["cream","dessert","sweet","ice"]},{"emoji":"🍩","tags":["breakfast","dessert","doughnut","donut","sweet"]},{"emoji":"🍪","tags":["cookie","dessert","chocolate chip","sweet"]},{"emoji":"🎂","tags":["birthday","cake","dessert","celebration","pastry","sweet"]},{"emoji":"🍰","tags":["cake","dessert","shortcake","pastry","slice","sweet"]},{"emoji":"🧁","tags":["bakery","cupcake","sweet"]},{"emoji":"🥧","tags":["filling","pastry","pie"]},{"emoji":"🍫","tags":["bar","chocolate","sweet","dessert"]},{"emoji":"🍬","tags":["candy","dessert","sweet"]},{"emoji":"🍭","tags":["candy","dessert","sweet","lollipop"]},{"emoji":"🍮","tags":["custard","dessert","sweet","pudding"]},{"emoji":"🍯","tags":["honey","honeypot","sweet","pot"]},{"emoji":"🍼","tags":["baby","bottle","milk","drink"]},{"emoji":"🥛","tags":["drink","glass","milk","glass of milk"]},{"emoji":"☕","tags":["beverage","coffee","hot","drink","steaming","tea"]},{"emoji":"🫖","tags":["drink","pot","teapot","tea"]},{"emoji":"🍵","tags":["beverage","cup","tea","drink","teacup","teacup without handle"]},{"emoji":"🍶","tags":["bar","beverage","cup","bottle","drink","sake"]},{"emoji":"🍾","tags":["bar","bottle","cork","bottle with popping cork","drink","popping"]},{"emoji":"🍷","tags":["bar","beverage","glass","drink","wine"]},{"emoji":"🍸","tags":["bar","cocktail","glass","drink"]},{"emoji":"🍹","tags":["bar","drink","tropical"]},{"emoji":"🍺","tags":["bar","beer","mug","drink"]},{"emoji":"🍻","tags":["bar","beer","clinking beer mugs","clink","drink","mug"]},{"emoji":"🥂","tags":["celebrate","clink","drink","clinking glasses","glass"]},{"emoji":"🥃","tags":["glass","liquor","tumbler","shot","whisky"]},{"emoji":"🥤","tags":["cup with straw","juice","soda"]},{"emoji":"🧋","tags":["bubble","milk","tea","pearl"]},{"emoji":"🧃","tags":["beverage","box","straw","juice","sweet"]},{"emoji":"🧉","tags":["drink","mate"]},{"emoji":"🧊","tags":["cold","ice","iceberg","ice cube"]},{"emoji":"🥢","tags":["chopsticks","hashi"]},{"emoji":"🍽️","tags":["cooking","fork","knife","fork and knife with plate","plate"]},{"emoji":"🍴","tags":["cooking","cutlery","fork and knife","fork","knife"]},{"emoji":"🥄","tags":["spoon","tableware"]},{"emoji":"🔪","tags":["cooking","hocho","knife","kitchen knife","tool","weapon"]},{"emoji":"🏺","tags":["amphora","Aquarius","drink","cooking","jug","zodiac"]},{"emoji":"🌍","tags":["Africa","earth","globe","Europe","globe showing Europe-Africa","world"]},{"emoji":"🌎","tags":["Americas","earth","globe showing Americas","globe","world"]},{"emoji":"🌏","tags":["Asia","Australia","globe","earth","globe showing Asia-Australia","world"]},{"emoji":"🌐","tags":["earth","globe","meridians","globe with meridians","world"]},{"emoji":"🗺️","tags":["map","world"]},{"emoji":"🗾","tags":["Japan","map","map of Japan"]},{"emoji":"🧭","tags":["compass","magnetic","orienteering","navigation"]},{"emoji":"🏔️","tags":["cold","mountain","snow-capped mountain","snow"]},{"emoji":"⛰️","tags":["mountain"]},{"emoji":"🌋","tags":["eruption","mountain","volcano"]},{"emoji":"🗻","tags":["fuji","mount fuji","mountain"]},{"emoji":"🏕️","tags":["camping"]},{"emoji":"🏖️","tags":["beach","beach with umbrella","umbrella"]},{"emoji":"🏜️","tags":["desert"]},{"emoji":"🏝️","tags":["desert","island"]},{"emoji":"🏞️","tags":["national park","park"]},{"emoji":"🏟️","tags":["stadium"]},{"emoji":"🏛️","tags":["classical","classical building"]},{"emoji":"🏗️","tags":["building construction","construction"]},{"emoji":"🧱","tags":["brick","bricks","mortar","clay","wall"]},{"emoji":"🪨","tags":["boulder","heavy","solid","rock","stone"]},{"emoji":"🪵","tags":["log","lumber","wood","timber"]},{"emoji":"🛖","tags":["house","hut","yurt","roundhouse"]},{"emoji":"🏘️","tags":["houses"]},{"emoji":"🏚️","tags":["derelict","house"]},{"emoji":"🏠","tags":["home","house"]},{"emoji":"🏡","tags":["garden","home","house with garden","house"]},{"emoji":"🏢","tags":["building","office building"]},{"emoji":"🏣","tags":["Japanese","Japanese post office","post"]},{"emoji":"🏤","tags":["European","post","post office"]},{"emoji":"🏥","tags":["doctor","hospital","medicine"]},{"emoji":"🏦","tags":["bank","building"]},{"emoji":"🏨","tags":["building","hotel"]},{"emoji":"🏩","tags":["hotel","love"]},{"emoji":"🏪","tags":["convenience","store"]},{"emoji":"🏫","tags":["building","school"]},{"emoji":"🏬","tags":["department","store"]},{"emoji":"🏭","tags":["building","factory"]},{"emoji":"🏯","tags":["castle","Japanese"]},{"emoji":"🏰","tags":["castle","European"]},{"emoji":"💒","tags":["chapel","romance","wedding"]},{"emoji":"🗼","tags":["Tokyo","tower"]},{"emoji":"🗽","tags":["liberty","statue","Statue of Liberty"]},{"emoji":"⛪","tags":["Christian","church","religion","cross"]},{"emoji":"🕌","tags":["islam","mosque","religion","Muslim"]},{"emoji":"🛕","tags":["hindu","temple"]},{"emoji":"🕍","tags":["Jew","Jewish","synagogue","religion","temple"]},{"emoji":"⛩️","tags":["religion","shinto","shrine"]},{"emoji":"🕋","tags":["islam","kaaba","religion","Muslim"]},{"emoji":"⛲","tags":["fountain"]},{"emoji":"⛺","tags":["camping","tent"]},{"emoji":"🌁","tags":["fog","foggy"]},{"emoji":"🌃","tags":["night","night with stars","star"]},{"emoji":"🏙️","tags":["city","cityscape"]},{"emoji":"🌄","tags":["morning","mountain","sunrise","sun","sunrise over mountains"]},{"emoji":"🌅","tags":["morning","sun","sunrise"]},{"emoji":"🌆","tags":["city","cityscape at dusk","evening","dusk","landscape","sunset"]},{"emoji":"🌇","tags":["dusk","sun","sunset"]},{"emoji":"🌉","tags":["bridge","bridge at night","night"]},{"emoji":"♨️","tags":["hot springs"]},{"emoji":"🎠","tags":["carousel","horse"]},{"emoji":"🎡","tags":["amusement park","ferris","wheel"]},{"emoji":"🎢","tags":["amusement park","coaster","roller"]},{"emoji":"💈","tags":["barber","haircut","pole"]},{"emoji":"🎪","tags":["circus","tent"]},{"emoji":"🚂","tags":["engine","locomotive","steam","railway","train"]},{"emoji":"🚃","tags":["train car","electric","train","railway","tram","trolleybus"]},{"emoji":"🚄","tags":["high-speed train","railway","speed","shinkansen","train"]},{"emoji":"🚅","tags":["bullet","railway","speed","shinkansen","train"]},{"emoji":"🚆","tags":["railway","train"]},{"emoji":"🚇","tags":["metro","subway"]},{"emoji":"🚈","tags":["light rail","railway"]},{"emoji":"🚉","tags":["railway","station","train"]},{"emoji":"🚊","tags":["tram","trolleybus"]},{"emoji":"🚝","tags":["monorail","vehicle"]},{"emoji":"🚞","tags":["train car","mountain","railway"]},{"emoji":"🚋","tags":["train car","tram","trolleybus"]},{"emoji":"🚌","tags":["bus","vehicle"]},{"emoji":"🚍","tags":["bus","oncoming"]},{"emoji":"🚎","tags":["bus","tram","trolleybus","trolley"]},{"emoji":"🚐","tags":["bus","minibus"]},{"emoji":"🚑","tags":["ambulance","vehicle"]},{"emoji":"🚒","tags":["engine","fire","truck"]},{"emoji":"🚓","tags":["car","patrol","police"]},{"emoji":"🚔","tags":["car","oncoming","police"]},{"emoji":"🚕","tags":["taxi","vehicle"]},{"emoji":"🚖","tags":["oncoming","taxi"]},{"emoji":"🚗","tags":["automobile","car"]},{"emoji":"🚘","tags":["automobile","car","oncoming"]},{"emoji":"🚙","tags":["recreational","sport utility","sport utility vehicle"]},{"emoji":"🛻","tags":["pick-up","pickup","truck"]},{"emoji":"🚚","tags":["delivery","truck"]},{"emoji":"🚛","tags":["articulated lorry","lorry","truck","semi"]},{"emoji":"🚜","tags":["tractor","vehicle"]},{"emoji":"🏎️","tags":["car","racing","race","racecar"]},{"emoji":"🏍️","tags":["motorcycle","racing"]},{"emoji":"🛵","tags":["motor","scooter","moped"]},{"emoji":"🦽","tags":["accessibility","manual wheelchair","wheelchair"]},{"emoji":"🦼","tags":["accessibility","motorized wheelchair","wheelchair"]},{"emoji":"🛺","tags":["auto rickshaw","tuk tuk"]},{"emoji":"🚲","tags":["bicycle","bike"]},{"emoji":"🛴","tags":["kick","scooter"]},{"emoji":"🛹","tags":["board","skateboard"]},{"emoji":"🛼","tags":["roller","skate"]},{"emoji":"🚏","tags":["bus","busstop","stop"]},{"emoji":"🛣️","tags":["highway","motorway","road"]},{"emoji":"🛤️","tags":["railway","railway track","train"]},{"emoji":"🛢️","tags":["drum","oil"]},{"emoji":"⛽","tags":["diesel","fuel","gas","fuelpump","pump","station"]},{"emoji":"🚨","tags":["beacon","police","light","revolving","alarm","alert","emergency","siren"]},{"emoji":"🚥","tags":["horizontal traffic light","light","traffic","signal","stoplight"]},{"emoji":"🚦","tags":["light","signal","vertical traffic light","traffic","stoplight"]},{"emoji":"🛑","tags":["octagonal","sign","stop"]},{"emoji":"🚧","tags":["barrier","construction"]},{"emoji":"⚓","tags":["anchor","ship","tool"]},{"emoji":"⛵","tags":["boat","resort","sea","sailboat","yacht"]},{"emoji":"🛶","tags":["boat","canoe"]},{"emoji":"🚤","tags":["boat","speedboat"]},{"emoji":"🛳️","tags":["passenger","ship"]},{"emoji":"⛴️","tags":["boat","ferry","passenger"]},{"emoji":"🛥️","tags":["boat","motor boat","motorboat"]},{"emoji":"🚢","tags":["boat","passenger","ship"]},{"emoji":"✈️","tags":["aeroplane","airplane","small airplane","plane"]},{"emoji":"🛩️","tags":["aeroplane","airplane","small airplane","plane"]},{"emoji":"🛫","tags":["aeroplane","airplane","departure","check-in","departures","plane"]},{"emoji":"🛬","tags":["aeroplane","airplane","arrivals","airplane arrival","arriving","landing","plane"]},{"emoji":"🪂","tags":["hang-glide","parachute","skydive","parasail"]},{"emoji":"💺","tags":["chair","seat"]},{"emoji":"🚁","tags":["helicopter","vehicle"]},{"emoji":"🚟","tags":["railway","suspension"]},{"emoji":"🚠","tags":["cable","gondola","mountain cableway","mountain"]},{"emoji":"🚡","tags":["aerial","cable","gondola","car","tramway"]},{"emoji":"🛰️","tags":["satellite","space"]},{"emoji":"🚀","tags":["rocket","space","spaceship"]},{"emoji":"🛸","tags":["flying saucer","UFO"]},{"emoji":"🛎️","tags":["bell","bellhop","hotel"]},{"emoji":"🧳","tags":["luggage","packing","travel"]},{"emoji":"⌛","tags":["hourglass done","sand","timer"]},{"emoji":"⏳","tags":["hourglass","hourglass not done","timer","sand"]},{"emoji":"⌚","tags":["clock","watch"]},{"emoji":"⏰","tags":["alarm","clock"]},{"emoji":"⏱️","tags":["clock","stopwatch"]},{"emoji":"⏲️","tags":["clock","timer"]},{"emoji":"🕰️","tags":["clock","mantelpiece clock"]},{"emoji":"🕛","tags":["0","12","clock","12:00","o’clock","twelve"]},{"emoji":"🕧","tags":["12","12:30","thirty","clock","twelve","twelve-thirty"]},{"emoji":"🕐","tags":["0","1","clock","1:00","o’clock","one"]},{"emoji":"🕜","tags":["1","1:30","one","clock","one-thirty","thirty"]},{"emoji":"🕑","tags":["0","2","clock","2:00","o’clock","two"]},{"emoji":"🕝","tags":["2","2:30","thirty","clock","two","two-thirty"]},{"emoji":"🕒","tags":["0","3","clock","3:00","o’clock","three"]},{"emoji":"🕞","tags":["3","3:30","thirty","clock","three","three-thirty"]},{"emoji":"🕓","tags":["0","4","clock","4:00","four","o’clock"]},{"emoji":"🕟","tags":["4","4:30","four","clock","four-thirty","thirty"]},{"emoji":"🕔","tags":["0","5","clock","5:00","five","o’clock"]},{"emoji":"🕠","tags":["5","5:30","five","clock","five-thirty","thirty"]},{"emoji":"🕕","tags":["0","6","clock","6:00","o’clock","six"]},{"emoji":"🕡","tags":["6","6:30","six","clock","six-thirty","thirty"]},{"emoji":"🕖","tags":["0","7","clock","7:00","o’clock","seven"]},{"emoji":"🕢","tags":["7","7:30","seven","clock","seven-thirty","thirty"]},{"emoji":"🕗","tags":["0","8","clock","8:00","eight","o’clock"]},{"emoji":"🕣","tags":["8","8:30","eight","clock","eight-thirty","thirty"]},{"emoji":"🕘","tags":["0","9","clock","9:00","nine","o’clock"]},{"emoji":"🕤","tags":["9","9:30","nine","clock","nine-thirty","thirty"]},{"emoji":"🕙","tags":["0","10","clock","10:00","o’clock","ten"]},{"emoji":"🕥","tags":["10","10:30","ten","clock","ten-thirty","thirty"]},{"emoji":"🕚","tags":["0","11","clock","11:00","eleven","o’clock"]},{"emoji":"🕦","tags":["11","11:30","eleven","clock","eleven-thirty","thirty"]},{"emoji":"🌑","tags":["dark","moon","new moon"]},{"emoji":"🌒","tags":["crescent","moon","waxing"]},{"emoji":"🌓","tags":["first quarter moon","moon","quarter"]},{"emoji":"🌔","tags":["gibbous","moon","waxing"]},{"emoji":"🌕","tags":["full","moon"]},{"emoji":"🌖","tags":["gibbous","moon","waning"]},{"emoji":"🌗","tags":["last quarter moon","moon","quarter"]},{"emoji":"🌘","tags":["crescent","moon","waning"]},{"emoji":"🌙","tags":["crescent","moon"]},{"emoji":"🌚","tags":["face","moon","new moon face"]},{"emoji":"🌛","tags":["face","first quarter moon face","quarter","moon"]},{"emoji":"🌜","tags":["face","last quarter moon face","quarter","moon"]},{"emoji":"🌡️","tags":["thermometer","weather"]},{"emoji":"☀️","tags":["sun","bright"]},{"emoji":"🌝","tags":["bright","face","moon","full"]},{"emoji":"🌞","tags":["bright","face","sun with face","sun"]},{"emoji":"🪐","tags":["ringed planet","saturn","saturnine"]},{"emoji":"⭐","tags":["star"]},{"emoji":"🌟","tags":["glittery","glow","shining","glowing star","sparkle","star"]},{"emoji":"🌠","tags":["falling","shooting","star"]},{"emoji":"🌌","tags":["milky way","space","galaxy"]},{"emoji":"☁️","tags":["cloud"]},{"emoji":"⛅","tags":["cloud","sun","sun behind cloud"]},{"emoji":"⛈️","tags":["cloud","cloud with lightning and rain","thunder","rain"]},{"emoji":"🌤️","tags":["cloud","sun","sun behind small cloud"]},{"emoji":"🌥️","tags":["cloud","sun","sun behind large cloud"]},{"emoji":"🌦️","tags":["cloud","rain","sun behind rain cloud","sun"]},{"emoji":"🌧️","tags":["cloud","cloud with rain","rain"]},{"emoji":"🌨️","tags":["cloud","cloud with snow","snow","cold"]},{"emoji":"🌩️","tags":["cloud","cloud with lightning","lightning"]},{"emoji":"🌪️","tags":["cloud","tornado","whirlwind"]},{"emoji":"🌫️","tags":["cloud","fog"]},{"emoji":"🌬️","tags":["blow","cloud","wind","face"]},{"emoji":"🌀","tags":["cyclone","dizzy","twister","hurricane","typhoon"]},{"emoji":"🌈","tags":["rain","rainbow","lgbt","lgbtq","lgbtqia","pride","gay","lesbian","queer","transgender","trans","intersex","asexual"]},{"emoji":"🌂","tags":["closed umbrella","clothing","umbrella","rain"]},{"emoji":"☂️","tags":["umbrella","rain"]},{"emoji":"☔","tags":["clothing","drop","umbrella","rain","umbrella with rain drops"]},{"emoji":"⛱️","tags":["rain","sun","umbrella on ground","umbrella"]},{"emoji":"⚡","tags":["danger","electric","lightning","high voltage","voltage","zap"]},{"emoji":"❄️","tags":[]},{"emoji":"☃️","tags":["cold","snow","snowman"]},{"emoji":"⛄","tags":["cold","snow","snowman without snow","snowman"]},{"emoji":"☄️","tags":["comet","space"]},{"emoji":"🔥","tags":["fire","flame","tool"]},{"emoji":"💧","tags":["cold","comic","droplet","drop","sweat","water"]},{"emoji":"🌊","tags":["ocean","water","wave"]},{"emoji":"🎃","tags":["celebration","halloween","jack-o-lantern","jack","lantern","pumpkin"]},{"emoji":"🎄","tags":["celebration","Christmas","tree"]},{"emoji":"🎆","tags":["celebration","fireworks"]},{"emoji":"🎇","tags":["celebration","fireworks","sparkler","sparkle"]},{"emoji":"🧨","tags":["dynamite","explosive","fireworks","firecracker"]},{"emoji":"✨","tags":["*","sparkle","star","sparkles"]},{"emoji":"🎈","tags":["balloon","celebration"]},{"emoji":"🎉","tags":["celebration","party","tada","popper","confetti"]},{"emoji":"🎊","tags":["ball","celebration","confetti","party"]},{"emoji":"🎋","tags":["banner","celebration","tanabata tree","Japanese","tree"]},{"emoji":"🎍","tags":["bamboo","celebration","pine","Japanese","pine decoration"]},{"emoji":"🎎","tags":["celebration","doll","Japanese","festival","Japanese dolls"]},{"emoji":"🎏","tags":["carp","celebration","streamer"]},{"emoji":"🎐","tags":["bell","celebration","wind","chime"]},{"emoji":"🎑","tags":["celebration","ceremony","moon viewing ceremony","moon"]},{"emoji":"🧧","tags":["gift","good luck","lai see","hóngbāo","money","red envelope"]},{"emoji":"🎀","tags":["celebration","ribbon","bow"]},{"emoji":"🎁","tags":["box","celebration","present","gift","wrapped"]},{"emoji":"🎗️","tags":["celebration","reminder","ribbon"]},{"emoji":"🎟️","tags":["admission","admission tickets","ticket"]},{"emoji":"🎫","tags":["admission","ticket"]},{"emoji":"🎖️","tags":["celebration","medal","military","award"]},{"emoji":"🏆","tags":["prize","trophy","award","win"]},{"emoji":"🏅","tags":["medal","sports medal","star","win"]},{"emoji":"🥇","tags":["1st place medal","first","medal","gold","1","award","prize","win"]},{"emoji":"🥈","tags":["2nd place medal","medal","silver","second","2","award","prize","win"]},{"emoji":"🥉","tags":["3rd place medal","bronze","third","medal","3","award","prize","win"]},{"emoji":"⚽","tags":["ball","football","soccer"]},{"emoji":"⚾","tags":["ball","baseball"]},{"emoji":"🥎","tags":["ball","glove","underarm","softball"]},{"emoji":"🏀","tags":["ball","basketball","hoop"]},{"emoji":"🏐","tags":["ball","game","volleyball"]},{"emoji":"🏈","tags":["american","ball","football"]},{"emoji":"🏉","tags":["ball","football","rugby"]},{"emoji":"🎾","tags":["ball","racquet","tennis"]},{"emoji":"🥏","tags":["flying disc","ultimate"]},{"emoji":"🎳","tags":["ball","bowling","game"]},{"emoji":"🏏","tags":["ball","bat","game","cricket game"]},{"emoji":"🏑","tags":["ball","field","hockey","game","stick"]},{"emoji":"🏒","tags":["game","hockey","puck","ice","stick"]},{"emoji":"🥍","tags":["ball","goal","stick","lacrosse"]},{"emoji":"🏓","tags":["ball","bat","paddle","game","ping pong","table tennis"]},{"emoji":"🏸","tags":["badminton","birdie","racquet","game","shuttlecock"]},{"emoji":"🥊","tags":["boxing","glove"]},{"emoji":"🥋","tags":["judo","karate","martial arts uniform","martial arts","taekwondo","uniform"]},{"emoji":"🥅","tags":["goal","net"]},{"emoji":"⛳","tags":["flag in hole","golf","hole"]},{"emoji":"⛸️","tags":["ice","skate"]},{"emoji":"🎣","tags":["fish","fishing pole","pole"]},{"emoji":"🤿","tags":["diving","diving mask","snorkeling","scuba"]},{"emoji":"🎽","tags":["athletics","running","shirt","sash"]},{"emoji":"🎿","tags":["ski","skis","snow"]},{"emoji":"🛷","tags":["sled","sledge","sleigh"]},{"emoji":"🥌","tags":["curling stone","game","rock"]},{"emoji":"🎯","tags":["bullseye","dart","game","direct hit","hit","target"]},{"emoji":"🪀","tags":["fluctuate","toy","yo-yo"]},{"emoji":"🪁","tags":["fly","kite","soar"]},{"emoji":"🎱","tags":["8","ball","eight","billiard","game","pool 8 ball"]},{"emoji":"🔮","tags":["ball","crystal","fantasy","fairy tale","fortune","tool"]},{"emoji":"🪄","tags":["magic","magic wand","wizard","witch"]},{"emoji":"🧿","tags":["bead","charm","nazar","evil-eye","nazar amulet","talisman"]},{"emoji":"🎮","tags":["controller","game","video game"]},{"emoji":"🕹️","tags":["game","joystick","video game"]},{"emoji":"🎰","tags":["game","slot","slot machine","gambling","casino"]},{"emoji":"🎲","tags":["dice","die","game"]},{"emoji":"🧩","tags":["clue","interlocking","piece","jigsaw","puzzle"]},{"emoji":"🧸","tags":["plaything","plush","teddy bear","stuffed","toy"]},{"emoji":"🪅","tags":["celebration","party","piñata"]},{"emoji":"🪆","tags":["doll","nesting","russia","nesting dolls","matryoshka"]},{"emoji":"♠️","tags":["spade suit","card suit"]},{"emoji":"♥️","tags":[]},{"emoji":"♦️","tags":["diamond suit","card suit"]},{"emoji":"♣️","tags":["club suit","card suit"]},{"emoji":"♟️","tags":[]},{"emoji":"🃏","tags":["card","game","wildcard","joker"]},{"emoji":"🀄","tags":["game","mahjong","red","mahjong red dragon"]},{"emoji":"🎴","tags":["card","flower","game","flower playing cards","Japanese","playing"]},{"emoji":"🎭","tags":["art","mask","performing arts","performing","theater","theatre"]},{"emoji":"🖼️","tags":["art","frame","museum","framed picture","painting","picture"]},{"emoji":"🎨","tags":["art","artist palette","painting","museum","palette"]},{"emoji":"🧵","tags":["needle","sewing","string","spool","thread"]},{"emoji":"🪡","tags":["embroidery","needle","stitches","sewing","sutures","tailoring"]},{"emoji":"🧶","tags":["ball","crochet","yarn","knit"]},{"emoji":"🪢","tags":["knot","rope","tie","tangled","twine","twist"]},{"emoji":"👓","tags":["clothing","eye","eyewear","eyeglasses","glasses"]},{"emoji":"🕶️","tags":["dark","eye","glasses","eyewear","sunglasses"]},{"emoji":"🥽","tags":["eye protection","goggles","welding","swimming"]},{"emoji":"🥼","tags":["doctor","experiment","scientist","lab coat"]},{"emoji":"🦺","tags":["emergency","safety","vest"]},{"emoji":"👔","tags":["clothing","necktie","tie"]},{"emoji":"👕","tags":["clothing","shirt","tshirt","t-shirt"]},{"emoji":"👖","tags":["clothing","jeans","trousers","pants"]},{"emoji":"🧣","tags":["neck","scarf"]},{"emoji":"🧤","tags":["gloves","hand","mittens"]},{"emoji":"🧥","tags":["coat","jacket"]},{"emoji":"🧦","tags":["socks","stocking"]},{"emoji":"👗","tags":["clothing","dress"]},{"emoji":"👘","tags":["clothing","kimono"]},{"emoji":"🥻","tags":["clothing","dress","sari"]},{"emoji":"🩱","tags":["bathing suit","one-piece swimsuit"]},{"emoji":"🩲","tags":["bathing suit","briefs","swimsuit","one-piece","underwear"]},{"emoji":"🩳","tags":["bathing suit","pants","underwear","shorts"]},{"emoji":"👙","tags":["bikini","clothing","swim"]},{"emoji":"👚","tags":["clothing","woman","woman’s clothes"]},{"emoji":"👛","tags":["clothing","coin","purse"]},{"emoji":"👜","tags":["bag","clothing","purse","handbag"]},{"emoji":"👝","tags":["bag","clothing","pouch","clutch bag"]},{"emoji":"🛍️","tags":["bag","hotel","shopping bags","shopping"]},{"emoji":"🎒","tags":["backpack","bag","satchel","rucksack","school"]},{"emoji":"🩴","tags":["beach sandals","sandals","thong sandals","thong sandal","thongs","zōri"]},{"emoji":"👞","tags":["clothing","man","shoe","man’s shoe"]},{"emoji":"👟","tags":["athletic","clothing","shoe","running shoe","sneaker"]},{"emoji":"🥾","tags":["backpacking","boot","hiking","camping"]},{"emoji":"🥿","tags":["ballet flat","flat shoe","slipper","slip-on"]},{"emoji":"👠","tags":["clothing","heel","shoe","high-heeled shoe","woman"]},{"emoji":"👡","tags":["clothing","sandal","woman","shoe","woman’s sandal"]},{"emoji":"🩰","tags":["ballet","ballet shoes","dance"]},{"emoji":"👢","tags":["boot","clothing","woman","shoe","woman’s boot"]},{"emoji":"👑","tags":["clothing","crown","queen","king"]},{"emoji":"👒","tags":["clothing","hat","woman’s hat","woman"]},{"emoji":"🎩","tags":["clothing","hat","tophat","top"]},{"emoji":"🎓","tags":["cap","celebration","graduation","clothing","hat"]},{"emoji":"🧢","tags":["baseball cap","billed cap"]},{"emoji":"🪖","tags":["army","helmet","soldier","military","warrior"]},{"emoji":"⛑️","tags":["aid","cross","hat","face","helmet","rescue worker’s helmet"]},{"emoji":"📿","tags":["beads","clothing","prayer","necklace","religion"]},{"emoji":"💄","tags":["cosmetics","lipstick","makeup"]},{"emoji":"💍","tags":["diamond","ring"]},{"emoji":"💎","tags":["diamond","gem","jewel","gem stone"]},{"emoji":"🔇","tags":["mute","muted speaker","silent","quiet","speaker","volume"]},{"emoji":"🔈","tags":["soft","speaker low volume"]},{"emoji":"🔉","tags":["medium","speaker medium volume"]},{"emoji":"🔊","tags":["loud","speaker high volume"]},{"emoji":"📢","tags":["loud","loudspeaker","public address"]},{"emoji":"📣","tags":["cheering","megaphone"]},{"emoji":"📯","tags":["horn","post","postal"]},{"emoji":"🔔","tags":["bell","ring"]},{"emoji":"🔕","tags":["bell","bell with slash","mute","forbidden","quiet","silent"]},{"emoji":"🎼","tags":["music","musical score","score"]},{"emoji":"🎵","tags":["music","musical note","note"]},{"emoji":"🎶","tags":["music","musical notes","notes","note"]},{"emoji":"🎙️","tags":["mic","microphone","studio","music"]},{"emoji":"🎚️","tags":["level","music","slider"]},{"emoji":"🎛️","tags":["control","knobs","music"]},{"emoji":"🎤","tags":["karaoke","mic","microphone"]},{"emoji":"🎧","tags":["earbud","headphone"]},{"emoji":"📻","tags":["radio","video"]},{"emoji":"🎷","tags":["instrument","music","saxophone","sax"]},{"emoji":"🪗","tags":["accordion","concertina","squeeze box"]},{"emoji":"🎸","tags":["guitar","instrument","music"]},{"emoji":"🎹","tags":["instrument","keyboard","musical keyboard","music","piano"]},{"emoji":"🎺","tags":["instrument","music","trumpet"]},{"emoji":"🎻","tags":["instrument","music","violin"]},{"emoji":"🪕","tags":["banjo","music","stringed"]},{"emoji":"🥁","tags":["drum","drumsticks","music"]},{"emoji":"🪘","tags":["beat","conga","long drum","drum","rhythm"]},{"emoji":"📱","tags":["cell","mobile","telephone","phone","smartphone"]},{"emoji":"📲","tags":["arrow","cell","mobile phone with arrow","mobile","phone","receive","smartphone"]},{"emoji":"☎️","tags":[]},{"emoji":"📞","tags":["phone","receiver","telephone"]},{"emoji":"📟","tags":["pager","beeper"]},{"emoji":"📠","tags":["fax","fax machine"]},{"emoji":"🔋","tags":["battery"]},{"emoji":"🔌","tags":["electric","electricity","plug"]},{"emoji":"💻","tags":["computer","laptop","personal","pc"]},{"emoji":"🖥️","tags":["computer","desktop","monitor","screen"]},{"emoji":"🖨️","tags":["computer","printer"]},{"emoji":"⌨️","tags":["keyboard","computer keyboard"]},{"emoji":"🖱️","tags":["computer","computer mouse"]},{"emoji":"🖲️","tags":["computer","trackball"]},{"emoji":"💽","tags":["computer","disk","optical","minidisk"]},{"emoji":"💾","tags":["computer","disk","save","floppy"]},{"emoji":"💿","tags":["cd","computer","optical","disk"]},{"emoji":"📀","tags":["blu-ray","computer","dvd","disk","optical"]},{"emoji":"🧮","tags":["abacus","calculation"]},{"emoji":"🎥","tags":["camera","cinema","movie"]},{"emoji":"🎞️","tags":["cinema","film","movie","frames"]},{"emoji":"📽️","tags":["cinema","film","projector","movie","video","camera","blue"]},{"emoji":"🎬","tags":["clapper","clapper board","movie"]},{"emoji":"📺","tags":["television","tv","video"]},{"emoji":"📷","tags":["camera","video"]},{"emoji":"📸","tags":["camera","camera with flash","video","flash"]},{"emoji":"📹","tags":["camera","video","camcorder"]},{"emoji":"📼","tags":["tape","vhs","videocassette","video"]},{"emoji":"🔍","tags":["glass","magnifying","search","magnifying glass tilted left","tool"]},{"emoji":"🔎","tags":["glass","magnifying","search","magnifying glass tilted right","tool"]},{"emoji":"🕯️","tags":["candle","light"]},{"emoji":"💡","tags":["bulb","comic","idea","electric","light","lightbulb"]},{"emoji":"🔦","tags":["electric","flashlight","tool","light","torch"]},{"emoji":"🏮","tags":["bar","lantern","red","light","red paper lantern"]},{"emoji":"🪔","tags":["diya","lamp","oil"]},{"emoji":"📔","tags":["book","cover","notebook","decorated","notebook with decorative cover"]},{"emoji":"📕","tags":["book","closed"]},{"emoji":"📖","tags":["book","open"]},{"emoji":"📗","tags":["book","green"]},{"emoji":"📘","tags":["blue","book"]},{"emoji":"📙","tags":["book","orange"]},{"emoji":"📚","tags":["book","books"]},{"emoji":"📓","tags":["notebook"]},{"emoji":"📒","tags":["ledger","notebook"]},{"emoji":"📃","tags":["curl","document","page with curl","page"]},{"emoji":"📜","tags":["paper","scroll"]},{"emoji":"📄","tags":["document","page","page facing up"]},{"emoji":"📰","tags":["news","newspaper","paper"]},{"emoji":"🗞️","tags":["news","newspaper","rolled","paper","rolled-up newspaper"]},{"emoji":"📑","tags":["bookmark","mark","tabs","marker"]},{"emoji":"🔖","tags":["bookmark","mark"]},{"emoji":"🏷️","tags":["label","tag"]},{"emoji":"💰","tags":["bag","dollar","moneybag","money"]},{"emoji":"🪙","tags":["coin","gold","money","metal","silver","treasure"]},{"emoji":"💴","tags":["banknote","bill","money","currency","note","yen"]},{"emoji":"💵","tags":["banknote","bill","dollar","currency","money","note"]},{"emoji":"💶","tags":["banknote","bill","euro","currency","money","note"]},{"emoji":"💷","tags":["banknote","bill","money","currency","note","pound"]},{"emoji":"💸","tags":["banknote","bill","money","fly","money with wings","wings"]},{"emoji":"💳","tags":["card","credit","money"]},{"emoji":"🧾","tags":["accounting","bookkeeping","proof","evidence","receipt"]},{"emoji":"💹","tags":["chart","chart increasing with yen","growth","graph","money","yen"]},{"emoji":"✉️","tags":["envelope","letter","mail"]},{"emoji":"📧","tags":["e-mail","email","mail","letter"]},{"emoji":"📨","tags":["e-mail","email","incoming","envelope","letter","receive"]},{"emoji":"📩","tags":["arrow","e-mail","envelope","email","envelope with arrow","outgoing"]},{"emoji":"📤","tags":["box","letter","outbox","mail","sent","tray"]},{"emoji":"📥","tags":["box","inbox","mail","letter","receive","tray"]},{"emoji":"📦","tags":["box","package","parcel"]},{"emoji":"📫","tags":["closed","closed mailbox with raised flag","mailbox","mail","postbox"]},{"emoji":"📪","tags":["closed","closed mailbox with lowered flag","mail","lowered","mailbox","postbox"]},{"emoji":"📬","tags":["mail","mailbox","open mailbox with raised flag","open","postbox"]},{"emoji":"📭","tags":["lowered","mail","open","mailbox","open mailbox with lowered flag","postbox"]},{"emoji":"📮","tags":["mail","mailbox","postbox"]},{"emoji":"🗳️","tags":["ballot","ballot box with ballot","box"]},{"emoji":"✏️","tags":[]},{"emoji":"✒️","tags":["pen","pen nib","nib"]},{"emoji":"🖋️","tags":["fountain","pen"]},{"emoji":"🖊️","tags":["ballpoint","pen"]},{"emoji":"🖌️","tags":["paintbrush","painting"]},{"emoji":"🖍️","tags":["crayon"]},{"emoji":"📝","tags":["memo","pencil","pencil and paper","paper","write"]},{"emoji":"💼","tags":["briefcase"]},{"emoji":"📁","tags":["file","folder"]},{"emoji":"📂","tags":["file","folder","open"]},{"emoji":"🗂️","tags":["card","dividers","index"]},{"emoji":"📅","tags":["calendar","date"]},{"emoji":"📆","tags":["calendar","tear-off calendar"]},{"emoji":"🗒️","tags":["note","pad","spiral notepad","spiral"]},{"emoji":"🗓️","tags":["calendar","pad","spiral"]},{"emoji":"📇","tags":["card","index","rolodex"]},{"emoji":"📈","tags":["chart","chart increasing","growth","graph","trend","upward"]},{"emoji":"📉","tags":["chart","chart decreasing","graph","down","trend"]},{"emoji":"📊","tags":["bar","chart","graph"]},{"emoji":"📋","tags":["clipboard"]},{"emoji":"📌","tags":["pin","pushpin"]},{"emoji":"📍","tags":["pin","pushpin","round pushpin"]},{"emoji":"📎","tags":["paperclip"]},{"emoji":"🖇️","tags":["link","linked paperclips","paperclip"]},{"emoji":"📏","tags":["ruler","straight edge","straight ruler"]},{"emoji":"📐","tags":["ruler","set","triangular ruler","triangle"]},{"emoji":"✂️","tags":["scissors","cut"]},{"emoji":"🗃️","tags":["box","card","file"]},{"emoji":"🗄️","tags":["cabinet","file","filing"]},{"emoji":"🗑️","tags":["wastebasket","trash","can"]},{"emoji":"🔒","tags":["closed","locked","lock","padlock"]},{"emoji":"🔓","tags":["lock","open","unlocked","unlock","padlock"]},{"emoji":"🔏","tags":["ink","lock","nib","locked with pen","pen","privacy","padlock"]},{"emoji":"🔐","tags":["closed","key","locked with key","lock","secure","padlock"]},{"emoji":"🔑","tags":["key","lock","password"]},{"emoji":"🗝️","tags":["clue","key","old","lock"]},{"emoji":"🔨","tags":["hammer","tool"]},{"emoji":"🪓","tags":["axe","chop","split","hatchet","wood"]},{"emoji":"⛏️","tags":["mining","pick","tool"]},{"emoji":"⚒️","tags":["hammer","pick"]},{"emoji":"🛠️","tags":["hammer","hammer and wrench","tool","spanner","wrench"]},{"emoji":"🗡️","tags":["dagger","knife","weapon"]},{"emoji":"⚔️","tags":["sword","fencing","crossed swords"]},{"emoji":"🔫","tags":["gun","handgun","revolver","pistol","tool","water","weapon"]},{"emoji":"🪃","tags":["australia","boomerang","repercussion","rebound"]},{"emoji":"🏹","tags":["archer","arrow","bow and arrow","bow","Sagittarius","zodiac"]},{"emoji":"🛡️","tags":["shield","weapon"]},{"emoji":"🪚","tags":["carpenter","carpentry saw","saw","lumber","tool"]},{"emoji":"🔧","tags":["spanner","tool","wrench"]},{"emoji":"🪛","tags":["screw","screwdriver","tool"]},{"emoji":"🔩","tags":["bolt","nut","tool","nut and bolt"]},{"emoji":"⚙️","tags":["gear","cog","machine"]},{"emoji":"🗜️","tags":["clamp","compress","vice","tool"]},{"emoji":"⚖️","tags":["scale","balance","law"]},{"emoji":"🦯","tags":["accessibility","blind","white cane"]},{"emoji":"🔗","tags":["link"]},{"emoji":"⛓️","tags":["chain","chains"]},{"emoji":"🪝","tags":["catch","crook","ensnare","curve","hook","selling point"]},{"emoji":"🧰","tags":["chest","mechanic","toolbox","tool"]},{"emoji":"🧲","tags":["attraction","horseshoe","magnetic","magnet"]},{"emoji":"🪜","tags":["climb","ladder","step","rung"]},{"emoji":"⚗️","tags":[]},{"emoji":"🧪","tags":["chemist","chemistry","lab","experiment","science","test tube"]},{"emoji":"🧫","tags":["bacteria","biologist","culture","biology","lab","petri dish"]},{"emoji":"🧬","tags":["biologist","dna","gene","evolution","genetics","life"]},{"emoji":"🔬","tags":["microscope","science","tool"]},{"emoji":"🔭","tags":["science","telescope","tool"]},{"emoji":"📡","tags":["antenna","dish","satellite"]},{"emoji":"💉","tags":["medicine","needle","sick","shot","syringe"]},{"emoji":"🩸","tags":["bleed","blood donation","injury","drop of blood","medicine","menstruation"]},{"emoji":"💊","tags":["doctor","medicine","sick","pill"]},{"emoji":"🩹","tags":["adhesive bandage","bandage"]},{"emoji":"🩺","tags":["doctor","stethoscope","medicine","listen"]},{"emoji":"🚪","tags":["door"]},{"emoji":"🛗","tags":["accessibility","elevator","lift","hoist"]},{"emoji":"🪞","tags":["mirror","reflection","speculum","reflector"]},{"emoji":"🪟","tags":["frame","fresh air","transparent","opening","view","window"]},{"emoji":"🛏️","tags":["bed","hotel","sleep"]},{"emoji":"🛋️","tags":["couch","couch and lamp","lamp","hotel"]},{"emoji":"🪑","tags":["chair","seat","sit"]},{"emoji":"🚽","tags":["toilet"]},{"emoji":"🪠","tags":["force cup","plumber","suction","plunger","toilet"]},{"emoji":"🚿","tags":["shower","water"]},{"emoji":"🛁","tags":["bath","bathtub","water"]},{"emoji":"🪤","tags":["bait","mouse trap","snare","mousetrap","trap"]},{"emoji":"🪒","tags":["razor","sharp","shave"]},{"emoji":"🧴","tags":["lotion","lotion bottle","shampoo","moisturizer","sunscreen"]},{"emoji":"🧷","tags":["diaper","punk rock","safety pin"]},{"emoji":"🧹","tags":["broom","cleaning","witch","sweeping"]},{"emoji":"🧺","tags":["basket","farming","picnic","laundry"]},{"emoji":"🧻","tags":["paper towels","roll of paper","toilet paper"]},{"emoji":"🪣","tags":["bucket","cask","vat","pail"]},{"emoji":"🧼","tags":["bar","bathing","lather","cleaning","soap","soapdish"]},{"emoji":"🪥","tags":["bathroom","brush","dental","clean","hygiene","teeth","toothbrush"]},{"emoji":"🧽","tags":["absorbing","cleaning","sponge","porous"]},{"emoji":"🧯","tags":["extinguish","fire","quench","fire extinguisher"]},{"emoji":"🛒","tags":["cart","shopping","trolley"]},{"emoji":"🚬","tags":["cigarette","smoking","tobacco"]},{"emoji":"⚰️","tags":["coffin","death","dead","casket","funeral"]},{"emoji":"🪦","tags":["cemetery","grave","headstone","graveyard","tombstone","gravestone"]},{"emoji":"⚱️","tags":["funeral","urn","death","dead"]},{"emoji":"🗿","tags":["face","moai","statue","moyai"]},{"emoji":"🪧","tags":["demonstration","picket","protest","placard","sign"]},{"emoji":"🏧","tags":["atm","ATM sign","bank","automated","teller"]},{"emoji":"🚮","tags":["litter","litter bin","litter in bin sign"]},{"emoji":"🚰","tags":["drinking","potable","water"]},{"emoji":"♿","tags":["access","wheelchair symbol"]},{"emoji":"🚹","tags":["lavatory","man","restroom","men’s room","wc"]},{"emoji":"🚺","tags":["lavatory","restroom","woman","wc","women’s room"]},{"emoji":"🚻","tags":["lavatory","restroom","WC"]},{"emoji":"🚼","tags":["baby","baby symbol","changing"]},{"emoji":"🚾","tags":["closet","lavatory","water","restroom","wc"]},{"emoji":"🛂","tags":["control","passport"]},{"emoji":"🛃","tags":["customs"]},{"emoji":"🛄","tags":["baggage","claim"]},{"emoji":"🛅","tags":["baggage","left luggage","luggage","locker"]},{"emoji":"⚠️","tags":["caution","warning","alert","danger"]},{"emoji":"🚸","tags":["child","children crossing","pedestrian","crossing","traffic"]},{"emoji":"⛔","tags":["entry","forbidden","not","no","prohibited","traffic"]},{"emoji":"🚫","tags":["entry","forbidden","not","no","prohibited"]},{"emoji":"🚳","tags":["bicycle","bike","no","forbidden","no bicycles","prohibited"]},{"emoji":"🚭","tags":["forbidden","no","prohibited","not","smoking"]},{"emoji":"🚯","tags":["forbidden","litter","no littering","no","not","prohibited"]},{"emoji":"🚱","tags":["non-drinking","non-potable","water"]},{"emoji":"🚷","tags":["forbidden","no","not","no pedestrians","pedestrian","prohibited"]},{"emoji":"📵","tags":["cell","forbidden","no","mobile","no mobile phones","phone"]},{"emoji":"🔞","tags":["18","age restriction","no one under eighteen","eighteen","prohibited","underage"]},{"emoji":"☢️","tags":["warning","hazard","danger","radioactive"]},{"emoji":"☣️","tags":["warning","hazard","danger","radioactive"]},{"emoji":"⬆️","tags":["arrow","up"]},{"emoji":"↗️","tags":["arrow","up right"]},{"emoji":"➡️","tags":["arrow","right"]},{"emoji":"↘️","tags":["arrow","down right"]},{"emoji":"⬇️","tags":["arrow","down right"]},{"emoji":"↙️","tags":["arrow","down left"]},{"emoji":"⬅️","tags":["arrow","left"]},{"emoji":"↖️","tags":["arrow","up left"]},{"emoji":"↕️","tags":["arrow","up and down","vertical","height"]},{"emoji":"↔️","tags":["arrow","left and right","horizontal","width"]},{"emoji":"↩️","tags":["arrow","return"]},{"emoji":"↪️","tags":["arrow","forward"]},{"emoji":"⤴️","tags":["arrow","up bend"]},{"emoji":"⤵️","tags":["arrow","down bend"]},{"emoji":"🔃","tags":["arrow","clockwise","reload","clockwise vertical arrows"]},{"emoji":"🔄","tags":["anticlockwise","arrow","counterclockwise arrows button","counterclockwise","withershins"]},{"emoji":"🔙","tags":["arrow","back","BACK arrow"]},{"emoji":"🔚","tags":["arrow","end","END arrow"]},{"emoji":"🔛","tags":["arrow","mark","ON! arrow","on"]},{"emoji":"🔜","tags":["arrow","soon","SOON arrow"]},{"emoji":"🔝","tags":["arrow","top","up","TOP arrow"]},{"emoji":"🛐","tags":["place of worship","religion","worship","pray"]},{"emoji":"⚛️","tags":[]},{"emoji":"🕉️","tags":["Hindu","om","religion"]},{"emoji":"✡️","tags":["star","jewish","start of david"]},{"emoji":"☸️","tags":["wheel","dharma"]},{"emoji":"☯️","tags":["yin","yang","yin yang"]},{"emoji":"✝️","tags":["cross","christianity"]},{"emoji":"☦️","tags":["cross","orthodox cross"]},{"emoji":"☪️","tags":["star and crescent","islam"]},{"emoji":"☮️","tags":["peace","peace sign"]},{"emoji":"🕎","tags":["candelabrum","candlestick","religion","menorah"]},{"emoji":"🔯","tags":["dotted six-pointed star","fortune","star"]},{"emoji":"♈","tags":["Aries","ram","zodiac"]},{"emoji":"♉","tags":["bull","ox","zodiac","Taurus"]},{"emoji":"♊","tags":["Gemini","twins","zodiac"]},{"emoji":"♋","tags":["Cancer","crab","zodiac"]},{"emoji":"♌","tags":["Leo","lion","zodiac"]},{"emoji":"♍","tags":["Virgo","zodiac"]},{"emoji":"♎","tags":["balance","justice","scales","Libra","zodiac"]},{"emoji":"♏","tags":["Scorpio","scorpion","zodiac","scorpius"]},{"emoji":"♐","tags":["archer","Sagittarius","zodiac"]},{"emoji":"♑","tags":["Capricorn","goat","zodiac"]},{"emoji":"♒","tags":["Aquarius","bearer","zodiac","water"]},{"emoji":"♓","tags":["fish","Pisces","zodiac"]},{"emoji":"⛎","tags":["bearer","Ophiuchus","snake","serpent","zodiac"]},{"emoji":"🔀","tags":["arrow","crossed","shuffle tracks button"]},{"emoji":"🔁","tags":["arrow","clockwise","repeat button","repeat"]},{"emoji":"🔂","tags":["arrow","clockwise","repeat single button","once"]},{"emoji":"▶️","tags":["play","go","play button","right arrow","triangle"]},{"emoji":"⏩","tags":["arrow","double","fast-forward button","fast","forward"]},{"emoji":"⏭️","tags":["arrow","next scene","next track button","next track","triangle"]},{"emoji":"⏯️","tags":["arrow","pause","play or pause button","play","right","triangle"]},{"emoji":"◀️","tags":["left arrow","triangle"]},{"emoji":"⏪","tags":["arrow","double","rewind","fast reverse button"]},{"emoji":"⏮️","tags":["arrow","last track button","previous track","previous scene","triangle"]},{"emoji":"🔼","tags":["arrow","button","upwards button","red"]},{"emoji":"⏫","tags":["arrow","double","fast up button"]},{"emoji":"🔽","tags":["arrow","button","downwards button","down","red"]},{"emoji":"⏬","tags":["arrow","double","fast down button","down"]},{"emoji":"⏸️","tags":["bar","double","pause button","pause","vertical"]},{"emoji":"⏹️","tags":["square","stop","stop button"]},{"emoji":"⏺️","tags":["circle","record","record button"]},{"emoji":"⏏️","tags":["eject","eject button"]},{"emoji":"🎦","tags":["camera","cinema","movie","film"]},{"emoji":"🔅","tags":["brightness","dim","low","dim button"]},{"emoji":"🔆","tags":["bright","bright button","brightness"]},{"emoji":"📶","tags":["antenna","antenna bars","cell","bar","mobile","phone"]},{"emoji":"📳","tags":["cell","mobile","phone","mode","telephone","vibration"]},{"emoji":"📴","tags":["cell","mobile","phone","off","telephone"]},{"emoji":"⚧️","tags":["transgender","transgender symbol"]},{"emoji":"✖️","tags":["stop","x","cross"]},{"emoji":"➕","tags":["+","math","sign","plus"]},{"emoji":"➖","tags":["-","−","minus","math","sign"]},{"emoji":"➗","tags":["÷","divide","math","division","sign"]},{"emoji":"♾️","tags":["infinity","infinite","endless"]},{"emoji":"‼️","tags":[]},{"emoji":"⁉️","tags":["!?","exclamation","question"]},{"emoji":"❓","tags":["?","mark","question","punctuation","red question mark"]},{"emoji":"❔","tags":["?","mark","punctuation","outlined","question","white question mark"]},{"emoji":"❕","tags":["!","exclamation","outlined","mark","punctuation","white exclamation mark"]},{"emoji":"❗","tags":["!","exclamation","punctuation","mark","red exclamation mark"]},{"emoji":"〰️","tags":["wave","wavey","wavey dash"]},{"emoji":"💱","tags":["currency","exchange","money"]},{"emoji":"💲","tags":["money","dollars","cash","usd","rich"]},{"emoji":"♻️","tags":[]},{"emoji":"⚜️","tags":["fleur","fleur-de-lis"]},{"emoji":"🔱","tags":["anchor","emblem","tool","ship","trident"]},{"emoji":"📛","tags":["badge","name"]},{"emoji":"🔰","tags":["beginner","chevron","Japanese symbol for beginner","Japanese","leaf"]},{"emoji":"⭕","tags":["circle","hollow red circle","o","large","red","mark"]},{"emoji":"✅","tags":["✓","button","mark","check","yes"]},{"emoji":"☑️","tags":["check","check box","done","todo"]},{"emoji":"✔️","tags":["✓","check","mark","yes"]},{"emoji":"❌","tags":["×","cancel","mark","cross","multiplication","multiply","x","no"]},{"emoji":"❎","tags":["×","cross mark button","square","mark","x","no"]},{"emoji":"➰","tags":["curl","curly loop","loop"]},{"emoji":"➿","tags":["loop","curl","twist","double curly loop"]},{"emoji":"〽️","tags":["part alternation mark"]},{"emoji":"✳️","tags":[]},{"emoji":"✴️","tags":[]},{"emoji":"❇️","tags":["sparkle"]},{"emoji":"©️","tags":["copyright"]},{"emoji":"®️","tags":["registered","reserved"]},{"emoji":"™️","tags":["tm","trademark"]},{"emoji":"🔠","tags":["ABCD","input","letters","latin","uppercase"]},{"emoji":"🔡","tags":["abcd","input","letters","latin","lowercase"]},{"emoji":"🔢","tags":["1234","input","numbers"]},{"emoji":"🔣","tags":["〒♪&%","input","input symbols"]},{"emoji":"🔤","tags":["abc","alphabet","latin","input","letters"]},{"emoji":"🅰️","tags":[]},{"emoji":"🆎","tags":["ab","AB button (blood type)","blood type"]},{"emoji":"🅱️","tags":["b","letter b"]},{"emoji":"🆑","tags":["cl","CL button"]},{"emoji":"🆒","tags":["cool","COOL button"]},{"emoji":"🆓","tags":["free","FREE button"]},{"emoji":"ℹ️","tags":["i","letter i"]},{"emoji":"🆔","tags":["id","ID button","identity"]},{"emoji":"Ⓜ️","tags":["m","m in circle"]},{"emoji":"🆕","tags":["new","NEW button"]},{"emoji":"🆖","tags":["ng","NG button"]},{"emoji":"🅾️","tags":[]},{"emoji":"🆗","tags":["OK","OK button"]},{"emoji":"🅿️","tags":["p","letter p"]},{"emoji":"🆘","tags":["help","sos","SOS button"]},{"emoji":"🆙","tags":["mark","up","UP! button"]},{"emoji":"🆚","tags":["versus","vs","VS button"]},{"emoji":"🈁","tags":["here","Japanese","katakana","Japanese “here” button","ココ"]},{"emoji":"🈂️","tags":[]},{"emoji":"🈷️","tags":[]},{"emoji":"🈶","tags":["not free of charge","ideograph","Japanese “not free of charge” button","Japanese","有"]},{"emoji":"🈯","tags":["reserved","ideograph","Japanese “reserved” button","Japanese","指"]},{"emoji":"🉐","tags":["bargain","ideograph","Japanese “bargain” button","Japanese","得"]},{"emoji":"🈹","tags":["discount","ideograph","Japanese “discount” button","Japanese","割"]},{"emoji":"🈚","tags":["free of charge","ideograph","Japanese “free of charge” button","Japanese","無"]},{"emoji":"🈲","tags":["prohibited","ideograph","Japanese “prohibited” button","Japanese","禁"]},{"emoji":"🉑","tags":["acceptable","ideograph","Japanese “acceptable” button","Japanese","可"]},{"emoji":"🈸","tags":["application","ideograph","Japanese “application” button","Japanese","申"]},{"emoji":"🈴","tags":["passing grade","ideograph","Japanese “passing grade” button","Japanese","合"]},{"emoji":"🈳","tags":["vacancy","ideograph","Japanese “vacancy” button","Japanese","空"]},{"emoji":"㊗️","tags":["congratulations","ideograph","japanese"]},{"emoji":"㊙️","tags":[]},{"emoji":"🈺","tags":["open for business","ideograph","Japanese “open for business” button","Japanese","営"]},{"emoji":"🈵","tags":["no vacancy","ideograph","Japanese “no vacancy” button","Japanese","満"]},{"emoji":"🔴","tags":["circle","geometric","red"]},{"emoji":"🟠","tags":["circle","orange"]},{"emoji":"🟡","tags":["circle","yellow"]},{"emoji":"🟢","tags":["circle","green"]},{"emoji":"🔵","tags":["blue","circle","geometric"]},{"emoji":"🟣","tags":["circle","purple"]},{"emoji":"🟤","tags":["brown","circle"]},{"emoji":"⚫","tags":["black circle","circle","geometric","black"]},{"emoji":"⚪","tags":["circle","geometric","white circle","white"]},{"emoji":"🟥","tags":["red","square"]},{"emoji":"🟧","tags":["orange","square"]},{"emoji":"🟨","tags":["square","yellow"]},{"emoji":"🟩","tags":["green","square"]},{"emoji":"🟦","tags":["blue","square"]},{"emoji":"🟪","tags":["purple","square"]},{"emoji":"🟫","tags":["brown","square"]},{"emoji":"⬛","tags":["black large square","geometric","square","black"]},{"emoji":"⬜","tags":["geometric","square","white large square","white"]},{"emoji":"◼️","tags":[]},{"emoji":"◻️","tags":["geometric","square","white medium square","white"]},{"emoji":"◾","tags":["black medium-small square","geometric","square"]},{"emoji":"◽","tags":["geometric","square","white medium-small square"]},{"emoji":"▪️","tags":[]},{"emoji":"▫️","tags":[]},{"emoji":"🔶","tags":["diamond","geometric","orange","large orange diamond"]},{"emoji":"🔷","tags":["blue","diamond","large blue diamond","geometric"]},{"emoji":"🔸","tags":["diamond","geometric","small orange diamond","orange"]},{"emoji":"🔹","tags":["blue","diamond","small blue diamond","geometric"]},{"emoji":"🔺","tags":["geometric","red","red triangle pointed up"]},{"emoji":"🔻","tags":["down","geometric","red triangle pointed down","red"]},{"emoji":"💠","tags":["comic","diamond","geometric","diamond with a dot","inside"]},{"emoji":"🔘","tags":["button","radio button","circle"]},{"emoji":"🔳","tags":["button","geometric","square","outlined","white square button"]},{"emoji":"🔲","tags":["button","geometric","square","outlined"]},{"emoji":"🏁","tags":["checkered","chequered","racing","chequered flag"]},{"emoji":"🚩","tags":["post","triangular flag"]},{"emoji":"🎌","tags":["celebration","cross","crossed flags","crossed","Japanese"]},{"emoji":"🏴","tags":["black flag","waving"]},{"emoji":"🏳️","tags":["waving","white flag"]},{"emoji":"🏳️‍🌈","tags":["pride","rainbow","rainbow flag","lgbtqia","lgbt","lgbtqa","queer","gay","lesbian","intersex","bisexual","transgender","trans","asexual","questioning"]},{"emoji":"🏴‍☠️","tags":["Jolly Roger","pirate","plunder","pirate flag","treasure"]},{"emoji":"☹️","tags":["face","frown","sad","slightly frowning face"]},{"emoji":"☠️","tags":["death","skull","crossbones","skull and crossbones","bones","dead"]},{"emoji":"❤️","tags":["heart","red heart"]},{"emoji":"♥️","tags":["heart suit","card suit"]},{"emoji":"🗨️","tags":["balloon","bubble","dialog","comic","speech"]},{"emoji":"❄️","tags":["snow","snowflake","cold","ice"]},{"emoji":"☎️","tags":["phone","telephone"]},{"emoji":"✏️","tags":["pencil"]},{"emoji":"⚗️","tags":["alembic","science","chemistry"]},{"emoji":"⚛️","tags":["atom"]},{"emoji":"✳️","tags":["asterisk","eight spoke"]},{"emoji":"✴️","tags":["star","eight pointed"]},{"emoji":"‼️","tags":["!","exclamation","double exclamation"]},{"emoji":"♻️","tags":["recyce","reuse","green"]},{"emoji":"🅰️","tags":["a","letter a"]},{"emoji":"🅾️","tags":["o","letter o"]},{"emoji":"🈂️","tags":["service charger","ideograph","japanese"]},{"emoji":"🈷️","tags":["monthly amount","ideograph","japanese"]},{"emoji":"㊙️","tags":["secret","ideograph","japanese"]},{"emoji":"▪️","tags":["black small square","geometric","square","black"]},{"emoji":"◼️","tags":["black medium square","geometric","square","black"]},{"emoji":"▫️","tags":["geometric","square","white small square","white"]},{"emoji":"#️⃣","tags":["keycap","pound","hash"]},{"emoji":"*️⃣","tags":["keycap","asterisk"]},{"emoji":"0️⃣","tags":["keycap","0","zero"]},{"emoji":"1️⃣","tags":["keycap","1","one"]},{"emoji":"2️⃣","tags":["keycap","2","two"]},{"emoji":"3️⃣","tags":["keycap","3","three"]},{"emoji":"4️⃣","tags":["keycap","4","four"]},{"emoji":"5️⃣","tags":["keycap","5","five"]},{"emoji":"6️⃣","tags":["keycap","6","six"]},{"emoji":"7️⃣","tags":["keycap","7","seven"]},{"emoji":"8️⃣","tags":["keycap","8","eight"]},{"emoji":"9️⃣","tags":["keycap","9","nine"]},{"emoji":"🔟","tags":["keycap","10","ten"]}] \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index 308e05b2d3..dad5b09288 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/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 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()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java b/app/src/main/java/org/thoughtcrime/securesms/animation/AnimationCompleteListener.java new file mode 100644 index 0000000000..3063a04c91 --- /dev/null +++ b/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) {} +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/animation/ResizeAnimation.java b/app/src/main/java/org/thoughtcrime/securesms/animation/ResizeAnimation.java new file mode 100644 index 0000000000..5308484398 --- /dev/null +++ b/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; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/CompositeEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/CompositeEmojiPageModel.java index 28c4e9f385..cc36ab31c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/CompositeEmojiPageModel.java +++ b/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 models; - public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull EmojiPageModel... models) { + public CompositeEmojiPageModel(@AttrRes int iconAttr, @NonNull List 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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emoji.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emoji.java index 52d56cb48e..6935bf17ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/Emoji.java +++ b/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 variations; + private final List rawVariations; public Emoji(String... variations) { - this.variations = Arrays.asList(variations); + this(Arrays.asList(variations), Collections.emptyList()); + } + + public Emoji(List variations) { + this(variations, Collections.emptyList()); + } + + public Emoji(List variations, List rawVariations) { + this.variations = variations; + this.rawVariations = rawVariations; } public String getValue() { @@ -18,4 +31,15 @@ public class Emoji { public List 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; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java index 4475e6174a..757ccc36b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEditText.java +++ b/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 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 androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.widget.AppCompatEditText; + +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; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEventListener.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiEventListener.java new file mode 100644 index 0000000000..608aa0c394 --- /dev/null +++ b/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); +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java index 2b32d5e4d9..b60650dbee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiFilter.java +++ b/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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiImageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiImageView.java new file mode 100644 index 0000000000..f77043e81e --- /dev/null +++ b/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); + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiItemDecoration.kt new file mode 100644 index 0000000000..bed28b0b18 --- /dev/null +++ b/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 + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiKeyboardProvider.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiKeyboardProvider.java index 3c816b2e56..72419f24c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiKeyboardProvider.java +++ b/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); - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java index 4c4d577584..bd944840e1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageModel.java +++ b/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 getEmoji(); List getDisplayEmoji(); boolean hasSpriteMap(); - String getSprite(); + @Nullable Uri getSpriteUri(); boolean isDynamic(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java index bbcffb2e13..4b9abf9810 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageView.java +++ b/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 androidx.annotation.NonNull; -import androidx.recyclerview.widget.GridLayoutManager; -import androidx.recyclerview.widget.RecyclerView; -import android.view.LayoutInflater; +import android.graphics.drawable.Drawable; +import android.util.AttributeSet; import android.view.MotionEvent; import android.view.View; -import android.widget.FrameLayout; +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 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 java.util.List; +import java.util.Optional; import network.loki.messenger.R; -public class EmojiPageView extends FrameLayout implements VariationSelectorListener { - private static final String TAG = EmojiPageView.class.getSimpleName(); - - 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); - recyclerView.setLayoutManager(layoutManager); - recyclerView.setAdapter(adapter); + 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> 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)); + + 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> 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(); + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageViewGridAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageViewGridAdapter.java index b679054f3f..69b7279ed3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageViewGridAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPageViewGridAdapter.java @@ -1,94 +1,40 @@ 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 androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; + +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; -import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener; -import java.util.ArrayList; -import java.util.List; +public class EmojiPageViewGridAdapter extends MappingAdapter implements PopupWindow.OnDismissListener { -public class EmojiPageViewGridAdapter extends RecyclerView.Adapter implements PopupWindow.OnDismissListener { + private final VariationSelectorListener variationSelectorListener; - private final List emojiList; - private final EmojiProvider emojiProvider; - private final EmojiVariationSelectorPopup popup; - private final VariationSelectorListener variationSelectorListener; - private final EmojiEventListener emojiEventListener; - - 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); - } - @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)); - } - - @Override - public void onBindViewHolder(@NonNull EmojiViewHolder viewHolder, int i) { - Emoji emoji = emojiList.get(i); - - Drawable drawable = emojiProvider.getEmojiDrawable(emoji.getValue()); - - if (drawable != null) { - viewHolder.textView.setVisibility(View.GONE); - viewHolder.imageView.setVisibility(View.VISIBLE); - - viewHolder.imageView.setImageDrawable(drawable); - } else { - viewHolder.textView.setVisibility(View.VISIBLE); - viewHolder.imageView.setVisibility(View.GONE); - - viewHolder.textView.setEmoji(emoji.getValue()); - } - - viewHolder.itemView.setOnClickListener(v -> { - emojiEventListener.onEmojiSelected(emoji.getValue()); - }); - - 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); - } - } - - @Override - public int getItemCount() { - return emojiList.size(); - } - - public void setEmoji(@NonNull List emojiList) { - this.emojiList.clear(); - this.emojiList.addAll(emojiList); - notifyDataSetChanged(); + 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)); } @Override @@ -96,18 +42,196 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter, HasKey { - private final ImageView imageView; - private final AsciiEmojiView textView; - private final ImageView hintCorner; + private final String key; + private final int title; - public EmojiViewHolder(@NonNull View itemView) { - 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); + public EmojiHeader(@NonNull String key, int title) { + this.key = key; + this.title = title; } + + @Override + public @NonNull String getKey() { + return key; + } + + @Override + public boolean areItemsTheSame(@NonNull EmojiHeader newItem) { + return title == newItem.title; + } + + @Override + public boolean areContentsTheSame(@NonNull EmojiHeader newItem) { + return areItemsTheSame(newItem); + } + } + + static class EmojiHeaderViewHolder extends MappingViewHolder { + + 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); + } + } + + public static class EmojiModel implements MappingModel, 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); + } + } + + static class EmojiViewHolder extends MappingViewHolder { + + 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); + } + } + } + + public static class EmojiTextModel implements MappingModel, 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 EmojiTextViewHolder extends MappingViewHolder { + + private final EmojiEventListener emojiEventListener; + private final AsciiEmojiView textView; + + public EmojiTextViewHolder(@NonNull View itemView, + @NonNull EmojiEventListener emojiEventListener) + { + super(itemView); + + 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 { + @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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPages.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPages.java index 436880edef..78d085fb71 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPages.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiPages.java @@ -1,7 +1,10 @@ package org.thoughtcrime.securesms.components.emoji; +import android.net.Uri; + import network.loki.messenger.R; import org.session.libsignal.utilities.Pair; +import org.thoughtcrime.securesms.emoji.EmojiCategory; import java.util.Arrays; import java.util.LinkedList; @@ -9,53 +12,53 @@ import java.util.List; class EmojiPages { - private static final EmojiPageModel PAGE_PEOPLE_0 = new StaticEmojiPageModel(R.attr.emoji_category_people, new Emoji[] { - new Emoji("\ud83d\ude00"), new Emoji("\ud83d\ude01"), new Emoji("\ud83d\ude02"), new Emoji("\ud83e\udd23"), new Emoji("\ud83d\ude03"), new Emoji("\ud83d\ude04"), new Emoji("\ud83d\ude05"), new Emoji("\ud83d\ude06"), new Emoji("\ud83d\ude09"), new Emoji("\ud83d\ude0a"), new Emoji("\ud83d\ude0b"), new Emoji("\ud83d\ude0e"), new Emoji("\ud83d\ude0d"), new Emoji("\ud83d\ude18"), new Emoji("\ud83d\ude17"), new Emoji("\ud83d\ude19"), new Emoji("\ud83d\ude1a"), new Emoji("\u263a\ufe0f"), new Emoji("\ud83d\ude42"), new Emoji("\ud83e\udd17"), new Emoji("\ud83e\udd29"), new Emoji("\ud83e\udd14"), new Emoji("\ud83e\udd28"), new Emoji("\ud83d\ude10"), new Emoji("\ud83d\ude11"), new Emoji("\ud83d\ude36"), new Emoji("\ud83d\ude44"), new Emoji("\ud83d\ude0f"), new Emoji("\ud83d\ude23"), new Emoji("\ud83d\ude25"), new Emoji("\ud83d\ude2e"), new Emoji("\ud83e\udd10"), new Emoji("\ud83d\ude2f"), new Emoji("\ud83d\ude2a"), new Emoji("\ud83d\ude2b"), new Emoji("\ud83d\ude34"), new Emoji("\ud83d\ude0c"), new Emoji("\ud83d\ude1b"), new Emoji("\ud83d\ude1c"), new Emoji("\ud83d\ude1d"), new Emoji("\ud83e\udd24"), new Emoji("\ud83d\ude12"), new Emoji("\ud83d\ude13"), new Emoji("\ud83d\ude14"), new Emoji("\ud83d\ude15"), new Emoji("\ud83d\ude43"), new Emoji("\ud83e\udd11"), new Emoji("\ud83d\ude32"), new Emoji("\u2639\ufe0f"), new Emoji("\ud83d\ude41"), new Emoji("\ud83d\ude16"), new Emoji("\ud83d\ude1e"), new Emoji("\ud83d\ude1f"), new Emoji("\ud83d\ude24"), new Emoji("\ud83d\ude22"), new Emoji("\ud83d\ude2d"), new Emoji("\ud83d\ude26"), new Emoji("\ud83d\ude27"), new Emoji("\ud83d\ude28"), new Emoji("\ud83d\ude29"), new Emoji("\ud83e\udd2f"), new Emoji("\ud83d\ude2c"), new Emoji("\ud83d\ude30"), new Emoji("\ud83d\ude31"), new Emoji("\ud83d\ude33"), new Emoji("\ud83e\udd2a"), new Emoji("\ud83d\ude35"), new Emoji("\ud83d\ude21"), new Emoji("\ud83d\ude20"), new Emoji("\ud83e\udd2c"), new Emoji("\ud83d\ude37"), new Emoji("\ud83e\udd12"), new Emoji("\ud83e\udd15"), new Emoji("\ud83e\udd22"), new Emoji("\ud83e\udd2e"), new Emoji("\ud83e\udd27"), new Emoji("\ud83d\ude07"), new Emoji("\ud83e\udd20"), new Emoji("\ud83e\udd21"), new Emoji("\ud83e\udd25"), new Emoji("\ud83e\udd2b"), new Emoji("\ud83e\udd2d"), new Emoji("\ud83e\uddd0"), new Emoji("\ud83e\udd13"), new Emoji("\ud83d\ude08"), new Emoji("\ud83d\udc7f"), new Emoji("\ud83d\udc79"), new Emoji("\ud83d\udc7a"), new Emoji("\ud83d\udc80"), new Emoji("\u2620\ufe0f"), new Emoji("\ud83d\udc7b"), new Emoji("\ud83d\udc7d"), new Emoji("\ud83d\udc7e"), new Emoji("\ud83e\udd16"), new Emoji("\ud83d\udca9"), new Emoji("\ud83d\ude3a"), new Emoji("\ud83d\ude38"), new Emoji("\ud83d\ude39"), new Emoji("\ud83d\ude3b"), new Emoji("\ud83d\ude3c"), new Emoji("\ud83d\ude3d"), new Emoji("\ud83d\ude40"), new Emoji("\ud83d\ude3f"), new Emoji("\ud83d\ude3e"), new Emoji("\ud83d\ude48"), new Emoji("\ud83d\ude49"), new Emoji("\ud83d\ude4a"), new Emoji("\ud83d\udc76", "\ud83d\udc76\ud83c\udffb", "\ud83d\udc76\ud83c\udffc", "\ud83d\udc76\ud83c\udffd", "\ud83d\udc76\ud83c\udffe", "\ud83d\udc76\ud83c\udfff"), new Emoji("\ud83e\uddd2", "\ud83e\uddd2\ud83c\udffb", "\ud83e\uddd2\ud83c\udffc", "\ud83e\uddd2\ud83c\udffd", "\ud83e\uddd2\ud83c\udffe", "\ud83e\uddd2\ud83c\udfff"), new Emoji("\ud83d\udc66", "\ud83d\udc66\ud83c\udffb", "\ud83d\udc66\ud83c\udffc", "\ud83d\udc66\ud83c\udffd", "\ud83d\udc66\ud83c\udffe", "\ud83d\udc66\ud83c\udfff"), new Emoji("\ud83d\udc67", "\ud83d\udc67\ud83c\udffb", "\ud83d\udc67\ud83c\udffc", "\ud83d\udc67\ud83c\udffd", "\ud83d\udc67\ud83c\udffe", "\ud83d\udc67\ud83c\udfff"), new Emoji("\ud83e\uddd1", "\ud83e\uddd1\ud83c\udffb", "\ud83e\uddd1\ud83c\udffc", "\ud83e\uddd1\ud83c\udffd", "\ud83e\uddd1\ud83c\udffe", "\ud83e\uddd1\ud83c\udfff"), new Emoji("\ud83d\udc68", "\ud83d\udc68\ud83c\udffb", "\ud83d\udc68\ud83c\udffc", "\ud83d\udc68\ud83c\udffd", "\ud83d\udc68\ud83c\udffe", "\ud83d\udc68\ud83c\udfff"), new Emoji("\ud83d\udc69", "\ud83d\udc69\ud83c\udffb", "\ud83d\udc69\ud83c\udffc", "\ud83d\udc69\ud83c\udffd", "\ud83d\udc69\ud83c\udffe", "\ud83d\udc69\ud83c\udfff"), new Emoji("\ud83e\uddd3", "\ud83e\uddd3\ud83c\udffb", "\ud83e\uddd3\ud83c\udffc", "\ud83e\uddd3\ud83c\udffd", "\ud83e\uddd3\ud83c\udffe", "\ud83e\uddd3\ud83c\udfff"), new Emoji("\ud83d\udc74", "\ud83d\udc74\ud83c\udffb", "\ud83d\udc74\ud83c\udffc", "\ud83d\udc74\ud83c\udffd", "\ud83d\udc74\ud83c\udffe", "\ud83d\udc74\ud83c\udfff"), new Emoji("\ud83d\udc75", "\ud83d\udc75\ud83c\udffb", "\ud83d\udc75\ud83c\udffc", "\ud83d\udc75\ud83c\udffd", "\ud83d\udc75\ud83c\udffe", "\ud83d\udc75\ud83c\udfff"), new Emoji("\ud83d\udc68\u200d\u2695\ufe0f", "\ud83d\udc68\ud83c\udffb\u200d\u2695\ufe0f", "\ud83d\udc68\ud83c\udffc\u200d\u2695\ufe0f", "\ud83d\udc68\ud83c\udffd\u200d\u2695\ufe0f", "\ud83d\udc68\ud83c\udffe\u200d\u2695\ufe0f", "\ud83d\udc68\ud83c\udfff\u200d\u2695\ufe0f"), new Emoji("\ud83d\udc69\u200d\u2695\ufe0f", "\ud83d\udc69\ud83c\udffb\u200d\u2695\ufe0f", "\ud83d\udc69\ud83c\udffc\u200d\u2695\ufe0f", "\ud83d\udc69\ud83c\udffd\u200d\u2695\ufe0f", "\ud83d\udc69\ud83c\udffe\u200d\u2695\ufe0f", "\ud83d\udc69\ud83c\udfff\u200d\u2695\ufe0f"), new Emoji("\ud83d\udc68\u200d\ud83c\udf93", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udf93", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udf93", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udf93", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udf93", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udf93"), new Emoji("\ud83d\udc69\u200d\ud83c\udf93", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udf93", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udf93", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udf93", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udf93", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udf93"), new Emoji("\ud83d\udc68\u200d\ud83c\udfeb", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udfeb", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udfeb", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udfeb", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udfeb", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udfeb"), new Emoji("\ud83d\udc69\u200d\ud83c\udfeb", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udfeb", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udfeb", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udfeb", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udfeb", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udfeb"), new Emoji("\ud83d\udc68\u200d\u2696\ufe0f", "\ud83d\udc68\ud83c\udffb\u200d\u2696\ufe0f", "\ud83d\udc68\ud83c\udffc\u200d\u2696\ufe0f", "\ud83d\udc68\ud83c\udffd\u200d\u2696\ufe0f", "\ud83d\udc68\ud83c\udffe\u200d\u2696\ufe0f", "\ud83d\udc68\ud83c\udfff\u200d\u2696\ufe0f"), new Emoji("\ud83d\udc69\u200d\u2696\ufe0f", "\ud83d\udc69\ud83c\udffb\u200d\u2696\ufe0f", "\ud83d\udc69\ud83c\udffc\u200d\u2696\ufe0f", "\ud83d\udc69\ud83c\udffd\u200d\u2696\ufe0f", "\ud83d\udc69\ud83c\udffe\u200d\u2696\ufe0f", "\ud83d\udc69\ud83c\udfff\u200d\u2696\ufe0f"), new Emoji("\ud83d\udc68\u200d\ud83c\udf3e", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udf3e", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udf3e", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udf3e", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udf3e", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udf3e"), new Emoji("\ud83d\udc69\u200d\ud83c\udf3e", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udf3e", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udf3e", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udf3e", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udf3e", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udf3e"), new Emoji("\ud83d\udc68\u200d\ud83c\udf73", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udf73", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udf73", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udf73", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udf73", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udf73"), new Emoji("\ud83d\udc69\u200d\ud83c\udf73", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udf73", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udf73", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udf73", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udf73", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udf73"), new Emoji("\ud83d\udc68\u200d\ud83d\udd27", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\udd27", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\udd27", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\udd27", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\udd27", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\udd27"), new Emoji("\ud83d\udc69\u200d\ud83d\udd27", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\udd27", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\udd27", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\udd27", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\udd27", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\udd27"), new Emoji("\ud83d\udc68\u200d\ud83c\udfed", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udfed", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udfed", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udfed", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udfed", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udfed"), new Emoji("\ud83d\udc69\u200d\ud83c\udfed", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udfed", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udfed", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udfed", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udfed", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udfed"), new Emoji("\ud83d\udc68\u200d\ud83d\udcbc", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\udcbc", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\udcbc", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\udcbc", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\udcbc", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\udcbc"), new Emoji("\ud83d\udc69\u200d\ud83d\udcbc", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\udcbc", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\udcbc", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\udcbc", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\udcbc", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\udcbc"), new Emoji("\ud83d\udc68\u200d\ud83d\udd2c", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\udd2c", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\udd2c", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\udd2c", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\udd2c", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\udd2c"), new Emoji("\ud83d\udc69\u200d\ud83d\udd2c", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\udd2c", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\udd2c", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\udd2c", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\udd2c", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\udd2c"), new Emoji("\ud83d\udc68\u200d\ud83d\udcbb", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\udcbb", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\udcbb", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\udcbb", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\udcbb", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\udcbb"), new Emoji("\ud83d\udc69\u200d\ud83d\udcbb", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\udcbb", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\udcbb", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\udcbb", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\udcbb", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\udcbb"), new Emoji("\ud83d\udc68\u200d\ud83c\udfa4", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udfa4", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udfa4", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udfa4", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udfa4", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udfa4"), new Emoji("\ud83d\udc69\u200d\ud83c\udfa4", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udfa4", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udfa4", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udfa4", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udfa4", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udfa4"), new Emoji("\ud83d\udc68\u200d\ud83c\udfa8", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udfa8", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udfa8", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udfa8", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udfa8", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udfa8"), new Emoji("\ud83d\udc69\u200d\ud83c\udfa8", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udfa8", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udfa8", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udfa8", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udfa8", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udfa8"), new Emoji("\ud83d\udc68\u200d\u2708\ufe0f", "\ud83d\udc68\ud83c\udffb\u200d\u2708\ufe0f", "\ud83d\udc68\ud83c\udffc\u200d\u2708\ufe0f", "\ud83d\udc68\ud83c\udffd\u200d\u2708\ufe0f", "\ud83d\udc68\ud83c\udffe\u200d\u2708\ufe0f", "\ud83d\udc68\ud83c\udfff\u200d\u2708\ufe0f"), new Emoji("\ud83d\udc69\u200d\u2708\ufe0f", "\ud83d\udc69\ud83c\udffb\u200d\u2708\ufe0f", "\ud83d\udc69\ud83c\udffc\u200d\u2708\ufe0f", "\ud83d\udc69\ud83c\udffd\u200d\u2708\ufe0f", "\ud83d\udc69\ud83c\udffe\u200d\u2708\ufe0f", "\ud83d\udc69\ud83c\udfff\u200d\u2708\ufe0f"), new Emoji("\ud83d\udc68\u200d\ud83d\ude80", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\ude80", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\ude80", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\ude80", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\ude80", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\ude80"), new Emoji("\ud83d\udc69\u200d\ud83d\ude80", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\ude80", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\ude80", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\ude80", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\ude80"), new Emoji("\ud83d\udc68\u200d\ud83d\ude92", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\ude92", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\ude92", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\ude92", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\ude92", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\ude92"), new Emoji("\ud83d\udc69\u200d\ud83d\ude92", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\ude92", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\ude92", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude92", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\ude92", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\ude92"), new Emoji("\ud83d\udc6e\u200d\u2642\ufe0f", "\ud83d\udc6e\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc6e\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc6e\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc6e\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc6e\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc6e\u200d\u2640\ufe0f", "\ud83d\udc6e\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc6e\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc6e\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc6e\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc6e\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udd75\ufe0f\u200d\u2642\ufe0f", "\ud83d\udd75\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udd75\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udd75\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udd75\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udd75\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udd75\ufe0f\u200d\u2640\ufe0f", "\ud83d\udd75\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udd75\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udd75\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udd75\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udd75\ud83c\udfff\u200d\u2640\ufe0f"), - }, "emoji/People_0.png"); + private static final EmojiPageModel PAGE_PEOPLE_0 = new StaticEmojiPageModel(EmojiCategory.PEOPLE, Arrays.asList( + new Emoji("\ud83d\ude00"), new Emoji("\ud83d\ude01"), new Emoji("\ud83d\ude02"), new Emoji("\ud83e\udd23"), new Emoji("\ud83d\ude03"), new Emoji("\ud83d\ude04"), new Emoji("\ud83d\ude05"), new Emoji("\ud83d\ude06"), new Emoji("\ud83d\ude09"), new Emoji("\ud83d\ude0a"), new Emoji("\ud83d\ude0b"), new Emoji("\ud83d\ude0e"), new Emoji("\ud83d\ude0d"), new Emoji("\ud83d\ude18"), new Emoji("\ud83d\ude17"), new Emoji("\ud83d\ude19"), new Emoji("\ud83d\ude1a"), new Emoji("\u263a\ufe0f"), new Emoji("\ud83d\ude42"), new Emoji("\ud83e\udd17"), new Emoji("\ud83e\udd29"), new Emoji("\ud83e\udd14"), new Emoji("\ud83e\udd28"), new Emoji("\ud83d\ude10"), new Emoji("\ud83d\ude11"), new Emoji("\ud83d\ude36"), new Emoji("\ud83d\ude44"), new Emoji("\ud83d\ude0f"), new Emoji("\ud83d\ude23"), new Emoji("\ud83d\ude25"), new Emoji("\ud83d\ude2e"), new Emoji("\ud83e\udd10"), new Emoji("\ud83d\ude2f"), new Emoji("\ud83d\ude2a"), new Emoji("\ud83d\ude2b"), new Emoji("\ud83d\ude34"), new Emoji("\ud83d\ude0c"), new Emoji("\ud83d\ude1b"), new Emoji("\ud83d\ude1c"), new Emoji("\ud83d\ude1d"), new Emoji("\ud83e\udd24"), new Emoji("\ud83d\ude12"), new Emoji("\ud83d\ude13"), new Emoji("\ud83d\ude14"), new Emoji("\ud83d\ude15"), new Emoji("\ud83d\ude43"), new Emoji("\ud83e\udd11"), new Emoji("\ud83d\ude32"), new Emoji("\u2639\ufe0f"), new Emoji("\ud83d\ude41"), new Emoji("\ud83d\ude16"), new Emoji("\ud83d\ude1e"), new Emoji("\ud83d\ude1f"), new Emoji("\ud83d\ude24"), new Emoji("\ud83d\ude22"), new Emoji("\ud83d\ude2d"), new Emoji("\ud83d\ude26"), new Emoji("\ud83d\ude27"), new Emoji("\ud83d\ude28"), new Emoji("\ud83d\ude29"), new Emoji("\ud83e\udd2f"), new Emoji("\ud83d\ude2c"), new Emoji("\ud83d\ude30"), new Emoji("\ud83d\ude31"), new Emoji("\ud83d\ude33"), new Emoji("\ud83e\udd2a"), new Emoji("\ud83d\ude35"), new Emoji("\ud83d\ude21"), new Emoji("\ud83d\ude20"), new Emoji("\ud83e\udd2c"), new Emoji("\ud83d\ude37"), new Emoji("\ud83e\udd12"), new Emoji("\ud83e\udd15"), new Emoji("\ud83e\udd22"), new Emoji("\ud83e\udd2e"), new Emoji("\ud83e\udd27"), new Emoji("\ud83d\ude07"), new Emoji("\ud83e\udd20"), new Emoji("\ud83e\udd21"), new Emoji("\ud83e\udd25"), new Emoji("\ud83e\udd2b"), new Emoji("\ud83e\udd2d"), new Emoji("\ud83e\uddd0"), new Emoji("\ud83e\udd13"), new Emoji("\ud83d\ude08"), new Emoji("\ud83d\udc7f"), new Emoji("\ud83d\udc79"), new Emoji("\ud83d\udc7a"), new Emoji("\ud83d\udc80"), new Emoji("\u2620\ufe0f"), new Emoji("\ud83d\udc7b"), new Emoji("\ud83d\udc7d"), new Emoji("\ud83d\udc7e"), new Emoji("\ud83e\udd16"), new Emoji("\ud83d\udca9"), new Emoji("\ud83d\ude3a"), new Emoji("\ud83d\ude38"), new Emoji("\ud83d\ude39"), new Emoji("\ud83d\ude3b"), new Emoji("\ud83d\ude3c"), new Emoji("\ud83d\ude3d"), new Emoji("\ud83d\ude40"), new Emoji("\ud83d\ude3f"), new Emoji("\ud83d\ude3e"), new Emoji("\ud83d\ude48"), new Emoji("\ud83d\ude49"), new Emoji("\ud83d\ude4a"), new Emoji("\ud83d\udc76", "\ud83d\udc76\ud83c\udffb", "\ud83d\udc76\ud83c\udffc", "\ud83d\udc76\ud83c\udffd", "\ud83d\udc76\ud83c\udffe", "\ud83d\udc76\ud83c\udfff"), new Emoji("\ud83e\uddd2", "\ud83e\uddd2\ud83c\udffb", "\ud83e\uddd2\ud83c\udffc", "\ud83e\uddd2\ud83c\udffd", "\ud83e\uddd2\ud83c\udffe", "\ud83e\uddd2\ud83c\udfff"), new Emoji("\ud83d\udc66", "\ud83d\udc66\ud83c\udffb", "\ud83d\udc66\ud83c\udffc", "\ud83d\udc66\ud83c\udffd", "\ud83d\udc66\ud83c\udffe", "\ud83d\udc66\ud83c\udfff"), new Emoji("\ud83d\udc67", "\ud83d\udc67\ud83c\udffb", "\ud83d\udc67\ud83c\udffc", "\ud83d\udc67\ud83c\udffd", "\ud83d\udc67\ud83c\udffe", "\ud83d\udc67\ud83c\udfff"), new Emoji("\ud83e\uddd1", "\ud83e\uddd1\ud83c\udffb", "\ud83e\uddd1\ud83c\udffc", "\ud83e\uddd1\ud83c\udffd", "\ud83e\uddd1\ud83c\udffe", "\ud83e\uddd1\ud83c\udfff"), new Emoji("\ud83d\udc68", "\ud83d\udc68\ud83c\udffb", "\ud83d\udc68\ud83c\udffc", "\ud83d\udc68\ud83c\udffd", "\ud83d\udc68\ud83c\udffe", "\ud83d\udc68\ud83c\udfff"), new Emoji("\ud83d\udc69", "\ud83d\udc69\ud83c\udffb", "\ud83d\udc69\ud83c\udffc", "\ud83d\udc69\ud83c\udffd", "\ud83d\udc69\ud83c\udffe", "\ud83d\udc69\ud83c\udfff"), new Emoji("\ud83e\uddd3", "\ud83e\uddd3\ud83c\udffb", "\ud83e\uddd3\ud83c\udffc", "\ud83e\uddd3\ud83c\udffd", "\ud83e\uddd3\ud83c\udffe", "\ud83e\uddd3\ud83c\udfff"), new Emoji("\ud83d\udc74", "\ud83d\udc74\ud83c\udffb", "\ud83d\udc74\ud83c\udffc", "\ud83d\udc74\ud83c\udffd", "\ud83d\udc74\ud83c\udffe", "\ud83d\udc74\ud83c\udfff"), new Emoji("\ud83d\udc75", "\ud83d\udc75\ud83c\udffb", "\ud83d\udc75\ud83c\udffc", "\ud83d\udc75\ud83c\udffd", "\ud83d\udc75\ud83c\udffe", "\ud83d\udc75\ud83c\udfff"), new Emoji("\ud83d\udc68\u200d\u2695\ufe0f", "\ud83d\udc68\ud83c\udffb\u200d\u2695\ufe0f", "\ud83d\udc68\ud83c\udffc\u200d\u2695\ufe0f", "\ud83d\udc68\ud83c\udffd\u200d\u2695\ufe0f", "\ud83d\udc68\ud83c\udffe\u200d\u2695\ufe0f", "\ud83d\udc68\ud83c\udfff\u200d\u2695\ufe0f"), new Emoji("\ud83d\udc69\u200d\u2695\ufe0f", "\ud83d\udc69\ud83c\udffb\u200d\u2695\ufe0f", "\ud83d\udc69\ud83c\udffc\u200d\u2695\ufe0f", "\ud83d\udc69\ud83c\udffd\u200d\u2695\ufe0f", "\ud83d\udc69\ud83c\udffe\u200d\u2695\ufe0f", "\ud83d\udc69\ud83c\udfff\u200d\u2695\ufe0f"), new Emoji("\ud83d\udc68\u200d\ud83c\udf93", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udf93", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udf93", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udf93", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udf93", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udf93"), new Emoji("\ud83d\udc69\u200d\ud83c\udf93", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udf93", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udf93", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udf93", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udf93", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udf93"), new Emoji("\ud83d\udc68\u200d\ud83c\udfeb", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udfeb", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udfeb", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udfeb", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udfeb", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udfeb"), new Emoji("\ud83d\udc69\u200d\ud83c\udfeb", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udfeb", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udfeb", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udfeb", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udfeb", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udfeb"), new Emoji("\ud83d\udc68\u200d\u2696\ufe0f", "\ud83d\udc68\ud83c\udffb\u200d\u2696\ufe0f", "\ud83d\udc68\ud83c\udffc\u200d\u2696\ufe0f", "\ud83d\udc68\ud83c\udffd\u200d\u2696\ufe0f", "\ud83d\udc68\ud83c\udffe\u200d\u2696\ufe0f", "\ud83d\udc68\ud83c\udfff\u200d\u2696\ufe0f"), new Emoji("\ud83d\udc69\u200d\u2696\ufe0f", "\ud83d\udc69\ud83c\udffb\u200d\u2696\ufe0f", "\ud83d\udc69\ud83c\udffc\u200d\u2696\ufe0f", "\ud83d\udc69\ud83c\udffd\u200d\u2696\ufe0f", "\ud83d\udc69\ud83c\udffe\u200d\u2696\ufe0f", "\ud83d\udc69\ud83c\udfff\u200d\u2696\ufe0f"), new Emoji("\ud83d\udc68\u200d\ud83c\udf3e", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udf3e", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udf3e", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udf3e", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udf3e", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udf3e"), new Emoji("\ud83d\udc69\u200d\ud83c\udf3e", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udf3e", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udf3e", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udf3e", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udf3e", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udf3e"), new Emoji("\ud83d\udc68\u200d\ud83c\udf73", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udf73", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udf73", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udf73", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udf73", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udf73"), new Emoji("\ud83d\udc69\u200d\ud83c\udf73", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udf73", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udf73", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udf73", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udf73", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udf73"), new Emoji("\ud83d\udc68\u200d\ud83d\udd27", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\udd27", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\udd27", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\udd27", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\udd27", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\udd27"), new Emoji("\ud83d\udc69\u200d\ud83d\udd27", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\udd27", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\udd27", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\udd27", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\udd27", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\udd27"), new Emoji("\ud83d\udc68\u200d\ud83c\udfed", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udfed", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udfed", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udfed", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udfed", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udfed"), new Emoji("\ud83d\udc69\u200d\ud83c\udfed", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udfed", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udfed", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udfed", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udfed", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udfed"), new Emoji("\ud83d\udc68\u200d\ud83d\udcbc", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\udcbc", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\udcbc", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\udcbc", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\udcbc", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\udcbc"), new Emoji("\ud83d\udc69\u200d\ud83d\udcbc", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\udcbc", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\udcbc", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\udcbc", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\udcbc", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\udcbc"), new Emoji("\ud83d\udc68\u200d\ud83d\udd2c", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\udd2c", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\udd2c", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\udd2c", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\udd2c", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\udd2c"), new Emoji("\ud83d\udc69\u200d\ud83d\udd2c", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\udd2c", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\udd2c", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\udd2c", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\udd2c", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\udd2c"), new Emoji("\ud83d\udc68\u200d\ud83d\udcbb", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\udcbb", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\udcbb", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\udcbb", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\udcbb", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\udcbb"), new Emoji("\ud83d\udc69\u200d\ud83d\udcbb", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\udcbb", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\udcbb", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\udcbb", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\udcbb", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\udcbb"), new Emoji("\ud83d\udc68\u200d\ud83c\udfa4", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udfa4", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udfa4", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udfa4", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udfa4", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udfa4"), new Emoji("\ud83d\udc69\u200d\ud83c\udfa4", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udfa4", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udfa4", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udfa4", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udfa4", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udfa4"), new Emoji("\ud83d\udc68\u200d\ud83c\udfa8", "\ud83d\udc68\ud83c\udffb\u200d\ud83c\udfa8", "\ud83d\udc68\ud83c\udffc\u200d\ud83c\udfa8", "\ud83d\udc68\ud83c\udffd\u200d\ud83c\udfa8", "\ud83d\udc68\ud83c\udffe\u200d\ud83c\udfa8", "\ud83d\udc68\ud83c\udfff\u200d\ud83c\udfa8"), new Emoji("\ud83d\udc69\u200d\ud83c\udfa8", "\ud83d\udc69\ud83c\udffb\u200d\ud83c\udfa8", "\ud83d\udc69\ud83c\udffc\u200d\ud83c\udfa8", "\ud83d\udc69\ud83c\udffd\u200d\ud83c\udfa8", "\ud83d\udc69\ud83c\udffe\u200d\ud83c\udfa8", "\ud83d\udc69\ud83c\udfff\u200d\ud83c\udfa8"), new Emoji("\ud83d\udc68\u200d\u2708\ufe0f", "\ud83d\udc68\ud83c\udffb\u200d\u2708\ufe0f", "\ud83d\udc68\ud83c\udffc\u200d\u2708\ufe0f", "\ud83d\udc68\ud83c\udffd\u200d\u2708\ufe0f", "\ud83d\udc68\ud83c\udffe\u200d\u2708\ufe0f", "\ud83d\udc68\ud83c\udfff\u200d\u2708\ufe0f"), new Emoji("\ud83d\udc69\u200d\u2708\ufe0f", "\ud83d\udc69\ud83c\udffb\u200d\u2708\ufe0f", "\ud83d\udc69\ud83c\udffc\u200d\u2708\ufe0f", "\ud83d\udc69\ud83c\udffd\u200d\u2708\ufe0f", "\ud83d\udc69\ud83c\udffe\u200d\u2708\ufe0f", "\ud83d\udc69\ud83c\udfff\u200d\u2708\ufe0f"), new Emoji("\ud83d\udc68\u200d\ud83d\ude80", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\ude80", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\ude80", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\ude80", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\ude80", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\ude80"), new Emoji("\ud83d\udc69\u200d\ud83d\ude80", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\ude80", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\ude80", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude80", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\ude80", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\ude80"), new Emoji("\ud83d\udc68\u200d\ud83d\ude92", "\ud83d\udc68\ud83c\udffb\u200d\ud83d\ude92", "\ud83d\udc68\ud83c\udffc\u200d\ud83d\ude92", "\ud83d\udc68\ud83c\udffd\u200d\ud83d\ude92", "\ud83d\udc68\ud83c\udffe\u200d\ud83d\ude92", "\ud83d\udc68\ud83c\udfff\u200d\ud83d\ude92"), new Emoji("\ud83d\udc69\u200d\ud83d\ude92", "\ud83d\udc69\ud83c\udffb\u200d\ud83d\ude92", "\ud83d\udc69\ud83c\udffc\u200d\ud83d\ude92", "\ud83d\udc69\ud83c\udffd\u200d\ud83d\ude92", "\ud83d\udc69\ud83c\udffe\u200d\ud83d\ude92", "\ud83d\udc69\ud83c\udfff\u200d\ud83d\ude92"), new Emoji("\ud83d\udc6e\u200d\u2642\ufe0f", "\ud83d\udc6e\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc6e\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc6e\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc6e\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc6e\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc6e\u200d\u2640\ufe0f", "\ud83d\udc6e\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc6e\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc6e\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc6e\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc6e\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udd75\ufe0f\u200d\u2642\ufe0f", "\ud83d\udd75\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udd75\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udd75\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udd75\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udd75\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udd75\ufe0f\u200d\u2640\ufe0f", "\ud83d\udd75\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udd75\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udd75\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udd75\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udd75\ud83c\udfff\u200d\u2640\ufe0f") + ), Uri.parse("emoji/People_0.png")); - private static final EmojiPageModel PAGE_PEOPLE_1 = new StaticEmojiPageModel(R.attr.emoji_category_people, new Emoji[] { - new Emoji("\ud83d\udc82\u200d\u2642\ufe0f", "\ud83d\udc82\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc82\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc82\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc82\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc82\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc82\u200d\u2640\ufe0f", "\ud83d\udc82\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc82\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc82\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc82\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc82\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc77\u200d\u2642\ufe0f", "\ud83d\udc77\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc77\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc77\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc77\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc77\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc77\u200d\u2640\ufe0f", "\ud83d\udc77\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc77\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc77\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc77\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc77\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd34", "\ud83e\udd34\ud83c\udffb", "\ud83e\udd34\ud83c\udffc", "\ud83e\udd34\ud83c\udffd", "\ud83e\udd34\ud83c\udffe", "\ud83e\udd34\ud83c\udfff"), new Emoji("\ud83d\udc78", "\ud83d\udc78\ud83c\udffb", "\ud83d\udc78\ud83c\udffc", "\ud83d\udc78\ud83c\udffd", "\ud83d\udc78\ud83c\udffe", "\ud83d\udc78\ud83c\udfff"), new Emoji("\ud83d\udc73\u200d\u2642\ufe0f", "\ud83d\udc73\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc73\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc73\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc73\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc73\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc73\u200d\u2640\ufe0f", "\ud83d\udc73\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc73\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc73\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc73\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc73\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc72", "\ud83d\udc72\ud83c\udffb", "\ud83d\udc72\ud83c\udffc", "\ud83d\udc72\ud83c\udffd", "\ud83d\udc72\ud83c\udffe", "\ud83d\udc72\ud83c\udfff"), new Emoji("\ud83e\uddd5", "\ud83e\uddd5\ud83c\udffb", "\ud83e\uddd5\ud83c\udffc", "\ud83e\uddd5\ud83c\udffd", "\ud83e\uddd5\ud83c\udffe", "\ud83e\uddd5\ud83c\udfff"), new Emoji("\ud83e\uddd4", "\ud83e\uddd4\ud83c\udffb", "\ud83e\uddd4\ud83c\udffc", "\ud83e\uddd4\ud83c\udffd", "\ud83e\uddd4\ud83c\udffe", "\ud83e\uddd4\ud83c\udfff"), new Emoji("\ud83d\udc71\u200d\u2642\ufe0f", "\ud83d\udc71\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc71\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc71\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc71\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc71\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc71\u200d\u2640\ufe0f", "\ud83d\udc71\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc71\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc71\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc71\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc71\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd35", "\ud83e\udd35\ud83c\udffb", "\ud83e\udd35\ud83c\udffc", "\ud83e\udd35\ud83c\udffd", "\ud83e\udd35\ud83c\udffe", "\ud83e\udd35\ud83c\udfff"), new Emoji("\ud83d\udc70", "\ud83d\udc70\ud83c\udffb", "\ud83d\udc70\ud83c\udffc", "\ud83d\udc70\ud83c\udffd", "\ud83d\udc70\ud83c\udffe", "\ud83d\udc70\ud83c\udfff"), new Emoji("\ud83e\udd30", "\ud83e\udd30\ud83c\udffb", "\ud83e\udd30\ud83c\udffc", "\ud83e\udd30\ud83c\udffd", "\ud83e\udd30\ud83c\udffe", "\ud83e\udd30\ud83c\udfff"), new Emoji("\ud83e\udd31", "\ud83e\udd31\ud83c\udffb", "\ud83e\udd31\ud83c\udffc", "\ud83e\udd31\ud83c\udffd", "\ud83e\udd31\ud83c\udffe", "\ud83e\udd31\ud83c\udfff"), new Emoji("\ud83d\udc7c", "\ud83d\udc7c\ud83c\udffb", "\ud83d\udc7c\ud83c\udffc", "\ud83d\udc7c\ud83c\udffd", "\ud83d\udc7c\ud83c\udffe", "\ud83d\udc7c\ud83c\udfff"), new Emoji("\ud83c\udf85", "\ud83c\udf85\ud83c\udffb", "\ud83c\udf85\ud83c\udffc", "\ud83c\udf85\ud83c\udffd", "\ud83c\udf85\ud83c\udffe", "\ud83c\udf85\ud83c\udfff"), new Emoji("\ud83e\udd36", "\ud83e\udd36\ud83c\udffb", "\ud83e\udd36\ud83c\udffc", "\ud83e\udd36\ud83c\udffd", "\ud83e\udd36\ud83c\udffe", "\ud83e\udd36\ud83c\udfff"), new Emoji("\ud83e\uddd9\u200d\u2640\ufe0f", "\ud83e\uddd9\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\uddd9\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\uddd9\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\uddd9\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\uddd9\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddd9\u200d\u2642\ufe0f", "\ud83e\uddd9\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\uddd9\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\uddd9\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\uddd9\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\uddd9\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\uddda\u200d\u2640\ufe0f", "\ud83e\uddda\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\uddda\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\uddda\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\uddda\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\uddda\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddda\u200d\u2642\ufe0f", "\ud83e\uddda\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\uddda\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\uddda\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\uddda\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\uddda\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udddb\u200d\u2640\ufe0f", "\ud83e\udddb\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udddb\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udddb\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udddb\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udddb\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udddb\u200d\u2642\ufe0f", "\ud83e\udddb\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udddb\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udddb\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udddb\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udddb\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udddc\u200d\u2640\ufe0f", "\ud83e\udddc\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udddc\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udddc\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udddc\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udddc\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udddc\u200d\u2642\ufe0f", "\ud83e\udddc\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udddc\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udddc\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udddc\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udddc\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udddd\u200d\u2640\ufe0f", "\ud83e\udddd\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udddd\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udddd\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udddd\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udddd\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udddd\u200d\u2642\ufe0f", "\ud83e\udddd\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udddd\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udddd\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udddd\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udddd\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\uddde\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddde\u200d\u2642\ufe0f"), new Emoji("\ud83e\udddf\u200d\u2640\ufe0f"), new Emoji("\ud83e\udddf\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude4d\u200d\u2642\ufe0f", "\ud83d\ude4d\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude4d\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude4d\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude4d\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude4d\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude4d\u200d\u2640\ufe0f", "\ud83d\ude4d\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude4d\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude4d\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude4d\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude4d\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\ude4e\u200d\u2642\ufe0f", "\ud83d\ude4e\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude4e\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude4e\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude4e\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude4e\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude4e\u200d\u2640\ufe0f", "\ud83d\ude4e\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude4e\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude4e\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude4e\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude4e\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\ude45\u200d\u2642\ufe0f", "\ud83d\ude45\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude45\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude45\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude45\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude45\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude45\u200d\u2640\ufe0f", "\ud83d\ude45\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude45\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude45\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude45\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude45\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\ude46\u200d\u2642\ufe0f", "\ud83d\ude46\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude46\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude46\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude46\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude46\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude46\u200d\u2640\ufe0f", "\ud83d\ude46\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude46\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude46\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude46\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude46\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc81\u200d\u2642\ufe0f", "\ud83d\udc81\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc81\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc81\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc81\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc81\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc81\u200d\u2640\ufe0f", "\ud83d\udc81\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc81\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc81\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc81\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc81\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\ude4b\u200d\u2642\ufe0f", "\ud83d\ude4b\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude4b\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude4b\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude4b\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude4b\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude4b\u200d\u2640\ufe0f", "\ud83d\ude4b\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude4b\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude4b\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude4b\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude4b\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\ude47\u200d\u2642\ufe0f", "\ud83d\ude47\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude47\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude47\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude47\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude47\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude47\u200d\u2640\ufe0f", "\ud83d\ude47\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude47\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude47\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude47\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude47\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd26", "\ud83e\udd26\ud83c\udffb", "\ud83e\udd26\ud83c\udffc", "\ud83e\udd26\ud83c\udffd", "\ud83e\udd26\ud83c\udffe", "\ud83e\udd26\ud83c\udfff"), new Emoji("\ud83e\udd26\u200d\u2642\ufe0f", "\ud83e\udd26\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd26\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd26\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd26\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd26\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd26\u200d\u2640\ufe0f", "\ud83e\udd26\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd26\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd26\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd26\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd26\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd37", "\ud83e\udd37\ud83c\udffb", "\ud83e\udd37\ud83c\udffc", "\ud83e\udd37\ud83c\udffd", "\ud83e\udd37\ud83c\udffe", "\ud83e\udd37\ud83c\udfff"), new Emoji("\ud83e\udd37\u200d\u2642\ufe0f", "\ud83e\udd37\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd37\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd37\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd37\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd37\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd37\u200d\u2640\ufe0f", "\ud83e\udd37\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd37\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd37\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd37\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd37\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc86\u200d\u2642\ufe0f", "\ud83d\udc86\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc86\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc86\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc86\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc86\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc86\u200d\u2640\ufe0f", "\ud83d\udc86\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc86\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc86\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc86\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc86\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc87\u200d\u2642\ufe0f", "\ud83d\udc87\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc87\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc87\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc87\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc87\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc87\u200d\u2640\ufe0f", "\ud83d\udc87\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc87\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc87\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc87\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc87\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udeb6\u200d\u2642\ufe0f", "\ud83d\udeb6\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udeb6\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udeb6\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udeb6\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udeb6\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udeb6\u200d\u2640\ufe0f", "\ud83d\udeb6\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udeb6\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udeb6\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udeb6\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udeb6\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83c\udfc3\u200d\u2642\ufe0f", "\ud83c\udfc3\ud83c\udffb\u200d\u2642\ufe0f", "\ud83c\udfc3\ud83c\udffc\u200d\u2642\ufe0f", "\ud83c\udfc3\ud83c\udffd\u200d\u2642\ufe0f", "\ud83c\udfc3\ud83c\udffe\u200d\u2642\ufe0f", "\ud83c\udfc3\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83c\udfc3\u200d\u2640\ufe0f", "\ud83c\udfc3\ud83c\udffb\u200d\u2640\ufe0f", "\ud83c\udfc3\ud83c\udffc\u200d\u2640\ufe0f", "\ud83c\udfc3\ud83c\udffd\u200d\u2640\ufe0f", "\ud83c\udfc3\ud83c\udffe\u200d\u2640\ufe0f", "\ud83c\udfc3\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc83", "\ud83d\udc83\ud83c\udffb", "\ud83d\udc83\ud83c\udffc", "\ud83d\udc83\ud83c\udffd", "\ud83d\udc83\ud83c\udffe", "\ud83d\udc83\ud83c\udfff"), new Emoji("\ud83d\udd7a", "\ud83d\udd7a\ud83c\udffb", "\ud83d\udd7a\ud83c\udffc", "\ud83d\udd7a\ud83c\udffd", "\ud83d\udd7a\ud83c\udffe", "\ud83d\udd7a\ud83c\udfff"), new Emoji("\ud83d\udc6f\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc6f\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddd6\u200d\u2640\ufe0f", "\ud83e\uddd6\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\uddd6\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\uddd6\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\uddd6\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\uddd6\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddd6\u200d\u2642\ufe0f", "\ud83e\uddd6\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\uddd6\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\uddd6\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\uddd6\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\uddd6\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\uddd7\u200d\u2640\ufe0f", "\ud83e\uddd7\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\uddd7\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\uddd7\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\uddd7\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\uddd7\ud83c\udfff\u200d\u2640\ufe0f"), - }, "emoji/People_1.png"); + private static final EmojiPageModel PAGE_PEOPLE_1 = new StaticEmojiPageModel(EmojiCategory.PEOPLE, Arrays.asList( + new Emoji("\ud83d\udc82\u200d\u2642\ufe0f", "\ud83d\udc82\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc82\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc82\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc82\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc82\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc82\u200d\u2640\ufe0f", "\ud83d\udc82\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc82\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc82\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc82\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc82\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc77\u200d\u2642\ufe0f", "\ud83d\udc77\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc77\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc77\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc77\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc77\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc77\u200d\u2640\ufe0f", "\ud83d\udc77\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc77\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc77\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc77\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc77\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd34", "\ud83e\udd34\ud83c\udffb", "\ud83e\udd34\ud83c\udffc", "\ud83e\udd34\ud83c\udffd", "\ud83e\udd34\ud83c\udffe", "\ud83e\udd34\ud83c\udfff"), new Emoji("\ud83d\udc78", "\ud83d\udc78\ud83c\udffb", "\ud83d\udc78\ud83c\udffc", "\ud83d\udc78\ud83c\udffd", "\ud83d\udc78\ud83c\udffe", "\ud83d\udc78\ud83c\udfff"), new Emoji("\ud83d\udc73\u200d\u2642\ufe0f", "\ud83d\udc73\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc73\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc73\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc73\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc73\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc73\u200d\u2640\ufe0f", "\ud83d\udc73\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc73\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc73\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc73\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc73\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc72", "\ud83d\udc72\ud83c\udffb", "\ud83d\udc72\ud83c\udffc", "\ud83d\udc72\ud83c\udffd", "\ud83d\udc72\ud83c\udffe", "\ud83d\udc72\ud83c\udfff"), new Emoji("\ud83e\uddd5", "\ud83e\uddd5\ud83c\udffb", "\ud83e\uddd5\ud83c\udffc", "\ud83e\uddd5\ud83c\udffd", "\ud83e\uddd5\ud83c\udffe", "\ud83e\uddd5\ud83c\udfff"), new Emoji("\ud83e\uddd4", "\ud83e\uddd4\ud83c\udffb", "\ud83e\uddd4\ud83c\udffc", "\ud83e\uddd4\ud83c\udffd", "\ud83e\uddd4\ud83c\udffe", "\ud83e\uddd4\ud83c\udfff"), new Emoji("\ud83d\udc71\u200d\u2642\ufe0f", "\ud83d\udc71\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc71\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc71\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc71\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc71\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc71\u200d\u2640\ufe0f", "\ud83d\udc71\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc71\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc71\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc71\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc71\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd35", "\ud83e\udd35\ud83c\udffb", "\ud83e\udd35\ud83c\udffc", "\ud83e\udd35\ud83c\udffd", "\ud83e\udd35\ud83c\udffe", "\ud83e\udd35\ud83c\udfff"), new Emoji("\ud83d\udc70", "\ud83d\udc70\ud83c\udffb", "\ud83d\udc70\ud83c\udffc", "\ud83d\udc70\ud83c\udffd", "\ud83d\udc70\ud83c\udffe", "\ud83d\udc70\ud83c\udfff"), new Emoji("\ud83e\udd30", "\ud83e\udd30\ud83c\udffb", "\ud83e\udd30\ud83c\udffc", "\ud83e\udd30\ud83c\udffd", "\ud83e\udd30\ud83c\udffe", "\ud83e\udd30\ud83c\udfff"), new Emoji("\ud83e\udd31", "\ud83e\udd31\ud83c\udffb", "\ud83e\udd31\ud83c\udffc", "\ud83e\udd31\ud83c\udffd", "\ud83e\udd31\ud83c\udffe", "\ud83e\udd31\ud83c\udfff"), new Emoji("\ud83d\udc7c", "\ud83d\udc7c\ud83c\udffb", "\ud83d\udc7c\ud83c\udffc", "\ud83d\udc7c\ud83c\udffd", "\ud83d\udc7c\ud83c\udffe", "\ud83d\udc7c\ud83c\udfff"), new Emoji("\ud83c\udf85", "\ud83c\udf85\ud83c\udffb", "\ud83c\udf85\ud83c\udffc", "\ud83c\udf85\ud83c\udffd", "\ud83c\udf85\ud83c\udffe", "\ud83c\udf85\ud83c\udfff"), new Emoji("\ud83e\udd36", "\ud83e\udd36\ud83c\udffb", "\ud83e\udd36\ud83c\udffc", "\ud83e\udd36\ud83c\udffd", "\ud83e\udd36\ud83c\udffe", "\ud83e\udd36\ud83c\udfff"), new Emoji("\ud83e\uddd9\u200d\u2640\ufe0f", "\ud83e\uddd9\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\uddd9\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\uddd9\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\uddd9\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\uddd9\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddd9\u200d\u2642\ufe0f", "\ud83e\uddd9\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\uddd9\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\uddd9\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\uddd9\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\uddd9\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\uddda\u200d\u2640\ufe0f", "\ud83e\uddda\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\uddda\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\uddda\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\uddda\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\uddda\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddda\u200d\u2642\ufe0f", "\ud83e\uddda\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\uddda\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\uddda\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\uddda\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\uddda\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udddb\u200d\u2640\ufe0f", "\ud83e\udddb\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udddb\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udddb\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udddb\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udddb\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udddb\u200d\u2642\ufe0f", "\ud83e\udddb\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udddb\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udddb\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udddb\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udddb\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udddc\u200d\u2640\ufe0f", "\ud83e\udddc\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udddc\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udddc\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udddc\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udddc\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udddc\u200d\u2642\ufe0f", "\ud83e\udddc\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udddc\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udddc\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udddc\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udddc\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udddd\u200d\u2640\ufe0f", "\ud83e\udddd\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udddd\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udddd\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udddd\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udddd\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udddd\u200d\u2642\ufe0f", "\ud83e\udddd\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udddd\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udddd\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udddd\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udddd\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\uddde\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddde\u200d\u2642\ufe0f"), new Emoji("\ud83e\udddf\u200d\u2640\ufe0f"), new Emoji("\ud83e\udddf\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude4d\u200d\u2642\ufe0f", "\ud83d\ude4d\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude4d\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude4d\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude4d\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude4d\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude4d\u200d\u2640\ufe0f", "\ud83d\ude4d\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude4d\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude4d\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude4d\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude4d\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\ude4e\u200d\u2642\ufe0f", "\ud83d\ude4e\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude4e\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude4e\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude4e\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude4e\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude4e\u200d\u2640\ufe0f", "\ud83d\ude4e\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude4e\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude4e\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude4e\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude4e\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\ude45\u200d\u2642\ufe0f", "\ud83d\ude45\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude45\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude45\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude45\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude45\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude45\u200d\u2640\ufe0f", "\ud83d\ude45\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude45\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude45\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude45\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude45\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\ude46\u200d\u2642\ufe0f", "\ud83d\ude46\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude46\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude46\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude46\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude46\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude46\u200d\u2640\ufe0f", "\ud83d\ude46\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude46\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude46\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude46\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude46\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc81\u200d\u2642\ufe0f", "\ud83d\udc81\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc81\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc81\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc81\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc81\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc81\u200d\u2640\ufe0f", "\ud83d\udc81\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc81\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc81\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc81\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc81\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\ude4b\u200d\u2642\ufe0f", "\ud83d\ude4b\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude4b\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude4b\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude4b\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude4b\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude4b\u200d\u2640\ufe0f", "\ud83d\ude4b\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude4b\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude4b\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude4b\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude4b\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\ude47\u200d\u2642\ufe0f", "\ud83d\ude47\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\ude47\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\ude47\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\ude47\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\ude47\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\ude47\u200d\u2640\ufe0f", "\ud83d\ude47\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\ude47\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\ude47\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\ude47\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\ude47\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd26", "\ud83e\udd26\ud83c\udffb", "\ud83e\udd26\ud83c\udffc", "\ud83e\udd26\ud83c\udffd", "\ud83e\udd26\ud83c\udffe", "\ud83e\udd26\ud83c\udfff"), new Emoji("\ud83e\udd26\u200d\u2642\ufe0f", "\ud83e\udd26\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd26\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd26\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd26\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd26\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd26\u200d\u2640\ufe0f", "\ud83e\udd26\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd26\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd26\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd26\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd26\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd37", "\ud83e\udd37\ud83c\udffb", "\ud83e\udd37\ud83c\udffc", "\ud83e\udd37\ud83c\udffd", "\ud83e\udd37\ud83c\udffe", "\ud83e\udd37\ud83c\udfff"), new Emoji("\ud83e\udd37\u200d\u2642\ufe0f", "\ud83e\udd37\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd37\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd37\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd37\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd37\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd37\u200d\u2640\ufe0f", "\ud83e\udd37\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd37\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd37\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd37\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd37\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc86\u200d\u2642\ufe0f", "\ud83d\udc86\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc86\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc86\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc86\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc86\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc86\u200d\u2640\ufe0f", "\ud83d\udc86\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc86\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc86\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc86\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc86\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc87\u200d\u2642\ufe0f", "\ud83d\udc87\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udc87\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udc87\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udc87\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udc87\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc87\u200d\u2640\ufe0f", "\ud83d\udc87\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udc87\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udc87\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udc87\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udc87\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udeb6\u200d\u2642\ufe0f", "\ud83d\udeb6\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udeb6\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udeb6\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udeb6\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udeb6\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udeb6\u200d\u2640\ufe0f", "\ud83d\udeb6\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udeb6\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udeb6\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udeb6\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udeb6\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83c\udfc3\u200d\u2642\ufe0f", "\ud83c\udfc3\ud83c\udffb\u200d\u2642\ufe0f", "\ud83c\udfc3\ud83c\udffc\u200d\u2642\ufe0f", "\ud83c\udfc3\ud83c\udffd\u200d\u2642\ufe0f", "\ud83c\udfc3\ud83c\udffe\u200d\u2642\ufe0f", "\ud83c\udfc3\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83c\udfc3\u200d\u2640\ufe0f", "\ud83c\udfc3\ud83c\udffb\u200d\u2640\ufe0f", "\ud83c\udfc3\ud83c\udffc\u200d\u2640\ufe0f", "\ud83c\udfc3\ud83c\udffd\u200d\u2640\ufe0f", "\ud83c\udfc3\ud83c\udffe\u200d\u2640\ufe0f", "\ud83c\udfc3\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc83", "\ud83d\udc83\ud83c\udffb", "\ud83d\udc83\ud83c\udffc", "\ud83d\udc83\ud83c\udffd", "\ud83d\udc83\ud83c\udffe", "\ud83d\udc83\ud83c\udfff"), new Emoji("\ud83d\udd7a", "\ud83d\udd7a\ud83c\udffb", "\ud83d\udd7a\ud83c\udffc", "\ud83d\udd7a\ud83c\udffd", "\ud83d\udd7a\ud83c\udffe", "\ud83d\udd7a\ud83c\udfff"), new Emoji("\ud83d\udc6f\u200d\u2642\ufe0f"), new Emoji("\ud83d\udc6f\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddd6\u200d\u2640\ufe0f", "\ud83e\uddd6\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\uddd6\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\uddd6\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\uddd6\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\uddd6\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddd6\u200d\u2642\ufe0f", "\ud83e\uddd6\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\uddd6\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\uddd6\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\uddd6\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\uddd6\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\uddd7\u200d\u2640\ufe0f", "\ud83e\uddd7\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\uddd7\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\uddd7\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\uddd7\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\uddd7\ud83c\udfff\u200d\u2640\ufe0f") + ), Uri.parse("emoji/People_1.png")); - private static final EmojiPageModel PAGE_PEOPLE_2 = new StaticEmojiPageModel(R.attr.emoji_category_people, new Emoji[] { - new Emoji("\ud83e\uddd7\u200d\u2642\ufe0f", "\ud83e\uddd7\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\uddd7\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\uddd7\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\uddd7\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\uddd7\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\uddd8\u200d\u2640\ufe0f", "\ud83e\uddd8\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\uddd8\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\uddd8\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\uddd8\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\uddd8\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddd8\u200d\u2642\ufe0f", "\ud83e\uddd8\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\uddd8\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\uddd8\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\uddd8\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\uddd8\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udec0", "\ud83d\udec0\ud83c\udffb", "\ud83d\udec0\ud83c\udffc", "\ud83d\udec0\ud83c\udffd", "\ud83d\udec0\ud83c\udffe", "\ud83d\udec0\ud83c\udfff"), new Emoji("\ud83d\udecc", "\ud83d\udecc\ud83c\udffb", "\ud83d\udecc\ud83c\udffc", "\ud83d\udecc\ud83c\udffd", "\ud83d\udecc\ud83c\udffe", "\ud83d\udecc\ud83c\udfff"), new Emoji("\ud83d\udd74\ufe0f", "\ud83d\udd74\ud83c\udffb", "\ud83d\udd74\ud83c\udffc", "\ud83d\udd74\ud83c\udffd", "\ud83d\udd74\ud83c\udffe", "\ud83d\udd74\ud83c\udfff"), new Emoji("\ud83d\udde3\ufe0f"), new Emoji("\ud83d\udc64"), new Emoji("\ud83d\udc65"), new Emoji("\ud83e\udd3a"), new Emoji("\ud83c\udfc7", "\ud83c\udfc7\ud83c\udffb", "\ud83c\udfc7\ud83c\udffc", "\ud83c\udfc7\ud83c\udffd", "\ud83c\udfc7\ud83c\udffe", "\ud83c\udfc7\ud83c\udfff"), new Emoji("\u26f7\ufe0f"), new Emoji("\ud83c\udfc2", "\ud83c\udfc2\ud83c\udffb", "\ud83c\udfc2\ud83c\udffc", "\ud83c\udfc2\ud83c\udffd", "\ud83c\udfc2\ud83c\udffe", "\ud83c\udfc2\ud83c\udfff"), new Emoji("\ud83c\udfcc\ufe0f\u200d\u2642\ufe0f", "\ud83c\udfcc\ud83c\udffb\u200d\u2642\ufe0f", "\ud83c\udfcc\ud83c\udffc\u200d\u2642\ufe0f", "\ud83c\udfcc\ud83c\udffd\u200d\u2642\ufe0f", "\ud83c\udfcc\ud83c\udffe\u200d\u2642\ufe0f", "\ud83c\udfcc\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83c\udfcc\ufe0f\u200d\u2640\ufe0f", "\ud83c\udfcc\ud83c\udffb\u200d\u2640\ufe0f", "\ud83c\udfcc\ud83c\udffc\u200d\u2640\ufe0f", "\ud83c\udfcc\ud83c\udffd\u200d\u2640\ufe0f", "\ud83c\udfcc\ud83c\udffe\u200d\u2640\ufe0f", "\ud83c\udfcc\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83c\udfc4\u200d\u2642\ufe0f", "\ud83c\udfc4\ud83c\udffb\u200d\u2642\ufe0f", "\ud83c\udfc4\ud83c\udffc\u200d\u2642\ufe0f", "\ud83c\udfc4\ud83c\udffd\u200d\u2642\ufe0f", "\ud83c\udfc4\ud83c\udffe\u200d\u2642\ufe0f", "\ud83c\udfc4\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83c\udfc4\u200d\u2640\ufe0f", "\ud83c\udfc4\ud83c\udffb\u200d\u2640\ufe0f", "\ud83c\udfc4\ud83c\udffc\u200d\u2640\ufe0f", "\ud83c\udfc4\ud83c\udffd\u200d\u2640\ufe0f", "\ud83c\udfc4\ud83c\udffe\u200d\u2640\ufe0f", "\ud83c\udfc4\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udea3\u200d\u2642\ufe0f", "\ud83d\udea3\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udea3\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udea3\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udea3\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udea3\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udea3\u200d\u2640\ufe0f", "\ud83d\udea3\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udea3\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udea3\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udea3\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udea3\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83c\udfca\u200d\u2642\ufe0f", "\ud83c\udfca\ud83c\udffb\u200d\u2642\ufe0f", "\ud83c\udfca\ud83c\udffc\u200d\u2642\ufe0f", "\ud83c\udfca\ud83c\udffd\u200d\u2642\ufe0f", "\ud83c\udfca\ud83c\udffe\u200d\u2642\ufe0f", "\ud83c\udfca\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83c\udfca\u200d\u2640\ufe0f", "\ud83c\udfca\ud83c\udffb\u200d\u2640\ufe0f", "\ud83c\udfca\ud83c\udffc\u200d\u2640\ufe0f", "\ud83c\udfca\ud83c\udffd\u200d\u2640\ufe0f", "\ud83c\udfca\ud83c\udffe\u200d\u2640\ufe0f", "\ud83c\udfca\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\u26f9\ufe0f\u200d\u2642\ufe0f", "\u26f9\ud83c\udffb\u200d\u2642\ufe0f", "\u26f9\ud83c\udffc\u200d\u2642\ufe0f", "\u26f9\ud83c\udffd\u200d\u2642\ufe0f", "\u26f9\ud83c\udffe\u200d\u2642\ufe0f", "\u26f9\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\u26f9\ufe0f\u200d\u2640\ufe0f", "\u26f9\ud83c\udffb\u200d\u2640\ufe0f", "\u26f9\ud83c\udffc\u200d\u2640\ufe0f", "\u26f9\ud83c\udffd\u200d\u2640\ufe0f", "\u26f9\ud83c\udffe\u200d\u2640\ufe0f", "\u26f9\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83c\udfcb\ufe0f\u200d\u2642\ufe0f", "\ud83c\udfcb\ud83c\udffb\u200d\u2642\ufe0f", "\ud83c\udfcb\ud83c\udffc\u200d\u2642\ufe0f", "\ud83c\udfcb\ud83c\udffd\u200d\u2642\ufe0f", "\ud83c\udfcb\ud83c\udffe\u200d\u2642\ufe0f", "\ud83c\udfcb\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83c\udfcb\ufe0f\u200d\u2640\ufe0f", "\ud83c\udfcb\ud83c\udffb\u200d\u2640\ufe0f", "\ud83c\udfcb\ud83c\udffc\u200d\u2640\ufe0f", "\ud83c\udfcb\ud83c\udffd\u200d\u2640\ufe0f", "\ud83c\udfcb\ud83c\udffe\u200d\u2640\ufe0f", "\ud83c\udfcb\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udeb4\u200d\u2642\ufe0f", "\ud83d\udeb4\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udeb4\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udeb4\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udeb4\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udeb4\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udeb4\u200d\u2640\ufe0f", "\ud83d\udeb4\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udeb4\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udeb4\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udeb4\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udeb4\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udeb5\u200d\u2642\ufe0f", "\ud83d\udeb5\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udeb5\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udeb5\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udeb5\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udeb5\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udeb5\u200d\u2640\ufe0f", "\ud83d\udeb5\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udeb5\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udeb5\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udeb5\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udeb5\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83c\udfce\ufe0f"), new Emoji("\ud83c\udfcd\ufe0f"), new Emoji("\ud83e\udd38", "\ud83e\udd38\ud83c\udffb", "\ud83e\udd38\ud83c\udffc", "\ud83e\udd38\ud83c\udffd", "\ud83e\udd38\ud83c\udffe", "\ud83e\udd38\ud83c\udfff"), new Emoji("\ud83e\udd38\u200d\u2642\ufe0f", "\ud83e\udd38\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd38\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd38\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd38\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd38\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd38\u200d\u2640\ufe0f", "\ud83e\udd38\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd38\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd38\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd38\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd38\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd3c"), new Emoji("\ud83e\udd3c\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd3c\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd3d", "\ud83e\udd3d\ud83c\udffb", "\ud83e\udd3d\ud83c\udffc", "\ud83e\udd3d\ud83c\udffd", "\ud83e\udd3d\ud83c\udffe", "\ud83e\udd3d\ud83c\udfff"), new Emoji("\ud83e\udd3d\u200d\u2642\ufe0f", "\ud83e\udd3d\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd3d\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd3d\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd3d\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd3d\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd3d\u200d\u2640\ufe0f", "\ud83e\udd3d\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd3d\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd3d\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd3d\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd3d\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd3e", "\ud83e\udd3e\ud83c\udffb", "\ud83e\udd3e\ud83c\udffc", "\ud83e\udd3e\ud83c\udffd", "\ud83e\udd3e\ud83c\udffe", "\ud83e\udd3e\ud83c\udfff"), new Emoji("\ud83e\udd3e\u200d\u2642\ufe0f", "\ud83e\udd3e\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd3e\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd3e\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd3e\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd3e\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd3e\u200d\u2640\ufe0f", "\ud83e\udd3e\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd3e\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd3e\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd3e\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd3e\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd39", "\ud83e\udd39\ud83c\udffb", "\ud83e\udd39\ud83c\udffc", "\ud83e\udd39\ud83c\udffd", "\ud83e\udd39\ud83c\udffe", "\ud83e\udd39\ud83c\udfff"), new Emoji("\ud83e\udd39\u200d\u2642\ufe0f", "\ud83e\udd39\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd39\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd39\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd39\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd39\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd39\u200d\u2640\ufe0f", "\ud83e\udd39\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd39\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd39\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd39\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd39\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc6b"), new Emoji("\ud83d\udc6c"), new Emoji("\ud83d\udc6d"), new Emoji("\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68"), new Emoji("\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68"), new Emoji("\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69"), new Emoji("\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc68"), new Emoji("\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68"), new Emoji("\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc69"), new Emoji("\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67"), new Emoji("\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc67"), new Emoji("\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67"), new Emoji("\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d\udc67"), new Emoji("\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67"), new Emoji("\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc67"), new Emoji("\ud83d\udc68\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc67"), new Emoji("\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d\udc67"), new Emoji("\ud83d\udc69\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc67"), new Emoji("\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc67"), new Emoji("\ud83e\udd33", "\ud83e\udd33\ud83c\udffb", "\ud83e\udd33\ud83c\udffc", "\ud83e\udd33\ud83c\udffd", "\ud83e\udd33\ud83c\udffe", "\ud83e\udd33\ud83c\udfff"), new Emoji("\ud83d\udcaa", "\ud83d\udcaa\ud83c\udffb", "\ud83d\udcaa\ud83c\udffc", "\ud83d\udcaa\ud83c\udffd", "\ud83d\udcaa\ud83c\udffe", "\ud83d\udcaa\ud83c\udfff"), new Emoji("\ud83d\udc48", "\ud83d\udc48\ud83c\udffb", "\ud83d\udc48\ud83c\udffc", "\ud83d\udc48\ud83c\udffd", "\ud83d\udc48\ud83c\udffe", "\ud83d\udc48\ud83c\udfff"), new Emoji("\ud83d\udc49", "\ud83d\udc49\ud83c\udffb", "\ud83d\udc49\ud83c\udffc", "\ud83d\udc49\ud83c\udffd", "\ud83d\udc49\ud83c\udffe", "\ud83d\udc49\ud83c\udfff"), new Emoji("\u261d\ufe0f", "\u261d\ud83c\udffb", "\u261d\ud83c\udffc", "\u261d\ud83c\udffd", "\u261d\ud83c\udffe", "\u261d\ud83c\udfff"), new Emoji("\ud83d\udc46", "\ud83d\udc46\ud83c\udffb", "\ud83d\udc46\ud83c\udffc", "\ud83d\udc46\ud83c\udffd", "\ud83d\udc46\ud83c\udffe", "\ud83d\udc46\ud83c\udfff"), new Emoji("\ud83d\udd95", "\ud83d\udd95\ud83c\udffb", "\ud83d\udd95\ud83c\udffc", "\ud83d\udd95\ud83c\udffd", "\ud83d\udd95\ud83c\udffe", "\ud83d\udd95\ud83c\udfff"), new Emoji("\ud83d\udc47", "\ud83d\udc47\ud83c\udffb", "\ud83d\udc47\ud83c\udffc", "\ud83d\udc47\ud83c\udffd", "\ud83d\udc47\ud83c\udffe", "\ud83d\udc47\ud83c\udfff"), new Emoji("\u270c\ufe0f", "\u270c\ud83c\udffb", "\u270c\ud83c\udffc", "\u270c\ud83c\udffd", "\u270c\ud83c\udffe", "\u270c\ud83c\udfff"), new Emoji("\ud83e\udd1e", "\ud83e\udd1e\ud83c\udffb", "\ud83e\udd1e\ud83c\udffc", "\ud83e\udd1e\ud83c\udffd", "\ud83e\udd1e\ud83c\udffe", "\ud83e\udd1e\ud83c\udfff"), new Emoji("\ud83d\udd96", "\ud83d\udd96\ud83c\udffb", "\ud83d\udd96\ud83c\udffc", "\ud83d\udd96\ud83c\udffd", "\ud83d\udd96\ud83c\udffe", "\ud83d\udd96\ud83c\udfff"), new Emoji("\ud83e\udd18", "\ud83e\udd18\ud83c\udffb", "\ud83e\udd18\ud83c\udffc", "\ud83e\udd18\ud83c\udffd", "\ud83e\udd18\ud83c\udffe", "\ud83e\udd18\ud83c\udfff"), new Emoji("\ud83e\udd19", "\ud83e\udd19\ud83c\udffb", "\ud83e\udd19\ud83c\udffc", "\ud83e\udd19\ud83c\udffd", "\ud83e\udd19\ud83c\udffe", "\ud83e\udd19\ud83c\udfff"), new Emoji("\ud83d\udd90\ufe0f", "\ud83d\udd90\ud83c\udffb", "\ud83d\udd90\ud83c\udffc", "\ud83d\udd90\ud83c\udffd", "\ud83d\udd90\ud83c\udffe", "\ud83d\udd90\ud83c\udfff"), new Emoji("\u270b", "\u270b\ud83c\udffb", "\u270b\ud83c\udffc", "\u270b\ud83c\udffd", "\u270b\ud83c\udffe", "\u270b\ud83c\udfff"), new Emoji("\ud83d\udc4c", "\ud83d\udc4c\ud83c\udffb", "\ud83d\udc4c\ud83c\udffc", "\ud83d\udc4c\ud83c\udffd", "\ud83d\udc4c\ud83c\udffe", "\ud83d\udc4c\ud83c\udfff"), new Emoji("\ud83d\udc4d", "\ud83d\udc4d\ud83c\udffb", "\ud83d\udc4d\ud83c\udffc", "\ud83d\udc4d\ud83c\udffd", "\ud83d\udc4d\ud83c\udffe", "\ud83d\udc4d\ud83c\udfff"), new Emoji("\ud83d\udc4e", "\ud83d\udc4e\ud83c\udffb", "\ud83d\udc4e\ud83c\udffc", "\ud83d\udc4e\ud83c\udffd", "\ud83d\udc4e\ud83c\udffe", "\ud83d\udc4e\ud83c\udfff"), new Emoji("\u270a", "\u270a\ud83c\udffb", "\u270a\ud83c\udffc", "\u270a\ud83c\udffd", "\u270a\ud83c\udffe", "\u270a\ud83c\udfff"), new Emoji("\ud83d\udc4a", "\ud83d\udc4a\ud83c\udffb", "\ud83d\udc4a\ud83c\udffc", "\ud83d\udc4a\ud83c\udffd", "\ud83d\udc4a\ud83c\udffe", "\ud83d\udc4a\ud83c\udfff"), - }, "emoji/People_2.png"); + private static final EmojiPageModel PAGE_PEOPLE_2 = new StaticEmojiPageModel(EmojiCategory.PEOPLE, Arrays.asList( + new Emoji("\ud83e\uddd7\u200d\u2642\ufe0f", "\ud83e\uddd7\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\uddd7\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\uddd7\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\uddd7\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\uddd7\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\uddd8\u200d\u2640\ufe0f", "\ud83e\uddd8\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\uddd8\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\uddd8\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\uddd8\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\uddd8\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\uddd8\u200d\u2642\ufe0f", "\ud83e\uddd8\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\uddd8\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\uddd8\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\uddd8\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\uddd8\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udec0", "\ud83d\udec0\ud83c\udffb", "\ud83d\udec0\ud83c\udffc", "\ud83d\udec0\ud83c\udffd", "\ud83d\udec0\ud83c\udffe", "\ud83d\udec0\ud83c\udfff"), new Emoji("\ud83d\udecc", "\ud83d\udecc\ud83c\udffb", "\ud83d\udecc\ud83c\udffc", "\ud83d\udecc\ud83c\udffd", "\ud83d\udecc\ud83c\udffe", "\ud83d\udecc\ud83c\udfff"), new Emoji("\ud83d\udd74\ufe0f", "\ud83d\udd74\ud83c\udffb", "\ud83d\udd74\ud83c\udffc", "\ud83d\udd74\ud83c\udffd", "\ud83d\udd74\ud83c\udffe", "\ud83d\udd74\ud83c\udfff"), new Emoji("\ud83d\udde3\ufe0f"), new Emoji("\ud83d\udc64"), new Emoji("\ud83d\udc65"), new Emoji("\ud83e\udd3a"), new Emoji("\ud83c\udfc7", "\ud83c\udfc7\ud83c\udffb", "\ud83c\udfc7\ud83c\udffc", "\ud83c\udfc7\ud83c\udffd", "\ud83c\udfc7\ud83c\udffe", "\ud83c\udfc7\ud83c\udfff"), new Emoji("\u26f7\ufe0f"), new Emoji("\ud83c\udfc2", "\ud83c\udfc2\ud83c\udffb", "\ud83c\udfc2\ud83c\udffc", "\ud83c\udfc2\ud83c\udffd", "\ud83c\udfc2\ud83c\udffe", "\ud83c\udfc2\ud83c\udfff"), new Emoji("\ud83c\udfcc\ufe0f\u200d\u2642\ufe0f", "\ud83c\udfcc\ud83c\udffb\u200d\u2642\ufe0f", "\ud83c\udfcc\ud83c\udffc\u200d\u2642\ufe0f", "\ud83c\udfcc\ud83c\udffd\u200d\u2642\ufe0f", "\ud83c\udfcc\ud83c\udffe\u200d\u2642\ufe0f", "\ud83c\udfcc\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83c\udfcc\ufe0f\u200d\u2640\ufe0f", "\ud83c\udfcc\ud83c\udffb\u200d\u2640\ufe0f", "\ud83c\udfcc\ud83c\udffc\u200d\u2640\ufe0f", "\ud83c\udfcc\ud83c\udffd\u200d\u2640\ufe0f", "\ud83c\udfcc\ud83c\udffe\u200d\u2640\ufe0f", "\ud83c\udfcc\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83c\udfc4\u200d\u2642\ufe0f", "\ud83c\udfc4\ud83c\udffb\u200d\u2642\ufe0f", "\ud83c\udfc4\ud83c\udffc\u200d\u2642\ufe0f", "\ud83c\udfc4\ud83c\udffd\u200d\u2642\ufe0f", "\ud83c\udfc4\ud83c\udffe\u200d\u2642\ufe0f", "\ud83c\udfc4\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83c\udfc4\u200d\u2640\ufe0f", "\ud83c\udfc4\ud83c\udffb\u200d\u2640\ufe0f", "\ud83c\udfc4\ud83c\udffc\u200d\u2640\ufe0f", "\ud83c\udfc4\ud83c\udffd\u200d\u2640\ufe0f", "\ud83c\udfc4\ud83c\udffe\u200d\u2640\ufe0f", "\ud83c\udfc4\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udea3\u200d\u2642\ufe0f", "\ud83d\udea3\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udea3\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udea3\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udea3\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udea3\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udea3\u200d\u2640\ufe0f", "\ud83d\udea3\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udea3\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udea3\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udea3\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udea3\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83c\udfca\u200d\u2642\ufe0f", "\ud83c\udfca\ud83c\udffb\u200d\u2642\ufe0f", "\ud83c\udfca\ud83c\udffc\u200d\u2642\ufe0f", "\ud83c\udfca\ud83c\udffd\u200d\u2642\ufe0f", "\ud83c\udfca\ud83c\udffe\u200d\u2642\ufe0f", "\ud83c\udfca\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83c\udfca\u200d\u2640\ufe0f", "\ud83c\udfca\ud83c\udffb\u200d\u2640\ufe0f", "\ud83c\udfca\ud83c\udffc\u200d\u2640\ufe0f", "\ud83c\udfca\ud83c\udffd\u200d\u2640\ufe0f", "\ud83c\udfca\ud83c\udffe\u200d\u2640\ufe0f", "\ud83c\udfca\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\u26f9\ufe0f\u200d\u2642\ufe0f", "\u26f9\ud83c\udffb\u200d\u2642\ufe0f", "\u26f9\ud83c\udffc\u200d\u2642\ufe0f", "\u26f9\ud83c\udffd\u200d\u2642\ufe0f", "\u26f9\ud83c\udffe\u200d\u2642\ufe0f", "\u26f9\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\u26f9\ufe0f\u200d\u2640\ufe0f", "\u26f9\ud83c\udffb\u200d\u2640\ufe0f", "\u26f9\ud83c\udffc\u200d\u2640\ufe0f", "\u26f9\ud83c\udffd\u200d\u2640\ufe0f", "\u26f9\ud83c\udffe\u200d\u2640\ufe0f", "\u26f9\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83c\udfcb\ufe0f\u200d\u2642\ufe0f", "\ud83c\udfcb\ud83c\udffb\u200d\u2642\ufe0f", "\ud83c\udfcb\ud83c\udffc\u200d\u2642\ufe0f", "\ud83c\udfcb\ud83c\udffd\u200d\u2642\ufe0f", "\ud83c\udfcb\ud83c\udffe\u200d\u2642\ufe0f", "\ud83c\udfcb\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83c\udfcb\ufe0f\u200d\u2640\ufe0f", "\ud83c\udfcb\ud83c\udffb\u200d\u2640\ufe0f", "\ud83c\udfcb\ud83c\udffc\u200d\u2640\ufe0f", "\ud83c\udfcb\ud83c\udffd\u200d\u2640\ufe0f", "\ud83c\udfcb\ud83c\udffe\u200d\u2640\ufe0f", "\ud83c\udfcb\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udeb4\u200d\u2642\ufe0f", "\ud83d\udeb4\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udeb4\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udeb4\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udeb4\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udeb4\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udeb4\u200d\u2640\ufe0f", "\ud83d\udeb4\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udeb4\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udeb4\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udeb4\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udeb4\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udeb5\u200d\u2642\ufe0f", "\ud83d\udeb5\ud83c\udffb\u200d\u2642\ufe0f", "\ud83d\udeb5\ud83c\udffc\u200d\u2642\ufe0f", "\ud83d\udeb5\ud83c\udffd\u200d\u2642\ufe0f", "\ud83d\udeb5\ud83c\udffe\u200d\u2642\ufe0f", "\ud83d\udeb5\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83d\udeb5\u200d\u2640\ufe0f", "\ud83d\udeb5\ud83c\udffb\u200d\u2640\ufe0f", "\ud83d\udeb5\ud83c\udffc\u200d\u2640\ufe0f", "\ud83d\udeb5\ud83c\udffd\u200d\u2640\ufe0f", "\ud83d\udeb5\ud83c\udffe\u200d\u2640\ufe0f", "\ud83d\udeb5\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83c\udfce\ufe0f"), new Emoji("\ud83c\udfcd\ufe0f"), new Emoji("\ud83e\udd38", "\ud83e\udd38\ud83c\udffb", "\ud83e\udd38\ud83c\udffc", "\ud83e\udd38\ud83c\udffd", "\ud83e\udd38\ud83c\udffe", "\ud83e\udd38\ud83c\udfff"), new Emoji("\ud83e\udd38\u200d\u2642\ufe0f", "\ud83e\udd38\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd38\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd38\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd38\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd38\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd38\u200d\u2640\ufe0f", "\ud83e\udd38\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd38\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd38\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd38\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd38\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd3c"), new Emoji("\ud83e\udd3c\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd3c\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd3d", "\ud83e\udd3d\ud83c\udffb", "\ud83e\udd3d\ud83c\udffc", "\ud83e\udd3d\ud83c\udffd", "\ud83e\udd3d\ud83c\udffe", "\ud83e\udd3d\ud83c\udfff"), new Emoji("\ud83e\udd3d\u200d\u2642\ufe0f", "\ud83e\udd3d\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd3d\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd3d\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd3d\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd3d\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd3d\u200d\u2640\ufe0f", "\ud83e\udd3d\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd3d\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd3d\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd3d\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd3d\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd3e", "\ud83e\udd3e\ud83c\udffb", "\ud83e\udd3e\ud83c\udffc", "\ud83e\udd3e\ud83c\udffd", "\ud83e\udd3e\ud83c\udffe", "\ud83e\udd3e\ud83c\udfff"), new Emoji("\ud83e\udd3e\u200d\u2642\ufe0f", "\ud83e\udd3e\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd3e\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd3e\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd3e\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd3e\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd3e\u200d\u2640\ufe0f", "\ud83e\udd3e\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd3e\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd3e\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd3e\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd3e\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83e\udd39", "\ud83e\udd39\ud83c\udffb", "\ud83e\udd39\ud83c\udffc", "\ud83e\udd39\ud83c\udffd", "\ud83e\udd39\ud83c\udffe", "\ud83e\udd39\ud83c\udfff"), new Emoji("\ud83e\udd39\u200d\u2642\ufe0f", "\ud83e\udd39\ud83c\udffb\u200d\u2642\ufe0f", "\ud83e\udd39\ud83c\udffc\u200d\u2642\ufe0f", "\ud83e\udd39\ud83c\udffd\u200d\u2642\ufe0f", "\ud83e\udd39\ud83c\udffe\u200d\u2642\ufe0f", "\ud83e\udd39\ud83c\udfff\u200d\u2642\ufe0f"), new Emoji("\ud83e\udd39\u200d\u2640\ufe0f", "\ud83e\udd39\ud83c\udffb\u200d\u2640\ufe0f", "\ud83e\udd39\ud83c\udffc\u200d\u2640\ufe0f", "\ud83e\udd39\ud83c\udffd\u200d\u2640\ufe0f", "\ud83e\udd39\ud83c\udffe\u200d\u2640\ufe0f", "\ud83e\udd39\ud83c\udfff\u200d\u2640\ufe0f"), new Emoji("\ud83d\udc6b"), new Emoji("\ud83d\udc6c"), new Emoji("\ud83d\udc6d"), new Emoji("\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68"), new Emoji("\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68"), new Emoji("\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc69"), new Emoji("\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc68"), new Emoji("\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68"), new Emoji("\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc69"), new Emoji("\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67"), new Emoji("\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc67"), new Emoji("\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67"), new Emoji("\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d\udc67"), new Emoji("\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67"), new Emoji("\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc67"), new Emoji("\ud83d\udc68\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc67"), new Emoji("\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d\udc66"), new Emoji("\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d\udc67"), new Emoji("\ud83d\udc69\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc67"), new Emoji("\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc66"), new Emoji("\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d\udc67"), new Emoji("\ud83e\udd33", "\ud83e\udd33\ud83c\udffb", "\ud83e\udd33\ud83c\udffc", "\ud83e\udd33\ud83c\udffd", "\ud83e\udd33\ud83c\udffe", "\ud83e\udd33\ud83c\udfff"), new Emoji("\ud83d\udcaa", "\ud83d\udcaa\ud83c\udffb", "\ud83d\udcaa\ud83c\udffc", "\ud83d\udcaa\ud83c\udffd", "\ud83d\udcaa\ud83c\udffe", "\ud83d\udcaa\ud83c\udfff"), new Emoji("\ud83d\udc48", "\ud83d\udc48\ud83c\udffb", "\ud83d\udc48\ud83c\udffc", "\ud83d\udc48\ud83c\udffd", "\ud83d\udc48\ud83c\udffe", "\ud83d\udc48\ud83c\udfff"), new Emoji("\ud83d\udc49", "\ud83d\udc49\ud83c\udffb", "\ud83d\udc49\ud83c\udffc", "\ud83d\udc49\ud83c\udffd", "\ud83d\udc49\ud83c\udffe", "\ud83d\udc49\ud83c\udfff"), new Emoji("\u261d\ufe0f", "\u261d\ud83c\udffb", "\u261d\ud83c\udffc", "\u261d\ud83c\udffd", "\u261d\ud83c\udffe", "\u261d\ud83c\udfff"), new Emoji("\ud83d\udc46", "\ud83d\udc46\ud83c\udffb", "\ud83d\udc46\ud83c\udffc", "\ud83d\udc46\ud83c\udffd", "\ud83d\udc46\ud83c\udffe", "\ud83d\udc46\ud83c\udfff"), new Emoji("\ud83d\udd95", "\ud83d\udd95\ud83c\udffb", "\ud83d\udd95\ud83c\udffc", "\ud83d\udd95\ud83c\udffd", "\ud83d\udd95\ud83c\udffe", "\ud83d\udd95\ud83c\udfff"), new Emoji("\ud83d\udc47", "\ud83d\udc47\ud83c\udffb", "\ud83d\udc47\ud83c\udffc", "\ud83d\udc47\ud83c\udffd", "\ud83d\udc47\ud83c\udffe", "\ud83d\udc47\ud83c\udfff"), new Emoji("\u270c\ufe0f", "\u270c\ud83c\udffb", "\u270c\ud83c\udffc", "\u270c\ud83c\udffd", "\u270c\ud83c\udffe", "\u270c\ud83c\udfff"), new Emoji("\ud83e\udd1e", "\ud83e\udd1e\ud83c\udffb", "\ud83e\udd1e\ud83c\udffc", "\ud83e\udd1e\ud83c\udffd", "\ud83e\udd1e\ud83c\udffe", "\ud83e\udd1e\ud83c\udfff"), new Emoji("\ud83d\udd96", "\ud83d\udd96\ud83c\udffb", "\ud83d\udd96\ud83c\udffc", "\ud83d\udd96\ud83c\udffd", "\ud83d\udd96\ud83c\udffe", "\ud83d\udd96\ud83c\udfff"), new Emoji("\ud83e\udd18", "\ud83e\udd18\ud83c\udffb", "\ud83e\udd18\ud83c\udffc", "\ud83e\udd18\ud83c\udffd", "\ud83e\udd18\ud83c\udffe", "\ud83e\udd18\ud83c\udfff"), new Emoji("\ud83e\udd19", "\ud83e\udd19\ud83c\udffb", "\ud83e\udd19\ud83c\udffc", "\ud83e\udd19\ud83c\udffd", "\ud83e\udd19\ud83c\udffe", "\ud83e\udd19\ud83c\udfff"), new Emoji("\ud83d\udd90\ufe0f", "\ud83d\udd90\ud83c\udffb", "\ud83d\udd90\ud83c\udffc", "\ud83d\udd90\ud83c\udffd", "\ud83d\udd90\ud83c\udffe", "\ud83d\udd90\ud83c\udfff"), new Emoji("\u270b", "\u270b\ud83c\udffb", "\u270b\ud83c\udffc", "\u270b\ud83c\udffd", "\u270b\ud83c\udffe", "\u270b\ud83c\udfff"), new Emoji("\ud83d\udc4c", "\ud83d\udc4c\ud83c\udffb", "\ud83d\udc4c\ud83c\udffc", "\ud83d\udc4c\ud83c\udffd", "\ud83d\udc4c\ud83c\udffe", "\ud83d\udc4c\ud83c\udfff"), new Emoji("\ud83d\udc4d", "\ud83d\udc4d\ud83c\udffb", "\ud83d\udc4d\ud83c\udffc", "\ud83d\udc4d\ud83c\udffd", "\ud83d\udc4d\ud83c\udffe", "\ud83d\udc4d\ud83c\udfff"), new Emoji("\ud83d\udc4e", "\ud83d\udc4e\ud83c\udffb", "\ud83d\udc4e\ud83c\udffc", "\ud83d\udc4e\ud83c\udffd", "\ud83d\udc4e\ud83c\udffe", "\ud83d\udc4e\ud83c\udfff"), new Emoji("\u270a", "\u270a\ud83c\udffb", "\u270a\ud83c\udffc", "\u270a\ud83c\udffd", "\u270a\ud83c\udffe", "\u270a\ud83c\udfff"), new Emoji("\ud83d\udc4a", "\ud83d\udc4a\ud83c\udffb", "\ud83d\udc4a\ud83c\udffc", "\ud83d\udc4a\ud83c\udffd", "\ud83d\udc4a\ud83c\udffe", "\ud83d\udc4a\ud83c\udfff") + ), Uri.parse("emoji/People_2.png")); - private static final EmojiPageModel PAGE_PEOPLE_3 = new StaticEmojiPageModel(R.attr.emoji_category_people, new Emoji[] { - new Emoji("\ud83e\udd1b", "\ud83e\udd1b\ud83c\udffb", "\ud83e\udd1b\ud83c\udffc", "\ud83e\udd1b\ud83c\udffd", "\ud83e\udd1b\ud83c\udffe", "\ud83e\udd1b\ud83c\udfff"), new Emoji("\ud83e\udd1c", "\ud83e\udd1c\ud83c\udffb", "\ud83e\udd1c\ud83c\udffc", "\ud83e\udd1c\ud83c\udffd", "\ud83e\udd1c\ud83c\udffe", "\ud83e\udd1c\ud83c\udfff"), new Emoji("\ud83e\udd1a", "\ud83e\udd1a\ud83c\udffb", "\ud83e\udd1a\ud83c\udffc", "\ud83e\udd1a\ud83c\udffd", "\ud83e\udd1a\ud83c\udffe", "\ud83e\udd1a\ud83c\udfff"), new Emoji("\ud83d\udc4b", "\ud83d\udc4b\ud83c\udffb", "\ud83d\udc4b\ud83c\udffc", "\ud83d\udc4b\ud83c\udffd", "\ud83d\udc4b\ud83c\udffe", "\ud83d\udc4b\ud83c\udfff"), new Emoji("\ud83e\udd1f", "\ud83e\udd1f\ud83c\udffb", "\ud83e\udd1f\ud83c\udffc", "\ud83e\udd1f\ud83c\udffd", "\ud83e\udd1f\ud83c\udffe", "\ud83e\udd1f\ud83c\udfff"), new Emoji("\u270d\ufe0f", "\u270d\ud83c\udffb", "\u270d\ud83c\udffc", "\u270d\ud83c\udffd", "\u270d\ud83c\udffe", "\u270d\ud83c\udfff"), new Emoji("\ud83d\udc4f", "\ud83d\udc4f\ud83c\udffb", "\ud83d\udc4f\ud83c\udffc", "\ud83d\udc4f\ud83c\udffd", "\ud83d\udc4f\ud83c\udffe", "\ud83d\udc4f\ud83c\udfff"), new Emoji("\ud83d\udc50", "\ud83d\udc50\ud83c\udffb", "\ud83d\udc50\ud83c\udffc", "\ud83d\udc50\ud83c\udffd", "\ud83d\udc50\ud83c\udffe", "\ud83d\udc50\ud83c\udfff"), new Emoji("\ud83d\ude4c", "\ud83d\ude4c\ud83c\udffb", "\ud83d\ude4c\ud83c\udffc", "\ud83d\ude4c\ud83c\udffd", "\ud83d\ude4c\ud83c\udffe", "\ud83d\ude4c\ud83c\udfff"), new Emoji("\ud83e\udd32", "\ud83e\udd32\ud83c\udffb", "\ud83e\udd32\ud83c\udffc", "\ud83e\udd32\ud83c\udffd", "\ud83e\udd32\ud83c\udffe", "\ud83e\udd32\ud83c\udfff"), new Emoji("\ud83d\ude4f", "\ud83d\ude4f\ud83c\udffb", "\ud83d\ude4f\ud83c\udffc", "\ud83d\ude4f\ud83c\udffd", "\ud83d\ude4f\ud83c\udffe", "\ud83d\ude4f\ud83c\udfff"), new Emoji("\ud83e\udd1d"), new Emoji("\ud83d\udc85", "\ud83d\udc85\ud83c\udffb", "\ud83d\udc85\ud83c\udffc", "\ud83d\udc85\ud83c\udffd", "\ud83d\udc85\ud83c\udffe", "\ud83d\udc85\ud83c\udfff"), new Emoji("\ud83d\udc42", "\ud83d\udc42\ud83c\udffb", "\ud83d\udc42\ud83c\udffc", "\ud83d\udc42\ud83c\udffd", "\ud83d\udc42\ud83c\udffe", "\ud83d\udc42\ud83c\udfff"), new Emoji("\ud83d\udc43", "\ud83d\udc43\ud83c\udffb", "\ud83d\udc43\ud83c\udffc", "\ud83d\udc43\ud83c\udffd", "\ud83d\udc43\ud83c\udffe", "\ud83d\udc43\ud83c\udfff"), new Emoji("\ud83d\udc63"), new Emoji("\ud83d\udc40"), new Emoji("\ud83d\udc41\ufe0f"), new Emoji("\ud83d\udc41\ufe0f\u200d\ud83d\udde8\ufe0f"), new Emoji("\ud83e\udde0"), new Emoji("\ud83d\udc45"), new Emoji("\ud83d\udc44"), new Emoji("\ud83d\udc8b"), new Emoji("\ud83d\udc98"), new Emoji("\u2764\ufe0f"), new Emoji("\ud83d\udc93"), new Emoji("\ud83d\udc94"), new Emoji("\ud83d\udc95"), new Emoji("\ud83d\udc96"), new Emoji("\ud83d\udc97"), new Emoji("\ud83d\udc99"), new Emoji("\ud83d\udc9a"), new Emoji("\ud83d\udc9b"), new Emoji("\ud83e\udde1"), new Emoji("\ud83d\udc9c"), new Emoji("\ud83d\udda4"), new Emoji("\ud83d\udc9d"), new Emoji("\ud83d\udc9e"), new Emoji("\ud83d\udc9f"), new Emoji("\u2763\ufe0f"), new Emoji("\ud83d\udc8c"), new Emoji("\ud83d\udca4"), new Emoji("\ud83d\udca2"), new Emoji("\ud83d\udca3"), new Emoji("\ud83d\udca5"), new Emoji("\ud83d\udca6"), new Emoji("\ud83d\udca8"), new Emoji("\ud83d\udcab"), new Emoji("\ud83d\udcac"), new Emoji("\ud83d\udde8\ufe0f"), new Emoji("\ud83d\uddef\ufe0f"), new Emoji("\ud83d\udcad"), new Emoji("\ud83d\udd73\ufe0f"), new Emoji("\ud83d\udc53"), new Emoji("\ud83d\udd76\ufe0f"), new Emoji("\ud83d\udc54"), new Emoji("\ud83d\udc55"), new Emoji("\ud83d\udc56"), new Emoji("\ud83e\udde3"), new Emoji("\ud83e\udde4"), new Emoji("\ud83e\udde5"), new Emoji("\ud83e\udde6"), new Emoji("\ud83d\udc57"), new Emoji("\ud83d\udc58"), new Emoji("\ud83d\udc59"), new Emoji("\ud83d\udc5a"), new Emoji("\ud83d\udc5b"), new Emoji("\ud83d\udc5c"), new Emoji("\ud83d\udc5d"), new Emoji("\ud83d\udecd\ufe0f"), new Emoji("\ud83c\udf92"), new Emoji("\ud83d\udc5e"), new Emoji("\ud83d\udc5f"), new Emoji("\ud83d\udc60"), new Emoji("\ud83d\udc61"), new Emoji("\ud83d\udc62"), new Emoji("\ud83d\udc51"), new Emoji("\ud83d\udc52"), new Emoji("\ud83c\udfa9"), new Emoji("\ud83c\udf93"), new Emoji("\ud83e\udde2"), new Emoji("\u26d1\ufe0f"), new Emoji("\ud83d\udcff"), new Emoji("\ud83d\udc84"), new Emoji("\ud83d\udc8d"), new Emoji("\ud83d\udc8e"), - }, "emoji/People_3.png"); + private static final EmojiPageModel PAGE_PEOPLE_3 = new StaticEmojiPageModel(EmojiCategory.PEOPLE, Arrays.asList( + new Emoji("\ud83e\udd1b", "\ud83e\udd1b\ud83c\udffb", "\ud83e\udd1b\ud83c\udffc", "\ud83e\udd1b\ud83c\udffd", "\ud83e\udd1b\ud83c\udffe", "\ud83e\udd1b\ud83c\udfff"), new Emoji("\ud83e\udd1c", "\ud83e\udd1c\ud83c\udffb", "\ud83e\udd1c\ud83c\udffc", "\ud83e\udd1c\ud83c\udffd", "\ud83e\udd1c\ud83c\udffe", "\ud83e\udd1c\ud83c\udfff"), new Emoji("\ud83e\udd1a", "\ud83e\udd1a\ud83c\udffb", "\ud83e\udd1a\ud83c\udffc", "\ud83e\udd1a\ud83c\udffd", "\ud83e\udd1a\ud83c\udffe", "\ud83e\udd1a\ud83c\udfff"), new Emoji("\ud83d\udc4b", "\ud83d\udc4b\ud83c\udffb", "\ud83d\udc4b\ud83c\udffc", "\ud83d\udc4b\ud83c\udffd", "\ud83d\udc4b\ud83c\udffe", "\ud83d\udc4b\ud83c\udfff"), new Emoji("\ud83e\udd1f", "\ud83e\udd1f\ud83c\udffb", "\ud83e\udd1f\ud83c\udffc", "\ud83e\udd1f\ud83c\udffd", "\ud83e\udd1f\ud83c\udffe", "\ud83e\udd1f\ud83c\udfff"), new Emoji("\u270d\ufe0f", "\u270d\ud83c\udffb", "\u270d\ud83c\udffc", "\u270d\ud83c\udffd", "\u270d\ud83c\udffe", "\u270d\ud83c\udfff"), new Emoji("\ud83d\udc4f", "\ud83d\udc4f\ud83c\udffb", "\ud83d\udc4f\ud83c\udffc", "\ud83d\udc4f\ud83c\udffd", "\ud83d\udc4f\ud83c\udffe", "\ud83d\udc4f\ud83c\udfff"), new Emoji("\ud83d\udc50", "\ud83d\udc50\ud83c\udffb", "\ud83d\udc50\ud83c\udffc", "\ud83d\udc50\ud83c\udffd", "\ud83d\udc50\ud83c\udffe", "\ud83d\udc50\ud83c\udfff"), new Emoji("\ud83d\ude4c", "\ud83d\ude4c\ud83c\udffb", "\ud83d\ude4c\ud83c\udffc", "\ud83d\ude4c\ud83c\udffd", "\ud83d\ude4c\ud83c\udffe", "\ud83d\ude4c\ud83c\udfff"), new Emoji("\ud83e\udd32", "\ud83e\udd32\ud83c\udffb", "\ud83e\udd32\ud83c\udffc", "\ud83e\udd32\ud83c\udffd", "\ud83e\udd32\ud83c\udffe", "\ud83e\udd32\ud83c\udfff"), new Emoji("\ud83d\ude4f", "\ud83d\ude4f\ud83c\udffb", "\ud83d\ude4f\ud83c\udffc", "\ud83d\ude4f\ud83c\udffd", "\ud83d\ude4f\ud83c\udffe", "\ud83d\ude4f\ud83c\udfff"), new Emoji("\ud83e\udd1d"), new Emoji("\ud83d\udc85", "\ud83d\udc85\ud83c\udffb", "\ud83d\udc85\ud83c\udffc", "\ud83d\udc85\ud83c\udffd", "\ud83d\udc85\ud83c\udffe", "\ud83d\udc85\ud83c\udfff"), new Emoji("\ud83d\udc42", "\ud83d\udc42\ud83c\udffb", "\ud83d\udc42\ud83c\udffc", "\ud83d\udc42\ud83c\udffd", "\ud83d\udc42\ud83c\udffe", "\ud83d\udc42\ud83c\udfff"), new Emoji("\ud83d\udc43", "\ud83d\udc43\ud83c\udffb", "\ud83d\udc43\ud83c\udffc", "\ud83d\udc43\ud83c\udffd", "\ud83d\udc43\ud83c\udffe", "\ud83d\udc43\ud83c\udfff"), new Emoji("\ud83d\udc63"), new Emoji("\ud83d\udc40"), new Emoji("\ud83d\udc41\ufe0f"), new Emoji("\ud83d\udc41\ufe0f\u200d\ud83d\udde8\ufe0f"), new Emoji("\ud83e\udde0"), new Emoji("\ud83d\udc45"), new Emoji("\ud83d\udc44"), new Emoji("\ud83d\udc8b"), new Emoji("\ud83d\udc98"), new Emoji("\u2764\ufe0f"), new Emoji("\ud83d\udc93"), new Emoji("\ud83d\udc94"), new Emoji("\ud83d\udc95"), new Emoji("\ud83d\udc96"), new Emoji("\ud83d\udc97"), new Emoji("\ud83d\udc99"), new Emoji("\ud83d\udc9a"), new Emoji("\ud83d\udc9b"), new Emoji("\ud83e\udde1"), new Emoji("\ud83d\udc9c"), new Emoji("\ud83d\udda4"), new Emoji("\ud83d\udc9d"), new Emoji("\ud83d\udc9e"), new Emoji("\ud83d\udc9f"), new Emoji("\u2763\ufe0f"), new Emoji("\ud83d\udc8c"), new Emoji("\ud83d\udca4"), new Emoji("\ud83d\udca2"), new Emoji("\ud83d\udca3"), new Emoji("\ud83d\udca5"), new Emoji("\ud83d\udca6"), new Emoji("\ud83d\udca8"), new Emoji("\ud83d\udcab"), new Emoji("\ud83d\udcac"), new Emoji("\ud83d\udde8\ufe0f"), new Emoji("\ud83d\uddef\ufe0f"), new Emoji("\ud83d\udcad"), new Emoji("\ud83d\udd73\ufe0f"), new Emoji("\ud83d\udc53"), new Emoji("\ud83d\udd76\ufe0f"), new Emoji("\ud83d\udc54"), new Emoji("\ud83d\udc55"), new Emoji("\ud83d\udc56"), new Emoji("\ud83e\udde3"), new Emoji("\ud83e\udde4"), new Emoji("\ud83e\udde5"), new Emoji("\ud83e\udde6"), new Emoji("\ud83d\udc57"), new Emoji("\ud83d\udc58"), new Emoji("\ud83d\udc59"), new Emoji("\ud83d\udc5a"), new Emoji("\ud83d\udc5b"), new Emoji("\ud83d\udc5c"), new Emoji("\ud83d\udc5d"), new Emoji("\ud83d\udecd\ufe0f"), new Emoji("\ud83c\udf92"), new Emoji("\ud83d\udc5e"), new Emoji("\ud83d\udc5f"), new Emoji("\ud83d\udc60"), new Emoji("\ud83d\udc61"), new Emoji("\ud83d\udc62"), new Emoji("\ud83d\udc51"), new Emoji("\ud83d\udc52"), new Emoji("\ud83c\udfa9"), new Emoji("\ud83c\udf93"), new Emoji("\ud83e\udde2"), new Emoji("\u26d1\ufe0f"), new Emoji("\ud83d\udcff"), new Emoji("\ud83d\udc84"), new Emoji("\ud83d\udc8d"), new Emoji("\ud83d\udc8e") + ), Uri.parse("emoji/People_3.png")); - private static final EmojiPageModel PAGE_PEOPLE = new CompositeEmojiPageModel(R.attr.emoji_category_people, PAGE_PEOPLE_0, PAGE_PEOPLE_1, PAGE_PEOPLE_2, PAGE_PEOPLE_3); + private static final EmojiPageModel PAGE_PEOPLE = new CompositeEmojiPageModel(R.attr.emoji_category_people, Arrays.asList(PAGE_PEOPLE_0, PAGE_PEOPLE_1, PAGE_PEOPLE_2, PAGE_PEOPLE_3)); - private static final EmojiPageModel PAGE_NATURE = new StaticEmojiPageModel(R.attr.emoji_category_nature, new Emoji[] { - new Emoji("\ud83d\udc35"), new Emoji("\ud83d\udc12"), new Emoji("\ud83e\udd8d"), new Emoji("\ud83d\udc36"), new Emoji("\ud83d\udc15"), new Emoji("\ud83d\udc29"), new Emoji("\ud83d\udc3a"), new Emoji("\ud83e\udd8a"), new Emoji("\ud83d\udc31"), new Emoji("\ud83d\udc08"), new Emoji("\ud83e\udd81"), new Emoji("\ud83d\udc2f"), new Emoji("\ud83d\udc05"), new Emoji("\ud83d\udc06"), new Emoji("\ud83d\udc34"), new Emoji("\ud83d\udc0e"), new Emoji("\ud83e\udd84"), new Emoji("\ud83e\udd93"), new Emoji("\ud83e\udd8c"), new Emoji("\ud83d\udc2e"), new Emoji("\ud83d\udc02"), new Emoji("\ud83d\udc03"), new Emoji("\ud83d\udc04"), new Emoji("\ud83d\udc37"), new Emoji("\ud83d\udc16"), new Emoji("\ud83d\udc17"), new Emoji("\ud83d\udc3d"), new Emoji("\ud83d\udc0f"), new Emoji("\ud83d\udc11"), new Emoji("\ud83d\udc10"), new Emoji("\ud83d\udc2a"), new Emoji("\ud83d\udc2b"), new Emoji("\ud83e\udd92"), new Emoji("\ud83d\udc18"), new Emoji("\ud83e\udd8f"), new Emoji("\ud83d\udc2d"), new Emoji("\ud83d\udc01"), new Emoji("\ud83d\udc00"), new Emoji("\ud83d\udc39"), new Emoji("\ud83d\udc30"), new Emoji("\ud83d\udc07"), new Emoji("\ud83d\udc3f\ufe0f"), new Emoji("\ud83e\udd94"), new Emoji("\ud83e\udd87"), new Emoji("\ud83d\udc3b"), new Emoji("\ud83d\udc28"), new Emoji("\ud83d\udc3c"), new Emoji("\ud83d\udc3e"), new Emoji("\ud83e\udd83"), new Emoji("\ud83d\udc14"), new Emoji("\ud83d\udc13"), new Emoji("\ud83d\udc23"), new Emoji("\ud83d\udc24"), new Emoji("\ud83d\udc25"), new Emoji("\ud83d\udc26"), new Emoji("\ud83d\udc27"), new Emoji("\ud83d\udd4a\ufe0f"), new Emoji("\ud83e\udd85"), new Emoji("\ud83e\udd86"), new Emoji("\ud83e\udd89"), new Emoji("\ud83d\udc38"), new Emoji("\ud83d\udc0a"), new Emoji("\ud83d\udc22"), new Emoji("\ud83e\udd8e"), new Emoji("\ud83d\udc0d"), new Emoji("\ud83d\udc32"), new Emoji("\ud83d\udc09"), new Emoji("\ud83e\udd95"), new Emoji("\ud83e\udd96"), new Emoji("\ud83d\udc33"), new Emoji("\ud83d\udc0b"), new Emoji("\ud83d\udc2c"), new Emoji("\ud83d\udc1f"), new Emoji("\ud83d\udc20"), new Emoji("\ud83d\udc21"), new Emoji("\ud83e\udd88"), new Emoji("\ud83d\udc19"), new Emoji("\ud83d\udc1a"), new Emoji("\ud83e\udd80"), new Emoji("\ud83e\udd90"), new Emoji("\ud83e\udd91"), new Emoji("\ud83d\udc0c"), new Emoji("\ud83e\udd8b"), new Emoji("\ud83d\udc1b"), new Emoji("\ud83d\udc1c"), new Emoji("\ud83d\udc1d"), new Emoji("\ud83d\udc1e"), new Emoji("\ud83e\udd97"), new Emoji("\ud83d\udd77\ufe0f"), new Emoji("\ud83d\udd78\ufe0f"), new Emoji("\ud83e\udd82"), new Emoji("\ud83d\udc90"), new Emoji("\ud83c\udf38"), new Emoji("\ud83d\udcae"), new Emoji("\ud83c\udff5\ufe0f"), new Emoji("\ud83c\udf39"), new Emoji("\ud83e\udd40"), new Emoji("\ud83c\udf3a"), new Emoji("\ud83c\udf3b"), new Emoji("\ud83c\udf3c"), new Emoji("\ud83c\udf37"), new Emoji("\ud83c\udf31"), new Emoji("\ud83c\udf32"), new Emoji("\ud83c\udf33"), new Emoji("\ud83c\udf34"), new Emoji("\ud83c\udf35"), new Emoji("\ud83c\udf3e"), new Emoji("\ud83c\udf3f"), new Emoji("\u2618\ufe0f"), new Emoji("\ud83c\udf40"), new Emoji("\ud83c\udf41"), new Emoji("\ud83c\udf42"), new Emoji("\ud83c\udf43"), - }, "emoji/Nature.png"); + private static final EmojiPageModel PAGE_NATURE = new StaticEmojiPageModel(EmojiCategory.NATURE, Arrays.asList( + new Emoji("\ud83d\udc35"), new Emoji("\ud83d\udc12"), new Emoji("\ud83e\udd8d"), new Emoji("\ud83d\udc36"), new Emoji("\ud83d\udc15"), new Emoji("\ud83d\udc29"), new Emoji("\ud83d\udc3a"), new Emoji("\ud83e\udd8a"), new Emoji("\ud83d\udc31"), new Emoji("\ud83d\udc08"), new Emoji("\ud83e\udd81"), new Emoji("\ud83d\udc2f"), new Emoji("\ud83d\udc05"), new Emoji("\ud83d\udc06"), new Emoji("\ud83d\udc34"), new Emoji("\ud83d\udc0e"), new Emoji("\ud83e\udd84"), new Emoji("\ud83e\udd93"), new Emoji("\ud83e\udd8c"), new Emoji("\ud83d\udc2e"), new Emoji("\ud83d\udc02"), new Emoji("\ud83d\udc03"), new Emoji("\ud83d\udc04"), new Emoji("\ud83d\udc37"), new Emoji("\ud83d\udc16"), new Emoji("\ud83d\udc17"), new Emoji("\ud83d\udc3d"), new Emoji("\ud83d\udc0f"), new Emoji("\ud83d\udc11"), new Emoji("\ud83d\udc10"), new Emoji("\ud83d\udc2a"), new Emoji("\ud83d\udc2b"), new Emoji("\ud83e\udd92"), new Emoji("\ud83d\udc18"), new Emoji("\ud83e\udd8f"), new Emoji("\ud83d\udc2d"), new Emoji("\ud83d\udc01"), new Emoji("\ud83d\udc00"), new Emoji("\ud83d\udc39"), new Emoji("\ud83d\udc30"), new Emoji("\ud83d\udc07"), new Emoji("\ud83d\udc3f\ufe0f"), new Emoji("\ud83e\udd94"), new Emoji("\ud83e\udd87"), new Emoji("\ud83d\udc3b"), new Emoji("\ud83d\udc28"), new Emoji("\ud83d\udc3c"), new Emoji("\ud83d\udc3e"), new Emoji("\ud83e\udd83"), new Emoji("\ud83d\udc14"), new Emoji("\ud83d\udc13"), new Emoji("\ud83d\udc23"), new Emoji("\ud83d\udc24"), new Emoji("\ud83d\udc25"), new Emoji("\ud83d\udc26"), new Emoji("\ud83d\udc27"), new Emoji("\ud83d\udd4a\ufe0f"), new Emoji("\ud83e\udd85"), new Emoji("\ud83e\udd86"), new Emoji("\ud83e\udd89"), new Emoji("\ud83d\udc38"), new Emoji("\ud83d\udc0a"), new Emoji("\ud83d\udc22"), new Emoji("\ud83e\udd8e"), new Emoji("\ud83d\udc0d"), new Emoji("\ud83d\udc32"), new Emoji("\ud83d\udc09"), new Emoji("\ud83e\udd95"), new Emoji("\ud83e\udd96"), new Emoji("\ud83d\udc33"), new Emoji("\ud83d\udc0b"), new Emoji("\ud83d\udc2c"), new Emoji("\ud83d\udc1f"), new Emoji("\ud83d\udc20"), new Emoji("\ud83d\udc21"), new Emoji("\ud83e\udd88"), new Emoji("\ud83d\udc19"), new Emoji("\ud83d\udc1a"), new Emoji("\ud83e\udd80"), new Emoji("\ud83e\udd90"), new Emoji("\ud83e\udd91"), new Emoji("\ud83d\udc0c"), new Emoji("\ud83e\udd8b"), new Emoji("\ud83d\udc1b"), new Emoji("\ud83d\udc1c"), new Emoji("\ud83d\udc1d"), new Emoji("\ud83d\udc1e"), new Emoji("\ud83e\udd97"), new Emoji("\ud83d\udd77\ufe0f"), new Emoji("\ud83d\udd78\ufe0f"), new Emoji("\ud83e\udd82"), new Emoji("\ud83d\udc90"), new Emoji("\ud83c\udf38"), new Emoji("\ud83d\udcae"), new Emoji("\ud83c\udff5\ufe0f"), new Emoji("\ud83c\udf39"), new Emoji("\ud83e\udd40"), new Emoji("\ud83c\udf3a"), new Emoji("\ud83c\udf3b"), new Emoji("\ud83c\udf3c"), new Emoji("\ud83c\udf37"), new Emoji("\ud83c\udf31"), new Emoji("\ud83c\udf32"), new Emoji("\ud83c\udf33"), new Emoji("\ud83c\udf34"), new Emoji("\ud83c\udf35"), new Emoji("\ud83c\udf3e"), new Emoji("\ud83c\udf3f"), new Emoji("\u2618\ufe0f"), new Emoji("\ud83c\udf40"), new Emoji("\ud83c\udf41"), new Emoji("\ud83c\udf42"), new Emoji("\ud83c\udf43") + ), Uri.parse("emoji/Nature.png")); - private static final EmojiPageModel PAGE_FOODS = new StaticEmojiPageModel(R.attr.emoji_category_foods, new Emoji[] { - new Emoji("\ud83c\udf47"), new Emoji("\ud83c\udf48"), new Emoji("\ud83c\udf49"), new Emoji("\ud83c\udf4a"), new Emoji("\ud83c\udf4b"), new Emoji("\ud83c\udf4c"), new Emoji("\ud83c\udf4d"), new Emoji("\ud83c\udf4e"), new Emoji("\ud83c\udf4f"), new Emoji("\ud83c\udf50"), new Emoji("\ud83c\udf51"), new Emoji("\ud83c\udf52"), new Emoji("\ud83c\udf53"), new Emoji("\ud83e\udd5d"), new Emoji("\ud83c\udf45"), new Emoji("\ud83e\udd65"), new Emoji("\ud83e\udd51"), new Emoji("\ud83c\udf46"), new Emoji("\ud83e\udd54"), new Emoji("\ud83e\udd55"), new Emoji("\ud83c\udf3d"), new Emoji("\ud83c\udf36\ufe0f"), new Emoji("\ud83e\udd52"), new Emoji("\ud83e\udd66"), new Emoji("\ud83c\udf44"), new Emoji("\ud83e\udd5c"), new Emoji("\ud83c\udf30"), new Emoji("\ud83c\udf5e"), new Emoji("\ud83e\udd50"), new Emoji("\ud83e\udd56"), new Emoji("\ud83e\udd68"), new Emoji("\ud83e\udd5e"), new Emoji("\ud83e\uddc0"), new Emoji("\ud83c\udf56"), new Emoji("\ud83c\udf57"), new Emoji("\ud83e\udd69"), new Emoji("\ud83e\udd53"), new Emoji("\ud83c\udf54"), new Emoji("\ud83c\udf5f"), new Emoji("\ud83c\udf55"), new Emoji("\ud83c\udf2d"), new Emoji("\ud83e\udd6a"), new Emoji("\ud83c\udf2e"), new Emoji("\ud83c\udf2f"), new Emoji("\ud83e\udd59"), new Emoji("\ud83e\udd5a"), new Emoji("\ud83c\udf73"), new Emoji("\ud83e\udd58"), new Emoji("\ud83c\udf72"), new Emoji("\ud83e\udd63"), new Emoji("\ud83e\udd57"), new Emoji("\ud83c\udf7f"), new Emoji("\ud83e\udd6b"), new Emoji("\ud83c\udf71"), new Emoji("\ud83c\udf58"), new Emoji("\ud83c\udf59"), new Emoji("\ud83c\udf5a"), new Emoji("\ud83c\udf5b"), new Emoji("\ud83c\udf5c"), new Emoji("\ud83c\udf5d"), new Emoji("\ud83c\udf60"), new Emoji("\ud83c\udf62"), new Emoji("\ud83c\udf63"), new Emoji("\ud83c\udf64"), new Emoji("\ud83c\udf65"), new Emoji("\ud83c\udf61"), new Emoji("\ud83e\udd5f"), new Emoji("\ud83e\udd60"), new Emoji("\ud83e\udd61"), new Emoji("\ud83c\udf66"), new Emoji("\ud83c\udf67"), new Emoji("\ud83c\udf68"), new Emoji("\ud83c\udf69"), new Emoji("\ud83c\udf6a"), new Emoji("\ud83c\udf82"), new Emoji("\ud83c\udf70"), new Emoji("\ud83e\udd67"), new Emoji("\ud83c\udf6b"), new Emoji("\ud83c\udf6c"), new Emoji("\ud83c\udf6d"), new Emoji("\ud83c\udf6e"), new Emoji("\ud83c\udf6f"), new Emoji("\ud83c\udf7c"), new Emoji("\ud83e\udd5b"), new Emoji("\u2615"), new Emoji("\ud83c\udf75"), new Emoji("\ud83c\udf76"), new Emoji("\ud83c\udf7e"), new Emoji("\ud83c\udf77"), new Emoji("\ud83c\udf78"), new Emoji("\ud83c\udf79"), new Emoji("\ud83c\udf7a"), new Emoji("\ud83c\udf7b"), new Emoji("\ud83e\udd42"), new Emoji("\ud83e\udd43"), new Emoji("\ud83e\udd64"), new Emoji("\ud83e\udd62"), new Emoji("\ud83c\udf7d\ufe0f"), new Emoji("\ud83c\udf74"), new Emoji("\ud83e\udd44"), new Emoji("\ud83d\udd2a"), new Emoji("\ud83c\udffa"), - }, "emoji/Foods.png"); + private static final EmojiPageModel PAGE_FOODS = new StaticEmojiPageModel(EmojiCategory.FOODS, Arrays.asList( + new Emoji("\ud83c\udf47"), new Emoji("\ud83c\udf48"), new Emoji("\ud83c\udf49"), new Emoji("\ud83c\udf4a"), new Emoji("\ud83c\udf4b"), new Emoji("\ud83c\udf4c"), new Emoji("\ud83c\udf4d"), new Emoji("\ud83c\udf4e"), new Emoji("\ud83c\udf4f"), new Emoji("\ud83c\udf50"), new Emoji("\ud83c\udf51"), new Emoji("\ud83c\udf52"), new Emoji("\ud83c\udf53"), new Emoji("\ud83e\udd5d"), new Emoji("\ud83c\udf45"), new Emoji("\ud83e\udd65"), new Emoji("\ud83e\udd51"), new Emoji("\ud83c\udf46"), new Emoji("\ud83e\udd54"), new Emoji("\ud83e\udd55"), new Emoji("\ud83c\udf3d"), new Emoji("\ud83c\udf36\ufe0f"), new Emoji("\ud83e\udd52"), new Emoji("\ud83e\udd66"), new Emoji("\ud83c\udf44"), new Emoji("\ud83e\udd5c"), new Emoji("\ud83c\udf30"), new Emoji("\ud83c\udf5e"), new Emoji("\ud83e\udd50"), new Emoji("\ud83e\udd56"), new Emoji("\ud83e\udd68"), new Emoji("\ud83e\udd5e"), new Emoji("\ud83e\uddc0"), new Emoji("\ud83c\udf56"), new Emoji("\ud83c\udf57"), new Emoji("\ud83e\udd69"), new Emoji("\ud83e\udd53"), new Emoji("\ud83c\udf54"), new Emoji("\ud83c\udf5f"), new Emoji("\ud83c\udf55"), new Emoji("\ud83c\udf2d"), new Emoji("\ud83e\udd6a"), new Emoji("\ud83c\udf2e"), new Emoji("\ud83c\udf2f"), new Emoji("\ud83e\udd59"), new Emoji("\ud83e\udd5a"), new Emoji("\ud83c\udf73"), new Emoji("\ud83e\udd58"), new Emoji("\ud83c\udf72"), new Emoji("\ud83e\udd63"), new Emoji("\ud83e\udd57"), new Emoji("\ud83c\udf7f"), new Emoji("\ud83e\udd6b"), new Emoji("\ud83c\udf71"), new Emoji("\ud83c\udf58"), new Emoji("\ud83c\udf59"), new Emoji("\ud83c\udf5a"), new Emoji("\ud83c\udf5b"), new Emoji("\ud83c\udf5c"), new Emoji("\ud83c\udf5d"), new Emoji("\ud83c\udf60"), new Emoji("\ud83c\udf62"), new Emoji("\ud83c\udf63"), new Emoji("\ud83c\udf64"), new Emoji("\ud83c\udf65"), new Emoji("\ud83c\udf61"), new Emoji("\ud83e\udd5f"), new Emoji("\ud83e\udd60"), new Emoji("\ud83e\udd61"), new Emoji("\ud83c\udf66"), new Emoji("\ud83c\udf67"), new Emoji("\ud83c\udf68"), new Emoji("\ud83c\udf69"), new Emoji("\ud83c\udf6a"), new Emoji("\ud83c\udf82"), new Emoji("\ud83c\udf70"), new Emoji("\ud83e\udd67"), new Emoji("\ud83c\udf6b"), new Emoji("\ud83c\udf6c"), new Emoji("\ud83c\udf6d"), new Emoji("\ud83c\udf6e"), new Emoji("\ud83c\udf6f"), new Emoji("\ud83c\udf7c"), new Emoji("\ud83e\udd5b"), new Emoji("\u2615"), new Emoji("\ud83c\udf75"), new Emoji("\ud83c\udf76"), new Emoji("\ud83c\udf7e"), new Emoji("\ud83c\udf77"), new Emoji("\ud83c\udf78"), new Emoji("\ud83c\udf79"), new Emoji("\ud83c\udf7a"), new Emoji("\ud83c\udf7b"), new Emoji("\ud83e\udd42"), new Emoji("\ud83e\udd43"), new Emoji("\ud83e\udd64"), new Emoji("\ud83e\udd62"), new Emoji("\ud83c\udf7d\ufe0f"), new Emoji("\ud83c\udf74"), new Emoji("\ud83e\udd44"), new Emoji("\ud83d\udd2a"), new Emoji("\ud83c\udffa") + ), Uri.parse("emoji/Foods.png")); - private static final EmojiPageModel PAGE_ACTIVITY = new StaticEmojiPageModel(R.attr.emoji_category_activity, new Emoji[] { - new Emoji("\ud83c\udf83"), new Emoji("\ud83c\udf84"), new Emoji("\ud83c\udf86"), new Emoji("\ud83c\udf87"), new Emoji("\u2728"), new Emoji("\ud83c\udf88"), new Emoji("\ud83c\udf89"), new Emoji("\ud83c\udf8a"), new Emoji("\ud83c\udf8b"), new Emoji("\ud83c\udf8d"), new Emoji("\ud83c\udf8e"), new Emoji("\ud83c\udf8f"), new Emoji("\ud83c\udf90"), new Emoji("\ud83c\udf91"), new Emoji("\ud83c\udf80"), new Emoji("\ud83c\udf81"), new Emoji("\ud83c\udf97\ufe0f"), new Emoji("\ud83c\udf9f\ufe0f"), new Emoji("\ud83c\udfab"), new Emoji("\ud83c\udf96\ufe0f"), new Emoji("\ud83c\udfc6"), new Emoji("\ud83c\udfc5"), new Emoji("\ud83e\udd47"), new Emoji("\ud83e\udd48"), new Emoji("\ud83e\udd49"), new Emoji("\u26bd"), new Emoji("\u26be"), new Emoji("\ud83c\udfc0"), new Emoji("\ud83c\udfd0"), new Emoji("\ud83c\udfc8"), new Emoji("\ud83c\udfc9"), new Emoji("\ud83c\udfbe"), new Emoji("\ud83c\udfb1"), new Emoji("\ud83c\udfb3"), new Emoji("\ud83c\udfcf"), new Emoji("\ud83c\udfd1"), new Emoji("\ud83c\udfd2"), new Emoji("\ud83c\udfd3"), new Emoji("\ud83c\udff8"), new Emoji("\ud83e\udd4a"), new Emoji("\ud83e\udd4b"), new Emoji("\ud83e\udd45"), new Emoji("\ud83c\udfaf"), new Emoji("\u26f3"), new Emoji("\u26f8\ufe0f"), new Emoji("\ud83c\udfa3"), new Emoji("\ud83c\udfbd"), new Emoji("\ud83c\udfbf"), new Emoji("\ud83d\udef7"), new Emoji("\ud83e\udd4c"), new Emoji("\ud83c\udfae"), new Emoji("\ud83d\udd79\ufe0f"), new Emoji("\ud83c\udfb2"), new Emoji("\u2660\ufe0f"), new Emoji("\u2665\ufe0f"), new Emoji("\u2666\ufe0f"), new Emoji("\u2663\ufe0f"), new Emoji("\ud83c\udccf"), new Emoji("\ud83c\udc04"), new Emoji("\ud83c\udfb4"), - }, "emoji/Activity.png"); + private static final EmojiPageModel PAGE_ACTIVITY = new StaticEmojiPageModel(EmojiCategory.ACTIVITY, Arrays.asList( + new Emoji("\ud83c\udf83"), new Emoji("\ud83c\udf84"), new Emoji("\ud83c\udf86"), new Emoji("\ud83c\udf87"), new Emoji("\u2728"), new Emoji("\ud83c\udf88"), new Emoji("\ud83c\udf89"), new Emoji("\ud83c\udf8a"), new Emoji("\ud83c\udf8b"), new Emoji("\ud83c\udf8d"), new Emoji("\ud83c\udf8e"), new Emoji("\ud83c\udf8f"), new Emoji("\ud83c\udf90"), new Emoji("\ud83c\udf91"), new Emoji("\ud83c\udf80"), new Emoji("\ud83c\udf81"), new Emoji("\ud83c\udf97\ufe0f"), new Emoji("\ud83c\udf9f\ufe0f"), new Emoji("\ud83c\udfab"), new Emoji("\ud83c\udf96\ufe0f"), new Emoji("\ud83c\udfc6"), new Emoji("\ud83c\udfc5"), new Emoji("\ud83e\udd47"), new Emoji("\ud83e\udd48"), new Emoji("\ud83e\udd49"), new Emoji("\u26bd"), new Emoji("\u26be"), new Emoji("\ud83c\udfc0"), new Emoji("\ud83c\udfd0"), new Emoji("\ud83c\udfc8"), new Emoji("\ud83c\udfc9"), new Emoji("\ud83c\udfbe"), new Emoji("\ud83c\udfb1"), new Emoji("\ud83c\udfb3"), new Emoji("\ud83c\udfcf"), new Emoji("\ud83c\udfd1"), new Emoji("\ud83c\udfd2"), new Emoji("\ud83c\udfd3"), new Emoji("\ud83c\udff8"), new Emoji("\ud83e\udd4a"), new Emoji("\ud83e\udd4b"), new Emoji("\ud83e\udd45"), new Emoji("\ud83c\udfaf"), new Emoji("\u26f3"), new Emoji("\u26f8\ufe0f"), new Emoji("\ud83c\udfa3"), new Emoji("\ud83c\udfbd"), new Emoji("\ud83c\udfbf"), new Emoji("\ud83d\udef7"), new Emoji("\ud83e\udd4c"), new Emoji("\ud83c\udfae"), new Emoji("\ud83d\udd79\ufe0f"), new Emoji("\ud83c\udfb2"), new Emoji("\u2660\ufe0f"), new Emoji("\u2665\ufe0f"), new Emoji("\u2666\ufe0f"), new Emoji("\u2663\ufe0f"), new Emoji("\ud83c\udccf"), new Emoji("\ud83c\udc04"), new Emoji("\ud83c\udfb4") + ), Uri.parse("emoji/Activity.png")); - private static final EmojiPageModel PAGE_PLACES =new StaticEmojiPageModel(R.attr.emoji_category_places, new Emoji[] { - new Emoji("\ud83c\udf0d"), new Emoji("\ud83c\udf0e"), new Emoji("\ud83c\udf0f"), new Emoji("\ud83c\udf10"), new Emoji("\ud83d\uddfa\ufe0f"), new Emoji("\ud83d\uddfe"), new Emoji("\ud83c\udfd4\ufe0f"), new Emoji("\u26f0\ufe0f"), new Emoji("\ud83c\udf0b"), new Emoji("\ud83d\uddfb"), new Emoji("\ud83c\udfd5\ufe0f"), new Emoji("\ud83c\udfd6\ufe0f"), new Emoji("\ud83c\udfdc\ufe0f"), new Emoji("\ud83c\udfdd\ufe0f"), new Emoji("\ud83c\udfde\ufe0f"), new Emoji("\ud83c\udfdf\ufe0f"), new Emoji("\ud83c\udfdb\ufe0f"), new Emoji("\ud83c\udfd7\ufe0f"), new Emoji("\ud83c\udfd8\ufe0f"), new Emoji("\ud83c\udfd9\ufe0f"), new Emoji("\ud83c\udfda\ufe0f"), new Emoji("\ud83c\udfe0"), new Emoji("\ud83c\udfe1"), new Emoji("\ud83c\udfe2"), new Emoji("\ud83c\udfe3"), new Emoji("\ud83c\udfe4"), new Emoji("\ud83c\udfe5"), new Emoji("\ud83c\udfe6"), new Emoji("\ud83c\udfe8"), new Emoji("\ud83c\udfe9"), new Emoji("\ud83c\udfea"), new Emoji("\ud83c\udfeb"), new Emoji("\ud83c\udfec"), new Emoji("\ud83c\udfed"), new Emoji("\ud83c\udfef"), new Emoji("\ud83c\udff0"), new Emoji("\ud83d\udc92"), new Emoji("\ud83d\uddfc"), new Emoji("\ud83d\uddfd"), new Emoji("\u26ea"), new Emoji("\ud83d\udd4c"), new Emoji("\ud83d\udd4d"), new Emoji("\u26e9\ufe0f"), new Emoji("\ud83d\udd4b"), new Emoji("\u26f2"), new Emoji("\u26fa"), new Emoji("\ud83c\udf01"), new Emoji("\ud83c\udf03"), new Emoji("\ud83c\udf04"), new Emoji("\ud83c\udf05"), new Emoji("\ud83c\udf06"), new Emoji("\ud83c\udf07"), new Emoji("\ud83c\udf09"), new Emoji("\u2668\ufe0f"), new Emoji("\ud83c\udf0c"), new Emoji("\ud83c\udfa0"), new Emoji("\ud83c\udfa1"), new Emoji("\ud83c\udfa2"), new Emoji("\ud83d\udc88"), new Emoji("\ud83c\udfaa"), new Emoji("\ud83c\udfad"), new Emoji("\ud83d\uddbc\ufe0f"), new Emoji("\ud83c\udfa8"), new Emoji("\ud83c\udfb0"), new Emoji("\ud83d\ude82"), new Emoji("\ud83d\ude83"), new Emoji("\ud83d\ude84"), new Emoji("\ud83d\ude85"), new Emoji("\ud83d\ude86"), new Emoji("\ud83d\ude87"), new Emoji("\ud83d\ude88"), new Emoji("\ud83d\ude89"), new Emoji("\ud83d\ude8a"), new Emoji("\ud83d\ude9d"), new Emoji("\ud83d\ude9e"), new Emoji("\ud83d\ude8b"), new Emoji("\ud83d\ude8c"), new Emoji("\ud83d\ude8d"), new Emoji("\ud83d\ude8e"), new Emoji("\ud83d\ude90"), new Emoji("\ud83d\ude91"), new Emoji("\ud83d\ude92"), new Emoji("\ud83d\ude93"), new Emoji("\ud83d\ude94"), new Emoji("\ud83d\ude95"), new Emoji("\ud83d\ude96"), new Emoji("\ud83d\ude97"), new Emoji("\ud83d\ude98"), new Emoji("\ud83d\ude99"), new Emoji("\ud83d\ude9a"), new Emoji("\ud83d\ude9b"), new Emoji("\ud83d\ude9c"), new Emoji("\ud83d\udeb2"), new Emoji("\ud83d\udef4"), new Emoji("\ud83d\udef5"), new Emoji("\ud83d\ude8f"), new Emoji("\ud83d\udee3\ufe0f"), new Emoji("\ud83d\udee4\ufe0f"), new Emoji("\u26fd"), new Emoji("\ud83d\udea8"), new Emoji("\ud83d\udea5"), new Emoji("\ud83d\udea6"), new Emoji("\ud83d\udea7"), new Emoji("\ud83d\uded1"), new Emoji("\u2693"), new Emoji("\u26f5"), new Emoji("\ud83d\udef6"), new Emoji("\ud83d\udea4"), new Emoji("\ud83d\udef3\ufe0f"), new Emoji("\u26f4\ufe0f"), new Emoji("\ud83d\udee5\ufe0f"), new Emoji("\ud83d\udea2"), new Emoji("\u2708\ufe0f"), new Emoji("\ud83d\udee9\ufe0f"), new Emoji("\ud83d\udeeb"), new Emoji("\ud83d\udeec"), new Emoji("\ud83d\udcba"), new Emoji("\ud83d\ude81"), new Emoji("\ud83d\ude9f"), new Emoji("\ud83d\udea0"), new Emoji("\ud83d\udea1"), new Emoji("\ud83d\udef0\ufe0f"), new Emoji("\ud83d\ude80"), new Emoji("\ud83d\udef8"), new Emoji("\ud83d\udece\ufe0f"), new Emoji("\ud83d\udeaa"), new Emoji("\ud83d\udecf\ufe0f"), new Emoji("\ud83d\udecb\ufe0f"), new Emoji("\ud83d\udebd"), new Emoji("\ud83d\udebf"), new Emoji("\ud83d\udec1"), new Emoji("\u231b"), new Emoji("\u23f3"), new Emoji("\u231a"), new Emoji("\u23f0"), new Emoji("\u23f1\ufe0f"), new Emoji("\u23f2\ufe0f"), new Emoji("\ud83d\udd70\ufe0f"), new Emoji("\ud83d\udd5b"), new Emoji("\ud83d\udd67"), new Emoji("\ud83d\udd50"), new Emoji("\ud83d\udd5c"), new Emoji("\ud83d\udd51"), new Emoji("\ud83d\udd5d"), new Emoji("\ud83d\udd52"), new Emoji("\ud83d\udd5e"), new Emoji("\ud83d\udd53"), new Emoji("\ud83d\udd5f"), new Emoji("\ud83d\udd54"), new Emoji("\ud83d\udd60"), new Emoji("\ud83d\udd55"), new Emoji("\ud83d\udd61"), new Emoji("\ud83d\udd56"), new Emoji("\ud83d\udd62"), new Emoji("\ud83d\udd57"), new Emoji("\ud83d\udd63"), new Emoji("\ud83d\udd58"), new Emoji("\ud83d\udd64"), new Emoji("\ud83d\udd59"), new Emoji("\ud83d\udd65"), new Emoji("\ud83d\udd5a"), new Emoji("\ud83d\udd66"), new Emoji("\ud83c\udf11"), new Emoji("\ud83c\udf12"), new Emoji("\ud83c\udf13"), new Emoji("\ud83c\udf14"), new Emoji("\ud83c\udf15"), new Emoji("\ud83c\udf16"), new Emoji("\ud83c\udf17"), new Emoji("\ud83c\udf18"), new Emoji("\ud83c\udf19"), new Emoji("\ud83c\udf1a"), new Emoji("\ud83c\udf1b"), new Emoji("\ud83c\udf1c"), new Emoji("\ud83c\udf21\ufe0f"), new Emoji("\u2600\ufe0f"), new Emoji("\ud83c\udf1d"), new Emoji("\ud83c\udf1e"), new Emoji("\u2b50"), new Emoji("\ud83c\udf1f"), new Emoji("\ud83c\udf20"), new Emoji("\u2601\ufe0f"), new Emoji("\u26c5"), new Emoji("\u26c8\ufe0f"), new Emoji("\ud83c\udf24\ufe0f"), new Emoji("\ud83c\udf25\ufe0f"), new Emoji("\ud83c\udf26\ufe0f"), new Emoji("\ud83c\udf27\ufe0f"), new Emoji("\ud83c\udf28\ufe0f"), new Emoji("\ud83c\udf29\ufe0f"), new Emoji("\ud83c\udf2a\ufe0f"), new Emoji("\ud83c\udf2b\ufe0f"), new Emoji("\ud83c\udf2c\ufe0f"), new Emoji("\ud83c\udf00"), new Emoji("\ud83c\udf08"), new Emoji("\ud83c\udf02"), new Emoji("\u2602\ufe0f"), new Emoji("\u2614"), new Emoji("\u26f1\ufe0f"), new Emoji("\u26a1"), new Emoji("\u2744\ufe0f"), new Emoji("\u2603\ufe0f"), new Emoji("\u26c4"), new Emoji("\u2604\ufe0f"), new Emoji("\ud83d\udd25"), new Emoji("\ud83d\udca7"), new Emoji("\ud83c\udf0a"), - }, "emoji/Places.png"); + private static final EmojiPageModel PAGE_PLACES = new StaticEmojiPageModel(EmojiCategory.PLACES, Arrays.asList( + new Emoji("\ud83c\udf0d"), new Emoji("\ud83c\udf0e"), new Emoji("\ud83c\udf0f"), new Emoji("\ud83c\udf10"), new Emoji("\ud83d\uddfa\ufe0f"), new Emoji("\ud83d\uddfe"), new Emoji("\ud83c\udfd4\ufe0f"), new Emoji("\u26f0\ufe0f"), new Emoji("\ud83c\udf0b"), new Emoji("\ud83d\uddfb"), new Emoji("\ud83c\udfd5\ufe0f"), new Emoji("\ud83c\udfd6\ufe0f"), new Emoji("\ud83c\udfdc\ufe0f"), new Emoji("\ud83c\udfdd\ufe0f"), new Emoji("\ud83c\udfde\ufe0f"), new Emoji("\ud83c\udfdf\ufe0f"), new Emoji("\ud83c\udfdb\ufe0f"), new Emoji("\ud83c\udfd7\ufe0f"), new Emoji("\ud83c\udfd8\ufe0f"), new Emoji("\ud83c\udfd9\ufe0f"), new Emoji("\ud83c\udfda\ufe0f"), new Emoji("\ud83c\udfe0"), new Emoji("\ud83c\udfe1"), new Emoji("\ud83c\udfe2"), new Emoji("\ud83c\udfe3"), new Emoji("\ud83c\udfe4"), new Emoji("\ud83c\udfe5"), new Emoji("\ud83c\udfe6"), new Emoji("\ud83c\udfe8"), new Emoji("\ud83c\udfe9"), new Emoji("\ud83c\udfea"), new Emoji("\ud83c\udfeb"), new Emoji("\ud83c\udfec"), new Emoji("\ud83c\udfed"), new Emoji("\ud83c\udfef"), new Emoji("\ud83c\udff0"), new Emoji("\ud83d\udc92"), new Emoji("\ud83d\uddfc"), new Emoji("\ud83d\uddfd"), new Emoji("\u26ea"), new Emoji("\ud83d\udd4c"), new Emoji("\ud83d\udd4d"), new Emoji("\u26e9\ufe0f"), new Emoji("\ud83d\udd4b"), new Emoji("\u26f2"), new Emoji("\u26fa"), new Emoji("\ud83c\udf01"), new Emoji("\ud83c\udf03"), new Emoji("\ud83c\udf04"), new Emoji("\ud83c\udf05"), new Emoji("\ud83c\udf06"), new Emoji("\ud83c\udf07"), new Emoji("\ud83c\udf09"), new Emoji("\u2668\ufe0f"), new Emoji("\ud83c\udf0c"), new Emoji("\ud83c\udfa0"), new Emoji("\ud83c\udfa1"), new Emoji("\ud83c\udfa2"), new Emoji("\ud83d\udc88"), new Emoji("\ud83c\udfaa"), new Emoji("\ud83c\udfad"), new Emoji("\ud83d\uddbc\ufe0f"), new Emoji("\ud83c\udfa8"), new Emoji("\ud83c\udfb0"), new Emoji("\ud83d\ude82"), new Emoji("\ud83d\ude83"), new Emoji("\ud83d\ude84"), new Emoji("\ud83d\ude85"), new Emoji("\ud83d\ude86"), new Emoji("\ud83d\ude87"), new Emoji("\ud83d\ude88"), new Emoji("\ud83d\ude89"), new Emoji("\ud83d\ude8a"), new Emoji("\ud83d\ude9d"), new Emoji("\ud83d\ude9e"), new Emoji("\ud83d\ude8b"), new Emoji("\ud83d\ude8c"), new Emoji("\ud83d\ude8d"), new Emoji("\ud83d\ude8e"), new Emoji("\ud83d\ude90"), new Emoji("\ud83d\ude91"), new Emoji("\ud83d\ude92"), new Emoji("\ud83d\ude93"), new Emoji("\ud83d\ude94"), new Emoji("\ud83d\ude95"), new Emoji("\ud83d\ude96"), new Emoji("\ud83d\ude97"), new Emoji("\ud83d\ude98"), new Emoji("\ud83d\ude99"), new Emoji("\ud83d\ude9a"), new Emoji("\ud83d\ude9b"), new Emoji("\ud83d\ude9c"), new Emoji("\ud83d\udeb2"), new Emoji("\ud83d\udef4"), new Emoji("\ud83d\udef5"), new Emoji("\ud83d\ude8f"), new Emoji("\ud83d\udee3\ufe0f"), new Emoji("\ud83d\udee4\ufe0f"), new Emoji("\u26fd"), new Emoji("\ud83d\udea8"), new Emoji("\ud83d\udea5"), new Emoji("\ud83d\udea6"), new Emoji("\ud83d\udea7"), new Emoji("\ud83d\uded1"), new Emoji("\u2693"), new Emoji("\u26f5"), new Emoji("\ud83d\udef6"), new Emoji("\ud83d\udea4"), new Emoji("\ud83d\udef3\ufe0f"), new Emoji("\u26f4\ufe0f"), new Emoji("\ud83d\udee5\ufe0f"), new Emoji("\ud83d\udea2"), new Emoji("\u2708\ufe0f"), new Emoji("\ud83d\udee9\ufe0f"), new Emoji("\ud83d\udeeb"), new Emoji("\ud83d\udeec"), new Emoji("\ud83d\udcba"), new Emoji("\ud83d\ude81"), new Emoji("\ud83d\ude9f"), new Emoji("\ud83d\udea0"), new Emoji("\ud83d\udea1"), new Emoji("\ud83d\udef0\ufe0f"), new Emoji("\ud83d\ude80"), new Emoji("\ud83d\udef8"), new Emoji("\ud83d\udece\ufe0f"), new Emoji("\ud83d\udeaa"), new Emoji("\ud83d\udecf\ufe0f"), new Emoji("\ud83d\udecb\ufe0f"), new Emoji("\ud83d\udebd"), new Emoji("\ud83d\udebf"), new Emoji("\ud83d\udec1"), new Emoji("\u231b"), new Emoji("\u23f3"), new Emoji("\u231a"), new Emoji("\u23f0"), new Emoji("\u23f1\ufe0f"), new Emoji("\u23f2\ufe0f"), new Emoji("\ud83d\udd70\ufe0f"), new Emoji("\ud83d\udd5b"), new Emoji("\ud83d\udd67"), new Emoji("\ud83d\udd50"), new Emoji("\ud83d\udd5c"), new Emoji("\ud83d\udd51"), new Emoji("\ud83d\udd5d"), new Emoji("\ud83d\udd52"), new Emoji("\ud83d\udd5e"), new Emoji("\ud83d\udd53"), new Emoji("\ud83d\udd5f"), new Emoji("\ud83d\udd54"), new Emoji("\ud83d\udd60"), new Emoji("\ud83d\udd55"), new Emoji("\ud83d\udd61"), new Emoji("\ud83d\udd56"), new Emoji("\ud83d\udd62"), new Emoji("\ud83d\udd57"), new Emoji("\ud83d\udd63"), new Emoji("\ud83d\udd58"), new Emoji("\ud83d\udd64"), new Emoji("\ud83d\udd59"), new Emoji("\ud83d\udd65"), new Emoji("\ud83d\udd5a"), new Emoji("\ud83d\udd66"), new Emoji("\ud83c\udf11"), new Emoji("\ud83c\udf12"), new Emoji("\ud83c\udf13"), new Emoji("\ud83c\udf14"), new Emoji("\ud83c\udf15"), new Emoji("\ud83c\udf16"), new Emoji("\ud83c\udf17"), new Emoji("\ud83c\udf18"), new Emoji("\ud83c\udf19"), new Emoji("\ud83c\udf1a"), new Emoji("\ud83c\udf1b"), new Emoji("\ud83c\udf1c"), new Emoji("\ud83c\udf21\ufe0f"), new Emoji("\u2600\ufe0f"), new Emoji("\ud83c\udf1d"), new Emoji("\ud83c\udf1e"), new Emoji("\u2b50"), new Emoji("\ud83c\udf1f"), new Emoji("\ud83c\udf20"), new Emoji("\u2601\ufe0f"), new Emoji("\u26c5"), new Emoji("\u26c8\ufe0f"), new Emoji("\ud83c\udf24\ufe0f"), new Emoji("\ud83c\udf25\ufe0f"), new Emoji("\ud83c\udf26\ufe0f"), new Emoji("\ud83c\udf27\ufe0f"), new Emoji("\ud83c\udf28\ufe0f"), new Emoji("\ud83c\udf29\ufe0f"), new Emoji("\ud83c\udf2a\ufe0f"), new Emoji("\ud83c\udf2b\ufe0f"), new Emoji("\ud83c\udf2c\ufe0f"), new Emoji("\ud83c\udf00"), new Emoji("\ud83c\udf08"), new Emoji("\ud83c\udf02"), new Emoji("\u2602\ufe0f"), new Emoji("\u2614"), new Emoji("\u26f1\ufe0f"), new Emoji("\u26a1"), new Emoji("\u2744\ufe0f"), new Emoji("\u2603\ufe0f"), new Emoji("\u26c4"), new Emoji("\u2604\ufe0f"), new Emoji("\ud83d\udd25"), new Emoji("\ud83d\udca7"), new Emoji("\ud83c\udf0a") + ), Uri.parse("emoji/Places.png")); - private static final EmojiPageModel PAGE_OBJECTS = new StaticEmojiPageModel(R.attr.emoji_category_objects, new Emoji[] { - new Emoji("\ud83d\udd07"), new Emoji("\ud83d\udd08"), new Emoji("\ud83d\udd09"), new Emoji("\ud83d\udd0a"), new Emoji("\ud83d\udce2"), new Emoji("\ud83d\udce3"), new Emoji("\ud83d\udcef"), new Emoji("\ud83d\udd14"), new Emoji("\ud83d\udd15"), new Emoji("\ud83c\udfbc"), new Emoji("\ud83c\udfb5"), new Emoji("\ud83c\udfb6"), new Emoji("\ud83c\udf99\ufe0f"), new Emoji("\ud83c\udf9a\ufe0f"), new Emoji("\ud83c\udf9b\ufe0f"), new Emoji("\ud83c\udfa4"), new Emoji("\ud83c\udfa7"), new Emoji("\ud83d\udcfb"), new Emoji("\ud83c\udfb7"), new Emoji("\ud83c\udfb8"), new Emoji("\ud83c\udfb9"), new Emoji("\ud83c\udfba"), new Emoji("\ud83c\udfbb"), new Emoji("\ud83e\udd41"), new Emoji("\ud83d\udcf1"), new Emoji("\ud83d\udcf2"), new Emoji("\u260e\ufe0f"), new Emoji("\ud83d\udcde"), new Emoji("\ud83d\udcdf"), new Emoji("\ud83d\udce0"), new Emoji("\ud83d\udd0b"), new Emoji("\ud83d\udd0c"), new Emoji("\ud83d\udcbb"), new Emoji("\ud83d\udda5\ufe0f"), new Emoji("\ud83d\udda8\ufe0f"), new Emoji("\u2328\ufe0f"), new Emoji("\ud83d\uddb1\ufe0f"), new Emoji("\ud83d\uddb2\ufe0f"), new Emoji("\ud83d\udcbd"), new Emoji("\ud83d\udcbe"), new Emoji("\ud83d\udcbf"), new Emoji("\ud83d\udcc0"), new Emoji("\ud83c\udfa5"), new Emoji("\ud83c\udf9e\ufe0f"), new Emoji("\ud83d\udcfd\ufe0f"), new Emoji("\ud83c\udfac"), new Emoji("\ud83d\udcfa"), new Emoji("\ud83d\udcf7"), new Emoji("\ud83d\udcf8"), new Emoji("\ud83d\udcf9"), new Emoji("\ud83d\udcfc"), new Emoji("\ud83d\udd0d"), new Emoji("\ud83d\udd0e"), new Emoji("\ud83d\udd2c"), new Emoji("\ud83d\udd2d"), new Emoji("\ud83d\udce1"), new Emoji("\ud83d\udd6f\ufe0f"), new Emoji("\ud83d\udca1"), new Emoji("\ud83d\udd26"), new Emoji("\ud83c\udfee"), new Emoji("\ud83d\udcd4"), new Emoji("\ud83d\udcd5"), new Emoji("\ud83d\udcd6"), new Emoji("\ud83d\udcd7"), new Emoji("\ud83d\udcd8"), new Emoji("\ud83d\udcd9"), new Emoji("\ud83d\udcda"), new Emoji("\ud83d\udcd3"), new Emoji("\ud83d\udcd2"), new Emoji("\ud83d\udcc3"), new Emoji("\ud83d\udcdc"), new Emoji("\ud83d\udcc4"), new Emoji("\ud83d\udcf0"), new Emoji("\ud83d\uddde\ufe0f"), new Emoji("\ud83d\udcd1"), new Emoji("\ud83d\udd16"), new Emoji("\ud83c\udff7\ufe0f"), new Emoji("\ud83d\udcb0"), new Emoji("\ud83d\udcb4"), new Emoji("\ud83d\udcb5"), new Emoji("\ud83d\udcb6"), new Emoji("\ud83d\udcb7"), new Emoji("\ud83d\udcb8"), new Emoji("\ud83d\udcb3"), new Emoji("\ud83d\udcb9"), new Emoji("\ud83d\udcb1"), new Emoji("\ud83d\udcb2"), new Emoji("\u2709\ufe0f"), new Emoji("\ud83d\udce7"), new Emoji("\ud83d\udce8"), new Emoji("\ud83d\udce9"), new Emoji("\ud83d\udce4"), new Emoji("\ud83d\udce5"), new Emoji("\ud83d\udce6"), new Emoji("\ud83d\udceb"), new Emoji("\ud83d\udcea"), new Emoji("\ud83d\udcec"), new Emoji("\ud83d\udced"), new Emoji("\ud83d\udcee"), new Emoji("\ud83d\uddf3\ufe0f"), new Emoji("\u270f\ufe0f"), new Emoji("\u2712\ufe0f"), new Emoji("\ud83d\udd8b\ufe0f"), new Emoji("\ud83d\udd8a\ufe0f"), new Emoji("\ud83d\udd8c\ufe0f"), new Emoji("\ud83d\udd8d\ufe0f"), new Emoji("\ud83d\udcdd"), new Emoji("\ud83d\udcbc"), new Emoji("\ud83d\udcc1"), new Emoji("\ud83d\udcc2"), new Emoji("\ud83d\uddc2\ufe0f"), new Emoji("\ud83d\udcc5"), new Emoji("\ud83d\udcc6"), new Emoji("\ud83d\uddd2\ufe0f"), new Emoji("\ud83d\uddd3\ufe0f"), new Emoji("\ud83d\udcc7"), new Emoji("\ud83d\udcc8"), new Emoji("\ud83d\udcc9"), new Emoji("\ud83d\udcca"), new Emoji("\ud83d\udccb"), new Emoji("\ud83d\udccc"), new Emoji("\ud83d\udccd"), new Emoji("\ud83d\udcce"), new Emoji("\ud83d\udd87\ufe0f"), new Emoji("\ud83d\udccf"), new Emoji("\ud83d\udcd0"), new Emoji("\u2702\ufe0f"), new Emoji("\ud83d\uddc3\ufe0f"), new Emoji("\ud83d\uddc4\ufe0f"), new Emoji("\ud83d\uddd1\ufe0f"), new Emoji("\ud83d\udd12"), new Emoji("\ud83d\udd13"), new Emoji("\ud83d\udd0f"), new Emoji("\ud83d\udd10"), new Emoji("\ud83d\udd11"), new Emoji("\ud83d\udddd\ufe0f"), new Emoji("\ud83d\udd28"), new Emoji("\u26cf\ufe0f"), new Emoji("\u2692\ufe0f"), new Emoji("\ud83d\udee0\ufe0f"), new Emoji("\ud83d\udde1\ufe0f"), new Emoji("\u2694\ufe0f"), new Emoji("\ud83d\udd2b"), new Emoji("\ud83c\udff9"), new Emoji("\ud83d\udee1\ufe0f"), new Emoji("\ud83d\udd27"), new Emoji("\ud83d\udd29"), new Emoji("\u2699\ufe0f"), new Emoji("\ud83d\udddc\ufe0f"), new Emoji("\u2697\ufe0f"), new Emoji("\u2696\ufe0f"), new Emoji("\ud83d\udd17"), new Emoji("\u26d3\ufe0f"), new Emoji("\ud83d\udc89"), new Emoji("\ud83d\udc8a"), new Emoji("\ud83d\udeac"), new Emoji("\u26b0\ufe0f"), new Emoji("\u26b1\ufe0f"), new Emoji("\ud83d\uddff"), new Emoji("\ud83d\udee2\ufe0f"), new Emoji("\ud83d\udd2e"), new Emoji("\ud83d\uded2"), - }, "emoji/Objects.png"); + private static final EmojiPageModel PAGE_OBJECTS = new StaticEmojiPageModel(EmojiCategory.OBJECTS, Arrays.asList( + new Emoji("\ud83d\udd07"), new Emoji("\ud83d\udd08"), new Emoji("\ud83d\udd09"), new Emoji("\ud83d\udd0a"), new Emoji("\ud83d\udce2"), new Emoji("\ud83d\udce3"), new Emoji("\ud83d\udcef"), new Emoji("\ud83d\udd14"), new Emoji("\ud83d\udd15"), new Emoji("\ud83c\udfbc"), new Emoji("\ud83c\udfb5"), new Emoji("\ud83c\udfb6"), new Emoji("\ud83c\udf99\ufe0f"), new Emoji("\ud83c\udf9a\ufe0f"), new Emoji("\ud83c\udf9b\ufe0f"), new Emoji("\ud83c\udfa4"), new Emoji("\ud83c\udfa7"), new Emoji("\ud83d\udcfb"), new Emoji("\ud83c\udfb7"), new Emoji("\ud83c\udfb8"), new Emoji("\ud83c\udfb9"), new Emoji("\ud83c\udfba"), new Emoji("\ud83c\udfbb"), new Emoji("\ud83e\udd41"), new Emoji("\ud83d\udcf1"), new Emoji("\ud83d\udcf2"), new Emoji("\u260e\ufe0f"), new Emoji("\ud83d\udcde"), new Emoji("\ud83d\udcdf"), new Emoji("\ud83d\udce0"), new Emoji("\ud83d\udd0b"), new Emoji("\ud83d\udd0c"), new Emoji("\ud83d\udcbb"), new Emoji("\ud83d\udda5\ufe0f"), new Emoji("\ud83d\udda8\ufe0f"), new Emoji("\u2328\ufe0f"), new Emoji("\ud83d\uddb1\ufe0f"), new Emoji("\ud83d\uddb2\ufe0f"), new Emoji("\ud83d\udcbd"), new Emoji("\ud83d\udcbe"), new Emoji("\ud83d\udcbf"), new Emoji("\ud83d\udcc0"), new Emoji("\ud83c\udfa5"), new Emoji("\ud83c\udf9e\ufe0f"), new Emoji("\ud83d\udcfd\ufe0f"), new Emoji("\ud83c\udfac"), new Emoji("\ud83d\udcfa"), new Emoji("\ud83d\udcf7"), new Emoji("\ud83d\udcf8"), new Emoji("\ud83d\udcf9"), new Emoji("\ud83d\udcfc"), new Emoji("\ud83d\udd0d"), new Emoji("\ud83d\udd0e"), new Emoji("\ud83d\udd2c"), new Emoji("\ud83d\udd2d"), new Emoji("\ud83d\udce1"), new Emoji("\ud83d\udd6f\ufe0f"), new Emoji("\ud83d\udca1"), new Emoji("\ud83d\udd26"), new Emoji("\ud83c\udfee"), new Emoji("\ud83d\udcd4"), new Emoji("\ud83d\udcd5"), new Emoji("\ud83d\udcd6"), new Emoji("\ud83d\udcd7"), new Emoji("\ud83d\udcd8"), new Emoji("\ud83d\udcd9"), new Emoji("\ud83d\udcda"), new Emoji("\ud83d\udcd3"), new Emoji("\ud83d\udcd2"), new Emoji("\ud83d\udcc3"), new Emoji("\ud83d\udcdc"), new Emoji("\ud83d\udcc4"), new Emoji("\ud83d\udcf0"), new Emoji("\ud83d\uddde\ufe0f"), new Emoji("\ud83d\udcd1"), new Emoji("\ud83d\udd16"), new Emoji("\ud83c\udff7\ufe0f"), new Emoji("\ud83d\udcb0"), new Emoji("\ud83d\udcb4"), new Emoji("\ud83d\udcb5"), new Emoji("\ud83d\udcb6"), new Emoji("\ud83d\udcb7"), new Emoji("\ud83d\udcb8"), new Emoji("\ud83d\udcb3"), new Emoji("\ud83d\udcb9"), new Emoji("\ud83d\udcb1"), new Emoji("\ud83d\udcb2"), new Emoji("\u2709\ufe0f"), new Emoji("\ud83d\udce7"), new Emoji("\ud83d\udce8"), new Emoji("\ud83d\udce9"), new Emoji("\ud83d\udce4"), new Emoji("\ud83d\udce5"), new Emoji("\ud83d\udce6"), new Emoji("\ud83d\udceb"), new Emoji("\ud83d\udcea"), new Emoji("\ud83d\udcec"), new Emoji("\ud83d\udced"), new Emoji("\ud83d\udcee"), new Emoji("\ud83d\uddf3\ufe0f"), new Emoji("\u270f\ufe0f"), new Emoji("\u2712\ufe0f"), new Emoji("\ud83d\udd8b\ufe0f"), new Emoji("\ud83d\udd8a\ufe0f"), new Emoji("\ud83d\udd8c\ufe0f"), new Emoji("\ud83d\udd8d\ufe0f"), new Emoji("\ud83d\udcdd"), new Emoji("\ud83d\udcbc"), new Emoji("\ud83d\udcc1"), new Emoji("\ud83d\udcc2"), new Emoji("\ud83d\uddc2\ufe0f"), new Emoji("\ud83d\udcc5"), new Emoji("\ud83d\udcc6"), new Emoji("\ud83d\uddd2\ufe0f"), new Emoji("\ud83d\uddd3\ufe0f"), new Emoji("\ud83d\udcc7"), new Emoji("\ud83d\udcc8"), new Emoji("\ud83d\udcc9"), new Emoji("\ud83d\udcca"), new Emoji("\ud83d\udccb"), new Emoji("\ud83d\udccc"), new Emoji("\ud83d\udccd"), new Emoji("\ud83d\udcce"), new Emoji("\ud83d\udd87\ufe0f"), new Emoji("\ud83d\udccf"), new Emoji("\ud83d\udcd0"), new Emoji("\u2702\ufe0f"), new Emoji("\ud83d\uddc3\ufe0f"), new Emoji("\ud83d\uddc4\ufe0f"), new Emoji("\ud83d\uddd1\ufe0f"), new Emoji("\ud83d\udd12"), new Emoji("\ud83d\udd13"), new Emoji("\ud83d\udd0f"), new Emoji("\ud83d\udd10"), new Emoji("\ud83d\udd11"), new Emoji("\ud83d\udddd\ufe0f"), new Emoji("\ud83d\udd28"), new Emoji("\u26cf\ufe0f"), new Emoji("\u2692\ufe0f"), new Emoji("\ud83d\udee0\ufe0f"), new Emoji("\ud83d\udde1\ufe0f"), new Emoji("\u2694\ufe0f"), new Emoji("\ud83d\udd2b"), new Emoji("\ud83c\udff9"), new Emoji("\ud83d\udee1\ufe0f"), new Emoji("\ud83d\udd27"), new Emoji("\ud83d\udd29"), new Emoji("\u2699\ufe0f"), new Emoji("\ud83d\udddc\ufe0f"), new Emoji("\u2697\ufe0f"), new Emoji("\u2696\ufe0f"), new Emoji("\ud83d\udd17"), new Emoji("\u26d3\ufe0f"), new Emoji("\ud83d\udc89"), new Emoji("\ud83d\udc8a"), new Emoji("\ud83d\udeac"), new Emoji("\u26b0\ufe0f"), new Emoji("\u26b1\ufe0f"), new Emoji("\ud83d\uddff"), new Emoji("\ud83d\udee2\ufe0f"), new Emoji("\ud83d\udd2e"), new Emoji("\ud83d\uded2") + ), Uri.parse("emoji/Objects.png")); - private static final EmojiPageModel PAGE_SYMBOLS = new StaticEmojiPageModel(R.attr.emoji_category_symbol, new Emoji[] { - new Emoji("\ud83c\udfe7"), new Emoji("\ud83d\udeae"), new Emoji("\ud83d\udeb0"), new Emoji("\u267f"), new Emoji("\ud83d\udeb9"), new Emoji("\ud83d\udeba"), new Emoji("\ud83d\udebb"), new Emoji("\ud83d\udebc"), new Emoji("\ud83d\udebe"), new Emoji("\ud83d\udec2"), new Emoji("\ud83d\udec3"), new Emoji("\ud83d\udec4"), new Emoji("\ud83d\udec5"), new Emoji("\u26a0\ufe0f"), new Emoji("\ud83d\udeb8"), new Emoji("\u26d4"), new Emoji("\ud83d\udeab"), new Emoji("\ud83d\udeb3"), new Emoji("\ud83d\udead"), new Emoji("\ud83d\udeaf"), new Emoji("\ud83d\udeb1"), new Emoji("\ud83d\udeb7"), new Emoji("\ud83d\udcf5"), new Emoji("\ud83d\udd1e"), new Emoji("\u2622\ufe0f"), new Emoji("\u2623\ufe0f"), new Emoji("\u2b06\ufe0f"), new Emoji("\u2197\ufe0f"), new Emoji("\u27a1\ufe0f"), new Emoji("\u2198\ufe0f"), new Emoji("\u2b07\ufe0f"), new Emoji("\u2199\ufe0f"), new Emoji("\u2b05\ufe0f"), new Emoji("\u2196\ufe0f"), new Emoji("\u2195\ufe0f"), new Emoji("\u2194\ufe0f"), new Emoji("\u21a9\ufe0f"), new Emoji("\u21aa\ufe0f"), new Emoji("\u2934\ufe0f"), new Emoji("\u2935\ufe0f"), new Emoji("\ud83d\udd03"), new Emoji("\ud83d\udd04"), new Emoji("\ud83d\udd19"), new Emoji("\ud83d\udd1a"), new Emoji("\ud83d\udd1b"), new Emoji("\ud83d\udd1c"), new Emoji("\ud83d\udd1d"), new Emoji("\ud83d\uded0"), new Emoji("\u269b\ufe0f"), new Emoji("\ud83d\udd49\ufe0f"), new Emoji("\u2721\ufe0f"), new Emoji("\u2638\ufe0f"), new Emoji("\u262f\ufe0f"), new Emoji("\u271d\ufe0f"), new Emoji("\u2626\ufe0f"), new Emoji("\u262a\ufe0f"), new Emoji("\u262e\ufe0f"), new Emoji("\ud83d\udd4e"), new Emoji("\ud83d\udd2f"), new Emoji("\u2648"), new Emoji("\u2649"), new Emoji("\u264a"), new Emoji("\u264b"), new Emoji("\u264c"), new Emoji("\u264d"), new Emoji("\u264e"), new Emoji("\u264f"), new Emoji("\u2650"), new Emoji("\u2651"), new Emoji("\u2652"), new Emoji("\u2653"), new Emoji("\u26ce"), new Emoji("\ud83d\udd00"), new Emoji("\ud83d\udd01"), new Emoji("\ud83d\udd02"), new Emoji("\u25b6\ufe0f"), new Emoji("\u23e9"), new Emoji("\u23ed\ufe0f"), new Emoji("\u23ef\ufe0f"), new Emoji("\u25c0\ufe0f"), new Emoji("\u23ea"), new Emoji("\u23ee\ufe0f"), new Emoji("\ud83d\udd3c"), new Emoji("\u23eb"), new Emoji("\ud83d\udd3d"), new Emoji("\u23ec"), new Emoji("\u23f8\ufe0f"), new Emoji("\u23f9\ufe0f"), new Emoji("\u23fa\ufe0f"), new Emoji("\u23cf\ufe0f"), new Emoji("\ud83c\udfa6"), new Emoji("\ud83d\udd05"), new Emoji("\ud83d\udd06"), new Emoji("\ud83d\udcf6"), new Emoji("\ud83d\udcf3"), new Emoji("\ud83d\udcf4"), new Emoji("\u267b\ufe0f"), new Emoji("\u269c\ufe0f"), new Emoji("\ud83d\udd31"), new Emoji("\ud83d\udcdb"), new Emoji("\ud83d\udd30"), new Emoji("\u2b55"), new Emoji("\u2705"), new Emoji("\u2611\ufe0f"), new Emoji("\u2714\ufe0f"), new Emoji("\u2716\ufe0f"), new Emoji("\u274c"), new Emoji("\u274e"), new Emoji("\u2795"), new Emoji("\u2796"), new Emoji("\u2797"), new Emoji("\u27b0"), new Emoji("\u27bf"), new Emoji("\u303d\ufe0f"), new Emoji("\u2733\ufe0f"), new Emoji("\u2734\ufe0f"), new Emoji("\u2747\ufe0f"), new Emoji("\u203c\ufe0f"), new Emoji("\u2049\ufe0f"), new Emoji("\u2753"), new Emoji("\u2754"), new Emoji("\u2755"), new Emoji("\u2757"), new Emoji("\u3030\ufe0f"), new Emoji("\u00a9\ufe0f"), new Emoji("\u00ae\ufe0f"), new Emoji("\u2122\ufe0f"), new Emoji("\u0023\ufe0f\u20e3"), new Emoji("\u002a\ufe0f\u20e3"), new Emoji("\u0030\ufe0f\u20e3"), new Emoji("\u0031\ufe0f\u20e3"), new Emoji("\u0032\ufe0f\u20e3"), new Emoji("\u0033\ufe0f\u20e3"), new Emoji("\u0034\ufe0f\u20e3"), new Emoji("\u0035\ufe0f\u20e3"), new Emoji("\u0036\ufe0f\u20e3"), new Emoji("\u0037\ufe0f\u20e3"), new Emoji("\u0038\ufe0f\u20e3"), new Emoji("\u0039\ufe0f\u20e3"), new Emoji("\ud83d\udd1f"), new Emoji("\ud83d\udcaf"), new Emoji("\ud83d\udd20"), new Emoji("\ud83d\udd21"), new Emoji("\ud83d\udd22"), new Emoji("\ud83d\udd23"), new Emoji("\ud83d\udd24"), new Emoji("\ud83c\udd70\ufe0f"), new Emoji("\ud83c\udd8e"), new Emoji("\ud83c\udd71\ufe0f"), new Emoji("\ud83c\udd91"), new Emoji("\ud83c\udd92"), new Emoji("\ud83c\udd93"), new Emoji("\u2139\ufe0f"), new Emoji("\ud83c\udd94"), new Emoji("\u24c2\ufe0f"), new Emoji("\ud83c\udd95"), new Emoji("\ud83c\udd96"), new Emoji("\ud83c\udd7e\ufe0f"), new Emoji("\ud83c\udd97"), new Emoji("\ud83c\udd7f\ufe0f"), new Emoji("\ud83c\udd98"), new Emoji("\ud83c\udd99"), new Emoji("\ud83c\udd9a"), new Emoji("\ud83c\ude01"), new Emoji("\ud83c\ude02\ufe0f"), new Emoji("\ud83c\ude37\ufe0f"), new Emoji("\ud83c\ude36"), new Emoji("\ud83c\ude2f"), new Emoji("\ud83c\ude50"), new Emoji("\ud83c\ude39"), new Emoji("\ud83c\ude1a"), new Emoji("\ud83c\ude32"), new Emoji("\ud83c\ude51"), new Emoji("\ud83c\ude38"), new Emoji("\ud83c\ude34"), new Emoji("\ud83c\ude33"), new Emoji("\u3297\ufe0f"), new Emoji("\u3299\ufe0f"), new Emoji("\ud83c\ude3a"), new Emoji("\ud83c\ude35"), new Emoji("\u25aa\ufe0f"), new Emoji("\u25ab\ufe0f"), new Emoji("\u25fb\ufe0f"), new Emoji("\u25fc\ufe0f"), new Emoji("\u25fd"), new Emoji("\u25fe"), new Emoji("\u2b1b"), new Emoji("\u2b1c"), new Emoji("\ud83d\udd36"), new Emoji("\ud83d\udd37"), new Emoji("\ud83d\udd38"), new Emoji("\ud83d\udd39"), new Emoji("\ud83d\udd3a"), new Emoji("\ud83d\udd3b"), new Emoji("\ud83d\udca0"), new Emoji("\ud83d\udd18"), new Emoji("\ud83d\udd32"), new Emoji("\ud83d\udd33"), new Emoji("\u26aa"), new Emoji("\u26ab"), new Emoji("\ud83d\udd34"), new Emoji("\ud83d\udd35"), - }, "emoji/Symbols.png"); + private static final EmojiPageModel PAGE_SYMBOLS = new StaticEmojiPageModel(EmojiCategory.SYMBOLS, Arrays.asList( + new Emoji("\ud83c\udfe7"), new Emoji("\ud83d\udeae"), new Emoji("\ud83d\udeb0"), new Emoji("\u267f"), new Emoji("\ud83d\udeb9"), new Emoji("\ud83d\udeba"), new Emoji("\ud83d\udebb"), new Emoji("\ud83d\udebc"), new Emoji("\ud83d\udebe"), new Emoji("\ud83d\udec2"), new Emoji("\ud83d\udec3"), new Emoji("\ud83d\udec4"), new Emoji("\ud83d\udec5"), new Emoji("\u26a0\ufe0f"), new Emoji("\ud83d\udeb8"), new Emoji("\u26d4"), new Emoji("\ud83d\udeab"), new Emoji("\ud83d\udeb3"), new Emoji("\ud83d\udead"), new Emoji("\ud83d\udeaf"), new Emoji("\ud83d\udeb1"), new Emoji("\ud83d\udeb7"), new Emoji("\ud83d\udcf5"), new Emoji("\ud83d\udd1e"), new Emoji("\u2622\ufe0f"), new Emoji("\u2623\ufe0f"), new Emoji("\u2b06\ufe0f"), new Emoji("\u2197\ufe0f"), new Emoji("\u27a1\ufe0f"), new Emoji("\u2198\ufe0f"), new Emoji("\u2b07\ufe0f"), new Emoji("\u2199\ufe0f"), new Emoji("\u2b05\ufe0f"), new Emoji("\u2196\ufe0f"), new Emoji("\u2195\ufe0f"), new Emoji("\u2194\ufe0f"), new Emoji("\u21a9\ufe0f"), new Emoji("\u21aa\ufe0f"), new Emoji("\u2934\ufe0f"), new Emoji("\u2935\ufe0f"), new Emoji("\ud83d\udd03"), new Emoji("\ud83d\udd04"), new Emoji("\ud83d\udd19"), new Emoji("\ud83d\udd1a"), new Emoji("\ud83d\udd1b"), new Emoji("\ud83d\udd1c"), new Emoji("\ud83d\udd1d"), new Emoji("\ud83d\uded0"), new Emoji("\u269b\ufe0f"), new Emoji("\ud83d\udd49\ufe0f"), new Emoji("\u2721\ufe0f"), new Emoji("\u2638\ufe0f"), new Emoji("\u262f\ufe0f"), new Emoji("\u271d\ufe0f"), new Emoji("\u2626\ufe0f"), new Emoji("\u262a\ufe0f"), new Emoji("\u262e\ufe0f"), new Emoji("\ud83d\udd4e"), new Emoji("\ud83d\udd2f"), new Emoji("\u2648"), new Emoji("\u2649"), new Emoji("\u264a"), new Emoji("\u264b"), new Emoji("\u264c"), new Emoji("\u264d"), new Emoji("\u264e"), new Emoji("\u264f"), new Emoji("\u2650"), new Emoji("\u2651"), new Emoji("\u2652"), new Emoji("\u2653"), new Emoji("\u26ce"), new Emoji("\ud83d\udd00"), new Emoji("\ud83d\udd01"), new Emoji("\ud83d\udd02"), new Emoji("\u25b6\ufe0f"), new Emoji("\u23e9"), new Emoji("\u23ed\ufe0f"), new Emoji("\u23ef\ufe0f"), new Emoji("\u25c0\ufe0f"), new Emoji("\u23ea"), new Emoji("\u23ee\ufe0f"), new Emoji("\ud83d\udd3c"), new Emoji("\u23eb"), new Emoji("\ud83d\udd3d"), new Emoji("\u23ec"), new Emoji("\u23f8\ufe0f"), new Emoji("\u23f9\ufe0f"), new Emoji("\u23fa\ufe0f"), new Emoji("\u23cf\ufe0f"), new Emoji("\ud83c\udfa6"), new Emoji("\ud83d\udd05"), new Emoji("\ud83d\udd06"), new Emoji("\ud83d\udcf6"), new Emoji("\ud83d\udcf3"), new Emoji("\ud83d\udcf4"), new Emoji("\u267b\ufe0f"), new Emoji("\u269c\ufe0f"), new Emoji("\ud83d\udd31"), new Emoji("\ud83d\udcdb"), new Emoji("\ud83d\udd30"), new Emoji("\u2b55"), new Emoji("\u2705"), new Emoji("\u2611\ufe0f"), new Emoji("\u2714\ufe0f"), new Emoji("\u2716\ufe0f"), new Emoji("\u274c"), new Emoji("\u274e"), new Emoji("\u2795"), new Emoji("\u2796"), new Emoji("\u2797"), new Emoji("\u27b0"), new Emoji("\u27bf"), new Emoji("\u303d\ufe0f"), new Emoji("\u2733\ufe0f"), new Emoji("\u2734\ufe0f"), new Emoji("\u2747\ufe0f"), new Emoji("\u203c\ufe0f"), new Emoji("\u2049\ufe0f"), new Emoji("\u2753"), new Emoji("\u2754"), new Emoji("\u2755"), new Emoji("\u2757"), new Emoji("\u3030\ufe0f"), new Emoji("\u00a9\ufe0f"), new Emoji("\u00ae\ufe0f"), new Emoji("\u2122\ufe0f"), new Emoji("\u0023\ufe0f\u20e3"), new Emoji("\u002a\ufe0f\u20e3"), new Emoji("\u0030\ufe0f\u20e3"), new Emoji("\u0031\ufe0f\u20e3"), new Emoji("\u0032\ufe0f\u20e3"), new Emoji("\u0033\ufe0f\u20e3"), new Emoji("\u0034\ufe0f\u20e3"), new Emoji("\u0035\ufe0f\u20e3"), new Emoji("\u0036\ufe0f\u20e3"), new Emoji("\u0037\ufe0f\u20e3"), new Emoji("\u0038\ufe0f\u20e3"), new Emoji("\u0039\ufe0f\u20e3"), new Emoji("\ud83d\udd1f"), new Emoji("\ud83d\udcaf"), new Emoji("\ud83d\udd20"), new Emoji("\ud83d\udd21"), new Emoji("\ud83d\udd22"), new Emoji("\ud83d\udd23"), new Emoji("\ud83d\udd24"), new Emoji("\ud83c\udd70\ufe0f"), new Emoji("\ud83c\udd8e"), new Emoji("\ud83c\udd71\ufe0f"), new Emoji("\ud83c\udd91"), new Emoji("\ud83c\udd92"), new Emoji("\ud83c\udd93"), new Emoji("\u2139\ufe0f"), new Emoji("\ud83c\udd94"), new Emoji("\u24c2\ufe0f"), new Emoji("\ud83c\udd95"), new Emoji("\ud83c\udd96"), new Emoji("\ud83c\udd7e\ufe0f"), new Emoji("\ud83c\udd97"), new Emoji("\ud83c\udd7f\ufe0f"), new Emoji("\ud83c\udd98"), new Emoji("\ud83c\udd99"), new Emoji("\ud83c\udd9a"), new Emoji("\ud83c\ude01"), new Emoji("\ud83c\ude02\ufe0f"), new Emoji("\ud83c\ude37\ufe0f"), new Emoji("\ud83c\ude36"), new Emoji("\ud83c\ude2f"), new Emoji("\ud83c\ude50"), new Emoji("\ud83c\ude39"), new Emoji("\ud83c\ude1a"), new Emoji("\ud83c\ude32"), new Emoji("\ud83c\ude51"), new Emoji("\ud83c\ude38"), new Emoji("\ud83c\ude34"), new Emoji("\ud83c\ude33"), new Emoji("\u3297\ufe0f"), new Emoji("\u3299\ufe0f"), new Emoji("\ud83c\ude3a"), new Emoji("\ud83c\ude35"), new Emoji("\u25aa\ufe0f"), new Emoji("\u25ab\ufe0f"), new Emoji("\u25fb\ufe0f"), new Emoji("\u25fc\ufe0f"), new Emoji("\u25fd"), new Emoji("\u25fe"), new Emoji("\u2b1b"), new Emoji("\u2b1c"), new Emoji("\ud83d\udd36"), new Emoji("\ud83d\udd37"), new Emoji("\ud83d\udd38"), new Emoji("\ud83d\udd39"), new Emoji("\ud83d\udd3a"), new Emoji("\ud83d\udd3b"), new Emoji("\ud83d\udca0"), new Emoji("\ud83d\udd18"), new Emoji("\ud83d\udd32"), new Emoji("\ud83d\udd33"), new Emoji("\u26aa"), new Emoji("\u26ab"), new Emoji("\ud83d\udd34"), new Emoji("\ud83d\udd35") + ), Uri.parse("emoji/Symbols.png")); - private static final EmojiPageModel PAGE_FLAGS = new StaticEmojiPageModel(R.attr.emoji_category_flags, new Emoji[] { - new Emoji("\ud83c\udfc1"), new Emoji("\ud83d\udea9"), new Emoji("\ud83c\udf8c"), new Emoji("\ud83c\udff4"), new Emoji("\ud83c\udff3\ufe0f"), new Emoji("\ud83c\udff3\ufe0f\u200d\ud83c\udf08"), new Emoji("\ud83c\udde6\ud83c\udde8"), new Emoji("\ud83c\udde6\ud83c\udde9"), new Emoji("\ud83c\udde6\ud83c\uddea"), new Emoji("\ud83c\udde6\ud83c\uddeb"), new Emoji("\ud83c\udde6\ud83c\uddec"), new Emoji("\ud83c\udde6\ud83c\uddee"), new Emoji("\ud83c\udde6\ud83c\uddf1"), new Emoji("\ud83c\udde6\ud83c\uddf2"), new Emoji("\ud83c\udde6\ud83c\uddf4"), new Emoji("\ud83c\udde6\ud83c\uddf6"), new Emoji("\ud83c\udde6\ud83c\uddf7"), new Emoji("\ud83c\udde6\ud83c\uddf8"), new Emoji("\ud83c\udde6\ud83c\uddf9"), new Emoji("\ud83c\udde6\ud83c\uddfa"), new Emoji("\ud83c\udde6\ud83c\uddfc"), new Emoji("\ud83c\udde6\ud83c\uddfd"), new Emoji("\ud83c\udde6\ud83c\uddff"), new Emoji("\ud83c\udde7\ud83c\udde6"), new Emoji("\ud83c\udde7\ud83c\udde7"), new Emoji("\ud83c\udde7\ud83c\udde9"), new Emoji("\ud83c\udde7\ud83c\uddea"), new Emoji("\ud83c\udde7\ud83c\uddeb"), new Emoji("\ud83c\udde7\ud83c\uddec"), new Emoji("\ud83c\udde7\ud83c\udded"), new Emoji("\ud83c\udde7\ud83c\uddee"), new Emoji("\ud83c\udde7\ud83c\uddef"), new Emoji("\ud83c\udde7\ud83c\uddf1"), new Emoji("\ud83c\udde7\ud83c\uddf2"), new Emoji("\ud83c\udde7\ud83c\uddf3"), new Emoji("\ud83c\udde7\ud83c\uddf4"), new Emoji("\ud83c\udde7\ud83c\uddf6"), new Emoji("\ud83c\udde7\ud83c\uddf7"), new Emoji("\ud83c\udde7\ud83c\uddf8"), new Emoji("\ud83c\udde7\ud83c\uddf9"), new Emoji("\ud83c\udde7\ud83c\uddfb"), new Emoji("\ud83c\udde7\ud83c\uddfc"), new Emoji("\ud83c\udde7\ud83c\uddfe"), new Emoji("\ud83c\udde7\ud83c\uddff"), new Emoji("\ud83c\udde8\ud83c\udde6"), new Emoji("\ud83c\udde8\ud83c\udde8"), new Emoji("\ud83c\udde8\ud83c\udde9"), new Emoji("\ud83c\udde8\ud83c\uddeb"), new Emoji("\ud83c\udde8\ud83c\uddec"), new Emoji("\ud83c\udde8\ud83c\udded"), new Emoji("\ud83c\udde8\ud83c\uddee"), new Emoji("\ud83c\udde8\ud83c\uddf0"), new Emoji("\ud83c\udde8\ud83c\uddf1"), new Emoji("\ud83c\udde8\ud83c\uddf2"), new Emoji("\ud83c\udde8\ud83c\uddf3"), new Emoji("\ud83c\udde8\ud83c\uddf4"), new Emoji("\ud83c\udde8\ud83c\uddf5"), new Emoji("\ud83c\udde8\ud83c\uddf7"), new Emoji("\ud83c\udde8\ud83c\uddfa"), new Emoji("\ud83c\udde8\ud83c\uddfb"), new Emoji("\ud83c\udde8\ud83c\uddfc"), new Emoji("\ud83c\udde8\ud83c\uddfd"), new Emoji("\ud83c\udde8\ud83c\uddfe"), new Emoji("\ud83c\udde8\ud83c\uddff"), new Emoji("\ud83c\udde9\ud83c\uddea"), new Emoji("\ud83c\udde9\ud83c\uddec"), new Emoji("\ud83c\udde9\ud83c\uddef"), new Emoji("\ud83c\udde9\ud83c\uddf0"), new Emoji("\ud83c\udde9\ud83c\uddf2"), new Emoji("\ud83c\udde9\ud83c\uddf4"), new Emoji("\ud83c\udde9\ud83c\uddff"), new Emoji("\ud83c\uddea\ud83c\udde6"), new Emoji("\ud83c\uddea\ud83c\udde8"), new Emoji("\ud83c\uddea\ud83c\uddea"), new Emoji("\ud83c\uddea\ud83c\uddec"), new Emoji("\ud83c\uddea\ud83c\udded"), new Emoji("\ud83c\uddea\ud83c\uddf7"), new Emoji("\ud83c\uddea\ud83c\uddf8"), new Emoji("\ud83c\uddea\ud83c\uddf9"), new Emoji("\ud83c\uddea\ud83c\uddfa"), new Emoji("\ud83c\uddeb\ud83c\uddee"), new Emoji("\ud83c\uddeb\ud83c\uddef"), new Emoji("\ud83c\uddeb\ud83c\uddf0"), new Emoji("\ud83c\uddeb\ud83c\uddf2"), new Emoji("\ud83c\uddeb\ud83c\uddf4"), new Emoji("\ud83c\uddeb\ud83c\uddf7"), new Emoji("\ud83c\uddec\ud83c\udde6"), new Emoji("\ud83c\uddec\ud83c\udde7"), new Emoji("\ud83c\uddec\ud83c\udde9"), new Emoji("\ud83c\uddec\ud83c\uddea"), new Emoji("\ud83c\uddec\ud83c\uddeb"), new Emoji("\ud83c\uddec\ud83c\uddec"), new Emoji("\ud83c\uddec\ud83c\udded"), new Emoji("\ud83c\uddec\ud83c\uddee"), new Emoji("\ud83c\uddec\ud83c\uddf1"), new Emoji("\ud83c\uddec\ud83c\uddf2"), new Emoji("\ud83c\uddec\ud83c\uddf3"), new Emoji("\ud83c\uddec\ud83c\uddf5"), new Emoji("\ud83c\uddec\ud83c\uddf6"), new Emoji("\ud83c\uddec\ud83c\uddf7"), new Emoji("\ud83c\uddec\ud83c\uddf8"), new Emoji("\ud83c\uddec\ud83c\uddf9"), new Emoji("\ud83c\uddec\ud83c\uddfa"), new Emoji("\ud83c\uddec\ud83c\uddfc"), new Emoji("\ud83c\uddec\ud83c\uddfe"), new Emoji("\ud83c\udded\ud83c\uddf0"), new Emoji("\ud83c\udded\ud83c\uddf2"), new Emoji("\ud83c\udded\ud83c\uddf3"), new Emoji("\ud83c\udded\ud83c\uddf7"), new Emoji("\ud83c\udded\ud83c\uddf9"), new Emoji("\ud83c\udded\ud83c\uddfa"), new Emoji("\ud83c\uddee\ud83c\udde8"), new Emoji("\ud83c\uddee\ud83c\udde9"), new Emoji("\ud83c\uddee\ud83c\uddea"), new Emoji("\ud83c\uddee\ud83c\uddf1"), new Emoji("\ud83c\uddee\ud83c\uddf2"), new Emoji("\ud83c\uddee\ud83c\uddf3"), new Emoji("\ud83c\uddee\ud83c\uddf4"), new Emoji("\ud83c\uddee\ud83c\uddf6"), new Emoji("\ud83c\uddee\ud83c\uddf7"), new Emoji("\ud83c\uddee\ud83c\uddf8"), new Emoji("\ud83c\uddee\ud83c\uddf9"), new Emoji("\ud83c\uddef\ud83c\uddea"), new Emoji("\ud83c\uddef\ud83c\uddf2"), new Emoji("\ud83c\uddef\ud83c\uddf4"), new Emoji("\ud83c\uddef\ud83c\uddf5"), new Emoji("\ud83c\uddf0\ud83c\uddea"), new Emoji("\ud83c\uddf0\ud83c\uddec"), new Emoji("\ud83c\uddf0\ud83c\udded"), new Emoji("\ud83c\uddf0\ud83c\uddee"), new Emoji("\ud83c\uddf0\ud83c\uddf2"), new Emoji("\ud83c\uddf0\ud83c\uddf3"), new Emoji("\ud83c\uddf0\ud83c\uddf5"), new Emoji("\ud83c\uddf0\ud83c\uddf7"), new Emoji("\ud83c\uddf0\ud83c\uddfc"), new Emoji("\ud83c\uddf0\ud83c\uddfe"), new Emoji("\ud83c\uddf0\ud83c\uddff"), new Emoji("\ud83c\uddf1\ud83c\udde6"), new Emoji("\ud83c\uddf1\ud83c\udde7"), new Emoji("\ud83c\uddf1\ud83c\udde8"), new Emoji("\ud83c\uddf1\ud83c\uddee"), new Emoji("\ud83c\uddf1\ud83c\uddf0"), new Emoji("\ud83c\uddf1\ud83c\uddf7"), new Emoji("\ud83c\uddf1\ud83c\uddf8"), new Emoji("\ud83c\uddf1\ud83c\uddf9"), new Emoji("\ud83c\uddf1\ud83c\uddfa"), new Emoji("\ud83c\uddf1\ud83c\uddfb"), new Emoji("\ud83c\uddf1\ud83c\uddfe"), new Emoji("\ud83c\uddf2\ud83c\udde6"), new Emoji("\ud83c\uddf2\ud83c\udde8"), new Emoji("\ud83c\uddf2\ud83c\udde9"), new Emoji("\ud83c\uddf2\ud83c\uddea"), new Emoji("\ud83c\uddf2\ud83c\uddeb"), new Emoji("\ud83c\uddf2\ud83c\uddec"), new Emoji("\ud83c\uddf2\ud83c\udded"), new Emoji("\ud83c\uddf2\ud83c\uddf0"), new Emoji("\ud83c\uddf2\ud83c\uddf1"), new Emoji("\ud83c\uddf2\ud83c\uddf2"), new Emoji("\ud83c\uddf2\ud83c\uddf3"), new Emoji("\ud83c\uddf2\ud83c\uddf4"), new Emoji("\ud83c\uddf2\ud83c\uddf5"), new Emoji("\ud83c\uddf2\ud83c\uddf6"), new Emoji("\ud83c\uddf2\ud83c\uddf7"), new Emoji("\ud83c\uddf2\ud83c\uddf8"), new Emoji("\ud83c\uddf2\ud83c\uddf9"), new Emoji("\ud83c\uddf2\ud83c\uddfa"), new Emoji("\ud83c\uddf2\ud83c\uddfb"), new Emoji("\ud83c\uddf2\ud83c\uddfc"), new Emoji("\ud83c\uddf2\ud83c\uddfd"), new Emoji("\ud83c\uddf2\ud83c\uddfe"), new Emoji("\ud83c\uddf2\ud83c\uddff"), new Emoji("\ud83c\uddf3\ud83c\udde6"), new Emoji("\ud83c\uddf3\ud83c\udde8"), new Emoji("\ud83c\uddf3\ud83c\uddea"), new Emoji("\ud83c\uddf3\ud83c\uddeb"), new Emoji("\ud83c\uddf3\ud83c\uddec"), new Emoji("\ud83c\uddf3\ud83c\uddee"), new Emoji("\ud83c\uddf3\ud83c\uddf1"), new Emoji("\ud83c\uddf3\ud83c\uddf4"), new Emoji("\ud83c\uddf3\ud83c\uddf5"), new Emoji("\ud83c\uddf3\ud83c\uddf7"), new Emoji("\ud83c\uddf3\ud83c\uddfa"), new Emoji("\ud83c\uddf3\ud83c\uddff"), new Emoji("\ud83c\uddf4\ud83c\uddf2"), new Emoji("\ud83c\uddf5\ud83c\udde6"), new Emoji("\ud83c\uddf5\ud83c\uddea"), new Emoji("\ud83c\uddf5\ud83c\uddeb"), new Emoji("\ud83c\uddf5\ud83c\uddec"), new Emoji("\ud83c\uddf5\ud83c\udded"), new Emoji("\ud83c\uddf5\ud83c\uddf0"), new Emoji("\ud83c\uddf5\ud83c\uddf1"), new Emoji("\ud83c\uddf5\ud83c\uddf2"), new Emoji("\ud83c\uddf5\ud83c\uddf3"), new Emoji("\ud83c\uddf5\ud83c\uddf7"), new Emoji("\ud83c\uddf5\ud83c\uddf8"), new Emoji("\ud83c\uddf5\ud83c\uddf9"), new Emoji("\ud83c\uddf5\ud83c\uddfc"), new Emoji("\ud83c\uddf5\ud83c\uddfe"), new Emoji("\ud83c\uddf6\ud83c\udde6"), new Emoji("\ud83c\uddf7\ud83c\uddea"), new Emoji("\ud83c\uddf7\ud83c\uddf4"), new Emoji("\ud83c\uddf7\ud83c\uddf8"), new Emoji("\ud83c\uddf7\ud83c\uddfa"), new Emoji("\ud83c\uddf7\ud83c\uddfc"), new Emoji("\ud83c\uddf8\ud83c\udde6"), new Emoji("\ud83c\uddf8\ud83c\udde7"), new Emoji("\ud83c\uddf8\ud83c\udde8"), new Emoji("\ud83c\uddf8\ud83c\udde9"), new Emoji("\ud83c\uddf8\ud83c\uddea"), new Emoji("\ud83c\uddf8\ud83c\uddec"), new Emoji("\ud83c\uddf8\ud83c\udded"), new Emoji("\ud83c\uddf8\ud83c\uddee"), new Emoji("\ud83c\uddf8\ud83c\uddef"), new Emoji("\ud83c\uddf8\ud83c\uddf0"), new Emoji("\ud83c\uddf8\ud83c\uddf1"), new Emoji("\ud83c\uddf8\ud83c\uddf2"), new Emoji("\ud83c\uddf8\ud83c\uddf3"), new Emoji("\ud83c\uddf8\ud83c\uddf4"), new Emoji("\ud83c\uddf8\ud83c\uddf7"), new Emoji("\ud83c\uddf8\ud83c\uddf8"), new Emoji("\ud83c\uddf8\ud83c\uddf9"), new Emoji("\ud83c\uddf8\ud83c\uddfb"), new Emoji("\ud83c\uddf8\ud83c\uddfd"), new Emoji("\ud83c\uddf8\ud83c\uddfe"), new Emoji("\ud83c\uddf8\ud83c\uddff"), new Emoji("\ud83c\uddf9\ud83c\udde6"), new Emoji("\ud83c\uddf9\ud83c\udde8"), new Emoji("\ud83c\uddf9\ud83c\udde9"), new Emoji("\ud83c\uddf9\ud83c\uddeb"), new Emoji("\ud83c\uddf9\ud83c\uddec"), new Emoji("\ud83c\uddf9\ud83c\udded"), new Emoji("\ud83c\uddf9\ud83c\uddef"), new Emoji("\ud83c\uddf9\ud83c\uddf0"), new Emoji("\ud83c\uddf9\ud83c\uddf1"), new Emoji("\ud83c\uddf9\ud83c\uddf2"), new Emoji("\ud83c\uddf9\ud83c\uddf3"), new Emoji("\ud83c\uddf9\ud83c\uddf4"), new Emoji("\ud83c\uddf9\ud83c\uddf7"), new Emoji("\ud83c\uddf9\ud83c\uddf9"), new Emoji("\ud83c\uddf9\ud83c\uddfb"), new Emoji("\ud83c\uddf9\ud83c\uddfc"), new Emoji("\ud83c\uddf9\ud83c\uddff"), new Emoji("\ud83c\uddfa\ud83c\udde6"), new Emoji("\ud83c\uddfa\ud83c\uddec"), new Emoji("\ud83c\uddfa\ud83c\uddf2"), new Emoji("\ud83c\uddfa\ud83c\uddf8"), new Emoji("\ud83c\uddfa\ud83c\uddfe"), new Emoji("\ud83c\uddfa\ud83c\uddff"), new Emoji("\ud83c\uddfb\ud83c\udde6"), new Emoji("\ud83c\uddfb\ud83c\udde8"), new Emoji("\ud83c\uddfb\ud83c\uddea"), new Emoji("\ud83c\uddfb\ud83c\uddec"), new Emoji("\ud83c\uddfb\ud83c\uddee"), new Emoji("\ud83c\uddfb\ud83c\uddf3"), new Emoji("\ud83c\uddfb\ud83c\uddfa"), new Emoji("\ud83c\uddfc\ud83c\uddeb"), new Emoji("\ud83c\uddfc\ud83c\uddf8"), new Emoji("\ud83c\uddfd\ud83c\uddf0"), new Emoji("\ud83c\uddfe\ud83c\uddea"), new Emoji("\ud83c\uddfe\ud83c\uddf9"), new Emoji("\ud83c\uddff\ud83c\udde6"), new Emoji("\ud83c\uddff\ud83c\uddf2"), new Emoji("\ud83c\uddff\ud83c\uddfc"), new Emoji("\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f"), new Emoji("\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f"), new Emoji("\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f"), - }, "emoji/Flags.png"); + private static final EmojiPageModel PAGE_FLAGS = new StaticEmojiPageModel(EmojiCategory.FLAGS, Arrays.asList( + new Emoji("\ud83c\udfc1"), new Emoji("\ud83d\udea9"), new Emoji("\ud83c\udf8c"), new Emoji("\ud83c\udff4"), new Emoji("\ud83c\udff3\ufe0f"), new Emoji("\ud83c\udff3\ufe0f\u200d\ud83c\udf08"), new Emoji("\ud83c\udde6\ud83c\udde8"), new Emoji("\ud83c\udde6\ud83c\udde9"), new Emoji("\ud83c\udde6\ud83c\uddea"), new Emoji("\ud83c\udde6\ud83c\uddeb"), new Emoji("\ud83c\udde6\ud83c\uddec"), new Emoji("\ud83c\udde6\ud83c\uddee"), new Emoji("\ud83c\udde6\ud83c\uddf1"), new Emoji("\ud83c\udde6\ud83c\uddf2"), new Emoji("\ud83c\udde6\ud83c\uddf4"), new Emoji("\ud83c\udde6\ud83c\uddf6"), new Emoji("\ud83c\udde6\ud83c\uddf7"), new Emoji("\ud83c\udde6\ud83c\uddf8"), new Emoji("\ud83c\udde6\ud83c\uddf9"), new Emoji("\ud83c\udde6\ud83c\uddfa"), new Emoji("\ud83c\udde6\ud83c\uddfc"), new Emoji("\ud83c\udde6\ud83c\uddfd"), new Emoji("\ud83c\udde6\ud83c\uddff"), new Emoji("\ud83c\udde7\ud83c\udde6"), new Emoji("\ud83c\udde7\ud83c\udde7"), new Emoji("\ud83c\udde7\ud83c\udde9"), new Emoji("\ud83c\udde7\ud83c\uddea"), new Emoji("\ud83c\udde7\ud83c\uddeb"), new Emoji("\ud83c\udde7\ud83c\uddec"), new Emoji("\ud83c\udde7\ud83c\udded"), new Emoji("\ud83c\udde7\ud83c\uddee"), new Emoji("\ud83c\udde7\ud83c\uddef"), new Emoji("\ud83c\udde7\ud83c\uddf1"), new Emoji("\ud83c\udde7\ud83c\uddf2"), new Emoji("\ud83c\udde7\ud83c\uddf3"), new Emoji("\ud83c\udde7\ud83c\uddf4"), new Emoji("\ud83c\udde7\ud83c\uddf6"), new Emoji("\ud83c\udde7\ud83c\uddf7"), new Emoji("\ud83c\udde7\ud83c\uddf8"), new Emoji("\ud83c\udde7\ud83c\uddf9"), new Emoji("\ud83c\udde7\ud83c\uddfb"), new Emoji("\ud83c\udde7\ud83c\uddfc"), new Emoji("\ud83c\udde7\ud83c\uddfe"), new Emoji("\ud83c\udde7\ud83c\uddff"), new Emoji("\ud83c\udde8\ud83c\udde6"), new Emoji("\ud83c\udde8\ud83c\udde8"), new Emoji("\ud83c\udde8\ud83c\udde9"), new Emoji("\ud83c\udde8\ud83c\uddeb"), new Emoji("\ud83c\udde8\ud83c\uddec"), new Emoji("\ud83c\udde8\ud83c\udded"), new Emoji("\ud83c\udde8\ud83c\uddee"), new Emoji("\ud83c\udde8\ud83c\uddf0"), new Emoji("\ud83c\udde8\ud83c\uddf1"), new Emoji("\ud83c\udde8\ud83c\uddf2"), new Emoji("\ud83c\udde8\ud83c\uddf3"), new Emoji("\ud83c\udde8\ud83c\uddf4"), new Emoji("\ud83c\udde8\ud83c\uddf5"), new Emoji("\ud83c\udde8\ud83c\uddf7"), new Emoji("\ud83c\udde8\ud83c\uddfa"), new Emoji("\ud83c\udde8\ud83c\uddfb"), new Emoji("\ud83c\udde8\ud83c\uddfc"), new Emoji("\ud83c\udde8\ud83c\uddfd"), new Emoji("\ud83c\udde8\ud83c\uddfe"), new Emoji("\ud83c\udde8\ud83c\uddff"), new Emoji("\ud83c\udde9\ud83c\uddea"), new Emoji("\ud83c\udde9\ud83c\uddec"), new Emoji("\ud83c\udde9\ud83c\uddef"), new Emoji("\ud83c\udde9\ud83c\uddf0"), new Emoji("\ud83c\udde9\ud83c\uddf2"), new Emoji("\ud83c\udde9\ud83c\uddf4"), new Emoji("\ud83c\udde9\ud83c\uddff"), new Emoji("\ud83c\uddea\ud83c\udde6"), new Emoji("\ud83c\uddea\ud83c\udde8"), new Emoji("\ud83c\uddea\ud83c\uddea"), new Emoji("\ud83c\uddea\ud83c\uddec"), new Emoji("\ud83c\uddea\ud83c\udded"), new Emoji("\ud83c\uddea\ud83c\uddf7"), new Emoji("\ud83c\uddea\ud83c\uddf8"), new Emoji("\ud83c\uddea\ud83c\uddf9"), new Emoji("\ud83c\uddea\ud83c\uddfa"), new Emoji("\ud83c\uddeb\ud83c\uddee"), new Emoji("\ud83c\uddeb\ud83c\uddef"), new Emoji("\ud83c\uddeb\ud83c\uddf0"), new Emoji("\ud83c\uddeb\ud83c\uddf2"), new Emoji("\ud83c\uddeb\ud83c\uddf4"), new Emoji("\ud83c\uddeb\ud83c\uddf7"), new Emoji("\ud83c\uddec\ud83c\udde6"), new Emoji("\ud83c\uddec\ud83c\udde7"), new Emoji("\ud83c\uddec\ud83c\udde9"), new Emoji("\ud83c\uddec\ud83c\uddea"), new Emoji("\ud83c\uddec\ud83c\uddeb"), new Emoji("\ud83c\uddec\ud83c\uddec"), new Emoji("\ud83c\uddec\ud83c\udded"), new Emoji("\ud83c\uddec\ud83c\uddee"), new Emoji("\ud83c\uddec\ud83c\uddf1"), new Emoji("\ud83c\uddec\ud83c\uddf2"), new Emoji("\ud83c\uddec\ud83c\uddf3"), new Emoji("\ud83c\uddec\ud83c\uddf5"), new Emoji("\ud83c\uddec\ud83c\uddf6"), new Emoji("\ud83c\uddec\ud83c\uddf7"), new Emoji("\ud83c\uddec\ud83c\uddf8"), new Emoji("\ud83c\uddec\ud83c\uddf9"), new Emoji("\ud83c\uddec\ud83c\uddfa"), new Emoji("\ud83c\uddec\ud83c\uddfc"), new Emoji("\ud83c\uddec\ud83c\uddfe"), new Emoji("\ud83c\udded\ud83c\uddf0"), new Emoji("\ud83c\udded\ud83c\uddf2"), new Emoji("\ud83c\udded\ud83c\uddf3"), new Emoji("\ud83c\udded\ud83c\uddf7"), new Emoji("\ud83c\udded\ud83c\uddf9"), new Emoji("\ud83c\udded\ud83c\uddfa"), new Emoji("\ud83c\uddee\ud83c\udde8"), new Emoji("\ud83c\uddee\ud83c\udde9"), new Emoji("\ud83c\uddee\ud83c\uddea"), new Emoji("\ud83c\uddee\ud83c\uddf1"), new Emoji("\ud83c\uddee\ud83c\uddf2"), new Emoji("\ud83c\uddee\ud83c\uddf3"), new Emoji("\ud83c\uddee\ud83c\uddf4"), new Emoji("\ud83c\uddee\ud83c\uddf6"), new Emoji("\ud83c\uddee\ud83c\uddf7"), new Emoji("\ud83c\uddee\ud83c\uddf8"), new Emoji("\ud83c\uddee\ud83c\uddf9"), new Emoji("\ud83c\uddef\ud83c\uddea"), new Emoji("\ud83c\uddef\ud83c\uddf2"), new Emoji("\ud83c\uddef\ud83c\uddf4"), new Emoji("\ud83c\uddef\ud83c\uddf5"), new Emoji("\ud83c\uddf0\ud83c\uddea"), new Emoji("\ud83c\uddf0\ud83c\uddec"), new Emoji("\ud83c\uddf0\ud83c\udded"), new Emoji("\ud83c\uddf0\ud83c\uddee"), new Emoji("\ud83c\uddf0\ud83c\uddf2"), new Emoji("\ud83c\uddf0\ud83c\uddf3"), new Emoji("\ud83c\uddf0\ud83c\uddf5"), new Emoji("\ud83c\uddf0\ud83c\uddf7"), new Emoji("\ud83c\uddf0\ud83c\uddfc"), new Emoji("\ud83c\uddf0\ud83c\uddfe"), new Emoji("\ud83c\uddf0\ud83c\uddff"), new Emoji("\ud83c\uddf1\ud83c\udde6"), new Emoji("\ud83c\uddf1\ud83c\udde7"), new Emoji("\ud83c\uddf1\ud83c\udde8"), new Emoji("\ud83c\uddf1\ud83c\uddee"), new Emoji("\ud83c\uddf1\ud83c\uddf0"), new Emoji("\ud83c\uddf1\ud83c\uddf7"), new Emoji("\ud83c\uddf1\ud83c\uddf8"), new Emoji("\ud83c\uddf1\ud83c\uddf9"), new Emoji("\ud83c\uddf1\ud83c\uddfa"), new Emoji("\ud83c\uddf1\ud83c\uddfb"), new Emoji("\ud83c\uddf1\ud83c\uddfe"), new Emoji("\ud83c\uddf2\ud83c\udde6"), new Emoji("\ud83c\uddf2\ud83c\udde8"), new Emoji("\ud83c\uddf2\ud83c\udde9"), new Emoji("\ud83c\uddf2\ud83c\uddea"), new Emoji("\ud83c\uddf2\ud83c\uddeb"), new Emoji("\ud83c\uddf2\ud83c\uddec"), new Emoji("\ud83c\uddf2\ud83c\udded"), new Emoji("\ud83c\uddf2\ud83c\uddf0"), new Emoji("\ud83c\uddf2\ud83c\uddf1"), new Emoji("\ud83c\uddf2\ud83c\uddf2"), new Emoji("\ud83c\uddf2\ud83c\uddf3"), new Emoji("\ud83c\uddf2\ud83c\uddf4"), new Emoji("\ud83c\uddf2\ud83c\uddf5"), new Emoji("\ud83c\uddf2\ud83c\uddf6"), new Emoji("\ud83c\uddf2\ud83c\uddf7"), new Emoji("\ud83c\uddf2\ud83c\uddf8"), new Emoji("\ud83c\uddf2\ud83c\uddf9"), new Emoji("\ud83c\uddf2\ud83c\uddfa"), new Emoji("\ud83c\uddf2\ud83c\uddfb"), new Emoji("\ud83c\uddf2\ud83c\uddfc"), new Emoji("\ud83c\uddf2\ud83c\uddfd"), new Emoji("\ud83c\uddf2\ud83c\uddfe"), new Emoji("\ud83c\uddf2\ud83c\uddff"), new Emoji("\ud83c\uddf3\ud83c\udde6"), new Emoji("\ud83c\uddf3\ud83c\udde8"), new Emoji("\ud83c\uddf3\ud83c\uddea"), new Emoji("\ud83c\uddf3\ud83c\uddeb"), new Emoji("\ud83c\uddf3\ud83c\uddec"), new Emoji("\ud83c\uddf3\ud83c\uddee"), new Emoji("\ud83c\uddf3\ud83c\uddf1"), new Emoji("\ud83c\uddf3\ud83c\uddf4"), new Emoji("\ud83c\uddf3\ud83c\uddf5"), new Emoji("\ud83c\uddf3\ud83c\uddf7"), new Emoji("\ud83c\uddf3\ud83c\uddfa"), new Emoji("\ud83c\uddf3\ud83c\uddff"), new Emoji("\ud83c\uddf4\ud83c\uddf2"), new Emoji("\ud83c\uddf5\ud83c\udde6"), new Emoji("\ud83c\uddf5\ud83c\uddea"), new Emoji("\ud83c\uddf5\ud83c\uddeb"), new Emoji("\ud83c\uddf5\ud83c\uddec"), new Emoji("\ud83c\uddf5\ud83c\udded"), new Emoji("\ud83c\uddf5\ud83c\uddf0"), new Emoji("\ud83c\uddf5\ud83c\uddf1"), new Emoji("\ud83c\uddf5\ud83c\uddf2"), new Emoji("\ud83c\uddf5\ud83c\uddf3"), new Emoji("\ud83c\uddf5\ud83c\uddf7"), new Emoji("\ud83c\uddf5\ud83c\uddf8"), new Emoji("\ud83c\uddf5\ud83c\uddf9"), new Emoji("\ud83c\uddf5\ud83c\uddfc"), new Emoji("\ud83c\uddf5\ud83c\uddfe"), new Emoji("\ud83c\uddf6\ud83c\udde6"), new Emoji("\ud83c\uddf7\ud83c\uddea"), new Emoji("\ud83c\uddf7\ud83c\uddf4"), new Emoji("\ud83c\uddf7\ud83c\uddf8"), new Emoji("\ud83c\uddf7\ud83c\uddfa"), new Emoji("\ud83c\uddf7\ud83c\uddfc"), new Emoji("\ud83c\uddf8\ud83c\udde6"), new Emoji("\ud83c\uddf8\ud83c\udde7"), new Emoji("\ud83c\uddf8\ud83c\udde8"), new Emoji("\ud83c\uddf8\ud83c\udde9"), new Emoji("\ud83c\uddf8\ud83c\uddea"), new Emoji("\ud83c\uddf8\ud83c\uddec"), new Emoji("\ud83c\uddf8\ud83c\udded"), new Emoji("\ud83c\uddf8\ud83c\uddee"), new Emoji("\ud83c\uddf8\ud83c\uddef"), new Emoji("\ud83c\uddf8\ud83c\uddf0"), new Emoji("\ud83c\uddf8\ud83c\uddf1"), new Emoji("\ud83c\uddf8\ud83c\uddf2"), new Emoji("\ud83c\uddf8\ud83c\uddf3"), new Emoji("\ud83c\uddf8\ud83c\uddf4"), new Emoji("\ud83c\uddf8\ud83c\uddf7"), new Emoji("\ud83c\uddf8\ud83c\uddf8"), new Emoji("\ud83c\uddf8\ud83c\uddf9"), new Emoji("\ud83c\uddf8\ud83c\uddfb"), new Emoji("\ud83c\uddf8\ud83c\uddfd"), new Emoji("\ud83c\uddf8\ud83c\uddfe"), new Emoji("\ud83c\uddf8\ud83c\uddff"), new Emoji("\ud83c\uddf9\ud83c\udde6"), new Emoji("\ud83c\uddf9\ud83c\udde8"), new Emoji("\ud83c\uddf9\ud83c\udde9"), new Emoji("\ud83c\uddf9\ud83c\uddeb"), new Emoji("\ud83c\uddf9\ud83c\uddec"), new Emoji("\ud83c\uddf9\ud83c\udded"), new Emoji("\ud83c\uddf9\ud83c\uddef"), new Emoji("\ud83c\uddf9\ud83c\uddf0"), new Emoji("\ud83c\uddf9\ud83c\uddf1"), new Emoji("\ud83c\uddf9\ud83c\uddf2"), new Emoji("\ud83c\uddf9\ud83c\uddf3"), new Emoji("\ud83c\uddf9\ud83c\uddf4"), new Emoji("\ud83c\uddf9\ud83c\uddf7"), new Emoji("\ud83c\uddf9\ud83c\uddf9"), new Emoji("\ud83c\uddf9\ud83c\uddfb"), new Emoji("\ud83c\uddf9\ud83c\uddfc"), new Emoji("\ud83c\uddf9\ud83c\uddff"), new Emoji("\ud83c\uddfa\ud83c\udde6"), new Emoji("\ud83c\uddfa\ud83c\uddec"), new Emoji("\ud83c\uddfa\ud83c\uddf2"), new Emoji("\ud83c\uddfa\ud83c\uddf8"), new Emoji("\ud83c\uddfa\ud83c\uddfe"), new Emoji("\ud83c\uddfa\ud83c\uddff"), new Emoji("\ud83c\uddfb\ud83c\udde6"), new Emoji("\ud83c\uddfb\ud83c\udde8"), new Emoji("\ud83c\uddfb\ud83c\uddea"), new Emoji("\ud83c\uddfb\ud83c\uddec"), new Emoji("\ud83c\uddfb\ud83c\uddee"), new Emoji("\ud83c\uddfb\ud83c\uddf3"), new Emoji("\ud83c\uddfb\ud83c\uddfa"), new Emoji("\ud83c\uddfc\ud83c\uddeb"), new Emoji("\ud83c\uddfc\ud83c\uddf8"), new Emoji("\ud83c\uddfd\ud83c\uddf0"), new Emoji("\ud83c\uddfe\ud83c\uddea"), new Emoji("\ud83c\uddfe\ud83c\uddf9"), new Emoji("\ud83c\uddff\ud83c\udde6"), new Emoji("\ud83c\uddff\ud83c\uddf2"), new Emoji("\ud83c\uddff\ud83c\uddfc"), new Emoji("\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f"), new Emoji("\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc73\udb40\udc63\udb40\udc74\udb40\udc7f"), new Emoji("\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc77\udb40\udc6c\udb40\udc73\udb40\udc7f") + ), Uri.parse("emoji/Flags.png")); - private static final EmojiPageModel PAGE_EMOTICONS = new StaticEmojiPageModel(R.attr.emoji_category_emoticons, new String[] { + private static final EmojiPageModel PAGE_EMOTICONS = new StaticEmojiPageModel(EmojiCategory.EMOTICONS, new String[] { ":-)", ";-)", "(-:", ":->", ":-D", "\\o/", ":-P", "B-)", ":-$", ":-*", "O:-)", "=-O", "O_O", "O_o", "o_O", ":O", ":-!", ":-x", diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java index 47f73d45a0..e51abe1ca3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiProvider.java +++ b/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 { +public 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); + 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 final EmojiTree emojiTree = new EmojiTree(); - - 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); - - List emojis = page.getEmoji(); - for (int i = 0; i < emojis.size(); i++) { - emojiTree.add(emojis.get(i), new EmojiDrawInfo(pageBitmap, i)); - } - } - } - - for (Pair obsolete : EmojiPages.OBSOLETE) { - emojiTree.add(obsolete.first(), emojiTree.getEmoji(obsolete.second(), 0, obsolete.second().length())); - } - } - - @Nullable EmojiParser.CandidateList getCandidates(@Nullable CharSequence text) { + public static @Nullable EmojiParser.CandidateList getCandidates(@Nullable CharSequence text) { if (text == null) return null; - return new EmojiParser(emojiTree).findCandidates(text); + return new EmojiParser(EmojiSource.getLatest().getEmojiTree()).findCandidates(text); } - @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv) { - return emojify(getCandidates(text), text, tv); + static @Nullable Spannable emojify(@Nullable CharSequence text, @NonNull TextView tv, boolean jumboEmoji) { + if (tv.isInEditMode()) { + return null; + } else { + return emojify(getCandidates(text), text, tv, jumboEmoji); + } } - @Nullable Spannable emojify(@Nullable EmojiParser.CandidateList matches, - @Nullable CharSequence text, - @NonNull TextView tv) { - if (matches == null || text == null) return null; - SpannableStringBuilder builder = new SpannableStringBuilder(text); + static @Nullable Spannable emojify(@Nullable EmojiParser.CandidateList matches, + @Nullable CharSequence text, + @NonNull TextView tv, + boolean jumboEmoji) + { + if (matches == null || text == null || tv.isInEditMode()) return null; + SpannableStringBuilder builder = new SpannableStringBuilder(text); for (EmojiParser.Candidate candidate : matches) { - Drawable drawable = getEmojiDrawable(candidate.getDrawInfo()); + Drawable drawable = getEmojiDrawable(tv.getContext(), candidate.getDrawInfo(), tv::requestLayout, jumboEmoji); if (drawable != null) { builder.setSpan(new EmojiSpan(drawable, tv), candidate.getStartIndex(), candidate.getEndIndex(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); } } return builder; } - @Nullable Drawable getEmojiDrawable(CharSequence emoji) { - EmojiDrawInfo drawInfo = emojiTree.getEmoji(emoji, 0, emoji.length()); - return getEmojiDrawable(drawInfo); + static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji) { + return getEmojiDrawable(context, emoji, false); } - private @Nullable Drawable getEmojiDrawable(@Nullable EmojiDrawInfo drawInfo) { - if (drawInfo == null) { + static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable CharSequence emoji, boolean jumboEmoji) { + if (TextUtils.isEmpty(emoji)) { return null; } - final EmojiDrawable drawable = new EmojiDrawable(drawInfo, decodeScale); - drawInfo.getPage().get().addListener(new FutureTaskListener() { - @Override public void onSuccess(final Bitmap result) { - Util.runOnMain(() -> drawable.setBitmap(result)); - } + EmojiDrawInfo drawInfo = EmojiSource.getLatest().getEmojiTree().getEmoji(emoji, 0, emoji.length()); + return getEmojiDrawable(context, drawInfo, null, jumboEmoji); + } + + /** + * Gets an EmojiDrawable from the Page Cache + * + * @param context Context object used in reading and writing from disk + * @param drawInfo Information about the emoji being displayed + * @param onEmojiLoaded Runnable which will trigger when an emoji is loaded from disk + */ + private static @Nullable Drawable getEmojiDrawable(@NonNull Context context, @Nullable EmojiDrawInfo drawInfo, @Nullable Runnable onEmojiLoaded, boolean jumboEmoji) { + if (drawInfo == null) { + return null; + } + + final int lowMemoryDecodeScale = Util.isLowMemory(context) ? 2 : 1; + final EmojiSource source = EmojiSource.getLatest(); + final EmojiDrawable drawable = new EmojiDrawable(source, drawInfo, lowMemoryDecodeScale); + final AtomicBoolean jumboLoaded = new AtomicBoolean(false); + + EmojiPageCache.LoadResult loadResult = EmojiPageCache.INSTANCE.load(context, drawInfo.getPage(), lowMemoryDecodeScale); + + if (loadResult instanceof EmojiPageCache.LoadResult.Immediate) { + runOnMain(() -> drawable.setBitmap(((EmojiPageCache.LoadResult.Immediate) loadResult).getBitmap())); + } else if (loadResult instanceof EmojiPageCache.LoadResult.Async) { + ((EmojiPageCache.LoadResult.Async) loadResult).getTask().addListener(new FutureTaskListener() { + @Override + public void onSuccess(Bitmap result) { + runOnMain(() -> { + if (!jumboLoaded.get()) { + drawable.setBitmap(result); + if (onEmojiLoaded != null) { + onEmojiLoaded.run(); + } + } + }); + } + + @Override + public void onFailure(ExecutionException exception) { + Log.d(TAG, "Failed to load emoji bitmap resource", exception); + } + }); + } else { + throw new IllegalStateException("Unexpected subclass " + loadResult.getClass()); + } - @Override public void onFailure(ExecutionException error) { - Log.w(TAG, error); - } - }); return drawable; } - class EmojiDrawable extends Drawable { - private final EmojiDrawInfo info; - private Bitmap bmp; - private float intrinsicWidth; - private float intrinsicHeight; + static final class EmojiDrawable extends Drawable { + private final float intrinsicWidth; + private final float intrinsicHeight; + private final Rect emojiBounds; + + private Bitmap bmp; + private boolean isSingleBitmap; @Override public int getIntrinsicWidth() { - return (int)intrinsicWidth; + return (int) intrinsicWidth; } @Override public int getIntrinsicHeight() { - return (int)intrinsicHeight; + return (int) intrinsicHeight; } - EmojiDrawable(EmojiDrawInfo info, float decodeScale) { - this.info = info; - this.intrinsicWidth = EMOJI_RAW_WIDTH * decodeScale; - this.intrinsicHeight = EMOJI_RAW_HEIGHT * decodeScale; + EmojiDrawable(@NonNull EmojiSource source, @NonNull EmojiDrawInfo info, int lowMemoryDecodeScale) { + this.intrinsicWidth = (source.getMetrics().getRawWidth() * source.getDecodeScale()) / lowMemoryDecodeScale; + this.intrinsicHeight = (source.getMetrics().getRawHeight() * source.getDecodeScale()) / lowMemoryDecodeScale; + + final int glyphWidth = (int) (intrinsicWidth); + final int glyphHeight = (int) (intrinsicHeight); + final int index = info.getIndex(); + final int emojiPerRow = source.getMetrics().getPerRow(); + final int xStart = (index % emojiPerRow) * glyphWidth; + final int yStart = (index / emojiPerRow) * glyphHeight; + + this.emojiBounds = new Rect(xStart + 1, + yStart + 1, + xStart + glyphWidth - 1, + yStart + glyphHeight - 1); } @Override @@ -156,22 +168,23 @@ class EmojiProvider { return; } - final int row = info.getIndex() / EMOJI_PER_ROW; - final int row_index = info.getIndex() % EMOJI_PER_ROW; - canvas.drawBitmap(bmp, - new Rect((int)(row_index * intrinsicWidth), - (int)(row * intrinsicHeight + row * verticalPad)+1, - (int)(((row_index + 1) * intrinsicWidth)-1), - (int)((row + 1) * intrinsicHeight + row * verticalPad)-1), - getBounds(), - paint); + isSingleBitmap ? null : emojiBounds, + getBounds(), + PAINT); } - @TargetApi(VERSION_CODES.HONEYCOMB_MR1) public void setBitmap(Bitmap bitmap) { - Util.assertMainThread(); - if (VERSION.SDK_INT < VERSION_CODES.HONEYCOMB_MR1 || bmp == null || !bmp.sameAs(bitmap)) { + setBitmap(bitmap, false); + } + + public void setSingleBitmap(Bitmap bitmap) { + setBitmap(bitmap, true); + } + + private void setBitmap(Bitmap bitmap, boolean isSingleBitmap) { + this.isSingleBitmap = isSingleBitmap; + if (bmp == null || !bmp.sameAs(bitmap)) { bmp = bitmap; invalidateSelf(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java index 90e828cdbd..11714f09ce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiSpan.java @@ -1,41 +1,56 @@ package org.thoughtcrime.securesms.components.emoji; +import android.content.Context; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Paint.FontMetricsInt; import android.graphics.drawable.Drawable; -import androidx.annotation.NonNull; import android.widget.TextView; +import androidx.annotation.NonNull; + import network.loki.messenger.R; public class EmojiSpan extends AnimatingImageSpan { private final float SHIFT_FACTOR = 1.5f; - private final int size; - private final FontMetricsInt fm; + private int size; + private FontMetricsInt fontMetrics; public EmojiSpan(@NonNull Drawable drawable, @NonNull TextView tv) { super(drawable, tv); - fm = tv.getPaint().getFontMetricsInt(); - size = fm != null ? Math.abs(fm.descent) + Math.abs(fm.ascent) - : tv.getResources().getDimensionPixelSize(R.dimen.conversation_item_body_text_size); + fontMetrics = tv.getPaint().getFontMetricsInt(); + size = fontMetrics != null ? Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent) + : tv.getResources().getDimensionPixelSize(R.dimen.conversation_item_body_text_size); + getDrawable().setBounds(0, 0, size, size); + } + + public EmojiSpan(@NonNull Context context, @NonNull Drawable drawable, @NonNull Paint paint) { + super(drawable, null); + fontMetrics = paint.getFontMetricsInt(); + size = fontMetrics != null ? Math.abs(fontMetrics.descent) + Math.abs(fontMetrics.ascent) + : context.getResources().getDimensionPixelSize(R.dimen.conversation_item_body_text_size); + getDrawable().setBounds(0, 0, size, size); } @Override public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, FontMetricsInt fm) { - if (fm != null && this.fm != null) { - fm.ascent = this.fm.ascent; - fm.descent = this.fm.descent; - fm.top = this.fm.top; - fm.bottom = this.fm.bottom; - fm.leading = this.fm.leading; - return size; + if (fm != null && this.fontMetrics != null) { + fm.ascent = this.fontMetrics.ascent; + fm.descent = this.fontMetrics.descent; + fm.top = this.fontMetrics.top; + fm.bottom = this.fontMetrics.bottom; + fm.leading = this.fontMetrics.leading; } else { - return super.getSize(paint, text, start, end, fm); + this.fontMetrics = paint.getFontMetricsInt(); + this.size = Math.abs(this.fontMetrics.descent) + Math.abs(this.fontMetrics.ascent); + + getDrawable().setBounds(0, 0, size, size); } + + return size; } @Override @@ -43,6 +58,7 @@ public class EmojiSpan extends AnimatingImageSpan { int height = bottom - top; int centeringMargin = (height - size) / 2; int adjustedMargin = (int) (centeringMargin * SHIFT_FACTOR); + int adjustedBottom = bottom - adjustedMargin; super.draw(canvas, text, start, end, x, top, y, bottom - adjustedMargin, paint); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java index be730f275d..d512e0924c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiTextView.java @@ -49,8 +49,7 @@ public class EmojiTextView extends AppCompatTextView { } @Override public void setText(@Nullable CharSequence text, BufferType type) { - EmojiProvider provider = EmojiProvider.getInstance(getContext()); - EmojiParser.CandidateList candidates = provider.getCandidates(text); + EmojiParser.CandidateList candidates = EmojiProvider.getCandidates(text); if (scaleEmojis && candidates != null && candidates.allEmojis) { int emojis = candidates.size(); @@ -82,7 +81,7 @@ public class EmojiTextView extends AppCompatTextView { ellipsizeAnyTextForMaxLength(); } } else { - CharSequence emojified = provider.emojify(candidates, text, this); + CharSequence emojified = EmojiProvider.emojify(candidates, text, this, false); super.setText(new SpannableStringBuilder(emojified).append(Optional.fromNullable(overflowText).or("")), BufferType.SPANNABLE); // Android fails to ellipsize spannable strings. (https://issuetracker.google.com/issues/36991688) @@ -107,12 +106,12 @@ public class EmojiTextView extends AppCompatTextView { SpannableStringBuilder newContent = new SpannableStringBuilder(); newContent.append(getText().subSequence(0, maxLength)).append(ELLIPSIS).append(Optional.fromNullable(overflowText).or("")); - EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent); + EmojiParser.CandidateList newCandidates = EmojiProvider.getCandidates(newContent); if (useSystemEmoji || newCandidates == null || newCandidates.size() == 0) { super.setText(newContent, BufferType.NORMAL); } else { - CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this); + CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, false); super.setText(emojified, BufferType.SPANNABLE); } } @@ -141,8 +140,8 @@ public class EmojiTextView extends AppCompatTextView { .append(ellipsized.subSequence(0, ellipsized.length())) .append(Optional.fromNullable(overflowText).or("")); - EmojiParser.CandidateList newCandidates = EmojiProvider.getInstance(getContext()).getCandidates(newContent); - CharSequence emojified = EmojiProvider.getInstance(getContext()).emojify(newCandidates, newContent, this); + EmojiParser.CandidateList newCandidates = EmojiProvider.getCandidates(newContent); + CharSequence emojified = EmojiProvider.emojify(newCandidates, newContent, this, false); super.setText(emojified, BufferType.SPANNABLE); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java new file mode 100644 index 0000000000..1748f3b22c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiUtil.java @@ -0,0 +1,54 @@ +package org.thoughtcrime.securesms.components.emoji; + +import androidx.annotation.NonNull; + +import org.thoughtcrime.securesms.emoji.EmojiSource; +import org.thoughtcrime.securesms.emoji.ObsoleteEmoji; + +import java.util.HashSet; +import java.util.Set; +import java.util.regex.Pattern; + +public final class EmojiUtil { + private static final Pattern EMOJI_PATTERN = Pattern.compile("^(?:(?:[\u00a9\u00ae\u203c\u2049\u2122\u2139\u2194-\u2199\u21a9-\u21aa\u231a-\u231b\u2328\u23cf\u23e9-\u23f3\u23f8-\u23fa\u24c2\u25aa-\u25ab\u25b6\u25c0\u25fb-\u25fe\u2600-\u2604\u260e\u2611\u2614-\u2615\u2618\u261d\u2620\u2622-\u2623\u2626\u262a\u262e-\u262f\u2638-\u263a\u2648-\u2653\u2660\u2663\u2665-\u2666\u2668\u267b\u267f\u2692-\u2694\u2696-\u2697\u2699\u269b-\u269c\u26a0-\u26a1\u26aa-\u26ab\u26b0-\u26b1\u26bd-\u26be\u26c4-\u26c5\u26c8\u26ce-\u26cf\u26d1\u26d3-\u26d4\u26e9-\u26ea\u26f0-\u26f5\u26f7-\u26fa\u26fd\u2702\u2705\u2708-\u270d\u270f\u2712\u2714\u2716\u271d\u2721\u2728\u2733-\u2734\u2744\u2747\u274c\u274e\u2753-\u2755\u2757\u2763-\u2764\u2795-\u2797\u27a1\u27b0\u27bf\u2934-\u2935\u2b05-\u2b07\u2b1b-\u2b1c\u2b50\u2b55\u3030\u303d\u3297\u3299\ud83c\udc04\ud83c\udccf\ud83c\udd70-\ud83c\udd71\ud83c\udd7e-\ud83c\udd7f\ud83c\udd8e\ud83c\udd91-\ud83c\udd9a\ud83c\ude01-\ud83c\ude02\ud83c\ude1a\ud83c\ude2f\ud83c\ude32-\ud83c\ude3a\ud83c\ude50-\ud83c\ude51\u200d\ud83c\udf00-\ud83d\uddff\ud83d\ude00-\ud83d\ude4f\ud83d\ude80-\ud83d\udeff\ud83e\udd00-\ud83e\uddff\udb40\udc20-\udb40\udc7f]|\u200d[\u2640\u2642]|[\ud83c\udde6-\ud83c\uddff]{2}|.[\u20e0\u20e3\ufe0f]+)+)+$"); + private static final String EMOJI_REGEX = "[^\\p{L}\\p{M}\\p{N}\\p{P}\\p{Z}\\p{Cf}\\p{Cs}\\s]"; + + private EmojiUtil() {} + + /** + * This will return all ways we know of expressing a singular emoji. This is to aid in search, + * where some platforms may send an emoji we've locally marked as 'obsolete'. + */ + public static @NonNull Set getAllRepresentations(@NonNull String emoji) { + Set out = new HashSet<>(); + + out.add(emoji); + + for (ObsoleteEmoji obsoleteEmoji : EmojiSource.getLatest().getObsolete()) { + if (obsoleteEmoji.getObsolete().equals(emoji)) { + out.add(obsoleteEmoji.getReplaceWith()); + } else if (obsoleteEmoji.getReplaceWith().equals(emoji)) { + out.add(obsoleteEmoji.getObsolete()); + } + } + + return out; + } + + /** + * When provided an emoji that is a skin variation of another, this will return the default yellow + * version. This is to aid in search, so using a variation will still find all emojis tagged with + * the default version. + * + * If the emoji has no skin variations, this function will return the original emoji. + */ + public static @NonNull String getCanonicalRepresentation(@NonNull String emoji) { + String canonical = EmojiSource.getLatest().getVariationsToCanonical().get(emoji); + return canonical != null ? canonical : emoji; + } + + public static boolean isCanonicallyEqual(@NonNull String left, @NonNull String right) { + return getCanonicalRepresentation(left).equals(getCanonicalRepresentation(right)); + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiVariationSelectorPopup.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiVariationSelectorPopup.java index 6ddec92567..7f8282d2c6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiVariationSelectorPopup.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiVariationSelectorPopup.java @@ -8,8 +8,6 @@ import android.widget.PopupWindow; import androidx.annotation.NonNull; -import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener; - import java.util.List; import network.loki.messenger.R; @@ -37,7 +35,7 @@ public class EmojiVariationSelectorPopup extends PopupWindow { for (String variation : variations) { ImageView imageView = (ImageView) LayoutInflater.from(context).inflate(R.layout.emoji_variation_selector_item, list, false); - imageView.setImageDrawable(EmojiProvider.getInstance(context).getEmojiDrawable(variation)); + imageView.setImageDrawable(EmojiProvider.getEmojiDrawable(context, variation)); imageView.setOnClickListener(v -> { listener.onEmojiSelected(variation); dismiss(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java index ec6babe405..6cc39c8d14 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java @@ -2,30 +2,36 @@ package org.thoughtcrime.securesms.components.emoji; import android.content.Context; import android.content.SharedPreferences; +import android.net.Uri; import android.os.AsyncTask; import android.preference.PreferenceManager; + import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import com.annimon.stream.Stream; import com.fasterxml.jackson.databind.type.CollectionType; import com.fasterxml.jackson.databind.type.TypeFactory; -import network.loki.messenger.R; -import org.session.libsignal.utilities.Log; - import org.session.libsignal.utilities.JsonUtil; +import org.session.libsignal.utilities.Log; import java.io.IOException; import java.util.ArrayList; -import java.util.Collections; +import java.util.Arrays; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; +import network.loki.messenger.R; + public class RecentEmojiPageModel implements EmojiPageModel { private static final String TAG = RecentEmojiPageModel.class.getSimpleName(); private static final String EMOJI_LRU_PREFERENCE = "pref_recent_emoji2"; private static final int EMOJI_LRU_SIZE = 50; + public static final String KEY = "Recents"; + public static final List DEFAULT_REACTIONS_LIST = + Arrays.asList("\ud83d\ude02", "\ud83e\udd70", "\ud83d\ude22", "\ud83d\ude21", "\ud83d\ude2e", "\ud83d\ude08"); private final SharedPreferences prefs; private final LinkedHashSet recentlyUsed; @@ -47,14 +53,28 @@ public class RecentEmojiPageModel implements EmojiPageModel { } } + @Override + public String getKey() { + return KEY; + } + @Override public int getIconAttr() { return R.attr.emoji_category_recent; } @Override public List getEmoji() { - List emoji = new ArrayList<>(recentlyUsed); - Collections.reverse(emoji); - return emoji; + List recent = new ArrayList<>(recentlyUsed); + List out = new ArrayList<>(DEFAULT_REACTIONS_LIST.size()); + + for (int i = 0; i < DEFAULT_REACTIONS_LIST.size(); i++) { + if (recent.size() > i) { + out.add(recent.get(i)); + } else { + out.add(DEFAULT_REACTIONS_LIST.get(i)); + } + } + + return out; } @Override public List getDisplayEmoji() { @@ -65,7 +85,9 @@ public class RecentEmojiPageModel implements EmojiPageModel { return false; } - @Override public String getSprite() { + @Nullable + @Override + public Uri getSpriteUri() { return null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java index e1b248b5cc..3270889ec6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/StaticEmojiPageModel.java @@ -1,38 +1,40 @@ package org.thoughtcrime.securesms.components.emoji; -import androidx.annotation.AttrRes; +import android.net.Uri; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.util.ArrayList; +import org.thoughtcrime.securesms.emoji.EmojiCategory; + import java.util.Arrays; +import java.util.Collections; import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; public class StaticEmojiPageModel implements EmojiPageModel { - @AttrRes private final int iconAttr; - @NonNull private final List emoji; - @Nullable private final String sprite; + private final @NonNull EmojiCategory category; + private final @NonNull List emoji; + private final @Nullable Uri sprite; - public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull String[] strings, @Nullable String sprite) { - List emoji = new ArrayList<>(strings.length); - for (String s : strings) { - emoji.add(new Emoji(s)); - } + public StaticEmojiPageModel(@NonNull EmojiCategory category, @NonNull String[] strings, @Nullable Uri sprite) { + this(category, Arrays.stream(strings).map(s -> new Emoji(Collections.singletonList(s))).collect(Collectors.toList()), sprite); + } - this.iconAttr = iconAttr; - this.emoji = emoji; + public StaticEmojiPageModel(@NonNull EmojiCategory category, @NonNull List emoji, @Nullable Uri sprite) { + this.category = category; + this.emoji = Collections.unmodifiableList(emoji); this.sprite = sprite; } - public StaticEmojiPageModel(@AttrRes int iconAttr, @NonNull Emoji[] emoji, @Nullable String sprite) { - this.iconAttr = iconAttr; - this.emoji = Arrays.asList(emoji); - this.sprite = sprite; + @Override + public String getKey() { + return category.getKey(); } public int getIconAttr() { - return iconAttr; + return category.getIcon(); } @Override @@ -55,7 +57,7 @@ public class StaticEmojiPageModel implements EmojiPageModel { } @Override - public @Nullable String getSprite() { + public @Nullable Uri getSpriteUri() { return sprite; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiDrawInfo.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiDrawInfo.java deleted file mode 100644 index 387af40a08..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiDrawInfo.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.thoughtcrime.securesms.components.emoji.parsing; - - -import androidx.annotation.NonNull; - -public class EmojiDrawInfo { - - private final EmojiPageBitmap page; - private final int index; - - public EmojiDrawInfo(final @NonNull EmojiPageBitmap page, final int index) { - this.page = page; - this.index = index; - } - - public @NonNull EmojiPageBitmap getPage() { - return page; - } - - public int getIndex() { - return index; - } - - @Override - public @NonNull String toString() { - return "DrawInfo{" + - "page=" + page + - ", index=" + index + - '}'; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiDrawInfo.kt b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiDrawInfo.kt new file mode 100644 index 0000000000..28de3aca78 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiDrawInfo.kt @@ -0,0 +1,5 @@ +package org.thoughtcrime.securesms.components.emoji.parsing + +import org.thoughtcrime.securesms.emoji.EmojiPage + +data class EmojiDrawInfo(val page: EmojiPage, val index: Int, private val emoji: String, val rawEmoji: String?, val jumboSheet: String?) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java index a01e8f329f..3c3a4fa3eb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiPageBitmap.java @@ -48,7 +48,7 @@ public class EmojiPageBitmap { } else { Callable callable = () -> { try { - Log.i(TAG, "loading page " + model.getSprite()); + Log.i(TAG, "loading page " + model.getSpriteUri().toString()); return loadPage(); } catch (IOException ioe) { Log.w(TAG, ioe); @@ -76,7 +76,7 @@ public class EmojiPageBitmap { float scale = decodeScale; AssetManager assetManager = context.getAssets(); - InputStream assetStream = assetManager.open(model.getSprite()); + InputStream assetStream = assetManager.open(model.getSpriteUri().toString()); BitmapFactory.Options options = new BitmapFactory.Options(); if (org.thoughtcrime.securesms.util.Util.isLowMemory(context)) { @@ -85,7 +85,7 @@ public class EmojiPageBitmap { scale = decodeScale * 2; } - Stopwatch stopwatch = new Stopwatch(model.getSprite()); + Stopwatch stopwatch = new Stopwatch(model.getSpriteUri().toString()); Bitmap bitmap = BitmapFactory.decodeStream(assetStream, null, options); stopwatch.split("decode"); @@ -94,7 +94,7 @@ public class EmojiPageBitmap { stopwatch.stop(TAG); bitmapReference = new SoftReference<>(scaledBitmap); - Log.i(TAG, "onPageLoaded(" + model.getSprite() + ") originalByteCount: " + bitmap.getByteCount() + Log.i(TAG, "onPageLoaded(" + model.getSpriteUri().toString() + ") originalByteCount: " + bitmap.getByteCount() + " scaledByteCount: " + scaledBitmap.getByteCount() + " scaledSize: " + scaledBitmap.getWidth() + "x" + scaledBitmap.getHeight()); return scaledBitmap; @@ -102,6 +102,6 @@ public class EmojiPageBitmap { @Override public @NonNull String toString() { - return model.getSprite(); + return model.getSpriteUri().toString(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiTree.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiTree.java index af8caa568d..399df58f6b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiTree.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/parsing/EmojiTree.java @@ -74,10 +74,10 @@ public class EmojiTree { } } - public @Nullable EmojiDrawInfo getEmoji(CharSequence unicode, int startPosition, int endPostiion) { + public @Nullable EmojiDrawInfo getEmoji(CharSequence unicode, int startPosition, int endPosition) { EmojiTreeNode tree = root; - for (int i=startPosition; i Unit) { + + private val mappingAdapter = MappingAdapter().apply { + registerFactory(DisplayItem::class.java, LayoutFactory({ ItemViewHolder(it, onItemClick) }, R.layout.context_menu_item)) + } + + init { + recyclerView.apply { + adapter = mappingAdapter + layoutManager = LinearLayoutManager(context) + itemAnimator = null + } + } + + fun setItems(items: List) { + mappingAdapter.submitList(items.toAdapterItems()) + } + + private fun List.toAdapterItems(): List { + return this.mapIndexed { index, item -> + val displayType: DisplayType = when { + this.size == 1 -> DisplayType.ONLY + index == 0 -> DisplayType.TOP + index == this.size - 1 -> DisplayType.BOTTOM + else -> DisplayType.MIDDLE + } + + DisplayItem(item, displayType) + } + } + + private data class DisplayItem( + val item: ActionItem, + val displayType: DisplayType + ) : MappingModel { + override fun areItemsTheSame(newItem: DisplayItem): Boolean { + return this == newItem + } + + override fun areContentsTheSame(newItem: DisplayItem): Boolean { + return this == newItem + } + } + + private enum class DisplayType { + TOP, BOTTOM, MIDDLE, ONLY + } + + private class ItemViewHolder( + itemView: View, + private val onItemClick: () -> Unit, + ) : MappingViewHolder(itemView) { + val icon: ImageView = itemView.findViewById(R.id.context_menu_item_icon) + val title: TextView = itemView.findViewById(R.id.context_menu_item_title) + + override fun bind(model: DisplayItem) { + if (model.item.iconRes > 0) { + val typedValue = TypedValue() + context.theme.resolveAttribute(model.item.iconRes, typedValue, true) + icon.setImageDrawable(ContextCompat.getDrawable(context, typedValue.resourceId)) + } + title.text = model.item.title + itemView.setOnClickListener { + model.item.action.run() + onItemClick() + } + + when (model.displayType) { + DisplayType.TOP -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_top) + DisplayType.BOTTOM -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_bottom) + DisplayType.MIDDLE -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_middle) + DisplayType.ONLY -> itemView.setBackgroundResource(R.drawable.context_menu_item_background_only) + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 236c01c688..8283cd946c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -34,6 +34,7 @@ import android.widget.Toast import androidx.activity.viewModels import androidx.annotation.DimenRes import androidx.appcompat.app.AlertDialog +import androidx.core.view.drawToBitmap import androidx.core.view.isVisible import androidx.lifecycle.Observer import androidx.lifecycle.ViewModelProvider @@ -47,18 +48,20 @@ import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import network.loki.messenger.R -import network.loki.messenger.databinding.ActivityConversationV2ActionBarBinding import network.loki.messenger.databinding.ActivityConversationV2Binding import network.loki.messenger.databinding.ViewVisibleMessageBinding import nl.komponents.kovenant.ui.successUi +import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.mentions.Mention import org.session.libsession.messaging.mentions.MentionsManager import org.session.libsession.messaging.messages.control.DataExtractionNotification import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage +import org.session.libsession.messaging.messages.visible.Reaction import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupApi +import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability import org.session.libsession.messaging.sending_receiving.MessageSender import org.session.libsession.messaging.sending_receiving.attachments.Attachment import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview @@ -68,6 +71,7 @@ import org.session.libsession.utilities.Address import org.session.libsession.utilities.Address.Companion.fromSerialized import org.session.libsession.utilities.GroupUtil import org.session.libsession.utilities.MediaTypes +import org.session.libsession.utilities.Stub import org.session.libsession.utilities.TextSecurePreferences import org.session.libsession.utilities.concurrent.SimpleTask import org.session.libsession.utilities.recipients.Recipient @@ -82,6 +86,8 @@ import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.audio.AudioRecorder import org.thoughtcrime.securesms.contacts.SelectContactsActivity.Companion.selectedContactsKey import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher +import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener +import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener import org.thoughtcrime.securesms.conversation.v2.dialogs.BlockedDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.LinkPreviewDialog import org.thoughtcrime.securesms.conversation.v2.dialogs.SendSeedDialog @@ -92,9 +98,8 @@ import org.thoughtcrime.securesms.conversation.v2.input_bar.mentions.MentionCand import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallback import org.thoughtcrime.securesms.conversation.v2.menus.ConversationActionModeCallbackDelegate import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuHelper -import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView -import org.thoughtcrime.securesms.conversation.v2.messages.VoiceMessageViewDelegate +import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.conversation.v2.search.SearchBottomBar import org.thoughtcrime.securesms.conversation.v2.search.SearchViewModel import org.thoughtcrime.securesms.conversation.v2.utilities.AttachmentManager @@ -110,13 +115,17 @@ import org.thoughtcrime.securesms.database.LokiMessageDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase +import org.thoughtcrime.securesms.database.ReactionDatabase import org.thoughtcrime.securesms.database.SessionContactDatabase import org.thoughtcrime.securesms.database.SmsDatabase import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.model.MessageId import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord +import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.giph.ui.GiphyActivity +import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository import org.thoughtcrime.securesms.linkpreview.LinkPreviewUtil import org.thoughtcrime.securesms.linkpreview.LinkPreviewViewModel @@ -132,6 +141,8 @@ import org.thoughtcrime.securesms.mms.Slide import org.thoughtcrime.securesms.mms.SlideDeck import org.thoughtcrime.securesms.mms.VideoSlide import org.thoughtcrime.securesms.permissions.Permissions +import org.thoughtcrime.securesms.reactions.ReactionsDialogFragment +import org.thoughtcrime.securesms.reactions.any.ReactWithAnyEmojiDialogFragment import org.thoughtcrime.securesms.util.ActivityDispatcher import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.DateUtils @@ -154,12 +165,12 @@ import kotlin.math.sqrt // price we pay is a bit of back and forth between the input bar and the conversation activity. @AndroidEntryPoint class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDelegate, - InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher, - ConversationActionModeCallbackDelegate, VisibleMessageContentViewDelegate, RecipientModifiedListener, - SearchBottomBar.EventListener, VoiceMessageViewDelegate, LoaderManager.LoaderCallbacks { + InputBarRecordingViewDelegate, AttachmentManager.AttachmentListener, ActivityDispatcher, + ConversationActionModeCallbackDelegate, VisibleMessageViewDelegate, RecipientModifiedListener, + SearchBottomBar.EventListener, LoaderManager.LoaderCallbacks, + OnReactionSelectedListener, ReactWithAnyEmojiDialogFragment.Callback, ReactionsDialogFragment.Callback { private var binding: ActivityConversationV2Binding? = null - private var actionBarBinding: ActivityConversationV2ActionBarBinding? = null @Inject lateinit var textSecurePreferences: TextSecurePreferences @Inject lateinit var threadDb: ThreadDatabase @@ -172,6 +183,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe @Inject lateinit var mmsDb: MmsDatabase @Inject lateinit var lokiMessageDb: LokiMessageDatabase @Inject lateinit var storage: Storage + @Inject lateinit var reactionDb: ReactionDatabase @Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory private val screenWidth = Resources.getSystem().displayMetrics.widthPixels @@ -203,7 +215,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } ?: finish() } - viewModelFactory.create(threadId) + viewModelFactory.create(threadId, MessagingModuleConfiguration.shared.getUserED25519KeyPair()) } private var actionMode: ActionMode? = null private var unreadCount = 0 @@ -252,21 +264,27 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe onItemPress = { message, position, view, event -> handlePress(message, position, view, event) }, - onItemSwipeToReply = { message, position -> - handleSwipeToReply(message, position) + onItemSwipeToReply = { message, _ -> + handleSwipeToReply(message) }, - onItemLongPress = { message, position -> - handleLongPress(message, position) + onItemLongPress = { message, position, view -> + if (!isMessageRequestThread() && + (viewModel.openGroup == null || Capability.REACTIONS.name.lowercase() in viewModel.serverCapabilities) + ) { + showEmojiPicker(message, view) + } else { + handleLongPress(message, position) + } }, - glide, onDeselect = { message, position -> actionMode?.let { onDeselect(message, position, it) } }, + glide = glide, lifecycleCoroutineScope = lifecycleScope ) - adapter.visibleMessageContentViewDelegate = this + adapter.visibleMessageViewDelegate = this adapter } @@ -279,6 +297,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe private val messageToScrollTimestamp = AtomicLong(-1) private val messageToScrollAuthor = AtomicReference(null) + private lateinit var reactionDelegate: ConversationReactionDelegate + private val reactWithAnyEmojiStartPage = -1 + // region Settings companion object { // Extras @@ -294,8 +315,6 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe const val PICK_FROM_LIBRARY = 12 const val INVITE_CONTACTS = 124 - //flag - const val IS_UNSEND_REQUESTS_ENABLED = true } // endregion @@ -339,14 +358,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe showOrHideInputIfNeeded() setUpMessageRequestsBar() viewModel.recipient?.let { recipient -> - if (recipient.isOpenGroupRecipient) { - val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) - if (openGroup == null) { - Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() - return finish() - } + if (recipient.isOpenGroupRecipient && viewModel.openGroup == null) { + Toast.makeText(this, "This thread has been deleted.", Toast.LENGTH_LONG).show() + return finish() } } + + val reactionOverlayStub: Stub = + ViewUtil.findStubById(this, R.id.conversation_reaction_scrubber_stub) + reactionDelegate = ConversationReactionDelegate(reactionOverlayStub) + reactionDelegate.setOnReactionSelectedListener(this) } override fun onResume() { @@ -413,22 +434,22 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe // called from onCreate private fun setUpToolBar() { + setSupportActionBar(binding?.toolbar) val actionBar = supportActionBar ?: return - actionBarBinding = ActivityConversationV2ActionBarBinding.inflate(layoutInflater) actionBar.title = "" - actionBar.customView = actionBarBinding!!.root - actionBar.setDisplayShowCustomEnabled(true) - actionBarBinding!!.conversationTitleView.text = viewModel.recipient?.toShortString() + actionBar.setDisplayHomeAsUpEnabled(true) + actionBar.setHomeButtonEnabled(true) + binding!!.toolbarContent.conversationTitleView.text = viewModel.recipient?.toShortString() @DimenRes val sizeID: Int = if (viewModel.recipient?.isClosedGroupRecipient == true) { R.dimen.medium_profile_picture_size } else { R.dimen.small_profile_picture_size } val size = resources.getDimension(sizeID).roundToInt() - actionBarBinding!!.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size) - actionBarBinding!!.profilePictureView.root.glide = glide + binding!!.toolbarContent.profilePictureView.root.layoutParams = LinearLayout.LayoutParams(size, size) + binding!!.toolbarContent.profilePictureView.root.glide = glide MentionManagerUtilities.populateUserPublicKeyCacheIfNeeded(viewModel.threadId, this) - val profilePictureView = actionBarBinding!!.profilePictureView.root + val profilePictureView = binding!!.toolbarContent.profilePictureView.root viewModel.recipient?.let { recipient -> profilePictureView.update(recipient) } @@ -529,8 +550,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } private fun getLatestOpenGroupInfoIfNeeded() { - val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) ?: return - OpenGroupApi.getMemberCount(openGroup.room, openGroup.server).successUi { updateSubtitle() } + viewModel.openGroup?.let { + OpenGroupApi.getMemberCount(it.room, it.server).successUi { updateSubtitle() } + } } // called from onCreate @@ -609,7 +631,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe tearDownRecipientObserver() super.onDestroy() binding = null - actionBarBinding = null +// actionBarBinding = null } // endregion @@ -625,9 +647,9 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe updateSubtitle() showOrHideInputIfNeeded() if (recipient != null) { - actionBarBinding?.profilePictureView?.root?.update(recipient) + binding?.toolbarContent?.profilePictureView?.root?.update(recipient) } - actionBarBinding?.conversationTitleView?.text = recipient?.toShortString() + binding?.toolbarContent?.conversationTitleView?.text = recipient?.toShortString() } } @@ -865,12 +887,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe val wasTypingIndicatorVisibleBefore = binding.typingIndicatorViewContainer.isVisible binding.typingIndicatorViewContainer.isVisible = wasTypingIndicatorVisibleBefore && isScrolledToBottom binding.typingIndicatorViewContainer.isVisible - binding.scrollToBottomButton.isVisible = !isScrolledToBottom && adapter.itemCount > 0 + showOrHidScrollToBottomButton() val firstVisiblePosition = layoutManager?.findFirstVisibleItemPosition() ?: -1 unreadCount = min(unreadCount, firstVisiblePosition).coerceAtLeast(0) updateUnreadCountIndicator() } + private fun showOrHidScrollToBottomButton(show: Boolean = true) { + binding?.scrollToBottomButton?.isVisible = show && !isScrolledToBottom && adapter.itemCount > 0 + } + private fun updateUnreadCountIndicator() { val binding = binding ?: return val formattedUnreadCount = if (unreadCount < 10000) unreadCount.toString() else "9999+" @@ -882,7 +908,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } private fun updateSubtitle() { - val actionBarBinding = actionBarBinding ?: return + val actionBarBinding = binding?.toolbarContent ?: return val recipient = viewModel.recipient ?: return actionBarBinding.muteIconImageView.isVisible = recipient.isMuted actionBarBinding.conversationSubtitleView.isVisible = true @@ -893,11 +919,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_muted_forever) } } else if (recipient.isGroupRecipient) { - val openGroup = lokiThreadDb.getOpenGroupChat(viewModel.threadId) - if (openGroup != null) { + viewModel.openGroup?.let { openGroup -> val userCount = lokiApiDb.getUserCount(openGroup.room, openGroup.server) ?: 0 actionBarBinding.conversationSubtitleView.text = getString(R.string.ConversationActivity_member_count, userCount) - } else { + } ?: run { actionBarBinding.conversationSubtitleView.isVisible = false } } else { @@ -942,7 +967,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } // `position` is the adapter position; not the visual position - private fun handleSwipeToReply(message: MessageRecord, position: Int) { + private fun handleSwipeToReply(message: MessageRecord) { val recipient = viewModel.recipient ?: return binding?.inputBar?.draftQuote(recipient, message, glide) } @@ -966,6 +991,164 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } + private fun showEmojiPicker(message: MessageRecord, visibleMessageView: VisibleMessageView) { + val messageContentBitmap = try { + visibleMessageView.messageContentView.drawToBitmap() + } catch (e: Exception) { + Log.e("Loki", "Failed to show emoji picker", e) + return + } + ViewUtil.hideKeyboard(this, visibleMessageView); + binding?.reactionsShade?.isVisible = true + showOrHidScrollToBottomButton(false) + binding?.conversationRecyclerView?.suppressLayout(true) + reactionDelegate.setOnActionSelectedListener(ReactionsToolbarListener(message)) + reactionDelegate.setOnHideListener(object: ConversationReactionOverlay.OnHideListener { + override fun startHide() { + binding?.reactionsShade?.let { + ViewUtil.fadeOut(it, resources.getInteger(R.integer.reaction_scrubber_hide_duration), View.GONE) + } + showOrHidScrollToBottomButton(true) + } + + override fun onHide() { + binding?.conversationRecyclerView?.suppressLayout(false) + + WindowUtil.setLightStatusBarFromTheme(this@ConversationActivityV2); + WindowUtil.setLightNavigationBarFromTheme(this@ConversationActivityV2); + } + + }) + val contentBounds = Rect() + visibleMessageView.messageContentView.getGlobalVisibleRect(contentBounds) + val selectedConversationModel = SelectedConversationModel( + messageContentBitmap, + contentBounds.left.toFloat(), + contentBounds.top.toFloat(), + visibleMessageView.messageContentView.width, + message.isOutgoing, + visibleMessageView.messageContentView + ) + reactionDelegate.show(this, message, selectedConversationModel) + } + + override fun dispatchTouchEvent(ev: MotionEvent): Boolean { + return reactionDelegate.applyTouchEvent(ev) || super.dispatchTouchEvent(ev) + } + + override fun onReactionSelected(messageRecord: MessageRecord, emoji: String) { + reactionDelegate.hide() + val oldRecord = messageRecord.reactions.find { it.author == textSecurePreferences.getLocalNumber() } + if (oldRecord != null && oldRecord.emoji == emoji) { + sendEmojiRemoval(emoji, messageRecord) + } else { + sendEmojiReaction(emoji, messageRecord) + } + } + + private fun sendEmojiReaction(emoji: String, originalMessage: MessageRecord) { + // Create the message + val recipient = viewModel.recipient ?: return + val reactionMessage = VisibleMessage() + val emojiTimestamp = System.currentTimeMillis() + reactionMessage.sentTimestamp = emojiTimestamp + val author = textSecurePreferences.getLocalNumber()!! + // Put the message in the database + val reaction = ReactionRecord( + messageId = originalMessage.id, + isMms = originalMessage.isMms, + author = author, + emoji = emoji, + count = 1, + dateSent = emojiTimestamp, + dateReceived = emojiTimestamp + ) + reactionDb.addReaction(MessageId(originalMessage.id, originalMessage.isMms), reaction) + // Send it + reactionMessage.reaction = Reaction.from(originalMessage.timestamp, originalMessage.recipient.address.serialize(), emoji, true) + if (recipient.isOpenGroupRecipient) { + val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return + viewModel.openGroup?.let { + OpenGroupApi.addReaction(it.room, it.server, messageServerId, emoji) + } + } else { + MessageSender.send(reactionMessage, recipient.address) + } + LoaderManager.getInstance(this).restartLoader(0, null, this) + } + + private fun sendEmojiRemoval(emoji: String, originalMessage: MessageRecord) { + val recipient = viewModel.recipient ?: return + val message = VisibleMessage() + val emojiTimestamp = System.currentTimeMillis() + message.sentTimestamp = emojiTimestamp + val author = textSecurePreferences.getLocalNumber()!! + reactionDb.deleteReaction(emoji, MessageId(originalMessage.id, originalMessage.isMms), author) + message.reaction = Reaction.from(originalMessage.timestamp, author, emoji, false) + if (recipient.isOpenGroupRecipient) { + val messageServerId = lokiMessageDb.getServerID(originalMessage.id, !originalMessage.isMms) ?: return + viewModel.openGroup?.let { + OpenGroupApi.deleteReaction(it.room, it.server, messageServerId, emoji) + } + } else { + MessageSender.send(message, recipient.address) + } + LoaderManager.getInstance(this).restartLoader(0, null, this) + } + + override fun onCustomReactionSelected(messageRecord: MessageRecord, hasAddedCustomEmoji: Boolean) { + val oldRecord = messageRecord.reactions.find { record -> record.author == textSecurePreferences.getLocalNumber() } + + if (oldRecord != null && hasAddedCustomEmoji) { + reactionDelegate.hide() + sendEmojiRemoval(oldRecord.emoji, messageRecord) + } else { + reactionDelegate.hideForReactWithAny() + + ReactWithAnyEmojiDialogFragment + .createForMessageRecord(messageRecord, reactWithAnyEmojiStartPage) + .show(supportFragmentManager, "BOTTOM"); + } + } + + override fun onReactWithAnyEmojiDialogDismissed() { + reactionDelegate.hide() + } + + override fun onReactWithAnyEmojiSelected(emoji: String, messageId: MessageId) { + reactionDelegate.hide() + val message = if (messageId.mms) { + mmsDb.getMessageRecord(messageId.id) + } else { + smsDb.getMessageRecord(messageId.id) + } + val oldRecord = reactionDb.getReactions(messageId).find { it.author == textSecurePreferences.getLocalNumber() } + if (oldRecord?.emoji == emoji) { + sendEmojiRemoval(emoji, message) + } else { + sendEmojiReaction(emoji, message) + } + } + + override fun onRemoveReaction(emoji: String, messageId: MessageId) { + val message = if (messageId.mms) { + mmsDb.getMessageRecord(messageId.id) + } else { + smsDb.getMessageRecord(messageId.id) + } + sendEmojiRemoval(emoji, message) + } + + override fun onClearAll(emoji: String, messageId: MessageId) { + reactionDb.deleteEmojiReactions(emoji, messageId) + viewModel.openGroup?.let { openGroup -> + lokiMessageDb.getServerID(messageId.id, !messageId.mms)?.let { serverId -> + OpenGroupApi.deleteAllReactions(openGroup.room, openGroup.server, serverId, emoji) + } + } + threadDb.notifyThreadUpdated(viewModel.threadId) + } + override fun onMicrophoneButtonMove(event: MotionEvent) { val rawX = event.rawX val chevronImageView = binding?.inputBarRecordingView?.chevronImageView ?: return @@ -1047,6 +1230,30 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe binding?.conversationRecyclerView?.scrollToPosition(lastSeenItemPosition) } + override fun onReactionClicked(emoji: String, messageId: MessageId, userWasSender: Boolean) { + val message = if (messageId.mms) { + mmsDb.getMessageRecord(messageId.id) + } else { + smsDb.getMessageRecord(messageId.id) + } + if (userWasSender) { + sendEmojiRemoval(emoji, message) + } else { + sendEmojiReaction(emoji, message) + } + } + + override fun onReactionLongClicked(messageId: MessageId) { + if (viewModel.recipient?.isGroupRecipient == true) { + val isUserModerator = viewModel.openGroup?.let { openGroup -> + val userPublicKey = textSecurePreferences.getLocalNumber() ?: return@let false + OpenGroupManager.isUserModerator(this, openGroup.id, userPublicKey, viewModel.blindedPublicKey) + } ?: false + val fragment = ReactionsDialogFragment.create(messageId, isUserModerator) + fragment.show(supportFragmentManager, null) + } + } + override fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) { if (indexInAdapter < 0 || indexInAdapter >= adapter.itemCount) { return } val viewHolder = binding?.conversationRecyclerView?.findViewHolderForAdapterPosition(indexInAdapter) as? ConversationAdapter.VisibleMessageViewHolder ?: return @@ -1303,34 +1510,16 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe stopAudioHandler.removeCallbacks(stopVoiceMessageRecordingTask) } - // Remove this after the unsend request is enabled - fun deleteMessagesWithoutUnsendRequest(messages: Set) { - val messageCount = messages.size - val builder = AlertDialog.Builder(this) - builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) - builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) - builder.setCancelable(true) - builder.setPositiveButton(R.string.delete) { _, _ -> - viewModel.deleteMessagesWithoutUnsendRequest(messages) - endActionMode() - } - builder.setNegativeButton(android.R.string.cancel) { dialog, _ -> - dialog.dismiss() - endActionMode() - } - builder.show() + override fun selectMessages(messages: Set) { + handleLongPress(messages.first(), 0) //TODO: begin selection mode } override fun deleteMessages(messages: Set) { val recipient = viewModel.recipient ?: return - if (!IS_UNSEND_REQUESTS_ENABLED) { - deleteMessagesWithoutUnsendRequest(messages) - return - } val allSentByCurrentUser = messages.all { it.isOutgoing } val allHasHash = messages.all { lokiMessageDb.getMessageServerHash(it.id) != null } if (recipient.isOpenGroupRecipient) { - val messageCount = messages.size + val messageCount = 1 val builder = AlertDialog.Builder(this) builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) @@ -1369,7 +1558,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } bottomSheet.show(supportFragmentManager, bottomSheet.tag) } else { - val messageCount = messages.size + val messageCount = 1 val builder = AlertDialog.Builder(this) builder.setTitle(resources.getQuantityString(R.plurals.ConversationFragment_delete_selected_messages, messageCount, messageCount)) builder.setMessage(resources.getQuantityString(R.plurals.ConversationFragment_this_will_permanently_delete_all_n_selected_messages, messageCount, messageCount)) @@ -1466,9 +1655,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } override fun showMessageDetail(messages: Set) { - val message = messages.first() val intent = Intent(this, MessageDetailActivity::class.java) - intent.putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, message.timestamp) + intent.putExtra(MessageDetailActivity.MESSAGE_TIMESTAMP, messages.first().timestamp) push(intent) endActionMode() } @@ -1549,8 +1737,8 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe if (result == null) return@Observer if (result.getResults().isNotEmpty()) { result.getResults()[result.position]?.let { - jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs, - { searchViewModel.onMissingResult() }) + jumpToMessage(it.messageRecipient.address, it.receivedTimestampMs) { + searchViewModel.onMissingResult() } } } binding?.searchBottomBar?.setData(result.position, result.getResults().size) @@ -1600,4 +1788,21 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe } } // endregion + + inner class ReactionsToolbarListener constructor(val message: MessageRecord) : OnActionSelectedListener { + + override fun onActionSelected(action: ConversationReactionOverlay.Action) { + val selectedItems = setOf(message) + when (action) { + ConversationReactionOverlay.Action.REPLY -> reply(selectedItems) + ConversationReactionOverlay.Action.RESEND -> resendMessage(selectedItems) + ConversationReactionOverlay.Action.DOWNLOAD -> saveAttachment(selectedItems) + ConversationReactionOverlay.Action.COPY_MESSAGE -> copyMessages(selectedItems) + ConversationReactionOverlay.Action.VIEW_INFO -> showMessageDetail(selectedItems) + ConversationReactionOverlay.Action.SELECT -> selectMessages(selectedItems) + ConversationReactionOverlay.Action.DELETE -> deleteMessages(selectedItems) + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt index 10ae36fb8a..17a47a843f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapter.kt @@ -24,23 +24,30 @@ import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageBinding import org.session.libsession.messaging.contacts.Contact import org.thoughtcrime.securesms.conversation.v2.messages.ControlMessageView -import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageContentViewDelegate import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageView +import org.thoughtcrime.securesms.conversation.v2.messages.VisibleMessageViewDelegate import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.GlideRequests import org.thoughtcrime.securesms.preferences.PrivacySettingsActivity -class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit, - private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, private val onItemLongPress: (MessageRecord, Int) -> Unit, - private val glide: GlideRequests, private val onDeselect: (MessageRecord, Int) -> Unit, lifecycleCoroutineScope: LifecycleCoroutineScope) +class ConversationAdapter( + context: Context, + cursor: Cursor, + private val onItemPress: (MessageRecord, Int, VisibleMessageView, MotionEvent) -> Unit, + private val onItemSwipeToReply: (MessageRecord, Int) -> Unit, + private val onItemLongPress: (MessageRecord, Int, VisibleMessageView) -> Unit, + private val onDeselect: (MessageRecord, Int) -> Unit, + private val glide: GlideRequests, + lifecycleCoroutineScope: LifecycleCoroutineScope +) : CursorRecyclerViewAdapter(context, cursor) { private val messageDB by lazy { DatabaseComponent.get(context).mmsSmsDatabase() } private val contactDB by lazy { DatabaseComponent.get(context).sessionContactDatabase() } var selectedItems = mutableSetOf() private var searchQuery: String? = null - var visibleMessageContentViewDelegate: VisibleMessageContentViewDelegate? = null + var visibleMessageViewDelegate: VisibleMessageViewDelegate? = null private val updateQueue = Channel(1024, onBufferOverflow = BufferOverflow.DROP_OLDEST) private val contactCache = SparseArray(100) @@ -99,7 +106,6 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr val messageBefore = getMessageBefore(position, cursor) when (viewHolder) { is VisibleMessageViewHolder -> { - val view = viewHolder.view val visibleMessageView = ViewVisibleMessageBinding.bind(viewHolder.view).visibleMessageView val isSelected = selectedItems.contains(message) visibleMessageView.snIsSelected = isSelected @@ -114,17 +120,16 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr } val contact = contactCache[senderIdHash] - visibleMessageView.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery, contact, senderId) + visibleMessageView.bind(message, messageBefore, getMessageAfter(position, cursor), glide, searchQuery, contact, senderId, visibleMessageViewDelegate) if (!message.isDeleted) { visibleMessageView.onPress = { event -> onItemPress(message, viewHolder.adapterPosition, visibleMessageView, event) } visibleMessageView.onSwipeToReply = { onItemSwipeToReply(message, viewHolder.adapterPosition) } - visibleMessageView.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition) } + visibleMessageView.onLongPress = { onItemLongPress(message, viewHolder.adapterPosition, visibleMessageView) } } else { visibleMessageView.onPress = null visibleMessageView.onSwipeToReply = null visibleMessageView.onLongPress = null } - visibleMessageView.contentViewDelegate = visibleMessageContentViewDelegate } is ControlMessageViewHolder -> { viewHolder.view.bind(message, messageBefore) @@ -149,6 +154,11 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr } } + fun toggleSelection(message: MessageRecord, position: Int) { + if (selectedItems.contains(message)) selectedItems.remove(message) else selectedItems.add(message) + notifyItemChanged(position) + } + override fun onItemViewRecycled(viewHolder: ViewHolder?) { when (viewHolder) { is VisibleMessageViewHolder -> viewHolder.view.findViewById(R.id.visibleMessageView).recycle() @@ -196,11 +206,6 @@ class ConversationAdapter(context: Context, cursor: Cursor, private val onItemPr } } - fun toggleSelection(message: MessageRecord, position: Int) { - if (selectedItems.contains(message)) selectedItems.remove(message) else selectedItems.add(message) - notifyItemChanged(position) - } - fun findLastSeenItemPosition(lastSeenTimestamp: Long): Int? { val cursor = this.cursor if (lastSeenTimestamp <= 0L || cursor == null || !isActiveCursor) return null diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationContextMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationContextMenu.kt new file mode 100644 index 0000000000..0a76e81878 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationContextMenu.kt @@ -0,0 +1,58 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import android.content.Context +import android.view.Gravity +import android.view.LayoutInflater +import android.view.MotionEvent +import android.view.View +import android.view.ViewGroup +import android.widget.PopupWindow +import androidx.core.content.ContextCompat +import network.loki.messenger.R +import org.thoughtcrime.securesms.components.menu.ActionItem +import org.thoughtcrime.securesms.components.menu.ContextMenuList + +/** + * The context menu shown after long pressing a message in ConversationActivity. + */ +class ConversationContextMenu(private val anchor: View, items: List) : PopupWindow( + LayoutInflater.from(anchor.context).inflate(R.layout.context_menu, null), + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, +) { + + val context: Context = anchor.context + + private val contextMenuList = ContextMenuList( + recyclerView = contentView.findViewById(R.id.context_menu_list), + onItemClick = { dismiss() }, + ) + + init { + setBackgroundDrawable(ContextCompat.getDrawable(context, R.drawable.context_menu_background)) + animationStyle = R.style.ConversationContextMenuAnimation + + isFocusable = false + isOutsideTouchable = true + + elevation = 20f + + setTouchInterceptor { _, event -> + event.action == MotionEvent.ACTION_OUTSIDE + } + + contextMenuList.setItems(items) + + contentView.measure( + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED), + View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED) + ) + } + + fun getMaxWidth(): Int = contentView.measuredWidth + fun getMaxHeight(): Int = contentView.measuredHeight + + fun show(offsetX: Int, offsetY: Int) { + showAsDropDown(anchor, offsetX, offsetY, Gravity.TOP or Gravity.START) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionDelegate.kt new file mode 100644 index 0000000000..a090462c46 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionDelegate.kt @@ -0,0 +1,94 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import android.app.Activity +import android.graphics.PointF +import android.view.MotionEvent +import org.session.libsession.utilities.Stub +import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnActionSelectedListener +import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnHideListener +import org.thoughtcrime.securesms.conversation.v2.ConversationReactionOverlay.OnReactionSelectedListener +import org.thoughtcrime.securesms.database.model.MessageRecord + +/** + * Delegate class that mimics the ConversationReactionOverlay public API + * + * This allows us to properly stub out the ConversationReactionOverlay View class while still + * respecting listeners and other positional information that can be set BEFORE we want to actually + * resolve the view. + */ +internal class ConversationReactionDelegate(private val overlayStub: Stub) { + private val lastSeenDownPoint = PointF() + private var onReactionSelectedListener: OnReactionSelectedListener? = null + private var onActionSelectedListener: OnActionSelectedListener? = null + private var onHideListener: OnHideListener? = null + val isShowing: Boolean + get() = overlayStub.resolved() && overlayStub.get().isShowing + + fun show( + activity: Activity, + messageRecord: MessageRecord, + selectedConversationModel: SelectedConversationModel + ) { + resolveOverlay().show( + activity, + messageRecord, + lastSeenDownPoint, + selectedConversationModel + ) + } + + fun hide() { + overlayStub.get().hide() + } + + fun hideForReactWithAny() { + overlayStub.get().hideForReactWithAny() + } + + fun setOnReactionSelectedListener(onReactionSelectedListener: OnReactionSelectedListener) { + this.onReactionSelectedListener = onReactionSelectedListener + if (overlayStub.resolved()) { + overlayStub.get().setOnReactionSelectedListener(onReactionSelectedListener) + } + } + + fun setOnActionSelectedListener(onActionSelectedListener: OnActionSelectedListener) { + this.onActionSelectedListener = onActionSelectedListener + if (overlayStub.resolved()) { + overlayStub.get().setOnActionSelectedListener(onActionSelectedListener) + } + } + + fun setOnHideListener(onHideListener: OnHideListener) { + this.onHideListener = onHideListener + if (overlayStub.resolved()) { + overlayStub.get().setOnHideListener(onHideListener) + } + } + + val messageRecord: MessageRecord + get() { + check(overlayStub.resolved()) { "Cannot call getMessageRecord right now." } + return overlayStub.get().messageRecord + } + + fun applyTouchEvent(motionEvent: MotionEvent): Boolean { + return if (!overlayStub.resolved() || !overlayStub.get().isShowing) { + if (motionEvent.action == MotionEvent.ACTION_DOWN) { + lastSeenDownPoint[motionEvent.x] = motionEvent.y + } + false + } else { + overlayStub.get().applyTouchEvent(motionEvent) + } + } + + private fun resolveOverlay(): ConversationReactionOverlay { + val overlay = overlayStub.get() + overlay.requestFitSystemWindows() + overlay.setOnHideListener(onHideListener) + overlay.setOnActionSelectedListener(onActionSelectedListener) + overlay.setOnReactionSelectedListener(onReactionSelectedListener) + return overlay + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java new file mode 100644 index 0000000000..52d4e155f2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationReactionOverlay.java @@ -0,0 +1,882 @@ +package org.thoughtcrime.securesms.conversation.v2; + +import android.animation.Animator; +import android.animation.AnimatorSet; +import android.animation.ObjectAnimator; +import android.animation.ValueAnimator; +import android.app.Activity; +import android.content.Context; +import android.graphics.Bitmap; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.drawable.BitmapDrawable; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.MotionEvent; +import android.view.View; +import android.view.Window; +import android.view.animation.DecelerateInterpolator; +import android.view.animation.Interpolator; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.ConstraintLayout; +import androidx.core.content.ContextCompat; +import androidx.core.view.ViewKt; +import androidx.vectordrawable.graphics.drawable.AnimatorInflaterCompat; + +import com.annimon.stream.Stream; + +import org.session.libsession.messaging.open_groups.OpenGroup; +import org.session.libsession.utilities.TextSecurePreferences; +import org.session.libsession.utilities.ThemeUtil; +import org.session.libsession.utilities.recipients.Recipient; +import org.thoughtcrime.securesms.components.emoji.EmojiImageView; +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel; +import org.thoughtcrime.securesms.components.menu.ActionItem; +import org.thoughtcrime.securesms.conversation.v2.menus.ConversationMenuItemHelper; +import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.ReactionRecord; +import org.thoughtcrime.securesms.dependencies.DatabaseComponent; +import org.thoughtcrime.securesms.util.AnimationCompleteListener; +import org.thoughtcrime.securesms.util.DateUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import kotlin.Unit; +import network.loki.messenger.R; + +public final class ConversationReactionOverlay extends FrameLayout { + + public static final float LONG_PRESS_SCALE_FACTOR = 0.95f; + private static final Interpolator INTERPOLATOR = new DecelerateInterpolator(); + + private final Rect emojiViewGlobalRect = new Rect(); + private final Rect emojiStripViewBounds = new Rect(); + private float segmentSize; + + private final Boundary horizontalEmojiBoundary = new Boundary(); + private final Boundary verticalScrubBoundary = new Boundary(); + private final PointF deadzoneTouchPoint = new PointF(); + + private Activity activity; + private MessageRecord messageRecord; + private SelectedConversationModel selectedConversationModel; + private OverlayState overlayState = OverlayState.HIDDEN; + private RecentEmojiPageModel recentEmojiPageModel; + + private boolean downIsOurs; + private int selected = -1; + private int customEmojiIndex; + private int originalStatusBarColor; + private int originalNavigationBarColor; + + private View dropdownAnchor; + private LinearLayout conversationItem; + private View backgroundView; + private ConstraintLayout foregroundView; + private EmojiImageView[] emojiViews; + + private ConversationContextMenu contextMenu; + + private float touchDownDeadZoneSize; + private float distanceFromTouchDownPointToBottomOfScrubberDeadZone; + private int scrubberWidth; + private int selectedVerticalTranslation; + private int scrubberHorizontalMargin; + private int animationEmojiStartDelayFactor; + private int statusBarHeight; + + private OnReactionSelectedListener onReactionSelectedListener; + private OnActionSelectedListener onActionSelectedListener; + private OnHideListener onHideListener; + + private AnimatorSet revealAnimatorSet = new AnimatorSet(); + private AnimatorSet hideAnimatorSet = new AnimatorSet(); + + public ConversationReactionOverlay(@NonNull Context context) { + super(context); + } + + public ConversationReactionOverlay(@NonNull Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + } + + @Override + protected void onFinishInflate() { + super.onFinishInflate(); + + dropdownAnchor = findViewById(R.id.dropdown_anchor); + conversationItem = findViewById(R.id.conversation_item); + backgroundView = findViewById(R.id.conversation_reaction_scrubber_background); + foregroundView = findViewById(R.id.conversation_reaction_scrubber_foreground); + + emojiViews = new EmojiImageView[] { findViewById(R.id.reaction_1), + findViewById(R.id.reaction_2), + findViewById(R.id.reaction_3), + findViewById(R.id.reaction_4), + findViewById(R.id.reaction_5), + findViewById(R.id.reaction_6), + findViewById(R.id.reaction_7) }; + + customEmojiIndex = emojiViews.length - 1; + + distanceFromTouchDownPointToBottomOfScrubberDeadZone = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_scrub_deadzone_distance_from_touch_bottom); + + touchDownDeadZoneSize = getResources().getDimensionPixelSize(R.dimen.conversation_reaction_touch_deadzone_size); + scrubberWidth = getResources().getDimensionPixelOffset(R.dimen.reaction_scrubber_width); + selectedVerticalTranslation = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_vertical_translation); + scrubberHorizontalMargin = getResources().getDimensionPixelOffset(R.dimen.conversation_reaction_scrub_horizontal_margin); + + animationEmojiStartDelayFactor = getResources().getInteger(R.integer.reaction_scrubber_emoji_reveal_duration_start_delay_factor); + + initAnimators(); + } + + public void show(@NonNull Activity activity, + @NonNull MessageRecord messageRecord, + @NonNull PointF lastSeenDownPoint, + @NonNull SelectedConversationModel selectedConversationModel) + { + if (overlayState != OverlayState.HIDDEN) { + return; + } + + this.messageRecord = messageRecord; + this.selectedConversationModel = selectedConversationModel; + overlayState = OverlayState.UNINITAILIZED; + selected = -1; + recentEmojiPageModel = new RecentEmojiPageModel(activity); + + setupSelectedEmoji(); + + View statusBarBackground = activity.findViewById(android.R.id.statusBarBackground); + statusBarHeight = statusBarBackground == null ? 0 : statusBarBackground.getHeight(); + + Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap(); + + View conversationBubble = conversationItem.findViewById(R.id.conversation_item_bubble); + conversationBubble.setLayoutParams(new LinearLayout.LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight())); + conversationBubble.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot)); + TextView conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp); + conversationTimestamp.setText(DateUtils.getDisplayFormattedTimeSpanString(getContext(), Locale.getDefault(), messageRecord.getTimestamp())); + + updateConversationTimestamp(messageRecord); + + boolean isMessageOnLeft = selectedConversationModel.isOutgoing() ^ ViewUtil.isLtr(this); + + conversationItem.setScaleX(LONG_PRESS_SCALE_FACTOR); + conversationItem.setScaleY(LONG_PRESS_SCALE_FACTOR); + + setVisibility(View.INVISIBLE); + + this.activity = activity; + updateSystemUiOnShow(activity); + + ViewKt.doOnLayout(this, v -> { + showAfterLayout(messageRecord, lastSeenDownPoint, isMessageOnLeft); + return Unit.INSTANCE; + }); + } + + private void updateConversationTimestamp(MessageRecord message) { + View bubble = conversationItem.findViewById(R.id.conversation_item_bubble); + View timestamp = conversationItem.findViewById(R.id.conversation_item_timestamp); + conversationItem.removeAllViewsInLayout(); + conversationItem.addView(message.isOutgoing() ? timestamp : bubble); + conversationItem.addView(message.isOutgoing() ? bubble : timestamp); + conversationItem.requestLayout(); + } + + private void showAfterLayout(@NonNull MessageRecord messageRecord, + @NonNull PointF lastSeenDownPoint, + boolean isMessageOnLeft) { + contextMenu = new ConversationContextMenu(dropdownAnchor, getMenuActionItems(messageRecord)); + + float itemX = isMessageOnLeft ? scrubberHorizontalMargin : + selectedConversationModel.getBubbleX() - conversationItem.getWidth() + selectedConversationModel.getBubbleWidth(); + conversationItem.setX(itemX); + conversationItem.setY(selectedConversationModel.getBubbleY() - statusBarHeight); + + Bitmap conversationItemSnapshot = selectedConversationModel.getBitmap(); + boolean isWideLayout = contextMenu.getMaxWidth() + scrubberWidth < getWidth(); + + int overlayHeight = getHeight(); + int bubbleWidth = selectedConversationModel.getBubbleWidth(); + + float endX = itemX; + float endY = conversationItem.getY(); + float endApparentTop = endY; + float endScale = 1f; + + float menuPadding = DimensionUnit.DP.toPixels(12f); + float reactionBarTopPadding = DimensionUnit.DP.toPixels(32f); + int reactionBarHeight = backgroundView.getHeight(); + + float reactionBarBackgroundY; + + if (isWideLayout) { + boolean everythingFitsVertically = reactionBarHeight + menuPadding + reactionBarTopPadding + conversationItemSnapshot.getHeight() < overlayHeight; + if (everythingFitsVertically) { + boolean reactionBarFitsAboveItem = conversationItem.getY() > reactionBarHeight + menuPadding + reactionBarTopPadding; + + if (reactionBarFitsAboveItem) { + reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight; + } else { + endY = reactionBarHeight + menuPadding + reactionBarTopPadding; + reactionBarBackgroundY = reactionBarTopPadding; + } + } else { + float spaceAvailableForItem = overlayHeight - reactionBarHeight - menuPadding - reactionBarTopPadding; + + endScale = spaceAvailableForItem / conversationItem.getHeight(); + endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1); + endY = reactionBarHeight + menuPadding + reactionBarTopPadding - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale); + reactionBarBackgroundY = reactionBarTopPadding; + } + } else { + float reactionBarOffset = DimensionUnit.DP.toPixels(48); + float spaceForReactionBar = Math.max(reactionBarHeight + reactionBarOffset, 0); + boolean everythingFitsVertically = contextMenu.getMaxHeight() + conversationItemSnapshot.getHeight() + menuPadding + spaceForReactionBar < overlayHeight; + + if (everythingFitsVertically) { + float bubbleBottom = selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight(); + boolean menuFitsBelowItem = bubbleBottom + menuPadding + contextMenu.getMaxHeight() <= overlayHeight + statusBarHeight; + + if (menuFitsBelowItem) { + if (conversationItem.getY() < 0) { + endY = 0; + } + float contextMenuTop = endY + conversationItemSnapshot.getHeight(); + reactionBarBackgroundY = getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY); + + if (reactionBarBackgroundY <= reactionBarTopPadding) { + endY = backgroundView.getHeight() + menuPadding + reactionBarTopPadding; + } + } else { + endY = overlayHeight - contextMenu.getMaxHeight() - menuPadding - conversationItemSnapshot.getHeight(); + + float contextMenuTop = endY + conversationItemSnapshot.getHeight(); + reactionBarBackgroundY = getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop, menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY); + } + + endApparentTop = endY; + } else if (reactionBarOffset + reactionBarHeight + contextMenu.getMaxHeight() + menuPadding < overlayHeight) { + float spaceAvailableForItem = (float) overlayHeight - contextMenu.getMaxHeight() - menuPadding - spaceForReactionBar; + + endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight(); + endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1); + endY = spaceForReactionBar - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale); + + float contextMenuTop = endY + (conversationItemSnapshot.getHeight() * endScale); + reactionBarBackgroundY = reactionBarTopPadding;//getReactionBarOffsetForTouch(selectedConversationModel.getBubbleY(), contextMenuTop + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale), menuPadding, reactionBarOffset, reactionBarHeight, reactionBarTopPadding, endY); + endApparentTop = endY + Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale); + } else { + contextMenu.setHeight(contextMenu.getMaxHeight() / 2); + + int menuHeight = contextMenu.getHeight(); + boolean fitsVertically = menuHeight + conversationItem.getHeight() + menuPadding * 2 + reactionBarHeight + reactionBarTopPadding < overlayHeight; + + if (fitsVertically) { + float bubbleBottom = selectedConversationModel.getBubbleY() + conversationItemSnapshot.getHeight(); + boolean menuFitsBelowItem = bubbleBottom + menuPadding + menuHeight <= overlayHeight + statusBarHeight; + + if (menuFitsBelowItem) { + reactionBarBackgroundY = conversationItem.getY() - menuPadding - reactionBarHeight; + + if (reactionBarBackgroundY < reactionBarTopPadding) { + endY = reactionBarTopPadding + reactionBarHeight + menuPadding; + reactionBarBackgroundY = reactionBarTopPadding; + } + } else { + endY = overlayHeight - menuHeight - menuPadding - conversationItemSnapshot.getHeight(); + reactionBarBackgroundY = endY - reactionBarHeight - menuPadding; + } + endApparentTop = endY; + } else { + float spaceAvailableForItem = (float) overlayHeight - menuHeight - menuPadding * 2 - reactionBarHeight - reactionBarTopPadding; + + endScale = spaceAvailableForItem / conversationItemSnapshot.getHeight(); + endX += Util.halfOffsetFromScale(conversationItemSnapshot.getWidth(), endScale) * (isMessageOnLeft ? -1 : 1); + endY = reactionBarHeight - Util.halfOffsetFromScale(conversationItemSnapshot.getHeight(), endScale) + menuPadding + reactionBarTopPadding; + reactionBarBackgroundY = reactionBarTopPadding; + endApparentTop = reactionBarHeight + menuPadding + reactionBarTopPadding; + } + } + } + + reactionBarBackgroundY = Math.max(reactionBarBackgroundY, -statusBarHeight); + + hideAnimatorSet.end(); + setVisibility(View.VISIBLE); + + float scrubberX; + if (isMessageOnLeft) { + scrubberX = scrubberHorizontalMargin; + } else { + scrubberX = getWidth() - scrubberWidth - scrubberHorizontalMargin; + } + + foregroundView.setX(scrubberX); + foregroundView.setY(reactionBarBackgroundY + reactionBarHeight / 2f - foregroundView.getHeight() / 2f); + + backgroundView.setX(scrubberX); + backgroundView.setY(reactionBarBackgroundY); + + verticalScrubBoundary.update(reactionBarBackgroundY, + lastSeenDownPoint.y + distanceFromTouchDownPointToBottomOfScrubberDeadZone); + + updateBoundsOnLayoutChanged(); + + revealAnimatorSet.start(); + + if (isWideLayout) { + float scrubberRight = scrubberX + scrubberWidth; + float offsetX = isMessageOnLeft ? scrubberRight + menuPadding : scrubberX - contextMenu.getMaxWidth() - menuPadding; + contextMenu.show((int) offsetX, (int) Math.min(backgroundView.getY(), overlayHeight - contextMenu.getMaxHeight())); + } else { + float contentX = isMessageOnLeft ? scrubberHorizontalMargin : selectedConversationModel.getBubbleX(); + float offsetX = isMessageOnLeft ? contentX : -contextMenu.getMaxWidth() + contentX + bubbleWidth; + + float menuTop = endApparentTop + (conversationItemSnapshot.getHeight() * endScale); + contextMenu.show((int) offsetX, (int) (menuTop + menuPadding)); + } + + int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration); + + conversationItem.animate() + .x(endX) + .y(endY) + .scaleX(endScale) + .scaleY(endScale) + .setDuration(revealDuration); + } + + private float getReactionBarOffsetForTouch(float itemY, + float contextMenuTop, + float contextMenuPadding, + float reactionBarOffset, + int reactionBarHeight, + float spaceNeededBetweenTopOfScreenAndTopOfReactionBar, + float messageTop) + { + float adjustedTouchY = itemY - statusBarHeight; + float reactionStartingPoint = Math.min(adjustedTouchY, contextMenuTop); + + float spaceBetweenTopOfMessageAndTopOfContextMenu = Math.abs(messageTop - contextMenuTop); + + if (spaceBetweenTopOfMessageAndTopOfContextMenu < DimensionUnit.DP.toPixels(150)) { + float offsetToMakeReactionBarOffsetMatchMenuPadding = reactionBarOffset - contextMenuPadding; + reactionStartingPoint = messageTop + offsetToMakeReactionBarOffsetMatchMenuPadding; + } + + return Math.max(reactionStartingPoint - reactionBarOffset - reactionBarHeight, spaceNeededBetweenTopOfScreenAndTopOfReactionBar); + } + + private void updateSystemUiOnShow(@NonNull Activity activity) { + Window window = activity.getWindow(); + int barColor = ContextCompat.getColor(getContext(), R.color.reactions_screen_dark_shade_color); + + originalStatusBarColor = window.getStatusBarColor(); + WindowUtil.setStatusBarColor(window, barColor); + + originalNavigationBarColor = window.getNavigationBarColor(); + WindowUtil.setNavigationBarColor(window, barColor); + + if (!ThemeUtil.isDarkTheme(getContext())) { + WindowUtil.clearLightStatusBar(window); + WindowUtil.clearLightNavigationBar(window); + } + } + + public void hide() { + hideInternal(onHideListener); + } + + public void hideForReactWithAny() { + hideInternal(onHideListener); + } + + private void hideInternal(@Nullable OnHideListener onHideListener) { + overlayState = OverlayState.HIDDEN; + + AnimatorSet animatorSet = newHideAnimatorSet(); + hideAnimatorSet = animatorSet; + + revealAnimatorSet.end(); + animatorSet.start(); + + if (onHideListener != null) { + onHideListener.startHide(); + } + + if (selectedConversationModel.getFocusedView() != null) { + ViewUtil.focusAndShowKeyboard(selectedConversationModel.getFocusedView()); + } + + animatorSet.addListener(new AnimationCompleteListener() { + @Override public void onAnimationEnd(Animator animation) { + animatorSet.removeListener(this); + + if (onHideListener != null) { + onHideListener.onHide(); + } + } + }); + + if (contextMenu != null) { + contextMenu.dismiss(); + } + } + + public boolean isShowing() { + return overlayState != OverlayState.HIDDEN; + } + + public @NonNull MessageRecord getMessageRecord() { + return messageRecord; + } + + @Override + protected void onLayout(boolean changed, int l, int t, int r, int b) { + super.onLayout(changed, l, t, r, b); + + updateBoundsOnLayoutChanged(); + } + + private void updateBoundsOnLayoutChanged() { + backgroundView.getGlobalVisibleRect(emojiStripViewBounds); + emojiViews[0].getGlobalVisibleRect(emojiViewGlobalRect); + emojiStripViewBounds.left = getStart(emojiViewGlobalRect); + emojiViews[emojiViews.length - 1].getGlobalVisibleRect(emojiViewGlobalRect); + emojiStripViewBounds.right = getEnd(emojiViewGlobalRect); + + segmentSize = emojiStripViewBounds.width() / (float) emojiViews.length; + } + + private int getStart(@NonNull Rect rect) { + if (ViewUtil.isLtr(this)) { + return rect.left; + } else { + return rect.right; + } + } + + private int getEnd(@NonNull Rect rect) { + if (ViewUtil.isLtr(this)) { + return rect.right; + } else { + return rect.left; + } + } + + public boolean applyTouchEvent(@NonNull MotionEvent motionEvent) { + if (!isShowing()) { + throw new IllegalStateException("Touch events should only be propagated to this method if we are displaying the scrubber."); + } + + if ((motionEvent.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) != 0) { + return true; + } + + if (overlayState == OverlayState.UNINITAILIZED) { + downIsOurs = false; + + deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY()); + + overlayState = OverlayState.DEADZONE; + } + + if (overlayState == OverlayState.DEADZONE) { + float deltaX = Math.abs(deadzoneTouchPoint.x - motionEvent.getX()); + float deltaY = Math.abs(deadzoneTouchPoint.y - motionEvent.getY()); + + if (deltaX > touchDownDeadZoneSize || deltaY > touchDownDeadZoneSize) { + overlayState = OverlayState.SCRUB; + } else { + if (motionEvent.getAction() == MotionEvent.ACTION_UP) { + overlayState = OverlayState.TAP; + + if (downIsOurs) { + handleUpEvent(); + return true; + } + } + + return MotionEvent.ACTION_MOVE == motionEvent.getAction(); + } + } + + switch (motionEvent.getAction()) { + case MotionEvent.ACTION_DOWN: + selected = getSelectedIndexViaDownEvent(motionEvent); + + deadzoneTouchPoint.set(motionEvent.getX(), motionEvent.getY()); + overlayState = OverlayState.DEADZONE; + downIsOurs = true; + return true; + case MotionEvent.ACTION_MOVE: + selected = getSelectedIndexViaMoveEvent(motionEvent); + return true; + case MotionEvent.ACTION_UP: + handleUpEvent(); + return downIsOurs; + case MotionEvent.ACTION_CANCEL: + hide(); + return downIsOurs; + default: + return false; + } + } + + private void setupSelectedEmoji() { + final List emojis = recentEmojiPageModel.getEmoji(); + + for (int i = 0; i < emojiViews.length; i++) { + final EmojiImageView view = emojiViews[i]; + + view.setScaleX(1.0f); + view.setScaleY(1.0f); + view.setTranslationY(0); + + boolean isAtCustomIndex = i == customEmojiIndex; + + if (isAtCustomIndex) { + view.setImageDrawable(ContextCompat.getDrawable(getContext(), R.drawable.ic_baseline_add_24)); + view.setTag(null); + } else { + view.setImageEmoji(emojis.get(i)); + } + } + } + + private int getSelectedIndexViaDownEvent(@NonNull MotionEvent motionEvent) { + return getSelectedIndexViaMotionEvent(motionEvent, new Boundary(emojiStripViewBounds.top, emojiStripViewBounds.bottom)); + } + + private int getSelectedIndexViaMoveEvent(@NonNull MotionEvent motionEvent) { + return getSelectedIndexViaMotionEvent(motionEvent, verticalScrubBoundary); + } + + private int getSelectedIndexViaMotionEvent(@NonNull MotionEvent motionEvent, @NonNull Boundary boundary) { + int selected = -1; + + if (backgroundView.getVisibility() != View.VISIBLE) { + return selected; + } + + for (int i = 0; i < emojiViews.length; i++) { + final float emojiLeft = (segmentSize * i) + emojiStripViewBounds.left; + horizontalEmojiBoundary.update(emojiLeft, emojiLeft + segmentSize); + + if (horizontalEmojiBoundary.contains(motionEvent.getX()) && boundary.contains(motionEvent.getY())) { + selected = i; + } + } + + if (this.selected != -1 && this.selected != selected) { + shrinkView(emojiViews[this.selected]); + } + + if (this.selected != selected && selected != -1) { + growView(emojiViews[selected]); + } + + return selected; + } + + private void growView(@NonNull View view) { + view.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP); + view.animate() + .scaleY(1.5f) + .scaleX(1.5f) + .translationY(-selectedVerticalTranslation) + .setDuration(200) + .setInterpolator(INTERPOLATOR) + .start(); + } + + private void shrinkView(@NonNull View view) { + view.animate() + .scaleX(1.0f) + .scaleY(1.0f) + .translationY(0) + .setDuration(200) + .setInterpolator(INTERPOLATOR) + .start(); + } + + private void handleUpEvent() { + if (selected != -1 && onReactionSelectedListener != null && backgroundView.getVisibility() == View.VISIBLE) { + if (selected == customEmojiIndex) { + onReactionSelectedListener.onCustomReactionSelected(messageRecord, emojiViews[selected].getTag() != null); + } else { + onReactionSelectedListener.onReactionSelected(messageRecord, recentEmojiPageModel.getEmoji().get(selected)); + } + } else { + hide(); + } + } + + public void setOnReactionSelectedListener(@Nullable OnReactionSelectedListener onReactionSelectedListener) { + this.onReactionSelectedListener = onReactionSelectedListener; + } + + public void setOnActionSelectedListener(@Nullable OnActionSelectedListener onActionSelectedListener) { + this.onActionSelectedListener = onActionSelectedListener; + } + + public void setOnHideListener(@Nullable OnHideListener onHideListener) { + this.onHideListener = onHideListener; + } + + private @Nullable String getOldEmoji(@NonNull MessageRecord messageRecord) { + return Stream.of(messageRecord.getReactions()) + .filter(record -> record.getAuthor().equals(TextSecurePreferences.getLocalNumber(getContext()))) + .findFirst() + .map(ReactionRecord::getEmoji) + .orElse(null); + } + + private @NonNull List getMenuActionItems(@NonNull MessageRecord message) { + List items = new ArrayList<>(); + + // Prepare + boolean containsControlMessage = message.isUpdate(); + boolean hasText = !message.getBody().isEmpty(); + OpenGroup openGroup = DatabaseComponent.get(getContext()).lokiThreadDatabase().getOpenGroupChat(message.getThreadId()); + Recipient recipient = DatabaseComponent.get(getContext()).threadDatabase().getRecipientForThreadId(message.getThreadId()); + String userPublicKey = TextSecurePreferences.getLocalNumber(getContext()); + // Select message + items.add(new ActionItem(R.attr.menu_select_icon, getContext().getResources().getString(R.string.conversation_context__menu_select), () -> handleActionItemClicked(Action.SELECT))); + // Reply + if (!message.isPending() && !message.isFailed()) { + items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_reply), () -> handleActionItemClicked(Action.REPLY))); + } + // Copy message text + if (!containsControlMessage && hasText) { + items.add(new ActionItem(R.attr.menu_copy_icon, getContext().getResources().getString(R.string.copy), () -> handleActionItemClicked(Action.COPY_MESSAGE))); + } + // Copy Session ID + if (recipient.isGroupRecipient() && !recipient.isOpenGroupRecipient() && !message.getRecipient().getAddress().toString().equals(userPublicKey)) { + items.add(new ActionItem(R.attr.menu_copy_icon, getContext().getResources().getString(R.string.activity_conversation_menu_copy_session_id), () -> handleActionItemClicked(Action.COPY_SESSION_ID))); + } + // Delete message + if (ConversationMenuItemHelper.userCanDeleteSelectedItems(getContext(), message, openGroup, userPublicKey)) { + items.add(new ActionItem(R.attr.menu_trash_icon, getContext().getResources().getString(R.string.delete), () -> handleActionItemClicked(Action.DELETE))); + } + // Ban user + if (ConversationMenuItemHelper.userCanBanSelectedUsers(getContext(), message, openGroup, userPublicKey)) { + items.add(new ActionItem(0, getContext().getResources().getString(R.string.conversation_context__menu_ban_user), () -> handleActionItemClicked(Action.BAN_USER))); + } + // Ban and delete all + if (ConversationMenuItemHelper.userCanBanSelectedUsers(getContext(), message, openGroup, userPublicKey)) { + items.add(new ActionItem(0, getContext().getResources().getString(R.string.conversation_context__menu_ban_and_delete_all), () -> handleActionItemClicked(Action.BAN_AND_DELETE_ALL))); + } + // Message detail + if (message.isFailed()) { + items.add(new ActionItem(R.attr.menu_info_icon, getContext().getResources().getString(R.string.conversation_context__menu_message_details), () -> handleActionItemClicked(Action.VIEW_INFO))); + } + // Resend + if (message.isFailed()) { + items.add(new ActionItem(R.attr.menu_reply_icon, getContext().getResources().getString(R.string.conversation_context__menu_resend_message), () -> handleActionItemClicked(Action.RESEND))); + } + // Save media + if (message.isMms() && ((MediaMmsMessageRecord)message).containsMediaSlide()) { + items.add(new ActionItem(R.attr.menu_save_icon, getContext().getResources().getString(R.string.conversation_context_image__save_attachment), () -> handleActionItemClicked(Action.DOWNLOAD))); + } + + backgroundView.setVisibility(View.VISIBLE); + foregroundView.setVisibility(View.VISIBLE); + + return items; + } + + private void handleActionItemClicked(@NonNull Action action) { + hideInternal(new OnHideListener() { + @Override public void startHide() { + if (onHideListener != null) { + onHideListener.startHide(); + } + } + + @Override public void onHide() { + if (onHideListener != null) { + onHideListener.onHide(); + } + + if (onActionSelectedListener != null) { + onActionSelectedListener.onActionSelected(action); + } + } + }); + } + + private void initAnimators() { + + int revealDuration = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_duration); + int revealOffset = getContext().getResources().getInteger(R.integer.reaction_scrubber_reveal_offset); + + List reveals = Stream.of(emojiViews) + .mapIndexed((idx, v) -> { + Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_reveal); + anim.setTarget(v); + anim.setStartDelay(idx * animationEmojiStartDelayFactor); + return anim; + }) + .toList(); + + Animator backgroundRevealAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_in); + backgroundRevealAnim.setTarget(backgroundView); + backgroundRevealAnim.setDuration(revealDuration); + backgroundRevealAnim.setStartDelay(revealOffset); + reveals.add(backgroundRevealAnim); + + revealAnimatorSet.setInterpolator(INTERPOLATOR); + revealAnimatorSet.playTogether(reveals); + } + + private @NonNull AnimatorSet newHideAnimatorSet() { + AnimatorSet set = new AnimatorSet(); + + set.addListener(new AnimationCompleteListener() { + @Override + public void onAnimationEnd(Animator animation) { + setVisibility(View.GONE); + } + }); + set.setInterpolator(INTERPOLATOR); + + set.playTogether(newHideAnimators()); + + return set; + } + + private @NonNull List newHideAnimators() { + int duration = getContext().getResources().getInteger(R.integer.reaction_scrubber_hide_duration); + + List animators = new ArrayList<>(Stream.of(emojiViews) + .mapIndexed((idx, v) -> { + Animator anim = AnimatorInflaterCompat.loadAnimator(getContext(), R.animator.reactions_scrubber_hide); + anim.setTarget(v); + return anim; + }) + .toList()); + + Animator backgroundHideAnim = AnimatorInflaterCompat.loadAnimator(getContext(), android.R.animator.fade_out); + backgroundHideAnim.setTarget(backgroundView); + backgroundHideAnim.setDuration(duration); + animators.add(backgroundHideAnim); + + ObjectAnimator itemScaleXAnim = new ObjectAnimator(); + itemScaleXAnim.setProperty(View.SCALE_X); + itemScaleXAnim.setFloatValues(1f); + itemScaleXAnim.setTarget(conversationItem); + itemScaleXAnim.setDuration(duration); + animators.add(itemScaleXAnim); + + ObjectAnimator itemScaleYAnim = new ObjectAnimator(); + itemScaleYAnim.setProperty(View.SCALE_Y); + itemScaleYAnim.setFloatValues(1f); + itemScaleYAnim.setTarget(conversationItem); + itemScaleYAnim.setDuration(duration); + animators.add(itemScaleYAnim); + + ObjectAnimator itemXAnim = new ObjectAnimator(); + itemXAnim.setProperty(View.X); + itemXAnim.setFloatValues(selectedConversationModel.getBubbleX()); + itemXAnim.setTarget(conversationItem); + itemXAnim.setDuration(duration); + animators.add(itemXAnim); + + ObjectAnimator itemYAnim = new ObjectAnimator(); + itemYAnim.setProperty(View.Y); + itemYAnim.setFloatValues(selectedConversationModel.getBubbleY() - statusBarHeight); + itemYAnim.setTarget(conversationItem); + itemYAnim.setDuration(duration); + animators.add(itemYAnim); + + if (activity != null) { + ValueAnimator statusBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalStatusBarColor); + statusBarAnim.setDuration(duration); + statusBarAnim.addUpdateListener(animation -> { + WindowUtil.setStatusBarColor(activity.getWindow(), (int) animation.getAnimatedValue()); + }); + animators.add(statusBarAnim); + + ValueAnimator navigationBarAnim = ValueAnimator.ofArgb(activity.getWindow().getStatusBarColor(), originalNavigationBarColor); + navigationBarAnim.setDuration(duration); + navigationBarAnim.addUpdateListener(animation -> { + WindowUtil.setNavigationBarColor(activity.getWindow(), (int) animation.getAnimatedValue()); + }); + animators.add(navigationBarAnim); + } + + return animators; + } + + public interface OnHideListener { + void startHide(); + void onHide(); + } + + public interface OnReactionSelectedListener { + void onReactionSelected(@NonNull MessageRecord messageRecord, String emoji); + void onCustomReactionSelected(@NonNull MessageRecord messageRecord, boolean hasAddedCustomEmoji); + } + + public interface OnActionSelectedListener { + void onActionSelected(@NonNull Action action); + } + + private static class Boundary { + private float min; + private float max; + + Boundary() {} + + Boundary(float min, float max) { + update(min, max); + } + + private void update(float min, float max) { + this.min = min; + this.max = max; + } + + public boolean contains(float value) { + if (min < max) { + return this.min < value && this.max > value; + } else { + return this.min > value && this.max < value; + } + } + } + + private enum OverlayState { + HIDDEN, + UNINITAILIZED, + DEADZONE, + SCRUB, + TAP + } + + public enum Action { + REPLY, + RESEND, + DOWNLOAD, + COPY_MESSAGE, + COPY_SESSION_ID, + VIEW_INFO, + SELECT, + DELETE, + BAN_USER, + BAN_AND_DELETE_ALL, + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt index e86fd935d3..01db225cd6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModel.kt @@ -3,21 +3,29 @@ package org.thoughtcrime.securesms.conversation.v2 import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.viewModelScope +import com.goterl.lazysodium.utils.KeyPair import dagger.assisted.Assisted import dagger.assisted.AssistedInject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch +import org.session.libsession.messaging.open_groups.OpenGroup +import org.session.libsession.messaging.utilities.SessionId +import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.repository.ConversationRepository import java.util.UUID class ConversationViewModel( val threadId: Long, - private val repository: ConversationRepository + val edKeyPair: KeyPair?, + private val repository: ConversationRepository, + private val storage: Storage ) : ViewModel() { private val _uiState = MutableStateFlow(ConversationUiState()) @@ -26,6 +34,18 @@ class ConversationViewModel( val recipient: Recipient? get() = repository.maybeGetRecipientForThreadId(threadId) + val openGroup: OpenGroup? + get() = storage.getOpenGroup(threadId) + + val serverCapabilities: List + get() = openGroup?.let { storage.getServerCapabilities(it.server) } ?: listOf() + + val blindedPublicKey: String? + get() = if (openGroup == null || edKeyPair == null) null else { + SodiumUtilities.blindedKeyPair(openGroup!!.publicKey, edKeyPair)?.publicKey?.asBytes + ?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString + } + init { _uiState.update { it.copy(isOxenHostedOpenGroup = repository.isOxenHostedOpenGroup(threadId)) @@ -137,17 +157,19 @@ class ConversationViewModel( @dagger.assisted.AssistedFactory interface AssistedFactory { - fun create(threadId: Long): Factory + fun create(threadId: Long, edKeyPair: KeyPair?): Factory } @Suppress("UNCHECKED_CAST") class Factory @AssistedInject constructor( @Assisted private val threadId: Long, - private val repository: ConversationRepository + @Assisted private val edKeyPair: KeyPair?, + private val repository: ConversationRepository, + private val storage: Storage ) : ViewModelProvider.Factory { override fun create(modelClass: Class): T { - return ConversationViewModel(threadId, repository) as T + return ConversationViewModel(threadId, edKeyPair, repository, storage) as T } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DimensionUnit.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DimensionUnit.java new file mode 100644 index 0000000000..c057f126ae --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DimensionUnit.java @@ -0,0 +1,73 @@ +package org.thoughtcrime.securesms.conversation.v2; + +import android.content.res.Resources; + +import androidx.annotation.Dimension; +import androidx.annotation.Px; + +/** + * Core utility for converting different dimensional values. + */ +public enum DimensionUnit { + PIXELS { + @Override + @Px + public float toPixels(@Px float pixels) { + return pixels; + } + + @Override + @Dimension(unit = Dimension.DP) + public float toDp(@Px float pixels) { + return pixels / Resources.getSystem().getDisplayMetrics().density; + } + + @Override + @Dimension(unit = Dimension.SP) + public float toSp(@Px float pixels) { + return pixels / Resources.getSystem().getDisplayMetrics().scaledDensity; + } + }, + DP { + @Override + @Px + public float toPixels(@Dimension(unit = Dimension.DP) float dp) { + return dp * Resources.getSystem().getDisplayMetrics().density; + } + + @Override + @Dimension(unit = Dimension.DP) + public float toDp(@Dimension(unit = Dimension.DP) float dp) { + return dp; + } + + @Override + @Dimension(unit = Dimension.SP) + public float toSp(@Dimension(unit = Dimension.DP) float dp) { + return PIXELS.toSp(toPixels(dp)); + } + }, + SP { + @Override + @Px + public float toPixels(@Dimension(unit = Dimension.SP) float sp) { + return sp * Resources.getSystem().getDisplayMetrics().scaledDensity; + } + + @Override + @Dimension(unit = Dimension.DP) + public float toDp(@Dimension(unit = Dimension.SP) float sp) { + return PIXELS.toDp(toPixels(sp)); + } + + @Override + @Dimension(unit = Dimension.SP) + public float toSp(@Dimension(unit = Dimension.SP) float sp) { + return sp; + } + }; + + public abstract float toPixels(float value); + public abstract float toDp(float value); + public abstract float toSp(float value); +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/NoCrossfadeChangeDefaultAnimator.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/NoCrossfadeChangeDefaultAnimator.java new file mode 100644 index 0000000000..a5d6e7fb59 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/NoCrossfadeChangeDefaultAnimator.java @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.conversation.v2; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.List; + +/** + * Disable animations for changes to same item + */ +public class NoCrossfadeChangeDefaultAnimator extends DefaultItemAnimator { + @Override + public boolean animateChange(RecyclerView.ViewHolder oldHolder, RecyclerView.ViewHolder newHolder, int fromX, int fromY, int toX, int toY) { + if (oldHolder == newHolder) { + if (oldHolder != null) { + dispatchChangeFinished(oldHolder, true); + } + } else { + if (oldHolder != null) { + dispatchChangeFinished(oldHolder, true); + } + if (newHolder != null) { + dispatchChangeFinished(newHolder, false); + } + } + return false; + } + + @Override + public boolean canReuseUpdatedViewHolder(@NonNull RecyclerView.ViewHolder viewHolder, @NonNull List payloads) { + return true; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/SelectedConversationModel.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/SelectedConversationModel.kt new file mode 100644 index 0000000000..5e2bb4ec8a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/SelectedConversationModel.kt @@ -0,0 +1,17 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import android.graphics.Bitmap +import android.view.View + +/** + * Contains information on a single selected conversation item. This is used when transitioning + * between selected and unselected states. + */ +data class SelectedConversationModel( + val bitmap: Bitmap, + val bubbleX: Float, + val bubbleY: Float, + val bubbleWidth: Int, + val isOutgoing: Boolean, + val focusedView: View?, +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.java new file mode 100644 index 0000000000..e81f06d0ee --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/Util.java @@ -0,0 +1,381 @@ +/* + * Copyright (C) 2011 Whisper Systems + * + * 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.thoughtcrime.securesms.conversation.v2; + +import android.annotation.TargetApi; +import android.app.ActivityManager; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.graphics.Typeface; +import android.net.Uri; +import android.os.Build.VERSION; +import android.os.Build.VERSION_CODES; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.StyleSpan; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.annimon.stream.Stream; +import com.google.android.mms.pdu_alt.CharacterSets; +import com.google.android.mms.pdu_alt.EncodedStringValue; + +import org.session.libsignal.utilities.Log; +import org.thoughtcrime.securesms.components.ComposeText; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import network.loki.messenger.R; + +public class Util { + private static final String TAG = Log.tag(Util.class); + + private static final long BUILD_LIFESPAN = TimeUnit.DAYS.toMillis(90); + + public static List asList(T... elements) { + List result = new LinkedList<>(); + Collections.addAll(result, elements); + return result; + } + + public static String join(String[] list, String delimiter) { + return join(Arrays.asList(list), delimiter); + } + + public static String join(Collection list, String delimiter) { + StringBuilder result = new StringBuilder(); + int i = 0; + + for (T item : list) { + result.append(item); + + if (++i < list.size()) + result.append(delimiter); + } + + return result.toString(); + } + + public static String join(long[] list, String delimeter) { + List boxed = new ArrayList<>(list.length); + + for (int i = 0; i < list.length; i++) { + boxed.add(list[i]); + } + + return join(boxed, delimeter); + } + + @SafeVarargs + public static @NonNull List join(@NonNull List... lists) { + int totalSize = Stream.of(lists).reduce(0, (sum, list) -> sum + list.size()); + List joined = new ArrayList<>(totalSize); + + for (List list : lists) { + joined.addAll(list); + } + + return joined; + } + + public static String join(List list, String delimeter) { + StringBuilder sb = new StringBuilder(); + + for (int j = 0; j < list.size(); j++) { + if (j != 0) sb.append(delimeter); + sb.append(list.get(j)); + } + + return sb.toString(); + } + + public static String rightPad(String value, int length) { + if (value.length() >= length) { + return value; + } + + StringBuilder out = new StringBuilder(value); + while (out.length() < length) { + out.append(" "); + } + + return out.toString(); + } + + public static boolean isEmpty(EncodedStringValue[] value) { + return value == null || value.length == 0; + } + + public static boolean isEmpty(ComposeText value) { + return value == null || value.getText() == null || TextUtils.isEmpty(value.getTextTrimmed()); + } + + public static boolean isEmpty(Collection collection) { + return collection == null || collection.isEmpty(); + } + + public static boolean isEmpty(@Nullable CharSequence charSequence) { + return charSequence == null || charSequence.length() == 0; + } + + public static boolean hasItems(@Nullable Collection collection) { + return collection != null && !collection.isEmpty(); + } + + public static V getOrDefault(@NonNull Map map, K key, V defaultValue) { + return map.containsKey(key) ? map.get(key) : defaultValue; + } + + public static String getFirstNonEmpty(String... values) { + for (String value : values) { + if (!Util.isEmpty(value)) { + return value; + } + } + return ""; + } + + public static @NonNull String emptyIfNull(@Nullable String value) { + return value != null ? value : ""; + } + + public static @NonNull CharSequence emptyIfNull(@Nullable CharSequence value) { + return value != null ? value : ""; + } + + public static CharSequence getBoldedString(String value) { + SpannableString spanned = new SpannableString(value); + spanned.setSpan(new StyleSpan(Typeface.BOLD), 0, + spanned.length(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + return spanned; + } + + public static @NonNull String toIsoString(byte[] bytes) { + try { + return new String(bytes, CharacterSets.MIMENAME_ISO_8859_1); + } catch (UnsupportedEncodingException e) { + throw new AssertionError("ISO_8859_1 must be supported!"); + } + } + + public static byte[] toIsoBytes(String isoString) { + try { + return isoString.getBytes(CharacterSets.MIMENAME_ISO_8859_1); + } catch (UnsupportedEncodingException e) { + throw new AssertionError("ISO_8859_1 must be supported!"); + } + } + + public static byte[] toUtf8Bytes(String utf8String) { + try { + return utf8String.getBytes(CharacterSets.MIMENAME_UTF_8); + } catch (UnsupportedEncodingException e) { + throw new AssertionError("UTF_8 must be supported!"); + } + } + + public static void wait(Object lock, long timeout) { + try { + lock.wait(timeout); + } catch (InterruptedException ie) { + throw new AssertionError(ie); + } + } + + public static List split(String source, String delimiter) { + List results = new LinkedList<>(); + + if (TextUtils.isEmpty(source)) { + return results; + } + + String[] elements = source.split(delimiter); + Collections.addAll(results, elements); + + return results; + } + + public static byte[][] split(byte[] input, int firstLength, int secondLength) { + byte[][] parts = new byte[2][]; + + parts[0] = new byte[firstLength]; + System.arraycopy(input, 0, parts[0], 0, firstLength); + + parts[1] = new byte[secondLength]; + System.arraycopy(input, firstLength, parts[1], 0, secondLength); + + return parts; + } + + public static byte[] combine(byte[]... elements) { + try { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + + for (byte[] element : elements) { + baos.write(element); + } + + return baos.toByteArray(); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + public static byte[] trim(byte[] input, int length) { + byte[] result = new byte[length]; + System.arraycopy(input, 0, result, 0, result.length); + + return result; + } + + public static byte[] getSecretBytes(int size) { + return getSecretBytes(new SecureRandom(), size); + } + + public static byte[] getSecretBytes(@NonNull SecureRandom secureRandom, int size) { + byte[] secret = new byte[size]; + secureRandom.nextBytes(secret); + return secret; + } + + public static T getRandomElement(T[] elements) { + return elements[new SecureRandom().nextInt(elements.length)]; + } + + public static T getRandomElement(List elements) { + return elements.get(new SecureRandom().nextInt(elements.size())); + } + + public static boolean equals(@Nullable Object a, @Nullable Object b) { + return a == b || (a != null && a.equals(b)); + } + + public static int hashCode(@Nullable Object... objects) { + return Arrays.hashCode(objects); + } + + public static @Nullable Uri uri(@Nullable String uri) { + if (uri == null) return null; + else return Uri.parse(uri); + } + + @TargetApi(VERSION_CODES.KITKAT) + public static boolean isLowMemory(Context context) { + ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); + + return (VERSION.SDK_INT >= VERSION_CODES.KITKAT && activityManager.isLowRamDevice()) || + activityManager.getLargeMemoryClass() <= 64; + } + + public static int clamp(int value, int min, int max) { + return Math.min(Math.max(value, min), max); + } + + public static long clamp(long value, long min, long max) { + return Math.min(Math.max(value, min), max); + } + + public static float clamp(float value, float min, float max) { + return Math.min(Math.max(value, min), max); + } + + /** + * Returns half of the difference between the given length, and the length when scaled by the + * given scale. + */ + public static float halfOffsetFromScale(int length, float scale) { + float scaledLength = length * scale; + return (length - scaledLength) / 2; + } + + public static @Nullable String readTextFromClipboard(@NonNull Context context) { + { + ClipboardManager clipboardManager = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE); + + if (clipboardManager.hasPrimaryClip() && clipboardManager.getPrimaryClip().getItemCount() > 0) { + return clipboardManager.getPrimaryClip().getItemAt(0).getText().toString(); + } else { + return null; + } + } + } + + public static void writeTextToClipboard(@NonNull Context context, @NonNull String text) { + writeTextToClipboard(context, context.getString(R.string.app_name), text); + } + + public static void writeTextToClipboard(@NonNull Context context, @NonNull String label, @NonNull String text) { + ClipboardManager clipboard = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(label, text); + clipboard.setPrimaryClip(clip); + } + + public static int toIntExact(long value) { + if ((int)value != value) { + throw new ArithmeticException("integer overflow"); + } + return (int)value; + } + + public static boolean isEquals(@Nullable Long first, long second) { + return first != null && first == second; + } + + @SafeVarargs + public static List concatenatedList(Collection ... items) { + final List concat = new ArrayList<>(Stream.of(items).reduce(0, (sum, list) -> sum + list.size())); + + for (Collection list : items) { + concat.addAll(list); + } + + return concat; + } + + public static boolean isLong(String value) { + try { + Long.parseLong(value); + return true; + } catch (NumberFormatException e) { + return false; + } + } + + public static int parseInt(String integer, int defaultValue) { + try { + return Integer.parseInt(integer); + } catch (NumberFormatException e) { + return defaultValue; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ViewUtil.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ViewUtil.java new file mode 100644 index 0000000000..814090b036 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ViewUtil.java @@ -0,0 +1,380 @@ +/** + * Copyright (C) 2015 Open Whisper Systems + * + * 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.thoughtcrime.securesms.conversation.v2; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Context; +import android.content.res.Resources; +import android.util.TypedValue; +import android.view.Gravity; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.ViewStub; +import android.view.ViewTreeObserver; +import android.view.animation.AlphaAnimation; +import android.view.animation.Animation; +import android.view.inputmethod.InputMethodManager; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.annotation.IdRes; +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.view.ContextThemeWrapper; +import androidx.interpolator.view.animation.FastOutSlowInInterpolator; +import androidx.lifecycle.Lifecycle; + +import org.session.libsession.utilities.ServiceUtil; +import org.session.libsession.utilities.Stub; +import org.session.libsignal.utilities.ListenableFuture; +import org.session.libsignal.utilities.SettableFuture; + +public final class ViewUtil { + + private ViewUtil() { + } + + public static void focusAndMoveCursorToEndAndOpenKeyboard(@NonNull EditText input) { + int numberLength = input.getText().length(); + input.setSelection(numberLength, numberLength); + + focusAndShowKeyboard(input); + } + + public static void focusAndShowKeyboard(@NonNull View view) { + view.requestFocus(); + if (view.hasWindowFocus()) { + showTheKeyboardNow(view); + } else { + view.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() { + @Override + public void onWindowFocusChanged(boolean hasFocus) { + if (hasFocus) { + showTheKeyboardNow(view); + view.getViewTreeObserver().removeOnWindowFocusChangeListener(this); + } + } + }); + } + } + + private static void showTheKeyboardNow(@NonNull View view) { + if (view.isFocused()) { + view.post(() -> { + InputMethodManager inputMethodManager = ServiceUtil.getInputMethodManager(view.getContext()); + inputMethodManager.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT); + }); + } + } + + @SuppressWarnings("unchecked") + public static T inflateStub(@NonNull View parent, @IdRes int stubId) { + return (T)((ViewStub)parent.findViewById(stubId)).inflate(); + } + + public static Stub findStubById(@NonNull Activity parent, @IdRes int resId) { + return new Stub<>(parent.findViewById(resId)); + } + + public static Stub findStubById(@NonNull View parent, @IdRes int resId) { + return new Stub<>(parent.findViewById(resId)); + } + + private static Animation getAlphaAnimation(float from, float to, int duration) { + final Animation anim = new AlphaAnimation(from, to); + anim.setInterpolator(new FastOutSlowInInterpolator()); + anim.setDuration(duration); + return anim; + } + + public static void fadeIn(final @NonNull View view, final int duration) { + animateIn(view, getAlphaAnimation(0f, 1f, duration)); + } + + public static ListenableFuture fadeOut(final @NonNull View view, final int duration) { + return fadeOut(view, duration, View.GONE); + } + + public static ListenableFuture fadeOut(@NonNull View view, int duration, int visibility) { + return animateOut(view, getAlphaAnimation(1f, 0f, duration), visibility); + } + + public static ListenableFuture animateOut(final @NonNull View view, final @NonNull Animation animation) { + return animateOut(view, animation, View.GONE); + } + + public static ListenableFuture animateOut(final @NonNull View view, final @NonNull Animation animation, final int visibility) { + final SettableFuture future = new SettableFuture(); + if (view.getVisibility() == visibility) { + future.set(true); + } else { + view.clearAnimation(); + animation.reset(); + animation.setStartTime(0); + animation.setAnimationListener(new Animation.AnimationListener() { + @Override + public void onAnimationStart(Animation animation) {} + + @Override + public void onAnimationRepeat(Animation animation) {} + + @Override + public void onAnimationEnd(Animation animation) { + view.setVisibility(visibility); + future.set(true); + } + }); + view.startAnimation(animation); + } + return future; + } + + public static void animateIn(final @NonNull View view, final @NonNull Animation animation) { + if (view.getVisibility() == View.VISIBLE) return; + + view.clearAnimation(); + animation.reset(); + animation.setStartTime(0); + view.setVisibility(View.VISIBLE); + view.startAnimation(animation); + } + + @SuppressWarnings("unchecked") + public static T inflate(@NonNull LayoutInflater inflater, + @NonNull ViewGroup parent, + @LayoutRes int layoutResId) + { + return (T)(inflater.inflate(layoutResId, parent, false)); + } + + @SuppressLint("RtlHardcoded") + public static void setTextViewGravityStart(final @NonNull TextView textView, @NonNull Context context) { + if (isRtl(context)) { + textView.setGravity(Gravity.RIGHT); + } else { + textView.setGravity(Gravity.LEFT); + } + } + + public static void mirrorIfRtl(View view, Context context) { + if (isRtl(context)) { + view.setScaleX(-1.0f); + } + } + + public static boolean isLtr(@NonNull View view) { + return isLtr(view.getContext()); + } + + public static boolean isLtr(@NonNull Context context) { + return context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_LTR; + } + + public static boolean isRtl(@NonNull View view) { + return isRtl(view.getContext()); + } + + public static boolean isRtl(@NonNull Context context) { + return context.getResources().getConfiguration().getLayoutDirection() == View.LAYOUT_DIRECTION_RTL; + } + + public static float pxToDp(float px) { + return px / Resources.getSystem().getDisplayMetrics().density; + } + + public static int dpToPx(Context context, int dp) { + return (int)((dp * context.getResources().getDisplayMetrics().density) + 0.5); + } + + public static int dpToPx(int dp) { + return Math.round(dp * Resources.getSystem().getDisplayMetrics().density); + } + + public static int dpToSp(int dp) { + return (int) (dpToPx(dp) / Resources.getSystem().getDisplayMetrics().scaledDensity); + } + + public static int spToPx(float sp) { + return (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, sp, Resources.getSystem().getDisplayMetrics()); + } + + public static void updateLayoutParams(@NonNull View view, int width, int height) { + view.getLayoutParams().width = width; + view.getLayoutParams().height = height; + view.requestLayout(); + } + + public static void updateLayoutParamsIfNonNull(@Nullable View view, int width, int height) { + if (view != null) { + updateLayoutParams(view, width, height); + } + } + + public static void setVisibilityIfNonNull(@Nullable View view, int visibility) { + if (view != null) { + view.setVisibility(visibility); + } + } + + public static int getLeftMargin(@NonNull View view) { + if (isLtr(view)) { + return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin; + } + return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).rightMargin; + } + + public static int getRightMargin(@NonNull View view) { + if (isLtr(view)) { + return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).rightMargin; + } + return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin; + } + + public static int getTopMargin(@NonNull View view) { + return ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).topMargin; + } + + public static void setLeftMargin(@NonNull View view, int margin) { + if (isLtr(view)) { + ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin = margin; + } else { + ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).rightMargin = margin; + } + view.forceLayout(); + view.requestLayout(); + } + + public static void setRightMargin(@NonNull View view, int margin) { + if (isLtr(view)) { + ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).rightMargin = margin; + } else { + ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).leftMargin = margin; + } + view.forceLayout(); + view.requestLayout(); + } + + public static void setTopMargin(@NonNull View view, int margin) { + ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).topMargin = margin; + view.requestLayout(); + } + + public static void setBottomMargin(@NonNull View view, int margin) { + ((ViewGroup.MarginLayoutParams) view.getLayoutParams()).bottomMargin = margin; + view.requestLayout(); + } + + public static int getWidth(@NonNull View view) { + return view.getLayoutParams().width; + } + + public static void setPaddingTop(@NonNull View view, int padding) { + view.setPadding(view.getPaddingLeft(), padding, view.getPaddingRight(), view.getPaddingBottom()); + } + + public static void setPaddingBottom(@NonNull View view, int padding) { + view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), view.getPaddingRight(), padding); + } + + public static void setPadding(@NonNull View view, int padding) { + view.setPadding(padding, padding, padding, padding); + } + + public static void setPaddingStart(@NonNull View view, int padding) { + if (isLtr(view)) { + view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); + } else { + view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), padding, view.getPaddingBottom()); + } + } + + public static void setPaddingEnd(@NonNull View view, int padding) { + if (isLtr(view)) { + view.setPadding(view.getPaddingLeft(), view.getPaddingTop(), padding, view.getPaddingBottom()); + } else { + view.setPadding(padding, view.getPaddingTop(), view.getPaddingRight(), view.getPaddingBottom()); + } + } + + public static boolean isPointInsideView(@NonNull View view, float x, float y) { + int[] location = new int[2]; + + view.getLocationOnScreen(location); + + int viewX = location[0]; + int viewY = location[1]; + + return x > viewX && x < viewX + view.getWidth() && + y > viewY && y < viewY + view.getHeight(); + } + + public static int getStatusBarHeight(@NonNull View view) { + int result = 0; + int resourceId = view.getResources().getIdentifier("status_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = view.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + public static int getNavigationBarHeight(@NonNull View view) { + int result = 0; + int resourceId = view.getResources().getIdentifier("navigation_bar_height", "dimen", "android"); + if (resourceId > 0) { + result = view.getResources().getDimensionPixelSize(resourceId); + } + return result; + } + + public static void hideKeyboard(@NonNull Context context, @NonNull View view) { + InputMethodManager inputManager = (InputMethodManager) context.getSystemService(Activity.INPUT_METHOD_SERVICE); + inputManager.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + + /** + * Enables or disables a view and all child views recursively. + */ + public static void setEnabledRecursive(@NonNull View view, boolean enabled) { + view.setEnabled(enabled); + if (view instanceof ViewGroup) { + ViewGroup viewGroup = (ViewGroup) view; + for (int i = 0; i < viewGroup.getChildCount(); i++) { + setEnabledRecursive(viewGroup.getChildAt(i), enabled); + } + } + } + + public static @Nullable Lifecycle getActivityLifecycle(@NonNull View view) { + return getActivityLifecycle(view.getContext()); + } + + private static @Nullable Lifecycle getActivityLifecycle(@Nullable Context context) { + if (context instanceof ContextThemeWrapper) { + return getActivityLifecycle(((ContextThemeWrapper) context).getBaseContext()); + } + + if (context instanceof AppCompatActivity) { + return ((AppCompatActivity) context).getLifecycle(); + } + + return null; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/WindowUtil.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/WindowUtil.java new file mode 100644 index 0000000000..4bff4e76aa --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/WindowUtil.java @@ -0,0 +1,98 @@ +package org.thoughtcrime.securesms.conversation.v2; + +import android.app.Activity; +import android.graphics.Rect; +import android.os.Build; +import android.view.View; +import android.view.Window; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; + +import org.session.libsession.utilities.ThemeUtil; + +public final class WindowUtil { + + private WindowUtil() { + } + + public static void setLightNavigationBarFromTheme(@NonNull Activity activity) { + if (Build.VERSION.SDK_INT < 27) return; + + final boolean isLightNavigationBar = ThemeUtil.getThemedBoolean(activity, android.R.attr.windowLightNavigationBar); + + if (isLightNavigationBar) setLightNavigationBar(activity.getWindow()); + else clearLightNavigationBar(activity.getWindow()); + } + + public static void clearLightNavigationBar(@NonNull Window window) { + if (Build.VERSION.SDK_INT < 27) return; + + clearSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); + } + + public static void setLightNavigationBar(@NonNull Window window) { + if (Build.VERSION.SDK_INT < 27) return; + + setSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR); + } + + public static void setNavigationBarColor(@NonNull Window window, @ColorInt int color) { + if (Build.VERSION.SDK_INT < 21) return; + + window.setNavigationBarColor(color); + } + + public static void setLightStatusBarFromTheme(@NonNull Activity activity) { + if (Build.VERSION.SDK_INT < 23) return; + + final boolean isLightStatusBar = ThemeUtil.getThemedBoolean(activity, android.R.attr.windowLightStatusBar); + + if (isLightStatusBar) setLightStatusBar(activity.getWindow()); + else clearLightStatusBar(activity.getWindow()); + } + + public static void clearLightStatusBar(@NonNull Window window) { + if (Build.VERSION.SDK_INT < 23) return; + + clearSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } + + public static void setLightStatusBar(@NonNull Window window) { + if (Build.VERSION.SDK_INT < 23) return; + + setSystemUiFlags(window, View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR); + } + + public static void setStatusBarColor(@NonNull Window window, @ColorInt int color) { + if (Build.VERSION.SDK_INT < 21) return; + + window.setStatusBarColor(color); + } + + /** + * A sort of roundabout way of determining if the status bar is present by seeing if there's a + * vertical window offset. + */ + public static boolean isStatusBarPresent(@NonNull Window window) { + Rect rectangle = new Rect(); + window.getDecorView().getWindowVisibleDisplayFrame(rectangle); + return rectangle.top > 0; + } + + private static void clearSystemUiFlags(@NonNull Window window, int flags) { + View view = window.getDecorView(); + int uiFlags = view.getSystemUiVisibility(); + + uiFlags &= ~flags; + view.setSystemUiVisibility(uiFlags); + } + + private static void setSystemUiFlags(@NonNull Window window, int flags) { + View view = window.getDecorView(); + int uiFlags = view.getSystemUiVisibility(); + + uiFlags |= flags; + view.setSystemUiVisibility(uiFlags); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt index 44c782577e..444c389e04 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/dialogs/JoinOpenGroupDialog.kt @@ -40,7 +40,7 @@ class JoinOpenGroupDialog(private val name: String, private val url: String) : B ThreadUtils.queue { try { OpenGroupManager.add(openGroup.server, openGroup.room, openGroup.serverPublicKey, activity) - MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(url) + MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(openGroup.server) ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(activity) } catch (e: Exception) { Toast.makeText(activity, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt index ffd3a41bfd..cab24ce8be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationActionModeCallback.kt @@ -10,7 +10,6 @@ import org.session.libsession.messaging.utilities.SessionId import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsession.utilities.TextSecurePreferences import org.session.libsignal.utilities.IdPrefix -import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 import org.thoughtcrime.securesms.conversation.v2.ConversationAdapter import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord @@ -43,14 +42,6 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p ?.let { SessionId(IdPrefix.BLINDED, it) }?.hexString fun userCanDeleteSelectedItems(): Boolean { val allSentByCurrentUser = selectedItems.all { it.isOutgoing } - - // Remove this after the unsend request is enabled - if (!ConversationActivityV2.IS_UNSEND_REQUESTS_ENABLED) { - if (openGroup == null) { return true } - if (allSentByCurrentUser) { return true } - return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey, blindedPublicKey) - } - val allReceivedByCurrentUser = selectedItems.all { !it.isOutgoing } if (openGroup == null) { return allSentByCurrentUser || allReceivedByCurrentUser } if (allSentByCurrentUser) { return true } @@ -115,6 +106,7 @@ class ConversationActionModeCallback(private val adapter: ConversationAdapter, p interface ConversationActionModeCallbackDelegate { + fun selectMessages(messages: Set) fun deleteMessages(messages: Set) fun banUser(messages: Set) fun banAndDeleteAll(messages: Set) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuItemHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuItemHelper.kt new file mode 100644 index 0000000000..a72ae848fa --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/menus/ConversationMenuItemHelper.kt @@ -0,0 +1,24 @@ +package org.thoughtcrime.securesms.conversation.v2.menus + +import android.content.Context +import org.session.libsession.messaging.open_groups.OpenGroup +import org.thoughtcrime.securesms.database.model.MessageRecord +import org.thoughtcrime.securesms.groups.OpenGroupManager + +object ConversationMenuItemHelper { + + @JvmStatic + fun userCanDeleteSelectedItems(context: Context, message: MessageRecord, openGroup: OpenGroup?, userPublicKey: String): Boolean { + if (openGroup == null) return message.isOutgoing || !message.isOutgoing + if (message.isOutgoing) return true + return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey) + } + + @JvmStatic + fun userCanBanSelectedUsers(context: Context, message: MessageRecord, openGroup: OpenGroup?, userPublicKey: String): Boolean { + if (openGroup == null) return false + if (message.isOutgoing) return false // Users can't ban themselves + return OpenGroupManager.isUserModerator(context, openGroup.groupId, userPublicKey) + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.java new file mode 100644 index 0000000000..54a8e7626c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/EmojiReactionsView.java @@ -0,0 +1,343 @@ +package org.thoughtcrime.securesms.conversation.v2.messages; + +import android.content.Context; +import android.content.res.TypedArray; +import android.os.Handler; +import android.os.Looper; +import android.util.AttributeSet; +import android.view.HapticFeedbackConstants; +import android.view.LayoutInflater; +import android.view.MotionEvent; +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.constraintlayout.widget.Group; +import androidx.core.content.ContextCompat; + +import com.google.android.flexbox.FlexboxLayout; +import com.google.android.flexbox.JustifyContent; + +import org.session.libsession.utilities.TextSecurePreferences; +import org.thoughtcrime.securesms.components.emoji.EmojiImageView; +import org.thoughtcrime.securesms.components.emoji.EmojiUtil; +import org.thoughtcrime.securesms.conversation.v2.ViewUtil; +import org.thoughtcrime.securesms.database.model.MessageId; +import org.thoughtcrime.securesms.database.model.ReactionRecord; +import org.thoughtcrime.securesms.util.NumberUtil; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import network.loki.messenger.R; + +public class EmojiReactionsView extends LinearLayout implements View.OnTouchListener { + + // Normally 6dp, but we have 1dp left+right margin on the pills themselves + private final int OUTER_MARGIN = ViewUtil.dpToPx(2); + private static final int DEFAULT_THRESHOLD = 5; + + private List records; + private long messageId; + private ViewGroup container; + private Group showLess; + private VisibleMessageViewDelegate delegate; + private Handler gestureHandler = new Handler(Looper.getMainLooper()); + private Runnable pressCallback; + private Runnable longPressCallback; + private long onDownTimestamp = 0; + private static long longPressDurationThreshold = 250; + private static long maxDoubleTapInterval = 200; + private boolean extended = false; + + public EmojiReactionsView(Context context) { + super(context); + init(null); + } + + public EmojiReactionsView(Context context, @Nullable AttributeSet attrs) { + super(context, attrs); + init(attrs); + } + + private void init(@Nullable AttributeSet attrs) { + inflate(getContext(), R.layout.view_emoji_reactions, this); + + this.container = findViewById(R.id.layout_emoji_container); + this.showLess = findViewById(R.id.group_show_less); + + records = new ArrayList<>(); + + if (attrs != null) { + TypedArray typedArray = getContext().getTheme().obtainStyledAttributes(attrs, R.styleable.EmojiReactionsView, 0, 0); + typedArray.recycle(); + } + } + + public void clear() { + this.records.clear(); + container.removeAllViews(); + } + + public void setReactions(long messageId, @NonNull List records, boolean outgoing, VisibleMessageViewDelegate delegate) { + this.delegate = delegate; + if (records.equals(this.records)) { + return; + } + + FlexboxLayout containerLayout = (FlexboxLayout) this.container; + containerLayout.setJustifyContent(outgoing ? JustifyContent.FLEX_END : JustifyContent.FLEX_START); + this.records.clear(); + this.records.addAll(records); + if (this.messageId != messageId) { + extended = false; + } + this.messageId = messageId; + + displayReactions(extended ? Integer.MAX_VALUE : DEFAULT_THRESHOLD); + } + + @Override + public boolean onTouch(View v, MotionEvent event) { + if (v.getTag() == null) return false; + + Reaction reaction = (Reaction) v.getTag(); + int action = event.getAction(); + if (action == MotionEvent.ACTION_DOWN) onDown(new MessageId(reaction.messageId, reaction.isMms)); + else if (action == MotionEvent.ACTION_CANCEL) removeLongPressCallback(); + else if (action == MotionEvent.ACTION_UP) onUp(reaction); + return true; + } + + private void displayReactions(int threshold) { + String userPublicKey = TextSecurePreferences.getLocalNumber(getContext()); + List reactions = buildSortedReactionsList(records, userPublicKey, threshold); + + container.removeAllViews(); + LinearLayout overflowContainer = new LinearLayout(getContext()); + overflowContainer.setOrientation(LinearLayout.HORIZONTAL); + int innerPadding = ViewUtil.dpToPx(4); + overflowContainer.setPaddingRelative(innerPadding,innerPadding,innerPadding,innerPadding); + + for (Reaction reaction : reactions) { + if (container.getChildCount() + 1 >= DEFAULT_THRESHOLD && threshold != Integer.MAX_VALUE && reactions.size() > threshold) { + if (overflowContainer.getParent() == null) { + container.addView(overflowContainer); + ViewGroup.LayoutParams overflowParams = overflowContainer.getLayoutParams(); + overflowParams.height = ViewUtil.dpToPx(26); + overflowContainer.setLayoutParams(overflowParams); + overflowContainer.setBackground(ContextCompat.getDrawable(getContext(), R.drawable.reaction_pill_dialog_background)); + } + View pill = buildPill(getContext(), this, reaction, true); + pill.setOnClickListener(v -> { + extended = true; + displayReactions(Integer.MAX_VALUE); + }); + pill.findViewById(R.id.reactions_pill_count).setVisibility(View.GONE); + pill.findViewById(R.id.reactions_pill_spacer).setVisibility(View.GONE); + overflowContainer.addView(pill); + } else { + View pill = buildPill(getContext(), this, reaction, false); + pill.setTag(reaction); + pill.setOnTouchListener(this); + container.addView(pill); + int pixelSize = ViewUtil.dpToPx(1); + MarginLayoutParams params = (MarginLayoutParams) pill.getLayoutParams(); + params.setMargins(pixelSize, 0, pixelSize, 0); + pill.setLayoutParams(params); + } + } + + int overflowChildren = overflowContainer.getChildCount(); + int negativeMargin = ViewUtil.dpToPx(-8); + for (int i = 0; i < overflowChildren; i++) { + View child = overflowContainer.getChildAt(i); + MarginLayoutParams childParams = (MarginLayoutParams) child.getLayoutParams(); + if ((i == 0 && overflowChildren > 1) || i + 1 < overflowChildren) { + // if first and there is more than one child, or we are not the last child then set negative right margin + childParams.setMargins(0,0, negativeMargin, 0); + child.setLayoutParams(childParams); + } + } + + if (threshold == Integer.MAX_VALUE) { + showLess.setVisibility(VISIBLE); + for (int id : showLess.getReferencedIds()) { + findViewById(id).setOnClickListener(view -> { + extended = false; + displayReactions(DEFAULT_THRESHOLD); + }); + } + } else { + showLess.setVisibility(GONE); + } + } + + private void onReactionClicked(Reaction reaction) { + if (reaction.messageId != 0) { + MessageId messageId = new MessageId(reaction.messageId, reaction.isMms); + delegate.onReactionClicked(reaction.emoji, messageId, reaction.userWasSender); + } + } + + private static @NonNull List buildSortedReactionsList(@NonNull List records, String userPublicKey, int threshold) { + Map counters = new LinkedHashMap<>(); + + for (ReactionRecord record : records) { + String baseEmoji = EmojiUtil.getCanonicalRepresentation(record.getEmoji()); + Reaction info = counters.get(baseEmoji); + + if (info == null) { + info = new Reaction(record.getMessageId(), record.isMms(), record.getEmoji(), record.getCount(), record.getSortId(), record.getDateReceived(), userPublicKey.equals(record.getAuthor())); + } else { + info.update(record.getEmoji(), record.getCount(), record.getDateReceived(), userPublicKey.equals(record.getAuthor())); + } + + counters.put(baseEmoji, info); + } + + List reactions = new ArrayList<>(counters.values()); + + Collections.sort(reactions, Collections.reverseOrder()); + + if (reactions.size() >= threshold + 2 && threshold != Integer.MAX_VALUE) { + List shortened = new ArrayList<>(threshold + 2); + shortened.addAll(reactions.subList(0, threshold + 2)); + return shortened; + } else { + return reactions; + } + } + + private static View buildPill(@NonNull Context context, @NonNull ViewGroup parent, @NonNull Reaction reaction, boolean isCompact) { + View root = LayoutInflater.from(context).inflate(R.layout.reactions_pill, parent, false); + EmojiImageView emojiView = root.findViewById(R.id.reactions_pill_emoji); + TextView countView = root.findViewById(R.id.reactions_pill_count); + View spacer = root.findViewById(R.id.reactions_pill_spacer); + + if (isCompact) { + root.setPaddingRelative(1,1,1,1); + ViewGroup.LayoutParams layoutParams = root.getLayoutParams(); + layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT; + root.setLayoutParams(layoutParams); + } + + if (reaction.emoji != null) { + emojiView.setImageEmoji(reaction.emoji); + + if (reaction.count >= 1) { + countView.setText(NumberUtil.getFormattedNumber(reaction.count)); + } else { + countView.setVisibility(GONE); + spacer.setVisibility(GONE); + } + } else { + emojiView.setVisibility(GONE); + spacer.setVisibility(GONE); + countView.setText(context.getString(R.string.ReactionsConversationView_plus, reaction.count)); + } + + if (reaction.userWasSender && !isCompact) { + root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background_selected)); + countView.setTextColor(ContextCompat.getColor(context, R.color.reactions_pill_selected_text_color)); + } else { + if (!isCompact) { + root.setBackground(ContextCompat.getDrawable(context, R.drawable.reaction_pill_background)); + } + } + + return root; + } + + private void onDown(MessageId messageId) { + removeLongPressCallback(); + Runnable newLongPressCallback = () -> { + performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); + if (delegate != null) { + delegate.onReactionLongClicked(messageId); + } + }; + this.longPressCallback = newLongPressCallback; + gestureHandler.postDelayed(newLongPressCallback, longPressDurationThreshold); + onDownTimestamp = new Date().getTime(); + } + + private void removeLongPressCallback() { + if (longPressCallback != null) { + gestureHandler.removeCallbacks(longPressCallback); + } + } + + private void onUp(Reaction reaction) { + if ((new Date().getTime() - onDownTimestamp) < longPressDurationThreshold) { + removeLongPressCallback(); + if (pressCallback != null) { + gestureHandler.removeCallbacks(pressCallback); + this.pressCallback = null; + } else { + Runnable newPressCallback = () -> { + onReactionClicked(reaction); + pressCallback = null; + }; + this.pressCallback = newPressCallback; + gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval); + } + } + } + + private static class Reaction implements Comparable { + private final long messageId; + private final boolean isMms; + private String emoji; + private long count; + private long sortIndex; + private long lastSeen; + private boolean userWasSender; + + Reaction(long messageId, boolean isMms, @Nullable String emoji, long count, long sortIndex, long lastSeen, boolean userWasSender) { + this.messageId = messageId; + this.isMms = isMms; + this.emoji = emoji; + this.count = count; + this.sortIndex = sortIndex; + this.lastSeen = lastSeen; + this.userWasSender = userWasSender; + } + + void update(@NonNull String emoji, long count, long lastSeen, boolean userWasSender) { + if (!this.userWasSender) { + if (userWasSender || lastSeen > this.lastSeen) { + this.emoji = emoji; + } + } + + this.count = this.count + count; + this.lastSeen = Math.max(this.lastSeen, lastSeen); + this.userWasSender = this.userWasSender || userWasSender; + } + + @NonNull Reaction merge(@NonNull Reaction other) { + this.count = this.count + other.count; + this.lastSeen = Math.max(this.lastSeen, other.lastSeen); + this.userWasSender = this.userWasSender || other.userWasSender; + return this; + } + + @Override + public int compareTo(Reaction rhs) { + Reaction lhs = this; + if (lhs.count == rhs.count ) { + return Long.compare(lhs.sortIndex, rhs.sortIndex); + } else { + return Long.compare(lhs.count, rhs.count); + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt index 4dc454dda1..7a00830ef3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageContentView.kt @@ -53,7 +53,7 @@ class VisibleMessageContentView : LinearLayout { private lateinit var binding: ViewVisibleMessageContentBinding var onContentClick: MutableList<((event: MotionEvent) -> Unit)> = mutableListOf() var onContentDoubleTap: (() -> Unit)? = null - var delegate: VisibleMessageContentViewDelegate? = null + var delegate: VisibleMessageViewDelegate? = null var indexInAdapter: Int = -1 // region Lifecycle @@ -87,13 +87,13 @@ class VisibleMessageContentView : LinearLayout { if (message.isDeleted) { binding.deletedMessageView.root.isVisible = true - binding.deletedMessageView.root.bind(message, VisibleMessageContentView.getTextColor(context,message)) + binding.deletedMessageView.root.bind(message, getTextColor(context, message)) return } else { binding.deletedMessageView.root.isVisible = false } // clear the - binding.bodyTextView.text = "" + binding.bodyTextView.text = null binding.quoteView.root.isVisible = message is MmsMessageRecord && message.quote != null @@ -149,64 +149,70 @@ class VisibleMessageContentView : LinearLayout { } } - if (message is MmsMessageRecord && message.linkPreviews.isNotEmpty()) { - binding.linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster) - onContentClick.add { event -> binding.linkPreviewView.calculateHit(event) } - // Body text view is inside the link preview for layout convenience - } else if (message is MmsMessageRecord && message.slideDeck.audioSlide != null) { - hideBody = true - // Audio attachment - if (contactIsTrusted || message.isOutgoing) { - binding.voiceMessageView.root.indexInAdapter = indexInAdapter - binding.voiceMessageView.root.delegate = context as? ConversationActivityV2 - binding.voiceMessageView.root.bind(message, isStartOfMessageCluster, isEndOfMessageCluster) - // We have to use onContentClick (rather than a click listener directly on the voice - // message view) so as to not interfere with all the other gestures. - onContentClick.add { binding.voiceMessageView.root.togglePlayback() } - onContentDoubleTap = { binding.voiceMessageView.root.handleDoubleTap() } - } else { - // TODO: move this out to its own area - binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message)) - onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) } + when { + message is MmsMessageRecord && message.linkPreviews.isNotEmpty() -> { + binding.linkPreviewView.bind(message, glide, isStartOfMessageCluster, isEndOfMessageCluster) + onContentClick.add { event -> binding.linkPreviewView.calculateHit(event) } + // Body text view is inside the link preview for layout convenience } - } else if (message is MmsMessageRecord && message.slideDeck.documentSlide != null) { - hideBody = true - // Document attachment - if (contactIsTrusted || message.isOutgoing) { - binding.documentView.root.bind(message, VisibleMessageContentView.getTextColor(context, message)) - } else { - binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message)) - onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) } + message is MmsMessageRecord && message.slideDeck.audioSlide != null -> { + hideBody = true + // Audio attachment + if (contactIsTrusted || message.isOutgoing) { + binding.voiceMessageView.root.indexInAdapter = indexInAdapter + binding.voiceMessageView.root.delegate = context as? ConversationActivityV2 + binding.voiceMessageView.root.bind(message, isStartOfMessageCluster, isEndOfMessageCluster) + // We have to use onContentClick (rather than a click listener directly on the voice + // message view) so as to not interfere with all the other gestures. + onContentClick.add { binding.voiceMessageView.root.togglePlayback() } + onContentDoubleTap = { binding.voiceMessageView.root.handleDoubleTap() } + } else { + // TODO: move this out to its own area + binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.AUDIO, VisibleMessageContentView.getTextColor(context,message)) + onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) } + } } - } else if (message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty()) { - /* - * Images / Video attachment - */ - if (contactIsTrusted || message.isOutgoing) { - // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups - // bind after add view because views are inflated and calculated during bind - binding.albumThumbnailView.bind( + message is MmsMessageRecord && message.slideDeck.documentSlide != null -> { + hideBody = true + // Document attachment + if (contactIsTrusted || message.isOutgoing) { + binding.documentView.root.bind(message, VisibleMessageContentView.getTextColor(context, message)) + } else { + binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.DOCUMENT, VisibleMessageContentView.getTextColor(context,message)) + onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) } + } + } + message is MmsMessageRecord && message.slideDeck.asAttachments().isNotEmpty() -> { + /* + * Images / Video attachment + */ + if (contactIsTrusted || message.isOutgoing) { + // isStart and isEnd of cluster needed for calculating the mask for full bubble image groups + // bind after add view because views are inflated and calculated during bind + binding.albumThumbnailView.bind( glideRequests = glide, message = message, isStart = isStartOfMessageCluster, isEnd = isEndOfMessageCluster - ) - val layoutParams = binding.albumThumbnailView.layoutParams as ConstraintLayout.LayoutParams - layoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f - binding.albumThumbnailView.layoutParams = layoutParams - onContentClick.add { event -> - binding.albumThumbnailView.calculateHitObject(event, message, thread) + ) + val layoutParams = binding.albumThumbnailView.layoutParams as ConstraintLayout.LayoutParams + layoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f + binding.albumThumbnailView.layoutParams = layoutParams + onContentClick.add { event -> + binding.albumThumbnailView.calculateHitObject(event, message, thread) + } + } else { + hideBody = true + binding.albumThumbnailView.clearViews() + binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message)) + onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) } } - } else { - hideBody = true - binding.albumThumbnailView.clearViews() - binding.untrustedView.root.bind(UntrustedAttachmentView.AttachmentType.MEDIA, VisibleMessageContentView.getTextColor(context,message)) - onContentClick.add { binding.untrustedView.root.showTrustDialog(message.individualRecipient) } } - } else if (message.isOpenGroupInvitation) { - hideBody = true - binding.openGroupInvitationView.root.bind(message, VisibleMessageContentView.getTextColor(context, message)) - onContentClick.add { binding.openGroupInvitationView.root.joinOpenGroup() } + message.isOpenGroupInvitation -> { + hideBody = true + binding.openGroupInvitationView.root.bind(message, VisibleMessageContentView.getTextColor(context, message)) + onContentClick.add { binding.openGroupInvitationView.root.joinOpenGroup() } + } } binding.bodyTextView.isVisible = message.body.isNotEmpty() && !hideBody @@ -312,8 +318,3 @@ class VisibleMessageContentView : LinearLayout { } // endregion } - -interface VisibleMessageContentViewDelegate { - - fun scrollToMessageIfPossible(timestamp: Long) -} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 0c910b8f06..bd68693005 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -26,12 +26,14 @@ import network.loki.messenger.R import network.loki.messenger.databinding.ViewVisibleMessageBinding import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.contacts.Contact.ContactContext +import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.utilities.Address import org.session.libsession.utilities.ViewUtil import org.session.libsignal.utilities.IdPrefix import org.session.libsignal.utilities.ThreadUtils import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 +import org.thoughtcrime.securesms.database.LokiAPIDatabase import org.thoughtcrime.securesms.database.LokiThreadDatabase import org.thoughtcrime.securesms.database.MmsDatabase import org.thoughtcrime.securesms.database.MmsSmsDatabase @@ -59,6 +61,7 @@ class VisibleMessageView : LinearLayout { @Inject lateinit var threadDb: ThreadDatabase @Inject lateinit var lokiThreadDb: LokiThreadDatabase + @Inject lateinit var lokiApiDb: LokiAPIDatabase @Inject lateinit var mmsSmsDb: MmsSmsDatabase @Inject lateinit var smsDb: SmsDatabase @Inject lateinit var mmsDb: MmsDatabase @@ -83,7 +86,7 @@ class VisibleMessageView : LinearLayout { var onPress: ((event: MotionEvent) -> Unit)? = null var onSwipeToReply: (() -> Unit)? = null var onLongPress: (() -> Unit)? = null - var contentViewDelegate: VisibleMessageContentViewDelegate? = null + val messageContentView: VisibleMessageContentView by lazy { binding.messageContentView } companion object { const val swipeToReplyThreshold = 64.0f // dp @@ -105,14 +108,21 @@ class VisibleMessageView : LinearLayout { private fun initialize() { isHapticFeedbackEnabled = true setWillNotDraw(false) - binding.expirationTimerViewContainer.disableClipping() + binding.messageInnerContainer.disableClipping() binding.messageContentView.disableClipping() } // endregion // region Updating - fun bind(message: MessageRecord, previous: MessageRecord?, next: MessageRecord?, - glide: GlideRequests, searchQuery: String?, contact: Contact?, senderSessionID: String, + fun bind( + message: MessageRecord, + previous: MessageRecord?, + next: MessageRecord?, + glide: GlideRequests, + searchQuery: String?, + contact: Contact?, + senderSessionID: String, + delegate: VisibleMessageViewDelegate?, ) { val threadID = message.threadId val thread = threadDb.getRecipientForThreadId(threadID) ?: return @@ -132,9 +142,9 @@ class VisibleMessageView : LinearLayout { else ViewUtil.dpToPx(context,2) if (binding.profilePictureView.root.visibility == View.GONE) { - val expirationParams = binding.expirationTimerViewContainer.layoutParams as MarginLayoutParams + val expirationParams = binding.messageInnerContainer.layoutParams as MarginLayoutParams expirationParams.bottomMargin = bottomMargin - binding.expirationTimerViewContainer.layoutParams = expirationParams + binding.messageInnerContainer.layoutParams = expirationParams } else { val avatarLayoutParams = binding.profilePictureView.root.layoutParams as MarginLayoutParams avatarLayoutParams.bottomMargin = bottomMargin @@ -198,7 +208,20 @@ class VisibleMessageView : LinearLayout { } // Expiration timer updateExpirationTimer(message) - // Calculate max message bubble width + // Emoji Reactions + val emojiLayoutParams = binding.emojiReactionsView.layoutParams as ConstraintLayout.LayoutParams + emojiLayoutParams.horizontalBias = if (message.isOutgoing) 1f else 0f + binding.emojiReactionsView.layoutParams = emojiLayoutParams + val capabilities = lokiThreadDb.getOpenGroupChat(threadID)?.server?.let { lokiApiDb.getServerCapabilities(it) } + if (message.reactions.isNotEmpty() && + (capabilities.isNullOrEmpty() || capabilities.contains(OpenGroupApi.Capability.REACTIONS.name.lowercase())) + ) { + binding.emojiReactionsView.setReactions(message.id, message.reactions, message.isOutgoing, delegate) + binding.emojiReactionsView.isVisible = true + } else { + binding.emojiReactionsView.isVisible = false + } + // Populate content view binding.messageContentView.indexInAdapter = indexInAdapter binding.messageContentView.bind( @@ -210,7 +233,7 @@ class VisibleMessageView : LinearLayout { searchQuery, message.isOutgoing || isGroupThread || (contact?.isTrusted ?: false) ) - binding.messageContentView.delegate = contentViewDelegate + binding.messageContentView.delegate = delegate onDoubleTap = { binding.messageContentView.onContentDoubleTap?.invoke() } } @@ -245,7 +268,7 @@ class VisibleMessageView : LinearLayout { } private fun updateExpirationTimer(message: MessageRecord) { - val container = binding.expirationTimerViewContainer + val container = binding.messageInnerContainer val content = binding.messageContentView val expiration = binding.expirationTimerView val spacing = binding.messageContentSpacing @@ -297,8 +320,8 @@ class VisibleMessageView : LinearLayout { override fun onDraw(canvas: Canvas) { val spacing = context.resources.getDimensionPixelSize(R.dimen.small_spacing) val iconSize = toPx(24, context.resources) - val left = binding.expirationTimerViewContainer.left + binding.messageContentView.right + spacing - val top = height - (binding.expirationTimerViewContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2) + val left = binding.messageInnerContainer.left + binding.messageContentView.right + spacing + val top = height - (binding.messageInnerContainer.height / 2) - binding.profilePictureView.root.marginBottom - (iconSize / 2) val right = left + iconSize val bottom = top + iconSize swipeToReplyIconRect.left = left @@ -388,7 +411,7 @@ class VisibleMessageView : LinearLayout { } else { val newPressCallback = Runnable { onPress(event) } this.pressCallback = newPressCallback - gestureHandler.postDelayed(newPressCallback, VisibleMessageView.maxDoubleTapInterval) + gestureHandler.postDelayed(newPressCallback, maxDoubleTapInterval) } } resetPosition() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageViewDelegate.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageViewDelegate.kt new file mode 100644 index 0000000000..6788dd3f38 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageViewDelegate.kt @@ -0,0 +1,15 @@ +package org.thoughtcrime.securesms.conversation.v2.messages + +import org.thoughtcrime.securesms.database.model.MessageId + +interface VisibleMessageViewDelegate { + + fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) + + fun scrollToMessageIfPossible(timestamp: Long) + + fun onReactionClicked(emoji: String, messageId: MessageId, userWasSender: Boolean) + + fun onReactionLongClicked(messageId: MessageId) + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt index fa4778e832..451368e1cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VoiceMessageView.kt @@ -35,7 +35,7 @@ class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener { private var progress = 0.0 private var duration = 0L private var player: AudioSlidePlayer? = null - var delegate: VoiceMessageViewDelegate? = null + var delegate: VisibleMessageViewDelegate? = null var indexInAdapter = -1 // region Lifecycle @@ -141,8 +141,3 @@ class VoiceMessageView : RelativeLayout, AudioSlidePlayer.Listener { } // endregion } - -interface VoiceMessageViewDelegate { - - fun playVoiceMessageAtIndexIfPossible(indexInAdapter: Int) -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java index 7e4093ade3..8c9916b87c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/AttachmentDatabase.java @@ -74,6 +74,8 @@ import java.util.HashMap; import java.util.LinkedList; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.TreeSet; import java.util.concurrent.Callable; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; @@ -674,7 +676,7 @@ public class AttachmentDatabase extends Database { return new LinkedList<>(); } - List result = new LinkedList<>(); + Set result = new TreeSet<>((o1, o2) -> o1.getAttachmentId().equals(o2.getAttachmentId()) ? 0 : 1); JSONArray array = new JSONArray(cursor.getString(cursor.getColumnIndexOrThrow(ATTACHMENT_JSON_ALIAS))); for (int i=0;i(result); } else { int urlIndex = cursor.getColumnIndex(URL); return Collections.singletonList(new DatabaseAttachment(new AttachmentId(cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/EmojiSearchDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/EmojiSearchDatabase.kt new file mode 100644 index 0000000000..f6e389a47d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/EmojiSearchDatabase.kt @@ -0,0 +1,101 @@ +package org.thoughtcrime.securesms.database + +import android.content.Context +import androidx.core.content.contentValuesOf +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.database.model.EmojiSearchData +import org.thoughtcrime.securesms.util.CursorUtil +import kotlin.math.max +import kotlin.math.roundToInt + +/** + * Contains all info necessary for full-text search of emoji tags. + */ +class EmojiSearchDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { + + companion object { + const val TABLE_NAME = "emoji_search" + const val LABEL = "label" + const val EMOJI = "emoji" + const val CREATE_EMOJI_SEARCH_TABLE_COMMAND = "CREATE VIRTUAL TABLE $TABLE_NAME USING fts5($LABEL, $EMOJI UNINDEXED)" + } + + /** + * @param query A search query. Doesn't need any special formatted -- it'll be sanitized. + * @return A list of emoji that are related to the search term, ordered by relevance. + */ + fun query(originalQuery: String, originalLimit: Int): List { + val query: String = originalQuery.trim() + + if (query.isEmpty()) { + return emptyList() + } + + val limit: Int = max(originalLimit, 100) + val entries = mutableListOf() + + readableDatabase.query(TABLE_NAME, arrayOf(LABEL, EMOJI), "$LABEL LIKE ?", arrayOf("%$query%"), null, null, null, "$limit") + .use { cursor -> + while (cursor.moveToNext()) { + entries += Entry( + label = CursorUtil.requireString(cursor, LABEL), + emoji = CursorUtil.requireString(cursor, EMOJI) + ) + } + } + + return entries + .sortedWith { lhs, rhs -> + similarityScore(query, lhs.label) - similarityScore(query, rhs.label) + } + .distinctBy { it.emoji } + .take(originalLimit) + .map { it.emoji } + } + + /** + * Deletes the content of the current search index and replaces it with the new one. + */ + fun setSearchIndex(searchIndex: List) { + writableDatabase.beginTransaction() + writableDatabase.delete(TABLE_NAME, null, null) + + for (searchData in searchIndex) { + for (label in searchData.tags) { + val values = contentValuesOf( + LABEL to label, + EMOJI to searchData.emoji + ) + writableDatabase.insert(TABLE_NAME, null, values) + } + } + writableDatabase.setTransactionSuccessful() + writableDatabase.endTransaction() + } + + /** + * Ranks how "similar" a match is to the original search term. + * A lower score means more similar, with 0 being a perfect match. + * + * We know that the `searchTerm` must be a substring of the `match`. + * We determine similarity by how many letters appear before or after the `searchTerm` in the `match`. + * We give letters that come before the term a bigger weight than those that come after as a way to prefer matches that are prefixed by the `searchTerm`. + */ + private fun similarityScore(searchTerm: String, match: String): Int { + if (searchTerm == match) { + return 0 + } + + val startIndex = match.indexOf(searchTerm) + + val prefixCount = startIndex + val suffixCount = match.length - (startIndex + searchTerm.length) + + val prefixRankWeight = 1.5f + val suffixRankWeight = 1f + + return ((prefixCount * prefixRankWeight) + (suffixCount * suffixRankWeight)).roundToInt() + } + + private data class Entry(val label: String, val emoji: String) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java index c0c08828b2..feaccc3983 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/GroupDatabase.java @@ -14,6 +14,7 @@ import com.annimon.stream.Stream; import net.sqlcipher.database.SQLiteDatabase; +import org.jetbrains.annotations.NotNull; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.GroupRecord; import org.session.libsession.utilities.TextSecurePreferences; @@ -441,7 +442,15 @@ public class GroupDatabase extends Database implements LokiOpenGroupDatabaseProt } } - public static class Reader implements Closeable { + public void migrateEncodedGroup(@NotNull String legacyEncodedGroupId, @NotNull String newEncodedGroupId) { + String query = GROUP_ID+" = ?"; + ContentValues contentValues = new ContentValues(1); + contentValues.put(GROUP_ID, newEncodedGroupId); + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.update(TABLE_NAME, contentValues, query, new String[]{legacyEncodedGroupId}); + } + + public static class Reader implements Closeable { private final Cursor cursor; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt index 0e3d6f19db..6aeadc2b7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiAPIDatabase.kt @@ -59,9 +59,9 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( private val token = "token" @JvmStatic val createOpenGroupAuthTokenTableCommand = "CREATE TABLE $openGroupAuthTokenTable ($server TEXT PRIMARY KEY, $token TEXT);" // Last message server IDs - private val lastMessageServerIDTable = "loki_api_last_message_server_id_cache" + private const val lastMessageServerIDTable = "loki_api_last_message_server_id_cache" private val lastMessageServerIDTableIndex = "loki_api_last_message_server_id_cache_index" - private val lastMessageServerID = "last_message_server_id" + private const val lastMessageServerID = "last_message_server_id" @JvmStatic val createLastMessageServerIDTableCommand = "CREATE TABLE $lastMessageServerIDTable ($lastMessageServerIDTableIndex STRING PRIMARY KEY, $lastMessageServerID INTEGER DEFAULT 0);" // Last deletion server IDs private val lastDeletionServerIDTable = "loki_api_last_deletion_server_id_cache" @@ -153,6 +153,9 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( "$requestSignature STRING NULLABLE DEFAULT NULL, $authorizationSignature STRING NULLABLE DEFAULT NULL, PRIMARY KEY ($masterPublicKey, $slavePublicKey));" private val sessionRequestTimestampCache = "session_request_timestamp_cache" @JvmStatic val createSessionRequestTimestampCacheCommand = "CREATE TABLE $sessionRequestTimestampCache ($publicKey STRING PRIMARY KEY, $timestamp STRING);" + + const val RESET_SEQ_NO = "UPDATE $lastMessageServerIDTable SET $lastMessageServerID = 0;" + // endregion } @@ -377,18 +380,29 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( database.delete(lastDeletionServerIDTable, "$lastDeletionServerIDTableIndex = ?", wrap(index)) } - fun removeLastDeletionServerID(group: Long, server: String) { + override fun migrateLegacyOpenGroup(legacyServerId: String, newServerId: String) { val database = databaseHelper.writableDatabase - val index = "$server.$group" - database.delete(lastDeletionServerIDTable,"$lastDeletionServerIDTableIndex = ?", wrap(index)) - } - - fun getUserCount(group: Long, server: String): Int? { - val database = databaseHelper.readableDatabase - val index = "$server.$group" - return database.get(userCountTable, "$publicChatID = ?", wrap(index)) { cursor -> - cursor.getInt(userCount) - }?.toInt() + database.beginTransaction() + val authRow = wrap(mapOf(server to newServerId)) + database.update(openGroupAuthTokenTable, authRow, "$server = ?", wrap(legacyServerId)) + val lastMessageRow = wrap(mapOf(lastMessageServerIDTableIndex to newServerId)) + database.update(lastMessageServerIDTable, lastMessageRow, + "$lastMessageServerIDTableIndex = ?", wrap(legacyServerId)) + val lastDeletionRow = wrap(mapOf(lastDeletionServerIDTableIndex to newServerId)) + database.update( + lastDeletionServerIDTable, lastDeletionRow, + "$lastDeletionServerIDTableIndex = ?", wrap(legacyServerId)) + val userCountRow = wrap(mapOf(publicChatID to newServerId)) + database.update( + userCountTable, userCountRow, + "$publicChatID = ?", wrap(legacyServerId) + ) + val publicKeyRow = wrap(mapOf(server to newServerId)) + database.update( + openGroupPublicKeyTable, publicKeyRow, + "$server = ?", wrap(legacyServerId) + ) + database.endTransaction() } fun getUserCount(room: String, server: String): Int? { @@ -399,13 +413,6 @@ class LokiAPIDatabase(context: Context, helper: SQLCipherOpenHelper) : Database( }?.toInt() } - override fun setUserCount(group: Long, server: String, newValue: Int) { - val database = databaseHelper.writableDatabase - val index = "$server.$group" - val row = wrap(mapOf( publicChatID to index, Companion.userCount to newValue.toString() )) - database.insertOrUpdate(userCountTable, row, "$publicChatID = ?", wrap(index)) - } - override fun setUserCount(room: String, server: String, newValue: Int) { val database = databaseHelper.writableDatabase val index = "$server.$room" diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt index 3fcbad60c1..3cfdd13017 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/LokiMessageDatabase.kt @@ -177,4 +177,12 @@ class LokiMessageDatabase(context: Context, helper: SQLCipherOpenHelper) : Datab val database = databaseHelper.writableDatabase database.delete(messageHashTable, "${Companion.messageID} = ?", arrayOf(messageID.toString())) } + + fun migrateThreadId(legacyThreadId: Long, newThreadId: Long) { + val database = databaseHelper.writableDatabase + val contentValues = ContentValues(1) + contentValues.put(threadID, newThreadId) + database.update(messageThreadMappingTable, contentValues, "$threadID = ?", arrayOf(legacyThreadId.toString())) + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java index 42e41a191b..022338687d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java @@ -7,15 +7,16 @@ import android.text.TextUtils; import net.sqlcipher.database.SQLiteDatabase; +import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Document; import org.session.libsession.utilities.IdentityKeyMismatch; import org.session.libsession.utilities.IdentityKeyMismatchList; -import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; -import org.session.libsignal.utilities.Log; import org.session.libsignal.crypto.IdentityKey; - -import org.session.libsession.utilities.Address; import org.session.libsignal.utilities.JsonUtil; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.util.SqlUtil; +import org.session.libsignal.utilities.Log; +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import java.io.IOException; import java.util.ArrayList; @@ -44,6 +45,8 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn public abstract void updateThreadId(long fromId, long toId); + public abstract MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException; + public void addMismatchedIdentity(long messageId, Address address, IdentityKey identityKey) { try { addToDocument(messageId, MISMATCHED_IDENTITIES, @@ -64,6 +67,30 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn } } + void updateReactionsUnread(SQLiteDatabase db, long messageId, boolean hasReactions, boolean isRemoval) { + try { + MessageRecord message = getMessageRecord(messageId); + ContentValues values = new ContentValues(); + + if (!hasReactions) { + values.put(REACTIONS_UNREAD, 0); + } else if (!isRemoval) { + values.put(REACTIONS_UNREAD, 1); + } + + if (message.isOutgoing() && hasReactions) { + values.put(NOTIFIED, 0); + } + + if (values.size() > 0) { + db.update(getTableName(), values, ID_WHERE, SqlUtil.buildArgs(messageId)); + } + notifyConversationListeners(message.getThreadId()); + } catch (NoSuchMessageException e) { + Log.w(TAG, "Failed to find message " + messageId); + } + } + protected , I> void removeFromDocument(long messageId, String column, I object, Class clazz) throws IOException { SQLiteDatabase database = databaseHelper.getWritableDatabase(); database.beginTransaction(); @@ -159,6 +186,15 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn } } + public void migrateThreadId(long oldThreadId, long newThreadId) { + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + String where = THREAD_ID+" = ?"; + String[] args = new String[]{oldThreadId+""}; + ContentValues contentValues = new ContentValues(); + contentValues.put(THREAD_ID, newThreadId); + db.update(getTableName(), contentValues, where, args); + } + public static class SyncMessageId { private final Address address; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt index a412dd14bd..d82c6bb278 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.kt @@ -55,8 +55,6 @@ import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ThreadUtils.queue import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.attachments.MmsNotificationAttachment -import org.thoughtcrime.securesms.database.MmsDatabase -import org.thoughtcrime.securesms.database.NoSuchMessageException import org.thoughtcrime.securesms.database.SmsDatabase.InsertListener import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord @@ -267,9 +265,9 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa private fun rawQuery(where: String, arguments: Array?): Cursor { val database = databaseHelper.readableDatabase return database.rawQuery( - "SELECT " + MMS_PROJECTION.joinToString(",")+ - " FROM " + TABLE_NAME + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + - " ON (" + TABLE_NAME + "." + ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" + + "SELECT " + MMS_PROJECTION.joinToString(",") + " FROM " + TABLE_NAME + + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + " ON (" + TABLE_NAME + "." + ID + " = " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + ")" + + " LEFT OUTER JOIN " + ReactionDatabase.TABLE_NAME + " ON (" + TABLE_NAME + "." + ID + " = " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + " AND " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + " = 1)" + " WHERE " + where + " GROUP BY " + TABLE_NAME + "." + ID, arguments ) } @@ -401,7 +399,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa fun setMessagesRead(threadId: Long): List { return setMessagesRead( - THREAD_ID + " = ? AND " + READ + " = 0", + THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)", arrayOf(threadId.toString()) ) } @@ -440,6 +438,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa } val contentValues = ContentValues() contentValues.put(READ, 1) + contentValues.put(REACTIONS_UNREAD, 0) database.update(TABLE_NAME, contentValues, where, arguments) database.setTransactionSuccessful() } finally { @@ -1006,6 +1005,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa notifyConversationListListeners() } + @Throws(NoSuchMessageException::class) + override fun getMessageRecord(messageId: Long): MessageRecord { + rawQuery(RAW_ID_WHERE, arrayOf("$messageId")).use { cursor -> + return Reader(cursor).next ?: throw NoSuchMessageException("No message for ID: $messageId") + } + } + fun deleteThread(threadId: Long) { deleteThreads(setOf(threadId)) } @@ -1266,7 +1272,7 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa message.outgoingQuote!!.missing, SlideDeck(context, message.outgoingQuote!!.attachments!!) ) else null, - message.sharedContacts, message.linkPreviews, false + message.sharedContacts, message.linkPreviews, listOf(), false ) } @@ -1391,12 +1397,13 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa .toList() ) val quote = getQuote(cursor) + val reactions = get(context).reactionDatabase().getReactions(cursor) return MediaMmsMessageRecord( id, recipient, recipient, addressDeviceId, dateSent, dateReceived, deliveryReceiptCount, threadId, body, slideDeck!!, partCount, box, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, - readReceiptCount, quote, contacts, previews, unidentified + readReceiptCount, quote, contacts, previews, reactions, unidentified ) } @@ -1571,9 +1578,23 @@ class MmsDatabase(context: Context, databaseHelper: SQLCipherOpenHelper) : Messa "'" + AttachmentDatabase.STICKER_PACK_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_ID + ", " + "'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " + "'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + - ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS + ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, + "json_group_array(json_object(" + + "'" + ReactionDatabase.ROW_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.ROW_ID + ", " + + "'" + ReactionDatabase.MESSAGE_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + ", " + + "'" + ReactionDatabase.IS_MMS + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + ", " + + "'" + ReactionDatabase.AUTHOR_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.AUTHOR_ID + ", " + + "'" + ReactionDatabase.EMOJI + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.EMOJI + ", " + + "'" + ReactionDatabase.SERVER_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.SERVER_ID + ", " + + "'" + ReactionDatabase.COUNT + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.COUNT + ", " + + "'" + ReactionDatabase.SORT_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.SORT_ID + ", " + + "'" + ReactionDatabase.DATE_SENT + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.DATE_SENT + ", " + + "'" + ReactionDatabase.DATE_RECEIVED + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.DATE_RECEIVED + + ")) AS " + ReactionDatabase.REACTION_JSON_ALIAS ) private const val RAW_ID_WHERE: String = "$TABLE_NAME._id = ?" - const val createMessageRequestResponseCommand: String = "ALTER TABLE $TABLE_NAME ADD COLUMN $MESSAGE_REQUEST_RESPONSE INTEGER DEFAULT 0;" + const val CREATE_MESSAGE_REQUEST_RESPONSE_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $MESSAGE_REQUEST_RESPONSE INTEGER DEFAULT 0;" + const val CREATE_REACTIONS_UNREAD_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $REACTIONS_UNREAD INTEGER DEFAULT 0;" + const val CREATE_REACTIONS_LAST_SEEN_COMMAND = "ALTER TABLE $TABLE_NAME ADD COLUMN $REACTIONS_LAST_SEEN INTEGER DEFAULT 0;" } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java index 5905433b93..c4fe3d2437 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsColumns.java @@ -21,6 +21,8 @@ public interface MmsSmsColumns { public static final String NOTIFIED = "notified"; public static final String UNIDENTIFIED = "unidentified"; public static final String MESSAGE_REQUEST_RESPONSE = "message_request_response"; + public static final String REACTIONS_UNREAD = "reactions_unread"; + public static final String REACTIONS_LAST_SEEN = "reactions_last_seen"; public static class Types { protected static final long TOTAL_MASK = 0xFFFFFFFF; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index cf8ed7097d..59aee0719a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -74,7 +74,8 @@ public class MmsSmsDatabase extends Database { MmsDatabase.QUOTE_MISSING, MmsDatabase.QUOTE_ATTACHMENT, MmsDatabase.SHARED_CONTACTS, - MmsDatabase.LINK_PREVIEWS}; + MmsDatabase.LINK_PREVIEWS, + ReactionDatabase.REACTION_JSON_ALIAS}; public MmsSmsDatabase(Context context, SQLCipherOpenHelper databaseHelper) { super(context, databaseHelper); @@ -145,7 +146,7 @@ public class MmsSmsDatabase extends Database { public Cursor getUnread() { String order = MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " ASC"; - String selection = MmsSmsColumns.READ + " = 0 AND " + MmsSmsColumns.NOTIFIED + " = 0"; + String selection = "(" + MmsSmsColumns.READ + " = 0 OR " + MmsSmsColumns.REACTIONS_UNREAD + " = 1) AND " + MmsSmsColumns.NOTIFIED + " = 0"; return queryTables(PROJECTION, selection, order, null); } @@ -219,6 +220,18 @@ public class MmsSmsDatabase extends Database { } private Cursor queryTables(String[] projection, String selection, String order, String limit) { + String reactionsColumn = "json_group_array(json_object(" + + "'" + ReactionDatabase.ROW_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.ROW_ID + ", " + + "'" + ReactionDatabase.MESSAGE_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + ", " + + "'" + ReactionDatabase.IS_MMS + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + ", " + + "'" + ReactionDatabase.AUTHOR_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.AUTHOR_ID + ", " + + "'" + ReactionDatabase.EMOJI + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.EMOJI + ", " + + "'" + ReactionDatabase.SERVER_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.SERVER_ID + ", " + + "'" + ReactionDatabase.COUNT + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.COUNT + ", " + + "'" + ReactionDatabase.SORT_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.SORT_ID + ", " + + "'" + ReactionDatabase.DATE_SENT + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.DATE_SENT + ", " + + "'" + ReactionDatabase.DATE_RECEIVED + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.DATE_RECEIVED + + ")) AS " + ReactionDatabase.REACTION_JSON_ALIAS; String[] mmsProjection = {MmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT, MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED, MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " AS " + MmsSmsColumns.ID, @@ -248,6 +261,7 @@ public class MmsSmsDatabase extends Database { "'" + AttachmentDatabase.STICKER_PACK_KEY + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_PACK_KEY + ", " + "'" + AttachmentDatabase.STICKER_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.STICKER_ID + ")) AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, + reactionsColumn, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, @@ -274,6 +288,7 @@ public class MmsSmsDatabase extends Database { + " || '::' || " + SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.UNIQUE_ROW_ID, "NULL AS " + AttachmentDatabase.ATTACHMENT_JSON_ALIAS, + reactionsColumn, SmsDatabase.BODY, MmsSmsColumns.READ, MmsSmsColumns.THREAD_ID, SmsDatabase.TYPE, SmsDatabase.ADDRESS, SmsDatabase.ADDRESS_DEVICE_ID, SmsDatabase.SUBJECT, MmsDatabase.MESSAGE_TYPE, MmsDatabase.MESSAGE_BOX, SmsDatabase.STATUS, MmsDatabase.PART_COUNT, @@ -299,10 +314,14 @@ public class MmsSmsDatabase extends Database { mmsQueryBuilder.setDistinct(true); smsQueryBuilder.setDistinct(true); - smsQueryBuilder.setTables(SmsDatabase.TABLE_NAME); - mmsQueryBuilder.setTables(MmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + - AttachmentDatabase.TABLE_NAME + - " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID); + smsQueryBuilder.setTables(SmsDatabase.TABLE_NAME + + " LEFT OUTER JOIN " + ReactionDatabase.TABLE_NAME + + " ON " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + " = " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " AND " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + " = 0"); + mmsQueryBuilder.setTables(MmsDatabase.TABLE_NAME + + " LEFT OUTER JOIN " + AttachmentDatabase.TABLE_NAME + + " ON " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.MMS_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + + " LEFT OUTER JOIN " + ReactionDatabase.TABLE_NAME + + " ON " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " AND " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + " = 1"); Set mmsColumnsPresent = new HashSet<>(); @@ -362,6 +381,16 @@ public class MmsSmsDatabase extends Database { mmsColumnsPresent.add(MmsDatabase.QUOTE_ATTACHMENT); mmsColumnsPresent.add(MmsDatabase.SHARED_CONTACTS); mmsColumnsPresent.add(MmsDatabase.LINK_PREVIEWS); + mmsColumnsPresent.add(ReactionDatabase.MESSAGE_ID); + mmsColumnsPresent.add(ReactionDatabase.IS_MMS); + mmsColumnsPresent.add(ReactionDatabase.AUTHOR_ID); + mmsColumnsPresent.add(ReactionDatabase.EMOJI); + mmsColumnsPresent.add(ReactionDatabase.SERVER_ID); + mmsColumnsPresent.add(ReactionDatabase.COUNT); + mmsColumnsPresent.add(ReactionDatabase.SORT_ID); + mmsColumnsPresent.add(ReactionDatabase.DATE_SENT); + mmsColumnsPresent.add(ReactionDatabase.DATE_RECEIVED); + mmsColumnsPresent.add(ReactionDatabase.REACTION_JSON_ALIAS); Set smsColumnsPresent = new HashSet<>(); smsColumnsPresent.add(MmsSmsColumns.ID); @@ -383,11 +412,22 @@ public class MmsSmsDatabase extends Database { smsColumnsPresent.add(SmsDatabase.DATE_RECEIVED); smsColumnsPresent.add(SmsDatabase.STATUS); smsColumnsPresent.add(SmsDatabase.UNIDENTIFIED); + smsColumnsPresent.add(ReactionDatabase.ROW_ID); + smsColumnsPresent.add(ReactionDatabase.MESSAGE_ID); + smsColumnsPresent.add(ReactionDatabase.IS_MMS); + smsColumnsPresent.add(ReactionDatabase.AUTHOR_ID); + smsColumnsPresent.add(ReactionDatabase.EMOJI); + smsColumnsPresent.add(ReactionDatabase.SERVER_ID); + smsColumnsPresent.add(ReactionDatabase.COUNT); + smsColumnsPresent.add(ReactionDatabase.SORT_ID); + smsColumnsPresent.add(ReactionDatabase.DATE_SENT); + smsColumnsPresent.add(ReactionDatabase.DATE_RECEIVED); + smsColumnsPresent.add(ReactionDatabase.REACTION_JSON_ALIAS); @SuppressWarnings("deprecation") - String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 4, MMS_TRANSPORT, selection, null, MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, null); + String mmsSubQuery = mmsQueryBuilder.buildUnionSubQuery(TRANSPORT, mmsProjection, mmsColumnsPresent, 5, MMS_TRANSPORT, selection, null, MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID, null); @SuppressWarnings("deprecation") - String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 4, SMS_TRANSPORT, selection, null, null, null); + String smsSubQuery = smsQueryBuilder.buildUnionSubQuery(TRANSPORT, smsProjection, smsColumnsPresent, 5, SMS_TRANSPORT, selection, null, SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID, null); SQLiteQueryBuilder unionQueryBuilder = new SQLiteQueryBuilder(); String unionQuery = unionQueryBuilder.buildUnionQuery(new String[] {smsSubQuery, mmsSubQuery}, order, limit); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt b/app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt new file mode 100644 index 0000000000..9d5ec69c51 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ReactionDatabase.kt @@ -0,0 +1,258 @@ +package org.thoughtcrime.securesms.database + +import android.content.ContentValues +import android.content.Context +import android.database.Cursor +import org.json.JSONArray +import org.json.JSONException +import org.session.libsignal.utilities.JsonUtil.SaneJSONObject +import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.database.model.ReactionRecord +import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.util.CursorUtil + +/** + * Store reactions on messages. + */ +class ReactionDatabase(context: Context, helper: SQLCipherOpenHelper) : Database(context, helper) { + + companion object { + const val TABLE_NAME = "reaction" + const val REACTION_JSON_ALIAS = "reaction_json" + const val ROW_ID = "reaction_id" + const val MESSAGE_ID = "message_id" + const val IS_MMS = "is_mms" + const val AUTHOR_ID = "author_id" + const val SERVER_ID = "server_id" + const val COUNT = "count" + const val SORT_ID = "sort_id" + const val EMOJI = "emoji" + const val DATE_SENT = "reaction_date_sent" + const val DATE_RECEIVED = "reaction_date_received" + + @JvmField + val CREATE_REACTION_TABLE_COMMAND = """ + CREATE TABLE $TABLE_NAME ( + $ROW_ID INTEGER PRIMARY KEY, + $MESSAGE_ID INTEGER NOT NULL, + $IS_MMS INTEGER NOT NULL, + $AUTHOR_ID INTEGER NOT NULL REFERENCES ${RecipientDatabase.TABLE_NAME} (${RecipientDatabase.ID}) ON DELETE CASCADE, + $EMOJI TEXT NOT NULL, + $SERVER_ID TEXT NOT NULL, + $COUNT INTEGER NOT NULL, + $SORT_ID INTEGER NOT NULL, + $DATE_SENT INTEGER NOT NULL, + $DATE_RECEIVED INTEGER NOT NULL, + UNIQUE($MESSAGE_ID, $IS_MMS, $EMOJI, $AUTHOR_ID) ON CONFLICT REPLACE + ) + """.trimIndent() + + @JvmField + val CREATE_REACTION_TRIGGERS = arrayOf( + """ + CREATE TRIGGER reactions_sms_delete AFTER DELETE ON ${SmsDatabase.TABLE_NAME} + BEGIN + DELETE FROM $TABLE_NAME WHERE $MESSAGE_ID = old.${MmsSmsColumns.ID} AND $IS_MMS = 0; + END + """, + """ + CREATE TRIGGER reactions_mms_delete AFTER DELETE ON ${MmsDatabase.TABLE_NAME} + BEGIN + DELETE FROM $TABLE_NAME WHERE $MESSAGE_ID = old.${MmsSmsColumns.ID} AND $IS_MMS = 1; + END + """ + ) + + private fun readReaction(cursor: Cursor): ReactionRecord { + return ReactionRecord( + messageId = CursorUtil.requireLong(cursor, MESSAGE_ID), + isMms = CursorUtil.requireInt(cursor, IS_MMS) == 1, + emoji = CursorUtil.requireString(cursor, EMOJI), + author = CursorUtil.requireString(cursor, AUTHOR_ID), + serverId = CursorUtil.requireString(cursor, SERVER_ID), + count = CursorUtil.requireLong(cursor, COUNT), + sortId = CursorUtil.requireLong(cursor, SORT_ID), + dateSent = CursorUtil.requireLong(cursor, DATE_SENT), + dateReceived = CursorUtil.requireLong(cursor, DATE_RECEIVED) + ) + } + } + + fun getReactions(messageId: MessageId): List { + val query = "$MESSAGE_ID = ? AND $IS_MMS = ? ORDER BY $SORT_ID" + val args = arrayOf("${messageId.id}", "${if (messageId.mms) 1 else 0}") + + val reactions: MutableList = mutableListOf() + + readableDatabase.query(TABLE_NAME, null, query, args, null, null, null).use { cursor -> + while (cursor.moveToNext()) { + reactions += readReaction(cursor) + } + } + + return reactions + } + + fun addReaction(messageId: MessageId, reaction: ReactionRecord) { + + writableDatabase.beginTransaction() + try { + val values = ContentValues().apply { + put(MESSAGE_ID, messageId.id) + put(IS_MMS, if (messageId.mms) 1 else 0) + put(EMOJI, reaction.emoji) + put(AUTHOR_ID, reaction.author) + put(SERVER_ID, reaction.serverId) + put(COUNT, reaction.count) + put(SORT_ID, reaction.sortId) + put(DATE_SENT, reaction.dateSent) + put(DATE_RECEIVED, reaction.dateReceived) + } + + writableDatabase.insert(TABLE_NAME, null, values) + + if (messageId.mms) { + DatabaseComponent.get(context).mmsDatabase().updateReactionsUnread(writableDatabase, messageId.id, hasReactions(messageId), false) + } else { + DatabaseComponent.get(context).smsDatabase().updateReactionsUnread(writableDatabase, messageId.id, hasReactions(messageId), false) + } + + writableDatabase.setTransactionSuccessful() + } finally { + writableDatabase.endTransaction() + } + } + + fun deleteReaction(emoji: String, messageId: MessageId, author: String) { + deleteReactions( + messageId = messageId, + query = "$MESSAGE_ID = ? AND $IS_MMS = ? AND $EMOJI = ? AND $AUTHOR_ID = ?", + args = arrayOf("${messageId.id}", "${if (messageId.mms) 1 else 0}", emoji, author) + ) + } + + fun deleteEmojiReactions(emoji: String, messageId: MessageId) { + deleteReactions( + messageId = messageId, + query = "$MESSAGE_ID = ? AND $IS_MMS = ? AND $EMOJI = ?", + args = arrayOf("${messageId.id}", "${if (messageId.mms) 1 else 0}", emoji) + ) + } + + fun deleteMessageReactions(messageId: MessageId) { + deleteReactions( + messageId = messageId, + query = "$MESSAGE_ID = ? AND $IS_MMS = ?", + args = arrayOf("${messageId.id}", "${if (messageId.mms) 1 else 0}") + ) + } + + private fun deleteReactions(messageId: MessageId, query: String, args: Array) { + writableDatabase.beginTransaction() + try { + writableDatabase.delete(TABLE_NAME, query, args) + + if (messageId.mms) { + DatabaseComponent.get(context).mmsDatabase().updateReactionsUnread(writableDatabase, messageId.id, hasReactions(messageId), true) + } else { + DatabaseComponent.get(context).smsDatabase().updateReactionsUnread(writableDatabase, messageId.id, hasReactions(messageId), true) + } + + writableDatabase.setTransactionSuccessful() + } finally { + writableDatabase.endTransaction() + } + } + + private fun hasReactions(messageId: MessageId): Boolean { + val query = "$MESSAGE_ID = ? AND $IS_MMS = ?" + val args = arrayOf("${messageId.id}", "${if (messageId.mms) 1 else 0}") + + readableDatabase.query(TABLE_NAME, arrayOf(MESSAGE_ID), query, args, null, null, null).use { cursor -> + return cursor.moveToFirst() + } + } + + fun getReactions(cursor: Cursor): List { + return try { + if (cursor.getColumnIndex(REACTION_JSON_ALIAS) != -1) { + if (cursor.isNull(cursor.getColumnIndexOrThrow(REACTION_JSON_ALIAS))) { + return listOf() + } + val result = mutableSetOf() + val array = JSONArray(cursor.getString(cursor.getColumnIndexOrThrow(REACTION_JSON_ALIAS))) + for (i in 0 until array.length()) { + val `object` = SaneJSONObject(array.getJSONObject(i)) + if (!`object`.isNull(ROW_ID)) { + result.add( + ReactionRecord( + `object`.getLong(ROW_ID), + `object`.getLong(MESSAGE_ID), + `object`.getInt(IS_MMS) == 1, + `object`.getString(AUTHOR_ID), + `object`.getString(EMOJI), + `object`.getString(SERVER_ID), + `object`.getLong(COUNT), + `object`.getLong(SORT_ID), + `object`.getLong(DATE_SENT), + `object`.getLong(DATE_RECEIVED) + ) + ) + } + } + result.sortedBy { it.dateSent } + } else { + listOf( + ReactionRecord( + cursor.getLong(cursor.getColumnIndexOrThrow(ROW_ID)), + cursor.getLong(cursor.getColumnIndexOrThrow(MESSAGE_ID)), + cursor.getInt(cursor.getColumnIndexOrThrow(IS_MMS)) == 1, + cursor.getString(cursor.getColumnIndexOrThrow(AUTHOR_ID)), + cursor.getString(cursor.getColumnIndexOrThrow(EMOJI)), + cursor.getString(cursor.getColumnIndexOrThrow(SERVER_ID)), + cursor.getLong(cursor.getColumnIndexOrThrow(COUNT)), + cursor.getLong(cursor.getColumnIndexOrThrow(SORT_ID)), + cursor.getLong(cursor.getColumnIndexOrThrow(DATE_SENT)), + cursor.getLong(cursor.getColumnIndexOrThrow(DATE_RECEIVED)) + ) + ) + } + } catch (e: JSONException) { + throw AssertionError(e) + } + } + + fun getReactionFor(timestamp: Long, sender: String): ReactionRecord? { + val query = "$DATE_SENT = ? AND $AUTHOR_ID = ?" + val args = arrayOf("$timestamp", sender) + + readableDatabase.query(TABLE_NAME, null, query, args, null, null, null).use { cursor -> + return if (cursor.moveToFirst()) readReaction(cursor) else null + } + } + + fun updateReaction(reaction: ReactionRecord) { + writableDatabase.beginTransaction() + try { + val values = ContentValues().apply { + put(EMOJI, reaction.emoji) + put(AUTHOR_ID, reaction.author) + put(SERVER_ID, reaction.serverId) + put(COUNT, reaction.count) + put(SORT_ID, reaction.sortId) + put(DATE_SENT, reaction.dateSent) + put(DATE_RECEIVED, reaction.dateReceived) + } + + val query = "$ROW_ID = ?" + val args = arrayOf("${reaction.id}") + writableDatabase.update(TABLE_NAME, values, query, args) + + writableDatabase.setTransactionSuccessful() + } finally { + writableDatabase.endTransaction() + } + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java index 9639ed0a4e..2834eb341d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/RecipientDatabase.java @@ -34,7 +34,7 @@ public class RecipientDatabase extends Database { private static final String TAG = RecipientDatabase.class.getSimpleName(); static final String TABLE_NAME = "recipient_preferences"; - private static final String ID = "_id"; + static final String ID = "_id"; public static final String ADDRESS = "recipient_ids"; static final String BLOCK = "block"; static final String APPROVED = "approved"; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 44ba225942..67243f73b6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -23,6 +23,9 @@ import android.database.Cursor; import android.text.TextUtils; import android.util.Pair; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + import com.annimon.stream.Stream; import net.sqlcipher.database.SQLiteDatabase; @@ -43,11 +46,13 @@ import org.session.libsignal.utilities.Log; import org.session.libsignal.utilities.guava.Optional; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.database.model.SmsMessageRecord; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import java.io.IOException; import java.security.SecureRandom; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -98,9 +103,24 @@ public class SmsDatabase extends MessagingDatabase { PROTOCOL, READ, STATUS, TYPE, REPLY_PATH_PRESENT, SUBJECT, BODY, SERVICE_CENTER, DELIVERY_RECEIPT_COUNT, MISMATCHED_IDENTITIES, SUBSCRIPTION_ID, EXPIRES_IN, EXPIRE_STARTED, - NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED + NOTIFIED, READ_RECEIPT_COUNT, UNIDENTIFIED, + "json_group_array(json_object(" + + "'" + ReactionDatabase.ROW_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.ROW_ID + ", " + + "'" + ReactionDatabase.MESSAGE_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + ", " + + "'" + ReactionDatabase.IS_MMS + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + ", " + + "'" + ReactionDatabase.AUTHOR_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.AUTHOR_ID + ", " + + "'" + ReactionDatabase.EMOJI + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.EMOJI + ", " + + "'" + ReactionDatabase.SERVER_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.SERVER_ID + ", " + + "'" + ReactionDatabase.COUNT + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.COUNT + ", " + + "'" + ReactionDatabase.SORT_ID + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.SORT_ID + ", " + + "'" + ReactionDatabase.DATE_SENT + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.DATE_SENT + ", " + + "'" + ReactionDatabase.DATE_RECEIVED + "', " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.DATE_RECEIVED + + ")) AS " + ReactionDatabase.REACTION_JSON_ALIAS }; + public static String CREATE_REACTIONS_UNREAD_COMMAND = "ALTER TABLE "+ TABLE_NAME + " " + + "ADD COLUMN " + REACTIONS_UNREAD + " INTEGER DEFAULT 0;"; + private static final EarlyReceiptCache earlyDeliveryReceiptCache = new EarlyReceiptCache(); private static final EarlyReceiptCache earlyReadReceiptCache = new EarlyReceiptCache(); @@ -294,7 +314,7 @@ public class SmsDatabase extends MessagingDatabase { } public List setMessagesRead(long threadId) { - return setMessagesRead(THREAD_ID + " = ? AND " + READ + " = 0", new String[] {String.valueOf(threadId)}); + return setMessagesRead(THREAD_ID + " = ? AND (" + READ + " = 0 OR " + REACTIONS_UNREAD + " = 1)", new String[] {String.valueOf(threadId)}); } public List setAllMessagesRead() { @@ -321,6 +341,7 @@ public class SmsDatabase extends MessagingDatabase { ContentValues contentValues = new ContentValues(); contentValues.put(READ, 1); + contentValues.put(REACTIONS_UNREAD, 0); database.update(TABLE_NAME, contentValues, where, arguments); database.setTransactionSuccessful(); @@ -533,15 +554,21 @@ public class SmsDatabase extends MessagingDatabase { return messageId; } + private Cursor rawQuery(@NonNull String where, @Nullable String[] arguments) { + SQLiteDatabase database = databaseHelper.getReadableDatabase(); + return database.rawQuery("SELECT " + Util.join(MESSAGE_PROJECTION, ",") + + " FROM " + SmsDatabase.TABLE_NAME + " LEFT OUTER JOIN " + ReactionDatabase.TABLE_NAME + + " ON (" + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID + " = " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.MESSAGE_ID + " AND " + ReactionDatabase.TABLE_NAME + "." + ReactionDatabase.IS_MMS + " = 0)" + + " WHERE " + where + " GROUP BY " + SmsDatabase.TABLE_NAME + "." + SmsDatabase.ID, arguments); + } + public Cursor getExpirationStartedMessages() { String where = EXPIRE_STARTED + " > 0"; - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - return db.query(TABLE_NAME, MESSAGE_PROJECTION, where, null, null, null, null); + return rawQuery(where, null); } public SmsMessageRecord getMessage(long messageId) throws NoSuchMessageException { - SQLiteDatabase db = databaseHelper.getReadableDatabase(); - Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""}, null, null, null); + Cursor cursor = rawQuery(ID_WHERE, new String[]{messageId + ""}); Reader reader = new Reader(cursor); SmsMessageRecord record = reader.getNext(); @@ -580,6 +607,11 @@ public class SmsDatabase extends MessagingDatabase { notifyConversationListListeners(); } + @Override + public MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException { + return getMessage(messageId); + } + private boolean isDuplicate(IncomingTextMessage message, long threadId) { SQLiteDatabase database = databaseHelper.getReadableDatabase(); Cursor cursor = database.query(TABLE_NAME, null, DATE_SENT + " = ? AND " + ADDRESS + " = ? AND " + THREAD_ID + " = ?", @@ -704,7 +736,7 @@ public class SmsDatabase extends MessagingDatabase { 0, message.isSecureMessage() ? MmsSmsColumns.Types.getOutgoingEncryptedMessageType() : MmsSmsColumns.Types.getOutgoingSmsMessageType(), threadId, 0, new LinkedList(), message.getExpiresIn(), - System.currentTimeMillis(), 0, false); + System.currentTimeMillis(), 0, false, Collections.emptyList()); } } @@ -752,12 +784,13 @@ public class SmsDatabase extends MessagingDatabase { List mismatches = getMismatches(mismatchDocument); Recipient recipient = Recipient.from(context, address, true); + List reactions = DatabaseComponent.get(context).reactionDatabase().getReactions(cursor); return new SmsMessageRecord(messageId, body, recipient, recipient, dateSent, dateReceived, deliveryReceiptCount, type, threadId, status, mismatches, - expiresIn, expireStarted, readReceiptCount, unidentified); + expiresIn, expireStarted, readReceiptCount, unidentified, reactions); } private List getMismatches(String document) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt index 88f9409838..a47aa87b2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/Storage.kt @@ -12,6 +12,7 @@ import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.JobQueue import org.session.libsession.messaging.jobs.MessageReceiveJob import org.session.libsession.messaging.jobs.MessageSendJob +import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.signal.IncomingEncryptedMessage @@ -22,6 +23,7 @@ import org.session.libsession.messaging.messages.signal.OutgoingGroupMediaMessag import org.session.libsession.messaging.messages.signal.OutgoingMediaMessage import org.session.libsession.messaging.messages.signal.OutgoingTextMessage import org.session.libsession.messaging.messages.visible.Attachment +import org.session.libsession.messaging.messages.visible.Reaction import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.GroupMember import org.session.libsession.messaging.open_groups.OpenGroup @@ -49,6 +51,8 @@ import org.session.libsignal.utilities.KeyHelper import org.session.libsignal.utilities.guava.Optional import org.thoughtcrime.securesms.ApplicationContext import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.database.model.ReactionRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.groups.OpenGroupManager import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob @@ -567,9 +571,8 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, OpenGroupManager.addOpenGroup(urlAsString, context) } - override fun onOpenGroupAdded(urlAsString: String) { - val server = OpenGroup.getServer(urlAsString) - OpenGroupManager.restartPollerForServer(server.toString().removeSuffix("/")) + override fun onOpenGroupAdded(server: String) { + OpenGroupManager.restartPollerForServer(server.removeSuffix("/")) } override fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean { @@ -888,4 +891,57 @@ class Storage(context: Context, helper: SQLCipherOpenHelper) : Database(context, db.addBlindedIdMapping(mapping) return mapping } + + override fun addReaction(reaction: Reaction) { + val timestamp = reaction.timestamp + val localId = reaction.localId + val isMms = reaction.isMms + val messageId = if (localId != null && localId > 0 && isMms != null) { + MessageId(localId, isMms) + } else if (timestamp != null && timestamp > 0) { + val messageRecord = DatabaseComponent.get(context).mmsSmsDatabase().getMessageForTimestamp(timestamp) ?: return + MessageId(messageRecord.id, messageRecord.isMms) + } else return + DatabaseComponent.get(context).reactionDatabase().addReaction( + messageId, + ReactionRecord( + messageId = messageId.id, + isMms = messageId.mms, + author = reaction.publicKey!!, + emoji = reaction.emoji!!, + serverId = reaction.serverId!!, + count = reaction.count!!, + sortId = reaction.index!!, + dateSent = reaction.dateSent!!, + dateReceived = reaction.dateReceived!! + ) + ) + } + + override fun removeReaction(emoji: String, messageTimestamp: Long, author: String) { + val messageRecord = DatabaseComponent.get(context).mmsSmsDatabase().getMessageForTimestamp(messageTimestamp) ?: return + val messageId = MessageId(messageRecord.id, messageRecord.isMms) + DatabaseComponent.get(context).reactionDatabase().deleteReaction(emoji, messageId, author) + } + + override fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long) { + val database = DatabaseComponent.get(context).reactionDatabase() + var reaction = database.getReactionFor(message.sentTimestamp!!, sender) ?: return + if (openGroupSentTimestamp != -1L) { + addReceivedMessageTimestamp(openGroupSentTimestamp) + reaction = reaction.copy(dateSent = openGroupSentTimestamp) + } + message.serverHash?.let { + reaction = reaction.copy(serverId = it) + } + message.openGroupServerMessageID?.let { + reaction = reaction.copy(serverId = "$it") + } + database.updateReaction(reaction) + } + + override fun deleteReactions(messageId: Long, mms: Boolean) { + DatabaseComponent.get(context).reactionDatabase().deleteMessageReactions(MessageId(messageId, mms)) + } + } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java index a64051ea27..db477c2c0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/ThreadDatabase.java @@ -34,6 +34,7 @@ import com.annimon.stream.Stream; import net.sqlcipher.database.SQLiteDatabase; +import org.jetbrains.annotations.NotNull; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.Contact; import org.session.libsession.utilities.DelimiterUtil; @@ -56,12 +57,15 @@ import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.ThreadRecord; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; +import org.thoughtcrime.securesms.groups.OpenGroupMigrator; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.notifications.MarkReadReceiver; import org.thoughtcrime.securesms.util.SessionMetaProtocol; import java.io.Closeable; +import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.LinkedList; import java.util.List; @@ -765,6 +769,88 @@ public class ThreadDatabase extends Database { return query; } + @NotNull + public List getHttpOxenOpenGroups() { + String where = TABLE_NAME+"."+ADDRESS+" LIKE ?"; + String selection = OpenGroupMigrator.HTTP_PREFIX+OpenGroupMigrator.OPEN_GET_SESSION_TRAILING_DOT_ENCODED +"%"; + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String query = createQuery(where, 0); + Cursor cursor = db.rawQuery(query, new String[]{selection}); + + if (cursor == null) { + return Collections.emptyList(); + } + List threads = new ArrayList<>(); + try { + Reader reader = readerFor(cursor); + ThreadRecord record; + while ((record = reader.getNext()) != null) { + threads.add(record); + } + } finally { + cursor.close(); + } + return threads; + } + + @NotNull + public List getLegacyOxenOpenGroups() { + String where = TABLE_NAME+"."+ADDRESS+" LIKE ?"; + String selection = OpenGroupMigrator.LEGACY_GROUP_ENCODED_ID+"%"; + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String query = createQuery(where, 0); + Cursor cursor = db.rawQuery(query, new String[]{selection}); + + if (cursor == null) { + return Collections.emptyList(); + } + List threads = new ArrayList<>(); + try { + Reader reader = readerFor(cursor); + ThreadRecord record; + while ((record = reader.getNext()) != null) { + threads.add(record); + } + } finally { + cursor.close(); + } + return threads; + } + + @NotNull + public List getHttpsOxenOpenGroups() { + String where = TABLE_NAME+"."+ADDRESS+" LIKE ?"; + String selection = OpenGroupMigrator.NEW_GROUP_ENCODED_ID+"%"; + SQLiteDatabase db = databaseHelper.getReadableDatabase(); + String query = createQuery(where, 0); + Cursor cursor = db.rawQuery(query, new String[]{selection}); + if (cursor == null) { + return Collections.emptyList(); + } + List threads = new ArrayList<>(); + try { + Reader reader = readerFor(cursor); + ThreadRecord record; + while ((record = reader.getNext()) != null) { + threads.add(record); + } + } finally { + cursor.close(); + } + return threads; + } + + public void migrateEncodedGroup(long threadId, @NotNull String newEncodedGroupId) { + ContentValues contentValues = new ContentValues(1); + contentValues.put(ADDRESS, newEncodedGroupId); + SQLiteDatabase db = databaseHelper.getWritableDatabase(); + db.update(TABLE_NAME, contentValues, ID_WHERE, new String[] {threadId+""}); + } + + public void notifyThreadUpdated(long threadId) { + notifyConversationListeners(threadId); + } + public interface ProgressListener { void onProgress(int complete, int total); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java index 6f17d64366..d2266b3924 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SQLCipherOpenHelper.java @@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.crypto.DatabaseSecret; import org.thoughtcrime.securesms.database.AttachmentDatabase; import org.thoughtcrime.securesms.database.BlindedIdMappingDatabase; import org.thoughtcrime.securesms.database.DraftDatabase; +import org.thoughtcrime.securesms.database.EmojiSearchDatabase; import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.GroupMemberDatabase; import org.thoughtcrime.securesms.database.GroupReceiptDatabase; @@ -27,6 +28,7 @@ import org.thoughtcrime.securesms.database.LokiThreadDatabase; import org.thoughtcrime.securesms.database.LokiUserDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.PushDatabase; +import org.thoughtcrime.securesms.database.ReactionDatabase; import org.thoughtcrime.securesms.database.RecipientDatabase; import org.thoughtcrime.securesms.database.SearchDatabase; import org.thoughtcrime.securesms.database.SessionContactDatabase; @@ -70,9 +72,12 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { private static final int lokiV33 = 54; private static final int lokiV34 = 55; private static final int lokiV35 = 56; + private static final int lokiV36 = 57; + private static final int lokiV37 = 58; + private static final int lokiV38 = 59; // Loki - onUpgrade(...) must be updated to use Loki version numbers if Signal makes any database changes - private static final int DATABASE_VERSION = lokiV35; + private static final int DATABASE_VERSION = lokiV38; private static final String DATABASE_NAME = "signal.db"; private final Context context; @@ -158,7 +163,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(GroupDatabase.getCreateUpdatedTimestampCommand()); db.execSQL(RecipientDatabase.getCreateApprovedCommand()); db.execSQL(RecipientDatabase.getCreateApprovedMeCommand()); - db.execSQL(MmsDatabase.createMessageRequestResponseCommand); + db.execSQL(MmsDatabase.CREATE_MESSAGE_REQUEST_RESPONSE_COMMAND); + db.execSQL(MmsDatabase.CREATE_REACTIONS_UNREAD_COMMAND); + db.execSQL(SmsDatabase.CREATE_REACTIONS_UNREAD_COMMAND); + db.execSQL(MmsDatabase.CREATE_REACTIONS_LAST_SEEN_COMMAND); db.execSQL(LokiAPIDatabase.CREATE_FORK_INFO_TABLE_COMMAND); db.execSQL(LokiAPIDatabase.CREATE_DEFAULT_FORK_INFO_COMMAND); db.execSQL(LokiAPIDatabase.UPDATE_HASHES_INCLUDE_NAMESPACE_COMMAND); @@ -169,6 +177,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(LokiAPIDatabase.DROP_LEGACY_RECEIVED_HASHES); db.execSQL(BlindedIdMappingDatabase.CREATE_BLINDED_ID_MAPPING_TABLE_COMMAND); db.execSQL(GroupMemberDatabase.CREATE_GROUP_MEMBER_TABLE_COMMAND); + db.execSQL(LokiAPIDatabase.RESET_SEQ_NO); // probably not needed but consistent with all migrations + db.execSQL(EmojiSearchDatabase.CREATE_EMOJI_SEARCH_TABLE_COMMAND); + db.execSQL(ReactionDatabase.CREATE_REACTION_TABLE_COMMAND); executeStatements(db, SmsDatabase.CREATE_INDEXS); executeStatements(db, MmsDatabase.CREATE_INDEXS); @@ -177,6 +188,8 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { executeStatements(db, DraftDatabase.CREATE_INDEXS); executeStatements(db, GroupDatabase.CREATE_INDEXS); executeStatements(db, GroupReceiptDatabase.CREATE_INDEXES); + + executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS); } @Override @@ -355,7 +368,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(RecipientDatabase.getCreateApprovedCommand()); db.execSQL(RecipientDatabase.getCreateApprovedMeCommand()); db.execSQL(RecipientDatabase.getUpdateApprovedCommand()); - db.execSQL(MmsDatabase.createMessageRequestResponseCommand); + db.execSQL(MmsDatabase.CREATE_MESSAGE_REQUEST_RESPONSE_COMMAND); } if (oldVersion < lokiV32) { @@ -385,6 +398,22 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper { db.execSQL(GroupMemberDatabase.CREATE_GROUP_MEMBER_TABLE_COMMAND); } + if (oldVersion < lokiV36) { + db.execSQL(LokiAPIDatabase.RESET_SEQ_NO); + } + + if (oldVersion < lokiV37) { + db.execSQL(MmsDatabase.CREATE_REACTIONS_UNREAD_COMMAND); + db.execSQL(SmsDatabase.CREATE_REACTIONS_UNREAD_COMMAND); + db.execSQL(MmsDatabase.CREATE_REACTIONS_LAST_SEEN_COMMAND); + db.execSQL(ReactionDatabase.CREATE_REACTION_TABLE_COMMAND); + executeStatements(db, ReactionDatabase.CREATE_REACTION_TRIGGERS); + } + + if (oldVersion < lokiV38) { + db.execSQL(EmojiSearchDatabase.CREATE_EMOJI_SEARCH_TABLE_COMMAND); + } + db.setTransactionSuccessful(); } finally { db.endTransaction(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/EmojiSearchData.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/EmojiSearchData.java new file mode 100644 index 0000000000..ad5cfe94bf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/EmojiSearchData.java @@ -0,0 +1,28 @@ +package org.thoughtcrime.securesms.database.model; + +import androidx.annotation.NonNull; + +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +/** + * Ties together an emoji with it's associated search tags. + */ +public final class EmojiSearchData { + @JsonProperty + private String emoji; + + @JsonProperty + private List tags; + + public EmojiSearchData() {} + + public @NonNull String getEmoji() { + return emoji; + } + + public @NonNull List getTags() { + return tags; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java index 3385ba3a56..570cb48bce 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MediaMmsMessageRecord.java @@ -18,8 +18,10 @@ package org.thoughtcrime.securesms.database.model; import android.content.Context; import android.text.SpannableString; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; + import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview; import org.session.libsession.utilities.Contact; import org.session.libsession.utilities.IdentityKeyMismatch; @@ -28,7 +30,9 @@ import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase.Status; import org.thoughtcrime.securesms.mms.SlideDeck; + import java.util.List; + import network.loki.messenger.R; /** @@ -43,21 +47,22 @@ public class MediaMmsMessageRecord extends MmsMessageRecord { private final int partCount; public MediaMmsMessageRecord(long id, Recipient conversationRecipient, - Recipient individualRecipient, int recipientDeviceId, - long dateSent, long dateReceived, int deliveryReceiptCount, - long threadId, String body, - @NonNull SlideDeck slideDeck, - int partCount, long mailbox, - List mismatches, - List failures, int subscriptionId, - long expiresIn, long expireStarted, int readReceiptCount, - @Nullable Quote quote, @NonNull List contacts, - @NonNull List linkPreviews, boolean unidentified) + Recipient individualRecipient, int recipientDeviceId, + long dateSent, long dateReceived, int deliveryReceiptCount, + long threadId, String body, + @NonNull SlideDeck slideDeck, + int partCount, long mailbox, + List mismatches, + List failures, int subscriptionId, + long expiresIn, long expireStarted, int readReceiptCount, + @Nullable Quote quote, @NonNull List contacts, + @NonNull List linkPreviews, + @NonNull List reactions, boolean unidentified) { super(id, body, conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures, expiresIn, expireStarted, slideDeck, readReceiptCount, quote, contacts, - linkPreviews, unidentified); + linkPreviews, unidentified, reactions); this.partCount = partCount; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageId.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageId.kt new file mode 100644 index 0000000000..e7155aa781 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageId.kt @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.database.model + +/** + * Represents a pair of values that can be used to find a message. Because we have two tables, + * that means this has both the primary key and a boolean indicating which table it's in. + */ +data class MessageId( + val id: Long, + @get:JvmName("isMms") val mms: Boolean +) { + fun serialize(): String { + return "$id|$mms" + } + + companion object { + /** + * Returns null for invalid IDs. Useful when pulling a possibly-unset ID from a database, or something like that. + */ + @JvmStatic + fun fromNullable(id: Long, mms: Boolean): MessageId? { + return if (id > 0) { + MessageId(id, mms) + } else { + null + } + } + + @JvmStatic + fun deserialize(serialized: String): MessageId { + val parts: List = serialized.split("|") + return MessageId(parts[0].toLong(), parts[1].toBoolean()) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 0f5247cf8b..da71103753 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -50,6 +50,7 @@ public abstract class MessageRecord extends DisplayRecord { private final long expireStarted; private final boolean unidentified; public final long id; + private final List reactions; public abstract boolean isMms(); public abstract boolean isMmsNotification(); @@ -61,7 +62,7 @@ public abstract class MessageRecord extends DisplayRecord { List mismatches, List networkFailures, long expiresIn, long expireStarted, - int readReceiptCount, boolean unidentified) + int readReceiptCount, boolean unidentified, List reactions) { super(body, conversationRecipient, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, readReceiptCount); @@ -72,6 +73,7 @@ public abstract class MessageRecord extends DisplayRecord { this.expiresIn = expiresIn; this.expireStarted = expireStarted; this.unidentified = unidentified; + this.reactions = reactions; } public long getId() { @@ -147,4 +149,9 @@ public abstract class MessageRecord extends DisplayRecord { public int hashCode() { return (int)getId(); } + + public @NonNull List getReactions() { + return reactions; + } + } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java index 937b74ec58..46e4199622 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MmsMessageRecord.java @@ -25,9 +25,9 @@ public abstract class MmsMessageRecord extends MessageRecord { List networkFailures, long expiresIn, long expireStarted, @NonNull SlideDeck slideDeck, int readReceiptCount, @Nullable Quote quote, @NonNull List contacts, - @NonNull List linkPreviews, boolean unidentified) + @NonNull List linkPreviews, boolean unidentified, List reactions) { - super(id, body, conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, expiresIn, expireStarted, readReceiptCount, unidentified); + super(id, body, conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, deliveryStatus, deliveryReceiptCount, type, mismatches, networkFailures, expiresIn, expireStarted, readReceiptCount, unidentified, reactions); this.slideDeck = slideDeck; this.quote = quote; this.contacts.addAll(contacts); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java index 4c3aa98868..c1c87800d2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/NotificationMmsMessageRecord.java @@ -16,17 +16,18 @@ */ package org.thoughtcrime.securesms.database.model; +import static java.util.Collections.emptyList; + import android.content.Context; import android.text.SpannableString; + import androidx.annotation.NonNull; -import org.session.libsession.utilities.IdentityKeyMismatch; -import org.session.libsession.utilities.NetworkFailure; + import org.session.libsession.utilities.recipients.Recipient; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase.Status; import org.thoughtcrime.securesms.mms.SlideDeck; -import java.util.Collections; -import java.util.LinkedList; + import network.loki.messenger.R; /** @@ -53,8 +54,8 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord { { super(id, "", conversationRecipient, individualRecipient, dateSent, dateReceived, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, - new LinkedList(), new LinkedList(), - 0, 0, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false); + emptyList(), emptyList(), + 0, 0, slideDeck, readReceiptCount, null, emptyList(), emptyList(), false, emptyList()); this.contentLocation = contentLocation; this.messageSize = messageSize; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.kt b/app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.kt new file mode 100644 index 0000000000..187c5b2d4b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/ReactionRecord.kt @@ -0,0 +1,14 @@ +package org.thoughtcrime.securesms.database.model + +data class ReactionRecord( + val id: Long = 0, + val messageId: Long, + val isMms: Boolean, + val author: String, + val emoji: String, + val serverId: String = "", + val count: Long = 0, + val sortId: Long = 0, + val dateSent: Long = 0, + val dateReceived: Long = 0 +) \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java index 319ff6fcaf..c1d50def2f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/SmsMessageRecord.java @@ -43,12 +43,12 @@ public class SmsMessageRecord extends MessageRecord { long type, long threadId, int status, List mismatches, long expiresIn, long expireStarted, - int readReceiptCount, boolean unidentified) + int readReceiptCount, boolean unidentified, List reactions) { super(id, body, recipient, individualRecipient, dateSent, dateReceived, threadId, status, deliveryReceiptCount, type, mismatches, new LinkedList<>(), - expiresIn, expireStarted, readReceiptCount, unidentified); + expiresIn, expireStarted, readReceiptCount, unidentified, reactions); } public long getType() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt index 4b1d346cbf..648b9c43ec 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseComponent.kt @@ -40,6 +40,8 @@ interface DatabaseComponent { fun lokiBackupFilesDatabase(): LokiBackupFilesDatabase fun sessionJobDatabase(): SessionJobDatabase fun sessionContactDatabase(): SessionContactDatabase + fun reactionDatabase(): ReactionDatabase + fun emojiSearchDatabase(): EmojiSearchDatabase fun storage(): Storage fun attachmentProvider(): MessageDataProvider fun blindedIdMappingDatabase(): BlindedIdMappingDatabase diff --git a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt index a6afdc75f3..029daefbf1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/dependencies/DatabaseModule.kt @@ -125,6 +125,14 @@ object DatabaseModule { @Singleton fun provideGroupMemberDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = GroupMemberDatabase(context, openHelper) + @Provides + @Singleton + fun provideReactionDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = ReactionDatabase(context, openHelper) + + @Provides + @Singleton + fun provideEmojiSearchDatabase(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = EmojiSearchDatabase(context, openHelper) + @Provides @Singleton fun provideStorage(@ApplicationContext context: Context, openHelper: SQLCipherOpenHelper) = Storage(context,openHelper) diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt new file mode 100644 index 0000000000..714996e6c8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiCategory.kt @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.emoji + +import androidx.annotation.AttrRes +import androidx.annotation.StringRes +import network.loki.messenger.R + +/** + * All the different Emoji categories the app is aware of in the order we want to display them. + */ +enum class EmojiCategory(val priority: Int, val key: String, @AttrRes val icon: Int) { + PEOPLE(0, "People", R.attr.emoji_category_people), + NATURE(1, "Nature", R.attr.emoji_category_nature), + FOODS(2, "Foods", R.attr.emoji_category_foods), + ACTIVITY(3, "Activity", R.attr.emoji_category_activity), + PLACES(4, "Places", R.attr.emoji_category_places), + OBJECTS(5, "Objects", R.attr.emoji_category_objects), + SYMBOLS(6, "Symbols", R.attr.emoji_category_symbol), + FLAGS(7, "Flags", R.attr.emoji_category_flags), + EMOTICONS(8, "Emoticons", R.attr.emoji_category_emoticons); + + @StringRes + fun getCategoryLabel(): Int { + return getCategoryLabel(icon) + } + + companion object { + @JvmStatic + fun forKey(key: String) = values().first { it.key == key } + + @JvmStatic + @StringRes + fun getCategoryLabel(@AttrRes iconAttr: Int): Int { + return when (iconAttr) { + R.attr.emoji_category_people -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people + R.attr.emoji_category_nature -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature + R.attr.emoji_category_foods -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food + R.attr.emoji_category_activity -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities + R.attr.emoji_category_places -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places + R.attr.emoji_category_objects -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects + R.attr.emoji_category_symbol -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols + R.attr.emoji_category_flags -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags + R.attr.emoji_category_emoticons -> R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons + else -> throw AssertionError() + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiJsonParser.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiJsonParser.kt new file mode 100644 index 0000000000..97be172adb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiJsonParser.kt @@ -0,0 +1,138 @@ +package org.thoughtcrime.securesms.emoji + +import android.net.Uri +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import org.session.libsignal.utilities.Hex +import org.thoughtcrime.securesms.components.emoji.CompositeEmojiPageModel +import org.thoughtcrime.securesms.components.emoji.Emoji +import org.thoughtcrime.securesms.components.emoji.EmojiPageModel +import org.thoughtcrime.securesms.components.emoji.StaticEmojiPageModel +import java.io.InputStream +import java.nio.charset.Charset + +typealias UriFactory = (sprite: String, format: String) -> Uri + +/** + * Takes an emoji_data.json file data and parses it into an EmojiSource + */ +object EmojiJsonParser { + private val OBJECT_MAPPER = ObjectMapper() + private const val ESTIMATED_EMOJI_COUNT = 3500 + + @JvmStatic + fun verify(body: InputStream) { + parse(body) { _, _ -> Uri.EMPTY }.getOrThrow() + } + + fun parse(body: InputStream, uriFactory: UriFactory): Result { + return try { + Result.success(buildEmojiSourceFromNode(OBJECT_MAPPER.readTree(body), uriFactory)) + } catch (e: Exception) { + Result.failure(e) + } + } + + private fun buildEmojiSourceFromNode(node: JsonNode, uriFactory: UriFactory): ParsedEmojiData { + val format: String = node["format"].textValue() + val obsolete: List = node["obsolete"].toObseleteList() + val dataPages: List = getDataPages(format, node["emoji"], uriFactory) + val jumboPages: Map = getJumboPages(node["jumbomoji"]) + val displayPages: List = mergeToDisplayPages(dataPages) + val metrics: EmojiMetrics = node["metrics"].toEmojiMetrics() + val densities: List = node["densities"].toDensityList() + + return ParsedEmojiData(metrics, densities, format, displayPages, dataPages, jumboPages, obsolete) + } + + private fun getDataPages(format: String, emoji: JsonNode, uriFactory: UriFactory): List { + return emoji.fields() + .asSequence() + .sortedWith { lhs, rhs -> + val lhsCategory = EmojiCategory.forKey(lhs.key.asCategoryKey()) + val rhsCategory = EmojiCategory.forKey(rhs.key.asCategoryKey()) + val comp = lhsCategory.priority.compareTo(rhsCategory.priority) + + if (comp == 0) { + val lhsIndex = lhs.key.getPageIndex() + val rhsIndex = rhs.key.getPageIndex() + + lhsIndex.compareTo(rhsIndex) + } else { + comp + } + } + .map { createPage(it.key, format, it.value, uriFactory) } + .toList() + } + + private fun getJumboPages(jumbo: JsonNode?): Map { + if (jumbo != null) { + return jumbo.fields() + .asSequence() + .map { (page: String, node: JsonNode) -> + node.associate { it.textValue() to page } + } + .flatMap { it.entries } + .associateTo(HashMap(ESTIMATED_EMOJI_COUNT)) { it.key to it.value } + } + return emptyMap() + } + + private fun createPage(pageName: String, format: String, page: JsonNode, uriFactory: UriFactory): EmojiPageModel { + val category = EmojiCategory.forKey(pageName.asCategoryKey()) + val pageList = page.mapIndexed { i, data -> + if (data.size() == 0) { + throw IllegalStateException("Page index $pageName.$i had no data") + } else { + val variations: MutableList = mutableListOf() + val rawVariations: MutableList = mutableListOf() + data.forEach { + variations += it.textValue().encodeAsUtf16() + rawVariations += it.textValue() + } + + Emoji(variations, rawVariations) + } + } + + return StaticEmojiPageModel(category, pageList, uriFactory(pageName, format)) + } + + private fun mergeToDisplayPages(dataPages: List): List { + return dataPages.groupBy { it.iconAttr } + .map { (icon, pages) -> if (pages.size <= 1) pages.first() else CompositeEmojiPageModel(icon, pages) } + } +} + +private fun JsonNode?.toObseleteList(): List { + return if (this == null) { + listOf() + } else { + map { node -> + ObsoleteEmoji(node["obsoleted"].textValue().encodeAsUtf16(), node["replace_with"].textValue().encodeAsUtf16()) + }.toList() + } +} + +private fun JsonNode.toEmojiMetrics(): EmojiMetrics { + return EmojiMetrics(this["raw_width"].asInt(), this["raw_height"].asInt(), this["per_row"].asInt()) +} + +private fun JsonNode.toDensityList(): List { + return map { it.textValue() } +} + +private fun String.encodeAsUtf16() = String(Hex.fromStringCondensed(this), Charset.forName("UTF-16")) +private fun String.asCategoryKey() = replace("(_\\d+)*$".toRegex(), "") +private fun String.getPageIndex() = "^.*_(\\d+)+$".toRegex().find(this)?.let { it.groupValues[1] }?.toInt() ?: throw IllegalStateException("No index.") + +data class ParsedEmojiData( + override val metrics: EmojiMetrics, + override val densities: List, + override val format: String, + override val displayPages: List, + override val dataPages: List, + override val jumboPages: Map, + override val obsolete: List +) : EmojiData diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPage.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPage.kt new file mode 100644 index 0000000000..dbd10cd424 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPage.kt @@ -0,0 +1,16 @@ +package org.thoughtcrime.securesms.emoji + +import android.net.Uri +import com.bumptech.glide.load.Key +import java.security.MessageDigest + +typealias EmojiPageFactory = (Uri) -> EmojiPage + +sealed class EmojiPage(open val uri: Uri) : Key { + override fun updateDiskCacheKey(messageDigest: MessageDigest) { + messageDigest.update("EmojiPage".encodeToByteArray()) + messageDigest.update(uri.toString().encodeToByteArray()) + } + + data class Asset(override val uri: Uri) : EmojiPage(uri) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPageCache.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPageCache.kt new file mode 100644 index 0000000000..56ff13141e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiPageCache.kt @@ -0,0 +1,85 @@ +package org.thoughtcrime.securesms.emoji + +import android.content.Context +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.annotation.MainThread +import androidx.annotation.WorkerThread +import org.session.libsession.utilities.ListenableFutureTask +import org.session.libsession.utilities.SoftHashMap +import org.session.libsession.utilities.concurrent.SimpleTask +import org.session.libsignal.utilities.Log +import java.io.IOException +import java.io.InputStream + +object EmojiPageCache { + + private val TAG = Log.tag(EmojiPageCache::class.java) + + private val cache: SoftHashMap = SoftHashMap() + private val tasks: HashMap> = hashMapOf() + + @MainThread + fun load(context: Context, emojiPage: EmojiPage, inSampleSize: Int): LoadResult { + val applicationContext = context.applicationContext + val emojiPageRequest = EmojiPageRequest(emojiPage, inSampleSize) + val bitmap: Bitmap? = cache[emojiPageRequest] + val task: ListenableFutureTask? = tasks[emojiPageRequest] + + return when { + bitmap != null -> LoadResult.Immediate(bitmap) + task != null -> LoadResult.Async(task) + else -> { + val newTask = ListenableFutureTask { + try { + Log.i(TAG, "Loading page $emojiPageRequest") + loadInternal(applicationContext, emojiPageRequest) + } catch (e: IOException) { + Log.w(TAG, e) + null + } + } + + tasks[emojiPageRequest] = newTask + + SimpleTask.run(newTask::run) { + try { + val newBitmap: Bitmap? = newTask.get() + if (newBitmap == null) { + Log.w(TAG, "Failed to load emoji bitmap for request $emojiPageRequest") + } else { + cache[emojiPageRequest] = newBitmap + } + } finally { + tasks.remove(emojiPageRequest) + } + } + + LoadResult.Async(newTask) + } + } + } + + fun clear() { + cache.clear() + } + + @WorkerThread + private fun loadInternal(context: Context, emojiPageRequest: EmojiPageRequest): Bitmap? { + val inputStream: InputStream = when (emojiPageRequest.emojiPage) { + is EmojiPage.Asset -> context.assets.open(emojiPageRequest.emojiPage.uri.toString().replace("file:///android_asset/", "")) + } + + val bitmapOptions = BitmapFactory.Options() + bitmapOptions.inSampleSize = emojiPageRequest.inSampleSize + + return inputStream.use { BitmapFactory.decodeStream(it, null, bitmapOptions) } + } + + private data class EmojiPageRequest(val emojiPage: EmojiPage, val inSampleSize: Int) + + sealed class LoadResult { + data class Immediate(val bitmap: Bitmap) : LoadResult() + data class Async(val task: ListenableFutureTask) : LoadResult() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt new file mode 100644 index 0000000000..0b221eb3d1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt @@ -0,0 +1,161 @@ +package org.thoughtcrime.securesms.emoji + +import android.net.Uri +import androidx.annotation.WorkerThread +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.thoughtcrime.securesms.ApplicationContext +import org.thoughtcrime.securesms.components.emoji.Emoji +import org.thoughtcrime.securesms.components.emoji.EmojiPageModel +import org.thoughtcrime.securesms.components.emoji.StaticEmojiPageModel +import org.thoughtcrime.securesms.components.emoji.parsing.EmojiDrawInfo +import org.thoughtcrime.securesms.components.emoji.parsing.EmojiTree +import org.thoughtcrime.securesms.util.ScreenDensity +import java.io.InputStream +import java.util.concurrent.CountDownLatch +import java.util.concurrent.atomic.AtomicReference + +/** + * The entry point for the application to request Emoji data for custom emojis. + */ +class EmojiSource( + val decodeScale: Float, + private val emojiData: EmojiData, + private val emojiPageFactory: EmojiPageFactory +) : EmojiData by emojiData { + + val variationsToCanonical: Map by lazy { + val map = mutableMapOf() + + for (page: EmojiPageModel in dataPages) { + for (emoji: Emoji in page.displayEmoji) { + for (variation: String in emoji.variations) { + map[variation] = emoji.value + } + } + } + + map + } + + val canonicalToVariations: Map> by lazy { + val map = mutableMapOf>() + + for (page: EmojiPageModel in dataPages) { + for (emoji: Emoji in page.displayEmoji) { + map[emoji.value] = emoji.variations + } + } + + map + } + + val maxEmojiLength: Int by lazy { + dataPages.map { it.emoji.map(String::length) } + .flatten() + .maxOrZero() + } + + val emojiTree: EmojiTree by lazy { + val tree = EmojiTree() + + dataPages + .filter { it.spriteUri != null } + .forEach { page -> + val emojiPage = emojiPageFactory(page.spriteUri!!) + + var overallIndex = 0 + page.displayEmoji.forEach { emoji: Emoji -> + emoji.variations.forEachIndexed { variationIndex, variation -> + val raw = emoji.getRawVariation(variationIndex) + tree.add(variation, EmojiDrawInfo(emojiPage, overallIndex++, variation, raw, jumboPages[raw])) + } + } + } + + obsolete.forEach { + tree.add(it.obsolete, tree.getEmoji(it.replaceWith, 0, it.replaceWith.length)) + } + + tree + } + + companion object { + + private val emojiSource = AtomicReference() + private val emojiLatch = CountDownLatch(1) + + @JvmStatic + val latest: EmojiSource + get() { + emojiLatch.await() + return emojiSource.get() + } + + @JvmStatic + @WorkerThread + fun refresh() { + emojiSource.set(getEmojiSource()) + emojiLatch.countDown() + } + + private fun getEmojiSource(): EmojiSource { + return loadAssetBasedEmojis() + } + + private fun loadAssetBasedEmojis(): EmojiSource { + val context = MessagingModuleConfiguration.shared.context + val emojiData: InputStream = ApplicationContext.getInstance(context).assets.open("emoji/emoji_data.json") + + emojiData.use { + val parsedData: ParsedEmojiData = EmojiJsonParser.parse(it, ::getAssetsUri).getOrThrow() + return EmojiSource( + ScreenDensity.xhdpiRelativeDensityScaleFactor("xhdpi"), + parsedData.copy( + displayPages = parsedData.displayPages + PAGE_EMOTICONS, + dataPages = parsedData.dataPages + PAGE_EMOTICONS + ) + ) { uri: Uri -> EmojiPage.Asset(uri) } + } + } + } +} + +private fun List.maxOrZero(): Int = maxOrNull() ?: 0 + +interface EmojiData { + val metrics: EmojiMetrics + val densities: List + val format: String + val displayPages: List + val dataPages: List + val jumboPages: Map + val obsolete: List +} + +data class ObsoleteEmoji(val obsolete: String, val replaceWith: String) + +data class EmojiMetrics(val rawHeight: Int, val rawWidth: Int, val perRow: Int) + +private fun getAssetsUri(name: String, format: String): Uri = Uri.parse("file:///android_asset/emoji/$name.$format") + +private val PAGE_EMOTICONS: EmojiPageModel = StaticEmojiPageModel( + EmojiCategory.EMOTICONS, + arrayOf( + ":-)", ";-)", "(-:", ":->", ":-D", "\\o/", + ":-P", "B-)", ":-$", ":-*", "O:-)", "=-O", + "O_O", "O_o", "o_O", ":O", ":-!", ":-x", + ":-|", ":-\\", ":-(", ":'(", ":-[", ">:-(", + "^.^", "^_^", "\\(\u02c6\u02da\u02c6)/", + "\u30fd(\u00b0\u25c7\u00b0 )\u30ce", "\u00af\\(\u00b0_o)/\u00af", + "\u00af\\_(\u30c4)_/\u00af", "(\u00ac_\u00ac)", + "(>_<)", "(\u2565\ufe4f\u2565)", "(\u261e\uff9f\u30ee\uff9f)\u261e", + "\u261c(\uff9f\u30ee\uff9f\u261c)", "\u261c(\u2312\u25bd\u2312)\u261e", + "(\u256f\u00b0\u25a1\u00b0)\u256f\ufe35", "\u253b\u2501\u253b", + "\u252c\u2500\u252c", "\u30ce(\u00b0\u2013\u00b0\u30ce)", + "(^._.^)\uff89", "\u0e05^\u2022\ufecc\u2022^\u0e05", + "\u0295\u2022\u1d25\u2022\u0294", "(\u2022_\u2022)", + " \u25a0-\u25a0\u00ac <(\u2022_\u2022) ", "(\u25a0_\u25a0\u00ac)", + "\u01aa(\u0693\u05f2)\u200e\u01aa\u200b\u200b" + ), + null +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt index c2d37ba2e9..ca7ff0af17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/JoinPublicChatActivity.kt @@ -24,14 +24,14 @@ import kotlinx.coroutines.withContext import network.loki.messenger.R import network.loki.messenger.databinding.ActivityJoinPublicChatBinding import network.loki.messenger.databinding.FragmentEnterChatUrlBinding -import okhttp3.HttpUrl import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.messaging.open_groups.OpenGroupApi.DefaultGroup import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.OpenGroupUrlParser +import org.session.libsession.utilities.OpenGroupUrlParser.Error import org.session.libsession.utilities.recipients.Recipient import org.session.libsignal.utilities.Log -import org.session.libsignal.utilities.PublicKeyValidation import org.thoughtcrime.securesms.BaseActionBarActivity import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.conversation.v2.ConversationActivityV2 @@ -83,32 +83,27 @@ class JoinPublicChatActivity : PassphraseRequiredActionBarActivity(), ScanQRCode fun joinPublicChatIfPossible(url: String) { // Add "http" if not entered explicitly - val stringWithExplicitScheme = if (!url.startsWith("http")) "http://$url" else url - val url = HttpUrl.parse(stringWithExplicitScheme) ?: return Toast.makeText(this,R.string.invalid_url, Toast.LENGTH_SHORT).show() - val room = url.pathSegments().firstOrNull() - val publicKey = url.queryParameter("public_key") - val isV2OpenGroup = !room.isNullOrEmpty() - if (isV2OpenGroup && (publicKey == null || !PublicKeyValidation.isValid(publicKey, 64,false))) { - return Toast.makeText(this, R.string.invalid_public_key, Toast.LENGTH_SHORT).show() + val openGroup = try { + OpenGroupUrlParser.parseUrl(url) + } catch (e: Error) { + when (e) { + is Error.MalformedURL -> return Toast.makeText(this, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() + is Error.InvalidPublicKey -> return Toast.makeText(this, R.string.invalid_public_key, Toast.LENGTH_SHORT).show() + is Error.NoPublicKey -> return Toast.makeText(this, R.string.invalid_public_key, Toast.LENGTH_SHORT).show() + is Error.NoRoom -> return Toast.makeText(this, R.string.activity_join_public_chat_error, Toast.LENGTH_SHORT).show() + } } showLoader() lifecycleScope.launch(Dispatchers.IO) { try { - val (threadID, groupID) = if (isV2OpenGroup) { - val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).apply { - if (url.port() != 80 || url.port() != 443) { this.port(url.port()) } // Non-standard port; add to server - }.build() + val sanitizedServer = openGroup.server.removeSuffix("/") + val openGroupID = "$sanitizedServer.${openGroup.room}" + OpenGroupManager.add(sanitizedServer, openGroup.room, openGroup.serverPublicKey, this@JoinPublicChatActivity) + val storage = MessagingModuleConfiguration.shared.storage + storage.onOpenGroupAdded(sanitizedServer) + val threadID = GroupManager.getOpenGroupThreadID(openGroupID, this@JoinPublicChatActivity) + val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray()) - val sanitizedServer = server.toString().removeSuffix("/") - val openGroupID = "$sanitizedServer.${room!!}" - OpenGroupManager.add(sanitizedServer, room, publicKey!!, this@JoinPublicChatActivity) - MessagingModuleConfiguration.shared.storage.onOpenGroupAdded(stringWithExplicitScheme) - val threadID = GroupManager.getOpenGroupThreadID(openGroupID, this@JoinPublicChatActivity) - val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray()) - threadID to groupID - } else { - throw Exception("No longer supported.") - } ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(this@JoinPublicChatActivity) withContext(Dispatchers.Main) { val recipient = Recipient.from(this@JoinPublicChatActivity, Address.fromSerialized(groupID), false) diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt index 82ba43ab15..d39ba709df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupManager.kt @@ -8,6 +8,7 @@ import org.session.libsession.messaging.open_groups.GroupMemberRole import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.pollers.OpenGroupPoller +import org.session.libsignal.utilities.Log import org.session.libsignal.utilities.ThreadUtils import org.thoughtcrime.securesms.dependencies.DatabaseComponent import java.util.concurrent.Executors @@ -71,7 +72,7 @@ object OpenGroupManager { storage.removeLastInboxMessageId(server) storage.removeLastOutboxMessageId(server) // Store the public key - storage.setOpenGroupPublicKey(server,publicKey) + storage.setOpenGroupPublicKey(server, publicKey) // Get capabilities val capabilities = OpenGroupApi.getCapabilities(server).get() storage.setServerCapabilities(server, capabilities.capabilities) @@ -92,6 +93,7 @@ object OpenGroupManager { pollers[server]?.stop() pollers[server]?.startIfNeeded() ?: run { val poller = OpenGroupPoller(server, executorService) + Log.d("Loki", "Starting poller for open group: $server") pollers[server] = poller poller.startIfNeeded() } @@ -133,7 +135,7 @@ object OpenGroupManager { val server = OpenGroup.getServer(urlAsString) val room = url.pathSegments().firstOrNull() ?: return val publicKey = url.queryParameter("public_key") ?: return - add(server.toString().removeSuffix("/"), room, publicKey, context) + add(server.toString().removeSuffix("/"), room, publicKey, context) // assume migrated from calling function } fun updateOpenGroup(openGroup: OpenGroup, context: Context) { @@ -148,8 +150,12 @@ object OpenGroupManager { val standardRoles = memberDatabase.getGroupMemberRoles(groupId, standardPublicKey) val blindedRoles = blindedPublicKey?.let { memberDatabase.getGroupMemberRoles(groupId, it) } ?: emptyList() - return GroupMemberRole.ADMIN in standardRoles || GroupMemberRole.MODERATOR in standardRoles || - GroupMemberRole.ADMIN in blindedRoles || GroupMemberRole.MODERATOR in blindedRoles + // roles to check against + val moderatorRoles = listOf( + GroupMemberRole.MODERATOR, GroupMemberRole.ADMIN, + GroupMemberRole.HIDDEN_MODERATOR, GroupMemberRole.HIDDEN_ADMIN + ) + return standardRoles.any { it in moderatorRoles } || blindedRoles.any { it in moderatorRoles } } } \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupMigrator.kt b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupMigrator.kt new file mode 100644 index 0000000000..642d191614 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/groups/OpenGroupMigrator.kt @@ -0,0 +1,139 @@ +package org.thoughtcrime.securesms.groups + +import androidx.annotation.VisibleForTesting +import org.session.libsession.messaging.open_groups.OpenGroupApi +import org.session.libsession.utilities.recipients.Recipient +import org.session.libsignal.utilities.Hex +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.dependencies.DatabaseComponent + +object OpenGroupMigrator { + const val HTTP_PREFIX = "__loki_public_chat_group__!687474703a2f2f" + private const val HTTPS_PREFIX = "__loki_public_chat_group__!68747470733a2f2f" + const val OPEN_GET_SESSION_TRAILING_DOT_ENCODED = "6f70656e2e67657473657373696f6e2e6f72672e" + const val LEGACY_GROUP_ENCODED_ID = "__loki_public_chat_group__!687474703a2f2f3131362e3230332e37302e33332e" // old IP based toByteArray() + const val NEW_GROUP_ENCODED_ID = "__loki_public_chat_group__!68747470733a2f2f6f70656e2e67657473657373696f6e2e6f72672e" // new URL based toByteArray() + + data class OpenGroupMapping(val stub: String, val legacyThreadId: Long, val newThreadId: Long?) + + @VisibleForTesting + fun Recipient.roomStub(): String? { + if (!isOpenGroupRecipient) return null + val serialized = address.serialize() + if (serialized.startsWith(LEGACY_GROUP_ENCODED_ID)) { + return serialized.replace(LEGACY_GROUP_ENCODED_ID,"") + } else if (serialized.startsWith(NEW_GROUP_ENCODED_ID)) { + return serialized.replace(NEW_GROUP_ENCODED_ID,"") + } else if (serialized.startsWith(HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED)) { + return serialized.replace(HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED, "") + } + return null + } + + @VisibleForTesting + fun getExistingMappings(legacy: List, new: List): List { + val legacyStubsMapping = legacy.mapNotNull { thread -> + val stub = thread.recipient.roomStub() + stub?.let { it to thread.threadId } + } + val newStubsMapping = new.mapNotNull { thread -> + val stub = thread.recipient.roomStub() + stub?.let { it to thread.threadId } + } + return legacyStubsMapping.map { (legacyEncodedStub, legacyId) -> + // get 'new' open group thread ID if stubs match + OpenGroupMapping( + legacyEncodedStub, + legacyId, + newStubsMapping.firstOrNull { (newEncodedStub, _) -> newEncodedStub == legacyEncodedStub }?.second + ) + } + } + + @JvmStatic + fun migrate(databaseComponent: DatabaseComponent) { + // migrate thread db + val threadDb = databaseComponent.threadDatabase() + + val legacyOpenGroups = threadDb.legacyOxenOpenGroups + val httpBasedNewGroups = threadDb.httpOxenOpenGroups + if (legacyOpenGroups.isEmpty() && httpBasedNewGroups.isEmpty()) return // no need to migrate + + val newOpenGroups = threadDb.httpsOxenOpenGroups + val firstStepMigration = getExistingMappings(legacyOpenGroups, newOpenGroups) + + val secondStepMigration = getExistingMappings(httpBasedNewGroups, newOpenGroups) + + val groupDb = databaseComponent.groupDatabase() + val lokiApiDb = databaseComponent.lokiAPIDatabase() + val smsDb = databaseComponent.smsDatabase() + val mmsDb = databaseComponent.mmsDatabase() + val lokiMessageDatabase = databaseComponent.lokiMessageDatabase() + val lokiThreadDatabase = databaseComponent.lokiThreadDatabase() + + firstStepMigration.forEach { (stub, old, new) -> + val legacyEncodedGroupId = LEGACY_GROUP_ENCODED_ID+stub + if (new == null) { + val newEncodedGroupId = NEW_GROUP_ENCODED_ID+stub + // migrate thread and group encoded values + threadDb.migrateEncodedGroup(old, newEncodedGroupId) + groupDb.migrateEncodedGroup(legacyEncodedGroupId, newEncodedGroupId) + // migrate Loki API DB values + // decode the hex to bytes, decode byte array to string i.e. "oxen" or "session" + val decodedStub = Hex.fromStringCondensed(stub).decodeToString() + val legacyLokiServerId = "${OpenGroupApi.legacyDefaultServer}.$decodedStub" + val newLokiServerId = "${OpenGroupApi.defaultServer}.$decodedStub" + lokiApiDb.migrateLegacyOpenGroup(legacyLokiServerId, newLokiServerId) + // migrate loki thread db server info + val oldServerInfo = lokiThreadDatabase.getOpenGroupChat(old) + val newServerInfo = oldServerInfo!!.copy(server = OpenGroupApi.defaultServer, id = newLokiServerId) + lokiThreadDatabase.setOpenGroupChat(newServerInfo, old) + } else { + // has a legacy and a new one + // migrate SMS and MMS tables + smsDb.migrateThreadId(old, new) + mmsDb.migrateThreadId(old, new) + lokiMessageDatabase.migrateThreadId(old, new) + // delete group for legacy ID + groupDb.delete(legacyEncodedGroupId) + // delete thread for legacy ID + threadDb.deleteConversation(old) + lokiThreadDatabase.removeOpenGroupChat(old) + } + // maybe migrate jobs here + } + + secondStepMigration.forEach { (stub, old, new) -> + val legacyEncodedGroupId = HTTP_PREFIX + OPEN_GET_SESSION_TRAILING_DOT_ENCODED + stub + if (new == null) { + val newEncodedGroupId = NEW_GROUP_ENCODED_ID+stub + // migrate thread and group encoded values + threadDb.migrateEncodedGroup(old, newEncodedGroupId) + groupDb.migrateEncodedGroup(legacyEncodedGroupId, newEncodedGroupId) + // migrate Loki API DB values + // decode the hex to bytes, decode byte array to string i.e. "oxen" or "session" + val decodedStub = Hex.fromStringCondensed(stub).decodeToString() + val legacyLokiServerId = "${OpenGroupApi.httpDefaultServer}.$decodedStub" + val newLokiServerId = "${OpenGroupApi.defaultServer}.$decodedStub" + lokiApiDb.migrateLegacyOpenGroup(legacyLokiServerId, newLokiServerId) + // migrate loki thread db server info + val oldServerInfo = lokiThreadDatabase.getOpenGroupChat(old) + val newServerInfo = oldServerInfo!!.copy(server = OpenGroupApi.defaultServer, id = newLokiServerId) + lokiThreadDatabase.setOpenGroupChat(newServerInfo, old) + } else { + // has a legacy and a new one + // migrate SMS and MMS tables + smsDb.migrateThreadId(old, new) + mmsDb.migrateThreadId(old, new) + lokiMessageDatabase.migrateThreadId(old, new) + // delete group for legacy ID + groupDb.delete(legacyEncodedGroupId) + // delete thread for legacy ID + threadDb.deleteConversation(old) + lokiThreadDatabase.removeOpenGroupChat(old) + } + // maybe migrate jobs here + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt new file mode 100644 index 0000000000..13a4eb6ff0 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt @@ -0,0 +1,195 @@ +package org.thoughtcrime.securesms.keyboard.emoji + +import android.animation.Animator +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.ColorDrawable +import android.util.AttributeSet +import android.view.View +import android.widget.EditText +import androidx.appcompat.widget.AppCompatImageView +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.content.ContextCompat +import androidx.core.content.res.use +import androidx.core.view.ViewCompat +import androidx.core.view.isVisible +import androidx.core.widget.ImageViewCompat +import androidx.core.widget.doAfterTextChanged +import network.loki.messenger.R +import org.thoughtcrime.securesms.animation.AnimationCompleteListener +import org.thoughtcrime.securesms.animation.ResizeAnimation +import org.thoughtcrime.securesms.conversation.v2.ViewUtil + +private const val REVEAL_DURATION = 250L + +/** + * Search bar to be used in the various keyboard views (emoji, sticker, gif) + */ +class KeyboardPageSearchView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : ConstraintLayout(context, attrs, defStyleAttr) { + + var callbacks: Callbacks? = null + + private var state: State = State.HIDE_REQUESTED + private var targetInputWidth: Int = -1 + + private val navButton: AppCompatImageView + private val clearButton: AppCompatImageView + private val input: EditText + + init { + inflate(context, R.layout.keyboard_pager_search_bar, this) + + navButton = findViewById(R.id.emoji_search_nav_icon) + clearButton = findViewById(R.id.emoji_search_clear_icon) + input = findViewById(R.id.emoji_search_entry) + + input.doAfterTextChanged { + if (it.isNullOrEmpty()) { + clearButton.setImageDrawable(null) + clearButton.isClickable = false + } else { + clearButton.setImageResource(R.drawable.ic_x) + clearButton.isClickable = true + } + + if (it.isNullOrEmpty()) { + callbacks?.onQueryChanged("") + } else { + callbacks?.onQueryChanged(it.toString()) + } + } + + input.setOnFocusChangeListener { _, hasFocus -> + if (hasFocus) { + callbacks?.onFocusGained() + } else { + callbacks?.onFocusLost() + } + } + + clearButton.setOnClickListener { clearQuery() } + + context.obtainStyledAttributes(attrs, R.styleable.KeyboardPageSearchView, 0, 0).use { typedArray -> + val showAlways: Boolean = typedArray.getBoolean(R.styleable.KeyboardPageSearchView_show_always, false) + if (showAlways) { + alpha = 1f + state = State.SHOW_REQUESTED + } else { + alpha = 0f + input.layoutParams = input.layoutParams.apply { width = 1 } + state = State.HIDE_REQUESTED + } + + input.hint = typedArray.getString(R.styleable.KeyboardPageSearchView_search_hint) ?: "" + + val backgroundTint = typedArray.getColor(R.styleable.KeyboardPageSearchView_search_bar_tint, ContextCompat.getColor(context, R.color.signal_background_primary)) + val backgroundTintList = ColorStateList.valueOf(backgroundTint) + input.background = ColorDrawable(backgroundTint) + ViewCompat.setBackgroundTintList(findViewById(R.id.emoji_search_nav), backgroundTintList) + ViewCompat.setBackgroundTintList(findViewById(R.id.emoji_search_clear), backgroundTintList) + + val iconTint = typedArray.getColorStateList(R.styleable.KeyboardPageSearchView_search_icon_tint) ?: ContextCompat.getColorStateList(context, R.color.signal_icon_tint_tab_selected) + ImageViewCompat.setImageTintList(navButton, iconTint) + ImageViewCompat.setImageTintList(clearButton, iconTint) + input.setHintTextColor(iconTint) + + val clickOnly: Boolean = typedArray.getBoolean(R.styleable.KeyboardPageSearchView_click_only, false) + if (clickOnly) { + val clickIntercept: View = findViewById(R.id.keyboard_search_click_only) + clickIntercept.isVisible = true + clickIntercept.setOnClickListener { callbacks?.onClicked() } + } + } + } + + fun showRequested(): Boolean = state == State.SHOW_REQUESTED + + fun enableBackNavigation(enable: Boolean = true) { + navButton.setImageResource(if (enable) R.drawable.ic_arrow_left_24 else R.drawable.ic_search_24) + if (enable) { + navButton.setImageResource(R.drawable.ic_arrow_left_24) + navButton.setOnClickListener { callbacks?.onNavigationClicked() } + } else { + navButton.setImageResource(R.drawable.ic_search_24) + navButton.setOnClickListener(null) + } + } + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + targetInputWidth = w - ViewUtil.dpToPx(32) - ViewUtil.dpToPx(90) + } + + fun show() { + if (state == State.SHOW_REQUESTED) { + return + } + + visibility = VISIBLE + state = State.SHOW_REQUESTED + + post { + animate() + .setDuration(REVEAL_DURATION) + .alpha(1f) + .setListener(null) + + val resizeAnimation = ResizeAnimation(input, targetInputWidth, input.measuredHeight) + resizeAnimation.duration = REVEAL_DURATION + input.startAnimation(resizeAnimation) + } + } + + fun hide() { + if (state == State.HIDE_REQUESTED) { + return + } + + state = State.HIDE_REQUESTED + + post { + animate() + .setDuration(REVEAL_DURATION) + .alpha(0f) + .setListener(object : AnimationCompleteListener() { + override fun onAnimationEnd(animation: Animator?) { + visibility = INVISIBLE + } + }) + + val resizeAnimation = ResizeAnimation(input, 1, input.measuredHeight) + resizeAnimation.duration = REVEAL_DURATION + input.startAnimation(resizeAnimation) + } + } + + fun presentForEmojiSearch() { + ViewUtil.focusAndShowKeyboard(input) + enableBackNavigation() + } + + override fun clearFocus() { + super.clearFocus() + clearChildFocus(input) + } + + fun clearQuery() { + input.text.clear() + } + + interface Callbacks { + fun onFocusLost() = Unit + fun onFocusGained() = Unit + fun onNavigationClicked() = Unit + fun onQueryChanged(query: String) = Unit + fun onClicked() = Unit + } + + enum class State { + SHOW_REQUESTED, + HIDE_REQUESTED + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt new file mode 100644 index 0000000000..6d888389aa --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchRepository.kt @@ -0,0 +1,67 @@ +package org.thoughtcrime.securesms.keyboard.emoji.search + +import android.content.Context +import android.net.Uri +import io.reactivex.Single +import io.reactivex.schedulers.Schedulers +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsession.utilities.concurrent.SignalExecutors +import org.thoughtcrime.securesms.components.emoji.Emoji +import org.thoughtcrime.securesms.components.emoji.EmojiPageModel +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel +import org.thoughtcrime.securesms.database.EmojiSearchDatabase +import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.emoji.EmojiSource +import java.util.function.Consumer + +private const val MINIMUM_QUERY_THRESHOLD = 1 +private const val MINIMUM_INLINE_QUERY_THRESHOLD = 2 +private const val EMOJI_SEARCH_LIMIT = 20 + +private val NOT_PUNCTUATION = "[A-Za-z0-9 ]".toRegex() + +class EmojiSearchRepository(private val context: Context) { + + private val emojiSearchDatabase: EmojiSearchDatabase = DatabaseComponent.get(context).emojiSearchDatabase() + + fun submitQuery(query: String, limit: Int = EMOJI_SEARCH_LIMIT): Single> { + val result = if (query.length >= MINIMUM_INLINE_QUERY_THRESHOLD && NOT_PUNCTUATION.matches(query.substring(query.lastIndex))) { + Single.fromCallable { emojiSearchDatabase.query(query, limit) } + } else { + Single.just(emptyList()) + } + + return result.subscribeOn(Schedulers.io()) + } + + fun submitQuery(query: String, limit: Int = EMOJI_SEARCH_LIMIT, consumer: Consumer) { + SignalExecutors.SERIAL.execute { + val emoji: List = emojiSearchDatabase.query(query, limit) + + val displayEmoji: List = emoji + .mapNotNull { canonical -> EmojiSource.latest.canonicalToVariations[canonical] } + .map { Emoji(it) } + + consumer.accept(EmojiSearchResultsPageModel(emoji, displayEmoji)) + } + } + + private class EmojiSearchResultsPageModel( + private val emoji: List, + private val displayEmoji: List + ) : EmojiPageModel { + override fun getKey(): String = "" + + override fun getIconAttr(): Int = -1 + + override fun getEmoji(): List = emoji + + override fun getDisplayEmoji(): List = displayEmoji + + override fun hasSpriteMap(): Boolean = false + + override fun getSpriteUri(): Uri? = null + + override fun isDynamic(): Boolean = false + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java index 319f009974..eac40f6818 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendFragment.java @@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.components.ComposeText; import org.thoughtcrime.securesms.components.ControllableViewPager; import org.thoughtcrime.securesms.components.InputAwareLayout; import org.thoughtcrime.securesms.components.emoji.EmojiEditText; +import org.thoughtcrime.securesms.components.emoji.EmojiEventListener; import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider; import org.thoughtcrime.securesms.components.emoji.EmojiToggle; import org.thoughtcrime.securesms.components.emoji.MediaKeyboard; @@ -382,7 +383,7 @@ public class MediaSendFragment extends Fragment implements ViewTreeObserver.OnGl private void onEmojiToggleClicked(View v) { if (!emojiDrawer.resolved()) { - emojiDrawer.get().setProviders(0, new EmojiKeyboardProvider(requireContext(), new EmojiKeyboardProvider.EmojiEventListener() { + emojiDrawer.get().setProviders(0, new EmojiKeyboardProvider(requireContext(), new EmojiEventListener() { @Override public void onKeyEvent(KeyEvent keyEvent) { getActiveInputField().dispatchKeyEvent(keyEvent); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt index 17f4440e40..680de3882a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsAdapter.kt @@ -1,12 +1,9 @@ package org.thoughtcrime.securesms.messagerequests -import android.annotation.SuppressLint import android.content.Context import android.database.Cursor -import android.os.Build import android.text.SpannableString import android.text.style.ForegroundColorSpan -import android.util.Log import android.view.ViewGroup import android.widget.PopupMenu import androidx.recyclerview.widget.RecyclerView @@ -15,7 +12,7 @@ import org.thoughtcrime.securesms.database.CursorRecyclerViewAdapter import org.thoughtcrime.securesms.database.model.ThreadRecord import org.thoughtcrime.securesms.dependencies.DatabaseComponent import org.thoughtcrime.securesms.mms.GlideRequests - +import org.thoughtcrime.securesms.util.forceShowIcon class MessageRequestsAdapter( context: Context, @@ -62,7 +59,7 @@ class MessageRequestsAdapter( s.setSpan(ForegroundColorSpan(context.getColor(R.color.destructive)), 0, s.length, 0) item.title = s } - popupMenu.forceShowIcon() //TODO: call setForceShowIcon(true) after update to appcompat 1.4.1+ + popupMenu.forceShowIcon() popupMenu.show() } @@ -75,20 +72,3 @@ interface ConversationClickListener { fun onConversationClick(thread: ThreadRecord) fun onLongConversationClick(thread: ThreadRecord) } - -@SuppressLint("PrivateApi") -private fun PopupMenu.forceShowIcon() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - this.setForceShowIcon(true) - } else { - try { - val popupField = PopupMenu::class.java.getDeclaredField("mPopup") - popupField.isAccessible = true - val menu = popupField.get(this) - menu.javaClass.getDeclaredMethod("setForceShowIcon", Boolean::class.java) - .invoke(menu, true) - } catch (exception: Exception) { - Log.d("Loki", "Couldn't show message request popupmenu due to error: $exception.") - } - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt index 2405a305f4..48a5725521 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/BackgroundPollWorker.kt @@ -37,7 +37,7 @@ class BackgroundPollWorker(val context: Context, params: WorkerParameters) : Wor val workRequest = builder.build() WorkManager.getInstance(context).enqueueUniquePeriodicWork( TAG, - ExistingPeriodicWorkPolicy.KEEP, + ExistingPeriodicWorkPolicy.REPLACE, workRequest ) } diff --git a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java index ee379b0b82..3ead3fab00 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java +++ b/app/src/main/java/org/thoughtcrime/securesms/notifications/DefaultMessageNotifier.java @@ -42,7 +42,11 @@ import androidx.core.app.NotificationManagerCompat; import com.goterl.lazysodium.utils.KeyPair; +import org.session.libsession.messaging.MessagingModuleConfiguration; import org.session.libsession.messaging.open_groups.OpenGroup; +import com.annimon.stream.Optional; +import com.annimon.stream.Stream; + import org.session.libsession.messaging.sending_receiving.notifications.MessageNotifier; import org.session.libsession.messaging.utilities.SessionId; import org.session.libsession.messaging.utilities.SodiumUtilities; @@ -69,6 +73,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.database.model.Quote; +import org.thoughtcrime.securesms.database.model.ReactionRecord; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.service.KeyCachingService; @@ -166,18 +171,16 @@ public class DefaultMessageNotifier implements MessageNotifier { NotificationManager notifications = ServiceUtil.getNotificationManager(context); notifications.cancel(SUMMARY_NOTIFICATION_ID); - if (Build.VERSION.SDK_INT >= 23) { - try { - StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); + try { + StatusBarNotification[] activeNotifications = notifications.getActiveNotifications(); - for (StatusBarNotification activeNotification : activeNotifications) { - notifications.cancel(activeNotification.getId()); - } - } catch (Throwable e) { - // XXX Appears to be a ROM bug, see #6043 - Log.w(TAG, e); - notifications.cancelAll(); + for (StatusBarNotification activeNotification : activeNotifications) { + notifications.cancel(activeNotification.getId()); } + } catch (Throwable e) { + // XXX Appears to be a ROM bug, see #6043 + Log.w(TAG, e); + notifications.cancelAll(); } } @@ -293,13 +296,13 @@ public class DefaultMessageNotifier implements MessageNotifier { sendSingleThreadNotification(context, new NotificationState(notificationState.getNotificationsForThread(threadId)), false, true); } sendMultipleThreadNotification(context, notificationState, signal); - } else if (notificationState.getMessageCount() > 0){ + } else if (notificationState.getMessageCount() > 0) { sendSingleThreadNotification(context, notificationState, signal, false); } else { cancelActiveNotifications(context); } } catch (Exception e) { - Log.e(TAG, "Error creating notification",e); + Log.e(TAG, "Error creating notification", e); } cancelOrphanedNotifications(context, notificationState); updateBadge(context, notificationState.getMessageCount()); @@ -484,13 +487,9 @@ public class DefaultMessageNotifier implements MessageNotifier { return; } - if (Build.VERSION.SDK_INT >= 21) { - ringtone.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) - .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) - .build()); - } else { - ringtone.setStreamType(AudioManager.STREAM_NOTIFICATION); - } + ringtone.setAudioAttributes(new AudioAttributes.Builder().setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN) + .setUsage(AudioAttributes.USAGE_NOTIFICATION_COMMUNICATION_INSTANT) + .build()); ringtone.play(); } @@ -501,6 +500,8 @@ public class DefaultMessageNotifier implements MessageNotifier { NotificationState notificationState = new NotificationState(); MmsSmsDatabase.Reader reader = DatabaseComponent.get(context).mmsSmsDatabase().readerFor(cursor); ThreadDatabase threadDatabase = DatabaseComponent.get(context).threadDatabase(); + LokiThreadDatabase lokiThreadDatabase= DatabaseComponent.get(context).lokiThreadDatabase(); + KeyPair edKeyPair = MessagingModuleConfiguration.getShared().getGetUserED25519KeyPair().invoke(); MessageRecord record; Map cache = new HashMap(); @@ -516,7 +517,6 @@ public class DefaultMessageNotifier implements MessageNotifier { long timestamp = record.getTimestamp(); boolean messageRequest = false; - if (threadId != -1) { threadRecipients = threadDatabase.getRecipientForThreadId(threadId); messageRequest = threadRecipients != null && !threadRecipients.isGroupRecipient() && @@ -543,15 +543,14 @@ public class DefaultMessageNotifier implements MessageNotifier { } else if (record.isOpenGroupInvitation()) { body = SpanUtil.italic(context.getString(R.string.ThreadRecord_open_group_invitation)); } - + String userPublicKey = TextSecurePreferences.getLocalNumber(context); + String blindedPublicKey = cache.get(threadId); + if (blindedPublicKey == null) { + blindedPublicKey = generateBlindedId(threadId, context); + cache.put(threadId, blindedPublicKey); + } if (threadRecipients == null || !threadRecipients.isMuted()) { if (threadRecipients != null && threadRecipients.notifyType == RecipientDatabase.NOTIFY_TYPE_MENTIONS) { - String userPublicKey = TextSecurePreferences.getLocalNumber(context); - String blindedPublicKey = cache.get(threadId); - if (blindedPublicKey == null) { - blindedPublicKey = generateBlindedId(threadId, context); - cache.put(threadId, blindedPublicKey); - } // check if mentioned here boolean isQuoteMentioned = false; if (record instanceof MmsMessageRecord) { @@ -569,6 +568,20 @@ public class DefaultMessageNotifier implements MessageNotifier { } else { notificationState.addNotification(new NotificationItem(id, mms, recipient, conversationRecipient, threadRecipients, threadId, body, timestamp, slideDeck)); } + + String userBlindedPublicKey = blindedPublicKey; + Optional lastReact = Stream.of(record.getReactions()) + .filter(r -> !(r.getAuthor().equals(userPublicKey) || r.getAuthor().equals(userBlindedPublicKey))) + .findLast(); + + if (lastReact.isPresent()) { + if (threadRecipients != null && !threadRecipients.isGroupRecipient()) { + ReactionRecord reaction = lastReact.get(); + Recipient reactor = Recipient.from(context, Address.fromSerialized(reaction.getAuthor()), false); + String emoji = context.getString(R.string.reaction_notification, reactor.toShortString(), reaction.getEmoji()); + notificationState.addNotification(new NotificationItem(id, mms, reactor, reactor, threadRecipients, threadId, emoji, reaction.getDateSent(), slideDeck)); + } + } } } @@ -581,7 +594,7 @@ public class DefaultMessageNotifier implements MessageNotifier { OpenGroup openGroup = lokiThreadDatabase.getOpenGroupChat(threadId); KeyPair edKeyPair = KeyPairUtilities.INSTANCE.getUserED25519KeyPair(context); if (openGroup != null && edKeyPair != null) { - KeyPair blindedKeyPair = SodiumUtilities.INSTANCE.blindedKeyPair(openGroup.getPublicKey(), edKeyPair); + KeyPair blindedKeyPair = SodiumUtilities.blindedKeyPair(openGroup.getPublicKey(), edKeyPair); if (blindedKeyPair != null) { return new SessionId(IdPrefix.BLINDED, blindedKeyPair.getPublicKey().getAsBytes()).getHexString(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/EmojiCount.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/EmojiCount.java new file mode 100644 index 0000000000..46030f7504 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/EmojiCount.java @@ -0,0 +1,43 @@ +package org.thoughtcrime.securesms.reactions; + +import androidx.annotation.NonNull; + +import com.annimon.stream.Stream; + +import java.util.List; + +final class EmojiCount { + + static EmojiCount all(@NonNull List reactions) { + return new EmojiCount("", "", reactions); + } + + private final String baseEmoji; + private final String displayEmoji; + private final List reactions; + + EmojiCount(@NonNull String baseEmoji, + @NonNull String emoji, + @NonNull List reactions) + { + this.baseEmoji = baseEmoji; + this.displayEmoji = emoji; + this.reactions = reactions; + } + + public @NonNull String getBaseEmoji() { + return baseEmoji; + } + + public @NonNull String getDisplayEmoji() { + return displayEmoji; + } + + public int getCount() { + return Stream.of(reactions).reduce(0, (count, reaction) -> count + reaction.getCount()); + } + + public @NonNull List getReactions() { + return reactions; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt new file mode 100644 index 0000000000..b405f3b364 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.kt @@ -0,0 +1,17 @@ +package org.thoughtcrime.securesms.reactions + +import org.session.libsession.utilities.recipients.Recipient + +/** + * A UI model for a reaction in the [ReactionsDialogFragment] + */ +data class ReactionDetails( + val sender: Recipient, + val baseEmoji: String, + val displayEmoji: String, + val timestamp: Long, + val serverId: String, + val localId: Long, + val isMms: Boolean, + val count: Int +) diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java new file mode 100644 index 0000000000..f1cbea16c3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionRecipientsAdapter.java @@ -0,0 +1,196 @@ +package org.thoughtcrime.securesms.reactions; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import org.session.libsession.messaging.utilities.SessionId; +import org.thoughtcrime.securesms.components.ProfilePictureView; +import org.thoughtcrime.securesms.components.emoji.EmojiImageView; +import org.thoughtcrime.securesms.database.model.MessageId; +import org.thoughtcrime.securesms.mms.GlideApp; + +import java.util.Collections; +import java.util.List; + +import network.loki.messenger.R; + +final class ReactionRecipientsAdapter extends RecyclerView.Adapter { + + private static final int MAX_REACTORS = 5; + private static final int HEADER_COUNT = 1; + private static final int HEADER_POSITION = 0; + + private static final int FOOTER_COUNT = 1; + private static final int FOOTER_POSITION = 6; + + private static final int HEADER_TYPE = 0; + private static final int RECIPIENT_TYPE = 1; + private static final int FOOTER_TYPE = 2; + + private ReactionViewPagerAdapter.Listener callback; + private List data = Collections.emptyList(); + private MessageId messageId; + private boolean isUserModerator; + private EmojiCount emojiData; + + public ReactionRecipientsAdapter(ReactionViewPagerAdapter.Listener callback) { + this.callback = callback; + } + + public void updateData(MessageId messageId, EmojiCount newData, boolean isUserModerator) { + this.messageId = messageId; + emojiData = newData; + data = newData.getReactions(); + this.isUserModerator = isUserModerator; + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + switch (position) { + case HEADER_POSITION: + return HEADER_TYPE; + case FOOTER_POSITION: + return FOOTER_TYPE; + default: + return RECIPIENT_TYPE; + } + } + + @Override + public @NonNull + ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + switch (viewType) { + case HEADER_TYPE: + return new HeaderViewHolder(callback, LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_recycler_header, parent, false)); + case FOOTER_TYPE: + return new FooterViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_recycler_footer, parent, false)); + default: + return new RecipientViewHolder(callback, LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_recipient_item, parent, false)); + } + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + if (holder instanceof RecipientViewHolder) { + ((RecipientViewHolder) holder).bind(data.get(position - HEADER_COUNT)); + } else if (holder instanceof HeaderViewHolder) { + ((HeaderViewHolder) holder).bind(emojiData, messageId, isUserModerator); + } else if (holder instanceof FooterViewHolder) { + ((FooterViewHolder) holder).bind(emojiData); + } + } + + @Override + public void onViewRecycled(@NonNull ViewHolder holder) { + if (holder instanceof RecipientViewHolder) { + ((RecipientViewHolder) holder).unbind(); + } + } + + @Override + public int getItemCount() { + if (data.isEmpty()) { + return 0; + } else if (emojiData.getCount() <= MAX_REACTORS) { + return data.size() + HEADER_COUNT; + } else { + return MAX_REACTORS + HEADER_COUNT + FOOTER_COUNT; + } + } + + static class ViewHolder extends RecyclerView.ViewHolder { + public ViewHolder(@NonNull View itemView) { + super(itemView); + } + } + + static class HeaderViewHolder extends ViewHolder { + + private final ReactionViewPagerAdapter.Listener callback; + + public HeaderViewHolder(ReactionViewPagerAdapter.Listener callback, @NonNull View itemView) { + super(itemView); + this.callback = callback; + } + + private void bind(@NonNull final EmojiCount emoji, final MessageId messageId, boolean isUserModerator) { + View clearAll = itemView.findViewById(R.id.header_view_clear_all); + clearAll.setVisibility(isUserModerator ? View.VISIBLE : View.GONE); + clearAll.setOnClickListener(isUserModerator ? (View.OnClickListener) v -> { + callback.onClearAll(emoji.getBaseEmoji(), messageId); + } : null); + EmojiImageView emojiView = itemView.findViewById(R.id.header_view_emoji); + emojiView.setImageEmoji(emoji.getDisplayEmoji()); + TextView count = itemView.findViewById(R.id.header_view_emoji_count); + count.setText(String.format(" · %s", emoji.getCount())); + } + } + + static final class RecipientViewHolder extends ViewHolder { + + private ReactionViewPagerAdapter.Listener callback; + private final ProfilePictureView avatar; + private final TextView recipient; + private final ImageView remove; + + public RecipientViewHolder(ReactionViewPagerAdapter.Listener callback, @NonNull View itemView) { + super(itemView); + this.callback = callback; + avatar = itemView.findViewById(R.id.reactions_bottom_view_avatar); + avatar.glide = GlideApp.with(itemView); + recipient = itemView.findViewById(R.id.reactions_bottom_view_recipient_name); + remove = itemView.findViewById(R.id.reactions_bottom_view_recipient_remove); + } + + void bind(@NonNull ReactionDetails reaction) { + this.remove.setOnClickListener((v) -> { + MessageId messageId = new MessageId(reaction.getLocalId(), reaction.isMms()); + callback.onRemoveReaction(reaction.getBaseEmoji(), messageId, reaction.getTimestamp()); + }); + + this.avatar.update(reaction.getSender()); + + if (reaction.getSender().isLocalNumber()) { + this.recipient.setText(R.string.ReactionsRecipientAdapter_you); + this.remove.setVisibility(View.VISIBLE); + } else { + String name = reaction.getSender().getName(); + if (name != null && new SessionId(name).getPrefix() != null) { + name = name.substring(0, 4) + "..." + name.substring(name.length() - 4); + } + this.recipient.setText(name); + this.remove.setVisibility(View.GONE); + } + } + + void unbind() { + avatar.recycle(); + } + + } + + static class FooterViewHolder extends ViewHolder { + + public FooterViewHolder(@NonNull View itemView) { + super(itemView); + } + + private void bind(@NonNull final EmojiCount emoji) { + if (emoji.getCount() > 5) { + TextView count = itemView.findViewById(R.id.footer_view_emoji_count); + count.setText(itemView.getContext().getResources().getQuantityString(R.plurals.ReactionsRecipientAdapter_other_reactors, emoji.getCount() - 5, emoji.getCount() - 5, emoji.getBaseEmoji())); + itemView.setVisibility(View.VISIBLE); + } else { + itemView.setVisibility(View.GONE); + } + } + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionViewPagerAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionViewPagerAdapter.java new file mode 100644 index 0000000000..330c1552ce --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionViewPagerAdapter.java @@ -0,0 +1,122 @@ +package org.thoughtcrime.securesms.reactions; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DividerItemDecoration; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.database.model.MessageId; +import org.thoughtcrime.securesms.util.ContextUtil; +import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil; + +import java.util.List; + +import network.loki.messenger.R; + +/** + * ReactionViewPagerAdapter provides pages to a ViewPager2 which contains the reactions on a given message. + */ +class ReactionViewPagerAdapter extends ListAdapter { + + private Listener callback; + private int selectedPosition = 0; + private MessageId messageId = null; + private boolean isUserModerator = false; + + protected ReactionViewPagerAdapter(Listener callback) { + super(new AlwaysChangedDiffUtil<>()); + this.callback = callback; + } + + public void setIsUserModerator(boolean isUserModerator) { + this.isUserModerator = isUserModerator; + } + + public void setMessageId(MessageId messageId) { + this.messageId = messageId; + } + + @Override + public int getItemCount() { + return super.getItemCount(); + } + + @NonNull EmojiCount getEmojiCount(int position) { + return getItem(position); + } + + void enableNestedScrollingForPosition(int position) { + selectedPosition = position; + + notifyItemRangeChanged(0, getItemCount(), new Object()); + } + + @Override + public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return new ViewHolder(callback, + LayoutInflater.from(parent.getContext()).inflate(R.layout.reactions_bottom_sheet_dialog_fragment_recycler, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List payloads) { + if (payloads.isEmpty()) { + onBindViewHolder(holder, position); + } else { + holder.setSelected(selectedPosition); + } + } + + @Override + public void onBindViewHolder(@NonNull ViewHolder holder, int position) { + holder.onBind(messageId, getItem(position), isUserModerator); + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + recyclerView.setNestedScrollingEnabled(false); + ViewGroup.LayoutParams params = recyclerView.getLayoutParams(); + params.height = (int) (recyclerView.getResources().getDisplayMetrics().heightPixels * 0.80); + recyclerView.setLayoutParams(params); + recyclerView.setHasFixedSize(true); + } + + static class ViewHolder extends RecyclerView.ViewHolder { + private final RecyclerView recycler; + private final ReactionRecipientsAdapter adapter; + + public ViewHolder(Listener callback, @NonNull View itemView) { + super(itemView); + adapter = new ReactionRecipientsAdapter(callback); + recycler = itemView.findViewById(R.id.reactions_bottom_view_recipient_recycler); + + ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT); + + recycler.setLayoutParams(params); + DividerItemDecoration decoration = new DividerItemDecoration(itemView.getContext(), LinearLayoutManager.VERTICAL); + decoration.setDrawable(ContextUtil.requireDrawable(itemView.getContext(), R.drawable.vertical_divider)); + recycler.addItemDecoration(decoration); + recycler.setAdapter(adapter); + } + + public void onBind(MessageId messageId, @NonNull EmojiCount emojiCount, boolean isUserModerator) { + adapter.updateData(messageId, emojiCount, isUserModerator); + } + + public void setSelected(int position) { + recycler.setNestedScrollingEnabled(getAdapterPosition() == position); + } + } + + public interface Listener { + void onRemoveReaction(@NonNull String emoji, @NonNull MessageId messageId, long timestamp); + + void onClearAll(@NonNull String emoji, @NonNull MessageId messageId); + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDialogFragment.java new file mode 100644 index 0000000000..c327da03ba --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsDialogFragment.java @@ -0,0 +1,199 @@ +package org.thoughtcrime.securesms.reactions; + +import android.content.Context; +import android.os.Bundle; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.FrameLayout; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.viewpager2.widget.ViewPager2; + +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.tabs.TabLayout; +import com.google.android.material.tabs.TabLayoutMediator; + +import org.session.libsession.utilities.ThemeUtil; +import org.thoughtcrime.securesms.components.emoji.EmojiImageView; +import org.thoughtcrime.securesms.database.model.MessageId; +import org.thoughtcrime.securesms.util.LifecycleDisposable; +import org.thoughtcrime.securesms.util.NumberUtil; + +import java.util.Objects; + +import network.loki.messenger.R; + +public final class ReactionsDialogFragment extends BottomSheetDialogFragment implements ReactionViewPagerAdapter.Listener { + + private static final String ARGS_MESSAGE_ID = "reactions.args.message.id"; + private static final String ARGS_IS_MMS = "reactions.args.is.mms"; + private static final String ARGS_IS_MODERATOR = "reactions.args.is.moderator"; + + private ViewPager2 recipientPagerView; + private ReactionViewPagerAdapter recipientsAdapter; + private Callback callback; + + private final LifecycleDisposable disposables = new LifecycleDisposable(); + + public static DialogFragment create(MessageId messageId, boolean isUserModerator) { + Bundle args = new Bundle(); + DialogFragment fragment = new ReactionsDialogFragment(); + + args.putLong(ARGS_MESSAGE_ID, messageId.getId()); + args.putBoolean(ARGS_IS_MMS, messageId.isMms()); + args.putBoolean(ARGS_IS_MODERATOR, isUserModerator); + + fragment.setArguments(args); + + return fragment; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (getParentFragment() instanceof Callback) { + callback = (Callback) getParentFragment(); + } else { + callback = (Callback) context; + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + if (ThemeUtil.isDarkTheme(requireContext())) { + setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_TextSecure_BottomSheetDialog_Fixed_ReactWithAny); + } else { + setStyle(DialogFragment.STYLE_NORMAL, R.style.Theme_TextSecure_Light_BottomSheetDialog_Fixed_ReactWithAny); + } + + super.onCreate(savedInstanceState); + } + + @Override + public @Nullable View onCreateView(@NonNull LayoutInflater inflater, + @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) + { + return inflater.inflate(R.layout.reactions_bottom_sheet_dialog_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + recipientPagerView = view.findViewById(R.id.reactions_bottom_view_recipient_pager); + + disposables.bindTo(getViewLifecycleOwner()); + + setUpRecipientsRecyclerView(); + setUpTabMediator(savedInstanceState); + + MessageId messageId = new MessageId(requireArguments().getLong(ARGS_MESSAGE_ID), requireArguments().getBoolean(ARGS_IS_MMS)); + recipientsAdapter.setIsUserModerator(requireArguments().getBoolean(ARGS_IS_MODERATOR)); + recipientsAdapter.setMessageId(messageId); + setUpViewModel(messageId); + } + + private void setUpTabMediator(@Nullable Bundle savedInstanceState) { + if (savedInstanceState == null) { + FrameLayout container = requireDialog().findViewById(R.id.container); + TabLayout emojiTabs = requireDialog().findViewById(R.id.emoji_tabs); + + ViewCompat.setOnApplyWindowInsetsListener(container, (v, insets) -> insets.consumeSystemWindowInsets()); + + TabLayoutMediator mediator = new TabLayoutMediator(emojiTabs, recipientPagerView, (tab, position) -> { + tab.setCustomView(R.layout.reactions_pill); + + View customView = Objects.requireNonNull(tab.getCustomView()); + EmojiImageView emoji = customView.findViewById(R.id.reactions_pill_emoji); + TextView text = customView.findViewById(R.id.reactions_pill_count); + EmojiCount emojiCount = recipientsAdapter.getEmojiCount(position); + + customView.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.reaction_pill_dialog_background)); + emoji.setImageEmoji(emojiCount.getDisplayEmoji()); + text.setText(NumberUtil.getFormattedNumber(emojiCount.getCount())); + }); + + emojiTabs.addOnTabSelectedListener(new TabLayout.OnTabSelectedListener() { + @Override + public void onTabSelected(TabLayout.Tab tab) { + View customView = tab.getCustomView(); + TextView text = customView.findViewById(R.id.reactions_pill_count); + customView.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.reaction_pill_background_selected)); + text.setTextColor(ContextCompat.getColor(requireContext(), R.color.reactions_pill_selected_text_color)); + } + + @Override + public void onTabUnselected(TabLayout.Tab tab) { + View customView = tab.getCustomView(); + TextView text = customView.findViewById(R.id.reactions_pill_count); + customView.setBackground(ContextCompat.getDrawable(requireContext(), R.drawable.reaction_pill_dialog_background)); + text.setTextColor(ContextCompat.getColor(requireContext(), R.color.reactions_pill_text_color)); + } + @Override + public void onTabReselected(TabLayout.Tab tab) {} + }); + mediator.attach(); + } + } + + private void setUpRecipientsRecyclerView() { + recipientsAdapter = new ReactionViewPagerAdapter(this); + + recipientPagerView.registerOnPageChangeCallback(new ViewPager2.OnPageChangeCallback() { + @Override + public void onPageSelected(int position) { + recipientPagerView.post(() -> recipientsAdapter.enableNestedScrollingForPosition(position)); + } + + @Override + public void onPageScrollStateChanged(int state) { + if (state == ViewPager2.SCROLL_STATE_IDLE) { + recipientPagerView.requestLayout(); + } + } + }); + + recipientPagerView.setAdapter(recipientsAdapter); + } + + private void setUpViewModel(@NonNull MessageId messageId) { + ReactionsViewModel.Factory factory = new ReactionsViewModel.Factory(messageId); + + ReactionsViewModel viewModel = new ViewModelProvider(this, factory).get(ReactionsViewModel.class); + + disposables.add(viewModel.getEmojiCounts().subscribe(emojiCounts -> { + if (emojiCounts.size() < 1) { + dismiss(); + return; + } + + recipientsAdapter.submitList(emojiCounts); + })); + } + + @Override + public void onRemoveReaction(@NonNull String emoji, @NonNull MessageId messageId, long timestamp) { + callback.onRemoveReaction(emoji, messageId); + dismiss(); + } + + @Override + public void onClearAll(@NonNull String emoji, @NonNull MessageId messageId) { + callback.onClearAll(emoji, messageId); + dismiss(); + } + + public interface Callback { + void onRemoveReaction(@NonNull String emoji, @NonNull MessageId messageId); + + void onClearAll(@NonNull String emoji, @NonNull MessageId messageId); + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt new file mode 100644 index 0000000000..8687a0f9bb --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsRepository.kt @@ -0,0 +1,39 @@ +package org.thoughtcrime.securesms.reactions + +import io.reactivex.Observable +import io.reactivex.ObservableEmitter +import io.reactivex.schedulers.Schedulers +import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.components.emoji.EmojiUtil +import org.thoughtcrime.securesms.database.model.MessageId +import org.thoughtcrime.securesms.database.model.ReactionRecord +import org.thoughtcrime.securesms.dependencies.DatabaseComponent + +class ReactionsRepository { + + fun getReactions(messageId: MessageId): Observable> { + return Observable.create { emitter: ObservableEmitter> -> + emitter.onNext(fetchReactionDetails(messageId)) + }.subscribeOn(Schedulers.io()) + } + + private fun fetchReactionDetails(messageId: MessageId): List { + val context = MessagingModuleConfiguration.shared.context + val reactions: List = DatabaseComponent.get(context).reactionDatabase().getReactions(messageId) + + return reactions.map { reaction -> + ReactionDetails( + sender = Recipient.from(context, Address.fromSerialized(reaction.author), false), + baseEmoji = EmojiUtil.getCanonicalRepresentation(reaction.emoji), + displayEmoji = reaction.emoji, + timestamp = reaction.dateReceived, + serverId = reaction.serverId, + localId = reaction.messageId, + isMms = reaction.isMms, + count = reaction.count.toInt() + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java new file mode 100644 index 0000000000..8d345f8d3c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionsViewModel.java @@ -0,0 +1,81 @@ +package org.thoughtcrime.securesms.reactions; + +import androidx.annotation.NonNull; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import com.annimon.stream.Stream; + +import org.thoughtcrime.securesms.database.model.MessageId; + +import java.util.Comparator; +import java.util.List; +import java.util.Map; + +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; + +public class ReactionsViewModel extends ViewModel { + + private final MessageId messageId; + private final ReactionsRepository repository; + + public ReactionsViewModel(@NonNull MessageId messageId) { + this.messageId = messageId; + this.repository = new ReactionsRepository(); + } + + public @NonNull + Observable> getEmojiCounts() { + return repository.getReactions(messageId) + .map(reactionList -> Stream.of(reactionList) + .groupBy(ReactionDetails::getBaseEmoji) + .sorted(this::compareReactions) + .map(entry -> new EmojiCount(entry.getKey(), + getCountDisplayEmoji(entry.getValue()), + entry.getValue())) + .toList()) + .observeOn(AndroidSchedulers.mainThread()); + } + + private int compareReactions(@NonNull Map.Entry> lhs, @NonNull Map.Entry> rhs) { + int lengthComparison = -Integer.compare(lhs.getValue().size(), rhs.getValue().size()); + if (lengthComparison != 0) return lengthComparison; + + long latestTimestampLhs = getLatestTimestamp(lhs.getValue()); + long latestTimestampRhs = getLatestTimestamp(rhs.getValue()); + + return -Long.compare(latestTimestampLhs, latestTimestampRhs); + } + + private long getLatestTimestamp(List reactions) { + return Stream.of(reactions) + .max(Comparator.comparingLong(ReactionDetails::getTimestamp)) + .map(ReactionDetails::getTimestamp) + .orElse(-1L); + } + + private @NonNull String getCountDisplayEmoji(@NonNull List reactions) { + for (ReactionDetails reaction : reactions) { + if (reaction.getSender().isLocalNumber()) { + return reaction.getDisplayEmoji(); + } + } + + return reactions.get(reactions.size() - 1).getDisplayEmoji(); + } + + static final class Factory implements ViewModelProvider.Factory { + + private final MessageId messageId; + + Factory(@NonNull MessageId messageId) { + this.messageId = messageId; + } + + @Override + public @NonNull T create(@NonNull Class modelClass) { + return modelClass.cast(new ReactionsViewModel(messageId)); + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiDialogFragment.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiDialogFragment.java new file mode 100644 index 0000000000..3c8661fd82 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiDialogFragment.java @@ -0,0 +1,231 @@ +package org.thoughtcrime.securesms.reactions.any; + +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; +import android.text.TextUtils; +import android.view.KeyEvent; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.core.view.ViewCompat; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; +import androidx.loader.app.LoaderManager; + +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; +import com.google.android.material.shape.CornerFamily; +import com.google.android.material.shape.MaterialShapeDrawable; +import com.google.android.material.shape.ShapeAppearanceModel; + +import org.thoughtcrime.securesms.components.emoji.EmojiEventListener; +import org.thoughtcrime.securesms.components.emoji.EmojiPageView; +import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter; +import org.thoughtcrime.securesms.conversation.v2.ViewUtil; +import org.thoughtcrime.securesms.database.model.MessageId; +import org.thoughtcrime.securesms.database.model.MessageRecord; +import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView; +import org.thoughtcrime.securesms.util.LifecycleDisposable; + +import network.loki.messenger.R; + +public final class ReactWithAnyEmojiDialogFragment extends BottomSheetDialogFragment implements EmojiEventListener, + EmojiPageViewGridAdapter.VariationSelectorListener +{ + + private static final String ARG_MESSAGE_ID = "arg_message_id"; + private static final String ARG_IS_MMS = "arg_is_mms"; + private static final String ARG_START_PAGE = "arg_start_page"; + private static final String ARG_SHADOWS = "arg_shadows"; + + private ReactWithAnyEmojiViewModel viewModel; + private Callback callback; + private EmojiPageView emojiPageView; + private KeyboardPageSearchView search; + + private final LifecycleDisposable disposables = new LifecycleDisposable(); + + public static DialogFragment createForMessageRecord(@NonNull MessageRecord messageRecord, int startingPage) { + DialogFragment fragment = new ReactWithAnyEmojiDialogFragment(); + Bundle args = new Bundle(); + + args.putLong(ARG_MESSAGE_ID, messageRecord.getId()); + args.putBoolean(ARG_IS_MMS, messageRecord.isMms()); + args.putInt(ARG_START_PAGE, startingPage); + fragment.setArguments(args); + + return fragment; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + + if (getParentFragment() instanceof Callback) { + callback = (Callback) getParentFragment(); + } else { + callback = (Callback) context; + } + } + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setStyle(DialogFragment.STYLE_NORMAL, R.style.Widget_TextSecure_ReactWithAny); + } + + @Override + public @NonNull Dialog onCreateDialog(Bundle savedInstanceState) { + BottomSheetDialog dialog = (BottomSheetDialog) super.onCreateDialog(savedInstanceState); + dialog.getBehavior().setPeekHeight((int) (getResources().getDisplayMetrics().heightPixels * 0.50)); + + ShapeAppearanceModel shapeAppearanceModel = ShapeAppearanceModel.builder() + .setTopLeftCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 18)) + .setTopRightCorner(CornerFamily.ROUNDED, ViewUtil.dpToPx(requireContext(), 18)) + .build(); + + MaterialShapeDrawable dialogBackground = new MaterialShapeDrawable(shapeAppearanceModel); + + dialogBackground.setTint(ContextCompat.getColor(requireContext(), R.color.react_with_any_background)); + + dialog.getBehavior().addBottomSheetCallback(new BottomSheetBehavior.BottomSheetCallback() { + @Override + public void onStateChanged(@NonNull View bottomSheet, int newState) { + if (bottomSheet.getBackground() != dialogBackground) { + ViewCompat.setBackground(bottomSheet, dialogBackground); + } + } + + @Override + public void onSlide(@NonNull View bottomSheet, float slideOffset) { } + }); + + boolean shadows = requireArguments().getBoolean(ARG_SHADOWS, true); + if (!shadows) { + Window window = dialog.getWindow(); + if (window != null) { + window.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND); + } + } + + return dialog; + } + + @Override + public @Nullable View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) { + return inflater.inflate(R.layout.react_with_any_emoji_dialog_fragment, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + disposables.bindTo(getViewLifecycleOwner()); + + emojiPageView = view.findViewById(R.id.react_with_any_emoji_page_view); + emojiPageView.initialize(this, this, true); + + search = view.findViewById(R.id.react_with_any_emoji_search); + + initializeViewModel(); + +// EmojiKeyboardPageCategoriesAdapter categoriesAdapter = new EmojiKeyboardPageCategoriesAdapter(key -> { +// scrollTo(key); +// viewModel.selectPage(key); +// }); + + disposables.add(viewModel.getEmojiList().subscribe(pages -> emojiPageView.setList(pages, null))); +// disposables.add(viewModel.getCategories().subscribe(categoriesAdapter::submitList)); +// disposables.add(viewModel.getSelectedKey().subscribe(key -> categoriesRecycler.post(() -> { +// int index = categoriesAdapter.indexOfFirst(EmojiKeyboardPageCategoryMappingModel.class, m -> m.getKey().equals(key)); +// +// if (index != -1) { +// categoriesRecycler.smoothScrollToPosition(index); +// } +// }))); + + search.setCallbacks(new SearchCallbacks()); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + LoaderManager.getInstance(requireActivity()).destroyLoader((int) requireArguments().getLong(ARG_MESSAGE_ID)); + } + + @Override + public void onDismiss(@NonNull DialogInterface dialog) { + super.onDismiss(dialog); + + callback.onReactWithAnyEmojiDialogDismissed(); + } + + private void initializeViewModel() { + Bundle args = requireArguments(); + ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext()); + ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS)); + + viewModel = new ViewModelProvider(this, factory).get(ReactWithAnyEmojiViewModel.class); + } + + @Override + public void onEmojiSelected(String emoji) { + viewModel.onEmojiSelected(emoji); + Bundle args = requireArguments(); + MessageId messageId = new MessageId(args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS)); + callback.onReactWithAnyEmojiSelected(emoji, messageId); + dismiss(); + } + + @Override + public void onKeyEvent(KeyEvent keyEvent) { + } + + @Override + public void onVariationSelectorStateChanged(boolean open) { } + + public interface Callback { + void onReactWithAnyEmojiDialogDismissed(); + + void onReactWithAnyEmojiSelected(@NonNull String emoji, MessageId messageId); + } + + private class SearchCallbacks implements KeyboardPageSearchView.Callbacks { + @Override + public void onQueryChanged(@NonNull String query) { + boolean hasQuery = !TextUtils.isEmpty(query); + search.enableBackNavigation(hasQuery); + /*if (hasQuery) { + ViewUtil.fadeOut(tabBar, 250, View.INVISIBLE); + } else { + ViewUtil.fadeIn(tabBar, 250); + }*/ + viewModel.onQueryChanged(query); + } + + @Override + public void onNavigationClicked() { + search.clearQuery(); + search.clearFocus(); + ViewUtil.hideKeyboard(requireContext(), requireView()); + } + + @Override + public void onFocusGained() { + ((BottomSheetDialog) requireDialog()).getBehavior().setState(BottomSheetBehavior.STATE_EXPANDED); + } + + @Override + public void onClicked() { } + + @Override + public void onFocusLost() { } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPage.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPage.java new file mode 100644 index 0000000000..46d8bbcd80 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPage.java @@ -0,0 +1,62 @@ +package org.thoughtcrime.securesms.reactions.any; + +import androidx.annotation.AttrRes; +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import org.session.libsignal.utilities.guava.Preconditions; + +import java.util.List; +import java.util.Objects; + +/** + * Represents a swipeable page in the ReactWithAnyEmoji dialog fragment, encapsulating any + * {@link ReactWithAnyEmojiPageBlock}s contained on that page. It is assumed that there is at least + * one page present. + * + * This class also exposes several properties based off of that list, in order to allow the ReactWithAny + * bottom sheet to properly lay out its tabs and assign labels as the user moves between pages. + */ +class ReactWithAnyEmojiPage { + + private final List pageBlocks; + + ReactWithAnyEmojiPage(@NonNull List pageBlocks) { + Preconditions.checkArgument(!pageBlocks.isEmpty()); + + this.pageBlocks = pageBlocks; + } + + public @NonNull String getKey() { + return pageBlocks.get(0).getPageModel().getKey(); + } + + public @StringRes int getLabel() { + return pageBlocks.get(0).getLabel(); + } + + public boolean hasEmoji() { + return !pageBlocks.get(0).getPageModel().getEmoji().isEmpty(); + } + + public List getPageBlocks() { + return pageBlocks; + } + + public @AttrRes int getIconAttr() { + return pageBlocks.get(0).getPageModel().getIconAttr(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ReactWithAnyEmojiPage that = (ReactWithAnyEmojiPage) o; + return pageBlocks.equals(that.pageBlocks); + } + + @Override + public int hashCode() { + return Objects.hash(pageBlocks); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPageBlock.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPageBlock.java new file mode 100644 index 0000000000..d3099d1fa4 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPageBlock.java @@ -0,0 +1,46 @@ +package org.thoughtcrime.securesms.reactions.any; + +import androidx.annotation.NonNull; +import androidx.annotation.StringRes; + +import org.thoughtcrime.securesms.components.emoji.EmojiPageModel; + +import java.util.Objects; + +/** + * Wraps a single "class" of Emojis, be it a predefined category, recents, etc. and provides + * a label for that "class". + */ +class ReactWithAnyEmojiPageBlock { + + private final int label; + private final EmojiPageModel pageModel; + + ReactWithAnyEmojiPageBlock(@StringRes int label, @NonNull EmojiPageModel pageModel) { + this.label = label; + this.pageModel = pageModel; + } + + public @StringRes int getLabel() { + return label; + } + + public EmojiPageModel getPageModel() { + return pageModel; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ReactWithAnyEmojiPageBlock that = (ReactWithAnyEmojiPageBlock) o; + return label == that.label && + pageModel.getIconAttr() == that.pageModel.getIconAttr() && + Objects.equals(pageModel.getEmoji(), that.pageModel.getEmoji()); + } + + @Override + public int hashCode() { + return Objects.hash(label, pageModel.getEmoji()); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java new file mode 100644 index 0000000000..e17634a7a7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java @@ -0,0 +1,51 @@ +package org.thoughtcrime.securesms.reactions.any; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import com.annimon.stream.Stream; + +import org.session.libsignal.utilities.Log; +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel; +import org.thoughtcrime.securesms.emoji.EmojiCategory; +import org.thoughtcrime.securesms.emoji.EmojiSource; + +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import network.loki.messenger.R; + +final class ReactWithAnyEmojiRepository { + + private static final String TAG = Log.tag(ReactWithAnyEmojiRepository.class); + + private final Context context; + private final RecentEmojiPageModel recentEmojiPageModel; + private final List emojiPages; + + ReactWithAnyEmojiRepository(@NonNull Context context) { + this.context = context; + this.recentEmojiPageModel = new RecentEmojiPageModel(context); + this.emojiPages = new LinkedList<>(); + + emojiPages.addAll(Stream.of(EmojiSource.getLatest().getDisplayPages()) + .filterNot(p -> p.getIconAttr() == EmojiCategory.EMOTICONS.getIcon()) + .map(page -> new ReactWithAnyEmojiPage(Collections.singletonList(new ReactWithAnyEmojiPageBlock(EmojiCategory.getCategoryLabel(page.getIconAttr()), page)))) + .toList()); + } + + List getEmojiPageModels() { + List pages = new LinkedList<>(); + + pages.add(new ReactWithAnyEmojiPage(Collections.singletonList(new ReactWithAnyEmojiPageBlock(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__recently_used, recentEmojiPageModel)))); + pages.addAll(emojiPages); + + return pages; + } + + void addEmojiToMessage(@NonNull String emoji) { + recentEmojiPageModel.onCodePointSelected(emoji); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java new file mode 100644 index 0000000000..6bdb1fbb28 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java @@ -0,0 +1,121 @@ +package org.thoughtcrime.securesms.reactions.any; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.lifecycle.ViewModel; +import androidx.lifecycle.ViewModelProvider; + +import org.session.libsession.messaging.MessagingModuleConfiguration; +import org.thoughtcrime.securesms.components.emoji.EmojiPageModel; +import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter; +import org.thoughtcrime.securesms.database.model.MessageId; +import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchRepository; +import org.thoughtcrime.securesms.reactions.ReactionsRepository; +import org.thoughtcrime.securesms.util.adapter.mapping.MappingModelList; + +import java.util.List; + +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.subjects.BehaviorSubject; + +public final class ReactWithAnyEmojiViewModel extends ViewModel { + + private static final int SEARCH_LIMIT = 40; + private final EmojiSearchRepository emojiSearchRepository; + + private final ReactWithAnyEmojiRepository repository; + private final Observable emojiList; + private final BehaviorSubject searchResults; + + private ReactWithAnyEmojiViewModel(@NonNull ReactWithAnyEmojiRepository repository, + long messageId, + boolean isMms, + @NonNull EmojiSearchRepository emojiSearchRepository) + { + this.repository = repository; + this.emojiSearchRepository = emojiSearchRepository; + this.searchResults = BehaviorSubject.createDefault(new EmojiSearchResult()); + + Observable> emojiPages = new ReactionsRepository().getReactions(new MessageId(messageId, isMms)) + .map(thisMessagesReactions -> repository.getEmojiPageModels()); + + Observable emojiList = emojiPages.map(pages -> { + MappingModelList list = new MappingModelList(); + + for (ReactWithAnyEmojiPage page : pages) { + String key = page.getKey(); + for (ReactWithAnyEmojiPageBlock block : page.getPageBlocks()) { + list.add(new EmojiPageViewGridAdapter.EmojiHeader(key, block.getLabel())); + list.addAll(toMappingModels(block.getPageModel())); + } + } + + return list; + }); + + this.emojiList = Observable.combineLatest(emojiList, searchResults.distinctUntilChanged(), (all, search) -> { + if (search.query.isEmpty()) { + return all; + } else { + if (search.model.getDisplayEmoji().isEmpty()) { + return MappingModelList.singleton(new EmojiPageViewGridAdapter.EmojiNoResultsModel()); + } + return toMappingModels(search.model); + } + }); + } + + @NonNull Observable getEmojiList() { + return emojiList.observeOn(AndroidSchedulers.mainThread()); + } + + void onEmojiSelected(@NonNull String emoji) { + repository.addEmojiToMessage(emoji); + } + + public void onQueryChanged(String query) { + emojiSearchRepository.submitQuery(query, SEARCH_LIMIT, m -> searchResults.onNext(new EmojiSearchResult(query, m))); + } + + private static @NonNull MappingModelList toMappingModels(@NonNull EmojiPageModel model) { + return model.getDisplayEmoji() + .stream() + .map(e -> new EmojiPageViewGridAdapter.EmojiModel(model.getKey(), e)) + .collect(MappingModelList.collect()); + } + + private static class EmojiSearchResult { + private final String query; + private final EmojiPageModel model; + + private EmojiSearchResult(@NonNull String query, @Nullable EmojiPageModel model) { + this.query = query; + this.model = model; + } + + public EmojiSearchResult() { + this("", null); + } + } + + static class Factory implements ViewModelProvider.Factory { + + private final ReactWithAnyEmojiRepository repository; + private final long messageId; + private final boolean isMms; + + Factory(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) { + this.repository = repository; + this.messageId = messageId; + this.isMms = isMms; + } + + @Override + public @NonNull T create(@NonNull Class modelClass) { + //noinspection ConstantConditions + return modelClass.cast(new ReactWithAnyEmojiViewModel(repository, messageId, isMms, new EmojiSearchRepository(MessagingModuleConfiguration.getShared().getContext()))); + } + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java new file mode 100644 index 0000000000..0e70c41a90 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java @@ -0,0 +1,63 @@ +package org.thoughtcrime.securesms.reactions.any; + +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.annimon.stream.Stream; + +import org.thoughtcrime.securesms.components.emoji.Emoji; +import org.thoughtcrime.securesms.components.emoji.EmojiPageModel; +import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel; + +import java.util.List; + +import network.loki.messenger.R; + +/** + * Contains the Emojis that have been used in reactions for a given message. + */ +class ThisMessageEmojiPageModel implements EmojiPageModel { + + private final List emoji; + + ThisMessageEmojiPageModel(@NonNull List emoji) { + this.emoji = emoji; + } + + @Override + public String getKey() { + return RecentEmojiPageModel.KEY; + } + + @Override + public int getIconAttr() { + return R.attr.emoji_category_recent; + } + + @Override + public @NonNull List getEmoji() { + return emoji; + } + + @Override + public @NonNull List getDisplayEmoji() { + return Stream.of(getEmoji()).map(Emoji::new).toList(); + } + + @Override + public boolean hasSpriteMap() { + return false; + } + + @Override + public @Nullable Uri getSpriteUri() { + return null; + } + + @Override + public boolean isDynamic() { + return true; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ContextUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ContextUtil.java new file mode 100644 index 0000000000..f89cf738e8 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ContextUtil.java @@ -0,0 +1,19 @@ +package org.thoughtcrime.securesms.util; + +import android.content.Context; +import android.graphics.drawable.Drawable; + +import androidx.annotation.DrawableRes; +import androidx.annotation.NonNull; +import androidx.core.content.ContextCompat; + +import java.util.Objects; + +public final class ContextUtil { + private ContextUtil() {} + + public static @NonNull Drawable requireDrawable(@NonNull Context context, @DrawableRes int drawable) { + return Objects.requireNonNull(ContextCompat.getDrawable(context, drawable)); + } + +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java new file mode 100644 index 0000000000..e328e34e0b --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CursorUtil.java @@ -0,0 +1,89 @@ +package org.thoughtcrime.securesms.util; + +import android.database.Cursor; + +import androidx.annotation.NonNull; + +import java.util.Optional; + + +public final class CursorUtil { + + private CursorUtil() {} + + public static String requireString(@NonNull Cursor cursor, @NonNull String column) { + return cursor.getString(cursor.getColumnIndexOrThrow(column)); + } + + public static int requireInt(@NonNull Cursor cursor, @NonNull String column) { + return cursor.getInt(cursor.getColumnIndexOrThrow(column)); + } + + public static float requireFloat(@NonNull Cursor cursor, @NonNull String column) { + return cursor.getFloat(cursor.getColumnIndexOrThrow(column)); + } + + public static long requireLong(@NonNull Cursor cursor, @NonNull String column) { + return cursor.getLong(cursor.getColumnIndexOrThrow(column)); + } + + public static boolean requireBoolean(@NonNull Cursor cursor, @NonNull String column) { + return requireInt(cursor, column) != 0; + } + + public static byte[] requireBlob(@NonNull Cursor cursor, @NonNull String column) { + return cursor.getBlob(cursor.getColumnIndexOrThrow(column)); + } + + public static boolean isNull(@NonNull Cursor cursor, @NonNull String column) { + return cursor.isNull(cursor.getColumnIndexOrThrow(column)); + } + + public static Optional getString(@NonNull Cursor cursor, @NonNull String column) { + if (cursor.getColumnIndex(column) < 0) { + return Optional.empty(); + } else { + return Optional.ofNullable(requireString(cursor, column)); + } + } + + public static Optional getInt(@NonNull Cursor cursor, @NonNull String column) { + if (cursor.getColumnIndex(column) < 0) { + return Optional.empty(); + } else { + return Optional.of(requireInt(cursor, column)); + } + } + + public static Optional getBoolean(@NonNull Cursor cursor, @NonNull String column) { + if (cursor.getColumnIndex(column) < 0) { + return Optional.empty(); + } else { + return Optional.of(requireBoolean(cursor, column)); + } + } + + public static Optional getBlob(@NonNull Cursor cursor, @NonNull String column) { + if (cursor.getColumnIndex(column) < 0) { + return Optional.empty(); + } else { + return Optional.ofNullable(requireBlob(cursor, column)); + } + } + + /** + * Reads each column as a string, and concatenates them together into a single string separated by | + */ + public static String readRowAsString(@NonNull Cursor cursor) { + StringBuilder row = new StringBuilder(); + + for (int i = 0, len = cursor.getColumnCount(); i < len; i++) { + row.append(cursor.getString(i)); + if (i < len - 1) { + row.append(" | "); + } + } + + return row.toString(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DrawableUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/DrawableUtil.java new file mode 100644 index 0000000000..8115c92521 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/DrawableUtil.java @@ -0,0 +1,49 @@ +package org.thoughtcrime.securesms.util; + +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.drawable.Drawable; + +import androidx.annotation.ColorInt; +import androidx.annotation.NonNull; +import androidx.core.graphics.drawable.DrawableCompat; + +import org.thoughtcrime.securesms.conversation.v2.ViewUtil; + +public final class DrawableUtil { + + private static final int SHORTCUT_INFO_BITMAP_SIZE = ViewUtil.dpToPx(108); + public static final int SHORTCUT_INFO_WRAPPED_SIZE = ViewUtil.dpToPx(72); + private static final int SHORTCUT_INFO_PADDING = (SHORTCUT_INFO_BITMAP_SIZE - SHORTCUT_INFO_WRAPPED_SIZE) / 2; + + private DrawableUtil() {} + + public static @NonNull Bitmap toBitmap(@NonNull Drawable drawable, int width, int height) { + Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(bitmap); + + drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); + drawable.draw(canvas); + + return bitmap; + } + + public static @NonNull Bitmap wrapBitmapForShortcutInfo(@NonNull Bitmap toWrap) { + Bitmap bitmap = Bitmap.createBitmap(SHORTCUT_INFO_BITMAP_SIZE, SHORTCUT_INFO_BITMAP_SIZE, Bitmap.Config.ARGB_8888); + Bitmap scaled = Bitmap.createScaledBitmap(toWrap, SHORTCUT_INFO_WRAPPED_SIZE, SHORTCUT_INFO_WRAPPED_SIZE, true); + + Canvas canvas = new Canvas(bitmap); + canvas.drawBitmap(scaled, SHORTCUT_INFO_PADDING, SHORTCUT_INFO_PADDING, null); + + return bitmap; + } + + /** + * Returns a new {@link Drawable} that safely wraps and tints the provided drawable. + */ + public static @NonNull Drawable tint(@NonNull Drawable drawable, @ColorInt int tint) { + Drawable tinted = DrawableCompat.wrap(drawable).mutate(); + DrawableCompat.setTint(tinted, tint); + return tinted; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/InsetItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/util/InsetItemDecoration.kt new file mode 100644 index 0000000000..bfda0081af --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/InsetItemDecoration.kt @@ -0,0 +1,47 @@ +package org.thoughtcrime.securesms.util + +import android.graphics.Rect +import android.view.View +import androidx.recyclerview.widget.RecyclerView + +private typealias Predicate = (view: View, parent: RecyclerView) -> Boolean +private val ALWAYS_TRUE: Predicate = { _, _ -> true } + +/** + * Externally configurable inset "setter" for recycler views. + * + * Primary constructor provides full external control of view insets. + * Secondary constructors provide basic predicate based insets on the horizontal and vertical. + */ +open class InsetItemDecoration( + private val setInset: SetInset +) : RecyclerView.ItemDecoration() { + + constructor(horizontalInset: Int = 0, verticalInset: Int = 0) : this(horizontalInset, verticalInset, ALWAYS_TRUE) + constructor(horizontalInset: Int = 0, verticalInset: Int = 0, predicate: Predicate) : this(horizontalInset, horizontalInset, verticalInset, verticalInset, predicate) + constructor(leftInset: Int = 0, rightInset: Int = 0, topInset: Int = 0, bottomInset: Int = 0, predicate: Predicate = ALWAYS_TRUE) : this( + setInset = object : SetInset() { + override fun setInset(outRect: Rect, view: View, parent: RecyclerView) { + if (predicate == ALWAYS_TRUE || predicate.invoke(view, parent)) { + outRect.left = leftInset + outRect.right = rightInset + outRect.top = topInset + outRect.bottom = bottomInset + } + } + } + ) + + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + super.getItemOffsets(outRect, view, parent, state) + setInset.setInset(outRect, view, parent) + } + + abstract class SetInset { + abstract fun setInset(outRect: Rect, view: View, parent: RecyclerView) + + fun getPosition(view: View, parent: RecyclerView): Int { + return parent.getChildAdapterPosition(view) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt b/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt new file mode 100644 index 0000000000..e8f0e81841 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/LifecycleDisposable.kt @@ -0,0 +1,37 @@ +package org.thoughtcrime.securesms.util + +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import io.reactivex.disposables.CompositeDisposable +import io.reactivex.disposables.Disposable + +/** + * A lifecycle-aware [Disposable] that, after being bound to a lifecycle, will automatically dispose all contained disposables at the proper time. + */ +class LifecycleDisposable : DefaultLifecycleObserver { + val disposables: CompositeDisposable = CompositeDisposable() + + fun bindTo(lifecycleOwner: LifecycleOwner): LifecycleDisposable { + return bindTo(lifecycleOwner.lifecycle) + } + + fun bindTo(lifecycle: Lifecycle): LifecycleDisposable { + lifecycle.addObserver(this) + return this + } + + fun add(disposable: Disposable): LifecycleDisposable { + disposables.add(disposable) + return this + } + + override fun onDestroy(owner: LifecycleOwner) { + owner.lifecycle.removeObserver(this) + disposables.clear() + } + + operator fun plusAssign(disposable: Disposable) { + add(disposable) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ListUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/ListUtil.java new file mode 100644 index 0000000000..fa2281184d --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ListUtil.java @@ -0,0 +1,34 @@ +package org.thoughtcrime.securesms.util; + +import androidx.annotation.NonNull; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.stream.Stream; + +public final class ListUtil { + private ListUtil() {} + + public static List> chunk(@NonNull List list, int chunkSize) { + List> chunks = new ArrayList<>(list.size() / chunkSize); + + for (int i = 0; i < list.size(); i += chunkSize) { + List chunk = list.subList(i, Math.min(list.size(), i + chunkSize)); + chunks.add(chunk); + } + + return chunks; + } + + @SafeVarargs + public static List concat(Collection... items) { + final List concat = new ArrayList<>(Stream.of(items).map(Collection::size).reduce(0, Integer::sum)); + + for (Collection list : items) { + concat.addAll(list); + } + + return concat; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/NumberUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/NumberUtil.kt new file mode 100644 index 0000000000..c35ddbefaa --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/NumberUtil.kt @@ -0,0 +1,22 @@ +package org.thoughtcrime.securesms.util + +import java.util.Locale + +object NumberUtil { + + @JvmStatic + fun getFormattedNumber(count: Long): String { + val isNegative = count < 0 + val absoluteCount = Math.abs(count) + if (absoluteCount < 1000) return count.toString() + val thousands = absoluteCount / 1000 + val hundreds = (absoluteCount - thousands * 1000) / 100 + val negativePrefix = if (isNegative) "-" else "" + return if (hundreds == 0L) { + String.format(Locale.ROOT, "$negativePrefix%dk", thousands) + } else { + String.format(Locale.ROOT, "$negativePrefix%d.%dk", thousands, hundreds) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/PopupMenuUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/PopupMenuUtil.kt new file mode 100644 index 0000000000..a1105cfff3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/PopupMenuUtil.kt @@ -0,0 +1,24 @@ +package org.thoughtcrime.securesms.util + +import android.annotation.SuppressLint +import android.os.Build +import android.util.Log +import android.widget.PopupMenu + +@SuppressLint("PrivateApi") +@Deprecated(message = "Not needed when using appcompat 1.4.1+", replaceWith = ReplaceWith("setForceShowIcon(true)")) +fun PopupMenu.forceShowIcon() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + this.setForceShowIcon(true) + } else { + try { + val popupField = PopupMenu::class.java.getDeclaredField("mPopup") + popupField.isAccessible = true + val menu = popupField.get(this) + menu.javaClass.getDeclaredMethod("setForceShowIcon", Boolean::class.java) + .invoke(menu, true) + } catch (exception: Exception) { + Log.d("Loki", "Couldn't show message request popupmenu due to error: $exception.") + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ScreenDensity.java b/app/src/main/java/org/thoughtcrime/securesms/util/ScreenDensity.java new file mode 100644 index 0000000000..7e45d51996 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ScreenDensity.java @@ -0,0 +1,85 @@ +package org.thoughtcrime.securesms.util; + +import android.content.Context; +import android.util.DisplayMetrics; + +import androidx.annotation.NonNull; + +import org.session.libsession.messaging.MessagingModuleConfiguration; +import org.thoughtcrime.securesms.ApplicationContext; + +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * Helper class to get density information about a device's display + */ +public final class ScreenDensity { + + private static final String UNKNOWN = "unknown"; + + private static final float XHDPI_TO_LDPI = 0.25f; + private static final float XHDPI_TO_MDPI = 0.5f; + private static final float XHDPI_TO_HDPI = 0.75f; + + private static final LinkedHashMap LEVELS = new LinkedHashMap() {{ + put(DisplayMetrics.DENSITY_LOW, "ldpi"); + put(DisplayMetrics.DENSITY_MEDIUM, "mdpi"); + put(DisplayMetrics.DENSITY_HIGH, "hdpi"); + put(DisplayMetrics.DENSITY_XHIGH, "xhdpi"); + put(DisplayMetrics.DENSITY_XXHIGH, "xxhdpi"); + put(DisplayMetrics.DENSITY_XXXHIGH, "xxxhdpi"); + }}; + + private final String bucket; + private final int density; + + public ScreenDensity(String bucket, int density) { + this.bucket = bucket; + this.density = density; + } + + public static @NonNull ScreenDensity get(@NonNull Context context) { + int density = context.getResources().getDisplayMetrics().densityDpi; + + String bucket = UNKNOWN; + + for (Map.Entry entry : LEVELS.entrySet()) { + bucket = entry.getValue(); + if (entry.getKey() > density) { + break; + } + } + + return new ScreenDensity(bucket, density); + } + + public String getBucket() { + return bucket; + } + + public boolean isKnownDensity() { + return !bucket.equals(UNKNOWN); + } + + @Override + public @NonNull String toString() { + return bucket + " (" + density + ")"; + } + + public static float xhdpiRelativeDensityScaleFactor(@NonNull String density) { + switch (density) { + case "ldpi": + return XHDPI_TO_LDPI; + case "mdpi": + return XHDPI_TO_MDPI; + case "hdpi": + return XHDPI_TO_HDPI; + case "xhdpi": + return 1f; + default: + throw new IllegalStateException("Unsupported density: " + density); + } + + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.kt new file mode 100644 index 0000000000..4d92cd9de1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SqlUtil.kt @@ -0,0 +1,107 @@ +package org.thoughtcrime.securesms.util + +import androidx.annotation.VisibleForTesting +import java.util.stream.Collectors + +object SqlUtil { + /** The maximum number of arguments (i.e. question marks) allowed in a SQL statement. */ + private const val MAX_QUERY_ARGS = 999 + + @JvmStatic + fun buildArgs(vararg objects: Any?): Array { + return objects.map { + when (it) { + null -> throw NullPointerException("Cannot have null arg!") + else -> it.toString() + } + }.toTypedArray() + } + + @JvmStatic + fun buildArgs(argument: Long): Array { + return arrayOf(argument.toString()) + } + + /** + * A convenient way of making queries in the form: WHERE [column] IN (?, ?, ..., ?) + * Handles breaking it + */ + @JvmStatic + fun buildCollectionQuery(column: String, values: Collection): List { + return buildCollectionQuery(column, values, MAX_QUERY_ARGS) + } + + @VisibleForTesting + @JvmStatic + fun buildCollectionQuery(column: String, values: Collection, maxSize: Int): List { + require(!values.isEmpty()) { "Must have values!" } + + return values + .chunked(maxSize) + .map { batch -> buildSingleCollectionQuery(column, batch) } + } + + /** + * A convenient way of making queries in the form: WHERE [column] IN (?, ?, ..., ?) + * + * Important: Should only be used if you know the number of values is < 1000. Otherwise you risk creating a SQL statement this is too large. + * Prefer [buildCollectionQuery] when possible. + */ + @JvmStatic + fun buildSingleCollectionQuery(column: String, values: Collection): Query { + require(!values.isEmpty()) { "Must have values!" } + + val query = StringBuilder() + val args = arrayOfNulls(values.size) + + for ((i, value) in values.withIndex()) { + query.append("?") + args[i] = value + if (i != values.size - 1) { + query.append(", ") + } + } + return Query("$column IN ($query)", buildArgs(*args)) + } + + @JvmStatic + fun buildCustomCollectionQuery(query: String, argList: List>): List { + return buildCustomCollectionQuery(query, argList, MAX_QUERY_ARGS) + } + + @JvmStatic + @VisibleForTesting + fun buildCustomCollectionQuery(query: String, argList: List>, maxQueryArgs: Int): List { + val batchSize: Int = maxQueryArgs / argList[0].size + return ListUtil.chunk(argList, batchSize) + .stream() + .map { argBatch -> buildSingleCustomCollectionQuery(query, argBatch) } + .collect(Collectors.toList()) + } + + private fun buildSingleCustomCollectionQuery(query: String, argList: List>): Query { + val outputQuery = StringBuilder() + val outputArgs: MutableList = mutableListOf() + + var i = 0 + val len = argList.size + + while (i < len) { + outputQuery.append("(").append(query).append(")") + if (i < len - 1) { + outputQuery.append(" OR ") + } + + val args = argList[i] + for (arg in args) { + outputArgs += arg + } + + i++ + } + + return Query(outputQuery.toString(), outputArgs.toTypedArray()) + } + + class Query(val where: String, val whereArgs: Array) +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java index 41b18f0247..97f1257b28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/Util.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/Util.java @@ -16,11 +16,8 @@ */ package org.thoughtcrime.securesms.util; -import android.annotation.TargetApi; import android.app.ActivityManager; import android.content.Context; -import android.os.Build.VERSION; -import android.os.Build.VERSION_CODES; import android.text.TextUtils; import org.thoughtcrime.securesms.components.ComposeText; @@ -29,12 +26,9 @@ import network.loki.messenger.BuildConfig; public class Util { - @TargetApi(VERSION_CODES.KITKAT) public static boolean isLowMemory(Context context) { ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE); - - return (VERSION.SDK_INT >= VERSION_CODES.KITKAT && activityManager.isLowRamDevice()) || - activityManager.getLargeMemoryClass() <= 64; + return (activityManager.isLowRamDevice()) || activityManager.getLargeMemoryClass() <= 64; } public static boolean isEmpty(ComposeText value) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/AlwaysChangedDiffUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/AlwaysChangedDiffUtil.java new file mode 100644 index 0000000000..ab58c9cdaf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/AlwaysChangedDiffUtil.java @@ -0,0 +1,16 @@ +package org.thoughtcrime.securesms.util.adapter; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DiffUtil; + +public final class AlwaysChangedDiffUtil extends DiffUtil.ItemCallback { + @Override + public boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem) { + return false; + } + + @Override + public boolean areContentsTheSame(@NonNull T oldItem, @NonNull T newItem) { + return false; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/Factory.java b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/Factory.java new file mode 100644 index 0000000000..5dd7793f57 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/Factory.java @@ -0,0 +1,9 @@ +package org.thoughtcrime.securesms.util.adapter.mapping; + +import android.view.ViewGroup; + +import androidx.annotation.NonNull; + +public interface Factory> { + @NonNull MappingViewHolder createViewHolder(@NonNull ViewGroup parent); +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/LayoutFactory.java b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/LayoutFactory.java new file mode 100644 index 0000000000..438bb89e1c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/LayoutFactory.java @@ -0,0 +1,26 @@ +package org.thoughtcrime.securesms.util.adapter.mapping; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; + +import java.util.function.Function; + + +public class LayoutFactory> implements Factory { + private Function> creator; + private final int layout; + + public LayoutFactory(@NonNull Function> creator, @LayoutRes int layout) { + this.creator = creator; + this.layout = layout; + } + + @Override + public @NonNull MappingViewHolder createViewHolder(@NonNull ViewGroup parent) { + return creator.apply(LayoutInflater.from(parent.getContext()).inflate(layout, parent, false)); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingAdapter.java new file mode 100644 index 0000000000..83ade37ea1 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingAdapter.java @@ -0,0 +1,129 @@ +package org.thoughtcrime.securesms.util.adapter.mapping; + +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.LayoutRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.DefaultItemAnimator; +import androidx.recyclerview.widget.ListAdapter; +import androidx.recyclerview.widget.RecyclerView; + +import org.thoughtcrime.securesms.conversation.v2.NoCrossfadeChangeDefaultAnimator; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.function.Function; + +import kotlin.collections.CollectionsKt; +import kotlin.jvm.functions.Function1; + +/** + * A reusable and composable {@link RecyclerView.Adapter} built on-top of {@link ListAdapter} to + * provide async item diffing support. + *

+ * The adapter makes use of mapping a model class to view holder factory at runtime via one of the {@link #registerFactory(Class, Factory)} + * methods. The factory creates a view holder specifically designed to handle the paired model type. This allows the view holder concretely + * deal with the model type it cares about. Due to the enforcement of matching generics during factory registration we can safely ignore or + * override compiler typing recommendations when binding and diffing. + *

+ * General pattern for implementation: + *
    + *
  1. Create {@link MappingModel}s for the items in the list. These encapsulate data massaging methods for views to use and the diff logic.
  2. + *
  3. Create {@link MappingViewHolder}s for each item type in the list and their corresponding {@link Factory}.
  4. + *
  5. Create an instance or subclass of {@link MappingAdapter} and register the mapping of model type to view holder factory for that model type.
  6. + *
+ * Event listeners, click or otherwise, are handled at the view holder level and should be passed into the appropriate view holder factories. This + * pattern mimics how we pass data into view models via factories. + *

+ * NOTE: There can only be on factory registered per model type. Registering two for the same type will result in the last one being used. However, the + * same factory can be registered multiple times for multiple model types (if the model type class hierarchy supports it). + */ +public class MappingAdapter extends ListAdapter, MappingViewHolder> { + + final Map> factories; + final Map, Integer> itemTypes; + int typeCount; + + public MappingAdapter() { + super(new MappingDiffCallback()); + + factories = new HashMap<>(); + itemTypes = new HashMap<>(); + typeCount = 0; + } + + @Override + public void onViewAttachedToWindow(@NonNull MappingViewHolder holder) { + super.onViewAttachedToWindow(holder); + holder.onAttachedToWindow(); + } + + @Override + public void onViewDetachedFromWindow(@NonNull MappingViewHolder holder) { + super.onViewDetachedFromWindow(holder); + holder.onDetachedFromWindow(); + } + + @Override + public void onAttachedToRecyclerView(@NonNull RecyclerView recyclerView) { + super.onAttachedToRecyclerView(recyclerView); + if (recyclerView.getItemAnimator() != null && recyclerView.getItemAnimator().getClass() == DefaultItemAnimator.class) { + recyclerView.setItemAnimator(new NoCrossfadeChangeDefaultAnimator()); + } + } + + public > void registerFactory(Class clazz, Factory factory) { + int type = typeCount++; + factories.put(type, factory); + itemTypes.put(clazz, type); + } + + public > void registerFactory(@NonNull Class clazz, @NonNull Function> creator, @LayoutRes int layout) { + registerFactory(clazz, new LayoutFactory<>(creator, layout)); + } + + @Override + public int getItemViewType(int position) { + Integer type = itemTypes.get(getItem(position).getClass()); + if (type != null) { + return type; + } + throw new AssertionError("No view holder factory for type: " + getItem(position).getClass()); + } + + @Override + public @NonNull MappingViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + return Objects.requireNonNull(factories.get(viewType)).createViewHolder(parent); + } + + @Override + public void onBindViewHolder(@NonNull MappingViewHolder holder, int position, @NonNull List payloads) { + holder.setPayload(payloads); + onBindViewHolder(holder, position); + } + + @Override + public void onBindViewHolder(@NonNull MappingViewHolder holder, int position) { + //noinspection unchecked + holder.bind(getItem(position)); + } + + public > int indexOfFirst(@NonNull Class clazz, @NonNull Function1 predicate) { + return CollectionsKt.indexOfFirst(getCurrentList(), m -> { + //noinspection unchecked + return clazz.isAssignableFrom(m.getClass()) && predicate.invoke((T) m); + }); + } + + public @NonNull Optional> getModel(int index) { + List> currentList = getCurrentList(); + if (index >= 0 && index < currentList.size()) { + return Optional.ofNullable(currentList.get(index)); + } + return Optional.empty(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingDiffCallback.java b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingDiffCallback.java new file mode 100644 index 0000000000..b48d4ad8cd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingDiffCallback.java @@ -0,0 +1,38 @@ +package org.thoughtcrime.securesms.util.adapter.mapping; + +import android.annotation.SuppressLint; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.DiffUtil; + +class MappingDiffCallback extends DiffUtil.ItemCallback> { + @Override + public boolean areItemsTheSame(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) { + if (oldItem.getClass() == newItem.getClass()) { + //noinspection unchecked + return oldItem.areItemsTheSame(newItem); + } + return false; + } + + @SuppressLint("DiffUtilEquals") + @Override + public boolean areContentsTheSame(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) { + if (oldItem.getClass() == newItem.getClass()) { + //noinspection unchecked + return oldItem.areContentsTheSame(newItem); + } + return false; + } + + @Override + public @Nullable Object getChangePayload(@NonNull MappingModel oldItem, @NonNull MappingModel newItem) { + if (oldItem.getClass() == newItem.getClass()) { + //noinspection unchecked + return oldItem.getChangePayload(newItem); + } + + return null; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingModel.java b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingModel.java new file mode 100644 index 0000000000..e6eae75023 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingModel.java @@ -0,0 +1,13 @@ +package org.thoughtcrime.securesms.util.adapter.mapping; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public interface MappingModel { + boolean areItemsTheSame(@NonNull T newItem); + boolean areContentsTheSame(@NonNull T newItem); + + default @Nullable Object getChangePayload(@NonNull T newItem) { + return null; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingModelList.kt b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingModelList.kt new file mode 100644 index 0000000000..ab09b31bbf --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingModelList.kt @@ -0,0 +1,65 @@ +package org.thoughtcrime.securesms.util.adapter.mapping + +import java.util.function.BiConsumer +import java.util.function.BinaryOperator +import java.util.function.Function +import java.util.function.Supplier +import java.util.stream.Collector + +class MappingModelList : ArrayList?> { + constructor() {} + constructor(c: Collection?>) : super(c) {} + + companion object { + @JvmStatic + fun singleton(model: MappingModel<*>): MappingModelList { + val list = MappingModelList() + list.add(model) + return list + } + + @JvmStatic + fun collect(): Collector, MappingModelList, MappingModelList> { + return object : Collector, MappingModelList, MappingModelList> { + override fun supplier(): Supplier { + return java.util.function.Supplier { MappingModelList() } + } + + override fun accumulator(): BiConsumer> { + return java.util.function.BiConsumer { obj: MappingModelList, e: MappingModel<*> -> obj.add(e) } + } + + override fun combiner(): BinaryOperator { + return BinaryOperator { left: MappingModelList, right: MappingModelList -> + left.addAll(right) + left + } + } + + override fun finisher(): Function { + return Function.identity() + } + + override fun characteristics(): Set { + return setOf(Collector.Characteristics.IDENTITY_FINISH) + } + } + } + + /*fun toMappingModelList(): Collector, MappingModelList, MappingModelList> { + return object : Collector, MappingModelList, MappingModelList?> { + override fun supplier(): Supplier { + return Supplier { MappingModelList() } + } + + override fun accumulator(): BiConsumer> { + return BiConsumer { obj: MappingModelList, e: MappingModel<*> -> obj.add(e) } + } + + override fun finisher(): Function { + return Function { mappingModels: MappingModelList? -> mappingModels } + } + } + }*/ + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingViewHolder.java new file mode 100644 index 0000000000..fde4990cdd --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/util/adapter/mapping/MappingViewHolder.java @@ -0,0 +1,53 @@ +package org.thoughtcrime.securesms.util.adapter.mapping; + +import android.content.Context; +import android.view.View; + +import androidx.annotation.IdRes; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import java.util.LinkedList; +import java.util.List; + +public abstract class MappingViewHolder extends RecyclerView.ViewHolder { + + protected final Context context; + protected final List payload; + + public MappingViewHolder(@NonNull View itemView) { + super(itemView); + context = itemView.getContext(); + payload = new LinkedList<>(); + } + + public T findViewById(@IdRes int id) { + return itemView.findViewById(id); + } + + public @NonNull Context getContext() { + return itemView.getContext(); + } + + public void onAttachedToWindow() { + } + + public void onDetachedFromWindow() { + } + + public abstract void bind(@NonNull Model model); + + public void setPayload(@NonNull List payload) { + this.payload.clear(); + this.payload.addAll(payload); + } + + public static final class SimpleViewHolder extends MappingViewHolder { + public SimpleViewHolder(@NonNull View itemView) { + super(itemView); + } + + @Override + public void bind(@NonNull Model model) { } + } +} diff --git a/app/src/main/res/anim/delay_fade_in.xml b/app/src/main/res/anim/delay_fade_in.xml new file mode 100644 index 0000000000..b8e737fa41 --- /dev/null +++ b/app/src/main/res/anim/delay_fade_in.xml @@ -0,0 +1,7 @@ + diff --git a/app/src/main/res/anim/shrink_fade_out.xml b/app/src/main/res/anim/shrink_fade_out.xml new file mode 100644 index 0000000000..c5de08754c --- /dev/null +++ b/app/src/main/res/anim/shrink_fade_out.xml @@ -0,0 +1,30 @@ + + + + + + + diff --git a/app/src/main/res/animator/reactions_scrubber_hide.xml b/app/src/main/res/animator/reactions_scrubber_hide.xml new file mode 100644 index 0000000000..0a5fc05468 --- /dev/null +++ b/app/src/main/res/animator/reactions_scrubber_hide.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/animator/reactions_scrubber_reveal.xml b/app/src/main/res/animator/reactions_scrubber_reveal.xml new file mode 100644 index 0000000000..a5b78a3b85 --- /dev/null +++ b/app/src/main/res/animator/reactions_scrubber_reveal.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/icon_tab_selector.xml b/app/src/main/res/color/icon_tab_selector.xml new file mode 100644 index 0000000000..30cd1bdc27 --- /dev/null +++ b/app/src/main/res/color/icon_tab_selector.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-notnight/reaction_pill_background.xml b/app/src/main/res/drawable-notnight/reaction_pill_background.xml new file mode 100644 index 0000000000..3ff042175a --- /dev/null +++ b/app/src/main/res/drawable-notnight/reaction_pill_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable-notnight/reaction_pill_background_selected.xml b/app/src/main/res/drawable-notnight/reaction_pill_background_selected.xml new file mode 100644 index 0000000000..2c735fb23d --- /dev/null +++ b/app/src/main/res/drawable-notnight/reaction_pill_background_selected.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable-notnight/reaction_pill_dialog_background.xml b/app/src/main/res/drawable-notnight/reaction_pill_dialog_background.xml new file mode 100644 index 0000000000..2d9fa321b8 --- /dev/null +++ b/app/src/main/res/drawable-notnight/reaction_pill_dialog_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/bottom_sheet_handle.xml b/app/src/main/res/drawable/bottom_sheet_handle.xml new file mode 100644 index 0000000000..689cf2e602 --- /dev/null +++ b/app/src/main/res/drawable/bottom_sheet_handle.xml @@ -0,0 +1,10 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/context_menu_background.xml b/app/src/main/res/drawable/context_menu_background.xml new file mode 100644 index 0000000000..e5a1ec0dd8 --- /dev/null +++ b/app/src/main/res/drawable/context_menu_background.xml @@ -0,0 +1,9 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/context_menu_item_background_bottom.xml b/app/src/main/res/drawable/context_menu_item_background_bottom.xml new file mode 100644 index 0000000000..ef07cf4c56 --- /dev/null +++ b/app/src/main/res/drawable/context_menu_item_background_bottom.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/context_menu_item_background_middle.xml b/app/src/main/res/drawable/context_menu_item_background_middle.xml new file mode 100644 index 0000000000..cd77b4644a --- /dev/null +++ b/app/src/main/res/drawable/context_menu_item_background_middle.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/context_menu_item_background_only.xml b/app/src/main/res/drawable/context_menu_item_background_only.xml new file mode 100644 index 0000000000..b54231f5b6 --- /dev/null +++ b/app/src/main/res/drawable/context_menu_item_background_only.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/context_menu_item_background_top.xml b/app/src/main/res/drawable/context_menu_item_background_top.xml new file mode 100644 index 0000000000..9c748a532f --- /dev/null +++ b/app/src/main/res/drawable/context_menu_item_background_top.xml @@ -0,0 +1,15 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/conversation_reaction_overlay_background.xml b/app/src/main/res/drawable/conversation_reaction_overlay_background.xml new file mode 100644 index 0000000000..1a8b214ed9 --- /dev/null +++ b/app/src/main/res/drawable/conversation_reaction_overlay_background.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_any_emoji_32.xml b/app/src/main/res/drawable/ic_any_emoji_32.xml new file mode 100644 index 0000000000..ab79446437 --- /dev/null +++ b/app/src/main/res/drawable/ic_any_emoji_32.xml @@ -0,0 +1,18 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_arrow_left_24.xml b/app/src/main/res/drawable/ic_arrow_left_24.xml new file mode 100644 index 0000000000..3b4a9cab4d --- /dev/null +++ b/app/src/main/res/drawable/ic_arrow_left_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_baseline_check_circle_outline_24.xml b/app/src/main/res/drawable/ic_baseline_check_circle_outline_24.xml new file mode 100644 index 0000000000..b985a9d03c --- /dev/null +++ b/app/src/main/res/drawable/ic_baseline_check_circle_outline_24.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_chevron_up_light.xml b/app/src/main/res/drawable/ic_chevron_up_light.xml new file mode 100644 index 0000000000..728180ab6d --- /dev/null +++ b/app/src/main/res/drawable/ic_chevron_up_light.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_emoji.xml b/app/src/main/res/drawable/ic_emoji.xml new file mode 100644 index 0000000000..f36026d583 --- /dev/null +++ b/app/src/main/res/drawable/ic_emoji.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_outline_disabled_by_default_24.xml b/app/src/main/res/drawable/ic_outline_disabled_by_default_24.xml new file mode 100644 index 0000000000..7db0071309 --- /dev/null +++ b/app/src/main/res/drawable/ic_outline_disabled_by_default_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/ic_search_24.xml b/app/src/main/res/drawable/ic_search_24.xml new file mode 100644 index 0000000000..1d79a060b1 --- /dev/null +++ b/app/src/main/res/drawable/ic_search_24.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_x.xml b/app/src/main/res/drawable/ic_x.xml new file mode 100644 index 0000000000..4fa8aa41a3 --- /dev/null +++ b/app/src/main/res/drawable/ic_x.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/reaction_pill_background.xml b/app/src/main/res/drawable/reaction_pill_background.xml new file mode 100644 index 0000000000..02d56da9ba --- /dev/null +++ b/app/src/main/res/drawable/reaction_pill_background.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/reaction_pill_background_selected.xml b/app/src/main/res/drawable/reaction_pill_background_selected.xml new file mode 100644 index 0000000000..cdc2488629 --- /dev/null +++ b/app/src/main/res/drawable/reaction_pill_background_selected.xml @@ -0,0 +1,6 @@ + + + + + + diff --git a/app/src/main/res/drawable/reaction_pill_dialog_background.xml b/app/src/main/res/drawable/reaction_pill_dialog_background.xml new file mode 100644 index 0000000000..0b79d420dd --- /dev/null +++ b/app/src/main/res/drawable/reaction_pill_dialog_background.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/drawable/reactions_old_background.xml b/app/src/main/res/drawable/reactions_old_background.xml new file mode 100644 index 0000000000..b59298254a --- /dev/null +++ b/app/src/main/res/drawable/reactions_old_background.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/search_bar_end.xml b/app/src/main/res/drawable/search_bar_end.xml new file mode 100644 index 0000000000..cf31e32384 --- /dev/null +++ b/app/src/main/res/drawable/search_bar_end.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/search_bar_start.xml b/app/src/main/res/drawable/search_bar_start.xml new file mode 100644 index 0000000000..8ee0f611ca --- /dev/null +++ b/app/src/main/res/drawable/search_bar_start.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/toolbar_shadow.xml b/app/src/main/res/drawable/toolbar_shadow.xml new file mode 100644 index 0000000000..ce05d55a1b --- /dev/null +++ b/app/src/main/res/drawable/toolbar_shadow.xml @@ -0,0 +1,8 @@ + + + + diff --git a/app/src/main/res/drawable/vertical_divider.xml b/app/src/main/res/drawable/vertical_divider.xml new file mode 100644 index 0000000000..88e5898e99 --- /dev/null +++ b/app/src/main/res/drawable/vertical_divider.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_conversation_v2.xml b/app/src/main/res/layout/activity_conversation_v2.xml index 84cbeeaeae..71de482d0c 100644 --- a/app/src/main/res/layout/activity_conversation_v2.xml +++ b/app/src/main/res/layout/activity_conversation_v2.xml @@ -8,6 +8,18 @@ android:layout_height="match_parent" android:orientation="vertical"> + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/context_menu_item.xml b/app/src/main/res/layout/context_menu_item.xml new file mode 100644 index 0000000000..04354f5b10 --- /dev/null +++ b/app/src/main/res/layout/context_menu_item.xml @@ -0,0 +1,30 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conversation_reaction_scrubber.xml b/app/src/main/res/layout/conversation_reaction_scrubber.xml new file mode 100644 index 0000000000..910cd31be5 --- /dev/null +++ b/app/src/main/res/layout/conversation_reaction_scrubber.xml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/emoji_display_item_grid.xml b/app/src/main/res/layout/emoji_display_item_grid.xml new file mode 100644 index 0000000000..e1729197e7 --- /dev/null +++ b/app/src/main/res/layout/emoji_display_item_grid.xml @@ -0,0 +1,18 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/emoji_grid_header.xml b/app/src/main/res/layout/emoji_grid_header.xml new file mode 100644 index 0000000000..0b59c52ac0 --- /dev/null +++ b/app/src/main/res/layout/emoji_grid_header.xml @@ -0,0 +1,13 @@ + + diff --git a/app/src/main/res/layout/emoji_grid_no_results.xml b/app/src/main/res/layout/emoji_grid_no_results.xml new file mode 100644 index 0000000000..7ced3c60eb --- /dev/null +++ b/app/src/main/res/layout/emoji_grid_no_results.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/layout/emoji_text_display_item_grid.xml b/app/src/main/res/layout/emoji_text_display_item_grid.xml new file mode 100644 index 0000000000..32555bfe65 --- /dev/null +++ b/app/src/main/res/layout/emoji_text_display_item_grid.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/keyboard_pager_search_bar.xml b/app/src/main/res/layout/keyboard_pager_search_bar.xml new file mode 100644 index 0000000000..d695752d48 --- /dev/null +++ b/app/src/main/res/layout/keyboard_pager_search_bar.xml @@ -0,0 +1,89 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/react_with_any_emoji_dialog_fragment.xml b/app/src/main/res/layout/react_with_any_emoji_dialog_fragment.xml new file mode 100644 index 0000000000..2369e00b58 --- /dev/null +++ b/app/src/main/res/layout/react_with_any_emoji_dialog_fragment.xml @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/react_with_any_emoji_status_fade.xml b/app/src/main/res/layout/react_with_any_emoji_status_fade.xml new file mode 100644 index 0000000000..f2b9378c6c --- /dev/null +++ b/app/src/main/res/layout/react_with_any_emoji_status_fade.xml @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment.xml b/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment.xml new file mode 100644 index 0000000000..f07d321d79 --- /dev/null +++ b/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recipient_item.xml b/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recipient_item.xml new file mode 100644 index 0000000000..9616b18b56 --- /dev/null +++ b/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recipient_item.xml @@ -0,0 +1,50 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recycler.xml b/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recycler.xml new file mode 100644 index 0000000000..91d4c6c861 --- /dev/null +++ b/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recycler.xml @@ -0,0 +1,12 @@ + + diff --git a/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recycler_footer.xml b/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recycler_footer.xml new file mode 100644 index 0000000000..dc21b299ba --- /dev/null +++ b/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recycler_footer.xml @@ -0,0 +1,9 @@ + + \ No newline at end of file diff --git a/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recycler_header.xml b/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recycler_header.xml new file mode 100644 index 0000000000..338ea70244 --- /dev/null +++ b/app/src/main/res/layout/reactions_bottom_sheet_dialog_fragment_recycler_header.xml @@ -0,0 +1,40 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/reactions_pill.xml b/app/src/main/res/layout/reactions_pill.xml new file mode 100644 index 0000000000..16b026cb48 --- /dev/null +++ b/app/src/main/res/layout/reactions_pill.xml @@ -0,0 +1,35 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_conversation.xml b/app/src/main/res/layout/view_conversation.xml index 4e6df7fafb..0e0173a31c 100644 --- a/app/src/main/res/layout/view_conversation.xml +++ b/app/src/main/res/layout/view_conversation.xml @@ -124,10 +124,10 @@ android:id="@+id/snippetTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:maxLines="1" android:ellipsize="end" - android:textSize="@dimen/medium_font_size" + android:maxLines="1" android:textColor="@color/text" + android:textSize="@dimen/medium_font_size" tools:text="Sorry, gotta go fight crime again" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_visible_message.xml b/app/src/main/res/layout/view_visible_message.xml index 28377be50c..83aec99712 100644 --- a/app/src/main/res/layout/view_visible_message.xml +++ b/app/src/main/res/layout/view_visible_message.xml @@ -11,49 +11,50 @@ android:id="@+id/dateBreakTextView" android:layout_width="match_parent" android:layout_height="@dimen/large_spacing" - tools:text="@tools:sample/date/hhmmss" android:gravity="center" android:textColor="@color/text" android:textSize="@dimen/very_small_font_size" - android:textStyle="bold" /> + android:textStyle="bold" + tools:text="@tools:sample/date/hhmmss" /> + android:gravity="bottom" + android:paddingBottom="@dimen/small_spacing"> + android:layout_height="1dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" /> + android:layout_gravity="center" + app:layout_constraintEnd_toStartOf="@+id/messageInnerContainer" + app:layout_constraintBottom_toBottomOf="@id/messageInnerContainer" + app:layout_constraintStart_toEndOf="@+id/startSpacing" + tools:visibility="visible" /> + android:src="@drawable/ic_crown" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="@+id/profilePictureView" + app:layout_constraintEnd_toEndOf="@+id/profilePictureView" + tools:visibility="visible" /> + app:layout_constraintStart_toStartOf="@+id/messageInnerContainer" + app:layout_constraintTop_toTopOf="parent" + tools:text="@tools:sample/full_names" /> + android:layout_height="wrap_content" /> + android:layout_marginHorizontal="@dimen/small_spacing" + android:visibility="invisible" + tools:visibility="visible" /> + android:layout_height="0dp" + android:layout_weight="1" + android:minWidth="@dimen/very_large_spacing" /> + + + app:layout_constraintBottom_toBottomOf="@+id/messageInnerContainer"> + + #333132 #0A000000 #8000E97B + + + @color/core_black + + @color/core_white + + @color/core_grey_60 + @color/core_grey_75 + @color/transparent_black_70 + @color/transparent_white_87 + + @color/core_white + @color/core_grey_05 + @color/core_grey_02 + @color/core_white + @color/signal_background_secondary + + @color/core_grey_90 + @color/core_grey_25 + @color/core_grey_65 + + @color/core_white + @color/core_grey_05 + @color/core_grey_60 + @color/core_white + + @color/transparent_black + @color/transparent_black_40 + + + @color/core_grey_75 + @color/core_grey_45 + + + @color/core_grey_60 + @color/core_black + + @color/core_grey_45 + + @color/transparent_black_15 + @color/core_white + @color/transparent_white_20 + + @color/core_grey_05 + @color/core_grey_25 + @color/core_grey_15 + \ No newline at end of file diff --git a/app/src/main/res/values-notnight-v21/themes.xml b/app/src/main/res/values-notnight-v21/themes.xml index abbf3f8718..737c91df67 100644 --- a/app/src/main/res/values-notnight-v21/themes.xml +++ b/app/src/main/res/values-notnight-v21/themes.xml @@ -2,6 +2,7 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 4ebdfd8632..481b36dd59 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -69,6 +69,7 @@ + @@ -108,6 +109,7 @@ + @@ -214,8 +216,20 @@ + + + + + + + + + + + + @@ -300,4 +314,13 @@ + + + + + + + + + diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 5ae56b70cd..2a454c97b3 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -72,11 +72,14 @@ #ff4d4d4d #ff383838 + #26000000 #30000000 #40000000 #70000000 + #dfffffff #90000000 + #33ffffff #30ffffff #40ffffff #aaffffff @@ -96,4 +99,53 @@ #121212 #171717 + + + @color/core_white + + @color/transparent_black_15 + + @color/core_grey_75 + + @color/core_grey_35 + @color/core_grey_15 + @color/transparent_black_70 + #df5e5e5e + + @color/core_grey_95 + @color/core_grey_75 + @color/core_grey_90 + @color/core_grey_75 + @color/core_grey_65 + + @color/core_grey_05 + @color/core_grey_60 + @color/core_grey_25 + + @color/core_grey_75 + @color/core_grey_65 + @color/core_grey_25 + @color/core_grey_65 + + + @color/transparent + @color/transparent_white_40 + + + @color/core_white + @color/core_grey_25 + + @color/core_grey_25 + @color/core_white + + @color/core_grey_15 + + @color/transparent_black_15 + @color/core_white + @color/transparent_white_20 + + @color/core_grey_75 + @color/core_grey_45 + @color/core_grey_45 + diff --git a/app/src/main/res/values/core_colors.xml b/app/src/main/res/values/core_colors.xml index e99bedcc97..a1114eaae4 100644 --- a/app/src/main/res/values/core_colors.xml +++ b/app/src/main/res/values/core_colors.xml @@ -10,9 +10,12 @@ #eeefef #d5d6d6 #bbbdbe + #9e9e9e #898a8c #6b6d70 + #525252 #3d3e44 + #2D2D2D #23252a #17191d #0f1012 diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 7209f3ea51..87ee7f1d55 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -128,4 +128,21 @@ 56dp 120dp + 18dp + + 136dp + 40dp + 30dp + 25dp + 16dp + + 25dp + 0dp + 320dp + + 44dp + + 390dp + 40dp + diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index bb4c483c99..4d1a6c4077 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -1,4 +1,9 @@ 300 + + 200 + 100 + 150 + 10 \ 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 9b315026d6..4eecdf97b8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -562,8 +562,10 @@ Ban user Ban and delete all Resend message + Reply Reply to message Call + Select Save attachment @@ -925,4 +927,36 @@ Having notifications disabled will prevent you from receiving calls, go to Session notification settings? Dismiss + + This Message + Recently Used + Smileys & People + Nature + Food + Activities + Places + Objects + Symbols + Flags + Emoticons + No results found + + + All · %1$d + + + +%1$d + + + You + %1$s reacted to a message %2$s + Show less + Search emoji + Back to emoji + Clear search entry + + + And %1$d other has reacted %2$s to this message + And %1$d others have reacted %2$s to this message + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 4ad16737c1..615c7b5090 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -264,4 +264,8 @@ ?attr/selectableItemBackground + + diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 34cf69fd8a..0e9cbb7213 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -3,6 +3,7 @@ @@ -296,4 +299,57 @@ true + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt index c93a4c0837..2a0ecc62df 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/conversation/v2/ConversationViewModelTest.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.conversation.v2 +import com.goterl.lazysodium.utils.KeyPair import kotlinx.coroutines.flow.first import org.hamcrest.CoreMatchers.endsWith import org.hamcrest.CoreMatchers.equalTo @@ -14,6 +15,7 @@ import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.session.libsession.utilities.recipients.Recipient import org.thoughtcrime.securesms.BaseViewModelTest +import org.thoughtcrime.securesms.database.Storage import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.repository.ConversationRepository import org.thoughtcrime.securesms.repository.ResultOf @@ -22,12 +24,14 @@ import org.mockito.Mockito.`when` as whenever class ConversationViewModelTest: BaseViewModelTest() { private val repository = mock(ConversationRepository::class.java) + private val storage = mock(Storage::class.java) private val threadId = 123L + private val edKeyPair = mock(KeyPair::class.java) private lateinit var recipient: Recipient private val viewModel: ConversationViewModel by lazy { - ConversationViewModel(threadId, repository) + ConversationViewModel(threadId, edKeyPair, repository, storage) } @Before diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/NumberUtilTests.kt b/app/src/test/java/org/thoughtcrime/securesms/util/NumberUtilTests.kt new file mode 100644 index 0000000000..6d741cf16b --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/NumberUtilTests.kt @@ -0,0 +1,59 @@ +package org.thoughtcrime.securesms.util + +import org.junit.Assert.assertEquals +import org.junit.Test + + +class NumberUtilTests { + + @Test + fun `it should display numbers less than 1000 as they are`() { + val formatString = NumberUtil.getFormattedNumber(900) + assertEquals("900", formatString) + } + + @Test + fun `it should display exactly 1000 as 1k`() { + val formatString = NumberUtil.getFormattedNumber(1000) + assertEquals("1k", formatString) + } + + @Test + fun `it should display numbers less than 10_000 properly`() { + val formatString = NumberUtil.getFormattedNumber(1300) + assertEquals("1.3k", formatString) + val multipleKFormatString = NumberUtil.getFormattedNumber(3100) + assertEquals("3.1k", multipleKFormatString) + } + + @Test + fun `it should display zero properly`() { + val formatString = NumberUtil.getFormattedNumber(0) + assertEquals("0", formatString) + } + + @Test + fun `it shouldn't care about negative numbers`() { + val formatString = NumberUtil.getFormattedNumber(-10) + assertEquals("-10", formatString) + } + + @Test + fun `it shouldn't get about large negative numbers`() { + val formatString = NumberUtil.getFormattedNumber(-1200) + assertEquals("-1.2k", formatString) + } + + @Test + fun `it should display numbers above 10k properly`() { + val formatString = NumberUtil.getFormattedNumber(12300) + assertEquals("12.3k", formatString) + } + + @Test + fun `it should display numbers above 100k properly`() { + val formatString = NumberUtil.getFormattedNumber(132560) + assertEquals("132.5k", formatString) + } + +} \ No newline at end of file diff --git a/app/src/test/java/org/thoughtcrime/securesms/util/OpenGroupMigrationTests.kt b/app/src/test/java/org/thoughtcrime/securesms/util/OpenGroupMigrationTests.kt new file mode 100644 index 0000000000..dcf8ca231b --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/util/OpenGroupMigrationTests.kt @@ -0,0 +1,281 @@ +package org.thoughtcrime.securesms.util + +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import org.mockito.kotlin.KStubbing +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.session.libsession.messaging.open_groups.OpenGroup +import org.session.libsession.messaging.open_groups.OpenGroupApi +import org.session.libsession.utilities.Address +import org.session.libsession.utilities.recipients.Recipient +import org.thoughtcrime.securesms.database.GroupDatabase +import org.thoughtcrime.securesms.database.LokiAPIDatabase +import org.thoughtcrime.securesms.database.LokiMessageDatabase +import org.thoughtcrime.securesms.database.LokiThreadDatabase +import org.thoughtcrime.securesms.database.MmsDatabase +import org.thoughtcrime.securesms.database.SmsDatabase +import org.thoughtcrime.securesms.database.ThreadDatabase +import org.thoughtcrime.securesms.database.model.ThreadRecord +import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.groups.OpenGroupMigrator +import org.thoughtcrime.securesms.groups.OpenGroupMigrator.OpenGroupMapping +import org.thoughtcrime.securesms.groups.OpenGroupMigrator.roomStub + +class OpenGroupMigrationTests { + + companion object { + const val EXAMPLE_LEGACY_ENCODED_OPEN_GROUP = "__loki_public_chat_group__!687474703a2f2f3131362e3230332e37302e33332e6f78656e" + const val EXAMPLE_NEW_ENCODED_OPEN_GROUP = "__loki_public_chat_group__!68747470733a2f2f6f70656e2e67657473657373696f6e2e6f72672e6f78656e" + const val OXEN_STUB_HEX = "6f78656e" + + const val EXAMPLE_LEGACY_SERVER_ID = "http://116.203.70.33.oxen" + const val EXAMPLE_NEW_SERVER_ID = "https://open.getsession.org.oxen" + + const val LEGACY_THREAD_ID = 1L + const val NEW_THREAD_ID = 2L + } + + private fun legacyOpenGroupRecipient(additionalMocks: ((KStubbing) -> Unit) ? = null) = mock { + on { address } doReturn Address.fromSerialized(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP) + on { isOpenGroupRecipient } doReturn true + additionalMocks?.let { it(this) } + } + + private fun newOpenGroupRecipient(additionalMocks: ((KStubbing) -> Unit) ? = null) = mock { + on { address } doReturn Address.fromSerialized(EXAMPLE_NEW_ENCODED_OPEN_GROUP) + on { isOpenGroupRecipient } doReturn true + additionalMocks?.let { it(this) } + } + + private fun legacyThreadRecord(additionalRecipientMocks: ((KStubbing) -> Unit) ? = null, additionalThreadMocks: ((KStubbing) -> Unit)? = null) = mock { + val returnedRecipient = legacyOpenGroupRecipient(additionalRecipientMocks) + on { recipient } doReturn returnedRecipient + on { threadId } doReturn LEGACY_THREAD_ID + } + + private fun newThreadRecord(additionalRecipientMocks: ((KStubbing) -> Unit)? = null, additionalThreadMocks: ((KStubbing) -> Unit)? = null) = mock { + val returnedRecipient = newOpenGroupRecipient(additionalRecipientMocks) + on { recipient } doReturn returnedRecipient + on { threadId } doReturn NEW_THREAD_ID + } + + @Test + fun `it should generate the correct room stubs for legacy groups`() { + val mockRecipient = legacyOpenGroupRecipient() + assertEquals(OXEN_STUB_HEX, mockRecipient.roomStub()) + } + + @Test + fun `it should generate the correct room stubs for new groups`() { + val mockNewRecipient = newOpenGroupRecipient() + assertEquals(OXEN_STUB_HEX, mockNewRecipient.roomStub()) + } + + @Test + fun `it should return correct mappings`() { + val legacyThread = legacyThreadRecord() + val newThread = newThreadRecord() + + val expectedMapping = listOf( + OpenGroupMapping(OXEN_STUB_HEX, LEGACY_THREAD_ID, NEW_THREAD_ID) + ) + + assertTrue(expectedMapping.containsAll(OpenGroupMigrator.getExistingMappings(listOf(legacyThread), listOf(newThread)))) + } + + @Test + fun `it should return no mappings if there are no legacy open groups`() { + val mappings = OpenGroupMigrator.getExistingMappings(listOf(), listOf()) + assertTrue(mappings.isEmpty()) + } + + @Test + fun `it should return no mappings if there are only new open groups`() { + val newThread = newThreadRecord() + val mappings = OpenGroupMigrator.getExistingMappings(emptyList(), listOf(newThread)) + assertTrue(mappings.isEmpty()) + } + + @Test + fun `it should return null new thread in mappings if there are only legacy open groups`() { + val legacyThread = legacyThreadRecord() + val mappings = OpenGroupMigrator.getExistingMappings(listOf(legacyThread), emptyList()) + val expectedMappings = listOf( + OpenGroupMapping(OXEN_STUB_HEX, LEGACY_THREAD_ID, null) + ) + assertTrue(expectedMappings.containsAll(mappings)) + } + + @Test + fun `test migration thread DB calls legacy and returns if no legacy official groups`() { + val mockedThreadDb = mock { + on { legacyOxenOpenGroups } doReturn emptyList() + } + val mockedDbComponent = mock { + on { threadDatabase() } doReturn mockedThreadDb + } + + OpenGroupMigrator.migrate(mockedDbComponent) + + verify(mockedDbComponent).threadDatabase() + verify(mockedThreadDb).legacyOxenOpenGroups + verifyNoMoreInteractions(mockedThreadDb) + } + + @Test + fun `it should migrate on thread, group and loki dbs with correct values for legacy only migration`() { + // mock threadDB + val capturedThreadId = argumentCaptor() + val capturedNewEncoded = argumentCaptor() + val mockedThreadDb = mock { + val legacyThreadRecord = legacyThreadRecord() + on { legacyOxenOpenGroups } doReturn listOf(legacyThreadRecord) + on { httpsOxenOpenGroups } doReturn emptyList() + on { migrateEncodedGroup(capturedThreadId.capture(), capturedNewEncoded.capture()) } doAnswer {} + } + + // mock groupDB + val capturedGroupLegacyEncoded = argumentCaptor() + val capturedGroupNewEncoded = argumentCaptor() + val mockedGroupDb = mock { + on { + migrateEncodedGroup( + capturedGroupLegacyEncoded.capture(), + capturedGroupNewEncoded.capture() + ) + } doAnswer {} + } + + // mock LokiAPIDB + val capturedLokiLegacyGroup = argumentCaptor() + val capturedLokiNewGroup = argumentCaptor() + val mockedLokiApi = mock { + on { migrateLegacyOpenGroup(capturedLokiLegacyGroup.capture(), capturedLokiNewGroup.capture()) } doAnswer {} + } + + val pubKey = OpenGroupApi.defaultServerPublicKey + val room = "oxen" + val legacyServer = OpenGroupApi.legacyDefaultServer + val newServer = OpenGroupApi.defaultServer + + val lokiThreadOpenGroup = argumentCaptor() + val mockedLokiThreadDb = mock { + on { getOpenGroupChat(eq(LEGACY_THREAD_ID)) } doReturn OpenGroup(legacyServer, room, "Oxen", 0, pubKey) + on { setOpenGroupChat(lokiThreadOpenGroup.capture(), eq(LEGACY_THREAD_ID)) } doAnswer {} + } + + val mockedDbComponent = mock { + on { threadDatabase() } doReturn mockedThreadDb + on { groupDatabase() } doReturn mockedGroupDb + on { lokiAPIDatabase() } doReturn mockedLokiApi + on { lokiThreadDatabase() } doReturn mockedLokiThreadDb + } + + OpenGroupMigrator.migrate(mockedDbComponent) + + // expect threadDB migration to reflect new thread values: + // thread ID = 1, encoded ID = new encoded ID + assertEquals(LEGACY_THREAD_ID, capturedThreadId.firstValue) + assertEquals(EXAMPLE_NEW_ENCODED_OPEN_GROUP, capturedNewEncoded.firstValue) + + // expect groupDB migration to reflect new thread values: + // legacy encoded ID, new encoded ID + assertEquals(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP, capturedGroupLegacyEncoded.firstValue) + assertEquals(EXAMPLE_NEW_ENCODED_OPEN_GROUP, capturedGroupNewEncoded.firstValue) + + // expect Loki API DB migration to reflect new thread values: + assertEquals("${OpenGroupApi.legacyDefaultServer}.oxen", capturedLokiLegacyGroup.firstValue) + assertEquals("${OpenGroupApi.defaultServer}.oxen", capturedLokiNewGroup.firstValue) + + assertEquals(newServer, lokiThreadOpenGroup.firstValue.server) + + } + + @Test + fun `it should migrate and delete legacy thread with conflicting new and old values`() { + + // mock threadDB + val capturedThreadId = argumentCaptor() + val mockedThreadDb = mock { + val legacyThreadRecord = legacyThreadRecord() + val newThreadRecord = newThreadRecord() + on { legacyOxenOpenGroups } doReturn listOf(legacyThreadRecord) + on { httpsOxenOpenGroups } doReturn listOf(newThreadRecord) + on { deleteConversation(capturedThreadId.capture()) } doAnswer {} + } + + // mock groupDB + val capturedGroupLegacyEncoded = argumentCaptor() + val mockedGroupDb = mock { + on { delete(capturedGroupLegacyEncoded.capture()) } doReturn true + } + + // mock LokiAPIDB + val capturedLokiLegacyGroup = argumentCaptor() + val capturedLokiNewGroup = argumentCaptor() + val mockedLokiApi = mock { + on { migrateLegacyOpenGroup(capturedLokiLegacyGroup.capture(), capturedLokiNewGroup.capture()) } doAnswer {} + } + + // mock messaging dbs + val migrateMmsFromThreadId = argumentCaptor() + val migrateMmsToThreadId = argumentCaptor() + + val mockedMmsDb = mock { + on { migrateThreadId(migrateMmsFromThreadId.capture(), migrateMmsToThreadId.capture()) } doAnswer {} + } + + val migrateSmsFromThreadId = argumentCaptor() + val migrateSmsToThreadId = argumentCaptor() + val mockedSmsDb = mock { + on { migrateThreadId(migrateSmsFromThreadId.capture(), migrateSmsToThreadId.capture()) } doAnswer {} + } + + val lokiFromThreadId = argumentCaptor() + val lokiToThreadId = argumentCaptor() + val mockedLokiMessageDatabase = mock { + on { migrateThreadId(lokiFromThreadId.capture(), lokiToThreadId.capture()) } doAnswer {} + } + + val mockedLokiThreadDb = mock { + on { removeOpenGroupChat(eq(LEGACY_THREAD_ID)) } doAnswer {} + } + + val mockedDbComponent = mock { + on { threadDatabase() } doReturn mockedThreadDb + on { groupDatabase() } doReturn mockedGroupDb + on { lokiAPIDatabase() } doReturn mockedLokiApi + on { mmsDatabase() } doReturn mockedMmsDb + on { smsDatabase() } doReturn mockedSmsDb + on { lokiMessageDatabase() } doReturn mockedLokiMessageDatabase + on { lokiThreadDatabase() } doReturn mockedLokiThreadDb + } + + OpenGroupMigrator.migrate(mockedDbComponent) + + // should delete thread by thread ID + assertEquals(LEGACY_THREAD_ID, capturedThreadId.firstValue) + + // should delete group by legacy encoded ID + assertEquals(EXAMPLE_LEGACY_ENCODED_OPEN_GROUP, capturedGroupLegacyEncoded.firstValue) + + // should migrate SMS from legacy thread ID to new thread ID + assertEquals(LEGACY_THREAD_ID, migrateSmsFromThreadId.firstValue) + assertEquals(NEW_THREAD_ID, migrateSmsToThreadId.firstValue) + + // should migrate MMS from legacy thread ID to new thread ID + assertEquals(LEGACY_THREAD_ID, migrateMmsFromThreadId.firstValue) + assertEquals(NEW_THREAD_ID, migrateMmsToThreadId.firstValue) + + } + + + +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt index f07d8a3538..eb40df6e09 100644 --- a/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt +++ b/libsession/src/main/java/org/session/libsession/database/MessageDataProvider.kt @@ -33,7 +33,7 @@ interface MessageDataProvider { fun insertAttachment(messageId: Long, attachmentId: AttachmentId, stream : InputStream) fun updateAudioAttachmentDuration(attachmentId: AttachmentId, durationMs: Long, threadId: Long) fun isMmsOutgoing(mmsMessageId: Long): Boolean - fun isOutgoingMessage(mmsId: Long): Boolean + fun isOutgoingMessage(timestamp: Long): Boolean fun handleSuccessfulAttachmentUpload(attachmentId: Long, attachmentStream: SignalServiceAttachmentStream, attachmentKey: ByteArray, uploadResult: UploadResult) fun handleFailedAttachmentUpload(attachmentId: Long) fun getMessageForQuote(timestamp: Long, author: Address): Pair? diff --git a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt index 7be4c36805..e9e76f12ea 100644 --- a/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt +++ b/libsession/src/main/java/org/session/libsession/database/StorageProtocol.kt @@ -7,9 +7,11 @@ import org.session.libsession.messaging.contacts.Contact import org.session.libsession.messaging.jobs.AttachmentUploadJob import org.session.libsession.messaging.jobs.Job import org.session.libsession.messaging.jobs.MessageSendJob +import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ConfigurationMessage import org.session.libsession.messaging.messages.control.MessageRequestResponse import org.session.libsession.messaging.messages.visible.Attachment +import org.session.libsession.messaging.messages.visible.Reaction import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.sending_receiving.attachments.AttachmentId @@ -65,7 +67,7 @@ interface StorageProtocol { fun updateOpenGroup(openGroup: OpenGroup) fun getOpenGroup(threadId: Long): OpenGroup? fun addOpenGroup(urlAsString: String) - fun onOpenGroupAdded(urlAsString: String) + fun onOpenGroupAdded(server: String) fun hasBackgroundGroupAddJob(groupJoinUrl: String): Boolean fun setOpenGroupServerMessageID(messageID: Long, serverID: Long, threadID: Long, isSms: Boolean) fun getOpenGroup(room: String, server: String): OpenGroup? @@ -188,4 +190,8 @@ interface StorageProtocol { fun removeLastOutboxMessageId(server: String) fun getOrCreateBlindedIdMapping(blindedId: String, server: String, serverPublicKey: String, fromOutbox: Boolean = false): BlindedIdMapping + fun addReaction(reaction: Reaction) + fun removeReaction(emoji: String, messageTimestamp: Long, author: String) + fun updateReactionIfNeeded(message: Message, sender: String, openGroupSentTimestamp: Long) + fun deleteReactions(messageId: Long, mms: Boolean) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt index af4a2e160f..aa37e0f0a8 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BackgroundGroupAddJob.kt @@ -6,6 +6,7 @@ import org.session.libsession.messaging.open_groups.OpenGroup import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.utilities.Data import org.session.libsession.utilities.GroupUtil +import org.session.libsession.utilities.OpenGroupUrlParser import org.session.libsignal.utilities.Log class BackgroundGroupAddJob(val joinUrl: String): Job { @@ -30,31 +31,28 @@ class BackgroundGroupAddJob(val joinUrl: String): Job { override fun execute() { try { + val openGroup = OpenGroupUrlParser.parseUrl(joinUrl) val storage = MessagingModuleConfiguration.shared.storage val allOpenGroups = storage.getAllOpenGroups().map { it.value.joinURL } - if (allOpenGroups.contains(joinUrl)) { + if (allOpenGroups.contains(openGroup.joinUrl())) { Log.e("OpenGroupDispatcher", "Failed to add group because", DuplicateGroupException()) delegate?.handleJobFailed(this, DuplicateGroupException()) return } // get image - val url = HttpUrl.parse(joinUrl) ?: throw Exception("Group joinUrl isn't valid") - val server = OpenGroup.getServer(joinUrl) - val serverString = server.toString().removeSuffix("/") - val publicKey = url.queryParameter("public_key") ?: throw Exception("Group public key isn't valid") - val room = url.pathSegments().firstOrNull() ?: throw Exception("Group room isn't valid") - storage.setOpenGroupPublicKey(serverString, publicKey) - // get info and auth token - storage.addOpenGroup(joinUrl) - val info = OpenGroupApi.getRoomInfo(room, serverString).get() + storage.setOpenGroupPublicKey(openGroup.server, openGroup.serverPublicKey) + val (capabilities, info) = OpenGroupApi.getCapabilitiesAndRoomInfo(openGroup.room, openGroup.server, false).get() + storage.setServerCapabilities(openGroup.server, capabilities.capabilities) val imageId = info.imageId + storage.addOpenGroup(openGroup.joinUrl()) if (imageId != null) { - val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(serverString, room, imageId).get() - val groupId = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray()) + val bytes = OpenGroupApi.downloadOpenGroupProfilePicture(openGroup.server, openGroup.room, imageId).get() + val groupId = GroupUtil.getEncodedOpenGroupID("${openGroup.server}.${openGroup.room}".toByteArray()) storage.updateProfilePicture(groupId, bytes) storage.updateTimestampUpdated(groupId, System.currentTimeMillis()) } - storage.onOpenGroupAdded(joinUrl) + Log.d(KEY, "onOpenGroupAdded(${openGroup.server})") + storage.onOpenGroupAdded(openGroup.server) } catch (e: Exception) { Log.e("OpenGroupDispatcher", "Failed to add group because",e) delegate?.handleJobFailed(this, e) diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt index 9b899bbe27..07c104cfda 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/BatchMessageReceiveJob.kt @@ -13,8 +13,10 @@ import org.session.libsession.messaging.messages.Message import org.session.libsession.messaging.messages.control.ExpirationTimerUpdate import org.session.libsession.messaging.messages.visible.ParsedMessage import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.MessageReceiver import org.session.libsession.messaging.sending_receiving.handle +import org.session.libsession.messaging.sending_receiving.handleOpenGroupReactions import org.session.libsession.messaging.sending_receiving.handleVisibleMessage import org.session.libsession.messaging.utilities.Data import org.session.libsession.messaging.utilities.SessionId @@ -27,7 +29,8 @@ import org.session.libsignal.utilities.Log data class MessageReceiveParameters( val data: ByteArray, val serverHash: String? = null, - val openGroupMessageServerID: Long? = null + val openGroupMessageServerID: Long? = null, + val reactions: Map? = null ) class BatchMessageReceiveJob( @@ -114,11 +117,14 @@ class BatchMessageReceiveJob( runThreadUpdate = false, runProfileUpdate = true ) - if (messageId != null) { + if (messageId != null && message.reaction == null) { val isUserBlindedSender = message.sender == serverPublicKey?.let { SodiumUtilities.blindedKeyPair(it, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) }?.let { SessionId( IdPrefix.BLINDED, it.publicKey.asBytes).hexString } messageIds += messageId to (message.sender == localUserPublicKey || isUserBlindedSender) } + parameters.openGroupMessageServerID?.let { + MessageReceiver.handleOpenGroupReactions(threadId, it, parameters.reactions) + } } else { MessageReceiver.handle(message, proto, openGroupID) } diff --git a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt index 3016e068f4..cdd9e0a3ac 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/jobs/MessageSendJob.kt @@ -44,7 +44,7 @@ class MessageSendJob(val message: Message, val destination: Destination) : Job { } if (message != null) { - if (!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!)) return // The message has been deleted + if (!messageDataProvider.isOutgoingMessage(message.sentTimestamp!!) && message.reaction == null) return // The message has been deleted val attachmentIDs = mutableListOf() attachmentIDs.addAll(message.attachmentIDs) message.quote?.let { it.attachmentID?.let { attachmentID -> attachmentIDs.add(attachmentID) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Reaction.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Reaction.kt new file mode 100644 index 0000000000..2a34e883bd --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/Reaction.kt @@ -0,0 +1,72 @@ +package org.session.libsession.messaging.messages.visible + +import org.session.libsignal.protos.SignalServiceProtos +import org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action +import org.session.libsignal.utilities.Log + +class Reaction() { + var timestamp: Long? = 0 + var localId: Long? = 0 + var isMms: Boolean? = false + var publicKey: String? = null + var emoji: String? = null + var react: Boolean? = true + var serverId: String? = null + var count: Long? = 0 + var index: Long? = 0 + var dateSent: Long? = 0 + var dateReceived: Long? = 0 + + fun isValid(): Boolean { + return (timestamp != null && publicKey != null) + } + + companion object { + const val TAG = "Quote" + + fun fromProto(proto: SignalServiceProtos.DataMessage.Reaction): Reaction? { + val react = proto.action == Action.REACT + return Reaction(publicKey = proto.author, emoji = proto.emoji, react = react, timestamp = proto.id, count = 1) + } + + fun from(timestamp: Long, author: String, emoji: String, react: Boolean): Reaction? { + return Reaction(author, emoji, react, timestamp) + } + } + + internal constructor(publicKey: String, emoji: String, react: Boolean, timestamp: Long? = 0, localId: Long? = 0, isMms: Boolean? = false, serverId: String? = null, count: Long? = 0, index: Long? = 0) : this() { + this.timestamp = timestamp + this.publicKey = publicKey + this.emoji = emoji + this.react = react + this.serverId = serverId + this.localId = localId + this.isMms = isMms + this.count = count + this.index = index + } + + fun toProto(): SignalServiceProtos.DataMessage.Reaction? { + val timestamp = timestamp + val publicKey = publicKey + val emoji = emoji + val react = react ?: true + if (timestamp == null || publicKey == null || emoji == null) { + Log.w(TAG, "Couldn't construct reaction proto from: $this") + return null + } + val reactionProto = SignalServiceProtos.DataMessage.Reaction.newBuilder() + reactionProto.id = timestamp + reactionProto.author = publicKey + reactionProto.emoji = emoji + reactionProto.action = if (react) Action.REACT else Action.REMOVE + // Build + return try { + reactionProto.build() + } catch (e: Exception) { + Log.w(TAG, "Couldn't construct reaction proto from: $this") + null + } + } + +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt index f188716303..379af2b658 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/messages/visible/VisibleMessage.kt @@ -23,6 +23,7 @@ class VisibleMessage : Message() { var linkPreview: LinkPreview? = null var profile: Profile? = null var openGroupInvitation: OpenGroupInvitation? = null + var reaction: Reaction? = null override val isSelfSendValid: Boolean = true @@ -31,9 +32,9 @@ class VisibleMessage : Message() { if (!super.isValid()) return false if (attachmentIDs.isNotEmpty()) return true if (openGroupInvitation != null) return true + if (reaction != null) return true val text = text?.trim() ?: return false - if (text.isNotEmpty()) return true - return false + return text.isNotEmpty() } // endregion @@ -65,6 +66,11 @@ class VisibleMessage : Message() { // TODO Contact val profile = Profile.fromProto(dataMessage) if (profile != null) { result.profile = profile } + val reactionProto = if (dataMessage.hasReaction()) dataMessage.reaction else null + if (reactionProto != null) { + val reaction = Reaction.fromProto(reactionProto) + result.reaction = reaction + } return result } } @@ -74,10 +80,10 @@ class VisibleMessage : Message() { val dataMessage: SignalServiceProtos.DataMessage.Builder // Profile val profileProto = profile?.toProto() - if (profileProto != null) { - dataMessage = profileProto.toBuilder() + dataMessage = if (profileProto != null) { + profileProto.toBuilder() } else { - dataMessage = SignalServiceProtos.DataMessage.newBuilder() + SignalServiceProtos.DataMessage.newBuilder() } // Text if (text != null) { dataMessage.body = text } @@ -86,6 +92,11 @@ class VisibleMessage : Message() { if (quoteProto != null) { dataMessage.quote = quoteProto } + // Reaction + val reactionProto = reaction?.toProto() + if (reactionProto != null) { + dataMessage.reaction = reactionProto + } // Link preview val linkPreviewProto = linkPreview?.toProto() if (linkPreviewProto != null) { @@ -132,12 +143,12 @@ class VisibleMessage : Message() { dataMessage.syncTarget = syncTarget } // Build - try { + return try { proto.dataMessage = dataMessage.build() - return proto.build() + proto.build() } catch (e: Exception) { Log.w(TAG, "Couldn't construct visible message proto from: $this") - return null + null } } // endregion @@ -151,6 +162,6 @@ class VisibleMessage : Message() { } fun isMediaMessage(): Boolean { - return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null + return attachmentIDs.isNotEmpty() || quote != null || linkPreview != null || reaction != null } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt index 79abfeb59a..d7472bb797 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/Endpoint.kt @@ -33,6 +33,15 @@ sealed class Endpoint(val value: String) { data class RoomDeleteMessages(val roomToken: String, val sessionId: String) : Endpoint("room/$roomToken/all/$sessionId") + data class Reactors(val roomToken: String, val messageId: Long, val emoji: String): + Endpoint("room/$roomToken/reactors/$messageId/$emoji") + + data class Reaction(val roomToken: String, val messageId: Long, val emoji: String): + Endpoint("room/$roomToken/reaction/$messageId/$emoji") + + data class ReactionDelete(val roomToken: String, val messageId: Long, val emoji: String): + Endpoint("room/$roomToken/reactions/$messageId/$emoji") + // Pinning data class RoomPinMessage(val roomToken: String, val messageId: Long) : diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/GroupMember.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/GroupMember.kt index 0559a12d5d..8335e0a2da 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/GroupMember.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/GroupMember.kt @@ -7,5 +7,5 @@ data class GroupMember( ) enum class GroupMemberRole { - STANDARD, ZOOMBIE, MODERATOR, ADMIN + STANDARD, ZOOMBIE, MODERATOR, ADMIN, HIDDEN_MODERATOR, HIDDEN_ADMIN } diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt index 3d5d590838..daa735aa4c 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupApi.kt @@ -55,9 +55,16 @@ object OpenGroupApi { now - lastOpenDate } - const val defaultServerPublicKey = - "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" - const val defaultServer = "http://116.203.70.33" + const val defaultServerPublicKey = "a03c383cf63c3c4efe67acc52112a6dd734b3a946b9545f488aaa93da7991238" + const val legacyServerIP = "116.203.70.33" + const val legacyDefaultServer = "http://116.203.70.33" // TODO: migrate all references to use new value + + /** For migration purposes only, don't use this value in joining groups */ + const val httpDefaultServer = "http://open.getsession.org" + + const val defaultServer = "https://open.getsession.org" + + val pendingReactions = mutableListOf() sealed class Error(message: String) : Exception(message) { object Generic : Error("An error occurred.") @@ -114,6 +121,7 @@ object OpenGroupApi { data class BatchRequestInfo( val request: BatchRequest, val endpoint: Endpoint, + val queryParameters: Map = mapOf(), val responseType: TypeReference ) @@ -139,6 +147,10 @@ object OpenGroupApi { val missing: List = emptyList() ) + enum class Capability { + BLIND, REACTIONS + } + @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) data class RoomPollInfo( val token: String = "", @@ -179,7 +191,39 @@ object OpenGroupApi { val whisperMods: String = "", val whisperTo: String = "", val data: String? = null, - val signature: String? = null + val signature: String? = null, + val reactions: Map? = null, + ) + + data class Reaction( + val count: Long = 0, + val reactors: List = emptyList(), + val you: Boolean = false, + val index: Long = 0 + ) + + data class AddReactionResponse( + val seqNo: Long, + val added: Boolean + ) + + data class DeleteReactionResponse( + val seqNo: Long, + val removed: Boolean + ) + + data class DeleteAllReactionsResponse( + val seqNo: Long, + val removed: Boolean + ) + + data class PendingReaction( + val server: String, + val room: String, + val messageId: Long, + val emoji: String, + val add: Boolean, + var seqNo: Long? = null ) @JsonNaming(PropertyNamingStrategy.SnakeCaseStrategy::class) @@ -240,15 +284,12 @@ object OpenGroupApi { } private fun send(request: Request): Promise { - val url = HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL) - val urlBuilder = HttpUrl.Builder() - .scheme(url.scheme()) - .host(url.host()) - .port(url.port()) - .addPathSegments(request.endpoint.value) - if (request.verb == GET) { + HttpUrl.parse(request.server) ?: return Promise.ofFail(Error.InvalidURL) + val urlBuilder = StringBuilder("${request.server}/${request.endpoint.value}") + if (request.verb == GET && request.queryParameters.isNotEmpty()) { + urlBuilder.append("?") for ((key, value) in request.queryParameters) { - urlBuilder.addQueryParameter(key, value) + urlBuilder.append("$key=$value") } } fun execute(): Promise { @@ -258,7 +299,7 @@ object OpenGroupApi { ?: return Promise.ofFail(Error.NoPublicKey) val ed25519KeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return Promise.ofFail(Error.NoEd25519KeyPair) - val urlRequest = urlBuilder.build() + val urlRequest = urlBuilder.toString() val headers = request.headers.toMutableMap() if (request.isAuthRequired) { val nonce = sodium.nonce(16) @@ -294,9 +335,9 @@ object OpenGroupApi { .plus(nonce) .plus("$timestamp".toByteArray(Charsets.US_ASCII)) .plus(request.verb.rawValue.toByteArray()) - .plus(urlRequest.encodedPath().toByteArray()) + .plus("/${request.endpoint.value}".toByteArray()) .plus(bodyHash) - if (serverCapabilities.contains("blind")) { + if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) { SodiumUtilities.blindedKeyPair(publicKey, ed25519KeyPair)?.let { keyPair -> pubKey = SessionId( IdPrefix.BLINDED, @@ -404,12 +445,19 @@ object OpenGroupApi { fileIds: List? = null ): Promise { val signedMessage = message.sign(room, server, fallbackSigningType = IdPrefix.STANDARD) ?: return Promise.ofFail(Error.SigningFailed) + val parameters = signedMessage.toJSON().toMutableMap() + + // add file IDs if there are any (from attachments) + if (!fileIds.isNullOrEmpty()) { + parameters += "files" to fileIds + } + val request = Request( verb = POST, room = room, server = server, endpoint = Endpoint.RoomMessage(room), - parameters = signedMessage.toJSON() + parameters = parameters ) return getResponseBodyJson(request).map { json -> @Suppress("UNCHECKED_CAST") val rawMessage = json as? Map @@ -470,6 +518,63 @@ object OpenGroupApi { } return messages } + + fun getReactors(room: String, server: String, messageId: Long, emoji: String): Promise, Exception> { + val request = Request( + verb = GET, + room = room, + server = server, + endpoint = Endpoint.Reactors(room, messageId, emoji) + ) + return getResponseBody(request).map { response -> + JsonUtil.fromJson(response, Map::class.java) + } + } + + fun addReaction(room: String, server: String, messageId: Long, emoji: String): Promise { + val request = Request( + verb = PUT, + room = room, + server = server, + endpoint = Endpoint.Reaction(room, messageId, emoji), + parameters = emptyMap() + ) + val pendingReaction = PendingReaction(server, room, messageId, emoji, true) + return getResponseBody(request).map { response -> + JsonUtil.fromJson(response, AddReactionResponse::class.java).also { + val index = pendingReactions.indexOf(pendingReaction) + pendingReactions[index].seqNo = it.seqNo + } + } + } + + fun deleteReaction(room: String, server: String, messageId: Long, emoji: String): Promise { + val request = Request( + verb = DELETE, + room = room, + server = server, + endpoint = Endpoint.Reaction(room, messageId, emoji) + ) + val pendingReaction = PendingReaction(server, room, messageId, emoji, true) + return getResponseBody(request).map { response -> + JsonUtil.fromJson(response, DeleteReactionResponse::class.java).also { + val index = pendingReactions.indexOf(pendingReaction) + pendingReactions[index].seqNo = it.seqNo + } + } + } + + fun deleteAllReactions(room: String, server: String, messageId: Long, emoji: String): Promise { + val request = Request( + verb = DELETE, + room = room, + server = server, + endpoint = Endpoint.ReactionDelete(room, messageId, emoji) + ) + return getResponseBody(request).map { response -> + JsonUtil.fromJson(response, DeleteAllReactionsResponse::class.java) + } + } // endregion // region Message Deletion @@ -608,7 +713,7 @@ object OpenGroupApi { BatchRequestInfo( request = BatchRequest( method = GET, - path = "/room/$room/messages/recent" + path = "/room/$room/messages/recent?t=r&reactors=5" ), endpoint = Endpoint.RoomMessagesRecent(room), responseType = object : TypeReference>(){} @@ -617,7 +722,7 @@ object OpenGroupApi { BatchRequestInfo( request = BatchRequest( method = GET, - path = "/room/$room/messages/since/$lastMessageServerId" + path = "/room/$room/messages/since/$lastMessageServerId?t=r&reactors=5" ), endpoint = Endpoint.RoomMessagesSince(room, lastMessageServerId), responseType = object : TypeReference>(){} @@ -626,7 +731,7 @@ object OpenGroupApi { ) } val serverCapabilities = storage.getServerCapabilities(server) - if (serverCapabilities.contains("blind")) { + if (serverCapabilities.contains(Capability.BLIND.name.lowercase())) { requests.add( if (lastInboxMessageId == null) { BatchRequestInfo( @@ -689,14 +794,16 @@ object OpenGroupApi { private fun sequentialBatch( server: String, - requests: MutableList> + requests: MutableList>, + authRequired: Boolean = true ): Promise>, Exception> { val request = Request( verb = POST, room = null, server = server, endpoint = Endpoint.Sequence, - parameters = requests.map { it.request } + parameters = requests.map { it.request }, + isAuthRequired = authRequired ) return getBatchResponseJson(request, requests) } @@ -803,7 +910,11 @@ object OpenGroupApi { } } - fun getCapabilitiesAndRoomInfo(room: String, server: String): Promise, Exception> { + fun getCapabilitiesAndRoomInfo( + room: String, + server: String, + authRequired: Boolean = true + ): Promise, Exception> { val requests = mutableListOf>( BatchRequestInfo( request = BatchRequest( @@ -822,7 +933,7 @@ object OpenGroupApi { responseType = object : TypeReference(){} ) ) - return sequentialBatch(server, requests).map { + return sequentialBatch(server, requests, authRequired).map { val capabilities = it.firstOrNull()?.body as? Capabilities ?: throw Error.ParsingFailed val roomInfo = it.lastOrNull()?.body as? RoomInfo ?: throw Error.ParsingFailed capabilities to roomInfo diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt index db9ae0c0c0..1b5b870bfa 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupMessage.kt @@ -1,6 +1,7 @@ package org.session.libsession.messaging.open_groups import org.session.libsession.messaging.MessagingModuleConfiguration +import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability import org.session.libsession.messaging.utilities.SodiumUtilities import org.session.libsignal.crypto.PushTransportDetails import org.session.libsignal.protos.SignalServiceProtos @@ -19,12 +20,13 @@ data class OpenGroupMessage( /** * The serialized protobuf in base64 encoding. */ - val base64EncodedData: String, + val base64EncodedData: String?, /** * When sending a message, the sender signs the serialized protobuf with their private key so that * a receiving user can verify that the message wasn't tampered with. */ - val base64EncodedSignature: String? = null + val base64EncodedSignature: String? = null, + val reactions: Map? = null ) { companion object { @@ -47,12 +49,12 @@ data class OpenGroupMessage( } fun sign(room: String, server: String, fallbackSigningType: IdPrefix): OpenGroupMessage? { - if (base64EncodedData.isEmpty()) return null + if (base64EncodedData.isNullOrEmpty()) return null val userEdKeyPair = MessagingModuleConfiguration.shared.getUserED25519KeyPair() ?: return null val openGroup = MessagingModuleConfiguration.shared.storage.getOpenGroup(room, server) ?: return null val serverCapabilities = MessagingModuleConfiguration.shared.storage.getServerCapabilities(server) val signature = when { - serverCapabilities.contains("blind") -> { + serverCapabilities.contains(Capability.BLIND.name.lowercase()) -> { val blindedKeyPair = SodiumUtilities.blindedKeyPair(openGroup.publicKey, userEdKeyPair) ?: return null SodiumUtilities.sogsSignature( decode(base64EncodedData), @@ -78,7 +80,7 @@ data class OpenGroupMessage( return copy(base64EncodedSignature = Base64.encodeBytes(signature)) } - fun toJSON(): Map { + fun toJSON(): Map { val json = mutableMapOf( "data" to base64EncodedData, "timestamp" to sentTimestamp ) serverID?.let { json["server_id"] = it } sender?.let { json["public_key"] = it } diff --git a/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupUtils.kt b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupUtils.kt new file mode 100644 index 0000000000..ebf2965b1a --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/messaging/open_groups/OpenGroupUtils.kt @@ -0,0 +1,9 @@ +package org.session.libsession.messaging.open_groups + +fun String.migrateLegacyServerUrl() = if (contains(OpenGroupApi.legacyServerIP)) { + OpenGroupApi.defaultServer +} else if (contains(OpenGroupApi.httpDefaultServer)) { + OpenGroupApi.defaultServer +} else { + this +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt index d17fbe6aff..d6a4618d96 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/MessageSender.kt @@ -1,6 +1,5 @@ package org.session.libsession.messaging.sending_receiving -import com.goterl.lazysodium.utils.KeyPair import nl.komponents.kovenant.Promise import nl.komponents.kovenant.deferred import org.session.libsession.messaging.MessagingModuleConfiguration @@ -19,6 +18,7 @@ import org.session.libsession.messaging.messages.visible.Profile import org.session.libsession.messaging.messages.visible.Quote import org.session.libsession.messaging.messages.visible.VisibleMessage import org.session.libsession.messaging.open_groups.OpenGroupApi +import org.session.libsession.messaging.open_groups.OpenGroupApi.Capability import org.session.libsession.messaging.open_groups.OpenGroupMessage import org.session.libsession.messaging.utilities.MessageWrapper import org.session.libsession.messaging.utilities.SessionId @@ -243,7 +243,7 @@ object MessageSender { } else -> {} } - val messageSender = if (serverCapabilities.contains("blind") && blindedPublicKey != null) { + val messageSender = if (serverCapabilities.contains(Capability.BLIND.name.lowercase()) && blindedPublicKey != null) { SessionId(IdPrefix.BLINDED, blindedPublicKey!!).hexString } else { SessionId(IdPrefix.UN_BLINDED, userEdKeyPair.publicKey.asBytes).hexString @@ -338,8 +338,23 @@ object MessageSender { storage.setMessageServerHash(messageID, it) } // Track the open group server message ID - if (message.openGroupServerMessageID != null && destination is Destination.LegacyOpenGroup) { - val encoded = GroupUtil.getEncodedOpenGroupID("${destination.server}.${destination.roomToken}".toByteArray()) + if (message.openGroupServerMessageID != null && (destination is Destination.LegacyOpenGroup || destination is Destination.OpenGroup)) { + val server: String + val room: String + when (destination) { + is Destination.LegacyOpenGroup -> { + server = destination.server + room = destination.roomToken + } + is Destination.OpenGroup -> { + server = destination.server + room = destination.roomToken + } + else -> { + throw Exception("Destination was a different destination than we were expecting") + } + } + val encoded = GroupUtil.getEncodedOpenGroupID("$server.$room".toByteArray()) val threadID = storage.getThreadId(Address.fromSerialized(encoded)) if (threadID != null && threadID >= 0) { storage.setOpenGroupServerMessageID(messageID, message.openGroupServerMessageID!!, threadID, !(message as VisibleMessage).isMediaMessage()) @@ -352,6 +367,8 @@ object MessageSender { if (message is VisibleMessage && !isSyncMessage) { SSKEnvironment.shared.messageExpirationManager.startAnyExpiration(message.sentTimestamp!!, userPublicKey) } + } ?: run { + storage.updateReactionIfNeeded(message, message.sender?:userPublicKey, openGroupSentTimestamp) } // Sync the message if: // • it's a visible message @@ -432,4 +449,5 @@ object MessageSender { fun explicitLeave(groupPublicKey: String, notifyUser: Boolean): Promise { return leave(groupPublicKey, notifyUser) } + } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt index f540839560..732bfef582 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/ReceivedMessageHandler.kt @@ -16,7 +16,9 @@ import org.session.libsession.messaging.messages.control.ReadReceipt import org.session.libsession.messaging.messages.control.TypingIndicator import org.session.libsession.messaging.messages.control.UnsendRequest import org.session.libsession.messaging.messages.visible.Attachment +import org.session.libsession.messaging.messages.visible.Reaction import org.session.libsession.messaging.messages.visible.VisibleMessage +import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.sending_receiving.attachments.PointerAttachment import org.session.libsession.messaging.sending_receiving.data_extraction.DataExtractionNotificationInfoMessage import org.session.libsession.messaging.sending_receiving.link_preview.LinkPreview @@ -47,6 +49,7 @@ import org.session.libsignal.utilities.removingIdPrefixIfNeeded import org.session.libsignal.utilities.toHexString import java.security.MessageDigest import java.util.LinkedList +import kotlin.math.min internal fun MessageReceiver.isBlocked(publicKey: String): Boolean { val context = MessagingModuleConfiguration.shared.context @@ -157,7 +160,10 @@ private fun handleConfigurationMessage(message: ConfigurationMessage) { } } val allV2OpenGroups = storage.getAllOpenGroups().map { it.value.joinURL } - for (openGroup in message.openGroups) { + for (openGroup in message.openGroups.map { + it.replace(OpenGroupApi.legacyDefaultServer, OpenGroupApi.defaultServer) + .replace(OpenGroupApi.httpDefaultServer, OpenGroupApi.defaultServer) + }) { if (allV2OpenGroups.contains(openGroup)) continue Log.d("OpenGroup", "All open groups doesn't contain $openGroup") if (!storage.hasBackgroundGroupAddJob(openGroup)) { @@ -256,11 +262,11 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, val author = Address.fromSerialized(quote.author) val messageDataProvider = MessagingModuleConfiguration.shared.messageDataProvider val messageInfo = messageDataProvider.getMessageForQuote(quote.id, author) - if (messageInfo != null) { + quoteModel = if (messageInfo != null) { val attachments = if (messageInfo.second) messageDataProvider.getAttachmentsAndLinkPreviewFor(messageInfo.first) else ArrayList() - quoteModel = QuoteModel(quote.id, author,null,false, attachments) + QuoteModel(quote.id, author,null,false, attachments) } else { - quoteModel = QuoteModel(quote.id, author,null, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList)) + QuoteModel(quote.id, author,null, true, PointerAttachment.forPointers(proto.dataMessage.quote.attachmentsList)) } } // Parse link preview if needed @@ -288,22 +294,104 @@ fun MessageReceiver.handleVisibleMessage(message: VisibleMessage, return@mapNotNull attachment } } - // Persist the message - message.threadID = threadID - val messageID = storage.persist( - message, quoteModel, linkPreviews, - message.groupPublicKey, openGroupID, - attachments, runIncrement, runThreadUpdate - ) ?: return null - val openGroupServerID = message.openGroupServerMessageID - if (openGroupServerID != null) { - val isSms = !(message.isMediaMessage() || attachments.isNotEmpty()) - storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, isSms) + // Parse reaction if needed + message.reaction?.let { reaction -> + if (reaction.react == true) { + reaction.serverId = message.openGroupServerMessageID?.toString() ?: message.serverHash.orEmpty() + reaction.dateSent = message.sentTimestamp ?: 0 + reaction.dateReceived = message.receivedTimestamp ?: 0 + storage.addReaction(reaction) + } else { + storage.removeReaction(reaction.emoji!!, reaction.timestamp!!, reaction.publicKey!!) + } + } ?: run { + // Persist the message + message.threadID = threadID + val messageID = + storage.persist(message, quoteModel, linkPreviews, message.groupPublicKey, openGroupID, + attachments, runIncrement, runThreadUpdate + ) ?: return null + val openGroupServerID = message.openGroupServerMessageID + if (openGroupServerID != null) { + val isSms = !(message.isMediaMessage() || attachments.isNotEmpty()) + storage.setOpenGroupServerMessageID(messageID, openGroupServerID, threadID, isSms) + } + return messageID } // Cancel any typing indicators if needed cancelTypingIndicatorsIfNeeded(message.sender!!) - return messageID + return null } + +fun MessageReceiver.handleOpenGroupReactions( + threadId: Long, + openGroupMessageServerID: Long, + reactions: Map? +) { + if (reactions.isNullOrEmpty()) return + val storage = MessagingModuleConfiguration.shared.storage + val (messageId, isSms) = MessagingModuleConfiguration.shared.messageDataProvider.getMessageID(openGroupMessageServerID, threadId) ?: return + storage.deleteReactions(messageId, !isSms) + val userPublicKey = storage.getUserPublicKey()!! + val openGroup = storage.getOpenGroup(threadId) + val blindedPublicKey = openGroup?.publicKey?.let { serverPublicKey -> + SodiumUtilities.blindedKeyPair(serverPublicKey, MessagingModuleConfiguration.shared.getUserED25519KeyPair()!!) + ?.let { SessionId(IdPrefix.BLINDED, it.publicKey.asBytes).hexString } + } + for ((emoji, reaction) in reactions) { + val pendingUserReaction = OpenGroupApi.pendingReactions + .filter { it.server == openGroup?.server && it.room == openGroup.room && it.messageId == openGroupMessageServerID && it.add } + .sortedByDescending { it.seqNo } + .any { it.emoji == emoji } + val shouldAddUserReaction = pendingUserReaction || reaction.you || reaction.reactors.contains(userPublicKey) + val reactorIds = reaction.reactors.filter { it != blindedPublicKey && it != userPublicKey } + val count = if (reaction.you) reaction.count - 1 else reaction.count + // Add the first reaction (with the count) + reactorIds.firstOrNull()?.let { + storage.addReaction(Reaction( + localId = messageId, + isMms = !isSms, + publicKey = it, + emoji = emoji, + react = true, + serverId = "$openGroupMessageServerID", + count = count, + index = reaction.index + )) + } + + // Add all other reactions + val maxAllowed = if (shouldAddUserReaction) 4 else 5 + val lastIndex = min(maxAllowed, reactorIds.size) + reactorIds.slice(1 until lastIndex).map { reactor -> + storage.addReaction(Reaction( + localId = messageId, + isMms = !isSms, + publicKey = reactor, + emoji = emoji, + react = true, + serverId = "$openGroupMessageServerID", + count = 0, // Only want this on the first reaction + index = reaction.index + )) + } + + // Add the current user reaction (if applicable and not already included) + if (shouldAddUserReaction) { + storage.addReaction(Reaction( + localId = messageId, + isMms = !isSms, + publicKey = userPublicKey, + emoji = emoji, + react = true, + serverId = "$openGroupMessageServerID", + count = 1, + index = reaction.index + )) + } + } +} + //endregion // region Closed Groups diff --git a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt index ced4cbbba2..5a681594d0 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/sending_receiving/pollers/OpenGroupPoller.kt @@ -22,6 +22,7 @@ import org.session.libsession.messaging.open_groups.OpenGroupApi import org.session.libsession.messaging.open_groups.OpenGroupMessage import org.session.libsession.messaging.sending_receiving.MessageReceiver import org.session.libsession.messaging.sending_receiving.handle +import org.session.libsession.messaging.sending_receiving.handleOpenGroupReactions import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.utilities.Address import org.session.libsession.utilities.GroupUtil @@ -136,10 +137,16 @@ class OpenGroupPoller(private val server: String, private val executorService: S pollInfo.details?.moderators?.forEach { storage.addGroupMember(GroupMember(groupId, it, GroupMemberRole.MODERATOR)) } + pollInfo.details?.hiddenModerators?.forEach { + storage.addGroupMember(GroupMember(groupId, it, GroupMemberRole.HIDDEN_MODERATOR)) + } // - Admins pollInfo.details?.admins?.forEach { storage.addGroupMember(GroupMember(groupId, it, GroupMemberRole.ADMIN)) } + pollInfo.details?.hiddenAdmins?.forEach { + storage.addGroupMember(GroupMember(groupId, it, GroupMemberRole.HIDDEN_ADMIN)) + } } private fun handleMessages( @@ -147,22 +154,23 @@ class OpenGroupPoller(private val server: String, private val executorService: S roomToken: String, messages: List ) { - val openGroupId = "$server.$roomToken" val sortedMessages = messages.sortedBy { it.seqno } - sortedMessages.maxOfOrNull { it.seqno }?.let { - MessagingModuleConfiguration.shared.storage.setLastMessageServerID(roomToken, server, it) + sortedMessages.maxOfOrNull { it.seqno }?.let { seqNo -> + MessagingModuleConfiguration.shared.storage.setLastMessageServerID(roomToken, server, seqNo) + OpenGroupApi.pendingReactions.removeAll { !(it.seqNo == null || it.seqNo!! > seqNo) } } - val (deletions, additions) = sortedMessages.partition { it.deleted || it.data.isNullOrBlank() } - handleNewMessages(openGroupId, additions.map { + val (deletions, additions) = sortedMessages.partition { it.deleted } + handleNewMessages(server, roomToken, additions.map { OpenGroupMessage( serverID = it.id, sender = it.sessionId, sentTimestamp = (it.posted * 1000).toLong(), - base64EncodedData = it.data!!, - base64EncodedSignature = it.signature + base64EncodedData = it.data, + base64EncodedSignature = it.signature, + reactions = it.reactions ) }) - handleDeletedMessages(openGroupId, deletions.map { it.id }) + handleDeletedMessages(server, roomToken, deletions.map { it.id }) } private fun handleDirectMessages( @@ -219,43 +227,52 @@ class OpenGroupPoller(private val server: String, private val executorService: S } } - private fun handleNewMessages(openGroupID: String, messages: List) { + private fun handleNewMessages(server: String, roomToken: String, messages: List) { val storage = MessagingModuleConfiguration.shared.storage + val openGroupID = "$server.$roomToken" val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray()) // check thread still exists val threadId = storage.getThreadId(Address.fromSerialized(groupID)) ?: -1 val threadExists = threadId >= 0 if (!hasStarted || !threadExists) { return } - val envelopes = messages.sortedBy { it.serverID!! }.map { message -> - val senderPublicKey = message.sender!! - val builder = SignalServiceProtos.Envelope.newBuilder() - builder.type = SignalServiceProtos.Envelope.Type.SESSION_MESSAGE - builder.source = senderPublicKey - builder.sourceDevice = 1 - builder.content = message.toProto().toByteString() - builder.timestamp = message.sentTimestamp - builder.build() to message.serverID + val envelopes = mutableListOf?>>() + messages.sortedBy { it.serverID!! }.forEach { message -> + if (!message.base64EncodedData.isNullOrEmpty()) { + val envelope = SignalServiceProtos.Envelope.newBuilder() + .setType(SignalServiceProtos.Envelope.Type.SESSION_MESSAGE) + .setSource(message.sender!!) + .setSourceDevice(1) + .setContent(message.toProto().toByteString()) + .setTimestamp(message.sentTimestamp) + .build() + envelopes.add(Triple( message.serverID, envelope, message.reactions)) + } else if (!message.reactions.isNullOrEmpty()) { + message.serverID?.let { + MessageReceiver.handleOpenGroupReactions(threadId, it, message.reactions) + } + } } envelopes.chunked(BatchMessageReceiveJob.BATCH_DEFAULT_NUMBER).forEach { list -> - val parameters = list.map { (message, serverId) -> - MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId) + val parameters = list.map { (serverId, message, reactions) -> + MessageReceiveParameters(message.toByteArray(), openGroupMessageServerID = serverId, reactions = reactions) } JobQueue.shared.add(BatchMessageReceiveJob(parameters, openGroupID)) } if (envelopes.isNotEmpty()) { - JobQueue.shared.add(TrimThreadJob(threadId,openGroupID)) + JobQueue.shared.add(TrimThreadJob(threadId, openGroupID)) } } - private fun handleDeletedMessages(openGroupID: String, serverIds: List) { + private fun handleDeletedMessages(server: String, roomToken: String, serverIds: List) { + val openGroupId = "$server.$roomToken" val storage = MessagingModuleConfiguration.shared.storage - val groupID = GroupUtil.getEncodedOpenGroupID(openGroupID.toByteArray()) + val groupID = GroupUtil.getEncodedOpenGroupID(openGroupId.toByteArray()) val threadID = storage.getThreadId(Address.fromSerialized(groupID)) ?: return if (serverIds.isNotEmpty()) { - val deleteJob = OpenGroupDeleteJob(serverIds.toLongArray(), threadID, openGroupID) + val deleteJob = OpenGroupDeleteJob(serverIds.toLongArray(), threadID, openGroupId) JobQueue.shared.add(deleteJob) } } diff --git a/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt b/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt index e4dded5a78..9a1de4f2d5 100644 --- a/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt +++ b/libsession/src/main/java/org/session/libsession/messaging/utilities/SodiumUtilities.kt @@ -55,6 +55,7 @@ object SodiumUtilities { } /* Constructs a "blinded" key pair (`ka, kA`) based on an open group server `publicKey` and an ed25519 `keyPair` */ + @JvmStatic fun blindedKeyPair(serverPublicKey: String, edKeyPair: KeyPair): KeyPair? { if (edKeyPair.publicKey.asBytes.size != PUBLIC_KEY_LENGTH || edKeyPair.secretKey.asBytes.size != SECRET_KEY_LENGTH) return null val kBytes = generateBlindingFactor(serverPublicKey) ?: return null diff --git a/libsession/src/main/java/org/session/libsession/utilities/OpenGroupUrlParser.kt b/libsession/src/main/java/org/session/libsession/utilities/OpenGroupUrlParser.kt index ac7f9ad64f..d39128d5dc 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/OpenGroupUrlParser.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/OpenGroupUrlParser.kt @@ -1,6 +1,7 @@ package org.session.libsession.utilities import okhttp3.HttpUrl +import org.session.libsession.messaging.open_groups.migrateLegacyServerUrl object OpenGroupUrlParser { @@ -20,7 +21,7 @@ object OpenGroupUrlParser { // If the URL is malformed, throw an exception val url = HttpUrl.parse(urlWithPrefix) ?: throw Error.MalformedURL // Parse components - val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).port(url.port()).build().toString().removeSuffix(suffix) + val server = HttpUrl.Builder().scheme(url.scheme()).host(url.host()).port(url.port()).build().toString().removeSuffix(suffix).migrateLegacyServerUrl() val room = url.pathSegments().firstOrNull { !it.isNullOrEmpty() } ?: throw Error.NoRoom val publicKey = url.queryParameter(queryPrefix) ?: throw Error.NoPublicKey if (publicKey.length != 64) throw Error.InvalidPublicKey @@ -33,4 +34,6 @@ object OpenGroupUrlParser { } } -class V2OpenGroupInfo(val server: String, val room: String, val serverPublicKey: String) +class V2OpenGroupInfo(val server: String, val room: String, val serverPublicKey: String) { + fun joinUrl() = "$server/$room?public_key=$serverPublicKey" +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/ThemeUtil.java b/libsession/src/main/java/org/session/libsession/utilities/ThemeUtil.java index cf9eb05d08..dc77aae5ac 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/ThemeUtil.java +++ b/libsession/src/main/java/org/session/libsession/utilities/ThemeUtil.java @@ -3,6 +3,7 @@ package org.session.libsession.utilities; import android.content.Context; import android.content.res.Resources; import android.graphics.Color; +import android.graphics.drawable.Drawable; import android.util.TypedValue; import android.view.LayoutInflater; @@ -10,7 +11,9 @@ import androidx.annotation.AttrRes; import androidx.annotation.ColorInt; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.annotation.StyleRes; +import androidx.appcompat.content.res.AppCompatResources; import androidx.appcompat.view.ContextThemeWrapper; import org.session.libsignal.utilities.Log; @@ -24,6 +27,17 @@ public class ThemeUtil { return getAttributeText(context, R.attr.theme_type, "light").equals("dark"); } + public static boolean getThemedBoolean(@NonNull Context context, @AttrRes int attr) { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + + if (theme.resolveAttribute(attr, typedValue, true)) { + return typedValue.data != 0; + } + + return false; + } + @ColorInt public static int getThemedColor(@NonNull Context context, @AttrRes int attr) { TypedValue typedValue = new TypedValue(); @@ -50,6 +64,18 @@ public class ThemeUtil { } } + public static @Nullable + Drawable getThemedDrawable(@NonNull Context context, @AttrRes int attr) { + TypedValue typedValue = new TypedValue(); + Resources.Theme theme = context.getTheme(); + + if (theme.resolveAttribute(attr, typedValue, true)) { + return AppCompatResources.getDrawable(context, typedValue.resourceId); + } + + return null; + } + public static LayoutInflater getThemedInflater(@NonNull Context context, @NonNull LayoutInflater inflater, @StyleRes int theme) { Context contextThemeWrapper = new ContextThemeWrapper(context, theme); return inflater.cloneInContext(contextThemeWrapper); diff --git a/libsession/src/main/res/values/attrs.xml b/libsession/src/main/res/values/attrs.xml index 558e90799a..59e987c54f 100644 --- a/libsession/src/main/res/values/attrs.xml +++ b/libsession/src/main/res/values/attrs.xml @@ -69,6 +69,7 @@ + @@ -108,6 +109,7 @@ + @@ -303,4 +305,13 @@ + + + + + + + + + diff --git a/libsignal/protobuf/SignalService.proto b/libsignal/protobuf/SignalService.proto index 11015bcd9f..e1c1c856d9 100644 --- a/libsignal/protobuf/SignalService.proto +++ b/libsignal/protobuf/SignalService.proto @@ -148,6 +148,20 @@ message DataMessage { optional uint32 expirationTimer = 8; } + message Reaction { + enum Action { + REACT = 0; + REMOVE = 1; + } + // @required + required uint64 id = 1; + // @required + required string author = 2; + optional string emoji = 3; + // @required + required Action action = 4; + } + optional string body = 1; repeated AttachmentPointer attachments = 2; optional GroupContext group = 3; @@ -157,6 +171,7 @@ message DataMessage { optional uint64 timestamp = 7; optional Quote quote = 8; repeated Preview preview = 10; + optional Reaction reaction = 11; optional LokiProfile profile = 101; optional OpenGroupInvitation openGroupInvitation = 102; optional ClosedGroupControlMessage closedGroupControlMessage = 104; diff --git a/libsignal/src/androidTest/java/org/session/libsignal/ExampleInstrumentedTest.kt b/libsignal/src/androidTest/java/org/session/libsignal/ExampleInstrumentedTest.kt deleted file mode 100644 index a0ce5a7f29..0000000000 --- a/libsignal/src/androidTest/java/org/session/libsignal/ExampleInstrumentedTest.kt +++ /dev/null @@ -1,24 +0,0 @@ -package org.session.libsignal - -import androidx.test.platform.app.InstrumentationRegistry -import androidx.test.ext.junit.runners.AndroidJUnit4 - -import org.junit.Test -import org.junit.runner.RunWith - -import org.junit.Assert.* - -/** - * Instrumented test, which will execute on an Android device. - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -@RunWith(AndroidJUnit4::class) -class ExampleInstrumentedTest { - @Test - fun useAppContext() { - // Context of the app under test. - val appContext = InstrumentationRegistry.getInstrumentation().targetContext - assertEquals("org.session.libsignal.test", appContext.packageName) - } -} \ No newline at end of file diff --git a/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt b/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt index 1bf093128d..a1866bf21e 100644 --- a/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt +++ b/libsignal/src/main/java/org/session/libsignal/database/LokiAPIDatabaseProtocol.kt @@ -20,7 +20,6 @@ interface LokiAPIDatabaseProtocol { fun setReceivedMessageHashValues(publicKey: String, newValue: Set, namespace: Int) fun getAuthToken(server: String): String? fun setAuthToken(server: String, newValue: String?) - fun setUserCount(group: Long, server: String, newValue: Int) fun setUserCount(room: String, server: String, newValue: Int) fun getLastMessageServerID(room: String, server: String): Long? fun setLastMessageServerID(room: String, server: String, newValue: Long) @@ -36,5 +35,5 @@ interface LokiAPIDatabaseProtocol { fun isClosedGroup(groupPublicKey: String): Boolean fun getForkInfo(): ForkInfo fun setForkInfo(forkInfo: ForkInfo) - + fun migrateLegacyOpenGroup(legacyServerId: String, newServerId: String) } diff --git a/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java b/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java index 25bbb0e447..ead1b6255e 100644 --- a/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java +++ b/libsignal/src/main/java/org/session/libsignal/protos/SignalServiceProtos.java @@ -5616,6 +5616,20 @@ public final class SignalServiceProtos { org.session.libsignal.protos.SignalServiceProtos.DataMessage.PreviewOrBuilder getPreviewOrBuilder( int index); + // optional .signalservice.DataMessage.Reaction reaction = 11; + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + boolean hasReaction(); + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction getReaction(); + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + org.session.libsignal.protos.SignalServiceProtos.DataMessage.ReactionOrBuilder getReactionOrBuilder(); + // optional .signalservice.DataMessage.LokiProfile profile = 101; /** * optional .signalservice.DataMessage.LokiProfile profile = 101; @@ -5791,9 +5805,22 @@ public final class SignalServiceProtos { preview_.add(input.readMessage(org.session.libsignal.protos.SignalServiceProtos.DataMessage.Preview.PARSER, extensionRegistry)); break; } + case 90: { + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Builder subBuilder = null; + if (((bitField0_ & 0x00000080) == 0x00000080)) { + subBuilder = reaction_.toBuilder(); + } + reaction_ = input.readMessage(org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.PARSER, extensionRegistry); + if (subBuilder != null) { + subBuilder.mergeFrom(reaction_); + reaction_ = subBuilder.buildPartial(); + } + bitField0_ |= 0x00000080; + break; + } case 810: { org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.Builder subBuilder = null; - if (((bitField0_ & 0x00000080) == 0x00000080)) { + if (((bitField0_ & 0x00000100) == 0x00000100)) { subBuilder = profile_.toBuilder(); } profile_ = input.readMessage(org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.PARSER, extensionRegistry); @@ -5801,12 +5828,12 @@ public final class SignalServiceProtos { subBuilder.mergeFrom(profile_); profile_ = subBuilder.buildPartial(); } - bitField0_ |= 0x00000080; + bitField0_ |= 0x00000100; break; } case 818: { org.session.libsignal.protos.SignalServiceProtos.DataMessage.OpenGroupInvitation.Builder subBuilder = null; - if (((bitField0_ & 0x00000100) == 0x00000100)) { + if (((bitField0_ & 0x00000200) == 0x00000200)) { subBuilder = openGroupInvitation_.toBuilder(); } openGroupInvitation_ = input.readMessage(org.session.libsignal.protos.SignalServiceProtos.DataMessage.OpenGroupInvitation.PARSER, extensionRegistry); @@ -5814,12 +5841,12 @@ public final class SignalServiceProtos { subBuilder.mergeFrom(openGroupInvitation_); openGroupInvitation_ = subBuilder.buildPartial(); } - bitField0_ |= 0x00000100; + bitField0_ |= 0x00000200; break; } case 834: { org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Builder subBuilder = null; - if (((bitField0_ & 0x00000200) == 0x00000200)) { + if (((bitField0_ & 0x00000400) == 0x00000400)) { subBuilder = closedGroupControlMessage_.toBuilder(); } closedGroupControlMessage_ = input.readMessage(org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.PARSER, extensionRegistry); @@ -5827,11 +5854,11 @@ public final class SignalServiceProtos { subBuilder.mergeFrom(closedGroupControlMessage_); closedGroupControlMessage_ = subBuilder.buildPartial(); } - bitField0_ |= 0x00000200; + bitField0_ |= 0x00000400; break; } case 842: { - bitField0_ |= 0x00000400; + bitField0_ |= 0x00000800; syncTarget_ = input.readBytes(); break; } @@ -12734,6 +12761,1022 @@ public final class SignalServiceProtos { // @@protoc_insertion_point(class_scope:signalservice.DataMessage.ClosedGroupControlMessage) } + public interface ReactionOrBuilder + extends com.google.protobuf.MessageOrBuilder { + + // required uint64 id = 1; + /** + * required uint64 id = 1; + * + *
+       * @required
+       * 
+ */ + boolean hasId(); + /** + * required uint64 id = 1; + * + *
+       * @required
+       * 
+ */ + long getId(); + + // required string author = 2; + /** + * required string author = 2; + * + *
+       * @required
+       * 
+ */ + boolean hasAuthor(); + /** + * required string author = 2; + * + *
+       * @required
+       * 
+ */ + java.lang.String getAuthor(); + /** + * required string author = 2; + * + *
+       * @required
+       * 
+ */ + com.google.protobuf.ByteString + getAuthorBytes(); + + // optional string emoji = 3; + /** + * optional string emoji = 3; + */ + boolean hasEmoji(); + /** + * optional string emoji = 3; + */ + java.lang.String getEmoji(); + /** + * optional string emoji = 3; + */ + com.google.protobuf.ByteString + getEmojiBytes(); + + // required .signalservice.DataMessage.Reaction.Action action = 4; + /** + * required .signalservice.DataMessage.Reaction.Action action = 4; + * + *
+       * @required
+       * 
+ */ + boolean hasAction(); + /** + * required .signalservice.DataMessage.Reaction.Action action = 4; + * + *
+       * @required
+       * 
+ */ + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action getAction(); + } + /** + * Protobuf type {@code signalservice.DataMessage.Reaction} + */ + public static final class Reaction extends + com.google.protobuf.GeneratedMessage + implements ReactionOrBuilder { + // Use Reaction.newBuilder() to construct. + private Reaction(com.google.protobuf.GeneratedMessage.Builder builder) { + super(builder); + this.unknownFields = builder.getUnknownFields(); + } + private Reaction(boolean noInit) { this.unknownFields = com.google.protobuf.UnknownFieldSet.getDefaultInstance(); } + + private static final Reaction defaultInstance; + public static Reaction getDefaultInstance() { + return defaultInstance; + } + + public Reaction getDefaultInstanceForType() { + return defaultInstance; + } + + private final com.google.protobuf.UnknownFieldSet unknownFields; + @java.lang.Override + public final com.google.protobuf.UnknownFieldSet + getUnknownFields() { + return this.unknownFields; + } + private Reaction( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + initFields(); + int mutable_bitField0_ = 0; + com.google.protobuf.UnknownFieldSet.Builder unknownFields = + com.google.protobuf.UnknownFieldSet.newBuilder(); + try { + boolean done = false; + while (!done) { + int tag = input.readTag(); + switch (tag) { + case 0: + done = true; + break; + default: { + if (!parseUnknownField(input, unknownFields, + extensionRegistry, tag)) { + done = true; + } + break; + } + case 8: { + bitField0_ |= 0x00000001; + id_ = input.readUInt64(); + break; + } + case 18: { + bitField0_ |= 0x00000002; + author_ = input.readBytes(); + break; + } + case 26: { + bitField0_ |= 0x00000004; + emoji_ = input.readBytes(); + break; + } + case 32: { + int rawValue = input.readEnum(); + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action value = org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action.valueOf(rawValue); + if (value == null) { + unknownFields.mergeVarintField(4, rawValue); + } else { + bitField0_ |= 0x00000008; + action_ = value; + } + break; + } + } + } + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + throw e.setUnfinishedMessage(this); + } catch (java.io.IOException e) { + throw new com.google.protobuf.InvalidProtocolBufferException( + e.getMessage()).setUnfinishedMessage(this); + } finally { + this.unknownFields = unknownFields.build(); + makeExtensionsImmutable(); + } + } + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.session.libsignal.protos.SignalServiceProtos.internal_static_signalservice_DataMessage_Reaction_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.session.libsignal.protos.SignalServiceProtos.internal_static_signalservice_DataMessage_Reaction_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.class, org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Builder.class); + } + + public static com.google.protobuf.Parser PARSER = + new com.google.protobuf.AbstractParser() { + public Reaction parsePartialFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return new Reaction(input, extensionRegistry); + } + }; + + @java.lang.Override + public com.google.protobuf.Parser getParserForType() { + return PARSER; + } + + /** + * Protobuf enum {@code signalservice.DataMessage.Reaction.Action} + */ + public enum Action + implements com.google.protobuf.ProtocolMessageEnum { + /** + * REACT = 0; + */ + REACT(0, 0), + /** + * REMOVE = 1; + */ + REMOVE(1, 1), + ; + + /** + * REACT = 0; + */ + public static final int REACT_VALUE = 0; + /** + * REMOVE = 1; + */ + public static final int REMOVE_VALUE = 1; + + + public final int getNumber() { return value; } + + public static Action valueOf(int value) { + switch (value) { + case 0: return REACT; + case 1: return REMOVE; + default: return null; + } + } + + public static com.google.protobuf.Internal.EnumLiteMap + internalGetValueMap() { + return internalValueMap; + } + private static com.google.protobuf.Internal.EnumLiteMap + internalValueMap = + new com.google.protobuf.Internal.EnumLiteMap() { + public Action findValueByNumber(int number) { + return Action.valueOf(number); + } + }; + + public final com.google.protobuf.Descriptors.EnumValueDescriptor + getValueDescriptor() { + return getDescriptor().getValues().get(index); + } + public final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptorForType() { + return getDescriptor(); + } + public static final com.google.protobuf.Descriptors.EnumDescriptor + getDescriptor() { + return org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.getDescriptor().getEnumTypes().get(0); + } + + private static final Action[] VALUES = values(); + + public static Action valueOf( + com.google.protobuf.Descriptors.EnumValueDescriptor desc) { + if (desc.getType() != getDescriptor()) { + throw new java.lang.IllegalArgumentException( + "EnumValueDescriptor is not for this type."); + } + return VALUES[desc.getIndex()]; + } + + private final int index; + private final int value; + + private Action(int index, int value) { + this.index = index; + this.value = value; + } + + // @@protoc_insertion_point(enum_scope:signalservice.DataMessage.Reaction.Action) + } + + private int bitField0_; + // required uint64 id = 1; + public static final int ID_FIELD_NUMBER = 1; + private long id_; + /** + * required uint64 id = 1; + * + *
+       * @required
+       * 
+ */ + public boolean hasId() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * required uint64 id = 1; + * + *
+       * @required
+       * 
+ */ + public long getId() { + return id_; + } + + // required string author = 2; + public static final int AUTHOR_FIELD_NUMBER = 2; + private java.lang.Object author_; + /** + * required string author = 2; + * + *
+       * @required
+       * 
+ */ + public boolean hasAuthor() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + /** + * required string author = 2; + * + *
+       * @required
+       * 
+ */ + public java.lang.String getAuthor() { + java.lang.Object ref = author_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { + author_ = s; + } + return s; + } + } + /** + * required string author = 2; + * + *
+       * @required
+       * 
+ */ + public com.google.protobuf.ByteString + getAuthorBytes() { + java.lang.Object ref = author_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + author_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + // optional string emoji = 3; + public static final int EMOJI_FIELD_NUMBER = 3; + private java.lang.Object emoji_; + /** + * optional string emoji = 3; + */ + public boolean hasEmoji() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + /** + * optional string emoji = 3; + */ + public java.lang.String getEmoji() { + java.lang.Object ref = emoji_; + if (ref instanceof java.lang.String) { + return (java.lang.String) ref; + } else { + com.google.protobuf.ByteString bs = + (com.google.protobuf.ByteString) ref; + java.lang.String s = bs.toStringUtf8(); + if (bs.isValidUtf8()) { + emoji_ = s; + } + return s; + } + } + /** + * optional string emoji = 3; + */ + public com.google.protobuf.ByteString + getEmojiBytes() { + java.lang.Object ref = emoji_; + if (ref instanceof java.lang.String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + emoji_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + + // required .signalservice.DataMessage.Reaction.Action action = 4; + public static final int ACTION_FIELD_NUMBER = 4; + private org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action action_; + /** + * required .signalservice.DataMessage.Reaction.Action action = 4; + * + *
+       * @required
+       * 
+ */ + public boolean hasAction() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + /** + * required .signalservice.DataMessage.Reaction.Action action = 4; + * + *
+       * @required
+       * 
+ */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action getAction() { + return action_; + } + + private void initFields() { + id_ = 0L; + author_ = ""; + emoji_ = ""; + action_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action.REACT; + } + private byte memoizedIsInitialized = -1; + public final boolean isInitialized() { + byte isInitialized = memoizedIsInitialized; + if (isInitialized != -1) return isInitialized == 1; + + if (!hasId()) { + memoizedIsInitialized = 0; + return false; + } + if (!hasAuthor()) { + memoizedIsInitialized = 0; + return false; + } + if (!hasAction()) { + memoizedIsInitialized = 0; + return false; + } + memoizedIsInitialized = 1; + return true; + } + + public void writeTo(com.google.protobuf.CodedOutputStream output) + throws java.io.IOException { + getSerializedSize(); + if (((bitField0_ & 0x00000001) == 0x00000001)) { + output.writeUInt64(1, id_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + output.writeBytes(2, getAuthorBytes()); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + output.writeBytes(3, getEmojiBytes()); + } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + output.writeEnum(4, action_.getNumber()); + } + getUnknownFields().writeTo(output); + } + + private int memoizedSerializedSize = -1; + public int getSerializedSize() { + int size = memoizedSerializedSize; + if (size != -1) return size; + + size = 0; + if (((bitField0_ & 0x00000001) == 0x00000001)) { + size += com.google.protobuf.CodedOutputStream + .computeUInt64Size(1, id_); + } + if (((bitField0_ & 0x00000002) == 0x00000002)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(2, getAuthorBytes()); + } + if (((bitField0_ & 0x00000004) == 0x00000004)) { + size += com.google.protobuf.CodedOutputStream + .computeBytesSize(3, getEmojiBytes()); + } + if (((bitField0_ & 0x00000008) == 0x00000008)) { + size += com.google.protobuf.CodedOutputStream + .computeEnumSize(4, action_.getNumber()); + } + size += getUnknownFields().getSerializedSize(); + memoizedSerializedSize = size; + return size; + } + + private static final long serialVersionUID = 0L; + @java.lang.Override + protected java.lang.Object writeReplace() + throws java.io.ObjectStreamException { + return super.writeReplace(); + } + + public static org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parseFrom( + com.google.protobuf.ByteString data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parseFrom( + com.google.protobuf.ByteString data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parseFrom(byte[] data) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data); + } + public static org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parseFrom( + byte[] data, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws com.google.protobuf.InvalidProtocolBufferException { + return PARSER.parseFrom(data, extensionRegistry); + } + public static org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parseFrom(java.io.InputStream input) + throws java.io.IOException { + return PARSER.parseFrom(input); + } + public static org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parseFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseFrom(input, extensionRegistry); + } + public static org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parseDelimitedFrom(java.io.InputStream input) + throws java.io.IOException { + return PARSER.parseDelimitedFrom(input); + } + public static org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parseDelimitedFrom( + java.io.InputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseDelimitedFrom(input, extensionRegistry); + } + public static org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parseFrom( + com.google.protobuf.CodedInputStream input) + throws java.io.IOException { + return PARSER.parseFrom(input); + } + public static org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parseFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + return PARSER.parseFrom(input, extensionRegistry); + } + + public static Builder newBuilder() { return Builder.create(); } + public Builder newBuilderForType() { return newBuilder(); } + public static Builder newBuilder(org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction prototype) { + return newBuilder().mergeFrom(prototype); + } + public Builder toBuilder() { return newBuilder(this); } + + @java.lang.Override + protected Builder newBuilderForType( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + Builder builder = new Builder(parent); + return builder; + } + /** + * Protobuf type {@code signalservice.DataMessage.Reaction} + */ + public static final class Builder extends + com.google.protobuf.GeneratedMessage.Builder + implements org.session.libsignal.protos.SignalServiceProtos.DataMessage.ReactionOrBuilder { + public static final com.google.protobuf.Descriptors.Descriptor + getDescriptor() { + return org.session.libsignal.protos.SignalServiceProtos.internal_static_signalservice_DataMessage_Reaction_descriptor; + } + + protected com.google.protobuf.GeneratedMessage.FieldAccessorTable + internalGetFieldAccessorTable() { + return org.session.libsignal.protos.SignalServiceProtos.internal_static_signalservice_DataMessage_Reaction_fieldAccessorTable + .ensureFieldAccessorsInitialized( + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.class, org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Builder.class); + } + + // Construct using org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.newBuilder() + private Builder() { + maybeForceBuilderInitialization(); + } + + private Builder( + com.google.protobuf.GeneratedMessage.BuilderParent parent) { + super(parent); + maybeForceBuilderInitialization(); + } + private void maybeForceBuilderInitialization() { + if (com.google.protobuf.GeneratedMessage.alwaysUseFieldBuilders) { + } + } + private static Builder create() { + return new Builder(); + } + + public Builder clear() { + super.clear(); + id_ = 0L; + bitField0_ = (bitField0_ & ~0x00000001); + author_ = ""; + bitField0_ = (bitField0_ & ~0x00000002); + emoji_ = ""; + bitField0_ = (bitField0_ & ~0x00000004); + action_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action.REACT; + bitField0_ = (bitField0_ & ~0x00000008); + return this; + } + + public Builder clone() { + return create().mergeFrom(buildPartial()); + } + + public com.google.protobuf.Descriptors.Descriptor + getDescriptorForType() { + return org.session.libsignal.protos.SignalServiceProtos.internal_static_signalservice_DataMessage_Reaction_descriptor; + } + + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction getDefaultInstanceForType() { + return org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.getDefaultInstance(); + } + + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction build() { + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction result = buildPartial(); + if (!result.isInitialized()) { + throw newUninitializedMessageException(result); + } + return result; + } + + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction buildPartial() { + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction result = new org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction(this); + int from_bitField0_ = bitField0_; + int to_bitField0_ = 0; + if (((from_bitField0_ & 0x00000001) == 0x00000001)) { + to_bitField0_ |= 0x00000001; + } + result.id_ = id_; + if (((from_bitField0_ & 0x00000002) == 0x00000002)) { + to_bitField0_ |= 0x00000002; + } + result.author_ = author_; + if (((from_bitField0_ & 0x00000004) == 0x00000004)) { + to_bitField0_ |= 0x00000004; + } + result.emoji_ = emoji_; + if (((from_bitField0_ & 0x00000008) == 0x00000008)) { + to_bitField0_ |= 0x00000008; + } + result.action_ = action_; + result.bitField0_ = to_bitField0_; + onBuilt(); + return result; + } + + public Builder mergeFrom(com.google.protobuf.Message other) { + if (other instanceof org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction) { + return mergeFrom((org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction)other); + } else { + super.mergeFrom(other); + return this; + } + } + + public Builder mergeFrom(org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction other) { + if (other == org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.getDefaultInstance()) return this; + if (other.hasId()) { + setId(other.getId()); + } + if (other.hasAuthor()) { + bitField0_ |= 0x00000002; + author_ = other.author_; + onChanged(); + } + if (other.hasEmoji()) { + bitField0_ |= 0x00000004; + emoji_ = other.emoji_; + onChanged(); + } + if (other.hasAction()) { + setAction(other.getAction()); + } + this.mergeUnknownFields(other.getUnknownFields()); + return this; + } + + public final boolean isInitialized() { + if (!hasId()) { + + return false; + } + if (!hasAuthor()) { + + return false; + } + if (!hasAction()) { + + return false; + } + return true; + } + + public Builder mergeFrom( + com.google.protobuf.CodedInputStream input, + com.google.protobuf.ExtensionRegistryLite extensionRegistry) + throws java.io.IOException { + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction parsedMessage = null; + try { + parsedMessage = PARSER.parsePartialFrom(input, extensionRegistry); + } catch (com.google.protobuf.InvalidProtocolBufferException e) { + parsedMessage = (org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction) e.getUnfinishedMessage(); + throw e; + } finally { + if (parsedMessage != null) { + mergeFrom(parsedMessage); + } + } + return this; + } + private int bitField0_; + + // required uint64 id = 1; + private long id_ ; + /** + * required uint64 id = 1; + * + *
+         * @required
+         * 
+ */ + public boolean hasId() { + return ((bitField0_ & 0x00000001) == 0x00000001); + } + /** + * required uint64 id = 1; + * + *
+         * @required
+         * 
+ */ + public long getId() { + return id_; + } + /** + * required uint64 id = 1; + * + *
+         * @required
+         * 
+ */ + public Builder setId(long value) { + bitField0_ |= 0x00000001; + id_ = value; + onChanged(); + return this; + } + /** + * required uint64 id = 1; + * + *
+         * @required
+         * 
+ */ + public Builder clearId() { + bitField0_ = (bitField0_ & ~0x00000001); + id_ = 0L; + onChanged(); + return this; + } + + // required string author = 2; + private java.lang.Object author_ = ""; + /** + * required string author = 2; + * + *
+         * @required
+         * 
+ */ + public boolean hasAuthor() { + return ((bitField0_ & 0x00000002) == 0x00000002); + } + /** + * required string author = 2; + * + *
+         * @required
+         * 
+ */ + public java.lang.String getAuthor() { + java.lang.Object ref = author_; + if (!(ref instanceof java.lang.String)) { + java.lang.String s = ((com.google.protobuf.ByteString) ref) + .toStringUtf8(); + author_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * required string author = 2; + * + *
+         * @required
+         * 
+ */ + public com.google.protobuf.ByteString + getAuthorBytes() { + java.lang.Object ref = author_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + author_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * required string author = 2; + * + *
+         * @required
+         * 
+ */ + public Builder setAuthor( + java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + author_ = value; + onChanged(); + return this; + } + /** + * required string author = 2; + * + *
+         * @required
+         * 
+ */ + public Builder clearAuthor() { + bitField0_ = (bitField0_ & ~0x00000002); + author_ = getDefaultInstance().getAuthor(); + onChanged(); + return this; + } + /** + * required string author = 2; + * + *
+         * @required
+         * 
+ */ + public Builder setAuthorBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000002; + author_ = value; + onChanged(); + return this; + } + + // optional string emoji = 3; + private java.lang.Object emoji_ = ""; + /** + * optional string emoji = 3; + */ + public boolean hasEmoji() { + return ((bitField0_ & 0x00000004) == 0x00000004); + } + /** + * optional string emoji = 3; + */ + public java.lang.String getEmoji() { + java.lang.Object ref = emoji_; + if (!(ref instanceof java.lang.String)) { + java.lang.String s = ((com.google.protobuf.ByteString) ref) + .toStringUtf8(); + emoji_ = s; + return s; + } else { + return (java.lang.String) ref; + } + } + /** + * optional string emoji = 3; + */ + public com.google.protobuf.ByteString + getEmojiBytes() { + java.lang.Object ref = emoji_; + if (ref instanceof String) { + com.google.protobuf.ByteString b = + com.google.protobuf.ByteString.copyFromUtf8( + (java.lang.String) ref); + emoji_ = b; + return b; + } else { + return (com.google.protobuf.ByteString) ref; + } + } + /** + * optional string emoji = 3; + */ + public Builder setEmoji( + java.lang.String value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000004; + emoji_ = value; + onChanged(); + return this; + } + /** + * optional string emoji = 3; + */ + public Builder clearEmoji() { + bitField0_ = (bitField0_ & ~0x00000004); + emoji_ = getDefaultInstance().getEmoji(); + onChanged(); + return this; + } + /** + * optional string emoji = 3; + */ + public Builder setEmojiBytes( + com.google.protobuf.ByteString value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000004; + emoji_ = value; + onChanged(); + return this; + } + + // required .signalservice.DataMessage.Reaction.Action action = 4; + private org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action action_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action.REACT; + /** + * required .signalservice.DataMessage.Reaction.Action action = 4; + * + *
+         * @required
+         * 
+ */ + public boolean hasAction() { + return ((bitField0_ & 0x00000008) == 0x00000008); + } + /** + * required .signalservice.DataMessage.Reaction.Action action = 4; + * + *
+         * @required
+         * 
+ */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action getAction() { + return action_; + } + /** + * required .signalservice.DataMessage.Reaction.Action action = 4; + * + *
+         * @required
+         * 
+ */ + public Builder setAction(org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action value) { + if (value == null) { + throw new NullPointerException(); + } + bitField0_ |= 0x00000008; + action_ = value; + onChanged(); + return this; + } + /** + * required .signalservice.DataMessage.Reaction.Action action = 4; + * + *
+         * @required
+         * 
+ */ + public Builder clearAction() { + bitField0_ = (bitField0_ & ~0x00000008); + action_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Action.REACT; + onChanged(); + return this; + } + + // @@protoc_insertion_point(builder_scope:signalservice.DataMessage.Reaction) + } + + static { + defaultInstance = new Reaction(true); + defaultInstance.initFields(); + } + + // @@protoc_insertion_point(class_scope:signalservice.DataMessage.Reaction) + } + private int bitField0_; // optional string body = 1; public static final int BODY_FIELD_NUMBER = 1; @@ -12958,6 +14001,28 @@ public final class SignalServiceProtos { return preview_.get(index); } + // optional .signalservice.DataMessage.Reaction reaction = 11; + public static final int REACTION_FIELD_NUMBER = 11; + private org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction reaction_; + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public boolean hasReaction() { + return ((bitField0_ & 0x00000080) == 0x00000080); + } + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction getReaction() { + return reaction_; + } + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.ReactionOrBuilder getReactionOrBuilder() { + return reaction_; + } + // optional .signalservice.DataMessage.LokiProfile profile = 101; public static final int PROFILE_FIELD_NUMBER = 101; private org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile profile_; @@ -12965,7 +14030,7 @@ public final class SignalServiceProtos { * optional .signalservice.DataMessage.LokiProfile profile = 101; */ public boolean hasProfile() { - return ((bitField0_ & 0x00000080) == 0x00000080); + return ((bitField0_ & 0x00000100) == 0x00000100); } /** * optional .signalservice.DataMessage.LokiProfile profile = 101; @@ -12987,7 +14052,7 @@ public final class SignalServiceProtos { * optional .signalservice.DataMessage.OpenGroupInvitation openGroupInvitation = 102; */ public boolean hasOpenGroupInvitation() { - return ((bitField0_ & 0x00000100) == 0x00000100); + return ((bitField0_ & 0x00000200) == 0x00000200); } /** * optional .signalservice.DataMessage.OpenGroupInvitation openGroupInvitation = 102; @@ -13009,7 +14074,7 @@ public final class SignalServiceProtos { * optional .signalservice.DataMessage.ClosedGroupControlMessage closedGroupControlMessage = 104; */ public boolean hasClosedGroupControlMessage() { - return ((bitField0_ & 0x00000200) == 0x00000200); + return ((bitField0_ & 0x00000400) == 0x00000400); } /** * optional .signalservice.DataMessage.ClosedGroupControlMessage closedGroupControlMessage = 104; @@ -13031,7 +14096,7 @@ public final class SignalServiceProtos { * optional string syncTarget = 105; */ public boolean hasSyncTarget() { - return ((bitField0_ & 0x00000400) == 0x00000400); + return ((bitField0_ & 0x00000800) == 0x00000800); } /** * optional string syncTarget = 105; @@ -13077,6 +14142,7 @@ public final class SignalServiceProtos { timestamp_ = 0L; quote_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.Quote.getDefaultInstance(); preview_ = java.util.Collections.emptyList(); + reaction_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.getDefaultInstance(); profile_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.getDefaultInstance(); openGroupInvitation_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.OpenGroupInvitation.getDefaultInstance(); closedGroupControlMessage_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.getDefaultInstance(); @@ -13111,6 +14177,12 @@ public final class SignalServiceProtos { return false; } } + if (hasReaction()) { + if (!getReaction().isInitialized()) { + memoizedIsInitialized = 0; + return false; + } + } if (hasOpenGroupInvitation()) { if (!getOpenGroupInvitation().isInitialized()) { memoizedIsInitialized = 0; @@ -13158,15 +14230,18 @@ public final class SignalServiceProtos { output.writeMessage(10, preview_.get(i)); } if (((bitField0_ & 0x00000080) == 0x00000080)) { - output.writeMessage(101, profile_); + output.writeMessage(11, reaction_); } if (((bitField0_ & 0x00000100) == 0x00000100)) { - output.writeMessage(102, openGroupInvitation_); + output.writeMessage(101, profile_); } if (((bitField0_ & 0x00000200) == 0x00000200)) { - output.writeMessage(104, closedGroupControlMessage_); + output.writeMessage(102, openGroupInvitation_); } if (((bitField0_ & 0x00000400) == 0x00000400)) { + output.writeMessage(104, closedGroupControlMessage_); + } + if (((bitField0_ & 0x00000800) == 0x00000800)) { output.writeBytes(105, getSyncTargetBytes()); } getUnknownFields().writeTo(output); @@ -13216,17 +14291,21 @@ public final class SignalServiceProtos { } if (((bitField0_ & 0x00000080) == 0x00000080)) { size += com.google.protobuf.CodedOutputStream - .computeMessageSize(101, profile_); + .computeMessageSize(11, reaction_); } if (((bitField0_ & 0x00000100) == 0x00000100)) { size += com.google.protobuf.CodedOutputStream - .computeMessageSize(102, openGroupInvitation_); + .computeMessageSize(101, profile_); } if (((bitField0_ & 0x00000200) == 0x00000200)) { size += com.google.protobuf.CodedOutputStream - .computeMessageSize(104, closedGroupControlMessage_); + .computeMessageSize(102, openGroupInvitation_); } if (((bitField0_ & 0x00000400) == 0x00000400)) { + size += com.google.protobuf.CodedOutputStream + .computeMessageSize(104, closedGroupControlMessage_); + } + if (((bitField0_ & 0x00000800) == 0x00000800)) { size += com.google.protobuf.CodedOutputStream .computeBytesSize(105, getSyncTargetBytes()); } @@ -13342,6 +14421,7 @@ public final class SignalServiceProtos { getGroupFieldBuilder(); getQuoteFieldBuilder(); getPreviewFieldBuilder(); + getReactionFieldBuilder(); getProfileFieldBuilder(); getOpenGroupInvitationFieldBuilder(); getClosedGroupControlMessageFieldBuilder(); @@ -13387,26 +14467,32 @@ public final class SignalServiceProtos { } else { previewBuilder_.clear(); } + if (reactionBuilder_ == null) { + reaction_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.getDefaultInstance(); + } else { + reactionBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000200); if (profileBuilder_ == null) { profile_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.getDefaultInstance(); } else { profileBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000200); + bitField0_ = (bitField0_ & ~0x00000400); if (openGroupInvitationBuilder_ == null) { openGroupInvitation_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.OpenGroupInvitation.getDefaultInstance(); } else { openGroupInvitationBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000400); + bitField0_ = (bitField0_ & ~0x00000800); if (closedGroupControlMessageBuilder_ == null) { closedGroupControlMessage_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.getDefaultInstance(); } else { closedGroupControlMessageBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000800); - syncTarget_ = ""; bitField0_ = (bitField0_ & ~0x00001000); + syncTarget_ = ""; + bitField0_ = (bitField0_ & ~0x00002000); return this; } @@ -13492,29 +14578,37 @@ public final class SignalServiceProtos { if (((from_bitField0_ & 0x00000200) == 0x00000200)) { to_bitField0_ |= 0x00000080; } + if (reactionBuilder_ == null) { + result.reaction_ = reaction_; + } else { + result.reaction_ = reactionBuilder_.build(); + } + if (((from_bitField0_ & 0x00000400) == 0x00000400)) { + to_bitField0_ |= 0x00000100; + } if (profileBuilder_ == null) { result.profile_ = profile_; } else { result.profile_ = profileBuilder_.build(); } - if (((from_bitField0_ & 0x00000400) == 0x00000400)) { - to_bitField0_ |= 0x00000100; + if (((from_bitField0_ & 0x00000800) == 0x00000800)) { + to_bitField0_ |= 0x00000200; } if (openGroupInvitationBuilder_ == null) { result.openGroupInvitation_ = openGroupInvitation_; } else { result.openGroupInvitation_ = openGroupInvitationBuilder_.build(); } - if (((from_bitField0_ & 0x00000800) == 0x00000800)) { - to_bitField0_ |= 0x00000200; + if (((from_bitField0_ & 0x00001000) == 0x00001000)) { + to_bitField0_ |= 0x00000400; } if (closedGroupControlMessageBuilder_ == null) { result.closedGroupControlMessage_ = closedGroupControlMessage_; } else { result.closedGroupControlMessage_ = closedGroupControlMessageBuilder_.build(); } - if (((from_bitField0_ & 0x00001000) == 0x00001000)) { - to_bitField0_ |= 0x00000400; + if (((from_bitField0_ & 0x00002000) == 0x00002000)) { + to_bitField0_ |= 0x00000800; } result.syncTarget_ = syncTarget_; result.bitField0_ = to_bitField0_; @@ -13608,6 +14702,9 @@ public final class SignalServiceProtos { } } } + if (other.hasReaction()) { + mergeReaction(other.getReaction()); + } if (other.hasProfile()) { mergeProfile(other.getProfile()); } @@ -13618,7 +14715,7 @@ public final class SignalServiceProtos { mergeClosedGroupControlMessage(other.getClosedGroupControlMessage()); } if (other.hasSyncTarget()) { - bitField0_ |= 0x00001000; + bitField0_ |= 0x00002000; syncTarget_ = other.syncTarget_; onChanged(); } @@ -13651,6 +14748,12 @@ public final class SignalServiceProtos { return false; } } + if (hasReaction()) { + if (!getReaction().isInitialized()) { + + return false; + } + } if (hasOpenGroupInvitation()) { if (!getOpenGroupInvitation().isInitialized()) { @@ -14608,6 +15711,123 @@ public final class SignalServiceProtos { return previewBuilder_; } + // optional .signalservice.DataMessage.Reaction reaction = 11; + private org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction reaction_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.getDefaultInstance(); + private com.google.protobuf.SingleFieldBuilder< + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction, org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Builder, org.session.libsignal.protos.SignalServiceProtos.DataMessage.ReactionOrBuilder> reactionBuilder_; + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public boolean hasReaction() { + return ((bitField0_ & 0x00000200) == 0x00000200); + } + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction getReaction() { + if (reactionBuilder_ == null) { + return reaction_; + } else { + return reactionBuilder_.getMessage(); + } + } + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public Builder setReaction(org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction value) { + if (reactionBuilder_ == null) { + if (value == null) { + throw new NullPointerException(); + } + reaction_ = value; + onChanged(); + } else { + reactionBuilder_.setMessage(value); + } + bitField0_ |= 0x00000200; + return this; + } + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public Builder setReaction( + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Builder builderForValue) { + if (reactionBuilder_ == null) { + reaction_ = builderForValue.build(); + onChanged(); + } else { + reactionBuilder_.setMessage(builderForValue.build()); + } + bitField0_ |= 0x00000200; + return this; + } + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public Builder mergeReaction(org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction value) { + if (reactionBuilder_ == null) { + if (((bitField0_ & 0x00000200) == 0x00000200) && + reaction_ != org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.getDefaultInstance()) { + reaction_ = + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.newBuilder(reaction_).mergeFrom(value).buildPartial(); + } else { + reaction_ = value; + } + onChanged(); + } else { + reactionBuilder_.mergeFrom(value); + } + bitField0_ |= 0x00000200; + return this; + } + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public Builder clearReaction() { + if (reactionBuilder_ == null) { + reaction_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.getDefaultInstance(); + onChanged(); + } else { + reactionBuilder_.clear(); + } + bitField0_ = (bitField0_ & ~0x00000200); + return this; + } + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Builder getReactionBuilder() { + bitField0_ |= 0x00000200; + onChanged(); + return getReactionFieldBuilder().getBuilder(); + } + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + public org.session.libsignal.protos.SignalServiceProtos.DataMessage.ReactionOrBuilder getReactionOrBuilder() { + if (reactionBuilder_ != null) { + return reactionBuilder_.getMessageOrBuilder(); + } else { + return reaction_; + } + } + /** + * optional .signalservice.DataMessage.Reaction reaction = 11; + */ + private com.google.protobuf.SingleFieldBuilder< + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction, org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Builder, org.session.libsignal.protos.SignalServiceProtos.DataMessage.ReactionOrBuilder> + getReactionFieldBuilder() { + if (reactionBuilder_ == null) { + reactionBuilder_ = new com.google.protobuf.SingleFieldBuilder< + org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction, org.session.libsignal.protos.SignalServiceProtos.DataMessage.Reaction.Builder, org.session.libsignal.protos.SignalServiceProtos.DataMessage.ReactionOrBuilder>( + reaction_, + getParentForChildren(), + isClean()); + reaction_ = null; + } + return reactionBuilder_; + } + // optional .signalservice.DataMessage.LokiProfile profile = 101; private org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile profile_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.getDefaultInstance(); private com.google.protobuf.SingleFieldBuilder< @@ -14616,7 +15836,7 @@ public final class SignalServiceProtos { * optional .signalservice.DataMessage.LokiProfile profile = 101; */ public boolean hasProfile() { - return ((bitField0_ & 0x00000200) == 0x00000200); + return ((bitField0_ & 0x00000400) == 0x00000400); } /** * optional .signalservice.DataMessage.LokiProfile profile = 101; @@ -14641,7 +15861,7 @@ public final class SignalServiceProtos { } else { profileBuilder_.setMessage(value); } - bitField0_ |= 0x00000200; + bitField0_ |= 0x00000400; return this; } /** @@ -14655,7 +15875,7 @@ public final class SignalServiceProtos { } else { profileBuilder_.setMessage(builderForValue.build()); } - bitField0_ |= 0x00000200; + bitField0_ |= 0x00000400; return this; } /** @@ -14663,7 +15883,7 @@ public final class SignalServiceProtos { */ public Builder mergeProfile(org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile value) { if (profileBuilder_ == null) { - if (((bitField0_ & 0x00000200) == 0x00000200) && + if (((bitField0_ & 0x00000400) == 0x00000400) && profile_ != org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.getDefaultInstance()) { profile_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.newBuilder(profile_).mergeFrom(value).buildPartial(); @@ -14674,7 +15894,7 @@ public final class SignalServiceProtos { } else { profileBuilder_.mergeFrom(value); } - bitField0_ |= 0x00000200; + bitField0_ |= 0x00000400; return this; } /** @@ -14687,14 +15907,14 @@ public final class SignalServiceProtos { } else { profileBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000200); + bitField0_ = (bitField0_ & ~0x00000400); return this; } /** * optional .signalservice.DataMessage.LokiProfile profile = 101; */ public org.session.libsignal.protos.SignalServiceProtos.DataMessage.LokiProfile.Builder getProfileBuilder() { - bitField0_ |= 0x00000200; + bitField0_ |= 0x00000400; onChanged(); return getProfileFieldBuilder().getBuilder(); } @@ -14733,7 +15953,7 @@ public final class SignalServiceProtos { * optional .signalservice.DataMessage.OpenGroupInvitation openGroupInvitation = 102; */ public boolean hasOpenGroupInvitation() { - return ((bitField0_ & 0x00000400) == 0x00000400); + return ((bitField0_ & 0x00000800) == 0x00000800); } /** * optional .signalservice.DataMessage.OpenGroupInvitation openGroupInvitation = 102; @@ -14758,7 +15978,7 @@ public final class SignalServiceProtos { } else { openGroupInvitationBuilder_.setMessage(value); } - bitField0_ |= 0x00000400; + bitField0_ |= 0x00000800; return this; } /** @@ -14772,7 +15992,7 @@ public final class SignalServiceProtos { } else { openGroupInvitationBuilder_.setMessage(builderForValue.build()); } - bitField0_ |= 0x00000400; + bitField0_ |= 0x00000800; return this; } /** @@ -14780,7 +16000,7 @@ public final class SignalServiceProtos { */ public Builder mergeOpenGroupInvitation(org.session.libsignal.protos.SignalServiceProtos.DataMessage.OpenGroupInvitation value) { if (openGroupInvitationBuilder_ == null) { - if (((bitField0_ & 0x00000400) == 0x00000400) && + if (((bitField0_ & 0x00000800) == 0x00000800) && openGroupInvitation_ != org.session.libsignal.protos.SignalServiceProtos.DataMessage.OpenGroupInvitation.getDefaultInstance()) { openGroupInvitation_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.OpenGroupInvitation.newBuilder(openGroupInvitation_).mergeFrom(value).buildPartial(); @@ -14791,7 +16011,7 @@ public final class SignalServiceProtos { } else { openGroupInvitationBuilder_.mergeFrom(value); } - bitField0_ |= 0x00000400; + bitField0_ |= 0x00000800; return this; } /** @@ -14804,14 +16024,14 @@ public final class SignalServiceProtos { } else { openGroupInvitationBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000400); + bitField0_ = (bitField0_ & ~0x00000800); return this; } /** * optional .signalservice.DataMessage.OpenGroupInvitation openGroupInvitation = 102; */ public org.session.libsignal.protos.SignalServiceProtos.DataMessage.OpenGroupInvitation.Builder getOpenGroupInvitationBuilder() { - bitField0_ |= 0x00000400; + bitField0_ |= 0x00000800; onChanged(); return getOpenGroupInvitationFieldBuilder().getBuilder(); } @@ -14850,7 +16070,7 @@ public final class SignalServiceProtos { * optional .signalservice.DataMessage.ClosedGroupControlMessage closedGroupControlMessage = 104; */ public boolean hasClosedGroupControlMessage() { - return ((bitField0_ & 0x00000800) == 0x00000800); + return ((bitField0_ & 0x00001000) == 0x00001000); } /** * optional .signalservice.DataMessage.ClosedGroupControlMessage closedGroupControlMessage = 104; @@ -14875,7 +16095,7 @@ public final class SignalServiceProtos { } else { closedGroupControlMessageBuilder_.setMessage(value); } - bitField0_ |= 0x00000800; + bitField0_ |= 0x00001000; return this; } /** @@ -14889,7 +16109,7 @@ public final class SignalServiceProtos { } else { closedGroupControlMessageBuilder_.setMessage(builderForValue.build()); } - bitField0_ |= 0x00000800; + bitField0_ |= 0x00001000; return this; } /** @@ -14897,7 +16117,7 @@ public final class SignalServiceProtos { */ public Builder mergeClosedGroupControlMessage(org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage value) { if (closedGroupControlMessageBuilder_ == null) { - if (((bitField0_ & 0x00000800) == 0x00000800) && + if (((bitField0_ & 0x00001000) == 0x00001000) && closedGroupControlMessage_ != org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.getDefaultInstance()) { closedGroupControlMessage_ = org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.newBuilder(closedGroupControlMessage_).mergeFrom(value).buildPartial(); @@ -14908,7 +16128,7 @@ public final class SignalServiceProtos { } else { closedGroupControlMessageBuilder_.mergeFrom(value); } - bitField0_ |= 0x00000800; + bitField0_ |= 0x00001000; return this; } /** @@ -14921,14 +16141,14 @@ public final class SignalServiceProtos { } else { closedGroupControlMessageBuilder_.clear(); } - bitField0_ = (bitField0_ & ~0x00000800); + bitField0_ = (bitField0_ & ~0x00001000); return this; } /** * optional .signalservice.DataMessage.ClosedGroupControlMessage closedGroupControlMessage = 104; */ public org.session.libsignal.protos.SignalServiceProtos.DataMessage.ClosedGroupControlMessage.Builder getClosedGroupControlMessageBuilder() { - bitField0_ |= 0x00000800; + bitField0_ |= 0x00001000; onChanged(); return getClosedGroupControlMessageFieldBuilder().getBuilder(); } @@ -14965,7 +16185,7 @@ public final class SignalServiceProtos { * optional string syncTarget = 105; */ public boolean hasSyncTarget() { - return ((bitField0_ & 0x00001000) == 0x00001000); + return ((bitField0_ & 0x00002000) == 0x00002000); } /** * optional string syncTarget = 105; @@ -15005,7 +16225,7 @@ public final class SignalServiceProtos { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00001000; + bitField0_ |= 0x00002000; syncTarget_ = value; onChanged(); return this; @@ -15014,7 +16234,7 @@ public final class SignalServiceProtos { * optional string syncTarget = 105; */ public Builder clearSyncTarget() { - bitField0_ = (bitField0_ & ~0x00001000); + bitField0_ = (bitField0_ & ~0x00002000); syncTarget_ = getDefaultInstance().getSyncTarget(); onChanged(); return this; @@ -15027,7 +16247,7 @@ public final class SignalServiceProtos { if (value == null) { throw new NullPointerException(); } - bitField0_ |= 0x00001000; + bitField0_ |= 0x00002000; syncTarget_ = value; onChanged(); return this; @@ -24555,6 +25775,11 @@ public final class SignalServiceProtos { private static com.google.protobuf.GeneratedMessage.FieldAccessorTable internal_static_signalservice_DataMessage_ClosedGroupControlMessage_KeyPairWrapper_fieldAccessorTable; + private static com.google.protobuf.Descriptors.Descriptor + internal_static_signalservice_DataMessage_Reaction_descriptor; + private static + com.google.protobuf.GeneratedMessage.FieldAccessorTable + internal_static_signalservice_DataMessage_Reaction_fieldAccessorTable; private static com.google.protobuf.Descriptors.Descriptor internal_static_signalservice_CallMessage_descriptor; private static @@ -24631,7 +25856,7 @@ public final class SignalServiceProtos { "(\014\"\226\001\n\032DataExtractionNotification\022<\n\004typ" + "e\030\001 \002(\0162..signalservice.DataExtractionNo" + "tification.Type\022\021\n\ttimestamp\030\002 \001(\004\"\'\n\004Ty" + - "pe\022\016\n\nSCREENSHOT\020\001\022\017\n\013MEDIA_SAVED\020\002\"\245\014\n\013" + + "pe\022\016\n\nSCREENSHOT\020\001\022\017\n\013MEDIA_SAVED\020\002\"\361\r\n\013" + "DataMessage\022\014\n\004body\030\001 \001(\t\0225\n\013attachments" + "\030\002 \003(\0132 .signalservice.AttachmentPointer", "\022*\n\005group\030\003 \001(\0132\033.signalservice.GroupCon" + @@ -24639,76 +25864,81 @@ public final class SignalServiceProtos { "\022\022\n\nprofileKey\030\006 \001(\014\022\021\n\ttimestamp\030\007 \001(\004\022" + "/\n\005quote\030\010 \001(\0132 .signalservice.DataMessa" + "ge.Quote\0223\n\007preview\030\n \003(\0132\".signalservic" + - "e.DataMessage.Preview\0227\n\007profile\030e \001(\0132&" + - ".signalservice.DataMessage.LokiProfile\022K" + - "\n\023openGroupInvitation\030f \001(\0132..signalserv" + - "ice.DataMessage.OpenGroupInvitation\022W\n\031c" + - "losedGroupControlMessage\030h \001(\01324.signals", - "ervice.DataMessage.ClosedGroupControlMes" + - "sage\022\022\n\nsyncTarget\030i \001(\t\032\225\002\n\005Quote\022\n\n\002id" + - "\030\001 \002(\004\022\016\n\006author\030\002 \002(\t\022\014\n\004text\030\003 \001(\t\022F\n\013" + - "attachments\030\004 \003(\01321.signalservice.DataMe" + - "ssage.Quote.QuotedAttachment\032\231\001\n\020QuotedA" + - "ttachment\022\023\n\013contentType\030\001 \001(\t\022\020\n\010fileNa" + - "me\030\002 \001(\t\0223\n\tthumbnail\030\003 \001(\0132 .signalserv" + - "ice.AttachmentPointer\022\r\n\005flags\030\004 \001(\r\"\032\n\005" + - "Flags\022\021\n\rVOICE_MESSAGE\020\001\032V\n\007Preview\022\013\n\003u" + - "rl\030\001 \002(\t\022\r\n\005title\030\002 \001(\t\022/\n\005image\030\003 \001(\0132 ", - ".signalservice.AttachmentPointer\032:\n\013Loki" + - "Profile\022\023\n\013displayName\030\001 \001(\t\022\026\n\016profileP" + - "icture\030\002 \001(\t\0320\n\023OpenGroupInvitation\022\013\n\003u" + - "rl\030\001 \002(\t\022\014\n\004name\030\003 \002(\t\032\374\003\n\031ClosedGroupCo" + - "ntrolMessage\022G\n\004type\030\001 \002(\01629.signalservi" + - "ce.DataMessage.ClosedGroupControlMessage" + - ".Type\022\021\n\tpublicKey\030\002 \001(\014\022\014\n\004name\030\003 \001(\t\0221" + - "\n\021encryptionKeyPair\030\004 \001(\0132\026.signalservic" + - "e.KeyPair\022\017\n\007members\030\005 \003(\014\022\016\n\006admins\030\006 \003" + - "(\014\022U\n\010wrappers\030\007 \003(\0132C.signalservice.Dat", - "aMessage.ClosedGroupControlMessage.KeyPa" + - "irWrapper\022\027\n\017expirationTimer\030\010 \001(\r\032=\n\016Ke" + - "yPairWrapper\022\021\n\tpublicKey\030\001 \002(\014\022\030\n\020encry" + - "ptedKeyPair\030\002 \002(\014\"r\n\004Type\022\007\n\003NEW\020\001\022\027\n\023EN" + - "CRYPTION_KEY_PAIR\020\003\022\017\n\013NAME_CHANGE\020\004\022\021\n\r" + - "MEMBERS_ADDED\020\005\022\023\n\017MEMBERS_REMOVED\020\006\022\017\n\013" + - "MEMBER_LEFT\020\007\"$\n\005Flags\022\033\n\027EXPIRATION_TIM" + - "ER_UPDATE\020\002\"\352\001\n\013CallMessage\022-\n\004type\030\001 \002(" + - "\0162\037.signalservice.CallMessage.Type\022\014\n\004sd" + - "ps\030\002 \003(\t\022\027\n\017sdpMLineIndexes\030\003 \003(\r\022\017\n\007sdp", - "Mids\030\004 \003(\t\022\014\n\004uuid\030\005 \002(\t\"f\n\004Type\022\r\n\tPRE_" + - "OFFER\020\006\022\t\n\005OFFER\020\001\022\n\n\006ANSWER\020\002\022\026\n\022PROVIS" + - "IONAL_ANSWER\020\003\022\022\n\016ICE_CANDIDATES\020\004\022\014\n\010EN" + - "D_CALL\020\005\"\245\004\n\024ConfigurationMessage\022E\n\014clo" + - "sedGroups\030\001 \003(\0132/.signalservice.Configur" + - "ationMessage.ClosedGroup\022\022\n\nopenGroups\030\002" + - " \003(\t\022\023\n\013displayName\030\003 \001(\t\022\026\n\016profilePict" + - "ure\030\004 \001(\t\022\022\n\nprofileKey\030\005 \001(\014\022=\n\010contact" + - "s\030\006 \003(\0132+.signalservice.ConfigurationMes" + - "sage.Contact\032\233\001\n\013ClosedGroup\022\021\n\tpublicKe", - "y\030\001 \001(\014\022\014\n\004name\030\002 \001(\t\0221\n\021encryptionKeyPa" + - "ir\030\003 \001(\0132\026.signalservice.KeyPair\022\017\n\007memb" + - "ers\030\004 \003(\014\022\016\n\006admins\030\005 \003(\014\022\027\n\017expirationT" + - "imer\030\006 \001(\r\032\223\001\n\007Contact\022\021\n\tpublicKey\030\001 \002(" + - "\014\022\014\n\004name\030\002 \002(\t\022\026\n\016profilePicture\030\003 \001(\t\022" + - "\022\n\nprofileKey\030\004 \001(\014\022\022\n\nisApproved\030\005 \001(\010\022" + - "\021\n\tisBlocked\030\006 \001(\010\022\024\n\014didApproveMe\030\007 \001(\010" + - "\",\n\026MessageRequestResponse\022\022\n\nisApproved" + - "\030\001 \002(\010\"u\n\016ReceiptMessage\0220\n\004type\030\001 \002(\0162\"" + - ".signalservice.ReceiptMessage.Type\022\021\n\tti", - "mestamp\030\002 \003(\004\"\036\n\004Type\022\014\n\010DELIVERY\020\000\022\010\n\004R" + - "EAD\020\001\"\354\001\n\021AttachmentPointer\022\n\n\002id\030\001 \002(\006\022" + - "\023\n\013contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\022\014\n\004siz" + - "e\030\004 \001(\r\022\021\n\tthumbnail\030\005 \001(\014\022\016\n\006digest\030\006 \001" + - "(\014\022\020\n\010fileName\030\007 \001(\t\022\r\n\005flags\030\010 \001(\r\022\r\n\005w" + - "idth\030\t \001(\r\022\016\n\006height\030\n \001(\r\022\017\n\007caption\030\013 " + - "\001(\t\022\013\n\003url\030e \001(\t\"\032\n\005Flags\022\021\n\rVOICE_MESSA" + - "GE\020\001\"\365\001\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022.\n\004typ" + - "e\030\002 \001(\0162 .signalservice.GroupContext.Typ" + - "e\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004 \003(\t\0220\n\006avat", - "ar\030\005 \001(\0132 .signalservice.AttachmentPoint" + - "er\022\016\n\006admins\030\006 \003(\t\"H\n\004Type\022\013\n\007UNKNOWN\020\000\022" + - "\n\n\006UPDATE\020\001\022\013\n\007DELIVER\020\002\022\010\n\004QUIT\020\003\022\020\n\014RE" + - "QUEST_INFO\020\004B3\n\034org.session.libsignal.pr" + - "otosB\023SignalServiceProtos" + "e.DataMessage.Preview\0225\n\010reaction\030\013 \001(\0132" + + "#.signalservice.DataMessage.Reaction\0227\n\007" + + "profile\030e \001(\0132&.signalservice.DataMessag" + + "e.LokiProfile\022K\n\023openGroupInvitation\030f \001" + + "(\0132..signalservice.DataMessage.OpenGroup", + "Invitation\022W\n\031closedGroupControlMessage\030" + + "h \001(\01324.signalservice.DataMessage.Closed" + + "GroupControlMessage\022\022\n\nsyncTarget\030i \001(\t\032" + + "\225\002\n\005Quote\022\n\n\002id\030\001 \002(\004\022\016\n\006author\030\002 \002(\t\022\014\n" + + "\004text\030\003 \001(\t\022F\n\013attachments\030\004 \003(\01321.signa" + + "lservice.DataMessage.Quote.QuotedAttachm" + + "ent\032\231\001\n\020QuotedAttachment\022\023\n\013contentType\030" + + "\001 \001(\t\022\020\n\010fileName\030\002 \001(\t\0223\n\tthumbnail\030\003 \001" + + "(\0132 .signalservice.AttachmentPointer\022\r\n\005" + + "flags\030\004 \001(\r\"\032\n\005Flags\022\021\n\rVOICE_MESSAGE\020\001\032", + "V\n\007Preview\022\013\n\003url\030\001 \002(\t\022\r\n\005title\030\002 \001(\t\022/" + + "\n\005image\030\003 \001(\0132 .signalservice.Attachment" + + "Pointer\032:\n\013LokiProfile\022\023\n\013displayName\030\001 " + + "\001(\t\022\026\n\016profilePicture\030\002 \001(\t\0320\n\023OpenGroup" + + "Invitation\022\013\n\003url\030\001 \002(\t\022\014\n\004name\030\003 \002(\t\032\374\003" + + "\n\031ClosedGroupControlMessage\022G\n\004type\030\001 \002(" + + "\01629.signalservice.DataMessage.ClosedGrou" + + "pControlMessage.Type\022\021\n\tpublicKey\030\002 \001(\014\022" + + "\014\n\004name\030\003 \001(\t\0221\n\021encryptionKeyPair\030\004 \001(\013" + + "2\026.signalservice.KeyPair\022\017\n\007members\030\005 \003(", + "\014\022\016\n\006admins\030\006 \003(\014\022U\n\010wrappers\030\007 \003(\0132C.si" + + "gnalservice.DataMessage.ClosedGroupContr" + + "olMessage.KeyPairWrapper\022\027\n\017expirationTi" + + "mer\030\010 \001(\r\032=\n\016KeyPairWrapper\022\021\n\tpublicKey" + + "\030\001 \002(\014\022\030\n\020encryptedKeyPair\030\002 \002(\014\"r\n\004Type" + + "\022\007\n\003NEW\020\001\022\027\n\023ENCRYPTION_KEY_PAIR\020\003\022\017\n\013NA" + + "ME_CHANGE\020\004\022\021\n\rMEMBERS_ADDED\020\005\022\023\n\017MEMBER" + + "S_REMOVED\020\006\022\017\n\013MEMBER_LEFT\020\007\032\222\001\n\010Reactio" + + "n\022\n\n\002id\030\001 \002(\004\022\016\n\006author\030\002 \002(\t\022\r\n\005emoji\030\003" + + " \001(\t\022:\n\006action\030\004 \002(\0162*.signalservice.Dat", + "aMessage.Reaction.Action\"\037\n\006Action\022\t\n\005RE" + + "ACT\020\000\022\n\n\006REMOVE\020\001\"$\n\005Flags\022\033\n\027EXPIRATION" + + "_TIMER_UPDATE\020\002\"\352\001\n\013CallMessage\022-\n\004type\030" + + "\001 \002(\0162\037.signalservice.CallMessage.Type\022\014" + + "\n\004sdps\030\002 \003(\t\022\027\n\017sdpMLineIndexes\030\003 \003(\r\022\017\n" + + "\007sdpMids\030\004 \003(\t\022\014\n\004uuid\030\005 \002(\t\"f\n\004Type\022\r\n\t" + + "PRE_OFFER\020\006\022\t\n\005OFFER\020\001\022\n\n\006ANSWER\020\002\022\026\n\022PR" + + "OVISIONAL_ANSWER\020\003\022\022\n\016ICE_CANDIDATES\020\004\022\014" + + "\n\010END_CALL\020\005\"\245\004\n\024ConfigurationMessage\022E\n" + + "\014closedGroups\030\001 \003(\0132/.signalservice.Conf", + "igurationMessage.ClosedGroup\022\022\n\nopenGrou" + + "ps\030\002 \003(\t\022\023\n\013displayName\030\003 \001(\t\022\026\n\016profile" + + "Picture\030\004 \001(\t\022\022\n\nprofileKey\030\005 \001(\014\022=\n\010con" + + "tacts\030\006 \003(\0132+.signalservice.Configuratio" + + "nMessage.Contact\032\233\001\n\013ClosedGroup\022\021\n\tpubl" + + "icKey\030\001 \001(\014\022\014\n\004name\030\002 \001(\t\0221\n\021encryptionK" + + "eyPair\030\003 \001(\0132\026.signalservice.KeyPair\022\017\n\007" + + "members\030\004 \003(\014\022\016\n\006admins\030\005 \003(\014\022\027\n\017expirat" + + "ionTimer\030\006 \001(\r\032\223\001\n\007Contact\022\021\n\tpublicKey\030" + + "\001 \002(\014\022\014\n\004name\030\002 \002(\t\022\026\n\016profilePicture\030\003 ", + "\001(\t\022\022\n\nprofileKey\030\004 \001(\014\022\022\n\nisApproved\030\005 " + + "\001(\010\022\021\n\tisBlocked\030\006 \001(\010\022\024\n\014didApproveMe\030\007" + + " \001(\010\",\n\026MessageRequestResponse\022\022\n\nisAppr" + + "oved\030\001 \002(\010\"u\n\016ReceiptMessage\0220\n\004type\030\001 \002" + + "(\0162\".signalservice.ReceiptMessage.Type\022\021" + + "\n\ttimestamp\030\002 \003(\004\"\036\n\004Type\022\014\n\010DELIVERY\020\000\022" + + "\010\n\004READ\020\001\"\354\001\n\021AttachmentPointer\022\n\n\002id\030\001 " + + "\002(\006\022\023\n\013contentType\030\002 \001(\t\022\013\n\003key\030\003 \001(\014\022\014\n" + + "\004size\030\004 \001(\r\022\021\n\tthumbnail\030\005 \001(\014\022\016\n\006digest" + + "\030\006 \001(\014\022\020\n\010fileName\030\007 \001(\t\022\r\n\005flags\030\010 \001(\r\022", + "\r\n\005width\030\t \001(\r\022\016\n\006height\030\n \001(\r\022\017\n\007captio" + + "n\030\013 \001(\t\022\013\n\003url\030e \001(\t\"\032\n\005Flags\022\021\n\rVOICE_M" + + "ESSAGE\020\001\"\365\001\n\014GroupContext\022\n\n\002id\030\001 \001(\014\022.\n" + + "\004type\030\002 \001(\0162 .signalservice.GroupContext" + + ".Type\022\014\n\004name\030\003 \001(\t\022\017\n\007members\030\004 \003(\t\0220\n\006" + + "avatar\030\005 \001(\0132 .signalservice.AttachmentP" + + "ointer\022\016\n\006admins\030\006 \003(\t\"H\n\004Type\022\013\n\007UNKNOW" + + "N\020\000\022\n\n\006UPDATE\020\001\022\013\n\007DELIVER\020\002\022\010\n\004QUIT\020\003\022\020" + + "\n\014REQUEST_INFO\020\004B3\n\034org.session.libsigna" + + "l.protosB\023SignalServiceProtos" }; com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() { @@ -24756,7 +25986,7 @@ public final class SignalServiceProtos { internal_static_signalservice_DataMessage_fieldAccessorTable = new com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_signalservice_DataMessage_descriptor, - new java.lang.String[] { "Body", "Attachments", "Group", "Flags", "ExpireTimer", "ProfileKey", "Timestamp", "Quote", "Preview", "Profile", "OpenGroupInvitation", "ClosedGroupControlMessage", "SyncTarget", }); + new java.lang.String[] { "Body", "Attachments", "Group", "Flags", "ExpireTimer", "ProfileKey", "Timestamp", "Quote", "Preview", "Reaction", "Profile", "OpenGroupInvitation", "ClosedGroupControlMessage", "SyncTarget", }); internal_static_signalservice_DataMessage_Quote_descriptor = internal_static_signalservice_DataMessage_descriptor.getNestedTypes().get(0); internal_static_signalservice_DataMessage_Quote_fieldAccessorTable = new @@ -24799,6 +26029,12 @@ public final class SignalServiceProtos { com.google.protobuf.GeneratedMessage.FieldAccessorTable( internal_static_signalservice_DataMessage_ClosedGroupControlMessage_KeyPairWrapper_descriptor, new java.lang.String[] { "PublicKey", "EncryptedKeyPair", }); + internal_static_signalservice_DataMessage_Reaction_descriptor = + internal_static_signalservice_DataMessage_descriptor.getNestedTypes().get(5); + internal_static_signalservice_DataMessage_Reaction_fieldAccessorTable = new + com.google.protobuf.GeneratedMessage.FieldAccessorTable( + internal_static_signalservice_DataMessage_Reaction_descriptor, + new java.lang.String[] { "Id", "Author", "Emoji", "Action", }); internal_static_signalservice_CallMessage_descriptor = getDescriptor().getMessageTypes().get(7); internal_static_signalservice_CallMessage_fieldAccessorTable = new