Add in-chat payment messages.

main
Cody Henthorne 2022-11-10 14:45:45 -05:00 zatwierdzone przez Greyson Parrelli
rodzic 28193c2f61
commit 1dc29fda12
43 zmienionych plików z 708 dodań i 98 usunięć

Wyświetl plik

@ -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());
}
}
}

Wyświetl plik

@ -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;

Wyświetl plik

@ -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);

Wyświetl plik

@ -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,
}

Wyświetl plik

@ -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 -> {

Wyświetl plik

@ -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);

Wyświetl plik

@ -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)

Wyświetl plik

@ -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")
}
}

Wyświetl plik

@ -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));
}
}

Wyświetl plik

@ -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));

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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();

Wyświetl plik

@ -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";

Wyświetl plik

@ -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));
}

Wyświetl plik

@ -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);
}
}

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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() {

Wyświetl plik

@ -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());

Wyświetl plik

@ -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)
{

Wyświetl plik

@ -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))
)
}
}
}

Wyświetl plik

@ -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))

Wyświetl plik

@ -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;
}
}
}

Wyświetl plik

@ -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

Wyświetl plik

@ -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) {

Wyświetl plik

@ -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.");
}

Wyświetl plik

@ -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
)
}
}
}

Wyświetl plik

@ -193,6 +193,10 @@ public class OutgoingMediaMessage {
return false;
}
public boolean isPaymentsNotification() {
return false;
}
public boolean isRequestToActivatePayments() {
return false;
}

Wyświetl plik

@ -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
}

Wyświetl plik

@ -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)
}

Wyświetl plik

@ -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);

Wyświetl plik

@ -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);

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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.
*/

Wyświetl plik

@ -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()
)
}

Wyświetl plik

@ -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());
}
}

Wyświetl plik

@ -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" />

Wyświetl plik

@ -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"

Wyświetl plik

@ -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"

Wyświetl plik

@ -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>

Wyświetl plik

@ -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>

Wyświetl plik

@ -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()

Wyświetl plik

@ -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
)
}
}