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

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