mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-04-22 09:49:30 +01:00
Add message editing feature.
This commit is contained in:
@@ -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")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user