diff --git a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt index ec55e36f8f..7c211a6bc7 100644 --- a/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt +++ b/app/src/androidTest/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemShapeTest.kt @@ -339,7 +339,11 @@ class V2ConversationItemShapeTest { override fun onMessageRequestAcceptOptionsClicked() = Unit override fun onItemDoubleClick(item: MultiselectPart) = Unit + override fun onPaymentTombstoneClicked() = Unit + override fun onDisplayMediaNoLongerAvailableSheet() = Unit + + override fun onShowUnverifiedProfileSheet(forGroup: Boolean) = Unit } } diff --git a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt index 25307917c8..86158fef32 100644 --- a/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt +++ b/app/src/debug/java/org/thoughtcrime/securesms/components/settings/app/internal/conversation/test/InternalConversationTestFragment.kt @@ -327,5 +327,9 @@ class InternalConversationTestFragment : Fragment(R.layout.conversation_test_fra override fun onMessageRequestAcceptOptionsClicked() { Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() } + + override fun onShowUnverifiedProfileSheet(forGroup: Boolean) { + Toast.makeText(requireContext(), "Can't touch this.", Toast.LENGTH_SHORT).show() + } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java index 35010a86dc..e49dcc0ed9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/BindableConversationItem.java @@ -135,5 +135,6 @@ public interface BindableConversationItem extends Unbindable, GiphyMp4Playable, void onItemDoubleClick(MultiselectPart multiselectPart); void onPaymentTombstoneClicked(); void onDisplayMediaNoLongerAvailableSheet(); + void onShowUnverifiedProfileSheet(boolean forGroup); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index 0642db83bb..75a18929e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -33,6 +33,7 @@ import org.signal.core.util.Result import org.signal.core.util.concurrent.LifecycleDisposable import org.signal.core.util.concurrent.addTo import org.signal.core.util.getParcelableArrayListExtraCompat +import org.signal.donations.InAppPaymentType import org.thoughtcrime.securesms.AvatarPreviewActivity import org.thoughtcrime.securesms.BlockUnblockDialog import org.thoughtcrime.securesms.InviteActivity @@ -52,6 +53,8 @@ import org.thoughtcrime.securesms.components.settings.DSLSettingsFragment import org.thoughtcrime.securesms.components.settings.DSLSettingsIcon import org.thoughtcrime.securesms.components.settings.DSLSettingsText import org.thoughtcrime.securesms.components.settings.NO_TINT +import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity +import org.thoughtcrime.securesms.components.settings.app.subscription.donate.CheckoutFlowActivity import org.thoughtcrime.securesms.components.settings.configure import org.thoughtcrime.securesms.components.settings.conversation.preferences.AvatarPreference import org.thoughtcrime.securesms.components.settings.conversation.preferences.BioTextPreference @@ -492,6 +495,18 @@ class ConversationSettingsFragment : DSLSettingsFragment( dividerPref() } + if (state.recipient.isReleaseNotes) { + textPref( + icon = DSLSettingsIcon.from(R.drawable.symbol_official_20), + title = DSLSettingsText.from(R.string.ReleaseNotes__this_is_official_chat) + ) + textPref( + icon = DSLSettingsIcon.from(R.drawable.symbol_bell_20), + title = DSLSettingsText.from(R.string.ReleaseNotes__keep_up_to_date) + ) + dividerPref() + } + val summary = DSLSettingsText.from(formatDisappearingMessagesLifespan(state.disappearingMessagesLifespan)) val icon = if (state.disappearingMessagesLifespan <= 0 || state.recipient.isBlocked) { R.drawable.symbol_timer_slash_24 @@ -633,6 +648,27 @@ class ConversationSettingsFragment : DSLSettingsFragment( ) } + if (state.recipient.isReleaseNotes) { + dividerPref() + sectionHeaderPref(R.string.preferences__help) + + externalLinkPref( + icon = DSLSettingsIcon.from(R.drawable.symbol_help_24), + title = DSLSettingsText.from(R.string.HelpSettingsFragment__support_center), + linkId = R.string.support_center_url + ) + clickPref( + icon = DSLSettingsIcon.from(R.drawable.symbol_invite_24), + title = DSLSettingsText.from(R.string.HelpSettingsFragment__contact_us), + onClick = { startActivity(AppSettingsActivity.help(requireContext())) } + ) + clickPref( + icon = DSLSettingsIcon.from(R.drawable.symbol_heart_24), + title = DSLSettingsText.from(R.string.preferences__donate_to_signal), + onClick = { startActivity(CheckoutFlowActivity.createIntent(requireContext(), InAppPaymentType.ONE_TIME_DONATION)) } + ) + } + state.withRecipientSettingsState { recipientSettingsState -> if (state.recipient.badges.isNotEmpty() && !state.recipient.isSelf) { dividerPref() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt index 8a3acd0dbb..94995ce5a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/dsl.kt @@ -213,9 +213,10 @@ class DSLConfiguration { fun textPref( title: DSLSettingsText? = null, - summary: DSLSettingsText? = null + summary: DSLSettingsText? = null, + icon: DSLSettingsIcon? = null ) { - val preference = TextPreference(title, summary) + val preference = TextPreference(title, summary, icon) children.add(preference) } @@ -257,8 +258,9 @@ abstract class PreferenceModel>( class TextPreference( title: DSLSettingsText?, - summary: DSLSettingsText? -) : PreferenceModel(title = title, summary = summary) + summary: DSLSettingsText?, + icon: DSLSettingsIcon? = null +) : PreferenceModel(title = title, summary = summary, icon = icon) class LearnMoreTextPreference( override val title: DSLSettingsText?, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java index 1c158eddb5..96bd93e17d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java @@ -5,12 +5,14 @@ import android.graphics.PorterDuff; import android.graphics.drawable.Drawable; import android.text.SpannableStringBuilder; import android.text.TextUtils; +import android.text.method.LinkMovementMethod; import android.util.AttributeSet; import android.view.View; import androidx.annotation.DrawableRes; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.StringRes; import androidx.constraintlayout.widget.ConstraintLayout; import androidx.core.content.ContextCompat; import androidx.core.view.ViewKt; @@ -92,25 +94,39 @@ public class ConversationHeaderView extends ConstraintLayout { return title.toString(); } - 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(); - } - - binding.messageRequestAbout.setText(about); - binding.messageRequestAbout.setVisibility(TextUtils.isEmpty(about) ? GONE : VISIBLE); + public void showReleaseNoteHeader() { + binding.messageRequestInfo.setVisibility(View.GONE); + binding.releaseHeaderContainer.setVisibility(View.VISIBLE); + binding.releaseHeaderDescription1.setText(prependIcon(getContext().getString(R.string.ReleaseNotes__this_is_official_chat_period), R.drawable.symbol_official_20)); + binding.releaseHeaderDescription2.setText(prependIcon(getContext().getString(R.string.ReleaseNotes__keep_up_to_date_period), R.drawable.symbol_bell_20)); } - public void setSubtitle(@NonNull CharSequence subtitle, @DrawableRes int iconRes) { + public void setAbout(@NonNull Recipient recipient) { + String about = recipient.getCombinedAboutAndEmoji(); + binding.messageRequestAbout.setText(about); + binding.messageRequestAbout.setVisibility(TextUtils.isEmpty(about) || recipient.isReleaseNotes() ? GONE : VISIBLE); + } + + public void setSubtitle(@NonNull CharSequence subtitle, @DrawableRes int iconRes, @Nullable Runnable onClick) { if (TextUtils.isEmpty(subtitle)) { hideSubtitle(); return; } - binding.messageRequestSubtitle.setText(prependIcon(subtitle, iconRes)); + if (onClick != null) { + binding.messageRequestSubtitle.setMovementMethod(LinkMovementMethod.getInstance()); + CharSequence builder = SpanUtil.clickSubstring( + subtitle, + subtitle, + listener -> onClick.run(), + ContextCompat.getColor(getContext(), R.color.signal_colorOnSurface), + true + ); + binding.messageRequestSubtitle.setText(prependIcon(builder, iconRes)); + } else { + binding.messageRequestSubtitle.setText(prependIcon(subtitle, iconRes)); + } + binding.messageRequestSubtitle.setVisibility(View.VISIBLE); } @@ -134,6 +150,32 @@ public class ConversationHeaderView extends ConstraintLayout { binding.messageRequestButton.setVisibility(View.VISIBLE); } + public void showWarningSubtitle() { + binding.messageRequestReviewCarefully.setVisibility(View.VISIBLE); + } + + public void hideWarningSubtitle() { + binding.messageRequestReviewCarefully.setVisibility(View.GONE); + } + + public void setUnverifiedNameSubtitle(@DrawableRes int iconRes, @StringRes int clickableRes, boolean forGroup, @Nullable Runnable onClick) { + binding.messageRequestProfileNameUnverified.setVisibility(View.VISIBLE); + binding.messageRequestProfileNameUnverified.setMovementMethod(LinkMovementMethod.getInstance()); + CharSequence builder = SpanUtil.clickSubstring( + getContext(), + R.string.ConversationFragment_profile_names_not_verified, + clickableRes, + listener -> onClick.run(), + true, + R.color.signal_colorOnSurface + ); + binding.messageRequestProfileNameUnverified.setText(prependIcon(builder, iconRes, forGroup)); + } + + public void hideUnverifiedNameSubtitle() { + binding.messageRequestProfileNameUnverified.setVisibility(View.GONE); + } + public void showBackgroundBubble(boolean enabled) { if (enabled) { setBackgroundResource(R.drawable.wallpaper_bubble_background_18); @@ -176,6 +218,9 @@ public class ConversationHeaderView extends ConstraintLayout { binding.messageRequestInfoOutline.setVisibility(View.VISIBLE); binding.messageRequestDivider.setVisibility(View.INVISIBLE); } + } else if (ViewKt.isVisible(binding.releaseHeaderContainer)) { + binding.messageRequestInfoOutline.setVisibility(View.GONE); + binding.messageRequestDivider.setVisibility(View.INVISIBLE); } else { binding.messageRequestInfoOutline.setVisibility(View.GONE); binding.messageRequestDivider.setVisibility(View.GONE); @@ -183,9 +228,15 @@ public class ConversationHeaderView extends ConstraintLayout { } private @NonNull CharSequence prependIcon(@NonNull CharSequence input, @DrawableRes int iconRes) { + return prependIcon(input, iconRes, false); + } + + + private @NonNull CharSequence prependIcon(@NonNull CharSequence input, @DrawableRes int iconRes, boolean useIntrinsicWidth) { Drawable drawable = ContextCompat.getDrawable(getContext(), iconRes); Preconditions.checkNotNull(drawable); - drawable.setBounds(0, 0, (int) DimensionUnit.SP.toPixels(20), (int) DimensionUnit.SP.toPixels(20)); + int width = useIntrinsicWidth ? drawable.getIntrinsicWidth() : (int) DimensionUnit.SP.toPixels(20); + drawable.setBounds(0, 0, width, (int) DimensionUnit.SP.toPixels(20)); drawable.setColorFilter(ContextCompat.getColor(getContext(), R.color.signal_colorOnSurface), PorterDuff.Mode.SRC_ATOP); return new SpannableStringBuilder() diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java index e04a2a9698..ed9cd0a899 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java @@ -186,9 +186,10 @@ public class ConversationTitleView extends ConstraintLayout { } private void setRecipientTitle(@NonNull Recipient recipient) { - if (recipient.isGroup()) setGroupRecipientTitle(recipient); - else if (recipient.isSelf()) setSelfTitle(); - else setIndividualRecipientTitle(recipient); + if (recipient.isGroup()) setGroupRecipientTitle(recipient); + else if (recipient.isSelf()) setSelfTitle(); + else if (recipient.isReleaseNotes()) setReleaseNotesTitle(recipient); + else setIndividualRecipientTitle(recipient); } private void setGroupRecipientTitle(@NonNull Recipient recipient) { @@ -200,6 +201,13 @@ public class ConversationTitleView extends ConstraintLayout { updateSubtitleVisibility(); } + private void setReleaseNotesTitle(@NonNull Recipient recipient) { + final String displayName = recipient.getDisplayName(getContext()); + this.title.setText(displayName); + this.subtitle.setText(R.string.ReleaseNotes__official_only_chat); + updateSubtitleVisibility(); + } + private void setIndividualRecipientTitle(@NonNull Recipient recipient) { final String displayName = recipient.getDisplayName(getContext()); this.title.setText(displayName); diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt index 7c015deea4..63d31e4a77 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationAdapterV2.kt @@ -5,13 +5,13 @@ package org.thoughtcrime.securesms.conversation.v2 +import android.content.Context import android.text.TextUtils import android.view.GestureDetector import android.view.GestureDetector.SimpleOnGestureListener import android.view.MotionEvent import android.view.View import android.view.ViewGroup -import androidx.core.text.HtmlCompat import androidx.core.view.children import androidx.fragment.app.DialogFragment import androidx.lifecycle.LifecycleOwner @@ -23,6 +23,7 @@ import org.signal.core.util.toOptional import org.thoughtcrime.securesms.BindableConversationItem import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.Unbindable +import org.thoughtcrime.securesms.components.settings.conversation.ConversationSettingsActivity import org.thoughtcrime.securesms.conversation.ConversationAdapter.ItemClickListener import org.thoughtcrime.securesms.conversation.ConversationAdapterBridge import org.thoughtcrime.securesms.conversation.ConversationHeaderView @@ -78,6 +79,7 @@ class ConversationAdapterV2( companion object { private val TAG = Log.tag(ConversationAdapterV2::class.java) + private val MIN_GROUPS_THRESHOLD = 2 } private val _selected = hashSetOf() @@ -539,41 +541,66 @@ class ConversationAdapterV2( val title: String = conversationBanner.setTitle(recipient) { displayDialogFragment(AboutSheet.create(recipient)) } + + if (recipient.isReleaseNotes) { + conversationBanner.showReleaseNoteHeader() + } + conversationBanner.setAbout(recipient) if (recipient.isGroup) { + if (!groupInfo.hasExistingContacts) { + conversationBanner.setUnverifiedNameSubtitle(R.drawable.symbol_group_question_20, R.string.ConversationFragment_group_names, true) { + clickListener.onShowUnverifiedProfileSheet(true) + } + } else { + conversationBanner.hideUnverifiedNameSubtitle() + } + if (groupInfo.pendingMemberCount > 0) { val invited = context.resources.getQuantityString(R.plurals.MessageRequestProfileView_invited, groupInfo.pendingMemberCount, groupInfo.pendingMemberCount) - conversationBanner.setSubtitle(context.resources.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, groupInfo.fullMemberCount, groupInfo.fullMemberCount, invited), R.drawable.symbol_group_light_20) + conversationBanner.setSubtitle(context.resources.getQuantityString(R.plurals.MessageRequestProfileView_members_and_invited, groupInfo.fullMemberCount, groupInfo.fullMemberCount, invited), R.drawable.symbol_group_light_20) { goToGroupSettings(recipient) } } else if (groupInfo.fullMemberCount > 0) { - conversationBanner.setSubtitle(context.resources.getQuantityString(R.plurals.MessageRequestProfileView_members, groupInfo.fullMemberCount, groupInfo.fullMemberCount), R.drawable.symbol_group_light_20) + conversationBanner.setSubtitle(context.resources.getQuantityString(R.plurals.MessageRequestProfileView_members, groupInfo.fullMemberCount, groupInfo.fullMemberCount), R.drawable.symbol_group_light_20) { goToGroupSettings(recipient) } } else { conversationBanner.hideSubtitle() } } else if (isSelf) { - conversationBanner.setSubtitle(context.getString(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation), R.drawable.symbol_note_light_24) + conversationBanner.setSubtitle(context.getString(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation), R.drawable.symbol_note_light_24, null) } else { + if (recipient.nickname.isEmpty && !recipient.isSystemContact) { + conversationBanner.setUnverifiedNameSubtitle(R.drawable.symbol_person_question_16, R.string.ConversationFragment_profile_names, false) { + clickListener.onShowUnverifiedProfileSheet(false) + } + } else { + conversationBanner.hideUnverifiedNameSubtitle() + } + val subtitle: String? = recipient.takeIf { it.shouldShowE164 }?.e164?.map { e164: String? -> PhoneNumberFormatter.prettyPrint(e164!!) }?.orElse(null) if (subtitle == null || subtitle == title) { conversationBanner.hideSubtitle() } else { - conversationBanner.setSubtitle(subtitle, R.drawable.symbol_phone_light_20) + conversationBanner.setSubtitle(subtitle, R.drawable.symbol_phone_light_20, null) } } conversationBanner.hideButton() - if (messageRequestState?.isAccepted == false && sharedGroups.isEmpty() && !isSelf && !recipient.isGroup) { - conversationBanner.setDescription(context.getString(R.string.ConversationUpdateItem_no_groups_in_common_review_requests_carefully), R.drawable.symbol_error_circle_24) + if (messageRequestState?.isAccepted == false && !isSelf && !recipient.isGroup) { + if (sharedGroups.size < MIN_GROUPS_THRESHOLD) { + conversationBanner.showWarningSubtitle() + } conversationBanner.setButton(context.getString(R.string.ConversationFragment_safety_tips)) { clickListener.onShowSafetyTips(false) } - } else if (messageRequestState?.isAccepted == false && recipient.isGroup && !groupInfo.hasExistingContacts) { - conversationBanner.setDescription(context.getString(R.string.ConversationUpdateItem_no_contacts_in_this_group_review_requests_carefully), R.drawable.symbol_error_circle_24) + conversationBanner.setDescription(getDescription(context, sharedGroups), R.drawable.symbol_group_light_20) + } else if (messageRequestState?.isAccepted == false && recipient.isGroup) { + conversationBanner.showWarningSubtitle() conversationBanner.setButton(context.getString(R.string.ConversationFragment_safety_tips)) { clickListener.onShowSafetyTips(true) } - } else if (sharedGroups.isEmpty() || isSelf) { + } else if ((recipient.isGroup && sharedGroups.isEmpty()) || isSelf) { + conversationBanner.hideWarningSubtitle() if (TextUtils.isEmpty(groupInfo.description)) { conversationBanner.setLinkifyDescription(false) conversationBanner.hideDescription() @@ -592,23 +619,37 @@ class ConversationAdapterV2( } } } else { - val description: String = when (sharedGroups.size) { - 1 -> context.getString(R.string.MessageRequestProfileView_member_of_one_group, sharedGroups[0]) - 2 -> context.getString(R.string.MessageRequestProfileView_member_of_two_groups, sharedGroups[0], sharedGroups[1]) - 3 -> context.getString(R.string.MessageRequestProfileView_member_of_many_groups, sharedGroups[0], sharedGroups[1], sharedGroups[2]) - else -> { - val others: Int = sharedGroups.size - 2 - context.getString( - R.string.MessageRequestProfileView_member_of_many_groups, - sharedGroups[0], - sharedGroups[1], - context.resources.getQuantityString(R.plurals.MessageRequestProfileView_member_of_d_additional_groups, others, others) - ) - } - } - conversationBanner.setDescription(HtmlCompat.fromHtml(description, 0), R.drawable.symbol_group_light_20) + conversationBanner.hideWarningSubtitle() + conversationBanner.setDescription(getDescription(context, sharedGroups), R.drawable.symbol_group_light_20) } } + + private fun getDescription(context: Context, sharedGroups: List): String { + return when (sharedGroups.size) { + 0 -> context.getString(R.string.ConversationUpdateItem_no_groups_in_common_review_requests_carefully) + 1 -> context.getString(R.string.MessageRequestProfileView_member_of_one_group, sharedGroups[0]) + 2 -> context.getString(R.string.MessageRequestProfileView_member_of_two_groups, sharedGroups[0], sharedGroups[1]) + 3 -> context.getString(R.string.MessageRequestProfileView_member_of_many_groups, sharedGroups[0], sharedGroups[1], sharedGroups[2]) + else -> { + val others: Int = sharedGroups.size - 2 + context.getString( + R.string.MessageRequestProfileView_member_of_many_groups, + sharedGroups[0], + sharedGroups[1], + context.resources.getQuantityString(R.plurals.MessageRequestProfileView_member_of_d_additional_groups, others, others) + ) + } + } + } + + private fun goToGroupSettings(recipient: Recipient) { + val intent = ConversationSettingsActivity.forGroup(getContext(), recipient.requireGroupId()) + val bundle = ConversationSettingsActivity.createTransitionBundle( + getContext(), + conversationBanner.getViewById(R.id.message_request_avatar) + ) + getContext().startActivity(intent, bundle) + } } private inner class OnScrollStateChangedListener : RecyclerView.OnScrollListener() { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index a850a82309..93e938a41c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -2952,6 +2952,10 @@ class ConversationFragment : ConversationDialogs.displaySafetyNumberLearnMoreDialog(this@ConversationFragment, recipient) } + override fun onShowUnverifiedProfileSheet(forGroup: Boolean) { + UnverifiedProfileNameBottomSheet.show(parentFragmentManager, forGroup) + } + override fun onJoinGroupCallClicked() { val activity = activity ?: return val recipient = viewModel.recipientSnapshot ?: return diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt index f5304be60d..2759bc17b4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/DisabledInputView.kt @@ -15,6 +15,7 @@ import android.widget.FrameLayout import android.widget.TextView import androidx.core.content.ContextCompat import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.messagerequests.MessageRequestState import org.thoughtcrime.securesms.messagerequests.MessageRequestsBottomView @@ -82,7 +83,25 @@ class DisabledInputView @JvmOverloads constructor( setMessageRequestData(recipient, messageRequestState) setWallpaperEnabled(recipient.hasWallpaper) - setAcceptOnClickListener { listener?.onAcceptMessageRequestClicked() } + setAcceptOnClickListener { + if (messageRequestState.isFewConnectionsIndividual) { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.MessageRequestBottomView_accept_request) + .setMessage(R.string.MessageRequestBottomView_review_requests_carefully) + .setPositiveButton(R.string.MessageRequestBottomView_accept) { _, _ -> listener?.onAcceptMessageRequestClicked() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } else if (messageRequestState.isGroupV2Add) { + MaterialAlertDialogBuilder(context) + .setTitle(R.string.MessageRequestBottomView_join_group) + .setMessage(R.string.MessageRequestBottomView_review_requests_carefully_groups) + .setPositiveButton(R.string.MessageRequestBottomView_join) { _, _ -> listener?.onAcceptMessageRequestClicked() } + .setNegativeButton(android.R.string.cancel, null) + .show() + } else { + listener?.onAcceptMessageRequestClicked() + } + } setDeleteOnClickListener { listener?.onDeleteClicked() } setBlockOnClickListener { listener?.onBlockClicked() } setUnblockOnClickListener { listener?.onUnblockClicked() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/SafetyTipsBottomSheetDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/SafetyTipsBottomSheetDialog.kt index c3f92041d4..7895373d58 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/SafetyTipsBottomSheetDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/SafetyTipsBottomSheetDialog.kt @@ -88,6 +88,7 @@ data class SafetyTipData( ) private val tips = listOf( + SafetyTipData(heroImage = R.drawable.safety_tip0, titleText = R.string.SafetyTips_tip0_title, messageText = R.string.SafetyTips_tip0_message), SafetyTipData(heroImage = R.drawable.safety_tip1, titleText = R.string.SafetyTips_tip1_title, messageText = R.string.SafetyTips_tip1_message), SafetyTipData(heroImage = R.drawable.safety_tip2, titleText = R.string.SafetyTips_tip2_title, messageText = R.string.SafetyTips_tip2_message), SafetyTipData(heroImage = R.drawable.safety_tip3, titleText = R.string.SafetyTips_tip3_title, messageText = R.string.SafetyTips_tip3_message), diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/UnverifiedProfileNameBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/UnverifiedProfileNameBottomSheet.kt new file mode 100644 index 0000000000..1f2550316c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/UnverifiedProfileNameBottomSheet.kt @@ -0,0 +1,165 @@ +package org.thoughtcrime.securesms.conversation.v2 + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.core.os.bundleOf +import androidx.fragment.app.FragmentManager +import org.signal.core.ui.BottomSheets +import org.signal.core.ui.Previews +import org.signal.core.ui.SignalPreview +import org.signal.core.ui.horizontalGutters +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment +import org.thoughtcrime.securesms.util.BottomSheetUtil + +/** + * Bottom sheet shown in message request state that explains that profile names are unverified + */ +class UnverifiedProfileNameBottomSheet : ComposeBottomSheetDialogFragment() { + + override val peekHeightPercentage: Float = 0.75f + + companion object { + private const val FOR_GROUP_ARG = "for_group" + + @JvmStatic + fun show(fragmentManager: FragmentManager, forGroup: Boolean) { + UnverifiedProfileNameBottomSheet() + .apply { + arguments = bundleOf( + FOR_GROUP_ARG to forGroup + ) + } + .show(fragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG) + } + } + + @Composable + override fun SheetContent() { + ProfileNameSheet( + forGroup = requireArguments().getBoolean(FOR_GROUP_ARG, false) + ) + } +} + +@Composable +private fun ProfileNameSheet(forGroup: Boolean = true) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .fillMaxWidth() + .horizontalGutters() + ) { + BottomSheets.Handle() + + val (imageVector, placeholder, text) = + if (forGroup) { + Triple( + R.drawable.symbol_group_question_55, + stringResource(R.string.ConversationFragment_group_names), + stringResource(id = R.string.ProfileNameBottomSheet__group_names_on_signal, stringResource(R.string.ConversationFragment_group_names)) + ) + } else { + Triple( + R.drawable.symbol_person_question_40, + stringResource(R.string.ConversationFragment_profile_names), + stringResource(id = R.string.ProfileNameBottomSheet__profile_names_on_signal, stringResource(R.string.ConversationFragment_profile_names)) + ) + } + + Icon( + imageVector = ImageVector.vectorResource(imageVector), + contentDescription = null, + modifier = Modifier + .padding(top = 38.dp, bottom = 24.dp) + .size(height = 56.dp, width = 72.dp) + ) + + val annotatedText = remember { + buildAnnotatedString { + val start = text.indexOf(placeholder) + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append(text.substring(start, start + placeholder.length)) + } + append(text.substring(start + placeholder.length)) + } + } + + Text( + text = annotatedText, + modifier = Modifier.padding(bottom = 20.dp) + ) + + if (forGroup) { + InfoRow(stringResource(R.string.ProfileNameBottomSheet__be_cautious_of_groups)) + InfoRow(stringResource(R.string.ProfileNameBottomSheet__profile_names_in_groups)) + } else { + InfoRow(stringResource(R.string.ProfileNameBottomSheet__profile_names_arent_verified)) + InfoRow(stringResource(R.string.ProfileNameBottomSheet__be_cautious_of_accounts)) + } + + InfoRow(stringResource(R.string.ProfileNameBottomSheet__dont_share_personal)) + + Spacer(Modifier.size(55.dp)) + } +} + +@Composable +fun InfoRow(text: String) { + Row( + modifier = Modifier + .height(IntrinsicSize.Min) + .fillMaxWidth() + .padding(start = 16.dp, bottom = 12.dp) + ) { + Box( + modifier = Modifier + .width(4.dp) + .padding(vertical = 5.dp) + .fillMaxHeight() + .clip(RoundedCornerShape(10.dp)) + .background(color = MaterialTheme.colorScheme.outline.copy(.4f)) + ) + + Text( + text = text, + modifier = Modifier.padding(start = 12.dp), + style = MaterialTheme.typography.bodyLarge + ) + } +} + +@SignalPreview +@Composable +private fun ProfileNameSheetPreview() { + Previews.BottomSheetPreview { + ProfileNameSheet() + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt index f9b0cad872..018d347630 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagedetails/MessageDetailsFragment.kt @@ -386,6 +386,10 @@ class MessageDetailsFragment : FullScreenDialogFragment(), MessageDetailsAdapter Log.w(TAG, "Not yet implemented!", Exception()) } + override fun onShowUnverifiedProfileSheet(forGroup: Boolean) { + Log.w(TAG, "Not yet implemented!", Exception()) + } + interface Callback { fun onMessageDetailsFragmentDismissed() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java index 10cfc35dd6..27a9ea2474 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -47,7 +47,8 @@ import kotlin.Unit; public final class MessageRequestRepository { - private static final String TAG = Log.tag(MessageRequestRepository.class); + private static final String TAG = Log.tag(MessageRequestRepository.class); + private static final int MIN_GROUPS_THRESHOLD = 2; private final Context context; private final Executor executor; @@ -68,7 +69,7 @@ public final class MessageRequestRepository { if (groupRecord.get().isV2Group()) { List recipients = Recipient.resolvedList(groupRecord.get().getMembers()); for (Recipient recipient : recipients) { - if ((recipient.isProfileSharing() || recipient.getHasGroupsInCommon()) && !recipient.isSelf()) { + if ((recipient.isProfileSharing() || recipient.isSystemContact()) && !recipient.isSelf()) { groupHasExistingContacts = true; break; } @@ -139,8 +140,11 @@ public final class MessageRequestRepository { } else { Recipient.HiddenState hiddenState = RecipientUtil.getRecipientHiddenState(threadId); boolean reportedAsSpam = reportedAsSpam(threadId); + List sharedGroups = SignalDatabase.groups().getPushGroupNamesContainingMember(recipient.getId()); - if (hiddenState == Recipient.HiddenState.NOT_HIDDEN) { + if (hiddenState == Recipient.HiddenState.NOT_HIDDEN && sharedGroups.size() < MIN_GROUPS_THRESHOLD) { + return new MessageRequestState(MessageRequestState.State.INDIVIDUAL_FEW_CONNECTIONS, reportedAsSpam); + } else if (hiddenState == Recipient.HiddenState.NOT_HIDDEN) { return new MessageRequestState(MessageRequestState.State.INDIVIDUAL, reportedAsSpam); } else if (hiddenState == Recipient.HiddenState.HIDDEN) { return new MessageRequestState(MessageRequestState.State.NONE_HIDDEN, reportedAsSpam); diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestState.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestState.kt index ce620d5865..c5ddd33ad4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestState.kt @@ -19,6 +19,12 @@ data class MessageRequestState @JvmOverloads constructor(val state: State = Stat val isBlocked: Boolean get() = state == State.INDIVIDUAL_BLOCKED || state == State.BLOCKED_GROUP + val isFewConnectionsIndividual: Boolean + get() = state == State.INDIVIDUAL_FEW_CONNECTIONS + + val isGroupV2Add: Boolean + get() = state == State.GROUP_V2_ADD + /** * An enum representing the possible message request states a user can be in. */ @@ -50,6 +56,9 @@ data class MessageRequestState @JvmOverloads constructor(val state: State = Stat /** A user is blocked */ INDIVIDUAL_BLOCKED, + /** A message request and secondary confirmation is needed for an individual with less than 2 common groups */ + INDIVIDUAL_FEW_CONNECTIONS, + /** A message request is needed for an individual since they have been hidden */ INDIVIDUAL_HIDDEN } diff --git a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.kt b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.kt index 96dc8f9b6c..33dbc77db3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.kt @@ -90,7 +90,8 @@ class MessageRequestsBottomView @JvmOverloads constructor(context: Context, attr accept.setText(R.string.MessageRequestBottomView_accept) } - MessageRequestState.State.INDIVIDUAL -> { + MessageRequestState.State.INDIVIDUAL, + MessageRequestState.State.INDIVIDUAL_FEW_CONNECTIONS -> { question.text = HtmlCompat.fromHtml( context.getString( R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept, diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt index ca096a7b70..51d1177bf1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/about/AboutSheet.kt @@ -28,11 +28,11 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.res.painterResource +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp @@ -40,6 +40,7 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.os.bundleOf import androidx.core.widget.TextViewCompat import org.signal.core.ui.BottomSheets +import org.signal.core.ui.SignalPreview import org.signal.core.ui.theme.SignalTheme import org.signal.core.util.getParcelableCompat import org.signal.core.util.isNotNullOrBlank @@ -48,6 +49,7 @@ import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.avatar.AvatarImage import org.thoughtcrime.securesms.components.emoji.EmojiTextView import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment +import org.thoughtcrime.securesms.conversation.v2.UnverifiedProfileNameBottomSheet import org.thoughtcrime.securesms.nicknames.ViewNoteSheet import org.thoughtcrime.securesms.phonenumbers.PhoneNumberFormatter import org.thoughtcrime.securesms.recipients.Recipient @@ -112,7 +114,8 @@ class AboutSheet : ComposeBottomSheetDialogFragment() { ), onClickSignalConnections = this::openSignalConnectionsSheet, onAvatarClicked = this::openProfilePhotoViewer, - onNoteClicked = this::openNoteSheet + onNoteClicked = this::openNoteSheet, + onUnverifiedProfileClicked = this::openUnverifiedProfileSheet ) } } @@ -130,6 +133,11 @@ class AboutSheet : ComposeBottomSheetDialogFragment() { dismiss() ViewNoteSheet.create(recipientId).show(parentFragmentManager, null) } + + private fun openUnverifiedProfileSheet() { + dismiss() + UnverifiedProfileNameBottomSheet.show(fragmentManager = parentFragmentManager, forGroup = false) + } } private data class AboutModel( @@ -153,7 +161,8 @@ private fun Content( model: AboutModel, onClickSignalConnections: () -> Unit, onAvatarClicked: () -> Unit, - onNoteClicked: () -> Unit + onNoteClicked: () -> Unit, + onUnverifiedProfileClicked: () -> Unit = {} ) { Box( contentAlignment = Alignment.Center, @@ -190,7 +199,7 @@ private fun Content( ) AboutRow( - startIcon = painterResource(R.drawable.symbol_person_24), + startIcon = ImageVector.vectorResource(R.drawable.symbol_person_24), text = if (!model.isSelf && model.displayName.isNotBlank() && model.profileName.isNotBlank() && model.displayName != model.profileName) { stringResource(id = R.string.AboutSheet__user_set_display_name_and_profile_name, model.displayName, model.profileName) } else { @@ -203,7 +212,7 @@ private fun Content( val textColor = LocalContentColor.current AboutRow( - startIcon = painterResource(R.drawable.symbol_edit_24), + startIcon = ImageVector.vectorResource(R.drawable.symbol_edit_24), text = { Row { AndroidView(factory = ::EmojiTextView) { @@ -219,9 +228,19 @@ private fun Content( ) } + if (!model.isSelf && !model.profileSharing && !model.systemContact) { + AboutRow( + startIcon = ImageVector.vectorResource(id = R.drawable.symbol_person_question_24), + text = stringResource(id = R.string.AboutSheet__profile_names_are_not_verified), + endIcon = ImageVector.vectorResource(id = R.drawable.symbol_chevron_right_compact_bold_16), + modifier = Modifier.align(alignment = Alignment.Start), + onClick = onUnverifiedProfileClicked + ) + } + if (!model.isSelf && model.verified) { AboutRow( - startIcon = painterResource(id = R.drawable.symbol_safety_number_24), + startIcon = ImageVector.vectorResource(id = R.drawable.symbol_safety_number_24), text = stringResource(id = R.string.AboutSheet__verified), modifier = Modifier.align(alignment = Alignment.Start), onClick = onClickSignalConnections @@ -231,25 +250,30 @@ private fun Content( if (!model.isSelf) { if (model.profileSharing || model.systemContact) { AboutRow( - startIcon = painterResource(id = R.drawable.symbol_connections_24), + startIcon = ImageVector.vectorResource(id = R.drawable.symbol_connections_24), text = stringResource(id = R.string.AboutSheet__signal_connection), - endIcon = painterResource(id = R.drawable.symbol_chevron_right_compact_bold_16), + endIcon = ImageVector.vectorResource(id = R.drawable.symbol_chevron_right_compact_bold_16), modifier = Modifier.align(alignment = Alignment.Start), onClick = onClickSignalConnections ) + } else if (model.groupsInCommon == 0) { + AboutRow( + startIcon = ImageVector.vectorResource(id = R.drawable.symbol_chat_badge_24), + text = stringResource(id = R.string.AboutSheet__pending_message_request), + modifier = Modifier.align(alignment = Alignment.Start) + ) } else { AboutRow( - startIcon = painterResource(id = R.drawable.symbol_chat_x), + startIcon = ImageVector.vectorResource(id = R.drawable.symbol_chat_x), text = stringResource(id = R.string.AboutSheet__no_direct_message, model.shortName), - modifier = Modifier.align(alignment = Alignment.Start), - onClick = onClickSignalConnections + modifier = Modifier.align(alignment = Alignment.Start) ) } } if (!model.isSelf && model.systemContact) { AboutRow( - startIcon = painterResource(id = R.drawable.symbol_person_circle_24), + startIcon = ImageVector.vectorResource(id = R.drawable.symbol_person_circle_24), text = stringResource(id = R.string.AboutSheet__s_is_in_your_system_contacts, model.shortName), modifier = Modifier.fillMaxWidth() ) @@ -257,7 +281,7 @@ private fun Content( if (model.formattedE164.isNotNullOrBlank()) { AboutRow( - startIcon = painterResource(R.drawable.symbol_phone_24), + startIcon = ImageVector.vectorResource(R.drawable.symbol_phone_24), text = model.formattedE164, modifier = Modifier.fillMaxWidth() ) @@ -271,9 +295,9 @@ private fun Content( } val groupsInCommonIcon = if (!model.profileSharing && model.groupsInCommon == 0) { - painterResource(R.drawable.symbol_error_circle_24) + ImageVector.vectorResource(R.drawable.symbol_error_circle_24) } else { - painterResource(R.drawable.symbol_group_24) + ImageVector.vectorResource(R.drawable.symbol_group_24) } AboutRow( @@ -285,10 +309,10 @@ private fun Content( if (model.note.isNotBlank()) { AboutRow( - startIcon = painterResource(id = R.drawable.symbol_note_light_24), + startIcon = ImageVector.vectorResource(id = R.drawable.symbol_note_light_24), text = model.note, modifier = Modifier.fillMaxWidth(), - endIcon = painterResource(id = R.drawable.symbol_chevron_right_compact_bold_16), + endIcon = ImageVector.vectorResource(id = R.drawable.symbol_chevron_right_compact_bold_16), onClick = onNoteClicked ) } @@ -299,10 +323,10 @@ private fun Content( @Composable private fun AboutRow( - startIcon: Painter, + startIcon: ImageVector, text: String, modifier: Modifier = Modifier, - endIcon: Painter? = null, + endIcon: ImageVector? = null, onClick: (() -> Unit)? = null ) { AboutRow( @@ -324,10 +348,10 @@ private fun AboutRow( @Composable private fun AboutRow( - startIcon: Painter, + startIcon: ImageVector, text: @Composable RowScope.() -> Unit, modifier: Modifier = Modifier, - endIcon: Painter? = null, + endIcon: ImageVector? = null, onClick: (() -> Unit)? = null ) { val padHorizontal = if (onClick != null) 19.dp else 32.dp @@ -350,7 +374,7 @@ private fun AboutRow( } ) { Icon( - painter = startIcon, + imageVector = startIcon, contentDescription = null, modifier = Modifier .padding(end = 16.dp) @@ -361,7 +385,7 @@ private fun AboutRow( if (endIcon != null) { Icon( - painter = endIcon, + imageVector = endIcon, contentDescription = null, tint = MaterialTheme.colorScheme.outline ) @@ -549,6 +573,35 @@ private fun ContentPreviewNotAConnection() { } } +@SignalPreview +@Composable +private fun ContentPreviewNotAConnectionNoGroups() { + SignalTheme { + Surface { + Content( + model = AboutModel( + isSelf = false, + displayName = "Peter Parker", + shortName = "Peter", + profileName = "Peter Parker", + about = "(spoilers) dead", + verified = false, + hasAvatar = true, + recipientForAvatar = Recipient.UNKNOWN, + formattedE164 = null, + profileSharing = false, + systemContact = false, + groupsInCommon = 0, + note = "" + ), + onClickSignalConnections = {}, + onAvatarClicked = {}, + onNoteClicked = {} + ) + } + } +} + @Preview(name = "Light Theme", group = "about row", uiMode = Configuration.UI_MODE_NIGHT_NO) @Preview(name = "Dark Theme", group = "about row", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable @@ -556,9 +609,9 @@ private fun AboutRowPreview() { SignalTheme { Surface { AboutRow( - startIcon = painterResource(R.drawable.symbol_person_24), + startIcon = ImageVector.vectorResource(R.drawable.symbol_person_24), text = "Maya Johnson", - endIcon = painterResource(id = R.drawable.symbol_chevron_right_compact_bold_16) + endIcon = ImageVector.vectorResource(id = R.drawable.symbol_chevron_right_compact_bold_16) ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java b/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java index 99a7931b35..b915df0e66 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/SpanUtil.java @@ -186,6 +186,10 @@ public final class SpanUtil { return spannable; } + public static Spannable clickSubstring(@NonNull Context context, @StringRes int mainString, @StringRes int clickableString, @NonNull View.OnClickListener clickListener) { + return clickSubstring(context, mainString, clickableString, clickListener, false, R.color.signal_accent_primary); + } + /** * Takes two resources: * - one resource that has a single string placeholder @@ -198,8 +202,10 @@ public final class SpanUtil { * * -> This is a clickable string. * (where "clickable" is blue and will trigger the provided click listener when clicked) + * + * Can optionally configure the color & if it's underlined. Default is blue with no underline. */ - public static Spannable clickSubstring(@NonNull Context context, @StringRes int mainString, @StringRes int clickableString, @NonNull View.OnClickListener clickListener) { + public static Spannable clickSubstring(@NonNull Context context, @StringRes int mainString, @StringRes int clickableString, @NonNull View.OnClickListener clickListener, boolean shouldUnderline, int linkColor) { String main = context.getString(mainString, SPAN_PLACE_HOLDER); String clickable = context.getString(clickableString); @@ -217,8 +223,8 @@ public final class SpanUtil { @Override public void updateDrawState(@NonNull TextPaint ds) { super.updateDrawState(ds); - ds.setUnderlineText(false); - ds.setColor(context.getResources().getColor(R.color.signal_accent_primary)); + ds.setUnderlineText(shouldUnderline); + ds.setColor(context.getResources().getColor(linkColor)); } }, start, start + clickable.length(), Spanned.SPAN_INCLUSIVE_EXCLUSIVE); @@ -238,13 +244,21 @@ public final class SpanUtil { public static CharSequence clickSubstring(@NonNull CharSequence fullString, @NonNull CharSequence substring, @NonNull View.OnClickListener clickListener, - @ColorInt int linkColor) + @ColorInt int linkColor) { + return clickSubstring(fullString, substring, clickListener, linkColor, false); + } + + public static CharSequence clickSubstring(@NonNull CharSequence fullString, + @NonNull CharSequence substring, + @NonNull View.OnClickListener clickListener, + @ColorInt int linkColor, + boolean shouldUnderline) { ClickableSpan clickable = new ClickableSpan() { @Override public void updateDrawState(@NonNull TextPaint ds) { super.updateDrawState(ds); - ds.setUnderlineText(false); + ds.setUnderlineText(shouldUnderline); ds.setColor(linkColor); } diff --git a/app/src/main/res/drawable-xxhdpi/safety_tip1.png b/app/src/main/res/drawable-xxhdpi/safety_tip1.png deleted file mode 100644 index adfaf2d763..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/safety_tip1.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/safety_tip2.png b/app/src/main/res/drawable-xxhdpi/safety_tip2.png deleted file mode 100644 index fc480f1b3e..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/safety_tip2.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/safety_tip3.png b/app/src/main/res/drawable-xxhdpi/safety_tip3.png deleted file mode 100644 index edbc3797cb..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/safety_tip3.png and /dev/null differ diff --git a/app/src/main/res/drawable-xxhdpi/safety_tip4.png b/app/src/main/res/drawable-xxhdpi/safety_tip4.png deleted file mode 100644 index 2a0e16c1be..0000000000 Binary files a/app/src/main/res/drawable-xxhdpi/safety_tip4.png and /dev/null differ diff --git a/app/src/main/res/drawable/release_header_background.xml b/app/src/main/res/drawable/release_header_background.xml new file mode 100644 index 0000000000..bcd3166348 --- /dev/null +++ b/app/src/main/res/drawable/release_header_background.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/safety_tip0.webp b/app/src/main/res/drawable/safety_tip0.webp new file mode 100644 index 0000000000..6179e5345f Binary files /dev/null and b/app/src/main/res/drawable/safety_tip0.webp differ diff --git a/app/src/main/res/drawable/safety_tip1.webp b/app/src/main/res/drawable/safety_tip1.webp new file mode 100644 index 0000000000..301548bc27 Binary files /dev/null and b/app/src/main/res/drawable/safety_tip1.webp differ diff --git a/app/src/main/res/drawable/safety_tip2.webp b/app/src/main/res/drawable/safety_tip2.webp new file mode 100644 index 0000000000..ae77bec0bf Binary files /dev/null and b/app/src/main/res/drawable/safety_tip2.webp differ diff --git a/app/src/main/res/drawable/safety_tip3.webp b/app/src/main/res/drawable/safety_tip3.webp new file mode 100644 index 0000000000..89a8c9a861 Binary files /dev/null and b/app/src/main/res/drawable/safety_tip3.webp differ diff --git a/app/src/main/res/drawable/safety_tip4.webp b/app/src/main/res/drawable/safety_tip4.webp new file mode 100644 index 0000000000..24700877bd Binary files /dev/null and b/app/src/main/res/drawable/safety_tip4.webp differ diff --git a/app/src/main/res/drawable/symbol_bell_20.xml b/app/src/main/res/drawable/symbol_bell_20.xml new file mode 100644 index 0000000000..2a95c2bcdf --- /dev/null +++ b/app/src/main/res/drawable/symbol_bell_20.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/symbol_error_triangle_filled_16.xml b/app/src/main/res/drawable/symbol_error_triangle_filled_16.xml new file mode 100644 index 0000000000..ca4d77af5d --- /dev/null +++ b/app/src/main/res/drawable/symbol_error_triangle_filled_16.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/symbol_group_question_20.xml b/app/src/main/res/drawable/symbol_group_question_20.xml new file mode 100644 index 0000000000..8172837129 --- /dev/null +++ b/app/src/main/res/drawable/symbol_group_question_20.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/symbol_group_question_55.xml b/app/src/main/res/drawable/symbol_group_question_55.xml new file mode 100644 index 0000000000..bf5caa74f2 --- /dev/null +++ b/app/src/main/res/drawable/symbol_group_question_55.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/symbol_official_20.xml b/app/src/main/res/drawable/symbol_official_20.xml new file mode 100644 index 0000000000..8f45695073 --- /dev/null +++ b/app/src/main/res/drawable/symbol_official_20.xml @@ -0,0 +1,24 @@ + + + + + + + + + + diff --git a/app/src/main/res/drawable/symbol_person_question_16.xml b/app/src/main/res/drawable/symbol_person_question_16.xml new file mode 100644 index 0000000000..3f7e44fe95 --- /dev/null +++ b/app/src/main/res/drawable/symbol_person_question_16.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/symbol_person_question_24.xml b/app/src/main/res/drawable/symbol_person_question_24.xml new file mode 100644 index 0000000000..1bdce7d1fd --- /dev/null +++ b/app/src/main/res/drawable/symbol_person_question_24.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/symbol_person_question_40.xml b/app/src/main/res/drawable/symbol_person_question_40.xml new file mode 100644 index 0000000000..8f6ffbd78f --- /dev/null +++ b/app/src/main/res/drawable/symbol_person_question_40.xml @@ -0,0 +1,21 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/conversation_header_view.xml b/app/src/main/res/layout/conversation_header_view.xml index c82918b289..c23808ed57 100644 --- a/app/src/main/res/layout/conversation_header_view.xml +++ b/app/src/main/res/layout/conversation_header_view.xml @@ -92,9 +92,50 @@ android:layout_marginTop="16dp" android:layout_marginBottom="16dp" android:background="@color/signal_dark_colorTransparentInverse2" - app:layout_constraintBottom_toTopOf="@id/message_request_info" + app:layout_constraintBottom_toTopOf="@id/message_request_barrier" app:layout_constraintTop_toBottomOf="@id/message_request_about" /> + + + + + + + + + + + + + + @@ -136,7 +211,6 @@ android:id="@+id/message_request_description" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="15dp" android:gravity="center" android:textAppearance="@style/Signal.Text.BodyMedium" android:visibility="gone" @@ -145,7 +219,6 @@ app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/message_request_subtitle" - app:layout_goneMarginTop="0dp" tools:text="Member of NYC Rock Climbers, Dinner Party and Friends" tools:visibility="visible" /> @@ -154,7 +227,7 @@ style="@style/Widget.Signal.Button.Small" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_marginTop="12dp" + android:layout_marginTop="16dp" android:textColor="@color/signal_colorOnSurface" app:layout_constrainedWidth="true" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 758ce90826..c277f52a90 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -670,6 +670,14 @@ Reported as spam and blocked You accepted a message request from %1$s. If this was a mistake, you can choose an action below. + + Review carefully + + %s are not verified + + Profile names + + Group names Safety Tips @@ -681,6 +689,10 @@ Previous tip Next tip + + Fake names and accounts + + Signal will never contact you for your registration code or PIN. Be cautious of requests that impersonate others. Profile names are chosen by their account holder and aren\'t verified. Crypto or money scams @@ -698,6 +710,21 @@ Be careful of businesses or government agencies contacting you. Messages involving tax agencies, couriers, and more can be spam. + + %s on Signal are chosen by their account holder. + + Profile names aren’t verified + + Be cautious of accounts that impersonate others + + Don’t share personal information with people you don’t know + + %1$s are chosen by members of the group. + + Be cautious of groups that impersonate organizations and businesses + + Profile names of members in groups are not verified + Clear filter @@ -2104,6 +2131,16 @@ Report… + + Accept request? + + Review requests carefully. Profile names are chosen by their account owner and aren’t verified. + + Join group? + + Review requests carefully. Group names are chosen by members of the group and aren’t verified. + + Join Passphrases don\'t match! @@ -2487,12 +2524,16 @@ Signal connection Verified + + Profile names are not verified + + Pending message request No direct messages with %1$s %1$s is in your phone contacts - You have no groups in common + No groups in common Review requests carefully @@ -3273,7 +3314,8 @@ Update contact Block request - No groups in common. Review requests carefully. + + No groups in common No contacts in this group. Review requests carefully. View The disappearing message time will be set to %1$s when you message them. @@ -6230,6 +6272,16 @@ Signal Release Notes & News + + This is the official and only chat from Signal. + + Keep up to date with news and release notes. + + This is the official and only chat from Signal + + Keep up to date with news and release notes + + Official and only Signal chat All activity