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