mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 18:00:02 +01:00
Add message editing feature.
This commit is contained in:
@@ -109,13 +109,11 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onActivatePaymentsClicked();
|
||||
void onSendPaymentClicked(@NonNull RecipientId recipientId);
|
||||
void onScheduledIndicatorClicked(@NonNull View view, @NonNull MessageRecord messageRecord);
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
void onViewGiftBadgeClicked(@NonNull MessageRecord messageRecord);
|
||||
void onGiftBadgeRevealed(@NonNull MessageRecord messageRecord);
|
||||
|
||||
void goToMediaPreview(ConversationItem parent, View sharedElement, MediaIntentFactory.MediaPreviewArgs args);
|
||||
void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
public class AnimatingToggle extends FrameLayout {
|
||||
|
||||
private View current;
|
||||
|
||||
private View previous;
|
||||
private final Animation inAnimation;
|
||||
private final Animation outAnimation;
|
||||
|
||||
@@ -55,9 +55,17 @@ public class AnimatingToggle extends FrameLayout {
|
||||
|
||||
public void display(@Nullable View view) {
|
||||
if (view == current && current.getVisibility() == View.VISIBLE) return;
|
||||
if (current != null) ViewUtil.animateOut(current, outAnimation, View.GONE);
|
||||
if (view != null) ViewUtil.animateIn(view, inAnimation);
|
||||
|
||||
if (previous != null && previous.getAnimation() == outAnimation) {
|
||||
previous.clearAnimation();
|
||||
previous.setVisibility(View.GONE);
|
||||
}
|
||||
if (current != null) {
|
||||
ViewUtil.animateOut(current, outAnimation, View.GONE);
|
||||
}
|
||||
if (view != null) {
|
||||
ViewUtil.animateIn(view, inAnimation);
|
||||
}
|
||||
previous = current;
|
||||
current = view;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.airbnb.lottie.model.KeyPath;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
@@ -35,7 +36,6 @@ import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.permissions.Permissions;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.service.ExpiringMessageManager;
|
||||
import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
@@ -143,8 +143,8 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
timerView.stopAnimation();
|
||||
}
|
||||
|
||||
public void setMessageRecord(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
||||
presentDate(messageRecord, locale);
|
||||
public void setMessageRecord(@NonNull MessageRecord messageRecord, @NonNull Locale locale, @NonNull ConversationItemDisplayMode displayMode) {
|
||||
presentDate(messageRecord, locale, displayMode);
|
||||
presentSimInfo(messageRecord);
|
||||
presentTimer(messageRecord);
|
||||
presentInsecureIndicator(messageRecord);
|
||||
@@ -218,7 +218,7 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public TextView getDateView() {
|
||||
public View getDateView() {
|
||||
return dateView;
|
||||
}
|
||||
|
||||
@@ -300,7 +300,7 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
return speedToggleHitRect;
|
||||
}
|
||||
|
||||
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale) {
|
||||
private void presentDate(@NonNull MessageRecord messageRecord, @NonNull Locale locale, @NonNull ConversationItemDisplayMode displayMode) {
|
||||
dateView.forceLayout();
|
||||
if (messageRecord.isFailed()) {
|
||||
int errorMsg;
|
||||
@@ -320,7 +320,11 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
} else if (MessageRecordUtil.isScheduled(messageRecord)) {
|
||||
dateView.setText(DateUtils.getOnlyTimeString(getContext(), locale, ((MediaMmsMessageRecord) messageRecord).getScheduledDate()));
|
||||
} else {
|
||||
dateView.setText(DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp()));
|
||||
String date = DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, messageRecord.getTimestamp());
|
||||
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord instanceof MediaMmsMessageRecord && ((MediaMmsMessageRecord) messageRecord).isEditMessage()) {
|
||||
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date);
|
||||
}
|
||||
dateView.setText(date);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -360,17 +364,12 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
}
|
||||
} else if (!messageRecord.isOutgoing() && !messageRecord.isMediaPending()) {
|
||||
SignalExecutors.BOUNDED.execute(() -> {
|
||||
ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager();
|
||||
long id = messageRecord.getId();
|
||||
boolean mms = messageRecord.isMms();
|
||||
long id = messageRecord.getId();
|
||||
boolean mms = messageRecord.isMms();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
if (mms) {
|
||||
SignalDatabase.messages().markExpireStarted(id);
|
||||
} else {
|
||||
SignalDatabase.messages().markExpireStarted(id);
|
||||
}
|
||||
|
||||
expirationManager.scheduleDeletion(id, mms, messageRecord.getExpiresIn());
|
||||
SignalDatabase.messages().markExpireStarted(id, now);
|
||||
ApplicationDependencies.getExpiringMessageManager().scheduleDeletion(id, mms, now, messageRecord.getExpiresIn());
|
||||
});
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -5,6 +5,7 @@ import android.animation.ValueAnimator;
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.ColorDrawable;
|
||||
import android.net.Uri;
|
||||
import android.text.SpannableString;
|
||||
import android.text.format.DateUtils;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.KeyEvent;
|
||||
@@ -39,9 +40,13 @@ import org.thoughtcrime.securesms.components.emoji.EmojiEventListener;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiToggle;
|
||||
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard;
|
||||
import org.thoughtcrime.securesms.components.voice.VoiceNotePlaybackState;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationStickerSuggestionAdapter;
|
||||
import org.thoughtcrime.securesms.conversation.VoiceNoteDraftView;
|
||||
import org.thoughtcrime.securesms.database.DraftTable;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.Quote;
|
||||
import org.thoughtcrime.securesms.database.model.StickerRecord;
|
||||
import org.thoughtcrime.securesms.keyboard.KeyboardPage;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
@@ -53,6 +58,7 @@ import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
import org.thoughtcrime.securesms.util.concurrent.AssertedSuccessListener;
|
||||
import org.thoughtcrime.securesms.util.concurrent.ListenableFuture;
|
||||
@@ -88,6 +94,8 @@ public class InputPanel extends LinearLayout
|
||||
private View recordingContainer;
|
||||
private View recordLockCancel;
|
||||
private ViewGroup composeContainer;
|
||||
private View editMessageLabel;
|
||||
private View editMessageCancel;
|
||||
|
||||
private MicrophoneRecorderView microphoneRecorderView;
|
||||
private SlideToCancel slideToCancel;
|
||||
@@ -105,6 +113,7 @@ public class InputPanel extends LinearLayout
|
||||
private boolean hideForSelection;
|
||||
|
||||
private ConversationStickerSuggestionAdapter stickerSuggestionAdapter;
|
||||
private MessageRecord messageToEdit;
|
||||
|
||||
public InputPanel(Context context) {
|
||||
super(context);
|
||||
@@ -144,6 +153,8 @@ public class InputPanel extends LinearLayout
|
||||
findViewById(R.id.microphone),
|
||||
TimeUnit.HOURS.toSeconds(1),
|
||||
() -> microphoneRecorderView.cancelAction(false));
|
||||
this.editMessageLabel = findViewById(R.id.edit_message);
|
||||
this.editMessageCancel = findViewById(R.id.input_panel_exit_edit_mode);
|
||||
|
||||
this.recordLockCancel.setOnClickListener(v -> microphoneRecorderView.cancelAction(true));
|
||||
|
||||
@@ -167,6 +178,8 @@ public class InputPanel extends LinearLayout
|
||||
|
||||
stickerSuggestion.setLayoutManager(new LinearLayoutManager(getContext(), LinearLayoutManager.HORIZONTAL, false));
|
||||
stickerSuggestion.setAdapter(stickerSuggestionAdapter);
|
||||
|
||||
editMessageCancel.setOnClickListener(v -> exitEditMessageMode());
|
||||
}
|
||||
|
||||
public void setListener(final @NonNull Listener listener) {
|
||||
@@ -183,7 +196,7 @@ public class InputPanel extends LinearLayout
|
||||
public void setQuote(@NonNull GlideRequests glideRequests,
|
||||
long id,
|
||||
@NonNull Recipient author,
|
||||
@NonNull CharSequence body,
|
||||
@Nullable CharSequence body,
|
||||
@NonNull SlideDeck attachments,
|
||||
@NonNull QuoteModel.Type quoteType)
|
||||
{
|
||||
@@ -372,6 +385,52 @@ public class InputPanel extends LinearLayout
|
||||
quoteView.setWallpaperEnabled(enabled);
|
||||
}
|
||||
|
||||
public void enterEditMessageMode(@NonNull GlideRequests glideRequests, @NonNull ConversationMessage messageToEdit, boolean fromDraft) {
|
||||
SpannableString textToEdit = messageToEdit.getDisplayBody(getContext());
|
||||
if (!fromDraft) {
|
||||
composeText.setText(textToEdit);
|
||||
composeText.setSelection(textToEdit.length());
|
||||
}
|
||||
Quote quote = MessageRecordUtil.getQuote(messageToEdit.getMessageRecord());
|
||||
if (quote == null) {
|
||||
clearQuote();
|
||||
} else {
|
||||
setQuote(glideRequests, quote.getId(), Recipient.resolved(quote.getAuthor()), quote.getDisplayText(), quote.getAttachment(), quote.getQuoteType());
|
||||
}
|
||||
this.messageToEdit = messageToEdit.getMessageRecord();
|
||||
updateEditModeUi();
|
||||
}
|
||||
|
||||
public void exitEditMessageMode() {
|
||||
if (messageToEdit != null) {
|
||||
composeText.setText("");
|
||||
messageToEdit = null;
|
||||
quoteView.setMessageType(QuoteView.MessageType.PREVIEW);
|
||||
}
|
||||
updateEditModeUi();
|
||||
}
|
||||
|
||||
private void updateEditModeUi() {
|
||||
if (inEditMessageMode()) {
|
||||
ViewUtil.focusAndShowKeyboard(composeText);
|
||||
editMessageLabel.setVisibility(View.VISIBLE);
|
||||
editMessageCancel.setVisibility(View.VISIBLE);
|
||||
if (listener != null) {
|
||||
listener.onEnterEditMode();
|
||||
}
|
||||
} else {
|
||||
editMessageLabel.setVisibility(View.GONE);
|
||||
editMessageCancel.setVisibility(View.GONE);
|
||||
if (listener != null) {
|
||||
listener.onExitEditMode();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean inEditMessageMode() {
|
||||
return messageToEdit != null;
|
||||
}
|
||||
|
||||
public void setHideForMessageRequestState(boolean hideForMessageRequestState) {
|
||||
this.hideForMessageRequestState = hideForMessageRequestState;
|
||||
updateVisibility();
|
||||
@@ -617,6 +676,16 @@ public class InputPanel extends LinearLayout
|
||||
}
|
||||
}
|
||||
|
||||
public @Nullable MessageRecord getEditMessage() {
|
||||
return messageToEdit;
|
||||
}
|
||||
public @Nullable MessageId getEditMessageId() {
|
||||
if (messageToEdit == null) {
|
||||
return null;
|
||||
}
|
||||
return new MessageId(messageToEdit.getId());
|
||||
}
|
||||
|
||||
public interface Listener extends VoiceNoteDraftView.Listener {
|
||||
void onRecorderStarted();
|
||||
void onRecorderLocked();
|
||||
@@ -628,6 +697,8 @@ public class InputPanel extends LinearLayout
|
||||
void onStickerSuggestionSelected(@NonNull StickerRecord sticker);
|
||||
void onQuoteChanged(long id, @NonNull RecipientId author);
|
||||
void onQuoteCleared();
|
||||
void onEnterEditMode();
|
||||
void onExitEditMode();
|
||||
}
|
||||
|
||||
private static class SlideToCancel {
|
||||
|
||||
@@ -202,6 +202,7 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver {
|
||||
params.width = thumbWidth;
|
||||
|
||||
thumbnailView.setLayoutParams(params);
|
||||
dismissView.setVisibility(messageType == MessageType.PREVIEW ? View.VISIBLE : View.GONE);
|
||||
}
|
||||
|
||||
public void setQuote(GlideRequests glideRequests,
|
||||
|
||||
@@ -110,19 +110,19 @@ public class ConversationAdapter
|
||||
private final Set<MultiselectPart> selected;
|
||||
private final Calendar calendar;
|
||||
|
||||
private String searchQuery;
|
||||
private ConversationMessage recordToPulse;
|
||||
private View typingView;
|
||||
private View footerView;
|
||||
private PagingController pagingController;
|
||||
private boolean hasWallpaper;
|
||||
private boolean isMessageRequestAccepted;
|
||||
private ConversationMessage inlineContent;
|
||||
private Colorizer colorizer;
|
||||
private boolean isTypingViewEnabled;
|
||||
private boolean condensedMode;
|
||||
private boolean scheduledMessagesMode;
|
||||
private PulseRequest pulseRequest;
|
||||
private String searchQuery;
|
||||
private ConversationMessage recordToPulse;
|
||||
private View typingView;
|
||||
private View footerView;
|
||||
private PagingController pagingController;
|
||||
private boolean hasWallpaper;
|
||||
private boolean isMessageRequestAccepted;
|
||||
private ConversationMessage inlineContent;
|
||||
private Colorizer colorizer;
|
||||
private boolean isTypingViewEnabled;
|
||||
private ConversationItemDisplayMode condensedMode;
|
||||
private boolean scheduledMessagesMode;
|
||||
private PulseRequest pulseRequest;
|
||||
|
||||
public ConversationAdapter(@NonNull Context context,
|
||||
@NonNull LifecycleOwner lifecycleOwner,
|
||||
@@ -258,7 +258,7 @@ public class ConversationAdapter
|
||||
}
|
||||
}
|
||||
|
||||
public void setCondensedMode(boolean condensedMode) {
|
||||
public void setCondensedMode(ConversationItemDisplayMode condensedMode) {
|
||||
this.condensedMode = condensedMode;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
@@ -283,7 +283,7 @@ public class ConversationAdapter
|
||||
ConversationMessage previousMessage = adapterPosition < getItemCount() - 1 && !isFooterPosition(adapterPosition + 1) ? getItem(adapterPosition + 1) : null;
|
||||
ConversationMessage nextMessage = adapterPosition > 0 && !isHeaderPosition(adapterPosition - 1) ? getItem(adapterPosition - 1) : null;
|
||||
|
||||
ConversationItemDisplayMode displayMode = condensedMode ? ConversationItemDisplayMode.CONDENSED : ConversationItemDisplayMode.STANDARD;
|
||||
ConversationItemDisplayMode displayMode = condensedMode != null ? condensedMode : ConversationItemDisplayMode.STANDARD;
|
||||
|
||||
conversationViewHolder.getBindable().bind(lifecycleOwner,
|
||||
conversationMessage,
|
||||
@@ -295,7 +295,7 @@ public class ConversationAdapter
|
||||
recipient,
|
||||
searchQuery,
|
||||
conversationMessage == recordToPulse,
|
||||
hasWallpaper && !condensedMode,
|
||||
hasWallpaper && displayMode.displayWallpaper(),
|
||||
isMessageRequestAccepted,
|
||||
conversationMessage == inlineContent,
|
||||
colorizer,
|
||||
|
||||
@@ -112,6 +112,7 @@ import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectFor
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs;
|
||||
import org.thoughtcrime.securesms.conversation.quotes.MessageQuotesBottomSheet;
|
||||
import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog;
|
||||
import org.thoughtcrime.securesms.conversation.ui.error.EnableCallNotificationSettingsDialog;
|
||||
import org.thoughtcrime.securesms.conversation.v2.AddToContactsContract;
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver;
|
||||
@@ -180,8 +181,8 @@ import org.thoughtcrime.securesms.util.CommunicationActions;
|
||||
import org.thoughtcrime.securesms.util.HtmlUtil;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
|
||||
import org.thoughtcrime.securesms.util.SignalLocalMetrics;
|
||||
import org.thoughtcrime.securesms.util.SignalProxyUtil;
|
||||
@@ -836,6 +837,15 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
}));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowEditAction()) {
|
||||
items.add(new ActionItem(R.drawable.symbol_edit_24, getResources().getString(R.string.conversation_selection__menu_edit), () -> {
|
||||
handleEditMessage(getSelectedConversationMessage());
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowForwardAction()) {
|
||||
items.add(new ActionItem(R.drawable.symbol_forward_24, getResources().getString(R.string.conversation_selection__menu_forward), () -> handleForwardMessageParts(selectedParts)));
|
||||
}
|
||||
@@ -1078,7 +1088,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
|
||||
int deleteForEveryoneResId = isNoteToSelfDelete ? R.string.ConversationFragment_delete_everywhere : R.string.ConversationFragment_delete_for_everyone;
|
||||
|
||||
if (RemoteDeleteUtil.isValidSend(messageRecords, System.currentTimeMillis()) && (!isNoteToSelfDelete || TextSecurePreferences.isMultiDevice(requireContext()))) {
|
||||
if (MessageConstraintsUtil.isValidRemoteDeleteSend(messageRecords, System.currentTimeMillis()) && (!isNoteToSelfDelete || TextSecurePreferences.isMultiDevice(requireContext()))) {
|
||||
builder.setNeutralButton(deleteForEveryoneResId, (dialog, which) -> handleDeleteForEveryone(messageRecords));
|
||||
}
|
||||
|
||||
@@ -1148,6 +1158,10 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
listener.handleReplyMessage(message);
|
||||
}
|
||||
|
||||
private void handleEditMessage(@NonNull ConversationMessage selectedConversationMessage) {
|
||||
listener.handleEditMessage(selectedConversationMessage);
|
||||
}
|
||||
|
||||
private void handleSaveAttachment(final MediaMmsMessageRecord message) {
|
||||
if (message.isViewOnce()) {
|
||||
throw new AssertionError("Cannot save a view-once message.");
|
||||
@@ -1455,6 +1469,7 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
void openAttachmentKeyboard();
|
||||
void setThreadId(long threadId);
|
||||
void handleReplyMessage(ConversationMessage conversationMessage);
|
||||
void handleEditMessage(@NonNull ConversationMessage conversationMessage);
|
||||
void onMessageActionToolbarOpened();
|
||||
void onMessageActionToolbarClosed();
|
||||
void onBottomActionBarVisibilityChanged(int visibility);
|
||||
@@ -2110,6 +2125,15 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
requireActivity().startActivity(MediaIntentFactory.create(requireActivity(), args), options.toBundle());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEditedIndicatorClicked(@NonNull MessageRecord messageRecord) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
EditMessageHistoryDialog.show(getChildFragmentManager(), messageRecord.getToRecipient().getId(), messageRecord.getId());
|
||||
} else {
|
||||
EditMessageHistoryDialog.show(getChildFragmentManager(), messageRecord.getFromRecipient().getId(), messageRecord.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivatePaymentsClicked() {
|
||||
Intent intent = new Intent(requireContext(), PaymentsActivity.class);
|
||||
@@ -2268,6 +2292,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
case REPLY:
|
||||
handleReplyMessage(conversationMessage);
|
||||
break;
|
||||
case EDIT:
|
||||
handleEditMessage(conversationMessage);
|
||||
break;
|
||||
case FORWARD:
|
||||
handleForwardMessageParts(conversationMessage.getMultiselectCollection().toSet());
|
||||
break;
|
||||
|
||||
@@ -419,7 +419,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
@Override
|
||||
public void updateTimestamps() {
|
||||
getActiveFooter(messageRecord).setMessageRecord(messageRecord, locale);
|
||||
getActiveFooter(messageRecord).setMessageRecord(messageRecord, locale, displayMode);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -526,10 +526,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
!messageRecord.isRemoteDelete() &&
|
||||
bodyText.getLastLineWidth() > 0)
|
||||
{
|
||||
TextView dateView = footer.getDateView();
|
||||
int footerWidth = footer.getMeasuredWidth();
|
||||
int availableWidth = getAvailableMessageBubbleWidth(bodyText);
|
||||
int collapsedTopMargin = -1 * (dateView.getMeasuredHeight() + ViewUtil.dpToPx(4));
|
||||
View dateView = footer.getDateView();
|
||||
int footerWidth = footer.getMeasuredWidth();
|
||||
int availableWidth = getAvailableMessageBubbleWidth(bodyText);
|
||||
int collapsedTopMargin = -1 * (dateView.getMeasuredHeight() + ViewUtil.dpToPx(4));
|
||||
|
||||
if (bodyText.isSingleLine() && !messageRecord.isFailed()) {
|
||||
int maxBubbleWidth = hasBigImageLinkPreview(messageRecord) || hasThumbnail(messageRecord) ? readDimen(R.dimen.media_bubble_max_width) : getMaxBubbleWidth();
|
||||
@@ -1666,7 +1666,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
if (isFooterVisible(current, next, isGroupThread)) {
|
||||
ConversationItemFooter activeFooter = getActiveFooter(current);
|
||||
activeFooter.setVisibility(VISIBLE);
|
||||
activeFooter.setMessageRecord(current, locale);
|
||||
activeFooter.setMessageRecord(current, locale, displayMode);
|
||||
|
||||
if (MessageRecordUtil.isEditMessage(current)) {
|
||||
activeFooter.getDateView().setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onEditedIndicatorClicked(current);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
activeFooter.getDateView().setOnClickListener(null);
|
||||
activeFooter.getDateView().setClickable(false);
|
||||
}
|
||||
|
||||
if (hasWallpaper && hasNoBubble((messageRecord))) {
|
||||
if (messageRecord.isOutgoing()) {
|
||||
@@ -1714,7 +1725,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private void setHasBeenQuoted(@NonNull ConversationMessage message) {
|
||||
if (message.hasBeenQuoted() && !isCondensedMode() && quotedIndicator != null && batchSelected.isEmpty()) {
|
||||
if (message.hasBeenQuoted() && !isCondensedMode() && quotedIndicator != null && batchSelected.isEmpty() && displayMode != ConversationItemDisplayMode.EXTRA_CONDENSED) {
|
||||
quotedIndicator.setVisibility(VISIBLE);
|
||||
quotedIndicator.setOnClickListener(quotedIndicatorClickListener);
|
||||
} else if (quotedIndicator != null) {
|
||||
@@ -1737,7 +1748,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private boolean forceFooter(@NonNull MessageRecord messageRecord) {
|
||||
return hasAudio(messageRecord);
|
||||
return hasAudio(messageRecord) || MessageRecordUtil.isEditMessage(messageRecord);
|
||||
}
|
||||
|
||||
private ConversationItemFooter getActiveFooter(@NonNull MessageRecord messageRecord) {
|
||||
@@ -1841,7 +1852,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
int background;
|
||||
|
||||
if (isSingularMessage(current, previous, next, isGroupThread)) {
|
||||
if (isSingularMessage(current, previous, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EXTRA_CONDENSED) {
|
||||
if (current.isOutgoing()) {
|
||||
background = R.drawable.message_bubble_background_sent_alone;
|
||||
outliner.setRadius(bigRadius);
|
||||
@@ -1922,6 +1933,10 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
|
||||
private boolean isFooterVisible(@NonNull MessageRecord current, @NonNull Optional<MessageRecord> next, boolean isGroupThread) {
|
||||
if (displayMode == ConversationItemDisplayMode.EXTRA_CONDENSED) {
|
||||
return false;
|
||||
}
|
||||
|
||||
boolean differentTimestamps = next.isPresent() && !DateUtils.isSameExtendedRelativeTimestamp(next.get().getTimestamp(), current.getTimestamp());
|
||||
|
||||
return forceFooter(messageRecord) || current.getExpiresIn() > 0 || !current.isSecure() || current.isPending() || current.isPendingInsecureSmsFallback() ||
|
||||
@@ -1937,11 +1952,11 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
int spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_collapse);
|
||||
int spacingBottom = spacingTop;
|
||||
|
||||
if (isStartOfMessageCluster(current, previous, isGroupThread)) {
|
||||
if (isStartOfMessageCluster(current, previous, isGroupThread) && (displayMode != ConversationItemDisplayMode.EXTRA_CONDENSED || next.isEmpty())) {
|
||||
spacingTop = readDimen(context, R.dimen.conversation_vertical_message_spacing_default);
|
||||
}
|
||||
|
||||
if (isEndOfMessageCluster(current, next, isGroupThread)) {
|
||||
if (isEndOfMessageCluster(current, next, isGroupThread) || displayMode == ConversationItemDisplayMode.EXTRA_CONDENSED) {
|
||||
spacingBottom = readDimen(context, R.dimen.conversation_vertical_message_spacing_default);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,13 @@ enum class ConversationItemDisplayMode {
|
||||
/** Smaller bubbles, often trimming text and shrinking images. Used for quote threads. */
|
||||
CONDENSED,
|
||||
|
||||
/** Smaller bubbles, no footers */
|
||||
EXTRA_CONDENSED,
|
||||
|
||||
/** Less length restrictions. Used to show more info in message details. */
|
||||
DETAILED
|
||||
DETAILED;
|
||||
|
||||
fun displayWallpaper(): Boolean {
|
||||
return this == STANDARD || this == DETAILED
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,12 +282,14 @@ import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.Debouncer;
|
||||
import org.thoughtcrime.securesms.util.Dialogs;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper;
|
||||
import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable;
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageUtil;
|
||||
import org.thoughtcrime.securesms.util.PlayStoreUtil;
|
||||
import org.thoughtcrime.securesms.util.ServiceUtil;
|
||||
@@ -399,6 +401,7 @@ public class ConversationParentFragment extends Fragment
|
||||
private AnimatingToggle buttonToggle;
|
||||
private SendButton sendButton;
|
||||
private ImageButton attachButton;
|
||||
private ImageButton sendEditButton;
|
||||
protected ConversationTitleView titleView;
|
||||
private TextView charactersLeft;
|
||||
private ConversationFragment fragment;
|
||||
@@ -794,7 +797,8 @@ public class ConversationParentFragment extends Fragment
|
||||
initiating,
|
||||
true,
|
||||
null,
|
||||
result.getScheduledTime()).addListener(new AssertedSuccessListener<Void>() {
|
||||
result.getScheduledTime(),
|
||||
null).addListener(new AssertedSuccessListener<Void>() {
|
||||
@Override
|
||||
public void onSuccess(Void result) {
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(() -> {
|
||||
@@ -1591,6 +1595,7 @@ public class ConversationParentFragment extends Fragment
|
||||
inputPanel.setEnabled(canSendMessages);
|
||||
sendButton.setEnabled(canSendMessages);
|
||||
attachButton.setEnabled(canSendMessages);
|
||||
sendEditButton.setEnabled(canSendMessages);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1670,6 +1675,24 @@ public class ConversationParentFragment extends Fragment
|
||||
|
||||
quoteResult.addListener(listener);
|
||||
break;
|
||||
case Draft.MESSAGE_EDIT:
|
||||
SettableFuture<Boolean> messageEditResult = new SettableFuture<>();
|
||||
disposables.add(draftViewModel.loadDraftEditMessage(draft.getValue()).subscribe(
|
||||
conversationMessage -> {
|
||||
inputPanel.enterEditMessageMode(glideRequests, conversationMessage, true);
|
||||
messageEditResult.set(true);
|
||||
},
|
||||
err -> {
|
||||
Log.e(TAG, "Failed to restore message edit from a draft.", err);
|
||||
messageEditResult.set(false);
|
||||
},
|
||||
() -> {
|
||||
Log.e(TAG, "Failed to load message edit. No matching message record.");
|
||||
messageEditResult.set(false);
|
||||
}
|
||||
));
|
||||
messageEditResult.addListener(listener);
|
||||
break;
|
||||
case Draft.VOICE_NOTE:
|
||||
case Draft.BODY_RANGES:
|
||||
listener.onSuccess(true);
|
||||
@@ -1846,6 +1869,7 @@ public class ConversationParentFragment extends Fragment
|
||||
buttonToggle = view.findViewById(R.id.button_toggle);
|
||||
sendButton = view.findViewById(R.id.send_button);
|
||||
attachButton = view.findViewById(R.id.attach_button);
|
||||
sendEditButton = view.findViewById(R.id.send_edit_button);
|
||||
composeText = view.findViewById(R.id.embedded_text_editor);
|
||||
charactersLeft = view.findViewById(R.id.space_left);
|
||||
emojiDrawerStub = ViewUtil.findStubById(view, R.id.emoji_drawer_stub);
|
||||
@@ -1902,6 +1926,7 @@ public class ConversationParentFragment extends Fragment
|
||||
attachButton.setOnClickListener(new AttachButtonListener());
|
||||
attachButton.setOnLongClickListener(new AttachButtonLongClickListener());
|
||||
sendButton.setOnClickListener(sendButtonListener);
|
||||
sendEditButton.setOnClickListener(v -> handleSendEditMessage());
|
||||
sendButton.setScheduledSendListener(new SendButton.ScheduledSendListener() {
|
||||
@Override
|
||||
public void onSendScheduled() {
|
||||
@@ -2759,6 +2784,8 @@ public class ConversationParentFragment extends Fragment
|
||||
callback.onSendComplete(threadId);
|
||||
|
||||
draftViewModel.onSendComplete(threadId);
|
||||
|
||||
inputPanel.exitEditMessageMode();
|
||||
}
|
||||
|
||||
private void sendMessage(@Nullable String metricId) {
|
||||
@@ -2794,6 +2821,7 @@ public class ConversationParentFragment extends Fragment
|
||||
MessageSendType sendType = sendButton.getSelectedSendType();
|
||||
long expiresIn = TimeUnit.SECONDS.toMillis(recipient.getExpiresInSeconds());
|
||||
boolean initiating = threadId == -1;
|
||||
boolean isEditMessage = inputPanel.inEditMessageMode();
|
||||
boolean needsSplit = !sendType.usesSmsTransport() && message.length() > sendType.calculateCharacters(message).maxPrimaryMessageSize;
|
||||
boolean isMediaMessage = attachmentManager.isAttachmentPresent() ||
|
||||
recipient.isGroup() ||
|
||||
@@ -2806,14 +2834,19 @@ public class ConversationParentFragment extends Fragment
|
||||
|
||||
Log.i(TAG, "[sendMessage] recipient: " + recipient.getId() + ", threadId: " + threadId + ", sendType: " + (sendType.usesSignalTransport() ? "signal" : "sms") + ", isManual: " + sendButton.isManualSelection());
|
||||
|
||||
if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !viewModel.getConversationStateSnapshot().isMmsEnabled()) {
|
||||
if (!sendType.usesSignalTransport() && isEditMessage) {
|
||||
Toast.makeText(requireContext(),
|
||||
R.string.ConversationActivity_edit_sms_message_error,
|
||||
Toast.LENGTH_LONG)
|
||||
.show();
|
||||
} else if ((recipient.isMmsGroup() || recipient.getEmail().isPresent()) && !viewModel.getConversationStateSnapshot().isMmsEnabled()) {
|
||||
handleManualMmsRequired();
|
||||
} else if (sendType.usesSignalTransport() && (identityRecords.isUnverified(true) || identityRecords.isUntrusted(true))) {
|
||||
handleRecentSafetyNumberChange();
|
||||
} else if (isMediaMessage) {
|
||||
sendMediaMessage(sendType, expiresIn, false, initiating, metricId, scheduledDate);
|
||||
sendMediaMessage(sendType, expiresIn, false, initiating, metricId, scheduledDate, inputPanel.getEditMessageId());
|
||||
} else {
|
||||
sendTextMessage(sendType, expiresIn, initiating, metricId, scheduledDate);
|
||||
sendTextMessage(sendType, expiresIn, initiating, metricId, scheduledDate, inputPanel.getEditMessageId());
|
||||
}
|
||||
} catch (RecipientFormattingException ex) {
|
||||
Toast.makeText(requireContext(),
|
||||
@@ -2862,7 +2895,8 @@ public class ConversationParentFragment extends Fragment
|
||||
null,
|
||||
true,
|
||||
result.getBodyRanges(),
|
||||
-1);
|
||||
-1,
|
||||
0);
|
||||
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
|
||||
@@ -2884,7 +2918,7 @@ public class ConversationParentFragment extends Fragment
|
||||
}, this::sendComplete);
|
||||
}
|
||||
|
||||
private void sendMediaMessage(@NonNull MessageSendType sendType, final long expiresIn, final boolean viewOnce, final boolean initiating, @Nullable String metricId, long scheduledDate)
|
||||
private void sendMediaMessage(@NonNull MessageSendType sendType, final long expiresIn, final boolean viewOnce, final boolean initiating, @Nullable String metricId, long scheduledDate, @Nullable MessageId editMessageId)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
Log.i(TAG, "Sending media message...");
|
||||
@@ -2903,7 +2937,8 @@ public class ConversationParentFragment extends Fragment
|
||||
initiating,
|
||||
true,
|
||||
metricId,
|
||||
scheduledDate);
|
||||
scheduledDate,
|
||||
editMessageId);
|
||||
}
|
||||
|
||||
private ListenableFuture<Void> sendMediaMessage(@NonNull RecipientId recipientId,
|
||||
@@ -2921,7 +2956,7 @@ public class ConversationParentFragment extends Fragment
|
||||
final boolean clearComposeBox,
|
||||
final @Nullable String metricId)
|
||||
{
|
||||
return sendMediaMessage(recipientId, sendType, body, slideDeck, quote, contacts, previews, mentions, styling, expiresIn, viewOnce, initiating, clearComposeBox, metricId, -1);
|
||||
return sendMediaMessage(recipientId, sendType, body, slideDeck, quote, contacts, previews, mentions, styling, expiresIn, viewOnce, initiating, clearComposeBox, metricId, -1, null);
|
||||
}
|
||||
|
||||
private ListenableFuture<Void> sendMediaMessage(@NonNull RecipientId recipientId,
|
||||
@@ -2938,7 +2973,8 @@ public class ConversationParentFragment extends Fragment
|
||||
final boolean initiating,
|
||||
final boolean clearComposeBox,
|
||||
final @Nullable String metricId,
|
||||
final long scheduledDate)
|
||||
final long scheduledDate,
|
||||
@Nullable MessageId editMessageId)
|
||||
{
|
||||
if (ExpiredBuildReminder.isEligible()) {
|
||||
showExpiredDialog();
|
||||
@@ -2952,7 +2988,7 @@ public class ConversationParentFragment extends Fragment
|
||||
|
||||
if (SignalStore.uiHints().hasNotSeenTextFormattingAlert() && styling != null && styling.getRangesCount() > 0) {
|
||||
final String finalBody = body;
|
||||
Dialogs.showFormattedTextDialog(requireContext(), () -> sendMediaMessage(recipientId, sendType, finalBody, slideDeck, quote, contacts, previews, mentions, styling, expiresIn, viewOnce, initiating, clearComposeBox, metricId, scheduledDate));
|
||||
Dialogs.showFormattedTextDialog(requireContext(), () -> sendMediaMessage(recipientId, sendType, finalBody, slideDeck, quote, contacts, previews, mentions, styling, expiresIn, viewOnce, initiating, clearComposeBox, metricId, scheduledDate, editMessageId));
|
||||
return new SettableFuture<>(null);
|
||||
}
|
||||
|
||||
@@ -2988,7 +3024,8 @@ public class ConversationParentFragment extends Fragment
|
||||
null,
|
||||
false,
|
||||
styling,
|
||||
scheduledDate);
|
||||
scheduledDate,
|
||||
editMessageId != null ? editMessageId.getId() : 0);
|
||||
|
||||
final SettableFuture<Void> future = new SettableFuture<>();
|
||||
final Context context = requireContext().getApplicationContext();
|
||||
@@ -3028,7 +3065,12 @@ public class ConversationParentFragment extends Fragment
|
||||
return future;
|
||||
}
|
||||
|
||||
private void sendTextMessage(@NonNull MessageSendType sendType, final long expiresIn, final boolean initiating, final @Nullable String metricId, long scheduledDate)
|
||||
private void sendTextMessage(@NonNull MessageSendType sendType,
|
||||
final long expiresIn,
|
||||
final boolean initiating,
|
||||
final @Nullable String metricId,
|
||||
long scheduledDate,
|
||||
@Nullable MessageId messageToEdit)
|
||||
throws InvalidMessageException
|
||||
{
|
||||
if (ExpiredBuildReminder.isEligible()) {
|
||||
@@ -3049,8 +3091,11 @@ public class ConversationParentFragment extends Fragment
|
||||
final OutgoingMessage message;
|
||||
|
||||
if (sendPush) {
|
||||
if (scheduledDate > 0) {
|
||||
message = OutgoingMessage.text(recipient.get(), messageBody, expiresIn, System.currentTimeMillis(), null).sendAt(scheduledDate);
|
||||
if (messageToEdit != null) {
|
||||
message = OutgoingMessage.editText(recipient.get(), messageBody, System.currentTimeMillis(), null, messageToEdit.getId());
|
||||
} else if (scheduledDate > 0) {
|
||||
message = OutgoingMessage.text(recipient.get(), messageBody, expiresIn, System.currentTimeMillis(), null)
|
||||
.sendAt(scheduledDate);
|
||||
} else {
|
||||
message = OutgoingMessage.text(recipient.get(), messageBody, expiresIn, System.currentTimeMillis(), null);
|
||||
}
|
||||
@@ -3111,6 +3156,13 @@ public class ConversationParentFragment extends Fragment
|
||||
return;
|
||||
}
|
||||
|
||||
if (inputPanel.inEditMessageMode()) {
|
||||
buttonToggle.display(sendEditButton);
|
||||
quickAttachmentToggle.hide();
|
||||
inlineAttachmentToggle.hide();
|
||||
return;
|
||||
}
|
||||
|
||||
if (draftViewModel.getVoiceNoteDraft() != null) {
|
||||
buttonToggle.display(sendButton);
|
||||
quickAttachmentToggle.hide();
|
||||
@@ -3336,7 +3388,8 @@ public class ConversationParentFragment extends Fragment
|
||||
initiating,
|
||||
true,
|
||||
null,
|
||||
scheduledDate);
|
||||
scheduledDate,
|
||||
null);
|
||||
}
|
||||
|
||||
private void sendSticker(@NonNull StickerRecord stickerRecord, boolean clearCompose) {
|
||||
@@ -3653,7 +3706,11 @@ public class ConversationParentFragment extends Fragment
|
||||
@Override
|
||||
public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
|
||||
if (actionId == EditorInfo.IME_ACTION_SEND) {
|
||||
sendButton.performClick();
|
||||
if (inputPanel.isInEditMode()) {
|
||||
sendEditButton.performClick();
|
||||
} else {
|
||||
sendButton.performClick();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -3737,7 +3794,13 @@ public class ConversationParentFragment extends Fragment
|
||||
}
|
||||
|
||||
private void handleSaveDraftOnTextChange(@NonNull CharSequence text) {
|
||||
textDraftSaveDebouncer.publish(() -> draftViewModel.setTextDraft(StringUtil.trimSequence(text).toString(), MentionAnnotation.getMentionsFromAnnotations(text), MessageStyler.getStyling(text)));
|
||||
textDraftSaveDebouncer.publish(() -> {
|
||||
if (inputPanel.inEditMessageMode()) {
|
||||
draftViewModel.setMessageEditDraft(inputPanel.getEditMessageId(), StringUtil.trimSequence(text).toString(), MentionAnnotation.getMentionsFromAnnotations(text), MessageStyler.getStyling(text));
|
||||
} else {
|
||||
draftViewModel.setTextDraft(StringUtil.trimSequence(text).toString(), MentionAnnotation.getMentionsFromAnnotations(text), MessageStyler.getStyling(text));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void handleTypingIndicatorOnTextChange(@NonNull String text) {
|
||||
@@ -4050,6 +4113,62 @@ public class ConversationParentFragment extends Fragment
|
||||
inputPanel.clickOnComposeInput();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleEditMessage(@NonNull ConversationMessage conversationMessage) {
|
||||
if (!FeatureFlags.editMessageSending()) {
|
||||
return;
|
||||
}
|
||||
if (isSearchRequested) {
|
||||
searchViewItem.collapseActionView();
|
||||
}
|
||||
disposables.add(viewModel.resolveMessageToEdit(conversationMessage).subscribe(updatedMessage -> {
|
||||
inputPanel.enterEditMessageMode(glideRequests, updatedMessage, false);
|
||||
}));
|
||||
}
|
||||
|
||||
private void handleSendEditMessage() {
|
||||
if (!FeatureFlags.editMessageSending()) {
|
||||
Log.w(TAG, "Edit message sending disabled, forcing exit of edit mode");
|
||||
inputPanel.exitEditMessageMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!inputPanel.inEditMessageMode()) {
|
||||
Log.w(TAG, "Not in edit message mode, unknown state, forcing re-exit");
|
||||
inputPanel.exitEditMessageMode();
|
||||
return;
|
||||
}
|
||||
|
||||
MessageRecord editMessage = inputPanel.getEditMessage();
|
||||
if (editMessage == null) {
|
||||
Log.w(TAG, "No edit message found, forcing exit");
|
||||
inputPanel.exitEditMessageMode();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!MessageConstraintsUtil.isValidEditMessageSend(editMessage, System.currentTimeMillis())) {
|
||||
Log.i(TAG, "Edit message no longer valid");
|
||||
final int editDurationHours = MessageConstraintsUtil.getEditMessageThresholdHours();
|
||||
Dialogs.showAlertDialog(requireContext(), null, getResources().getQuantityString(R.plurals.ConversationActivity_edit_message_too_old, editDurationHours, editDurationHours));
|
||||
return;
|
||||
}
|
||||
|
||||
String metricId = recipient.get().isGroup() ? SignalLocalMetrics.GroupMessageSend.start()
|
||||
: SignalLocalMetrics.IndividualMessageSend.start();
|
||||
|
||||
sendMessage(metricId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnterEditMode() {
|
||||
updateToggleButtonState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onExitEditMode() {
|
||||
updateToggleButtonState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMessageActionToolbarOpened() {
|
||||
searchViewItem.collapseActionView();
|
||||
|
||||
@@ -42,6 +42,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.thoughtcrime.securesms.util.ThemeUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
@@ -738,6 +739,10 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
items.add(new ActionItem(R.drawable.symbol_reply_24, getResources().getString(R.string.conversation_selection__menu_reply), () -> handleActionItemClicked(Action.REPLY)));
|
||||
}
|
||||
|
||||
if (FeatureFlags.editMessageSending() && menuState.shouldShowEditAction()) {
|
||||
items.add(new ActionItem(R.drawable.symbol_edit_24, getResources().getString(R.string.conversation_selection__menu_edit), () -> handleActionItemClicked(Action.EDIT)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowForwardAction()) {
|
||||
items.add(new ActionItem(R.drawable.symbol_forward_24, getResources().getString(R.string.conversation_selection__menu_forward), () -> handleActionItemClicked(Action.FORWARD)));
|
||||
}
|
||||
@@ -968,6 +973,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
||||
|
||||
public enum Action {
|
||||
REPLY,
|
||||
EDIT,
|
||||
FORWARD,
|
||||
RESEND,
|
||||
DOWNLOAD,
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.os.Build;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
|
||||
import org.signal.core.util.StreamUtil;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.contacts.sync.ContactDiscovery;
|
||||
@@ -15,22 +16,28 @@ import org.thoughtcrime.securesms.database.RecipientTable;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.ThreadTable;
|
||||
import org.thoughtcrime.securesms.database.model.GroupRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.MultiDeviceViewedUpdateJob;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority;
|
||||
import org.thoughtcrime.securesms.mms.TextSlide;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.util.BubbleUtil;
|
||||
import org.thoughtcrime.securesms.util.ConversationUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
@@ -184,6 +191,28 @@ public class ConversationRepository {
|
||||
}).subscribeOn(Schedulers.io());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Single<ConversationMessage> resolveMessageToEdit(@NonNull ConversationMessage message) {
|
||||
return Single.fromCallable(() -> {
|
||||
MessageRecord messageRecord = message.getMessageRecord();
|
||||
if (MessageRecordUtil.hasTextSlide(messageRecord)) {
|
||||
TextSlide textSlide = MessageRecordUtil.requireTextSlide(messageRecord);
|
||||
if (textSlide.getUri() == null) {
|
||||
return message;
|
||||
}
|
||||
|
||||
try (InputStream stream = PartAuthority.getAttachmentStream(context, textSlide.getUri())) {
|
||||
String body = StreamUtil.readFullyAsString(stream);
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, body);
|
||||
} catch (IOException e) {
|
||||
Log.w(TAG, "Failed to read text slide data.");
|
||||
}
|
||||
}
|
||||
return message;
|
||||
}).subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
Observable<Integer> getUnreadCount(long threadId, long afterTime) {
|
||||
if (threadId <= -1L || afterTime <= 0L) {
|
||||
return Observable.just(0);
|
||||
|
||||
@@ -58,6 +58,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.BackpressureStrategy;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Observable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.processors.PublishProcessor;
|
||||
@@ -441,6 +442,11 @@ public class ConversationViewModel extends ViewModel {
|
||||
return LiveDataReactiveStreams.fromPublisher(activeProfile.toFlowable(BackpressureStrategy.LATEST));
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Single<ConversationMessage> resolveMessageToEdit(@NonNull ConversationMessage message) {
|
||||
return conversationRepository.resolveMessageToEdit(message);
|
||||
}
|
||||
|
||||
void setArgs(@NonNull ConversationIntents.Args args) {
|
||||
this.args = args;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -25,6 +26,7 @@ final class MenuState {
|
||||
private final boolean delete;
|
||||
private final boolean reactions;
|
||||
private final boolean paymentDetails;
|
||||
private final boolean edit;
|
||||
|
||||
private MenuState(@NonNull Builder builder) {
|
||||
forward = builder.forward;
|
||||
@@ -36,6 +38,7 @@ final class MenuState {
|
||||
delete = builder.delete;
|
||||
reactions = builder.reactions;
|
||||
paymentDetails = builder.paymentDetails;
|
||||
edit = builder.edit;
|
||||
}
|
||||
|
||||
boolean shouldShowForwardAction() {
|
||||
@@ -74,6 +77,10 @@ final class MenuState {
|
||||
return paymentDetails;
|
||||
}
|
||||
|
||||
boolean shouldShowEditAction() {
|
||||
return edit;
|
||||
}
|
||||
|
||||
static MenuState getMenuState(@NonNull Recipient conversationRecipient,
|
||||
@NonNull Set<MultiselectPart> selectedParts,
|
||||
boolean shouldShowMessageRequest,
|
||||
@@ -152,7 +159,8 @@ final class MenuState {
|
||||
.shouldShowReplyAction(false)
|
||||
.shouldShowDetailsAction(false)
|
||||
.shouldShowSaveAttachmentAction(false)
|
||||
.shouldShowResendAction(false);
|
||||
.shouldShowResendAction(false)
|
||||
.shouldShowEdit(false);
|
||||
} else {
|
||||
MessageRecord messageRecord = selectedParts.iterator().next().getMessageRecord();
|
||||
|
||||
@@ -169,6 +177,10 @@ final class MenuState {
|
||||
.shouldShowForwardAction(shouldShowForwardAction)
|
||||
.shouldShowDetailsAction(!actionMessage && !conversationRecipient.isReleaseNotes())
|
||||
.shouldShowReplyAction(canReplyToMessage(conversationRecipient, actionMessage, messageRecord, shouldShowMessageRequest, isNonAdminInAnnouncementGroup));
|
||||
|
||||
builder.shouldShowEdit(!actionMessage &&
|
||||
hasText &&
|
||||
MessageConstraintsUtil.isValidEditMessageSend(messageRecord, System.currentTimeMillis()));
|
||||
}
|
||||
|
||||
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText && !hasGift && !hasPayment)
|
||||
@@ -204,23 +216,7 @@ final class MenuState {
|
||||
}
|
||||
|
||||
static boolean isActionMessage(@NonNull MessageRecord messageRecord) {
|
||||
return messageRecord.isGroupAction() ||
|
||||
messageRecord.isCallLog() ||
|
||||
messageRecord.isJoined() ||
|
||||
messageRecord.isExpirationTimerUpdate() ||
|
||||
messageRecord.isEndSession() ||
|
||||
messageRecord.isIdentityUpdate() ||
|
||||
messageRecord.isIdentityVerified() ||
|
||||
messageRecord.isIdentityDefault() ||
|
||||
messageRecord.isProfileChange() ||
|
||||
messageRecord.isGroupV1MigrationEvent() ||
|
||||
messageRecord.isChatSessionRefresh() ||
|
||||
messageRecord.isInMemoryMessageRecord() ||
|
||||
messageRecord.isChangeNumber() ||
|
||||
messageRecord.isBoostRequest() ||
|
||||
messageRecord.isPaymentsRequestToActivate() ||
|
||||
messageRecord.isPaymentsActivated() ||
|
||||
messageRecord.isSmsExportType();
|
||||
return messageRecord.isInMemoryMessageRecord() || messageRecord.isUpdate();
|
||||
}
|
||||
|
||||
private final static class Builder {
|
||||
@@ -234,6 +230,7 @@ final class MenuState {
|
||||
private boolean delete;
|
||||
private boolean reactions;
|
||||
private boolean paymentDetails;
|
||||
private boolean edit;
|
||||
|
||||
@NonNull Builder shouldShowForwardAction(boolean forward) {
|
||||
this.forward = forward;
|
||||
@@ -280,6 +277,11 @@ final class MenuState {
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull Builder shouldShowEdit(boolean edit) {
|
||||
this.edit = edit;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
MenuState build() {
|
||||
return new MenuState(this);
|
||||
|
||||
@@ -93,7 +93,7 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
val colorizer = Colorizer()
|
||||
|
||||
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient, colorizer).apply {
|
||||
setCondensedMode(true)
|
||||
setCondensedMode(ConversationItemDisplayMode.CONDENSED)
|
||||
setScheduledMessagesMode(true)
|
||||
}
|
||||
|
||||
@@ -276,6 +276,7 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment
|
||||
override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onActivatePaymentsClicked() = Unit
|
||||
override fun onSendPaymentClicked(recipientId: RecipientId) = Unit
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -7,7 +7,9 @@ import android.text.SpannableString
|
||||
import io.reactivex.rxjava3.core.Maybe
|
||||
import io.reactivex.rxjava3.core.Single
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.signal.core.util.StreamUtil
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory
|
||||
@@ -21,14 +23,19 @@ import org.thoughtcrime.securesms.database.ThreadTable
|
||||
import org.thoughtcrime.securesms.database.adjustBodyRanges
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.mms.PartAuthority
|
||||
import org.thoughtcrime.securesms.mms.QuoteId
|
||||
import org.thoughtcrime.securesms.providers.BlobProvider
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor
|
||||
import org.thoughtcrime.securesms.util.hasTextSlide
|
||||
import org.thoughtcrime.securesms.util.requireTextSlide
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.Executor
|
||||
|
||||
class DraftRepository(
|
||||
@@ -38,6 +45,10 @@ class DraftRepository(
|
||||
private val saveDraftsExecutor: Executor = SerialMonoLifoExecutor(SignalExecutors.BOUNDED)
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val TAG = Log.tag(DraftRepository::class.java)
|
||||
}
|
||||
|
||||
fun deleteVoiceNoteDraftData(draft: DraftTable.Draft?) {
|
||||
if (draft != null) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
@@ -56,7 +67,11 @@ class DraftRepository(
|
||||
}
|
||||
|
||||
draftTable.replaceDrafts(actualThreadId, drafts)
|
||||
threadTable.updateSnippet(actualThreadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true)
|
||||
if (drafts.shouldUpdateSnippet()) {
|
||||
threadTable.updateSnippet(actualThreadId, drafts.getSnippet(context), drafts.getUriSnippet(), System.currentTimeMillis(), MessageTypes.BASE_DRAFT_TYPE, true)
|
||||
} else {
|
||||
threadTable.update(actualThreadId, unarchive = false, allowDeletion = false)
|
||||
}
|
||||
} else if (threadId > 0) {
|
||||
draftTable.clearDrafts(threadId)
|
||||
threadTable.update(threadId, unarchive = false, allowDeletion = false)
|
||||
@@ -101,5 +116,26 @@ class DraftRepository(
|
||||
}
|
||||
}
|
||||
|
||||
fun loadDraftMessageEdit(serialized: String): Maybe<ConversationMessage> {
|
||||
return Maybe.fromCallable {
|
||||
val messageId = MessageId.deserialize(serialized)
|
||||
val messageRecord: MessageRecord = SignalDatabase.messages.getMessageRecordOrNull(messageId.id) ?: return@fromCallable null
|
||||
if (messageRecord.hasTextSlide()) {
|
||||
val textSlide = messageRecord.requireTextSlide()
|
||||
if (textSlide.uri != null) {
|
||||
try {
|
||||
PartAuthority.getAttachmentStream(context, textSlide.uri!!).use { stream ->
|
||||
val body = StreamUtil.readFullyAsString(stream)
|
||||
return@fromCallable ConversationMessageFactory.createWithUnresolvedData(context, messageRecord, body)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
Log.e(TAG, "Failed to load text slide", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
ConversationMessageFactory.createWithUnresolvedData(context, messageRecord)
|
||||
}
|
||||
}
|
||||
|
||||
data class DatabaseDraft(val drafts: Drafts, val updatedText: CharSequence?)
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ data class DraftState(
|
||||
val bodyRangesDraft: DraftTable.Draft? = null,
|
||||
val quoteDraft: DraftTable.Draft? = null,
|
||||
val locationDraft: DraftTable.Draft? = null,
|
||||
val voiceNoteDraft: DraftTable.Draft? = null
|
||||
val voiceNoteDraft: DraftTable.Draft? = null,
|
||||
val messageEditDraft: DraftTable.Draft? = null
|
||||
) {
|
||||
|
||||
fun copyAndClearDrafts(threadId: Long = this.threadId): DraftState {
|
||||
@@ -26,6 +27,7 @@ data class DraftState(
|
||||
|
||||
fun toDrafts(): Drafts {
|
||||
return Drafts().apply {
|
||||
addIfNotNull(messageEditDraft)
|
||||
addIfNotNull(textDraft)
|
||||
addIfNotNull(bodyRangesDraft)
|
||||
addIfNotNull(quoteDraft)
|
||||
@@ -41,7 +43,8 @@ data class DraftState(
|
||||
bodyRangesDraft = drafts.getDraftOfType(DraftTable.Draft.BODY_RANGES),
|
||||
quoteDraft = drafts.getDraftOfType(DraftTable.Draft.QUOTE),
|
||||
locationDraft = drafts.getDraftOfType(DraftTable.Draft.LOCATION),
|
||||
voiceNoteDraft = drafts.getDraftOfType(DraftTable.Draft.VOICE_NOTE)
|
||||
voiceNoteDraft = drafts.getDraftOfType(DraftTable.Draft.VOICE_NOTE),
|
||||
messageEditDraft = drafts.getDraftOfType(DraftTable.Draft.MESSAGE_EDIT)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.DraftTable.Draft
|
||||
import org.thoughtcrime.securesms.database.MentionUtil
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.mms.QuoteId
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
@@ -67,6 +68,22 @@ class DraftViewModel @JvmOverloads constructor(
|
||||
store.update { it.copy(recipientId = recipient.id) }
|
||||
}
|
||||
|
||||
fun setMessageEditDraft(messageId: MessageId, text: String, mentions: List<Mention>, styleBodyRanges: BodyRangeList?) {
|
||||
store.update {
|
||||
val mentionRanges: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(mentions)
|
||||
|
||||
val bodyRanges: BodyRangeList? = if (styleBodyRanges == null) {
|
||||
mentionRanges
|
||||
} else if (mentionRanges == null) {
|
||||
styleBodyRanges
|
||||
} else {
|
||||
styleBodyRanges.toBuilder().addAllRanges(mentionRanges.rangesList).build()
|
||||
}
|
||||
|
||||
saveDrafts(it.copy(textDraft = text.toTextDraft(), bodyRangesDraft = bodyRanges?.toDraft(), messageEditDraft = Draft(Draft.MESSAGE_EDIT, messageId.serialize())))
|
||||
}
|
||||
}
|
||||
|
||||
fun setTextDraft(text: String, mentions: List<Mention>, styleBodyRanges: BodyRangeList?) {
|
||||
store.update {
|
||||
val mentionRanges: BodyRangeList? = MentionUtil.mentionsToBodyRangeList(mentions)
|
||||
@@ -131,6 +148,12 @@ class DraftViewModel @JvmOverloads constructor(
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun loadDraftEditMessage(serialized: String): Maybe<ConversationMessage> {
|
||||
return repository.loadDraftMessageEdit(serialized)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toTextDraft(): Draft? {
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialog
|
||||
import org.thoughtcrime.securesms.components.recyclerview.SmoothScrollingLinearLayoutManager
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationBottomSheetCallback
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
@@ -72,7 +73,7 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
val colorizer = Colorizer()
|
||||
|
||||
messageAdapter = ConversationAdapter(requireContext(), viewLifecycleOwner, GlideApp.with(this), Locale.getDefault(), ConversationAdapterListener(), conversationRecipient, colorizer).apply {
|
||||
setCondensedMode(true)
|
||||
setCondensedMode(ConversationItemDisplayMode.CONDENSED)
|
||||
}
|
||||
|
||||
val list: RecyclerView = view.findViewById<RecyclerView>(R.id.quotes_list).apply {
|
||||
@@ -250,6 +251,11 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
dismiss()
|
||||
getAdapterListener().onSendPaymentClicked(recipientId)
|
||||
}
|
||||
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
|
||||
dismiss()
|
||||
getAdapterListener().onEditedIndicatorClicked(messageRecord)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.edit
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.os.bundleOf
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.viewModels
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import io.reactivex.rxjava3.kotlin.subscribeBy
|
||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||
import org.thoughtcrime.securesms.components.FixedRoundedCornerBottomSheetDialogFragment
|
||||
import org.thoughtcrime.securesms.components.ViewBinderDelegate
|
||||
import org.thoughtcrime.securesms.conversation.ConversationAdapter
|
||||
import org.thoughtcrime.securesms.conversation.ConversationBottomSheetCallback
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MmsMessageRecord
|
||||
import org.thoughtcrime.securesms.databinding.MessageEditHistoryBottomSheetBinding
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ItemDecoration
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicy
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionPlayerHolder
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionRecycler
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange
|
||||
import org.thoughtcrime.securesms.mms.GlideApp
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.BottomSheetUtil
|
||||
import org.thoughtcrime.securesms.util.ViewModelFactory
|
||||
import org.thoughtcrime.securesms.util.fragments.requireListener
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* Show history of edits for a specific message.
|
||||
*/
|
||||
class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() {
|
||||
|
||||
override val peekHeightPercentage: Float = 0.4f
|
||||
|
||||
private val binding: MessageEditHistoryBottomSheetBinding by ViewBinderDelegate(MessageEditHistoryBottomSheetBinding::bind)
|
||||
private val messageId: Long by lazy { requireArguments().getLong(ARGUMENT_MESSAGE_ID) }
|
||||
private val conversationRecipient: Recipient by lazy { Recipient.resolved(requireArguments().getParcelable(ARGUMENT_CONVERSATION_RECIPIENT_ID)!!) }
|
||||
private val viewModel: EditMessageHistoryViewModel by viewModels(factoryProducer = ViewModelFactory.factoryProducer { EditMessageHistoryViewModel(messageId, conversationRecipient) })
|
||||
|
||||
private val disposables: LifecycleDisposable = LifecycleDisposable()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val view = MessageEditHistoryBottomSheetBinding.inflate(inflater, container, false).root
|
||||
view.minimumHeight = (resources.displayMetrics.heightPixels * peekHeightPercentage).toInt()
|
||||
return view
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
disposables.bindTo(viewLifecycleOwner)
|
||||
|
||||
val colorizer = Colorizer()
|
||||
|
||||
val messageAdapter = ConversationAdapter(
|
||||
requireContext(),
|
||||
viewLifecycleOwner,
|
||||
GlideApp.with(this),
|
||||
Locale.getDefault(),
|
||||
ConversationAdapterListener(),
|
||||
conversationRecipient,
|
||||
colorizer
|
||||
).apply {
|
||||
setCondensedMode(ConversationItemDisplayMode.EXTRA_CONDENSED)
|
||||
}
|
||||
|
||||
binding.editHistoryList.apply {
|
||||
layoutManager = LinearLayoutManager(requireContext(), LinearLayoutManager.VERTICAL, false)
|
||||
adapter = messageAdapter
|
||||
itemAnimator = null
|
||||
}
|
||||
|
||||
val recyclerViewColorizer = RecyclerViewColorizer(binding.editHistoryList)
|
||||
|
||||
disposables += viewModel
|
||||
.getEditHistory()
|
||||
.subscribeBy { messages ->
|
||||
if (messages.isEmpty()) {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
messageAdapter.submitList(messages)
|
||||
recyclerViewColorizer.setChatColors(conversationRecipient.chatColors)
|
||||
}
|
||||
|
||||
disposables += viewModel.getNameColorsMap().subscribe { map ->
|
||||
colorizer.onNameColorsChanged(map)
|
||||
messageAdapter.notifyItemRangeChanged(0, messageAdapter.itemCount, ConversationAdapter.PAYLOAD_NAME_COLORS)
|
||||
}
|
||||
|
||||
initializeGiphyMp4()
|
||||
}
|
||||
|
||||
private fun initializeGiphyMp4(): GiphyMp4ProjectionRecycler {
|
||||
val maxPlayback = GiphyMp4PlaybackPolicy.maxSimultaneousPlaybackInConversation()
|
||||
val holders = GiphyMp4ProjectionPlayerHolder.injectVideoViews(
|
||||
requireContext(),
|
||||
viewLifecycleOwner.lifecycle,
|
||||
binding.videoContainer,
|
||||
maxPlayback
|
||||
)
|
||||
val callback = GiphyMp4ProjectionRecycler(holders)
|
||||
|
||||
GiphyMp4PlaybackController.attach(binding.editHistoryList, callback, maxPlayback)
|
||||
binding.editHistoryList.addItemDecoration(GiphyMp4ItemDecoration(callback) {}, 0)
|
||||
|
||||
return callback
|
||||
}
|
||||
|
||||
private inner class ConversationAdapterListener : ConversationAdapter.ItemClickListener by requireListener<ConversationBottomSheetCallback>().getConversationAdapterListener() {
|
||||
override fun onQuoteClicked(messageRecord: MmsMessageRecord) = Unit
|
||||
override fun onScheduledIndicatorClicked(view: View, messageRecord: MessageRecord) = Unit
|
||||
override fun onGroupMemberClicked(recipientId: RecipientId, groupId: GroupId) = Unit
|
||||
override fun onItemClick(item: MultiselectPart) = Unit
|
||||
override fun onItemLongClick(itemView: View, item: MultiselectPart) = Unit
|
||||
override fun onQuotedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onReactionClicked(multiselectPart: MultiselectPart, messageId: Long, isMms: Boolean) = Unit
|
||||
override fun onMessageWithRecaptchaNeededClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onGroupMigrationLearnMoreClicked(membershipChange: GroupMigrationMembershipChange) = Unit
|
||||
override fun onChatSessionRefreshLearnMoreClicked() = Unit
|
||||
override fun onBadDecryptLearnMoreClicked(author: RecipientId) = Unit
|
||||
override fun onSafetyNumberLearnMoreClicked(recipient: Recipient) = Unit
|
||||
override fun onJoinGroupCallClicked() = Unit
|
||||
override fun onInviteFriendsToGroupClicked(groupId: GroupId.V2) = Unit
|
||||
override fun onEnableCallNotificationsClicked() = Unit
|
||||
override fun onCallToAction(action: String) = Unit
|
||||
override fun onDonateClicked() = Unit
|
||||
override fun onRecipientNameClicked(target: RecipientId) = Unit
|
||||
override fun onViewGiftBadgeClicked(messageRecord: MessageRecord) = Unit
|
||||
override fun onActivatePaymentsClicked() = Unit
|
||||
override fun onSendPaymentClicked(recipientId: RecipientId) = Unit
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) = Unit
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARGUMENT_MESSAGE_ID = "message_id"
|
||||
private const val ARGUMENT_CONVERSATION_RECIPIENT_ID = "recipient_id"
|
||||
|
||||
@JvmStatic
|
||||
fun show(fragmentManager: FragmentManager, threadRecipient: RecipientId, messageId: Long) {
|
||||
EditMessageHistoryDialog()
|
||||
.apply {
|
||||
arguments = bundleOf(
|
||||
ARGUMENT_MESSAGE_ID to messageId,
|
||||
ARGUMENT_CONVERSATION_RECIPIENT_ID to threadRecipient
|
||||
)
|
||||
}
|
||||
.show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.edit
|
||||
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.conversation.ConversationDataSource
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.database.DatabaseObserver
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
|
||||
object EditMessageHistoryRepository {
|
||||
|
||||
fun getEditHistory(messageId: Long): Observable<List<ConversationMessage>> {
|
||||
return Observable.create { emitter ->
|
||||
val threadId: Long = SignalDatabase.messages.getThreadIdForMessage(messageId)
|
||||
if (threadId < 0) {
|
||||
emitter.onNext(emptyList())
|
||||
return@create
|
||||
}
|
||||
|
||||
val databaseObserver: DatabaseObserver = ApplicationDependencies.getDatabaseObserver()
|
||||
val observer = DatabaseObserver.Observer { emitter.onNext(getEditHistorySync(messageId)) }
|
||||
|
||||
databaseObserver.registerConversationObserver(threadId, observer)
|
||||
|
||||
emitter.setCancellable { databaseObserver.unregisterObserver(observer) }
|
||||
emitter.onNext(getEditHistorySync(messageId))
|
||||
}.subscribeOn(Schedulers.io())
|
||||
}
|
||||
|
||||
private fun getEditHistorySync(messageId: Long): List<ConversationMessage> {
|
||||
val context = ApplicationDependencies.getApplication()
|
||||
val records = SignalDatabase
|
||||
.messages
|
||||
.getMessageEditHistory(messageId)
|
||||
.toList()
|
||||
|
||||
val attachmentHelper = ConversationDataSource.AttachmentHelper()
|
||||
.apply {
|
||||
addAll(records)
|
||||
fetchAttachments()
|
||||
}
|
||||
|
||||
return attachmentHelper
|
||||
.buildUpdatedModels(context, records)
|
||||
.map { ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(context, it) }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package org.thoughtcrime.securesms.conversation.ui.edit
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.rxjava3.core.Observable
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage
|
||||
import org.thoughtcrime.securesms.conversation.colors.GroupAuthorNameColorHelper
|
||||
import org.thoughtcrime.securesms.conversation.colors.NameColor
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
|
||||
/**
|
||||
* View model to show history of edits for a specific message.
|
||||
*/
|
||||
class EditMessageHistoryViewModel(private val messageId: Long, private val conversationRecipient: Recipient) : ViewModel() {
|
||||
private val groupAuthorNameColorHelper = GroupAuthorNameColorHelper()
|
||||
|
||||
fun getEditHistory(): Observable<List<ConversationMessage>> {
|
||||
return EditMessageHistoryRepository
|
||||
.getEditHistory(messageId)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
fun getNameColorsMap(): Observable<Map<RecipientId, NameColor>> {
|
||||
return conversationRecipient
|
||||
.live()
|
||||
.observable()
|
||||
.map { recipient ->
|
||||
if (recipient.groupId.isPresent) {
|
||||
groupAuthorNameColorHelper.getColorMap(recipient.groupId.get())
|
||||
} else {
|
||||
emptyMap()
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
}
|
||||
@@ -603,6 +603,10 @@ class ConversationFragment : LoggingFragment(R.layout.v2_conversation_fragment)
|
||||
requireActivity().startActivity(create(requireActivity(), args), options.toBundle())
|
||||
}
|
||||
|
||||
override fun onEditedIndicatorClicked(messageRecord: MessageRecord) {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
}
|
||||
|
||||
override fun onItemClick(item: MultiselectPart?) {
|
||||
// TODO [alex] -- ("Not yet implemented")
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
import org.signal.core.util.CursorUtil;
|
||||
import org.signal.core.util.SQLiteDatabaseExtensionsKt;
|
||||
import org.signal.core.util.SetUtil;
|
||||
import org.signal.core.util.SqlUtil;
|
||||
import org.signal.core.util.StreamUtil;
|
||||
@@ -1484,6 +1485,16 @@ public class AttachmentTable extends DatabaseTable {
|
||||
return EncryptedMediaDataSource.createFor(attachmentSecret, dataInfo.file, dataInfo.random, dataInfo.length);
|
||||
}
|
||||
|
||||
public void duplicateAttachmentsForMessage(long destinationMessageId, long sourceMessageId) {
|
||||
SQLiteDatabaseExtensionsKt.withinTransaction(getWritableDatabase(), db -> {
|
||||
db.execSQL("CREATE TEMPORARY TABLE tmp_part AS SELECT * FROM " + TABLE_NAME + " WHERE " + MMS_ID + " = ?", SqlUtil.buildArgs(sourceMessageId));
|
||||
db.execSQL("UPDATE tmp_part SET " + ROW_ID + " = NULL, " + MMS_ID + " = ?", SqlUtil.buildArgs(destinationMessageId));
|
||||
db.execSQL("INSERT INTO " + TABLE_NAME + " SELECT * FROM tmp_part");
|
||||
db.execSQL("DROP TABLE tmp_part");
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
static class DataInfo {
|
||||
private final File file;
|
||||
|
||||
@@ -141,6 +141,7 @@ class DraftTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
const val QUOTE = "quote"
|
||||
const val BODY_RANGES = "mention"
|
||||
const val VOICE_NOTE = "voice_note"
|
||||
const val MESSAGE_EDIT = "message_edit"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,6 +160,10 @@ class DraftTable(context: Context?, databaseHelper: SignalDatabase?) : DatabaseT
|
||||
return firstOrNull { it.type == type }
|
||||
}
|
||||
|
||||
fun shouldUpdateSnippet(): Boolean {
|
||||
return none { it.type == Draft.MESSAGE_EDIT }
|
||||
}
|
||||
|
||||
fun getSnippet(context: Context): String {
|
||||
val textDraft = getDraftOfType(Draft.TEXT)
|
||||
return if (textDraft != null) {
|
||||
|
||||
@@ -134,6 +134,7 @@ import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags
|
||||
import org.thoughtcrime.securesms.util.JsonUtils
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.Util
|
||||
import org.thoughtcrime.securesms.util.isStory
|
||||
@@ -203,6 +204,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
const val STORY_TYPE = "story_type"
|
||||
const val PARENT_STORY_ID = "parent_story_id"
|
||||
const val SCHEDULED_DATE = "scheduled_date"
|
||||
const val LATEST_REVISION_ID = "latest_revision_id"
|
||||
const val ORIGINAL_MESSAGE_ID = "original_message_id"
|
||||
const val REVISION_NUMBER = "revision_number"
|
||||
|
||||
const val CREATE_TABLE = """
|
||||
CREATE TABLE $TABLE_NAME (
|
||||
@@ -254,12 +258,15 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$PARENT_STORY_ID INTEGER DEFAULT 0,
|
||||
$EXPORT_STATE BLOB DEFAULT NULL,
|
||||
$EXPORTED INTEGER DEFAULT 0,
|
||||
$SCHEDULED_DATE INTEGER DEFAULT -1
|
||||
$SCHEDULED_DATE INTEGER DEFAULT -1,
|
||||
$LATEST_REVISION_ID INTEGER DEFAULT NULL REFERENCES $TABLE_NAME ($ID) ON DELETE CASCADE,
|
||||
$ORIGINAL_MESSAGE_ID INTEGER DEFAULT NULL REFERENCES $TABLE_NAME ($ID) ON DELETE CASCADE,
|
||||
$REVISION_NUMBER INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
|
||||
private const val INDEX_THREAD_DATE = "message_thread_date_index"
|
||||
private const val INDEX_THREAD_STORY_SCHEDULED_DATE = "message_thread_story_parent_story_scheduled_date_index"
|
||||
private const val INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID = "message_thread_story_parent_story_scheduled_date_latest_revision_id_index"
|
||||
|
||||
@JvmField
|
||||
val CREATE_INDEXS = arrayOf(
|
||||
@@ -271,8 +278,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
"CREATE INDEX IF NOT EXISTS message_reactions_unread_index ON $TABLE_NAME ($REACTIONS_UNREAD);",
|
||||
"CREATE INDEX IF NOT EXISTS message_story_type_index ON $TABLE_NAME ($STORY_TYPE);",
|
||||
"CREATE INDEX IF NOT EXISTS message_parent_story_id_index ON $TABLE_NAME ($PARENT_STORY_ID);",
|
||||
"CREATE INDEX IF NOT EXISTS $INDEX_THREAD_STORY_SCHEDULED_DATE ON $TABLE_NAME ($THREAD_ID, $DATE_RECEIVED, $STORY_TYPE, $PARENT_STORY_ID, $SCHEDULED_DATE);",
|
||||
"CREATE INDEX IF NOT EXISTS message_quote_id_quote_author_scheduled_date_index ON $TABLE_NAME ($QUOTE_ID, $QUOTE_AUTHOR, $SCHEDULED_DATE);",
|
||||
"CREATE INDEX IF NOT EXISTS $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID ON $TABLE_NAME ($THREAD_ID, $DATE_RECEIVED, $STORY_TYPE, $PARENT_STORY_ID, $SCHEDULED_DATE, $LATEST_REVISION_ID);",
|
||||
"CREATE INDEX IF NOT EXISTS message_quote_id_quote_author_scheduled_date_latest_revision_id_index ON $TABLE_NAME ($QUOTE_ID, $QUOTE_AUTHOR, $SCHEDULED_DATE, $LATEST_REVISION_ID);",
|
||||
"CREATE INDEX IF NOT EXISTS message_exported_index ON $TABLE_NAME ($EXPORTED);",
|
||||
"CREATE INDEX IF NOT EXISTS message_id_type_payment_transactions_index ON $TABLE_NAME ($ID,$TYPE) WHERE $TYPE & ${MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} != 0;"
|
||||
)
|
||||
@@ -323,7 +330,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
MESSAGE_RANGES,
|
||||
STORY_TYPE,
|
||||
PARENT_STORY_ID,
|
||||
SCHEDULED_DATE
|
||||
SCHEDULED_DATE,
|
||||
LATEST_REVISION_ID,
|
||||
ORIGINAL_MESSAGE_ID,
|
||||
REVISION_NUMBER
|
||||
)
|
||||
|
||||
private val MMS_PROJECTION: Array<String> = MMS_PROJECTION_BASE + "NULL AS ${AttachmentTable.ATTACHMENT_JSON_ALIAS}"
|
||||
@@ -380,7 +390,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
$TYPE & ${MessageTypes.GROUP_V2_LEAVE_BITS} != ${MessageTypes.GROUP_V2_LEAVE_BITS} AND
|
||||
$STORY_TYPE = 0 AND
|
||||
$PARENT_STORY_ID <= 0 AND
|
||||
$SCHEDULED_DATE = -1 AND
|
||||
$SCHEDULED_DATE = -1 AND
|
||||
$LATEST_REVISION_ID IS NULL AND
|
||||
$TYPE NOT IN (
|
||||
${MessageTypes.PROFILE_CHANGE_TYPE},
|
||||
${MessageTypes.GV1_MIGRATION_TYPE},
|
||||
@@ -965,8 +976,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun insertMessageInbox(message: IncomingTextMessage, type: Long = MessageTypes.BASE_INBOX_TYPE): Optional<InsertResult> {
|
||||
var type = type
|
||||
fun insertMessageInbox(message: IncomingTextMessage, editedMessage: MediaMmsMessageRecord? = null): Optional<InsertResult> {
|
||||
var type = MessageTypes.BASE_INBOX_TYPE
|
||||
var tryToCollapseJoinRequestEvents = false
|
||||
|
||||
if (message.isJoined) {
|
||||
@@ -1059,13 +1070,19 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
values.put(THREAD_ID, threadId)
|
||||
values.put(SERVER_GUID, message.serverGuid)
|
||||
|
||||
if (editedMessage != null) {
|
||||
values.put(ORIGINAL_MESSAGE_ID, editedMessage.getOriginalOrOwnMessageId().id)
|
||||
} else {
|
||||
values.putNull(ORIGINAL_MESSAGE_ID)
|
||||
}
|
||||
|
||||
return if (message.isPush && isDuplicate(message, threadId)) {
|
||||
Log.w(TAG, "Duplicate message (" + message.sentTimestampMillis + "), ignoring...")
|
||||
Optional.empty()
|
||||
} else {
|
||||
val messageId = writableDatabase.insert(TABLE_NAME, null, values)
|
||||
|
||||
if (unread) {
|
||||
if (unread && editedMessage == null) {
|
||||
threads.incrementUnread(threadId, 1, 0)
|
||||
}
|
||||
|
||||
@@ -1084,6 +1101,48 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
}
|
||||
|
||||
fun insertEditMessageInbox(threadId: Long, mediaMessage: IncomingMediaMessage, targetMessage: MediaMmsMessageRecord): Optional<InsertResult> {
|
||||
val insertResult = insertSecureDecryptedMessageInbox(mediaMessage, threadId, targetMessage)
|
||||
|
||||
if (insertResult.isPresent) {
|
||||
val (messageId) = insertResult.get()
|
||||
|
||||
if (targetMessage.expireStarted > 0) {
|
||||
markExpireStarted(messageId, targetMessage.expireStarted)
|
||||
}
|
||||
|
||||
writableDatabase.update(TABLE_NAME)
|
||||
.values(LATEST_REVISION_ID to messageId)
|
||||
.where("$ID = ? OR $LATEST_REVISION_ID = ?", targetMessage.id, targetMessage.id)
|
||||
.run()
|
||||
|
||||
reactions.moveReactionsToNewMessage(newMessageId = messageId, previousId = targetMessage.id)
|
||||
}
|
||||
|
||||
return insertResult
|
||||
}
|
||||
|
||||
fun insertEditMessageInbox(textMessage: IncomingTextMessage, targetMessage: MediaMmsMessageRecord): Optional<InsertResult> {
|
||||
val insertResult = insertMessageInbox(textMessage, targetMessage)
|
||||
|
||||
if (insertResult.isPresent) {
|
||||
val (messageId) = insertResult.get()
|
||||
|
||||
if (targetMessage.expireStarted > 0) {
|
||||
markExpireStarted(messageId, targetMessage.expireStarted)
|
||||
}
|
||||
|
||||
writableDatabase.update(TABLE_NAME)
|
||||
.values(LATEST_REVISION_ID to messageId)
|
||||
.where("$ID_WHERE OR $LATEST_REVISION_ID = ?", targetMessage.id, targetMessage.id)
|
||||
.run()
|
||||
|
||||
reactions.moveReactionsToNewMessage(newMessageId = messageId, previousId = targetMessage.id)
|
||||
}
|
||||
|
||||
return insertResult
|
||||
}
|
||||
|
||||
fun insertProfileNameChangeMessages(recipient: Recipient, newProfileName: String, previousProfileName: String) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
val groupRecords = groups.getGroupsContainingMember(recipient.id, false)
|
||||
@@ -1676,7 +1735,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
fun getScheduledMessageCountForThread(threadId: Long): Int {
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE")
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID")
|
||||
.where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE != ?", threadId, 0, 0, -1)
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
@@ -1685,8 +1744,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
fun getMessageCountForThread(threadId: Long): Int {
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE")
|
||||
.where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ?", threadId, 0, 0, -1)
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID")
|
||||
.where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ? AND $LATEST_REVISION_ID IS NULL", threadId, 0, 0, -1)
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
}
|
||||
@@ -1694,8 +1753,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
fun getMessageCountForThread(threadId: Long, beforeTime: Long): Int {
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE")
|
||||
.where("$THREAD_ID = ? AND $DATE_RECEIVED < ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ?", threadId, beforeTime, 0, 0, -1)
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID")
|
||||
.where("$THREAD_ID = ? AND $DATE_RECEIVED < ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ? AND $LATEST_REVISION_ID IS NULL", threadId, beforeTime, 0, 0, -1)
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
}
|
||||
@@ -1746,6 +1805,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val query = """
|
||||
$THREAD_ID = ? AND
|
||||
$STORY_TYPE = 0 AND
|
||||
$LATEST_REVISION_ID IS NULL AND
|
||||
$PARENT_STORY_ID <= 0 AND
|
||||
(
|
||||
NOT $TYPE & ${MessageTypes.IGNORABLE_TYPESMASK_WHEN_COUNTING} AND
|
||||
@@ -1844,11 +1904,31 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
}
|
||||
|
||||
private fun getOriginalEditedMessageRecord(messageId: Long): Long {
|
||||
return readableDatabase.select(ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$TABLE_NAME.$LATEST_REVISION_ID = ?", messageId)
|
||||
.orderBy("$ID DESC")
|
||||
.limit(1)
|
||||
.run()
|
||||
.readToSingleLong(0)
|
||||
}
|
||||
|
||||
fun getMessages(messageIds: Collection<Long?>): MmsReader {
|
||||
val ids = TextUtils.join(",", messageIds)
|
||||
return mmsReaderFor(rawQueryWithAttachments("$TABLE_NAME.$ID IN ($ids)", null))
|
||||
}
|
||||
|
||||
fun getMessageEditHistory(id: Long): MmsReader {
|
||||
val cursor = readableDatabase.select(*MMS_PROJECTION)
|
||||
.from(TABLE_NAME)
|
||||
.where("$TABLE_NAME.$ID = ? OR $TABLE_NAME.$LATEST_REVISION_ID = ?", id, id)
|
||||
.orderBy("$TABLE_NAME.$ID DESC")
|
||||
.run()
|
||||
|
||||
return mmsReaderFor(cursor)
|
||||
}
|
||||
|
||||
private fun updateMailboxBitmask(id: Long, maskOff: Long, maskOn: Long, threadId: Optional<Long>) {
|
||||
writableDatabase.withinTransaction { db ->
|
||||
db.execSQL(
|
||||
@@ -2378,6 +2458,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
null
|
||||
}
|
||||
|
||||
val editedMessage = getOriginalEditedMessageRecord(messageId)
|
||||
|
||||
OutgoingMessage(
|
||||
recipient = threadRecipient,
|
||||
body = body,
|
||||
@@ -2399,7 +2481,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
giftBadge = giftBadge,
|
||||
isSecure = MessageTypes.isSecureType(outboxType),
|
||||
bodyRanges = messageRanges,
|
||||
scheduledDate = scheduledDate
|
||||
scheduledDate = scheduledDate,
|
||||
messageToEdit = editedMessage
|
||||
)
|
||||
}
|
||||
} ?: throw NoSuchMessageException("No record found for id: $messageId")
|
||||
@@ -2410,7 +2493,8 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
retrieved: IncomingMediaMessage,
|
||||
contentLocation: String,
|
||||
candidateThreadId: Long,
|
||||
mailbox: Long
|
||||
mailbox: Long,
|
||||
editedMessage: MediaMmsMessageRecord?
|
||||
): Optional<InsertResult> {
|
||||
val threadId = if (candidateThreadId == -1L || retrieved.isGroupMessage) {
|
||||
getThreadIdFor(retrieved)
|
||||
@@ -2443,7 +2527,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
PARENT_STORY_ID to if (retrieved.parentStoryId != null) retrieved.parentStoryId.serialize() else 0,
|
||||
READ to if (silentUpdate || retrieved.isExpirationUpdate) 1 else 0,
|
||||
UNIDENTIFIED to retrieved.isUnidentified,
|
||||
SERVER_GUID to retrieved.serverGuid
|
||||
SERVER_GUID to retrieved.serverGuid,
|
||||
LATEST_REVISION_ID to null,
|
||||
ORIGINAL_MESSAGE_ID to editedMessage?.getOriginalOrOwnMessageId()?.id,
|
||||
REVISION_NUMBER to (editedMessage?.revisionNumber?.inc() ?: 0)
|
||||
)
|
||||
|
||||
val quoteAttachments: MutableList<Attachment> = mutableListOf()
|
||||
@@ -2483,6 +2570,24 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
unarchive = true
|
||||
)
|
||||
|
||||
if (editedMessage != null) {
|
||||
if (retrieved.quote != null && editedMessage.quote != null) {
|
||||
writableDatabase.execSQL(
|
||||
"""
|
||||
WITH o as (SELECT $QUOTE_ID, $QUOTE_AUTHOR, $QUOTE_BODY, $QUOTE_TYPE, $QUOTE_MISSING, $QUOTE_BODY_RANGES FROM $TABLE_NAME WHERE $ID = ${editedMessage.id})
|
||||
UPDATE $TABLE_NAME
|
||||
SET $QUOTE_ID = old.$QUOTE_ID, $QUOTE_AUTHOR = old.$QUOTE_AUTHOR, $QUOTE_BODY = old.$QUOTE_BODY, $QUOTE_TYPE = old.$QUOTE_TYPE, $QUOTE_MISSING = old.$QUOTE_MISSING, $QUOTE_BODY_RANGES = old.$QUOTE_BODY_RANGES
|
||||
FROM o old
|
||||
WHERE $TABLE_NAME.$ID = $messageId
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (retrieved.attachments.isEmpty() && editedMessage?.id != null && attachments.getAttachmentsForMessage(editedMessage.id).isNotEmpty()) {
|
||||
attachments.duplicateAttachmentsForMessage(messageId, editedMessage.id)
|
||||
}
|
||||
|
||||
val isNotStoryGroupReply = retrieved.parentStoryId == null || !retrieved.parentStoryId.isGroupReply()
|
||||
|
||||
if (!MessageTypes.isPaymentsActivated(mailbox) && !MessageTypes.isPaymentsRequestToActivate(mailbox) && !MessageTypes.isExpirationTimerUpdate(mailbox) && !retrieved.storyType.isStory && isNotStoryGroupReply) {
|
||||
@@ -2528,11 +2633,12 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
type = type or MessageTypes.SPECIAL_TYPE_PAYMENTS_ACTIVATED
|
||||
}
|
||||
|
||||
return insertMessageInbox(retrieved, contentLocation, threadId, type)
|
||||
return insertMessageInbox(retrieved, contentLocation, threadId, type, editedMessage = null)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
@Throws(MmsException::class)
|
||||
fun insertSecureDecryptedMessageInbox(retrieved: IncomingMediaMessage, threadId: Long): Optional<InsertResult> {
|
||||
fun insertSecureDecryptedMessageInbox(retrieved: IncomingMediaMessage, threadId: Long, edittedMediaMessage: MediaMmsMessageRecord? = null): Optional<InsertResult> {
|
||||
var type = MessageTypes.BASE_INBOX_TYPE or MessageTypes.SECURE_MESSAGE_BIT
|
||||
var hasSpecialType = false
|
||||
|
||||
@@ -2581,7 +2687,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
hasSpecialType = true
|
||||
}
|
||||
|
||||
return insertMessageInbox(retrieved, "", threadId, type)
|
||||
return insertMessageInbox(retrieved, "", threadId, type, edittedMediaMessage)
|
||||
}
|
||||
|
||||
fun insertMessageInbox(notification: NotificationInd, subscriptionId: Int): Pair<Long, Long> {
|
||||
@@ -2836,15 +2942,27 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
Log.w(TAG, "Found early delivery receipts for " + message.sentTimeMillis + ". Applying them.")
|
||||
}
|
||||
|
||||
var editedMessage: MessageRecord? = null
|
||||
if (message.isMessageEdit) {
|
||||
try {
|
||||
editedMessage = getMessageRecord(message.messageToEdit)
|
||||
if (!MessageConstraintsUtil.isValidEditMessageSend(editedMessage)) {
|
||||
throw MmsException("Message is not valid to edit")
|
||||
}
|
||||
} catch (e: NoSuchMessageException) {
|
||||
throw MmsException("Unable to locate edited message", e)
|
||||
}
|
||||
}
|
||||
|
||||
val contentValues = ContentValues()
|
||||
contentValues.put(DATE_SENT, message.sentTimeMillis)
|
||||
contentValues.put(MMS_MESSAGE_TYPE, PduHeaders.MESSAGE_TYPE_SEND_REQ)
|
||||
contentValues.put(TYPE, type)
|
||||
contentValues.put(THREAD_ID, threadId)
|
||||
contentValues.put(READ, 1)
|
||||
contentValues.put(DATE_RECEIVED, System.currentTimeMillis())
|
||||
contentValues.put(DATE_RECEIVED, editedMessage?.dateReceived ?: System.currentTimeMillis())
|
||||
contentValues.put(SMS_SUBSCRIPTION_ID, message.subscriptionId)
|
||||
contentValues.put(EXPIRES_IN, message.expiresIn)
|
||||
contentValues.put(EXPIRES_IN, editedMessage?.expiresIn ?: message.expiresIn)
|
||||
contentValues.put(VIEW_ONCE, message.isViewOnce)
|
||||
contentValues.put(FROM_RECIPIENT_ID, Recipient.self().id.serialize())
|
||||
contentValues.put(FROM_DEVICE_ID, SignalStore.account().deviceId)
|
||||
@@ -2854,6 +2972,14 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
contentValues.put(STORY_TYPE, message.storyType.code)
|
||||
contentValues.put(PARENT_STORY_ID, if (message.parentStoryId != null) message.parentStoryId.serialize() else 0)
|
||||
contentValues.put(SCHEDULED_DATE, message.scheduledDate)
|
||||
contentValues.putNull(LATEST_REVISION_ID)
|
||||
|
||||
if (editedMessage != null) {
|
||||
contentValues.put(ORIGINAL_MESSAGE_ID, editedMessage.getOriginalOrOwnMessageId().id)
|
||||
contentValues.put(REVISION_NUMBER, editedMessage.revisionNumber + 1)
|
||||
} else {
|
||||
contentValues.putNull(ORIGINAL_MESSAGE_ID)
|
||||
}
|
||||
|
||||
if (message.threadRecipient.isSelf && hasAudioAttachment(message.attachments)) {
|
||||
contentValues.put(VIEWED_RECEIPT_COUNT, 1L)
|
||||
@@ -2935,10 +3061,19 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
}
|
||||
|
||||
if (message.messageToEdit > 0) {
|
||||
writableDatabase.update(TABLE_NAME)
|
||||
.values(LATEST_REVISION_ID to messageId)
|
||||
.where("$ID_WHERE OR $LATEST_REVISION_ID = ?", message.messageToEdit, message.messageToEdit)
|
||||
.run()
|
||||
|
||||
reactions.moveReactionsToNewMessage(messageId, message.messageToEdit)
|
||||
}
|
||||
|
||||
threads.updateLastSeenAndMarkSentAndLastScrolledSilenty(threadId)
|
||||
|
||||
if (!message.storyType.isStory) {
|
||||
if (message.outgoingQuote == null) {
|
||||
if (message.outgoingQuote == null && editedMessage == null) {
|
||||
ApplicationDependencies.getDatabaseObserver().notifyMessageInsertObservers(threadId, MessageId(messageId))
|
||||
} else {
|
||||
ApplicationDependencies.getDatabaseObserver().notifyConversationListeners(threadId)
|
||||
@@ -3273,6 +3408,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
reader.filterNotNull()
|
||||
}
|
||||
}
|
||||
|
||||
fun getAllRateLimitedMessageIds(): Set<Long> {
|
||||
val db = databaseHelper.signalReadableDatabase
|
||||
val where = "(" + TYPE + " & " + MessageTypes.TOTAL_MASK + " & " + MessageTypes.MESSAGE_RATE_LIMITED_BIT + ") > 0"
|
||||
@@ -3389,7 +3525,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
private fun getMessagesInThreadAfterInclusive(threadId: Long, timestamp: Long, limit: Long): List<MessageRecord> {
|
||||
val where = "$TABLE_NAME.$THREAD_ID = ? AND $TABLE_NAME.$DATE_RECEIVED >= ? AND $TABLE_NAME.$SCHEDULED_DATE = -1"
|
||||
val where = "$TABLE_NAME.$THREAD_ID = ? AND $TABLE_NAME.$DATE_RECEIVED >= ? AND $TABLE_NAME.$SCHEDULED_DATE = -1 AND $TABLE_NAME.$LATEST_REVISION_ID IS NULL"
|
||||
val args = buildArgs(threadId, timestamp)
|
||||
|
||||
return mmsReaderFor(rawQueryWithAttachments(where, args, false, limit)).use { reader ->
|
||||
@@ -3858,7 +3994,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
fun getAllMessagesThatQuote(id: MessageId): List<MessageRecord> {
|
||||
val targetMessage: MessageRecord = getMessageRecord(id.id)
|
||||
|
||||
val query = "$QUOTE_ID = ${targetMessage.dateSent} AND $QUOTE_AUTHOR = ${targetMessage.fromRecipient.id.serialize()} AND $SCHEDULED_DATE = -1"
|
||||
val query = "$QUOTE_ID = ${targetMessage.dateSent} AND $QUOTE_AUTHOR = ${targetMessage.fromRecipient.id.serialize()} AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL"
|
||||
val order = "$DATE_RECEIVED DESC"
|
||||
|
||||
val records: MutableList<MessageRecord> = ArrayList()
|
||||
@@ -3873,8 +4009,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
|
||||
fun getQuotedMessagePosition(threadId: Long, quoteId: Long, authorId: RecipientId): Int {
|
||||
var position = 0
|
||||
readableDatabase
|
||||
.select(DATE_SENT, FROM_RECIPIENT_ID, REMOTE_DELETED)
|
||||
.select(DATE_SENT, FROM_RECIPIENT_ID, REMOTE_DELETED, LATEST_REVISION_ID)
|
||||
.from(TABLE_NAME)
|
||||
.where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1")
|
||||
.orderBy("$DATE_RECEIVED DESC")
|
||||
@@ -3887,8 +4024,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return if (cursor.requireBoolean(REMOTE_DELETED)) {
|
||||
-1
|
||||
} else {
|
||||
cursor.position
|
||||
position
|
||||
}
|
||||
} else if (cursor.requireLong(LATEST_REVISION_ID) == 0L) {
|
||||
position++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3899,7 +4038,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
readableDatabase
|
||||
.select(DATE_RECEIVED, FROM_RECIPIENT_ID, REMOTE_DELETED)
|
||||
.from(TABLE_NAME)
|
||||
.where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1")
|
||||
.where("$THREAD_ID = $threadId AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL")
|
||||
.orderBy("$DATE_RECEIVED DESC")
|
||||
.run()
|
||||
.forEach { cursor ->
|
||||
@@ -3938,10 +4077,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
|
||||
if (groupStoryId > 0) {
|
||||
order = "$DATE_RECEIVED ASC"
|
||||
selection = "$THREAD_ID = $threadId AND $DATE_RECEIVED < $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID = $groupStoryId AND $SCHEDULED_DATE = -1"
|
||||
selection = "$THREAD_ID = $threadId AND $DATE_RECEIVED < $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID = $groupStoryId AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL"
|
||||
} else {
|
||||
order = "$DATE_RECEIVED DESC"
|
||||
selection = "$THREAD_ID = $threadId AND $DATE_RECEIVED > $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1"
|
||||
selection = "$THREAD_ID = $threadId AND $DATE_RECEIVED > $receivedTimestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL"
|
||||
}
|
||||
|
||||
return readableDatabase
|
||||
@@ -3957,7 +4096,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return readableDatabase
|
||||
.select(DATE_RECEIVED)
|
||||
.from(TABLE_NAME)
|
||||
.where("$DATE_RECEIVED > $date AND $SCHEDULED_DATE = -1")
|
||||
.where("$DATE_RECEIVED > $date AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL")
|
||||
.orderBy("$DATE_RECEIVED ASC")
|
||||
.limit(1)
|
||||
.run()
|
||||
@@ -3968,7 +4107,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$DATE_RECEIVED < $date AND $SCHEDULED_DATE = -1")
|
||||
.where("$DATE_RECEIVED < $date AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL")
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
}
|
||||
@@ -3986,7 +4125,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from(TABLE_NAME)
|
||||
.where("$THREAD_ID = $threadId AND $DATE_RECEIVED >= $timestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1")
|
||||
.where("$THREAD_ID = $threadId AND $DATE_RECEIVED >= $timestamp AND $STORY_TYPE = 0 AND $PARENT_STORY_ID <= 0 AND $SCHEDULED_DATE = -1 AND $LATEST_REVISION_ID IS NULL")
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
}
|
||||
@@ -4017,7 +4156,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return readableDatabase
|
||||
.select("COUNT(*)")
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_DATE")
|
||||
.where("$READ = 0 AND $STORY_TYPE = 0 AND $THREAD_ID = $threadId AND $PARENT_STORY_ID <= 0")
|
||||
.where("$READ = 0 AND $STORY_TYPE = 0 AND $THREAD_ID = $threadId AND $PARENT_STORY_ID <= 0 AND $LATEST_REVISION_ID IS NULL")
|
||||
.run()
|
||||
.readToSingleInt()
|
||||
}
|
||||
@@ -4410,7 +4549,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
return readableDatabase
|
||||
.select(*MMS_PROJECTION)
|
||||
.from(TABLE_NAME)
|
||||
.where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ?", threadId, 0, 0, -1)
|
||||
.where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE = ? AND $LATEST_REVISION_ID IS NULL", threadId, 0, 0, -1)
|
||||
.orderBy("$DATE_RECEIVED DESC")
|
||||
.limit(limitStr)
|
||||
.run()
|
||||
@@ -4422,7 +4561,7 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
fun getScheduledMessagesInThread(threadId: Long): List<MessageRecord> {
|
||||
val cursor = readableDatabase
|
||||
.select(*MMS_PROJECTION)
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE")
|
||||
.from("$TABLE_NAME INDEXED BY $INDEX_THREAD_STORY_SCHEDULED_DATE_LATEST_REVISION_ID")
|
||||
.where("$THREAD_ID = ? AND $STORY_TYPE = ? AND $PARENT_STORY_ID <= ? AND $SCHEDULED_DATE != ?", threadId, 0, 0, -1)
|
||||
.orderBy("$SCHEDULED_DATE DESC, $ID DESC")
|
||||
.run()
|
||||
@@ -4690,6 +4829,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
}
|
||||
}
|
||||
|
||||
private fun MessageRecord.getOriginalOrOwnMessageId(): MessageId {
|
||||
return this.originalMessageId ?: MessageId(this.id)
|
||||
}
|
||||
|
||||
protected enum class ReceiptType(val columnName: String, val groupStatus: Int) {
|
||||
READ(READ_RECEIPT_COUNT, GroupReceiptTable.STATUS_READ),
|
||||
DELIVERY(DELIVERY_RECEIPT_COUNT, GroupReceiptTable.STATUS_DELIVERED),
|
||||
@@ -4974,6 +5117,9 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
val storyType = StoryType.fromCode(cursor.requireInt(STORY_TYPE))
|
||||
val parentStoryId = ParentStoryId.deserialize(cursor.requireLong(PARENT_STORY_ID))
|
||||
val scheduledDate = cursor.requireLong(SCHEDULED_DATE)
|
||||
val latestRevisionId: MessageId? = cursor.requireLong(LATEST_REVISION_ID).let { if (it == 0L) null else MessageId(it) }
|
||||
val originalMessageId: MessageId? = cursor.requireLong(ORIGINAL_MESSAGE_ID).let { if (it == 0L) null else MessageId(it) }
|
||||
val editCount = cursor.requireInt(REVISION_NUMBER)
|
||||
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
||||
readReceiptCount = 0
|
||||
@@ -5057,7 +5203,10 @@ open class MessageTable(context: Context?, databaseHelper: SignalDatabase) : Dat
|
||||
giftBadge,
|
||||
null,
|
||||
null,
|
||||
scheduledDate
|
||||
scheduledDate,
|
||||
latestRevisionId,
|
||||
originalMessageId,
|
||||
editCount
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.database.Cursor
|
||||
import org.signal.core.util.CursorUtil
|
||||
import org.signal.core.util.SqlUtil
|
||||
import org.signal.core.util.delete
|
||||
import org.signal.core.util.update
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.ReactionRecord
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
@@ -177,4 +178,12 @@ class ReactionTable(context: Context, databaseHelper: SignalDatabase) : Database
|
||||
.where("$MESSAGE_ID NOT IN (SELECT ${MessageTable.ID} FROM ${MessageTable.TABLE_NAME})")
|
||||
.run()
|
||||
}
|
||||
|
||||
fun moveReactionsToNewMessage(newMessageId: Long, previousId: Long) {
|
||||
writableDatabase
|
||||
.update(TABLE_NAME)
|
||||
.values(MESSAGE_ID to newMessageId)
|
||||
.where("$MESSAGE_ID = ?", previousId)
|
||||
.run()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,9 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
WHERE
|
||||
$FTS_TABLE_NAME MATCH ? AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.GROUP_V2_BIT} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} = 0
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.SCHEDULED_DATE} < 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.LATEST_REVISION_ID} IS NULL
|
||||
ORDER BY ${MessageTable.DATE_RECEIVED} DESC
|
||||
LIMIT 500
|
||||
"""
|
||||
@@ -99,7 +101,11 @@ class SearchTable(context: Context, databaseHelper: SignalDatabase) : DatabaseTa
|
||||
INNER JOIN ${ThreadTable.TABLE_NAME} ON $FTS_TABLE_NAME.$THREAD_ID = ${ThreadTable.TABLE_NAME}.${ThreadTable.ID}
|
||||
WHERE
|
||||
$FTS_TABLE_NAME MATCH ? AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = ?
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.THREAD_ID} = ? AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.GROUP_V2_BIT} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.TYPE} & ${MessageTypes.SPECIAL_TYPE_PAYMENTS_NOTIFICATION} = 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.SCHEDULED_DATE} < 0 AND
|
||||
${MessageTable.TABLE_NAME}.${MessageTable.LATEST_REVISION_ID} IS NULL
|
||||
ORDER BY ${MessageTable.DATE_RECEIVED} DESC
|
||||
LIMIT 500
|
||||
"""
|
||||
|
||||
@@ -161,7 +161,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
|
||||
Log.i(TAG, "Upgrading database: $oldVersion, $newVersion")
|
||||
val startTime = System.currentTimeMillis()
|
||||
db.setForeignKeyConstraintsEnabled(false)
|
||||
db.beginTransaction()
|
||||
try {
|
||||
migrate(context, db, oldVersion, newVersion)
|
||||
@@ -169,7 +168,6 @@ open class SignalDatabase(private val context: Application, databaseSecret: Data
|
||||
db.setTransactionSuccessful()
|
||||
} finally {
|
||||
db.endTransaction()
|
||||
db.setForeignKeyConstraintsEnabled(true)
|
||||
|
||||
// We have to re-begin the transaction for the calling code (see comment at start of method)
|
||||
db.beginTransaction()
|
||||
|
||||
@@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.database.helpers.migration.V182_CallTableMigra
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V183_CallLinkTableMigration
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V184_CallLinkReplaceIndexMigration
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V185_MessageRecipientsMigration
|
||||
import org.thoughtcrime.securesms.database.helpers.migration.V186_AddEditMessageColumnsMigration
|
||||
|
||||
/**
|
||||
* Contains all of the database migrations for [SignalDatabase]. Broken into a separate file for cleanliness.
|
||||
@@ -49,7 +50,7 @@ object SignalDatabaseMigrations {
|
||||
|
||||
val TAG: String = Log.tag(SignalDatabaseMigrations.javaClass)
|
||||
|
||||
const val DATABASE_VERSION = 185
|
||||
const val DATABASE_VERSION = 186
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -200,6 +201,10 @@ object SignalDatabaseMigrations {
|
||||
if (oldVersion < 185) {
|
||||
V185_MessageRecipientsMigration.migrate(context, db, oldVersion, newVersion)
|
||||
}
|
||||
|
||||
if (oldVersion < 186) {
|
||||
V186_AddEditMessageColumnsMigration.migrate(context, db, oldVersion, newVersion)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
package org.thoughtcrime.securesms.database.helpers.migration
|
||||
|
||||
import android.app.Application
|
||||
import net.zetetic.database.sqlcipher.SQLiteDatabase
|
||||
import org.signal.core.util.Stopwatch
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.core.util.readToList
|
||||
import org.signal.core.util.requireNonNullString
|
||||
|
||||
/**
|
||||
* Changes needed for edit message. New foreign keys require recreating the table.
|
||||
*/
|
||||
@Suppress("ClassName")
|
||||
object V186_AddEditMessageColumnsMigration : SignalDatabaseMigration {
|
||||
|
||||
private val TAG = Log.tag(V186_AddEditMessageColumnsMigration::class.java)
|
||||
|
||||
override fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
val stopwatch = Stopwatch("migration")
|
||||
|
||||
val dependentItems: List<SqlItem> = getAllDependentItems(db, "message")
|
||||
dependentItems.forEach { item ->
|
||||
val sql = "DROP ${item.type} IF EXISTS ${item.name}"
|
||||
Log.d(TAG, "Executing: $sql")
|
||||
db.execSQL(sql)
|
||||
}
|
||||
|
||||
stopwatch.split("drop-dependents")
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
CREATE TABLE message_tmp (
|
||||
_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
date_sent INTEGER NOT NULL,
|
||||
date_received INTEGER NOT NULL,
|
||||
date_server INTEGER DEFAULT -1,
|
||||
thread_id INTEGER NOT NULL REFERENCES thread (_id) ON DELETE CASCADE,
|
||||
from_recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
|
||||
from_device_id INTEGER,
|
||||
to_recipient_id INTEGER NOT NULL REFERENCES recipient (_id) ON DELETE CASCADE,
|
||||
type INTEGER NOT NULL,
|
||||
body TEXT,
|
||||
read INTEGER DEFAULT 0,
|
||||
ct_l TEXT,
|
||||
exp INTEGER,
|
||||
m_type INTEGER,
|
||||
m_size INTEGER,
|
||||
st INTEGER,
|
||||
tr_id TEXT,
|
||||
subscription_id INTEGER DEFAULT -1,
|
||||
receipt_timestamp INTEGER DEFAULT -1,
|
||||
delivery_receipt_count INTEGER DEFAULT 0,
|
||||
read_receipt_count INTEGER DEFAULT 0,
|
||||
viewed_receipt_count INTEGER DEFAULT 0,
|
||||
mismatched_identities TEXT DEFAULT NULL,
|
||||
network_failures TEXT DEFAULT NULL,
|
||||
expires_in INTEGER DEFAULT 0,
|
||||
expire_started INTEGER DEFAULT 0,
|
||||
notified INTEGER DEFAULT 0,
|
||||
quote_id INTEGER DEFAULT 0,
|
||||
quote_author INTEGER DEFAULT 0,
|
||||
quote_body TEXT DEFAULT NULL,
|
||||
quote_missing INTEGER DEFAULT 0,
|
||||
quote_mentions BLOB DEFAULT NULL,
|
||||
quote_type INTEGER DEFAULT 0,
|
||||
shared_contacts TEXT DEFAULT NULL,
|
||||
unidentified INTEGER DEFAULT 0,
|
||||
link_previews TEXT DEFAULT NULL,
|
||||
view_once INTEGER DEFAULT 0,
|
||||
reactions_unread INTEGER DEFAULT 0,
|
||||
reactions_last_seen INTEGER DEFAULT -1,
|
||||
remote_deleted INTEGER DEFAULT 0,
|
||||
mentions_self INTEGER DEFAULT 0,
|
||||
notified_timestamp INTEGER DEFAULT 0,
|
||||
server_guid TEXT DEFAULT NULL,
|
||||
message_ranges BLOB DEFAULT NULL,
|
||||
story_type INTEGER DEFAULT 0,
|
||||
parent_story_id INTEGER DEFAULT 0,
|
||||
export_state BLOB DEFAULT NULL,
|
||||
exported INTEGER DEFAULT 0,
|
||||
scheduled_date INTEGER DEFAULT -1,
|
||||
latest_revision_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE CASCADE,
|
||||
original_message_id INTEGER DEFAULT NULL REFERENCES message (_id) ON DELETE CASCADE,
|
||||
revision_number INTEGER DEFAULT 0
|
||||
)
|
||||
"""
|
||||
)
|
||||
stopwatch.split("create-table")
|
||||
|
||||
db.execSQL(
|
||||
"""
|
||||
INSERT INTO message_tmp
|
||||
SELECT
|
||||
_id,
|
||||
date_sent,
|
||||
date_received,
|
||||
date_server,
|
||||
thread_id,
|
||||
from_recipient_id,
|
||||
from_device_id,
|
||||
to_recipient_id,
|
||||
type,
|
||||
body,
|
||||
read,
|
||||
ct_l,
|
||||
exp,
|
||||
m_type,
|
||||
m_size,
|
||||
st,
|
||||
tr_id,
|
||||
subscription_id,
|
||||
receipt_timestamp,
|
||||
delivery_receipt_count,
|
||||
read_receipt_count,
|
||||
viewed_receipt_count,
|
||||
mismatched_identities,
|
||||
network_failures,
|
||||
expires_in,
|
||||
expire_started,
|
||||
notified,
|
||||
quote_id,
|
||||
quote_author,
|
||||
quote_body,
|
||||
quote_missing,
|
||||
quote_mentions,
|
||||
quote_type,
|
||||
shared_contacts,
|
||||
unidentified,
|
||||
link_previews,
|
||||
view_once,
|
||||
reactions_unread,
|
||||
reactions_last_seen,
|
||||
remote_deleted,
|
||||
mentions_self,
|
||||
notified_timestamp,
|
||||
server_guid,
|
||||
message_ranges,
|
||||
story_type,
|
||||
parent_story_id,
|
||||
export_state,
|
||||
exported,
|
||||
scheduled_date,
|
||||
NULL AS latest_revision_id,
|
||||
NULL AS original_message_id,
|
||||
0 as revision_number
|
||||
FROM message
|
||||
"""
|
||||
)
|
||||
stopwatch.split("copy-data")
|
||||
|
||||
db.execSQL("DROP TABLE message")
|
||||
stopwatch.split("drop-old")
|
||||
|
||||
db.execSQL("ALTER TABLE message_tmp RENAME TO message")
|
||||
stopwatch.split("rename-table")
|
||||
|
||||
dependentItems.forEach { item ->
|
||||
val sql = when (item.name) {
|
||||
"message_thread_story_parent_story_scheduled_date_index" -> "CREATE INDEX message_thread_story_parent_story_scheduled_date_latest_revision_id_index ON message (thread_id, date_received, story_type, parent_story_id, scheduled_date, latest_revision_id)"
|
||||
"message_quote_id_quote_author_scheduled_date_index" -> "CREATE INDEX message_quote_id_quote_author_scheduled_date_latest_revision_id_index ON message (quote_id, quote_author, scheduled_date, latest_revision_id)"
|
||||
else -> item.createStatement
|
||||
}
|
||||
Log.d(TAG, "Executing: $sql")
|
||||
db.execSQL(sql)
|
||||
}
|
||||
stopwatch.split("recreate-dependents")
|
||||
|
||||
db.execSQL("PRAGMA foreign_key_check")
|
||||
stopwatch.split("fk-check")
|
||||
|
||||
stopwatch.stop(TAG)
|
||||
}
|
||||
|
||||
private fun getAllDependentItems(db: SQLiteDatabase, tableName: String): List<SqlItem> {
|
||||
return db.rawQuery("SELECT type, name, sql FROM sqlite_schema WHERE tbl_name='$tableName' AND type != 'table'").readToList { cursor ->
|
||||
SqlItem(
|
||||
type = cursor.requireNonNullString("type"),
|
||||
name = cursor.requireNonNullString("name"),
|
||||
createStatement = cursor.requireNonNullString("sql")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class SqlItem(
|
||||
val type: String,
|
||||
val name: String,
|
||||
val createStatement: String
|
||||
)
|
||||
}
|
||||
@@ -54,7 +54,9 @@ public class InMemoryMessageRecord extends MessageRecord {
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
-1);
|
||||
-1,
|
||||
null,
|
||||
0);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -70,6 +70,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
private final Payment payment;
|
||||
private final CallTable.Call call;
|
||||
private final long scheduledDate;
|
||||
private final MessageId latestRevisionId;
|
||||
|
||||
public MediaMmsMessageRecord(long id,
|
||||
Recipient fromRecipient,
|
||||
@@ -106,18 +107,22 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
@Nullable GiftBadge giftBadge,
|
||||
@Nullable Payment payment,
|
||||
@Nullable CallTable.Call call,
|
||||
long scheduledDate)
|
||||
long scheduledDate,
|
||||
@Nullable MessageId latestRevisionId,
|
||||
@Nullable MessageId originalMessageId,
|
||||
int revisionNumber)
|
||||
{
|
||||
super(id, body, fromRecipient, fromDeviceId, toRecipient, dateSent,
|
||||
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck,
|
||||
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete, notifiedTimestamp, viewedReceiptCount, receiptTimestamp,
|
||||
storyType, parentStoryId, giftBadge);
|
||||
this.mentionsSelf = mentionsSelf;
|
||||
this.messageRanges = messageRanges;
|
||||
this.payment = payment;
|
||||
this.call = call;
|
||||
this.scheduledDate = scheduledDate;
|
||||
storyType, parentStoryId, giftBadge, originalMessageId, revisionNumber);
|
||||
this.mentionsSelf = mentionsSelf;
|
||||
this.messageRanges = messageRanges;
|
||||
this.payment = payment;
|
||||
this.call = call;
|
||||
this.scheduledDate = scheduledDate;
|
||||
this.latestRevisionId = latestRevisionId;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -204,18 +209,24 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
return scheduledDate;
|
||||
}
|
||||
|
||||
public @Nullable MessageId getLatestRevisionId() {
|
||||
return latestRevisionId;
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
|
||||
return new MediaMmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withoutQuote() {
|
||||
return new MediaMmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withAttachments(@NonNull Context context, @NonNull List<DatabaseAttachment> attachments) {
|
||||
@@ -236,14 +247,16 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
return new MediaMmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), slideDeck,
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), getCall(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withPayment(@NonNull Payment payment) {
|
||||
return new MediaMmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getScheduledDate());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment, getCall(), getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber());
|
||||
}
|
||||
|
||||
|
||||
@@ -251,7 +264,8 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
return new MediaMmsMessageRecord(getId(), getFromRecipient(), getFromDeviceId(), getToRecipient(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getScheduledDate());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment(), call, getScheduledDate(), getLatestRevisionId(),
|
||||
getOriginalMessageId(), getRevisionNumber());
|
||||
}
|
||||
|
||||
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {
|
||||
|
||||
@@ -102,6 +102,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
private final boolean remoteDelete;
|
||||
private final long notifiedTimestamp;
|
||||
private final long receiptTimestamp;
|
||||
private final MessageId originalMessageId;
|
||||
private final int revisionNumber;
|
||||
|
||||
protected Boolean isJumboji = null;
|
||||
|
||||
@@ -110,10 +112,18 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
int deliveryStatus, int deliveryReceiptCount, long type,
|
||||
Set<IdentityKeyMismatch> mismatches,
|
||||
Set<NetworkFailure> networkFailures,
|
||||
int subscriptionId, long expiresIn, long expireStarted,
|
||||
int readReceiptCount, boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions, boolean remoteDelete, long notifiedTimestamp,
|
||||
int viewedReceiptCount, long receiptTimestamp)
|
||||
int subscriptionId,
|
||||
long expiresIn,
|
||||
long expireStarted,
|
||||
int readReceiptCount,
|
||||
boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions,
|
||||
boolean remoteDelete,
|
||||
long notifiedTimestamp,
|
||||
int viewedReceiptCount,
|
||||
long receiptTimestamp,
|
||||
@Nullable MessageId originalMessageId,
|
||||
int revisionNumber)
|
||||
{
|
||||
super(body, fromRecipient, toRecipient, dateSent, dateReceived,
|
||||
threadId, deliveryStatus, deliveryReceiptCount, type,
|
||||
@@ -131,6 +141,8 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
this.remoteDelete = remoteDelete;
|
||||
this.notifiedTimestamp = notifiedTimestamp;
|
||||
this.receiptTimestamp = receiptTimestamp;
|
||||
this.originalMessageId = originalMessageId;
|
||||
this.revisionNumber = revisionNumber;
|
||||
}
|
||||
|
||||
public abstract boolean isMms();
|
||||
@@ -721,6 +733,18 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
|
||||
public boolean isEditMessage() {
|
||||
return originalMessageId != null;
|
||||
}
|
||||
|
||||
public @Nullable MessageId getOriginalMessageId() {
|
||||
return originalMessageId;
|
||||
}
|
||||
|
||||
public int getRevisionNumber() {
|
||||
return revisionNumber;
|
||||
}
|
||||
|
||||
public static final class InviteAddState {
|
||||
|
||||
private final boolean invited;
|
||||
|
||||
@@ -39,12 +39,13 @@ public abstract class MmsMessageRecord extends MessageRecord {
|
||||
@NonNull List<LinkPreview> linkPreviews, boolean unidentified,
|
||||
@NonNull List<ReactionRecord> reactions, boolean remoteDelete, long notifiedTimestamp,
|
||||
int viewedReceiptCount, long receiptTimestamp, @NonNull StoryType storyType,
|
||||
@Nullable ParentStoryId parentStoryId, @Nullable GiftBadge giftBadge)
|
||||
@Nullable ParentStoryId parentStoryId, @Nullable GiftBadge giftBadge, @Nullable MessageId originalMessageId,
|
||||
int revisionNumber)
|
||||
{
|
||||
super(id, body, fromRecipient, fromDeviceId, toRecipient,
|
||||
dateSent, dateReceived, dateServer, threadId, deliveryStatus, deliveryReceiptCount,
|
||||
type, mismatches, networkFailures, subscriptionId, expiresIn, expireStarted, readReceiptCount,
|
||||
unidentified, reactions, remoteDelete, notifiedTimestamp, viewedReceiptCount, receiptTimestamp);
|
||||
unidentified, reactions, remoteDelete, notifiedTimestamp, viewedReceiptCount, receiptTimestamp, originalMessageId, revisionNumber);
|
||||
|
||||
this.slideDeck = slideDeck;
|
||||
this.quote = quote;
|
||||
|
||||
@@ -60,7 +60,7 @@ public class NotificationMmsMessageRecord extends MmsMessageRecord {
|
||||
dateSent, dateReceived, -1, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox,
|
||||
new HashSet<>(), new HashSet<>(), subscriptionId,
|
||||
0, 0, false, slideDeck, readReceiptCount, null, Collections.emptyList(), Collections.emptyList(), false,
|
||||
Collections.emptyList(), false, 0, viewedReceiptCount, receiptTimestamp, storyType, parentStoryId, giftBadge);
|
||||
Collections.emptyList(), false, 0, viewedReceiptCount, receiptTimestamp, storyType, parentStoryId, giftBadge, null, 0);
|
||||
|
||||
this.contentLocation = contentLocation;
|
||||
this.messageSize = messageSize;
|
||||
|
||||
@@ -140,9 +140,10 @@ public class IndividualSendJob extends PushSendJob {
|
||||
throws IOException, MmsException, NoSuchMessageException, UndeliverableMessageException, RetryLaterException
|
||||
{
|
||||
ExpiringMessageManager expirationManager = ApplicationDependencies.getExpiringMessageManager();
|
||||
MessageTable database = SignalDatabase.messages();
|
||||
OutgoingMessage message = database.getOutgoingMessage(messageId);
|
||||
long threadId = database.getMessageRecord(messageId).getThreadId();
|
||||
MessageTable database = SignalDatabase.messages();
|
||||
OutgoingMessage message = database.getOutgoingMessage(messageId);
|
||||
long threadId = database.getMessageRecord(messageId).getThreadId();
|
||||
MessageRecord originalEditedMessage = message.getMessageToEdit() > 0 ? SignalDatabase.messages().getMessageRecordOrNull(message.getMessageToEdit()) : null;
|
||||
|
||||
if (database.isSent(messageId)) {
|
||||
warn(TAG, String.valueOf(message.getSentTimeMillis()), "Message " + messageId + " was already sent. Ignoring.");
|
||||
@@ -158,7 +159,7 @@ public class IndividualSendJob extends PushSendJob {
|
||||
byte[] profileKey = recipient.getProfileKey();
|
||||
UnidentifiedAccessMode accessMode = recipient.getUnidentifiedAccessMode();
|
||||
|
||||
boolean unidentified = deliver(message);
|
||||
boolean unidentified = deliver(message, originalEditedMessage);
|
||||
|
||||
database.markAsSent(messageId, true);
|
||||
markAttachmentsUploaded(messageId, message);
|
||||
@@ -181,7 +182,10 @@ public class IndividualSendJob extends PushSendJob {
|
||||
SignalDatabase.recipients().setUnidentifiedAccessMode(recipient.getId(), UnidentifiedAccessMode.DISABLED);
|
||||
}
|
||||
|
||||
if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
|
||||
if (originalEditedMessage != null && originalEditedMessage.getExpireStarted() > 0) {
|
||||
database.markExpireStarted(messageId, originalEditedMessage.getExpireStarted());
|
||||
expirationManager.scheduleDeletion(messageId, true, originalEditedMessage.getExpireStarted(), originalEditedMessage.getExpiresIn());
|
||||
} else if (message.getExpiresIn() > 0 && !message.isExpirationUpdate()) {
|
||||
database.markExpireStarted(messageId);
|
||||
expirationManager.scheduleDeletion(messageId, true, message.getExpiresIn());
|
||||
}
|
||||
@@ -214,7 +218,7 @@ public class IndividualSendJob extends PushSendJob {
|
||||
notifyMediaMessageDeliveryFailed(context, messageId);
|
||||
}
|
||||
|
||||
private boolean deliver(OutgoingMessage message)
|
||||
private boolean deliver(OutgoingMessage message, MessageRecord originalEditedMessage)
|
||||
throws IOException, InsecureFallbackApprovalException, UntrustedIdentityException, UndeliverableMessageException
|
||||
{
|
||||
if (message.getThreadRecipient() == null) {
|
||||
@@ -283,7 +287,12 @@ public class IndividualSendJob extends PushSendJob {
|
||||
|
||||
SignalServiceDataMessage mediaMessage = mediaMessageBuilder.build();
|
||||
|
||||
if (Util.equals(SignalStore.account().getAci(), address.getServiceId())) {
|
||||
if (originalEditedMessage != null) {
|
||||
SendMessageResult result = messageSender.sendEditMessage(address, UnidentifiedAccessUtil.getAccessFor(context, messageRecipient), ContentHint.RESENDABLE, mediaMessage, IndividualSendEvents.EMPTY, message.isUrgent(), originalEditedMessage.getDateSent());
|
||||
SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false);
|
||||
|
||||
return result.getSuccess().isUnidentified();
|
||||
} else if (Util.equals(SignalStore.account().getAci(), address.getServiceId())) {
|
||||
Optional<UnidentifiedAccessPair> syncAccess = UnidentifiedAccessUtil.getAccessForSync(context);
|
||||
SendMessageResult result = messageSender.sendSyncMessage(mediaMessage);
|
||||
SignalDatabase.messageLog().insertIfPossible(messageRecipient.getId(), message.getSentTimeMillis(), result, ContentHint.RESENDABLE, new MessageId(messageId), false);
|
||||
|
||||
@@ -52,6 +52,7 @@ import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||
@@ -177,9 +178,10 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
{
|
||||
SignalLocalMetrics.GroupMessageSend.onJobStarted(messageId);
|
||||
|
||||
MessageTable database = SignalDatabase.messages();
|
||||
OutgoingMessage message = database.getOutgoingMessage(messageId);
|
||||
long threadId = database.getMessageRecord(messageId).getThreadId();
|
||||
MessageTable database = SignalDatabase.messages();
|
||||
OutgoingMessage message = database.getOutgoingMessage(messageId);
|
||||
long threadId = database.getMessageRecord(messageId).getThreadId();
|
||||
MessageRecord originalEditedMessage = message.getMessageToEdit() > 0 ? SignalDatabase.messages().getMessageRecordOrNull(message.getMessageToEdit()) : null;
|
||||
Set<NetworkFailure> existingNetworkFailures = new HashSet<>(message.getNetworkFailures());
|
||||
Set<IdentityKeyMismatch> existingIdentityMismatches = new HashSet<>(message.getIdentityKeyMismatches());
|
||||
|
||||
@@ -227,7 +229,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
skipped = result.skipped;
|
||||
}
|
||||
|
||||
List<SendMessageResult> results = deliver(message, groupRecipient, target);
|
||||
List<SendMessageResult> results = deliver(message, originalEditedMessage, groupRecipient, target);
|
||||
processGroupMessageResults(context, messageId, threadId, groupRecipient, message, results, target, skipped, existingNetworkFailures, existingIdentityMismatches);
|
||||
Log.i(TAG, JobLogger.format(this, "Finished send."));
|
||||
|
||||
@@ -251,7 +253,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
SignalDatabase.messages().markAsSentFailed(messageId);
|
||||
}
|
||||
|
||||
private List<SendMessageResult> deliver(OutgoingMessage message, @NonNull Recipient groupRecipient, @NonNull List<Recipient> destinations)
|
||||
private List<SendMessageResult> deliver(OutgoingMessage message, @Nullable MessageRecord originalEditedMessage, @NonNull Recipient groupRecipient, @NonNull List<Recipient> destinations)
|
||||
throws IOException, UntrustedIdentityException, UndeliverableMessageException
|
||||
{
|
||||
try {
|
||||
@@ -317,7 +319,7 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
.withExpiration(groupRecipient.getExpiresInSeconds())
|
||||
.asGroupMessage(group)
|
||||
.build();
|
||||
return GroupSendUtil.sendResendableDataMessage(context, groupRecipient.requireGroupId().requireV2(), null, destinations, isRecipientUpdate, ContentHint.IMPLICIT, new MessageId(messageId), groupDataMessage, message.isUrgent(), false);
|
||||
return GroupSendUtil.sendResendableDataMessage(context, groupRecipient.requireGroupId().requireV2(), null, destinations, isRecipientUpdate, ContentHint.IMPLICIT, new MessageId(messageId), groupDataMessage, message.isUrgent(), false, null);
|
||||
} else {
|
||||
throw new UndeliverableMessageException("Messages can no longer be sent to V1 groups!");
|
||||
}
|
||||
@@ -369,6 +371,10 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
groupMessageBuilder.withQuote(getQuoteFor(message).orElse(null));
|
||||
}
|
||||
|
||||
SignalServiceDataMessage groupMessage = groupMessageBuilder.build();
|
||||
SignalServiceEditMessage editMessage = originalEditedMessage != null ? new SignalServiceEditMessage(originalEditedMessage.getDateSent(), groupMessage)
|
||||
: null;
|
||||
|
||||
Log.i(TAG, JobLogger.format(this, "Beginning message send."));
|
||||
|
||||
return GroupSendUtil.sendResendableDataMessage(context,
|
||||
@@ -378,9 +384,10 @@ public final class PushGroupSendJob extends PushSendJob {
|
||||
isRecipientUpdate,
|
||||
ContentHint.RESENDABLE,
|
||||
new MessageId(messageId),
|
||||
groupMessageBuilder.build(),
|
||||
groupMessage,
|
||||
message.isUrgent(),
|
||||
message.getStoryType().isStory() || message.getParentStoryId() != null);
|
||||
message.getStoryType().isStory() || message.getParentStoryId() != null,
|
||||
editMessage);
|
||||
}
|
||||
} catch (ServerRejectedException e) {
|
||||
throw new UndeliverableMessageException(e);
|
||||
|
||||
@@ -67,6 +67,7 @@ import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemo
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServicePreview;
|
||||
import org.whispersystems.signalservice.api.messages.shared.SharedContact;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.NonSuccessfulResponseCodeException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
@@ -313,6 +314,9 @@ public abstract class PushSendJob extends SendJob {
|
||||
|
||||
protected Optional<SignalServiceDataMessage.Quote> getQuoteFor(OutgoingMessage message) throws IOException {
|
||||
if (message.getOutgoingQuote() == null) return Optional.empty();
|
||||
if (message.isMessageEdit()) {
|
||||
return Optional.of(new SignalServiceDataMessage.Quote(0, ServiceId.UNKNOWN, "", null, null, SignalServiceDataMessage.Quote.Type.NORMAL, null));
|
||||
}
|
||||
|
||||
long quoteId = message.getOutgoingQuote().getId();
|
||||
String quoteBody = message.getOutgoingQuote().getText();
|
||||
|
||||
@@ -236,7 +236,8 @@ public class ReactionSendJob extends BaseJob {
|
||||
messageId,
|
||||
dataMessage,
|
||||
true,
|
||||
false);
|
||||
false,
|
||||
null);
|
||||
|
||||
if (includesSelf) {
|
||||
results.add(ApplicationDependencies.getSignalServiceMessageSender().sendSyncMessage(dataMessage));
|
||||
|
||||
@@ -217,7 +217,8 @@ public class RemoteDeleteSendJob extends BaseJob {
|
||||
new MessageId(messageId),
|
||||
dataMessage,
|
||||
true,
|
||||
isForStory);
|
||||
isForStory,
|
||||
null);
|
||||
|
||||
return GroupSendJobHelper.getCompletedSends(destinations, results);
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ConversationItemFooter;
|
||||
import org.thoughtcrime.securesms.components.FullScreenDialogFragment;
|
||||
import org.thoughtcrime.securesms.components.emoji.EmojiTextView;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode;
|
||||
import org.thoughtcrime.securesms.conversation.colors.ColorizerView;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -138,7 +139,7 @@ public class LongMessageFragment extends FullScreenDialogFragment {
|
||||
} else {
|
||||
text.setMentionBackgroundTint(ContextCompat.getColor(requireContext(), R.color.transparent_black_40));
|
||||
}
|
||||
footer.setMessageRecord(message.get().getMessageRecord(), Locale.getDefault());
|
||||
footer.setMessageRecord(message.get().getMessageRecord(), Locale.getDefault(), ConversationItemDisplayMode.STANDARD);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ import org.thoughtcrime.securesms.util.DateUtils
|
||||
import org.thoughtcrime.securesms.util.Debouncer
|
||||
import org.thoughtcrime.securesms.util.FullscreenHelper
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil
|
||||
import org.thoughtcrime.securesms.util.SaveAttachmentTask
|
||||
import org.thoughtcrime.securesms.util.SpanUtil
|
||||
import org.thoughtcrime.securesms.util.StorageUtil
|
||||
@@ -599,7 +599,7 @@ class MediaPreviewV2Fragment : LoggingFragment(R.layout.fragment_media_preview_v
|
||||
fun canRemotelyDelete(attachment: DatabaseAttachment): Boolean {
|
||||
val mmsId = attachment.mmsId
|
||||
val attachmentCount = SignalDatabase.attachments.getAttachmentsForMessage(mmsId).size
|
||||
return attachmentCount <= 1 && RemoteDeleteUtil.isValidSend(listOf(SignalDatabase.messages.getMessageRecord(mmsId)), System.currentTimeMillis())
|
||||
return attachmentCount <= 1 && MessageConstraintsUtil.isValidRemoteDeleteSend(listOf(SignalDatabase.messages.getMessageRecord(mmsId)), System.currentTimeMillis())
|
||||
}
|
||||
|
||||
private fun editMediaItem(currentItem: MediaTable.MediaRecord) {
|
||||
|
||||
@@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.databinding.MessageDetailsViewEditHistoryBinding;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
|
||||
final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.MessageDetailsViewState<?>, RecyclerView.ViewHolder> {
|
||||
@@ -40,6 +41,8 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
|
||||
return new RecipientHeaderViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_recipient_header, parent, false));
|
||||
case MessageDetailsViewState.RECIPIENT:
|
||||
return new RecipientViewHolder(LayoutInflater.from(parent.getContext()).inflate(R.layout.message_details_recipient, parent, false), callbacks);
|
||||
case MessageDetailsViewState.EDIT_HISTORY:
|
||||
return new ViewEditHistoryViewHolder(MessageDetailsViewEditHistoryBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false), callbacks);
|
||||
default:
|
||||
throw new AssertionError("unknown view type");
|
||||
}
|
||||
@@ -53,6 +56,8 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
|
||||
((RecipientHeaderViewHolder) holder).bind((RecipientHeader) getItem(position).data);
|
||||
} else if (holder instanceof RecipientViewHolder) {
|
||||
((RecipientViewHolder) holder).bind((RecipientDeliveryStatus) getItem(position).data);
|
||||
} else if (holder instanceof ViewEditHistoryViewHolder) {
|
||||
((ViewEditHistoryViewHolder) holder).bind((MessageRecord) getItem(position).data);
|
||||
} else {
|
||||
throw new AssertionError("unknown view holder");
|
||||
}
|
||||
@@ -72,6 +77,7 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
|
||||
if (oldData.getClass() == newData.getClass() && oldItem.itemType == newItem.itemType) {
|
||||
switch (oldItem.itemType) {
|
||||
case MessageDetailsViewState.MESSAGE_HEADER:
|
||||
case MessageDetailsViewState.EDIT_HISTORY:
|
||||
return true;
|
||||
case MessageDetailsViewState.RECIPIENT_HEADER:
|
||||
return oldData == newData;
|
||||
@@ -94,6 +100,7 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
|
||||
case MessageDetailsViewState.MESSAGE_HEADER:
|
||||
return false;
|
||||
case MessageDetailsViewState.RECIPIENT_HEADER:
|
||||
case MessageDetailsViewState.EDIT_HISTORY:
|
||||
return true;
|
||||
case MessageDetailsViewState.RECIPIENT:
|
||||
return ((RecipientDeliveryStatus) oldData).getDeliveryStatus() == ((RecipientDeliveryStatus) newData).getDeliveryStatus();
|
||||
@@ -108,6 +115,7 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
|
||||
public static final int MESSAGE_HEADER = 0;
|
||||
public static final int RECIPIENT_HEADER = 1;
|
||||
public static final int RECIPIENT = 2;
|
||||
public static final int EDIT_HISTORY = 3;
|
||||
|
||||
private final T data;
|
||||
private int itemType;
|
||||
@@ -120,5 +128,6 @@ final class MessageDetailsAdapter extends ListAdapter<MessageDetailsAdapter.Mess
|
||||
|
||||
interface Callbacks {
|
||||
void onErrorClicked(@NonNull MessageRecord messageRecord);
|
||||
void onViewEditHistoryClicked(MessageRecord record);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.FullScreenDialogFragment;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.colors.RecyclerViewColorizer;
|
||||
import org.thoughtcrime.securesms.conversation.ui.edit.EditMessageHistoryDialog;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackController;
|
||||
import org.thoughtcrime.securesms.giph.mp4.GiphyMp4ProjectionPlayerHolder;
|
||||
@@ -26,12 +27,13 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.safety.SafetyNumberBottomSheet;
|
||||
import org.thoughtcrime.securesms.util.Material3OnScrollHelper;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
public final class MessageDetailsFragment extends FullScreenDialogFragment {
|
||||
public final class MessageDetailsFragment extends FullScreenDialogFragment implements MessageDetailsAdapter.Callbacks {
|
||||
|
||||
private static final String MESSAGE_ID_EXTRA = "message_id";
|
||||
private static final String RECIPIENT_EXTRA = "recipient_id";
|
||||
@@ -89,7 +91,7 @@ public final class MessageDetailsFragment extends FullScreenDialogFragment {
|
||||
View toolbarShadow = view.findViewById(R.id.toolbar_shadow);
|
||||
|
||||
colorizer = new Colorizer();
|
||||
adapter = new MessageDetailsAdapter(getViewLifecycleOwner(), glideRequests, colorizer, this::onErrorClicked);
|
||||
adapter = new MessageDetailsAdapter(getViewLifecycleOwner(), glideRequests, colorizer, this);
|
||||
recyclerViewColorizer = new RecyclerViewColorizer(list);
|
||||
|
||||
list.setAdapter(adapter);
|
||||
@@ -127,6 +129,10 @@ public final class MessageDetailsFragment extends FullScreenDialogFragment {
|
||||
|
||||
list.add(new MessageDetailsViewState<>(details.getConversationMessage(), MessageDetailsViewState.MESSAGE_HEADER));
|
||||
|
||||
if (MessageRecordUtil.isEditMessage(details.getConversationMessage().getMessageRecord())) {
|
||||
list.add(new MessageDetailsViewState<>(details.getConversationMessage().getMessageRecord(), MessageDetailsViewState.EDIT_HISTORY));
|
||||
}
|
||||
|
||||
if (details.getConversationMessage().getMessageRecord().isOutgoing()) {
|
||||
addRecipients(list, RecipientHeader.NOT_SENT, details.getNotSent());
|
||||
addRecipients(list, RecipientHeader.VIEWED, details.getViewed());
|
||||
@@ -154,12 +160,22 @@ public final class MessageDetailsFragment extends FullScreenDialogFragment {
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onErrorClicked(@NonNull MessageRecord messageRecord) {
|
||||
@Override
|
||||
public void onErrorClicked(@NonNull MessageRecord messageRecord) {
|
||||
SafetyNumberBottomSheet
|
||||
.forMessageRecord(requireContext(), messageRecord)
|
||||
.show(getChildFragmentManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewEditHistoryClicked(MessageRecord record) {
|
||||
if (record.isOutgoing()) {
|
||||
EditMessageHistoryDialog.show(requireParentFragment().getChildFragmentManager(), record.getToRecipient().getId(), record.getId());
|
||||
} else {
|
||||
EditMessageHistoryDialog.show(requireParentFragment().getChildFragmentManager(), record.getFromRecipient().getId(), record.getId());
|
||||
}
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onMessageDetailsFragmentDismissed();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package org.thoughtcrime.securesms.messagedetails;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.databinding.MessageDetailsViewEditHistoryBinding;
|
||||
|
||||
public class ViewEditHistoryViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
private final org.thoughtcrime.securesms.databinding.MessageDetailsViewEditHistoryBinding binding;
|
||||
private final MessageDetailsAdapter.Callbacks callbacks;
|
||||
|
||||
public ViewEditHistoryViewHolder(@NonNull MessageDetailsViewEditHistoryBinding binding, @NonNull MessageDetailsAdapter.Callbacks callbacks) {
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
this.callbacks = callbacks;
|
||||
}
|
||||
|
||||
public void bind(@NonNull MessageRecord record) {
|
||||
binding.viewEditHistory.setOnClickListener(v -> callbacks.onViewEditHistoryClicked(record));
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@ import org.thoughtcrime.securesms.util.Base64
|
||||
import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry
|
||||
import org.thoughtcrime.securesms.util.LinkUtil
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences
|
||||
import org.thoughtcrime.securesms.util.isStory
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
@@ -317,7 +317,7 @@ object DataMessageProcessor {
|
||||
* Inserts an expiration update if the message timer doesn't match the thread timer.
|
||||
*/
|
||||
@Throws(StorageFailedException::class)
|
||||
private fun handlePossibleExpirationUpdate(
|
||||
fun handlePossibleExpirationUpdate(
|
||||
envelope: Envelope,
|
||||
metadata: EnvelopeMetadata,
|
||||
senderRecipientId: RecipientId,
|
||||
@@ -482,7 +482,7 @@ object DataMessageProcessor {
|
||||
return null
|
||||
}
|
||||
|
||||
val targetMessageId = MessageId(targetMessage.id)
|
||||
val targetMessageId = (targetMessage as? MediaMmsMessageRecord)?.latestRevisionId ?: MessageId(targetMessage.id)
|
||||
|
||||
if (isRemove) {
|
||||
SignalDatabase.reactions.deleteReaction(targetMessageId, senderRecipientId)
|
||||
@@ -502,7 +502,7 @@ object DataMessageProcessor {
|
||||
val targetSentTimestamp: Long = message.delete.targetSentTimestamp
|
||||
val targetMessage: MessageRecord? = SignalDatabase.messages.getMessageFor(targetSentTimestamp, senderRecipientId)
|
||||
|
||||
return if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, senderRecipientId, envelope.serverTimestamp)) {
|
||||
return if (targetMessage != null && MessageConstraintsUtil.isValidRemoteDeleteReceive(targetMessage, senderRecipientId, envelope.serverTimestamp)) {
|
||||
SignalDatabase.messages.markAsRemoteDelete(targetMessage.id)
|
||||
if (targetMessage.isStory()) {
|
||||
SignalDatabase.messages.deleteRemotelyDeletedStory(targetMessage.id)
|
||||
@@ -944,7 +944,7 @@ object DataMessageProcessor {
|
||||
GroupCallPeekJob.enqueue(groupRecipientId)
|
||||
}
|
||||
|
||||
private fun notifyTypingStoppedFromIncomingMessage(context: Context, senderRecipient: Recipient, threadRecipientId: RecipientId, device: Int) {
|
||||
fun notifyTypingStoppedFromIncomingMessage(context: Context, senderRecipient: Recipient, threadRecipientId: RecipientId, device: Int) {
|
||||
val threadId = SignalDatabase.threads.getThreadIdIfExistsFor(threadRecipientId)
|
||||
|
||||
if (threadId > 0 && TextSecurePreferences.isTypingIndicatorsEnabled(context)) {
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
package org.thoughtcrime.securesms.messages
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.concurrent.SignalExecutors
|
||||
import org.signal.core.util.orNull
|
||||
import org.thoughtcrime.securesms.database.MessageTable.InsertResult
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord
|
||||
import org.thoughtcrime.securesms.database.model.MessageId
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.database.model.toBodyRangeList
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.jobs.PushProcessEarlyMessagesJob
|
||||
import org.thoughtcrime.securesms.jobs.SendDeliveryReceiptJob
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.log
|
||||
import org.thoughtcrime.securesms.messages.MessageContentProcessorV2.Companion.warn
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.groupId
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.isMediaMessage
|
||||
import org.thoughtcrime.securesms.messages.SignalServiceProtoUtil.toPointers
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel
|
||||
import org.thoughtcrime.securesms.notifications.v2.ConversationId.Companion.forConversation
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.IncomingEncryptedMessage
|
||||
import org.thoughtcrime.securesms.sms.IncomingTextMessage
|
||||
import org.thoughtcrime.securesms.util.EarlyMessageCacheEntry
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil
|
||||
import org.thoughtcrime.securesms.util.hasAudio
|
||||
import org.thoughtcrime.securesms.util.hasSharedContact
|
||||
import org.whispersystems.signalservice.api.crypto.EnvelopeMetadata
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos.DataMessage
|
||||
import java.util.Optional
|
||||
|
||||
object EditMessageProcessor {
|
||||
fun process(
|
||||
context: Context,
|
||||
senderRecipient: Recipient,
|
||||
threadRecipient: Recipient,
|
||||
envelope: SignalServiceProtos.Envelope,
|
||||
content: SignalServiceProtos.Content,
|
||||
metadata: EnvelopeMetadata,
|
||||
earlyMessageCacheEntry: EarlyMessageCacheEntry?
|
||||
) {
|
||||
val editMessage = content.editMessage
|
||||
|
||||
log(envelope.timestamp, "[handleEditMessage] Edit message for " + editMessage.targetSentTimestamp)
|
||||
|
||||
var targetMessage: MediaMmsMessageRecord? = SignalDatabase.messages.getMessageFor(editMessage.targetSentTimestamp, senderRecipient.id) as MediaMmsMessageRecord
|
||||
val targetThreadRecipient: Recipient? = if (targetMessage != null) SignalDatabase.threads.getRecipientForThreadId(targetMessage.threadId) else null
|
||||
|
||||
if (targetMessage == null || targetThreadRecipient == null) {
|
||||
warn(envelope.timestamp, "[handleEditMessage] Could not find matching message! timestamp: ${editMessage.targetSentTimestamp} author: ${senderRecipient.id}")
|
||||
|
||||
if (earlyMessageCacheEntry != null) {
|
||||
ApplicationDependencies.getEarlyMessageCache().store(senderRecipient.id, editMessage.targetSentTimestamp, earlyMessageCacheEntry)
|
||||
PushProcessEarlyMessagesJob.enqueue()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
val message = editMessage.dataMessage
|
||||
val isMediaMessage = message.isMediaMessage
|
||||
val groupId: GroupId.V2? = message.groupV2.groupId
|
||||
|
||||
val originalMessage = targetMessage.originalMessageId?.let { SignalDatabase.messages.getMessageRecord(it.id) } ?: targetMessage
|
||||
val validTiming = MessageConstraintsUtil.isValidEditMessageReceive(originalMessage, senderRecipient, envelope.serverTimestamp)
|
||||
val validAuthor = senderRecipient.id == originalMessage.fromRecipient.id
|
||||
val validGroup = groupId == targetThreadRecipient.groupId.orNull()
|
||||
val validTarget = !originalMessage.isViewOnce && !originalMessage.hasAudio() && !originalMessage.hasSharedContact()
|
||||
|
||||
if (!validTiming || !validAuthor || !validGroup || !validTarget) {
|
||||
warn(envelope.timestamp, "[handleEditMessage] Invalid message edit! editTime: ${envelope.serverTimestamp}, targetTime: ${originalMessage.serverTimestamp}, editAuthor: ${senderRecipient.id}, targetAuthor: ${originalMessage.fromRecipient.id}, editThread: ${threadRecipient.id}, targetThread: ${targetThreadRecipient.id}, validity: (timing: $validTiming, author: $validAuthor, group: $validGroup, target: $validTarget)")
|
||||
return
|
||||
}
|
||||
|
||||
if (groupId != null && MessageContentProcessorV2.handleGv2PreProcessing(context, envelope.timestamp, content, metadata, groupId, message.groupV2, senderRecipient)) {
|
||||
warn(envelope.timestamp, "[handleEditMessage] Group processor indicated we should ignore this.")
|
||||
return
|
||||
}
|
||||
|
||||
DataMessageProcessor.notifyTypingStoppedFromIncomingMessage(context, senderRecipient, threadRecipient.id, metadata.sourceDeviceId)
|
||||
|
||||
targetMessage = targetMessage.withAttachments(context, SignalDatabase.attachments.getAttachmentsForMessage(targetMessage.id))
|
||||
|
||||
val insertResult: InsertResult? = if (isMediaMessage || targetMessage.quote != null || targetMessage.slideDeck.slides.isNotEmpty()) {
|
||||
handleEditMediaMessage(senderRecipient.id, groupId, envelope, metadata, message, targetMessage)
|
||||
} else {
|
||||
handleEditTextMessage(senderRecipient.id, groupId, envelope, metadata, message, targetMessage)
|
||||
}
|
||||
|
||||
if (insertResult != null) {
|
||||
SignalExecutors.BOUNDED.execute {
|
||||
ApplicationDependencies.getJobManager().add(SendDeliveryReceiptJob(senderRecipient.id, message.timestamp, MessageId(insertResult.messageId)))
|
||||
}
|
||||
|
||||
if (targetMessage.expireStarted > 0) {
|
||||
ApplicationDependencies.getExpiringMessageManager()
|
||||
.scheduleDeletion(
|
||||
insertResult.messageId,
|
||||
true,
|
||||
targetMessage.expireStarted,
|
||||
targetMessage.expiresIn
|
||||
)
|
||||
}
|
||||
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context, forConversation(insertResult.threadId))
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEditMediaMessage(
|
||||
senderRecipientId: RecipientId,
|
||||
groupId: GroupId.V2?,
|
||||
envelope: SignalServiceProtos.Envelope,
|
||||
metadata: EnvelopeMetadata,
|
||||
message: DataMessage,
|
||||
targetMessage: MediaMmsMessageRecord
|
||||
): InsertResult? {
|
||||
val messageRanges: BodyRangeList? = message.bodyRangesList.filter { it.hasStyle() }.toList().toBodyRangeList()
|
||||
val targetQuote = targetMessage.quote
|
||||
val quote: QuoteModel? = if (targetQuote != null && message.hasQuote()) {
|
||||
QuoteModel(
|
||||
targetQuote.id,
|
||||
targetQuote.author,
|
||||
targetQuote.displayText.toString(),
|
||||
targetQuote.isOriginalMissing,
|
||||
emptyList(),
|
||||
null,
|
||||
targetQuote.quoteType,
|
||||
null
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
val attachments = message.attachmentsList.toPointers()
|
||||
attachments.filter {
|
||||
MediaUtil.SlideType.LONG_TEXT == MediaUtil.getSlideTypeFromContentType(it.contentType)
|
||||
}
|
||||
val mediaMessage = IncomingMediaMessage(
|
||||
from = senderRecipientId,
|
||||
sentTimeMillis = message.timestamp,
|
||||
serverTimeMillis = envelope.serverTimestamp,
|
||||
receivedTimeMillis = targetMessage.receiptTimestamp,
|
||||
expiresIn = targetMessage.expiresIn,
|
||||
isViewOnce = message.isViewOnce,
|
||||
isUnidentified = metadata.sealedSender,
|
||||
body = message.body,
|
||||
groupId = groupId,
|
||||
attachments = attachments,
|
||||
quote = quote,
|
||||
sharedContacts = emptyList(),
|
||||
linkPreviews = DataMessageProcessor.getLinkPreviews(message.previewList, message.body ?: "", false),
|
||||
mentions = DataMessageProcessor.getMentions(message.bodyRangesList),
|
||||
serverGuid = envelope.serverGuid,
|
||||
messageRanges = messageRanges,
|
||||
isPushMessage = true
|
||||
)
|
||||
|
||||
return SignalDatabase.messages.insertEditMessageInbox(-1, mediaMessage, targetMessage).orNull()
|
||||
}
|
||||
|
||||
private fun handleEditTextMessage(
|
||||
senderRecipientId: RecipientId,
|
||||
groupId: GroupId.V2?,
|
||||
envelope: SignalServiceProtos.Envelope,
|
||||
metadata: EnvelopeMetadata,
|
||||
message: DataMessage,
|
||||
targetMessage: MediaMmsMessageRecord
|
||||
): InsertResult? {
|
||||
var textMessage = IncomingTextMessage(
|
||||
senderRecipientId,
|
||||
metadata.sourceDeviceId,
|
||||
envelope.timestamp,
|
||||
envelope.timestamp,
|
||||
targetMessage.receiptTimestamp,
|
||||
message.body,
|
||||
Optional.ofNullable(groupId),
|
||||
targetMessage.expiresIn,
|
||||
metadata.sealedSender,
|
||||
envelope.serverGuid
|
||||
)
|
||||
|
||||
textMessage = IncomingEncryptedMessage(textMessage, message.body)
|
||||
|
||||
return SignalDatabase.messages.insertEditMessageInbox(textMessage, targetMessage).orNull()
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@ import org.whispersystems.signalservice.api.crypto.UnidentifiedAccessPair;
|
||||
import org.whispersystems.signalservice.api.crypto.UntrustedIdentityException;
|
||||
import org.whispersystems.signalservice.api.messages.SendMessageResult;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceEditMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessage;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceStoryMessageRecipient;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceTypingMessage;
|
||||
@@ -94,29 +95,15 @@ public final class GroupSendUtil {
|
||||
@NonNull MessageId messageId,
|
||||
@NonNull SignalServiceDataMessage message,
|
||||
boolean urgent,
|
||||
boolean isForStory)
|
||||
boolean isForStory,
|
||||
@Nullable SignalServiceEditMessage editMessage)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
Preconditions.checkArgument(groupId == null || distributionListId == null, "Cannot supply both a groupId and a distributionListId!");
|
||||
|
||||
DistributionId distributionId = groupId != null ? getDistributionId(groupId) : getDistributionId(distributionListId);
|
||||
|
||||
return sendMessage(context, groupId, distributionId, messageId, allTargets, isRecipientUpdate, isForStory, DataSendOperation.resendable(message, contentHint, messageId, urgent, isForStory), null);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public static List<SendMessageResult> sendResendableStoryRelatedMessage(@NonNull Context context,
|
||||
@Nullable GroupId.V2 groupId,
|
||||
@NonNull DistributionListId distributionListId,
|
||||
@NonNull List<Recipient> allTargets,
|
||||
boolean isRecipientUpdate,
|
||||
ContentHint contentHint,
|
||||
@NonNull MessageId messageId,
|
||||
@NonNull SignalServiceDataMessage message,
|
||||
boolean urgent)
|
||||
throws IOException, UntrustedIdentityException
|
||||
{
|
||||
return sendMessage(context, groupId, getDistributionId(distributionListId), messageId, allTargets, isRecipientUpdate, true, DataSendOperation.resendable(message, contentHint, messageId, urgent, true), null);
|
||||
return sendMessage(context, groupId, distributionId, messageId, allTargets, isRecipientUpdate, isForStory, DataSendOperation.resendable(message, contentHint, messageId, urgent, isForStory, editMessage), null);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -480,22 +467,24 @@ public final class GroupSendUtil {
|
||||
private final boolean resendable;
|
||||
private final boolean urgent;
|
||||
private final boolean isForStory;
|
||||
private final SignalServiceEditMessage editMessage;
|
||||
|
||||
public static DataSendOperation resendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, @NonNull MessageId relatedMessageId, boolean urgent, boolean isForStory) {
|
||||
return new DataSendOperation(message, contentHint, true, relatedMessageId, urgent, isForStory);
|
||||
public static DataSendOperation resendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, @NonNull MessageId relatedMessageId, boolean urgent, boolean isForStory, @Nullable SignalServiceEditMessage editMessage) {
|
||||
return new DataSendOperation(editMessage != null ? editMessage.getDataMessage() : message, contentHint, true, relatedMessageId, urgent, isForStory, editMessage);
|
||||
}
|
||||
|
||||
public static DataSendOperation unresendable(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, boolean urgent) {
|
||||
return new DataSendOperation(message, contentHint, false, null, urgent, false);
|
||||
return new DataSendOperation(message, contentHint, false, null, urgent, false, null);
|
||||
}
|
||||
|
||||
private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, boolean resendable, @Nullable MessageId relatedMessageId, boolean urgent, boolean isForStory) {
|
||||
private DataSendOperation(@NonNull SignalServiceDataMessage message, @NonNull ContentHint contentHint, boolean resendable, @Nullable MessageId relatedMessageId, boolean urgent, boolean isForStory, @Nullable SignalServiceEditMessage editMessage) {
|
||||
this.message = message;
|
||||
this.contentHint = contentHint;
|
||||
this.resendable = resendable;
|
||||
this.relatedMessageId = relatedMessageId;
|
||||
this.urgent = urgent;
|
||||
this.isForStory = isForStory;
|
||||
this.editMessage = editMessage;
|
||||
|
||||
if (resendable && relatedMessageId == null) {
|
||||
throw new IllegalArgumentException("If a message is resendable, it must have a related message ID!");
|
||||
@@ -512,7 +501,7 @@ public final class GroupSendUtil {
|
||||
throws NoSessionException, UntrustedIdentityException, InvalidKeyException, IOException, InvalidRegistrationIdException
|
||||
{
|
||||
SenderKeyGroupEvents listener = relatedMessageId != null ? new SenderKeyMetricEventListener(relatedMessageId.getId()) : SenderKeyGroupEvents.EMPTY;
|
||||
return messageSender.sendGroupDataMessage(distributionId, targets, access, isRecipientUpdate, contentHint, message, listener, urgent, isForStory, partialListener);
|
||||
return messageSender.sendGroupDataMessage(distributionId, targets, access, isRecipientUpdate, contentHint, message, listener, urgent, isForStory, editMessage, partialListener);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -527,8 +516,14 @@ public final class GroupSendUtil {
|
||||
{
|
||||
// PniSignatures are only needed for 1:1 messages, but some message jobs use the GroupSendUtil methods to send 1:1
|
||||
if (targets.size() == 1 && relatedMessageId == null) {
|
||||
Recipient targetRecipient = targetRecipients.get(0);
|
||||
SendMessageResult result = messageSender.sendDataMessage(targets.get(0), access.get(0), contentHint, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, urgent, targetRecipient.needsPniSignature());
|
||||
Recipient targetRecipient = targetRecipients.get(0);
|
||||
SendMessageResult result;
|
||||
|
||||
if (editMessage != null) {
|
||||
result = messageSender.sendEditMessage(targets.get(0), access.get(0), contentHint, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, urgent, editMessage.getTargetSentTimestamp());
|
||||
} else {
|
||||
result = messageSender.sendDataMessage(targets.get(0), access.get(0), contentHint, message, SignalServiceMessageSender.IndividualSendEvents.EMPTY, urgent, targetRecipient.needsPniSignature());
|
||||
}
|
||||
|
||||
if (targetRecipient.needsPniSignature()) {
|
||||
SignalDatabase.pendingPniSignatureMessages().insertIfNecessary(targetRecipients.get(0).getId(), getSentTimestamp(), result);
|
||||
|
||||
@@ -134,7 +134,7 @@ import org.thoughtcrime.securesms.util.IdentityUtil;
|
||||
import org.thoughtcrime.securesms.util.LinkUtil;
|
||||
import org.thoughtcrime.securesms.util.MediaUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.RemoteDeleteUtil;
|
||||
import org.thoughtcrime.securesms.util.MessageConstraintsUtil;
|
||||
import org.thoughtcrime.securesms.util.TextSecurePreferences;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
@@ -1064,7 +1064,7 @@ public class MessageContentProcessor {
|
||||
|
||||
MessageRecord targetMessage = SignalDatabase.messages().getMessageFor(delete.getTargetSentTimestamp(), senderRecipient.getId());
|
||||
|
||||
if (targetMessage != null && RemoteDeleteUtil.isValidReceive(targetMessage, senderRecipient, content.getServerReceivedTimestamp())) {
|
||||
if (targetMessage != null && MessageConstraintsUtil.isValidRemoteDeleteReceive(targetMessage, senderRecipient.getId(), content.getServerReceivedTimestamp())) {
|
||||
MessageTable db = targetMessage.isMms() ? SignalDatabase.messages() : SignalDatabase.messages();
|
||||
db.markAsRemoteDelete(targetMessage.getId());
|
||||
if (MessageRecordUtil.isStory(targetMessage)) {
|
||||
@@ -2220,7 +2220,8 @@ public class MessageContentProcessor {
|
||||
null,
|
||||
true,
|
||||
bodyRanges,
|
||||
-1);
|
||||
-1,
|
||||
0);
|
||||
|
||||
if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||
handleSynchronizeSentExpirationUpdate(message);
|
||||
@@ -2342,7 +2343,8 @@ public class MessageContentProcessor {
|
||||
null,
|
||||
true,
|
||||
bodyRanges,
|
||||
-1);
|
||||
-1,
|
||||
0);
|
||||
|
||||
MessageTable messageTable = SignalDatabase.messages();
|
||||
long threadId = SignalDatabase.threads().getOrCreateThreadIdFor(recipient);
|
||||
@@ -2441,7 +2443,8 @@ public class MessageContentProcessor {
|
||||
giftBadge.orElse(null),
|
||||
true,
|
||||
bodyRanges,
|
||||
-1);
|
||||
-1,
|
||||
0);
|
||||
|
||||
if (recipient.getExpiresInSeconds() != message.getDataMessage().get().getExpiresInSeconds()) {
|
||||
handleSynchronizeSentExpirationUpdate(message);
|
||||
|
||||
@@ -111,6 +111,8 @@ open class MessageContentProcessorV2(private val context: Context) {
|
||||
getGroupRecipient(content.storyMessage.group, sender)
|
||||
} else if (content.dataMessage.hasGroupContext) {
|
||||
getGroupRecipient(content.dataMessage.groupV2, sender)
|
||||
} else if (content.editMessage.dataMessage.hasGroupContext) {
|
||||
getGroupRecipient(content.editMessage.dataMessage.groupV2, sender)
|
||||
} else {
|
||||
sender
|
||||
}
|
||||
@@ -379,6 +381,21 @@ open class MessageContentProcessorV2(private val context: Context) {
|
||||
content.hasDecryptionErrorMessage() -> {
|
||||
handleRetryReceipt(envelope, metadata, content.decryptionErrorMessage!!.toDecryptionErrorMessage(metadata), senderRecipient)
|
||||
}
|
||||
content.hasEditMessage() -> {
|
||||
if (FeatureFlags.editMessageReceiving()) {
|
||||
EditMessageProcessor.process(
|
||||
context,
|
||||
senderRecipient,
|
||||
threadRecipient,
|
||||
envelope,
|
||||
content,
|
||||
metadata,
|
||||
if (processingEarlyContent) null else EarlyMessageCacheEntry(envelope, content, metadata, serverDeliveredTimestamp)
|
||||
)
|
||||
} else {
|
||||
warn(envelope.timestamp, "Got message edit, but processing is disabled")
|
||||
}
|
||||
}
|
||||
content.hasSenderKeyDistributionMessage() || content.hasPniSignatureMessage() -> {
|
||||
// Already handled, here in order to prevent unrecognized message log
|
||||
}
|
||||
|
||||
@@ -125,9 +125,10 @@ public class ApplicationMigrations {
|
||||
static final int DECRYPTIONS_DRAINED = 80;
|
||||
static final int REBUILD_MESSAGE_FTS_INDEX_3 = 81;
|
||||
static final int TO_FROM_RECIPIENTS = 82;
|
||||
static final int REBUILD_MESSAGE_FTS_INDEX_4 = 83;
|
||||
}
|
||||
|
||||
public static final int CURRENT_VERSION = 82;
|
||||
public static final int CURRENT_VERSION = 83;
|
||||
|
||||
/**
|
||||
* This *must* be called after the {@link JobManager} has been instantiated, but *before* the call
|
||||
@@ -557,6 +558,10 @@ public class ApplicationMigrations {
|
||||
jobs.put(Version.TO_FROM_RECIPIENTS, new DatabaseMigrationJob());
|
||||
}
|
||||
|
||||
if (lastSeenVersion < Version.REBUILD_MESSAGE_FTS_INDEX_4) {
|
||||
jobs.put(Version.REBUILD_MESSAGE_FTS_INDEX_4, new RebuildMessageSearchIndexMigrationJob());
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,11 +50,13 @@ data class OutgoingMessage(
|
||||
val isEndSession: Boolean = false,
|
||||
val isIdentityVerified: Boolean = false,
|
||||
val isIdentityDefault: Boolean = false,
|
||||
val scheduledDate: Long = -1
|
||||
val scheduledDate: Long = -1,
|
||||
val messageToEdit: Long = 0
|
||||
) {
|
||||
|
||||
val isV2Group: Boolean = messageGroupContext != null && GroupV2UpdateMessageUtil.isGroupV2(messageGroupContext)
|
||||
val isJustAGroupLeave: Boolean = messageGroupContext != null && GroupV2UpdateMessageUtil.isJustAGroupLeave(messageGroupContext)
|
||||
val isMessageEdit: Boolean = messageToEdit != 0L
|
||||
|
||||
/**
|
||||
* Smaller constructor for calling from Java and legacy code using the original interface.
|
||||
@@ -80,7 +82,8 @@ data class OutgoingMessage(
|
||||
giftBadge: GiftBadge? = null,
|
||||
isSecure: Boolean = false,
|
||||
bodyRanges: BodyRangeList? = null,
|
||||
scheduledDate: Long = -1
|
||||
scheduledDate: Long = -1,
|
||||
messageToEdit: Long = 0
|
||||
) : this(
|
||||
threadRecipient = recipient,
|
||||
body = body ?: "",
|
||||
@@ -102,7 +105,8 @@ data class OutgoingMessage(
|
||||
giftBadge = giftBadge,
|
||||
isSecure = isSecure,
|
||||
bodyRanges = bodyRanges,
|
||||
scheduledDate = scheduledDate
|
||||
scheduledDate = scheduledDate,
|
||||
messageToEdit = messageToEdit
|
||||
)
|
||||
|
||||
/**
|
||||
@@ -200,6 +204,28 @@ data class OutgoingMessage(
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit a secure message that only contains text.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun editText(
|
||||
recipient: Recipient,
|
||||
body: String,
|
||||
sentTimeMillis: Long,
|
||||
bodyRanges: BodyRangeList?,
|
||||
messageToEdit: Long
|
||||
): OutgoingMessage {
|
||||
return OutgoingMessage(
|
||||
threadRecipient = recipient,
|
||||
sentTimeMillis = sentTimeMillis,
|
||||
body = body,
|
||||
isUrgent = true,
|
||||
isSecure = true,
|
||||
bodyRanges = bodyRanges,
|
||||
messageToEdit = messageToEdit
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper for creating a group update message when a state change occurs and needs to be sent to others.
|
||||
*/
|
||||
|
||||
@@ -102,7 +102,8 @@ public class RemoteReplyReceiver extends BroadcastReceiver {
|
||||
null,
|
||||
recipient.isPushGroup(),
|
||||
null,
|
||||
-1);
|
||||
-1,
|
||||
0);
|
||||
threadId = MessageSender.send(context, reply, -1, MessageSender.SendType.SIGNAL, null, null);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ object DeleteDialog {
|
||||
DeleteProgressDialogAsyncTask(context, messageRecords, emitter::onSuccess).executeOnExecutor(SignalExecutors.BOUNDED)
|
||||
}
|
||||
|
||||
if (RemoteDeleteUtil.isValidSend(messageRecords, System.currentTimeMillis())) {
|
||||
if (MessageConstraintsUtil.isValidRemoteDeleteSend(messageRecords, System.currentTimeMillis())) {
|
||||
builder.setNeutralButton(R.string.ConversationFragment_delete_for_everyone) { _, _ -> handleDeleteForEveryone(context, messageRecords, emitter) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,6 +109,8 @@ public final class FeatureFlags {
|
||||
private static final String CALLS_TAB = "android.calls.tab";
|
||||
private static final String TEXT_FORMATTING_SPOILER_SEND = "android.textFormatting.spoilerSend";
|
||||
private static final String AD_HOC_CALLING = "android.calling.ad.hoc";
|
||||
private static final String EDIT_MESSAGE_RECEIVE = "android.editMessage.receive";
|
||||
private static final String EDIT_MESSAGE_SEND = "android.editMessage.send";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
@@ -167,7 +169,9 @@ public final class FeatureFlags {
|
||||
TEXT_FORMATTING,
|
||||
ANY_ADDRESS_PORTS_KILL_SWITCH,
|
||||
CALLS_TAB,
|
||||
TEXT_FORMATTING_SPOILER_SEND
|
||||
TEXT_FORMATTING_SPOILER_SEND,
|
||||
EDIT_MESSAGE_RECEIVE,
|
||||
EDIT_MESSAGE_SEND
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
@@ -232,7 +236,9 @@ public final class FeatureFlags {
|
||||
PAYMENTS_REQUEST_ACTIVATE_FLOW,
|
||||
CDS_HARD_LIMIT,
|
||||
TEXT_FORMATTING,
|
||||
TEXT_FORMATTING_SPOILER_SEND
|
||||
TEXT_FORMATTING_SPOILER_SEND,
|
||||
EDIT_MESSAGE_RECEIVE,
|
||||
EDIT_MESSAGE_SEND
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -598,6 +604,14 @@ public final class FeatureFlags {
|
||||
return getBoolean(ANY_ADDRESS_PORTS_KILL_SWITCH, false);
|
||||
}
|
||||
|
||||
public static boolean editMessageReceiving() {
|
||||
return getBoolean(EDIT_MESSAGE_RECEIVE, false);
|
||||
}
|
||||
|
||||
public static boolean editMessageSending() {
|
||||
return getBoolean(EDIT_MESSAGE_SEND, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the calls tab is enabled
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
|
||||
/**
|
||||
* Helpers for determining if a message send/receive is valid for those that
|
||||
* have strict time limits.
|
||||
*/
|
||||
object MessageConstraintsUtil {
|
||||
private val RECEIVE_THRESHOLD = TimeUnit.DAYS.toMillis(1)
|
||||
private val SEND_THRESHOLD = TimeUnit.HOURS.toMillis(3)
|
||||
|
||||
private val MAX_EDIT_COUNT = 10
|
||||
|
||||
@JvmStatic
|
||||
fun isValidRemoteDeleteReceive(targetMessage: MessageRecord, deleteSenderId: RecipientId, deleteServerTimestamp: Long): Boolean {
|
||||
val selfIsDeleteSender = isSelf(deleteSenderId)
|
||||
|
||||
val isValidIncomingOutgoing = selfIsDeleteSender && targetMessage.isOutgoing || !selfIsDeleteSender && !targetMessage.isOutgoing
|
||||
val isValidSender = targetMessage.fromRecipient.id == deleteSenderId || selfIsDeleteSender && targetMessage.isOutgoing
|
||||
|
||||
val messageTimestamp = if (selfIsDeleteSender && targetMessage.isOutgoing) targetMessage.dateSent else targetMessage.serverTimestamp
|
||||
|
||||
return isValidIncomingOutgoing &&
|
||||
isValidSender &&
|
||||
((deleteServerTimestamp - messageTimestamp < RECEIVE_THRESHOLD) || (selfIsDeleteSender && targetMessage.isOutgoing))
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isValidEditMessageReceive(targetMessage: MessageRecord, editSender: Recipient, editServerTimestamp: Long): Boolean {
|
||||
return isValidRemoteDeleteReceive(targetMessage, editSender.id, editServerTimestamp)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isValidRemoteDeleteSend(targetMessages: Collection<MessageRecord>, currentTime: Long): Boolean {
|
||||
// TODO [greyson] [remote-delete] Update with server timestamp when available for outgoing messages
|
||||
return targetMessages.all { isValidRemoteDeleteSend(it, currentTime) }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getEditMessageThresholdHours(): Int {
|
||||
return SEND_THRESHOLD.hours.inWholeHours.toInt()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if at the current time a target message can be edited
|
||||
*/
|
||||
@JvmStatic
|
||||
fun isValidEditMessageSend(targetMessage: MessageRecord, currentTime: Long): Boolean {
|
||||
return isValidRemoteDeleteSend(targetMessage, currentTime) &&
|
||||
targetMessage.revisionNumber < 10 &&
|
||||
!targetMessage.isViewOnceMessage() &&
|
||||
!targetMessage.hasAudio() &&
|
||||
!targetMessage.hasSharedContact()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check regardless of timing, whether a target message can be edited
|
||||
*/
|
||||
@JvmStatic
|
||||
fun isValidEditMessageSend(targetMessage: MessageRecord): Boolean {
|
||||
return isValidEditMessageSend(targetMessage, targetMessage.dateSent)
|
||||
}
|
||||
|
||||
private fun isValidRemoteDeleteSend(message: MessageRecord, currentTime: Long): Boolean {
|
||||
return !message.isUpdate &&
|
||||
message.isOutgoing &&
|
||||
message.isPush &&
|
||||
(!message.toRecipient.isGroup || message.toRecipient.isActiveGroup) &&
|
||||
!message.isRemoteDelete &&
|
||||
!message.hasGiftBadge() &&
|
||||
!message.isPaymentNotification &&
|
||||
(currentTime - message.dateSent < SEND_THRESHOLD || message.toRecipient.isSelf)
|
||||
}
|
||||
|
||||
private fun isSelf(recipientId: RecipientId): Boolean {
|
||||
return Recipient.isSelfSet() && Recipient.self().id == recipientId
|
||||
}
|
||||
}
|
||||
@@ -151,3 +151,7 @@ fun MessageRecord.isScheduled(): Boolean {
|
||||
fun MessageRecord.getRecordQuoteType(): QuoteModel.Type {
|
||||
return if (hasGiftBadge()) QuoteModel.Type.GIFT_BADGE else QuoteModel.Type.NORMAL
|
||||
}
|
||||
|
||||
fun MessageRecord.isEditMessage(): Boolean {
|
||||
return this is MediaMmsMessageRecord && isEditMessage
|
||||
}
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public final class RemoteDeleteUtil {
|
||||
|
||||
private static final long RECEIVE_THRESHOLD = TimeUnit.DAYS.toMillis(1);
|
||||
private static final long SEND_THRESHOLD = TimeUnit.HOURS.toMillis(3);
|
||||
|
||||
private RemoteDeleteUtil() {}
|
||||
|
||||
public static boolean isValidReceive(@NonNull MessageRecord targetMessage, @NonNull Recipient deleteSender, long deleteServerTimestamp) {
|
||||
return isValidReceive(targetMessage, deleteSender.getId(), deleteServerTimestamp);
|
||||
}
|
||||
|
||||
public static boolean isValidReceive(@NonNull MessageRecord targetMessage, @NonNull RecipientId deleteSenderId, long deleteServerTimestamp) {
|
||||
boolean selfIsDeleteSender = isSelf(deleteSenderId);
|
||||
|
||||
boolean isValidIncomingOutgoing = (selfIsDeleteSender && targetMessage.isOutgoing()) ||
|
||||
(!selfIsDeleteSender && !targetMessage.isOutgoing());
|
||||
|
||||
boolean isValidSender = targetMessage.getFromRecipient().getId().equals(deleteSenderId) || selfIsDeleteSender && targetMessage.isOutgoing();
|
||||
|
||||
long messageTimestamp = selfIsDeleteSender && targetMessage.isOutgoing() ? targetMessage.getDateSent()
|
||||
: targetMessage.getServerTimestamp();
|
||||
|
||||
return isValidIncomingOutgoing &&
|
||||
isValidSender &&
|
||||
(((deleteServerTimestamp - messageTimestamp) < RECEIVE_THRESHOLD) || (selfIsDeleteSender && targetMessage.isOutgoing()));
|
||||
}
|
||||
|
||||
public static boolean isValidSend(@NonNull Collection<MessageRecord> targetMessages, long currentTime) {
|
||||
// TODO [greyson] [remote-delete] Update with server timestamp when available for outgoing messages
|
||||
return Stream.of(targetMessages).allMatch(message -> isValidSend(message, currentTime));
|
||||
}
|
||||
|
||||
private static boolean isValidSend(MessageRecord message, long currentTime) {
|
||||
return !message.isUpdate() &&
|
||||
message.isOutgoing() &&
|
||||
message.isPush() &&
|
||||
(!message.getToRecipient().isGroup() || message.getToRecipient().isActiveGroup()) &&
|
||||
!message.isRemoteDelete() &&
|
||||
!MessageRecordUtil.hasGiftBadge(message) &&
|
||||
!message.isPaymentNotification() &&
|
||||
(((currentTime - message.getDateSent()) < SEND_THRESHOLD) || message.getToRecipient().isSelf());
|
||||
}
|
||||
|
||||
private static boolean isSelf(@NonNull RecipientId recipientId) {
|
||||
return Recipient.isSelfSet() && Recipient.self().getId().equals(recipientId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
||||
/**
|
||||
* Simplifies [ViewModel] creation by providing default implementations of [ViewModelProvider.Factory]
|
||||
* and a factory producer that call through to a lambda to create the view model instance.
|
||||
*
|
||||
* Example use:
|
||||
*
|
||||
* private val viewModel: MyViewModel by viewModels(factoryProducer = ViewModelFactory.factoryProducer { MyViewModel(inputParams) })
|
||||
*/
|
||||
class ViewModelFactory<MODEL : ViewModel>(private val create: () -> MODEL) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return create() as T
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <MODEL : ViewModel> factoryProducer(create: () -> MODEL): () -> ViewModelProvider.Factory {
|
||||
return { ViewModelFactory(create) }
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user