From 59c49254e753ede12879e8b3f624f873c09515bf Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 23 Apr 2021 15:29:59 -0400 Subject: [PATCH] Insert temporary warning update message during message request state. --- .../securesms/BindableConversationItem.java | 2 + .../conversation/ConversationData.java | 75 ++++++++---- .../conversation/ConversationDataSource.java | 22 ++-- .../conversation/ConversationFragment.java | 19 ++- .../conversation/ConversationRepository.java | 46 ++++++-- .../conversation/ConversationUpdateItem.java | 10 ++ .../conversation/ConversationViewModel.java | 11 +- .../database/model/InMemoryMessageRecord.java | 111 ++++++++++++++++++ .../database/model/MessageRecord.java | 4 + app/src/main/res/values/signal_styles.xml | 2 +- app/src/main/res/values/strings.xml | 9 ++ 11 files changed, 263 insertions(+), 48 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index b0219c3de..5437c267c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState; import org.thoughtcrime.securesms.contactshare.Contact; import org.thoughtcrime.securesms.conversation.ConversationItem; import org.thoughtcrime.securesms.conversation.ConversationMessage; +import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Playable; @@ -75,6 +76,7 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable { void onInviteFriendsToGroupClicked(@NonNull GroupId.V2 groupId); void onEnableCallNotificationsClicked(); void onPlayInlineContent(ConversationMessage conversationMessage); + void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord); /** @return true if handled, false if you want to let the normal url handling continue */ boolean onUrlClicked(@NonNull String url); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java index 61d8682a8..176d7aad6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationData.java @@ -1,35 +1,37 @@ package org.thoughtcrime.securesms.conversation; +import androidx.annotation.NonNull; + /** * Represents metadata about a conversation. */ final class ConversationData { - private final long threadId; - private final long lastSeen; - private final int lastSeenPosition; - private final int lastScrolledPosition; - private final boolean hasSent; - private final boolean isMessageRequestAccepted; - private final int jumpToPosition; - private final int threadSize; + private final long threadId; + private final long lastSeen; + private final int lastSeenPosition; + private final int lastScrolledPosition; + private final boolean hasSent; + private final int jumpToPosition; + private final int threadSize; + private final MessageRequestData messageRequestData; ConversationData(long threadId, long lastSeen, int lastSeenPosition, int lastScrolledPosition, boolean hasSent, - boolean isMessageRequestAccepted, int jumpToPosition, - int threadSize) + int threadSize, + @NonNull MessageRequestData messageRequestData) { - this.threadId = threadId; - this.lastSeen = lastSeen; - this.lastSeenPosition = lastSeenPosition; - this.lastScrolledPosition = lastScrolledPosition; - this.hasSent = hasSent; - this.isMessageRequestAccepted = isMessageRequestAccepted; - this.jumpToPosition = jumpToPosition; - this.threadSize = threadSize; + this.threadId = threadId; + this.lastSeen = lastSeen; + this.lastSeenPosition = lastSeenPosition; + this.lastScrolledPosition = lastScrolledPosition; + this.hasSent = hasSent; + this.jumpToPosition = jumpToPosition; + this.threadSize = threadSize; + this.messageRequestData = messageRequestData; } public long getThreadId() { @@ -52,10 +54,6 @@ final class ConversationData { return hasSent; } - boolean isMessageRequestAccepted() { - return isMessageRequestAccepted; - } - boolean shouldJumpToMessage() { return jumpToPosition >= 0; } @@ -71,4 +69,37 @@ final class ConversationData { int getThreadSize() { return threadSize; } + + @NonNull MessageRequestData getMessageRequestData() { + return messageRequestData; + } + + static final class MessageRequestData { + + private final boolean messageRequestAccepted; + private final boolean groupsInCommon; + private final boolean isGroup; + + public MessageRequestData(boolean messageRequestAccepted) { + this(messageRequestAccepted, false, false); + } + + public MessageRequestData(boolean messageRequestAccepted, boolean groupsInCommon, boolean isGroup) { + this.messageRequestAccepted = messageRequestAccepted; + this.groupsInCommon = groupsInCommon; + this.isGroup = isGroup; + } + + public boolean isMessageRequestAccepted() { + return messageRequestAccepted; + } + + public boolean includeWarningUpdateMessage() { + return !messageRequestAccepted && !groupsInCommon; + } + + public boolean isGroup() { + return isGroup; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java index 6bdfdc6ef..427f248a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationDataSource.java @@ -9,9 +9,11 @@ import com.annimon.stream.Stream; import org.signal.core.util.logging.Log; import org.signal.paging.PagedDataSource; +import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData; import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory; import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MmsSmsDatabase; +import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord; import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.util.Stopwatch; @@ -30,18 +32,20 @@ class ConversationDataSource implements PagedDataSource { private static final String TAG = Log.tag(ConversationDataSource.class); - private final Context context; - private final long threadId; + private final Context context; + private final long threadId; + private final MessageRequestData messageRequestData; - ConversationDataSource(@NonNull Context context, long threadId) { - this.context = context; - this.threadId = threadId; + ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData) { + this.context = context; + this.threadId = threadId; + this.messageRequestData = messageRequestData; } @Override public int size() { long startTime = System.currentTimeMillis(); - int size = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId); + int size = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId) + (messageRequestData.includeWarningUpdateMessage() ? 1 : 0); Log.d(TAG, "size() for thread " + threadId + ": " + (System.currentTimeMillis() - startTime) + " ms"); @@ -55,7 +59,7 @@ class ConversationDataSource implements PagedDataSource { List records = new ArrayList<>(length); MentionHelper mentionHelper = new MentionHelper(); - try (MmsSmsDatabase.Reader reader = db.readerFor(db.getConversation(threadId, start, length))) { + try (MmsSmsDatabase.Reader reader = MmsSmsDatabase.readerFor(db.getConversation(threadId, start, length))) { MessageRecord record; while ((record = reader.getNext()) != null && !cancellationSignal.isCanceled()) { records.add(record); @@ -63,6 +67,10 @@ class ConversationDataSource implements PagedDataSource { } } + if (messageRequestData.includeWarningUpdateMessage() && (start + length >= size())) { + records.add(new InMemoryMessageRecord.NoGroupsInCommon(threadId, messageRequestData.isGroup())); + } + stopwatch.split("messages"); mentionHelper.fetchMentions(context); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java index 7d7331d48..2cf89efb9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationFragment.java @@ -64,6 +64,7 @@ import androidx.recyclerview.widget.RecyclerView.OnScrollListener; import com.annimon.stream.Collectors; import com.annimon.stream.Stream; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; import org.signal.core.util.StreamUtil; import org.signal.core.util.concurrent.SignalExecutors; @@ -93,6 +94,7 @@ import org.thoughtcrime.securesms.database.DatabaseFactory; import org.thoughtcrime.securesms.database.MessageDatabase; import org.thoughtcrime.securesms.database.MmsDatabase; import org.thoughtcrime.securesms.database.SmsDatabase; +import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord; import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.MmsMessageRecord; @@ -1040,7 +1042,7 @@ public class ConversationFragment extends LoggingFragment { adapter.setFooterView(conversationBanner); Runnable afterScroll = () -> { - if (!conversation.isMessageRequestAccepted()) { + if (!conversation.getMessageRequestData().isMessageRequestAccepted()) { snapToTopDataObserver.requestScrollPosition(adapter.getItemCount() - 1); } @@ -1065,7 +1067,7 @@ public class ConversationFragment extends LoggingFragment { getListAdapter().pulseAtPosition(conversation.getJumpToPosition()); }) .submit(); - } else if (conversation.isMessageRequestAccepted()) { + } else if (conversation.getMessageRequestData().isMessageRequestAccepted()) { snapToTopDataObserver.buildScrollPosition(conversation.shouldScrollToLastSeen() ? lastSeenPosition : lastScrolledPosition) .withOnPerformScroll((layoutManager, position) -> layoutManager.scrollToPositionWithOffset(position, list.getHeight())) .withOnScrollRequestComplete(afterScroll) @@ -1609,6 +1611,19 @@ public class ConversationFragment extends LoggingFragment { public void onPlayInlineContent(ConversationMessage conversationMessage) { getListAdapter().playInlineContent(conversationMessage); } + + @Override + public void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord) { + if (messageRecord instanceof InMemoryMessageRecord.NoGroupsInCommon) { + boolean isGroup = ((InMemoryMessageRecord.NoGroupsInCommon) messageRecord).isGroup(); + new MaterialAlertDialogBuilder(requireContext(), R.style.Signal_ThemeOverlay_Dialog_Rounded) + .setMessage(isGroup ? R.string.GroupsInCommonMessageRequest__none_of_your_contacts_or_people_you_chat_with_are_in_this_group + : R.string.GroupsInCommonMessageRequest__you_have_no_groups_in_common_with_this_person) + .setNeutralButton(R.string.GroupsInCommonMessageRequest__about_message_requests, (d, w) -> CommunicationActions.openBrowserLink(requireContext(), getString(R.string.GroupsInCommonMessageRequest__support_article))) + .setPositiveButton(R.string.GroupsInCommonMessageRequest__okay, null) + .show(); + } + } } public void refreshList() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java index 2c887275b..14655907e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationRepository.java @@ -10,13 +10,16 @@ import androidx.lifecycle.MutableLiveData; import org.signal.core.util.concurrent.SignalExecutors; import org.thoughtcrime.securesms.database.DatabaseFactory; +import org.thoughtcrime.securesms.database.GroupDatabase; import org.thoughtcrime.securesms.database.ThreadDatabase; import org.thoughtcrime.securesms.dependencies.ApplicationDependencies; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.util.BubbleUtil; import org.thoughtcrime.securesms.util.ConversationUtil; +import org.whispersystems.libsignal.util.guava.Optional; +import java.util.List; import java.util.concurrent.Executor; class ConversationRepository { @@ -51,16 +54,15 @@ class ConversationRepository { } private @NonNull ConversationData getConversationDataInternal(long threadId, int jumpToPosition) { - ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId); - int threadSize = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId); - - long lastSeen = metadata.getLastSeen(); - boolean hasSent = metadata.hasSent(); - int lastSeenPosition = 0; - long lastScrolled = metadata.getLastScrolled(); - int lastScrolledPosition = 0; - - boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId); + ThreadDatabase.ConversationMetadata metadata = DatabaseFactory.getThreadDatabase(context).getConversationMetadata(threadId); + int threadSize = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId); + long lastSeen = metadata.getLastSeen(); + boolean hasSent = metadata.hasSent(); + int lastSeenPosition = 0; + long lastScrolled = metadata.getLastScrolled(); + int lastScrolledPosition = 0; + boolean isMessageRequestAccepted = RecipientUtil.isMessageRequestAccepted(context, threadId); + ConversationData.MessageRequestData messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted); if (lastSeen > 0) { lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastSeen); @@ -74,6 +76,28 @@ class ConversationRepository { lastScrolledPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastScrolled); } - return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, isMessageRequestAccepted, jumpToPosition, threadSize); + if (!isMessageRequestAccepted) { + boolean isGroup = false; + boolean recipientIsKnownOrHasGroupsInCommon = false; + Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId); + if (threadRecipient.isGroup()) { + Optional group = DatabaseFactory.getGroupDatabase(context).getGroup(threadRecipient.getId()); + if (group.isPresent()) { + List recipients = Recipient.resolvedList(group.get().getMembers()); + for (Recipient recipient : recipients) { + if ((recipient.isProfileSharing() || recipient.hasGroupsInCommon()) && !recipient.isSelf()) { + recipientIsKnownOrHasGroupsInCommon = true; + break; + } + } + } + isGroup = true; + } else if (threadRecipient.hasGroupsInCommon()) { + recipientIsKnownOrHasGroupsInCommon = true; + } + messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted, recipientIsKnownOrHasGroupsInCommon, isGroup); + } + + return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, jumpToPosition, threadSize, messageRequestData); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 680773b23..03f4f0222 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -27,6 +27,7 @@ import org.thoughtcrime.securesms.VerifyIdentityActivity; import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog; import org.thoughtcrime.securesms.database.IdentityDatabase.IdentityRecord; import org.thoughtcrime.securesms.database.model.GroupCallUpdateDetailsUtil; +import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord; import org.thoughtcrime.securesms.database.model.LiveUpdateMessage; import org.thoughtcrime.securesms.database.model.MessageRecord; import org.thoughtcrime.securesms.database.model.UpdateDescription; @@ -340,6 +341,15 @@ public final class ConversationUpdateItem extends FrameLayout eventListener.onEnableCallNotificationsClicked(); } }); + } else if (conversationMessage.getMessageRecord().isInMemoryMessageRecord() && ((InMemoryMessageRecord) conversationMessage.getMessageRecord()).showActionButton()) { + InMemoryMessageRecord inMemoryMessageRecord = (InMemoryMessageRecord) conversationMessage.getMessageRecord(); + actionButton.setVisibility(VISIBLE); + actionButton.setText(inMemoryMessageRecord.getActionButtonText()); + actionButton.setOnClickListener(v -> { + if (eventListener != null) { + eventListener.onInMemoryMessageClicked(inMemoryMessageRecord); + } + }); } else { actionButton.setVisibility(GONE); actionButton.setOnClickListener(null); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java index 5aa84297d..957e5b87b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationViewModel.java @@ -21,7 +21,6 @@ import org.thoughtcrime.securesms.mediasend.Media; import org.thoughtcrime.securesms.mediasend.MediaRepository; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.util.livedata.LiveDataUtil; import org.thoughtcrime.securesms.wallpaper.ChatWallpaper; import org.whispersystems.libsignal.util.Pair; @@ -72,12 +71,14 @@ class ConversationViewModel extends ViewModel { }); LiveData>> pagedDataForThreadId = Transformations.map(metadata, data -> { - final int startPosition; + int startPosition; + ConversationData.MessageRequestData messageRequestData = data.getMessageRequestData(); + if (data.shouldJumpToMessage()) { startPosition = data.getJumpToPosition(); - } else if (data.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) { + } else if (messageRequestData.isMessageRequestAccepted() && data.shouldScrollToLastSeen()) { startPosition = data.getLastSeenPosition(); - } else if (data.isMessageRequestAccepted()) { + } else if (messageRequestData.isMessageRequestAccepted()) { startPosition = data.getLastScrolledPosition(); } else { startPosition = data.getThreadSize(); @@ -86,7 +87,7 @@ class ConversationViewModel extends ViewModel { ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver); ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), messageObserver); - ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId()); + ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData); PagingConfig config = new PagingConfig.Builder() .setPageSize(25) .setBufferPages(3) diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java new file mode 100644 index 000000000..a3f993bf3 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/InMemoryMessageRecord.java @@ -0,0 +1,111 @@ +package org.thoughtcrime.securesms.database.model; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.StringRes; + +import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.recipients.Recipient; + +import java.util.Collections; + +/** + * In memory message record for use in temporary conversation messages. + */ +public class InMemoryMessageRecord extends MessageRecord { + + private InMemoryMessageRecord(long id, + String body, + Recipient conversationRecipient, + long threadId, + long type) + { + super(id, + body, + conversationRecipient, + conversationRecipient, + 1, + System.currentTimeMillis(), + System.currentTimeMillis(), + System.currentTimeMillis(), + threadId, + 0, + 0, + type, + Collections.emptyList(), + Collections.emptyList(), + -1, + 0, + System.currentTimeMillis(), + 0, + false, + Collections.emptyList(), + false, + 0, + 0); + } + + @Override + public boolean isMms() { + return false; + } + + @Override + public boolean isMmsNotification() { + return false; + } + + @Override + public boolean isInMemoryMessageRecord() { + return true; + } + + public boolean showActionButton() { + return false; + } + + public @StringRes int getActionButtonText() { + return 0; + } + + /** + * Warning message to show during message request state if you do not have groups in common + * with an individual or do not know anyone in the group. + */ + public static final class NoGroupsInCommon extends InMemoryMessageRecord { + private final boolean isGroup; + + public NoGroupsInCommon(long threadId, boolean isGroup) { + super(-1, "", Recipient.UNKNOWN, threadId, 0); + this.isGroup = isGroup; + } + + @Override + public @Nullable UpdateDescription getUpdateDisplayBody(@NonNull Context context) { + return UpdateDescription.staticDescription(context.getString(isGroup ? R.string.ConversationUpdateItem_no_contacts_in_this_group_review_requests_carefully + : R.string.ConversationUpdateItem_no_groups_in_common_review_requests_carefully), + R.drawable.ic_update_info_16); + } + + @Override + public boolean isUpdate() { + return true; + } + + @Override + public boolean showActionButton() { + return true; + } + + public boolean isGroup() { + return isGroup; + } + + @Override + public @StringRes int getActionButtonText() { + return R.string.ConversationUpdateItem_learn_more; + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index 6b2971263..5cd33f574 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -489,6 +489,10 @@ public abstract class MessageRecord extends DisplayRecord { return MmsSmsColumns.Types.isFailedDecryptType(type); } + public boolean isInMemoryMessageRecord() { + return false; + } + protected static SpannableString emphasisAdded(String sequence) { SpannableString spannable = new SpannableString(sequence); spannable.setSpan(new RelativeSizeSpan(0.9f), 0, sequence.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); diff --git a/app/src/main/res/values/signal_styles.xml b/app/src/main/res/values/signal_styles.xml index fe9302b6d..3a3c83a98 100644 --- a/app/src/main/res/values/signal_styles.xml +++ b/app/src/main/res/values/signal_styles.xml @@ -117,7 +117,7 @@