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>
fork-5.53.8
Android Team 2021-05-26 10:47:14 -03:00 zatwierdzone przez Cody Henthorne
rodzic 66c3b1388a
commit 08e86b8c82
119 zmienionych plików z 3545 dodań i 721 usunięć

Wyświetl plik

@ -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"

Wyświetl plik

@ -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");

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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)));

Wyświetl plik

@ -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;

Wyświetl plik

@ -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();
}
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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

Wyświetl plik

@ -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,

Wyświetl plik

@ -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();
}
}
}

Wyświetl plik

@ -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();

Wyświetl plik

@ -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;
}
}

Wyświetl plik

@ -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)

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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;

Wyświetl plik

@ -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() {}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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);
}
}
}

Wyświetl plik

@ -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());

Wyświetl plik

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.keyboard
enum class KeyboardPage {
EMOJI,
STICKER,
GIF
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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()
}
}
}

Wyświetl plik

@ -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
}
}
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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)
}
}
}

Wyświetl plik

@ -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))
}
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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()
}
}

Wyświetl plik

@ -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
}
}
}
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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)))
}
}
}

Wyświetl plik

@ -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)
}
}

Wyświetl plik

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.keyboard.gif
import androidx.lifecycle.ViewModel
class GifKeyboardPageViewModel : ViewModel() {
var selectedTab: GifQuickSearchOption = GifQuickSearchOption.TRENDING
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -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) }
}
}
}

Wyświetl plik

@ -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 } }
}
}

Wyświetl plik

@ -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
}
}

Wyświetl plik

@ -0,0 +1,7 @@
package org.thoughtcrime.securesms.keyboard.sticker
import androidx.lifecycle.ViewModel
class StickerKeyboardPageViewModel : ViewModel() {
var selectedTab: Int = 0
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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)))
}
}
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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);
}

Wyświetl plik

@ -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("\"", "\"\"") + "\"";
}
}

Wyświetl plik

@ -1292,6 +1292,6 @@ public class TextSecurePreferences {
// NEVER rename these -- they're persisted by name
public enum MediaKeyboardMode {
EMOJI, STICKER
EMOJI, STICKER, GIF
}
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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" />

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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