diff --git a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt
index e2eb054880..0df53ef53c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/MainActivity.kt
@@ -70,10 +70,10 @@ import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar.show
import org.thoughtcrime.securesms.calls.log.CallLogFilter
import org.thoughtcrime.securesms.calls.log.CallLogFragment
import org.thoughtcrime.securesms.calls.new.NewCallActivity
-import org.thoughtcrime.securesms.components.ConnectivityWarningBottomSheet
import org.thoughtcrime.securesms.components.DebugLogsPromptDialogFragment
-import org.thoughtcrime.securesms.components.DeviceSpecificNotificationBottomSheet
import org.thoughtcrime.securesms.components.PromptBatterySaverDialogFragment
+import org.thoughtcrime.securesms.components.compose.ConnectivityWarningBottomSheet
+import org.thoughtcrime.securesms.components.compose.DeviceSpecificNotificationBottomSheet
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity
import org.thoughtcrime.securesms.components.settings.app.AppSettingsActivity.Companion.manageSubscriptions
import org.thoughtcrime.securesms.components.settings.app.notifications.manual.NotificationProfileSelectionFragment
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConnectivityWarningBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/compose/ConnectivityWarningBottomSheet.kt
similarity index 96%
rename from app/src/main/java/org/thoughtcrime/securesms/components/ConnectivityWarningBottomSheet.kt
rename to app/src/main/java/org/thoughtcrime/securesms/components/compose/ConnectivityWarningBottomSheet.kt
index 12ef0aa067..845bb9e6d3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/ConnectivityWarningBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/compose/ConnectivityWarningBottomSheet.kt
@@ -1,4 +1,9 @@
-package org.thoughtcrime.securesms.components
+/*
+ * Copyright 2025 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.components.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/components/compose/DeleteSyncEducationDialog.kt
similarity index 98%
rename from app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt
rename to app/src/main/java/org/thoughtcrime/securesms/components/compose/DeleteSyncEducationDialog.kt
index f2d994cafb..9410cb6dd9 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/DeleteSyncEducationDialog.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/compose/DeleteSyncEducationDialog.kt
@@ -3,7 +3,7 @@
* SPDX-License-Identifier: AGPL-3.0-only
*/
-package org.thoughtcrime.securesms.components
+package org.thoughtcrime.securesms.components.compose
import android.content.DialogInterface
import android.os.Bundle
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt b/app/src/main/java/org/thoughtcrime/securesms/components/compose/DeviceSpecificNotificationBottomSheet.kt
similarity index 97%
rename from app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt
rename to app/src/main/java/org/thoughtcrime/securesms/components/compose/DeviceSpecificNotificationBottomSheet.kt
index f96b80ad58..b431af6e26 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/DeviceSpecificNotificationBottomSheet.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/compose/DeviceSpecificNotificationBottomSheet.kt
@@ -1,4 +1,9 @@
-package org.thoughtcrime.securesms.components
+/*
+ * Copyright 2025 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.components.compose
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/compose/RoundCheckbox.kt b/app/src/main/java/org/thoughtcrime/securesms/components/compose/RoundCheckbox.kt
new file mode 100644
index 0000000000..f422ad1e9d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/compose/RoundCheckbox.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright 2025 Signal Messenger, LLC
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+package org.thoughtcrime.securesms.components.compose
+
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.scaleIn
+import androidx.compose.animation.scaleOut
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.border
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.aspectRatio
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.size
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.ColorFilter
+import androidx.compose.ui.graphics.vector.ImageVector
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.res.vectorResource
+import androidx.compose.ui.semantics.Role
+import androidx.compose.ui.semantics.contentDescription
+import androidx.compose.ui.semantics.role
+import androidx.compose.ui.semantics.semantics
+import androidx.compose.ui.unit.dp
+import org.signal.core.ui.compose.SignalPreview
+import org.signal.core.ui.compose.theme.SignalTheme
+import org.thoughtcrime.securesms.R
+
+/**
+ * A custom circular [Checkbox] that can be toggled between checked an unchecked states.
+ *
+ * @param checked Indicates whether the checkbox is checked or not.
+ * @param onCheckedChange A callback function invoked when this checkbox is clicked.
+ * @param modifier The [Modifier] to be applied to this checkbox.
+ */
+@Composable
+fun RoundCheckbox(
+ checked: Boolean,
+ onCheckedChange: (Boolean) -> Unit,
+ modifier: Modifier = Modifier
+) {
+ val contentDescription = if (checked) {
+ stringResource(R.string.SignalCheckbox_accessibility_checked_description)
+ } else {
+ stringResource(R.string.SignalCheckbox_accessibility_unchecked_description)
+ }
+
+ Box(
+ modifier = modifier
+ .padding(12.dp)
+ .size(24.dp)
+ .aspectRatio(1f)
+ .border(
+ width = 1.5.dp,
+ color = if (checked) {
+ MaterialTheme.colorScheme.primary
+ } else {
+ MaterialTheme.colorScheme.outline
+ },
+ shape = CircleShape
+ )
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = null,
+ onClick = { onCheckedChange(!checked) },
+ onClickLabel = stringResource(R.string.SignalCheckbox_accessibility_on_click_label)
+ )
+ .semantics(mergeDescendants = true) {
+ this.role = Role.Checkbox
+ this.contentDescription = contentDescription
+ }
+ ) {
+ AnimatedVisibility(
+ visible = checked,
+ enter = fadeIn(animationSpec = tween(durationMillis = 150)) + scaleIn(initialScale = 1.20f, animationSpec = tween(durationMillis = 500)),
+ exit = fadeOut(animationSpec = tween(durationMillis = 300)) + scaleOut(targetScale = 0.50f, animationSpec = tween(durationMillis = 600))
+ ) {
+ Image(
+ imageVector = ImageVector.vectorResource(id = R.drawable.ic_check_circle_solid_24),
+ colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.primary),
+ contentDescription = null,
+ modifier = Modifier.fillMaxSize()
+ )
+ }
+ }
+}
+
+@SignalPreview
+@Composable
+private fun RoundCheckboxCheckedPreview() = SignalTheme {
+ RoundCheckbox(checked = true, onCheckedChange = {})
+}
+
+@SignalPreview
+@Composable
+private fun RoundCheckboxUncheckedPreview() = SignalTheme {
+ RoundCheckbox(checked = false, onCheckedChange = {})
+}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt
index 69d47b4f93..39ae5a0249 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/backup/InternalBackupPlaygroundFragment.kt
@@ -28,7 +28,6 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
-import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.DropdownMenuItem
@@ -83,6 +82,7 @@ import org.signal.core.util.getLength
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentId
import org.thoughtcrime.securesms.backup.v2.MessageBackupTier
+import org.thoughtcrime.securesms.components.compose.RoundCheckbox
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.DialogState
import org.thoughtcrime.securesms.components.settings.app.internal.backup.InternalBackupPlaygroundViewModel.ScreenState
import org.thoughtcrime.securesms.compose.ComposeFragment
@@ -629,7 +629,7 @@ fun MediaList(
.padding(horizontal = 16.dp, vertical = 8.dp)
) {
if (selectionState.selecting) {
- Checkbox(
+ RoundCheckbox(
checked = selectionState.selected.contains(attachment.id),
onCheckedChange = { selected ->
selectionState = selectionState.copy(selected = if (selected) selectionState.selected + attachment.id else selectionState.selected - attachment.id)
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 21feed90ec..c3cf736f83 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
@@ -122,7 +122,6 @@ import org.thoughtcrime.securesms.calls.YouAreAlreadyInACallSnackbar
import org.thoughtcrime.securesms.components.AnimatingToggle
import org.thoughtcrime.securesms.components.ComposeText
import org.thoughtcrime.securesms.components.ConversationSearchBottomBar
-import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog
import org.thoughtcrime.securesms.components.HidingLinearLayout
import org.thoughtcrime.securesms.components.InputAwareConstraintLayout
import org.thoughtcrime.securesms.components.InputPanel
@@ -130,6 +129,7 @@ import org.thoughtcrime.securesms.components.InsetAwareConstraintLayout
import org.thoughtcrime.securesms.components.ScrollToPositionDelegate
import org.thoughtcrime.securesms.components.SendButton
import org.thoughtcrime.securesms.components.ViewBinderDelegate
+import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog
import org.thoughtcrime.securesms.components.emoji.EmojiEventListener
import org.thoughtcrime.securesms.components.emoji.MediaKeyboard
import org.thoughtcrime.securesms.components.emoji.RecentEmojiPageModel
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
index 3510e5906e..a30f9d0d45 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java
@@ -98,7 +98,7 @@ import org.thoughtcrime.securesms.banner.banners.OutdatedBuildBanner;
import org.thoughtcrime.securesms.banner.banners.ServiceOutageBanner;
import org.thoughtcrime.securesms.banner.banners.UnauthorizedBanner;
import org.thoughtcrime.securesms.banner.banners.UsernameOutOfSyncBanner;
-import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog;
+import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog;
import org.thoughtcrime.securesms.components.RatingManager;
import org.thoughtcrime.securesms.components.SignalProgressDialog;
import org.thoughtcrime.securesms.components.menu.ActionItem;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java
index 45e68902cf..a41fac5715 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediaoverview/MediaOverviewPageFragment.java
@@ -36,7 +36,7 @@ import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.LoggingFragment;
import org.thoughtcrime.securesms.R;
import org.thoughtcrime.securesms.attachments.DatabaseAttachment;
-import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog;
+import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog;
import org.thoughtcrime.securesms.components.menu.ActionItem;
import org.thoughtcrime.securesms.components.menu.SignalBottomActionBar;
import org.thoughtcrime.securesms.components.voice.VoiceNoteMediaController;
diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt
index 0b85bf2212..ac659f80dc 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/mediapreview/MediaPreviewV2Fragment.kt
@@ -47,8 +47,8 @@ import org.thoughtcrime.securesms.LoggingFragment
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.attachments.AttachmentSaver
import org.thoughtcrime.securesms.attachments.DatabaseAttachment
-import org.thoughtcrime.securesms.components.DeleteSyncEducationDialog
import org.thoughtcrime.securesms.components.ViewBinderDelegate
+import org.thoughtcrime.securesms.components.compose.DeleteSyncEducationDialog
import org.thoughtcrime.securesms.components.mention.MentionAnnotation
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragment
import org.thoughtcrime.securesms.conversation.mutiselect.forward.MultiselectForwardFragmentArgs
diff --git a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackListItems.kt b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackListItems.kt
index e9ed7c81ed..ecffeb1874 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackListItems.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/stickers/StickerPackListItems.kt
@@ -19,7 +19,6 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -37,6 +36,7 @@ import org.signal.core.ui.compose.SignalPreview
import org.signal.core.ui.compose.theme.SignalTheme
import org.signal.core.util.nullIfBlank
import org.thoughtcrime.securesms.R
+import org.thoughtcrime.securesms.components.compose.RoundCheckbox
import org.thoughtcrime.securesms.components.transfercontrols.TransferProgressIndicator
import org.thoughtcrime.securesms.components.transfercontrols.TransferProgressState
import org.thoughtcrime.securesms.compose.GlideImage
@@ -158,14 +158,14 @@ fun InstalledStickerPackRow(
color = if (selected) SignalTheme.colors.colorSurface2 else MaterialTheme.colorScheme.surface,
shape = RoundedCornerShape(18.dp)
)
- .padding(vertical = 10.dp)
+ .padding(horizontal = 4.dp, vertical = 10.dp)
) {
AnimatedVisibility(
visible = multiSelectEnabled,
enter = fadeIn() + expandHorizontally(),
exit = fadeOut() + shrinkHorizontally()
) {
- Checkbox(
+ RoundCheckbox(
checked = selected,
onCheckedChange = { onSelectionToggle(pack) },
modifier = Modifier.padding(end = 8.dp)
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b219b64a8c..1bb2b1c0d5 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -8498,5 +8498,12 @@
Navigate up
+
+ Toggle
+
+ Ticked
+
+ Unticked
+