diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java index 27e14dc5f..f0b93ae31 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/InputPanel.java @@ -179,9 +179,10 @@ public class InputPanel extends LinearLayout long id, @NonNull Recipient author, @NonNull CharSequence body, - @NonNull SlideDeck attachments) + @NonNull SlideDeck attachments, + @NonNull QuoteModel.Type quoteType) { - this.quoteView.setQuote(glideRequests, id, author, body, false, attachments, null, null); + this.quoteView.setQuote(glideRequests, id, author, body, false, attachments, null, null, quoteType); int originalHeight = this.quoteView.getVisibility() == VISIBLE ? this.quoteView.getMeasuredHeight() : 0; @@ -256,7 +257,7 @@ public class InputPanel extends LinearLayout public Optional getQuote() { if (quoteView.getQuoteId() > 0 && quoteView.getVisibility() == View.VISIBLE) { - return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody().toString(), false, quoteView.getAttachments(), quoteView.getMentions())); + return Optional.of(new QuoteModel(quoteView.getQuoteId(), quoteView.getAuthor().getId(), quoteView.getBody().toString(), false, quoteView.getAttachments(), quoteView.getMentions(), quoteView.getQuoteType())); } else { return Optional.empty(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java b/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java index 51959c024..ccfbc5a6b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/QuoteView.java @@ -17,6 +17,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.RequiresApi; +import androidx.appcompat.content.res.AppCompatResources; import androidx.core.content.ContextCompat; import com.bumptech.glide.load.engine.DiskCacheStrategy; @@ -30,6 +31,7 @@ import org.thoughtcrime.securesms.conversation.colors.ChatColors; import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.GlideRequests; +import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.Slide; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.LiveRecipient; @@ -87,16 +89,17 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { private EmojiImageView missingStoryReaction; private EmojiImageView storyReactionEmoji; - private long id; - private LiveRecipient author; - private CharSequence body; - private TextView mediaDescriptionText; - private TextView missingLinkText; - private SlideDeck attachments; - private MessageType messageType; - private int largeCornerRadius; - private int smallCornerRadius; - private CornerMask cornerMask; + private long id; + private LiveRecipient author; + private CharSequence body; + private TextView mediaDescriptionText; + private TextView missingLinkText; + private SlideDeck attachments; + private MessageType messageType; + private int largeCornerRadius; + private int smallCornerRadius; + private CornerMask cornerMask; + private QuoteModel.Type quoteType; private int thumbHeight; private int thumbWidth; @@ -206,7 +209,8 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { boolean originalMissing, @NonNull SlideDeck attachments, @Nullable ChatColors chatColors, - @Nullable String storyReaction) + @Nullable String storyReaction, + @NonNull QuoteModel.Type quoteType) { if (this.author != null) this.author.removeForeverObserver(this); @@ -214,10 +218,11 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { this.author = author.live(); this.body = body; this.attachments = attachments; + this.quoteType = quoteType; this.author.observeForever(this); setQuoteAuthor(author); - setQuoteText(body, attachments, originalMissing, storyReaction); + setQuoteText(resolveBody(body, quoteType), attachments, originalMissing, storyReaction); setQuoteAttachment(glideRequests, body, attachments, originalMissing); setQuoteMissingFooter(originalMissing); @@ -228,6 +233,10 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { } } + private @Nullable CharSequence resolveBody(@Nullable CharSequence body, @NonNull QuoteModel.Type quoteType) { + return quoteType == QuoteModel.Type.GIFT_BADGE ? getContext().getString(R.string.QuoteView__gift) : body; + } + public void setTopCornerSizes(boolean topLeftLarge, boolean topRightLarge) { cornerMask.setTopLeftRadius(topLeftLarge ? largeCornerRadius : smallCornerRadius); cornerMask.setTopRightRadius(topRightLarge ? largeCornerRadius : smallCornerRadius); @@ -386,6 +395,18 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { return; } + if (quoteType == QuoteModel.Type.GIFT_BADGE) { + attachmentVideoOverlayView.setVisibility(GONE); + attachmentContainerView.setVisibility(GONE); + thumbnailView.setVisibility(VISIBLE); + glideRequests.load(R.drawable.ic_gift_thumbnail) + .centerCrop() + .override(thumbWidth, thumbHeight) + .diskCacheStrategy(DiskCacheStrategy.RESOURCE) + .into(thumbnailView); + return; + } + Slide imageVideoSlide = slideDeck.getSlides().stream().filter(s -> s.hasImage() || s.hasVideo() || s.hasSticker()).findFirst().orElse(null); Slide documentSlide = slideDeck.getSlides().stream().filter(Slide::hasDocument).findFirst().orElse(null); Slide viewOnceSlide = slideDeck.getSlides().stream().filter(Slide::hasViewOnce).findFirst().orElse(null); @@ -459,6 +480,10 @@ public class QuoteView extends FrameLayout implements RecipientForeverObserver { return attachments.asAttachments(); } + public @NonNull QuoteModel.Type getQuoteType() { + return quoteType; + } + public @NonNull List getMentions() { return MentionAnnotation.getMentionsFromAnnotations(body); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index 7bfb60431..da5de5a91 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -1471,7 +1471,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo quote.isOriginalMissing(), quote.getAttachment(), chatColors, - isStoryReaction(current) ? current.getBody() : null); + isStoryReaction(current) ? current.getBody() : null, + quote.getQuoteType()); quoteView.setVisibility(View.VISIBLE); quoteView.setTextSize(TypedValue.COMPLEX_UNIT_SP, SignalStore.settings().getMessageFontSize()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java index 1f39d0f06..28e7e7707 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationParentFragment.java @@ -281,6 +281,7 @@ import org.thoughtcrime.securesms.util.FullscreenHelper; import org.thoughtcrime.securesms.util.IdentityUtil; import org.thoughtcrime.securesms.util.LifecycleDisposable; import org.thoughtcrime.securesms.util.MediaUtil; +import org.thoughtcrime.securesms.util.MessageRecordUtil; import org.thoughtcrime.securesms.util.MessageUtil; import org.thoughtcrime.securesms.util.PlayStoreUtil; import org.thoughtcrime.securesms.util.ServiceUtil; @@ -3876,7 +3877,8 @@ public class ConversationParentFragment extends Fragment messageRecord.getDateSent(), author, body, - slideDeck); + slideDeck, + MessageRecordUtil.getRecordQuoteType(messageRecord)); } else if (messageRecord.isMms() && !((MmsMessageRecord) messageRecord).getLinkPreviews().isEmpty()) { LinkPreview linkPreview = ((MmsMessageRecord) messageRecord).getLinkPreviews().get(0); @@ -3890,7 +3892,8 @@ public class ConversationParentFragment extends Fragment messageRecord.getDateSent(), author, conversationMessage.getDisplayBody(requireContext()), - slideDeck); + slideDeck, + MessageRecordUtil.getRecordQuoteType(messageRecord)); } else { SlideDeck slideDeck = messageRecord.isMms() ? ((MmsMessageRecord) messageRecord).getSlideDeck() : new SlideDeck(); @@ -3904,7 +3907,8 @@ public class ConversationParentFragment extends Fragment messageRecord.getDateSent(), author, conversationMessage.getDisplayBody(requireContext()), - slideDeck); + slideDeck, + MessageRecordUtil.getRecordQuoteType(messageRecord)); } inputPanel.clickOnComposeInput(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java index c60eaed56..8188b9ecf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/MenuState.java @@ -188,7 +188,6 @@ final class MenuState { messageRecord.isSecure() && (!conversationRecipient.isGroup() || conversationRecipient.isActiveGroup()) && !messageRecord.getRecipient().isBlocked() && - !MessageRecordUtil.hasGiftBadge(messageRecord) && !conversationRecipient.isReleaseNotes(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java index 06cf9f37a..0a9235ac6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsDatabase.java @@ -130,6 +130,7 @@ public class MmsDatabase extends MessageDatabase { static final String QUOTE_ATTACHMENT = "quote_attachment"; static final String QUOTE_MISSING = "quote_missing"; static final String QUOTE_MENTIONS = "quote_mentions"; + static final String QUOTE_TYPE = "quote_type"; static final String SHARED_CONTACTS = "shared_contacts"; static final String LINK_PREVIEWS = "previews"; @@ -171,6 +172,7 @@ public class MmsDatabase extends MessageDatabase { QUOTE_ATTACHMENT + " INTEGER DEFAULT -1, " + QUOTE_MISSING + " INTEGER DEFAULT 0, " + QUOTE_MENTIONS + " BLOB DEFAULT NULL," + + QUOTE_TYPE + " INTEGER DEFAULT 0," + SHARED_CONTACTS + " TEXT, " + UNIDENTIFIED + " INTEGER DEFAULT 0, " + LINK_PREVIEWS + " TEXT, " + @@ -209,7 +211,7 @@ public class MmsDatabase extends MessageDatabase { MESSAGE_SIZE, STATUS, TRANSACTION_ID, BODY, PART_COUNT, RECIPIENT_ID, ADDRESS_DEVICE_ID, DELIVERY_RECEIPT_COUNT, READ_RECEIPT_COUNT, MISMATCHED_IDENTITIES, NETWORK_FAILURE, SUBSCRIPTION_ID, - EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_MISSING, QUOTE_MENTIONS, + EXPIRES_IN, EXPIRE_STARTED, NOTIFIED, QUOTE_ID, QUOTE_AUTHOR, QUOTE_BODY, QUOTE_ATTACHMENT, QUOTE_TYPE, QUOTE_MISSING, QUOTE_MENTIONS, SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS_UNREAD, REACTIONS_LAST_SEEN, REMOTE_DELETED, MENTIONS_SELF, NOTIFIED_TIMESTAMP, VIEWED_RECEIPT_COUNT, RECEIPT_TIMESTAMP, MESSAGE_RANGES, STORY_TYPE, PARENT_STORY_ID, @@ -1216,6 +1218,7 @@ public class MmsDatabase extends MessageDatabase { values.putNull(QUOTE_BODY); values.putNull(QUOTE_AUTHOR); values.putNull(QUOTE_ATTACHMENT); + values.putNull(QUOTE_TYPE); values.putNull(QUOTE_ID); values.putNull(LINK_PREVIEWS); values.putNull(SHARED_CONTACTS); @@ -1536,6 +1539,7 @@ public class MmsDatabase extends MessageDatabase { long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_ID)); long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(QUOTE_AUTHOR)); String quoteText = cursor.getString(cursor.getColumnIndexOrThrow(QUOTE_BODY)); + int quoteType = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_TYPE)); boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(QUOTE_MISSING)) == 1; List quoteAttachments = Stream.of(associatedAttachments).filter(Attachment::isQuote).map(a -> (Attachment)a).toList(); List quoteMentions = parseQuoteMentions(context, cursor); @@ -1555,7 +1559,7 @@ public class MmsDatabase extends MessageDatabase { QuoteModel quote = null; if (quoteId > 0 && quoteAuthor > 0 && (!TextUtils.isEmpty(quoteText) || !quoteAttachments.isEmpty())) { - quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments, quoteMentions); + quote = new QuoteModel(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteAttachments, quoteMentions, QuoteModel.Type.fromCode(quoteType)); } if (!TextUtils.isEmpty(mismatchDocument)) { @@ -1739,6 +1743,7 @@ public class MmsDatabase extends MessageDatabase { contentValues.put(QUOTE_ID, retrieved.getQuote().getId()); contentValues.put(QUOTE_BODY, retrieved.getQuote().getText().toString()); contentValues.put(QUOTE_AUTHOR, retrieved.getQuote().getAuthor().serialize()); + contentValues.put(QUOTE_TYPE, retrieved.getQuote().getType().getCode()); contentValues.put(QUOTE_MISSING, retrieved.getQuote().isOriginalMissing() ? 1 : 0); BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(retrieved.getQuote().getMentions()); @@ -2011,6 +2016,7 @@ public class MmsDatabase extends MessageDatabase { contentValues.put(QUOTE_ID, message.getOutgoingQuote().getId()); contentValues.put(QUOTE_AUTHOR, message.getOutgoingQuote().getAuthor().serialize()); contentValues.put(QUOTE_BODY, updated.getBodyAsString()); + contentValues.put(QUOTE_TYPE, message.getOutgoingQuote().getType().getCode()); contentValues.put(QUOTE_MISSING, message.getOutgoingQuote().isOriginalMissing() ? 1 : 0); BodyRangeList mentionsList = MentionUtil.mentionsToBodyRangeList(updated.getMentions()); @@ -2485,7 +2491,8 @@ public class MmsDatabase extends MessageDatabase { quoteText, message.getOutgoingQuote().isOriginalMissing(), new SlideDeck(context, message.getOutgoingQuote().getAttachments()), - quoteMentions) : + quoteMentions, + message.getOutgoingQuote().getType()) : null, message.getSharedContacts(), message.getLinkPreviews(), @@ -2699,6 +2706,7 @@ public class MmsDatabase extends MessageDatabase { long quoteId = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_ID)); long quoteAuthor = cursor.getLong(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_AUTHOR)); CharSequence quoteText = cursor.getString(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_BODY)); + int quoteType = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_TYPE)); boolean quoteMissing = cursor.getInt(cursor.getColumnIndexOrThrow(MmsDatabase.QUOTE_MISSING)) == 1; List quoteMentions = parseQuoteMentions(context, cursor); List attachments = SignalDatabase.attachments().getAttachments(cursor); @@ -2713,7 +2721,7 @@ public class MmsDatabase extends MessageDatabase { quoteMentions = updated.getMentions(); } - return new Quote(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteDeck, quoteMentions); + return new Quote(quoteId, RecipientId.from(quoteAuthor), quoteText, quoteMissing, quoteDeck, quoteMentions, QuoteModel.Type.fromCode(quoteType)); } else { return null; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java index 236cf27ce..3f433a566 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/MmsSmsDatabase.java @@ -99,6 +99,7 @@ public class MmsSmsDatabase extends Database { MmsDatabase.QUOTE_BODY, MmsDatabase.QUOTE_MISSING, MmsDatabase.QUOTE_ATTACHMENT, + MmsDatabase.QUOTE_TYPE, MmsDatabase.QUOTE_MENTIONS, MmsDatabase.SHARED_CONTACTS, MmsDatabase.LINK_PREVIEWS, @@ -777,6 +778,7 @@ public class MmsSmsDatabase extends Database { MmsDatabase.QUOTE_BODY, MmsDatabase.QUOTE_MISSING, MmsDatabase.QUOTE_ATTACHMENT, + MmsDatabase.QUOTE_TYPE, MmsDatabase.QUOTE_MENTIONS, MmsDatabase.SHARED_CONTACTS, MmsDatabase.LINK_PREVIEWS, @@ -813,6 +815,7 @@ public class MmsSmsDatabase extends Database { MmsDatabase.QUOTE_BODY, MmsDatabase.QUOTE_MISSING, MmsDatabase.QUOTE_ATTACHMENT, + MmsDatabase.QUOTE_TYPE, MmsDatabase.QUOTE_MENTIONS, MmsDatabase.SHARED_CONTACTS, MmsDatabase.LINK_PREVIEWS, @@ -878,6 +881,7 @@ public class MmsSmsDatabase extends Database { mmsColumnsPresent.add(MmsDatabase.QUOTE_BODY); mmsColumnsPresent.add(MmsDatabase.QUOTE_MISSING); mmsColumnsPresent.add(MmsDatabase.QUOTE_ATTACHMENT); + mmsColumnsPresent.add(MmsDatabase.QUOTE_TYPE); mmsColumnsPresent.add(MmsDatabase.QUOTE_MENTIONS); mmsColumnsPresent.add(MmsDatabase.SHARED_CONTACTS); mmsColumnsPresent.add(MmsDatabase.LINK_PREVIEWS); diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt index ec3b0c813..1ba361190 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/database/helpers/SignalDatabaseMigrations.kt @@ -195,8 +195,9 @@ object SignalDatabaseMigrations { private const val REMOVE_KNOWN_UNKNOWNS = 139 private const val CDS_V2 = 140 private const val GROUP_SERVICE_ID = 141 + private const val QUOTE_TYPE = 142 - const val DATABASE_VERSION = 141 + const val DATABASE_VERSION = 142 @JvmStatic fun migrate(context: Application, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) { @@ -2528,6 +2529,10 @@ object SignalDatabaseMigrations { if (oldVersion < GROUP_SERVICE_ID) { db.execSQL("ALTER TABLE groups ADD COLUMN auth_service_id TEXT DEFAULT NULL") } + + if (oldVersion < QUOTE_TYPE) { + db.execSQL("ALTER TABLE mms ADD COLUMN quote_type INTEGER DEFAULT 0") + } } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java index f814a0876..795af6af5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/Quote.java @@ -6,6 +6,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import org.thoughtcrime.securesms.components.mention.MentionAnnotation; +import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.mms.SlideDeck; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -13,25 +14,28 @@ import java.util.List; public class Quote { - private final long id; - private final RecipientId author; - private final CharSequence text; - private final boolean missing; - private final SlideDeck attachment; - private final List mentions; + private final long id; + private final RecipientId author; + private final CharSequence text; + private final boolean missing; + private final SlideDeck attachment; + private final List mentions; + private final QuoteModel.Type quoteType; public Quote(long id, @NonNull RecipientId author, @Nullable CharSequence text, boolean missing, @NonNull SlideDeck attachment, - @NonNull List mentions) + @NonNull List mentions, + @NonNull QuoteModel.Type quoteType) { - this.id = id; - this.author = author; - this.missing = missing; - this.attachment = attachment; - this.mentions = mentions; + this.id = id; + this.author = author; + this.missing = missing; + this.attachment = attachment; + this.mentions = mentions; + this.quoteType = quoteType; SpannableString spannable = new SpannableString(text); MentionAnnotation.setMentionAnnotations(spannable, mentions); @@ -40,7 +44,7 @@ public class Quote { } public @NonNull Quote withAttachment(@NonNull SlideDeck updatedAttachment) { - return new Quote(id, author, text, missing, updatedAttachment, mentions); + return new Quote(id, author, text, missing, updatedAttachment, mentions, quoteType); } @@ -63,4 +67,8 @@ public class Quote { public @NonNull SlideDeck getAttachment() { return attachment; } + + public @NonNull QuoteModel.Type getQuoteType() { + return quoteType; + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java index 6747c35a4..976f70537 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java +++ b/app/src/main/java/org/thoughtcrime/securesms/jobs/PushSendJob.java @@ -41,6 +41,7 @@ import org.thoughtcrime.securesms.linkpreview.LinkPreview; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader; import org.thoughtcrime.securesms.mms.OutgoingMediaMessage; import org.thoughtcrime.securesms.mms.PartAuthority; +import org.thoughtcrime.securesms.mms.QuoteModel; import org.thoughtcrime.securesms.net.NotPushRegisteredException; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; @@ -303,6 +304,7 @@ public abstract class PushSendJob extends SendJob { String quoteBody = message.getOutgoingQuote().getText(); RecipientId quoteAuthor = message.getOutgoingQuote().getAuthor(); List quoteMentions = getMentionsFor(message.getOutgoingQuote().getMentions()); + QuoteModel.Type quoteType = message.getOutgoingQuote().getType(); List quoteAttachments = new LinkedList<>(); List filteredAttachments = Stream.of(message.getOutgoingQuote().getAttachments()) .filterNot(a -> MediaUtil.isViewOnceType(a.getContentType())) @@ -351,7 +353,7 @@ public abstract class PushSendJob extends SendJob { if (quoteAuthorRecipient.isMaybeRegistered()) { SignalServiceAddress quoteAddress = RecipientUtil.toSignalServiceAddress(context, quoteAuthorRecipient); - return Optional.of(new SignalServiceDataMessage.Quote(quoteId, quoteAddress, quoteBody, quoteAttachments, quoteMentions)); + return Optional.of(new SignalServiceDataMessage.Quote(quoteId, quoteAddress, quoteBody, quoteAttachments, quoteMentions, quoteType.getDataMessageType())); } else { return Optional.empty(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java index 29d3ed977..a1178dc21 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messages/MessageContentProcessor.java @@ -1565,7 +1565,7 @@ public final class MessageContentProcessor { } parentStoryId = new ParentStoryId.DirectReply(storyMessageId.getId()); - quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList()); + quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL); expiresInMillis = TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()); } else { warn(content.getTimestamp(), "Story has reactions disabled. Dropping reaction."); @@ -1656,7 +1656,7 @@ public final class MessageContentProcessor { displayText = story.getBody(); } - quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList()); + quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, displayText, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL); expiresInMillis = TimeUnit.SECONDS.toMillis(message.getExpiresInSeconds()); } else { warn(content.getTimestamp(), "Story has replies disabled. Dropping reply."); @@ -1929,7 +1929,7 @@ public final class MessageContentProcessor { quoteBody = story.getBody(); } - quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, quoteBody, false, story.getSlideDeck().asAttachments(), Collections.emptyList()); + quoteModel = new QuoteModel(storyContext.getSentTimestamp(), storyAuthorRecipient, quoteBody, false, story.getSlideDeck().asAttachments(), Collections.emptyList(), QuoteModel.Type.NORMAL); expiresInMillis = TimeUnit.SECONDS.toMillis(message.getExpirationStartTimestamp()); } else { warn(envelopeTimestamp, "Story has replies disabled. Dropping reply."); @@ -2722,7 +2722,7 @@ public final class MessageContentProcessor { } } - return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), false, attachments, mentions)); + return Optional.of(new QuoteModel(quote.get().getId(), author, message.getBody(), 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."); } @@ -2730,11 +2730,12 @@ public final class MessageContentProcessor { warn("Didn't find matching message record..."); return Optional.of(new QuoteModel(quote.get().getId(), - author, - quote.get().getText(), - true, - PointerAttachment.forPointers(quote.get().getAttachments()), - getMentions(quote.get().getMentions()))); + author, + quote.get().getText(), + true, + PointerAttachment.forPointers(quote.get().getAttachments()), + getMentions(quote.get().getMentions()), + QuoteModel.Type.fromDataMessageType(quote.get().getType()))); } private Optional getStickerAttachment(Optional sticker) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/mms/QuoteModel.java b/app/src/main/java/org/thoughtcrime/securesms/mms/QuoteModel.java index f1789cf60..e10424209 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mms/QuoteModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mms/QuoteModel.java @@ -7,6 +7,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.attachments.Attachment; import org.thoughtcrime.securesms.database.model.Mention; import org.thoughtcrime.securesms.recipients.RecipientId; +import org.whispersystems.signalservice.api.messages.SignalServiceDataMessage; import java.util.Collections; import java.util.List; @@ -19,14 +20,23 @@ public class QuoteModel { private final boolean missing; private final List attachments; private final List mentions; + private final Type type; - public QuoteModel(long id, @NonNull RecipientId author, String text, boolean missing, @Nullable List attachments, @Nullable List mentions) { + public QuoteModel(long id, + @NonNull RecipientId author, + String text, + boolean missing, + @Nullable List attachments, + @Nullable List mentions, + @NonNull Type type) + { this.id = id; this.author = author; this.text = text; this.missing = missing; this.attachments = attachments; this.mentions = mentions != null ? mentions : Collections.emptyList(); + this.type = type; } public long getId() { @@ -52,4 +62,49 @@ public class QuoteModel { public @NonNull List getMentions() { return mentions; } + + public Type getType() { + return type; + } + + public enum Type { + NORMAL(0, SignalServiceDataMessage.Quote.Type.NORMAL), + GIFT_BADGE(1, SignalServiceDataMessage.Quote.Type.GIFT_BADGE); + + private final int code; + private final SignalServiceDataMessage.Quote.Type dataMessageType; + + Type(int code, @NonNull SignalServiceDataMessage.Quote.Type dataMessageType) { + this.code = code; + this.dataMessageType = dataMessageType; + } + + public int getCode() { + return code; + } + + public @NonNull SignalServiceDataMessage.Quote.Type getDataMessageType() { + return dataMessageType; + } + + public static Type fromCode(int code) { + for (final Type value : values()) { + if (value.code == code) { + return value; + } + } + + throw new IllegalArgumentException("Invalid code: " + code); + } + + public static Type fromDataMessageType(@NonNull SignalServiceDataMessage.Quote.Type dataMessageType) { + for (final Type value : values()) { + if (value.dataMessageType == dataMessageType) { + return value; + } + } + + return NORMAL; + } + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/composer/StoryReplyComposer.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/composer/StoryReplyComposer.kt index 021a3dde2..816a8159e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/composer/StoryReplyComposer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/composer/StoryReplyComposer.kt @@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.components.emoji.MediaKeyboard import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.Mention import org.thoughtcrime.securesms.mms.GlideApp +import org.thoughtcrime.securesms.mms.QuoteModel import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.visible @@ -106,7 +107,8 @@ class StoryReplyComposer @JvmOverloads constructor( false, messageRecord.slideDeck, null, - null + null, + QuoteModel.Type.NORMAL ) quoteView.visible = true diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyRepository.kt index 553c3c656..801f62762 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/reply/direct/StoryDirectReplyRepository.kt @@ -55,7 +55,7 @@ class StoryDirectReplyRepository(context: Context) { StoryType.NONE, ParentStoryId.DirectReply(storyId), isReaction, - QuoteModel(message.dateSent, quoteAuthor.id, message.body, false, message.slideDeck.asAttachments(), null), + QuoteModel(message.dateSent, quoteAuthor.id, message.body, false, message.slideDeck.asAttachments(), null, QuoteModel.Type.NORMAL), emptyList(), emptyList(), emptyList(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.kt b/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.kt index e70fd4ca8..b2a4f2d3c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/MessageRecordUtil.kt @@ -9,6 +9,7 @@ import org.thoughtcrime.securesms.database.model.MediaMmsMessageRecord import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.database.model.MmsMessageRecord import org.thoughtcrime.securesms.database.model.databaseprotos.GiftBadge +import org.thoughtcrime.securesms.mms.QuoteModel import org.thoughtcrime.securesms.mms.TextSlide import org.thoughtcrime.securesms.stickers.StickerUrl @@ -127,3 +128,10 @@ fun MessageRecord.isTextOnly(context: Context): Boolean { !hasGiftBadge() ) } + +/** + * Returns the QuoteType for this record, as if it was being quoted. + */ +fun MessageRecord.getRecordQuoteType(): QuoteModel.Type { + return if (hasGiftBadge()) QuoteModel.Type.GIFT_BADGE else QuoteModel.Type.NORMAL +} diff --git a/app/src/main/res/drawable/ic_gift_thumbnail.xml b/app/src/main/res/drawable/ic_gift_thumbnail.xml new file mode 100644 index 000000000..9c309fc06 --- /dev/null +++ b/app/src/main/res/drawable/ic_gift_thumbnail.xml @@ -0,0 +1,21 @@ + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8f078f9f1..8a0e82168 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2107,6 +2107,8 @@ You ยท Story No longer available + + Gift Scroll to the bottom diff --git a/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt b/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt index 937d3a8d1..b6a792fb8 100644 --- a/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt +++ b/app/src/test/java/org/thoughtcrime/securesms/database/TestMms.kt @@ -107,6 +107,7 @@ object TestMms { values.putNull(MmsDatabase.QUOTE_BODY) values.putNull(MmsDatabase.QUOTE_AUTHOR) values.putNull(MmsDatabase.QUOTE_ATTACHMENT) + values.put(MmsDatabase.QUOTE_TYPE, -1) values.putNull(MmsDatabase.QUOTE_ID) values.putNull(MmsDatabase.LINK_PREVIEWS) values.putNull(MmsDatabase.SHARED_CONTACTS) diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java index 35c48e132..0485a3a0f 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/SignalServiceMessageSender.java @@ -846,7 +846,8 @@ public class SignalServiceMessageSender { DataMessage.Quote.Builder quoteBuilder = DataMessage.Quote.newBuilder() .setId(message.getQuote().get().getId()) .setText(message.getQuote().get().getText()) - .setAuthorUuid(message.getQuote().get().getAuthor().getServiceId().toString()); + .setAuthorUuid(message.getQuote().get().getAuthor().getServiceId().toString()) + .setType(message.getQuote().get().getType().getProtoType()); if (!message.getQuote().get().getMentions().isEmpty()) { for (SignalServiceDataMessage.Mention mention : message.getQuote().get().getMentions()) { diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java index 6695cfc21..4ba252fdd 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceContent.java @@ -1021,7 +1021,8 @@ public final class SignalServiceContent { address, content.getQuote().getText(), attachments, - createMentions(content.getQuote().getBodyRangesList(), content.getQuote().getText(), isGroupV2)); + createMentions(content.getQuote().getBodyRangesList(), content.getQuote().getText(), isGroupV2), + SignalServiceDataMessage.Quote.Type.fromProto(content.getQuote().getType())); } else { Log.w(TAG, "Quote was missing an author! Returning null."); return null; diff --git a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java index bc809d95d..d937372e0 100644 --- a/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java +++ b/libsignal/service/src/main/java/org/whispersystems/signalservice/api/messages/SignalServiceDataMessage.java @@ -13,6 +13,7 @@ import org.whispersystems.signalservice.api.messages.shared.SharedContact; import org.whispersystems.signalservice.api.push.ServiceId; import org.whispersystems.signalservice.api.push.SignalServiceAddress; import org.whispersystems.signalservice.api.util.OptionalUtil; +import org.whispersystems.signalservice.internal.push.SignalServiceProtos; import java.util.LinkedList; import java.util.List; @@ -456,13 +457,21 @@ public class SignalServiceDataMessage { private final String text; private final List attachments; private final List mentions; + private final Type type; - public Quote(long id, SignalServiceAddress author, String text, List attachments, List mentions) { + public Quote(long id, + SignalServiceAddress author, + String text, + List attachments, + List mentions, + Type type) + { this.id = id; this.author = author; this.text = text; this.attachments = attachments; this.mentions = mentions; + this.type = type; } public long getId() { @@ -485,6 +494,35 @@ public class SignalServiceDataMessage { return mentions; } + public Type getType() { + return type; + } + + public enum Type { + NORMAL(SignalServiceProtos.DataMessage.Quote.Type.NORMAL), + GIFT_BADGE(SignalServiceProtos.DataMessage.Quote.Type.GIFT_BADGE); + + private final SignalServiceProtos.DataMessage.Quote.Type protoType; + + Type(SignalServiceProtos.DataMessage.Quote.Type protoType) { + this.protoType = protoType; + } + + public SignalServiceProtos.DataMessage.Quote.Type getProtoType() { + return protoType; + } + + public static Type fromProto(SignalServiceProtos.DataMessage.Quote.Type protoType) { + for (final Type value : values()) { + if (value.protoType == protoType) { + return value; + } + } + + return NORMAL; + } + } + public static class QuotedAttachment { private final String contentType; private final String fileName; diff --git a/libsignal/service/src/main/proto/SignalService.proto b/libsignal/service/src/main/proto/SignalService.proto index a564843fd..6b5562dc6 100644 --- a/libsignal/service/src/main/proto/SignalService.proto +++ b/libsignal/service/src/main/proto/SignalService.proto @@ -138,6 +138,11 @@ message DataMessage { } message Quote { + enum Type { + NORMAL = 0; + GIFT_BADGE = 1; + } + message QuotedAttachment { optional string contentType = 1; optional string fileName = 2; @@ -150,6 +155,7 @@ message DataMessage { optional string text = 3; repeated QuotedAttachment attachments = 4; repeated BodyRange bodyRanges = 6; + optional Type type = 7; } message Contact {