diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java index 724b151da1..aa4c720715 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/emoji/RecentEmojiPageModel.java @@ -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 latestRecentlyUsed = new LinkedHashSet<>(recentlyUsed); - new AsyncTask() { - - @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 emojiSet) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java index 4a5222f46c..defa379c63 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/error/SafetyNumberChangeRepository.java @@ -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: diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java index a2f9068744..e5b9f785e9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MessagingDatabase.java @@ -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(*)"}; diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 2bad741e87..677ff1f2f7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java index 32d6913da4..dd0fd43274 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/SmsDatabase.java @@ -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); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java index 778738dbf7..7467e931dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushTextSendJob.java @@ -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."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java index 1571fa23f7..d2a600a1bf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/ReactionSendJob.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java index e3c127304f..6cb7922b52 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/RemoteDeleteSendJob.java @@ -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(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java index 2a962b050e..9d0cdd47e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSendJob.java @@ -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."); diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java index 270d1a3afd..3ca44ab800 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/SmsSentJob.java @@ -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: diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.java index 15d6165f82..8d8137c574 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/ReactionDetails.java @@ -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; diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java index f6c847fc75..be62b4933f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiAdapter.java @@ -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 { +final class ReactWithAnyEmojiAdapter extends ListAdapter { + + private static final int VIEW_TYPE_SINGLE = 0; + private static final int VIEW_TYPE_DUAL = 1; - private final List models; private final EmojiKeyboardProvider.EmojiEventListener emojiEventListener; private final EmojiPageViewGridAdapter.VariationSelectorListener variationSelectorListener; private final Callbacks callbacks; - ReactWithAnyEmojiAdapter(@NonNull List 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 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 pageArray = new SparseArray<>(); - private Callback callback; + private ReactWithAnyEmojiViewModel viewModel; + private TextSwitcher categoryLabel; + private ViewPager2 categoryPager; + private ReactWithAnyEmojiAdapter adapter; + private OnPageChanged onPageChanged; + private SparseArray 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 { diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPage.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPage.java new file mode 100644 index 0000000000..db25f0e583 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPage.java @@ -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 pageBlocks; + + ReactWithAnyEmojiPage(@NonNull List 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 getPageBlocks() { + return pageBlocks; + } + + public @AttrRes int getIconAttr() { + return pageBlocks.get(0).getPageModel().getIconAttr(); + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPageBlock.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPageBlock.java new file mode 100644 index 0000000000..e175312ae2 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiPageBlock.java @@ -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; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java index 229575ca30..47f21945ae 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiRepository.java @@ -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 emojiPageModels; + private final List 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 getEmojiPageModels() { - return emojiPageModels; + List getEmojiPageModels(@NonNull List thisMessagesReactions) { + List pages = new LinkedList<>(); + List 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(); + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java index ff5022d003..6c37332a9e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ReactWithAnyEmojiViewModel.java @@ -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> 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 getEmojiPageModels() { - return repository.getEmojiPageModels(); - } - - int getStartIndex() { - return repository.getEmojiPageModels().get(0).getEmoji().size() == 0 ? 1 : 0; + LiveData> 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 create(@NonNull Class modelClass) { //noinspection ConstantConditions - return modelClass.cast(new ReactWithAnyEmojiViewModel(repository, messageId, isMms)); + return modelClass.cast(new ReactWithAnyEmojiViewModel(reactionsLoader, repository, messageId, isMms)); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java new file mode 100644 index 0000000000..cf9bc69c18 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/reactions/any/ThisMessageEmojiPageModel.java @@ -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 emoji; + + ThisMessageEmojiPageModel(@NonNull List emoji) { + this.emoji = emoji; + } + + @Override + public int getIconAttr() { + return R.attr.emoji_category_recent; + } + + @Override + public @NonNull List getEmoji() { + return emoji; + } + + @Override + public @NonNull List 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; + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java index b0738ffd57..2c24a26303 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java +++ b/app/src/main/java/org/thoughtcrime/securesms/sms/MessageSender.java @@ -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); diff --git a/app/src/main/res/layout/react_with_any_emoji_dual_block_item.xml b/app/src/main/res/layout/react_with_any_emoji_dual_block_item.xml new file mode 100644 index 0000000000..c67ca8aaae --- /dev/null +++ b/app/src/main/res/layout/react_with_any_emoji_dual_block_item.xml @@ -0,0 +1,24 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 97e506ce5e..48536459cd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1809,6 +1809,7 @@ No email app found. + This Message Recently Used Smileys & People Nature