diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/EditDeviceNameFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/EditDeviceNameFragment.kt
index 2480b2795b..edf0d0eacd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/EditDeviceNameFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/EditDeviceNameFragment.kt
@@ -72,6 +72,7 @@ class EditDeviceNameFragment : ComposeFragment() {
LinkDeviceSettingsState.OneTimeEvent.ToastNetworkFailed,
is LinkDeviceSettingsState.OneTimeEvent.ToastUnlinked,
LinkDeviceSettingsState.OneTimeEvent.LaunchEmail,
+ LinkDeviceSettingsState.OneTimeEvent.SnackbarDeviceLimitReached,
LinkDeviceSettingsState.OneTimeEvent.SnackbarLinkCancelled -> Unit
}
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt
index 3be12fcf06..34e00c1378 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceFragment.kt
@@ -26,6 +26,7 @@ import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.text.InlineTextContent
import androidx.compose.foundation.text.appendInlineContent
import androidx.compose.foundation.verticalScroll
+import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.LocalTextStyle
@@ -74,6 +75,7 @@ import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.linkdevice.LinkDeviceSettingsState.DialogState
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.DateUtils
+import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.signal.core.ui.R as CoreUiR
@@ -129,6 +131,7 @@ class LinkDeviceFragment : ComposeFragment() {
val state by viewModel.state.collectAsStateWithLifecycle()
val navController: NavController by remember { mutableStateOf(findNavController()) }
val context = LocalContext.current
+ val atDeviceLimit = state.devices.size >= RemoteConfig.maxLinkedDevices
LaunchedEffect(state.dialogState) {
when (state.dialogState) {
@@ -158,6 +161,9 @@ class LinkDeviceFragment : ComposeFragment() {
LinkDeviceSettingsState.OneTimeEvent.SnackbarLinkCancelled -> {
Snackbar.make(requireView(), getString(R.string.LinkDeviceFragment__linking_cancelled), Snackbar.LENGTH_LONG).show()
}
+ LinkDeviceSettingsState.OneTimeEvent.SnackbarDeviceLimitReached -> {
+ Snackbar.make(requireView(), getString(R.string.LinkDeviceFragment__you_can_only_have_d_linked_devices, RemoteConfig.maxLinkedDevices), Snackbar.LENGTH_LONG).show()
+ }
LinkDeviceSettingsState.OneTimeEvent.ToastNetworkFailed -> {
Toast.makeText(requireContext(), getString(R.string.DeviceListActivity_network_failed), Toast.LENGTH_LONG).show()
}
@@ -195,10 +201,15 @@ class LinkDeviceFragment : ComposeFragment() {
DeviceListScreen(
state = state,
modifier = Modifier.padding(contentPadding),
+ atDeviceLimit = atDeviceLimit,
onLearnMoreClicked = { navController.safeNavigate(R.id.action_linkDeviceFragment_to_linkDeviceLearnMoreBottomSheet) },
onLinkNewDeviceClicked = {
- viewModel.stopExistingPolling()
- navController.navigateToQrScannerIfAuthed()
+ if (atDeviceLimit) {
+ viewModel.onDeviceLimitReached()
+ } else {
+ viewModel.stopExistingPolling()
+ navController.navigateToQrScannerIfAuthed()
+ }
},
onDeviceSelectedForRemoval = { device -> viewModel.setDeviceToRemove(device) },
onDeviceRemovalConfirmed = { device -> viewModel.removeDevice(device) },
@@ -272,6 +283,7 @@ class LinkDeviceFragment : ComposeFragment() {
fun DeviceListScreen(
state: LinkDeviceSettingsState,
modifier: Modifier = Modifier,
+ atDeviceLimit: Boolean = false,
onLearnMoreClicked: () -> Unit = {},
onLinkNewDeviceClicked: () -> Unit = {},
onDeviceSelectedForRemoval: (Device?) -> Unit = {},
@@ -411,8 +423,17 @@ fun DeviceListScreen(
Spacer(modifier = Modifier.size(20.dp))
+ val linkButtonColors = ButtonDefaults.filledTonalButtonColors()
Buttons.LargeTonal(
onClick = onLinkNewDeviceClicked,
+ colors = if (atDeviceLimit) {
+ linkButtonColors.copy(
+ containerColor = linkButtonColors.disabledContainerColor,
+ contentColor = linkButtonColors.disabledContentColor
+ )
+ } else {
+ linkButtonColors
+ },
modifier = Modifier
.defaultMinSize(300.dp)
.padding(bottom = 8.dp)
diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt
index 989b8dde2d..db2e75b0ac 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceSettingsState.kt
@@ -41,6 +41,7 @@ data class LinkDeviceSettingsState(
data class ToastUnlinked(val name: String) : OneTimeEvent
data class ToastLinked(val name: String) : OneTimeEvent
data object SnackbarLinkCancelled : OneTimeEvent
+ data object SnackbarDeviceLimitReached : OneTimeEvent
data object SnackbarNameChangeSuccess : OneTimeEvent
data object SnackbarNameChangeFailure : OneTimeEvent
data object ShowFinishedSheet : OneTimeEvent
diff --git a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt
index 13f7cce342..5df79869ee 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/linkdevice/LinkDeviceViewModel.kt
@@ -152,6 +152,10 @@ class LinkDeviceViewModel : ViewModel() {
pollJob?.cancel()
}
+ fun onDeviceLimitReached() {
+ _state.update { it.copy(oneTimeEvent = OneTimeEvent.SnackbarDeviceLimitReached) }
+ }
+
fun markQrEducationSheetSeen() {
SignalStore.uiHints.markHasSeenLinkDeviceQrEducationSheet()
_state.update {
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt
index 105420e1c6..e69755135b 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt
@@ -605,6 +605,15 @@ object RemoteConfig {
hotSwappable = true
)
+ /** The maximum number of linked devices a user can have. */
+ @JvmStatic
+ @get:JvmName("maxLinkedDevices")
+ val maxLinkedDevices: Int by remoteInt(
+ key = "global.maxLinkedDevices",
+ defaultValue = 5,
+ hotSwappable = true
+ )
+
/** The maximum number of grapheme */
@JvmStatic
val maxGroupNameGraphemeLength: Int by remoteValue(
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index c8337d50a7..137176592c 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1122,6 +1122,8 @@
Edit name
Linking cancelled
+
+ You can only have %1$d linked devices.
Do not close app