Add UI components for Release Channel.

This commit is contained in:
Cody Henthorne 2022-01-31 12:46:44 -05:00
parent 45a91e0896
commit 1b1001b0e9
61 changed files with 1011 additions and 323 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -111,6 +111,7 @@ public class AttachmentUtil {
return recipient.isSystemContact() ||
recipient.isProfileSharing() ||
message.isOutgoing() ||
recipient.isSelf();
recipient.isSelf() ||
recipient.isReleaseNotes();
}
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -76,11 +76,24 @@ message ProfileChangeDetails {
message BodyRangeList {
message BodyRange {
enum Style {
BOLD = 0;
ITALIC = 1;
}
message Button {
string label = 1;
string action = 2;
}
int32 start = 1;
int32 length = 2;
oneof associatedValue {
string mentionUuid = 3;
Style style = 4;
string link = 5;
Button button = 6;
}
}

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M8.6618,2.4548C9.3821,1.6717 10.6179,1.6717 11.3382,2.4548L11.4676,2.5955C11.8981,3.0635 12.5416,3.2726 13.1649,3.147L13.3524,3.1092C14.3954,2.8991 15.3951,3.6255 15.5176,4.6823L15.5396,4.8723C15.6128,5.5039 16.0105,6.0513 16.5885,6.3161L16.7624,6.3957C17.7297,6.8388 18.1116,8.0141 17.5895,8.9411L17.4956,9.1077C17.1836,9.6616 17.1836,10.3383 17.4956,10.8923L17.5895,11.0589C18.1116,11.9859 17.7297,13.1612 16.7624,13.6043L16.5885,13.6839C16.0105,13.9487 15.6128,14.4961 15.5396,15.1277L15.5176,15.3177C15.3951,16.3745 14.3954,17.1009 13.3524,16.8908L13.1649,16.853C12.5416,16.7274 11.8981,16.9365 11.4676,17.4045L11.3382,17.5452C10.6179,18.3283 9.3821,18.3283 8.6618,17.5452L8.5324,17.4045C8.1019,16.9365 7.4584,16.7274 6.8351,16.853L6.6476,16.8908C5.6046,17.1009 4.6049,16.3745 4.4824,15.3177L4.4604,15.1277C4.3872,14.4961 3.9895,13.9487 3.4115,13.6839L3.2376,13.6043C2.2703,13.1612 1.8884,11.9859 2.4105,11.0589L2.5044,10.8923C2.8164,10.3383 2.8164,9.6616 2.5044,9.1077L2.4105,8.9411C1.8884,8.0141 2.2703,6.8388 3.2376,6.3957L3.4115,6.3161C3.9895,6.0513 4.3872,5.5039 4.4604,4.8723L4.4824,4.6823C4.6049,3.6255 5.6046,2.8991 6.6476,3.1092L6.8351,3.147C7.4584,3.2726 8.1019,3.0635 8.5324,2.5955L8.6618,2.4548Z"
android:fillColor="#6191F3"/>
<path
android:pathData="M14.1264,8.153L13.1725,7.1991L9.0773,11.2947L7.1302,9.3476L6.1764,10.3014L9.0774,13.2019L14.1264,8.153Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10.6618,3.4548C11.3821,2.6717 12.6179,2.6717 13.3382,3.4548L13.7077,3.8566C14.1382,4.3245 14.7817,4.5336 15.405,4.408L15.9402,4.3002C16.9831,4.0901 17.9829,4.8165 18.1054,5.8733L18.1682,6.4156C18.2414,7.0472 18.6391,7.5946 19.2171,7.8594L19.7135,8.0867C20.6808,8.5298 21.0627,9.705 20.5405,10.632L20.2726,11.1077C19.9606,11.6617 19.9606,12.3383 20.2726,12.8923L20.5405,13.368C21.0627,14.295 20.6808,15.4702 19.7135,15.9133L19.2171,16.1406C18.6391,16.4054 18.2414,16.9528 18.1682,17.5844L18.1054,18.1267C17.9829,19.1836 16.9831,19.9099 15.9402,19.6998L15.405,19.592C14.7817,19.4664 14.1382,19.6755 13.7077,20.1434L13.3382,20.5452C12.6179,21.3283 11.3821,21.3283 10.6618,20.5452L10.2923,20.1434C9.8618,19.6755 9.2183,19.4664 8.595,19.592L8.0598,19.6998C7.0168,19.9099 6.0171,19.1836 5.8946,18.1267L5.8318,17.5844C5.7586,16.9528 5.3609,16.4054 4.7828,16.1406L4.2865,15.9133C3.3192,15.4702 2.9374,14.295 3.4595,13.368L3.7274,12.8923C4.0394,12.3383 4.0394,11.6617 3.7274,11.1077L3.4595,10.632C2.9374,9.7051 3.3192,8.5298 4.2865,8.0867L4.7828,7.8594C5.3609,7.5946 5.7586,7.0472 5.8318,6.4156L5.8946,5.8733C6.0171,4.8165 7.0168,4.0901 8.0598,4.3002L8.595,4.408C9.2183,4.5336 9.8618,4.3245 10.2923,3.8566L10.6618,3.4548Z"
android:fillColor="#6191F3"/>
<path
android:pathData="M16.5236,9.9477L15.525,8.9491L10.9748,13.4998L8.8113,11.3363L7.8127,12.3349L10.9749,15.4965L16.5236,9.9477Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M13.0278,3.6002C13.8201,2.7388 15.1794,2.7388 15.9717,3.6002L16.7384,4.4337C17.2119,4.9484 17.9198,5.1784 18.6054,5.0403L19.7156,4.8166C20.8629,4.5855 21.9626,5.3845 22.0973,6.547L22.2277,7.672C22.3082,8.3667 22.7457,8.9689 23.3815,9.2602L24.4112,9.7318C25.4752,10.2191 25.8953,11.5119 25.3209,12.5316L24.7651,13.5184C24.4219,14.1277 24.4219,14.8721 24.7651,15.4814L25.3209,16.4682C25.8953,17.4879 25.4752,18.7807 24.4112,19.2681L23.3815,19.7397C22.7457,20.0309 22.3082,20.6331 22.2277,21.3278L22.0973,22.4528C21.9626,23.6153 20.8629,24.4143 19.7156,24.1832L18.6054,23.9595C17.9198,23.8214 17.2119,24.0514 16.7384,24.5662L15.9717,25.3997C15.1794,26.261 13.8201,26.261 13.0278,25.3997L12.2611,24.5662C11.7876,24.0514 11.0797,23.8214 10.3941,23.9595L9.2839,24.1832C8.1366,24.4143 7.0369,23.6153 6.9022,22.4528L6.7718,21.3278C6.6913,20.6331 6.2538,20.0309 5.618,19.7397L4.5883,19.2681C3.5243,18.7807 3.1043,17.4879 3.6786,16.4682L4.2344,15.4814C4.5776,14.8721 4.5776,14.1277 4.2344,13.5184L3.6786,12.5316C3.1043,11.5119 3.5243,10.2191 4.5883,9.7318L5.618,9.2602C6.2538,8.9689 6.6913,8.3667 6.7718,7.672L6.9022,6.547C7.0369,5.3845 8.1366,4.5855 9.2839,4.8166L10.3941,5.0403C11.0797,5.1784 11.7876,4.9484 12.2611,4.4337L13.0278,3.6002Z"
android:fillColor="#6191F3"/>
<path
android:pathData="M20.1545,11.9346L18.9063,10.6863L13.2185,16.3746L10.5142,13.6703L9.2659,14.9185L13.2186,18.8705L20.1545,11.9346Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,22 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="154dp"
android:height="28dp"
android:viewportWidth="154"
android:viewportHeight="28">
<path
android:pathData="M0,0h154v28h-154z">
<aapt:attr name="android:fillColor">
<gradient
android:startY="-11.219"
android:startX="76.8858"
android:endY="38.8231"
android:endX="67.4908"
android:type="linear">
<item android:offset="0" android:color="#FF3370FF"/>
<item android:offset="0.537474" android:color="#FF3D68DE"/>
<item android:offset="1" android:color="#FF9845E8"/>
</gradient>
</aapt:attr>
</path>
</vector>

View file

@ -0,0 +1,12 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="16dp"
android:viewportWidth="16"
android:viewportHeight="16">
<path
android:pathData="M12.2,9.8467C12.1848,9.8096 12.1714,9.7718 12.16,9.7333C13.2184,8.7256 13.9806,7.4472 14.3637,6.0368C14.7469,4.6265 14.7364,3.1381 14.3333,1.7334C13.6215,1.5096 12.8795,1.3972 12.1333,1.4C11.0337,1.4043 9.9463,1.6303 8.936,2.0644C7.9257,2.4986 7.0133,3.1319 6.2533,3.9267H6.1867C5.96,3.8533 5.68,3.76 5.4333,3.7C5.2119,3.6496 4.9821,3.6479 4.7599,3.6951C4.5378,3.7423 4.3285,3.8372 4.1467,3.9734C3.5533,4.4134 2.0733,5.64 0.9333,6.5867C0.7541,6.7368 0.6227,6.9361 0.5552,7.1599C0.4877,7.3838 0.487,7.6225 0.5533,7.8467C0.6241,8.0942 0.765,8.3159 0.959,8.485C1.153,8.6542 1.3918,8.7636 1.6467,8.8L2.5333,8.9333C3.02,9.0067 3.4333,9.0667 3.74,9.1C3.906,9.8819 4.3115,10.5925 4.9,11.1333C5.4461,11.7255 6.1643,12.1312 6.9533,12.2933H7.0267C7.06,12.6 7.12,13.0067 7.1933,13.4867C7.24,13.7733 7.2867,14.0733 7.3267,14.3733C7.3631,14.6282 7.4725,14.867 7.6416,15.061C7.8108,15.255 8.0325,15.3959 8.28,15.4667C8.3928,15.4985 8.5095,15.5143 8.6267,15.5133C8.8007,15.5127 8.9726,15.4742 9.1303,15.4006C9.288,15.3269 9.4278,15.2198 9.54,15.0867C10.4867,13.9467 11.7133,12.4667 12.16,11.8733C12.2938,11.6904 12.3868,11.4809 12.4328,11.2589C12.4788,11.037 12.4767,10.8078 12.4267,10.5867C12.3667,10.3533 12.2733,10.0733 12.2,9.8467ZM1.7867,7.78C1.723,7.773 1.6629,7.7469 1.6142,7.7053C1.5654,7.6637 1.5303,7.6085 1.5133,7.5467C1.5003,7.5093 1.499,7.4687 1.5097,7.4306C1.5205,7.3924 1.5427,7.3585 1.5733,7.3334C2.7067,6.3934 4.1733,5.18 4.7467,4.7467C4.8141,4.7005 4.8907,4.6694 4.9713,4.6556C5.0518,4.6418 5.1344,4.6456 5.2133,4.6667C5.3333,4.6916 5.4514,4.725 5.5667,4.7667C4.3533,6.4534 3.88,7.4334 3.7333,8.1L2.6667,7.92L1.7867,7.78ZM5.6133,10.4467C5.157,10.0406 4.8437,9.4982 4.72,8.9C4.6333,8.46 4.52,7.92 6.4733,5.22C7.1389,4.3467 7.9943,3.636 8.9748,3.1417C9.9553,2.6474 11.0354,2.3824 12.1333,2.3667C12.5993,2.3682 13.064,2.4174 13.52,2.5134C13.7812,3.8178 13.6696,5.1695 13.198,6.4134C12.7263,7.6574 11.9137,8.7433 10.8533,9.5467C9.1467,10.78 8.08,11.3333 7.4867,11.3333C7.3745,11.3305 7.2629,11.3172 7.1533,11.2933C6.5606,11.1765 6.0209,10.8727 5.6133,10.4267V10.4467ZM11.3333,11.2867C10.9,11.86 9.6867,13.3267 8.7467,14.46C8.7211,14.492 8.686,14.5151 8.6465,14.5259C8.6069,14.5367 8.565,14.5346 8.5267,14.52C8.4653,14.5023 8.4105,14.4669 8.369,14.4183C8.3276,14.3697 8.3013,14.3101 8.2933,14.2467C8.2467,13.94 8.2,13.6333 8.16,13.3467C8.0933,12.94 8.0333,12.56 8.0067,12.28C8.6733,12.12 9.64,11.6133 11.34,10.4267C11.38,10.56 11.42,10.6933 11.4467,10.82C11.4631,10.9014 11.4615,10.9853 11.4419,11.0659C11.4223,11.1466 11.3852,11.2219 11.3333,11.2867Z"
android:fillColor="#ffffff"/>
<path
android:pathData="M8.7534,8.8733C8.9571,8.9315 9.1681,8.9607 9.38,8.96C9.9732,8.9611 10.5446,8.7368 10.9788,8.3327C11.4129,7.9285 11.6774,7.3745 11.7186,6.7828C11.7599,6.1911 11.5749,5.6057 11.2011,5.1452C10.8272,4.6847 10.2925,4.3833 9.7049,4.302C9.1173,4.2208 8.5208,4.3657 8.036,4.7075C7.5512,5.0493 7.2143,5.5624 7.0934,6.1431C6.9725,6.7238 7.0767,7.3288 7.3849,7.8356C7.693,8.3425 8.1822,8.7134 8.7534,8.8733ZM8.0867,6.2666C8.1329,6.0961 8.2125,5.9365 8.3209,5.797C8.4293,5.6576 8.5644,5.541 8.7182,5.4542C8.872,5.3674 9.0415,5.312 9.217,5.2912C9.3924,5.2705 9.5702,5.2848 9.74,5.3333C9.9099,5.3802 10.0688,5.4603 10.2076,5.569C10.3463,5.6778 10.4621,5.8129 10.5483,5.9667C10.6345,6.1204 10.6893,6.2897 10.7096,6.4648C10.7299,6.6399 10.7154,6.8172 10.6667,6.9866C10.588,7.2672 10.4196,7.5142 10.1873,7.69C9.9549,7.8658 9.6714,7.9606 9.38,7.96C9.2587,7.9576 9.138,7.9419 9.02,7.9133C8.6799,7.8166 8.3922,7.5888 8.22,7.28C8.1334,7.1277 8.0776,6.9598 8.0558,6.7859C8.0341,6.612 8.0469,6.4356 8.0934,6.2666H8.0867Z"
android:fillColor="#ffffff"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="20dp"
android:height="20dp"
android:viewportWidth="20"
android:viewportHeight="20">
<path
android:pathData="M8.6618,2.4548C9.3821,1.6717 10.6179,1.6717 11.3382,2.4548L11.4676,2.5955C11.8981,3.0635 12.5416,3.2726 13.1649,3.147L13.3524,3.1092C14.3954,2.8991 15.3951,3.6255 15.5176,4.6823L15.5396,4.8723C15.6128,5.5039 16.0105,6.0513 16.5885,6.3161L16.7624,6.3957C17.7297,6.8388 18.1116,8.0141 17.5895,8.9411L17.4956,9.1077C17.1836,9.6616 17.1836,10.3383 17.4956,10.8923L17.5895,11.0589C18.1116,11.9859 17.7297,13.1612 16.7624,13.6043L16.5885,13.6839C16.0105,13.9487 15.6128,14.4961 15.5396,15.1277L15.5176,15.3177C15.3951,16.3745 14.3954,17.1009 13.3524,16.8908L13.1649,16.853C12.5416,16.7274 11.8981,16.9365 11.4676,17.4045L11.3382,17.5452C10.6179,18.3283 9.3821,18.3283 8.6618,17.5452L8.5324,17.4045C8.1019,16.9365 7.4584,16.7274 6.8351,16.853L6.6476,16.8908C5.6046,17.1009 4.6049,16.3745 4.4824,15.3177L4.4604,15.1277C4.3872,14.4961 3.9895,13.9487 3.4115,13.6839L3.2376,13.6043C2.2703,13.1612 1.8884,11.9859 2.4105,11.0589L2.5044,10.8923C2.8164,10.3383 2.8164,9.6616 2.5044,9.1077L2.4105,8.9411C1.8884,8.0141 2.2703,6.8388 3.2376,6.3957L3.4115,6.3161C3.9895,6.0513 4.3872,5.5039 4.4604,4.8723L4.4824,4.6823C4.6049,3.6255 5.6046,2.8991 6.6476,3.1092L6.8351,3.147C7.4584,3.2726 8.1019,3.0635 8.5324,2.5955L8.6618,2.4548Z"
android:fillColor="#2C6BED"/>
<path
android:pathData="M14.1264,8.153L13.1725,7.1991L9.0773,11.2947L7.1302,9.3476L6.1764,10.3014L9.0774,13.2019L14.1264,8.153Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M10.6618,3.4548C11.3821,2.6717 12.6179,2.6717 13.3382,3.4548L13.7077,3.8566C14.1382,4.3245 14.7817,4.5336 15.405,4.408L15.9402,4.3002C16.9831,4.0901 17.9829,4.8165 18.1054,5.8733L18.1682,6.4156C18.2414,7.0472 18.6391,7.5946 19.2171,7.8594L19.7135,8.0867C20.6808,8.5298 21.0627,9.705 20.5405,10.632L20.2726,11.1077C19.9606,11.6617 19.9606,12.3383 20.2726,12.8923L20.5405,13.368C21.0627,14.295 20.6808,15.4702 19.7135,15.9133L19.2171,16.1406C18.6391,16.4054 18.2414,16.9528 18.1682,17.5844L18.1054,18.1267C17.9829,19.1836 16.9831,19.9099 15.9402,19.6998L15.405,19.592C14.7817,19.4664 14.1382,19.6755 13.7077,20.1434L13.3382,20.5452C12.6179,21.3283 11.3821,21.3283 10.6618,20.5452L10.2923,20.1434C9.8618,19.6755 9.2183,19.4664 8.595,19.592L8.0598,19.6998C7.0168,19.9099 6.0171,19.1836 5.8946,18.1267L5.8318,17.5844C5.7586,16.9528 5.3609,16.4054 4.7828,16.1406L4.2865,15.9133C3.3192,15.4702 2.9374,14.295 3.4595,13.368L3.7274,12.8923C4.0394,12.3383 4.0394,11.6617 3.7274,11.1077L3.4595,10.632C2.9374,9.7051 3.3192,8.5298 4.2865,8.0867L4.7828,7.8594C5.3609,7.5946 5.7586,7.0472 5.8318,6.4156L5.8946,5.8733C6.0171,4.8165 7.0168,4.0901 8.0598,4.3002L8.595,4.408C9.2183,4.5336 9.8618,4.3245 10.2923,3.8566L10.6618,3.4548Z"
android:fillColor="#2C6BED"/>
<path
android:pathData="M16.5236,9.9477L15.525,8.9491L10.9748,13.4998L8.8113,11.3363L7.8127,12.3349L10.9749,15.4965L16.5236,9.9477Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="28dp"
android:height="28dp"
android:viewportWidth="28"
android:viewportHeight="28">
<path
android:pathData="M13.0278,3.6002C13.8201,2.7388 15.1794,2.7388 15.9717,3.6002L16.7384,4.4337C17.2119,4.9484 17.9198,5.1784 18.6054,5.0403L19.7156,4.8166C20.8629,4.5855 21.9626,5.3845 22.0973,6.547L22.2277,7.672C22.3082,8.3667 22.7457,8.9689 23.3815,9.2602L24.4112,9.7318C25.4752,10.2191 25.8953,11.5119 25.3209,12.5316L24.7651,13.5184C24.4219,14.1277 24.4219,14.8721 24.7651,15.4814L25.3209,16.4682C25.8953,17.4879 25.4752,18.7807 24.4112,19.2681L23.3815,19.7397C22.7457,20.0309 22.3082,20.6331 22.2277,21.3278L22.0973,22.4528C21.9626,23.6153 20.8629,24.4143 19.7156,24.1832L18.6054,23.9595C17.9198,23.8214 17.2119,24.0514 16.7384,24.5662L15.9717,25.3997C15.1794,26.261 13.8201,26.261 13.0278,25.3997L12.2611,24.5662C11.7876,24.0514 11.0797,23.8214 10.3941,23.9595L9.2839,24.1832C8.1366,24.4143 7.0369,23.6153 6.9022,22.4528L6.7718,21.3278C6.6913,20.6331 6.2538,20.0309 5.618,19.7397L4.5883,19.2681C3.5243,18.7807 3.1043,17.4879 3.6786,16.4682L4.2344,15.4814C4.5776,14.8721 4.5776,14.1277 4.2344,13.5184L3.6786,12.5316C3.1043,11.5119 3.5243,10.2191 4.5883,9.7318L5.618,9.2602C6.2538,8.9689 6.6913,8.3667 6.7718,7.672L6.9022,6.547C7.0369,5.3845 8.1366,4.5855 9.2839,4.8166L10.3941,5.0403C11.0797,5.1784 11.7876,4.9484 12.2611,4.4337L13.0278,3.6002Z"
android:fillColor="#2C6BED"/>
<path
android:pathData="M20.1545,11.9346L18.9063,10.6863L13.2185,16.3746L10.5142,13.6703L9.2659,14.9185L13.2186,18.8705L20.1545,11.9346Z"
android:fillColor="#ffffff"
android:fillType="evenOdd"/>
</vector>

View file

@ -102,6 +102,12 @@
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<ViewStub
android:id="@+id/conversation_release_notes_unmute_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/conversation_activity_unmute" />
</FrameLayout>
<Button

View file

@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.button.MaterialButton
android:id="@+id/conversation_activity_unmute_button"
style="@style/Signal.Widget.Button.Large.Secondary.NoOutline"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="60dp"
android:text="@string/conversation_muted__unmute" />
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_gravity="top"
android:background="@color/signal_divider_minor" />
</FrameLayout>

View file

@ -62,6 +62,7 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
android:drawablePadding="4dp"
android:gravity="center"
android:paddingStart="16dp"
android:paddingEnd="16dp"

View file

@ -0,0 +1,7 @@
<com.google.android.material.button.MaterialButton xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
style="@style/Signal.Widget.Button.Medium.Primary"
android:id="@+id/conversation_item_call_to_action"
android:layout_width="match_parent"
android:layout_height="wrap_content"
tools:text="Click me" />

View file

@ -187,19 +187,26 @@
android:textColor="@color/signal_text_primary"
android:textColorLink="@color/signal_text_primary"
app:emoji_maxLength="1000"
app:scaleEmojis="true"
app:measureLastLine="true"
app:scaleEmojis="true"
tools:text="Mango pickle lorem ipsum" />
<ViewStub
android:id="@+id/conversation_item_call_to_action_stub"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout="@layout/conversation_item_call_to_action"
android:layout_margin="8dp" />
<org.thoughtcrime.securesms.components.ConversationItemFooter
android:id="@+id/conversation_item_footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginTop="-5dp"
android:layout_marginEnd="@dimen/message_bubble_horizontal_padding"
android:layout_marginBottom="@dimen/message_bubble_bottom_padding"
android:layout_gravity="end"
android:alpha="0.7"
android:clipChildren="false"
android:clipToPadding="false"
@ -212,9 +219,9 @@
android:id="@+id/conversation_item_sticker_footer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="end"
android:layout_marginStart="@dimen/message_bubble_horizontal_padding"
android:layout_marginTop="2dp"
android:layout_gravity="end"
android:clipChildren="false"
android:clipToPadding="false"
android:paddingStart="@dimen/message_bubble_horizontal_padding"

View file

@ -43,6 +43,13 @@
tools:text="Learn more"
tools:visibility="visible" />
<ViewStub
android:id="@+id/conversation_update_donate_action"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout="@layout/conversation_item_update_donate" />
</LinearLayout>
</org.thoughtcrime.securesms.conversation.ConversationUpdateItem>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="0dp"
android:foreground="?selectableItemBackground"
android:padding="0dp"
android:visibility="visible"
app:cardCornerRadius="18dp"
tools:visibility="visible">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.appcompat.widget.AppCompatImageView
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_gravity="center"
android:padding="0dp"
android:scaleType="fitXY"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/conversation_update_donate_action_button"
app:layout_constraintStart_toStartOf="@+id/conversation_update_donate_action_button"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/donate_update_item_background" />
<TextView
android:id="@+id/conversation_update_donate_action_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:drawablePadding="4dp"
android:fontFamily="sans-serif-medium"
android:paddingHorizontal="12dp"
android:textColor="@color/white"
android:textSize="13sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:drawableStart="@drawable/ic_boost_outline_16"
tools:text="Signal Boost" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View file

@ -7,13 +7,15 @@
<org.thoughtcrime.securesms.components.emoji.EmojiTextView
android:id="@+id/bio_preference_headline"
android:layout_width="0dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:layout_marginTop="12dp"
android:layout_marginEnd="32dp"
android:drawablePadding="4dp"
android:gravity="center"
android:textAppearance="@style/Signal.Text.Headline.Medium"
app:layout_constrainedWidth="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"

View file

@ -56,7 +56,6 @@
android:layout_height="wrap_content"
android:layout_gravity="center_vertical|start"
android:drawablePadding="3dp"
android:drawableTint="@color/signal_inverse_transparent_80"
android:ellipsize="end"
android:gravity="center_vertical"
android:maxLines="1"

View file

@ -114,6 +114,10 @@
<string name="BlockUnblockDialog_group_members_will_be_able_to_add_you">Group members will be able to add you to this group again.</string>
<string name="BlockUnblockDialog_you_will_be_able_to_call_and_message_each_other">You will be able to message and call each other and your name and photo will be shared with them.</string>
<string name="BlockUnblockDialog_blocked_people_wont_be_able_to_call_you_or_send_you_messages">Blocked people won\'t be able to call you or send you messages.</string>
<!-- Message shown on block dialog when blocking the Signal release notes recipient -->
<string name="BlockUnblockDialog_block_getting_signal_updates_and_news">Block getting Signal updates and news.</string>
<!-- Message shown on unblock dialog when unblocking the Signal release notes recipient -->
<string name="BlockUnblockDialog_resume_getting_signal_updates_and_news">Resume getting Signal updates and news.</string>
<string name="BlockUnblockDialog_unblock_s">Unblock %1$s?</string>
<string name="BlockUnblockDialog_block">Block</string>
<string name="BlockUnblockDialog_block_and_leave">Block and Leave</string>
@ -1253,6 +1257,10 @@
<string name="MessageRecord_you_marked_your_safety_number_with_s_unverified_from_another_device">You marked your safety number with %s unverified from another device</string>
<string name="MessageRecord_a_message_from_s_couldnt_be_delivered">A message from %s couldn\'t be delivered</string>
<string name="MessageRecord_s_changed_their_phone_number">%1$s changed their phone number.</string>
<!-- Update item message shown in the release channel when someone is already a sustainer so we ask them if they want to boost. -->
<string name="MessageRecord_like_this_new_feature_say_thanks_with_a_boost">Like this new feature? Say thanks with a Boost.</string>
<!-- Update item message shown in the release channel when someone is not a sustainer so we ask them to consider becoming one -->
<string name="MessageRecord_signal_is_powered_by_people_like_you_become_a_sustainer_today">Signal is powered by people like you. Become a sustainer today.</string>
<!-- Group Calling update messages -->
<string name="MessageRecord_s_started_a_group_call_s">%1$s started a group call · %2$s</string>
@ -1287,6 +1295,7 @@
<string name="MessageRequestBottomView_unblock">Unblock</string>
<string name="MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept">Let %1$s message you and share your name and photo with them? They won\'t know you\'ve seen their message until you accept.</string>
<string name="MessageRequestBottomView_do_you_want_to_let_s_message_you_wont_receive_any_messages_until_you_unblock_them">Let %1$s message you and share your name and photo with them? You won\'t receive any messages until you unblock them.</string>
<string name="MessageRequestBottomView_get_updates_and_news_from_s_you_wont_receive_any_updates_until_you_unblock_them">Get updates and news from %1$s? You won\'t receive any updates until you unblock them.</string>
<string name="MessageRequestBottomView_continue_your_conversation_with_this_group_and_share_your_name_and_photo">Continue your conversation with this group and share your name and photo with its members?</string>
<string name="MessageRequestBottomView_upgrade_this_group_to_activate_new_features">Upgrade this group to activate new features like @mentions and admins. Members who have not shared their name or photo in this group will be invited to join.</string>
<string name="MessageRequestBottomView_this_legacy_group_can_no_longer_be_used">This Legacy Group can no longer be used because it is too large. The maximum group size is %1$d.</string>
@ -2007,6 +2016,10 @@
<string name="ConversationUpdateItem_no_contacts_in_this_group_review_requests_carefully">No contacts in this group. Review requests carefully.</string>
<string name="ConversationUpdateItem_view">View</string>
<string name="ConversationUpdateItem_the_disappearing_message_time_will_be_set_to_s_when_you_message_them">The disappearing message time will be set to %1$s when you message them.</string>
<!-- Update item button text to show to boost a feature -->
<string name="ConversationUpdateItem_signal_boost">Signal Boost</string>
<!-- Update item button text to show to become a sustainer in the release notes channel -->
<string name="ConversationUpdateItem_become_a_sustainer">Become a Sustainer</string>
<!-- audio_view -->
<string name="audio_view__play_pause_accessibility_description">Play … Pause</string>
@ -4228,6 +4241,9 @@
<!-- Displayed in a toast when we fail to open the ringtone picker -->
<string name="NotificationSettingsFragment__failed_to_open_picker">Failed to open picker.</string>
<!-- Description shown for the Signal Release Notes channel -->
<string name="ReleaseNotes__signal_release_notes_and_news">Signal Release Notes &amp; News</string>
<!-- EOF -->
</resources>

View file

@ -78,6 +78,9 @@ class SmsDatabaseTest {
TestSms.insert(db, type = MmsSmsColumns.Types.CHANGE_NUMBER_TYPE)
assertFalse(smsDatabase.hasMeaningfulMessage(1))
TestSms.insert(db, type = MmsSmsColumns.Types.BOOST_REQUEST_TYPE)
assertFalse(smsDatabase.hasMeaningfulMessage(1))
TestSms.insert(db, type = MmsSmsColumns.Types.BASE_INBOX_TYPE or MmsSmsColumns.Types.GROUP_V2_LEAVE_BITS)
assertFalse(smsDatabase.hasMeaningfulMessage(1))
}