Add universal disappearing messages.

This commit is contained in:
Cody Henthorne
2021-05-18 15:19:33 -04:00
committed by Greyson Parrelli
parent 8c6a88374b
commit defd5e8047
70 changed files with 1513 additions and 251 deletions

View File

@@ -2412,6 +2412,17 @@ public class ConversationActivity extends PassphraseRequiredActivity
if (groupCallViewModel != null) {
groupCallViewModel.onRecipientChange(recipient);
}
if (this.threadId == -1) {
SimpleTask.run(() -> DatabaseFactory.getThreadDatabase(this).getThreadIdIfExistsFor(recipient.getId()), threadId -> {
if (this.threadId != threadId) {
Log.d(TAG, "Thread id changed via recipient change");
this.threadId = threadId;
fragment.reload(recipient, this.threadId);
setVisibleThread(this.threadId);
}
});
}
}
@Subscribe(threadMode = ThreadMode.MAIN)

View File

@@ -50,6 +50,7 @@ import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer;
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4Projection;
import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.util.CachedInflater;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.StickyHeaderDecoration;
@@ -374,6 +375,10 @@ public class ConversationAdapter
this.pagingController = pagingController;
}
public boolean isForRecipientId(@NonNull RecipientId recipientId) {
return recipient.getId().equals(recipientId);
}
void onBindLastSeenViewHolder(StickyHeaderViewHolder viewHolder, int position) {
viewHolder.setText(viewHolder.itemView.getContext().getResources().getQuantityString(R.plurals.ConversationAdapter_n_unread_messages, (position + 1), (position + 1)));

View File

@@ -14,6 +14,7 @@ final class ConversationData {
private final int jumpToPosition;
private final int threadSize;
private final MessageRequestData messageRequestData;
private final boolean showUniversalExpireTimerMessage;
ConversationData(long threadId,
long lastSeen,
@@ -22,16 +23,18 @@ final class ConversationData {
boolean hasSent,
int jumpToPosition,
int threadSize,
@NonNull MessageRequestData messageRequestData)
@NonNull MessageRequestData messageRequestData,
boolean showUniversalExpireTimerMessage)
{
this.threadId = threadId;
this.lastSeen = lastSeen;
this.lastSeenPosition = lastSeenPosition;
this.lastScrolledPosition = lastScrolledPosition;
this.hasSent = hasSent;
this.jumpToPosition = jumpToPosition;
this.threadSize = threadSize;
this.messageRequestData = messageRequestData;
this.threadId = threadId;
this.lastSeen = lastSeen;
this.lastSeenPosition = lastSeenPosition;
this.lastScrolledPosition = lastScrolledPosition;
this.hasSent = hasSent;
this.jumpToPosition = jumpToPosition;
this.threadSize = threadSize;
this.messageRequestData = messageRequestData;
this.showUniversalExpireTimerMessage = showUniversalExpireTimerMessage;
}
public long getThreadId() {
@@ -74,6 +77,10 @@ final class ConversationData {
return messageRequestData;
}
public boolean showUniversalExpireTimerMessage() {
return showUniversalExpireTimerMessage;
}
static final class MessageRequestData {
private final boolean messageRequestAccepted;

View File

@@ -35,17 +35,21 @@ class ConversationDataSource implements PagedDataSource<ConversationMessage> {
private final Context context;
private final long threadId;
private final MessageRequestData messageRequestData;
private final boolean showUniversalExpireTimerUpdate;
ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData) {
this.context = context;
this.threadId = threadId;
this.messageRequestData = messageRequestData;
ConversationDataSource(@NonNull Context context, long threadId, @NonNull MessageRequestData messageRequestData, boolean showUniversalExpireTimerUpdate) {
this.context = context;
this.threadId = threadId;
this.messageRequestData = messageRequestData;
this.showUniversalExpireTimerUpdate = showUniversalExpireTimerUpdate;
}
@Override
public int size() {
long startTime = System.currentTimeMillis();
int size = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId) + (messageRequestData.includeWarningUpdateMessage() ? 1 : 0);
int size = DatabaseFactory.getMmsSmsDatabase(context).getConversationCount(threadId) +
(messageRequestData.includeWarningUpdateMessage() ? 1 : 0) +
(showUniversalExpireTimerUpdate ? 1 : 0);
Log.d(TAG, "size() for thread " + threadId + ": " + (System.currentTimeMillis() - startTime) + " ms");
@@ -71,6 +75,10 @@ class ConversationDataSource implements PagedDataSource<ConversationMessage> {
records.add(new InMemoryMessageRecord.NoGroupsInCommon(threadId, messageRequestData.isGroup()));
}
if (showUniversalExpireTimerUpdate) {
records.add(new InMemoryMessageRecord.UniversalExpireTimerUpdate(threadId));
}
stopwatch.split("messages");
mentionHelper.fetchMentions(context);

View File

@@ -183,38 +183,37 @@ public class ConversationFragment extends LoggingFragment {
private ConversationFragmentListener listener;
private LiveRecipient recipient;
private long threadId;
private boolean isReacting;
private ActionMode actionMode;
private Locale locale;
private FrameLayout videoContainer;
private RecyclerView list;
private RecyclerView.ItemDecoration lastSeenDecoration;
private RecyclerView.ItemDecoration inlineDateDecoration;
private ViewSwitcher topLoadMoreView;
private ViewSwitcher bottomLoadMoreView;
private ConversationTypingView typingView;
private View composeDivider;
private ConversationScrollToView scrollToBottomButton;
private ConversationScrollToView scrollToMentionButton;
private TextView scrollDateHeader;
private ConversationBannerView conversationBanner;
private ConversationBannerView emptyConversationBanner;
private MessageRequestViewModel messageRequestViewModel;
private MessageCountsViewModel messageCountsViewModel;
private ConversationViewModel conversationViewModel;
private SnapToTopDataObserver snapToTopDataObserver;
private MarkReadHelper markReadHelper;
private Animation scrollButtonInAnimation;
private Animation mentionButtonInAnimation;
private Animation scrollButtonOutAnimation;
private Animation mentionButtonOutAnimation;
private OnScrollListener conversationScrollListener;
private int pulsePosition = -1;
private VoiceNoteMediaController voiceNoteMediaController;
private View toolbarShadow;
private Stopwatch startupStopwatch;
private LiveRecipient recipient;
private long threadId;
private boolean isReacting;
private ActionMode actionMode;
private Locale locale;
private FrameLayout videoContainer;
private RecyclerView list;
private RecyclerView.ItemDecoration lastSeenDecoration;
private RecyclerView.ItemDecoration inlineDateDecoration;
private ViewSwitcher topLoadMoreView;
private ViewSwitcher bottomLoadMoreView;
private ConversationTypingView typingView;
private View composeDivider;
private ConversationScrollToView scrollToBottomButton;
private ConversationScrollToView scrollToMentionButton;
private TextView scrollDateHeader;
private ConversationBannerView conversationBanner;
private MessageRequestViewModel messageRequestViewModel;
private MessageCountsViewModel messageCountsViewModel;
private ConversationViewModel conversationViewModel;
private SnapToTopDataObserver snapToTopDataObserver;
private MarkReadHelper markReadHelper;
private Animation scrollButtonInAnimation;
private Animation mentionButtonInAnimation;
private Animation scrollButtonOutAnimation;
private Animation mentionButtonOutAnimation;
private OnScrollListener conversationScrollListener;
private int pulsePosition = -1;
private VoiceNoteMediaController voiceNoteMediaController;
private View toolbarShadow;
private Stopwatch startupStopwatch;
private GiphyMp4ProjectionRecycler giphyMp4ProjectionRecycler;
@@ -240,15 +239,14 @@ public class ConversationFragment extends LoggingFragment {
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle bundle) {
final View view = inflater.inflate(R.layout.conversation_fragment, container, false);
videoContainer = view.findViewById(R.id.video_container);
list = view.findViewById(android.R.id.list);
composeDivider = view.findViewById(R.id.compose_divider);
videoContainer = view.findViewById(R.id.video_container);
list = view.findViewById(android.R.id.list);
composeDivider = view.findViewById(R.id.compose_divider);
scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom);
scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
scrollDateHeader = view.findViewById(R.id.scroll_date_header);
emptyConversationBanner = view.findViewById(R.id.empty_conversation_banner);
toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow);
scrollToBottomButton = view.findViewById(R.id.scroll_to_bottom);
scrollToMentionButton = view.findViewById(R.id.scroll_to_mention);
scrollDateHeader = view.findViewById(R.id.scroll_date_header);
toolbarShadow = requireActivity().findViewById(R.id.conversation_toolbar_shadow);
final LinearLayoutManager layoutManager = new SmoothScrollingLinearLayoutManager(getActivity(), true);
list.setHasFixedSize(false);
@@ -483,7 +481,6 @@ public class ConversationFragment extends LoggingFragment {
messageRequestViewModel.getRecipientInfo().observe(getViewLifecycleOwner(), recipientInfo -> {
presentMessageRequestProfileView(requireContext(), recipientInfo, conversationBanner);
presentMessageRequestProfileView(requireContext(), recipientInfo, emptyConversationBanner);
});
messageRequestViewModel.getMessageData().observe(getViewLifecycleOwner(), data -> {
@@ -606,7 +603,16 @@ public class ConversationFragment extends LoggingFragment {
}
private void initializeListAdapter() {
if (this.recipient != null && this.threadId != -1) {
if (threadId == -1) {
toolbarShadow.setVisibility(View.GONE);
}
if (this.recipient != null) {
if (getListAdapter() != null && getListAdapter().isForRecipientId(this.recipient.getId())) {
Log.d(TAG, "List adapter already initialized for " + this.recipient.getId());
return;
}
Log.d(TAG, "Initializing adapter for " + recipient.getId());
ConversationAdapter adapter = new ConversationAdapter(this, GlideApp.with(this), locale, selectionClickListener, this.recipient.get(), new AttachmentMediaSourceFactory(requireContext()));
adapter.setPagingController(conversationViewModel.getPagingController());
@@ -618,8 +624,6 @@ public class ConversationFragment extends LoggingFragment {
setLastSeen(conversationViewModel.getLastSeen());
emptyConversationBanner.setVisibility(View.GONE);
adapter.registerAdapterDataObserver(new RecyclerView.AdapterDataObserver() {
@Override
public void onItemRangeInserted(int positionStart, int itemCount) {
@@ -631,9 +635,6 @@ public class ConversationFragment extends LoggingFragment {
});
}
});
} else if (threadId == -1) {
emptyConversationBanner.setVisibility(View.VISIBLE);
toolbarShadow.setVisibility(View.GONE);
}
}
@@ -726,6 +727,7 @@ public class ConversationFragment extends LoggingFragment {
menu.findItem(R.id.menu_context_save_attachment).setVisible(menuState.shouldShowSaveAttachmentAction());
menu.findItem(R.id.menu_context_resend).setVisible(menuState.shouldShowResendAction());
menu.findItem(R.id.menu_context_copy).setVisible(menuState.shouldShowCopyAction());
menu.findItem(R.id.menu_context_delete_message).setVisible(menuState.shouldShowDeleteAction());
}
private @Nullable ConversationAdapter getListAdapter() {
@@ -756,7 +758,9 @@ public class ConversationFragment extends LoggingFragment {
snapToTopDataObserver.requestScrollPosition(0);
conversationViewModel.onConversationDataAvailable(recipient.getId(), threadId, -1);
messageCountsViewModel.setThreadId(threadId);
markReadHelper = new MarkReadHelper(threadId, requireContext());
initializeListAdapter();
initializeTypingObserver();
}
}
@@ -1229,7 +1233,6 @@ public class ConversationFragment extends LoggingFragment {
toolbar.getGlobalVisibleRect(rect);
ViewUtil.setTopMargin(scrollDateHeader, rect.bottom + ViewUtil.dpToPx(8));
ViewUtil.setTopMargin(conversationBanner, rect.bottom + ViewUtil.dpToPx(16));
ViewUtil.setTopMargin(emptyConversationBanner, rect.bottom + ViewUtil.dpToPx(16));
toolbar.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});

View File

@@ -13,6 +13,7 @@ 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.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.util.BubbleUtil;
@@ -32,11 +33,11 @@ class ConversationRepository {
this.executor = SignalExecutors.BOUNDED;
}
LiveData<ConversationData> getConversationData(long threadId, int jumpToPosition) {
LiveData<ConversationData> getConversationData(long threadId, @NonNull Recipient recipient, int jumpToPosition) {
MutableLiveData<ConversationData> liveData = new MutableLiveData<>();
executor.execute(() -> {
liveData.postValue(getConversationDataInternal(threadId, jumpToPosition));
liveData.postValue(getConversationDataInternal(threadId, recipient, jumpToPosition));
});
return liveData;
@@ -53,16 +54,17 @@ 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);
ConversationData.MessageRequestData messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted);
private @NonNull ConversationData getConversationDataInternal(long threadId, @NonNull Recipient conversationRecipient, 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);
ConversationData.MessageRequestData messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted);
boolean showUniversalExpireTimerUpdate = false;
if (lastSeen > 0) {
lastSeenPosition = DatabaseFactory.getMmsSmsDatabase(context).getMessagePositionOnOrAfterTimestamp(threadId, lastSeen);
@@ -79,9 +81,8 @@ class ConversationRepository {
if (!isMessageRequestAccepted) {
boolean isGroup = false;
boolean recipientIsKnownOrHasGroupsInCommon = false;
Recipient threadRecipient = DatabaseFactory.getThreadDatabase(context).getRecipientForThreadId(threadId);
if (threadRecipient.isGroup()) {
Optional<GroupDatabase.GroupRecord> group = DatabaseFactory.getGroupDatabase(context).getGroup(threadRecipient.getId());
if (conversationRecipient.isGroup()) {
Optional<GroupDatabase.GroupRecord> group = DatabaseFactory.getGroupDatabase(context).getGroup(conversationRecipient.getId());
if (group.isPresent()) {
List<Recipient> recipients = Recipient.resolvedList(group.get().getMembers());
for (Recipient recipient : recipients) {
@@ -92,12 +93,20 @@ class ConversationRepository {
}
}
isGroup = true;
} else if (threadRecipient.hasGroupsInCommon()) {
} else if (conversationRecipient.hasGroupsInCommon()) {
recipientIsKnownOrHasGroupsInCommon = true;
}
messageRequestData = new ConversationData.MessageRequestData(isMessageRequestAccepted, recipientIsKnownOrHasGroupsInCommon, isGroup);
}
return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, jumpToPosition, threadSize, messageRequestData);
if (SignalStore.settings().getUniversalExpireTimer() != 0 &&
conversationRecipient.getExpireMessages() == 0 &&
!conversationRecipient.isGroup() &&
(threadId == -1 || !DatabaseFactory.getMmsSmsDatabase(context).hasMeaningfulMessage(threadId)))
{
showUniversalExpireTimerUpdate = true;
}
return new ConversationData(threadId, lastSeen, lastSeenPosition, lastScrolledPosition, hasSent, jumpToPosition, threadSize, messageRequestData, showUniversalExpireTimerUpdate);
}
}

View File

@@ -69,8 +69,11 @@ public class ConversationViewModel extends ViewModel {
this.pagingController = new ProxyPagingController();
this.messageObserver = pagingController::onDataInvalidated;
LiveData<ConversationData> metadata = Transformations.switchMap(threadId, thread -> {
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(thread, jumpToPosition);
LiveData<Recipient> recipientLiveData = LiveDataUtil.mapAsync(recipientId, Recipient::resolved);
LiveData<ThreadAndRecipient> threadAndRecipient = LiveDataUtil.combineLatest(threadId, recipientLiveData, ThreadAndRecipient::new);
LiveData<ConversationData> metadata = Transformations.switchMap(threadAndRecipient, d -> {
LiveData<ConversationData> conversationData = conversationRepository.getConversationData(d.threadId, d.recipient, jumpToPosition);
jumpToPosition = -1;
@@ -94,12 +97,11 @@ public class ConversationViewModel extends ViewModel {
ApplicationDependencies.getDatabaseObserver().unregisterObserver(messageObserver);
ApplicationDependencies.getDatabaseObserver().registerConversationObserver(data.getThreadId(), messageObserver);
ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData);
PagingConfig config = new PagingConfig.Builder()
.setPageSize(25)
.setBufferPages(3)
.setStartIndex(Math.max(startPosition, 0))
.build();
ConversationDataSource dataSource = new ConversationDataSource(context, data.getThreadId(), messageRequestData, data.showUniversalExpireTimerMessage());
PagingConfig config = new PagingConfig.Builder().setPageSize(25)
.setBufferPages(3)
.setStartIndex(Math.max(startPosition, 0))
.build();
Log.d(TAG, "Starting at position: " + startPosition + " || jumpToPosition: " + data.getJumpToPosition() + ", lastSeenPosition: " + data.getLastSeenPosition() + ", lastScrolledPosition: " + data.getLastScrolledPosition());
return new Pair<>(data.getThreadId(), PagedData.create(dataSource, config));
@@ -213,9 +215,20 @@ public class ConversationViewModel extends ViewModel {
SHOW_RECAPTCHA
}
private static class ThreadAndRecipient {
private final long threadId;
private final Recipient recipient;
public ThreadAndRecipient(long threadId, Recipient recipient) {
this.threadId = threadId;
this.recipient = recipient;
}
}
static class Factory extends ViewModelProvider.NewInstanceFactory {
@Override
public @NonNull<T extends ViewModel> T create(@NonNull Class<T> modelClass) {
public @NonNull <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
//noinspection ConstantConditions
return modelClass.cast(new ConversationViewModel());
}

View File

@@ -17,6 +17,7 @@ final class MenuState {
private final boolean saveAttachment;
private final boolean resend;
private final boolean copy;
private final boolean delete;
private MenuState(@NonNull Builder builder) {
forward = builder.forward;
@@ -25,6 +26,7 @@ final class MenuState {
saveAttachment = builder.saveAttachment;
resend = builder.resend;
copy = builder.copy;
delete = builder.delete;
}
boolean shouldShowForwardAction() {
@@ -51,6 +53,10 @@ final class MenuState {
return copy;
}
boolean shouldShowDeleteAction() {
return delete;
}
static MenuState getMenuState(@NonNull Recipient conversationRecipient,
@NonNull Set<MessageRecord> messageRecords,
boolean shouldShowMessageRequest)
@@ -62,11 +68,14 @@ final class MenuState {
boolean sharedContact = false;
boolean viewOnce = false;
boolean remoteDelete = false;
boolean hasInMemory = false;
for (MessageRecord messageRecord : messageRecords) {
if (isActionMessage(messageRecord))
{
if (isActionMessage(messageRecord)) {
actionMessage = true;
if (messageRecord.isInMemoryMessageRecord()) {
hasInMemory = true;
}
}
if (messageRecord.getBody().length() > 0) {
@@ -109,6 +118,7 @@ final class MenuState {
}
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText)
.shouldShowDeleteAction(!hasInMemory)
.build();
}
@@ -134,7 +144,8 @@ final class MenuState {
messageRecord.isIdentityDefault() ||
messageRecord.isProfileChange() ||
messageRecord.isGroupV1MigrationEvent() ||
messageRecord.isFailedDecryptionType();
messageRecord.isFailedDecryptionType() ||
messageRecord.isInMemoryMessageRecord();
}
private final static class Builder {
@@ -145,6 +156,7 @@ final class MenuState {
private boolean saveAttachment;
private boolean resend;
private boolean copy;
private boolean delete;
@NonNull Builder shouldShowForwardAction(boolean forward) {
this.forward = forward;
@@ -176,6 +188,11 @@ final class MenuState {
return this;
}
@NonNull Builder shouldShowDeleteAction(boolean delete) {
this.delete = delete;
return this;
}
@NonNull
MenuState build() {
return new MenuState(this);