From ad2de507194d7b0c20a569bcccc6c2484947b3db Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 17 Nov 2025 11:08:50 +1100 Subject: [PATCH 1/3] Fixed avatar badges position and size --- .../mentions/MentionCandidateView.kt | 5 +- .../v2/messages/VisibleMessageView.kt | 16 +++--- .../securesms/ui/components/Avatar.kt | 49 ++++++++++++++++++- .../securesms/util/AvatarUtils.kt | 2 +- .../ic_crown_custom_enlarged_no_padding.xml | 12 +++++ .../res/layout/view_mention_candidate_v2.xml | 13 ----- .../main/res/layout/view_visible_message.xml | 14 ------ 7 files changed, 70 insertions(+), 41 deletions(-) create mode 100644 app/src/main/res/drawable/ic_crown_custom_enlarged_no_padding.xml diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt index 7b6619a1eb..46101962f9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/input_bar/mentions/MentionCandidateView.kt @@ -1,11 +1,11 @@ package org.thoughtcrime.securesms.conversation.v2.input_bar.mentions -import android.view.View import network.loki.messenger.databinding.ViewMentionCandidateV2Binding import org.thoughtcrime.securesms.conversation.v2.mention.MentionViewModel import org.thoughtcrime.securesms.ui.components.Avatar import org.thoughtcrime.securesms.ui.setThemedContent import org.thoughtcrime.securesms.ui.theme.LocalDimensions +import org.thoughtcrime.securesms.util.AvatarBadge fun ViewMentionCandidateV2Binding.update(candidate: MentionViewModel.Candidate) { mentionCandidateNameTextView.text = candidate.nameHighlighted @@ -13,8 +13,7 @@ fun ViewMentionCandidateV2Binding.update(candidate: MentionViewModel.Candidate) Avatar( size = LocalDimensions.current.iconMediumAvatar, data = candidate.member.avatarData, + badge = if (candidate.member.showAdminCrown) AvatarBadge.Admin else AvatarBadge.None ) } - - moderatorIconImageView.visibility = if (candidate.member.showAdminCrown) View.VISIBLE else View.GONE } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt index 8796834bbe..3f883900ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/messages/VisibleMessageView.kt @@ -61,6 +61,7 @@ import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.bold +import org.thoughtcrime.securesms.util.AvatarBadge import org.thoughtcrime.securesms.util.AvatarUtils import org.thoughtcrime.securesms.util.DateUtils import org.thoughtcrime.securesms.util.disableClipping @@ -176,7 +177,6 @@ class VisibleMessageView : FrameLayout { val isStartOfMessageCluster = isStartOfMessageCluster(message, previous, isGroupThread) val isEndOfMessageCluster = isEndOfMessageCluster(message, next, isGroupThread) // Show profile picture and sender name if this is a group thread AND the message is incoming - binding.moderatorIconImageView.isVisible = false binding.profilePictureView.visibility = when { threadRecipient.isGroupOrCommunityRecipient && !message.isOutgoing && isEndOfMessageCluster -> View.VISIBLE threadRecipient.isGroupOrCommunityRecipient -> View.INVISIBLE @@ -200,22 +200,22 @@ class VisibleMessageView : FrameLayout { if (isGroupThread && !message.isOutgoing) { if (isEndOfMessageCluster) { + val showProBadge = if (sender.address is Address.WithAccountId) { + (threadRecipient.data as? RecipientData.GroupLike) + ?.shouldShowAdminCrown(sender.address.accountId) == true + } else { + false + } binding.profilePictureView.setThemedContent { Avatar( size = LocalDimensions.current.iconMediumAvatar, data = avatarUtils.getUIDataFromRecipient(sender), + badge = if(showProBadge) AvatarBadge.Admin else AvatarBadge.None, modifier = Modifier.clickable { delegate?.showUserProfileModal(message.recipient) } ) } - - binding.moderatorIconImageView.isVisible = if (sender.address is Address.WithAccountId) { - (threadRecipient.data as? RecipientData.GroupLike) - ?.shouldShowAdminCrown(sender.address.accountId) == true - } else { - false - } } } if(!message.isOutgoing && (isStartOfMessageCluster && isGroupThread)){ diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt index 38d1c4abef..6266a2d493 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max import androidx.compose.ui.unit.sp import coil3.compose.AsyncImagePainter import coil3.compose.SubcomposeAsyncImage @@ -55,6 +56,7 @@ import org.thoughtcrime.securesms.util.AvatarUIData import org.thoughtcrime.securesms.util.AvatarUIElement import org.thoughtcrime.securesms.util.avatarOptions +private val MIN_BADGE_SIZE = 12.dp @Composable fun BaseAvatar( @@ -102,11 +104,20 @@ fun BaseAvatar( // Badge content, if any. if (badge != null) { + var badgeSize = size * 0.4f + var offset = 0.dp + + if(badgeSize < MIN_BADGE_SIZE){ + // apply an opinionated minimum size for the badge + badgeSize = MIN_BADGE_SIZE + offset = 1.dp // the forced min size looks better with a slight offset + } + Box( modifier = Modifier .align(Alignment.BottomEnd) - .offset(1.dp, 1.dp) // Used to make up for transparent padding in icon. - .size(size * 0.4f) + .offset(x = offset, y = offset) + .size(badgeSize) ) { badge() } @@ -387,6 +398,40 @@ fun PreviewAvatarSinglePhoto(){ } } +@Preview +@Composable +fun PreviewAvatarBadge(){ + PreviewTheme { + Avatar( + size = LocalDimensions.current.iconLarge, + badge = AvatarBadge.Admin, + data = AvatarUIData( + listOf(AvatarUIElement( + name = "AT", + color = primaryGreen, + remoteFile = null + ))) + ) + } +} + +@Preview +@Composable +fun PreviewAvatarBadgeSmall(){ + PreviewTheme { + Avatar( + size = LocalDimensions.current.iconMediumAvatar, + badge = AvatarBadge.Admin, + data = AvatarUIData( + listOf(AvatarUIElement( + name = "AT", + color = primaryGreen, + remoteFile = null + ))) + ) + } +} + @Preview @Composable fun PreviewAvatarElementUnclipped(){ diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtils.kt index a978a65e39..ff7c0d78ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/AvatarUtils.kt @@ -231,7 +231,7 @@ data class AvatarUIElement( sealed class AvatarBadge(@DrawableRes val icon: Int){ data object None: AvatarBadge(0) - data object Admin: AvatarBadge(R.drawable.ic_crown_custom_enlarged) + data object Admin: AvatarBadge(R.drawable.ic_crown_custom_enlarged_no_padding) data class Custom(@DrawableRes val iconRes: Int): AvatarBadge(iconRes) } diff --git a/app/src/main/res/drawable/ic_crown_custom_enlarged_no_padding.xml b/app/src/main/res/drawable/ic_crown_custom_enlarged_no_padding.xml new file mode 100644 index 0000000000..68c2a6cbd7 --- /dev/null +++ b/app/src/main/res/drawable/ic_crown_custom_enlarged_no_padding.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/layout/view_mention_candidate_v2.xml b/app/src/main/res/layout/view_mention_candidate_v2.xml index c813264b31..5c21663c97 100644 --- a/app/src/main/res/layout/view_mention_candidate_v2.xml +++ b/app/src/main/res/layout/view_mention_candidate_v2.xml @@ -25,19 +25,6 @@ app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent"/> - - - - Date: Mon, 17 Nov 2025 13:42:59 +1100 Subject: [PATCH 2/3] Avatar tweaks --- .../securesms/ui/ProComponents.kt | 6 +-- .../securesms/ui/components/Avatar.kt | 47 +------------------ 2 files changed, 5 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt index 640158f89d..a202ddd64c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/ProComponents.kt @@ -80,6 +80,7 @@ import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider import org.thoughtcrime.securesms.ui.theme.ThemeColors +import org.thoughtcrime.securesms.util.AvatarBadge import org.thoughtcrime.securesms.util.AvatarUIData @@ -750,7 +751,7 @@ fun AvatarQrWidget( label = "corner_radius" ) - // Scale animations for content + // Scale animations for content (used when going from QR back to avatar) val avatarScale by animateFloatAsState( targetValue = if (showQR) 0.8f else 1f, animationSpec = animationSpecFast, @@ -835,7 +836,6 @@ fun AvatarQrWidget( } Avatar( modifier = avatarModifier - .size(animatedSize) .graphicsLayer( alpha = avatarAlpha, scaleX = avatarScale, @@ -844,7 +844,7 @@ fun AvatarQrWidget( , size = animatedSize, maxSizeLoad = LocalDimensions.current.iconXXLargeAvatar, - data = avatarUIData + data = avatarUIData, ) // QR with scale and alpha diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt index 6266a2d493..f1e49a1d50 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/Avatar.kt @@ -104,20 +104,11 @@ fun BaseAvatar( // Badge content, if any. if (badge != null) { - var badgeSize = size * 0.4f - var offset = 0.dp - - if(badgeSize < MIN_BADGE_SIZE){ - // apply an opinionated minimum size for the badge - badgeSize = MIN_BADGE_SIZE - offset = 1.dp // the forced min size looks better with a slight offset - } - Box( modifier = Modifier .align(Alignment.BottomEnd) - .offset(x = offset, y = offset) - .size(badgeSize) + .offset(x = 1.dp, y = 1.dp) + .size(max(size * 0.4f, MIN_BADGE_SIZE)) ) { badge() } @@ -398,40 +389,6 @@ fun PreviewAvatarSinglePhoto(){ } } -@Preview -@Composable -fun PreviewAvatarBadge(){ - PreviewTheme { - Avatar( - size = LocalDimensions.current.iconLarge, - badge = AvatarBadge.Admin, - data = AvatarUIData( - listOf(AvatarUIElement( - name = "AT", - color = primaryGreen, - remoteFile = null - ))) - ) - } -} - -@Preview -@Composable -fun PreviewAvatarBadgeSmall(){ - PreviewTheme { - Avatar( - size = LocalDimensions.current.iconMediumAvatar, - badge = AvatarBadge.Admin, - data = AvatarUIData( - listOf(AvatarUIElement( - name = "AT", - color = primaryGreen, - remoteFile = null - ))) - ) - } -} - @Preview @Composable fun PreviewAvatarElementUnclipped(){ From f7984ed31a35b7cc01c3b9854436e9e89fc1a88e Mon Sep 17 00:00:00 2001 From: ThomasSession Date: Mon, 17 Nov 2025 14:16:08 +1100 Subject: [PATCH 3/3] Avoiding crash from some users when pasting an image from the clipboard --- .../thoughtcrime/securesms/util/FilenameUtils.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/FilenameUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/FilenameUtils.kt index 9476844413..b95876bac7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/FilenameUtils.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/FilenameUtils.kt @@ -92,12 +92,18 @@ object FilenameUtils { val projection = arrayOf(OpenableColumns.DISPLAY_NAME) val contentRes = context.contentResolver if (contentRes != null) { - val cursor = contentRes.query(uri, projection, null, null, null) - cursor?.use { - if (it.moveToFirst()) { - val nameIndex = it.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME) - extractedFilename = it.getString(nameIndex) + try { + val cursor = contentRes.query(uri, projection, null, null, null) + cursor?.use { + if (it.moveToFirst()) { + val nameIndex = it.getColumnIndex(OpenableColumns.DISPLAY_NAME) + if (nameIndex != -1) { + extractedFilename = it.getString(nameIndex) + } + } } + } catch (e: Exception) { + Log.w(TAG, "Unable to query display name for uri: $uri", e) } } }