Add section to recent reactions page listing emoji already applied to message.

master
Alex Hart 2020-07-30 17:02:17 -03:00 committed by Greyson Parrelli
parent e55f4fe6b6
commit 4c30b39e71
21 changed files with 430 additions and 131 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 &amp; People</string>
<string name="ReactWithAnyEmojiBottomSheetDialogFragment__nature">Nature</string>