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