diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index f56143f9fd..c71c2d9e07 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -492,7 +492,7 @@
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
ApplicationDependencies.getJobManager().beginJobLoop())
.addNonBlocking(EmojiSource::refresh)
- .addNonBlocking(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
.addPostRender(() -> RateLimitUtil.retryAllRateLimitedMessages(this))
.addPostRender(this::initializeExpiringMessageManager)
.addPostRender(() -> SignalStore.settings().setDefaultSms(Util.isDefaultSmsProvider(this)))
+ .addPostRender(() -> DownloadLatestEmojiDataJob.scheduleIfNecessary(this))
+ .addPostRender(EmojiSearchIndexDownloadJob::scheduleIfNecessary)
.execute();
Log.d(TAG, "onCreate() took " + (System.currentTimeMillis() - startTime) + " ms");
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareLayout.java b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareLayout.java
index 142ff5cbff..dae12efa93 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareLayout.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputAwareLayout.java
@@ -26,8 +26,8 @@ public class InputAwareLayout extends KeyboardAwareLinearLayout implements OnKey
addOnKeyboardShownListener(this);
}
- @Override public void onKeyboardShown() {
- hideAttachedInput(true);
+ @Override
+ public void onKeyboardShown() {
}
public void show(@NonNull final EditText imeTarget, @NonNull final InputView input) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java
index dfcd561c6e..ce000529c7 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java
@@ -37,6 +37,7 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
import org.thoughtcrime.securesms.database.model.StickerRecord;
+import org.thoughtcrime.securesms.keyboard.KeyboardPage;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
import org.thoughtcrime.securesms.linkpreview.LinkPreviewRepository;
@@ -279,8 +280,8 @@ public class InputPanel extends LinearLayout
mediaKeyboard.setVisibility(show ? View.VISIBLE : GONE);
}
- public void setMediaKeyboardToggleMode(boolean isSticker) {
- mediaKeyboard.setStickerMode(isSticker);
+ public void setMediaKeyboardToggleMode(@NonNull KeyboardPage page) {
+ mediaKeyboard.setStickerMode(page);
}
public boolean isStickerMode() {
@@ -291,6 +292,10 @@ public class InputPanel extends LinearLayout
return mediaKeyboard;
}
+ public MediaKeyboard.MediaKeyboardListener getMediaKeyboardListener() {
+ return mediaKeyboard;
+ }
+
public void setWallpaperEnabled(boolean enabled) {
if (enabled) {
setBackground(new ColorDrawable(getContext().getResources().getColor(R.color.wallpaper_compose_background)));
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 c43560ce6f..1870ec7a7a 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
@@ -23,6 +23,8 @@ import java.util.List;
/**
* A provider to select emoji in the {@link org.thoughtcrime.securesms.components.emoji.MediaKeyboard}.
+ *
+ * TODO [alex] -- Are we still using any of this?
*/
public class EmojiKeyboardProvider implements MediaKeyboardProvider,
MediaKeyboardProvider.TabIconProvider,
@@ -31,6 +33,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
{
private static final KeyEvent DELETE_KEY_EVENT = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
+ // TODO [alex] -- We are using this.
public static final String RECENT_STORAGE_KEY = "pref_recent_emoji2";
private final Context context;
@@ -146,7 +149,7 @@ public class EmojiKeyboardProvider implements MediaKeyboardProvider,
@Override
public @NonNull Object instantiateItem(@NonNull ViewGroup container, int position) {
- EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener, true);
+ EmojiPageView page = new EmojiPageView(context, emojiSelectionListener, variationSelectorListener, true, null);
page.setModel(pages.get(position));
container.addView(page);
return page;
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 5fe7ba6d73..5971f2198b 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
@@ -6,58 +6,111 @@ 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.recyclerview.widget.GridLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
+import com.annimon.stream.Stream;
+
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter.VariationSelectorListener;
+import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView;
+import org.thoughtcrime.securesms.util.MappingModelList;
public class EmojiPageView extends FrameLayout implements VariationSelectorListener {
private static final String TAG = Log.tag(EmojiPageView.class);
private EmojiPageModel model;
- private EmojiPageViewGridAdapter adapter;
+ private AdapterFactory adapterFactory;
private RecyclerView recyclerView;
- private GridLayoutManager layoutManager;
+ private RecyclerView.LayoutManager layoutManager;
private RecyclerView.OnItemTouchListener scrollDisabler;
private VariationSelectorListener variationSelectorListener;
private EmojiVariationSelectorPopup popup;
+ private boolean searchEnabled;
+ private SpanSizeLookup spanSizeLookup;
public EmojiPageView(@NonNull Context context,
@NonNull EmojiEventListener emojiSelectionListener,
@NonNull VariationSelectorListener variationSelectorListener,
- boolean allowVariations)
+ boolean allowVariations,
+ @Nullable KeyboardPageSearchView.Callbacks searchCallbacks)
+ {
+ this(context, emojiSelectionListener, variationSelectorListener, allowVariations, searchCallbacks, new GridLayoutManager(context, 8), R.layout.emoji_display_item);
+ }
+
+ public EmojiPageView(@NonNull Context context,
+ @NonNull EmojiEventListener emojiSelectionListener,
+ @NonNull VariationSelectorListener variationSelectorListener,
+ boolean allowVariations,
+ @Nullable KeyboardPageSearchView.Callbacks searchCallbacks,
+ @NonNull RecyclerView.LayoutManager layoutManager,
+ @LayoutRes int displayItemLayoutResId)
{
super(context);
final View view = LayoutInflater.from(getContext()).inflate(R.layout.emoji_grid_layout, this, true);
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(popup,
- emojiSelectionListener,
- this,
- allowVariations);
+ this.recyclerView = view.findViewById(R.id.emoji);
+ this.layoutManager = layoutManager;
+ this.scrollDisabler = new ScrollDisabler();
+ this.popup = new EmojiVariationSelectorPopup(context, emojiSelectionListener);
+ this.adapterFactory = () -> new EmojiPageViewGridAdapter(popup,
+ emojiSelectionListener,
+ this,
+ allowVariations,
+ displayItemLayoutResId,
+ searchCallbacks);
+
+ if (layoutManager instanceof GridLayoutManager) {
+ spanSizeLookup = new SpanSizeLookup();
+ ((GridLayoutManager) layoutManager).setSpanSizeLookup(spanSizeLookup);
+ }
recyclerView.setLayoutManager(layoutManager);
- recyclerView.setAdapter(adapter);
+ recyclerView.setItemAnimator(null);
}
public void onSelected() {
- if (model.isDynamic() && adapter != null) {
- adapter.notifyDataSetChanged();
+ if (model.isDynamic() && recyclerView.getAdapter() != null) {
+ recyclerView.getAdapter().notifyDataSetChanged();
}
}
- public void setModel(EmojiPageModel model) {
+ public void setModel(@Nullable EmojiPageModel model) {
this.model = model;
- adapter.setEmoji(model.getDisplayEmoji());
+
+ EmojiPageViewGridAdapter adapter = adapterFactory.create();
+ recyclerView.setAdapter(adapter);
+ adapter.submitList(getMappingModelList());
+ }
+
+ public void bindSearchableAdapter(@Nullable EmojiPageModel model) {
+ this.searchEnabled = true;
+ this.model = model;
+
+ EmojiPageViewGridAdapter adapter = adapterFactory.create();
+ recyclerView.setAdapter(adapter);
+ adapter.submitList(getMappingModelList(), () -> layoutManager.scrollToPosition(1));
+ }
+
+ private @NonNull MappingModelList getMappingModelList() {
+ MappingModelList mappingModels = new MappingModelList();
+
+ if (searchEnabled) {
+ mappingModels.add(new EmojiPageViewGridAdapter.SearchModel());
+ }
+
+ if (model != null) {
+ mappingModels.addAll(Stream.of(model.getDisplayEmoji()).map(EmojiPageViewGridAdapter.EmojiModel::new).toList());
+ }
+
+ return mappingModels;
}
@Override
@@ -69,8 +122,13 @@ 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 idealWidth = getContext().getResources().getDimensionPixelOffset(R.dimen.emoji_drawer_item_width);
+ int spanCount = Math.max(w / idealWidth, 1);
+
+ spanSizeLookup.setSpansPerRow(spanCount);
+ ((GridLayoutManager) layoutManager).setSpanCount(spanCount);
+ }
}
@Override
@@ -102,4 +160,22 @@ public class EmojiPageView extends FrameLayout implements VariationSelectorListe
@Override
public void onRequestDisallowInterceptTouchEvent(boolean b) { }
}
+
+ private class SpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
+
+ private int spansPerRow;
+
+ public void setSpansPerRow(int spansPerRow) {
+ this.spansPerRow = spansPerRow;
+ }
+
+ @Override
+ public int getSpanSize(int position) {
+ return position == 0 && searchEnabled ? spansPerRow : 1;
+ }
+ }
+
+ 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 69d79361fb..f9d8106323 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,95 +1,42 @@
package org.thoughtcrime.securesms.components.emoji;
import android.graphics.drawable.Drawable;
-import android.view.LayoutInflater;
import android.view.View;
-import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.PopupWindow;
+import androidx.annotation.LayoutRes;
import androidx.annotation.NonNull;
-import androidx.recyclerview.widget.RecyclerView;
+import androidx.annotation.Nullable;
+import org.jetbrains.annotations.NotNull;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider.EmojiEventListener;
+import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView;
+import org.thoughtcrime.securesms.util.MappingAdapter;
+import org.thoughtcrime.securesms.util.MappingModel;
+import org.thoughtcrime.securesms.util.MappingViewHolder;
-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 List emojiList;
- private final EmojiVariationSelectorPopup popup;
private final VariationSelectorListener variationSelectorListener;
- private final EmojiEventListener emojiEventListener;
- private final boolean allowVariations;
public EmojiPageViewGridAdapter(@NonNull EmojiVariationSelectorPopup popup,
@NonNull EmojiEventListener emojiEventListener,
@NonNull VariationSelectorListener variationSelectorListener,
- boolean allowVariations)
+ boolean allowVariations,
+ @LayoutRes int displayItemLayoutResId,
+ @Nullable KeyboardPageSearchView.Callbacks callbacks)
{
- this.emojiList = new ArrayList<>();
- this.popup = popup;
- this.emojiEventListener = emojiEventListener;
this.variationSelectorListener = variationSelectorListener;
- this.allowVariations = allowVariations;
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);
-
- final Drawable drawable = EmojiProvider.getEmojiDrawable(viewHolder.imageView.getContext(), 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 (allowVariations && 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(SearchModel.class, new LayoutFactory<>(v -> {
+ ((KeyboardPageSearchView) v).setCallbacks(callbacks);
+ return new SearchViewHolder(v);
+ }, R.layout.emoji_page_view_search));
+ registerFactory(EmojiModel.class, new LayoutFactory<>(v -> new EmojiViewHolder(v, emojiEventListener, variationSelectorListener, popup, allowVariations), displayItemLayoutResId));
}
@Override
@@ -97,18 +44,110 @@ public class EmojiPageViewGridAdapter extends RecyclerView.Adapter {
+ @Override
+ public boolean areItemsTheSame(@NonNull @NotNull SearchModel newItem) {
+ return true;
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull @NotNull SearchModel newItem) {
+ return true;
+ }
+ }
+
+ static class SearchViewHolder extends MappingViewHolder {
+ public SearchViewHolder(@NonNull View itemView) {
+ super(itemView);
+ }
+
+ @Override
+ public void bind(@NonNull @NotNull SearchModel model) {
+ }
+ }
+
+ static class EmojiModel implements MappingModel {
+
+ private final Emoji emoji;
+
+ EmojiModel(@NonNull Emoji emoji) {
+ this.emoji = emoji;
+ }
+
+ @Override
+ public boolean areItemsTheSame(@NonNull @NotNull EmojiModel newItem) {
+ return newItem.emoji.getValue().equals(emoji.getValue());
+ }
+
+ @Override
+ public boolean areContentsTheSame(@NonNull @NotNull 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;
private final AsciiEmojiView textView;
private final ImageView hintCorner;
- public EmojiViewHolder(@NonNull View itemView) {
+ 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);
this.textView = itemView.findViewById(R.id.emoji_text);
this.hintCorner = itemView.findViewById(R.id.emoji_variation_hint);
}
+
+ @Override
+ public void bind(@NonNull @NotNull EmojiModel model) {
+ final Drawable drawable = EmojiProvider.getEmojiDrawable(imageView.getContext(), model.emoji.getValue());
+
+ if (drawable != null) {
+ textView.setVisibility(View.GONE);
+ imageView.setVisibility(View.VISIBLE);
+
+ imageView.setImageDrawable(drawable);
+ } else {
+ textView.setVisibility(View.VISIBLE);
+ imageView.setVisibility(View.GONE);
+
+ textView.setEmoji(model.emoji.getValue());
+ }
+
+ itemView.setOnClickListener(v -> {
+ emojiEventListener.onEmojiSelected(model.emoji.getValue());
+ });
+
+ if (allowVariations && model.emoji.getVariations().size() > 1) {
+ itemView.setOnLongClickListener(v -> {
+ popup.dismiss();
+ popup.setVariations(model.emoji.getVariations());
+ popup.showAsDropDown(itemView, 0, -(2 * itemView.getHeight()));
+ variationSelectorListener.onVariationSelectorStateChanged(true);
+ return true;
+ });
+ hintCorner.setVisibility(View.VISIBLE);
+ } else {
+ itemView.setOnLongClickListener(null);
+ hintCorner.setVisibility(View.GONE);
+ }
+ }
}
public interface VariationSelectorListener {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java
index b0787e0bcc..3a01ff6c04 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/EmojiToggle.java
@@ -9,13 +9,16 @@ import androidx.appcompat.widget.AppCompatImageButton;
import androidx.core.content.ContextCompat;
import org.thoughtcrime.securesms.R;
+import org.thoughtcrime.securesms.keyboard.KeyboardPage;
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider;
+import org.thoughtcrime.securesms.util.ContextUtil;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.MediaKeyboardListener {
private Drawable emojiToggle;
private Drawable stickerToggle;
+ private Drawable gifToggle;
private Drawable mediaToggle;
private Drawable imeToggle;
@@ -45,9 +48,10 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
}
private void initialize() {
- this.emojiToggle = ContextCompat.getDrawable(getContext(), R.drawable.ic_emoji_smiley_24);
- this.stickerToggle = ContextCompat.getDrawable(getContext(), R.drawable.ic_sticker_24);
- this.imeToggle = ContextCompat.getDrawable(getContext(), R.drawable.ic_keyboard_24);
+ this.emojiToggle = ContextUtil.requireDrawable(getContext(), R.drawable.keyboard_pager_fragment_emoji_icon);
+ this.stickerToggle = ContextUtil.requireDrawable(getContext(), R.drawable.keyboard_pager_fragment_sticker_icon);
+ this.gifToggle = ContextUtil.requireDrawable(getContext(), R.drawable.keyboard_pager_fragment_gif_icon);
+ this.imeToggle = ContextUtil.requireDrawable(getContext(), R.drawable.ic_keyboard_24);
this.mediaToggle = emojiToggle;
setToMedia();
@@ -57,8 +61,18 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
drawer.setKeyboardListener(this);
}
- public void setStickerMode(boolean stickerMode) {
- this.mediaToggle = stickerMode ? stickerToggle : emojiToggle;
+ public void setStickerMode(@NonNull KeyboardPage page) {
+ switch (page) {
+ case EMOJI:
+ mediaToggle = emojiToggle;
+ break;
+ case STICKER:
+ mediaToggle = stickerToggle;
+ break;
+ case GIF:
+ mediaToggle = gifToggle;
+ break;
+ }
if (getDrawable() != imeToggle) {
setToMedia();
@@ -78,9 +92,18 @@ public class EmojiToggle extends AppCompatImageButton implements MediaKeyboard.M
}
@Override
- public void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider) {
- setStickerMode(provider instanceof StickerKeyboardProvider);
- TextSecurePreferences.setMediaKeyboardMode(getContext(), (provider instanceof StickerKeyboardProvider) ? TextSecurePreferences.MediaKeyboardMode.STICKER
- : TextSecurePreferences.MediaKeyboardMode.EMOJI);
+ public void onKeyboardChanged(@NonNull KeyboardPage page) {
+ setStickerMode(page);
+ switch (page) {
+ case EMOJI:
+ TextSecurePreferences.setMediaKeyboardMode(getContext(), TextSecurePreferences.MediaKeyboardMode.EMOJI);
+ break;
+ case STICKER:
+ TextSecurePreferences.setMediaKeyboardMode(getContext(), TextSecurePreferences.MediaKeyboardMode.STICKER);
+ break;
+ case GIF:
+ TextSecurePreferences.setMediaKeyboardMode(getContext(), TextSecurePreferences.MediaKeyboardMode.GIF);
+ break;
+ }
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java
index d632deceba..7127b9488b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboard.java
@@ -1,7 +1,6 @@
package org.thoughtcrime.securesms.components.emoji;
import android.content.Context;
-import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.View;
@@ -10,18 +9,20 @@ import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.recyclerview.widget.LinearLayoutManager;
-import androidx.recyclerview.widget.RecyclerView;
+import androidx.fragment.app.Fragment;
+import androidx.fragment.app.FragmentActivity;
+import androidx.fragment.app.FragmentManager;
+import androidx.fragment.app.FragmentTransaction;
import androidx.viewpager.widget.PagerAdapter;
-import androidx.viewpager.widget.ViewPager;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.components.InputAwareLayout.InputView;
-import org.thoughtcrime.securesms.components.RepeatableImageKey;
-import org.thoughtcrime.securesms.mms.GlideApp;
+import org.thoughtcrime.securesms.keyboard.KeyboardPage;
+import org.thoughtcrime.securesms.keyboard.KeyboardPagerFragment;
+import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
-import java.util.Arrays;
+import java.security.Key;
public class MediaKeyboard extends FrameLayout implements InputView,
MediaKeyboardProvider.Presenter,
@@ -29,22 +30,15 @@ public class MediaKeyboard extends FrameLayout implements InputView,
MediaKeyboardBottomTabAdapter.EventListener
{
- private static final String TAG = Log.tag(MediaKeyboard.class);
+ private static final String TAG = Log.tag(MediaKeyboard.class);
+ private static final String EMOJI_SEARCH = "emoji_search_fragment";
- private RecyclerView categoryTabs;
- private ViewPager categoryPager;
- private ViewGroup providerTabs;
- private RepeatableImageKey backspaceButton;
- private RepeatableImageKey backspaceButtonBackup;
- private View searchButton;
- private View addButton;
- @Nullable private MediaKeyboardListener keyboardListener;
- private MediaKeyboardProvider[] providers;
- private int providerIndex;
-
- private final boolean tabsAtBottom;
-
- private MediaKeyboardBottomTabAdapter categoryTabAdapter;
+ @Nullable private MediaKeyboardListener keyboardListener;
+ private boolean isInitialised;
+ private int latestKeyboardHeight;
+ private State keyboardState;
+ private KeyboardPagerFragment keyboardPagerFragment;
+ private FragmentManager fragmentManager;
public MediaKeyboard(Context context) {
this(context, null);
@@ -52,23 +46,6 @@ public class MediaKeyboard extends FrameLayout implements InputView,
public MediaKeyboard(Context context, AttributeSet attrs) {
super(context, attrs);
-
- TypedArray typedArray = context.getTheme().obtainStyledAttributes(attrs, R.styleable.MediaKeyboard, 0, 0);
-
- try {
- tabsAtBottom = typedArray.getInt(R.styleable.MediaKeyboard_tabs_gravity, 0) == 0;
- } finally {
- typedArray.recycle();
- }
- }
-
- public void setProviders(int startIndex, MediaKeyboardProvider... providers) {
- if (!Arrays.equals(this.providers, providers)) {
- this.providers = providers;
- this.providerIndex = startIndex;
-
- requestPresent(providers, providerIndex);
- }
}
public void setKeyboardListener(@Nullable MediaKeyboardListener listener) {
@@ -82,10 +59,12 @@ public class MediaKeyboard extends FrameLayout implements InputView,
@Override
public void show(int height, boolean immediate) {
- if (this.categoryPager == null) initView();
+ if (!isInitialised) initView();
+
+ latestKeyboardHeight = height;
ViewGroup.LayoutParams params = getLayoutParams();
- params.height = height;
+ params.height = (keyboardState == State.NORMAL) ? latestKeyboardHeight : ViewGroup.LayoutParams.WRAP_CONTENT;
Log.i(TAG, "showing emoji drawer with height " + params.height);
setLayoutParams(params);
@@ -93,19 +72,20 @@ public class MediaKeyboard extends FrameLayout implements InputView,
}
public void show() {
- if (this.categoryPager == null) initView();
+ if (!isInitialised) initView();
setVisibility(VISIBLE);
if (keyboardListener != null) keyboardListener.onShown();
-
- requestPresent(providers, providerIndex);
+ keyboardPagerFragment.show();
}
@Override
public void hide(boolean immediate) {
setVisibility(GONE);
+ onCloseEmojiSearchInternal(false);
if (keyboardListener != null) keyboardListener.onHidden();
Log.i(TAG, "hide()");
+ keyboardPagerFragment.hide();
}
@Override
@@ -117,30 +97,29 @@ public class MediaKeyboard extends FrameLayout implements InputView,
@Nullable MediaKeyboardProvider.SearchObserver searchObserver,
int startingIndex)
{
- if (categoryPager == null) return;
- if (!provider.equals(providers[providerIndex])) return;
- if (keyboardListener != null) keyboardListener.onKeyboardProviderChanged(provider);
-
- boolean isSolo = providers.length == 1;
-
- presentProviderStrip(isSolo);
- presentCategoryPager(pagerAdapter, tabIconProvider, startingIndex);
- presentProviderTabs(providers, providerIndex);
- presentSearchButton(searchObserver);
- presentBackspaceButton(backspaceObserver, isSolo);
- presentAddButton(addObserver);
+// if (categoryPager == null) return;
+// if (!provider.equals(providers[providerIndex])) return;
+// if (keyboardListener != null) keyboardListener.onKeyboardChanged(provider);
+//
+// boolean isSolo = providers.length == 1;
+//
+// presentProviderStrip(isSolo);
+// presentCategoryPager(pagerAdapter, tabIconProvider, startingIndex);
+// presentProviderTabs(providers, providerIndex);
+// presentSearchButton(searchObserver);
+// presentBackspaceButton(backspaceObserver, isSolo);
+// presentAddButton(addObserver);
}
@Override
public int getCurrentPosition() {
- return categoryPager != null ? categoryPager.getCurrentItem() : 0;
+// return categoryPager != null ? categoryPager.getCurrentItem() : 0;
+ return 0;
}
@Override
public void requestDismissal() {
hide(true);
- providerIndex = 0;
- if (keyboardListener != null) keyboardListener.onKeyboardProviderChanged(providers[providerIndex]);
}
@Override
@@ -150,148 +129,82 @@ public class MediaKeyboard extends FrameLayout implements InputView,
@Override
public void onTabSelected(int index) {
- if (categoryPager != null) {
- categoryPager.setCurrentItem(index);
- categoryTabs.smoothScrollToPosition(index);
- }
+// if (categoryPager != null) {
+// categoryPager.setCurrentItem(index);
+// categoryTabs.smoothScrollToPosition(index);
+// }
}
@Override
public void setViewPagerEnabled(boolean enabled) {
- if (categoryPager != null) {
- categoryPager.setEnabled(enabled);
+// if (categoryPager != null) {
+// categoryPager.setEnabled(enabled);
+// }
+ }
+
+ public void onCloseEmojiSearch() {
+ onCloseEmojiSearchInternal(true);
+ }
+
+ private void onCloseEmojiSearchInternal(boolean showAfterCommit) {
+ if (keyboardState == State.NORMAL) {
+ return;
}
+
+ keyboardState = State.NORMAL;
+
+ Fragment emojiSearch = fragmentManager.findFragmentByTag(EMOJI_SEARCH);
+ if (emojiSearch == null) {
+ return;
+ }
+
+ FragmentTransaction transaction = fragmentManager.beginTransaction()
+ .remove(emojiSearch)
+ .show(keyboardPagerFragment)
+ .setCustomAnimations(R.anim.fade_in, R.anim.fade_out);
+
+ if (showAfterCommit) {
+ transaction.runOnCommit(() -> show(latestKeyboardHeight, false));
+ }
+
+ transaction.commit();
+ }
+
+ public void onOpenEmojiSearch() {
+ if (keyboardState == State.EMOJI_SEARCH) {
+ return;
+ }
+
+ keyboardState = State.EMOJI_SEARCH;
+
+ fragmentManager.beginTransaction()
+ .hide(keyboardPagerFragment)
+ .add(R.id.media_keyboard_fragment_container, new EmojiSearchFragment(), EMOJI_SEARCH)
+ .runOnCommit(() -> show(latestKeyboardHeight, true))
+ .setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
+ .commit();
}
private void initView() {
- final View view = LayoutInflater.from(getContext()).inflate(R.layout.media_keyboard, this, true);
+ if (!isInitialised) {
+ LayoutInflater.from(getContext()).inflate(R.layout.media_keyboard, this, true);
- RecyclerView categoryTabsTop = view.findViewById(R.id.media_keyboard_tabs_top);
- RecyclerView categoryTabsBottom = view.findViewById(R.id.media_keyboard_tabs);
-
- this.categoryTabs = tabsAtBottom ? categoryTabsBottom : categoryTabsTop;
- this.categoryPager = view.findViewById(R.id.media_keyboard_pager);
- this.providerTabs = view.findViewById(R.id.media_keyboard_provider_tabs);
- this.backspaceButton = view.findViewById(R.id.media_keyboard_backspace);
- this.backspaceButtonBackup = view.findViewById(R.id.media_keyboard_backspace_backup);
- this.searchButton = view.findViewById(R.id.media_keyboard_search);
- this.addButton = view.findViewById(R.id.media_keyboard_add);
-
- this.categoryTabAdapter = new MediaKeyboardBottomTabAdapter(GlideApp.with(this), this, tabsAtBottom);
-
- categoryTabs.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
- categoryTabs.setAdapter(categoryTabAdapter);
- categoryTabs.setVisibility(VISIBLE);
- }
-
- private void requestPresent(@NonNull MediaKeyboardProvider[] providers, int newIndex) {
- providers[providerIndex].setController(null);
- providerIndex = newIndex;
-
- providers[providerIndex].setController(this);
- providers[providerIndex].requestPresentation(this, providers.length == 1);
- }
-
-
- private void presentCategoryPager(@NonNull PagerAdapter pagerAdapter,
- @NonNull MediaKeyboardProvider.TabIconProvider iconProvider,
- int startingIndex) {
- if (categoryPager.getAdapter() != pagerAdapter) {
- categoryPager.setAdapter(pagerAdapter);
+ keyboardState = State.NORMAL;
+ latestKeyboardHeight = -1;
+ isInitialised = true;
+ fragmentManager = ((FragmentActivity) getContext()).getSupportFragmentManager();
+ keyboardPagerFragment = (KeyboardPagerFragment) fragmentManager.findFragmentById(R.id.media_keyboard_fragment_container);
}
-
- categoryPager.setCurrentItem(startingIndex);
-
- categoryPager.clearOnPageChangeListeners();
- categoryPager.addOnPageChangeListener(new ViewPager.OnPageChangeListener() {
- @Override
- public void onPageScrolled(int i, float v, int i1) {
- }
-
- @Override
- public void onPageSelected(int i) {
- categoryTabAdapter.setActivePosition(i);
- categoryTabs.smoothScrollToPosition(i);
- providers[providerIndex].setCurrentPosition(i);
- }
-
- @Override
- public void onPageScrollStateChanged(int i) {
- }
- });
-
- categoryTabAdapter.setTabIconProvider(iconProvider, pagerAdapter.getCount());
- categoryTabAdapter.setActivePosition(startingIndex);
- }
-
- private void presentProviderTabs(@NonNull MediaKeyboardProvider[] providers, int selected) {
- providerTabs.removeAllViews();
-
- LayoutInflater inflater = LayoutInflater.from(getContext());
-
- for (int i = 0; i < providers.length; i++) {
- MediaKeyboardProvider provider = providers[i];
- View view = inflater.inflate(provider.getProviderIconView(i == selected), providerTabs, false);
-
- view.setTag(provider);
-
- final int index = i;
- view.setOnClickListener(v -> {
- requestPresent(providers, index);
- });
-
- providerTabs.addView(view);
- }
- }
-
- private void presentBackspaceButton(@Nullable MediaKeyboardProvider.BackspaceObserver backspaceObserver,
- boolean useBackupPosition)
- {
- if (backspaceObserver != null) {
- if (useBackupPosition) {
- backspaceButton.setVisibility(INVISIBLE);
- backspaceButton.setOnKeyEventListener(null);
- backspaceButtonBackup.setVisibility(VISIBLE);
- backspaceButtonBackup.setOnKeyEventListener(backspaceObserver::onBackspaceClicked);
- } else {
- backspaceButton.setVisibility(VISIBLE);
- backspaceButton.setOnKeyEventListener(backspaceObserver::onBackspaceClicked);
- backspaceButtonBackup.setVisibility(GONE);
- backspaceButtonBackup.setOnKeyEventListener(null);
- }
- } else {
- backspaceButton.setVisibility(INVISIBLE);
- backspaceButton.setOnKeyEventListener(null);
- backspaceButtonBackup.setVisibility(GONE);
- backspaceButton.setOnKeyEventListener(null);
- }
- }
-
- private void presentAddButton(@Nullable MediaKeyboardProvider.AddObserver addObserver) {
- if (addObserver != null) {
- addButton.setVisibility(VISIBLE);
- addButton.setOnClickListener(v -> addObserver.onAddClicked());
- } else {
- addButton.setVisibility(GONE);
- addButton.setOnClickListener(null);
- }
- }
-
- private void presentSearchButton(@Nullable MediaKeyboardProvider.SearchObserver searchObserver) {
- searchButton.setVisibility(searchObserver != null ? VISIBLE : INVISIBLE);
- }
-
- private void presentProviderStrip(boolean isSolo) {
- int visibility = isSolo ? View.GONE : View.VISIBLE;
-
- searchButton.setVisibility(visibility);
- backspaceButton.setVisibility(visibility);
- providerTabs.setVisibility(visibility);
}
public interface MediaKeyboardListener {
void onShown();
void onHidden();
- void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider);
+ void onKeyboardChanged(@NonNull KeyboardPage page);
+ }
+
+ private enum State {
+ NORMAL,
+ EMOJI_SEARCH
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboardBottomTabAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboardBottomTabAdapter.java
index 92176b3207..b2f5f962b4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboardBottomTabAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/MediaKeyboardBottomTabAdapter.java
@@ -16,22 +16,19 @@ public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter eventListener.onTabSelected(index));
}
@@ -98,7 +87,7 @@ public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter recentlyUsed;
+ public static boolean hasRecents(Context context, @NonNull String preferenceName) {
+ return PreferenceManager.getDefaultSharedPreferences(context).contains(preferenceName);
+ }
+
public RecentEmojiPageModel(Context context, @NonNull String preferenceName) {
this.prefs = PreferenceManager.getDefaultSharedPreferences(context);
this.preferenceName = preferenceName;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/appearance/AppearanceSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/appearance/AppearanceSettingsViewModel.kt
index 4411e1e925..d976f7283b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/appearance/AppearanceSettingsViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/appearance/AppearanceSettingsViewModel.kt
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.components.settings.app.appearance
import androidx.lifecycle.LiveData
import androidx.lifecycle.ViewModel
+import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.livedata.Store
@@ -28,6 +29,7 @@ class AppearanceSettingsViewModel : ViewModel() {
fun setLanguage(language: String) {
store.update { it.copy(language = language) }
SignalStore.settings().language = language
+ EmojiSearchIndexDownloadJob.scheduleImmediately()
}
fun setMessageFontSize(size: Int) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
index 0b43c8035b..1b08c5fc6b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationActivity.java
@@ -184,6 +184,11 @@ import org.thoughtcrime.securesms.jobs.GroupV2UpdateSelfProfileKeyJob;
import org.thoughtcrime.securesms.jobs.RequestGroupV2InfoJob;
import org.thoughtcrime.securesms.jobs.RetrieveProfileJob;
import org.thoughtcrime.securesms.jobs.ServiceOutageDetectionJob;
+import org.thoughtcrime.securesms.keyboard.KeyboardPage;
+import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel;
+import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment;
+import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
+import org.thoughtcrime.securesms.keyboard.gif.GifKeyboardPageFragment;
import org.thoughtcrime.securesms.keyvalue.PaymentsValues;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
@@ -319,7 +324,12 @@ public class ConversationActivity extends PassphraseRequiredActivity
ConversationReactionOverlay.OnReactionSelectedListener,
ReactWithAnyEmojiBottomSheetDialogFragment.Callback,
SafetyNumberChangeDialog.Callback,
- ReactionsBottomSheetDialogFragment.Callback
+ ReactionsBottomSheetDialogFragment.Callback,
+ MediaKeyboard.MediaKeyboardListener,
+ EmojiKeyboardProvider.EmojiEventListener,
+ GifKeyboardPageFragment.Host,
+ EmojiKeyboardPageFragment.Callback,
+ EmojiSearchFragment.Callback
{
private static final int SHORTCUT_ICON_SIZE = Build.VERSION.SDK_INT >= 26 ? ViewUtil.dpToPx(72) : ViewUtil.dpToPx(48 + 16 * 2);
@@ -337,7 +347,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private static final int TAKE_PHOTO = 7;
private static final int ADD_CONTACT = 8;
private static final int PICK_LOCATION = 9;
- private static final int PICK_GIF = 10;
+ public static final int PICK_GIF = 10;
private static final int SMS_DEFAULT = 11;
private static final int MEDIA_SENDER = 12;
@@ -687,12 +697,9 @@ public class ConversationActivity extends PassphraseRequiredActivity
attachmentManager.setLocation(place, getCurrentMediaConstraints());
break;
case PICK_GIF:
- setMedia(data.getData(),
- Objects.requireNonNull(MediaType.from(BlobProvider.getMimeType(data.getData()))),
- data.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0),
- data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0),
- false,
- true);
+ onGifSelectSuccess(data.getData(),
+ data.getIntExtra(GiphyActivity.EXTRA_WIDTH, 0),
+ data.getIntExtra(GiphyActivity.EXTRA_HEIGHT, 0));
break;
case SMS_DEFAULT:
initializeSecurity(isSecureText, isDefaultSms);
@@ -1097,7 +1104,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
AttachmentManager.selectGallery(this, MEDIA_SENDER, recipient.get(), composeText.getTextTrimmed(), sendButton.getSelectedTransport());
break;
case GIF:
- AttachmentManager.selectGif(this, PICK_GIF, !isSecureText, recipient.get().getChatColors().asSingleColor());
+ AttachmentManager.selectGif(this, PICK_GIF, !isSecureText);
break;
case FILE:
AttachmentManager.selectDocument(this, PICK_DOCUMENT);
@@ -2156,12 +2163,22 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (stickersAvailable) {
inputPanel.showMediaKeyboardToggle(true);
- inputPanel.setMediaKeyboardToggleMode(isSystemEmojiPreferred || keyboardMode == MediaKeyboardMode.STICKER);
+ switch (keyboardMode) {
+ case EMOJI:
+ inputPanel.setMediaKeyboardToggleMode(isSystemEmojiPreferred ? KeyboardPage.STICKER : KeyboardPage.EMOJI);
+ break;
+ case STICKER:
+ inputPanel.setMediaKeyboardToggleMode(KeyboardPage.STICKER);
+ break;
+ case GIF:
+ inputPanel.setMediaKeyboardToggleMode(KeyboardPage.GIF);
+ break;
+ }
if (stickerIntro) showStickerIntroductionTooltip();
}
if (emojiDrawerStub.resolved()) {
- initializeMediaKeyboardProviders(emojiDrawerStub.get(), stickersAvailable);
+ initializeMediaKeyboardProviders();
}
});
}
@@ -2258,7 +2275,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
private void showStickerIntroductionTooltip() {
TextSecurePreferences.setMediaKeyboardMode(this, MediaKeyboardMode.STICKER);
- inputPanel.setMediaKeyboardToggleMode(true);
+ inputPanel.setMediaKeyboardToggleMode(KeyboardPage.STICKER);
TooltipPopup.forTarget(inputPanel.getMediaKeyboardToggleAnchorView())
.setBackgroundTint(getResources().getColor(R.color.core_ultramarine))
@@ -2607,22 +2624,19 @@ public class ConversationActivity extends PassphraseRequiredActivity
}
}
- private void initializeMediaKeyboardProviders(@NonNull MediaKeyboard mediaKeyboard, boolean stickersAvailable) {
- boolean isSystemEmojiPreferred = SignalStore.settings().isPreferSystemEmoji();
+ private void initializeMediaKeyboardProviders() {
+ KeyboardPagerViewModel keyboardPagerViewModel = ViewModelProviders.of(this).get(KeyboardPagerViewModel.class);
- if (stickersAvailable) {
- if (isSystemEmojiPreferred) {
- mediaKeyboard.setProviders(0, new StickerKeyboardProvider(this, this));
- } else {
- MediaKeyboardMode keyboardMode = TextSecurePreferences.getMediaKeyboardMode(this);
- int index = keyboardMode == MediaKeyboardMode.STICKER ? 1 : 0;
-
- mediaKeyboard.setProviders(index,
- new EmojiKeyboardProvider(this, inputPanel),
- new StickerKeyboardProvider(this, this));
- }
- } else if (!isSystemEmojiPreferred) {
- mediaKeyboard.setProviders(0, new EmojiKeyboardProvider(this, inputPanel));
+ switch (TextSecurePreferences.getMediaKeyboardMode(this)) {
+ case EMOJI:
+ keyboardPagerViewModel.switchToPage(KeyboardPage.EMOJI);
+ break;
+ case STICKER:
+ keyboardPagerViewModel.switchToPage(KeyboardPage.STICKER);
+ break;
+ case GIF:
+ keyboardPagerViewModel.switchToPage(KeyboardPage.GIF);
+ break;
}
}
@@ -3105,7 +3119,7 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (!emojiDrawerStub.resolved()) {
Boolean stickersAvailable = stickerViewModel.getStickersAvailability().getValue();
- initializeMediaKeyboardProviders(emojiDrawerStub.get(), stickersAvailable == null ? false : stickersAvailable);
+ initializeMediaKeyboardProviders();
inputPanel.setMediaKeyboard(emojiDrawerStub.get());
}
@@ -3196,6 +3210,69 @@ public class ConversationActivity extends PassphraseRequiredActivity
reactionDelegate.hideMask();
}
+ @Override
+ public void onShown() {
+ if (inputPanel != null) {
+ inputPanel.getMediaKeyboardListener().onShown();
+ }
+ }
+
+ @Override
+ public void onHidden() {
+ if (inputPanel != null) {
+ inputPanel.getMediaKeyboardListener().onHidden();
+ }
+ }
+
+ @Override
+ public void onKeyboardChanged(@NonNull KeyboardPage page) {
+ if (inputPanel != null) {
+ inputPanel.getMediaKeyboardListener().onKeyboardChanged(page);
+ }
+ }
+
+ @Override
+ public void onEmojiSelected(String emoji) {
+ if (inputPanel != null) {
+ inputPanel.onEmojiSelected(emoji);
+ }
+ }
+
+ @Override
+ public void onKeyEvent(KeyEvent keyEvent) {
+ if (keyEvent != null) {
+ inputPanel.onKeyEvent(keyEvent);
+ }
+ }
+
+ @Override
+ public void onGifSelectSuccess(@NonNull Uri blobUri, int width, int height) {
+ setMedia(blobUri,
+ Objects.requireNonNull(MediaType.from(BlobProvider.getMimeType(blobUri))),
+ width,
+ height,
+ false,
+ true);
+ }
+
+ @Override
+ public boolean isMms() {
+ return !isSecureText;
+ }
+
+ @Override
+ public void openEmojiSearch() {
+ if (emojiDrawerStub.resolved()) {
+ emojiDrawerStub.get().onOpenEmojiSearch();
+ }
+ }
+
+ @Override public void closeEmojiSearch() {
+ if (emojiDrawerStub.resolved()) {
+ emojiDrawerStub.get().onCloseEmojiSearch();
+ }
+ }
+
// Listeners
private class QuickCameraToggleListener implements OnClickListener {
@@ -3289,7 +3366,11 @@ public class ConversationActivity extends PassphraseRequiredActivity
public void onTextChanged(CharSequence s, int start, int before,int count) {}
@Override
- public void onFocusChange(View v, boolean hasFocus) {}
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus && container.getCurrentInput() == emojiDrawerStub.get()) {
+ container.showSoftkey(composeText);
+ }
+ }
}
private class TypingStatusTextWatcher extends SimpleTextWatcher {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/Colorizer.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/Colorizer.kt
index 6761593c98..b37683e67f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/Colorizer.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/colors/Colorizer.kt
@@ -9,7 +9,6 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import org.thoughtcrime.securesms.R
-import org.thoughtcrime.securesms.conversation.colors.ui.ChatColorPreviewView
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.recipients.RecipientId
import org.thoughtcrime.securesms.util.Projection
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java
index f23312f30c..5295f27d1f 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/DatabaseFactory.java
@@ -60,11 +60,12 @@ public class DatabaseFactory {
private final SessionDatabase sessionDatabase;
private final SearchDatabase searchDatabase;
private final StickerDatabase stickerDatabase;
- private final UnknownStorageIdDatabase storageIdDatabase ;
+ private final UnknownStorageIdDatabase storageIdDatabase;
private final RemappedRecordsDatabase remappedRecordsDatabase;
private final MentionDatabase mentionDatabase;
private final PaymentDatabase paymentDatabase;
private final ChatColorsDatabase chatColorsDatabase;
+ private final EmojiSearchDatabase emojiSearchDatabase;
public static DatabaseFactory getInstance(Context context) {
if (instance == null) {
@@ -171,6 +172,10 @@ public class DatabaseFactory {
return getInstance(context).paymentDatabase;
}
+ public static EmojiSearchDatabase getEmojiSearchDatabase(Context context) {
+ return getInstance(context).emojiSearchDatabase;
+ }
+
public static SQLiteDatabase getBackupDatabase(Context context) {
return getInstance(context).databaseHelper.getReadableDatabase().getSqlCipherDatabase();
}
@@ -229,6 +234,7 @@ public class DatabaseFactory {
this.mentionDatabase = new MentionDatabase(context, databaseHelper);
this.paymentDatabase = new PaymentDatabase(context, databaseHelper);
this.chatColorsDatabase = new ChatColorsDatabase(context, databaseHelper);
+ this.emojiSearchDatabase = new EmojiSearchDatabase(context, databaseHelper);
}
public void onApplicationLevelUpgrade(@NonNull Context context, @NonNull MasterSecret masterSecret,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/EmojiSearchDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/EmojiSearchDatabase.java
new file mode 100644
index 0000000000..5afbaf463b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/EmojiSearchDatabase.java
@@ -0,0 +1,88 @@
+package org.thoughtcrime.securesms.database;
+
+import android.content.ContentValues;
+import android.content.Context;
+import android.database.Cursor;
+import android.text.TextUtils;
+
+import androidx.annotation.NonNull;
+
+import com.annimon.stream.Stream;
+
+import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
+import org.thoughtcrime.securesms.database.model.EmojiSearchData;
+import org.thoughtcrime.securesms.util.CursorUtil;
+import org.thoughtcrime.securesms.util.FtsUtil;
+import org.thoughtcrime.securesms.util.SqlUtil;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Contains all info necessary for full-text search of emoji tags.
+ */
+public class EmojiSearchDatabase extends Database {
+
+ public static final String TABLE_NAME = "emoji_search";
+
+ public static final String LABEL = "label";
+ public static final String EMOJI = "emoji";
+
+ public static final String CREATE_TABLE = "CREATE VIRTUAL TABLE " + TABLE_NAME + " USING fts5(" + LABEL + ", " + EMOJI + " UNINDEXED)";
+
+ public EmojiSearchDatabase(@NonNull Context context, @NonNull SQLCipherOpenHelper databaseHelper) {
+ super(context, databaseHelper);
+ }
+
+ /**
+ * @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.
+ */
+ public @NonNull List query(@NonNull String query, int limit) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+ String matchString = FtsUtil.createPrefixMatchString(query);
+ List results = new LinkedList<>();
+
+ if (TextUtils.isEmpty(matchString)) {
+ return results;
+ }
+
+ String[] projection = new String[] { EMOJI };
+ String selection = LABEL + " MATCH (?)";
+ String[] args = SqlUtil.buildArgs(matchString);
+
+ try (Cursor cursor = db.query(true, TABLE_NAME, projection, selection, args, null, null,"rank", String.valueOf(limit))) {
+ while (cursor.moveToNext()) {
+ results.add(CursorUtil.requireString(cursor, EMOJI));
+ }
+ }
+
+ return results;
+ }
+
+ /**
+ * Deletes the content of the current search index and replaces it with the new one.
+ */
+ public void setSearchIndex(@NonNull List searchIndex) {
+ SQLiteDatabase db = databaseHelper.getReadableDatabase();
+
+ db.beginTransaction();
+ try {
+ db.delete(TABLE_NAME, null, null);
+
+ for (EmojiSearchData searchData : searchIndex) {
+ for (String label : searchData.getTags()) {
+ ContentValues values = new ContentValues(2);
+ values.put(LABEL, label);
+ values.put(EMOJI, searchData.getEmoji());
+ db.insert(TABLE_NAME, null, values);
+ }
+ }
+ db.setTransactionSuccessful();
+ } finally {
+ db.endTransaction();
+ }
+ }
+}
+
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 59c58ded0c..45a7371eae 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
@@ -32,6 +32,7 @@ import org.thoughtcrime.securesms.crypto.MasterSecret;
import org.thoughtcrime.securesms.database.AttachmentDatabase;
import org.thoughtcrime.securesms.database.ChatColorsDatabase;
import org.thoughtcrime.securesms.database.DraftDatabase;
+import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
import org.thoughtcrime.securesms.database.GroupDatabase;
import org.thoughtcrime.securesms.database.GroupReceiptDatabase;
import org.thoughtcrime.securesms.database.IdentityDatabase;
@@ -190,8 +191,9 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
private static final int SERVER_GUID = 99;
private static final int CHAT_COLORS = 100;
private static final int AVATAR_COLORS = 101;
+ private static final int EMOJI_SEARCH = 102;
- private static final int DATABASE_VERSION = 101;
+ private static final int DATABASE_VERSION = 102;
private static final String DATABASE_NAME = "signal.db";
private final Context context;
@@ -224,6 +226,7 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
db.execSQL(MentionDatabase.CREATE_TABLE);
db.execSQL(PaymentDatabase.CREATE_TABLE);
db.execSQL(ChatColorsDatabase.CREATE_TABLE);
+ db.execSQL(EmojiSearchDatabase.CREATE_TABLE);
executeStatements(db, SearchDatabase.CREATE_TABLE);
executeStatements(db, RemappedRecordsDatabase.CREATE_TABLE);
@@ -1506,6 +1509,10 @@ public class SQLCipherOpenHelper extends SQLiteOpenHelper implements SignalDatab
}
}
+ if (oldVersion < EMOJI_SEARCH) {
+ db.execSQL("CREATE VIRTUAL TABLE emoji_search USING fts5(label, emoji UNINDEXED)");
+ }
+
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/emoji/EmojiFiles.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiFiles.kt
index f05e07812b..530853930a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiFiles.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiFiles.kt
@@ -111,7 +111,7 @@ object EmojiFiles {
}
@JvmStatic
- fun getLatestEmojiData(context: Context, version: Version): EmojiData? {
+ fun getLatestEmojiData(context: Context, version: Version): ParsedEmojiData? {
val names = NameCollection.read(context, version)
val dataUuid = names.getUUIDForEmojiData() ?: return null
val file = version.getFile(context, dataUuid)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt
index c790feed43..a899488ed1 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/emoji/EmojiSource.kt
@@ -92,7 +92,12 @@ class EmojiSource(
val context = ApplicationDependencies.getApplication()
val version = EmojiFiles.Version.readVersion(context) ?: return null
- val emojiData = EmojiFiles.getLatestEmojiData(context, version)
+ val emojiData = EmojiFiles.getLatestEmojiData(context, version)?.let {
+ it.copy(
+ displayPages = it.displayPages + PAGE_EMOTICONS,
+ dataPages = it.dataPages + PAGE_EMOTICONS
+ )
+ }
val density = ScreenDensity.xhdpiRelativeDensityScaleFactor(version.density)
return emojiData?.let {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Fragment.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Fragment.java
index daabada97f..5a0c80d602 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Fragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4Fragment.java
@@ -67,5 +67,6 @@ public class GiphyMp4Fragment extends Fragment {
adapter.submitList(images, progressBar::hide);
});
viewModel.getPagingController().observe(getViewLifecycleOwner(), adapter::setPagingController);
+ viewModel.getPagedData().observe(getViewLifecycleOwner(), unused -> recycler.scrollToPosition(0));
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java
index 5a684ef0cc..c2935f8a1e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewHolder.java
@@ -23,12 +23,15 @@ import org.thoughtcrime.securesms.giph.model.GiphyImage;
import org.thoughtcrime.securesms.mms.GlideApp;
import org.thoughtcrime.securesms.util.Projection;
import org.thoughtcrime.securesms.util.Util;
+import org.thoughtcrime.securesms.util.ViewUtil;
/**
* Holds a view which will either play back an MP4 gif or show its still.
*/
final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder implements GiphyMp4Playable {
+ private static final Projection.Corners CORNERS = new Projection.Corners(ViewUtil.dpToPx(8));
+
private final AspectRatioFrameLayout container;
private final ImageView stillImage;
private final GiphyMp4Adapter.Callback listener;
@@ -43,7 +46,7 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder implements GiphyM
@NonNull GiphyMp4MediaSourceFactory mediaSourceFactory)
{
super(itemView);
- this.container = (AspectRatioFrameLayout) itemView;
+ this.container = itemView.findViewById(R.id.container);
this.listener = listener;
this.stillImage = itemView.findViewById(R.id.still_image);
this.placeholder = new ColorDrawable(Util.getRandomElement(ChatColorsPalette.Names.getAll()).getColor(itemView.getContext()));
@@ -57,7 +60,6 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder implements GiphyM
mediaSource = mediaSourceFactory.create(Uri.parse(giphyImage.getMp4PreviewUrl()));
container.setAspectRatio(aspectRatio);
- container.setBackground(placeholder);
loadPlaceholderImage(giphyImage);
@@ -81,7 +83,7 @@ final class GiphyMp4ViewHolder extends RecyclerView.ViewHolder implements GiphyM
@Override
public @NonNull Projection getProjection(@NonNull ViewGroup recyclerView) {
- return Projection.relativeToParent(recyclerView, itemView, null);
+ return Projection.relativeToParent(recyclerView, container, CORNERS);
}
@Override
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java
index 9aa0142955..879ef3173d 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ViewModel.java
@@ -52,6 +52,10 @@ public final class GiphyMp4ViewModel extends ViewModel {
.toList()));
}
+ LiveData> getPagedData() {
+ return pagedData;
+ }
+
public void updateSearchQuery(@Nullable String query) {
if (!Objects.equals(query, this.query)) {
this.query = query;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java
index 185e6000ff..ed76d432b5 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/giph/ui/GiphyActivity.java
@@ -5,10 +5,8 @@ import android.content.Intent;
import android.os.Bundle;
import android.widget.Toast;
-import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AlertDialog;
-import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProviders;
@@ -17,21 +15,19 @@ import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Fragment;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4SaveResult;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ViewModel;
-import org.thoughtcrime.securesms.util.DynamicDarkToolbarTheme;
-import org.thoughtcrime.securesms.util.DynamicLanguage;
+import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView;
+import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
import org.thoughtcrime.securesms.util.DynamicTheme;
-import org.thoughtcrime.securesms.util.WindowUtil;
+import org.thoughtcrime.securesms.util.ViewUtil;
import org.thoughtcrime.securesms.util.views.SimpleProgressDialog;
-public class GiphyActivity extends PassphraseRequiredActivity implements GiphyActivityToolbar.OnFilterChangedListener {
+public class GiphyActivity extends PassphraseRequiredActivity implements KeyboardPageSearchView.Callbacks {
- public static final String EXTRA_IS_MMS = "extra_is_mms";
- public static final String EXTRA_WIDTH = "extra_width";
- public static final String EXTRA_HEIGHT = "extra_height";
- public static final String EXTRA_COLOR = "extra_color";
+ public static final String EXTRA_IS_MMS = "extra_is_mms";
+ public static final String EXTRA_WIDTH = "extra_width";
+ public static final String EXTRA_HEIGHT = "extra_height";
- private final DynamicTheme dynamicTheme = new DynamicDarkToolbarTheme();
- private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
+ private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
private GiphyMp4ViewModel giphyMp4ViewModel;
private AlertDialog progressDialog;
@@ -39,7 +35,6 @@ public class GiphyActivity extends PassphraseRequiredActivity implements GiphyAc
@Override
public void onPreCreate() {
dynamicTheme.onCreate(this);
- dynamicLanguage.onCreate(this);
}
@Override
@@ -60,17 +55,10 @@ public class GiphyActivity extends PassphraseRequiredActivity implements GiphyAc
}
private void initializeToolbar() {
- GiphyActivityToolbar toolbar = findViewById(R.id.giphy_toolbar);
- toolbar.setOnFilterChangedListener(this);
-
- final int conversationColor = getConversationColor();
- toolbar.setBackgroundColor(conversationColor);
- WindowUtil.setStatusBarColor(getWindow(), conversationColor);
-
- setSupportActionBar(toolbar);
-
- getSupportActionBar().setDisplayHomeAsUpEnabled(false);
- getSupportActionBar().setDisplayShowTitleEnabled(false);
+ KeyboardPageSearchView searchView = findViewById(R.id.giphy_search_text);
+ searchView.setCallbacks(this);
+ searchView.enableBackNavigation();
+ ViewUtil.focusAndShowKeyboard(searchView);
}
private void handleGiphyMp4SaveResult(@NonNull GiphyMp4SaveResult result) {
@@ -105,12 +93,23 @@ public class GiphyActivity extends PassphraseRequiredActivity implements GiphyAc
Toast.makeText(this, R.string.GiphyActivity_error_while_retrieving_full_resolution_gif, Toast.LENGTH_LONG).show();
}
- private @ColorInt int getConversationColor() {
- return getIntent().getIntExtra(EXTRA_COLOR, ActivityCompat.getColor(this, R.color.core_ultramarine));
+ @Override
+ public void onQueryChanged(@NonNull String query) {
+ giphyMp4ViewModel.updateSearchQuery(query);
}
@Override
- public void onFilterChanged(String filter) {
- giphyMp4ViewModel.updateSearchQuery(filter);
+ public void onNavigationClicked() {
+ ViewUtil.hideKeyboard(this, findViewById(android.R.id.content));
+ finish();
}
+
+ @Override
+ public void onFocusLost() {}
+
+ @Override
+ public void onFocusGained() {}
+
+ @Override
+ public void onClicked() {}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/DownloadLatestEmojiDataJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/DownloadLatestEmojiDataJob.java
index f2682cfa12..cba3a8dae3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/DownloadLatestEmojiDataJob.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/DownloadLatestEmojiDataJob.java
@@ -39,7 +39,6 @@ import java.util.regex.Pattern;
import okhttp3.Response;
import okhttp3.ResponseBody;
-import okio.HashingSink;
import okio.Okio;
import okio.Sink;
import okio.Source;
@@ -64,7 +63,7 @@ public class DownloadLatestEmojiDataJob extends BaseJob {
private EmojiFiles.Version targetVersion;
public static void scheduleIfNecessary(@NonNull Context context) {
- long nextScheduledCheck = SignalStore.emojiValues().getNextScheduledCheck();
+ long nextScheduledCheck = SignalStore.emojiValues().getNextScheduledImageCheck();
if (nextScheduledCheck <= System.currentTimeMillis()) {
Log.i(TAG, "Scheduling DownloadLatestEmojiDataJob.");
@@ -79,7 +78,7 @@ public class DownloadLatestEmojiDataJob extends BaseJob {
interval = INTERVAL_WITHOUT_REMOTE_DOWNLOAD;
}
- SignalStore.emojiValues().setNextScheduledCheck(System.currentTimeMillis() + interval);
+ SignalStore.emojiValues().setNextScheduledImageCheck(System.currentTimeMillis() + interval);
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/EmojiSearchIndexDownloadJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/EmojiSearchIndexDownloadJob.java
new file mode 100644
index 0000000000..6ef575cffb
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/EmojiSearchIndexDownloadJob.java
@@ -0,0 +1,217 @@
+package org.thoughtcrime.securesms.jobs;
+
+import androidx.annotation.NonNull;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import org.signal.core.util.logging.Log;
+import org.thoughtcrime.securesms.components.emoji.Emoji;
+import org.thoughtcrime.securesms.database.DatabaseFactory;
+import org.thoughtcrime.securesms.database.EmojiSearchDatabase;
+import org.thoughtcrime.securesms.database.model.EmojiSearchData;
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
+import org.thoughtcrime.securesms.jobmanager.Data;
+import org.thoughtcrime.securesms.jobmanager.Job;
+import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
+import org.thoughtcrime.securesms.keyvalue.EmojiValues;
+import org.thoughtcrime.securesms.keyvalue.SignalStore;
+import org.thoughtcrime.securesms.util.dynamiclanguage.DynamicLanguageContextWrapper;
+import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
+import org.whispersystems.signalservice.internal.util.JsonUtil;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import javax.annotation.Nullable;
+
+import okhttp3.Call;
+import okhttp3.OkHttpClient;
+import okhttp3.Request;
+import okhttp3.Response;
+
+/**
+ * Downloads a new emoji search index based on our current version and language, if needed.
+ */
+public final class EmojiSearchIndexDownloadJob extends BaseJob {
+
+ private static final String TAG = Log.tag(EmojiSearchIndexDownloadJob.class);
+
+ public static final String KEY = "EmojiSearchIndexDownloadJob";
+
+ private static final long INTERVAL_WITHOUT_INDEX = TimeUnit.DAYS.toMillis(1);
+ private static final long INTERVAL_WITH_INDEX = TimeUnit.DAYS.toMillis(7);
+
+ private EmojiSearchIndexDownloadJob() {
+ this(new Parameters.Builder()
+ .setQueue("EmojiSearchIndexDownloadJob")
+ .setMaxInstancesForFactory(2)
+ .addConstraint(NetworkConstraint.KEY)
+ .setLifespan(TimeUnit.DAYS.toMillis(1))
+ .setMaxAttempts(Parameters.UNLIMITED)
+ .build());
+ }
+
+ private EmojiSearchIndexDownloadJob(@NonNull Parameters parameters) {
+ super(parameters);
+ }
+
+ public static void scheduleImmediately() {
+ ApplicationDependencies.getJobManager().add(new EmojiSearchIndexDownloadJob());
+ }
+
+ public static void scheduleIfNecessary() {
+ long timeSinceCheck = System.currentTimeMillis() - SignalStore.emojiValues().getLastSearchIndexCheck();
+ boolean needsCheck = false;
+
+ if (SignalStore.emojiValues().hasSearchIndex()) {
+ needsCheck = timeSinceCheck > INTERVAL_WITH_INDEX;
+ } else {
+ needsCheck = timeSinceCheck > INTERVAL_WITHOUT_INDEX;
+ }
+
+ if (needsCheck) {
+ Log.i(TAG, "Need to check. It's been " + timeSinceCheck + " ms since the last check.");
+ scheduleImmediately();
+ } else {
+ Log.d(TAG, "Do not need to check. It's been " + timeSinceCheck + " ms since the last check.");
+ }
+ }
+
+ @Override
+ public @NonNull Data serialize() {
+ return Data.EMPTY;
+ }
+
+ @Override
+ public @NonNull String getFactoryKey() {
+ return KEY;
+ }
+
+ @Override
+ protected void onRun() throws Exception {
+ OkHttpClient client = ApplicationDependencies.getOkHttpClient();
+
+ Manifest manifest = downloadManifest(client);
+
+ Locale locale = DynamicLanguageContextWrapper.getUsersSelectedLocale(context);
+ String remoteLanguage = findMatchingLanguage(locale, manifest.getLanguages());
+
+ if (manifest.getVersion() == SignalStore.emojiValues().getSearchVersion() &&
+ remoteLanguage.equals(SignalStore.emojiValues().getSearchLanguage()))
+ {
+ Log.i(TAG, "Already using the latest version of " + manifest.getVersion() + " with the correct language " + remoteLanguage);
+ return;
+ }
+
+ Log.i(TAG, "Need to get a new search index. Downloading version: " + manifest.getVersion() + ", language: " + remoteLanguage);
+
+ List searchIndex = downloadSearchIndex(client, manifest.getVersion(), remoteLanguage);
+
+ DatabaseFactory.getEmojiSearchDatabase(context).setSearchIndex(searchIndex);
+ SignalStore.emojiValues().onSearchIndexUpdated(manifest.getVersion(), remoteLanguage);
+
+ Log.i(TAG, "Success! Now at version: " + manifest.getVersion() + ", language: " + remoteLanguage);
+ }
+
+ @Override
+ protected boolean onShouldRetry(@NonNull Exception e) {
+ return e instanceof IOException && !(e instanceof NonSuccessfulResponseCodeException);
+ }
+
+ @Override
+ public void onFailure() {
+
+ }
+
+ private static @NonNull Manifest downloadManifest(@NonNull OkHttpClient client) throws IOException {
+ String url = "https://updates.signal.org/dynamic/android/emoji/search/manifest.json";
+ String body = downloadFile(client, url);
+
+ return JsonUtil.fromJson(body, Manifest.class);
+ }
+
+ private static @NonNull List downloadSearchIndex(@NonNull OkHttpClient client, int version, @NonNull String language) throws IOException {
+ String url = "https://updates.signal.org/static/android/emoji/search/" + version + "/" + language + ".json";
+ String body = downloadFile(client, url);
+
+ return Arrays.asList(JsonUtil.fromJson(body, EmojiSearchData[].class));
+ }
+
+ private static @NonNull String downloadFile(@NonNull OkHttpClient client, @NonNull String url) throws IOException {
+ Call call = client.newCall(new Request.Builder().url(url).build());
+ Response response = call.execute();
+
+ if (response.code() != 200) {
+ throw new NonSuccessfulResponseCodeException(response.code());
+ }
+
+ if (response.body() == null) {
+ throw new NonSuccessfulResponseCodeException(404, "Missing body!");
+ }
+
+ return response.body().string();
+ }
+
+ private static @NonNull String findMatchingLanguage(@NonNull Locale locale, List languages) {
+ String parentLanguage = null;
+
+ for (String language : languages) {
+ Locale testLocale = new Locale(language);
+
+ if (locale.getLanguage().equals(testLocale.getLanguage())) {
+ if (locale.getVariant().equals(testLocale.getVariant())) {
+ Log.d(TAG, "Found an exact match: " + language);
+ return language;
+ } else if (locale.getVariant().equals("")) {
+ Log.d(TAG, "Found the parent language: " + language);
+ parentLanguage = language;
+ }
+ }
+ }
+
+ if (parentLanguage != null) {
+ Log.i(TAG, "No exact match found. Using parent language: " + parentLanguage);
+ return parentLanguage;
+ } else if (languages.contains("en")) {
+ Log.w(TAG, "No match, so falling back to en locale.");
+ return "en";
+ } else if (languages.contains("en_US")) {
+ Log.w(TAG, "No match, so falling back to en_US locale.");
+ return "en_US";
+ } else {
+ Log.w(TAG, "No match and no english fallback! Must return no language!");
+ return EmojiValues.NO_LANGUAGE;
+ }
+ }
+
+ private static class Manifest {
+ @JsonProperty
+ private int version;
+
+ @JsonProperty
+ private List languages;
+
+ public Manifest() {}
+
+ public int getVersion() {
+ return version;
+ }
+
+ public @NonNull List getLanguages() {
+ return languages != null ? languages : Collections.emptyList();
+ }
+ }
+
+ public static final class Factory implements Job.Factory {
+ @Override
+ public @NonNull EmojiSearchIndexDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
+ return new EmojiSearchIndexDownloadJob(parameters);
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
index 37f5f4016b..bbc3f61572 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/JobManagerFactories.java
@@ -81,6 +81,7 @@ public final class JobManagerFactories {
put(CreateSignedPreKeyJob.KEY, new CreateSignedPreKeyJob.Factory());
put(DirectoryRefreshJob.KEY, new DirectoryRefreshJob.Factory());
put(DownloadLatestEmojiDataJob.KEY, new DownloadLatestEmojiDataJob.Factory());
+ put(EmojiSearchIndexDownloadJob.KEY, new EmojiSearchIndexDownloadJob.Factory());
put(FcmRefreshJob.KEY, new FcmRefreshJob.Factory());
put(GroupV1MigrationJob.KEY, new GroupV1MigrationJob.Factory());
put(GroupCallUpdateSendJob.KEY, new GroupCallUpdateSendJob.Factory());
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPage.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPage.kt
new file mode 100644
index 0000000000..a709490f64
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPage.kt
@@ -0,0 +1,7 @@
+package org.thoughtcrime.securesms.keyboard
+
+enum class KeyboardPage {
+ EMOJI,
+ STICKER,
+ GIF
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPageCategoryIconViewHolder.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPageCategoryIconViewHolder.kt
new file mode 100644
index 0000000000..fa559012f3
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPageCategoryIconViewHolder.kt
@@ -0,0 +1,32 @@
+package org.thoughtcrime.securesms.keyboard
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import android.view.View
+import androidx.appcompat.widget.AppCompatImageView
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.util.MappingModel
+import org.thoughtcrime.securesms.util.MappingViewHolder
+
+interface KeyboardPageCategoryIconMappingModel> : MappingModel {
+ val key: String
+ val selected: Boolean
+
+ fun getIcon(context: Context): Drawable
+}
+
+class KeyboardPageCategoryIconViewHolder>(itemView: View, private val onPageSelected: (String) -> Unit) : MappingViewHolder(itemView) {
+
+ private val iconView: AppCompatImageView = itemView.findViewById(R.id.category_icon)
+ private val iconSelected: View = itemView.findViewById(R.id.category_icon_selected)
+
+ override fun bind(model: T) {
+ itemView.setOnClickListener {
+ onPageSelected(model.key)
+ }
+
+ iconView.setImageDrawable(model.getIcon(context))
+ iconView.isSelected = model.selected
+ iconSelected.isSelected = model.selected
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerFragment.kt
new file mode 100644
index 0000000000..a972a6b070
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerFragment.kt
@@ -0,0 +1,107 @@
+
+package org.thoughtcrime.securesms.keyboard
+
+import android.os.Bundle
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProviders
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
+import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment
+import org.thoughtcrime.securesms.keyboard.gif.GifKeyboardPageFragment
+import org.thoughtcrime.securesms.keyboard.sticker.StickerKeyboardPageFragment
+import org.thoughtcrime.securesms.util.visible
+import kotlin.reflect.KClass
+
+class KeyboardPagerFragment : Fragment(R.layout.keyboard_pager_fragment) {
+
+ private lateinit var emojiButton: View
+ private lateinit var stickerButton: View
+ private lateinit var gifButton: View
+ private lateinit var viewModel: KeyboardPagerViewModel
+
+ private val fragments: MutableMap, Fragment> = mutableMapOf()
+ private var currentFragment: Fragment? = null
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ emojiButton = view.findViewById(R.id.keyboard_pager_fragment_emoji)
+ stickerButton = view.findViewById(R.id.keyboard_pager_fragment_sticker)
+ gifButton = view.findViewById(R.id.keyboard_pager_fragment_gif)
+
+ viewModel = ViewModelProviders.of(requireActivity())[KeyboardPagerViewModel::class.java]
+
+ viewModel.page().observe(viewLifecycleOwner, this::onPageSelected)
+ viewModel.pages().observe(viewLifecycleOwner) { pages ->
+ emojiButton.visible = pages.contains(KeyboardPage.EMOJI) && pages.size > 1
+ stickerButton.visible = pages.contains(KeyboardPage.STICKER) && pages.size > 1
+ gifButton.visible = pages.contains(KeyboardPage.GIF) && pages.size > 1
+ }
+
+ emojiButton.setOnClickListener { viewModel.switchToPage(KeyboardPage.EMOJI) }
+ stickerButton.setOnClickListener { viewModel.switchToPage(KeyboardPage.STICKER) }
+ gifButton.setOnClickListener { viewModel.switchToPage(KeyboardPage.GIF) }
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ viewModel.page().value?.let(this::onPageSelected)
+ }
+
+ private fun onPageSelected(page: KeyboardPage) {
+ emojiButton.isSelected = page == KeyboardPage.EMOJI
+ stickerButton.isSelected = page == KeyboardPage.STICKER
+ gifButton.isSelected = page == KeyboardPage.GIF
+
+ when (page) {
+ KeyboardPage.EMOJI -> displayEmojiPage()
+ KeyboardPage.GIF -> displayGifPage()
+ KeyboardPage.STICKER -> displayStickerPage()
+ }
+
+ findListener()?.onKeyboardChanged(page)
+ }
+
+ private fun displayEmojiPage() = displayPage(::EmojiKeyboardPageFragment)
+
+ private fun displayGifPage() = displayPage(::GifKeyboardPageFragment)
+
+ private fun displayStickerPage() = displayPage(::StickerKeyboardPageFragment)
+
+ private inline fun displayPage(fragmentFactory: () -> F) {
+ if (currentFragment is F) {
+ return
+ }
+
+ val transaction = childFragmentManager.beginTransaction()
+
+ currentFragment?.let { transaction.hide(it) }
+
+ var fragment = fragments[F::class]
+ if (fragment == null) {
+ fragment = fragmentFactory()
+ transaction.add(R.id.fragment_container, fragment)
+ fragments[F::class] = fragment
+ } else {
+ transaction.show(fragment)
+ }
+
+ currentFragment = fragment
+ transaction.commitAllowingStateLoss()
+ }
+
+ fun show() {
+ if (isAdded && view != null) {
+ viewModel.page().value?.let(this::onPageSelected)
+ }
+ }
+
+ fun hide() {
+ if (isAdded && view != null) {
+ val transaction = childFragmentManager.beginTransaction()
+ fragments.values.forEach { transaction.remove(it) }
+ transaction.commitAllowingStateLoss()
+ currentFragment = null
+ fragments.clear()
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt
new file mode 100644
index 0000000000..230125043b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/KeyboardPagerViewModel.kt
@@ -0,0 +1,48 @@
+package org.thoughtcrime.securesms.keyboard
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.ViewModel
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
+import org.thoughtcrime.securesms.keyvalue.SignalStore
+import org.thoughtcrime.securesms.stickers.StickerSearchRepository
+import org.thoughtcrime.securesms.util.DefaultValueLiveData
+
+class KeyboardPagerViewModel : ViewModel() {
+
+ private val page: DefaultValueLiveData
+ private val pages: DefaultValueLiveData>
+
+ init {
+ val startingPages: MutableSet = KeyboardPage.values().toMutableSet()
+ if (SignalStore.settings().isPreferSystemEmoji) {
+ startingPages.remove(KeyboardPage.EMOJI)
+ }
+ pages = DefaultValueLiveData(startingPages)
+ page = DefaultValueLiveData(startingPages.first())
+
+ StickerSearchRepository(ApplicationDependencies.getApplication()).getStickerFeatureAvailability { available ->
+ if (!available) {
+ val updatedPages = pages.value.toMutableSet().apply { remove(KeyboardPage.STICKER) }
+ pages.postValue(updatedPages)
+ if (page.value == KeyboardPage.STICKER) {
+ switchToPage(KeyboardPage.GIF)
+ switchToPage(KeyboardPage.EMOJI)
+ }
+ }
+ }
+ }
+
+ fun page(): LiveData = page
+ fun pages(): LiveData> = pages
+
+ fun setOnlyPage(page: KeyboardPage) {
+ pages.value = setOf(page)
+ switchToPage(page)
+ }
+
+ fun switchToPage(page: KeyboardPage) {
+ if (this.pages.value.contains(page) && this.page.value != page) {
+ this.page.value = page
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/ListenerExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/ListenerExtensions.kt
new file mode 100644
index 0000000000..74dffa0126
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/ListenerExtensions.kt
@@ -0,0 +1,21 @@
+package org.thoughtcrime.securesms.keyboard
+
+import androidx.fragment.app.Fragment
+
+/**
+ * Given an input type [T], find an instance of it first looking through all
+ * parents, and then the activity.
+ *
+ * @return First instance found of type [T] or null
+ */
+inline fun Fragment.findListener(): T? {
+ var parent: Fragment? = parentFragment
+ while (parent != null) {
+ if (parent is T) {
+ return parent
+ }
+ parent = parent.parentFragment
+ }
+
+ return requireActivity() as? T
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageAdapter.kt
new file mode 100644
index 0000000000..4c3c2fc6f8
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageAdapter.kt
@@ -0,0 +1,35 @@
+package org.thoughtcrime.securesms.keyboard.emoji
+
+import android.view.ViewGroup
+import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
+import org.thoughtcrime.securesms.components.emoji.EmojiPageView
+import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingViewHolder
+
+class EmojiKeyboardPageAdapter(
+ private val emojiSelectionListener: EmojiKeyboardProvider.EmojiEventListener,
+ private val variationSelectorListener: EmojiPageViewGridAdapter.VariationSelectorListener,
+ private val searchCallbacks: KeyboardPageSearchView.Callbacks
+) : MappingAdapter() {
+
+ init {
+ registerFactory(EmojiPageMappingModel::class.java) { parent ->
+ val pageView = EmojiPageView(parent.context, emojiSelectionListener, variationSelectorListener, true, searchCallbacks)
+
+ val layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
+ pageView.layoutParams = layoutParams
+
+ ViewHolder(pageView)
+ }
+ }
+
+ private class ViewHolder(
+ private val emojiPageView: EmojiPageView,
+ ) : MappingViewHolder(emojiPageView) {
+
+ override fun bind(model: EmojiPageMappingModel) {
+ emojiPageView.bindSearchableAdapter(model.emojiPageModel)
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoriesAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoriesAdapter.kt
new file mode 100644
index 0000000000..3d0f23918d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoriesAdapter.kt
@@ -0,0 +1,12 @@
+package org.thoughtcrime.securesms.keyboard.emoji
+
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.keyboard.KeyboardPageCategoryIconViewHolder
+import org.thoughtcrime.securesms.util.MappingAdapter
+
+class EmojiKeyboardPageCategoriesAdapter(private val onPageSelected: (String) -> Unit) : MappingAdapter() {
+ init {
+ registerFactory(EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel::class.java, LayoutFactory({ v -> KeyboardPageCategoryIconViewHolder(v, onPageSelected) }, R.layout.keyboard_pager_category_icon))
+ registerFactory(EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel::class.java, LayoutFactory({ v -> KeyboardPageCategoryIconViewHolder(v, onPageSelected) }, R.layout.keyboard_pager_category_icon))
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoryMappingModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoryMappingModel.kt
new file mode 100644
index 0000000000..5b7c105ffa
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageCategoryMappingModel.kt
@@ -0,0 +1,46 @@
+package org.thoughtcrime.securesms.keyboard.emoji
+
+import android.content.Context
+import android.graphics.drawable.Drawable
+import androidx.annotation.AttrRes
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.emoji.EmojiCategory
+import org.thoughtcrime.securesms.keyboard.KeyboardPageCategoryIconMappingModel
+import org.thoughtcrime.securesms.util.ThemeUtil
+
+sealed class EmojiKeyboardPageCategoryMappingModel(
+ override val key: String,
+ @AttrRes val iconId: Int,
+ override val selected: Boolean
+) : KeyboardPageCategoryIconMappingModel {
+
+ override fun getIcon(context: Context): Drawable {
+ return requireNotNull(ThemeUtil.getThemedDrawable(context, iconId))
+ }
+
+ override fun areItemsTheSame(newItem: EmojiKeyboardPageCategoryMappingModel): Boolean {
+ return newItem.key == key
+ }
+
+ class RecentsMappingModel(selected: Boolean) : EmojiKeyboardPageCategoryMappingModel(KEY, R.attr.emoji_category_recent, selected) {
+ override fun areContentsTheSame(newItem: EmojiKeyboardPageCategoryMappingModel): Boolean {
+ return newItem is RecentsMappingModel && super.areContentsTheSame(newItem)
+ }
+
+ companion object {
+ const val KEY = "Recents"
+ }
+ }
+
+ class EmojiCategoryMappingModel(private val emojiCategory: EmojiCategory, selected: Boolean) : EmojiKeyboardPageCategoryMappingModel(emojiCategory.key, emojiCategory.icon, selected) {
+ override fun areContentsTheSame(newItem: EmojiKeyboardPageCategoryMappingModel): Boolean {
+ return newItem is EmojiCategoryMappingModel &&
+ super.areContentsTheSame(newItem) &&
+ newItem.emojiCategory == emojiCategory
+ }
+ }
+
+ override fun areContentsTheSame(newItem: EmojiKeyboardPageCategoryMappingModel): Boolean {
+ return areItemsTheSame(newItem) && selected == newItem.selected
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageFragment.kt
new file mode 100644
index 0000000000..cef4f4fd7e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageFragment.kt
@@ -0,0 +1,141 @@
+package org.thoughtcrime.securesms.keyboard.emoji
+
+import android.content.Context
+import android.os.Bundle
+import android.view.KeyEvent
+import android.view.View
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager2.widget.ViewPager2
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
+import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter
+import org.thoughtcrime.securesms.keyboard.findListener
+import org.thoughtcrime.securesms.keyvalue.SignalStore
+
+private val DELETE_KEY_EVENT: KeyEvent = KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)
+
+class EmojiKeyboardPageFragment : Fragment(R.layout.keyboard_pager_emoji_page_fragment), EmojiKeyboardProvider.EmojiEventListener, EmojiPageViewGridAdapter.VariationSelectorListener {
+
+ private lateinit var viewModel: EmojiKeyboardPageViewModel
+ private lateinit var emojiPager: ViewPager2
+ private lateinit var searchView: View
+ private lateinit var emojiCategoriesRecycler: RecyclerView
+ private lateinit var backspaceView: View
+ private lateinit var eventListener: EmojiKeyboardProvider.EmojiEventListener
+ private lateinit var callback: Callback
+ private lateinit var pagesAdapter: EmojiKeyboardPageAdapter
+ private lateinit var categoriesAdapter: EmojiKeyboardPageCategoriesAdapter
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ callback = context as Callback
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ emojiPager = view.findViewById(R.id.emoji_pager)
+ searchView = view.findViewById(R.id.emoji_search)
+ emojiCategoriesRecycler = view.findViewById(R.id.emoji_categories_recycler)
+ backspaceView = view.findViewById(R.id.emoji_backspace)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ viewModel = ViewModelProviders.of(requireActivity()).get(EmojiKeyboardPageViewModel::class.java)
+
+ pagesAdapter = EmojiKeyboardPageAdapter(this, this, EmojiKeyboardPageSearchViewCallbacks())
+
+ categoriesAdapter = EmojiKeyboardPageCategoriesAdapter { key ->
+ viewModel.onKeySelected(key)
+
+ val page = pagesAdapter.currentList.indexOfFirst {
+ (it as EmojiPageMappingModel).key == key
+ }
+
+ if (emojiPager.currentItem != page) {
+ emojiPager.currentItem = page
+ }
+ }
+
+ emojiPager.adapter = pagesAdapter
+ emojiCategoriesRecycler.adapter = categoriesAdapter
+
+ searchView.setOnClickListener {
+ callback.openEmojiSearch()
+ }
+
+ backspaceView.setOnClickListener { eventListener.onKeyEvent(DELETE_KEY_EVENT) }
+
+ viewModel.categories.observe(viewLifecycleOwner) { categories ->
+ categoriesAdapter.submitList(categories)
+ }
+
+ viewModel.pages.observe(viewLifecycleOwner) { pages ->
+ val registerPageCallback: Boolean = pagesAdapter.currentList.isEmpty() && pages.isNotEmpty()
+ pagesAdapter.submitList(pages) { updatePagerPosition(registerPageCallback) }
+ }
+
+ viewModel.selectedKey.observe(viewLifecycleOwner) { updateCategoryTab() }
+
+ eventListener = findListener() ?: throw AssertionError("No emoji listener found")
+ }
+
+ private fun updateCategoryTab() {
+ emojiCategoriesRecycler.post {
+ val index: Int = categoriesAdapter.currentList.indexOfFirst { (it as? EmojiKeyboardPageCategoryMappingModel)?.key == viewModel.selectedKey.value }
+
+ if (index != -1) {
+ emojiCategoriesRecycler.smoothScrollToPosition(index)
+ }
+ }
+ }
+
+ private fun updatePagerPosition(registerPageCallback: Boolean) {
+ val page = pagesAdapter.currentList.indexOfFirst {
+ (it as EmojiPageMappingModel).key == viewModel.selectedKey.value
+ }
+
+ if (emojiPager.currentItem != page && page != -1) {
+ emojiPager.setCurrentItem(page, false)
+ }
+
+ if (registerPageCallback) {
+ emojiPager.registerOnPageChangeCallback(PageChanged(pagesAdapter))
+ }
+ }
+
+ override fun onEmojiSelected(emoji: String) {
+ SignalStore.emojiValues().setPreferredVariation(emoji)
+ eventListener.onEmojiSelected(emoji)
+ viewModel.addToRecents(emoji)
+ }
+
+ override fun onKeyEvent(keyEvent: KeyEvent?) {
+ eventListener.onKeyEvent(keyEvent)
+ }
+
+ override fun onVariationSelectorStateChanged(open: Boolean) {
+ emojiPager.isUserInputEnabled = !open
+ }
+
+ private inner class PageChanged(private val adapter: EmojiKeyboardPageAdapter) : ViewPager2.OnPageChangeCallback() {
+ override fun onPageSelected(position: Int) {
+ val mappingModel: EmojiPageMappingModel = adapter.currentList[position] as EmojiPageMappingModel
+ viewModel.onKeySelected(mappingModel.key)
+ }
+ }
+
+ private inner class EmojiKeyboardPageSearchViewCallbacks : KeyboardPageSearchView.Callbacks {
+ override fun onClicked() {
+ callback.openEmojiSearch()
+ }
+ }
+
+ interface Callback {
+ fun openEmojiSearch()
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageViewModel.kt
new file mode 100644
index 0000000000..26db7dcb10
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiKeyboardPageViewModel.kt
@@ -0,0 +1,66 @@
+package org.thoughtcrime.securesms.keyboard.emoji
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.Transformations
+import androidx.lifecycle.ViewModel
+import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
+import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
+import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
+import org.thoughtcrime.securesms.emoji.EmojiCategory
+import org.thoughtcrime.securesms.emoji.EmojiSource
+import org.thoughtcrime.securesms.util.DefaultValueLiveData
+import org.thoughtcrime.securesms.util.MappingModelList
+
+class EmojiKeyboardPageViewModel : ViewModel() {
+
+ private val internalSelectedKey = DefaultValueLiveData(getStartingTab())
+
+ val selectedKey: LiveData
+ get() = internalSelectedKey
+
+ val categories: LiveData = Transformations.map(internalSelectedKey) { selected ->
+ MappingModelList().apply {
+ add(EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel(selected == EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel.KEY))
+
+ EmojiCategory.values().forEach {
+ add(EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel(it, it.key == selected))
+ }
+ }
+ }
+
+ val pages: LiveData = Transformations.map(categories) { categories ->
+ MappingModelList().apply {
+ categories.forEach {
+ add(getPageForCategory(it as EmojiKeyboardPageCategoryMappingModel))
+ }
+ }
+ }
+
+ fun onKeySelected(key: String) {
+ internalSelectedKey.value = key
+ }
+
+ private fun getPageForCategory(mappingModel: EmojiKeyboardPageCategoryMappingModel): EmojiPageMappingModel {
+ val page = if (mappingModel.key == EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel.KEY) {
+ RecentEmojiPageModel(ApplicationDependencies.getApplication(), EmojiKeyboardProvider.RECENT_STORAGE_KEY)
+ } else {
+ EmojiSource.latest.displayPages.first { it.iconAttr == mappingModel.iconId }
+ }
+
+ return EmojiPageMappingModel(mappingModel.key, page)
+ }
+
+ fun addToRecents(emoji: String) {
+ RecentEmojiPageModel(ApplicationDependencies.getApplication(), EmojiKeyboardProvider.RECENT_STORAGE_KEY).onCodePointSelected(emoji)
+ }
+
+ companion object {
+ fun getStartingTab(): String {
+ return if (RecentEmojiPageModel.hasRecents(ApplicationDependencies.getApplication(), EmojiKeyboardProvider.RECENT_STORAGE_KEY)) {
+ EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel.KEY
+ } else {
+ EmojiCategory.PEOPLE.key
+ }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiPageMappingModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiPageMappingModel.kt
new file mode 100644
index 0000000000..f9ac6bc22b
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/EmojiPageMappingModel.kt
@@ -0,0 +1,16 @@
+package org.thoughtcrime.securesms.keyboard.emoji
+
+import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
+import org.thoughtcrime.securesms.util.MappingModel
+
+class EmojiPageMappingModel(val key: String, val emojiPageModel: EmojiPageModel) : MappingModel {
+ override fun areItemsTheSame(newItem: EmojiPageMappingModel): Boolean {
+ return key == newItem.key
+ }
+
+ override fun areContentsTheSame(newItem: EmojiPageMappingModel): Boolean {
+ return areItemsTheSame(newItem) &&
+ newItem.emojiPageModel.spriteUri == emojiPageModel.spriteUri &&
+ newItem.emojiPageModel.iconAttr == emojiPageModel.iconAttr
+ }
+}
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..df3308685e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/KeyboardPageSearchView.kt
@@ -0,0 +1,183 @@
+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.widget.ImageViewCompat
+import androidx.core.widget.addTextChangedListener
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.animation.AnimationCompleteListener
+import org.thoughtcrime.securesms.animation.ResizeAnimation
+import org.thoughtcrime.securesms.util.ViewUtil
+import org.thoughtcrime.securesms.util.visible
+
+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.addTextChangedListener {
+ 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 {
+ input.text.clear()
+ }
+
+ 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_primary)
+ ImageViewCompat.setImageTintList(navButton, iconTint)
+ ImageViewCompat.setImageTintList(clearButton, 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.visible = true
+ clickIntercept.setOnClickListener { callbacks?.onClicked() }
+ }
+ }
+ }
+
+ fun showRequested(): Boolean = state == State.SHOW_REQUESTED
+
+ fun enableBackNavigation() {
+ navButton.setImageResource(R.drawable.ic_arrow_left_24)
+ navButton.setOnClickListener {
+ callbacks?.onNavigationClicked()
+ }
+ }
+
+ 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()
+ }
+
+ 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/EmojiSearchFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchFragment.kt
new file mode 100644
index 0000000000..62ee3e9ea7
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchFragment.kt
@@ -0,0 +1,77 @@
+package org.thoughtcrime.securesms.keyboard.emoji.search
+
+import android.content.Context
+import android.os.Bundle
+import android.view.View
+import android.widget.FrameLayout
+import android.widget.TextView
+import androidx.fragment.app.Fragment
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.LinearLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
+import org.thoughtcrime.securesms.components.emoji.EmojiPageView
+import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter
+import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
+import org.thoughtcrime.securesms.keyboard.findListener
+import org.thoughtcrime.securesms.util.ViewUtil
+
+class EmojiSearchFragment : Fragment(R.layout.emoji_search_fragment), EmojiPageViewGridAdapter.VariationSelectorListener {
+
+ private lateinit var viewModel: EmojiSearchViewModel
+ private lateinit var callback: Callback
+
+ override fun onAttach(context: Context) {
+ super.onAttach(context)
+
+ callback = context as Callback
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ val repository = EmojiSearchRepository(requireContext())
+ val factory = EmojiSearchViewModel.Factory(repository)
+
+ viewModel = ViewModelProviders.of(this, factory)[EmojiSearchViewModel::class.java]
+
+ val eventListener: EmojiKeyboardProvider.EmojiEventListener = requireNotNull(findListener())
+ val searchBar: KeyboardPageSearchView = view.findViewById(R.id.emoji_search_view)
+ val resultsContainer: FrameLayout = view.findViewById(R.id.emoji_search_results_container)
+ val noResults: TextView = view.findViewById(R.id.emoji_search_empty)
+ val emojiPageView = EmojiPageView(requireContext(), eventListener, this, true, null, LinearLayoutManager(requireContext(), RecyclerView.HORIZONTAL, false), R.layout.emoji_search_result_display_item)
+
+ resultsContainer.addView(emojiPageView)
+
+ searchBar.presentForEmojiSearch()
+ searchBar.callbacks = SearchCallbacks()
+
+ viewModel.pageModel.observe(viewLifecycleOwner) { pageModel ->
+ emojiPageView.setModel(pageModel)
+
+ if (pageModel.emoji.isNotEmpty() || pageModel.iconAttr == R.attr.emoji_category_recent) {
+ emojiPageView.visibility = View.VISIBLE
+ noResults.visibility = View.GONE
+ } else {
+ emojiPageView.visibility = View.INVISIBLE
+ noResults.visibility = View.VISIBLE
+ }
+ }
+ }
+
+ private inner class SearchCallbacks : KeyboardPageSearchView.Callbacks {
+ override fun onNavigationClicked() {
+ ViewUtil.hideKeyboard(requireContext(), requireView())
+ callback.closeEmojiSearch()
+ }
+
+ override fun onQueryChanged(query: String) {
+ viewModel.onQueryChanged(query)
+ }
+ }
+
+ interface Callback {
+ fun closeEmojiSearch()
+ }
+
+ override fun onVariationSelectorStateChanged(open: Boolean) = Unit
+}
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..0027af24f3
--- /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 org.signal.core.util.concurrent.SignalExecutors
+import org.thoughtcrime.securesms.components.emoji.Emoji
+import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider
+import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
+import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
+import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.database.EmojiSearchDatabase
+import org.thoughtcrime.securesms.emoji.EmojiSource
+
+private const val MINIMUM_QUERY_THRESHOLD = 1
+private const val EMOJI_SEARCH_LIMIT = 20
+
+class EmojiSearchRepository(private val context: Context) {
+
+ private val emojiSearchDatabase: EmojiSearchDatabase = DatabaseFactory.getEmojiSearchDatabase(context)
+
+ fun submitQuery(query: String, consumer: (EmojiPageModel) -> Unit) {
+ if (query.length < MINIMUM_QUERY_THRESHOLD) {
+ consumer(RecentEmojiPageModel(context, EmojiKeyboardProvider.RECENT_STORAGE_KEY))
+ } else {
+ SignalExecutors.SERIAL.execute {
+ val emoji: List = emojiSearchDatabase.query(query, EMOJI_SEARCH_LIMIT)
+
+ val variationMap: Map = EmojiSource.latest.variationMap
+ val emojiVariationSets: MutableMap> = mutableMapOf()
+
+ variationMap
+ .filterKeys { emoji.contains(it) }
+ .forEach { (variation, canonical) ->
+ val set: LinkedHashSet = emojiVariationSets.getOrDefault(canonical, linkedSetOf())
+
+ set.add(variation)
+ emojiVariationSets[canonical] = set
+ }
+
+ val displayEmoji: List = emoji.map { canonical ->
+ val variationSet: LinkedHashSet = linkedSetOf(canonical).apply {
+ addAll(emojiVariationSets.getOrDefault(canonical, linkedSetOf()))
+ }
+
+ Emoji(variationSet.toList())
+ }
+
+ consumer(EmojiSearchResultsPageModel(emoji, displayEmoji))
+ }
+ }
+ }
+
+ private class EmojiSearchResultsPageModel(
+ private val emoji: List,
+ private val displayEmoji: List
+ ) : EmojiPageModel {
+ override fun getIconAttr(): Int = -1
+
+ override fun getEmoji(): List = emoji
+
+ override fun getDisplayEmoji(): List = displayEmoji
+
+ override fun getSpriteUri(): Uri? = null
+
+ override fun isDynamic(): Boolean = false
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchViewModel.kt
new file mode 100644
index 0000000000..ff39708ca9
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/emoji/search/EmojiSearchViewModel.kt
@@ -0,0 +1,28 @@
+package org.thoughtcrime.securesms.keyboard.emoji.search
+
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.thoughtcrime.securesms.components.emoji.EmojiPageModel
+
+class EmojiSearchViewModel(private val repository: EmojiSearchRepository) : ViewModel() {
+
+ private val internalPageModel = MutableLiveData()
+
+ val pageModel: LiveData = internalPageModel
+
+ init {
+ onQueryChanged("")
+ }
+
+ fun onQueryChanged(query: String) {
+ repository.submitQuery(query, internalPageModel::postValue)
+ }
+
+ class Factory(private val repository: EmojiSearchRepository) : ViewModelProvider.Factory {
+ override fun create(modelClass: Class): T {
+ return requireNotNull(modelClass.cast(EmojiSearchViewModel(repository)))
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifKeyboardPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifKeyboardPageFragment.kt
new file mode 100644
index 0000000000..e0e9536267
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifKeyboardPageFragment.kt
@@ -0,0 +1,116 @@
+package org.thoughtcrime.securesms.keyboard.gif
+
+import android.net.Uri
+import android.os.Bundle
+import android.view.View
+import android.widget.Toast
+import androidx.appcompat.app.AlertDialog
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.RecyclerView
+import org.thoughtcrime.securesms.LoggingFragment
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.conversation.ConversationActivity
+import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Fragment
+import org.thoughtcrime.securesms.giph.mp4.GiphyMp4SaveResult
+import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ViewModel
+import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
+import org.thoughtcrime.securesms.keyboard.findListener
+import org.thoughtcrime.securesms.mms.AttachmentManager
+import org.thoughtcrime.securesms.util.views.SimpleProgressDialog
+
+class GifKeyboardPageFragment : LoggingFragment(R.layout.gif_keyboard_page_fragment) {
+
+ private lateinit var host: Host
+ private lateinit var quickSearchAdapter: GifQuickSearchAdapter
+ private lateinit var giphyMp4ViewModel: GiphyMp4ViewModel
+
+ private lateinit var viewModel: GifKeyboardPageViewModel
+
+ private var progressDialog: AlertDialog? = null
+ private lateinit var quickSearchList: RecyclerView
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ host = findListener() ?: throw AssertionError("Parent fragment or activity must implement Host")
+
+ childFragmentManager.beginTransaction()
+ .replace(R.id.gif_keyboard_giphy_frame, GiphyMp4Fragment.create(host.isMms()))
+ .commitAllowingStateLoss()
+
+ val searchKeyboard: KeyboardPageSearchView = view.findViewById(R.id.gif_keyboard_search_text)
+ searchKeyboard.callbacks = object : KeyboardPageSearchView.Callbacks {
+ override fun onClicked() {
+ openGifSearch()
+ }
+ }
+
+ view.findViewById(R.id.gif_keyboard_search).setOnClickListener { openGifSearch() }
+
+ quickSearchList = view.findViewById(R.id.gif_keyboard_quick_search_recycler)
+ quickSearchAdapter = GifQuickSearchAdapter(this::onQuickSearchSelected)
+ quickSearchList.adapter = quickSearchAdapter
+
+ giphyMp4ViewModel = ViewModelProviders.of(requireActivity(), GiphyMp4ViewModel.Factory(host.isMms())).get(GiphyMp4ViewModel::class.java)
+ giphyMp4ViewModel.saveResultEvents.observe(viewLifecycleOwner, this::handleGiphyMp4SaveResult)
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+ viewModel = ViewModelProviders.of(requireActivity()).get(GifKeyboardPageViewModel::class.java)
+ updateQuickSearchTabs()
+ }
+
+ private fun onQuickSearchSelected(gifQuickSearchOption: GifQuickSearchOption) {
+ if (viewModel.selectedTab == gifQuickSearchOption) {
+ return
+ }
+
+ viewModel.selectedTab = gifQuickSearchOption
+ giphyMp4ViewModel.updateSearchQuery(gifQuickSearchOption.query)
+
+ updateQuickSearchTabs()
+ }
+
+ private fun updateQuickSearchTabs() {
+ val quickSearches: List = GifQuickSearchOption.ranked
+ .map { search -> GifQuickSearch(search, search == viewModel.selectedTab) }
+
+ quickSearchAdapter.submitList(quickSearches, this::scrollToTab)
+ }
+
+ private fun scrollToTab() {
+ quickSearchList.post { quickSearchList.smoothScrollToPosition(GifQuickSearchOption.ranked.indexOf(viewModel.selectedTab)) }
+ }
+
+ private fun handleGiphyMp4SaveResult(result: GiphyMp4SaveResult) {
+ if (result is GiphyMp4SaveResult.Success) {
+ hideProgressDialog()
+ handleGiphyMp4SuccessfulResult(result)
+ } else if (result is GiphyMp4SaveResult.Error) {
+ hideProgressDialog()
+ handleGiphyMp4ErrorResult()
+ } else {
+ progressDialog = SimpleProgressDialog.show(requireContext())
+ }
+ }
+
+ private fun hideProgressDialog() {
+ progressDialog?.dismiss()
+ }
+
+ private fun handleGiphyMp4SuccessfulResult(success: GiphyMp4SaveResult.Success) {
+ host.onGifSelectSuccess(success.blobUri, success.width, success.height)
+ }
+
+ private fun handleGiphyMp4ErrorResult() {
+ Toast.makeText(requireContext(), R.string.GiphyActivity_error_while_retrieving_full_resolution_gif, Toast.LENGTH_LONG).show()
+ }
+
+ private fun openGifSearch() {
+ AttachmentManager.selectGif(requireActivity(), ConversationActivity.PICK_GIF, host.isMms())
+ }
+
+ interface Host {
+ fun isMms(): Boolean
+ fun onGifSelectSuccess(blobUri: Uri, width: Int, height: Int)
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifKeyboardPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifKeyboardPageViewModel.kt
new file mode 100644
index 0000000000..4a74b02e69
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifKeyboardPageViewModel.kt
@@ -0,0 +1,7 @@
+package org.thoughtcrime.securesms.keyboard.gif
+
+import androidx.lifecycle.ViewModel
+
+class GifKeyboardPageViewModel : ViewModel() {
+ var selectedTab: GifQuickSearchOption = GifQuickSearchOption.TRENDING
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifQuickSearch.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifQuickSearch.kt
new file mode 100644
index 0000000000..30b18e7f76
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifQuickSearch.kt
@@ -0,0 +1,13 @@
+package org.thoughtcrime.securesms.keyboard.gif
+
+import org.thoughtcrime.securesms.util.MappingModel
+
+data class GifQuickSearch(val gifQuickSearchOption: GifQuickSearchOption, val selected: Boolean) : MappingModel {
+ override fun areItemsTheSame(newItem: GifQuickSearch): Boolean {
+ return gifQuickSearchOption == newItem.gifQuickSearchOption
+ }
+
+ override fun areContentsTheSame(newItem: GifQuickSearch): Boolean {
+ return selected == newItem.selected
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifQuickSearchAdapter.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifQuickSearchAdapter.kt
new file mode 100644
index 0000000000..cbae8db66f
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifQuickSearchAdapter.kt
@@ -0,0 +1,25 @@
+package org.thoughtcrime.securesms.keyboard.gif
+
+import android.view.View
+import android.widget.ImageView
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.util.MappingAdapter
+import org.thoughtcrime.securesms.util.MappingViewHolder
+
+class GifQuickSearchAdapter(clickListener: (GifQuickSearchOption) -> Unit) : MappingAdapter() {
+ init {
+ registerFactory(GifQuickSearch::class.java, LayoutFactory({ v -> ViewHolder(v, clickListener) }, R.layout.keyboard_pager_category_icon))
+ }
+
+ private class ViewHolder(itemView: View, private val listener: (GifQuickSearchOption) -> Unit) : MappingViewHolder(itemView) {
+ private val image: ImageView = findViewById(R.id.category_icon)
+ private val imageSelected: View = findViewById(R.id.category_icon_selected)
+
+ override fun bind(model: GifQuickSearch) {
+ image.setImageResource(model.gifQuickSearchOption.image)
+ image.isSelected = model.selected
+ imageSelected.isSelected = model.selected
+ itemView.setOnClickListener { listener(model.gifQuickSearchOption) }
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifQuickSearchOption.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifQuickSearchOption.kt
new file mode 100644
index 0000000000..67f2cae418
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/gif/GifQuickSearchOption.kt
@@ -0,0 +1,18 @@
+package org.thoughtcrime.securesms.keyboard.gif
+
+import org.thoughtcrime.securesms.R
+
+enum class GifQuickSearchOption(private val rank: Int, val image: Int, val query: String) {
+ TRENDING(0, R.drawable.ic_gif_trending_24, ""),
+ CELEBRATE(1, R.drawable.ic_gif_celebrate_24, "celebrate"),
+ LOVE(2, R.drawable.ic_gif_love_24, "love"),
+ THUMBS_UP(3, R.drawable.ic_gif_thumbsup_24, "thumbs up"),
+ SURPRISED(4, R.drawable.ic_gif_surprised_24, "surprised"),
+ EXCITED(5, R.drawable.ic_gif_excited_24, "excited"),
+ SAD(6, R.drawable.ic_gif_sad_24, "sad"),
+ ANGRY(7, R.drawable.ic_gif_angry_24, "angry");
+
+ companion object {
+ val ranked: List by lazy { values().sortedBy { it.rank } }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt
new file mode 100644
index 0000000000..91e4b0b589
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageFragment.kt
@@ -0,0 +1,102 @@
+package org.thoughtcrime.securesms.keyboard.sticker
+
+import android.os.Bundle
+import android.view.View
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.RecyclerView
+import androidx.viewpager.widget.PagerAdapter
+import androidx.viewpager.widget.ViewPager
+import org.thoughtcrime.securesms.LoggingFragment
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.emoji.MediaKeyboardBottomTabAdapter
+import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider
+import org.thoughtcrime.securesms.mms.GlideApp
+import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider
+import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider.StickerEventListener
+
+class StickerKeyboardPageFragment : LoggingFragment(R.layout.keyboard_pager_sticker_page_fragment) {
+
+ private val presenter: StickerPresenter = StickerPresenter()
+ private lateinit var provider: StickerKeyboardProvider
+
+ private lateinit var stickerPager: ViewPager
+ private lateinit var searchView: View
+ private lateinit var stickerPacksRecycler: RecyclerView
+ private lateinit var manageStickers: View
+ private lateinit var tabAdapter: MediaKeyboardBottomTabAdapter
+
+ private lateinit var viewModel: StickerKeyboardPageViewModel
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ stickerPager = view.findViewById(R.id.sticker_pager)
+ searchView = view.findViewById(R.id.sticker_search)
+ manageStickers = view.findViewById(R.id.sticker_manage)
+ stickerPacksRecycler = view.findViewById(R.id.sticker_packs_recycler)
+
+ searchView.setOnClickListener { StickerSearchDialogFragment.show(requireActivity().supportFragmentManager) }
+ }
+
+ override fun onActivityCreated(savedInstanceState: Bundle?) {
+ super.onActivityCreated(savedInstanceState)
+
+ viewModel = ViewModelProviders.of(requireActivity()).get(StickerKeyboardPageViewModel::class.java)
+
+ tabAdapter = MediaKeyboardBottomTabAdapter(GlideApp.with(this), this::onTabSelected)
+ stickerPacksRecycler.adapter = tabAdapter
+
+ provider = StickerKeyboardProvider(requireActivity(), findListener() ?: throw AssertionError("No sticker listener"))
+ provider.requestPresentation(presenter, true)
+ }
+
+ private fun findListener(): StickerEventListener? {
+ return parentFragment as? StickerEventListener ?: requireActivity() as? StickerEventListener
+ }
+
+ private fun onTabSelected(index: Int) {
+ stickerPager.currentItem = index
+ stickerPacksRecycler.smoothScrollToPosition(index)
+ viewModel.selectedTab = index
+ }
+
+ private inner class StickerPresenter : MediaKeyboardProvider.Presenter {
+ override fun present(
+ provider: MediaKeyboardProvider,
+ pagerAdapter: PagerAdapter,
+ iconProvider: MediaKeyboardProvider.TabIconProvider,
+ backspaceObserver: MediaKeyboardProvider.BackspaceObserver?,
+ addObserver: MediaKeyboardProvider.AddObserver?,
+ searchObserver: MediaKeyboardProvider.SearchObserver?,
+ startingIndex: Int
+ ) {
+ if (stickerPager.adapter != pagerAdapter) {
+ stickerPager.adapter = pagerAdapter
+ }
+ stickerPager.currentItem = viewModel.selectedTab
+
+ stickerPager.clearOnPageChangeListeners()
+ stickerPager.addOnPageChangeListener(object : ViewPager.OnPageChangeListener {
+ override fun onPageSelected(position: Int) {
+ tabAdapter.setActivePosition(position)
+ stickerPacksRecycler.smoothScrollToPosition(position)
+ provider.setCurrentPosition(position)
+ }
+
+ override fun onPageScrolled(position: Int, positionOffset: Float, positionOffsetPixels: Int) = Unit
+ override fun onPageScrollStateChanged(state: Int) = Unit
+ })
+
+ tabAdapter.setTabIconProvider(iconProvider, pagerAdapter.count)
+ tabAdapter.setActivePosition(stickerPager.currentItem)
+
+ manageStickers.setOnClickListener { addObserver?.onAddClicked() }
+ }
+
+ override fun getCurrentPosition(): Int {
+ return stickerPager.currentItem
+ }
+
+ override fun requestDismissal() = Unit
+ override fun isVisible(): Boolean = true
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageViewModel.kt
new file mode 100644
index 0000000000..d2b4e25d9d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerKeyboardPageViewModel.kt
@@ -0,0 +1,7 @@
+package org.thoughtcrime.securesms.keyboard.sticker
+
+import androidx.lifecycle.ViewModel
+
+class StickerKeyboardPageViewModel : ViewModel() {
+ var selectedTab: Int = 0
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerSearchDialogFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerSearchDialogFragment.kt
new file mode 100644
index 0000000000..10a01dc66e
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerSearchDialogFragment.kt
@@ -0,0 +1,123 @@
+package org.thoughtcrime.securesms.keyboard.sticker
+
+import android.content.res.Configuration
+import android.graphics.Point
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import androidx.annotation.Px
+import androidx.fragment.app.DialogFragment
+import androidx.fragment.app.FragmentManager
+import androidx.lifecycle.ViewModelProviders
+import androidx.recyclerview.widget.GridLayoutManager
+import androidx.recyclerview.widget.RecyclerView
+import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.database.model.StickerRecord
+import org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
+import org.thoughtcrime.securesms.keyboard.findListener
+import org.thoughtcrime.securesms.mms.GlideApp
+import org.thoughtcrime.securesms.stickers.StickerKeyboardPageAdapter
+import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider
+import org.thoughtcrime.securesms.util.DeviceProperties
+import org.thoughtcrime.securesms.util.ViewUtil
+
+/**
+ * Search dialog for finding stickers.
+ */
+class StickerSearchDialogFragment : DialogFragment(), StickerKeyboardPageAdapter.EventListener {
+
+ private lateinit var search: KeyboardPageSearchView
+ private lateinit var list: RecyclerView
+ private lateinit var noResults: View
+
+ private lateinit var adapter: StickerKeyboardPageAdapter
+ private lateinit var layoutManager: GridLayoutManager
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ setStyle(STYLE_NO_FRAME, R.style.Signal_DayNight_Dialog_Animated_Bottom)
+ }
+
+ override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
+ return inflater.inflate(R.layout.sticker_search_dialog_fragment, container, false)
+ }
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ search = view.findViewById(R.id.sticker_search_text)
+ list = view.findViewById(R.id.sticker_search_list)
+ noResults = view.findViewById(R.id.sticker_search_no_results)
+
+ adapter = StickerKeyboardPageAdapter(GlideApp.with(this), this, DeviceProperties.shouldAllowApngStickerAnimation(requireContext()))
+ layoutManager = GridLayoutManager(requireContext(), 2)
+
+ list.layoutManager = layoutManager
+ list.adapter = adapter
+
+ onScreenWidthChanged(getScreenWidth())
+
+ val viewModel: StickerSearchViewModel = ViewModelProviders.of(this, StickerSearchViewModel.Factory(requireContext())).get(StickerSearchViewModel::class.java)
+
+ viewModel.searchResults.observe(viewLifecycleOwner) { stickerRecords ->
+ adapter.setStickers(stickerRecords, calculateStickerSize(getScreenWidth()))
+ noResults.visibility = if (stickerRecords.isEmpty()) View.VISIBLE else View.GONE
+ }
+
+ search.enableBackNavigation()
+ search.callbacks = object : KeyboardPageSearchView.Callbacks {
+ override fun onQueryChanged(query: String) {
+ viewModel.query(query)
+ }
+
+ override fun onNavigationClicked() {
+ ViewUtil.hideKeyboard(requireContext(), view)
+ dismissAllowingStateLoss()
+ }
+ }
+
+ search.requestFocus()
+ }
+
+ override fun onConfigurationChanged(newConfig: Configuration) {
+ super.onConfigurationChanged(newConfig)
+ onScreenWidthChanged(getScreenWidth())
+ }
+
+ private fun onScreenWidthChanged(@Px newWidth: Int) {
+ layoutManager.spanCount = calculateColumnCount(newWidth)
+ adapter.setStickerSize(calculateStickerSize(newWidth))
+ }
+
+ private fun getScreenWidth(): Int {
+ val size = Point()
+ requireActivity().windowManager.defaultDisplay.getSize(size)
+ return size.x
+ }
+
+ private fun calculateColumnCount(@Px screenWidth: Int): Int {
+ val modifier = resources.getDimensionPixelOffset(R.dimen.sticker_page_item_padding).toFloat()
+ val divisor = resources.getDimensionPixelOffset(R.dimen.sticker_page_item_divisor).toFloat()
+ return ((screenWidth - modifier) / divisor).toInt()
+ }
+
+ private fun calculateStickerSize(@Px screenWidth: Int): Int {
+ val multiplier = resources.getDimensionPixelOffset(R.dimen.sticker_page_item_multiplier).toFloat()
+ val columnCount = calculateColumnCount(screenWidth)
+ return ((screenWidth - (columnCount + 1) * multiplier) / columnCount).toInt()
+ }
+
+ companion object {
+ fun show(fragmentManager: FragmentManager) {
+ StickerSearchDialogFragment().show(fragmentManager, "TAG")
+ }
+ }
+
+ override fun onStickerClicked(sticker: StickerRecord) {
+ ViewUtil.hideKeyboard(requireContext(), requireView())
+ findListener()?.onStickerSelected(sticker)
+ dismissAllowingStateLoss()
+ }
+
+ override fun onStickerLongClicked(targetView: View) = Unit
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerSearchRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerSearchRepository.kt
new file mode 100644
index 0000000000..db4b233cf5
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerSearchRepository.kt
@@ -0,0 +1,55 @@
+package org.thoughtcrime.securesms.keyboard.sticker
+
+import android.content.Context
+import androidx.annotation.WorkerThread
+import org.thoughtcrime.securesms.components.emoji.EmojiUtil
+import org.thoughtcrime.securesms.database.DatabaseFactory
+import org.thoughtcrime.securesms.database.EmojiSearchDatabase
+import org.thoughtcrime.securesms.database.StickerDatabase
+import org.thoughtcrime.securesms.database.StickerDatabase.StickerRecordReader
+import org.thoughtcrime.securesms.database.model.StickerRecord
+
+private const val RECENT_LIMIT = 24
+private const val EMOJI_SEARCH_RESULTS_LIMIT = 20
+
+class StickerSearchRepository(context: Context) {
+
+ private val emojiSearchDatabase: EmojiSearchDatabase = DatabaseFactory.getEmojiSearchDatabase(context)
+ private val stickerDatabase: StickerDatabase = DatabaseFactory.getStickerDatabase(context)
+
+ @WorkerThread
+ fun search(query: String): List {
+ if (query.isEmpty()) {
+ return StickerRecordReader(stickerDatabase.getRecentlyUsedStickers(RECENT_LIMIT)).readAll()
+ }
+
+ val maybeEmojiQuery: List = findStickersForEmoji(query)
+ val searchResults: List = emojiSearchDatabase.query(query, EMOJI_SEARCH_RESULTS_LIMIT)
+ .map { findStickersForEmoji(it) }
+ .flatten()
+
+ return maybeEmojiQuery + searchResults
+ }
+
+ @WorkerThread
+ private fun findStickersForEmoji(emoji: String): List {
+ val searchEmoji: String = EmojiUtil.getCanonicalRepresentation(emoji)
+
+ return EmojiUtil.getAllRepresentations(searchEmoji)
+ .filterNotNull()
+ .map { candidate -> StickerRecordReader(stickerDatabase.getStickersByEmoji(candidate)).readAll() }
+ .flatten()
+ }
+}
+
+private fun StickerRecordReader.readAll(): List {
+ val stickers: MutableList = mutableListOf()
+ use { reader ->
+ var record: StickerRecord? = reader.next
+ while (record != null) {
+ stickers.add(record)
+ record = reader.next
+ }
+ }
+ return stickers
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerSearchViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerSearchViewModel.kt
new file mode 100644
index 0000000000..29e4661d88
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyboard/sticker/StickerSearchViewModel.kt
@@ -0,0 +1,28 @@
+package org.thoughtcrime.securesms.keyboard.sticker
+
+import android.content.Context
+import androidx.lifecycle.LiveData
+import androidx.lifecycle.MutableLiveData
+import androidx.lifecycle.ViewModel
+import androidx.lifecycle.ViewModelProvider
+import org.thoughtcrime.securesms.database.model.StickerRecord
+import org.thoughtcrime.securesms.util.livedata.LiveDataUtil
+
+class StickerSearchViewModel(private val searchRepository: StickerSearchRepository) : ViewModel() {
+
+ private val searchQuery: MutableLiveData = MutableLiveData("")
+
+ val searchResults: LiveData> = LiveDataUtil.mapAsync(searchQuery) { q -> searchRepository.search(q) }
+
+ fun query(query: String) {
+ searchQuery.postValue(query)
+ }
+
+ class Factory(context: Context) : ViewModelProvider.Factory {
+ val repository = StickerSearchRepository(context)
+
+ override fun create(modelClass: Class): T {
+ return requireNotNull(modelClass.cast(StickerSearchViewModel(repository)))
+ }
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/EmojiValues.java b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/EmojiValues.java
index 0b8b93a89c..232f419689 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/EmojiValues.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/EmojiValues.java
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.keyvalue;
import android.text.TextUtils;
import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
import org.thoughtcrime.securesms.util.Util;
@@ -23,6 +24,11 @@ public class EmojiValues extends SignalStoreValues {
private static final String PREFIX = "emojiPref__";
private static final String NEXT_SCHEDULED_CHECK = PREFIX + "next_scheduled_check";
private static final String REACTIONS_LIST = PREFIX + "reactions_list";
+ private static final String SEARCH_VERSION = PREFIX + "search_version";
+ private static final String SEARCH_LANGUAGE = PREFIX + "search_language";
+ private static final String LAST_SEARCH_CHECK = PREFIX + "last_search_check";
+
+ public static final String NO_LANGUAGE = "NO_LANGUAGE";
EmojiValues(@NonNull KeyValueStore store) {
super(store);
@@ -38,11 +44,11 @@ public class EmojiValues extends SignalStoreValues {
return Collections.singletonList(REACTIONS_LIST);
}
- public long getNextScheduledCheck() {
+ public long getNextScheduledImageCheck() {
return getStore().getLong(NEXT_SCHEDULED_CHECK, 0);
}
- public void setNextScheduledCheck(long nextScheduledCheck) {
+ public void setNextScheduledImageCheck(long nextScheduledCheck) {
putLong(NEXT_SCHEDULED_CHECK, nextScheduledCheck);
}
@@ -74,4 +80,31 @@ public class EmojiValues extends SignalStoreValues {
public void setReactions(List reactions) {
putString(REACTIONS_LIST, Util.join(reactions, ","));
}
+
+ public void onSearchIndexUpdated(int version, @NonNull String language) {
+ getStore().beginWrite()
+ .putInteger(SEARCH_VERSION, version)
+ .putString(SEARCH_LANGUAGE, language)
+ .apply();
+ }
+
+ public boolean hasSearchIndex() {
+ return getSearchVersion() > 0 && getSearchLanguage() != null;
+ }
+
+ public int getSearchVersion() {
+ return getInteger(SEARCH_VERSION, 0);
+ }
+
+ public @Nullable String getSearchLanguage() {
+ return getString(SEARCH_LANGUAGE, null);
+ }
+
+ public long getLastSearchIndexCheck() {
+ return getLong(LAST_SEARCH_CHECK, 0);
+ }
+
+ public void setLastSearchIndexCheck(int time) {
+ putLong(LAST_SEARCH_CHECK, time);
+ }
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
index 4abc74c56d..61ecece9cf 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/MediaSendActivity.java
@@ -53,6 +53,10 @@ import org.thoughtcrime.securesms.components.mention.MentionAnnotation;
import org.thoughtcrime.securesms.contactshare.SimpleTextWatcher;
import org.thoughtcrime.securesms.conversation.ui.mentions.MentionsPickerViewModel;
import org.thoughtcrime.securesms.imageeditor.model.EditorModel;
+import org.thoughtcrime.securesms.keyboard.KeyboardPage;
+import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel;
+import org.thoughtcrime.securesms.keyboard.emoji.EmojiKeyboardPageFragment;
+import org.thoughtcrime.securesms.keyboard.emoji.search.EmojiSearchFragment;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.mediapreview.MediaRailAdapter;
import org.thoughtcrime.securesms.mediasend.MediaSendViewModel.HudState;
@@ -108,7 +112,10 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
ViewTreeObserver.OnGlobalLayoutListener,
MediaRailAdapter.RailItemListener,
InputAwareLayout.OnKeyboardShownListener,
- InputAwareLayout.OnKeyboardHiddenListener
+ InputAwareLayout.OnKeyboardHiddenListener,
+ EmojiKeyboardProvider.EmojiEventListener,
+ EmojiKeyboardPageFragment.Callback,
+ EmojiSearchFragment.Callback
{
private static final String TAG = Log.tag(MediaSendActivity.class);
@@ -987,17 +994,9 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
private void onEmojiToggleClicked(View v) {
if (!emojiDrawer.resolved()) {
- emojiDrawer.get().setProviders(0, new EmojiKeyboardProvider(this, new EmojiKeyboardProvider.EmojiEventListener() {
- @Override
- public void onKeyEvent(KeyEvent keyEvent) {
- getActiveInputField().dispatchKeyEvent(keyEvent);
- }
+ KeyboardPagerViewModel keyboardPagerViewModel = ViewModelProviders.of(this).get(KeyboardPagerViewModel.class);
+ keyboardPagerViewModel.setOnlyPage(KeyboardPage.EMOJI);
- @Override
- public void onEmojiSelected(String emoji) {
- getActiveInputField().insertEmoji(emoji);
- }
- }));
emojiToggle.attach(emojiDrawer.get());
}
@@ -1008,6 +1007,16 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
}
}
+ @Override
+ public void onKeyEvent(KeyEvent keyEvent) {
+ getActiveInputField().dispatchKeyEvent(keyEvent);
+ }
+
+ @Override
+ public void onEmojiSelected(String emoji) {
+ getActiveInputField().insertEmoji(emoji);
+ }
+
private @Nullable MediaSendFragment getMediaSendFragment() {
return (MediaSendFragment) getSupportFragmentManager().findFragmentByTag(TAG_SEND);
}
@@ -1029,6 +1038,20 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
overridePendingTransition(R.anim.stationary, R.anim.camera_slide_to_bottom);
}
+ @Override
+ public void openEmojiSearch() {
+ if (emojiDrawer.resolved()) {
+ emojiDrawer.get().onOpenEmojiSearch();
+ }
+ }
+
+ @Override
+ public void closeEmojiSearch() {
+ if (emojiDrawer.resolved()) {
+ emojiDrawer.get().onCloseEmojiSearch();
+ }
+ }
+
private class ComposeKeyPressedListener implements View.OnKeyListener, View.OnClickListener, TextWatcher, View.OnFocusChangeListener {
int beforeLength;
@@ -1067,7 +1090,11 @@ public class MediaSendActivity extends PassphraseRequiredActivity implements Med
public void onTextChanged(CharSequence s, int start, int before,int count) {}
@Override
- public void onFocusChange(View v, boolean hasFocus) {}
+ public void onFocusChange(View v, boolean hasFocus) {
+ if (hasFocus && hud.getCurrentInput() == emojiDrawer.get()) {
+ hud.showSoftkey(composeText);
+ }
+ }
}
private class MentionPickerPlacer implements ViewTreeObserver.OnGlobalLayoutListener {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java
index dc0c5bc524..c34449a042 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mms/AttachmentManager.java
@@ -33,7 +33,6 @@ import android.util.Pair;
import android.view.View;
import android.widget.Toast;
-import androidx.annotation.ColorInt;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat;
@@ -403,10 +402,9 @@ public class AttachmentManager {
.execute();
}
- public static void selectGif(Activity activity, int requestCode, boolean isForMms, @ColorInt int color) {
+ public static void selectGif(Activity activity, int requestCode, boolean isForMms) {
Intent intent = new Intent(activity, GiphyActivity.class);
intent.putExtra(GiphyActivity.EXTRA_IS_MMS, isForMms);
- intent.putExtra(GiphyActivity.EXTRA_COLOR, color);
activity.startActivityForResult(intent, requestCode);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java
index 2343f98235..0dcff4c3cc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java
@@ -90,7 +90,7 @@ final class ReactWithAnyEmojiAdapter extends ListAdapter
- DatabaseFactory.getStickerDatabase(getApplicationContext())
- .updateStickerLastUsedTime(sticker.getRowId(), System.currentTimeMillis())
- );
-
- finish();
- }
-
- @Override
- public void onStickerManagementClicked() {
- startActivity(StickerManagementActivity.getIntent(ImageEditorStickerSelectActivity.this));
- }
- }
- ));
-
- mediaKeyboard.setKeyboardListener(new MediaKeyboard.MediaKeyboardListener() {
- @Override
- public void onShown() {
- }
-
- @Override
- public void onHidden() {
- finish();
- }
-
- @Override
- public void onKeyboardProviderChanged(@NonNull MediaKeyboardProvider provider) {
- }
- });
-
mediaKeyboard.show();
}
+ @Override
+ public void onShown() {
+ }
+
+ @Override
+ public void onHidden() {
+ finish();
+ }
+
+ @Override
+ public void onKeyboardChanged(@NonNull KeyboardPage page) {
+ }
+
+ @Override
+ public void onStickerSelected(@NonNull StickerRecord sticker) {
+ Intent intent = new Intent();
+ intent.setData(sticker.getUri());
+ setResult(RESULT_OK, intent);
+
+ SignalExecutors.BOUNDED.execute(() -> DatabaseFactory.getStickerDatabase(getApplicationContext())
+ .updateStickerLastUsedTime(sticker.getRowId(), System.currentTimeMillis()));
+ ViewUtil.hideKeyboard(this, findViewById(android.R.id.content));
+ finish();
+ }
+
+ @Override
+ public void onStickerManagementClicked() {
+ startActivity(StickerManagementActivity.getIntent(ImageEditorStickerSelectActivity.this));
+ }
+
@Override
public boolean onOptionsItemSelected(MenuItem item) {
if (item.getItemId() == android.R.id.home) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java
index 36aff425d1..57e6cbfae4 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/search/SearchRepository.java
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CursorUtil;
+import org.thoughtcrime.securesms.util.FtsUtil;
import org.thoughtcrime.securesms.util.Util;
import java.util.ArrayList;
@@ -56,23 +57,6 @@ public class SearchRepository {
private static final String TAG = Log.tag(SearchRepository.class);
- private static final Set BANNED_CHARACTERS = new HashSet<>();
- static {
- // Several ranges of invalid ASCII characters
- for (int i = 33; i <= 47; i++) {
- BANNED_CHARACTERS.add((char) i);
- }
- for (int i = 58; i <= 64; i++) {
- BANNED_CHARACTERS.add((char) i);
- }
- for (int i = 91; i <= 96; i++) {
- BANNED_CHARACTERS.add((char) i);
- }
- for (int i = 123; i <= 126; i++) {
- BANNED_CHARACTERS.add((char) i);
- }
- }
-
private final Context context;
private final SearchDatabase searchDatabase;
private final ContactRepository contactRepository;
@@ -104,7 +88,7 @@ public class SearchRepository {
}
serialExecutor.execute(() -> {
- String cleanQuery = sanitizeQuery(query);
+ String cleanQuery = FtsUtil.sanitize(query);
Future> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery));
Future> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery));
@@ -133,7 +117,7 @@ public class SearchRepository {
serialExecutor.execute(() -> {
long startTime = System.currentTimeMillis();
- List messages = queryMessages(sanitizeQuery(query), threadId);
+ List messages = queryMessages(FtsUtil.sanitize(query), threadId);
List mentionMessages = queryMentions(sanitizeQueryAsTokens(query), threadId);
Log.d(TAG, "[ConversationQuery] " + (System.currentTimeMillis() - startTime) + " ms");
@@ -346,35 +330,13 @@ public class SearchRepository {
return list;
}
- /**
- * Unfortunately {@link DatabaseUtils#sqlEscapeString(String)} is not sufficient for our purposes.
- * MATCH queries have a separate format of their own that disallow most "special" characters.
- *
- * Also, SQLite can't search for apostrophes, meaning we can't normally find words like "I'm".
- * However, if we replace the apostrophe with a space, then the query will find the match.
- */
- private String sanitizeQuery(@NonNull String query) {
- StringBuilder out = new StringBuilder();
-
- for (int i = 0; i < query.length(); i++) {
- char c = query.charAt(i);
- if (!BANNED_CHARACTERS.contains(c)) {
- out.append(c);
- } else if (c == '\'') {
- out.append(' ');
- }
- }
-
- return out.toString();
- }
-
private @NonNull List sanitizeQueryAsTokens(@NonNull String query) {
String[] parts = query.split("\\s+");
if (parts.length > 3) {
return Collections.emptyList();
}
- return Stream.of(parts).map(this::sanitizeQuery).toList();
+ return Stream.of(parts).map(FtsUtil::sanitize).toList();
}
private static @NonNull List mergeMessagesAndMentions(@NonNull List messages, @NonNull List mentionMessages) {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerKeyboardPageAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerKeyboardPageAdapter.java
index f7473b7a83..b14fa7b93a 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerKeyboardPageAdapter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerKeyboardPageAdapter.java
@@ -25,7 +25,7 @@ import java.util.List;
* Adapter for a specific page in the sticker keyboard. Shows the stickers in a grid.
* @see StickerKeyboardPageFragment
*/
-final class StickerKeyboardPageAdapter extends RecyclerView.Adapter {
+public final class StickerKeyboardPageAdapter extends RecyclerView.Adapter {
private final GlideRequests glideRequests;
private final EventListener eventListener;
@@ -34,7 +34,7 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter stickers, @Px int stickerSize) {
+ public void setStickers(@NonNull List stickers, @Px int stickerSize) {
this.stickers.clear();
this.stickers.addAll(stickers);
@@ -77,7 +77,7 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter BANNED_CHARACTERS = new HashSet<>();
+ static {
+ // Several ranges of invalid ASCII characters
+ for (int i = 33; i <= 47; i++) {
+ BANNED_CHARACTERS.add((char) i);
+ }
+ for (int i = 58; i <= 64; i++) {
+ BANNED_CHARACTERS.add((char) i);
+ }
+ for (int i = 91; i <= 96; i++) {
+ BANNED_CHARACTERS.add((char) i);
+ }
+ for (int i = 123; i <= 126; i++) {
+ BANNED_CHARACTERS.add((char) i);
+ }
+ }
+
+ private FtsUtil() {}
+
+ /**
+ * Unfortunately {@link DatabaseUtils#sqlEscapeString(String)} is not sufficient for our purposes.
+ * MATCH queries have a separate format of their own that disallow most "special" characters.
+ *
+ * Also, SQLite can't search for apostrophes, meaning we can't normally find words like "I'm".
+ * However, if we replace the apostrophe with a space, then the query will find the match.
+ */
+ public static @NonNull String sanitize(@NonNull String query) {
+ StringBuilder out = new StringBuilder();
+
+ for (int i = 0; i < query.length(); i++) {
+ char c = query.charAt(i);
+ if (!BANNED_CHARACTERS.contains(c)) {
+ out.append(c);
+ } else if (c == '\'') {
+ out.append(' ');
+ }
+ }
+
+ return out.toString();
+ }
+
+ /**
+ * Sanitizes the string (via {@link #sanitize(String)}) and appends * at the right spots such that each token in the query will be treated as a prefix.
+ */
+ public static @NonNull String createPrefixMatchString(@NonNull String query) {
+ query = FtsUtil.sanitize(query);
+
+ return Stream.of(query.split(" "))
+ .map(String::trim)
+ .filter(s -> s.length() > 0)
+ .map(FtsUtil::fixQuotes)
+ .collect(StringBuilder::new, (sb, s) -> sb.append(s).append("* "))
+ .toString();
+ }
+
+ private static String fixQuotes(String s) {
+ return "\"" + s.replace("\"", "\"\"") + "\"";
+ }
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
index 9d9b79638d..e24d33349e 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/TextSecurePreferences.java
@@ -1292,6 +1292,6 @@ public class TextSecurePreferences {
// NEVER rename these -- they're persisted by name
public enum MediaKeyboardMode {
- EMOJI, STICKER
+ EMOJI, STICKER, GIF
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ViewExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ViewExtensions.kt
new file mode 100644
index 0000000000..517c160b2a
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/ViewExtensions.kt
@@ -0,0 +1,12 @@
+package org.thoughtcrime.securesms.util
+
+import android.view.View
+
+var View.visible: Boolean
+ get() {
+ return this.visibility == View.VISIBLE
+ }
+
+ set(value) {
+ this.visibility = if (value) View.VISIBLE else View.GONE
+ }
diff --git a/app/src/main/res/anim/slide_fade_from_bottom.xml b/app/src/main/res/anim/slide_fade_from_bottom.xml
new file mode 100644
index 0000000000..4d1bdf9a49
--- /dev/null
+++ b/app/src/main/res/anim/slide_fade_from_bottom.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/anim/slide_fade_to_bottom.xml b/app/src/main/res/anim/slide_fade_to_bottom.xml
new file mode 100644
index 0000000000..5deb15083d
--- /dev/null
+++ b/app/src/main/res/anim/slide_fade_to_bottom.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-ldrtl/search_bar_end.xml b/app/src/main/res/drawable-ldrtl/search_bar_end.xml
new file mode 100644
index 0000000000..8ee0f611ca
--- /dev/null
+++ b/app/src/main/res/drawable-ldrtl/search_bar_end.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-ldrtl/search_bar_start.xml b/app/src/main/res/drawable-ldrtl/search_bar_start.xml
new file mode 100644
index 0000000000..cf31e32384
--- /dev/null
+++ b/app/src/main/res/drawable-ldrtl/search_bar_start.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable-night/ic_backspace_24.xml b/app/src/main/res/drawable-night/ic_backspace_24.xml
new file mode 100644
index 0000000000..c5216b1402
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_backspace_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_emoji.xml b/app/src/main/res/drawable-night/ic_emoji.xml
index ed160bb6d5..e6e3acabd9 100644
--- a/app/src/main/res/drawable-night/ic_emoji.xml
+++ b/app/src/main/res/drawable-night/ic_emoji.xml
@@ -1,4 +1,4 @@
-
+
diff --git a/app/src/main/res/drawable-night/ic_gif_angry_24.xml b/app/src/main/res/drawable-night/ic_gif_angry_24.xml
new file mode 100644
index 0000000000..7a7e4d39bc
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_gif_angry_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_gif_celebrate_24.xml b/app/src/main/res/drawable-night/ic_gif_celebrate_24.xml
new file mode 100644
index 0000000000..51e0069608
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_gif_celebrate_24.xml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_gif_excited_24.xml b/app/src/main/res/drawable-night/ic_gif_excited_24.xml
new file mode 100644
index 0000000000..9473ecf313
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_gif_excited_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_gif_love_24.xml b/app/src/main/res/drawable-night/ic_gif_love_24.xml
new file mode 100644
index 0000000000..5ecc9ecee9
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_gif_love_24.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_gif_sad_24.xml b/app/src/main/res/drawable-night/ic_gif_sad_24.xml
new file mode 100644
index 0000000000..42c9d91ba3
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_gif_sad_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_gif_surprised_24.xml b/app/src/main/res/drawable-night/ic_gif_surprised_24.xml
new file mode 100644
index 0000000000..3c4c882d30
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_gif_surprised_24.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_gif_thumbsup_24.xml b/app/src/main/res/drawable-night/ic_gif_thumbsup_24.xml
new file mode 100644
index 0000000000..0eeac4c252
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_gif_thumbsup_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_gif_trending_24.xml b/app/src/main/res/drawable-night/ic_gif_trending_24.xml
new file mode 100644
index 0000000000..d23204f1b5
--- /dev/null
+++ b/app/src/main/res/drawable-night/ic_gif_trending_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable-night/ic_sticker_24.xml b/app/src/main/res/drawable-night/ic_sticker_24.xml
index 6c2e76a671..93a93a4a10 100644
--- a/app/src/main/res/drawable-night/ic_sticker_24.xml
+++ b/app/src/main/res/drawable-night/ic_sticker_24.xml
@@ -4,6 +4,6 @@
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/app/src/main/res/drawable/ic_backspace_24.xml b/app/src/main/res/drawable/ic_backspace_24.xml
new file mode 100644
index 0000000000..8615a71b28
--- /dev/null
+++ b/app/src/main/res/drawable/ic_backspace_24.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
index 943277e7a3..726a0969ee 100644
--- a/app/src/main/res/drawable/ic_emoji.xml
+++ b/app/src/main/res/drawable/ic_emoji.xml
@@ -4,6 +4,6 @@
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/app/src/main/res/drawable/ic_gif_angry_24.xml b/app/src/main/res/drawable/ic_gif_angry_24.xml
new file mode 100644
index 0000000000..f4eac9e3d6
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gif_angry_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_gif_celebrate_24.xml b/app/src/main/res/drawable/ic_gif_celebrate_24.xml
new file mode 100644
index 0000000000..c34765110d
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gif_celebrate_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_gif_excited_24.xml b/app/src/main/res/drawable/ic_gif_excited_24.xml
new file mode 100644
index 0000000000..7fbd9c490c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gif_excited_24.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_gif_love_24.xml b/app/src/main/res/drawable/ic_gif_love_24.xml
new file mode 100644
index 0000000000..c628a9520c
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gif_love_24.xml
@@ -0,0 +1,11 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_gif_sad_24.xml b/app/src/main/res/drawable/ic_gif_sad_24.xml
new file mode 100644
index 0000000000..45578262c3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gif_sad_24.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_gif_surprised_24.xml b/app/src/main/res/drawable/ic_gif_surprised_24.xml
new file mode 100644
index 0000000000..d681deb4f5
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gif_surprised_24.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_gif_thumbsup_24.xml b/app/src/main/res/drawable/ic_gif_thumbsup_24.xml
new file mode 100644
index 0000000000..4c7076e776
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gif_thumbsup_24.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_gif_trending_24.xml b/app/src/main/res/drawable/ic_gif_trending_24.xml
new file mode 100644
index 0000000000..1b7e9f2264
--- /dev/null
+++ b/app/src/main/res/drawable/ic_gif_trending_24.xml
@@ -0,0 +1,10 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_sticker_24.xml b/app/src/main/res/drawable/ic_sticker_24.xml
index 94189573f9..5612e6fda4 100644
--- a/app/src/main/res/drawable/ic_sticker_24.xml
+++ b/app/src/main/res/drawable/ic_sticker_24.xml
@@ -4,6 +4,6 @@
android:viewportWidth="24"
android:viewportHeight="24">
diff --git a/app/src/main/res/drawable/keyboard_pager_fragment_category_selected.xml b/app/src/main/res/drawable/keyboard_pager_fragment_category_selected.xml
new file mode 100644
index 0000000000..0cbd7f68a7
--- /dev/null
+++ b/app/src/main/res/drawable/keyboard_pager_fragment_category_selected.xml
@@ -0,0 +1,13 @@
+
+
+ -
+
+
-
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/keyboard_pager_fragment_emoji_icon.xml b/app/src/main/res/drawable/keyboard_pager_fragment_emoji_icon.xml
new file mode 100644
index 0000000000..f2a6adffce
--- /dev/null
+++ b/app/src/main/res/drawable/keyboard_pager_fragment_emoji_icon.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/keyboard_pager_fragment_gif_icon.xml b/app/src/main/res/drawable/keyboard_pager_fragment_gif_icon.xml
new file mode 100644
index 0000000000..dc2a216682
--- /dev/null
+++ b/app/src/main/res/drawable/keyboard_pager_fragment_gif_icon.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/keyboard_pager_fragment_sticker_icon.xml b/app/src/main/res/drawable/keyboard_pager_fragment_sticker_icon.xml
new file mode 100644
index 0000000000..6b56c88055
--- /dev/null
+++ b/app/src/main/res/drawable/keyboard_pager_fragment_sticker_icon.xml
@@ -0,0 +1,10 @@
+
+
+
+
+
\ 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/layout/emoji_page_view_search.xml b/app/src/main/res/layout/emoji_page_view_search.xml
new file mode 100644
index 0000000000..654bc22fa4
--- /dev/null
+++ b/app/src/main/res/layout/emoji_page_view_search.xml
@@ -0,0 +1,13 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/emoji_search_fragment.xml b/app/src/main/res/layout/emoji_search_fragment.xml
new file mode 100644
index 0000000000..e01d28cf58
--- /dev/null
+++ b/app/src/main/res/layout/emoji_search_fragment.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/emoji_search_result_display_item.xml b/app/src/main/res/layout/emoji_search_result_display_item.xml
new file mode 100644
index 0000000000..e2b967cf42
--- /dev/null
+++ b/app/src/main/res/layout/emoji_search_result_display_item.xml
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/gif_keyboard_page_fragment.xml b/app/src/main/res/layout/gif_keyboard_page_fragment.xml
new file mode 100644
index 0000000000..1f2e3ac08a
--- /dev/null
+++ b/app/src/main/res/layout/gif_keyboard_page_fragment.xml
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/giphy_activity.xml b/app/src/main/res/layout/giphy_activity.xml
index b3cf06abad..0e54a4d28f 100644
--- a/app/src/main/res/layout/giphy_activity.xml
+++ b/app/src/main/res/layout/giphy_activity.xml
@@ -2,8 +2,7 @@
+ android:layout_height="match_parent">
+ android:background="@color/signal_background_primary">
-
+ android:background="@color/signal_background_primary"
+ android:minHeight="?attr/actionBarSize"
+ app:layout_scrollFlags="scroll|enterAlways">
+
+
+
+
diff --git a/app/src/main/res/layout/giphy_mp4.xml b/app/src/main/res/layout/giphy_mp4.xml
index dd985c0f65..226483d65f 100644
--- a/app/src/main/res/layout/giphy_mp4.xml
+++ b/app/src/main/res/layout/giphy_mp4.xml
@@ -1,13 +1,21 @@
-
+ android:layout_height="wrap_content">
-
+ android:layout_height="0dp"
+ android:layout_margin="3dp">
-
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/giphy_mp4_fragment.xml b/app/src/main/res/layout/giphy_mp4_fragment.xml
index 9bd80225bd..198e362954 100644
--- a/app/src/main/res/layout/giphy_mp4_fragment.xml
+++ b/app/src/main/res/layout/giphy_mp4_fragment.xml
@@ -33,7 +33,9 @@
android:id="@+id/giphy_recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
+ android:clipToPadding="false"
android:orientation="vertical"
+ android:padding="5dp"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
tools:listitem="@layout/giphy_mp4" />
diff --git a/app/src/main/res/layout/keyboard_pager_category_icon.xml b/app/src/main/res/layout/keyboard_pager_category_icon.xml
new file mode 100644
index 0000000000..e9183e073e
--- /dev/null
+++ b/app/src/main/res/layout/keyboard_pager_category_icon.xml
@@ -0,0 +1,24 @@
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/layout/keyboard_pager_emoji_page_fragment.xml b/app/src/main/res/layout/keyboard_pager_emoji_page_fragment.xml
new file mode 100644
index 0000000000..cc3ffa9732
--- /dev/null
+++ b/app/src/main/res/layout/keyboard_pager_emoji_page_fragment.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/keyboard_pager_fragment.xml b/app/src/main/res/layout/keyboard_pager_fragment.xml
new file mode 100644
index 0000000000..f8925985cd
--- /dev/null
+++ b/app/src/main/res/layout/keyboard_pager_fragment.xml
@@ -0,0 +1,72 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ 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..59ccfd9415
--- /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/keyboard_pager_sticker_page_fragment.xml b/app/src/main/res/layout/keyboard_pager_sticker_page_fragment.xml
new file mode 100644
index 0000000000..54b07db131
--- /dev/null
+++ b/app/src/main/res/layout/keyboard_pager_sticker_page_fragment.xml
@@ -0,0 +1,77 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/media_keyboard.xml b/app/src/main/res/layout/media_keyboard.xml
index c3d721c254..d05ff919e3 100644
--- a/app/src/main/res/layout/media_keyboard.xml
+++ b/app/src/main/res/layout/media_keyboard.xml
@@ -1,122 +1,13 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+ android:layout_height="match_parent" />
\ No newline at end of file
diff --git a/app/src/main/res/layout/media_keyboard_bottom_tab_item.xml b/app/src/main/res/layout/media_keyboard_bottom_tab_item.xml
index d54055a4b9..7baaf1c73e 100644
--- a/app/src/main/res/layout/media_keyboard_bottom_tab_item.xml
+++ b/app/src/main/res/layout/media_keyboard_bottom_tab_item.xml
@@ -1,33 +1,22 @@
-
+ android:background="?selectableItemBackgroundBorderless">
+ android:id="@+id/category_icon_selected"
+ android:layout_width="48dp"
+ android:layout_height="48dp"
+ android:background="@drawable/keyboard_pager_fragment_category_selected" />
-
-
-
-
-
+
+
diff --git a/app/src/main/res/layout/react_with_any_emoji_tab.xml b/app/src/main/res/layout/react_with_any_emoji_tab.xml
index 50e1792790..d4e9fcaacb 100644
--- a/app/src/main/res/layout/react_with_any_emoji_tab.xml
+++ b/app/src/main/res/layout/react_with_any_emoji_tab.xml
@@ -1,11 +1,18 @@
-
+ android:layout_margin="8dp">
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/react_with_any_emoji_tabs.xml b/app/src/main/res/layout/react_with_any_emoji_tabs.xml
index 1bd5496592..ea90a41f27 100644
--- a/app/src/main/res/layout/react_with_any_emoji_tabs.xml
+++ b/app/src/main/res/layout/react_with_any_emoji_tabs.xml
@@ -1,5 +1,5 @@
-
+ android:layout_gravity="top"
+ android:background="@color/signal_inverse_transparent_05" />
-
+
-
+
-
+
-
+
-
+
+
+
+
diff --git a/app/src/main/res/layout/sticker_keyboard_page.xml b/app/src/main/res/layout/sticker_keyboard_page.xml
index ddfee4d855..870b54bbe3 100644
--- a/app/src/main/res/layout/sticker_keyboard_page.xml
+++ b/app/src/main/res/layout/sticker_keyboard_page.xml
@@ -1,8 +1,10 @@
-
+ android:paddingEnd="4dp"
+ android:scrollIndicators="top|bottom"
+ tools:ignore="UnusedAttribute" />
diff --git a/app/src/main/res/layout/sticker_keyboard_page_list_item.xml b/app/src/main/res/layout/sticker_keyboard_page_list_item.xml
index 379a309777..40882fba07 100644
--- a/app/src/main/res/layout/sticker_keyboard_page_list_item.xml
+++ b/app/src/main/res/layout/sticker_keyboard_page_list_item.xml
@@ -1,6 +1,8 @@
-
+ android:layout_height="wrap_content"
+ android:background="?selectableItemBackground"
+ tools:srcCompat="@tools:sample/avatars" />
diff --git a/app/src/main/res/layout/sticker_search_dialog_fragment.xml b/app/src/main/res/layout/sticker_search_dialog_fragment.xml
new file mode 100644
index 0000000000..f9679709cd
--- /dev/null
+++ b/app/src/main/res/layout/sticker_search_dialog_fragment.xml
@@ -0,0 +1,59 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-night/dark_colors.xml b/app/src/main/res/values-night/dark_colors.xml
index 3a93f917a5..3f2daaf851 100644
--- a/app/src/main/res/values-night/dark_colors.xml
+++ b/app/src/main/res/values-night/dark_colors.xml
@@ -26,8 +26,8 @@
@color/core_grey_15
@color/core_grey_25
- @color/core_grey_10
- @color/core_grey_45
+ @color/core_white
+ @color/core_grey_15
@color/core_ultramarine_light
@color/transparent_black_15
@@ -141,4 +141,5 @@
@color/core_grey_80
@color/core_grey_80
+ @color/transparent_white_10
diff --git a/app/src/main/res/values/animations.xml b/app/src/main/res/values/animations.xml
index b5137d00f5..2b0cb09d55 100644
--- a/app/src/main/res/values/animations.xml
+++ b/app/src/main/res/values/animations.xml
@@ -5,6 +5,11 @@
- @anim/slide_to_top
+
+
+
+