Update release notes chat styling.

This commit is contained in:
Cody Henthorne
2026-04-27 10:21:29 -04:00
parent ceecacb47e
commit c2d7ee6926
25 changed files with 635 additions and 113 deletions
@@ -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
}
@@ -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)
@@ -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
}
@@ -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)
@@ -204,6 +204,7 @@ public final class ConversationItem extends RelativeLayout implements BindableCo
private Optional<MessageRecord> nextMessageRecord;
private Locale locale;
private boolean groupThread;
private boolean isReleaseNotes;
private LiveRecipient author;
private RequestManager requestManager;
private Optional<MessageRecord> 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);
}
@@ -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) {
@@ -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));
}
@@ -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()
@@ -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<ConversationHeaderView>()
.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<ConversationHeaderView>()
.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()
}
}
@@ -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 {
@@ -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 {
@@ -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<View>(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()
}
}
@@ -444,14 +444,21 @@ open class V2ConversationItemTextOnlyViewHolder<Model : MappingModel<Model>>(
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)
@@ -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)
@@ -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
@@ -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(() -> {