From c2d7ee69265812842bdcd3b83514150863e53bf6 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Mon, 27 Apr 2026 10:21:29 -0400 Subject: [PATCH] Update release notes chat styling. --- .../components/RotatedTiledDrawable.kt | 45 +++ .../internal/InternalSettingsRepository.kt | 6 +- .../preferences/BioTextPreference.kt | 2 +- .../conversation/ConversationHeaderView.kt | 8 +- .../conversation/ConversationItem.java | 24 +- .../conversation/ConversationOptionsMenu.kt | 8 + .../conversation/ConversationTitleView.java | 2 +- .../conversation/v2/ConversationFragment.kt | 129 ++++--- .../ConversationHeaderPositionDecoration.kt | 48 ++- .../v2/ConversationItemDecorations.kt | 11 +- .../v2/ConversationToolbarOnScrollHelper.kt | 13 +- .../conversation/v2/DisabledInputView.kt | 17 - .../V2ConversationItemTextOnlyViewHolder.kt | 11 +- .../v2/items/V2ConversationItemTheme.kt | 9 + .../securesms/recipients/Recipient.kt | 2 +- .../bottomsheet/RecipientDialogViewModel.java | 2 +- .../res/drawable/release_chat_background.xml | 336 ++++++++++++++++++ .../release_notes_date_header_background.xml | 9 + .../release_notes_input_pill_background.xml | 6 + .../layout/conversation_activity_unmute.xml | 16 - .../conversation_item_call_to_action.xml | 2 + .../res/layout/v2_conversation_fragment.xml | 18 + app/src/main/res/values-night/dark_colors.xml | 10 + app/src/main/res/values/light_colors.xml | 10 + app/src/main/res/values/strings.xml | 4 +- 25 files changed, 635 insertions(+), 113 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/components/RotatedTiledDrawable.kt create mode 100644 app/src/main/res/drawable/release_chat_background.xml create mode 100644 app/src/main/res/drawable/release_notes_date_header_background.xml create mode 100644 app/src/main/res/drawable/release_notes_input_pill_background.xml delete mode 100644 app/src/main/res/layout/conversation_activity_unmute.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/RotatedTiledDrawable.kt b/app/src/main/java/org/thoughtcrime/securesms/components/RotatedTiledDrawable.kt new file mode 100644 index 0000000000..d0b8ef107c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/components/RotatedTiledDrawable.kt @@ -0,0 +1,45 @@ +package org.thoughtcrime.securesms.components + +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.Matrix +import android.graphics.Paint +import android.graphics.PixelFormat +import android.graphics.Shader +import android.graphics.drawable.Drawable + +/** + * Draws [bitmap] as a repeating tiled pattern rotated by [rotationDegrees]. + */ +class RotatedTiledDrawable( + private val bitmap: Bitmap, + private val rotationDegrees: Float +) : Drawable() { + + private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + shader = android.graphics.BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT) + } + + override fun onBoundsChange(bounds: android.graphics.Rect) { + paint.shader.setLocalMatrix( + Matrix().apply { setRotate(rotationDegrees, bounds.exactCenterX(), bounds.exactCenterY()) } + ) + } + + override fun draw(canvas: Canvas) { + canvas.drawRect(bounds, paint) + } + + override fun setAlpha(alpha: Int) { + paint.alpha = alpha + invalidateSelf() + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + paint.colorFilter = colorFilter + invalidateSelf() + } + + override fun getOpacity(): Int = PixelFormat.TRANSLUCENT +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt index fb01cf2139..189ad69bdc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsRepository.kt @@ -8,6 +8,7 @@ import org.thoughtcrime.securesms.database.MessageTable import org.thoughtcrime.securesms.database.SignalDatabase import org.thoughtcrime.securesms.database.model.RemoteMegaphoneRecord import org.thoughtcrime.securesms.database.model.addButton +import org.thoughtcrime.securesms.database.model.addLink import org.thoughtcrime.securesms.database.model.addStyle import org.thoughtcrime.securesms.database.model.databaseprotos.BodyRangeList import org.thoughtcrime.securesms.dependencies.AppDependencies @@ -48,9 +49,12 @@ class InternalSettingsRepository(context: Context) { val title = "Release Note Title" val bodyText = "Release note body. Aren't I awesome?" - val body = "$title\n\n$bodyText" + val linkUrl = "https://signal.org" + val body = "$title\n\n$bodyText\n\n$linkUrl" + val linkStart = body.length - linkUrl.length val bodyRangeList = BodyRangeList.Builder() .addStyle(BodyRangeList.BodyRange.Style.BOLD, 0, title.length) + .addLink(linkUrl, linkStart, linkUrl.length) bodyRangeList.addButton("Call to Action Text", callToAction, body.lastIndex, 0) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt index dfb84ffc1f..7704556670 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/preferences/BioTextPreference.kt @@ -44,7 +44,7 @@ object BioTextPreference { override fun getSubhead1Text(context: Context): String? { return if (recipient.isReleaseNotes) { - context.getString(R.string.ReleaseNotes__signal_release_notes_and_news) + null } else { recipient.combinedAboutAndEmoji } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.kt index 98169e52f6..3d7592d297 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationHeaderView.kt @@ -46,6 +46,7 @@ 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.colorResource import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.stringResource @@ -209,7 +210,12 @@ private fun ConversationHeaderContent( .padding(top = AvatarOverlapAbove) .width(277.dp) .then( - if (hasWallpaper) { + if (isReleaseNotes) { + Modifier + .clip(BorderShape) + .background(colorResource(R.color.release_notes_header_background)) + .border(width = 2.dp, color = colorResource(R.color.release_notes_header_border), shape = BorderShape) + } else if (hasWallpaper) { Modifier .clip(BorderShape) .background(if (isSystemInDarkTheme()) SignalTheme.colors.colorTransparentInverse5 else SignalTheme.colors.colorTransparent5) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java index ae2b8832d5..bc5aa9fdee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationItem.java @@ -204,6 +204,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo private Optional nextMessageRecord; private Locale locale; private boolean groupThread; + private boolean isReleaseNotes; private LiveRecipient author; private RequestManager requestManager; private Optional previousMessage; @@ -412,6 +413,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo this.batchSelected = batchSelected; this.conversationRecipient = conversationRecipient.live(); this.groupThread = conversationRecipient.isGroup(); + this.isReleaseNotes = conversationRecipient.isReleaseNotes(); this.author = messageRecord.getFromRecipient().live(); this.canPlayContent = false; this.mediaItem = null; @@ -772,6 +774,9 @@ public final class ConversationItem extends RelativeLayout implements BindableCo } private @ColorInt int getDefaultBubbleColor(boolean hasWallpaper) { + if (isReleaseNotes) { + return ContextCompat.getColor(context, R.color.release_notes_bubble); + } return hasWallpaper ? defaultBubbleColorForWallpaper : defaultBubbleColor; } @@ -919,9 +924,18 @@ public final class ConversationItem extends RelativeLayout implements BindableCo footer.setOnlyShowSendingStatus(messageRecord.isRemoteDelete(), messageRecord); } else { bodyBubble.getBackground().setColorFilter(getDefaultBubbleColor(hasWallpaper), PorterDuff.Mode.SRC_IN); - footer.setTextColor(colorizer.getIncomingFooterTextColor(context, hasWallpaper)); - footer.setIconColor(colorizer.getIncomingFooterIconColor(context, hasWallpaper)); - footer.setRevealDotColor(colorizer.getIncomingFooterIconColor(context, hasWallpaper)); + if (isReleaseNotes) { + int releaseNotesTextColor = ContextCompat.getColor(context, R.color.release_notes_bubble_text); + bodyText.setTextColor(releaseNotesTextColor); + bodyText.setLinkTextColor(releaseNotesTextColor); + footer.setTextColor(releaseNotesTextColor); + footer.setIconColor(releaseNotesTextColor); + footer.setRevealDotColor(releaseNotesTextColor); + } else { + footer.setTextColor(colorizer.getIncomingFooterTextColor(context, hasWallpaper)); + footer.setIconColor(colorizer.getIncomingFooterIconColor(context, hasWallpaper)); + footer.setRevealDotColor(colorizer.getIncomingFooterIconColor(context, hasWallpaper)); + } footer.setOnlyShowSendingStatus(false, messageRecord); } @@ -1718,8 +1732,8 @@ public final class ConversationItem extends RelativeLayout implements BindableCo int end = messageBody.getSpanEnd(placeholder); URLSpan span = new InterceptableLongClickCopyLinkSpan(placeholder.getValue(), urlClickListener, - ContextCompat.getColor(getContext(), R.color.signal_accent_primary), - false); + ContextCompat.getColor(getContext(), isReleaseNotes ? R.color.release_notes_bubble_text : R.color.signal_accent_primary), + isReleaseNotes); messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationOptionsMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationOptionsMenu.kt index a4ac3dca8c..ce1b33122d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationOptionsMenu.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationOptionsMenu.kt @@ -167,6 +167,14 @@ internal object ConversationOptionsMenu { if (recipient.isReleaseNotes) { hideMenuItem(menu, R.id.menu_add_shortcut) + menu.findItem(R.id.menu_mute_notifications)?.apply { + setIcon(R.drawable.symbol_bell_24) + setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + } + menu.findItem(R.id.menu_unmute_notifications)?.apply { + setIcon(R.drawable.symbol_bell_slash_24) + setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS) + } } if (!SignalStore.labs.individualChatPlaintextExport) { 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 6e0c3868ab..e3b458054f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationTitleView.java @@ -119,7 +119,7 @@ public class ConversationTitleView extends ConstraintLayout { if (recipient != null && recipient.isBlocked()) { startDrawable = ContextUtil.requireDrawable(getContext(), R.drawable.symbol_block_16); startDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18)); - } else if (recipient != null && recipient.isMuted()) { + } else if (recipient != null && recipient.isMuted() && !recipient.isReleaseNotes()) { startDrawable = ContextUtil.requireDrawable(getContext(), R.drawable.ic_bell_disabled_16); startDrawable.setBounds(0, 0, ViewUtil.dpToPx(18), ViewUtil.dpToPx(18)); } 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 c9eb826355..8119b4ad8a 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 @@ -18,6 +18,7 @@ import android.content.Intent import android.content.IntentFilter import android.content.res.Configuration import android.graphics.Bitmap +import android.graphics.Color import android.graphics.PorterDuff import android.graphics.PorterDuffColorFilter import android.graphics.Rect @@ -42,6 +43,7 @@ import android.view.WindowManager import android.view.animation.AnimationUtils import android.view.inputmethod.EditorInfo import android.widget.ImageButton +import android.widget.ImageView import android.widget.TextView import android.widget.TextView.OnEditorActionListener import android.widget.Toast @@ -49,7 +51,9 @@ import androidx.activity.OnBackPressedCallback import androidx.activity.result.ActivityResultLauncher import androidx.annotation.MainThread import androidx.annotation.StringRes +import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.widget.SearchView +import androidx.constraintlayout.widget.ConstraintSet import androidx.core.app.ActivityOptionsCompat import androidx.core.content.ContextCompat import androidx.core.content.pm.ShortcutManagerCompat @@ -59,6 +63,7 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.doOnPreDraw import androidx.core.view.isInvisible import androidx.core.view.isVisible +import androidx.core.view.updatePadding import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentResultListener @@ -149,6 +154,7 @@ import org.thoughtcrime.securesms.components.InputAwareConstraintLayout import org.thoughtcrime.securesms.components.InputPanel import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout import org.thoughtcrime.securesms.components.ProgressCardDialogFragment +import org.thoughtcrime.securesms.components.RotatedTiledDrawable import org.thoughtcrime.securesms.components.ScrollToPositionDelegate import org.thoughtcrime.securesms.components.SendButton import org.thoughtcrime.securesms.components.SignalProgressDialog @@ -184,7 +190,6 @@ import org.thoughtcrime.securesms.conversation.ConversationAdapter import org.thoughtcrime.securesms.conversation.ConversationArgs import org.thoughtcrime.securesms.conversation.ConversationBottomSheetCallback import org.thoughtcrime.securesms.conversation.ConversationData -import org.thoughtcrime.securesms.conversation.ConversationHeaderView import org.thoughtcrime.securesms.conversation.ConversationIntents import org.thoughtcrime.securesms.conversation.ConversationIntents.ConversationScreenType import org.thoughtcrime.securesms.conversation.ConversationItem @@ -573,7 +578,7 @@ class ConversationFragment : private lateinit var attachmentManager: AttachmentManager private lateinit var multiselectItemDecoration: MultiselectItemDecoration private lateinit var openableGiftItemDecoration: OpenableGiftItemDecoration - private lateinit var threadHeaderMarginDecoration: ThreadHeaderMarginDecoration + private lateinit var conversationHeaderPositionDecoration: ConversationHeaderPositionDecoration private lateinit var conversationItemDecorations: ConversationItemDecorations private lateinit var optionsMenuCallback: ConversationOptionsMenuCallback @@ -601,6 +606,8 @@ class ConversationFragment : private var firstPinRender: Boolean = true private var skipNextBackPressHandling: Boolean = false private var collapsibleEventScrollPosition: CollapsibleEventScrollPosition? = null + private var releaseNotesLayoutApplied: Boolean = false + private var releaseNotesWallpaperApplied: Boolean = false private val jumpAndPulseScrollStrategy = object : ScrollToPositionDelegate.ScrollStrategy { override fun performScroll(recyclerView: RecyclerView, layoutManager: LinearLayoutManager, position: Int, smooth: Boolean) { @@ -694,6 +701,7 @@ class ConversationFragment : requireActivity(), binding.toolbarBackground, viewModel::wallpaperSnapshot, + { viewModel.recipientSnapshot?.isReleaseNotes == true }, viewLifecycleOwner, incognito = args.isIncognito ) @@ -761,10 +769,10 @@ class ConversationFragment : binding.toolbar.addOnLayoutChangeListener { _, _, _, _, bottom, _, _, _, oldBottom -> binding.conversationItemRecycler.padding(top = bottom) - if (bottom != oldBottom && ::threadHeaderMarginDecoration.isInitialized) { + if (bottom != oldBottom && ::conversationHeaderPositionDecoration.isInitialized) { val newMargin = bottom + 16.dp - if (threadHeaderMarginDecoration.toolbarMargin != newMargin) { - threadHeaderMarginDecoration.toolbarMargin = newMargin + if (conversationHeaderPositionDecoration.toolbarMargin != newMargin) { + conversationHeaderPositionDecoration.toolbarMargin = newMargin binding.conversationItemRecycler.invalidateItemDecorations() } } @@ -1562,6 +1570,10 @@ class ConversationFragment : presentConversationTitle(inputReadyState.conversationRecipient) val disabledInputView = binding.conversationDisabledInput + val isReleaseNotes = inputReadyState.conversationRecipient.isReleaseNotes + if (isReleaseNotes) { + applyReleaseNotesLayout() + } var inputDisabled = true when { @@ -1572,22 +1584,42 @@ class ConversationFragment : inputReadyState.isRequestingMember == true -> disabledInputView.showAsRequestingMember() inputReadyState.isActiveGroup == false -> disabledInputView.showAsNoLongerAMember() inputReadyState.isAnnouncementGroup == true && inputReadyState.isAdmin == false -> disabledInputView.showAsAnnouncementGroupAdminsOnly() - inputReadyState.conversationRecipient.isReleaseNotes -> disabledInputView.showAsReleaseNotesChannel(inputReadyState.conversationRecipient) + isReleaseNotes -> Unit inputReadyState.shouldShowInviteToSignal() -> disabledInputView.showAsInviteToSignal(requireContext(), inputReadyState.conversationRecipient, inputReadyState.threadContainsSms) else -> inputDisabled = false } inputPanel.setHideForMessageRequestState(inputDisabled) - if (inputDisabled) { + if (inputDisabled && !isReleaseNotes) { binding.navBar.setBackgroundColor(disabledInputView.color) - } else { + } else if (!inputDisabled) { disabledInputView.clear() } composeText.setMessageSendType(MessageSendType.SignalMessageSendType) } + private fun applyReleaseNotesLayout() { + if (releaseNotesLayoutApplied) { + return + } + releaseNotesLayoutApplied = true + + binding.conversationReleaseNotesFloatingLabel.visible = true + binding.conversationDisabledInput.visible = false + + val navBarInset = ViewCompat.getRootWindowInsets(binding.root)?.getInsets(WindowInsetsCompat.Type.navigationBars())?.bottom ?: 0 + binding.conversationItemRecycler.updatePadding(bottom = ViewUtil.dpToPx(72) + navBarInset) + binding.navBar.setBackgroundColor(Color.TRANSPARENT) + + ConstraintSet().apply { + clone(binding.root) + connect(binding.conversationItemRecyclerFrame.id, ConstraintSet.BOTTOM, ConstraintSet.PARENT_ID, ConstraintSet.BOTTOM) + applyTo(binding.root) + } + } + private fun presentIdentityRecordsState(identityRecordsState: IdentityRecordsState) { binding.conversationTitleView.root.setVerified(identityRecordsState.isVerified) @@ -1713,16 +1745,13 @@ class ConversationFragment : } private fun onRecipientChanged(recipient: Recipient) { - presentWallpaper(recipient.wallpaper) + presentWallpaper(recipient) presentConversationTitle(recipient) presentChatColors(recipient.chatColors) invalidateOptionsMenu() updateMessageRequestAcceptedState(!viewModel.hasMessageRequestState) recyclerViewColorizer.setChatColors(recipient.chatColors) - if (adapter.onHasWallpaperChanged(hasWallpaper = recipient.wallpaper != null)) { - conversationItemDecorations.hasWallpaper = recipient.wallpaper != null - } } @MainThread @@ -1842,17 +1871,19 @@ class ConversationFragment : } } - private fun presentWallpaper(chatWallpaper: ChatWallpaper?) { - if (chatWallpaper != null) { - chatWallpaper.loadInto(binding.conversationWallpaper) - ChatWallpaperDimLevelUtil.applyDimLevelForNightMode(binding.conversationWallpaperDim, chatWallpaper) + private fun presentWallpaper(recipient: Recipient) { + val chatWallpaper = recipient.wallpaper + if (recipient.isReleaseNotes) { + applyReleaseNotesWallpaper() } else { - binding.conversationWallpaperDim.visible = false + applyChatWallpaper(chatWallpaper) } + val wallpaperEnabled = chatWallpaper != null || recipient.isReleaseNotes + val toolbarTint = ContextCompat.getColor( requireContext(), - if (chatWallpaper != null) { + if (wallpaperEnabled) { CoreUiR.color.signal_colorNeutralInverse } else { CoreUiR.color.signal_colorOnSurface @@ -1863,7 +1894,6 @@ class ConversationFragment : binding.toolbar.setActionItemTint(toolbarTint) binding.toolbar.navigationIcon?.setTint(toolbarTint) - val wallpaperEnabled = chatWallpaper != null binding.conversationWallpaper.visible = wallpaperEnabled binding.scrollToBottom.setWallpaperEnabled(wallpaperEnabled) binding.scrollToMention.setWallpaperEnabled(wallpaperEnabled) @@ -1872,6 +1902,7 @@ class ConversationFragment : val stateChanged = adapter.onHasWallpaperChanged(wallpaperEnabled) conversationItemDecorations.hasWallpaper = wallpaperEnabled + conversationItemDecorations.isReleaseNotes = recipient.isReleaseNotes if (stateChanged) { binding.conversationItemRecycler.invalidateItemDecorations() } @@ -1888,12 +1919,39 @@ class ConversationFragment : ) if (!inputPanel.isHidden) { - setNavBarBackgroundColor(chatWallpaper) + setNavBarBackgroundColor(wallpaperEnabled) } } - private fun setNavBarBackgroundColor(chatWallpaper: ChatWallpaper?) { - val navColor = if (chatWallpaper != null) { + private fun applyReleaseNotesWallpaper() { + if (releaseNotesWallpaperApplied) { + return + } + releaseNotesWallpaperApplied = true + + val tinted = DrawableUtil.tint( + AppCompatResources.getDrawable(requireContext(), R.drawable.release_chat_background)!!, + ContextCompat.getColor(requireContext(), R.color.release_notes_background_pattern) + ) + val bitmap = DrawableUtil.toBitmap(tinted, tinted.intrinsicWidth, tinted.intrinsicHeight) + + binding.conversationWallpaper.scaleType = ImageView.ScaleType.MATRIX + binding.conversationWallpaper.setBackgroundColor(ContextCompat.getColor(requireContext(), R.color.release_notes_background)) + binding.conversationWallpaper.setImageDrawable(RotatedTiledDrawable(bitmap, -45f)) + binding.conversationWallpaperDim.visible = false + } + + private fun applyChatWallpaper(chatWallpaper: ChatWallpaper?) { + if (chatWallpaper != null) { + chatWallpaper.loadInto(binding.conversationWallpaper) + ChatWallpaperDimLevelUtil.applyDimLevelForNightMode(binding.conversationWallpaperDim, chatWallpaper) + } else { + binding.conversationWallpaperDim.visible = false + } + } + + private fun setNavBarBackgroundColor(hasWallpaper: Boolean) { + val navColor = if (hasWallpaper) { R.color.conversation_navigation_wallpaper } else { CoreUiR.color.signal_colorBackground @@ -2237,12 +2295,11 @@ class ConversationFragment : } ) - threadHeaderMarginDecoration = ThreadHeaderMarginDecoration() + conversationHeaderPositionDecoration = ConversationHeaderPositionDecoration() 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()) + conversationHeaderPositionDecoration.toolbarMargin = statusBarInset + resources.getDimensionPixelSize(R.dimen.signal_m3_toolbar_height) + 16.dp + binding.conversationItemRecycler.addItemDecoration(conversationHeaderPositionDecoration) conversationItemDecorations = ConversationItemDecorations(hasWallpaper = args.hasWallpaper) binding.conversationItemRecycler.addItemDecoration(conversationItemDecorations, 0) @@ -3330,7 +3387,8 @@ class ConversationFragment : private fun presentComposeDivider() { val isAtBottom = isScrolledToBottom() - if (isAtBottom && !wasAtBottom) { + val suppress = viewModel.recipientSnapshot?.isReleaseNotes == true + if ((isAtBottom && !wasAtBottom) || suppress) { ViewUtil.fadeOut(binding.composeDivider, 50, View.INVISIBLE) } else if (wasAtBottom && !isAtBottom) { ViewUtil.fadeIn(binding.composeDivider, 500) @@ -4734,10 +4792,6 @@ class ConversationFragment : launchIntent = this@ConversationFragment::startActivity ) } - - override fun onUnmuteReleaseNotesChannel() { - viewModel.muteConversation(0L) - } } //endregion @@ -5126,7 +5180,7 @@ class ConversationFragment : } override fun onInputHidden() { - setNavBarBackgroundColor(viewModel.wallpaperSnapshot) + setNavBarBackgroundColor(viewModel.wallpaperSnapshot != null || viewModel.recipientSnapshot?.isReleaseNotes == true) viewModel.setIsMediaKeyboardShowing(false) } @@ -5248,17 +5302,6 @@ class ConversationFragment : } } - private inner class ThreadHeaderMarginDecoration : RecyclerView.ItemDecoration() { - var toolbarMargin: Int = 0 - - override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { - super.getItemOffsets(outRect, view, parent, state) - if (view is ConversationHeaderView) { - outRect.top = toolbarMargin - } - } - } - private inner class VoiceMessageRecordingSessionCallbacks : VoiceMessageRecordingDelegate.SessionCallback { override fun onSessionWillBegin() { getVoiceNoteMediaController().pausePlayback() 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 index 998ffbc3ea..26c16b4bc1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationHeaderPositionDecoration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationHeaderPositionDecoration.kt @@ -7,35 +7,45 @@ package org.thoughtcrime.securesms.conversation.v2 import android.graphics.Canvas import android.graphics.Rect +import android.view.View 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. + * Reserves space above the [ConversationHeaderView] for the toolbar and adjusts the conversation RecyclerView's translationY so the header is pinned below the + * toolbar when content is short enough to fit the viewport. The toolbar margin is only contributed when a translation is actually going to be applied; when + * content overflows, no margin is added and no translation is applied. */ 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 - } + private val bounds = Rect() - // 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 + var toolbarMargin: Int = 0 - val childTop: Int = threadHeaderView.top - toolbarMargin - parent.translationY = min(0, -childTop).toFloat() + override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { + super.getItemOffsets(outRect, view, parent, state) + if (view is ConversationHeaderView && !parent.canScrollVertically(1)) { + outRect.top = toolbarMargin } } + + override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { + if (parent.canScrollVertically(1)) { + parent.translationY = 0f + return + } + + val threadHeaderView: ConversationHeaderView = parent.children + .filterIsInstance() + .firstOrNull() ?: run { + parent.translationY = 0f + return + } + + parent.getDecoratedBoundsWithMargins(threadHeaderView, bounds) + val margin = bounds.bottom - bounds.top - threadHeaderView.height + val childTop: Int = threadHeaderView.top - margin + parent.translationY = min(0, -childTop).toFloat() + } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationItemDecorations.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationItemDecorations.kt index 5f37cbec9c..b1955518e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationItemDecorations.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationItemDecorations.kt @@ -59,6 +59,12 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch unreadViewHolder?.updateForWallpaper() } + var isReleaseNotes: Boolean = false + set(value) { + field = value + headerCache.values.forEach { it.updateForWallpaper() } + } + var selfRecipientId: RecipientId? = null override fun getItemOffsets(outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State) { @@ -307,7 +313,10 @@ class ConversationItemDecorations(hasWallpaper: Boolean = false, private val sch } fun updateForWallpaper() { - if (hasWallpaper) { + if (isReleaseNotes) { + date.setBackgroundResource(R.drawable.release_notes_date_header_background) + date.setTextColor(ContextCompat.getColor(itemView.context, CoreUiR.color.signal_colorOnSurfaceVariant)) + } else if (hasWallpaper) { date.setBackgroundResource(R.drawable.wallpaper_bubble_background_18) date.setTextColor(ContextCompat.getColor(itemView.context, CoreUiR.color.signal_colorNeutralInverse)) } else { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationToolbarOnScrollHelper.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationToolbarOnScrollHelper.kt index 13f22a1db0..4629f25676 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationToolbarOnScrollHelper.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationToolbarOnScrollHelper.kt @@ -16,6 +16,7 @@ class ConversationToolbarOnScrollHelper( activity: FragmentActivity, toolbarBackground: View, private val wallpaperProvider: () -> ChatWallpaper?, + private val releaseNotesProvider: () -> Boolean, lifecycleOwner: LifecycleOwner, private val incognito: Boolean = false ) : Material3OnScrollHelper( @@ -25,10 +26,18 @@ class ConversationToolbarOnScrollHelper( setStatusBarColor = {} ) { override val activeColorSet: ColorSet - get() = if (incognito) ColorSet(R.color.conversation_toolbar_color_incognito) else ColorSet(getActiveToolbarColor(wallpaperProvider() != null)) + get() = when { + incognito -> ColorSet(R.color.conversation_toolbar_color_incognito) + releaseNotesProvider() -> ColorSet(R.color.release_notes_toolbar_scrolled) + else -> ColorSet(getActiveToolbarColor(wallpaperProvider() != null)) + } override val inactiveColorSet: ColorSet - get() = if (incognito) ColorSet(R.color.conversation_toolbar_color_incognito) else ColorSet(getInactiveToolbarColor(wallpaperProvider() != null)) + get() = when { + incognito -> ColorSet(R.color.conversation_toolbar_color_incognito) + releaseNotesProvider() -> ColorSet(R.color.release_notes_toolbar_transparent) + else -> ColorSet(getInactiveToolbarColor(wallpaperProvider() != null)) + } @ColorRes private fun getActiveToolbarColor(hasWallpaper: Boolean): Int { 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 b4b139f804..4b29f0fc24 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 @@ -48,7 +48,6 @@ class DisabledInputView @JvmOverloads constructor( private var requestingGroup: View? = null private var announcementGroupOnly: TextView? = null private var inviteToSignal: View? = null - private var releaseNoteChannel: View? = null private var incognitoView: View? = null private var currentView: View? = null @@ -187,21 +186,6 @@ class DisabledInputView @JvmOverloads constructor( ) } - fun showAsReleaseNotesChannel(recipient: Recipient) { - releaseNoteChannel = show( - existingView = releaseNoteChannel, - create = { inflater.inflate(R.layout.conversation_activity_unmute, this, false) }, - bind = { - if (recipient.isMuted) { - visible = true - findViewById(R.id.conversation_activity_unmute_button).setOnClickListener { listener?.onUnmuteReleaseNotesChannel() } - } else { - visible = false - } - } - ) - } - fun setWallpaperEnabled(wallpaperEnabled: Boolean) { color = ContextCompat.getColor(context, if (wallpaperEnabled) R.color.wallpaper_bubble_color else CoreUiR.color.signal_colorBackground) setBackgroundColor(color) @@ -272,7 +256,6 @@ class DisabledInputView @JvmOverloads constructor( fun onBlockClicked() fun onUnblockClicked() fun onInviteToSignal(recipient: Recipient) - fun onUnmuteReleaseNotesChannel() fun onReportSpamClicked() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt index 359bfffee0..47ee95225f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTextOnlyViewHolder.kt @@ -444,14 +444,21 @@ open class V2ConversationItemTextOnlyViewHolder>( V2ConversationItemUtils.linkifyUrlLinks(messageBody, conversationContext.selectedItems.isEmpty(), conversationContext.clickListener::onUrlClicked) if (conversationMessage.hasStyleLinks()) { + val isReleaseNotes = conversationMessage.threadRecipient.isReleaseNotes + val linkColor = if (isReleaseNotes) { + themeDelegate.getBodyTextColor(conversationMessage) + } else { + ContextCompat.getColor(getContext(), R.color.signal_accent_primary) + } + val underline = isReleaseNotes messageBody.getSpans(0, messageBody.length, PlaceholderURLSpan::class.java).forEach { placeholder -> val start = messageBody.getSpanStart(placeholder) val end = messageBody.getSpanEnd(placeholder) val span: URLSpan = InterceptableLongClickCopyLinkSpan( placeholder.value, conversationContext.clickListener::onUrlClicked, - ContextCompat.getColor(getContext(), R.color.signal_accent_primary), - false + linkColor, + underline ) messageBody.setSpan(span, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTheme.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTheme.kt index 6cfb09067d..212892cdf9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTheme.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/items/V2ConversationItemTheme.kt @@ -44,6 +44,10 @@ class V2ConversationItemTheme( return conversationContext.getColorizer().getIncomingFooterTextColor(context, conversationContext.hasWallpaper()) } + if (!conversationMessage.messageRecord.isOutgoing && conversationMessage.threadRecipient.isReleaseNotes) { + return ContextCompat.getColor(context, R.color.release_notes_bubble_text) + } + return getColor( conversationMessage, conversationContext.getColorizer()::getOutgoingFooterTextColor, @@ -55,6 +59,9 @@ class V2ConversationItemTheme( fun getBodyTextColor( conversationMessage: ConversationMessage ): Int { + if (!conversationMessage.messageRecord.isOutgoing && conversationMessage.threadRecipient.isReleaseNotes) { + return ContextCompat.getColor(context, R.color.release_notes_bubble_text) + } return getColor( conversationMessage, conversationContext.getColorizer()::getOutgoingBodyTextColor, @@ -79,6 +86,8 @@ class V2ConversationItemTheme( ): Int { return if (conversationMessage.messageRecord.isOutgoing) { Color.TRANSPARENT + } else if (conversationMessage.threadRecipient.isReleaseNotes) { + ContextCompat.getColor(context, R.color.release_notes_bubble) } else { if (conversationContext.hasWallpaper()) { ContextCompat.getColor(context, R.color.conversation_item_recv_bubble_color_wallpaper) diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt index adbd0ccf8d..57b4ae68b9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/Recipient.kt @@ -370,7 +370,7 @@ class Recipient( /** A cheap way to check if wallpaper is set without doing any unnecessary proto parsing. */ val hasWallpaper: Boolean - get() = wallpaperValue != null || SignalStore.wallpaper.hasWallpaperSet() + get() = wallpaperValue != null || SignalStore.wallpaper.hasWallpaperSet() || isReleaseNotes /** The color of the chat bubbles to use in a chat with this recipient. */ val chatColors: ChatColors diff --git a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java index 344e0457cd..4654fc7251 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java +++ b/app/src/main/java/org/thoughtcrime/securesms/recipients/ui/bottomsheet/RecipientDialogViewModel.java @@ -132,7 +132,7 @@ final class RecipientDialogViewModel extends ViewModel { private void updateRecipientDetailsState(@NonNull Recipient recipient) { GroupId groupId = recipientDialogRepository.getGroupId(); - String aboutText = recipient.isReleaseNotes() ? context.getString(R.string.ReleaseNotes__signal_release_notes_and_news) : recipient.getCombinedAboutAndEmoji(); + String aboutText = recipient.isReleaseNotes() ? null : recipient.getCombinedAboutAndEmoji(); if (groupId != null && groupId.isV2() && recipient.isIndividual() && !recipient.isSelf()) { SignalExecutors.BOUNDED.execute(() -> { diff --git a/app/src/main/res/drawable/release_chat_background.xml b/app/src/main/res/drawable/release_chat_background.xml new file mode 100644 index 0000000000..b874523a28 --- /dev/null +++ b/app/src/main/res/drawable/release_chat_background.xml @@ -0,0 +1,336 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/release_notes_date_header_background.xml b/app/src/main/res/drawable/release_notes_date_header_background.xml new file mode 100644 index 0000000000..0322d5337c --- /dev/null +++ b/app/src/main/res/drawable/release_notes_date_header_background.xml @@ -0,0 +1,9 @@ + + + + + + diff --git a/app/src/main/res/drawable/release_notes_input_pill_background.xml b/app/src/main/res/drawable/release_notes_input_pill_background.xml new file mode 100644 index 0000000000..c9860e3280 --- /dev/null +++ b/app/src/main/res/drawable/release_notes_input_pill_background.xml @@ -0,0 +1,6 @@ + + + + + diff --git a/app/src/main/res/layout/conversation_activity_unmute.xml b/app/src/main/res/layout/conversation_activity_unmute.xml deleted file mode 100644 index 9ef8741b60..0000000000 --- a/app/src/main/res/layout/conversation_activity_unmute.xml +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - diff --git a/app/src/main/res/layout/conversation_item_call_to_action.xml b/app/src/main/res/layout/conversation_item_call_to_action.xml index b12b4072f6..8aa6498ca6 100644 --- a/app/src/main/res/layout/conversation_item_call_to_action.xml +++ b/app/src/main/res/layout/conversation_item_call_to_action.xml @@ -5,4 +5,6 @@ android:id="@+id/conversation_item_call_to_action" android:layout_width="match_parent" android:layout_height="wrap_content" + android:backgroundTint="@color/release_notes_cta_background" + android:textColor="@color/signal_colorOnSurface" tools:text="Click me" /> diff --git a/app/src/main/res/layout/v2_conversation_fragment.xml b/app/src/main/res/layout/v2_conversation_fragment.xml index 3dfb98b6d1..92d34d0a17 100644 --- a/app/src/main/res/layout/v2_conversation_fragment.xml +++ b/app/src/main/res/layout/v2_conversation_fragment.xml @@ -292,6 +292,24 @@ app:layout_constraintEnd_toEndOf="@id/parent_end_guideline" app:layout_constraintStart_toStartOf="@id/parent_start_guideline" /> + + @color/signal_dark_colorSecondary #FFEB977D + + #FF444664 + #FFFFFFFF + #FF636583 + #FF272C3C + #FF3A3F4E + #FF353A49 + #FF2F3240 + #FF424757 + #00424757 diff --git a/app/src/main/res/values/light_colors.xml b/app/src/main/res/values/light_colors.xml index da94f0e8f0..96fc22ff99 100644 --- a/app/src/main/res/values/light_colors.xml +++ b/app/src/main/res/values/light_colors.xml @@ -211,4 +211,14 @@ #99F2F5F9 #FFB44828 + + #FF9294BC + #FFFFFFFF + #FFD0D1E9 + #FFE3E4E9 + #FFE3E4E9 + #FFF3F3F7 + #FFF6F7FF + #FFEAEDF8 + #00EAEDF8 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1891050862..b101043a06 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -722,6 +722,8 @@ Official chat The only official chat from Signal. Keep up to date with news and release notes. + + The only official chat from Signal %1$d Member @@ -6878,8 +6880,6 @@ Turn on - - Signal Release Notes & News This is the official and only chat from Signal