mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-05-03 07:01:05 +01:00
Add UI components for Release Channel.
This commit is contained in:
@@ -92,6 +92,8 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable,
|
||||
void onInMemoryMessageClicked(@NonNull InMemoryMessageRecord messageRecord);
|
||||
void onViewGroupDescriptionChange(@Nullable GroupId groupId, @NonNull String description, boolean isMessageRequestAccepted);
|
||||
void onChangeNumberUpdateContact(@NonNull Recipient recipient);
|
||||
void onCallToAction(@NonNull String action);
|
||||
void onDonateClicked();
|
||||
|
||||
/** @return true if handled, false if you want to let the normal url handling continue */
|
||||
boolean onUrlClicked(@NonNull String url);
|
||||
|
||||
@@ -76,6 +76,11 @@ public final class BlockUnblockDialog {
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_block, ((dialog, which) -> onBlock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
} else if (recipient.isReleaseNotes()) {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_block_getting_signal_updates_and_news);
|
||||
builder.setPositiveButton(R.string.BlockUnblockDialog_block, ((dialog, which) -> onBlock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
} else {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_block_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages);
|
||||
@@ -115,6 +120,12 @@ public final class BlockUnblockDialog {
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
}
|
||||
} else if (recipient.isReleaseNotes()) {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_resume_getting_signal_updates_and_news);
|
||||
|
||||
builder.setPositiveButton(R.string.RecipientPreferenceActivity_unblock, ((dialog, which) -> onUnblock.run()));
|
||||
builder.setNegativeButton(android.R.string.cancel, null);
|
||||
} else {
|
||||
builder.setTitle(resources.getString(R.string.BlockUnblockDialog_unblock_s, recipient.getDisplayName(context)));
|
||||
builder.setMessage(R.string.BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other);
|
||||
|
||||
@@ -143,11 +143,10 @@ object Avatars {
|
||||
)
|
||||
|
||||
data class ColorPair(
|
||||
val backgroundAvatarColor: AvatarColor,
|
||||
val foregroundAvatarColor: ForegroundColor
|
||||
@ColorInt val backgroundColor: Int,
|
||||
@ColorInt val foregroundColor: Int,
|
||||
val code: String
|
||||
) {
|
||||
@ColorInt val backgroundColor: Int = backgroundAvatarColor.colorInt()
|
||||
@ColorInt val foregroundColor: Int = foregroundAvatarColor.colorInt
|
||||
val code: String = backgroundAvatarColor.serialize()
|
||||
constructor(backgroundAvatarColor: AvatarColor, foregroundAvatarColor: ForegroundColor) : this(backgroundAvatarColor.colorInt(), foregroundAvatarColor.colorInt, backgroundAvatarColor.serialize())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.thoughtcrime.securesms.components;
|
||||
import android.content.Context;
|
||||
import android.graphics.PorterDuff;
|
||||
import android.graphics.PorterDuffColorFilter;
|
||||
import android.graphics.Typeface;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
@@ -18,6 +17,7 @@ import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.emoji.SimpleEmojiTextView;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.SpanUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
@@ -44,7 +44,7 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, boolean read, @Nullable String suffix) {
|
||||
setText(recipient, recipient.getDisplayName(getContext()), read, suffix);
|
||||
setText(recipient, recipient.getDisplayNameOrUsername(getContext()), read, suffix);
|
||||
}
|
||||
|
||||
public void setText(Recipient recipient, @Nullable CharSequence fromString, boolean read, @Nullable String suffix) {
|
||||
@@ -62,11 +62,19 @@ public class FromTextView extends SimpleEmojiTextView {
|
||||
builder.append(suffix);
|
||||
}
|
||||
|
||||
if (recipient.isReleaseNotes()) {
|
||||
Drawable official = ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_20);
|
||||
official.setBounds(0, 0, ViewUtil.dpToPx(20), ViewUtil.dpToPx(20));
|
||||
|
||||
builder.append(" ")
|
||||
.append(SpanUtil.buildCenteredImageSpan(official));
|
||||
}
|
||||
|
||||
setText(builder);
|
||||
|
||||
if (recipient.isBlocked()) setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_block_grey600_18dp, 0, 0, 0);
|
||||
else if (recipient.isMuted()) setCompoundDrawablesRelativeWithIntrinsicBounds(getMuted(), null, null, null);
|
||||
else setCompoundDrawablesWithIntrinsicBounds(0, 0, 0, 0);
|
||||
else setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
private Drawable getMuted() {
|
||||
|
||||
@@ -393,28 +393,32 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
enabled = it.canEditGroupAttributes
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__disappearing_messages),
|
||||
summary = summary,
|
||||
icon = DSLSettingsIcon.from(icon),
|
||||
isEnabled = enabled,
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToAppSettingsExpireTimer()
|
||||
.setInitialValue(state.disappearingMessagesLifespan)
|
||||
.setRecipientId(state.recipient.id)
|
||||
.setForResultMode(false)
|
||||
if (!state.recipient.isReleaseNotes) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.ConversationSettingsFragment__disappearing_messages),
|
||||
summary = summary,
|
||||
icon = DSLSettingsIcon.from(icon),
|
||||
isEnabled = enabled,
|
||||
onClick = {
|
||||
val action = ConversationSettingsFragmentDirections.actionConversationSettingsFragmentToAppSettingsExpireTimer()
|
||||
.setInitialValue(state.disappearingMessagesLifespan)
|
||||
.setRecipientId(state.recipient.id)
|
||||
.setForResultMode(false)
|
||||
|
||||
navController.safeNavigate(action)
|
||||
}
|
||||
)
|
||||
navController.safeNavigate(action)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_color_24),
|
||||
onClick = {
|
||||
startActivity(ChatWallpaperActivity.createIntent(requireContext(), state.recipient.id))
|
||||
}
|
||||
)
|
||||
if (!state.recipient.isReleaseNotes) {
|
||||
clickPref(
|
||||
title = DSLSettingsText.from(R.string.preferences__chat_color_and_wallpaper),
|
||||
icon = DSLSettingsIcon.from(R.drawable.ic_color_24),
|
||||
onClick = {
|
||||
startActivity(ChatWallpaperActivity.createIntent(requireContext(), state.recipient.id))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (!state.recipient.isSelf) {
|
||||
clickPref(
|
||||
@@ -507,7 +511,7 @@ class ConversationSettingsFragment : DSLSettingsFragment(
|
||||
)
|
||||
}
|
||||
|
||||
if (recipientSettingsState.selfHasGroups) {
|
||||
if (recipientSettingsState.selfHasGroups && !state.recipient.isReleaseNotes) {
|
||||
|
||||
dividerPref()
|
||||
|
||||
|
||||
@@ -130,8 +130,8 @@ sealed class ConversationSettingsViewModel(
|
||||
state.copy(
|
||||
recipient = recipient,
|
||||
buttonStripState = ButtonStripPreference.State(
|
||||
isVideoAvailable = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED && !recipient.isSelf && !recipient.isBlocked,
|
||||
isAudioAvailable = !recipient.isGroup && !recipient.isSelf && !recipient.isBlocked,
|
||||
isVideoAvailable = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED && !recipient.isSelf && !recipient.isBlocked && !recipient.isReleaseNotes,
|
||||
isAudioAvailable = !recipient.isGroup && !recipient.isSelf && !recipient.isBlocked && !recipient.isReleaseNotes,
|
||||
isAudioSecure = recipient.registered == RecipientDatabase.RegisteredState.REGISTERED,
|
||||
isMuted = recipient.isMuted,
|
||||
isMuteAvailable = !recipient.isSelf,
|
||||
@@ -141,7 +141,7 @@ sealed class ConversationSettingsViewModel(
|
||||
canModifyBlockedState = !recipient.isSelf && RecipientUtil.isBlockable(recipient),
|
||||
specificSettingsState = state.requireRecipientSettingsState().copy(
|
||||
contactLinkState = when {
|
||||
recipient.isSelf -> ContactLinkState.NONE
|
||||
recipient.isSelf || recipient.isReleaseNotes -> ContactLinkState.NONE
|
||||
recipient.isSystemContact -> ContactLinkState.OPEN
|
||||
else -> ContactLinkState.ADD
|
||||
}
|
||||
|
||||
@@ -26,8 +26,9 @@ object BioTextPreference {
|
||||
|
||||
abstract class BioTextPreferenceModel<T : BioTextPreferenceModel<T>> : PreferenceModel<T>() {
|
||||
abstract fun getHeadlineText(context: Context): String
|
||||
abstract fun getSubhead1Text(): String?
|
||||
abstract fun getSubhead1Text(context: Context): String?
|
||||
abstract fun getSubhead2Text(): String?
|
||||
abstract fun getCompoundDrawable(): Int
|
||||
}
|
||||
|
||||
class RecipientModel(
|
||||
@@ -36,10 +37,20 @@ object BioTextPreference {
|
||||
|
||||
override fun getHeadlineText(context: Context): String = recipient.getDisplayNameOrUsername(context)
|
||||
|
||||
override fun getSubhead1Text(): String? = recipient.combinedAboutAndEmoji
|
||||
override fun getSubhead1Text(context: Context): String? {
|
||||
return if (recipient.isReleaseNotes) {
|
||||
context.getString(R.string.ReleaseNotes__signal_release_notes_and_news)
|
||||
} else {
|
||||
recipient.combinedAboutAndEmoji
|
||||
}
|
||||
}
|
||||
|
||||
override fun getSubhead2Text(): String? = recipient.e164.transform(PhoneNumberFormatter::prettyPrint).orNull()
|
||||
|
||||
override fun getCompoundDrawable(): Int {
|
||||
return if (recipient.isReleaseNotes) R.drawable.ic_official_28 else 0
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(newItem: RecipientModel): Boolean {
|
||||
return super.areContentsTheSame(newItem) && newItem.recipient.hasSameContent(recipient)
|
||||
}
|
||||
@@ -55,10 +66,12 @@ object BioTextPreference {
|
||||
) : BioTextPreferenceModel<GroupModel>() {
|
||||
override fun getHeadlineText(context: Context): String = groupTitle
|
||||
|
||||
override fun getSubhead1Text(): String? = groupMembershipDescription
|
||||
override fun getSubhead1Text(context: Context): String? = groupMembershipDescription
|
||||
|
||||
override fun getSubhead2Text(): String? = null
|
||||
|
||||
override fun getCompoundDrawable(): Int = 0
|
||||
|
||||
override fun areContentsTheSame(newItem: GroupModel): Boolean {
|
||||
return super.areContentsTheSame(newItem) &&
|
||||
groupTitle == newItem.groupTitle &&
|
||||
@@ -78,8 +91,9 @@ object BioTextPreference {
|
||||
|
||||
override fun bind(model: T) {
|
||||
headline.text = model.getHeadlineText(context)
|
||||
headline.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, model.getCompoundDrawable(), 0)
|
||||
|
||||
model.getSubhead1Text().let {
|
||||
model.getSubhead1Text(context).let {
|
||||
subhead1.text = it
|
||||
subhead1.visibility = if (it == null) View.GONE else View.VISIBLE
|
||||
}
|
||||
|
||||
@@ -78,11 +78,26 @@ public class ConversationBannerView extends ConstraintLayout {
|
||||
}
|
||||
}
|
||||
|
||||
public void setTitle(@Nullable CharSequence title) {
|
||||
public String setTitle(@NonNull Recipient recipient) {
|
||||
if (recipient.isReleaseNotes()) {
|
||||
contactTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_official_28, 0);
|
||||
} else {
|
||||
contactTitle.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
String title = recipient.isSelf() ? getContext().getString(R.string.note_to_self) : recipient.getDisplayNameOrUsername(getContext());
|
||||
contactTitle.setText(title);
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setAbout(@Nullable String about) {
|
||||
public void setAbout(@NonNull Recipient recipient) {
|
||||
String about;
|
||||
if (recipient.isReleaseNotes()) {
|
||||
about = getContext().getString(R.string.ReleaseNotes__signal_release_notes_and_news);
|
||||
} else {
|
||||
about = recipient.getCombinedAboutAndEmoji();
|
||||
}
|
||||
|
||||
contactAbout.setText(about);
|
||||
contactAbout.setVisibility(TextUtils.isEmpty(about) ? GONE : VISIBLE);
|
||||
}
|
||||
|
||||
@@ -552,9 +552,8 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
conversationBanner.setAvatar(GlideApp.with(context), recipient);
|
||||
conversationBanner.showBackgroundBubble(recipient.hasWallpaper());
|
||||
|
||||
String title = isSelf ? context.getString(R.string.note_to_self) : recipient.getDisplayNameOrUsername(context);
|
||||
conversationBanner.setTitle(title);
|
||||
conversationBanner.setAbout(recipient.getCombinedAboutAndEmoji());
|
||||
String title = conversationBanner.setTitle(recipient);
|
||||
conversationBanner.setAbout(recipient);
|
||||
|
||||
if (recipient.isGroup()) {
|
||||
if (pendingMemberCount > 0) {
|
||||
@@ -1821,6 +1820,16 @@ public class ConversationFragment extends LoggingFragment implements Multiselect
|
||||
public void onChangeNumberUpdateContact(@NonNull Recipient recipient) {
|
||||
startActivity(RecipientExporter.export(recipient).asAddContactIntent());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCallToAction(@NonNull String action) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDonateClicked() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
public void refreshList() {
|
||||
|
||||
@@ -45,6 +45,7 @@ import android.view.MotionEvent;
|
||||
import android.view.TouchDelegate;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@@ -122,6 +123,7 @@ import org.thoughtcrime.securesms.util.DateUtils;
|
||||
import org.thoughtcrime.securesms.util.InterceptableLongClickCopyLinkSpan;
|
||||
import org.thoughtcrime.securesms.util.LongClickMovementMethod;
|
||||
import org.thoughtcrime.securesms.util.MessageRecordUtil;
|
||||
import org.thoughtcrime.securesms.util.PlaceholderURLSpan;
|
||||
import org.thoughtcrime.securesms.util.Projection;
|
||||
import org.thoughtcrime.securesms.util.ProjectionList;
|
||||
import org.thoughtcrime.securesms.util.SearchUtil;
|
||||
@@ -199,6 +201,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
private Stub<LinkPreviewView> linkPreviewStub;
|
||||
private Stub<BorderlessImageView> stickerStub;
|
||||
private Stub<ViewOnceMessageView> revealableStub;
|
||||
private Stub<Button> callToActionStub;
|
||||
private @Nullable EventListener eventListener;
|
||||
|
||||
private int defaultBubbleColor;
|
||||
@@ -277,6 +280,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
this.linkPreviewStub = new Stub<>(findViewById(R.id.link_preview_stub));
|
||||
this.stickerStub = new Stub<>(findViewById(R.id.sticker_view_stub));
|
||||
this.revealableStub = new Stub<>(findViewById(R.id.revealable_view_stub));
|
||||
this.callToActionStub = ViewUtil.findStubById(this, R.id.conversation_item_call_to_action_stub);
|
||||
this.groupSenderHolder = findViewById(R.id.group_sender_holder);
|
||||
this.quoteView = findViewById(R.id.quote_view);
|
||||
this.reply = findViewById(R.id.reply_icon_wrapper);
|
||||
@@ -443,6 +447,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
!hasAudio(messageRecord) &&
|
||||
isFooterVisible(messageRecord, nextMessageRecord, groupThread) &&
|
||||
!bodyText.isJumbomoji() &&
|
||||
conversationMessage.getBottomButton() == null &&
|
||||
bodyText.getLastLineWidth() > 0)
|
||||
{
|
||||
TextView dateView = footer.getDateView();
|
||||
@@ -922,6 +927,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
|
||||
bodyText.setText(StringUtil.trim(styledText));
|
||||
bodyText.setVisibility(View.VISIBLE);
|
||||
|
||||
if (conversationMessage.getBottomButton() != null) {
|
||||
callToActionStub.get().setVisibility(View.VISIBLE);
|
||||
callToActionStub.get().setText(conversationMessage.getBottomButton().getLabel());
|
||||
callToActionStub.get().setOnClickListener(v -> {
|
||||
if (eventListener != null) {
|
||||
eventListener.onCallToAction(conversationMessage.getBottomButton().getAction());
|
||||
}
|
||||
});
|
||||
} else if (callToActionStub.resolved()) {
|
||||
callToActionStub.get().setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1326,6 +1343,19 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
|
||||
}
|
||||
}
|
||||
|
||||
if (conversationMessage.hasStyleLinks()) {
|
||||
for (PlaceholderURLSpan placeholder : messageBody.getSpans(0, messageBody.length(), PlaceholderURLSpan.class)) {
|
||||
int start = messageBody.getSpanStart(placeholder);
|
||||
int end = messageBody.getSpanEnd(placeholder);
|
||||
URLSpan span = new InterceptableLongClickCopyLinkSpan(placeholder.getValue(),
|
||||
urlClickListener,
|
||||
ContextCompat.getColor(getContext(), R.color.signal_accent_primary),
|
||||
false);
|
||||
|
||||
messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
|
||||
List<Annotation> mentionAnnotations = MentionAnnotation.getMentionAnnotations(messageBody);
|
||||
for (Annotation annotation : mentionAnnotations) {
|
||||
messageBody.setSpan(new MentionClickableSpan(RecipientId.from(annotation.getValue())), messageBody.getSpanStart(annotation), messageBody.getSpanEnd(annotation), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
||||
@@ -16,6 +16,7 @@ import org.thoughtcrime.securesms.database.MentionUtil;
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.database.model.MessageRecord;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
|
||||
import java.security.MessageDigest;
|
||||
import java.util.Collections;
|
||||
@@ -26,10 +27,11 @@ import java.util.List;
|
||||
* for various presentations.
|
||||
*/
|
||||
public class ConversationMessage {
|
||||
@NonNull private final MessageRecord messageRecord;
|
||||
@NonNull private final List<Mention> mentions;
|
||||
@Nullable private final SpannableString body;
|
||||
@NonNull private final MultiselectCollection multiselectCollection;
|
||||
@NonNull private final MessageRecord messageRecord;
|
||||
@NonNull private final List<Mention> mentions;
|
||||
@Nullable private final SpannableString body;
|
||||
@NonNull private final MultiselectCollection multiselectCollection;
|
||||
@NonNull private final MessageStyler.Result styleResult;
|
||||
|
||||
private ConversationMessage(@NonNull MessageRecord messageRecord) {
|
||||
this(messageRecord, null, null);
|
||||
@@ -40,13 +42,26 @@ public class ConversationMessage {
|
||||
@Nullable List<Mention> mentions)
|
||||
{
|
||||
this.messageRecord = messageRecord;
|
||||
this.body = body != null ? SpannableString.valueOf(body) : null;
|
||||
this.mentions = mentions != null ? mentions : Collections.emptyList();
|
||||
|
||||
if (body != null) {
|
||||
this.body = SpannableString.valueOf(body);
|
||||
} else if (messageRecord.hasMessageRanges()) {
|
||||
this.body = SpannableString.valueOf(messageRecord.getBody());
|
||||
} else {
|
||||
this.body = null;
|
||||
}
|
||||
|
||||
if (!this.mentions.isEmpty() && this.body != null) {
|
||||
MentionAnnotation.setMentionAnnotations(this.body, this.mentions);
|
||||
}
|
||||
|
||||
if (this.body != null && messageRecord.hasMessageRanges()) {
|
||||
styleResult = MessageStyler.style(messageRecord.requireMessageRanges(), this.body);
|
||||
} else {
|
||||
styleResult = MessageStyler.Result.none();
|
||||
}
|
||||
|
||||
multiselectCollection = Multiselect.getParts(this);
|
||||
}
|
||||
|
||||
@@ -86,6 +101,14 @@ public class ConversationMessage {
|
||||
return (body != null) ? body : messageRecord.getDisplayBody(context);
|
||||
}
|
||||
|
||||
public boolean hasStyleLinks() {
|
||||
return styleResult.getHasStyleLinks();
|
||||
}
|
||||
|
||||
public @Nullable BodyRangeList.BodyRange.Button getBottomButton() {
|
||||
return styleResult.getBottomButton();
|
||||
}
|
||||
|
||||
/**
|
||||
* Factory providing multiple ways of creating {@link ConversationMessage}s.
|
||||
*/
|
||||
|
||||
@@ -401,6 +401,7 @@ public class ConversationParentFragment extends Fragment
|
||||
private Stub<TextView> cannotSendInAnnouncementGroupBanner;
|
||||
private View requestingMemberBanner;
|
||||
private View cancelJoinRequest;
|
||||
private Stub<View> releaseChannelUnmute;
|
||||
private Stub<View> mentionsSuggestions;
|
||||
private MaterialButton joinGroupCallButton;
|
||||
private boolean callingTooltipShown;
|
||||
@@ -942,8 +943,8 @@ public class ConversationParentFragment extends Fragment
|
||||
}
|
||||
|
||||
if (isSingleConversation()) {
|
||||
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
|
||||
else inflater.inflate(R.menu.conversation_callable_insecure, menu);
|
||||
if (isSecureText) inflater.inflate(R.menu.conversation_callable_secure, menu);
|
||||
else if (!recipient.get().isReleaseNotes()) inflater.inflate(R.menu.conversation_callable_insecure, menu);
|
||||
} else if (isGroupConversation()) {
|
||||
if (isActiveV2Group && Build.VERSION.SDK_INT > 19) {
|
||||
inflater.inflate(R.menu.conversation_callable_groupv2, menu);
|
||||
@@ -969,14 +970,14 @@ public class ConversationParentFragment extends Fragment
|
||||
|
||||
inflater.inflate(R.menu.conversation, menu);
|
||||
|
||||
if (isSingleConversation() && !isSecureText) {
|
||||
if (isSingleConversation() && !isSecureText && !recipient.get().isReleaseNotes()) {
|
||||
inflater.inflate(R.menu.conversation_insecure, menu);
|
||||
}
|
||||
|
||||
if (recipient != null && recipient.get().isMuted()) inflater.inflate(R.menu.conversation_muted, menu);
|
||||
else inflater.inflate(R.menu.conversation_unmuted, menu);
|
||||
|
||||
if (isSingleConversation() && getRecipient().getContactUri() == null) {
|
||||
if (isSingleConversation() && getRecipient().getContactUri() == null && !recipient.get().isReleaseNotes()) {
|
||||
inflater.inflate(R.menu.conversation_add_to_contacts, menu);
|
||||
}
|
||||
|
||||
@@ -1004,6 +1005,10 @@ public class ConversationParentFragment extends Fragment
|
||||
hideMenuItem(menu, R.id.menu_mute_notifications);
|
||||
}
|
||||
|
||||
if (recipient != null && recipient.get().isReleaseNotes()) {
|
||||
hideMenuItem(menu, R.id.menu_add_shortcut);
|
||||
}
|
||||
|
||||
hideMenuItem(menu, R.id.menu_group_recipients);
|
||||
|
||||
if (isActiveV2Group) {
|
||||
@@ -2049,6 +2054,7 @@ public class ConversationParentFragment extends Fragment
|
||||
cannotSendInAnnouncementGroupBanner = ViewUtil.findStubById(view, R.id.conversation_cannot_send_announcement_stub);
|
||||
requestingMemberBanner = view.findViewById(R.id.conversation_requesting_banner);
|
||||
cancelJoinRequest = view.findViewById(R.id.conversation_cancel_request);
|
||||
releaseChannelUnmute = ViewUtil.findStubById(view, R.id.conversation_release_notes_unmute_stub);
|
||||
joinGroupCallButton = view.findViewById(R.id.conversation_group_call_join);
|
||||
|
||||
container.setIsBubble(isInBubble());
|
||||
@@ -2721,6 +2727,20 @@ public class ConversationParentFragment extends Fragment
|
||||
inputPanel.setHideForBlockedState(true);
|
||||
makeDefaultSmsButton.setVisibility(View.VISIBLE);
|
||||
registerButton.setVisibility(View.GONE);
|
||||
} else if (recipient.isReleaseNotes() && !recipient.isBlocked()) {
|
||||
unblockButton.setVisibility(View.GONE);
|
||||
inputPanel.setHideForBlockedState(true);
|
||||
makeDefaultSmsButton.setVisibility(View.GONE);
|
||||
registerButton.setVisibility(View.GONE);
|
||||
|
||||
if (recipient.isMuted()) {
|
||||
View unmuteBanner = releaseChannelUnmute.get();
|
||||
unmuteBanner.setVisibility(View.VISIBLE);
|
||||
unmuteBanner.findViewById(R.id.conversation_activity_unmute_button)
|
||||
.setOnClickListener(v -> handleUnmuteNotifications());
|
||||
} else if (releaseChannelUnmute.resolved()) {
|
||||
releaseChannelUnmute.get().setVisibility(View.GONE);
|
||||
}
|
||||
} else {
|
||||
boolean inactivePushGroup = isPushGroupConversation() && !recipient.isActiveGroup();
|
||||
inputPanel.setHideForBlockedState(inactivePushGroup);
|
||||
@@ -2728,6 +2748,10 @@ public class ConversationParentFragment extends Fragment
|
||||
makeDefaultSmsButton.setVisibility(View.GONE);
|
||||
registerButton.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (releaseChannelUnmute.resolved() && !recipient.isReleaseNotes()) {
|
||||
releaseChannelUnmute.get().setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void calculateCharactersRemaining() {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package org.thoughtcrime.securesms.conversation;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.TextUtils;
|
||||
import android.util.AttributeSet;
|
||||
@@ -13,23 +12,21 @@ import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
|
||||
import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.badges.BadgeImageView;
|
||||
import org.thoughtcrime.securesms.badges.models.Badge;
|
||||
import org.thoughtcrime.securesms.components.AvatarImageView;
|
||||
import org.thoughtcrime.securesms.mms.GlideRequests;
|
||||
import org.thoughtcrime.securesms.recipients.LiveRecipient;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.util.ContextUtil;
|
||||
import org.thoughtcrime.securesms.util.DrawableUtil;
|
||||
import org.thoughtcrime.securesms.util.ExpirationUtil;
|
||||
import org.thoughtcrime.securesms.util.ViewUtil;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
public class ConversationTitleView extends RelativeLayout {
|
||||
|
||||
private AvatarImageView avatar;
|
||||
@@ -89,9 +86,9 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
Drawable endDrawable = null;
|
||||
|
||||
if (recipient != null && recipient.isBlocked()) {
|
||||
startDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_block_white_18dp);
|
||||
startDrawable = ContextUtil.requireDrawable(getContext(), R.drawable.ic_block_white_18dp);
|
||||
} else if (recipient != null && recipient.isMuted()) {
|
||||
startDrawable = Objects.requireNonNull(ContextCompat.getDrawable(getContext(), R.drawable.ic_bell_disabled_16));
|
||||
startDrawable = ContextUtil.requireDrawable(getContext(), R.drawable.ic_bell_disabled_16);
|
||||
startDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18));
|
||||
}
|
||||
|
||||
@@ -99,8 +96,19 @@ public class ConversationTitleView extends RelativeLayout {
|
||||
endDrawable = ContextCompat.getDrawable(getContext(), R.drawable.ic_profile_circle_outline_16);
|
||||
}
|
||||
|
||||
if (startDrawable != null) {
|
||||
startDrawable = DrawableUtil.tint(startDrawable, ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_80));
|
||||
}
|
||||
|
||||
if (endDrawable != null) {
|
||||
endDrawable = DrawableUtil.tint(endDrawable, ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_80));
|
||||
}
|
||||
|
||||
if (recipient != null && recipient.isReleaseNotes()) {
|
||||
endDrawable = ContextUtil.requireDrawable(getContext(), R.drawable.ic_official_24);
|
||||
}
|
||||
|
||||
title.setCompoundDrawablesRelativeWithIntrinsicBounds(startDrawable, null, endDrawable, null);
|
||||
TextViewCompat.setCompoundDrawableTintList(title, ColorStateList.valueOf(ContextCompat.getColor(getContext(), R.color.signal_inverse_transparent_80)));
|
||||
|
||||
if (recipient != null) {
|
||||
this.avatar.setAvatar(glideRequests, recipient, false);
|
||||
|
||||
@@ -7,11 +7,13 @@ import android.text.SpannableString;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.ViewTreeObserver;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.cardview.widget.CardView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.LiveData;
|
||||
@@ -23,6 +25,9 @@ import com.google.common.collect.Sets;
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.BindableConversationItem;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.util.views.AutoRounder;
|
||||
import org.thoughtcrime.securesms.util.views.Stub;
|
||||
import org.thoughtcrime.securesms.verify.VerifyIdentityActivity;
|
||||
import org.thoughtcrime.securesms.conversation.colors.Colorizer;
|
||||
import org.thoughtcrime.securesms.conversation.mutiselect.MultiselectPart;
|
||||
@@ -65,6 +70,7 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
|
||||
private TextView body;
|
||||
private MaterialButton actionButton;
|
||||
private Stub<CardView> donateButtonStub;
|
||||
private View background;
|
||||
private ConversationMessage conversationMessage;
|
||||
private Recipient conversationRecipient;
|
||||
@@ -92,9 +98,10 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
@Override
|
||||
public void onFinishInflate() {
|
||||
super.onFinishInflate();
|
||||
this.body = findViewById(R.id.conversation_update_body);
|
||||
this.actionButton = findViewById(R.id.conversation_update_action);
|
||||
this.background = findViewById(R.id.conversation_update_background);
|
||||
this.body = findViewById(R.id.conversation_update_body);
|
||||
this.actionButton = findViewById(R.id.conversation_update_action);
|
||||
this.donateButtonStub = ViewUtil.findStubById(this, R.id.conversation_update_donate_action);
|
||||
this.background = findViewById(R.id.conversation_update_background);
|
||||
|
||||
this.setOnClickListener(new InternalClickListener(null));
|
||||
}
|
||||
@@ -425,6 +432,34 @@ public final class ConversationUpdateItem extends FrameLayout
|
||||
actionButton.setVisibility(GONE);
|
||||
actionButton.setOnClickListener(null);
|
||||
}
|
||||
|
||||
if (conversationMessage.getMessageRecord().isBoostRequest()) {
|
||||
actionButton.setVisibility(GONE);
|
||||
|
||||
CardView donateButton = donateButtonStub.get();
|
||||
TextView buttonText = donateButton.findViewById(R.id.conversation_update_donate_action_button);
|
||||
boolean isSustainer = SignalStore.donationsValues().isLikelyASustainer();
|
||||
|
||||
donateButton.setVisibility(VISIBLE);
|
||||
donateButton.setOnClickListener(v -> {
|
||||
if (batchSelected.isEmpty() && eventListener != null) {
|
||||
eventListener.onDonateClicked();
|
||||
}
|
||||
});
|
||||
|
||||
if (isSustainer) {
|
||||
buttonText.setText(R.string.ConversationUpdateItem_signal_boost);
|
||||
buttonText.setCompoundDrawablesRelativeWithIntrinsicBounds(R.drawable.ic_boost_outline_16, 0, 0, 0);
|
||||
} else {
|
||||
buttonText.setText(R.string.ConversationUpdateItem_become_a_sustainer);
|
||||
buttonText.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
AutoRounder.autoSetCorners(donateButton, donateButton::setRadius);
|
||||
|
||||
} else if (donateButtonStub.resolved()) {
|
||||
donateButtonStub.get().setVisibility(GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void presentBackground(boolean collapseAbove, boolean collapseBelow, boolean hasWallpaper) {
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package org.thoughtcrime.securesms.conversation
|
||||
|
||||
import android.graphics.Typeface
|
||||
import android.text.SpannableString
|
||||
import android.text.Spanned
|
||||
import android.text.style.StyleSpan
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.util.PlaceholderURLSpan
|
||||
|
||||
/**
|
||||
* Helper for applying style-based [BodyRangeList.BodyRange]s to text.
|
||||
*/
|
||||
object MessageStyler {
|
||||
|
||||
@JvmStatic
|
||||
fun style(messageRanges: BodyRangeList, span: SpannableString): Result {
|
||||
var hasLinks = false
|
||||
var bottomButton: BodyRangeList.BodyRange.Button? = null
|
||||
|
||||
for (range in messageRanges.rangesList) {
|
||||
if (range.hasStyle()) {
|
||||
val style = range.style?.let {
|
||||
when (it) {
|
||||
BodyRangeList.BodyRange.Style.BOLD -> Typeface.BOLD
|
||||
BodyRangeList.BodyRange.Style.ITALIC -> Typeface.ITALIC
|
||||
BodyRangeList.BodyRange.Style.UNRECOGNIZED -> Typeface.NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
if (style != null && style != Typeface.NORMAL) {
|
||||
span.setSpan(StyleSpan(style), range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
}
|
||||
} else if (range.hasLink() && range.link != null) {
|
||||
span.setSpan(PlaceholderURLSpan(range.link), range.start, range.start + range.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
hasLinks = true
|
||||
} else if (range.hasButton() && range.button != null) {
|
||||
bottomButton = range.button
|
||||
}
|
||||
}
|
||||
|
||||
return Result(hasLinks, bottomButton)
|
||||
}
|
||||
|
||||
data class Result(val hasStyleLinks: Boolean = false, val bottomButton: BodyRangeList.BodyRange.Button? = null) {
|
||||
companion object {
|
||||
@JvmStatic
|
||||
val NO_STYLE = Result()
|
||||
|
||||
@JvmStatic
|
||||
fun none(): Result = NO_STYLE
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -525,7 +525,7 @@ public final class ConversationListItem extends ConstraintLayout
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_message_could_not_be_processed), defaultTint);
|
||||
} else if (SmsDatabase.Types.isProfileChange(thread.getType())) {
|
||||
return emphasisAdded(context, "", defaultTint);
|
||||
} else if (SmsDatabase.Types.isChangeNumber(thread.getType())) {
|
||||
} else if (SmsDatabase.Types.isChangeNumber(thread.getType()) || SmsDatabase.Types.isBoostRequest(thread.getType())) {
|
||||
return emphasisAdded(context, "", defaultTint);
|
||||
} else if (MmsSmsColumns.Types.isBadDecryptType(thread.getType())) {
|
||||
return emphasisAdded(context, context.getString(R.string.ThreadRecord_delivery_issue), defaultTint);
|
||||
|
||||
@@ -161,6 +161,7 @@ public abstract class MessageDatabase extends Database implements MmsSmsColumns
|
||||
public abstract void insertProfileNameChangeMessages(@NonNull Recipient recipient, @NonNull String newProfileName, @NonNull String previousProfileName);
|
||||
public abstract void insertGroupV1MigrationEvents(@NonNull RecipientId recipientId, long threadId, @NonNull GroupMigrationMembershipChange membershipChange);
|
||||
public abstract void insertNumberChangeMessages(@NonNull Recipient recipient);
|
||||
public abstract void insertBoostRequestMessage(@NonNull RecipientId recipientId, long threadId);
|
||||
|
||||
public abstract boolean deleteMessage(long messageId);
|
||||
abstract void deleteThread(long threadId);
|
||||
|
||||
@@ -28,6 +28,7 @@ import com.annimon.stream.Collectors;
|
||||
import com.annimon.stream.Stream;
|
||||
import com.google.android.mms.pdu_alt.NotificationInd;
|
||||
import com.google.android.mms.pdu_alt.PduHeaders;
|
||||
import com.google.protobuf.InvalidProtocolBufferException;
|
||||
|
||||
import net.zetetic.database.sqlcipher.SQLiteStatement;
|
||||
|
||||
@@ -123,6 +124,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
static final String SHARED_CONTACTS = "shared_contacts";
|
||||
static final String LINK_PREVIEWS = "previews";
|
||||
static final String MENTIONS_SELF = "mentions_self";
|
||||
static final String MESSAGE_RANGES = "ranges";
|
||||
|
||||
public static final String VIEW_ONCE = "reveal_duration";
|
||||
|
||||
@@ -168,7 +170,8 @@ public class MmsDatabase extends MessageDatabase {
|
||||
NOTIFIED_TIMESTAMP + " INTEGER DEFAULT 0, " +
|
||||
VIEWED_RECEIPT_COUNT + " INTEGER DEFAULT 0, " +
|
||||
SERVER_GUID + " TEXT DEFAULT NULL, "+
|
||||
RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1);";
|
||||
RECEIPT_TIMESTAMP + " INTEGER DEFAULT -1, " +
|
||||
MESSAGE_RANGES + " BLOB DEFAULT NULL);";
|
||||
|
||||
public static final String[] CREATE_INDEXS = {
|
||||
"CREATE INDEX IF NOT EXISTS mms_read_and_notified_and_thread_id_index ON " + TABLE_NAME + "(" + READ + "," + NOTIFIED + "," + THREAD_ID + ");",
|
||||
@@ -191,7 +194,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
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,
|
||||
SHARED_CONTACTS, LINK_PREVIEWS, UNIDENTIFIED, VIEW_ONCE, REACTIONS_UNREAD, REACTIONS_LAST_SEEN,
|
||||
REMOTE_DELETED, MENTIONS_SELF, NOTIFIED_TIMESTAMP, VIEWED_RECEIPT_COUNT, RECEIPT_TIMESTAMP,
|
||||
REMOTE_DELETED, MENTIONS_SELF, NOTIFIED_TIMESTAMP, VIEWED_RECEIPT_COUNT, RECEIPT_TIMESTAMP, MESSAGE_RANGES,
|
||||
"json_group_array(json_object(" +
|
||||
"'" + AttachmentDatabase.ROW_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.ROW_ID + ", " +
|
||||
"'" + AttachmentDatabase.UNIQUE_ID + "', " + AttachmentDatabase.TABLE_NAME + "." + AttachmentDatabase.UNIQUE_ID + ", " +
|
||||
@@ -496,6 +499,11 @@ public class MmsDatabase extends MessageDatabase {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertBoostRequestMessage(@NonNull RecipientId recipientId, long threadId) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void endTransaction(SQLiteDatabase database) {
|
||||
database.endTransaction();
|
||||
@@ -1351,7 +1359,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
return Optional.absent();
|
||||
}
|
||||
|
||||
long messageId = insertMediaMessage(threadId, retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), retrieved.getLinkPreviews(), retrieved.getMentions(), contentValues, null, true);
|
||||
long messageId = insertMediaMessage(threadId, retrieved.getBody(), retrieved.getAttachments(), quoteAttachments, retrieved.getSharedContacts(), retrieved.getLinkPreviews(), retrieved.getMentions(), retrieved.getMessageRanges(), contentValues, null, true);
|
||||
|
||||
if (!Types.isExpirationTimerUpdate(mailbox)) {
|
||||
SignalDatabase.threads().incrementUnread(threadId, 1);
|
||||
@@ -1540,7 +1548,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
|
||||
MentionUtil.UpdatedBodyAndMentions updatedBodyAndMentions = MentionUtil.updateBodyAndMentionsWithPlaceholders(message.getBody(), message.getMentions());
|
||||
|
||||
long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), contentValues, insertListener, false);
|
||||
long messageId = insertMediaMessage(threadId, updatedBodyAndMentions.getBodyAsString(), message.getAttachments(), quoteAttachments, message.getSharedContacts(), message.getLinkPreviews(), updatedBodyAndMentions.getMentions(), null, contentValues, insertListener, false);
|
||||
|
||||
if (message.getRecipient().isGroup()) {
|
||||
OutgoingGroupUpdateMessage outgoingGroupUpdateMessage = (message instanceof OutgoingGroupUpdateMessage) ? (OutgoingGroupUpdateMessage) message : null;
|
||||
@@ -1593,8 +1601,9 @@ public class MmsDatabase extends MessageDatabase {
|
||||
@NonNull List<Contact> sharedContacts,
|
||||
@NonNull List<LinkPreview> linkPreviews,
|
||||
@NonNull List<Mention> mentions,
|
||||
@Nullable BodyRangeList messageRanges,
|
||||
@NonNull ContentValues contentValues,
|
||||
@Nullable SmsDatabase.InsertListener insertListener,
|
||||
@Nullable InsertListener insertListener,
|
||||
boolean updateThread)
|
||||
throws MmsException
|
||||
{
|
||||
@@ -1616,6 +1625,10 @@ public class MmsDatabase extends MessageDatabase {
|
||||
contentValues.put(PART_COUNT, allAttachments.size());
|
||||
contentValues.put(MENTIONS_SELF, mentionsSelf ? 1 : 0);
|
||||
|
||||
if (messageRanges != null) {
|
||||
contentValues.put(MESSAGE_RANGES, messageRanges.toByteArray());
|
||||
}
|
||||
|
||||
db.beginTransaction();
|
||||
try {
|
||||
long messageId = db.insert(TABLE_NAME, null, contentValues);
|
||||
@@ -1993,7 +2006,8 @@ public class MmsDatabase extends MessageDatabase {
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
-1);
|
||||
-1,
|
||||
null);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2095,6 +2109,7 @@ public class MmsDatabase extends MessageDatabase {
|
||||
long notifiedTimestamp = CursorUtil.requireLong(cursor, NOTIFIED_TIMESTAMP);
|
||||
int viewedReceiptCount = cursor.getInt(cursor.getColumnIndexOrThrow(MmsSmsColumns.VIEWED_RECEIPT_COUNT));
|
||||
long receiptTimestamp = CursorUtil.requireLong(cursor, MmsSmsColumns.RECEIPT_TIMESTAMP);
|
||||
byte[] messageRangesData = CursorUtil.requireBlob(cursor, MESSAGE_RANGES);
|
||||
|
||||
if (!TextSecurePreferences.isReadReceiptsEnabled(context)) {
|
||||
readReceiptCount = 0;
|
||||
@@ -2114,13 +2129,22 @@ public class MmsDatabase extends MessageDatabase {
|
||||
Set<Attachment> previewAttachments = Stream.of(previews).filter(lp -> lp.getThumbnail().isPresent()).map(lp -> lp.getThumbnail().get()).collect(Collectors.toSet());
|
||||
SlideDeck slideDeck = buildSlideDeck(context, Stream.of(attachments).filterNot(contactAttachments::contains).filterNot(previewAttachments::contains).toList());
|
||||
Quote quote = getQuote(cursor);
|
||||
BodyRangeList messageRanges = null;
|
||||
|
||||
try {
|
||||
if (messageRangesData != null) {
|
||||
messageRanges = BodyRangeList.parseFrom(messageRangesData);
|
||||
}
|
||||
} catch (InvalidProtocolBufferException e) {
|
||||
Log.w(TAG, "Error parsing message ranges", e);
|
||||
}
|
||||
|
||||
return new MediaMmsMessageRecord(id, recipient, recipient,
|
||||
addressDeviceId, dateSent, dateReceived, dateServer, deliveryReceiptCount,
|
||||
threadId, body, slideDeck, partCount, box, mismatches,
|
||||
networkFailures, subscriptionId, expiresIn, expireStarted,
|
||||
isViewOnce, readReceiptCount, quote, contacts, previews, unidentified, Collections.emptyList(),
|
||||
remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount, receiptTimestamp);
|
||||
remoteDelete, mentionsSelf, notifiedTimestamp, viewedReceiptCount, receiptTimestamp, messageRanges);
|
||||
}
|
||||
|
||||
private Set<IdentityKeyMismatch> getMismatchedIdentities(String document) {
|
||||
|
||||
@@ -77,6 +77,7 @@ public interface MmsSmsColumns {
|
||||
protected static final long GROUP_CALL_TYPE = 12;
|
||||
protected static final long BAD_DECRYPT_TYPE = 13;
|
||||
protected static final long CHANGE_NUMBER_TYPE = 14;
|
||||
protected static final long BOOST_REQUEST_TYPE = 15;
|
||||
|
||||
protected static final long BASE_INBOX_TYPE = 20;
|
||||
protected static final long BASE_OUTBOX_TYPE = 21;
|
||||
@@ -340,6 +341,10 @@ public interface MmsSmsColumns {
|
||||
return type == CHANGE_NUMBER_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isBoostRequest(long type) {
|
||||
return type == BOOST_REQUEST_TYPE;
|
||||
}
|
||||
|
||||
public static boolean isGroupV2LeaveOnly(long type) {
|
||||
return (type & GROUP_V2_LEAVE_BITS) == GROUP_V2_LEAVE_BITS;
|
||||
}
|
||||
|
||||
@@ -109,10 +109,11 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.MENTIONS_SELF,
|
||||
MmsSmsColumns.NOTIFIED_TIMESTAMP,
|
||||
MmsSmsColumns.VIEWED_RECEIPT_COUNT,
|
||||
MmsSmsColumns.RECEIPT_TIMESTAMP};
|
||||
MmsSmsColumns.RECEIPT_TIMESTAMP,
|
||||
MmsDatabase.MESSAGE_RANGES};
|
||||
|
||||
private static final String SNIPPET_QUERY = "SELECT " + MmsSmsColumns.ID + ", 0 AS " + TRANSPORT + ", " + SmsDatabase.TYPE + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + SmsDatabase.TABLE_NAME + " " +
|
||||
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + SmsDatabase.TYPE + " NOT IN (" + SmsDatabase.Types.PROFILE_CHANGE_TYPE + ", " + SmsDatabase.Types.GV1_MIGRATION_TYPE + ", " + SmsDatabase.Types.CHANGE_NUMBER_TYPE + ") AND " + SmsDatabase.TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " +
|
||||
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + SmsDatabase.TYPE + " NOT IN (" + SmsDatabase.Types.PROFILE_CHANGE_TYPE + ", " + SmsDatabase.Types.GV1_MIGRATION_TYPE + ", " + SmsDatabase.Types.CHANGE_NUMBER_TYPE + ", " + SmsDatabase.Types.BOOST_REQUEST_TYPE + ") AND " + SmsDatabase.TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " +
|
||||
"UNION ALL " +
|
||||
"SELECT " + MmsSmsColumns.ID + ", 1 AS " + TRANSPORT + ", " + MmsDatabase.MESSAGE_BOX + " AS " + MmsSmsColumns.NORMALIZED_TYPE + ", " + MmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED + " FROM " + MmsDatabase.TABLE_NAME + " " +
|
||||
"WHERE " + MmsSmsColumns.THREAD_ID + " = ? AND " + MmsDatabase.MESSAGE_BOX + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + " " +
|
||||
@@ -715,7 +716,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.MENTIONS_SELF,
|
||||
MmsSmsColumns.NOTIFIED_TIMESTAMP,
|
||||
MmsSmsColumns.VIEWED_RECEIPT_COUNT,
|
||||
MmsSmsColumns.RECEIPT_TIMESTAMP};
|
||||
MmsSmsColumns.RECEIPT_TIMESTAMP,
|
||||
MmsDatabase.MESSAGE_RANGES};
|
||||
|
||||
String[] smsProjection = {SmsDatabase.DATE_SENT + " AS " + MmsSmsColumns.NORMALIZED_DATE_SENT,
|
||||
SmsDatabase.DATE_RECEIVED + " AS " + MmsSmsColumns.NORMALIZED_DATE_RECEIVED,
|
||||
@@ -748,7 +750,8 @@ public class MmsSmsDatabase extends Database {
|
||||
MmsDatabase.MENTIONS_SELF,
|
||||
MmsSmsColumns.NOTIFIED_TIMESTAMP,
|
||||
MmsSmsColumns.VIEWED_RECEIPT_COUNT,
|
||||
MmsSmsColumns.RECEIPT_TIMESTAMP};
|
||||
MmsSmsColumns.RECEIPT_TIMESTAMP,
|
||||
MmsDatabase.MESSAGE_RANGES};
|
||||
|
||||
SQLiteQueryBuilder mmsQueryBuilder = new SQLiteQueryBuilder();
|
||||
SQLiteQueryBuilder smsQueryBuilder = new SQLiteQueryBuilder();
|
||||
@@ -810,6 +813,7 @@ public class MmsSmsDatabase extends Database {
|
||||
mmsColumnsPresent.add(MmsSmsColumns.NOTIFIED_TIMESTAMP);
|
||||
mmsColumnsPresent.add(MmsSmsColumns.VIEWED_RECEIPT_COUNT);
|
||||
mmsColumnsPresent.add(MmsSmsColumns.RECEIPT_TIMESTAMP);
|
||||
mmsColumnsPresent.add(MmsDatabase.MESSAGE_RANGES);
|
||||
|
||||
Set<String> smsColumnsPresent = new HashSet<>();
|
||||
smsColumnsPresent.add(MmsSmsColumns.ID);
|
||||
|
||||
@@ -272,8 +272,8 @@ public class SmsDatabase extends MessageDatabase {
|
||||
}
|
||||
|
||||
private @NonNull SqlUtil.Query buildMeaningfulMessagesQuery(long threadId) {
|
||||
String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + ")";
|
||||
return SqlUtil.buildQuery(query, threadId, IGNORABLE_TYPESMASK_WHEN_COUNTING, Types.PROFILE_CHANGE_TYPE, Types.CHANGE_NUMBER_TYPE);
|
||||
String query = THREAD_ID + " = ? AND (NOT " + TYPE + " & ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " != ? AND " + TYPE + " & " + GROUP_V2_LEAVE_BITS + " != " + GROUP_V2_LEAVE_BITS + ")";
|
||||
return SqlUtil.buildQuery(query, threadId, IGNORABLE_TYPESMASK_WHEN_COUNTING, Types.PROFILE_CHANGE_TYPE, Types.CHANGE_NUMBER_TYPE, Types.BOOST_REQUEST_TYPE);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -1071,6 +1071,21 @@ public class SmsDatabase extends MessageDatabase {
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void insertBoostRequestMessage(@NonNull RecipientId recipientId, long threadId) {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put(RECIPIENT_ID, recipientId.serialize());
|
||||
values.put(ADDRESS_DEVICE_ID, 1);
|
||||
values.put(DATE_RECEIVED, System.currentTimeMillis());
|
||||
values.put(DATE_SENT, System.currentTimeMillis());
|
||||
values.put(READ, 1);
|
||||
values.put(TYPE, Types.BOOST_REQUEST_TYPE);
|
||||
values.put(THREAD_ID, threadId);
|
||||
values.putNull(BODY);
|
||||
|
||||
getWritableDatabase().insert(TABLE_NAME, null, values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<InsertResult> insertMessageInbox(IncomingTextMessage message, long type) {
|
||||
if (message.isJoined()) {
|
||||
|
||||
@@ -1522,6 +1522,7 @@ public class ThreadDatabase extends Database {
|
||||
return MmsSmsColumns.Types.isProfileChange(type) ||
|
||||
MmsSmsColumns.Types.isGroupV1MigrationEvent(type) ||
|
||||
MmsSmsColumns.Types.isChangeNumber(type) ||
|
||||
MmsSmsColumns.Types.isBoostRequest(type) ||
|
||||
MmsSmsColumns.Types.isGroupV2LeaveOnly(type);
|
||||
}
|
||||
|
||||
|
||||
@@ -183,8 +183,9 @@ object SignalDatabaseMigrations {
|
||||
private const val REACTION_BACKUP_CLEANUP = 125
|
||||
private const val REACTION_REMOTE_DELETE_CLEANUP = 126
|
||||
private const val PNI_CLEANUP = 127
|
||||
private const val MESSAGE_RANGES = 128
|
||||
|
||||
const val DATABASE_VERSION = 127
|
||||
const val DATABASE_VERSION = 128
|
||||
|
||||
@JvmStatic
|
||||
fun migrate(context: Context, db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
@@ -2263,6 +2264,10 @@ object SignalDatabaseMigrations {
|
||||
if (oldVersion < PNI_CLEANUP) {
|
||||
db.execSQL("UPDATE recipient SET pni = NULL WHERE phone IS NULL")
|
||||
}
|
||||
|
||||
if (oldVersion < MESSAGE_RANGES) {
|
||||
db.execSQL("ALTER TABLE mms ADD COLUMN ranges BLOB DEFAULT NULL")
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package org.thoughtcrime.securesms.database.model
|
||||
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
|
||||
/**
|
||||
* Collection of extensions to make working with database protos cleaner.
|
||||
*/
|
||||
|
||||
fun BodyRangeList.Builder.addStyle(style: BodyRangeList.BodyRange.Style, start: Int, length: Int): BodyRangeList.Builder {
|
||||
addRanges(
|
||||
BodyRangeList.BodyRange.newBuilder()
|
||||
.setStyle(style)
|
||||
.setStart(start)
|
||||
.setLength(length)
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun BodyRangeList.Builder.addLink(link: String, start: Int, length: Int): BodyRangeList.Builder {
|
||||
addRanges(
|
||||
BodyRangeList.BodyRange.newBuilder()
|
||||
.setLink(link)
|
||||
.setStart(start)
|
||||
.setLength(length)
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
fun BodyRangeList.Builder.addButton(label: String, action: String, start: Int, length: Int): BodyRangeList.Builder {
|
||||
addRanges(
|
||||
BodyRangeList.BodyRange.newBuilder()
|
||||
.setButton(BodyRangeList.BodyRange.Button.newBuilder().setLabel(label).setAction(action))
|
||||
.setStart(start)
|
||||
.setLength(length)
|
||||
)
|
||||
|
||||
return this
|
||||
}
|
||||
@@ -182,6 +182,10 @@ public abstract class DisplayRecord {
|
||||
return SmsDatabase.Types.isChangeNumber(type);
|
||||
}
|
||||
|
||||
public boolean isBoostRequest() {
|
||||
return MmsSmsColumns.Types.isBoostRequest(type);
|
||||
}
|
||||
|
||||
public int getDeliveryStatus() {
|
||||
return deliveryStatus;
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.thoughtcrime.securesms.database.MmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase.Status;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.mms.SlideDeck;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
@@ -56,8 +57,9 @@ import java.util.stream.Collectors;
|
||||
public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
private final static String TAG = Log.tag(MediaMmsMessageRecord.class);
|
||||
|
||||
private final int partCount;
|
||||
private final boolean mentionsSelf;
|
||||
private final int partCount;
|
||||
private final boolean mentionsSelf;
|
||||
private final BodyRangeList messageRanges;
|
||||
|
||||
public MediaMmsMessageRecord(long id,
|
||||
Recipient conversationRecipient,
|
||||
@@ -88,14 +90,16 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
boolean mentionsSelf,
|
||||
long notifiedTimestamp,
|
||||
int viewedReceiptCount,
|
||||
long receiptTimestamp)
|
||||
long receiptTimestamp,
|
||||
@Nullable BodyRangeList messageRanges)
|
||||
{
|
||||
super(id, body, conversationRecipient, individualRecipient, recipientDeviceId, dateSent,
|
||||
dateReceived, dateServer, threadId, Status.STATUS_NONE, deliveryReceiptCount, mailbox, mismatches, failures,
|
||||
subscriptionId, expiresIn, expireStarted, viewOnce, slideDeck,
|
||||
readReceiptCount, quote, contacts, linkPreviews, unidentified, reactions, remoteDelete, notifiedTimestamp, viewedReceiptCount, receiptTimestamp);
|
||||
this.partCount = partCount;
|
||||
this.mentionsSelf = mentionsSelf;
|
||||
this.partCount = partCount;
|
||||
this.mentionsSelf = mentionsSelf;
|
||||
this.messageRanges = messageRanges;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -128,11 +132,25 @@ public class MediaMmsMessageRecord extends MmsMessageRecord {
|
||||
return partCount;
|
||||
}
|
||||
|
||||
public @Nullable BodyRangeList getMessageRanges() {
|
||||
return messageRanges;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasMessageRanges() {
|
||||
return messageRanges != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public @NonNull BodyRangeList requireMessageRanges() {
|
||||
return Objects.requireNonNull(messageRanges);
|
||||
}
|
||||
|
||||
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());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges());
|
||||
}
|
||||
|
||||
public @NonNull MediaMmsMessageRecord withAttachments(@NonNull Context context, @NonNull List<DatabaseAttachment> attachments) {
|
||||
@@ -153,7 +171,7 @@ 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());
|
||||
getNotifiedTimestamp(), getViewedReceiptCount(), getReceiptTimestamp(), getMessageRanges());
|
||||
}
|
||||
|
||||
private static @NonNull List<Contact> updateContacts(@NonNull List<Contact> contacts, @NonNull Map<AttachmentId, DatabaseAttachment> attachmentIdMap) {
|
||||
|
||||
@@ -41,12 +41,14 @@ import org.thoughtcrime.securesms.database.MmsSmsColumns;
|
||||
import org.thoughtcrime.securesms.database.SmsDatabase;
|
||||
import org.thoughtcrime.securesms.database.documents.IdentityKeyMismatch;
|
||||
import org.thoughtcrime.securesms.database.documents.NetworkFailure;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.DecryptedGroupV2Context;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.GroupCallUpdateDetails;
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.ProfileChangeDetails;
|
||||
import org.thoughtcrime.securesms.emoji.EmojiSource;
|
||||
import org.thoughtcrime.securesms.emoji.JumboEmoji;
|
||||
import org.thoughtcrime.securesms.groups.GroupMigrationMembershipChange;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.profiles.ProfileName;
|
||||
import org.thoughtcrime.securesms.recipients.Recipient;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
@@ -199,6 +201,10 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return staticUpdateDescription(getProfileChangeDescription(context), R.drawable.ic_update_profile_16);
|
||||
} else if (isChangeNumber()) {
|
||||
return fromRecipient(getIndividualRecipient(), r -> context.getString(R.string.MessageRecord_s_changed_their_phone_number, r.getDisplayName(context)), R.drawable.ic_phone_16);
|
||||
} else if (isBoostRequest()) {
|
||||
int message = SignalStore.donationsValues().isLikelyASustainer() ? R.string.MessageRecord_like_this_new_feature_say_thanks_with_a_boost
|
||||
: R.string.MessageRecord_signal_is_powered_by_people_like_you_become_a_sustainer_today;
|
||||
return staticUpdateDescription(context.getString(message), 0);
|
||||
} else if (isEndSession()) {
|
||||
if (isOutgoing()) return staticUpdateDescription(context.getString(R.string.SmsMessageRecord_secure_session_reset), R.drawable.ic_update_info_16);
|
||||
else return fromRecipient(getIndividualRecipient(), r-> context.getString(R.string.SmsMessageRecord_secure_session_reset_s, r.getDisplayName(context)), R.drawable.ic_update_info_16);
|
||||
@@ -502,7 +508,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return isGroupAction() || isJoined() || isExpirationTimerUpdate() || isCallLog() ||
|
||||
isEndSession() || isIdentityUpdate() || isIdentityVerified() || isIdentityDefault() ||
|
||||
isProfileChange() || isGroupV1MigrationEvent() || isChatSessionRefresh() || isBadDecryptType() ||
|
||||
isChangeNumber();
|
||||
isChangeNumber() || isBoostRequest();
|
||||
}
|
||||
|
||||
public boolean isMediaPending() {
|
||||
@@ -620,6 +626,14 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
return isJumboji;
|
||||
}
|
||||
|
||||
public boolean hasMessageRanges() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public @NonNull BodyRangeList requireMessageRanges() {
|
||||
throw new NullPointerException();
|
||||
}
|
||||
|
||||
public static final class InviteAddState {
|
||||
|
||||
private final boolean invited;
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.thoughtcrime.securesms.jobmanager.Job;
|
||||
import org.thoughtcrime.securesms.jobmanager.JobLogger;
|
||||
import org.thoughtcrime.securesms.jobmanager.impl.NetworkConstraint;
|
||||
import org.thoughtcrime.securesms.mms.MmsException;
|
||||
import org.thoughtcrime.securesms.releasechannel.ReleaseChannel;
|
||||
import org.thoughtcrime.securesms.transport.RetryLaterException;
|
||||
import org.thoughtcrime.securesms.util.AttachmentUtil;
|
||||
import org.thoughtcrime.securesms.util.Base64;
|
||||
@@ -38,8 +39,14 @@ import org.whispersystems.signalservice.api.push.exceptions.RangeException;
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.Request;
|
||||
import okhttp3.Response;
|
||||
import okhttp3.ResponseBody;
|
||||
import okio.Okio;
|
||||
|
||||
public final class AttachmentDownloadJob extends BaseJob {
|
||||
|
||||
public static final String KEY = "AttachmentDownloadJob";
|
||||
@@ -139,7 +146,11 @@ public final class AttachmentDownloadJob extends BaseJob {
|
||||
Log.i(TAG, "Downloading push part " + attachmentId);
|
||||
database.setTransferState(messageId, attachmentId, AttachmentDatabase.TRANSFER_PROGRESS_STARTED);
|
||||
|
||||
retrieveAttachment(messageId, attachmentId, attachment);
|
||||
if (attachment.getCdnNumber() != ReleaseChannel.CDN_NUMBER) {
|
||||
retrieveAttachment(messageId, attachmentId, attachment);
|
||||
} else {
|
||||
retrieveUrlAttachment(messageId, attachmentId, attachment);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -222,6 +233,27 @@ public final class AttachmentDownloadJob extends BaseJob {
|
||||
}
|
||||
}
|
||||
|
||||
private void retrieveUrlAttachment(long messageId,
|
||||
final AttachmentId attachmentId,
|
||||
final Attachment attachment)
|
||||
throws IOException
|
||||
{
|
||||
Request request = new Request.Builder()
|
||||
.get()
|
||||
.url(Objects.requireNonNull(attachment.getFileName()))
|
||||
.build();
|
||||
|
||||
try (Response response = ApplicationDependencies.getOkHttpClient().newCall(request).execute()) {
|
||||
ResponseBody body = response.body();
|
||||
if (body != null) {
|
||||
SignalDatabase.attachments().insertAttachmentsForPlaceholder(messageId, attachmentId, Okio.buffer(body.source()).inputStream());
|
||||
}
|
||||
} catch (MmsException e) {
|
||||
Log.w(TAG, "Experienced exception while trying to download an attachment.", e);
|
||||
markFailed(messageId, attachmentId);
|
||||
}
|
||||
}
|
||||
|
||||
private void markFailed(long messageId, AttachmentId attachmentId) {
|
||||
try {
|
||||
AttachmentDatabase database = SignalDatabase.attachments();
|
||||
|
||||
@@ -75,7 +75,10 @@ public class MessageRequestsBottomView extends ConstraintLayout {
|
||||
|
||||
switch (messageData.getMessageState()) {
|
||||
case BLOCKED_INDIVIDUAL:
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_wont_receive_any_messages_until_you_unblock_them,
|
||||
int message = recipient.isReleaseNotes() ? R.string.MessageRequestBottomView_get_updates_and_news_from_s_you_wont_receive_any_updates_until_you_unblock_them
|
||||
: R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_wont_receive_any_messages_until_you_unblock_them;
|
||||
|
||||
question.setText(HtmlCompat.fromHtml(getContext().getString(message,
|
||||
HtmlUtil.bold(recipient.getShortDisplayName(getContext()))), 0));
|
||||
setActiveInactiveGroups(blockedButtons, normalButtons, gv1MigrationButtons);
|
||||
break;
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
package org.thoughtcrime.securesms.mms;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment;
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment;
|
||||
import org.thoughtcrime.securesms.contactshare.Contact;
|
||||
import org.thoughtcrime.securesms.database.model.Mention;
|
||||
import org.thoughtcrime.securesms.groups.GroupId;
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview;
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId;
|
||||
import org.thoughtcrime.securesms.util.GroupUtil;
|
||||
import org.whispersystems.libsignal.util.guava.Optional;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment;
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedList;
|
||||
import java.util.List;
|
||||
|
||||
public class IncomingMediaMessage {
|
||||
|
||||
private final RecipientId from;
|
||||
private final GroupId groupId;
|
||||
private final String body;
|
||||
private final boolean push;
|
||||
private final long sentTimeMillis;
|
||||
private final long serverTimeMillis;
|
||||
private final long receivedTimeMillis;
|
||||
private final int subscriptionId;
|
||||
private final long expiresIn;
|
||||
private final boolean expirationUpdate;
|
||||
private final QuoteModel quote;
|
||||
private final boolean unidentified;
|
||||
private final boolean viewOnce;
|
||||
private final String serverGuid;
|
||||
|
||||
private final List<Attachment> attachments = new LinkedList<>();
|
||||
private final List<Contact> sharedContacts = new LinkedList<>();
|
||||
private final List<LinkPreview> linkPreviews = new LinkedList<>();
|
||||
private final List<Mention> mentions = new LinkedList<>();
|
||||
|
||||
public IncomingMediaMessage(@NonNull RecipientId from,
|
||||
Optional<GroupId> groupId,
|
||||
String body,
|
||||
long sentTimeMillis,
|
||||
long serverTimeMillis,
|
||||
long receivedTimeMillis,
|
||||
List<Attachment> attachments,
|
||||
int subscriptionId,
|
||||
long expiresIn,
|
||||
boolean expirationUpdate,
|
||||
boolean viewOnce,
|
||||
boolean unidentified,
|
||||
Optional<List<Contact>> sharedContacts)
|
||||
{
|
||||
this.from = from;
|
||||
this.groupId = groupId.orNull();
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.serverTimeMillis = serverTimeMillis;
|
||||
this.receivedTimeMillis = receivedTimeMillis;
|
||||
this.body = body;
|
||||
this.push = false;
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.viewOnce = viewOnce;
|
||||
this.quote = null;
|
||||
this.unidentified = unidentified;
|
||||
this.serverGuid = null;
|
||||
|
||||
this.attachments.addAll(attachments);
|
||||
this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
|
||||
|
||||
}
|
||||
|
||||
public IncomingMediaMessage(@NonNull RecipientId from,
|
||||
long sentTimeMillis,
|
||||
long serverTimeMillis,
|
||||
long receivedTimeMillis,
|
||||
int subscriptionId,
|
||||
long expiresIn,
|
||||
boolean expirationUpdate,
|
||||
boolean viewOnce,
|
||||
boolean unidentified,
|
||||
Optional<String> body,
|
||||
Optional<SignalServiceGroupContext> group,
|
||||
Optional<List<SignalServiceAttachment>> attachments,
|
||||
Optional<QuoteModel> quote,
|
||||
Optional<List<Contact>> sharedContacts,
|
||||
Optional<List<LinkPreview>> linkPreviews,
|
||||
Optional<List<Mention>> mentions,
|
||||
Optional<Attachment> sticker,
|
||||
@Nullable String serverGuid)
|
||||
{
|
||||
this.push = true;
|
||||
this.from = from;
|
||||
this.sentTimeMillis = sentTimeMillis;
|
||||
this.serverTimeMillis = serverTimeMillis;
|
||||
this.receivedTimeMillis = receivedTimeMillis;
|
||||
this.body = body.orNull();
|
||||
this.subscriptionId = subscriptionId;
|
||||
this.expiresIn = expiresIn;
|
||||
this.expirationUpdate = expirationUpdate;
|
||||
this.viewOnce = viewOnce;
|
||||
this.quote = quote.orNull();
|
||||
this.unidentified = unidentified;
|
||||
|
||||
if (group.isPresent()) this.groupId = GroupUtil.idFromGroupContextOrThrow(group.get());
|
||||
else this.groupId = null;
|
||||
|
||||
this.attachments.addAll(PointerAttachment.forPointers(attachments));
|
||||
this.sharedContacts.addAll(sharedContacts.or(Collections.emptyList()));
|
||||
this.linkPreviews.addAll(linkPreviews.or(Collections.emptyList()));
|
||||
this.mentions.addAll(mentions.or(Collections.emptyList()));
|
||||
|
||||
if (sticker.isPresent()) {
|
||||
this.attachments.add(sticker.get());
|
||||
}
|
||||
|
||||
this.serverGuid = serverGuid;
|
||||
}
|
||||
|
||||
public int getSubscriptionId() {
|
||||
return subscriptionId;
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return body;
|
||||
}
|
||||
|
||||
public List<Attachment> getAttachments() {
|
||||
return attachments;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getFrom() {
|
||||
return from;
|
||||
}
|
||||
|
||||
public GroupId getGroupId() {
|
||||
return groupId;
|
||||
}
|
||||
|
||||
public boolean isPushMessage() {
|
||||
return push;
|
||||
}
|
||||
|
||||
public boolean isExpirationUpdate() {
|
||||
return expirationUpdate;
|
||||
}
|
||||
|
||||
public long getSentTimeMillis() {
|
||||
return sentTimeMillis;
|
||||
}
|
||||
|
||||
public long getServerTimeMillis() {
|
||||
return serverTimeMillis;
|
||||
}
|
||||
|
||||
public long getReceivedTimeMillis() {
|
||||
return receivedTimeMillis;
|
||||
}
|
||||
|
||||
public long getExpiresIn() {
|
||||
return expiresIn;
|
||||
}
|
||||
|
||||
public boolean isViewOnce() {
|
||||
return viewOnce;
|
||||
}
|
||||
|
||||
public boolean isGroupMessage() {
|
||||
return groupId != null;
|
||||
}
|
||||
|
||||
public QuoteModel getQuote() {
|
||||
return quote;
|
||||
}
|
||||
|
||||
public List<Contact> getSharedContacts() {
|
||||
return sharedContacts;
|
||||
}
|
||||
|
||||
public List<LinkPreview> getLinkPreviews() {
|
||||
return linkPreviews;
|
||||
}
|
||||
|
||||
public @NonNull List<Mention> getMentions() {
|
||||
return mentions;
|
||||
}
|
||||
|
||||
public boolean isUnidentified() {
|
||||
return unidentified;
|
||||
}
|
||||
|
||||
public @Nullable String getServerGuid() {
|
||||
return serverGuid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package org.thoughtcrime.securesms.mms
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.Attachment
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.contactshare.Contact
|
||||
import org.thoughtcrime.securesms.database.model.Mention
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.groups.GroupId
|
||||
import org.thoughtcrime.securesms.linkpreview.LinkPreview
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.thoughtcrime.securesms.util.GroupUtil
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceGroupContext
|
||||
|
||||
class IncomingMediaMessage(
|
||||
val from: RecipientId,
|
||||
val groupId: GroupId? = null,
|
||||
val body: String? = null,
|
||||
val isPushMessage: Boolean = false,
|
||||
val sentTimeMillis: Long,
|
||||
val serverTimeMillis: Long,
|
||||
val receivedTimeMillis: Long,
|
||||
val subscriptionId: Int = -1,
|
||||
val expiresIn: Long = 0,
|
||||
val isExpirationUpdate: Boolean = false,
|
||||
val quote: QuoteModel? = null,
|
||||
val isUnidentified: Boolean = false,
|
||||
val isViewOnce: Boolean = false,
|
||||
val serverGuid: String? = null,
|
||||
val messageRanges: BodyRangeList? = null,
|
||||
attachments: List<Attachment> = emptyList(),
|
||||
sharedContacts: List<Contact> = emptyList(),
|
||||
linkPreviews: List<LinkPreview> = emptyList(),
|
||||
mentions: List<Mention> = emptyList()
|
||||
) {
|
||||
|
||||
val attachments: List<Attachment> = ArrayList(attachments)
|
||||
val sharedContacts: List<Contact> = ArrayList(sharedContacts)
|
||||
val linkPreviews: List<LinkPreview> = ArrayList(linkPreviews)
|
||||
val mentions: List<Mention> = ArrayList(mentions)
|
||||
|
||||
val isGroupMessage: Boolean = groupId != null
|
||||
|
||||
constructor(
|
||||
from: RecipientId,
|
||||
groupId: Optional<GroupId>,
|
||||
body: String,
|
||||
sentTimeMillis: Long,
|
||||
serverTimeMillis: Long,
|
||||
receivedTimeMillis: Long,
|
||||
attachments: List<Attachment>,
|
||||
subscriptionId: Int,
|
||||
expiresIn: Long,
|
||||
expirationUpdate: Boolean,
|
||||
viewOnce: Boolean,
|
||||
unidentified: Boolean,
|
||||
sharedContacts: Optional<List<Contact>>
|
||||
) : this(
|
||||
from = from,
|
||||
groupId = groupId.orNull(),
|
||||
body = body,
|
||||
isPushMessage = false,
|
||||
sentTimeMillis = sentTimeMillis,
|
||||
serverTimeMillis = serverTimeMillis,
|
||||
receivedTimeMillis = receivedTimeMillis,
|
||||
subscriptionId = subscriptionId,
|
||||
expiresIn = expiresIn,
|
||||
isExpirationUpdate = expirationUpdate,
|
||||
quote = null,
|
||||
isUnidentified = unidentified,
|
||||
isViewOnce = viewOnce,
|
||||
serverGuid = null,
|
||||
attachments = ArrayList(attachments),
|
||||
sharedContacts = ArrayList(sharedContacts.or(emptyList()))
|
||||
)
|
||||
|
||||
constructor(
|
||||
from: RecipientId,
|
||||
sentTimeMillis: Long,
|
||||
serverTimeMillis: Long,
|
||||
receivedTimeMillis: Long,
|
||||
subscriptionId: Int,
|
||||
expiresIn: Long,
|
||||
expirationUpdate: Boolean,
|
||||
viewOnce: Boolean,
|
||||
unidentified: Boolean,
|
||||
body: Optional<String>,
|
||||
group: Optional<SignalServiceGroupContext>,
|
||||
attachments: Optional<List<SignalServiceAttachment>>,
|
||||
quote: Optional<QuoteModel>,
|
||||
sharedContacts: Optional<List<Contact>>,
|
||||
linkPreviews: Optional<List<LinkPreview>>,
|
||||
mentions: Optional<List<Mention>>,
|
||||
sticker: Optional<Attachment>,
|
||||
serverGuid: String?
|
||||
) : this(
|
||||
from = from,
|
||||
groupId = if (group.isPresent) GroupUtil.idFromGroupContextOrThrow(group.get()) else null,
|
||||
body = body.orNull(),
|
||||
isPushMessage = true,
|
||||
sentTimeMillis = sentTimeMillis,
|
||||
serverTimeMillis = serverTimeMillis,
|
||||
receivedTimeMillis = receivedTimeMillis,
|
||||
subscriptionId = subscriptionId,
|
||||
expiresIn = expiresIn,
|
||||
isExpirationUpdate = expirationUpdate,
|
||||
quote = quote.orNull(),
|
||||
isUnidentified = unidentified,
|
||||
isViewOnce = viewOnce,
|
||||
serverGuid = serverGuid,
|
||||
attachments = PointerAttachment.forPointers(attachments).apply { if (sticker.isPresent) add(sticker.get()) },
|
||||
sharedContacts = sharedContacts.or(emptyList()),
|
||||
linkPreviews = linkPreviews.or(emptyList()),
|
||||
mentions = mentions.or(emptyList())
|
||||
)
|
||||
}
|
||||
@@ -128,6 +128,7 @@ public class Recipient {
|
||||
private final Optional<Extras> extras;
|
||||
private final boolean hasGroupsInCommon;
|
||||
private final List<Badge> badges;
|
||||
private final boolean isReleaseNotesRecipient;
|
||||
|
||||
/**
|
||||
* Returns a {@link LiveRecipient}, which contains a {@link Recipient} that may or may not be
|
||||
@@ -382,6 +383,7 @@ public class Recipient {
|
||||
this.extras = Optional.absent();
|
||||
this.hasGroupsInCommon = false;
|
||||
this.badges = Collections.emptyList();
|
||||
this.isReleaseNotesRecipient = false;
|
||||
}
|
||||
|
||||
public Recipient(@NonNull RecipientId id, @NonNull RecipientDetails details, boolean resolved) {
|
||||
@@ -437,6 +439,7 @@ public class Recipient {
|
||||
this.extras = details.extras;
|
||||
this.hasGroupsInCommon = details.hasGroupsInCommon;
|
||||
this.badges = details.badges;
|
||||
this.isReleaseNotesRecipient = false;
|
||||
}
|
||||
|
||||
public @NonNull RecipientId getId() {
|
||||
@@ -1104,6 +1107,10 @@ public class Recipient {
|
||||
return mentionSetting;
|
||||
}
|
||||
|
||||
public boolean isReleaseNotes() {
|
||||
return isReleaseNotesRecipient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
@@ -169,16 +169,23 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
}
|
||||
|
||||
String name = recipient.isSelf() ? requireContext().getString(R.string.note_to_self)
|
||||
: recipient.getDisplayName(requireContext());
|
||||
: recipient.getDisplayName(requireContext());
|
||||
fullName.setText(name);
|
||||
fullName.setVisibility(TextUtils.isEmpty(name) ? View.GONE : View.VISIBLE);
|
||||
if (recipient.isSystemContact() && !recipient.isSelf()) {
|
||||
fullName.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_profile_circle_outline_16, 0);
|
||||
fullName.setCompoundDrawablePadding(ViewUtil.dpToPx(4));
|
||||
TextViewCompat.setCompoundDrawableTintList(fullName, ColorStateList.valueOf(ContextCompat.getColor(requireContext(), R.color.signal_text_primary)));
|
||||
} else if (recipient.isReleaseNotes()) {
|
||||
fullName.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_official_28, 0);
|
||||
fullName.setCompoundDrawablePadding(ViewUtil.dpToPx(4));
|
||||
}
|
||||
|
||||
String aboutText = recipient.getCombinedAboutAndEmoji();
|
||||
if (recipient.isReleaseNotes()) {
|
||||
aboutText = getString(R.string.ReleaseNotes__signal_release_notes_and_news);
|
||||
}
|
||||
|
||||
if (!Util.isEmpty(aboutText)) {
|
||||
about.setText(aboutText);
|
||||
about.setVisibility(View.VISIBLE);
|
||||
@@ -211,9 +218,9 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
}
|
||||
|
||||
ButtonStripPreference.State buttonStripState = new ButtonStripPreference.State(
|
||||
/* isMessageAvailable = */ !recipient.isBlocked() && !recipient.isSelf(),
|
||||
/* isMessageAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && !recipient.isReleaseNotes(),
|
||||
/* isVideoAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && recipient.isRegistered(),
|
||||
/* isAudioAvailable = */ !recipient.isBlocked() && !recipient.isSelf(),
|
||||
/* isAudioAvailable = */ !recipient.isBlocked() && !recipient.isSelf() && !recipient.isReleaseNotes(),
|
||||
/* isMuteAvailable = */ false,
|
||||
/* isSearchAvailable = */ false,
|
||||
/* isAudioSecure = */ recipient.isRegistered(),
|
||||
@@ -246,7 +253,11 @@ public final class RecipientBottomSheetDialogFragment extends BottomSheetDialogF
|
||||
|
||||
new ButtonStripPreference.ViewHolder(buttonStrip).bind(buttonStripModel);
|
||||
|
||||
if (recipient.isSystemContact() || recipient.isGroup() || recipient.isSelf() || recipient.isBlocked()) {
|
||||
if (recipient.isReleaseNotes()) {
|
||||
buttonStrip.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
if (recipient.isSystemContact() || recipient.isGroup() || recipient.isSelf() || recipient.isBlocked() || recipient.isReleaseNotes()) {
|
||||
addContactButton.setVisibility(View.GONE);
|
||||
} else {
|
||||
addContactButton.setVisibility(View.VISIBLE);
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
package org.thoughtcrime.securesms.releasechannel
|
||||
|
||||
import org.thoughtcrime.securesms.attachments.PointerAttachment
|
||||
import org.thoughtcrime.securesms.database.MessageDatabase
|
||||
import org.thoughtcrime.securesms.database.SignalDatabase
|
||||
import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList
|
||||
import org.thoughtcrime.securesms.mms.IncomingMediaMessage
|
||||
import org.thoughtcrime.securesms.recipients.RecipientId
|
||||
import org.whispersystems.libsignal.util.guava.Optional
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachment
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentPointer
|
||||
import org.whispersystems.signalservice.api.messages.SignalServiceAttachmentRemoteId
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* One stop shop for inserting Release Channel messages.
|
||||
*/
|
||||
object ReleaseChannel {
|
||||
|
||||
const val CDN_NUMBER = -1
|
||||
|
||||
fun insertAnnouncement(
|
||||
recipientId: RecipientId,
|
||||
body: String,
|
||||
threadId: Long,
|
||||
image: String? = null,
|
||||
serverUuid: String? = UUID.randomUUID().toString(),
|
||||
messageRanges: BodyRangeList? = null
|
||||
): MessageDatabase.InsertResult? {
|
||||
|
||||
val attachments: Optional<List<SignalServiceAttachment>> = if (image != null) {
|
||||
val attachment = SignalServiceAttachmentPointer(
|
||||
CDN_NUMBER,
|
||||
SignalServiceAttachmentRemoteId.from(""),
|
||||
"image/webp",
|
||||
null,
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
0,
|
||||
0,
|
||||
Optional.absent(),
|
||||
Optional.of(image),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Optional.absent(),
|
||||
Optional.absent(),
|
||||
System.currentTimeMillis()
|
||||
)
|
||||
|
||||
Optional.of(listOf(attachment))
|
||||
} else {
|
||||
Optional.absent()
|
||||
}
|
||||
|
||||
val message = IncomingMediaMessage(
|
||||
from = recipientId,
|
||||
sentTimeMillis = System.currentTimeMillis(),
|
||||
serverTimeMillis = System.currentTimeMillis(),
|
||||
receivedTimeMillis = System.currentTimeMillis(),
|
||||
body = body,
|
||||
attachments = PointerAttachment.forPointers(attachments),
|
||||
serverGuid = serverUuid,
|
||||
messageRanges = messageRanges
|
||||
)
|
||||
|
||||
return SignalDatabase.mms.insertSecureDecryptedMessageInbox(message, threadId).orNull()
|
||||
}
|
||||
}
|
||||
@@ -111,6 +111,7 @@ public class AttachmentUtil {
|
||||
return recipient.isSystemContact() ||
|
||||
recipient.isProfileSharing() ||
|
||||
message.isOutgoing() ||
|
||||
recipient.isSelf();
|
||||
recipient.isSelf() ||
|
||||
recipient.isReleaseNotes();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.view.View;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
/**
|
||||
@@ -14,7 +15,15 @@ public final class InterceptableLongClickCopyLinkSpan extends LongClickCopySpan
|
||||
public InterceptableLongClickCopyLinkSpan(@NonNull String url,
|
||||
@NonNull UrlClickHandler onClickListener)
|
||||
{
|
||||
super(url);
|
||||
this(url, onClickListener, null, true);
|
||||
}
|
||||
|
||||
public InterceptableLongClickCopyLinkSpan(@NonNull String url,
|
||||
@NonNull UrlClickHandler onClickListener,
|
||||
@ColorInt Integer textColor,
|
||||
boolean underline)
|
||||
{
|
||||
super(url, textColor, underline);
|
||||
this.onClickListener = onClickListener;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.annotation.TargetApi;
|
||||
import android.content.ClipData;
|
||||
import android.content.Context;
|
||||
import android.text.TextPaint;
|
||||
import android.text.style.URLSpan;
|
||||
@@ -21,23 +19,34 @@ public class LongClickCopySpan extends URLSpan {
|
||||
@ColorInt
|
||||
private int highlightColor;
|
||||
|
||||
private final Integer textColor;
|
||||
private final boolean underline;
|
||||
|
||||
public LongClickCopySpan(String url) {
|
||||
this(url, null, true);
|
||||
}
|
||||
|
||||
public LongClickCopySpan(String url, @ColorInt Integer textColor, boolean underline) {
|
||||
super(url);
|
||||
this.textColor = textColor;
|
||||
this.underline = underline;
|
||||
}
|
||||
|
||||
void onLongClick(View widget) {
|
||||
Context context = widget.getContext();
|
||||
String preparedUrl = prepareUrl(getURL());
|
||||
copyUrl(context, preparedUrl);
|
||||
Toast.makeText(context,
|
||||
context.getString(R.string.ConversationItem_copied_text, preparedUrl), Toast.LENGTH_SHORT).show();
|
||||
Toast.makeText(context, context.getString(R.string.ConversationItem_copied_text, preparedUrl), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
super.updateDrawState(ds);
|
||||
if (textColor != null) {
|
||||
ds.setColor(textColor);
|
||||
}
|
||||
ds.bgColor = highlightColor;
|
||||
ds.setUnderlineText(!isHighlighted);
|
||||
ds.setUnderlineText(!isHighlighted && underline);
|
||||
}
|
||||
|
||||
void setHighlighted(boolean highlighted, @ColorInt int highlightColor) {
|
||||
@@ -46,22 +55,7 @@ public class LongClickCopySpan extends URLSpan {
|
||||
}
|
||||
|
||||
private void copyUrl(Context context, String url) {
|
||||
int sdk = android.os.Build.VERSION.SDK_INT;
|
||||
if (sdk < android.os.Build.VERSION_CODES.HONEYCOMB) {
|
||||
@SuppressWarnings("deprecation") android.text.ClipboardManager clipboard =
|
||||
(android.text.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboard.setText(url);
|
||||
} else {
|
||||
copyUriSdk11(context, url);
|
||||
}
|
||||
}
|
||||
|
||||
@TargetApi(android.os.Build.VERSION_CODES.HONEYCOMB)
|
||||
private void copyUriSdk11(Context context, String url) {
|
||||
android.content.ClipboardManager clipboard =
|
||||
(android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText(context.getString(R.string.app_name), url);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
Util.writeTextToClipboard(context, url);
|
||||
}
|
||||
|
||||
private String prepareUrl(String url) {
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
/**
|
||||
* LinkifyCompat.addLinks() will strip pre-existing URLSpans. This acts as a way to
|
||||
* indicate where a link should be added without being stripped. The consumer is
|
||||
* responsible for replacing the placeholder with an actual URLSpan.
|
||||
*/
|
||||
class PlaceholderURLSpan(url: String) : android.text.Annotation("placeholderUrl", url)
|
||||
@@ -47,6 +47,7 @@ import com.google.i18n.phonenumbers.Phonenumber;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
import org.thoughtcrime.securesms.components.ComposeText;
|
||||
import org.thoughtcrime.securesms.keyvalue.SignalStore;
|
||||
import org.thoughtcrime.securesms.mms.OutgoingLegacyMmsConnection;
|
||||
@@ -466,10 +467,13 @@ public class Util {
|
||||
}
|
||||
|
||||
public static void writeTextToClipboard(@NonNull Context context, @NonNull String text) {
|
||||
{
|
||||
ClipboardManager clipboardManager = (ClipboardManager)context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
clipboardManager.setPrimaryClip(ClipData.newPlainText("Safety numbers", text));
|
||||
}
|
||||
writeTextToClipboard(context, context.getString(R.string.app_name), text);
|
||||
}
|
||||
|
||||
public static void writeTextToClipboard(@NonNull Context context, @NonNull String label, @NonNull String text) {
|
||||
android.content.ClipboardManager clipboard = (android.content.ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
|
||||
ClipData clip = ClipData.newPlainText(label, text);
|
||||
clipboard.setPrimaryClip(clip);
|
||||
}
|
||||
|
||||
public static int toIntExact(long value) {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package org.thoughtcrime.securesms.util.views
|
||||
|
||||
import android.view.View
|
||||
import android.view.ViewTreeObserver
|
||||
import androidx.core.util.Consumer
|
||||
|
||||
/**
|
||||
* Given a view and a corner radius set callback, calculate the appropriate radius to
|
||||
* make the view have fully rounded sides (height/2).
|
||||
*/
|
||||
class AutoRounder<T : View> private constructor(private val view: T, private val setRadius: Consumer<Float>) : ViewTreeObserver.OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
if (view.height > 0) {
|
||||
setRadius.accept(view.height.toFloat() / 2f)
|
||||
view.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun <VIEW : View> autoSetCorners(view: VIEW, setRadius: Consumer<Float>) {
|
||||
view.viewTreeObserver.addOnGlobalLayoutListener(AutoRounder(view, setRadius))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,7 +351,7 @@ public class VerifyDisplayFragment extends Fragment implements ViewTreeObserver.
|
||||
}
|
||||
|
||||
private void handleCopyToClipboard(Fingerprint fingerprint, int segmentCount) {
|
||||
Util.writeTextToClipboard(getActivity(), getFormattedSafetyNumbers(fingerprint, segmentCount));
|
||||
Util.writeTextToClipboard(requireContext(), "Safety numbers", getFormattedSafetyNumbers(fingerprint, segmentCount));
|
||||
}
|
||||
|
||||
private void handleCompareWithClipboard(Fingerprint fingerprint) {
|
||||
|
||||
Reference in New Issue
Block a user