Add section to recent reactions page listing emoji already applied to message.
parent
e55f4fe6b6
commit
4c30b39e71
|
@ -4,6 +4,8 @@ import android.content.Context;
|
|||
import android.content.SharedPreferences;
|
||||
import android.os.AsyncTask;
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
@ -13,6 +15,7 @@ import com.fasterxml.jackson.databind.type.TypeFactory;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.util.JsonUtils;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
|
@ -73,6 +76,7 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
|||
return true;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public void onCodePointSelected(String emoji) {
|
||||
recentlyUsed.remove(emoji);
|
||||
recentlyUsed.add(emoji);
|
||||
|
@ -84,22 +88,16 @@ public class RecentEmojiPageModel implements EmojiPageModel {
|
|||
}
|
||||
|
||||
final LinkedHashSet<String> latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed);
|
||||
new AsyncTask<Void, Void, Void>() {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(Void... params) {
|
||||
try {
|
||||
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
||||
prefs.edit()
|
||||
.putString(preferenceName, serialized)
|
||||
.apply();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
|
||||
return null;
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
String serialized = JsonUtils.toJson(latestRecentlyUsed);
|
||||
prefs.edit()
|
||||
.putString(preferenceName, serialized)
|
||||
.apply();
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, e);
|
||||
}
|
||||
}.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
||||
});
|
||||
}
|
||||
|
||||
private String[] toReversePrimitiveArray(@NonNull LinkedHashSet<String> emojiSet) {
|
||||
|
|
|
@ -80,7 +80,7 @@ final class SafetyNumberChangeRepository {
|
|||
try {
|
||||
switch (messageType) {
|
||||
case MmsSmsDatabase.SMS_TRANSPORT:
|
||||
return DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
return DatabaseFactory.getSmsDatabase(context).getMessageRecord(messageId);
|
||||
case MmsSmsDatabase.MMS_TRANSPORT:
|
||||
return DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
default:
|
||||
|
|
|
@ -16,6 +16,8 @@ import org.thoughtcrime.securesms.database.documents.Document;
|
|||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatchList;
|
||||
import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ReactionList;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.insights.InsightsConstants;
|
||||
|
@ -55,6 +57,8 @@ public abstract class MessagingDatabase extends Database implements MmsSmsColumn
|
|||
public abstract void markAsSending(long messageId);
|
||||
public abstract void markAsRemoteDelete(long messageId);
|
||||
|
||||
public abstract MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException;
|
||||
|
||||
final int getInsecureMessagesSentForThread(long threadId) {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
String[] projection = new String[]{"COUNT(*)"};
|
||||
|
|
|
@ -445,6 +445,7 @@ public class MmsDatabase extends MessagingDatabase {
|
|||
return rawQuery(RAW_ID_WHERE, new String[] {messageId + ""});
|
||||
}
|
||||
|
||||
@Override
|
||||
public MessageRecord getMessageRecord(long messageId) throws NoSuchMessageException {
|
||||
try (Cursor cursor = rawQuery(RAW_ID_WHERE, new String[] {messageId + ""})) {
|
||||
MessageRecord record = new Reader(cursor).getNext();
|
||||
|
|
|
@ -854,7 +854,8 @@ public class SmsDatabase extends MessagingDatabase {
|
|||
return db.query(TABLE_NAME, MESSAGE_PROJECTION, where, null, null, null, null);
|
||||
}
|
||||
|
||||
public SmsMessageRecord getMessage(long messageId) throws NoSuchMessageException {
|
||||
@Override
|
||||
public SmsMessageRecord getMessageRecord(long messageId) throws NoSuchMessageException {
|
||||
SQLiteDatabase db = databaseHelper.getReadableDatabase();
|
||||
Cursor cursor = db.query(TABLE_NAME, MESSAGE_PROJECTION, ID_WHERE, new String[]{messageId + ""}, null, null, null);
|
||||
Reader reader = new Reader(cursor);
|
||||
|
|
|
@ -14,7 +14,6 @@ import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
|||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobmanager.Data;
|
||||
import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobManager;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
|
@ -72,7 +71,7 @@ public class PushTextSendJob extends PushSendJob {
|
|||
public void onPushSend() throws NoSuchMessageException, RetryLaterException {
|
||||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(messageId);
|
||||
SmsMessageRecord record = database.getMessageRecord(messageId);
|
||||
|
||||
if (!record.isPending() && !record.isFailed()) {
|
||||
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
|
|
|
@ -67,7 +67,7 @@ public class ReactionSendJob extends BaseJob {
|
|||
throws NoSuchMessageException
|
||||
{
|
||||
MessageRecord message = isMms ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId)
|
||||
: DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
: DatabaseFactory.getSmsDatabase(context).getMessageRecord(messageId);
|
||||
|
||||
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
|
||||
|
||||
|
@ -140,7 +140,7 @@ public class ReactionSendJob extends BaseJob {
|
|||
message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
} else {
|
||||
db = DatabaseFactory.getSmsDatabase(context);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getMessageRecord(messageId);
|
||||
}
|
||||
|
||||
Recipient targetAuthor = message.isOutgoing() ? Recipient.self() : message.getIndividualRecipient();
|
||||
|
|
|
@ -57,7 +57,7 @@ public class RemoteDeleteSendJob extends BaseJob {
|
|||
throws NoSuchMessageException
|
||||
{
|
||||
MessageRecord message = isMms ? DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId)
|
||||
: DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
: DatabaseFactory.getSmsDatabase(context).getMessageRecord(messageId);
|
||||
|
||||
Recipient conversationRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(message.getThreadId());
|
||||
|
||||
|
@ -119,7 +119,7 @@ public class RemoteDeleteSendJob extends BaseJob {
|
|||
message = DatabaseFactory.getMmsDatabase(context).getMessageRecord(messageId);
|
||||
} else {
|
||||
db = DatabaseFactory.getSmsDatabase(context);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getMessage(messageId);
|
||||
message = DatabaseFactory.getSmsDatabase(context).getMessageRecord(messageId);
|
||||
}
|
||||
|
||||
long targetSentTimestamp = message.getDateSent();
|
||||
|
|
|
@ -78,7 +78,7 @@ public class SmsSendJob extends SendJob {
|
|||
}
|
||||
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(messageId);
|
||||
SmsMessageRecord record = database.getMessageRecord(messageId);
|
||||
|
||||
if (!record.isPending() && !record.isFailed()) {
|
||||
warn(TAG, "Message " + messageId + " was already sent. Ignoring.");
|
||||
|
|
|
@ -92,7 +92,7 @@ public class SmsSentJob extends BaseJob {
|
|||
private void handleSentResult(long messageId, int result) {
|
||||
try {
|
||||
SmsDatabase database = DatabaseFactory.getSmsDatabase(context);
|
||||
SmsMessageRecord record = database.getMessage(messageId);
|
||||
SmsMessageRecord record = database.getMessageRecord(messageId);
|
||||
|
||||
switch (result) {
|
||||
case Activity.RESULT_OK:
|
||||
|
|
|
@ -4,7 +4,7 @@ import androidx.annotation.NonNull;
|
|||
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
||||
class ReactionDetails {
|
||||
public class ReactionDetails {
|
||||
private final Recipient sender;
|
||||
private final String baseEmoji;
|
||||
private final String displayEmoji;
|
||||
|
|
|
@ -1,53 +1,82 @@
|
|||
package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.widget.NestedScrollView;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiKeyboardProvider;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageView;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageViewGridAdapter;
|
||||
import org.thoughtcrime.securesms.util.adapter.AlwaysChangedDiffUtil;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
final class ReactWithAnyEmojiAdapter extends RecyclerView.Adapter<ReactWithAnyEmojiAdapter.ViewHolder> {
|
||||
final class ReactWithAnyEmojiAdapter extends ListAdapter<ReactWithAnyEmojiPage, ReactWithAnyEmojiAdapter.ReactWithAnyEmojiPageViewHolder> {
|
||||
|
||||
private static final int VIEW_TYPE_SINGLE = 0;
|
||||
private static final int VIEW_TYPE_DUAL = 1;
|
||||
|
||||
private final List<EmojiPageModel> models;
|
||||
private final EmojiKeyboardProvider.EmojiEventListener emojiEventListener;
|
||||
private final EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener;
|
||||
private final Callbacks callbacks;
|
||||
|
||||
ReactWithAnyEmojiAdapter(@NonNull List<EmojiPageModel> models,
|
||||
@NonNull EmojiKeyboardProvider.EmojiEventListener emojiEventListener,
|
||||
ReactWithAnyEmojiAdapter(@NonNull EmojiKeyboardProvider.EmojiEventListener emojiEventListener,
|
||||
@NonNull EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener,
|
||||
@NonNull Callbacks callbacks)
|
||||
{
|
||||
this.models = models;
|
||||
super(new AlwaysChangedDiffUtil<>());
|
||||
|
||||
this.emojiEventListener = emojiEventListener;
|
||||
this.variationSelectorListener = variationSelectorListener;
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
return new ViewHolder(new EmojiPageView(parent.getContext(), emojiEventListener, variationSelectorListener, true));
|
||||
public ReactWithAnyEmojiPage getItem(int position) {
|
||||
return super.getItem(position);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
holder.bind(models.get(position));
|
||||
public @NonNull ReactWithAnyEmojiPageViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
switch (viewType) {
|
||||
case VIEW_TYPE_SINGLE:
|
||||
return new SinglePageBlockViewHolder(createEmojiPageView(parent.getContext()));
|
||||
case VIEW_TYPE_DUAL:
|
||||
EmojiPageView block1 = createEmojiPageView(parent.getContext());
|
||||
EmojiPageView block2 = createEmojiPageView(parent.getContext());
|
||||
NestedScrollView scrollView = (NestedScrollView) LayoutInflater.from(parent.getContext()).inflate(R.layout.react_with_any_emoji_dual_block_item, parent, false);
|
||||
LinearLayout container = scrollView.findViewById(R.id.react_with_any_emoji_dual_block_item_container);
|
||||
|
||||
block1.setRecyclerNestedScrollingEnabled(false);
|
||||
block2.setRecyclerNestedScrollingEnabled(false);
|
||||
|
||||
container.addView(block1, 0);
|
||||
container.addView(block2);
|
||||
|
||||
return new DualPageBlockViewHolder(scrollView, block1, block2);
|
||||
default:
|
||||
throw new IllegalArgumentException("Unknown viewType: " + viewType);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return models.size();
|
||||
public void onBindViewHolder(@NonNull ReactWithAnyEmojiPageViewHolder holder, int position) {
|
||||
holder.bind(getItem(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewAttachedToWindow(@NonNull ViewHolder holder) {
|
||||
callbacks.onViewHolderAttached(holder.getAdapterPosition(), holder.emojiPageView);
|
||||
public void onViewAttachedToWindow(@NonNull ReactWithAnyEmojiPageViewHolder holder) {
|
||||
callbacks.onViewHolderAttached(holder.getAdapterPosition(), holder);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -59,14 +88,32 @@ final class ReactWithAnyEmojiAdapter extends RecyclerView.Adapter<ReactWithAnyEm
|
|||
recyclerView.setHasFixedSize(true);
|
||||
}
|
||||
|
||||
static final class ViewHolder extends RecyclerView.ViewHolder {
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return getItem(position).getPageBlocks().size() > 1 ? VIEW_TYPE_DUAL : VIEW_TYPE_SINGLE;
|
||||
}
|
||||
|
||||
private EmojiPageView createEmojiPageView(@NonNull Context context) {
|
||||
return new EmojiPageView(context, emojiEventListener, variationSelectorListener, true);
|
||||
}
|
||||
|
||||
static abstract class ReactWithAnyEmojiPageViewHolder extends RecyclerView.ViewHolder implements ScrollableChild {
|
||||
|
||||
public ReactWithAnyEmojiPageViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
}
|
||||
|
||||
abstract void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage);
|
||||
}
|
||||
|
||||
static final class SinglePageBlockViewHolder extends ReactWithAnyEmojiPageViewHolder {
|
||||
|
||||
private final EmojiPageView emojiPageView;
|
||||
|
||||
ViewHolder(@NonNull EmojiPageView itemView) {
|
||||
public SinglePageBlockViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
|
||||
emojiPageView = itemView;
|
||||
emojiPageView = (EmojiPageView) itemView;
|
||||
|
||||
ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT);
|
||||
|
@ -74,12 +121,52 @@ final class ReactWithAnyEmojiAdapter extends RecyclerView.Adapter<ReactWithAnyEm
|
|||
emojiPageView.setLayoutParams(params);
|
||||
}
|
||||
|
||||
void bind(@NonNull EmojiPageModel model) {
|
||||
emojiPageView.setModel(model);
|
||||
@Override
|
||||
void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage) {
|
||||
emojiPageView.setModel(reactWithAnyEmojiPage.getPageBlocks().get(0).getPageModel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNestedScrollingEnabled(boolean isEnabled) {
|
||||
emojiPageView.setRecyclerNestedScrollingEnabled(isEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
static final class DualPageBlockViewHolder extends ReactWithAnyEmojiPageViewHolder {
|
||||
|
||||
private final EmojiPageView block1;
|
||||
private final EmojiPageView block2;
|
||||
private final TextView block2Label;
|
||||
|
||||
public DualPageBlockViewHolder(@NonNull View itemView,
|
||||
@NonNull EmojiPageView block1,
|
||||
@NonNull EmojiPageView block2)
|
||||
{
|
||||
super(itemView);
|
||||
|
||||
this.block1 = block1;
|
||||
this.block2 = block2;
|
||||
this.block2Label = itemView.findViewById(R.id.react_with_any_emoji_dual_block_item_block_2_label);
|
||||
}
|
||||
|
||||
@Override
|
||||
void bind(@NonNull ReactWithAnyEmojiPage reactWithAnyEmojiPage) {
|
||||
block1.setModel(reactWithAnyEmojiPage.getPageBlocks().get(0).getPageModel());
|
||||
block2.setModel(reactWithAnyEmojiPage.getPageBlocks().get(1).getPageModel());
|
||||
block2Label.setText(reactWithAnyEmojiPage.getPageBlocks().get(1).getLabel());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNestedScrollingEnabled(boolean isEnabled) {
|
||||
((NestedScrollView) itemView).setNestedScrollingEnabled(isEnabled);
|
||||
}
|
||||
}
|
||||
|
||||
interface Callbacks {
|
||||
void onViewHolderAttached(int adapterPosition, EmojiPageView pageView);
|
||||
void onViewHolderAttached(int adapterPosition, ScrollableChild pageView);
|
||||
}
|
||||
|
||||
interface ScrollableChild {
|
||||
void setNestedScrollingEnabled(boolean isEnabled);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ import androidx.annotation.Nullable;
|
|||
import androidx.core.view.ViewCompat;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager2.widget.ViewPager2;
|
||||
|
||||
|
@ -35,6 +36,7 @@ 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.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsLoader;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
|
@ -48,13 +50,14 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
|||
private static final String ARG_MESSAGE_ID = "arg_message_id";
|
||||
private static final String ARG_IS_MMS = "arg_is_mms";
|
||||
|
||||
private ReactWithAnyEmojiViewModel viewModel;
|
||||
private TextSwitcher categoryLabel;
|
||||
private ViewPager2 categoryPager;
|
||||
private ReactWithAnyEmojiAdapter adapter;
|
||||
private OnPageChanged onPageChanged;
|
||||
private SparseArray<EmojiPageView> pageArray = new SparseArray<>();
|
||||
private Callback callback;
|
||||
private ReactWithAnyEmojiViewModel viewModel;
|
||||
private TextSwitcher categoryLabel;
|
||||
private ViewPager2 categoryPager;
|
||||
private ReactWithAnyEmojiAdapter adapter;
|
||||
private OnPageChanged onPageChanged;
|
||||
private SparseArray<ReactWithAnyEmojiAdapter.ScrollableChild> pageArray = new SparseArray<>();
|
||||
private Callback callback;
|
||||
private ReactionsLoader reactionsLoader;
|
||||
|
||||
public static DialogFragment createForMessageRecord(@NonNull MessageRecord messageRecord) {
|
||||
DialogFragment fragment = new ReactWithAnyEmojiBottomSheetDialogFragment();
|
||||
|
@ -122,12 +125,18 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
|||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||
reactionsLoader = new ReactionsLoader(requireContext(),
|
||||
requireArguments().getLong(ARG_MESSAGE_ID),
|
||||
requireArguments().getBoolean(ARG_IS_MMS));
|
||||
|
||||
LoaderManager.getInstance(requireActivity()).initLoader((int) requireArguments().getLong(ARG_MESSAGE_ID), null, reactionsLoader);
|
||||
|
||||
initializeViewModel();
|
||||
|
||||
categoryLabel = view.findViewById(R.id.category_label);
|
||||
categoryPager = view.findViewById(R.id.category_pager);
|
||||
categoryLabel = view.findViewById(R.id.category_label);
|
||||
categoryPager = view.findViewById(R.id.category_pager);
|
||||
|
||||
adapter = new ReactWithAnyEmojiAdapter(viewModel.getEmojiPageModels(), this, this, (position, pageView) -> {
|
||||
adapter = new ReactWithAnyEmojiAdapter(this, this, (position, pageView) -> {
|
||||
pageArray.put(position, pageView);
|
||||
|
||||
if (categoryPager.getCurrentItem() == position) {
|
||||
|
@ -140,10 +149,15 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
|||
categoryPager.setAdapter(adapter);
|
||||
categoryPager.registerOnPageChangeCallback(onPageChanged);
|
||||
|
||||
int startPateIndex = viewModel.getStartIndex();
|
||||
viewModel.getEmojiPageModels().observe(getViewLifecycleOwner(), pages -> {
|
||||
int pageToSet = adapter.getItemCount() == 0 ? (pages.get(0).hasEmoji() ? 0 : 1) : -1;
|
||||
|
||||
categoryPager.setCurrentItem(startPateIndex, false);
|
||||
presentCategoryLabel(viewModel.getCategoryIconAttr(startPateIndex));
|
||||
adapter.submitList(pages);
|
||||
|
||||
if (pageToSet >= 0) {
|
||||
categoryPager.setCurrentItem(pageToSet);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -166,7 +180,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
|||
|
||||
new TabLayoutMediator(categoryTabs, categoryPager, (tab, position) -> {
|
||||
tab.setCustomView(react_with_any_emoji_tab)
|
||||
.setIcon(ThemeUtil.getThemedDrawable(requireContext(), viewModel.getCategoryIconAttr(position)));
|
||||
.setIcon(ThemeUtil.getThemedDrawable(requireContext(), adapter.getItem(position).getIconAttr()));
|
||||
}).attach();
|
||||
}
|
||||
}
|
||||
|
@ -174,6 +188,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
|||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
LoaderManager.getInstance(requireActivity()).destroyLoader((int) requireArguments().getLong(ARG_MESSAGE_ID));
|
||||
|
||||
categoryPager.unregisterOnPageChangeCallback(onPageChanged);
|
||||
}
|
||||
|
@ -188,7 +203,7 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
|||
private void initializeViewModel() {
|
||||
Bundle args = requireArguments();
|
||||
ReactWithAnyEmojiRepository repository = new ReactWithAnyEmojiRepository(requireContext());
|
||||
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
|
||||
ReactWithAnyEmojiViewModel.Factory factory = new ReactWithAnyEmojiViewModel.Factory(reactionsLoader, repository, args.getLong(ARG_MESSAGE_ID), args.getBoolean(ARG_IS_MMS));
|
||||
|
||||
viewModel = ViewModelProviders.of(this, factory).get(ReactWithAnyEmojiViewModel.class);
|
||||
}
|
||||
|
@ -210,53 +225,16 @@ public final class ReactWithAnyEmojiBottomSheetDialogFragment extends BottomShee
|
|||
|
||||
private void updateFocusedRecycler(int position) {
|
||||
for (int i = 0; i < pageArray.size(); i++) {
|
||||
pageArray.valueAt(i).setRecyclerNestedScrollingEnabled(false);
|
||||
pageArray.valueAt(i).setNestedScrollingEnabled(false);
|
||||
}
|
||||
|
||||
EmojiPageView toFocus = pageArray.get(position);
|
||||
ReactWithAnyEmojiAdapter.ScrollableChild toFocus = pageArray.get(position);
|
||||
if (toFocus != null) {
|
||||
toFocus.setRecyclerNestedScrollingEnabled(true);
|
||||
toFocus.setNestedScrollingEnabled(true);
|
||||
categoryPager.requestLayout();
|
||||
}
|
||||
|
||||
presentCategoryLabel(viewModel.getCategoryIconAttr(position));
|
||||
}
|
||||
|
||||
private void presentCategoryLabel(@AttrRes int iconAttr) {
|
||||
switch (iconAttr) {
|
||||
case R.attr.emoji_category_recent:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__recently_used));
|
||||
break;
|
||||
case R.attr.emoji_category_people:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people));
|
||||
break;
|
||||
case R.attr.emoji_category_nature:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature));
|
||||
break;
|
||||
case R.attr.emoji_category_foods:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food));
|
||||
break;
|
||||
case R.attr.emoji_category_activity:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities));
|
||||
break;
|
||||
case R.attr.emoji_category_places:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places));
|
||||
break;
|
||||
case R.attr.emoji_category_objects:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects));
|
||||
break;
|
||||
case R.attr.emoji_category_symbols:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols));
|
||||
break;
|
||||
case R.attr.emoji_category_flags:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags));
|
||||
break;
|
||||
case R.attr.emoji_category_emoticons:
|
||||
categoryLabel.setText(getString(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons));
|
||||
break;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
categoryLabel.setText(getString(adapter.getItem(position).getLabel()));
|
||||
}
|
||||
|
||||
private class OnPageChanged extends ViewPager2.OnPageChangeCallback {
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.whispersystems.libsignal.util.guava.Preconditions;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Represents a swipeable page in the ReactWithAnyEmoji dialog fragment, encapsulating any
|
||||
* {@link ReactWithAnyEmojiPageBlock}s contained on that page. It is assumed that there is at least
|
||||
* one page present.
|
||||
*
|
||||
* This class also exposes several properties based off of that list, in order to allow the ReactWithAny
|
||||
* bottom sheet to properly lay out its tabs and assign labels as the user moves between pages.
|
||||
*/
|
||||
class ReactWithAnyEmojiPage {
|
||||
|
||||
private final List<ReactWithAnyEmojiPageBlock> pageBlocks;
|
||||
|
||||
ReactWithAnyEmojiPage(@NonNull List<ReactWithAnyEmojiPageBlock> pageBlocks) {
|
||||
Preconditions.checkArgument(!pageBlocks.isEmpty());
|
||||
|
||||
this.pageBlocks = pageBlocks;
|
||||
}
|
||||
|
||||
public @StringRes int getLabel() {
|
||||
return pageBlocks.get(0).getLabel();
|
||||
}
|
||||
|
||||
public boolean hasEmoji() {
|
||||
return !pageBlocks.get(0).getPageModel().getEmoji().isEmpty();
|
||||
}
|
||||
|
||||
public List<ReactWithAnyEmojiPageBlock> getPageBlocks() {
|
||||
return pageBlocks;
|
||||
}
|
||||
|
||||
public @AttrRes int getIconAttr() {
|
||||
return pageBlocks.get(0).getPageModel().getIconAttr();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,29 @@
|
|||
package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
|
||||
/**
|
||||
* Wraps a single "class" of Emojis, be it a predefined category, recents, etc. and provides
|
||||
* a label for that "class".
|
||||
*/
|
||||
class ReactWithAnyEmojiPageBlock {
|
||||
|
||||
private final int label;
|
||||
private final EmojiPageModel pageModel;
|
||||
|
||||
ReactWithAnyEmojiPageBlock(@StringRes int label, @NonNull EmojiPageModel pageModel) {
|
||||
this.label = label;
|
||||
this.pageModel = pageModel;
|
||||
}
|
||||
|
||||
public @StringRes int getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public EmojiPageModel getPageModel() {
|
||||
return pageModel;
|
||||
}
|
||||
}
|
|
@ -2,44 +2,117 @@ package org.thoughtcrime.securesms.reactions.any;
|
|||
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiUtil;
|
||||
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel;
|
||||
import org.thoughtcrime.securesms.database.DatabaseFactory;
|
||||
import org.thoughtcrime.securesms.database.MessagingDatabase;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.logging.Log;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionDetails;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.sms.MessageSender;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.concurrent.SignalExecutors;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
final class ReactWithAnyEmojiRepository {
|
||||
|
||||
private static final String TAG = Log.tag(ReactWithAnyEmojiRepository.class);
|
||||
|
||||
private static final String RECENT_STORAGE_KEY = "reactions_recent_emoji";
|
||||
|
||||
private final Context context;
|
||||
private final RecentEmojiPageModel recentEmojiPageModel;
|
||||
private final List<EmojiPageModel> emojiPageModels;
|
||||
private final List<ReactWithAnyEmojiPage> emojiPages;
|
||||
|
||||
ReactWithAnyEmojiRepository(@NonNull Context context) {
|
||||
this.context = context;
|
||||
this.recentEmojiPageModel = new RecentEmojiPageModel(context, RECENT_STORAGE_KEY);
|
||||
this.emojiPageModels = new LinkedList<>();
|
||||
this.emojiPages = new LinkedList<>();
|
||||
|
||||
emojiPageModels.add(recentEmojiPageModel);
|
||||
emojiPageModels.addAll(EmojiUtil.getDisplayPages());
|
||||
emojiPageModels.remove(emojiPageModels.size() - 1);
|
||||
emojiPages.addAll(Stream.of(EmojiUtil.getDisplayPages())
|
||||
.map(page -> new ReactWithAnyEmojiPage(Collections.singletonList(new ReactWithAnyEmojiPageBlock(getCategoryLabel(page.getIconAttr()), page))))
|
||||
.toList());
|
||||
emojiPages.remove(emojiPages.size() - 1);
|
||||
}
|
||||
|
||||
List<EmojiPageModel> getEmojiPageModels() {
|
||||
return emojiPageModels;
|
||||
List<ReactWithAnyEmojiPage> getEmojiPageModels(@NonNull List<ReactionDetails> thisMessagesReactions) {
|
||||
List<ReactWithAnyEmojiPage> pages = new LinkedList<>();
|
||||
List<String> thisMessage = Stream.of(thisMessagesReactions)
|
||||
.map(ReactionDetails::getDisplayEmoji)
|
||||
.distinct()
|
||||
.toList();
|
||||
|
||||
if (thisMessage.isEmpty()) {
|
||||
pages.add(new ReactWithAnyEmojiPage(Collections.singletonList(new ReactWithAnyEmojiPageBlock(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__recently_used, recentEmojiPageModel))));
|
||||
} else {
|
||||
pages.add(new ReactWithAnyEmojiPage(Arrays.asList(new ReactWithAnyEmojiPageBlock(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__this_message, new ThisMessageEmojiPageModel(thisMessage)),
|
||||
new ReactWithAnyEmojiPageBlock(R.string.ReactWithAnyEmojiBottomSheetDialogFragment__recently_used, recentEmojiPageModel))));
|
||||
}
|
||||
|
||||
pages.addAll(emojiPages);
|
||||
|
||||
return pages;
|
||||
}
|
||||
|
||||
void addEmojiToMessage(@NonNull String emoji, long messageId, boolean isMms) {
|
||||
recentEmojiPageModel.onCodePointSelected(emoji);
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
try {
|
||||
MessagingDatabase db = isMms ? DatabaseFactory.getMmsDatabase(context) : DatabaseFactory.getSmsDatabase(context);
|
||||
MessageRecord messageRecord = db.getMessageRecord(messageId);
|
||||
ReactionRecord oldRecord = Stream.of(messageRecord.getReactions())
|
||||
.filter(record -> record.getAuthor().equals(Recipient.self().getId()))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
|
||||
SignalExecutors.BOUNDED.execute(() -> MessageSender.sendNewReaction(context, messageId, isMms, emoji));
|
||||
if (oldRecord != null && oldRecord.getEmoji().equals(emoji)) {
|
||||
MessageSender.sendReactionRemoval(context, messageRecord.getId(), messageRecord.isMms(), oldRecord);
|
||||
} else {
|
||||
MessageSender.sendNewReaction(context, messageRecord.getId(), messageRecord.isMms(), emoji);
|
||||
Util.runOnMain(() -> recentEmojiPageModel.onCodePointSelected(emoji));
|
||||
}
|
||||
} catch (NoSuchMessageException e) {
|
||||
Log.w(TAG, "Message not found! Ignoring.");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private @StringRes int getCategoryLabel(@AttrRes int iconAttr) {
|
||||
switch (iconAttr) {
|
||||
case R.attr.emoji_category_people:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people;
|
||||
case R.attr.emoji_category_nature:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__nature;
|
||||
case R.attr.emoji_category_foods:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__food;
|
||||
case R.attr.emoji_category_activity:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__activities;
|
||||
case R.attr.emoji_category_places:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__places;
|
||||
case R.attr.emoji_category_objects:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__objects;
|
||||
case R.attr.emoji_category_symbols:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__symbols;
|
||||
case R.attr.emoji_category_flags:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__flags;
|
||||
case R.attr.emoji_category_emoticons:
|
||||
return R.string.ReactWithAnyEmojiBottomSheetDialogFragment__emoticons;
|
||||
default:
|
||||
throw new AssertionError();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,32 +2,40 @@ package org.thoughtcrime.securesms.reactions.any;
|
|||
|
||||
import androidx.annotation.AttrRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.reactions.ReactionsLoader;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
||||
|
||||
private final ReactionsLoader reactionsLoader;
|
||||
private final ReactWithAnyEmojiRepository repository;
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
|
||||
private ReactWithAnyEmojiViewModel(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
|
||||
this.repository = repository;
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
private final LiveData<List<ReactWithAnyEmojiPage>> pages;
|
||||
|
||||
private ReactWithAnyEmojiViewModel(@NonNull ReactionsLoader reactionsLoader,
|
||||
@NonNull ReactWithAnyEmojiRepository repository,
|
||||
long messageId,
|
||||
boolean isMms) {
|
||||
this.reactionsLoader = reactionsLoader;
|
||||
this.repository = repository;
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
this.pages = Transformations.map(reactionsLoader.getReactions(), repository::getEmojiPageModels);
|
||||
}
|
||||
|
||||
List<EmojiPageModel> getEmojiPageModels() {
|
||||
return repository.getEmojiPageModels();
|
||||
}
|
||||
|
||||
int getStartIndex() {
|
||||
return repository.getEmojiPageModels().get(0).getEmoji().size() == 0 ? 1 : 0;
|
||||
LiveData<List<ReactWithAnyEmojiPage>> getEmojiPageModels() {
|
||||
return pages;
|
||||
}
|
||||
|
||||
void onEmojiSelected(@NonNull String emoji) {
|
||||
|
@ -35,26 +43,24 @@ public final class ReactWithAnyEmojiViewModel extends ViewModel {
|
|||
repository.addEmojiToMessage(emoji, messageId, isMms);
|
||||
}
|
||||
|
||||
@AttrRes int getCategoryIconAttr(int position) {
|
||||
return repository.getEmojiPageModels().get(position).getIconAttr();
|
||||
}
|
||||
|
||||
static class Factory implements ViewModelProvider.Factory {
|
||||
|
||||
private final ReactionsLoader reactionsLoader;
|
||||
private final ReactWithAnyEmojiRepository repository;
|
||||
private final long messageId;
|
||||
private final boolean isMms;
|
||||
|
||||
Factory(@NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
|
||||
this.repository = repository;
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
Factory(@NonNull ReactionsLoader reactionsLoader, @NonNull ReactWithAnyEmojiRepository repository, long messageId, boolean isMms) {
|
||||
this.reactionsLoader = reactionsLoader;
|
||||
this.repository = repository;
|
||||
this.messageId = messageId;
|
||||
this.isMms = isMms;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
|
||||
//noinspection ConstantConditions
|
||||
return modelClass.cast(new ReactWithAnyEmojiViewModel(repository, messageId, isMms));
|
||||
return modelClass.cast(new ReactWithAnyEmojiViewModel(reactionsLoader, repository, messageId, isMms));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package org.thoughtcrime.securesms.reactions.any;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.Emoji;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiPageModel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Contains the Emojis that have been used in reactions for a given message.
|
||||
*/
|
||||
class ThisMessageEmojiPageModel implements EmojiPageModel {
|
||||
|
||||
private final List<String> emoji;
|
||||
|
||||
ThisMessageEmojiPageModel(@NonNull List<String> emoji) {
|
||||
this.emoji = emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getIconAttr() {
|
||||
return R.attr.emoji_category_recent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<String> getEmoji() {
|
||||
return emoji;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull List<Emoji> getDisplayEmoji() {
|
||||
return Stream.of(getEmoji()).map(Emoji::new).toList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasSpriteMap() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable String getSprite() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isDynamic() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -502,7 +502,7 @@ public class MessageSender {
|
|||
ExpiringMessageManager expirationManager = ApplicationContext.getInstance(context).getExpiringMessageManager();
|
||||
SmsDatabase smsDatabase = DatabaseFactory.getSmsDatabase(context);
|
||||
MmsSmsDatabase mmsSmsDatabase = DatabaseFactory.getMmsSmsDatabase(context);
|
||||
SmsMessageRecord message = smsDatabase.getMessage(messageId);
|
||||
SmsMessageRecord message = smsDatabase.getMessageRecord(messageId);
|
||||
SyncMessageId syncId = new SyncMessageId(Recipient.self().getId(), message.getDateSent());
|
||||
|
||||
smsDatabase.markAsSent(messageId, true);
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.core.widget.NestedScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/react_with_any_emoji_dual_block_item_container"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/react_with_any_emoji_dual_block_item_block_2_label"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="10dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="10dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Subtitle2"
|
||||
android:textColor="?attr/title_text_color_secondary" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.core.widget.NestedScrollView>
|
|
@ -1809,6 +1809,7 @@
|
|||
<string name="HelpFragment__no_email_app_found">No email app found.</string>
|
||||
|
||||
<!-- ReactWithAnyEmojiBottomSheetDialogFragment -->
|
||||
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__this_message">This Message</string>
|
||||
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__recently_used">Recently Used</string>
|
||||
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__smileys_and_people">Smileys & People</string>
|
||||
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__nature">Nature</string>
|
||||
|
|
Loading…
Reference in New Issue