kopia lustrzana https://github.com/ryukoposting/Signal-Android
Add in-chat payment messages.
rodzic
28193c2f61
commit
1dc29fda12
|
@ -7,12 +7,14 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.signal.paging.PagedDataSource;
|
||||
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationData.MessageRequestData;
|
||||
import org.thoughtcrime.securesms.conversation.ConversationMessage.ConversationMessageFactory;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.InMemoryMessageRecord;
|
||||
|
@ -24,11 +26,12 @@ import org.thoughtcrime.securesms.database.model.ReactionRecord;
|
|||
import org.thoughtcrime.securesms.database.model.SmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.UpdateDescription;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.payments.Payment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.signal.core.util.Stopwatch;
|
||||
import org.thoughtcrime.securesms.util.Util;
|
||||
import org.whispersystems.signalservice.api.push.ServiceId;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
|
@ -39,6 +42,7 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
|
@ -96,6 +100,7 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
|||
MentionHelper mentionHelper = new MentionHelper();
|
||||
AttachmentHelper attachmentHelper = new AttachmentHelper();
|
||||
ReactionHelper reactionHelper = new ReactionHelper();
|
||||
PaymentHelper paymentHelper = new PaymentHelper();
|
||||
Set<ServiceId> referencedIds = new HashSet<>();
|
||||
|
||||
try (MmsSmsDatabase.Reader reader = MmsSmsDatabase.readerFor(db.getConversation(threadId, start, length))) {
|
||||
|
@ -105,6 +110,7 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
|||
mentionHelper.add(record);
|
||||
reactionHelper.add(record);
|
||||
attachmentHelper.add(record);
|
||||
paymentHelper.add(record);
|
||||
|
||||
UpdateDescription description = record.getUpdateDisplayBody(context, null);
|
||||
if (description != null) {
|
||||
|
@ -138,6 +144,12 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
|||
records = attachmentHelper.buildUpdatedModels(context, records);
|
||||
stopwatch.split("attachment-models");
|
||||
|
||||
paymentHelper.fetchPayments();
|
||||
stopwatch.split("payments");
|
||||
|
||||
records = paymentHelper.buildUpdatedModels(records);
|
||||
stopwatch.split("payment-models");
|
||||
|
||||
for (ServiceId serviceId : referencedIds) {
|
||||
Recipient.resolved(RecipientId.from(serviceId));
|
||||
}
|
||||
|
@ -192,6 +204,12 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
|||
|
||||
stopwatch.split("attachments");
|
||||
|
||||
if (record.isPaymentNotification()) {
|
||||
record = SignalDatabase.payments().updateMessageWithPayment(record);
|
||||
}
|
||||
|
||||
stopwatch.split("payments");
|
||||
|
||||
return ConversationMessage.ConversationMessageFactory.createWithUnresolvedData(ApplicationDependencies.getApplication(), record, mentions);
|
||||
} else {
|
||||
return null;
|
||||
|
@ -303,4 +321,40 @@ public class ConversationDataSource implements PagedDataSource<MessageId, Conver
|
|||
}
|
||||
}
|
||||
|
||||
private static class PaymentHelper {
|
||||
private final Map<UUID, Long> paymentMessages = new HashMap<>();
|
||||
private final Map<Long, Payment> messageIdToPayment = new HashMap<>();
|
||||
|
||||
public void add(MessageRecord messageRecord) {
|
||||
if (messageRecord.isMms() && messageRecord.isPaymentNotification()) {
|
||||
UUID paymentUuid = UuidUtil.parseOrNull(messageRecord.getBody());
|
||||
if (paymentUuid != null) {
|
||||
paymentMessages.put(paymentUuid, messageRecord.getId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchPayments() {
|
||||
List<Payment> payments = SignalDatabase.payments().getPayments(paymentMessages.keySet());
|
||||
for (Payment payment : payments) {
|
||||
if (payment != null) {
|
||||
messageIdToPayment.put(paymentMessages.get(payment.getUuid()), payment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull List<MessageRecord> buildUpdatedModels(@NonNull List<MessageRecord> records) {
|
||||
return records.stream()
|
||||
.map(record -> {
|
||||
if (record instanceof MediaMmsMessageRecord) {
|
||||
Payment payment = messageIdToPayment.get(record.getId());
|
||||
if (payment != null) {
|
||||
return ((MediaMmsMessageRecord) record).withPayment(payment);
|
||||
}
|
||||
}
|
||||
return record;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1168,6 +1168,15 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
});
|
||||
}
|
||||
|
||||
private void handleViewPaymentDetails(MessageRecord message) {
|
||||
if (message instanceof MediaMmsMessageRecord) {
|
||||
MediaMmsMessageRecord mediaMessage = (MediaMmsMessageRecord) message;
|
||||
if (mediaMessage.isPaymentNotification() && mediaMessage.getPayment() != null) {
|
||||
startActivity(PaymentsActivity.navigateToPaymentDetails(requireContext(), mediaMessage.getPayment().getUuid()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void performSave(final MediaMmsMessageRecord message) {
|
||||
List<SaveAttachmentTask.Attachment> attachments = Stream.of(message.getSlideDeck().getSlides())
|
||||
.filter(s -> s.getUri() != null && (s.hasImage() || s.hasVideo() || s.hasAudio() || s.hasDocument()))
|
||||
|
@ -2287,6 +2296,9 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
|||
case COPY:
|
||||
handleCopyMessage(conversationMessage.getMultiselectCollection().toSet());
|
||||
break;
|
||||
case PAYMENT_DETAILS:
|
||||
handleViewPaymentDetails(conversationMessage.getMessageRecord());
|
||||
break;
|
||||
case MULTISELECT:
|
||||
handleEnterMultiSelect(conversationMessage);
|
||||
break;
|
||||
|
|
|
@ -95,6 +95,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors;
|
|||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectCollection;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
import org.thoughtcrime.securesms.conversation.ui.payment.PaymentMessageView;
|
||||
import org.thoughtcrime.securesms.database.AttachmentDatabase;
|
||||
import org.thoughtcrime.securesms.database.MediaDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
|
@ -181,14 +182,14 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
private static final long MAX_CLUSTERING_TIME_DIFF = TimeUnit.MINUTES.toMillis(3);
|
||||
private static final int CONDENSED_MODE_MAX_LINES = 3;
|
||||
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
private Locale locale;
|
||||
private boolean groupThread;
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
private ValueAnimator pulseOutlinerAlphaAnimator;
|
||||
private ConversationMessage conversationMessage;
|
||||
private MessageRecord messageRecord;
|
||||
private Optional<MessageRecord> nextMessageRecord;
|
||||
private Locale locale;
|
||||
private boolean groupThread;
|
||||
private LiveRecipient recipient;
|
||||
private GlideRequests glideRequests;
|
||||
private ValueAnimator pulseOutlinerAlphaAnimator;
|
||||
private Optional<MessageRecord> previousMessage;
|
||||
private ConversationItemDisplayMode displayMode;
|
||||
|
||||
|
@ -224,6 +225,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
private Stub<ViewOnceMessageView> revealableStub;
|
||||
private Stub<Button> callToActionStub;
|
||||
private Stub<GiftMessageView> giftViewStub;
|
||||
private Stub<PaymentMessageView> paymentViewStub;
|
||||
private @Nullable EventListener eventListener;
|
||||
|
||||
private int defaultBubbleColor;
|
||||
|
@ -325,6 +327,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
this.storyReactionLabel = findViewById(R.id.story_reacted_label);
|
||||
this.giftViewStub = new Stub<>(findViewById(R.id.gift_view_stub));
|
||||
this.quotedIndicator = findViewById(R.id.quoted_indicator);
|
||||
this.paymentViewStub = new Stub<>(findViewById(R.id.payment_view_stub));
|
||||
|
||||
setOnClickListener(new ClickListener(null));
|
||||
|
||||
|
@ -1000,7 +1003,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
bodyText.setText(italics);
|
||||
bodyText.setVisibility(View.VISIBLE);
|
||||
bodyText.setOverflowText(null);
|
||||
} else if (isCaptionlessMms(messageRecord) || isStoryReaction(messageRecord) || isGiftMessage(messageRecord)) {
|
||||
} else if (isCaptionlessMms(messageRecord) || isStoryReaction(messageRecord) || isGiftMessage(messageRecord) || messageRecord.isPaymentNotification()) {
|
||||
bodyText.setText(null);
|
||||
bodyText.setOverflowText(null);
|
||||
bodyText.setVisibility(View.GONE);
|
||||
} else {
|
||||
Spannable styledText = conversationMessage.getDisplayBody(getContext());
|
||||
|
@ -1076,6 +1081,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
|
||||
paymentViewStub.setVisibility(View.GONE);
|
||||
|
||||
revealableStub.get().setMessage((MmsMessageRecord) messageRecord, hasWallpaper);
|
||||
revealableStub.get().setOnClickListener(revealableClickListener);
|
||||
|
@ -1093,6 +1099,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
|
||||
paymentViewStub.setVisibility(View.GONE);
|
||||
|
||||
sharedContactStub.get().setContact(((MediaMmsMessageRecord) messageRecord).getSharedContacts().get(0), glideRequests, locale);
|
||||
sharedContactStub.get().setEventListener(sharedContactEventListener);
|
||||
|
@ -1113,6 +1120,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
|
||||
paymentViewStub.setVisibility(View.GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0);
|
||||
|
@ -1160,6 +1168,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
|
||||
paymentViewStub.setVisibility(View.GONE);
|
||||
|
||||
audioViewStub.get().setAudio(Objects.requireNonNull(((MediaMmsMessageRecord) messageRecord).getSlideDeck().getAudioSlide()), new AudioViewCallbacks(), showControls, true);
|
||||
audioViewStub.get().setDownloadClickListener(singleDownloadClickListener);
|
||||
|
@ -1186,6 +1195,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
|
||||
paymentViewStub.setVisibility(View.GONE);
|
||||
|
||||
//noinspection ConstantConditions
|
||||
documentViewStub.get().setDocument(
|
||||
|
@ -1213,6 +1223,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
|
||||
paymentViewStub.setVisibility(View.GONE);
|
||||
|
||||
if (hasSticker(messageRecord)) {
|
||||
//noinspection ConstantConditions
|
||||
|
@ -1243,6 +1254,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
|
||||
paymentViewStub.setVisibility(View.GONE);
|
||||
|
||||
List<Slide> thumbnailSlides = ((MmsMessageRecord) messageRecord).getSlideDeck().getThumbnailSlides();
|
||||
mediaThumbnailStub.require().setMinimumThumbnailWidth(readDimen(isCaptionlessMms(messageRecord) ? R.dimen.media_bubble_min_width_solo
|
||||
|
@ -1296,11 +1308,28 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
if (stickerStub.resolved()) stickerStub.get().setVisibility(GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(GONE);
|
||||
paymentViewStub.setVisibility(View.GONE);
|
||||
|
||||
MmsMessageRecord mmsMessageRecord = (MmsMessageRecord) messageRecord;
|
||||
giftViewStub.get().setGiftBadge(glideRequests, Objects.requireNonNull(mmsMessageRecord.getGiftBadge()), messageRecord.isOutgoing(), giftMessageViewCallback);
|
||||
giftViewStub.get().setVisibility(VISIBLE);
|
||||
|
||||
footer.setVisibility(VISIBLE);
|
||||
} else if (messageRecord.isPaymentNotification()) {
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(GONE);
|
||||
if (audioViewStub.resolved()) audioViewStub.get().setVisibility(GONE);
|
||||
if (documentViewStub.resolved()) documentViewStub.get().setVisibility(GONE);
|
||||
if (sharedContactStub.resolved()) sharedContactStub.get().setVisibility(GONE);
|
||||
if (linkPreviewStub.resolved()) linkPreviewStub.get().setVisibility(GONE);
|
||||
if (stickerStub.resolved()) stickerStub.get().setVisibility(GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(GONE);
|
||||
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
|
||||
|
||||
MediaMmsMessageRecord mediaMmsMessageRecord = (MediaMmsMessageRecord) messageRecord;
|
||||
|
||||
paymentViewStub.setVisibility(View.VISIBLE);
|
||||
paymentViewStub.get().bindPayment(messageRecord.getIndividualRecipient(), Objects.requireNonNull(mediaMmsMessageRecord.getPayment()), colorizer);
|
||||
|
||||
footer.setVisibility(VISIBLE);
|
||||
} else {
|
||||
if (mediaThumbnailStub.resolved()) mediaThumbnailStub.require().setVisibility(View.GONE);
|
||||
|
@ -1311,6 +1340,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
|||
if (stickerStub.resolved()) stickerStub.get().setVisibility(View.GONE);
|
||||
if (revealableStub.resolved()) revealableStub.get().setVisibility(View.GONE);
|
||||
if (giftViewStub.resolved()) giftViewStub.get().setVisibility(View.GONE);
|
||||
paymentViewStub.setVisibility(View.GONE);
|
||||
|
||||
ViewUtil.updateLayoutParams(bodyText, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
ViewUtil.updateLayoutParamsIfNonNull(groupSenderHolder, ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
|
||||
|
|
|
@ -761,6 +761,10 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
items.add(new ActionItem(R.drawable.ic_copy_24_tinted, getResources().getString(R.string.conversation_selection__menu_copy), () -> handleActionItemClicked(Action.COPY)));
|
||||
}
|
||||
|
||||
if (menuState.shouldShowPaymentDetails()) {
|
||||
items.add(new ActionItem(R.drawable.ic_payments_24, getResources().getString(R.string.conversation_selection__menu_payment_details), () -> handleActionItemClicked(Action.PAYMENT_DETAILS)));
|
||||
}
|
||||
|
||||
items.add(new ActionItem(R.drawable.ic_select_24_tinted, getResources().getString(R.string.conversation_selection__menu_multi_select), () -> handleActionItemClicked(Action.MULTISELECT)));
|
||||
|
||||
if (menuState.shouldShowDetailsAction()) {
|
||||
|
@ -976,6 +980,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
|
|||
DOWNLOAD,
|
||||
COPY,
|
||||
MULTISELECT,
|
||||
PAYMENT_DETAILS,
|
||||
VIEW_INFO,
|
||||
DELETE,
|
||||
}
|
||||
|
|
|
@ -544,7 +544,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
|||
});
|
||||
|
||||
actionButton.setText(R.string.ConversationActivity__invite_to_signal);
|
||||
} else if (conversationMessage.getMessageRecord().isRequestToActivatePayments() && !conversationMessage.getMessageRecord().isOutgoing() && !SignalStore.paymentsValues().mobileCoinPaymentsEnabled()) {
|
||||
} else if (conversationMessage.getMessageRecord().isPaymentsRequestToActivate() && !conversationMessage.getMessageRecord().isOutgoing() && !SignalStore.paymentsValues().mobileCoinPaymentsEnabled()) {
|
||||
actionButton.setText(R.string.ConversationUpdateItem_activate_payments);
|
||||
actionButton.setVisibility(VISIBLE);
|
||||
actionButton.setOnClickListener(v -> {
|
||||
|
|
|
@ -24,6 +24,7 @@ final class MenuState {
|
|||
private final boolean copy;
|
||||
private final boolean delete;
|
||||
private final boolean reactions;
|
||||
private final boolean paymentDetails;
|
||||
|
||||
private MenuState(@NonNull Builder builder) {
|
||||
forward = builder.forward;
|
||||
|
@ -34,6 +35,7 @@ final class MenuState {
|
|||
copy = builder.copy;
|
||||
delete = builder.delete;
|
||||
reactions = builder.reactions;
|
||||
paymentDetails = builder.paymentDetails;
|
||||
}
|
||||
|
||||
boolean shouldShowForwardAction() {
|
||||
|
@ -68,6 +70,10 @@ final class MenuState {
|
|||
return reactions;
|
||||
}
|
||||
|
||||
boolean shouldShowPaymentDetails() {
|
||||
return paymentDetails;
|
||||
}
|
||||
|
||||
static MenuState getMenuState(@NonNull Recipient conversationRecipient,
|
||||
@NonNull Set<MultiselectPart> selectedParts,
|
||||
boolean shouldShowMessageRequest,
|
||||
|
@ -84,6 +90,7 @@ final class MenuState {
|
|||
boolean hasPendingMedia = false;
|
||||
boolean mediaIsSelected = false;
|
||||
boolean hasGift = false;
|
||||
boolean hasPayment = false;
|
||||
|
||||
for (MultiselectPart part : selectedParts) {
|
||||
MessageRecord messageRecord = part.getMessageRecord();
|
||||
|
@ -121,6 +128,10 @@ final class MenuState {
|
|||
if (MessageRecordUtil.hasGiftBadge(messageRecord)) {
|
||||
hasGift = true;
|
||||
}
|
||||
|
||||
if (messageRecord.isPaymentNotification()) {
|
||||
hasPayment = true;
|
||||
}
|
||||
}
|
||||
|
||||
boolean shouldShowForwardAction = !actionMessage &&
|
||||
|
@ -129,6 +140,7 @@ final class MenuState {
|
|||
!remoteDelete &&
|
||||
!hasPendingMedia &&
|
||||
!hasGift &&
|
||||
!hasPayment &&
|
||||
selectedParts.size() <= MAX_FORWARDABLE_COUNT;
|
||||
|
||||
int uniqueRecords = selectedParts.stream()
|
||||
|
@ -160,9 +172,10 @@ final class MenuState {
|
|||
.shouldShowReplyAction(canReplyToMessage(conversationRecipient, actionMessage, messageRecord, shouldShowMessageRequest, isNonAdminInAnnouncementGroup));
|
||||
}
|
||||
|
||||
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText && !hasGift)
|
||||
return builder.shouldShowCopyAction(!actionMessage && !remoteDelete && hasText && !hasGift && !hasPayment)
|
||||
.shouldShowDeleteAction(!hasInMemory && onlyContainsCompleteMessages(selectedParts))
|
||||
.shouldShowReactions(!conversationRecipient.isReleaseNotes())
|
||||
.shouldShowPaymentDetails(hasPayment)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
@ -206,7 +219,7 @@ final class MenuState {
|
|||
messageRecord.isInMemoryMessageRecord() ||
|
||||
messageRecord.isChangeNumber() ||
|
||||
messageRecord.isBoostRequest() ||
|
||||
messageRecord.isRequestToActivatePayments() ||
|
||||
messageRecord.isPaymentsRequestToActivate() ||
|
||||
messageRecord.isPaymentsActivated();
|
||||
}
|
||||
|
||||
|
@ -220,6 +233,7 @@ final class MenuState {
|
|||
private boolean copy;
|
||||
private boolean delete;
|
||||
private boolean reactions;
|
||||
private boolean paymentDetails;
|
||||
|
||||
@NonNull Builder shouldShowForwardAction(boolean forward) {
|
||||
this.forward = forward;
|
||||
|
@ -261,6 +275,11 @@ final class MenuState {
|
|||
return this;
|
||||
}
|
||||
|
||||
@NonNull Builder shouldShowPaymentDetails(boolean paymentDetails) {
|
||||
this.paymentDetails = paymentDetails;
|
||||
return this;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
MenuState build() {
|
||||
return new MenuState(this);
|
||||
|
|
|
@ -46,7 +46,7 @@ class MessageQuotesRepository {
|
|||
|
||||
@WorkerThread
|
||||
private fun getMessageInQuoteChainSync(application: Application, messageId: MessageId): List<ConversationMessage> {
|
||||
val originalRecord: MessageRecord? = if (messageId.mms) {
|
||||
var originalRecord: MessageRecord? = if (messageId.mms) {
|
||||
SignalDatabase.mms.getMessageRecordOrNull(messageId.id)
|
||||
} else {
|
||||
SignalDatabase.sms.getMessageRecordOrNull(messageId.id)
|
||||
|
@ -66,7 +66,7 @@ class MessageQuotesRepository {
|
|||
.buildUpdatedModels(replyRecords)
|
||||
.map { replyRecord ->
|
||||
val replyQuote: Quote? = replyRecord.getQuote()
|
||||
if (replyQuote != null && replyQuote.id == originalRecord.dateSent) {
|
||||
if (replyQuote != null && replyQuote.id == originalRecord!!.dateSent) {
|
||||
(replyRecord as MediaMmsMessageRecord).withoutQuote()
|
||||
} else {
|
||||
replyRecord
|
||||
|
@ -74,6 +74,10 @@ class MessageQuotesRepository {
|
|||
}
|
||||
.map { ConversationMessageFactory.createWithUnresolvedData(application, it) }
|
||||
|
||||
if (originalRecord.isPaymentNotification) {
|
||||
originalRecord = SignalDatabase.payments.updateMessageWithPayment(originalRecord)
|
||||
}
|
||||
|
||||
val originalMessage: List<ConversationMessage> = ConversationDataSource.ReactionHelper()
|
||||
.apply {
|
||||
add(originalRecord)
|
||||
|
|
|
@ -0,0 +1,63 @@
|
|||
package org.thoughtcrime.securesms.conversation.ui.payment
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.ColorStateList
|
||||
import android.text.style.TypefaceSpan
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.view.ViewCompat
|
||||
import org.thoughtcrime.securesms.R
|
||||
import org.thoughtcrime.securesms.components.quotes.QuoteViewColorTheme
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer
|
||||
import org.thoughtcrime.securesms.databinding.PaymentMessageViewBinding
|
||||
import org.thoughtcrime.securesms.payments.Direction
|
||||
import org.thoughtcrime.securesms.payments.Payment
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.util.visible
|
||||
|
||||
/**
|
||||
* Showing payment information in conversation.
|
||||
*/
|
||||
class PaymentMessageView @JvmOverloads constructor(
|
||||
context: Context,
|
||||
attrs: AttributeSet? = null
|
||||
) : FrameLayout(context, attrs) {
|
||||
|
||||
private val binding: PaymentMessageViewBinding
|
||||
|
||||
init {
|
||||
binding = PaymentMessageViewBinding.inflate(LayoutInflater.from(context), this, true)
|
||||
}
|
||||
|
||||
fun bindPayment(recipient: Recipient, payment: Payment, colorizer: Colorizer) {
|
||||
val outgoing = payment.direction == Direction.SENT
|
||||
|
||||
binding.paymentDirection.apply {
|
||||
if (outgoing) {
|
||||
text = context.getString(R.string.PaymentMessageView_you_sent_s, recipient.getShortDisplayName(context))
|
||||
setTextColor(colorizer.getOutgoingFooterTextColor(context))
|
||||
} else {
|
||||
text = context.getString(R.string.PaymentMessageView_s_sent_you, recipient.getShortDisplayName(context))
|
||||
setTextColor(colorizer.getIncomingFooterTextColor(context, recipient.hasWallpaper()))
|
||||
}
|
||||
}
|
||||
|
||||
binding.paymentNote.apply {
|
||||
text = payment.note
|
||||
visible = payment.note.isNotEmpty()
|
||||
setTextColor(if (outgoing) colorizer.getOutgoingBodyTextColor(context) else colorizer.getIncomingBodyTextColor(context, recipient.hasWallpaper()))
|
||||
}
|
||||
|
||||
val quoteViewColorTheme = QuoteViewColorTheme.resolveTheme(outgoing, false, recipient.hasWallpaper())
|
||||
|
||||
binding.paymentAmount.setTextColor(quoteViewColorTheme.getForegroundColor(context))
|
||||
binding.paymentAmount.setMoney(payment.amount, 0L, currencyTypefaceSpan)
|
||||
|
||||
ViewCompat.setBackgroundTintList(binding.paymentAmountLayout, ColorStateList.valueOf(quoteViewColorTheme.getBackgroundColor(context)))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val currencyTypefaceSpan = TypefaceSpan("sans-serif-light")
|
||||
}
|
||||
}
|
|
@ -1485,12 +1485,7 @@ public class ConversationListFragment extends MainFragment implements ActionMode
|
|||
}
|
||||
|
||||
private void goToSinglePayment(@NonNull UUID paymentId) {
|
||||
Intent intent = new Intent(requireContext(), PaymentsActivity.class);
|
||||
|
||||
intent.putExtra(PaymentsActivity.EXTRA_PAYMENTS_STARTING_ACTION, R.id.action_directly_to_paymentDetails);
|
||||
intent.putExtra(PaymentsActivity.EXTRA_STARTING_ARGUMENTS, new PaymentDetailsFragmentArgs.Builder(PaymentDetailsParcelable.forUuid(paymentId)).build().toBundle());
|
||||
|
||||
startActivity(intent);
|
||||
startActivity(PaymentsActivity.navigateToPaymentDetails(requireContext(), paymentId));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -522,7 +522,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns,
|
|||
public List<Long> getIncomingPaymentRequestThreads() {
|
||||
Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase(), "DISTINCT " + THREAD_ID)
|
||||
.from(getTableName())
|
||||
.where("(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE + " AND (" + getTypeField() + " & ?) != 0", Types.SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST)
|
||||
.where("(" + getTypeField() + " & " + Types.BASE_TYPE_MASK + ") = " + Types.BASE_INBOX_TYPE + " AND (" + getTypeField() + " & ?) != 0", Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST)
|
||||
.run();
|
||||
|
||||
return CursorExtensionsKt.readToList(cursor, c -> CursorUtil.requireLong(c, THREAD_ID));
|
||||
|
|
|
@ -79,6 +79,7 @@ import org.thoughtcrime.securesms.mms.OutgoingExpirationUpdateMessage;
|
|||
import org.thoughtcrime.securesms.mms.OutgoingGroupUpdateMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingPaymentsActivatedMessages;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingPaymentsNotificationMessage;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingRequestToActivatePaymentMessages;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingSecureMediaMessage;
|
||||
import org.thoughtcrime.securesms.mms.QuoteModel;
|
||||
|
@ -109,6 +110,7 @@ import java.util.LinkedList;
|
|||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.NoSuchElementException;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
@ -1796,7 +1798,9 @@ public class MmsDatabase extends MessageDatabase {
|
|||
return new OutgoingGroupUpdateMessage(recipient, new MessageGroupContext(body, Types.isGroupV2(outboxType)), attachments, timestamp, 0, false, quote, contacts, previews, mentions);
|
||||
} else if (Types.isExpirationTimerUpdate(outboxType)) {
|
||||
return new OutgoingExpirationUpdateMessage(recipient, timestamp, expiresIn);
|
||||
} else if (Types.isRequestToActivatePayments(outboxType)) {
|
||||
} else if (Types.isPaymentsNotification(outboxType)) {
|
||||
return new OutgoingPaymentsNotificationMessage(recipient, Objects.requireNonNull(body), timestamp, expiresIn);
|
||||
} else if (Types.isPaymentsRequestToActivate(outboxType)) {
|
||||
return new OutgoingRequestToActivatePaymentMessages(recipient, timestamp, expiresIn);
|
||||
} else if (Types.isPaymentsActivated(outboxType)) {
|
||||
return new OutgoingPaymentsActivatedMessages(recipient, timestamp, expiresIn);
|
||||
|
@ -1995,7 +1999,7 @@ public class MmsDatabase extends MessageDatabase {
|
|||
!keepThreadArchived);
|
||||
|
||||
boolean isNotStoryGroupReply = retrieved.getParentStoryId() == null || !retrieved.getParentStoryId().isGroupReply();
|
||||
if (!Types.isPaymentsActivated(mailbox) && !Types.isRequestToActivatePayments(mailbox) && !Types.isExpirationTimerUpdate(mailbox) && !retrieved.getStoryType().isStory() && isNotStoryGroupReply) {
|
||||
if (!Types.isPaymentsActivated(mailbox) && !Types.isPaymentsRequestToActivate(mailbox) && !Types.isExpirationTimerUpdate(mailbox) && !retrieved.getStoryType().isStory() && isNotStoryGroupReply) {
|
||||
boolean incrementUnreadMentions = !retrieved.getMentions().isEmpty() && retrieved.getMentions().stream().anyMatch(m -> m.getRecipientId().equals(Recipient.self().getId()));
|
||||
SignalDatabase.threads().incrementUnread(threadId, 1, incrementUnreadMentions ? 1 : 0);
|
||||
SignalDatabase.threads().update(threadId, !keepThreadArchived);
|
||||
|
@ -2021,8 +2025,12 @@ public class MmsDatabase extends MessageDatabase {
|
|||
type |= Types.EXPIRATION_TIMER_UPDATE_BIT;
|
||||
}
|
||||
|
||||
if (retrieved.isPaymentsNotification()) {
|
||||
type |= Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION;
|
||||
}
|
||||
|
||||
if (retrieved.isActivatePaymentsRequest()) {
|
||||
type |= Types.SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST;
|
||||
type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST;
|
||||
}
|
||||
|
||||
if (retrieved.isPaymentsActivated()) {
|
||||
|
@ -2060,11 +2068,18 @@ public class MmsDatabase extends MessageDatabase {
|
|||
type |= Types.SPECIAL_TYPE_GIFT_BADGE;
|
||||
}
|
||||
|
||||
if (retrieved.isPaymentsNotification()) {
|
||||
if (hasSpecialType) {
|
||||
throw new MmsException("Cannot insert message with multiple special types.");
|
||||
}
|
||||
type |= Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION;
|
||||
}
|
||||
|
||||
if (retrieved.isActivatePaymentsRequest()) {
|
||||
if (hasSpecialType) {
|
||||
throw new MmsException("Cannot insert message with multiple special types.");
|
||||
}
|
||||
type |= Types.SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST;
|
||||
type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST;
|
||||
}
|
||||
|
||||
if (retrieved.isPaymentsActivated()) {
|
||||
|
@ -2241,11 +2256,18 @@ public class MmsDatabase extends MessageDatabase {
|
|||
type |= Types.SPECIAL_TYPE_GIFT_BADGE;
|
||||
}
|
||||
|
||||
if (message.isPaymentsNotification()) {
|
||||
if (hasSpecialType) {
|
||||
throw new MmsException("Cannot insert message with multiple special types.");
|
||||
}
|
||||
type |= Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION;
|
||||
}
|
||||
|
||||
if (message.isRequestToActivatePayments()) {
|
||||
if (hasSpecialType) {
|
||||
throw new MmsException("Cannot insert message with multiple special types.");
|
||||
}
|
||||
type |= Types.SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST;
|
||||
type |= Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST;
|
||||
}
|
||||
|
||||
if (message.isPaymentsActivated()) {
|
||||
|
@ -2858,7 +2880,8 @@ public class MmsDatabase extends MessageDatabase {
|
|||
null,
|
||||
message.getStoryType(),
|
||||
message.getParentStoryId(),
|
||||
message.getGiftBadge());
|
||||
message.getGiftBadge(),
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -3047,7 +3070,7 @@ public class MmsDatabase extends MessageDatabase {
|
|||
networkFailures, subscriptionId, expiresIn, expireStarted,
|
||||
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, Collections.emptyList(),
|
||||
remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount, receiptTimestamp, messageRanges,
|
||||
storyType, parentStoryId, giftBadge);
|
||||
storyType, parentStoryId, giftBadge, null);
|
||||
}
|
||||
|
||||
private Set<IdentityKeyMismatch> getMismatchedIdentities(String document) {
|
||||
|
|
|
@ -47,7 +47,7 @@ public interface MmsSmsColumns {
|
|||
* {@link #TOTAL_MASK}.
|
||||
*
|
||||
* <pre>
|
||||
* ____________________________________________ SPECIAL TYPES (Story reactions) ({@link #SPECIAL_TYPES_MASK}
|
||||
* ____________________________________________ SPECIAL TYPES ({@link #SPECIAL_TYPES_MASK}
|
||||
* | _____________________________________ ENCRYPTION ({@link #ENCRYPTION_MASK})
|
||||
* | | _____________________________ SECURE MESSAGE INFORMATION (no mask, but look at {@link #SECURE_MESSAGE_BIT})
|
||||
* | | | ________________________ GROUPS (no mask, but look at {@link #GROUP_UPDATE_BIT})
|
||||
|
@ -143,7 +143,8 @@ public interface MmsSmsColumns {
|
|||
public static final long SPECIAL_TYPES_MASK = 0xF00000000L;
|
||||
public static final long SPECIAL_TYPE_STORY_REACTION = 0x100000000L;
|
||||
public static final long SPECIAL_TYPE_GIFT_BADGE = 0x200000000L;
|
||||
protected static final long SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST = 0x400000000L;
|
||||
protected static final long SPECIAL_TYPE_PAYMENTS_NOTIFICATION = 0x300000000L;
|
||||
protected static final long SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST = 0x400000000L;
|
||||
protected static final long SPECIAL_TYPE_PAYMENTS_ACTIVATED = 0x800000000L;
|
||||
|
||||
public static boolean isStoryReaction(long type) {
|
||||
|
@ -154,8 +155,12 @@ public interface MmsSmsColumns {
|
|||
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_GIFT_BADGE;
|
||||
}
|
||||
|
||||
public static boolean isRequestToActivatePayments(long type) {
|
||||
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST;
|
||||
public static boolean isPaymentsNotification(long type) {
|
||||
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_PAYMENTS_NOTIFICATION;
|
||||
}
|
||||
|
||||
public static boolean isPaymentsRequestToActivate(long type) {
|
||||
return (type & SPECIAL_TYPES_MASK) == SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST;
|
||||
}
|
||||
|
||||
public static boolean isPaymentsActivated(long type) {
|
||||
|
|
|
@ -14,7 +14,11 @@ import androidx.lifecycle.MutableLiveData;
|
|||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
import com.mobilecoin.lib.exceptions.SerializationException;
|
||||
|
||||
import org.signal.core.util.CursorExtensionsKt;
|
||||
import org.signal.core.util.SQLiteDatabaseExtensionsKt;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.CryptoValue;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.payments.CryptoValueUtil;
|
||||
|
@ -31,8 +35,11 @@ import org.signal.core.util.CursorUtil;
|
|||
import org.signal.core.util.SqlUtil;
|
||||
import org.thoughtcrime.securesms.util.livedata.LiveDataUtil;
|
||||
import org.whispersystems.signalservice.api.payments.Money;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
|
@ -104,10 +111,11 @@ public final class PaymentDatabase extends Database implements RecipientIdDataba
|
|||
@NonNull String note,
|
||||
@NonNull Money amount,
|
||||
@NonNull Money fee,
|
||||
@NonNull byte[] receipt)
|
||||
@NonNull byte[] receipt,
|
||||
boolean seen)
|
||||
throws PublicKeyConflictException, SerializationException
|
||||
{
|
||||
create(uuid, fromRecipient, null, timestamp, 0, note, Direction.RECEIVED, State.SUBMITTED, amount, fee, null, receipt, null, false);
|
||||
create(uuid, fromRecipient, null, timestamp, 0, note, Direction.RECEIVED, State.SUBMITTED, amount, fee, null, receipt, null, seen);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
|
@ -388,11 +396,44 @@ public final class PaymentDatabase extends Database implements RecipientIdDataba
|
|||
}
|
||||
}
|
||||
|
||||
public @NonNull List<Payment> getPayments(@Nullable Collection<UUID> paymentUuids) {
|
||||
if (paymentUuids == null || paymentUuids.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
List<SqlUtil.Query> queries = SqlUtil.buildCollectionQuery(PAYMENT_UUID, paymentUuids);
|
||||
List<Payment> payments = new LinkedList<>();
|
||||
|
||||
for (SqlUtil.Query query : queries) {
|
||||
Cursor cursor = SQLiteDatabaseExtensionsKt.select(getReadableDatabase())
|
||||
.from(TABLE_NAME)
|
||||
.where(query.getWhere(), (Object[]) query.getWhereArgs())
|
||||
.run();
|
||||
|
||||
payments.addAll(CursorExtensionsKt.readToList(cursor, PaymentDatabase::readPayment));
|
||||
}
|
||||
|
||||
return payments;
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
public @NonNull LiveData<List<PaymentTransaction>> getAllLive() {
|
||||
return LiveDataUtil.mapAsync(changeSignal, change -> getAll());
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public @NonNull MessageRecord updateMessageWithPayment(@NonNull MessageRecord record) {
|
||||
if (record.isPaymentNotification()) {
|
||||
Payment payment = getPayment(UuidUtil.parseOrThrow(record.getBody()));
|
||||
if (payment != null && record instanceof MediaMmsMessageRecord) {
|
||||
return ((MediaMmsMessageRecord) record).withPayment(payment);
|
||||
} else {
|
||||
throw new AssertionError("Payment not found for message");
|
||||
}
|
||||
}
|
||||
return record;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void remapRecipient(@NonNull RecipientId fromId, @NonNull RecipientId toId) {
|
||||
ContentValues values = new ContentValues();
|
||||
|
|
|
@ -88,7 +88,9 @@ public class SearchDatabase extends Database {
|
|||
"FROM " + MmsDatabase.TABLE_NAME + " " +
|
||||
"INNER JOIN " + MMS_FTS_TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + ID + " = " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.ID + " " +
|
||||
"INNER JOIN " + ThreadDatabase.TABLE_NAME + " ON " + MMS_FTS_TABLE_NAME + "." + THREAD_ID + " = " + ThreadDatabase.TABLE_NAME + "." + ThreadDatabase.ID + " " +
|
||||
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? AND " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + " & " + MmsSmsColumns.Types.GROUP_V2_BIT + " = 0 " +
|
||||
"WHERE " + MMS_FTS_TABLE_NAME + " MATCH ? " +
|
||||
"AND " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + " & " + MmsSmsColumns.Types.GROUP_V2_BIT + " = 0 " +
|
||||
"AND " + MmsDatabase.TABLE_NAME + "." + MmsDatabase.MESSAGE_BOX + " & " + MmsSmsColumns.Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION + " = 0 " +
|
||||
"ORDER BY " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " DESC " +
|
||||
"LIMIT 500";
|
||||
|
||||
|
|
|
@ -52,9 +52,11 @@ public final class ThreadBodyUtil {
|
|||
return String.format("%s %s", EmojiStrings.GIFT, getGiftSummary(context, record));
|
||||
} else if (MessageRecordUtil.isStoryReaction(record)) {
|
||||
return getStoryReactionSummary(context, record);
|
||||
} else if (MessageRecordUtil.isPaymentActivationRequest(record)) {
|
||||
} else if (record.isPaymentNotification()) {
|
||||
return String.format("%s %s", EmojiStrings.CARD, context.getString(R.string.ThreadRecord_payment));
|
||||
} else if (record.isPaymentsRequestToActivate()) {
|
||||
return String.format("%s %s", EmojiStrings.CARD, getPaymentActivationRequestSummary(context, record));
|
||||
} else if (MessageRecordUtil.isPaymentsActivated(record)) {
|
||||
} else if (record.isPaymentsActivated()) {
|
||||
return String.format("%s %s", EmojiStrings.CARD, getPaymentActivatedSummary(context, record));
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ import android.text.SpannableString;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
|
@ -233,11 +232,15 @@ public abstract class DisplayRecord {
|
|||
return SmsDatabase.Types.isPendingInsecureSmsFallbackType(type);
|
||||
}
|
||||
|
||||
public boolean isRequestToActivatePayments() {
|
||||
return SmsDatabase.Types.isRequestToActivatePayments(type);
|
||||
public boolean isPaymentNotification() {
|
||||
return MmsSmsColumns.Types.isPaymentsNotification(type);
|
||||
}
|
||||
|
||||
public boolean isPaymentsRequestToActivate() {
|
||||
return MmsSmsColumns.Types.isPaymentsRequestToActivate(type);
|
||||
}
|
||||
|
||||
public boolean isPaymentsActivated() {
|
||||
return SmsDatabase.Types.isPaymentsActivated(type);
|
||||
return MmsSmsColumns.Types.isPaymentsActivated(type);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,7 +37,9 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
|||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.payments.Payment;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.whispersystems.signalservice.api.payments.FormatterOptions;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
@ -61,6 +63,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||
private final int partCount;
|
||||
private final boolean mentionsSelf;
|
||||
private final BodyRangeList messageRanges;
|
||||
private final Payment payment;
|
||||
|
||||
public MediaMmsMessageRecord(long id,
|
||||
Recipient conversationRecipient,
|
||||
|
@ -95,7 +98,8 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||
@Nullable BodyRangeList messageRanges,
|
||||
@NonNull StoryType storyType,
|
||||
@Nullable ParentStoryId parentStoryId,
|
||||
@Nullable GiftBadge giftBadge)
|
||||
@Nullable GiftBadge giftBadge,
|
||||
@Nullable Payment payment)
|
||||
{
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
|
||||
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
|
@ -105,6 +109,7 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||
this.partCount = partCount;
|
||||
this.mentionsSelf = mentionsSelf;
|
||||
this.messageRanges = messageRanges;
|
||||
this.payment = payment;
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -128,6 +133,8 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||
return emphasisAdded(context.getString(R.string.MmsMessageRecord_mms_message_encrypted_for_non_existing_session));
|
||||
} else if (isLegacyMessage()) {
|
||||
return emphasisAdded(context.getString(R.string.MessageRecord_message_encrypted_with_a_legacy_protocol_version_that_is_no_longer_supported));
|
||||
} else if (isPaymentNotification() && payment != null) {
|
||||
return new SpannableString(context.getString(R.string.MessageRecord__payment_s, payment.getAmount().toString(FormatterOptions.defaults())));
|
||||
}
|
||||
|
||||
return super.getDisplayBody(context);
|
||||
|
@ -151,18 +158,22 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||
return Objects.requireNonNull(messageRanges);
|
||||
}
|
||||
|
||||
public @Nullable Payment getPayment() {
|
||||
return payment;
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withReactions(@NonNull List<ReactionRecord> reactions) {
|
||||
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getPartCount(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), reactions, isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withoutQuote() {
|
||||
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getPartCount(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), null, getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withAttachments(@NonNull Context context, @NonNull List<DatabaseAttachment> attachments) {
|
||||
|
@ -183,7 +194,14 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
|||
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), slideDeck,
|
||||
getPartCount(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), quote, contacts, linkPreviews, isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), getPayment());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withPayment(@NonNull Payment payment) {
|
||||
return new MediaMmsMessageRecord(getId(), getRecipient(), getIndividualRecipient(), getRecipientDeviceId(), getDateSent(), getDateReceived(), getServerTimestamp(), getDeliveryReceiptCount(), getThreadId(), getBody(), getSlideDeck(),
|
||||
getPartCount(), getType(), getIdentityKeyMismatches(), getNetworkFailures(), getSubscriptionId(), getExpiresIn(), getExpireStarted(), isViewOnce(),
|
||||
getReadReceiptCount(), getQuote(), getSharedContacts(), getLinkPreviews(), isUnidentified(), getReactions(), isRemoteDelete(), mentionsSelf,
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges(), getStoryType(), getParentStoryId(), getGiftBadge(), payment);
|
||||
}
|
||||
|
||||
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {
|
||||
|
|
|
@ -240,7 +240,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
int messageResource = SignalStore.misc().getSmsExportPhase().isSmsSupported() ? R.string.MessageRecord__you_will_no_longer_be_able_to_send_sms_messages_from_signal_soon
|
||||
: R.string.MessageRecord__you_can_no_longer_send_sms_messages_in_signal;
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(messageResource, r.getDisplayName(context)), R.drawable.ic_update_info_16);
|
||||
} else if (isRequestToActivatePayments()) {
|
||||
} else if (isPaymentsRequestToActivate()) {
|
||||
return isOutgoing() ? fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_you_sent_request, r.getShortDisplayName(context)), R.drawable.ic_card_activate_payments)
|
||||
: fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_wants_you_to_activate_payments, r.getShortDisplayName(context)), R.drawable.ic_card_activate_payments);
|
||||
} else if (isPaymentsActivated()) {
|
||||
|
@ -577,7 +577,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
|
||||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() ||
|
||||
isChangeNumber() || isBoostRequest() || isThreadMergeEventType() || isSmsExportType() ||
|
||||
isRequestToActivatePayments() || isPaymentsActivated();
|
||||
isPaymentsRequestToActivate() || isPaymentsActivated();
|
||||
}
|
||||
|
||||
public boolean isMediaPending() {
|
||||
|
|
|
@ -139,6 +139,7 @@ public final class JobManagerFactories {
|
|||
put(NullMessageSendJob.KEY, new NullMessageSendJob.Factory());
|
||||
put(PaymentLedgerUpdateJob.KEY, new PaymentLedgerUpdateJob.Factory());
|
||||
put(PaymentNotificationSendJob.KEY, new PaymentNotificationSendJob.Factory());
|
||||
put(PaymentNotificationSendJobV2.KEY, new PaymentNotificationSendJobV2.Factory());
|
||||
put(PaymentSendJob.KEY, new PaymentSendJob.Factory());
|
||||
put(PaymentTransactionCheckJob.KEY, new PaymentTransactionCheckJob.Factory());
|
||||
put(PnpInitializeDevicesJob.KEY, new PnpInitializeDevicesJob.Factory());
|
||||
|
|
|
@ -14,6 +14,7 @@ import org.thoughtcrime.securesms.recipients.Recipient;
|
|||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientUtil;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.FeatureFlags;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender;
|
||||
import org.whispersystems.signalservice.api.SignalServiceMessageSender.IndividualSendEvents;
|
||||
import org.whispersystems.signalservice.api.crypto.ContentHint;
|
||||
|
@ -40,7 +41,15 @@ public final class PaymentNotificationSendJob extends BaseJob {
|
|||
private final RecipientId recipientId;
|
||||
private final UUID uuid;
|
||||
|
||||
PaymentNotificationSendJob(@NonNull RecipientId recipientId,
|
||||
public static Job create(@NonNull RecipientId recipientId, @NonNull UUID uuid, @NonNull String queue) {
|
||||
if (FeatureFlags.paymentsInChatMessages()) {
|
||||
return new PaymentNotificationSendJobV2(recipientId, uuid);
|
||||
} else {
|
||||
return new PaymentNotificationSendJob(recipientId, uuid, queue);
|
||||
}
|
||||
}
|
||||
|
||||
private PaymentNotificationSendJob(@NonNull RecipientId recipientId,
|
||||
@NonNull UUID uuid,
|
||||
@NonNull String queue)
|
||||
{
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
package org.thoughtcrime.securesms.jobs
|
||||
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.jobmanager.Data
|
||||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.mms.OutgoingPaymentsNotificationMessage
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
import java.util.UUID
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
/**
|
||||
* Crafts a [OutgoingPaymentsNotificationMessage] and uses the regular media sending framework to send it
|
||||
* instead of attempting to send directly. The logic for actually creating over-the-wire representation is
|
||||
* now in [PushMediaSendJob] which gets enqueued by [MessageSender.send].
|
||||
*/
|
||||
class PaymentNotificationSendJobV2 private constructor(
|
||||
parameters: Parameters,
|
||||
private val recipientId: RecipientId,
|
||||
private val uuid: UUID
|
||||
) : BaseJob(parameters) {
|
||||
|
||||
companion object {
|
||||
const val KEY = "PaymentNotificationSendJobV2"
|
||||
private const val TAG = "PaymentNotiSendJobV2"
|
||||
private const val KEY_UUID = "uuid"
|
||||
private const val KEY_RECIPIENT = "recipient"
|
||||
}
|
||||
|
||||
constructor(recipientId: RecipientId, uuid: UUID) : this(Parameters.Builder().build(), recipientId, uuid)
|
||||
|
||||
override fun serialize(): Data {
|
||||
return Data.Builder()
|
||||
.putString(KEY_RECIPIENT, recipientId.serialize())
|
||||
.putString(KEY_UUID, uuid.toString())
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getFactoryKey(): String {
|
||||
return KEY
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
override fun onRun() {
|
||||
if (!Recipient.self().isRegistered) {
|
||||
throw NotPushRegisteredException()
|
||||
}
|
||||
|
||||
val recipient = Recipient.resolved(recipientId)
|
||||
if (recipient.isUnregistered) {
|
||||
Log.w(TAG, "$recipientId not registered!")
|
||||
return
|
||||
}
|
||||
|
||||
val payment = SignalDatabase.payments.getPayment(uuid)
|
||||
if (payment == null) {
|
||||
Log.w(TAG, "Could not find payment, cannot send notification $uuid")
|
||||
return
|
||||
}
|
||||
|
||||
MessageSender.send(
|
||||
context,
|
||||
OutgoingPaymentsNotificationMessage(
|
||||
recipient,
|
||||
uuid.toString(),
|
||||
System.currentTimeMillis(),
|
||||
recipient.expiresInSeconds.seconds.inWholeMilliseconds
|
||||
),
|
||||
SignalDatabase.threads.getOrCreateThreadIdFor(recipient),
|
||||
false,
|
||||
null,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
override fun onShouldRetry(e: Exception): Boolean = false
|
||||
override fun onFailure() = Unit
|
||||
|
||||
class Factory : Job.Factory<PaymentNotificationSendJobV2?> {
|
||||
override fun create(parameters: Parameters, data: Data): PaymentNotificationSendJobV2 {
|
||||
return PaymentNotificationSendJobV2(
|
||||
parameters,
|
||||
RecipientId.from(data.getString(KEY_RECIPIENT)),
|
||||
UUID.fromString(data.getString(KEY_UUID))
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -85,7 +85,7 @@ public final class PaymentSendJob extends BaseJob {
|
|||
.then(new MultiDeviceOutgoingPaymentSyncJob(uuid));
|
||||
|
||||
if (recipientId != null) {
|
||||
chain.then(new PaymentNotificationSendJob(recipientId, uuid, recipientId.toQueueKey(true)));
|
||||
chain.then(PaymentNotificationSendJob.create(recipientId, uuid, recipientId.toQueueKey(true)));
|
||||
}
|
||||
|
||||
chain.then(PaymentLedgerUpdateJob.updateLedgerToReflectPayment(uuid))
|
||||
|
|
|
@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.crypto.UnidentifiedAccessUtil;
|
|||
import org.thoughtcrime.securesms.database.MessageDatabase;
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase.SyncMessageId;
|
||||
import org.thoughtcrime.securesms.database.NoSuchMessageException;
|
||||
import org.thoughtcrime.securesms.database.PaymentDatabase;
|
||||
import org.thoughtcrime.securesms.database.RecipientDatabase.UnidentifiedAccessMode;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.MessageId;
|
||||
|
@ -46,6 +47,7 @@ import org.whispersystems.signalservice.api.push.SignalServiceAddress;
|
|||
import org.whispersystems.signalservice.api.push.exceptions.ProofRequiredException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.ServerRejectedException;
|
||||
import org.whispersystems.signalservice.api.push.exceptions.UnregisteredUserException;
|
||||
import org.whispersystems.signalservice.api.util.UuidUtil;
|
||||
import org.whispersystems.signalservice.internal.push.SignalServiceProtos;
|
||||
|
||||
import java.io.FileNotFoundException;
|
||||
|
@ -53,6 +55,7 @@ import java.io.IOException;
|
|||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class PushMediaSendJob extends PushSendJob {
|
||||
|
||||
|
@ -213,7 +216,7 @@ public class PushMediaSendJob extends PushSendJob {
|
|||
List<SharedContact> sharedContacts = getSharedContactsFor(message);
|
||||
List<SignalServicePreview> previews = getPreviewsFor(message);
|
||||
SignalServiceDataMessage.GiftBadge giftBadge = getGiftBadgeFor(message);
|
||||
SignalServiceDataMessage.Payment payment = getPaymentActivation(message);
|
||||
SignalServiceDataMessage.Payment payment = getPayment(message);
|
||||
SignalServiceDataMessage.Builder mediaMessageBuilder = SignalServiceDataMessage.newBuilder()
|
||||
.withBody(message.getBody())
|
||||
.withAttachments(serviceAttachments)
|
||||
|
@ -248,7 +251,7 @@ public class PushMediaSendJob extends PushSendJob {
|
|||
mediaMessageBuilder.withQuote(getQuoteFor(message).orElse(null));
|
||||
}
|
||||
|
||||
if (message.getGiftBadge() != null) {
|
||||
if (message.getGiftBadge() != null || message.isPaymentsNotification()) {
|
||||
mediaMessageBuilder.withBody(null);
|
||||
}
|
||||
|
||||
|
@ -281,19 +284,36 @@ public class PushMediaSendJob extends PushSendJob {
|
|||
}
|
||||
}
|
||||
|
||||
private SignalServiceDataMessage.Payment getPaymentActivation(OutgoingMediaMessage message) {
|
||||
SignalServiceProtos.DataMessage.Payment.Activation.Type type = null;
|
||||
private SignalServiceDataMessage.Payment getPayment(OutgoingMediaMessage message) {
|
||||
if (message.isPaymentsNotification()) {
|
||||
UUID paymentUuid = UuidUtil.parseOrThrow(message.getBody());
|
||||
PaymentDatabase.PaymentTransaction payment = SignalDatabase.payments().getPayment(paymentUuid);
|
||||
|
||||
if (message.isRequestToActivatePayments()) {
|
||||
type = SignalServiceProtos.DataMessage.Payment.Activation.Type.REQUEST;
|
||||
} else if (message.isPaymentsActivated()) {
|
||||
type = SignalServiceProtos.DataMessage.Payment.Activation.Type.ACTIVATED;
|
||||
}
|
||||
if (payment == null) {
|
||||
Log.w(TAG, "Could not find payment, cannot send notification " + paymentUuid);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
return new SignalServiceDataMessage.Payment(null, new SignalServiceDataMessage.PaymentActivation(type));
|
||||
if (payment.getReceipt() == null) {
|
||||
Log.w(TAG, "Could not find payment receipt, cannot send notification " + paymentUuid);
|
||||
return null;
|
||||
}
|
||||
|
||||
return new SignalServiceDataMessage.Payment(new SignalServiceDataMessage.PaymentNotification(payment.getReceipt(), payment.getNote()), null);
|
||||
} else {
|
||||
return null;
|
||||
SignalServiceProtos.DataMessage.Payment.Activation.Type type = null;
|
||||
|
||||
if (message.isRequestToActivatePayments()) {
|
||||
type = SignalServiceProtos.DataMessage.Payment.Activation.Type.REQUEST;
|
||||
} else if (message.isPaymentsActivated()) {
|
||||
type = SignalServiceProtos.DataMessage.Payment.Activation.Type.ACTIVATED;
|
||||
}
|
||||
|
||||
if (type != null) {
|
||||
return new SignalServiceDataMessage.Payment(null, new SignalServiceDataMessage.PaymentActivation(type));
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ import org.thoughtcrime.securesms.jobmanager.Data
|
|||
import org.thoughtcrime.securesms.jobmanager.Job
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore
|
||||
import org.thoughtcrime.securesms.mms.OutgoingPaymentsActivatedMessages
|
||||
import org.thoughtcrime.securesms.net.NotPushRegisteredException
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.sms.MessageSender
|
||||
|
||||
/**
|
||||
|
@ -27,6 +29,10 @@ class SendPaymentsActivatedJob(parameters: Parameters) : BaseJob(parameters) {
|
|||
|
||||
@Suppress("UsePropertyAccessSyntax")
|
||||
override fun onRun() {
|
||||
if (!Recipient.self().isRegistered) {
|
||||
throw NotPushRegisteredException()
|
||||
}
|
||||
|
||||
if (!SignalStore.paymentsValues().mobileCoinPaymentsEnabled()) {
|
||||
Log.w(TAG, "Payments aren't enabled, not going to attempt to send activation messages.")
|
||||
return
|
||||
|
|
|
@ -40,7 +40,12 @@ final class MessageRecordLiveData extends LiveData<MessageRecord> {
|
|||
@WorkerThread
|
||||
private synchronized void retrieve(MessageDatabase messageDatabase) {
|
||||
try {
|
||||
final MessageRecord record = messageDatabase.getMessageRecord(messageId.getId());
|
||||
MessageRecord record = messageDatabase.getMessageRecord(messageId.getId());
|
||||
|
||||
if (record.isPaymentNotification()) {
|
||||
record = SignalDatabase.payments().updateMessageWithPayment(record);
|
||||
}
|
||||
|
||||
postValue(record);
|
||||
ApplicationDependencies.getDatabaseObserver().registerVerboseConversationObserver(record.getThreadId(), observer);
|
||||
} catch (NoSuchMessageException ignored) {
|
||||
|
|
|
@ -297,7 +297,7 @@ public final class MessageContentProcessor {
|
|||
else if (message.getRemoteDelete().isPresent()) messageId = handleRemoteDelete(content, message, senderRecipient);
|
||||
else if (message.isActivatePaymentsRequest()) messageId = handlePaymentActivation(content, message, smsMessageId, senderRecipient, receivedTime, true, false);
|
||||
else if (message.isPaymentsActivated()) messageId = handlePaymentActivation(content, message, smsMessageId, senderRecipient, receivedTime, false, true);
|
||||
else if (message.getPayment().isPresent()) handlePayment(content, message, senderRecipient);
|
||||
else if (message.getPayment().isPresent()) messageId = handlePayment(content, message, smsMessageId, senderRecipient, receivedTime);
|
||||
else if (message.getStoryContext().isPresent()) messageId = handleStoryReply(content, message, senderRecipient, receivedTime);
|
||||
else if (message.getGiftBadge().isPresent()) messageId = handleGiftMessage(content, message, senderRecipient, threadRecipient, receivedTime);
|
||||
else if (isMediaMessage) messageId = handleMediaMessage(content, message, smsMessageId, senderRecipient, threadRecipient, receivedTime);
|
||||
|
@ -433,7 +433,13 @@ public final class MessageContentProcessor {
|
|||
return receivedTime;
|
||||
}
|
||||
|
||||
private void handlePayment(@NonNull SignalServiceContent content, @NonNull SignalServiceDataMessage message, @NonNull Recipient senderRecipient) {
|
||||
private @Nullable MessageId handlePayment(@NonNull SignalServiceContent content,
|
||||
@NonNull SignalServiceDataMessage message,
|
||||
@NonNull Optional<Long> smsMessageId,
|
||||
@NonNull Recipient senderRecipient,
|
||||
long receivedTime)
|
||||
throws StorageFailedException
|
||||
{
|
||||
log(content.getTimestamp(), "Payment message.");
|
||||
|
||||
if (!message.getPayment().isPresent()) {
|
||||
|
@ -442,13 +448,14 @@ public final class MessageContentProcessor {
|
|||
|
||||
if (!message.getPayment().get().getPaymentNotification().isPresent()) {
|
||||
warn(content.getTimestamp(), "Ignoring payment message without notification");
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
SignalServiceDataMessage.PaymentNotification paymentNotification = message.getPayment().get().getPaymentNotification().get();
|
||||
PaymentDatabase paymentDatabase = SignalDatabase.payments();
|
||||
UUID uuid = UUID.randomUUID();
|
||||
String queue = "Payment_" + PushProcessMessageJob.getQueueName(senderRecipient.getId());
|
||||
MessageId messageId = null;
|
||||
|
||||
try {
|
||||
paymentDatabase.createIncomingPayment(uuid,
|
||||
|
@ -457,18 +464,37 @@ public final class MessageContentProcessor {
|
|||
paymentNotification.getNote(),
|
||||
Money.MobileCoin.ZERO,
|
||||
Money.MobileCoin.ZERO,
|
||||
paymentNotification.getReceipt());
|
||||
paymentNotification.getReceipt(),
|
||||
FeatureFlags.paymentsInChatMessages());
|
||||
|
||||
if (FeatureFlags.paymentsInChatMessages()) {
|
||||
IncomingMediaMessage mediaMessage = IncomingMediaMessage.createIncomingPaymentNotification(senderRecipient.getId(),
|
||||
content,
|
||||
receivedTime,
|
||||
TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()),
|
||||
uuid);
|
||||
|
||||
Optional<InsertResult> insertResult = SignalDatabase.mms().insertSecureDecryptedMessageInbox(mediaMessage, -1);
|
||||
smsMessageId.ifPresent(smsId -> SignalDatabase.sms().deleteMessage(smsId));
|
||||
if (insertResult.isPresent()) {
|
||||
messageId = new MessageId(insertResult.get().getMessageId(), true);
|
||||
ApplicationDependencies.getMessageNotifier().updateNotification(context, ConversationId.forConversation(insertResult.get().getThreadId()));
|
||||
}
|
||||
}
|
||||
} catch (PaymentDatabase.PublicKeyConflictException e) {
|
||||
warn(content.getTimestamp(), "Ignoring payment with public key already in database");
|
||||
return;
|
||||
} catch (SerializationException e) {
|
||||
warn(content.getTimestamp(), "Ignoring payment with bad data.", e);
|
||||
} catch (MmsException e) {
|
||||
throw new StorageFailedException(e, content.getSender().getIdentifier(), content.getSenderDevice());
|
||||
} finally {
|
||||
ApplicationDependencies.getJobManager()
|
||||
.startChain(new PaymentTransactionCheckJob(uuid, queue))
|
||||
.then(PaymentLedgerUpdateJob.updateLedger())
|
||||
.enqueue();
|
||||
}
|
||||
|
||||
ApplicationDependencies.getJobManager()
|
||||
.startChain(new PaymentTransactionCheckJob(uuid, queue))
|
||||
.then(PaymentLedgerUpdateJob.updateLedger())
|
||||
.enqueue();
|
||||
return messageId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2926,9 +2952,15 @@ public final class MessageContentProcessor {
|
|||
.toList());
|
||||
}
|
||||
}
|
||||
|
||||
if (message.isPaymentNotification()) {
|
||||
message = SignalDatabase.payments().updateMessageWithPayment(message);
|
||||
}
|
||||
}
|
||||
|
||||
return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments, mentions, QuoteModel.Type.fromDataMessageType(quote.get().getType())));
|
||||
String body = message.isPaymentNotification() ? message.getDisplayBody(context).toString() : message.getBody();
|
||||
|
||||
return Optional.of(new QuoteModel(quote.get().getId(), author, body, false, attachments, mentions, QuoteModel.Type.fromDataMessageType(quote.get().getType())));
|
||||
} else if (message != null) {
|
||||
warn("Found the target for the quote, but it's flagged as remotely deleted.");
|
||||
}
|
||||
|
|
|
@ -12,8 +12,10 @@ import org.thoughtcrime.securesms.groups.GroupId
|
|||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceContent
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupV2
|
||||
import java.util.Optional
|
||||
import java.util.UUID
|
||||
|
||||
class IncomingMediaMessage(
|
||||
val from: RecipientId?,
|
||||
|
@ -39,6 +41,7 @@ class IncomingMediaMessage(
|
|||
linkPreviews: List<LinkPreview> = emptyList(),
|
||||
mentions: List<Mention> = emptyList(),
|
||||
val giftBadge: GiftBadge? = null,
|
||||
val isPaymentsNotification: Boolean = false,
|
||||
val isActivatePaymentsRequest: Boolean = false,
|
||||
val isPaymentsActivated: Boolean = false
|
||||
) {
|
||||
|
@ -138,4 +141,28 @@ class IncomingMediaMessage(
|
|||
isActivatePaymentsRequest = activatePaymentsRequest,
|
||||
isPaymentsActivated = paymentsActivated
|
||||
)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun createIncomingPaymentNotification(
|
||||
from: RecipientId,
|
||||
content: SignalServiceContent,
|
||||
receivedTime: Long,
|
||||
expiresIn: Long,
|
||||
paymentUuid: UUID
|
||||
): IncomingMediaMessage {
|
||||
return IncomingMediaMessage(
|
||||
from = from,
|
||||
body = paymentUuid.toString(),
|
||||
sentTimeMillis = content.timestamp,
|
||||
serverTimeMillis = content.serverReceivedTimestamp,
|
||||
receivedTimeMillis = receivedTime,
|
||||
expiresIn = expiresIn,
|
||||
isUnidentified = content.isNeedsReceipt,
|
||||
serverGuid = content.serverUuid,
|
||||
isPushMessage = true,
|
||||
isPaymentsNotification = true
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -193,6 +193,10 @@ public class OutgoingMediaMessage {
|
|||
return false;
|
||||
}
|
||||
|
||||
public boolean isPaymentsNotification() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isRequestToActivatePayments() {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -23,12 +23,14 @@ class OutgoingRequestToActivatePaymentMessages(
|
|||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
null, emptyList(), emptyList(), emptyList(),
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
null
|
||||
) {
|
||||
override fun isRequestToActivatePayments(): Boolean {
|
||||
return true
|
||||
}
|
||||
override fun isRequestToActivatePayments(): Boolean = true
|
||||
override fun isUrgent(): Boolean = false
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -50,10 +52,37 @@ class OutgoingPaymentsActivatedMessages(
|
|||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
null, emptyList(), emptyList(), emptyList(),
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
null
|
||||
) {
|
||||
override fun isPaymentsActivated(): Boolean {
|
||||
return true
|
||||
}
|
||||
override fun isPaymentsActivated(): Boolean = true
|
||||
override fun isUrgent(): Boolean = false
|
||||
}
|
||||
|
||||
class OutgoingPaymentsNotificationMessage(
|
||||
recipient: Recipient,
|
||||
paymentUuid: String,
|
||||
sentTimeMillis: Long,
|
||||
expiresIn: Long
|
||||
) : OutgoingSecureMediaMessage(
|
||||
recipient,
|
||||
paymentUuid,
|
||||
LinkedList(),
|
||||
sentTimeMillis,
|
||||
ThreadDatabase.DistributionTypes.CONVERSATION,
|
||||
expiresIn,
|
||||
false,
|
||||
StoryType.NONE,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
emptyList(),
|
||||
null
|
||||
) {
|
||||
override fun isPaymentsNotification(): Boolean = true
|
||||
}
|
|
@ -200,6 +200,8 @@ class MessageNotification(threadRecipient: Recipient, record: MessageRecord) : N
|
|||
ThreadBodyUtil.getFormattedBodyFor(context, record)
|
||||
} else if (record.isStoryReaction()) {
|
||||
ThreadBodyUtil.getFormattedBodyFor(context, record)
|
||||
} else if (record.isPaymentNotification()) {
|
||||
ThreadBodyUtil.getFormattedBodyFor(context, record)
|
||||
} else {
|
||||
MentionUtil.updateBodyWithDisplayNames(context, record)
|
||||
}
|
||||
|
|
|
@ -89,6 +89,10 @@ public final class MoneyView extends AppCompatTextView {
|
|||
}
|
||||
|
||||
public void setMoney(@NonNull Money money, boolean highlightCurrency, long timestamp) {
|
||||
setMoney(money, timestamp, (highlightCurrency ? new ForegroundColorSpan(ContextCompat.getColor(getContext(), R.color.payment_currency_code_foreground_color)) : null));
|
||||
}
|
||||
|
||||
public void setMoney(@NonNull Money money, long timestamp, @Nullable Object currencySpan) {
|
||||
String balance = money.toString(formatterOptions);
|
||||
int currencyIndex = balance.indexOf(money.getCurrency().getCurrencyCode());
|
||||
|
||||
|
@ -102,8 +106,8 @@ public final class MoneyView extends AppCompatTextView {
|
|||
balanceSpan = new SpannableString(balance);
|
||||
}
|
||||
|
||||
if (highlightCurrency) {
|
||||
balanceSpan.setSpan(new ForegroundColorSpan(ContextCompat.getColor(getContext(), R.color.payment_currency_code_foreground_color)), currencyIndex, currencyIndex + money.getCurrency().getCurrencyCode().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
if (currencySpan != null) {
|
||||
balanceSpan.setSpan(currencySpan, currencyIndex, currencyIndex + money.getCurrency().getCurrencyCode().length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
|
||||
setText(balanceSpan);
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
package org.thoughtcrime.securesms.payments.preferences;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.navigation.NavController;
|
||||
import androidx.navigation.Navigation;
|
||||
|
@ -10,10 +13,14 @@ import org.thoughtcrime.securesms.PassphraseRequiredActivity;
|
|||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.dependencies.ApplicationDependencies;
|
||||
import org.thoughtcrime.securesms.jobs.PaymentLedgerUpdateJob;
|
||||
import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsFragmentArgs;
|
||||
import org.thoughtcrime.securesms.payments.preferences.details.PaymentDetailsParcelable;
|
||||
import org.thoughtcrime.securesms.util.DynamicNoActionBarTheme;
|
||||
import org.thoughtcrime.securesms.util.DynamicTheme;
|
||||
import org.thoughtcrime.securesms.util.navigation.SafeNavigation;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
public class PaymentsActivity extends PassphraseRequiredActivity {
|
||||
|
||||
public static final String EXTRA_PAYMENTS_STARTING_ACTION = "payments_starting_action";
|
||||
|
@ -21,6 +28,15 @@ public class PaymentsActivity extends PassphraseRequiredActivity {
|
|||
|
||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||
|
||||
public static Intent navigateToPaymentDetails(@NonNull Context context, @NonNull UUID paymentId) {
|
||||
Intent intent = new Intent(context, PaymentsActivity.class);
|
||||
|
||||
intent.putExtra(EXTRA_PAYMENTS_STARTING_ACTION, R.id.action_directly_to_paymentDetails);
|
||||
intent.putExtra(EXTRA_STARTING_ARGUMENTS, new PaymentDetailsFragmentArgs.Builder(PaymentDetailsParcelable.forUuid(paymentId)).build().toBundle());
|
||||
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState, boolean ready) {
|
||||
dynamicTheme.onCreate(this);
|
||||
|
|
|
@ -254,7 +254,7 @@ public class MessageSender {
|
|||
final long threadId,
|
||||
final boolean forceSms,
|
||||
@Nullable final String metricId,
|
||||
final SmsDatabase.InsertListener insertListener)
|
||||
@Nullable final SmsDatabase.InsertListener insertListener)
|
||||
{
|
||||
Log.i(TAG, "Sending media message to " + message.getRecipient().getId() + ", thread: " + threadId);
|
||||
try {
|
||||
|
|
|
@ -107,6 +107,7 @@ public final class FeatureFlags {
|
|||
public static final String CREDIT_CARD_DISABLED_REGIONS = "global.donations.ccDisabledRegions";
|
||||
public static final String PAYPAL_DISABLED_REGIONS = "global.donations.paypalDisabledRegions";
|
||||
private static final String CDS_HARD_LIMIT = "android.cds.hardLimit";
|
||||
private static final String PAYMENTS_IN_CHAT_MESSAGES = "android.payments.inChatMessages";
|
||||
|
||||
/**
|
||||
* We will only store remote values for flags in this set. If you want a flag to be controllable
|
||||
|
@ -166,7 +167,8 @@ public final class FeatureFlags {
|
|||
CREDIT_CARD_DISABLED_REGIONS,
|
||||
PAYPAL_DISABLED_REGIONS,
|
||||
KEEP_MUTED_CHATS_ARCHIVED,
|
||||
CDS_HARD_LIMIT
|
||||
CDS_HARD_LIMIT,
|
||||
PAYMENTS_IN_CHAT_MESSAGES
|
||||
);
|
||||
|
||||
@VisibleForTesting
|
||||
|
@ -231,7 +233,8 @@ public final class FeatureFlags {
|
|||
CREDIT_CARD_PAYMENTS,
|
||||
PAYMENTS_REQUEST_ACTIVATE_FLOW,
|
||||
KEEP_MUTED_CHATS_ARCHIVED,
|
||||
CDS_HARD_LIMIT
|
||||
CDS_HARD_LIMIT,
|
||||
PAYMENTS_IN_CHAT_MESSAGES
|
||||
);
|
||||
|
||||
/**
|
||||
|
@ -565,6 +568,11 @@ public final class FeatureFlags {
|
|||
return getBoolean(PAYMENTS_REQUEST_ACTIVATE_FLOW, false);
|
||||
}
|
||||
|
||||
/** Whether client supports processing a payment notification as a in-chat message */
|
||||
public static boolean paymentsInChatMessages() {
|
||||
return getBoolean(PAYMENTS_IN_CHAT_MESSAGES, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether users can enable keeping conversations with incoming messages archived if the conversation is muted.
|
||||
*/
|
||||
|
|
|
@ -42,17 +42,11 @@ fun MessageRecord.hasThumbnail(): Boolean =
|
|||
isMms && (this as MmsMessageRecord).slideDeck.thumbnailSlide != null
|
||||
|
||||
fun MessageRecord.isStoryReaction(): Boolean =
|
||||
isMms && MmsSmsColumns.Types.isStoryReaction((this as MmsMessageRecord).type)
|
||||
isMms && MmsSmsColumns.Types.isStoryReaction(type)
|
||||
|
||||
fun MessageRecord.isStory(): Boolean =
|
||||
isMms && (this as MmsMessageRecord).storyType.isStory
|
||||
|
||||
fun MessageRecord.isPaymentActivationRequest(): Boolean =
|
||||
isMms && MmsSmsColumns.Types.isRequestToActivatePayments((this as MmsMessageRecord).type)
|
||||
|
||||
fun MessageRecord.isPaymentsActivated(): Boolean =
|
||||
isMms && MmsSmsColumns.Types.isPaymentsActivated((this as MmsMessageRecord).type)
|
||||
|
||||
fun MessageRecord.isBorderless(context: Context): Boolean {
|
||||
return isCaptionlessMms(context) &&
|
||||
hasThumbnail() &&
|
||||
|
@ -142,7 +136,8 @@ fun MessageRecord.isTextOnly(context: Context): Boolean {
|
|||
!hasSharedContact() &&
|
||||
!hasSticker() &&
|
||||
!isCaptionlessMms(context) &&
|
||||
!hasGiftBadge()
|
||||
!hasGiftBadge() &&
|
||||
!isPaymentNotification()
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@ public final class RemoteDeleteUtil {
|
|||
(!message.getRecipient().isGroup() || message.getRecipient().isActiveGroup()) &&
|
||||
!message.isRemoteDelete() &&
|
||||
!MessageRecordUtil.hasGiftBadge(message) &&
|
||||
!message.isPaymentNotification() &&
|
||||
(((currentTime - message.getDateSent()) < SEND_THRESHOLD) || message.getRecipient().isSelf());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<org.thoughtcrime.securesms.conversation.ui.payment.PaymentMessageView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
|
@ -208,6 +208,12 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/conversation_item_received_gift" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/payment_view_stub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/conversation_item_payment" />
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/conversation_item_body"
|
||||
style="@style/Signal.Text.Body"
|
||||
|
|
|
@ -139,6 +139,12 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/conversation_item_sent_gift" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/payment_view_stub"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout="@layout/conversation_item_payment" />
|
||||
|
||||
<ViewStub
|
||||
android:id="@+id/revealable_view_stub"
|
||||
android:layout_width="wrap_content"
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="8dp"
|
||||
android:paddingHorizontal="8dp">
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/payment_direction"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/Signal.Text.BodyMedium"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
tools:text="Maya sent you"
|
||||
tools:textColor="@color/core_grey_60" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/payment_amount_layout"
|
||||
android:layout_width="220dp"
|
||||
android:layout_height="80dp"
|
||||
android:layout_marginTop="5dp"
|
||||
android:background="@drawable/rounded_rectangle_secondary_18"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/payment_direction">
|
||||
|
||||
<org.thoughtcrime.securesms.payments.MoneyView
|
||||
android:id="@+id/payment_amount"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
app:autoSizeMaxTextSize="28sp"
|
||||
app:autoSizeMinTextSize="14sp"
|
||||
app:autoSizeTextType="uniform"
|
||||
app:money="MOB:275000000000000" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
|
||||
android:id="@+id/payment_note"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="9dp"
|
||||
android:textAppearance="@style/TextAppearance.Signal.Body1"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/payment_amount_layout"
|
||||
tools:text="It was my treat" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1463,6 +1463,8 @@
|
|||
<string name="MessageRecord__you_will_no_longer_be_able_to_send_sms_messages_from_signal_soon">You will no longer be able to send SMS messages from Signal soon. Invite %1$s to Signal to keep the conversation here.</string>
|
||||
<!-- In-conversation update message to indicate that the current contact is sms only and will need to migrate to signal to continue the conversation in signal. -->
|
||||
<string name="MessageRecord__you_can_no_longer_send_sms_messages_in_signal">You can no longer send SMS messages in Signal. Invite %1$s to Signal to keep the conversation here.</string>
|
||||
<!-- Body for quote when message being quoted is an in-app payment message -->
|
||||
<string name="MessageRecord__payment_s">Payment: %1$s</string>
|
||||
|
||||
<!-- MessageRequestBottomView -->
|
||||
<string name="MessageRequestBottomView_accept">Accept</string>
|
||||
|
@ -1960,6 +1962,8 @@
|
|||
<string name="ThreadRecord__reacted_s_to_your_story">Reacted %1$s to your story</string>
|
||||
<!-- Displayed in the conversation list when you reacted to someone\'s story -->
|
||||
<string name="ThreadRecord__reacted_s_to_their_story">Reacted %1$s to their story</string>
|
||||
<!-- Displayed in the conversation list when your most recent message is a payment to or from the person the conversation is with -->
|
||||
<string name="ThreadRecord_payment">Payment</string>
|
||||
|
||||
<!-- UpdateApkReadyListener -->
|
||||
<string name="UpdateApkReadyListener_Signal_update">Signal update</string>
|
||||
|
@ -3120,6 +3124,8 @@
|
|||
<string name="conversation_selection__menu_resend_message">Resend</string>
|
||||
<!-- Button to select a message and enter selection mode -->
|
||||
<string name="conversation_selection__menu_multi_select">Select</string>
|
||||
<!-- Button to view a in-chat payment message's full payment details -->
|
||||
<string name="conversation_selection__menu_payment_details">Payment details</string>
|
||||
|
||||
<!-- conversation_expiring_on -->
|
||||
|
||||
|
@ -5556,6 +5562,12 @@
|
|||
<!-- A toast that will be shown if we are unable to open the user's default contacts app. -->
|
||||
<string name="CdsPermanentErrorBottomSheet_no_contacts_toast">No contacts app found</string>
|
||||
|
||||
<!-- PaymentMessageView -->
|
||||
<!-- In-chat conversation message shown when you sent a payment to another person, placeholder is the other person name -->
|
||||
<string name="PaymentMessageView_you_sent_s">You sent %1$s</string>
|
||||
<!-- In-chat conversation message shown when another person sent a payment to you, placeholder is the other person name -->
|
||||
<string name="PaymentMessageView_s_sent_you">%1$s sent you</string>
|
||||
|
||||
<!-- EOF -->
|
||||
|
||||
</resources>
|
||||
|
|
|
@ -52,9 +52,10 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.PUSH_MESSAGE_BIT
|
|||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SECURE_MESSAGE_BIT
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SMS_EXPORT_TYPE
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPES_MASK
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_GIFT_BADGE
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_PAYMENTS_ACTIVATED
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_PAYMENTS_NOTIFICATION
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.SPECIAL_TYPE_STORY_REACTION
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.THREAD_MERGE_TYPE
|
||||
import org.thoughtcrime.securesms.database.MmsSmsColumns.Types.UNSUPPORTED_MESSAGE_TYPE
|
||||
|
@ -121,7 +122,8 @@ object MessageBitmaskColumnTransformer : ColumnTransformer {
|
|||
isSpecialType:${type and SPECIAL_TYPES_MASK != 0L}
|
||||
isStoryReaction:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_STORY_REACTION}
|
||||
isGiftBadge:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_GIFT_BADGE}
|
||||
isRequestToActivatePayments:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_ACTIVATE_PAYMENTS_REQUEST}
|
||||
isPaymentsNotificaiton:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_PAYMENTS_NOTIFICATION}
|
||||
isRequestToActivatePayments:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_PAYMENTS_ACTIVATE_REQUEST}
|
||||
isPaymentsActivated:${type and SPECIAL_TYPES_MASK == SPECIAL_TYPE_PAYMENTS_ACTIVATED}
|
||||
""".trimIndent()
|
||||
|
||||
|
|
|
@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
|||
import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck
|
||||
import org.thoughtcrime.securesms.payments.Payment
|
||||
import org.thoughtcrime.securesms.recipients.Recipient
|
||||
import org.thoughtcrime.securesms.stickers.StickerLocator
|
||||
import org.thoughtcrime.securesms.util.MediaUtil
|
||||
|
@ -135,7 +136,8 @@ object FakeMessageRecords {
|
|||
messageRanges: BodyRangeList? = null,
|
||||
storyType: StoryType = StoryType.NONE,
|
||||
parentStoryId: ParentStoryId? = null,
|
||||
giftBadge: GiftBadge? = null
|
||||
giftBadge: GiftBadge? = null,
|
||||
payment: Payment? = null
|
||||
): MediaMmsMessageRecord {
|
||||
return MediaMmsMessageRecord(
|
||||
id,
|
||||
|
@ -171,7 +173,8 @@ object FakeMessageRecords {
|
|||
messageRanges,
|
||||
storyType,
|
||||
parentStoryId,
|
||||
giftBadge
|
||||
giftBadge,
|
||||
payment
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
Ładowanie…
Reference in New Issue