Add Emoji Search, Sticker Search, and GIF Keyboard.
Co-authored-by: Alex Hart <alex@signal.org> Co-authored-by: Cody Henthorne <cody@signal.org> Co-authored-by: Greyson Parrelli<greyson@signal.org>
This commit is contained in:
parent
66c3b1388a
commit
08e86b8c82
119 changed files with 3545 additions and 721 deletions
|
@ -492,7 +492,7 @@
|
|||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".scribbles.ImageEditorStickerSelectActivity"
|
||||
android:theme="@style/TextSecure.DarkTheme"
|
||||
android:theme="@style/Signal.DayNight.NoActionBar"
|
||||
android:configChanges="touchscreen|keyboard|keyboardHidden|orientation|screenLayout|screenSize"/>
|
||||
|
||||
<activity android:name=".profiles.edit.EditProfileActivity"
|
||||
|
|
|
@ -44,6 +44,7 @@ import org.thoughtcrime.securesms.emoji.EmojiSource;
|
|||
import org.thoughtcrime.securesms.gcm.FcmJobService;
|
||||
import org.thoughtcrime.securesms.jobs.CreateSignedPreKeyJob;
|
||||
import org.thoughtcrime.securesms.jobs.DownloadLatestEmojiDataJob;
|
||||
import org.thoughtcrime.securesms.jobs.EmojiSearchIndexDownloadJob;
|
||||
import org.thoughtcrime.securesms.jobs.FcmRefreshJob;
|
||||
import org.thoughtcrime.securesms.jobs.GroupV1MigrationJob;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceContactUpdateJob;
|
||||
|
@ -156,10 +157,11 @@ public class ApplicationContext extends MultiDexApplication implements AppForegr
|
|||
.addNonBlocking(StorageSyncHelper::scheduleRoutineSync)
|
||||
.addNonBlocking(() -> 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");
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)));
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<EmojiPageViewGridAdapter.EmojiViewHolder> implements PopupWindow.OnDismissListener {
|
||||
|
||||
private final List<Emoji> 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<Emoji> 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<EmojiPageView
|
|||
variationSelectorListener.onVariationSelectorStateChanged(false);
|
||||
}
|
||||
|
||||
static class EmojiViewHolder extends RecyclerView.ViewHolder {
|
||||
static class SearchModel implements MappingModel<SearchModel> {
|
||||
@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<SearchModel> {
|
||||
public SearchViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void bind(@NonNull @NotNull SearchModel model) {
|
||||
}
|
||||
}
|
||||
|
||||
static class EmojiModel implements MappingModel<EmojiModel> {
|
||||
|
||||
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<EmojiModel> {
|
||||
|
||||
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 {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,22 +16,19 @@ public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKey
|
|||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
private final boolean highlightTop;
|
||||
|
||||
private TabIconProvider tabIconProvider;
|
||||
private int activePosition;
|
||||
private int count;
|
||||
|
||||
public MediaKeyboardBottomTabAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean highlightTop) {
|
||||
public MediaKeyboardBottomTabAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.highlightTop = highlightTop;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull MediaKeyboardBottomTabViewHolder onCreateViewHolder(@NonNull ViewGroup viewGroup, int i) {
|
||||
return new MediaKeyboardBottomTabViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.media_keyboard_bottom_tab_item, viewGroup, false),
|
||||
highlightTop);
|
||||
return new MediaKeyboardBottomTabViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.media_keyboard_bottom_tab_item, viewGroup, false));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -64,18 +61,12 @@ public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKey
|
|||
static class MediaKeyboardBottomTabViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final ImageView image;
|
||||
private final View indicator;
|
||||
private final View imageSelected;
|
||||
|
||||
public MediaKeyboardBottomTabViewHolder(@NonNull View itemView, boolean highlightTop) {
|
||||
public MediaKeyboardBottomTabViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
View indicatorTop = itemView.findViewById(R.id.media_keyboard_top_tab_indicator);
|
||||
View indicatorBottom = itemView.findViewById(R.id.media_keyboard_bottom_tab_indicator);
|
||||
|
||||
this.image = itemView.findViewById(R.id.media_keyboard_bottom_tab_image);
|
||||
this.indicator = highlightTop ? indicatorTop : indicatorBottom;
|
||||
|
||||
this.indicator.setVisibility(View.VISIBLE);
|
||||
this.image = itemView.findViewById(R.id.category_icon);
|
||||
this.imageSelected = itemView.findViewById(R.id.category_icon_selected);
|
||||
}
|
||||
|
||||
void bind(@NonNull GlideRequests glideRequests,
|
||||
|
@ -86,9 +77,7 @@ public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKey
|
|||
{
|
||||
tabIconProvider.loadCategoryTabIcon(glideRequests, image, index);
|
||||
image.setAlpha(selected ? 1 : 0.5f);
|
||||
image.setSelected(selected);
|
||||
|
||||
indicator.setVisibility(selected ? View.VISIBLE : View.INVISIBLE);
|
||||
imageSelected.setSelected(selected);
|
||||
|
||||
itemView.setOnClickListener(v -> eventListener.onTabSelected(index));
|
||||
}
|
||||
|
@ -98,7 +87,7 @@ public class MediaKeyboardBottomTabAdapter extends RecyclerView.Adapter<MediaKey
|
|||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
public interface EventListener {
|
||||
void onTabSelected(int index);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,6 +33,10 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
|||
private final String preferenceName;
|
||||
private final LinkedHashSet<String> 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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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<String> query(@NonNull String query, int limit) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String matchString = FtsUtil.createPrefixMatchString(query);
|
||||
List<String> 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<EmojiSearchData> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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();
|
||||
|
|
|
@ -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<String> tags;
|
||||
|
||||
public EmojiSearchData() {}
|
||||
|
||||
public @NonNull String getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
public @NonNull List<String> getTags() {
|
||||
return tags;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -52,6 +52,10 @@ public final class GiphyMp4ViewModel extends ViewModel {
|
|||
.toList()));
|
||||
}
|
||||
|
||||
LiveData<PagedData<GiphyImage>> getPagedData() {
|
||||
return pagedData;
|
||||
}
|
||||
|
||||
public void updateSearchQuery(@Nullable String query) {
|
||||
if (!Objects.equals(query, this.query)) {
|
||||
this.query = query;
|
||||
|
|
|
@ -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() {}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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<EmojiSearchData> 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<EmojiSearchData> 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<String> 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<String> languages;
|
||||
|
||||
public Manifest() {}
|
||||
|
||||
public int getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
public @NonNull List<String> getLanguages() {
|
||||
return languages != null ? languages : Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
public static final class Factory implements Job.Factory<EmojiSearchIndexDownloadJob> {
|
||||
@Override
|
||||
public @NonNull EmojiSearchIndexDownloadJob create(@NonNull Parameters parameters, @NonNull Data data) {
|
||||
return new EmojiSearchIndexDownloadJob(parameters);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package org.thoughtcrime.securesms.keyboard
|
||||
|
||||
enum class KeyboardPage {
|
||||
EMOJI,
|
||||
STICKER,
|
||||
GIF
|
||||
}
|
|
@ -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<T : KeyboardPageCategoryIconMappingModel<T>> : MappingModel<T> {
|
||||
val key: String
|
||||
val selected: Boolean
|
||||
|
||||
fun getIcon(context: Context): Drawable
|
||||
}
|
||||
|
||||
class KeyboardPageCategoryIconViewHolder<T : KeyboardPageCategoryIconMappingModel<T>>(itemView: View, private val onPageSelected: (String) -> Unit) : MappingViewHolder<T>(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
|
||||
}
|
||||
}
|
|
@ -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<KClass<*>, 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<MediaKeyboard.MediaKeyboardListener>()?.onKeyboardChanged(page)
|
||||
}
|
||||
|
||||
private fun displayEmojiPage() = displayPage(::EmojiKeyboardPageFragment)
|
||||
|
||||
private fun displayGifPage() = displayPage(::GifKeyboardPageFragment)
|
||||
|
||||
private fun displayStickerPage() = displayPage(::StickerKeyboardPageFragment)
|
||||
|
||||
private inline fun <reified F : Fragment> 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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<KeyboardPage>
|
||||
private val pages: DefaultValueLiveData<Set<KeyboardPage>>
|
||||
|
||||
init {
|
||||
val startingPages: MutableSet<KeyboardPage> = 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<KeyboardPage> = page
|
||||
fun pages(): LiveData<Set<KeyboardPage>> = 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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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 <reified T> Fragment.findListener(): T? {
|
||||
var parent: Fragment? = parentFragment
|
||||
while (parent != null) {
|
||||
if (parent is T) {
|
||||
return parent
|
||||
}
|
||||
parent = parent.parentFragment
|
||||
}
|
||||
|
||||
return requireActivity() as? T
|
||||
}
|
|
@ -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<EmojiPageMappingModel>(emojiPageView) {
|
||||
|
||||
override fun bind(model: EmojiPageMappingModel) {
|
||||
emojiPageView.bindSearchableAdapter(model.emojiPageModel)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<EmojiKeyboardPageCategoryMappingModel.RecentsMappingModel>(v, onPageSelected) }, R.layout.keyboard_pager_category_icon))
|
||||
registerFactory(EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel::class.java, LayoutFactory({ v -> KeyboardPageCategoryIconViewHolder<EmojiKeyboardPageCategoryMappingModel.EmojiCategoryMappingModel>(v, onPageSelected) }, R.layout.keyboard_pager_category_icon))
|
||||
}
|
||||
}
|
|
@ -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<EmojiKeyboardPageCategoryMappingModel> {
|
||||
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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<String>(getStartingTab())
|
||||
|
||||
val selectedKey: LiveData<String>
|
||||
get() = internalSelectedKey
|
||||
|
||||
val categories: LiveData<MappingModelList> = 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<MappingModelList> = 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<EmojiPageMappingModel> {
|
||||
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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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<String> = emojiSearchDatabase.query(query, EMOJI_SEARCH_LIMIT)
|
||||
|
||||
val variationMap: Map<String, String> = EmojiSource.latest.variationMap
|
||||
val emojiVariationSets: MutableMap<String, LinkedHashSet<String>> = mutableMapOf()
|
||||
|
||||
variationMap
|
||||
.filterKeys { emoji.contains(it) }
|
||||
.forEach { (variation, canonical) ->
|
||||
val set: LinkedHashSet<String> = emojiVariationSets.getOrDefault(canonical, linkedSetOf())
|
||||
|
||||
set.add(variation)
|
||||
emojiVariationSets[canonical] = set
|
||||
}
|
||||
|
||||
val displayEmoji: List<Emoji> = emoji.map { canonical ->
|
||||
val variationSet: LinkedHashSet<String> = linkedSetOf(canonical).apply {
|
||||
addAll(emojiVariationSets.getOrDefault(canonical, linkedSetOf()))
|
||||
}
|
||||
|
||||
Emoji(variationSet.toList())
|
||||
}
|
||||
|
||||
consumer(EmojiSearchResultsPageModel(emoji, displayEmoji))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class EmojiSearchResultsPageModel(
|
||||
private val emoji: List<String>,
|
||||
private val displayEmoji: List<Emoji>
|
||||
) : EmojiPageModel {
|
||||
override fun getIconAttr(): Int = -1
|
||||
|
||||
override fun getEmoji(): List<String> = emoji
|
||||
|
||||
override fun getDisplayEmoji(): List<Emoji> = displayEmoji
|
||||
|
||||
override fun getSpriteUri(): Uri? = null
|
||||
|
||||
override fun isDynamic(): Boolean = false
|
||||
}
|
||||
}
|
|
@ -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<EmojiPageModel>()
|
||||
|
||||
val pageModel: LiveData<EmojiPageModel> = internalPageModel
|
||||
|
||||
init {
|
||||
onQueryChanged("")
|
||||
}
|
||||
|
||||
fun onQueryChanged(query: String) {
|
||||
repository.submitQuery(query, internalPageModel::postValue)
|
||||
}
|
||||
|
||||
class Factory(private val repository: EmojiSearchRepository) : ViewModelProvider.Factory {
|
||||
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(EmojiSearchViewModel(repository)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<Host>() ?: 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<View>(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<GifQuickSearch> = 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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package org.thoughtcrime.securesms.keyboard.gif
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class GifKeyboardPageViewModel : ViewModel() {
|
||||
var selectedTab: GifQuickSearchOption = GifQuickSearchOption.TRENDING
|
||||
}
|
|
@ -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<GifQuickSearch> {
|
||||
override fun areItemsTheSame(newItem: GifQuickSearch): Boolean {
|
||||
return gifQuickSearchOption == newItem.gifQuickSearchOption
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: GifQuickSearch): Boolean {
|
||||
return selected == newItem.selected
|
||||
}
|
||||
}
|
|
@ -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<GifQuickSearch>(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) }
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<GifQuickSearchOption> by lazy { values().sortedBy { it.rank } }
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
package org.thoughtcrime.securesms.keyboard.sticker
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
class StickerKeyboardPageViewModel : ViewModel() {
|
||||
var selectedTab: Int = 0
|
||||
}
|
|
@ -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<StickerKeyboardProvider.StickerEventListener>()?.onStickerSelected(sticker)
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
|
||||
override fun onStickerLongClicked(targetView: View) = Unit
|
||||
}
|
|
@ -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<StickerRecord> {
|
||||
if (query.isEmpty()) {
|
||||
return StickerRecordReader(stickerDatabase.getRecentlyUsedStickers(RECENT_LIMIT)).readAll()
|
||||
}
|
||||
|
||||
val maybeEmojiQuery: List<StickerRecord> = findStickersForEmoji(query)
|
||||
val searchResults: List<StickerRecord> = emojiSearchDatabase.query(query, EMOJI_SEARCH_RESULTS_LIMIT)
|
||||
.map { findStickersForEmoji(it) }
|
||||
.flatten()
|
||||
|
||||
return maybeEmojiQuery + searchResults
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private fun findStickersForEmoji(emoji: String): List<StickerRecord> {
|
||||
val searchEmoji: String = EmojiUtil.getCanonicalRepresentation(emoji)
|
||||
|
||||
return EmojiUtil.getAllRepresentations(searchEmoji)
|
||||
.filterNotNull()
|
||||
.map { candidate -> StickerRecordReader(stickerDatabase.getStickersByEmoji(candidate)).readAll() }
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
private fun StickerRecordReader.readAll(): List<StickerRecord> {
|
||||
val stickers: MutableList<StickerRecord> = mutableListOf()
|
||||
use { reader ->
|
||||
var record: StickerRecord? = reader.next
|
||||
while (record != null) {
|
||||
stickers.add(record)
|
||||
record = reader.next
|
||||
}
|
||||
}
|
||||
return stickers
|
||||
}
|
|
@ -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<String> = MutableLiveData("")
|
||||
|
||||
val searchResults: LiveData<List<StickerRecord>> = 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 <T : ViewModel?> create(modelClass: Class<T>): T {
|
||||
return requireNotNull(modelClass.cast(StickerSearchViewModel(repository)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
@ -90,7 +90,7 @@ final class ReactWithAnyEmojiAdapter extends ListAdapter<ReactWithAnyEmojiPage,
|
|||
}
|
||||
|
||||
private EmojiPageView createEmojiPageView(@NonNull Context context) {
|
||||
return new EmojiPageView(context, emojiEventListener, variationSelectorListener, true);
|
||||
return new EmojiPageView(context, emojiEventListener, variationSelectorListener, true, null);
|
||||
}
|
||||
|
||||
static abstract class ReactWithAnyEmojiPageViewHolder extends RecyclerView.ViewHolder implements ScrollableChild {
|
||||
|
|
|
@ -1,76 +1,77 @@
|
|||
package org.thoughtcrime.securesms.scribbles;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.view.MenuItem;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.FragmentActivity;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.app.AppCompatDelegate;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPagerViewModel;
|
||||
import org.thoughtcrime.securesms.stickers.StickerKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.stickers.StickerManagementActivity;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
public final class ImageEditorStickerSelectActivity extends FragmentActivity {
|
||||
public final class ImageEditorStickerSelectActivity extends AppCompatActivity implements StickerKeyboardProvider.StickerEventListener, MediaKeyboard.MediaKeyboardListener {
|
||||
|
||||
@Override
|
||||
protected void attachBaseContext(@NonNull Context newBase) {
|
||||
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
|
||||
super.attachBaseContext(newBase);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
|
||||
setContentView(R.layout.scribble_select_new_sticker_activity);
|
||||
|
||||
KeyboardPagerViewModel keyboardPagerViewModel = ViewModelProviders.of(this).get(KeyboardPagerViewModel.class);
|
||||
keyboardPagerViewModel.setOnlyPage(KeyboardPage.STICKER);
|
||||
|
||||
MediaKeyboard mediaKeyboard = findViewById(R.id.emoji_drawer);
|
||||
|
||||
mediaKeyboard.setProviders(0, new StickerKeyboardProvider(this, new StickerKeyboardProvider.StickerEventListener() {
|
||||
@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())
|
||||
);
|
||||
|
||||
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) {
|
||||
|
|
|
@ -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<Character> 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<List<Recipient>> contacts = parallelExecutor.submit(() -> queryContacts(cleanQuery));
|
||||
Future<List<ThreadRecord>> conversations = parallelExecutor.submit(() -> queryConversations(cleanQuery));
|
||||
|
@ -133,7 +117,7 @@ public class SearchRepository {
|
|||
|
||||
serialExecutor.execute(() -> {
|
||||
long startTime = System.currentTimeMillis();
|
||||
List<MessageResult> messages = queryMessages(sanitizeQuery(query), threadId);
|
||||
List<MessageResult> messages = queryMessages(FtsUtil.sanitize(query), threadId);
|
||||
List<MessageResult> 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<String> 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<MessageResult> mergeMessagesAndMentions(@NonNull List<MessageResult> messages, @NonNull List<MessageResult> mentionMessages) {
|
||||
|
|
|
@ -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<StickerKeyboardPageAdapter.StickerKeyboardPageViewHolder> {
|
||||
public final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeyboardPageAdapter.StickerKeyboardPageViewHolder> {
|
||||
|
||||
private final GlideRequests glideRequests;
|
||||
private final EventListener eventListener;
|
||||
|
@ -34,7 +34,7 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
|||
|
||||
private int stickerSize;
|
||||
|
||||
StickerKeyboardPageAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean allowApngAnimation) {
|
||||
public StickerKeyboardPageAdapter(@NonNull GlideRequests glideRequests, @NonNull EventListener eventListener, boolean allowApngAnimation) {
|
||||
this.glideRequests = glideRequests;
|
||||
this.eventListener = eventListener;
|
||||
this.allowApngAnimation = allowApngAnimation;
|
||||
|
@ -68,7 +68,7 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
|||
return stickers.size();
|
||||
}
|
||||
|
||||
void setStickers(@NonNull List<StickerRecord> stickers, @Px int stickerSize) {
|
||||
public void setStickers(@NonNull List<StickerRecord> stickers, @Px int stickerSize) {
|
||||
this.stickers.clear();
|
||||
this.stickers.addAll(stickers);
|
||||
|
||||
|
@ -77,7 +77,7 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
|||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
void setStickerSize(@Px int stickerSize) {
|
||||
public void setStickerSize(@Px int stickerSize) {
|
||||
this.stickerSize = stickerSize;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
@ -131,7 +131,7 @@ final class StickerKeyboardPageAdapter extends RecyclerView.Adapter<StickerKeybo
|
|||
}
|
||||
}
|
||||
|
||||
interface EventListener {
|
||||
public interface EventListener {
|
||||
void onStickerClicked(@NonNull StickerRecord sticker);
|
||||
void onStickerLongClicked(@NonNull View targetView);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,71 @@
|
|||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.database.DatabaseUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
|
||||
public final class FtsUtil {
|
||||
private static final Set<Character> 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("\"", "\"\"") + "\"";
|
||||
}
|
||||
}
|
|
@ -1292,6 +1292,6 @@ public class TextSecurePreferences {
|
|||
|
||||
// NEVER rename these -- they're persisted by name
|
||||
public enum MediaKeyboardMode {
|
||||
EMOJI, STICKER
|
||||
EMOJI, STICKER, GIF
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
12
app/src/main/res/anim/slide_fade_from_bottom.xml
Normal file
12
app/src/main/res/anim/slide_fade_from_bottom.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:fromYDelta="33%"
|
||||
android:toYDelta="0%" />
|
||||
|
||||
<alpha
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:fromAlpha="0.0"
|
||||
android:toAlpha="1.0" />
|
||||
</set>
|
12
app/src/main/res/anim/slide_fade_to_bottom.xml
Normal file
12
app/src/main/res/anim/slide_fade_to_bottom.xml
Normal file
|
@ -0,0 +1,12 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<set xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<translate
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:fromYDelta="0%"
|
||||
android:toYDelta="33%" />
|
||||
|
||||
<alpha
|
||||
android:duration="@android:integer/config_shortAnimTime"
|
||||
android:fromAlpha="1.0"
|
||||
android:toAlpha="0.0" />
|
||||
</set>
|
5
app/src/main/res/drawable-ldrtl/search_bar_end.xml
Normal file
5
app/src/main/res/drawable-ldrtl/search_bar_end.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/signal_background_primary" />
|
||||
<corners android:topLeftRadius="100dp" android:bottomLeftRadius="100dp" />
|
||||
</shape>
|
5
app/src/main/res/drawable-ldrtl/search_bar_start.xml
Normal file
5
app/src/main/res/drawable-ldrtl/search_bar_start.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/signal_background_primary" />
|
||||
<corners android:topRightRadius="100dp" android:bottomRightRadius="100dp" />
|
||||
</shape>
|
10
app/src/main/res/drawable-night/ic_backspace_24.xml
Normal file
10
app/src/main/res/drawable-night/ic_backspace_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M22.6,4.535A2.719,2.719 0,0 0,21.465 3.4,4.344 4.344,0 0,0 19.154,3H8.846a4.344,4.344 0,0 0,-2.311 0.4,2.7 2.7,0 0,0 -0.947,0.88L0,12l5.588,7.719a2.7,2.7 0,0 0,0.947 0.88,4.344 4.344,0 0,0 2.311,0.4H19.154a4.344,4.344 0,0 0,2.311 -0.4A2.719,2.719 0,0 0,22.6 19.465a4.344,4.344 0,0 0,0.4 -2.311V6.846A4.344,4.344 0,0 0,22.6 4.535ZM19.061,16 L18,17.061l-4,-4 -4,4L8.939,16l4,-4 -4,-4L10,6.939l4,4 4,-4L19.061,8l-4,4Z"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -1,4 +1,4 @@
|
|||
<vector android:height="24dp" android:viewportHeight="28"
|
||||
android:viewportWidth="28" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FFBBBDBE" android:pathData="M14,2C7.6,1.8 2.2,6.9 2,13.3c0,0.2 0,0.4 0,0.7c-0.2,6.4 4.9,11.8 11.3,12c0.2,0 0.4,0 0.7,0c6.4,0.2 11.8,-4.9 12,-11.3c0,-0.2 0,-0.4 0,-0.7c0.2,-6.4 -4.9,-11.8 -11.3,-12C14.4,2 14.2,2 14,2zM17.6,9.5c0.9,0 1.6,1 1.6,2.2S18.5,14 17.6,14S16,13 16,11.8S16.7,9.5 17.6,9.5zM10.4,9.5c0.9,0 1.6,1 1.6,2.2S11.3,14 10.4,14s-1.6,-1 -1.6,-2.2S9.5,9.5 10.4,9.5zM20,18.3c-2.5,3.3 -7.2,3.9 -10.5,1.4c-0.5,-0.4 -1,-0.9 -1.4,-1.4c-0.3,-0.3 -0.2,-0.8 0.1,-1.1C8.5,16.9 9,17 9.2,17.3c0,0 0.1,0.1 0.1,0.1c1.1,1.5 2.8,2.3 4.7,2.3c1.9,0 3.6,-0.8 4.7,-2.3c0.2,-0.3 0.7,-0.4 1,-0.2S20.2,17.9 20,18.3L20,18.3z"/>
|
||||
<path android:fillColor="@color/signal_inverse_primary" android:pathData="M14,2C7.6,1.8 2.2,6.9 2,13.3c0,0.2 0,0.4 0,0.7c-0.2,6.4 4.9,11.8 11.3,12c0.2,0 0.4,0 0.7,0c6.4,0.2 11.8,-4.9 12,-11.3c0,-0.2 0,-0.4 0,-0.7c0.2,-6.4 -4.9,-11.8 -11.3,-12C14.4,2 14.2,2 14,2zM17.6,9.5c0.9,0 1.6,1 1.6,2.2S18.5,14 17.6,14S16,13 16,11.8S16.7,9.5 17.6,9.5zM10.4,9.5c0.9,0 1.6,1 1.6,2.2S11.3,14 10.4,14s-1.6,-1 -1.6,-2.2S9.5,9.5 10.4,9.5zM20,18.3c-2.5,3.3 -7.2,3.9 -10.5,1.4c-0.5,-0.4 -1,-0.9 -1.4,-1.4c-0.3,-0.3 -0.2,-0.8 0.1,-1.1C8.5,16.9 9,17 9.2,17.3c0,0 0.1,0.1 0.1,0.1c1.1,1.5 2.8,2.3 4.7,2.3c1.9,0 3.6,-0.8 4.7,-2.3c0.2,-0.3 0.7,-0.4 1,-0.2S20.2,17.9 20,18.3L20,18.3z"/>
|
||||
</vector>
|
||||
|
|
10
app/src/main/res/drawable-night/ic_gif_angry_24.xml
Normal file
10
app/src/main/res/drawable-night/ic_gif_angry_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M7.742,1.7529C9.0953,1.2148 10.5442,0.9586 12,1C13.4558,0.9586 14.9047,1.2148 16.258,1.7529C17.6113,2.2911 18.8405,3.0998 19.8704,4.1296C20.9002,5.1594 21.7089,6.3886 22.2471,7.742C22.7852,9.0953 23.0414,10.5442 23,12C23.0429,13.4562 22.7877,14.9057 22.25,16.2597C21.7124,17.6137 20.9037,18.8435 19.8736,19.8736C18.8435,20.9037 17.6137,21.7124 16.2597,22.25C14.9057,22.7877 13.4562,23.0429 12,23C10.5442,23.0414 9.0953,22.7852 7.742,22.2471C6.3886,21.7089 5.1594,20.9002 4.1296,19.8704C3.0998,18.8405 2.2911,17.6113 1.7529,16.258C1.2148,14.9047 0.9586,13.4558 1,12C0.9586,10.5442 1.2148,9.0953 1.7529,7.742C2.2911,6.3886 3.0998,5.1594 4.1296,4.1296C5.1594,3.0998 6.3886,2.2911 7.742,1.7529ZM6.496,8.084C6.7257,7.7393 7.1914,7.6462 7.536,7.876L10.536,9.876C10.8807,10.1057 10.9738,10.5714 10.744,10.916C10.5143,11.2607 10.0486,11.3538 9.704,11.124L9.617,11.066C9.619,11.1003 9.62,11.135 9.62,11.17C9.62,11.9045 9.1723,12.5 8.62,12.5C8.0677,12.5 7.62,11.9045 7.62,11.17C7.62,10.6966 7.8059,10.2811 8.0859,10.0453L6.704,9.124C6.3593,8.8943 6.2662,8.4286 6.496,8.084ZM11.8909,15.4099C12.448,15.4018 12.9995,15.5224 13.5024,15.7624C14.0053,16.0023 14.446,16.3551 14.7902,16.7933C15.0461,17.119 15.5176,17.1757 15.8433,16.9198C16.169,16.6639 16.2257,16.1924 15.9698,15.8667C15.483,15.247 14.8596,14.748 14.1484,14.4086C13.4403,14.0708 12.6641,13.9002 11.8798,13.9099C11.094,13.9006 10.3167,14.0727 9.6081,14.4128C8.8964,14.7544 8.2736,15.2563 7.7884,15.8791C7.5338,16.2058 7.5923,16.6771 7.9191,16.9316C8.2458,17.1862 8.7171,17.1277 8.9717,16.8009C9.3144,16.361 9.7544,16.0064 10.2572,15.7651C10.76,15.5237 11.3119,15.4022 11.8696,15.4099L11.8802,15.4101L11.8909,15.4099ZM15.87,11.17C15.87,11.9045 15.4223,12.5 14.87,12.5C14.3279,12.5 13.8866,11.9264 13.8705,11.2106C13.5567,11.317 13.1985,11.2048 13.006,10.916C12.7762,10.5714 12.8693,10.1057 13.214,9.876L16.214,7.876C16.5586,7.6462 17.0243,7.7393 17.254,8.084C17.4838,8.4286 17.3907,8.8943 17.046,9.124L15.51,10.148C15.73,10.392 15.87,10.7593 15.87,11.17Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
34
app/src/main/res/drawable-night/ic_gif_celebrate_24.xml
Normal file
34
app/src/main/res/drawable-night/ic_gif_celebrate_24.xml
Normal file
|
@ -0,0 +1,34 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="23dp"
|
||||
android:height="23dp"
|
||||
android:viewportWidth="23"
|
||||
android:viewportHeight="23">
|
||||
<group>
|
||||
<clip-path
|
||||
android:pathData="M0,0h22.02v22.02h-22.02z"/>
|
||||
<path
|
||||
android:pathData="M12.86,6.8C12.9546,6.8239 13.0529,6.8289 13.1494,6.8147C13.2459,6.8005 13.3387,6.7674 13.4223,6.7172C13.5059,6.667 13.5788,6.6007 13.6368,6.5223C13.6947,6.4438 13.7366,6.3547 13.76,6.26L15.08,0.93C15.1277,0.7364 15.0966,0.5317 14.9935,0.3611C14.8903,0.1904 14.7236,0.0677 14.53,0.02C14.3364,-0.0277 14.1318,0.0034 13.9611,0.1065C13.7904,0.2097 13.6677,0.3764 13.62,0.57L12.31,5.9C12.2862,5.9952 12.2816,6.0943 12.2965,6.1913C12.3113,6.2883 12.3453,6.3814 12.3965,6.4652C12.4477,6.5489 12.515,6.6217 12.5946,6.6792C12.6742,6.7366 12.7644,6.7777 12.86,6.8V6.8Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M17.76,3.41H16.14V5.06H17.76V3.41Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M8.22,19.59L12.47,18.05L3.89,9.48L2.38,13.75L8.22,19.59Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M10.97,2.04L9.89,0.94L8.81,2.04L9.89,3.13L10.97,2.04Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M17.69,11.53L18.77,12.62L19.85,11.53L18.77,10.43L17.69,11.53Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M22,7.65C21.9809,7.5538 21.943,7.4624 21.8883,7.381C21.8336,7.2996 21.7632,7.2299 21.6813,7.1759C21.5995,7.1219 21.5077,7.0848 21.4113,7.0666C21.3149,7.0484 21.2159,7.0496 21.12,7.07L15.72,8.16C15.5251,8.1998 15.3539,8.3154 15.2442,8.4813C15.1345,8.6473 15.0952,8.8501 15.135,9.045C15.1748,9.2399 15.2904,9.4111 15.4563,9.5208C15.6223,9.6305 15.8251,9.6698 16.02,9.63L21.42,8.54C21.6138,8.4968 21.7829,8.3795 21.8913,8.2132C21.9997,8.0468 22.0387,7.8447 22,7.65Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M18.19,14.02L7.92,3.61C7.802,3.4848 7.6589,3.386 7.5,3.32C7.3419,3.2518 7.1722,3.2144 7,3.21V3.21C6.7339,3.2078 6.4742,3.292 6.26,3.45C6.0409,3.6096 5.8767,3.8332 5.79,4.09L4.46,7.88L4.52,7.82L14.15,17.45L17.72,16.16C17.9235,16.0804 18.1051,15.9536 18.25,15.79C18.393,15.6236 18.4927,15.4243 18.54,15.21C18.595,14.9968 18.595,14.7732 18.54,14.56C18.4682,14.3546 18.3482,14.1694 18.19,14.02Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M1.78,15.44L0.08,20.27C0.0118,20.4657 -0.0121,20.674 0.01,20.88C0.04,21.0887 0.1188,21.2875 0.24,21.46C0.3607,21.6294 0.5179,21.7695 0.7,21.87C0.8841,21.9699 1.0905,22.0215 1.3,22.02C1.4473,22.0227 1.5935,21.9954 1.73,21.94L6.54,20.2L1.78,15.44Z"
|
||||
android:fillColor="#000000"/>
|
||||
</group>
|
||||
</vector>
|
9
app/src/main/res/drawable-night/ic_gif_excited_24.xml
Normal file
9
app/src/main/res/drawable-night/ic_gif_excited_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M22.25,7.74C21.44,5.7002 20.0218,3.9592 18.188,2.7534C16.3542,1.5476 14.1938,0.9354 12,1C10.5438,0.9571 9.0942,1.2123 7.7403,1.75C6.3863,2.2876 5.1565,3.0963 4.1264,4.1264C3.0963,5.1565 2.2876,6.3863 1.75,7.7403C1.2123,9.0942 0.9571,10.5438 1,12C0.9571,13.4562 1.2123,14.9057 1.75,16.2597C2.2876,17.6137 3.0963,18.8435 4.1264,19.8736C5.1565,20.9037 6.3863,21.7124 7.7403,22.25C9.0942,22.7877 10.5438,23.0429 12,23C13.4562,23.0429 14.9057,22.7877 16.2597,22.25C17.6137,21.7124 18.8435,20.9037 19.8736,19.8736C20.9037,18.8435 21.7124,17.6137 22.25,16.2597C22.7877,14.9057 23.0429,13.4562 23,12C23.0423,10.5438 22.7871,9.0942 22.25,7.74ZM15.5,7.21C16.33,7.21 17,8.33 17,9.71C17.0018,10.0477 16.958,10.384 16.87,10.71C16.7779,10.4178 16.6002,10.16 16.36,9.97C16.1221,9.7819 15.8327,9.6703 15.53,9.65C15.2108,9.674 14.9069,9.7963 14.66,10C14.4092,10.2014 14.2303,10.4785 14.15,10.79C14.0498,10.4388 13.9993,10.0752 14,9.71C14,8.33 14.67,7.21 15.5,7.21ZM8.5,7.21C9.33,7.21 10,8.33 10,9.71C10.0018,10.0477 9.958,10.384 9.87,10.71C9.7727,10.4288 9.5954,10.182 9.36,10C9.1221,9.8119 8.8327,9.7003 8.53,9.68C8.2108,9.704 7.9069,9.8263 7.66,10.03C7.4092,10.2314 7.2303,10.5085 7.15,10.82C7.047,10.4591 6.9965,10.0853 7,9.71C7,8.33 7.67,7.21 8.5,7.21ZM18.93,14C18.19,16 16.31,19.5 12,19.5C7.69,19.5 5.81,16 5.07,14C4.83,13.31 5.24,12.53 5.83,12.53H18.17C18.76,12.53 19.17,13.31 18.93,14Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
11
app/src/main/res/drawable-night/ic_gif_love_24.xml
Normal file
11
app/src/main/res/drawable-night/ic_gif_love_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M17.21,3.12C16.547,3.1189 15.8906,3.2515 15.28,3.51C14.6665,3.761 14.109,4.1315 13.64,4.6C13.1144,5.1201 12.6359,5.6856 12.21,6.29C11.7719,5.6805 11.2799,5.1115 10.74,4.59C10.0272,3.8927 9.1276,3.4168 8.15,3.22C7.1629,3.0239 6.1398,3.1248 5.21,3.51C4.2912,3.8921 3.5073,4.5396 2.9586,5.3698C2.4099,6.2 2.1214,7.1749 2.13,8.17C2.13,15.04 12.21,22.37 12.21,22.37C12.21,22.37 22.29,15.04 22.29,8.17C22.29,6.8439 21.7632,5.5722 20.8255,4.6345C19.8879,3.6968 18.6161,3.17 17.29,3.17L17.21,3.12Z"
|
||||
android:strokeWidth="0.25"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</vector>
|
9
app/src/main/res/drawable-night/ic_gif_sad_24.xml
Normal file
9
app/src/main/res/drawable-night/ic_gif_sad_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M22.25,7.74C21.44,5.7002 20.0218,3.9592 18.188,2.7534C16.3542,1.5476 14.1938,0.9354 12,1C10.5438,0.9571 9.0942,1.2123 7.7403,1.75C6.3863,2.2876 5.1565,3.0963 4.1264,4.1264C3.0963,5.1565 2.2876,6.3863 1.75,7.7403C1.2123,9.0942 0.9571,10.5438 1,12C0.9571,13.4562 1.2123,14.9057 1.75,16.2597C2.2876,17.6137 3.0963,18.8435 4.1264,19.8736C5.1565,20.9037 6.3863,21.7124 7.7403,22.25C9.0942,22.7877 10.5438,23.0429 12,23C13.4562,23.0429 14.9057,22.7877 16.2597,22.25C17.6137,21.7124 18.8435,20.9037 19.8736,19.8736C20.9037,18.8435 21.7124,17.6137 22.25,16.2597C22.7877,14.9057 23.0429,13.4562 23,12C23.0423,10.5438 22.7871,9.0942 22.25,7.74ZM15.36,7C16.19,7 16.86,7.9 16.86,9C16.86,10.1 16.19,11 15.36,11C14.53,11 13.86,10.1 13.86,9C13.86,7.9 14.53,7 15.36,7ZM8.53,7C9.36,7 10.03,7.9 10.03,9C10.03,10.1 9.36,11 8.53,11C7.7,11 7,10.1 7,9C7,7.9 7.7,7 8.53,7ZM16.87,17.35C16.7472,17.4274 16.6052,17.4689 16.46,17.47C16.3348,17.4712 16.2113,17.4407 16.1011,17.3813C15.991,17.3218 15.8977,17.2353 15.83,17.13C15.4188,16.4882 14.851,15.9617 14.18,15.6C13.5097,15.2402 12.7608,15.052 12,15.052C11.2392,15.052 10.4903,15.2402 9.82,15.6C9.149,15.9617 8.5812,16.4882 8.17,17.13C8.1162,17.2127 8.0465,17.284 7.9651,17.3399C7.8837,17.3957 7.7921,17.435 7.6956,17.4554C7.599,17.4758 7.4993,17.477 7.4023,17.4589C7.3053,17.4409 7.2127,17.4038 7.13,17.35C7.0473,17.2962 6.9759,17.2265 6.9201,17.1451C6.8643,17.0637 6.825,16.9721 6.8046,16.8756C6.7842,16.779 6.783,16.6793 6.8011,16.5823C6.8191,16.4853 6.8562,16.3927 6.91,16.31C7.464,15.473 8.2163,14.786 9.1,14.31C9.9927,13.8346 10.9886,13.586 12,13.586C13.0114,13.586 14.0073,13.8346 14.9,14.31C15.7837,14.786 16.536,15.473 17.09,16.31C17.194,16.4784 17.2292,16.6804 17.1883,16.8741C17.1473,17.0677 17.0333,17.2382 16.87,17.35V17.35Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
20
app/src/main/res/drawable-night/ic_gif_surprised_24.xml
Normal file
20
app/src/main/res/drawable-night/ic_gif_surprised_24.xml
Normal file
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M6.25,3C3.26,3 0.99,7.08 0.99,12.5C0.99,17.92 3.25,22 6.24,22C9.23,22 11.49,17.92 11.49,12.5C11.49,7.08 9.24,3 6.25,3ZM4.68,10.5C4.2742,10.5116 3.8746,10.6032 3.5041,10.7693C3.1337,10.9355 2.7996,11.173 2.521,11.4683C2.2424,11.7636 2.0247,12.111 1.8804,12.4905C1.7361,12.87 1.668,13.2742 1.68,13.68C1.6585,14.4979 1.9622,15.291 2.5246,15.8852C3.087,16.4795 3.8621,16.8265 4.68,16.85C5.4978,16.8265 6.273,16.4795 6.8354,15.8852C7.3978,15.291 7.7015,14.4979 7.68,13.68C7.692,13.2742 7.6239,12.87 7.4796,12.4905C7.3353,12.111 7.1176,11.7636 6.839,11.4683C6.5604,11.173 6.2263,10.9355 5.8559,10.7693C5.4854,10.6032 5.0858,10.5116 4.68,10.5Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M17.4,3C14.4,3 12.16,7.08 12.16,12.5C12.16,17.92 14.41,22 17.4,22C20.39,22 22.65,17.92 22.65,12.5C22.65,7.08 20.39,3 17.4,3ZM15.83,10.5C15.0113,10.5261 14.2361,10.8753 13.674,11.4711C13.1119,12.067 12.8084,12.8611 12.83,13.68C12.8085,14.4979 13.1122,15.291 13.6746,15.8852C14.237,16.4795 15.0121,16.8265 15.83,16.85C16.6479,16.8265 17.423,16.4795 17.9854,15.8852C18.5478,15.291 18.8515,14.4979 18.83,13.68C18.842,13.2742 18.7739,12.87 18.6296,12.4905C18.4853,12.111 18.2676,11.7636 17.989,11.4683C17.7104,11.173 17.3763,10.9355 17.0059,10.7693C16.6354,10.6032 16.2358,10.5116 15.83,10.5Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
<path
|
||||
android:pathData="M4,13m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M15,13m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
10
app/src/main/res/drawable-night/ic_gif_thumbsup_24.xml
Normal file
10
app/src/main/res/drawable-night/ic_gif_thumbsup_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M22,12.4697C21.9506,13.0362 21.727,13.5736 21.36,14.0083C21.5235,14.3466 21.6219,14.7126 21.65,15.0873C21.6124,15.6975 21.3852,16.2807 21,16.7557C21.093,17.0956 21.1533,17.4435 21.18,17.7948C21.118,18.4204 20.8258,19.0007 20.36,19.4233C20.5502,19.7485 20.6567,20.1158 20.67,20.4923C20.6868,21.1035 20.4603,21.6963 20.04,22.1408C19.6104,22.5824 19.0261,22.8403 18.41,22.8601H16.21C15.54,22.9201 14.41,22.97 13,22.97H10.64H7.64C2.73,16.7857 6.45,11.4407 6.45,11.4407C8.1,8.0138 11,2.9385 11.58,1.9294C11.8133,1.5532 12.1651,1.2651 12.58,1.1102C13.0043,0.9633 13.4657,0.9633 13.89,1.1102C14.422,1.2854 14.8775,1.6382 15.18,2.1093C15.4784,2.5819 15.6022,3.1437 15.53,3.6978C15.3796,5.1621 15.0712,6.6058 14.61,8.0038C14.3851,8.652 14.2276,9.3216 14.14,10.002C15.72,10.1918 19.72,10.1119 19.72,10.1119C20.0272,10.1183 20.3302,10.1852 20.6116,10.3086C20.893,10.432 21.1473,10.6096 21.36,10.8312C21.5719,11.047 21.7379,11.3034 21.8479,11.585C21.9579,11.8666 22.0096,12.1676 22,12.4697ZM2.22,12.3398C2.36,12.15 2.7,11.7104 4.8,11.5206C2.3461,16.1054 3.4576,18.4335 5.3844,22.469C5.452,22.6105 5.5205,22.7541 5.59,22.9001C4.8713,22.9225 4.1542,22.8211 3.47,22.6003C0.24,19.7829 0.58,14.5478 2.22,12.3398Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
10
app/src/main/res/drawable-night/ic_gif_trending_24.xml
Normal file
10
app/src/main/res/drawable-night/ic_gif_trending_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M22.7627,6.2602C22.7557,5.5699 22.1905,5.0159 21.5003,5.0228L16.0348,5.0773C15.3445,5.0842 14.7905,5.6494 14.7973,6.3397C14.8042,7.0301 15.3694,7.5841 16.0598,7.5772L18.5306,7.5525L13.9091,12.1741L10.9748,9.2397C10.4866,8.7516 9.6952,8.7516 9.207,9.2397L1.0252,17.4216C0.537,17.9097 0.537,18.7012 1.0252,19.1893C1.5134,19.6775 2.3048,19.6775 2.793,19.1893L10.0909,11.8914L13.0252,14.8257C13.5134,15.3139 14.3048,15.3139 14.793,14.8257L20.2933,9.3254L20.3173,11.718C20.3243,12.4083 20.8895,12.9623 21.5798,12.9554C22.2701,12.9485 22.8241,12.3832 22.8172,11.6929L22.7627,6.2602Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/signal_icon_tint_primary"
|
||||
android:fillColor="@color/signal_inverse_primary"
|
||||
android:pathData="M8.75,21.982c-0.021,0 -0.041,0 -0.062,0A7.326,7.326 0,0 1,5.072 21.2,5.55 5.55,0 0,1 2.8,18.928c-0.522,-0.977 -0.8,-1.947 -0.8,-4.62L2,9.692c0,-2.673 0.278,-3.643 0.8,-4.62A5.55,5.55 0,0 1,5.072 2.8c0.977,-0.522 1.947,-0.8 4.62,-0.8h4.616c2.673,0 3.643,0.278 4.62,0.8A5.55,5.55 0,0 1,21.2 5.072c0.522,0.977 0.8,1.947 0.8,4.62v2.257c0,0.1 -0.026,0.2 -0.036,0.3L17.191,12.249c-2.8,0 -3.872,0.3 -4.975,0.89a6.172,6.172 0,0 0,-2.575 2.575c-0.591,1.1 -0.891,2.177 -0.891,4.977ZM17.191,13.75c-2.719,0 -3.513,0.309 -4.268,0.712a4.7,4.7 0,0 0,-1.96 1.961c-0.4,0.755 -0.713,1.549 -0.713,4.269v0.9c0.078,-0.044 0.158,-0.083 0.232,-0.134L20.734,14.4a2.971,2.971 0,0 0,0.644 -0.647Z"/>
|
||||
</vector>
|
||||
|
|
9
app/src/main/res/drawable/ic_backspace_24.xml
Normal file
9
app/src/main/res/drawable/ic_backspace_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.154,4.5c1.186,0 1.408,0.119 1.6,0.223a1.237,1.237 0,0 1,0.519 0.519c0.1,0.2 0.223,0.418 0.223,1.6L21.496,17.154c0,1.186 -0.119,1.408 -0.223,1.6a1.237,1.237 0,0 1,-0.519 0.519c-0.2,0.1 -0.418,0.223 -1.6,0.223L8.846,19.496c-1.186,0 -1.408,-0.119 -1.6,-0.223a1.317,1.317 0,0 1,-0.439 -0.438L1.852,12 6.823,5.132a1.256,1.256 0,0 1,0.419 -0.409c0.2,-0.1 0.418,-0.223 1.6,-0.223L19.154,4.5m0,-1.5L8.846,3a4.344,4.344 0,0 0,-2.311 0.4,2.7 2.7,0 0,0 -0.947,0.88L0,12l5.588,7.719a2.7,2.7 0,0 0,0.947 0.88,4.344 4.344,0 0,0 2.311,0.4L19.154,20.999a4.344,4.344 0,0 0,2.311 -0.4A2.719,2.719 0,0 0,22.6 19.465a4.344,4.344 0,0 0,0.4 -2.311L23,6.846a4.344,4.344 0,0 0,-0.4 -2.311A2.719,2.719 0,0 0,21.465 3.4,4.344 4.344,0 0,0 19.154,3ZM19.061,8L18,6.939l-4,4 -4,-4L8.939,8l4,4 -4,4L10,17.061l4,-4 4,4L19.061,16l-4,-4Z"/>
|
||||
</vector>
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#FF636467"
|
||||
android:fillColor="@color/signal_inverse_primary"
|
||||
android:pathData="M12,18.7c-2.1,0 -4.2,-1 -5.5,-2.7c-0.2,-0.3 -0.2,-0.8 0.2,-1.1s0.8,-0.2 1.1,0.2l0,0c1,1.3 2.6,2.1 4.3,2.1c1.7,0 3.3,-0.8 4.3,-2.1c0.2,-0.3 0.7,-0.4 1,-0.2c0,0 0,0 0,0c0.3,0.2 0.4,0.7 0.2,1C16.2,17.6 14.2,18.7 12,18.7zM12,2.5c-5.1,-0.2 -9.3,3.8 -9.5,8.9c0,0.2 0,0.4 0,0.6c-0.2,5.1 3.8,9.3 8.9,9.5c0.2,0 0.4,0 0.6,0c5.1,0.2 9.3,-3.8 9.5,-8.9c0,-0.2 0,-0.4 0,-0.6c0.2,-5.1 -3.8,-9.3 -8.9,-9.5C12.4,2.5 12.2,2.5 12,2.5M12,1c5.9,-0.2 10.8,4.5 11,10.4c0,0.2 0,0.4 0,0.6c0.2,5.9 -4.5,10.8 -10.4,11c-0.2,0 -0.4,0 -0.6,0C6.1,23.2 1.2,18.5 1,12.6c0,-0.2 0,-0.4 0,-0.6C0.8,6.1 5.5,1.2 11.4,1C11.6,1 11.8,1 12,1zM8.5,8C7.7,8 7,8.9 7,10s0.7,2 1.5,2s1.5,-0.9 1.5,-2S9.3,8 8.5,8zM15.5,8C14.7,8 14,8.9 14,10s0.7,2 1.5,2s1.5,-0.9 1.5,-2S16.3,8 15.5,8z"/>
|
||||
</vector>
|
||||
|
|
10
app/src/main/res/drawable/ic_gif_angry_24.xml
Normal file
10
app/src/main/res/drawable/ic_gif_angry_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M15.686,3.135C14.5139,2.6729 13.2592,2.4568 12,2.5C10.7408,2.4568 9.4861,2.6729 8.314,3.135C7.1418,3.5971 6.0772,4.2954 5.1863,5.1863C4.2953,6.0772 3.5971,7.1418 3.135,8.314C2.6729,9.4861 2.4567,10.7408 2.5,12C2.4567,13.2592 2.6729,14.5139 3.135,15.686C3.5971,16.8582 4.2953,17.9228 5.1863,18.8137C6.0772,19.7047 7.1418,20.4029 8.314,20.865C9.4861,21.3271 10.7408,21.5432 12,21.5C13.2592,21.5432 14.5139,21.3271 15.686,20.865C16.8582,20.4029 17.9228,19.7047 18.8137,18.8137C19.7046,17.9228 20.4029,16.8582 20.865,15.686C21.3271,14.5139 21.5432,13.2592 21.5,12C21.5432,10.7408 21.3271,9.4861 20.865,8.314C20.4029,7.1418 19.7046,6.0772 18.8137,5.1863C17.9228,4.2954 16.8582,3.5971 15.686,3.135ZM7.742,1.7529C9.0953,1.2148 10.5442,0.9586 12,1C13.4558,0.9586 14.9047,1.2148 16.258,1.7529C17.6113,2.2911 18.8405,3.0998 19.8704,4.1296C20.9002,5.1595 21.7089,6.3887 22.2471,7.742C22.7852,9.0953 23.0414,10.5442 23,12C23.0429,13.4562 22.7877,14.9057 22.25,16.2597C21.7124,17.6137 20.9037,18.8435 19.8736,19.8736C18.8435,20.9037 17.6137,21.7124 16.2597,22.25C14.9057,22.7877 13.4562,23.0429 12,23C10.5442,23.0414 9.0953,22.7852 7.742,22.2471C6.3886,21.7089 5.1594,20.9002 4.1296,19.8704C3.0998,18.8406 2.2911,17.6113 1.7529,16.258C1.2148,14.9047 0.9586,13.4558 1,12C0.9586,10.5442 1.2148,9.0953 1.7529,7.742C2.2911,6.3887 3.0998,5.1595 4.1296,4.1296C5.1594,3.0998 6.3886,2.2911 7.742,1.7529ZM11.8909,15.4099C12.448,15.4018 12.9995,15.5224 13.5024,15.7624C14.0053,16.0023 14.446,16.3551 14.7902,16.7933C15.0461,17.119 15.5176,17.1757 15.8433,16.9198C16.169,16.6639 16.2257,16.1924 15.9698,15.8667C15.483,15.247 14.8596,14.748 14.1484,14.4086C13.4403,14.0708 12.6641,13.9002 11.8798,13.9099C11.094,13.9007 10.3167,14.0727 9.6081,14.4128C8.8964,14.7544 8.2736,15.2563 7.7884,15.8791C7.5338,16.2058 7.5923,16.6771 7.9191,16.9316C8.2458,17.1862 8.7171,17.1277 8.9716,16.8009C9.3144,16.361 9.7544,16.0064 10.2572,15.7651C10.76,15.5237 11.3119,15.4022 11.8696,15.4099L11.8802,15.4101L11.8909,15.4099ZM8.62,12.5C9.1723,12.5 9.62,11.9045 9.62,11.17C9.62,11.135 9.619,11.1004 9.617,11.066L9.704,11.124C10.0486,11.3538 10.5143,11.2607 10.744,10.916C10.9738,10.5714 10.8807,10.1057 10.536,9.876L7.536,7.876C7.1914,7.6462 6.7257,7.7393 6.496,8.084C6.2662,8.4286 6.3593,8.8943 6.704,9.124L8.0859,10.0453C7.8059,10.2811 7.62,10.6966 7.62,11.17C7.62,11.9045 8.0677,12.5 8.62,12.5ZM14.87,12.5C15.4223,12.5 15.87,11.9045 15.87,11.17C15.87,10.7593 15.73,10.392 15.51,10.148L17.046,9.124C17.3907,8.8943 17.4838,8.4286 17.254,8.084C17.0243,7.7393 16.5586,7.6462 16.214,7.876L13.214,9.876C12.8693,10.1057 12.7762,10.5714 13.006,10.916C13.1985,11.2048 13.5566,11.317 13.8705,11.2106C13.8866,11.9264 14.3279,12.5 14.87,12.5Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_gif_celebrate_24.xml
Normal file
10
app/src/main/res/drawable/ic_gif_celebrate_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M16.0789,1.9149C16.1782,1.5128 15.9328,1.1063 15.5307,1.0069C15.1286,0.9075 14.7221,1.1529 14.6227,1.555L13.3075,6.8762C13.2081,7.2783 13.4535,7.6848 13.8557,7.7842C14.2578,7.8836 14.6643,7.6382 14.7637,7.2361L16.0789,1.9149ZM17.9908,15.8365L8.069,5.8018V5.7626L6.6039,9.9228L13.9036,17.32L17.9908,15.8365ZM12.3519,17.8832L6.0496,11.4967L4.4207,16.1221C4.5376,16.1649 4.6454,16.2374 4.732,16.3386L7.5465,19.6274L12.3519,17.8832ZM2.5473,21.4419L3.8819,17.6522L6.0401,20.1741L2.5473,21.4419ZM7.2691,4.4224C7.4854,4.2646 7.7445,4.1782 8.011,4.175V4.1947C8.1805,4.1961 8.3481,4.2314 8.5041,4.2987C8.6601,4.366 8.8014,4.4638 8.92,4.5866L19.1899,14.9742C19.3447,15.1303 19.458,15.3235 19.5194,15.5359C19.5807,15.7484 19.5881,15.973 19.5408,16.1891C19.4936,16.4052 19.3932,16.6056 19.249,16.7718C19.1048,16.9381 18.9214,17.0647 18.716,17.1399L2.731,22.9216C2.5923,22.976 2.4445,23.0026 2.2958,23C2.0884,22.9997 1.8841,22.9489 1.7001,22.8519C1.516,22.7549 1.3577,22.6146 1.2383,22.4427C1.1189,22.2708 1.042,22.0724 1.014,21.8642C0.986,21.6559 1.0077,21.4439 1.0774,21.2459L6.8022,5.057C6.8897,4.8019 7.0527,4.5803 7.2691,4.4224ZM23.0097,8.6316C23.0921,9.0375 22.8299,9.4334 22.424,9.5159L17.0182,10.6134C16.6123,10.6958 16.2164,10.4336 16.134,10.0276C16.0516,9.6217 16.3138,9.2258 16.7198,9.1434L22.1255,8.0458C22.5314,7.9634 22.9273,8.2257 23.0097,8.6316ZM17.1398,4.3906H18.7644V6.037H17.1398V4.3906ZM10.8907,1.93L9.8103,3.0248L10.8907,4.1197L11.9711,3.0248L10.8907,1.93ZM18.6907,12.5114L19.771,11.4166L20.8514,12.5114L19.771,13.6062L18.6907,12.5114Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
18
app/src/main/res/drawable/ic_gif_excited_24.xml
Normal file
18
app/src/main/res/drawable/ic_gif_excited_24.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12.0043,2.5043C13.2635,2.4611 14.5182,2.6772 15.6904,3.1393C16.8625,3.6015 17.9271,4.2997 18.8181,5.1906C19.709,6.0815 20.4072,7.1461 20.8693,8.3183C21.3314,9.4904 21.5476,10.7451 21.5043,12.0043C21.5476,13.2635 21.3314,14.5182 20.8693,15.6904C20.4072,16.8625 19.709,17.9271 18.8181,18.8181C17.9271,19.709 16.8625,20.4072 15.6904,20.8693C14.5182,21.3314 13.2635,21.5476 12.0043,21.5043C10.7451,21.5476 9.4904,21.3314 8.3183,20.8693C7.1461,20.4072 6.0815,19.709 5.1906,18.8181C4.2997,17.9271 3.6015,16.8625 3.1393,15.6904C2.6772,14.5182 2.4611,13.2635 2.5043,12.0043C2.4611,10.7451 2.6772,9.4904 3.1393,8.3183C3.6015,7.1461 4.2997,6.0815 5.1906,5.1906C6.0815,4.2997 7.1461,3.6015 8.3183,3.1393C9.4904,2.6772 10.7451,2.4611 12.0043,2.5043ZM12.0043,1.0043C10.5485,0.9629 9.0996,1.2191 7.7463,1.7572C6.393,2.2954 5.1638,3.1041 4.134,4.134C3.1041,5.1638 2.2954,6.393 1.7572,7.7463C1.2191,9.0996 0.9629,10.5485 1.0043,12.0043C0.9629,13.4601 1.2191,14.909 1.7572,16.2624C2.2954,17.6157 3.1041,18.8449 4.134,19.8747C5.1638,20.9045 6.393,21.7133 7.7463,22.2514C9.0996,22.7895 10.5485,23.0458 12.0043,23.0043C13.4605,23.0472 14.9101,22.792 16.264,22.2544C17.618,21.7167 18.8478,20.908 19.8779,19.8779C20.908,18.8478 21.7167,17.618 22.2544,16.264C22.792,14.9101 23.0472,13.4605 23.0043,12.0043C23.0458,10.5485 22.7895,9.0996 22.2514,7.7463C21.7133,6.393 20.9045,5.1638 19.8747,4.134C18.8449,3.1041 17.6157,2.2954 16.2624,1.7572C14.909,1.2191 13.4601,0.9629 12.0043,1.0043Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M17.31,14C16.5,15.94 14.94,18 12,18C9.06,18 7.5,15.94 6.69,14H17.31ZM18.17,12.5H5.83C5.24,12.5 4.83,13.28 5.07,13.97C5.81,16 7.69,19.5 12,19.5C16.31,19.5 18.19,16 18.93,14C19.17,13.31 18.76,12.53 18.17,12.53V12.5Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M8.53,9.65C8.8339,9.672 9.1241,9.7849 9.363,9.9739C9.602,10.1629 9.7786,10.4194 9.87,10.71C9.9564,10.3837 10.0001,10.0476 10,9.71C10,8.33 9.33,7.21 8.5,7.21C7.67,7.21 7,8.33 7,9.71C7.0015,10.0751 7.0519,10.4383 7.15,10.79C7.2339,10.4806 7.4117,10.2048 7.6588,10.0007C7.906,9.7965 8.2103,9.674 8.53,9.65Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M15.53,9.65C15.8339,9.672 16.1241,9.7849 16.363,9.9739C16.602,10.1629 16.7786,10.4194 16.87,10.71C16.9564,10.3837 17.0001,10.0476 17,9.71C17,8.33 16.33,7.21 15.5,7.21C14.67,7.21 14,8.33 14,9.71C14.0015,10.0751 14.0519,10.4383 14.15,10.79C14.2339,10.4806 14.4117,10.2048 14.6588,10.0007C14.906,9.7965 15.2103,9.674 15.53,9.65Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
11
app/src/main/res/drawable/ic_gif_love_24.xml
Normal file
11
app/src/main/res/drawable/ic_gif_love_24.xml
Normal file
|
@ -0,0 +1,11 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M17.0417,4.2083C18.0141,4.2083 18.9468,4.5946 19.6344,5.2823C20.322,5.9699 20.7083,6.9025 20.7083,7.875C20.7083,12.4748 15.0177,17.9519 12,20.3536C8.9805,17.9583 3.2917,12.4849 3.2917,7.875C3.2916,7.149 3.5071,6.4392 3.9109,5.8358C4.3147,5.2324 4.8885,4.7624 5.5596,4.4854C6.2308,4.2085 6.969,4.137 7.6809,4.2801C8.3927,4.4231 9.046,4.7743 9.558,5.2891C10.0334,5.7478 10.4686,6.2465 10.8587,6.7796L11.9945,8.458L13.1385,6.7832C13.5312,6.2469 13.9691,5.7451 14.4475,5.2836C14.7873,4.9418 15.1916,4.6707 15.6369,4.4861C16.0822,4.3016 16.5596,4.2072 17.0417,4.2083V4.2083ZM17.0417,2.8333C16.379,2.8326 15.7228,2.9628 15.1107,3.2165C14.4986,3.4703 13.9426,3.8426 13.4749,4.3119C12.9335,4.8333 12.4399,5.402 12,6.0114C11.5601,5.402 11.0664,4.8333 10.5251,4.3119C9.8202,3.6063 8.9219,3.1257 7.9438,2.9307C6.9657,2.7358 5.9517,2.8353 5.0302,3.2168C4.1087,3.5982 3.321,4.2444 2.7668,5.0735C2.2125,5.9027 1.9167,6.8776 1.9167,7.875C1.9167,14.75 12,22.0833 12,22.0833C12,22.0833 22.0833,14.75 22.0833,7.875C22.0833,6.5379 21.5522,5.2555 20.6067,4.31C19.6612,3.3645 18.3788,2.8333 17.0417,2.8333V2.8333Z"
|
||||
android:strokeWidth="0.25"
|
||||
android:fillColor="#000000"
|
||||
android:strokeColor="#000000"/>
|
||||
</vector>
|
21
app/src/main/res/drawable/ic_gif_sad_24.xml
Normal file
21
app/src/main/res/drawable/ic_gif_sad_24.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,2.5C13.2592,2.4568 14.5139,2.6729 15.686,3.135C16.8582,3.5971 17.9228,4.2954 18.8137,5.1863C19.7046,6.0772 20.4029,7.1418 20.865,8.314C21.3271,9.4861 21.5432,10.7408 21.5,12C21.5432,13.2592 21.3271,14.5139 20.865,15.686C20.4029,16.8582 19.7046,17.9228 18.8137,18.8137C17.9228,19.7047 16.8582,20.4029 15.686,20.865C14.5139,21.3271 13.2592,21.5432 12,21.5C10.7408,21.5432 9.4861,21.3271 8.314,20.865C7.1418,20.4029 6.0772,19.7047 5.1863,18.8137C4.2953,17.9228 3.5971,16.8582 3.135,15.686C2.6729,14.5139 2.4567,13.2592 2.5,12C2.4567,10.7408 2.6729,9.4861 3.135,8.314C3.5971,7.1418 4.2953,6.0772 5.1863,5.1863C6.0772,4.2954 7.1418,3.5971 8.314,3.135C9.4861,2.6729 10.7408,2.4568 12,2.5ZM12,1C10.5442,0.9586 9.0953,1.2148 7.742,1.7529C6.3886,2.2911 5.1594,3.0998 4.1296,4.1296C3.0998,5.1595 2.2911,6.3887 1.7529,7.742C1.2148,9.0953 0.9586,10.5442 1,12C0.9586,13.4558 1.2148,14.9047 1.7529,16.258C2.2911,17.6113 3.0998,18.8406 4.1296,19.8704C5.1594,20.9002 6.3886,21.7089 7.742,22.2471C9.0953,22.7852 10.5442,23.0414 12,23C13.4562,23.0429 14.9057,22.7877 16.2597,22.25C17.6137,21.7124 18.8435,20.9037 19.8736,19.8736C20.9037,18.8435 21.7124,17.6137 22.25,16.2597C22.7877,14.9057 23.0429,13.4562 23,12C23.0414,10.5442 22.7852,9.0953 22.2471,7.742C21.7089,6.3887 20.9002,5.1595 19.8704,4.1296C18.8405,3.0998 17.6113,2.2911 16.258,1.7529C14.9047,1.2148 13.4558,0.9586 12,1Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M8.53,11C9.3584,11 10.03,10.1046 10.03,9C10.03,7.8954 9.3584,7 8.53,7C7.7016,7 7.03,7.8954 7.03,9C7.03,10.1046 7.7016,11 8.53,11Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M15.36,11C16.1884,11 16.86,10.1046 16.86,9C16.86,7.8954 16.1884,7 15.36,7C14.5316,7 13.86,7.8954 13.86,9C13.86,10.1046 14.5316,11 15.36,11Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M7.54,16.72C8.0214,15.9749 8.6818,15.3623 9.4609,14.9382C10.24,14.514 11.1129,14.2918 12,14.2918C12.8871,14.2918 13.76,14.514 14.5391,14.9382C15.3182,15.3623 15.9786,15.9749 16.46,16.72"
|
||||
android:strokeWidth="1.5"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#000000"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
18
app/src/main/res/drawable/ic_gif_surprised_24.xml
Normal file
18
app/src/main/res/drawable/ic_gif_surprised_24.xml
Normal file
|
@ -0,0 +1,18 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4.68,10.5C4.2742,10.5116 3.8746,10.6032 3.5041,10.7693C3.1337,10.9355 2.7996,11.173 2.521,11.4683C2.2424,11.7636 2.0247,12.111 1.8804,12.4905C1.7361,12.87 1.668,13.2742 1.68,13.68C1.6585,14.4979 1.9622,15.291 2.5246,15.8852C3.087,16.4795 3.8621,16.8265 4.68,16.85C5.4978,16.8265 6.273,16.4795 6.8354,15.8852C7.3978,15.291 7.7015,14.4979 7.68,13.68C7.692,13.2742 7.6239,12.87 7.4796,12.4905C7.3353,12.111 7.1176,11.7636 6.839,11.4683C6.5604,11.173 6.2263,10.9355 5.8559,10.7693C5.4854,10.6032 5.0858,10.5116 4.68,10.5ZM3.76,13.59C3.5611,13.59 3.3703,13.511 3.2297,13.3703C3.089,13.2297 3.01,13.0389 3.01,12.84C3.01,12.6411 3.089,12.4503 3.2297,12.3097C3.3703,12.169 3.5611,12.09 3.76,12.09C3.9589,12.09 4.1497,12.169 4.2903,12.3097C4.431,12.4503 4.51,12.6411 4.51,12.84C4.51,13.0389 4.431,13.2297 4.2903,13.3703C4.1497,13.511 3.9589,13.59 3.76,13.59Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M6.25,4.5C8.31,4.5 9.99,8.08 9.99,12.5C9.99,16.92 8.31,20.5 6.25,20.5C4.19,20.5 2.5,16.92 2.5,12.5C2.5,8.08 4.18,4.5 6.25,4.5ZM6.25,3C3.26,3 0.99,7.08 0.99,12.5C0.99,17.92 3.25,22 6.24,22C9.23,22 11.49,17.92 11.49,12.5C11.49,7.08 9.24,3 6.25,3Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M15.83,10.5C15.0113,10.5261 14.2361,10.8753 13.674,11.4711C13.1119,12.067 12.8084,12.8611 12.83,13.68C12.8085,14.4979 13.1122,15.291 13.6746,15.8852C14.237,16.4795 15.0121,16.8265 15.83,16.85C16.6479,16.8265 17.423,16.4795 17.9854,15.8852C18.5478,15.291 18.8515,14.4979 18.83,13.68C18.842,13.2742 18.7739,12.87 18.6296,12.4905C18.4853,12.111 18.2676,11.7636 17.989,11.4683C17.7104,11.173 17.3763,10.9355 17.0059,10.7693C16.6354,10.6032 16.2358,10.5116 15.83,10.5ZM14.92,13.59C14.7702,13.598 14.6215,13.5609 14.493,13.4835C14.3645,13.4061 14.2622,13.292 14.1992,13.1558C14.1363,13.0197 14.1156,12.8678 14.1399,12.7198C14.1641,12.5717 14.2322,12.4344 14.3353,12.3255C14.4385,12.2165 14.5719,12.141 14.7183,12.1087C14.8648,12.0764 15.0176,12.0887 15.157,12.1441C15.2964,12.1995 15.416,12.2954 15.5003,12.4195C15.5846,12.5435 15.6298,12.69 15.63,12.84C15.6327,12.9359 15.6164,13.0313 15.5822,13.1209C15.5479,13.2105 15.4963,13.2925 15.4304,13.3621C15.3645,13.4318 15.2855,13.4878 15.1979,13.5269C15.1103,13.566 15.0159,13.5874 14.92,13.59Z"
|
||||
android:fillColor="#000000"/>
|
||||
<path
|
||||
android:pathData="M17.4,4.5C19.47,4.5 21.15,8.08 21.15,12.5C21.15,16.92 19.47,20.5 17.4,20.5C15.33,20.5 13.66,16.92 13.66,12.5C13.66,8.08 15.33,4.5 17.4,4.5ZM17.4,3C14.4,3 12.16,7.08 12.16,12.5C12.16,17.92 14.41,22 17.4,22C20.39,22 22.65,17.92 22.65,12.5C22.65,7.08 20.39,3 17.4,3Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
9
app/src/main/res/drawable/ic_gif_thumbsup_24.xml
Normal file
9
app/src/main/res/drawable/ic_gif_thumbsup_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M21.3622,14.0344C21.728,13.5844 21.9464,13.0366 21.9886,12.463C22.0064,11.8538 21.777,11.2626 21.3506,10.8188C20.9243,10.375 20.3357,10.1147 19.7139,10.0951C19.7139,10.0951 15.7249,10.1812 14.1315,9.9874C14.2195,9.3202 14.3779,8.6637 14.604,8.0285C15.0632,6.6292 15.3689,5.186 15.5161,3.7232C15.5919,3.168 15.4706,2.604 15.1726,2.1259C14.8745,1.6478 14.4177,1.2846 13.8787,1.097C13.4524,0.9582 12.9904,0.9657 12.569,1.1183C12.1476,1.2709 11.7917,1.5595 11.56,1.9365C10.9557,2.916 8.0985,8.0285 6.4502,11.4297C6.3668,11.4132 6.2808,11.4132 6.1974,11.4297C2.8238,11.5158 2.3842,12.1078 2.2194,12.3338C0.582,14.5403 0.2414,19.7819 3.4721,22.6019C3.8677,22.9463 7.8677,23.0001 10.593,23.0001H13.0106C14.4172,23.0001 15.5491,22.9463 16.2194,22.8925H18.4172C19.0371,22.87 19.6228,22.6086 20.0468,22.1651C20.4708,21.7216 20.6987,21.1319 20.6809,20.5246C20.6651,20.1473 20.5597,19.7788 20.3732,19.4483C20.838,19.0221 21.1273,18.4439 21.1864,17.823C21.1746,17.4599 21.123,17.0991 21.0326,16.7467C21.4151,16.2672 21.6411,15.6857 21.6809,15.0784C21.6382,14.7148 21.5304,14.3613 21.3622,14.0344ZM3.4721,13.3348C4.1902,13.1506 4.9278,13.0495 5.6699,13.0334C5.2075,14.2039 5.0254,15.4627 5.1378,16.7129C5.2502,17.9632 5.6541,19.1715 6.3183,20.2448C6.5579,20.6309 6.8378,20.9916 7.1535,21.3211C5.8018,21.3211 4.7579,21.2457 4.3293,21.2242C1.9886,18.9317 2.3732,14.917 3.4721,13.3348ZM19.7688,13.3348C19.6444,13.4595 19.5652,13.6207 19.5434,13.7938C19.5215,13.9668 19.5583,14.1421 19.648,14.2927C19.8004,14.5497 19.9081,14.8298 19.9666,15.1215C19.8789,15.4749 19.7012,15.8007 19.4502,16.0686C19.3532,16.1709 19.2859,16.2966 19.255,16.4328C19.2241,16.569 19.2308,16.7108 19.2743,16.8436C19.3792,17.154 19.4456,17.4756 19.4721,17.8015C19.312,18.1429 19.0663,18.4391 18.7578,18.6626C18.5961,18.788 18.4891,18.969 18.4585,19.1689C18.4279,19.3687 18.476,19.5725 18.593,19.7389C18.7682,19.9547 18.8955,20.204 18.9666,20.4708C18.9824,20.6497 18.9273,20.8278 18.8128,20.968C18.6982,21.1083 18.533,21.2 18.3513,21.2242H16.1535C15.4611,21.2888 14.3183,21.3318 12.9337,21.3318H12.1974C11.626,21.3318 11.0216,21.3318 10.3952,21.278C9.8427,21.1802 9.3214,20.9568 8.8729,20.626C8.4244,20.2952 8.0614,19.866 7.8128,19.3729C7.2808,18.5107 6.9567,17.5409 6.8653,16.5374C6.7739,15.5339 6.9175,14.5232 7.2853,13.5823C8.582,10.5363 12.3183,3.8955 12.9996,2.733C13.0258,2.684 13.0654,2.643 13.1138,2.6145C13.1623,2.586 13.2178,2.5712 13.2743,2.5716H13.4172C13.6809,2.6577 13.9447,2.959 13.9007,3.5833C13.767,4.9158 13.4832,6.2297 13.0546,7.5011C12.56,9.0941 12.2633,10.1919 12.7359,10.9453C12.8473,11.1157 12.9956,11.26 13.1703,11.3679C13.345,11.4758 13.5419,11.5446 13.7469,11.5696C15.7395,11.7155 17.7384,11.7622 19.7359,11.7095C19.9156,11.7362 20.0781,11.829 20.1904,11.969C20.3027,12.109 20.3563,12.2856 20.3403,12.463C20.2409,12.8031 20.0513,13.1113 19.7908,13.3563L19.7688,13.3348Z"
|
||||
android:fillColor="#000000"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/ic_gif_trending_24.xml
Normal file
10
app/src/main/res/drawable/ic_gif_trending_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M22.2627,6.2652C22.2585,5.851 21.9194,5.5186 21.5052,5.5228L16.0398,5.5773C15.6256,5.5814 15.2932,5.9206 15.2973,6.3348C15.3014,6.749 15.6406,7.0814 16.0548,7.0772L19.7499,7.0404L13.9091,12.8812L10.6212,9.5933C10.3283,9.3004 9.8535,9.3004 9.5606,9.5933L1.3788,17.7751C1.0859,18.068 1.0859,18.5429 1.3788,18.8358C1.6717,19.1287 2.1465,19.1287 2.4394,18.8358L10.0909,11.1843L13.3788,14.4721C13.6717,14.765 14.1465,14.765 14.4394,14.4721L20.7813,8.1302L20.8173,11.713C20.8215,12.1272 21.1606,12.4596 21.5748,12.4554C21.989,12.4513 22.3214,12.1121 22.3172,11.6979L22.2627,6.2652Z"
|
||||
android:fillColor="#000000"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -4,6 +4,6 @@
|
|||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@color/signal_icon_tint_primary"
|
||||
android:fillColor="@color/signal_inverse_primary"
|
||||
android:pathData="M21.2,5.072A5.55,5.55 0,0 0,18.928 2.8c-0.977,-0.522 -1.947,-0.8 -4.62,-0.8L9.692,2c-2.673,0 -3.643,0.278 -4.62,0.8A5.55,5.55 0,0 0,2.8 5.072c-0.522,0.977 -0.8,1.947 -0.8,4.62v4.616c0,2.673 0.278,3.643 0.8,4.62A5.55,5.55 0,0 0,5.072 21.2a7.326,7.326 0,0 0,3.616 0.785h0.1a3,3 0,0 0,1.7 -0.53L20.734,14.4A3,3 0,0 0,22 11.949L22,9.692C22,7.019 21.722,6.049 21.2,5.072ZM8.739,20.485a5.82,5.82 0,0 1,-2.96 -0.608,4.017 4.017,0 0,1 -1.656,-1.656c-0.365,-0.683 -0.623,-1.363 -0.623,-3.913L3.5,9.692c0,-2.55 0.258,-3.231 0.623,-3.913A4.017,4.017 0,0 1,5.779 4.123C6.462,3.758 7.142,3.5 9.692,3.5h4.616c2.55,0 3.231,0.258 3.913,0.623a4.017,4.017 0,0 1,1.656 1.656c0.365,0.683 0.623,1.363 0.623,3.913v2.257a1.519,1.519 0,0 1,-0.036 0.3L17.191,12.249c-2.8,0 -3.872,0.3 -4.975,0.89a6.172,6.172 0,0 0,-2.575 2.575c-0.575,1.074 -0.872,2.132 -0.888,4.769L8.739,20.483ZM10.264,19.785a6.631,6.631 0,0 1,0.7 -3.362,4.7 4.7,0 0,1 1.96,-1.961c0.755,-0.4 1.549,-0.712 4.268,-0.712h1.837Z"/>
|
||||
</vector>
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<selector xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:state_selected="true">
|
||||
<layer-list>
|
||||
<item android:top="8dp" android:left="8dp" android:bottom="8dp" android:right="8dp">
|
||||
<shape android:shape="rectangle">
|
||||
<solid android:color="@color/keyboard_pager_fragment_category_selected" />
|
||||
<corners android:radius="8dp" />
|
||||
</shape>
|
||||
</item>
|
||||
</layer-list>
|
||||
</item>
|
||||
</selector>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/keyboard_pager_fragment_selected" />
|
||||
<item
|
||||
android:bottom="6dp"
|
||||
android:drawable="@drawable/ic_emoji"
|
||||
android:left="6dp"
|
||||
android:right="6dp"
|
||||
android:top="6dp" />
|
||||
</layer-list>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/keyboard_pager_fragment_selected" />
|
||||
<item
|
||||
android:bottom="6dp"
|
||||
android:drawable="@drawable/ic_gif_outline_24"
|
||||
android:left="6dp"
|
||||
android:right="6dp"
|
||||
android:top="6dp" />
|
||||
</layer-list>
|
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<item android:drawable="@drawable/keyboard_pager_fragment_selected" />
|
||||
<item
|
||||
android:bottom="6dp"
|
||||
android:drawable="@drawable/ic_sticker_24"
|
||||
android:left="6dp"
|
||||
android:right="6dp"
|
||||
android:top="6dp" />
|
||||
</layer-list>
|
5
app/src/main/res/drawable/search_bar_end.xml
Normal file
5
app/src/main/res/drawable/search_bar_end.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/signal_background_primary" />
|
||||
<corners android:topRightRadius="100dp" android:bottomRightRadius="100dp" />
|
||||
</shape>
|
5
app/src/main/res/drawable/search_bar_start.xml
Normal file
5
app/src/main/res/drawable/search_bar_start.xml
Normal file
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shape xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<solid android:color="@color/signal_background_primary" />
|
||||
<corners android:topLeftRadius="100dp" android:bottomLeftRadius="100dp" />
|
||||
</shape>
|
13
app/src/main/res/layout/emoji_page_view_search.xml
Normal file
13
app/src/main/res/layout/emoji_page_view_search.xml
Normal file
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:id="@+id/emoji_search_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:click_only="true"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:search_hint="@string/KeyboardPagerFragment_search_emoji"
|
||||
app:show_always="true" />
|
45
app/src/main/res/layout/emoji_search_fragment.xml
Normal file
45
app/src/main/res/layout/emoji_search_fragment.xml
Normal file
|
@ -0,0 +1,45 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:background="@color/signal_background_secondary"
|
||||
android:paddingBottom="33dp">
|
||||
|
||||
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||
android:id="@+id/emoji_search_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="8dp"
|
||||
android:transitionName="emoji_search_transition"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:search_hint="@string/KeyboardPagerFragment_search_emoji"
|
||||
app:show_always="true" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/emoji_search_empty"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:gravity="center"
|
||||
android:text="@string/EmojiSearchFragment__no_results_found"
|
||||
android:visibility="gone"
|
||||
app:layout_constraintBottom_toBottomOf="@id/emoji_search_results_container"
|
||||
app:layout_constraintTop_toTopOf="@id/emoji_search_results_container"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/emoji_search_results_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/emoji_drawer_item_width"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:orientation="horizontal"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@id/emoji_search_view" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
38
app/src/main/res/layout/emoji_search_result_display_item.xml
Normal file
38
app/src/main/res/layout/emoji_search_result_display_item.xml
Normal file
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="@dimen/emoji_drawer_item_width"
|
||||
android:layout_height="@dimen/emoji_drawer_item_width"
|
||||
android:padding="2dp"
|
||||
android:background="?selectableItemBackground">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/emoji_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="center"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingStart="6dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.AsciiEmojiView
|
||||
android:id="@+id/emoji_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:paddingStart="6dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/emoji_variation_hint"
|
||||
android:layout_width="7dp"
|
||||
android:layout_height="7dp"
|
||||
android:layout_gravity="bottom|right|end"
|
||||
app:srcCompat="@drawable/triangle_bottom_right_corner"
|
||||
android:tint="@color/core_grey_25"/>
|
||||
|
||||
</FrameLayout>
|
81
app/src/main/res/layout/gif_keyboard_page_fragment.xml
Normal file
81
app/src/main/res/layout/gif_keyboard_page_fragment.xml
Normal file
|
@ -0,0 +1,81 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:background="@color/signal_background_secondary">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/gif_keyboard_search_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||
android:id="@+id/gif_keyboard_search_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
app:search_hint="@string/KeyboardPagerFragment_search_giphy"
|
||||
app:show_always="true"
|
||||
app:click_only="true"/>
|
||||
</FrameLayout>
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/gif_keyboard_giphy_frame"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
app:layout_constraintBottom_toTopOf="@id/gif_keyboard_packs_background"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/gif_keyboard_search_frame"
|
||||
app:layout_goneMarginTop="0dp" />
|
||||
|
||||
<View
|
||||
android:id="@+id/gif_keyboard_packs_background"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="56dp"
|
||||
android:background="@color/signal_background_secondary"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toStartOf="parent" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatImageView
|
||||
android:id="@+id/gif_keyboard_search"
|
||||
android:layout_width="48dp"
|
||||
android:layout_height="48dp"
|
||||
android:background="?selectableItemBackground"
|
||||
android:contentDescription="@string/KeyboardPagerFragment_open_gif_search"
|
||||
android:padding="13dp"
|
||||
android:scaleType="fitCenter"
|
||||
app:tint="@color/icon_tab_selector"
|
||||
app:layout_constraintBottom_toBottomOf="@id/gif_keyboard_packs_background"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="@id/gif_keyboard_packs_background"
|
||||
app:srcCompat="@drawable/ic_search_24" />
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/gif_keyboard_quick_search_recycler"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
|
||||
app:layout_constraintBottom_toBottomOf="@id/gif_keyboard_packs_background"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintStart_toEndOf="@id/gif_keyboard_search"
|
||||
app:layout_constraintTop_toTopOf="@id/gif_keyboard_packs_background"
|
||||
tools:itemCount="10"
|
||||
tools:listitem="@layout/keyboard_pager_category_icon" />
|
||||
|
||||
<View
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="1dp"
|
||||
android:layout_gravity="top"
|
||||
android:background="@color/signal_inverse_transparent_05"
|
||||
app:layout_constraintBottom_toTopOf="@+id/gif_keyboard_packs_background"/>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -2,8 +2,7 @@
|
|||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<androidx.coordinatorlayout.widget.CoordinatorLayout
|
||||
android:layout_width="match_parent"
|
||||
|
@ -14,15 +13,27 @@
|
|||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorPrimary">
|
||||
android:background="@color/signal_background_primary">
|
||||
|
||||
<org.thoughtcrime.securesms.giph.ui.GiphyActivityToolbar
|
||||
<FrameLayout
|
||||
android:id="@+id/giphy_toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/colorPrimary"
|
||||
android:theme="?attr/actionBarStyle"
|
||||
app:layout_scrollFlags="scroll|enterAlways" />
|
||||
android:background="@color/signal_background_primary"
|
||||
android:minHeight="?attr/actionBarSize"
|
||||
app:layout_scrollFlags="scroll|enterAlways">
|
||||
|
||||
<org.thoughtcrime.securesms.keyboard.emoji.KeyboardPageSearchView
|
||||
android:id="@+id/giphy_search_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
app:search_bar_tint="@color/signal_background_secondary"
|
||||
app:search_hint="@string/KeyboardPagerFragment_search_giphy"
|
||||
app:search_icon_tint="@color/signal_icon_tint_secondary"
|
||||
app:show_always="true" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
|
|
|
@ -1,13 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp">
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/still_image"
|
||||
<com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
android:id="@+id/container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitXY" />
|
||||
android:layout_height="0dp"
|
||||
android:layout_margin="3dp">
|
||||
|
||||
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/still_image"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:scaleType="fitXY"
|
||||
app:shapeAppearanceOverlay="@style/Signal.ShapeOverlay.Rounded" />
|
||||
|
||||
</com.google.android.exoplayer2.ui.AspectRatioFrameLayout>
|
||||
</FrameLayout>
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue