Improve emoji sticker suggestions.

There was a bug around some emoji being marked as 'obsolete' and
therefore not being found.

I also made a change so that you can use skin variations of emoji and
still find emoji tagged with the default yellow version of it.

Fixes #9471
master
Greyson Parrelli 2020-03-16 11:43:52 -04:00
parent 1e2a27f902
commit f95a37956c
4 changed files with 104 additions and 26 deletions

View File

@ -0,0 +1,70 @@
package org.thoughtcrime.securesms.components.emoji;
import androidx.annotation.NonNull;
import org.whispersystems.libsignal.util.Pair;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
public final class EmojiUtil {
private static final Map<String, String> VARIATION_MAP = new HashMap<>();
static {
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
for (Emoji emoji : page.getDisplayEmoji()) {
for (String variation : emoji.getVariations()) {
VARIATION_MAP.put(variation, emoji.getValue());
}
}
}
}
public static final int MAX_EMOJI_LENGTH;
static {
int max = 0;
for (EmojiPageModel page : EmojiPages.DATA_PAGES) {
for (String emoji : page.getEmoji()) {
max = Math.max(max, emoji.length());
}
}
MAX_EMOJI_LENGTH = max;
}
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<String> getAllRepresentations(@NonNull String emoji) {
Set<String> out = new HashSet<>();
out.add(emoji);
for (Pair<String, String> pair : EmojiPages.OBSOLETE) {
if (pair.first().equals(emoji)) {
out.add(pair.second());
} else if (pair.second().equals(emoji)) {
out.add(pair.first());
}
}
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 = VARIATION_MAP.get(emoji);
return canonical != null ? canonical : emoji;
}
}

View File

@ -10,6 +10,7 @@ import android.os.Handler;
import androidx.annotation.NonNull;
import android.text.TextUtils;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseContentProviders;
import org.thoughtcrime.securesms.database.model.StickerRecord;
@ -17,21 +18,21 @@ import org.thoughtcrime.securesms.stickers.StickerSearchRepository;
import org.thoughtcrime.securesms.util.CloseableLiveData;
import org.thoughtcrime.securesms.util.Throttler;
import java.util.List;
class ConversationStickerViewModel extends ViewModel {
private static final int SEARCH_LIMIT = 10;
private final Application application;
private final StickerSearchRepository repository;
private final CloseableLiveData<CursorList<StickerRecord>> stickers;
private final MutableLiveData<Boolean> stickersAvailable;
private final Throttler availabilityThrottler;
private final ContentObserver packObserver;
private final Application application;
private final StickerSearchRepository repository;
private final MutableLiveData<List<StickerRecord>> stickers;
private final MutableLiveData<Boolean> stickersAvailable;
private final Throttler availabilityThrottler;
private final ContentObserver packObserver;
private ConversationStickerViewModel(@NonNull Application application, @NonNull StickerSearchRepository repository) {
this.application = application;
this.repository = repository;
this.stickers = new CloseableLiveData<>();
this.stickers = new MutableLiveData<>();
this.stickersAvailable = new MutableLiveData<>();
this.availabilityThrottler = new Throttler(500);
this.packObserver = new ContentObserver(new Handler()) {
@ -44,7 +45,7 @@ class ConversationStickerViewModel extends ViewModel {
application.getContentResolver().registerContentObserver(DatabaseContentProviders.StickerPack.CONTENT_URI, true, packObserver);
}
@NonNull LiveData<CursorList<StickerRecord>> getStickerResults() {
@NonNull LiveData<List<StickerRecord>> getStickerResults() {
return stickers;
}
@ -54,7 +55,7 @@ class ConversationStickerViewModel extends ViewModel {
}
void onInputTextUpdated(@NonNull String text) {
if (TextUtils.isEmpty(text) || text.length() > SEARCH_LIMIT) {
if (TextUtils.isEmpty(text) || text.length() > EmojiUtil.MAX_EMOJI_LENGTH) {
stickers.setValue(CursorList.emptyList());
} else {
repository.searchByEmoji(text, stickers::postValue);
@ -63,7 +64,6 @@ class ConversationStickerViewModel extends ViewModel {
@Override
protected void onCleared() {
stickers.close();
application.getContentResolver().unregisterContentObserver(packObserver);
}

View File

@ -30,6 +30,8 @@ import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.List;
public class StickerDatabase extends Database {

View File

@ -4,14 +4,20 @@ import android.content.Context;
import android.database.Cursor;
import androidx.annotation.NonNull;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.CursorList;
import org.thoughtcrime.securesms.database.DatabaseFactory;
import org.thoughtcrime.securesms.database.StickerDatabase;
import org.thoughtcrime.securesms.database.StickerDatabase.StickerRecordReader;
import org.thoughtcrime.securesms.database.model.StickerPackRecord;
import org.thoughtcrime.securesms.database.model.StickerRecord;
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
public final class StickerSearchRepository {
private final StickerDatabase stickerDatabase;
@ -22,15 +28,22 @@ public final class StickerSearchRepository {
this.attachmentDatabase = DatabaseFactory.getAttachmentDatabase(context);
}
public void searchByEmoji(@NonNull String emoji, @NonNull Callback<CursorList<StickerRecord>> callback) {
public void searchByEmoji(@NonNull String emoji, @NonNull Callback<List<StickerRecord>> callback) {
SignalExecutors.BOUNDED.execute(() -> {
Cursor cursor = stickerDatabase.getStickersByEmoji(emoji);
String searchEmoji = EmojiUtil.getCanonicalRepresentation(emoji);
List<StickerRecord> out = new ArrayList<>();
Set<String> possible = EmojiUtil.getAllRepresentations(searchEmoji);
if (cursor != null) {
callback.onResult(new CursorList<>(cursor, new StickerModelBuilder()));
} else {
callback.onResult(CursorList.emptyList());
for (String candidate : possible) {
try (StickerRecordReader reader = new StickerRecordReader(stickerDatabase.getStickersByEmoji(candidate))) {
StickerRecord record = null;
while ((record = reader.getNext()) != null) {
out.add(record);
}
}
}
callback.onResult(out);
});
}
@ -49,14 +62,7 @@ public final class StickerSearchRepository {
private static class StickerModelBuilder implements CursorList.ModelBuilder<StickerRecord> {
@Override
public StickerRecord build(@NonNull Cursor cursor) {
return new StickerDatabase.StickerRecordReader(cursor).getCurrent();
}
}
private static class StickerPackModelBuilder implements CursorList.ModelBuilder<StickerPackRecord> {
@Override
public StickerPackRecord build(@NonNull Cursor cursor) {
return new StickerDatabase.StickerPackRecordReader(cursor).getCurrent();
return new StickerRecordReader(cursor).getCurrent();
}
}