diff --git a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt index f81a6b9092..faa77c76f0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/backup/v2/importer/ChatItemArchiveImporter.kt @@ -718,7 +718,7 @@ class ChatItemArchiveImporter( when { itemStandardMessage != null -> contentValues.addStandardMessage(itemStandardMessage) itemRemoteDeletedMessage != null -> contentValues.put(MessageTable.DELETED_BY, fromRecipientId.toLong()) - itemUpdateMessage != null -> contentValues.addUpdateMessage(itemUpdateMessage, fromRecipientId, toRecipientId) + itemUpdateMessage != null -> contentValues.addUpdateMessage(itemUpdateMessage, fromRecipientId, toRecipientId, chatRecipientId) itemPaymentNotification != null -> contentValues.addPaymentNotification(this, chatRecipientId) itemGiftBadge != null -> contentValues.addGiftBadge(itemGiftBadge) itemViewOnceMessage != null -> contentValues.addViewOnce(itemViewOnceMessage) @@ -866,7 +866,7 @@ class ChatItemArchiveImporter( } } - private fun ContentValues.addUpdateMessage(updateMessage: ChatUpdateMessage, fromRecipientId: RecipientId, toRecipientId: RecipientId) { + private fun ContentValues.addUpdateMessage(updateMessage: ChatUpdateMessage, fromRecipientId: RecipientId, toRecipientId: RecipientId, chatRecipientId: RecipientId) { var typeFlags: Long = 0 val simpleUpdate = updateMessage.simpleUpdate val expirationTimerChange = updateMessage.expirationTimerChange @@ -907,6 +907,11 @@ class ChatItemArchiveImporter( put(MessageTable.FROM_RECIPIENT_ID, toRecipientId.serialize()) put(MessageTable.TO_RECIPIENT_ID, fromRecipientId.serialize()) } + + // directionless 1:1 message requests expect to recipient to be the other recipient not self + if (simpleUpdate.type == SimpleChatUpdate.Type.MESSAGE_REQUEST_ACCEPTED) { + put(MessageTable.TO_RECIPIENT_ID, chatRecipientId.serialize()) + } } expirationTimerChange != null -> { typeFlags = getAsLong(MessageTable.TYPE) or MessageTypes.EXPIRATION_TIMER_UPDATE_BIT diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java deleted file mode 100644 index 0a101aa0f8..0000000000 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.java +++ /dev/null @@ -1,327 +0,0 @@ -package org.thoughtcrime.securesms.conversation; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ObjectAnimator; -import android.content.Context; -import android.graphics.PorterDuff; -import android.graphics.drawable.Drawable; -import android.os.Handler; -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.appcompat.content.res.AppCompatResources; -import androidx.constraintlayout.widget.ConstraintLayout; -import androidx.core.content.ContextCompat; -import androidx.core.view.ViewKt; - -import com.bumptech.glide.RequestManager; - -import org.signal.core.util.DimensionUnit; -import org.signal.core.util.concurrent.SignalExecutors; -import org.signal.core.util.logging.Log; -import org.thoughtcrime.securesms.R; -import org.thoughtcrime.securesms.components.emoji.EmojiTextView; -import org.thoughtcrime.securesms.conversation.colors.AvatarGradientColors; -import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache; -import org.thoughtcrime.securesms.database.SignalDatabase; -import org.thoughtcrime.securesms.databinding.ConversationHeaderViewBinding; -import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob; -import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob; -import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.util.LongClickMovementMethod; -import org.thoughtcrime.securesms.util.SpanUtil; -import org.thoughtcrime.securesms.util.ViewUtil; -import org.whispersystems.signalservice.api.util.Preconditions; - -public class ConversationHeaderView extends ConstraintLayout { - - private static final String TAG = Log.tag(ConversationHeaderView.class); - private static final int FADE_DURATION = 150; - private static final int LOADING_DELAY = 800; - - private final ConversationHeaderViewBinding binding; - - private boolean inProgress = false; - private Handler handler = new Handler(); - - public ConversationHeaderView(Context context) { - this(context, null); - } - - public ConversationHeaderView(Context context, AttributeSet attrs) { - this(context, attrs, 0); - } - - public ConversationHeaderView(Context context, AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); - - inflate(getContext(), R.layout.conversation_header_view, this); - - binding = ConversationHeaderViewBinding.bind(this); - } - - public void showProgressBar(@NonNull Recipient recipient) { - if (!inProgress) { - inProgress = true; - animateAvatarLoading(recipient); - binding.messageRequestAvatarTapToView.setVisibility(GONE); - binding.messageRequestAvatarTapToView.setOnClickListener(null); - handler.postDelayed(() -> { - boolean isDownloading = AvatarDownloadStateCache.getDownloadState(recipient) == AvatarDownloadStateCache.DownloadState.IN_PROGRESS; - binding.progressBar.setVisibility(isDownloading ? View.VISIBLE : View.GONE); - }, LOADING_DELAY); - } - } - - public void hideProgressBar() { - inProgress = false; - binding.progressBar.setVisibility(View.GONE); - } - - public void showFailedAvatarDownload(@NonNull Recipient recipient) { - AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.NONE); - binding.progressBar.setVisibility(View.GONE); - binding.messageRequestAvatar.setImageDrawable(AvatarGradientColors.getGradientDrawable(recipient)); - } - - public void setBadge(@Nullable Recipient recipient) { - if (recipient == null || recipient.isSelf()) { - binding.messageRequestBadge.setBadge(null); - } else { - binding.messageRequestBadge.setBadgeFromRecipient(recipient); - } - } - - public void setAvatar(@NonNull RequestManager requestManager, @Nullable Recipient recipient) { - if (recipient == null) { - return; - } - - if (AvatarDownloadStateCache.getDownloadState(recipient) != AvatarDownloadStateCache.DownloadState.IN_PROGRESS) { - binding.messageRequestAvatar.setAvatar(requestManager, recipient, false, false, true); - hideProgressBar(); - } - - if (recipient.getShouldBlurAvatar() && recipient.getHasAvatar()) { - binding.messageRequestAvatarTapToView.setVisibility(VISIBLE); - binding.messageRequestAvatarTapToView.setOnClickListener(v -> { - AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.IN_PROGRESS); - SignalExecutors.BOUNDED.execute(() -> SignalDatabase.recipients().manuallyUpdateShowAvatar(recipient.getId(), true)); - if (recipient.isPushV2Group()) { - AvatarGroupsV2DownloadJob.enqueueUnblurredAvatar(recipient.requireGroupId().requireV2()); - } else { - RetrieveProfileAvatarJob.enqueueUnblurredAvatar(recipient); - } - }); - } else { - binding.messageRequestAvatarTapToView.setVisibility(GONE); - binding.messageRequestAvatarTapToView.setOnClickListener(null); - } - } - - public String setTitle(@NonNull Recipient recipient, @NonNull Runnable onTitleClicked) { - CharSequence title = recipient.getDisplayNameForHeadline(getContext()); - - if (recipient.isIndividual() && !recipient.isSelf()) { - binding.messageRequestTitle.setOnClickListener(v -> onTitleClicked.run()); - } else { - binding.messageRequestTitle.setOnClickListener(null); - } - - binding.messageRequestTitle.setText(title); - return title.toString(); - } - - 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 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 String substring, @Nullable Runnable onClick) { - if (TextUtils.isEmpty(subtitle)) { - hideSubtitle(); - return; - } - - if (onClick != null && substring != null) { - binding.messageRequestSubtitle.setMovementMethod(LinkMovementMethod.getInstance()); - CharSequence builder = SpanUtil.clickSubstring( - subtitle, - substring, - listener -> onClick.run(), - ContextCompat.getColor(getContext(), org.signal.core.ui.R.color.signal_colorOnSurface), - true - ); - binding.messageRequestSubtitle.setText(prependIcon(builder, iconRes)); - } else { - binding.messageRequestSubtitle.setText(prependIcon(subtitle, iconRes)); - } - - binding.messageRequestSubtitle.setVisibility(View.VISIBLE); - } - - public void setDescription(@Nullable CharSequence description, @DrawableRes int iconRes) { - if (TextUtils.isEmpty(description)) { - hideDescription(); - return; - } - - binding.messageRequestDescription.setText(prependIcon(description, iconRes)); - binding.messageRequestDescription.setVisibility(View.VISIBLE); - updateOutlineVisibility(); - } - - public @NonNull EmojiTextView getDescription() { - return binding.messageRequestDescription; - } - - public void setButton(@NonNull CharSequence button, Runnable onClick) { - binding.messageRequestButton.setText(button); - binding.messageRequestButton.setOnClickListener(v -> onClick.run()); - 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, boolean forGroup, @NonNull Runnable onClick) { - binding.messageRequestProfileNameUnverified.setVisibility(View.VISIBLE); - binding.messageRequestProfileNameUnverified.setOnClickListener(view -> onClick.run()); - - String substring = forGroup ? getContext().getString(R.string.ConversationFragment_group_names) - : getContext().getString(R.string.ConversationFragment_profile_names); - - String fullString = forGroup ? getContext().getString(R.string.ConversationFragment_group_names_not_verified, substring) - : getContext().getString(R.string.ConversationFragment_profile_names_not_verified, substring); - - CharSequence builder = SpanUtil.underlineSubstring(fullString, substring); - 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); - } else { - setBackground(null); - } - - updateOutlineVisibility(); - } - - public void hideSubtitle() { - binding.messageRequestSubtitle.setVisibility(View.GONE); - updateOutlineVisibility(); - } - - public void showDescription() { - binding.messageRequestDescription.setVisibility(View.VISIBLE); - updateOutlineVisibility(); - } - - public void hideDescription() { - binding.messageRequestDescription.setVisibility(View.GONE); - updateOutlineVisibility(); - } - - public void hideButton() { - binding.messageRequestButton.setVisibility(View.GONE); - } - - public void setLinkifyDescription(boolean enable) { - binding.messageRequestDescription.setMovementMethod(enable ? LongClickMovementMethod.getInstance(getContext()) : null); - } - - private void animateAvatarLoading(@NonNull Recipient recipient) { - Drawable loadingProfile = AppCompatResources.getDrawable(getContext(), R.drawable.circle_profile_photo); - ObjectAnimator animator = ObjectAnimator.ofFloat(binding.messageRequestAvatar, "alpha", 1f, 0f).setDuration(FADE_DURATION); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - if (AvatarDownloadStateCache.getDownloadState(recipient) == AvatarDownloadStateCache.DownloadState.IN_PROGRESS) { - binding.messageRequestAvatar.setImageDrawable(loadingProfile); - } - ObjectAnimator.ofFloat(binding.messageRequestAvatar, "alpha", 0f, 1f).setDuration(FADE_DURATION).start(); - } - }); - - animator.start(); - } - - private void updateOutlineVisibility() { - if (ViewKt.isVisible(binding.messageRequestSubtitle) || ViewKt.isVisible(binding.messageRequestDescription)) { - if (getBackground() != null) { - binding.messageRequestInfoOutline.setVisibility(View.GONE); - binding.messageRequestDivider.setVisibility(View.VISIBLE); - } else { - binding.messageRequestInfoOutline.setVisibility(View.VISIBLE); - binding.messageRequestDivider.setVisibility(View.GONE); - } - } else { - binding.messageRequestInfoOutline.setVisibility(View.GONE); - binding.messageRequestDivider.setVisibility(View.GONE); - } - } - - public void updateOutlineBoxSize() { - int visibleCount = 0; - for (int i = 0; i < binding.messageRequestInfo.getChildCount(); i++) { - if (ViewKt.isVisible(binding.messageRequestInfo.getChildAt(i))) { - visibleCount++; - } - } - - if (getBackground() != null) { - ViewUtil.setPaddingTop(binding.messageRequestInfo, 0); - ViewUtil.setPaddingBottom(binding.messageRequestInfo, getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_header_padding)); - int margin = getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_header_margin); - ViewUtil.setLeftMargin(this, margin); - ViewUtil.setRightMargin(this, margin); - } - - int padding = visibleCount == 1 ? getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_header_padding) : getContext().getResources().getDimensionPixelOffset(R.dimen.conversation_header_padding_expanded); - ViewUtil.setPaddingStart(binding.messageRequestInfo, padding); - ViewUtil.setPaddingEnd(binding.messageRequestInfo, padding); - } - - 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); - int width = useIntrinsicWidth ? drawable.getIntrinsicWidth() : (int) DimensionUnit.SP.toPixels(16); - drawable.setBounds(0, 0, width, (int) DimensionUnit.SP.toPixels(16)); - drawable.setColorFilter(ContextCompat.getColor(getContext(), org.signal.core.ui.R.color.signal_colorOnSurface), PorterDuff.Mode.SRC_ATOP); - - return new SpannableStringBuilder() - .append(SpanUtil.buildCenteredImageSpan(drawable)) - .append(SpanUtil.space(8, DimensionUnit.SP)) - .append(input); - } -} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.kt new file mode 100644 index 0000000000..b332734091 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.kt @@ -0,0 +1,647 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation + +import android.content.Context +import android.util.AttributeSet +import android.view.Gravity +import android.widget.ImageView +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.Crossfade +import androidx.compose.animation.core.tween +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.clickable +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.SnapshotMutationPolicy +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.AbstractComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.ViewCompositionStrategy +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.pluralStringResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.LinkAnnotation +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.TextLinkStyles +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDecoration +import androidx.compose.ui.text.withLink +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.compose.ui.viewinterop.AndroidView +import kotlinx.coroutines.delay +import org.signal.core.ui.compose.Buttons +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.Previews +import org.signal.core.ui.compose.theme.SignalTheme +import org.signal.core.util.BidiUtil +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.avatar.AvatarImage +import org.thoughtcrime.securesms.badges.models.Badge +import org.thoughtcrime.securesms.components.emoji.EmojiTextView +import org.thoughtcrime.securesms.components.settings.app.subscription.BadgeImageLarge +import org.thoughtcrime.securesms.conversation.colors.AvatarGradientColors +import org.thoughtcrime.securesms.conversation.v2.data.AvatarDownloadStateCache +import org.thoughtcrime.securesms.fonts.SignalSymbols +import org.thoughtcrime.securesms.fonts.SignalSymbols.buildSignalSymbolAnnotatedString +import org.thoughtcrime.securesms.fonts.SignalSymbols.signalSymbolText +import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil +import org.thoughtcrime.securesms.messagerequests.GroupInfo +import org.thoughtcrime.securesms.messagerequests.MessageRequestRecipientInfo +import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.recipients.RecipientId +import org.thoughtcrime.securesms.util.LongClickMovementMethod +import org.thoughtcrime.securesms.util.SignalE164Util +import org.signal.core.ui.R as CoreUiR + +private val AvatarSize = 74.dp +private val AvatarOverlapAbove = 16.dp +private val AvatarOverlapBelow = AvatarSize - AvatarOverlapAbove +private val BorderShape = RoundedCornerShape(40.dp) + +class ConversationHeaderView : AbstractComposeView { + constructor(context: Context) : super(context) + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + + init { + setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed) + } + + var callbacks: ConversationHeaderCallbacks = ConversationHeaderCallbacks.Empty + var recipientInfo: MessageRequestRecipientInfo? by mutableStateOf(null, policy = RecipientInfoContentPolicy) + var avatarDownloadState: AvatarDownloadStateCache.DownloadState by mutableStateOf(AvatarDownloadStateCache.DownloadState.NONE) + + @Composable + override fun Content() { + val info = recipientInfo ?: return + val recipient = info.recipient + val groupInfo = info.groupInfo + val isSelf = recipient.isSelf + val isReleaseNotes = recipient.isReleaseNotes + val isOfficialAccount = recipient.showVerified + + val showUnverifiedName = if (recipient.isGroup) { + !groupInfo.hasExistingContacts && !(groupInfo.fullMemberCount == 1 && groupInfo.isMember) + } else if (!isOfficialAccount) { + recipient.nickname.isEmpty && !recipient.isSystemContact + } else { + false + } + + val displayName = if (isSelf) BidiUtil.isolateBidi(context.getString(R.string.note_to_self)) else recipient.getDisplayName(context) + val phoneNumber = if (!recipient.isGroup && !isOfficialAccount && recipient.shouldShowE164) { + recipient.e164.map { SignalE164Util.prettyPrint(it) }.orElse(null)?.takeIf { it != displayName } + } else { + null + } + + SignalTheme { + ConversationHeaderContent( + recipientId = recipient.id, + displayName = displayName, + showVerified = isOfficialAccount, + isSystemContact = recipient.isSystemContact, + showChevron = recipient.isIndividual && !isOfficialAccount, + isSelf = isSelf, + isReleaseNotes = isReleaseNotes, + badge = if (!isOfficialAccount) recipient.featuredBadge else null, + showUnverifiedName = showUnverifiedName, + isGroup = recipient.isGroup, + hasWallpaper = recipient.hasWallpaper, + phoneNumber = phoneNumber, + groupInfo = if (recipient.isGroup) groupInfo else null, + groupDescription = if (recipient.isGroup) groupInfo.description else null, + linkifyGroupDescription = info.messageRequestState?.isAccepted == true, + sharedGroups = info.sharedGroups, + showSafetyTips = info.messageRequestState?.isAccepted == false, + avatarDownloadState = avatarDownloadState, + shouldBlurAvatar = recipient.shouldBlurAvatar && recipient.hasAvatar, + callbacks = callbacks + ) + } + } +} + +interface ConversationHeaderCallbacks { + fun onSafetyTipsClicked(forGroup: Boolean) = Unit + fun onUnverifiedNameClicked(forGroup: Boolean) = Unit + fun onTitleClicked() = Unit + fun onGroupSettingsClicked() = Unit + fun onShowGroupDescriptionClicked(groupName: String, description: String, linkifyWebLinks: Boolean) = Unit + fun onAvatarTapToViewClicked() = Unit + + companion object Empty : ConversationHeaderCallbacks +} + +private object RecipientInfoContentPolicy : SnapshotMutationPolicy { + override fun equivalent(a: MessageRequestRecipientInfo?, b: MessageRequestRecipientInfo?): Boolean { + if (a === b) return true + if (a == null || b == null) return false + return a.recipient.hasSameContent(b.recipient) && + a.groupInfo == b.groupInfo && + a.sharedGroups == b.sharedGroups && + a.messageRequestState == b.messageRequestState + } +} + +@Composable +private fun ConversationHeaderContent( + recipientId: RecipientId, + displayName: String, + showVerified: Boolean = false, + isSystemContact: Boolean = false, + showChevron: Boolean = false, + isSelf: Boolean = false, + isReleaseNotes: Boolean = false, + badge: Badge?, + showUnverifiedName: Boolean, + isGroup: Boolean, + hasWallpaper: Boolean = false, + phoneNumber: String? = null, + groupInfo: GroupInfo? = null, + groupDescription: String? = null, + linkifyGroupDescription: Boolean = false, + sharedGroups: List = emptyList(), + showSafetyTips: Boolean = false, + avatarDownloadState: AvatarDownloadStateCache.DownloadState, + shouldBlurAvatar: Boolean = false, + callbacks: ConversationHeaderCallbacks = ConversationHeaderCallbacks.Empty +) { + Box( + modifier = Modifier.fillMaxWidth(), + contentAlignment = Alignment.TopCenter + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + modifier = Modifier + .padding(top = AvatarOverlapAbove) + .width(277.dp) + .then( + if (hasWallpaper) { + Modifier + .clip(BorderShape) + .background(if (isSystemInDarkTheme()) SignalTheme.colors.colorTransparentInverse5 else SignalTheme.colors.colorTransparent5) + } else { + Modifier.border(width = 2.5.dp, color = SignalTheme.colors.colorSurface3, shape = BorderShape) + } + ) + .padding(top = AvatarOverlapBelow + 12.dp, bottom = 24.dp, start = 24.dp, end = 24.dp) + ) { + HeadlineDisplayName( + displayName = displayName, + showVerified = showVerified, + isSystemContact = isSystemContact, + showChevron = showChevron, + modifier = Modifier.clickable { callbacks.onTitleClicked() } + ) + + if (isSelf) { + OfficialChatPill() + Text( + text = stringResource(R.string.ConversationFragment__you_can_add_notes_for_yourself_in_this_conversation), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 8.dp) + ) + } + + if (isReleaseNotes) { + OfficialChatPill() + Text( + text = stringResource(R.string.ConversationFragment_release_notes_description), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = Modifier.padding(top = 8.dp) + ) + } + + if (showUnverifiedName) { + UnverifiedNamePill( + onClick = { callbacks.onUnverifiedNameClicked(isGroup) }, + modifier = Modifier.padding(top = 8.dp) + ) + } + + if (phoneNumber != null) { + Text( + text = signalSymbolText( + text = phoneNumber, + glyphStart = SignalSymbols.Glyph.PHONE + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) + ) + } + + if (!groupDescription.isNullOrEmpty()) { + GroupDescription( + description = groupDescription, + linkify = linkifyGroupDescription, + onMoreClicked = { callbacks.onShowGroupDescriptionClicked(displayName.toString(), groupDescription, linkifyGroupDescription) }, + modifier = Modifier.padding(top = 8.dp, bottom = 4.dp) + ) + } + + if (groupInfo != null) { + GroupMemberSubtitle( + groupInfo = groupInfo, + onGroupSettingsClicked = { callbacks.onGroupSettingsClicked() }, + modifier = Modifier.padding(top = 8.dp) + ) + } + + if (!isSelf && !isReleaseNotes && (sharedGroups.isNotEmpty() || !isGroup)) { + SharedGroupsDescription( + sharedGroups = sharedGroups, + modifier = Modifier.padding(top = 8.dp) + ) + } + + if (showSafetyTips) { + Buttons.Small( + onClick = { callbacks.onSafetyTipsClicked(isGroup) }, + modifier = Modifier.padding(top = 12.dp) + ) { + Text(text = stringResource(R.string.ConversationFragment_safety_tips)) + } + } + } + + AvatarWithBadge( + recipientId = recipientId, + badge = badge, + useProfile = !isSelf, + avatarDownloadState = avatarDownloadState, + shouldBlurAvatar = shouldBlurAvatar, + onTapToView = callbacks::onAvatarTapToViewClicked + ) + } +} + +@Composable +private fun AvatarWithBadge( + recipientId: RecipientId, + badge: Badge?, + useProfile: Boolean = true, + avatarDownloadState: AvatarDownloadStateCache.DownloadState, + shouldBlurAvatar: Boolean = false, + onTapToView: () -> Unit = {} +) { + val showBlur = shouldBlurAvatar && avatarDownloadState != AvatarDownloadStateCache.DownloadState.IN_PROGRESS + val showProgress = avatarDownloadState == AvatarDownloadStateCache.DownloadState.IN_PROGRESS + val showGradient = showBlur || showProgress || avatarDownloadState == AvatarDownloadStateCache.DownloadState.FAILED + + Box(contentAlignment = Alignment.Center) { + Crossfade( + targetState = showGradient, + animationSpec = tween(durationMillis = 220), + label = "avatar-crossfade" + ) { gradient -> + if (gradient) { + AndroidView( + factory = { context -> + ImageView(context).apply { + scaleType = ImageView.ScaleType.CENTER_CROP + } + }, + update = { view -> + view.setImageDrawable(AvatarGradientColors.getGradientDrawable(Recipient.resolved(recipientId))) + }, + modifier = Modifier + .size(AvatarSize) + .clip(CircleShape) + ) + } else { + AvatarImage( + recipientId = recipientId, + useProfile = useProfile, + modifier = Modifier.size(AvatarSize) + ) + } + } + + AnimatedVisibility( + visible = showProgress, + enter = fadeIn(tween(durationMillis = 220)), + exit = fadeOut(tween(durationMillis = 220)) + ) { + var showSpinner by remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + delay(800) + showSpinner = AvatarDownloadStateCache.getDownloadState(recipientId) == AvatarDownloadStateCache.DownloadState.IN_PROGRESS + } + + if (showSpinner) { + CircularProgressIndicator( + strokeWidth = 3.dp, + color = Color.White, + modifier = Modifier.size(36.dp) + ) + } + } + + AnimatedVisibility( + visible = showBlur, + enter = fadeIn(tween(durationMillis = 220)), + exit = fadeOut(tween(durationMillis = 220)) + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + modifier = Modifier + .size(AvatarSize) + .clip(CircleShape) + .clickable(onClick = onTapToView) + ) { + Icon( + painter = painterResource(R.drawable.ic_tap_outline_24), + contentDescription = null, + tint = Color.White + ) + Spacer(Modifier.size(4.dp)) + Text( + text = stringResource(R.string.MessageRequestProfileView_view), + style = MaterialTheme.typography.bodySmall, + color = Color.White + ) + } + } + + if (badge != null) { + BadgeImageLarge( + badge = badge, + modifier = Modifier + .size(36.dp) + .align(Alignment.BottomEnd) + ) + } + } +} + +@Composable +private fun UnverifiedNamePill( + onClick: () -> Unit = {}, + modifier: Modifier = Modifier +) { + Text( + text = signalSymbolText( + text = stringResource(R.string.ConversationFragment_name_not_verified), + glyphStart = SignalSymbols.Glyph.PERSON_QUESTION, + glyphStartWeight = SignalSymbols.Weight.BOLD, + glyphStartSize = 14.sp + ), + style = MaterialTheme.typography.bodyMedium, + fontWeight = FontWeight.Medium, + color = SignalTheme.colors.colorOnWarning, + modifier = modifier + .clip(RoundedCornerShape(26.dp)) + .clickable(onClick = onClick) + .background(SignalTheme.colors.colorWarning) + .padding(horizontal = 12.dp, vertical = 4.dp) + ) +} + +@Composable +private fun SharedGroupsDescription( + sharedGroups: List, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val description = when (sharedGroups.size) { + 0 -> stringResource(R.string.ConversationUpdateItem_no_groups_in_common_review_requests_carefully) + 1 -> stringResource(R.string.MessageRequestProfileView_member_of_one_group, sharedGroups[0]) + 2 -> stringResource(R.string.MessageRequestProfileView_member_of_two_groups, sharedGroups[0], sharedGroups[1]) + else -> { + val others = sharedGroups.size - 2 + stringResource( + R.string.MessageRequestProfileView_member_of_many_groups, + sharedGroups[0], + sharedGroups[1], + context.resources.getQuantityString(R.plurals.MessageRequestProfileView_member_of_d_additional_groups, others, others) + ) + } + } + + Text( + text = signalSymbolText( + text = description, + glyphStart = SignalSymbols.Glyph.GROUP + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = modifier + ) +} + +@Composable +private fun GroupMemberSubtitle( + groupInfo: GroupInfo, + onGroupSettingsClicked: () -> Unit, + modifier: Modifier = Modifier +) { + val context = LocalContext.current + val memberCount = groupInfo.fullMemberCount + + val styledText = if (groupInfo.isMember) { + val names = groupInfo.membersPreview.map { it.getDisplayName(context) } + val othersCount = memberCount - 3 + val othersText = if (othersCount > 0) pluralStringResource(R.plurals.MessageRequestProfileView_other_members, othersCount, othersCount) else null + + val fullText = when (names.size) { + 0 -> stringResource(R.string.MessageRequestProfileView_group_members_zero) + 1 -> stringResource(R.string.MessageRequestProfileView_group_members_one_and_you, names[0]) + 2 -> stringResource(R.string.MessageRequestProfileView_group_members_two_and_you, names[0], names[1]) + else -> stringResource(R.string.MessageRequestProfileView_group_members_other, names[0], names[1], names[2], othersText ?: "") + } + + buildSignalSymbolAnnotatedString(glyphStart = SignalSymbols.Glyph.GROUP) { + if (othersText != null) { + val othersStart = fullText.indexOf(othersText) + if (othersStart >= 0) { + append(fullText.take(othersStart)) + withLink(LinkAnnotation.Clickable(tag = "group_settings", styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.onSurface))) { onGroupSettingsClicked() }) { + withStyle(SpanStyle(textDecoration = TextDecoration.Underline)) { + append(othersText) + } + } + append(fullText.substring(othersStart + othersText.length)) + } else { + append(fullText) + } + } else { + append(fullText) + } + } + } else { + buildSignalSymbolAnnotatedString(glyphStart = SignalSymbols.Glyph.GROUP) { + withLink(LinkAnnotation.Clickable(tag = "group_settings", styles = TextLinkStyles(style = SpanStyle(color = MaterialTheme.colorScheme.onSurface))) { onGroupSettingsClicked() }) { + withStyle(SpanStyle(textDecoration = TextDecoration.Underline)) { + append(pluralStringResource(R.plurals.ConversationFragment_group_member_count, memberCount, memberCount)) + } + } + } + } + + Text( + text = styledText, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = modifier + ) +} + +@Composable +private fun GroupDescription( + description: String, + linkify: Boolean, + onMoreClicked: () -> Unit, + modifier: Modifier = Modifier +) { + AndroidView( + factory = { context -> + EmojiTextView(context).apply { + layoutParams = android.view.ViewGroup.LayoutParams( + android.view.ViewGroup.LayoutParams.MATCH_PARENT, + android.view.ViewGroup.LayoutParams.WRAP_CONTENT + ) + setTextAppearance(CoreUiR.style.Signal_Text_BodyMedium) + gravity = Gravity.CENTER + movementMethod = LongClickMovementMethod.getInstance(context) + } + }, + update = { view -> + GroupDescriptionUtil.setText(view.context, view, description, linkify) { + onMoreClicked() + } + }, + modifier = modifier.fillMaxWidth() + ) +} + +@Composable +private fun OfficialChatPill() { + val pillShape = RoundedCornerShape(26.dp) + + Text( + text = signalSymbolText( + text = stringResource(R.string.ConversationFragment_official_chat), + glyphStart = SignalSymbols.Glyph.OFFICIAL_BADGE, + glyphStartWeight = SignalSymbols.Weight.BOLD + ), + style = MaterialTheme.typography.labelLarge, + fontWeight = FontWeight.Medium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier + .padding(top = 8.dp) + .clip(pillShape) + .background(MaterialTheme.colorScheme.primaryContainer) + .padding(horizontal = 12.dp, vertical = 4.dp) + ) +} + +@DayNightPreviews +@Composable +private fun ConversationHeaderPreview() { + Previews.Preview { + ConversationHeaderContent( + recipientId = RecipientId.from(1), + displayName = "Katie Hall", + showChevron = true, + badge = null, + showUnverifiedName = true, + isGroup = false, + phoneNumber = "+1 (555) 867-5309", + sharedGroups = emptyList(), + showSafetyTips = true, + avatarDownloadState = AvatarDownloadStateCache.DownloadState.NONE + ) + } +} + +@DayNightPreviews +@Composable +private fun ConversationHeaderWithGroupsPreview() { + Previews.Preview { + ConversationHeaderContent( + recipientId = RecipientId.from(1), + displayName = "Katie Hall", + showChevron = true, + badge = null, + showUnverifiedName = false, + isGroup = false, + sharedGroups = listOf("NYC Rock Climbers", "Dinner Party"), + avatarDownloadState = AvatarDownloadStateCache.DownloadState.NONE + ) + } +} + +@DayNightPreviews +@Composable +private fun ConversationHeaderGroupPreview() { + Previews.Preview { + ConversationHeaderContent( + recipientId = RecipientId.from(1), + displayName = "Trail Crew", + badge = null, + showUnverifiedName = true, + isGroup = true, + groupInfo = GroupInfo(fullMemberCount = 12, isMember = false), + avatarDownloadState = AvatarDownloadStateCache.DownloadState.NONE + ) + } +} + +@DayNightPreviews +@Composable +private fun ConversationHeaderNoteToSelfPreview() { + Previews.Preview { + ConversationHeaderContent( + recipientId = RecipientId.from(1), + displayName = "Note to Self", + showVerified = true, + isSelf = true, + badge = null, + showUnverifiedName = false, + isGroup = false, + avatarDownloadState = AvatarDownloadStateCache.DownloadState.NONE + ) + } +} 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 c72ee35ddd..6e0c3868ab 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java @@ -199,13 +199,14 @@ public class ConversationTitleView extends ConstraintLayout { private void setSelfTitle() { this.title.setText(R.string.note_to_self); + this.subtitle.setText(R.string.ConversationFragment_official_chat); 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); + this.subtitle.setText(R.string.ConversationFragment_official_chat); updateSubtitleVisibility(); } @@ -221,7 +222,7 @@ public class ConversationTitleView extends ConstraintLayout { } private void updateSubtitleVisibility() { - subtitle.setVisibility(!isSelf && expirationBadgeContainer.getVisibility() != VISIBLE && !TextUtils.isEmpty(subtitle.getText()) ? VISIBLE : GONE); + subtitle.setVisibility(expirationBadgeContainer.getVisibility() != VISIBLE && !TextUtils.isEmpty(subtitle.getText()) ? VISIBLE : GONE); updateVerifiedSubtitleVisibility(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java index 49a7fd0021..3180e574f1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationUpdateItem.java @@ -729,7 +729,7 @@ public final class ConversationUpdateItem extends FrameLayout } }); } else if (conversationMessage.getMessageRecord().isMessageRequestAccepted()) { - actionButton.setText(R.string.ConversationUpdateItem_options); + actionButton.setText(R.string.ConversationUpdateItem_block_report); actionButton.setVisibility(VISIBLE); actionButton.setOnClickListener(v -> { if (batchSelected.isEmpty() && eventListener != null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/HeadlineDisplayName.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/HeadlineDisplayName.kt new file mode 100644 index 0000000000..21f1afbb6f --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/HeadlineDisplayName.kt @@ -0,0 +1,193 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.offset +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.text.InlineTextContent +import androidx.compose.foundation.text.appendInlineContent +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.text.Placeholder +import androidx.compose.ui.text.PlaceholderVerticalAlign +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.style.BaselineShift +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.text.style.TextDirection +import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import org.signal.core.ui.compose.DayNightPreviews +import org.signal.core.ui.compose.LargeFontPreviews +import org.signal.core.ui.compose.Previews +import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.components.emoji.Emojifier +import org.thoughtcrime.securesms.fonts.SignalSymbols +import org.thoughtcrime.securesms.fonts.SignalSymbols.SignalSymbol + +private const val VERIFIED_BADGE_ID = "verified_badge" + +/** + * Compose-native version of [org.thoughtcrime.securesms.recipients.Recipient.getDisplayNameForHeadline]. + */ +@Composable +fun HeadlineDisplayName( + displayName: String, + showVerified: Boolean, + isSystemContact: Boolean, + showChevron: Boolean, + modifier: Modifier = Modifier +) { + val isLtr = LocalLayoutDirection.current == LayoutDirection.Ltr + val chevronGlyph = if (isLtr) SignalSymbols.Glyph.CHEVRON_RIGHT else SignalSymbols.Glyph.CHEVRON_LEFT + val outlineColor = MaterialTheme.colorScheme.outline + val badgeOffset = with(LocalDensity.current) { (-1).sp.toDp() } + + Emojifier(text = displayName) { emojiText, emojiInlineContent -> + val styledText = buildAnnotatedString { + if (!isLtr) { + if (showChevron) { + SignalSymbol(chevronGlyph, fontSize = 18.sp, color = outlineColor, baselineShift = BaselineShift(0.1f)) + append("\u00A0") + } + if (showVerified) { + appendInlineContent(VERIFIED_BADGE_ID) + append("\u00A0") + } else if (isSystemContact) { + SignalSymbol(SignalSymbols.Glyph.PERSON_CIRCLE, fontSize = 18.sp, baselineShift = BaselineShift(0.1f)) + append("\u00A0") + } + } + + append(emojiText) + + if (isLtr) { + if (showVerified) { + append("\u00A0") + appendInlineContent(VERIFIED_BADGE_ID) + } else if (isSystemContact) { + append("\u00A0") + SignalSymbol(SignalSymbols.Glyph.PERSON_CIRCLE, fontSize = 18.sp, baselineShift = BaselineShift(0.1f)) + } + if (showChevron) { + append("\u00A0") + SignalSymbol(chevronGlyph, fontSize = 18.sp, color = outlineColor, baselineShift = BaselineShift(0.1f)) + } + } + } + + val inlineContent = if (showVerified) { + emojiInlineContent + mapOf( + VERIFIED_BADGE_ID to InlineTextContent( + placeholder = Placeholder(width = 22.sp, height = 22.sp, placeholderVerticalAlign = PlaceholderVerticalAlign.TextCenter) + ) { + Image( + painter = painterResource(R.drawable.ic_official_28), + contentDescription = null, + modifier = Modifier.fillMaxSize().offset(y = badgeOffset) + ) + } + ) + } else { + emojiInlineContent + } + + Text( + text = styledText, + inlineContent = inlineContent, + style = MaterialTheme.typography.titleLarge.copy(textDirection = TextDirection.Ltr), + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center, + modifier = modifier + ) + } +} + +@DayNightPreviews +@Composable +private fun HeadlineDisplayNamePreview() = Previews.Preview { + HeadlineDisplayName( + displayName = "Katie Hall", + showVerified = false, + isSystemContact = false, + showChevron = true + ) +} + +@DayNightPreviews +@Composable +private fun HeadlineDisplayNameVerifiedPreview() = Previews.Preview { + HeadlineDisplayName( + displayName = "Katie Hall", + showVerified = true, + isSystemContact = false, + showChevron = true + ) +} + +@DayNightPreviews +@Composable +private fun HeadlineDisplayNameSystemContactPreview() = Previews.Preview { + HeadlineDisplayName( + displayName = "Katie Hall", + showVerified = false, + isSystemContact = true, + showChevron = true + ) +} + +@DayNightPreviews +@Composable +private fun HeadlineDisplayNameLongTextChevronPreview() = Previews.Preview { + HeadlineDisplayName( + displayName = "J. Jonah Jameson Jr.", + showVerified = false, + isSystemContact = false, + showChevron = true, + modifier = Modifier.width(120.dp) + ) +} + +@DayNightPreviews +@Composable +private fun HeadlineDisplayNameLongTextSystemContactPreview() = Previews.Preview { + HeadlineDisplayName( + displayName = "J. Jonah Jameson Jr.", + showVerified = false, + isSystemContact = true, + showChevron = true, + modifier = Modifier.width(120.dp) + ) +} + +@LargeFontPreviews +@Composable +private fun HeadlineDisplayNameLargeFontChevronPreview() = Previews.Preview { + HeadlineDisplayName( + displayName = "Katie Hall", + showVerified = false, + isSystemContact = false, + showChevron = true + ) +} + +@LargeFontPreviews +@Composable +private fun HeadlineDisplayNameLargeFontSystemContactPreview() = Previews.Preview { + HeadlineDisplayName( + displayName = "Katie Hall", + showVerified = true, + isSystemContact = true, + showChevron = true + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesBottomSheet.kt index c0462757cc..4a948f23b2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/PinnedMessagesBottomSheet.kt @@ -154,7 +154,7 @@ class PinnedMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() val callback = GiphyMp4ProjectionRecycler(holders) GiphyMp4PlaybackController.attach(list, callback, maxPlayback) - list.addItemDecoration(GiphyMp4ItemDecoration(callback) {}, 0) + list.addItemDecoration(GiphyMp4ItemDecoration(callback), 0) return callback } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesBottomSheet.kt index a80372b4e5..e769c82bff 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ScheduledMessagesBottomSheet.kt @@ -145,7 +145,7 @@ class ScheduledMessagesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment val callback = GiphyMp4ProjectionRecycler(holders) GiphyMp4PlaybackController.attach(list, callback, maxPlayback) - list.addItemDecoration(GiphyMp4ItemDecoration(callback) {}, 0) + list.addItemDecoration(GiphyMp4ItemDecoration(callback), 0) return callback } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt index 84f10eea9f..bf8932b3e5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/quotes/MessageQuotesBottomSheet.kt @@ -139,7 +139,7 @@ class MessageQuotesBottomSheet : FixedRoundedCornerBottomSheetDialogFragment() { val callback = GiphyMp4ProjectionRecycler(holders) GiphyMp4PlaybackController.attach(list, callback, maxPlayback) - list.addItemDecoration(GiphyMp4ItemDecoration(callback) {}, 0) + list.addItemDecoration(GiphyMp4ItemDecoration(callback), 0) return callback } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt index 39e950c9ca..5abd7caa75 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ui/edit/EditMessageHistoryDialog.kt @@ -141,7 +141,7 @@ class EditMessageHistoryDialog : FixedRoundedCornerBottomSheetDialogFragment() { val callback = GiphyMp4ProjectionRecycler(holders) GiphyMp4PlaybackController.attach(binding.editHistoryList, callback, maxPlayback) - binding.editHistoryList.addItemDecoration(GiphyMp4ItemDecoration(callback) {}, 0) + binding.editHistoryList.addItemDecoration(GiphyMp4ItemDecoration(callback), 0) return callback } 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 ffd9c74e0c..d0396d5ec8 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,8 +5,6 @@ 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 @@ -18,6 +16,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.media3.common.MediaItem import androidx.recyclerview.widget.RecyclerView import com.bumptech.glide.RequestManager +import org.signal.core.util.concurrent.SignalExecutors import org.signal.core.util.logging.Log import org.signal.core.util.toOptional import org.thoughtcrime.securesms.BindableConversationItem @@ -26,6 +25,7 @@ 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.ConversationHeaderCallbacks import org.thoughtcrime.securesms.conversation.ConversationHeaderView import org.thoughtcrime.securesms.conversation.ConversationItemDisplayMode import org.thoughtcrime.securesms.conversation.ConversationMessage @@ -48,20 +48,20 @@ import org.thoughtcrime.securesms.conversation.v2.items.V2ConversationItemMediaV import org.thoughtcrime.securesms.conversation.v2.items.V2ConversationItemTextOnlyViewHolder import org.thoughtcrime.securesms.conversation.v2.items.V2Payload import org.thoughtcrime.securesms.conversation.v2.items.bridge +import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.MessageRecord import org.thoughtcrime.securesms.databinding.V2ConversationItemMediaIncomingBinding import org.thoughtcrime.securesms.databinding.V2ConversationItemMediaOutgoingBinding import org.thoughtcrime.securesms.databinding.V2ConversationItemTextOnlyIncomingBinding import org.thoughtcrime.securesms.databinding.V2ConversationItemTextOnlyOutgoingBinding import org.thoughtcrime.securesms.giph.mp4.GiphyMp4PlaybackPolicyEnforcer -import org.thoughtcrime.securesms.groups.v2.GroupDescriptionUtil +import org.thoughtcrime.securesms.jobs.AvatarGroupsV2DownloadJob +import org.thoughtcrime.securesms.jobs.RetrieveProfileAvatarJob import org.thoughtcrime.securesms.keyvalue.SignalStore -import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.ui.about.AboutSheet import org.thoughtcrime.securesms.util.CachedInflater import org.thoughtcrime.securesms.util.Projection import org.thoughtcrime.securesms.util.ProjectionList -import org.thoughtcrime.securesms.util.SignalE164Util import org.thoughtcrime.securesms.util.adapter.mapping.MappingViewHolder import org.thoughtcrime.securesms.util.adapter.mapping.PagingMappingAdapter import java.util.Locale @@ -565,165 +565,44 @@ class ConversationAdapterV2( inner class ThreadHeaderViewHolder(itemView: View) : MappingViewHolder(itemView) { private val conversationBanner: ConversationHeaderView = itemView as ConversationHeaderView + init { + conversationBanner.callbacks = object : ConversationHeaderCallbacks { + override fun onSafetyTipsClicked(forGroup: Boolean) = clickListener.onShowSafetyTips(forGroup) + + override fun onUnverifiedNameClicked(forGroup: Boolean) = clickListener.onShowUnverifiedProfileSheet(forGroup) + + override fun onTitleClicked() { + val recipient = conversationBanner.recipientInfo?.recipient ?: return + if (recipient.isIndividual && !recipient.isSelf) { + displayDialogFragment(AboutSheet.create(recipient)) + } + } + + override fun onGroupSettingsClicked() { + val recipient = conversationBanner.recipientInfo?.recipient ?: return + context.startActivity(ConversationSettingsActivity.forGroup(context, recipient.requireGroupId())) + } + + override fun onShowGroupDescriptionClicked(groupName: String, description: String, linkifyWebLinks: Boolean) { + clickListener.onShowGroupDescriptionClicked(groupName, description, linkifyWebLinks) + } + + override fun onAvatarTapToViewClicked() { + val recipient = conversationBanner.recipientInfo?.recipient ?: return + AvatarDownloadStateCache.set(recipient, AvatarDownloadStateCache.DownloadState.IN_PROGRESS) + SignalExecutors.BOUNDED.execute { SignalDatabase.recipients.manuallyUpdateShowAvatar(recipient.id, true) } + if (recipient.isPushV2Group) { + AvatarGroupsV2DownloadJob.enqueueUnblurredAvatar(recipient.requireGroupId().requireV2()) + } else { + RetrieveProfileAvatarJob.enqueueUnblurredAvatar(recipient) + } + } + } + } + override fun bind(model: ThreadHeader) { - val (recipient, groupInfo, sharedGroups, messageRequestState) = model.recipientInfo - val isSelf = recipient.id == Recipient.self().id - - when (model.avatarDownloadState) { - AvatarDownloadStateCache.DownloadState.NONE, - AvatarDownloadStateCache.DownloadState.FINISHED -> { - conversationBanner.setAvatar(requestManager, recipient) - } - AvatarDownloadStateCache.DownloadState.IN_PROGRESS -> { - conversationBanner.showProgressBar(recipient) - } - AvatarDownloadStateCache.DownloadState.FAILED -> { - conversationBanner.showFailedAvatarDownload(recipient) - } - } - - conversationBanner.showBackgroundBubble(recipient.hasWallpaper) - 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_16, true) { - clickListener.onShowUnverifiedProfileSheet(true) - } - } else { - conversationBanner.hideUnverifiedNameSubtitle() - } - - if (groupInfo.fullMemberCount > 0 || groupInfo.pendingMemberCount > 0) { - if (groupInfo.fullMemberCount == 1 && groupInfo.isMember) { - conversationBanner.hideUnverifiedNameSubtitle() - } - setSubtitle(context, groupInfo.pendingMemberCount, groupInfo.fullMemberCount, groupInfo.membersPreview, groupInfo.isMember, 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_compact_16, null, null) - } else { - if ((recipient.profileName.toString() == recipient.getDisplayName(context)) && recipient.nickname.isEmpty && !recipient.isSystemContact) { - conversationBanner.setUnverifiedNameSubtitle(R.drawable.symbol_person_question_16, false) { - clickListener.onShowUnverifiedProfileSheet(false) - } - } else { - conversationBanner.hideUnverifiedNameSubtitle() - } - - val subtitle: String? = recipient.takeIf { it.shouldShowE164 }?.e164?.map { e164: String? -> SignalE164Util.prettyPrint(e164!!) }?.orElse(null) - if (subtitle == null || subtitle == title) { - conversationBanner.hideSubtitle() - } else { - conversationBanner.setSubtitle(subtitle, R.drawable.symbol_phone_compact_16, null, null) - } - } - - conversationBanner.hideButton() - - 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) - } - conversationBanner.setDescription(getDescription(context, sharedGroups), R.drawable.symbol_group_compact_16) - } else if (messageRequestState?.isAccepted == false && recipient.isGroup) { - conversationBanner.showWarningSubtitle() - conversationBanner.setButton(context.getString(R.string.ConversationFragment_safety_tips)) { - clickListener.onShowSafetyTips(true) - } - } else if ((recipient.isGroup && sharedGroups.isEmpty()) || isSelf) { - conversationBanner.hideWarningSubtitle() - if (TextUtils.isEmpty(groupInfo.description)) { - conversationBanner.setLinkifyDescription(false) - conversationBanner.hideDescription() - } else { - conversationBanner.setLinkifyDescription(true) - val linkifyWebLinks = messageRequestState?.isAccepted == true - conversationBanner.showDescription() - - GroupDescriptionUtil.setText( - context, - conversationBanner.description, - groupInfo.description, - linkifyWebLinks - ) { - clickListener.onShowGroupDescriptionClicked(recipient.getDisplayName(context), groupInfo.description, linkifyWebLinks) - } - } - } else { - conversationBanner.hideWarningSubtitle() - conversationBanner.setDescription(getDescription(context, sharedGroups), R.drawable.symbol_group_compact_16) - } - conversationBanner.updateOutlineBoxSize() - } - - private fun setSubtitle(context: Context, pendingMemberCount: Int, size: Int, members: List, isMember: Boolean, recipient: Recipient) { - val names = members.map { member -> member.getDisplayName(context) } - val otherMembers = if (size > 3) context.resources.getQuantityString(R.plurals.MessageRequestProfileView_other_members, size - 3, size - 3) else null - val membersSubtitle = if (isMember) { - when (names.size) { - 0 -> context.getString(R.string.MessageRequestProfileView_group_members_zero) - 1 -> context.getString(R.string.MessageRequestProfileView_group_members_one_and_you, names[0]) - 2 -> context.getString(R.string.MessageRequestProfileView_group_members_two_and_you, names[0], names[1]) - else -> context.getString(R.string.MessageRequestProfileView_group_members_other, names[0], names[1], names[2], otherMembers) - } - } else { - when (names.size) { - 0 -> context.getString(R.string.MessageRequestProfileView_group_members_zero) - 1 -> context.getString(R.string.MessageRequestProfileView_group_members_one, names[0]) - 2 -> context.getString(R.string.MessageRequestProfileView_group_members_two, names[0], names[1]) - 3 -> context.getString(R.string.MessageRequestProfileView_group_members_three, names[0], names[1], names[2]) - else -> context.getString(R.string.MessageRequestProfileView_group_members_other, names[0], names[1], names[2], otherMembers) - } - } - - if (pendingMemberCount > 0) { - val invited = context.resources.getQuantityString(R.plurals.MessageRequestProfileView_invited, pendingMemberCount, pendingMemberCount) - val subtitle = context.getString(R.string.MessageRequestProfileView_member_names_and_invited, membersSubtitle, invited) - conversationBanner.setSubtitle(subtitle, R.drawable.symbol_group_compact_16, otherMembers) { goToGroupSettings(recipient) } - } else { - conversationBanner.setSubtitle(membersSubtitle, R.drawable.symbol_group_compact_16, otherMembers) { goToGroupSettings(recipient) } - } - } - - 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) + conversationBanner.recipientInfo = model.recipientInfo + conversationBanner.avatarDownloadState = model.avatarDownloadState } } 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 87fb28e90d..0960bce452 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 @@ -687,7 +687,9 @@ class ConversationFragment : ) conversationToolbarOnScrollHelper.attach(binding.conversationItemRecycler) presentConversationTitle(viewModel.recipientSnapshot) - presentGroupConversationSubtitle(createGroupSubtitleString(viewModel.titleViewParticipantsSnapshot)) + if (viewModel.recipientSnapshot?.isGroup == true) { + presentGroupConversationSubtitle(createGroupSubtitleString(viewModel.titleViewParticipantsSnapshot)) + } presentActionBarMenu() presentStoryRing() @@ -2204,6 +2206,7 @@ class ConversationFragment : val statusBarInset = ViewCompat.getRootWindowInsets(binding.root)?.getInsets(WindowInsetsCompat.Type.systemBars())?.top ?: 0 threadHeaderMarginDecoration.toolbarMargin = statusBarInset + resources.getDimensionPixelSize(R.dimen.signal_m3_toolbar_height) + 16.dp binding.conversationItemRecycler.addItemDecoration(threadHeaderMarginDecoration) + binding.conversationItemRecycler.addItemDecoration(ConversationHeaderPositionDecoration()) conversationItemDecorations = ConversationItemDecorations(hasWallpaper = args.hasWallpaper) binding.conversationItemRecycler.addItemDecoration(conversationItemDecorations, 0) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationHeaderPositionDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationHeaderPositionDecoration.kt new file mode 100644 index 0000000000..998ffbc3ea --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationHeaderPositionDecoration.kt @@ -0,0 +1,41 @@ +/* + * Copyright 2026 Signal Messenger, LLC + * SPDX-License-Identifier: AGPL-3.0-only + */ + +package org.thoughtcrime.securesms.conversation.v2 + +import android.graphics.Canvas +import android.graphics.Rect +import androidx.core.view.children +import androidx.recyclerview.widget.RecyclerView +import org.thoughtcrime.securesms.conversation.ConversationHeaderView +import kotlin.math.min + +/** + * Adjusts the Conversation's recycler view translationY so that the conversation header + * is pinned to the top of the visible area when content is too short to + * fill the screen. + */ +class ConversationHeaderPositionDecoration : RecyclerView.ItemDecoration() { + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (parent.childCount == 0 || parent.canScrollVertically(-1) || parent.canScrollVertically(1)) { + parent.translationY = 0f + } else { + val threadHeaderView: ConversationHeaderView = parent.children + .filterIsInstance() + .firstOrNull() ?: run { + parent.translationY = 0f + return + } + + // A decorator adds the margin for the toolbar, margin is the difference of the bounds "height" and the view height + val bounds = Rect() + parent.getDecoratedBoundsWithMargins(threadHeaderView, bounds) + val toolbarMargin = bounds.bottom - bounds.top - threadHeaderView.height + + val childTop: Int = threadHeaderView.top - toolbarMargin + parent.translationY = min(0, -childTop).toFloat() + } + } +} 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 fbc9a2ab3a..b4b139f804 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 @@ -94,10 +94,12 @@ class DisabledInputView @JvmOverloads constructor( setWallpaperEnabled(recipient.hasWallpaper) setAcceptOnClickListener { - if (messageRequestState.isFewConnectionsIndividual) { + if (messageRequestState.isIndividual) { + val signalWillNever = context.getString(R.string.MessageRequestBottomView_signal_will_never) + val body = context.getString(R.string.MessageRequestBottomView_accept_request_body, signalWillNever) MaterialAlertDialogBuilder(context) .setTitle(R.string.MessageRequestBottomView_accept_request) - .setMessage(R.string.MessageRequestBottomView_review_requests_carefully) + .setMessage(SpanUtil.boldSubstring(body, signalWillNever)) .setPositiveButton(R.string.MessageRequestBottomView_accept) { _, _ -> listener?.onAcceptMessageRequestClicked() } .setNegativeButton(android.R.string.cancel, null) .show() 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 95d3d7ae33..a19563cdb8 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 @@ -5,7 +5,6 @@ package org.thoughtcrime.securesms.conversation.v2 -import android.content.res.Configuration import androidx.annotation.DrawableRes import androidx.annotation.StringRes import androidx.compose.foundation.ExperimentalFoundationApi @@ -15,32 +14,39 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer 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.pager.HorizontalPager import androidx.compose.foundation.pager.rememberPagerState import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.platform.rememberNestedScrollInteropConnection -import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.style.TextAlign -import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.core.os.bundleOf import androidx.fragment.app.FragmentManager @@ -50,7 +56,6 @@ import org.signal.core.ui.compose.Buttons import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment import org.signal.core.ui.compose.DayNightPreviews import org.signal.core.ui.compose.Previews -import org.signal.core.ui.compose.theme.SignalTheme import org.thoughtcrime.securesms.R /** @@ -83,84 +88,196 @@ class SafetyTipsBottomSheetDialog : ComposeBottomSheetDialogFragment() { } } -data class SafetyTipData( +private data class SafetyTipSummary( + @DrawableRes val icon: Int, + @StringRes val titleText: Int, + @StringRes val messageText: Int +) + +private data class SafetyTipDetail( @DrawableRes val heroImage: Int, @StringRes val titleText: Int, @StringRes val messageText: Int ) -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), - SafetyTipData(heroImage = R.drawable.safety_tip4, titleText = R.string.SafetyTips_tip4_title, messageText = R.string.SafetyTips_tip4_message) +private val summaryTips = listOf( + SafetyTipSummary(icon = R.drawable.safetytip_48_01, titleText = R.string.SafetyTips_summary_tip0_title, messageText = R.string.SafetyTips_summary_tip0_message), + SafetyTipSummary(icon = R.drawable.safetytip_48_02, titleText = R.string.SafetyTips_summary_tip1_title, messageText = R.string.SafetyTips_summary_tip1_message), + SafetyTipSummary(icon = R.drawable.safetytip_48_03, titleText = R.string.SafetyTips_summary_tip2_title, messageText = R.string.SafetyTips_summary_tip2_message) +) + +private val detailTips = listOf( + SafetyTipDetail(heroImage = R.drawable.safetytip_240_01, titleText = R.string.SafetyTips_detail_tip0_title, messageText = R.string.SafetyTips_detail_tip0_message), + SafetyTipDetail(heroImage = R.drawable.safetytip_240_02, titleText = R.string.SafetyTips_detail_tip1_title, messageText = R.string.SafetyTips_detail_tip1_message), + SafetyTipDetail(heroImage = R.drawable.safetytip_240_03, titleText = R.string.SafetyTips_detail_tip2_title, messageText = R.string.SafetyTips_detail_tip2_message), + SafetyTipDetail(heroImage = R.drawable.safetytip_240_04, titleText = R.string.SafetyTips_detail_tip3_title, messageText = R.string.SafetyTips_detail_tip3_message), + SafetyTipDetail(heroImage = R.drawable.safetytip_240_05, titleText = R.string.SafetyTips_detail_tip4_title, messageText = R.string.SafetyTips_detail_tip4_message), + SafetyTipDetail(heroImage = R.drawable.safetytip_240_06, titleText = R.string.SafetyTips_detail_tip5_title, messageText = R.string.SafetyTips_detail_tip5_message) ) -@DayNightPreviews @Composable -private fun SafetyTipsContentPreview() { - Previews.Preview { - Surface { - SafetyTipsContent() +private fun SafetyTipsContent(forGroup: Boolean = false, modifier: Modifier = Modifier) { + var showDetails by rememberSaveable { mutableStateOf(false) } + + if (showDetails) { + SafetyTipsDetailContent(modifier = modifier) + } else { + SafetyTipsSummaryContent( + onViewMore = { showDetails = true }, + modifier = modifier + ) + } +} + +@Composable +private fun SafetyTipsSummaryContent( + onViewMore: () -> Unit, + modifier: Modifier = Modifier +) { + val scrollState = rememberScrollState() + + Column( + modifier = Modifier.fillMaxWidth() + ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + BottomSheets.Handle() + } + + Column( + modifier = modifier + .fillMaxWidth() + .weight(weight = 1f, fill = false) + .verticalScroll(state = scrollState) + .padding(horizontal = 36.dp) + ) { + Text( + text = stringResource(id = R.string.SafetyTips_title), + style = MaterialTheme.typography.titleLarge, + modifier = Modifier + .padding(top = 28.dp, bottom = 34.dp) + .align(Alignment.CenterHorizontally) + ) + + summaryTips.forEach { tip -> + SafetyTipSummaryRow(tip) + } + + Spacer(Modifier.height(8.dp)) + + Buttons.LargeTonal( + onClick = onViewMore, + modifier = Modifier.fillMaxWidth() + ) { + Text(text = stringResource(id = R.string.SafetyTips_view_more)) + } + + Spacer(Modifier.height(36.dp)) + } + } +} + +@Composable +private fun SafetyTipSummaryRow(tip: SafetyTipSummary) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 40.dp), + verticalAlignment = Alignment.Top + ) { + Image( + painter = painterResource(id = tip.icon), + contentDescription = null, + modifier = Modifier.size(48.dp) + ) + + Column( + modifier = Modifier + .weight(1f) + .padding(start = 24.dp) + ) { + Text( + text = stringResource(id = tip.titleText), + style = MaterialTheme.typography.titleMedium + ) + + Text( + text = stringResource(id = tip.messageText), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 2.dp) + ) } } } @OptIn(ExperimentalFoundationApi::class) @Composable -private fun SafetyTipsContent(forGroup: Boolean = false, modifier: Modifier = Modifier) { - Box( - contentAlignment = Alignment.Center, - modifier = Modifier.fillMaxWidth() - ) { - BottomSheets.Handle() - } - - val size = remember { tips.size } - val pagerState = rememberPagerState( - pageCount = { size } - ) - val scrollState = rememberScrollState() +private fun SafetyTipsDetailContent(modifier: Modifier = Modifier) { + val size = remember { detailTips.size } + val pagerState = rememberPagerState(pageCount = { size }) + val coroutineScope = rememberCoroutineScope() Column( modifier = Modifier.fillMaxWidth() ) { + Box( + contentAlignment = Alignment.Center, + modifier = Modifier.fillMaxWidth() + ) { + BottomSheets.Handle() + } + Column( modifier = modifier .fillMaxWidth() .weight(weight = 1f, fill = false) - .padding(top = 22.dp) - .verticalScroll(state = scrollState) ) { - Text( - text = stringResource(id = R.string.SafetyTips_title), - style = MaterialTheme.typography.headlineMedium.copy(textAlign = TextAlign.Center), - modifier = Modifier - .padding(start = 24.dp, end = 24.dp, bottom = 4.dp, top = 26.dp) - .fillMaxWidth() - ) - - Text( - text = if (forGroup) stringResource(id = R.string.SafetyTips_subtitle_group) else stringResource(id = R.string.SafetyTips_subtitle_individual), - style = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onSurfaceVariant), - modifier = Modifier - .padding(start = 36.dp, end = 36.dp) - .fillMaxWidth() - ) - HorizontalPager( state = pagerState, beyondViewportPageCount = size, - modifier = Modifier.padding(top = 24.dp) - ) { - SafetyTip(tips[it]) + verticalAlignment = Alignment.Top, + modifier = Modifier.weight(weight = 1f, fill = false) + ) { page -> + SafetyTipDetailPage(detailTips[page]) + } + } + + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier + .fillMaxWidth() + .padding(start = 24.dp, end = 24.dp, bottom = 36.dp, top = 16.dp) + ) { + if (pagerState.currentPage > 0) { + IconButton( + onClick = { + coroutineScope.launch { + pagerState.animateScrollToPage(pagerState.currentPage - 1) + } + }, + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + modifier = Modifier + .size(48.dp) + .clip(CircleShape) + ) { + Icon( + imageVector = ImageVector.vectorResource(R.drawable.symbol_arrow_right_24), + contentDescription = stringResource(R.string.SafetyTips_previous_tip), + modifier = Modifier.graphicsLayer(scaleX = -1f) + ) + } + } else { + Spacer(Modifier.size(48.dp)) } Row( - Modifier - .fillMaxWidth() - .padding(top = 20.dp), horizontalArrangement = Arrangement.Center ) { repeat(pagerState.pageCount) { iteration -> @@ -178,103 +295,92 @@ private fun SafetyTipsContent(forGroup: Boolean = false, modifier: Modifier = Mo ) } } - } - Surface( - shadowElevation = if (scrollState.canScrollForward) 8.dp else 0.dp, - modifier = Modifier.fillMaxWidth(), - color = SignalTheme.colors.colorSurface1, - contentColor = MaterialTheme.colorScheme.onSurface - ) { - Row( - horizontalArrangement = Arrangement.SpaceBetween, - modifier = Modifier - .padding(start = 24.dp, end = 24.dp, bottom = 36.dp, top = 24.dp) - .fillMaxWidth() - ) { - val coroutineScope = rememberCoroutineScope() - - TextButton( - onClick = { - coroutineScope.launch { - pagerState.animateScrollToPage(pagerState.currentPage - 1) - } - }, - enabled = pagerState.currentPage > 0, - modifier = Modifier - ) { - Text(text = stringResource(id = R.string.SafetyTips_previous_tip)) - } - - Buttons.LargeTonal( + if (pagerState.currentPage < pagerState.pageCount - 1) { + IconButton( onClick = { coroutineScope.launch { pagerState.animateScrollToPage(pagerState.currentPage + 1) } }, - enabled = pagerState.currentPage + 1 < pagerState.pageCount + colors = IconButtonDefaults.iconButtonColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + contentColor = MaterialTheme.colorScheme.onPrimaryContainer + ), + modifier = Modifier + .size(48.dp) + .clip(CircleShape) ) { - Text(text = stringResource(id = R.string.SafetyTips_next_tip)) + Icon( + imageVector = ImageVector.vectorResource(R.drawable.symbol_arrow_right_24), + contentDescription = stringResource(R.string.SafetyTips_next_tip) + ) } + } else { + Spacer(Modifier.size(48.dp)) } } } } -@Preview(name = "Light Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_NO) -@Preview(name = "Dark Theme", group = "screen", uiMode = Configuration.UI_MODE_NIGHT_YES) @Composable -private fun SafetyTipPreview() { - Previews.Preview { - Surface { - SafetyTip(tips[0]) - } - } -} - -@Composable -private fun SafetyTip(safetyTip: SafetyTipData) { - Surface( - shape = RoundedCornerShape(18.dp), - color = colorResource(id = R.color.safety_tip_background), - contentColor = MaterialTheme.colorScheme.onSurface, +private fun SafetyTipDetailPage(tip: SafetyTipDetail) { + Column( modifier = Modifier .fillMaxWidth() - .padding(start = 24.dp, end = 24.dp) + .verticalScroll(rememberScrollState()) + .padding(horizontal = 36.dp) ) { - Column( - horizontalAlignment = Alignment.CenterHorizontally, + Image( + painter = painterResource(id = tip.heroImage), + contentDescription = null, modifier = Modifier .fillMaxWidth() - ) { - Surface( - shape = RoundedCornerShape(12.dp), - color = colorResource(id = R.color.safety_tip_image_background), - modifier = Modifier - .padding(12.dp) - .fillMaxWidth() - ) { - Image( - painter = painterResource(id = safetyTip.heroImage), - contentDescription = null, - modifier = Modifier - .padding(16.dp) - ) - } + .padding(top = 16.dp, bottom = 16.dp) + .height(160.dp) + .align(Alignment.CenterHorizontally) + ) - Text( - text = stringResource(id = safetyTip.titleText), - style = MaterialTheme.typography.titleMedium, - modifier = Modifier - .padding(start = 24.dp, end = 24.dp, top = 8.dp, bottom = 4.dp) - ) + Text( + text = stringResource(id = tip.titleText), + style = MaterialTheme.typography.titleMedium + ) - Text( - text = stringResource(id = safetyTip.messageText), - style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurfaceVariant, textAlign = TextAlign.Center), - modifier = Modifier - .padding(start = 24.dp, end = 24.dp, bottom = 24.dp) - ) + Text( + text = stringResource(id = tip.messageText), + style = MaterialTheme.typography.bodyLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + modifier = Modifier.padding(top = 4.dp) + ) + } +} + +@DayNightPreviews +@Composable +private fun SafetyTipsSummaryPreview() { + Previews.Preview { + Surface { + SafetyTipsSummaryContent(onViewMore = {}) + } + } +} + +@DayNightPreviews +@Composable +private fun SafetyTipsDetailPreview() { + Previews.Preview { + Surface { + SafetyTipsDetailContent() + } + } +} + +@DayNightPreviews +@Composable +private fun SafetyTipDetailPagePreview() { + Previews.Preview { + Surface { + SafetyTipDetailPage(detailTips[0]) } } } 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 index d582c686d1..02afdc5f2f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/UnverifiedProfileNameBottomSheet.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/UnverifiedProfileNameBottomSheet.kt @@ -1,17 +1,12 @@ 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 @@ -37,6 +32,7 @@ import org.signal.core.ui.compose.ComposeBottomSheetDialogFragment import org.signal.core.ui.compose.DayNightPreviews import org.signal.core.ui.compose.Previews import org.signal.core.ui.compose.horizontalGutters +import org.signal.core.ui.compose.theme.SignalTheme import org.thoughtcrime.securesms.R /** @@ -70,7 +66,7 @@ class UnverifiedProfileNameBottomSheet : ComposeBottomSheetDialogFragment() { } @Composable -private fun ProfileNameSheet(forGroup: Boolean = true) { +private fun ProfileNameSheet(forGroup: Boolean = false) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier @@ -82,13 +78,13 @@ private fun ProfileNameSheet(forGroup: Boolean = true) { val (imageVector, placeholder, text) = if (forGroup) { Triple( - R.drawable.symbol_group_question_55, + R.drawable.symbol_group_questionmark_bold_40, 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, + R.drawable.symbol_person_questionmark_bold_40, stringResource(R.string.ConversationFragment_profile_names), stringResource(id = R.string.ProfileNameBottomSheet__profile_names_on_signal, stringResource(R.string.ConversationFragment_profile_names)) ) @@ -97,12 +93,16 @@ private fun ProfileNameSheet(forGroup: Boolean = true) { Icon( imageVector = ImageVector.vectorResource(imageVector), contentDescription = null, + tint = SignalTheme.colors.colorOnWarning, modifier = Modifier - .padding(top = 38.dp, bottom = 24.dp) - .size(height = 56.dp, width = 72.dp) + .padding(top = 30.dp, bottom = 20.dp) + .clip(RoundedCornerShape(50)) + .background(SignalTheme.colors.colorWarning) + .padding(horizontal = 18.dp, vertical = 8.dp) + .size(32.dp) ) - val annotatedText = remember { + val annotatedText = remember(text, placeholder) { buildAnnotatedString { val start = text.indexOf(placeholder) withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { @@ -114,43 +114,41 @@ private fun ProfileNameSheet(forGroup: Boolean = true) { Text( text = annotatedText, + style = MaterialTheme.typography.bodyLarge, modifier = Modifier.padding(bottom = 20.dp) ) + BulletRow(stringResource(R.string.ProfileNameBottomSheet__signal_cant_verify)) + BulletRow(stringResource(R.string.ProfileNameBottomSheet__signal_will_never_contact)) + if (forGroup) { - InfoRow(stringResource(R.string.ProfileNameBottomSheet__be_cautious_of_groups)) - InfoRow(stringResource(R.string.ProfileNameBottomSheet__profile_names_in_groups)) + BulletRow(stringResource(R.string.ProfileNameBottomSheet__be_cautious_of_groups)) } else { - InfoRow(stringResource(R.string.ProfileNameBottomSheet__profile_names_arent_verified)) - InfoRow(stringResource(R.string.ProfileNameBottomSheet__be_cautious_of_accounts)) + BulletRow(stringResource(R.string.ProfileNameBottomSheet__be_cautious_of_accounts)) } - InfoRow(stringResource(R.string.ProfileNameBottomSheet__dont_share_personal)) + BulletRow(stringResource(R.string.ProfileNameBottomSheet__dont_share_personal)) Spacer(Modifier.size(55.dp)) } } @Composable -fun InfoRow(text: String) { +private fun BulletRow(text: String) { Row( modifier = Modifier - .height(IntrinsicSize.Min) .fillMaxWidth() - .padding(start = 16.dp, bottom = 12.dp) + .padding(bottom = 12.dp), + verticalAlignment = Alignment.Top ) { - Box( - modifier = Modifier - .width(4.dp) - .padding(vertical = 5.dp) - .fillMaxHeight() - .clip(RoundedCornerShape(10.dp)) - .background(color = MaterialTheme.colorScheme.outline.copy(.4f)) + Text( + text = "\u2022", + style = MaterialTheme.typography.bodyLarge, + modifier = Modifier.padding(end = 8.dp) ) Text( text = text, - modifier = Modifier.padding(start = 12.dp), style = MaterialTheme.typography.bodyLarge ) } @@ -163,3 +161,11 @@ private fun ProfileNameSheetPreview() { ProfileNameSheet() } } + +@DayNightPreviews +@Composable +private fun ProfileNameSheetGroupPreview() { + Previews.BottomSheetContentPreview { + ProfileNameSheet(forGroup = true) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/AvatarDownloadStateCache.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/AvatarDownloadStateCache.kt index e6f1144233..e6de0954bc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/AvatarDownloadStateCache.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/data/AvatarDownloadStateCache.kt @@ -28,7 +28,12 @@ object AvatarDownloadStateCache { @JvmStatic fun getDownloadState(recipient: Recipient): DownloadState { - return cache[recipient.id]?.value ?: DownloadState.NONE + return getDownloadState(recipient.id) + } + + @JvmStatic + fun getDownloadState(recipientId: RecipientId): DownloadState { + return cache[recipientId]?.value ?: DownloadState.NONE } @JvmStatic diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java index df520c385e..34232878d1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java +++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java @@ -310,7 +310,8 @@ public abstract class MessageRecord extends DisplayRecord { } else if (isReportedSpam()) { return staticUpdateDescription(context.getString(R.string.MessageRecord_reported_as_spam), Glyph.SPAM); } else if (isMessageRequestAccepted()) { - return staticUpdateDescription(context.getString(R.string.MessageRecord_you_accepted_the_message_request), Glyph.THREAD); + return isGroupV2() ? staticUpdateDescription(context.getString(R.string.MessageRecord_you_accepted_the_group_request), Glyph.THREAD) + : fromRecipient(getToRecipient(), r -> context.getString(R.string.MessageRecord_you_accepted_s_message_request, r.getDisplayName(context)), Glyph.THREAD); } else if (isBlocked()) { return staticUpdateDescription(context.getString(isGroupV2() ? R.string.MessageRecord_you_blocked_this_group : R.string.MessageRecord_you_blocked_this_person), Glyph.BLOCK); } else if (isUnblocked()) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/fonts/SignalSymbols.kt b/app/src/main/java/org/thoughtcrime/securesms/fonts/SignalSymbols.kt index d5b73c9c49..76940def22 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/fonts/SignalSymbols.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/fonts/SignalSymbols.kt @@ -12,13 +12,18 @@ import android.text.TextPaint import android.text.style.MetricAffectingSpan import androidx.annotation.ColorRes import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.style.BaselineShift import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.TextUnit import androidx.core.content.ContextCompat +import androidx.core.text.buildSpannedString +import org.signal.core.util.BidiUtil import org.thoughtcrime.securesms.util.SpanUtil import org.thoughtcrime.securesms.util.ViewUtil @@ -162,6 +167,7 @@ object SignalSymbols { PERSON_X('\uE060'), PERSON_PLUS('\uE061'), PERSON_MINUS('\uE062'), + PERSON_QUESTION('\uE06A'), PHONE('\uE063'), PHONE_FILL('\uE064'), PHOTO('\uE065'), @@ -192,7 +198,6 @@ object SignalSymbols { X('\u00D7'), X_CIRCLE('\uE1EE'), X_SQUARE('\u2327'), - REFRESH('\uE000'), ACTIVATE_PAYMENTS('\uE000'), CALENDAR('\uE0A2') @@ -235,7 +240,7 @@ object SignalSymbols { glyphEndWeight: Weight = Weight.REGULAR, glyphStartSizeSp: Int = 16, glyphEndSizeSp: Int = 16 - ): AnnotatedString { + ): CharSequence { val isLtr = ViewUtil.isLtr(context) val leftGlyph = if (isLtr) glyphStart else glyphEnd val leftGlyphWeight = if (isLtr) glyphStartWeight else glyphEndWeight @@ -244,7 +249,7 @@ object SignalSymbols { val rightGlyphWeight = if (isLtr) glyphEndWeight else glyphStartWeight val rightGlyphSizeSp = if (isLtr) glyphEndSizeSp else glyphStartSizeSp - return buildAnnotatedString { + return buildSpannedString { if (leftGlyph != null) { val symbol = SpanUtil.ofSize( getSpannedString(context, leftGlyphWeight, leftGlyph), @@ -266,16 +271,60 @@ object SignalSymbols { } @Composable - fun AnnotatedString.Builder.SignalSymbol(weight: Weight, glyph: Glyph) { + fun AnnotatedString.Builder.SignalSymbol( + glyph: Glyph, + weight: Weight = Weight.REGULAR, + fontSize: TextUnit = TextUnit.Unspecified, + color: Color = Color.Unspecified, + baselineShift: BaselineShift? = null + ) { withStyle( SpanStyle( - fontFamily = FontFamily(getTypeface(LocalContext.current, weight)) + fontFamily = FontFamily(getTypeface(LocalContext.current, weight)), + fontSize = fontSize, + color = color, + baselineShift = baselineShift ) ) { append(glyph.unicode.toString()) } } + @Composable + fun buildSignalSymbolAnnotatedString( + glyphStart: Glyph? = null, + glyphEnd: Glyph? = null, + glyphStartWeight: Weight = Weight.REGULAR, + glyphEndWeight: Weight = Weight.REGULAR, + glyphStartSize: TextUnit = TextUnit.Unspecified, + glyphEndSize: TextUnit = TextUnit.Unspecified, + content: @Composable AnnotatedString.Builder.() -> Unit + ): AnnotatedString { + val context = LocalContext.current + + return buildAnnotatedString { + if (glyphStart != null) { + append(BidiUtil.BidiCodepoint.LRI) + withStyle(SpanStyle(fontFamily = FontFamily(getTypeface(context, glyphStartWeight)), fontSize = glyphStartSize)) { + append(glyphStart.unicode.toString()) + } + append(BidiUtil.BidiCodepoint.PDI) + append(" ") + } + + content() + + if (glyphEnd != null) { + append(" ") + append(BidiUtil.BidiCodepoint.LRI) + withStyle(SpanStyle(fontFamily = FontFamily(getTypeface(context, glyphEndWeight)), fontSize = glyphEndSize)) { + append(glyphEnd.unicode.toString()) + } + append(BidiUtil.BidiCodepoint.PDI) + } + } + } + @Composable fun signalSymbolText( text: String, @@ -283,19 +332,12 @@ object SignalSymbols { glyphEnd: Glyph? = null, glyphStartWeight: Weight = Weight.REGULAR, glyphEndWeight: Weight = Weight.REGULAR, - glyphStartSizeSp: Int = 16, - glyphEndSizeSp: Int = 16 + glyphStartSize: TextUnit = TextUnit.Unspecified, + glyphEndSize: TextUnit = TextUnit.Unspecified ): AnnotatedString { - return getSignalSymbolText( - context = LocalContext.current, - text = text, - glyphStart = glyphStart, - glyphEnd = glyphEnd, - glyphStartWeight = glyphStartWeight, - glyphEndWeight = glyphEndWeight, - glyphStartSizeSp = glyphStartSizeSp, - glyphEndSizeSp = glyphEndSizeSp - ) + return buildSignalSymbolAnnotatedString(glyphStart, glyphEnd, glyphStartWeight, glyphEndWeight, glyphStartSize, glyphEndSize) { + append(text) + } } private fun getTypeface(context: Context, weight: Weight): Typeface { diff --git a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt index 483cea1609..178212a1e6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/giph/mp4/GiphyMp4ItemDecoration.kt @@ -1,50 +1,18 @@ package org.thoughtcrime.securesms.giph.mp4 import android.graphics.Canvas -import android.graphics.Rect import androidx.core.view.children import androidx.recyclerview.widget.RecyclerView -import org.thoughtcrime.securesms.conversation.ConversationHeaderView -import kotlin.math.min /** * Decoration that will make the video display params update on each recycler redraw. */ class GiphyMp4ItemDecoration( - private val callback: GiphyMp4PlaybackController.Callback, - private val onRecyclerVerticalTranslationSet: ((Float) -> Unit)? = null + private val callback: GiphyMp4PlaybackController.Callback ) : RecyclerView.ItemDecoration() { override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { - setParentRecyclerTranslationY(parent) - parent.children.map { parent.getChildViewHolder(it) }.filterIsInstance(GiphyMp4Playable::class.java).forEach { callback.updateVideoDisplayPositionAndSize(parent, it) } } - - private fun setParentRecyclerTranslationY(parent: RecyclerView) { - if (parent.childCount == 0 || parent.canScrollVertically(-1) || parent.canScrollVertically(1)) { - parent.translationY = 0f - onRecyclerVerticalTranslationSet?.invoke(parent.translationY) - } else { - val threadHeaderView: ConversationHeaderView? = parent.children - .filterIsInstance() - .firstOrNull() - - if (threadHeaderView == null) { - parent.translationY = 0f - onRecyclerVerticalTranslationSet?.invoke(parent.translationY) - return - } - - // A decorator adds the margin for the toolbar, margin is difference of the bounds "height" and the view height - val bounds = Rect() - parent.getDecoratedBoundsWithMargins(threadHeaderView, bounds) - val toolbarMargin = bounds.bottom - bounds.top - threadHeaderView.height - - val childTop: Int = threadHeaderView.top - toolbarMargin - parent.translationY = min(0, -childTop).toFloat() - onRecyclerVerticalTranslationSet?.invoke(parent.translationY) - } - } } 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 7566ee4e12..4f98cc6414 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestRepository.java @@ -144,11 +144,8 @@ 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 && sharedGroups.size() < MIN_GROUPS_THRESHOLD) { - return new MessageRequestState(MessageRequestState.State.INDIVIDUAL_FEW_CONNECTIONS, reportedAsSpam); - } else if (hiddenState == Recipient.HiddenState.NOT_HIDDEN) { + 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 c5ddd33ad4..3d02a46cf3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestState.kt @@ -19,8 +19,8 @@ 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 isIndividual: Boolean + get() = state == State.INDIVIDUAL || state == State.INDIVIDUAL_HIDDEN val isGroupV2Add: Boolean get() = state == State.GROUP_V2_ADD @@ -56,9 +56,6 @@ 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 1d2aa40ee5..75aaceb488 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/messagerequests/MessageRequestsBottomView.kt @@ -5,14 +5,18 @@ import android.content.res.ColorStateList import android.text.Html import android.util.AttributeSet import android.view.View +import android.widget.TextView import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.text.HtmlCompat import com.google.android.material.button.MaterialButton +import org.signal.core.util.dp import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.fonts.SignalSymbols import org.thoughtcrime.securesms.messagerequests.MessageRequestBarColorTheme.Companion.resolveTheme import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.util.CommunicationActions import org.thoughtcrime.securesms.util.Debouncer +import org.thoughtcrime.securesms.util.padding import org.thoughtcrime.securesms.util.views.LearnMoreTextView import org.thoughtcrime.securesms.util.visible @@ -22,6 +26,7 @@ import org.thoughtcrime.securesms.util.visible class MessageRequestsBottomView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyleAttr: Int = 0) : ConstraintLayout(context, attrs, defStyleAttr) { private val showProgressDebouncer = Debouncer(250) + private val title: TextView private val question: LearnMoreTextView private val accept: MaterialButton private val block: MaterialButton @@ -34,6 +39,7 @@ class MessageRequestsBottomView @JvmOverloads constructor(context: Context, attr init { inflate(context, R.layout.message_request_bottom_bar, this) + title = findViewById(R.id.message_request_title) question = findViewById(R.id.message_request_question) accept = findViewById(R.id.message_request_accept) block = findViewById(R.id.message_request_block) @@ -51,6 +57,7 @@ class MessageRequestsBottomView @JvmOverloads constructor(context: Context, attr question.setOnLinkClickListener(null) updateButtonVisibility(messageRequestState) + updateTitleVisibility(messageRequestState) when (messageRequestState.state) { MessageRequestState.State.INDIVIDUAL_BLOCKED -> { @@ -91,25 +98,8 @@ class MessageRequestsBottomView @JvmOverloads constructor(context: Context, attr } 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, - bold(recipient.getShortDisplayName(context)) - ), - 0 - ) - accept.setText(R.string.MessageRequestBottomView_accept) - } - MessageRequestState.State.INDIVIDUAL_HIDDEN -> { - question.text = HtmlCompat.fromHtml( - context.getString( - R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_you_removed_them_before, - bold(recipient.getShortDisplayName(context)) - ), - 0 - ) + question.setText(R.string.MessageRequestBottomView_do_you_want_to_let_s_message_you_they_wont_know_youve_seen_their_messages_until_you_accept) accept.setText(R.string.MessageRequestBottomView_accept) } @@ -126,6 +116,22 @@ class MessageRequestsBottomView @JvmOverloads constructor(context: Context, attr report.visible = !messageState.reportedAsSpam } + private fun updateTitleVisibility(messageState: MessageRequestState) { + title.visible = !messageState.isBlocked + if (title.visible) { + title.text = SignalSymbols.getSignalSymbolText( + context = context, + text = context.getString(R.string.AboutSheet__review_requests_carefully), + glyphStart = SignalSymbols.Glyph.ERROR_TRIANGLE, + glyphStartWeight = SignalSymbols.Weight.REGULAR, + glyphStartSizeSp = 14 + ) + question.padding(top = 0) + } else { + question.padding(top = 16.dp) + } + } + fun showBusy() { showProgressDebouncer.publish { busyIndicator.visibility = VISIBLE } buttonBar.visibility = INVISIBLE 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 d540b665d1..89c2f63169 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 @@ -238,6 +238,16 @@ private fun Content( modifier = Modifier.fillMaxWidth() ) + 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.memberLabel != null || model.canEditMemberLabel)) { MemberLabelRow( memberLabel = model.memberLabel, @@ -267,16 +277,6 @@ 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 = ImageVector.vectorResource(id = R.drawable.symbol_safety_number_24), diff --git a/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/transferaccount/TransferScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/transferaccount/TransferScreen.kt index 55ca5e5fa0..43abcb3a73 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/transferaccount/TransferScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/registration/olddevice/transferaccount/TransferScreen.kt @@ -102,7 +102,7 @@ fun TransferAccountScreen( Text( text = buildAnnotatedString { - SignalSymbol(SignalSymbols.Weight.REGULAR, SignalSymbols.Glyph.LOCK) + SignalSymbol(SignalSymbols.Glyph.LOCK) append(" ") append(stringResource(id = R.string.TransferAccount_messages_e2e)) }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/starred/StarredMessagesActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/starred/StarredMessagesActivity.kt index 54f7c002f6..4508f75656 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/starred/StarredMessagesActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/starred/StarredMessagesActivity.kt @@ -296,7 +296,7 @@ private fun initializeGiphyMp4(lifecycle: Lifecycle, videoContainer: ViewGroup, val callback = GiphyMp4ProjectionRecycler(holders) GiphyMp4PlaybackController.attach(list, callback, maxPlayback) - list.addItemDecoration(GiphyMp4ItemDecoration(callback) {}, 0) + list.addItemDecoration(GiphyMp4ItemDecoration(callback), 0) } @Composable diff --git a/app/src/main/res/drawable/safetytip_240_01.xml b/app/src/main/res/drawable/safetytip_240_01.xml new file mode 100644 index 0000000000..5c0f1938cc --- /dev/null +++ b/app/src/main/res/drawable/safetytip_240_01.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/safetytip_240_02.xml b/app/src/main/res/drawable/safetytip_240_02.xml new file mode 100644 index 0000000000..c01eb68bef --- /dev/null +++ b/app/src/main/res/drawable/safetytip_240_02.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/safetytip_240_03.xml b/app/src/main/res/drawable/safetytip_240_03.xml new file mode 100644 index 0000000000..a3782bd1ef --- /dev/null +++ b/app/src/main/res/drawable/safetytip_240_03.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/safetytip_240_04.xml b/app/src/main/res/drawable/safetytip_240_04.xml new file mode 100644 index 0000000000..5ea0b5f19c --- /dev/null +++ b/app/src/main/res/drawable/safetytip_240_04.xml @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/safetytip_240_05.xml b/app/src/main/res/drawable/safetytip_240_05.xml new file mode 100644 index 0000000000..ef7f3fcb48 --- /dev/null +++ b/app/src/main/res/drawable/safetytip_240_05.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/safetytip_240_06.xml b/app/src/main/res/drawable/safetytip_240_06.xml new file mode 100644 index 0000000000..e8f43bc08f --- /dev/null +++ b/app/src/main/res/drawable/safetytip_240_06.xml @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/safetytip_48_01.xml b/app/src/main/res/drawable/safetytip_48_01.xml new file mode 100644 index 0000000000..e3d818ae95 --- /dev/null +++ b/app/src/main/res/drawable/safetytip_48_01.xml @@ -0,0 +1,20 @@ + + + + + + diff --git a/app/src/main/res/drawable/safetytip_48_02.xml b/app/src/main/res/drawable/safetytip_48_02.xml new file mode 100644 index 0000000000..a11892b2e4 --- /dev/null +++ b/app/src/main/res/drawable/safetytip_48_02.xml @@ -0,0 +1,33 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/safetytip_48_03.xml b/app/src/main/res/drawable/safetytip_48_03.xml new file mode 100644 index 0000000000..28cac59bf4 --- /dev/null +++ b/app/src/main/res/drawable/safetytip_48_03.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/app/src/main/res/drawable/symbol_group_questionmark_bold_40.xml b/app/src/main/res/drawable/symbol_group_questionmark_bold_40.xml new file mode 100644 index 0000000000..feec17edfc --- /dev/null +++ b/app/src/main/res/drawable/symbol_group_questionmark_bold_40.xml @@ -0,0 +1,30 @@ + + + + + + + + diff --git a/app/src/main/res/drawable/symbol_person_questionmark_bold_40.xml b/app/src/main/res/drawable/symbol_person_questionmark_bold_40.xml new file mode 100644 index 0000000000..41a4ae4550 --- /dev/null +++ b/app/src/main/res/drawable/symbol_person_questionmark_bold_40.xml @@ -0,0 +1,22 @@ + + + + + + diff --git a/app/src/main/res/layout/conversation_header_view.xml b/app/src/main/res/layout/conversation_header_view.xml deleted file mode 100644 index 1d7d66dda8..0000000000 --- a/app/src/main/res/layout/conversation_header_view.xml +++ /dev/null @@ -1,251 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/conversation_item_thread_header.xml b/app/src/main/res/layout/conversation_item_thread_header.xml index 113d533c22..50c3e6bc54 100644 --- a/app/src/main/res/layout/conversation_item_thread_header.xml +++ b/app/src/main/res/layout/conversation_item_thread_header.xml @@ -1,10 +1,8 @@ \ No newline at end of file diff --git a/app/src/main/res/layout/message_request_bottom_bar.xml b/app/src/main/res/layout/message_request_bottom_bar.xml index a1ef70a471..6f3ba7e1ee 100644 --- a/app/src/main/res/layout/message_request_bottom_bar.xml +++ b/app/src/main/res/layout/message_request_bottom_bar.xml @@ -5,6 +5,24 @@ tools:parentTag="androidx.constraintlayout.widget.ConstraintLayout" tools:viewBindingIgnore="true"> + + @color/signal_dark_colorSecondaryContainer @color/signal_dark_colorSecondary + + #FFEB977D diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index f26c60b13e..2b6a4b50e7 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -245,10 +245,6 @@ 64dp - 24dp - 32dp - 308dp - 40dp 150dp diff --git a/app/src/main/res/values/light_colors.xml b/app/src/main/res/values/light_colors.xml index 2eb00a503c..da94f0e8f0 100644 --- a/app/src/main/res/values/light_colors.xml +++ b/app/src/main/res/values/light_colors.xml @@ -209,4 +209,6 @@ @color/core_white #99F2F5F9 + + #FFB44828 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4b61c4f177..8fd9865792 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -716,10 +716,17 @@ 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 - - %s are not verified + + Name not verified + + Official chat + + The only official chat from Signal. Keep up to date with news and release notes. + + + %1$d Member + %1$d Members + Profile names @@ -745,43 +752,61 @@ Safety Tips - - Be careful when accepting message requests from people you don’t know. Watch out for: - - Review this request carefully. None of your contacts or people you chat with are in this group. Here are a few things to watch out for: - + + View more + 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 - - If someone you don’t know messages about cryptocurrency (like Bitcoin) or a financial opportunity, be careful—it’s likely spam. - - Vague or irrelevant messages - - Spammers often start with a simple message like “Hi” to draw you in. If you respond they may engage you further. - - Messages with links - - Be careful of messages from people you don’t know that have links to websites. Never visit links from people you don’t trust. - - Fake businesses and institutions - - Be careful of businesses or government agencies contacting you. Messages involving tax agencies, couriers, and more can be spam. + + + Don\’t respond to chats from Signal + + Signal will never message you for your registration code, PIN, or recovery key. Never respond to a chat pretending to be Signal. + + Review names and photos + + Look out for the \"Name not verified\" notice. Everyone sets their own profile name in Signal. + + Look out for scams + + Avoid vague messages that try to get you to reply. Be aware of financial tips and suspicious web links. + + + Don\’t respond to chats from Signal + + Signal will never message you for your registration code, PIN, or recover key. Don\’t reply to chats pretending to be Signal or Signal Support. Bad actors set up fake chats to try to take over your account. + + Review names and photos + + Look out for the \"Name not verified\" notice. Everyone sets their own profile name and photo in Signal. If you\’re unsure who a new request is from it\’s safer to ignore it. + + Vague or irrelevant messages + + Spammers often start with a simple message like \"Hi\" to draw you in. If you respond they may engage you further. + + Messages with web links + + Be careful of messages from people you don\’t know that have links to websites. Never visit links from people you don\’t trust. + + Crypto or money scams + + If someone you don\’t know messages about cryptocurrency (like Bitcoin) or a financial opportunity, be careful—it\’s likely spam. + + Fake businesses + + 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 + + Signal can\’t verify names and photos + + Signal will never contact you for your registration code, PIN, or recovery key Be cautious of accounts that impersonate others - Don’t share personal information with people you don’t know + Don\’t share personal information with people you don\’t know %1$s are chosen by members of the group. @@ -2221,8 +2246,10 @@ Payment Reported as spam - - You accepted the message request + + You accepted %1$s\'s message request + + You accepted the group request You blocked this person @@ -2246,7 +2273,7 @@ Unblock Let %1$s message you and share your name and photo with them? You have removed this person in the past. - Let %1$s message you and share your name and photo with them? They won\'t know you\'ve seen their message until you accept. + Let this person message you and share your name and photo with them? They won\'t know you\'ve seen their message until you accept. Let %1$s message you and share your name and photo with them? You won\'t receive any messages until you unblock them. @@ -2308,11 +2335,15 @@ %1$d others - Report… + Report Accept request? - Review requests carefully. Profile names are chosen by their account owner and aren’t verified. + Review requests carefully. Profile names are chosen by their account owner and aren\’t verified. + + Only accept requests from people you trust. %1$s message you for a registration code, PIN, or recovery key. + + Signal will never Join group? @@ -3702,7 +3733,7 @@ You have removed this person, messaging them again will add them back to your list. - Options + Block or Report… Update @@ -6849,10 +6880,6 @@ 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 diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/theme/ExtendedColors.kt b/core/ui/src/main/java/org/signal/core/ui/compose/theme/ExtendedColors.kt index a7c5677813..5e939f628c 100644 --- a/core/ui/src/main/java/org/signal/core/ui/compose/theme/ExtendedColors.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/theme/ExtendedColors.kt @@ -27,7 +27,9 @@ data class ExtendedColors( val colorTransparentInverse4: Color, val colorTransparentInverse5: Color, val colorNeutralInverse: Color, - val colorNeutralVariantInverse: Color + val colorNeutralVariantInverse: Color, + val colorWarning: Color, + val colorOnWarning: Color ) val LocalExtendedColors = staticCompositionLocalOf { @@ -53,6 +55,8 @@ val LocalExtendedColors = staticCompositionLocalOf { colorTransparentInverse4 = Color.Unspecified, colorTransparentInverse5 = Color.Unspecified, colorNeutralInverse = Color.Unspecified, - colorNeutralVariantInverse = Color.Unspecified + colorNeutralVariantInverse = Color.Unspecified, + colorWarning = Color.Unspecified, + colorOnWarning = Color.Unspecified ) } diff --git a/core/ui/src/main/java/org/signal/core/ui/compose/theme/SignalTheme.kt b/core/ui/src/main/java/org/signal/core/ui/compose/theme/SignalTheme.kt index 97d2b84368..2b994a8dd5 100644 --- a/core/ui/src/main/java/org/signal/core/ui/compose/theme/SignalTheme.kt +++ b/core/ui/src/main/java/org/signal/core/ui/compose/theme/SignalTheme.kt @@ -123,7 +123,9 @@ private val lightExtendedColors = ExtendedColors( colorTransparentInverse4 = Color(0xB8000000), colorTransparentInverse5 = Color(0xE0000000), colorNeutralInverse = Color(0xFF121212), - colorNeutralVariantInverse = Color(0xFF5C5C5C) + colorNeutralVariantInverse = Color(0xFF5C5C5C), + colorWarning = Color(0x1FB44828), + colorOnWarning = Color(0xFFB44828) ) private val darkExtendedColors = ExtendedColors( @@ -148,7 +150,9 @@ private val darkExtendedColors = ExtendedColors( colorTransparentInverse4 = Color(0xB8000000), colorTransparentInverse5 = Color(0xF5000000), colorNeutralInverse = Color(0xE0FFFFFF), - colorNeutralVariantInverse = Color(0xA3FFFFFF) + colorNeutralVariantInverse = Color(0xA3FFFFFF), + colorWarning = Color(0x1FEB977D), + colorOnWarning = Color(0xFFEB977D) ) private val darkColorScheme = darkColorScheme( diff --git a/core/util-jvm/src/main/java/org/signal/core/util/BidiUtil.kt b/core/util-jvm/src/main/java/org/signal/core/util/BidiUtil.kt index e0ad35966e..693ab6e3a6 100644 --- a/core/util-jvm/src/main/java/org/signal/core/util/BidiUtil.kt +++ b/core/util-jvm/src/main/java/org/signal/core/util/BidiUtil.kt @@ -10,6 +10,12 @@ import java.util.regex.Pattern object BidiUtil { private val ALL_ASCII_PATTERN: Pattern = Pattern.compile("^[\\x00-\\x7F]*$") + object BidiCodepoint { + const val LRI = "\u2066" + + const val PDI = "\u2069" + } + private object Bidi { /** Override text direction */ val OVERRIDES: Set = SetUtil.newHashSet(