Update linked devices screen.

This commit is contained in:
Michelle Tang
2024-06-05 17:17:03 -07:00
committed by Alex Hart
parent 5c181e774f
commit ac52b5b992
21 changed files with 911 additions and 11 deletions

View File

@@ -160,7 +160,11 @@ class AppSettingsFragment : DSLSettingsFragment(
title = DSLSettingsText.from(R.string.preferences__linked_devices),
icon = DSLSettingsIcon.from(R.drawable.symbol_devices_24),
onClick = {
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_deviceActivity)
if (FeatureFlags.linkedDevicesV2()) {
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_linkDeviceFragment)
} else {
findNavController().safeNavigate(R.id.action_appSettingsFragment_to_deviceActivity)
}
},
isEnabled = state.isRegisteredAndUpToDate()
)

View File

@@ -0,0 +1,6 @@
package org.thoughtcrime.securesms.linkdevice
/**
* Class that represents a linked device
*/
data class Device(val id: Long, val name: String, val createdMillis: Long, val lastSeenMillis: Long)

View File

@@ -0,0 +1,242 @@
package org.thoughtcrime.securesms.linkdevice
import android.widget.Toast
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.fragment.app.viewModels
import androidx.navigation.fragment.findNavController
import org.signal.core.ui.Buttons
import org.signal.core.ui.Dialogs
import org.signal.core.ui.Dividers
import org.signal.core.ui.Previews
import org.signal.core.ui.Scaffolds
import org.signal.core.ui.SignalPreview
import org.thoughtcrime.securesms.DeviceActivity
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeFragment
import org.thoughtcrime.securesms.util.BottomSheetUtil
import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
/**
* Fragment that shows current linked devices
*/
class LinkDeviceFragment : ComposeFragment() {
private val viewModel: LinkDeviceViewModel by viewModels()
@Composable
override fun FragmentContent() {
val state by viewModel.state
LaunchedEffect(state.toastDialog) {
if (state.toastDialog.isNotEmpty()) {
Toast.makeText(requireContext(), state.toastDialog, Toast.LENGTH_LONG).show()
}
}
Scaffolds.Settings(
title = stringResource(id = R.string.preferences__linked_devices),
onNavigationClick = { findNavController().popBackStack() },
navigationIconPainter = painterResource(id = R.drawable.ic_arrow_left_24),
navigationContentDescription = stringResource(id = R.string.Material3SearchToolbar__close)
) { contentPadding: PaddingValues ->
DeviceDescriptionScreen(
state = state,
modifier = Modifier.padding(contentPadding),
onLearnMore = this::openLearnMore,
onLinkDevice = this::openLinkNewDevice,
setDeviceToRemove = this::setDeviceToRemove,
onRemoveDevice = this::onRemoveDevice
)
}
}
override fun onResume() {
super.onResume()
viewModel.loadDevices(requireContext())
}
private fun openLearnMore() {
LinkDeviceLearnMoreBottomSheetFragment().show(childFragmentManager, BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)
}
private fun openLinkNewDevice() {
// TODO(Michelle): Use linkDeviceAddFragment
startActivity(DeviceActivity.getIntentForScanner(requireContext()))
// findNavController().safeNavigate(R.id.action_linkDeviceFragment_to_linkDeviceAddFragment)
}
private fun setDeviceToRemove(device: Device?) {
viewModel.setDeviceToRemove(device)
}
private fun onRemoveDevice(device: Device) {
viewModel.removeDevice(requireContext(), device)
}
}
@Composable
fun DeviceDescriptionScreen(
state: LinkDeviceSettingsState,
modifier: Modifier = Modifier,
onLearnMore: () -> Unit = {},
onLinkDevice: () -> Unit = {},
setDeviceToRemove: (Device?) -> Unit = {},
onRemoveDevice: (Device) -> Unit = {}
) {
if (state.progressDialogMessage != -1) {
Dialogs.IndeterminateProgressDialog(stringResource(id = state.progressDialogMessage))
}
if (state.deviceToRemove != null) {
val device: Device = state.deviceToRemove
Dialogs.SimpleAlertDialog(
title = stringResource(id = R.string.DeviceListActivity_unlink_s, device.name),
body = stringResource(id = R.string.DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive),
confirm = stringResource(R.string.LinkDeviceFragment__unlink),
dismiss = stringResource(android.R.string.cancel),
onConfirm = { onRemoveDevice(device) },
onDismiss = { setDeviceToRemove(null) }
)
}
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = modifier.verticalScroll(rememberScrollState())) {
Icon(
painter = painterResource(R.drawable.ic_devices_intro),
contentDescription = stringResource(R.string.preferences__linked_devices),
tint = Color.Unspecified
)
Text(
text = stringResource(id = R.string.LinkDeviceFragment__use_signal_on_desktop_ipad),
textAlign = TextAlign.Center,
modifier = Modifier.padding(start = 20.dp, end = 20.dp, top = 12.dp, bottom = 4.dp)
)
ClickableText(
text = AnnotatedString(stringResource(id = R.string.LearnMoreTextView_learn_more)),
style = TextStyle(color = MaterialTheme.colorScheme.primary)
) {
onLearnMore()
}
Spacer(modifier = Modifier.size(20.dp))
Buttons.LargeTonal(
onClick = onLinkDevice,
modifier = Modifier.width(300.dp)
) {
Text(stringResource(id = R.string.LinkDeviceFragment__link_a_new_device))
}
if (state.devices.isNotEmpty()) {
Dividers.Default()
Column {
Text(
text = stringResource(R.string.LinkDeviceFragment__my_linked_devices),
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(start = 24.dp, top = 12.dp, bottom = 24.dp)
)
state.devices.forEach { device ->
DeviceRow(device, setDeviceToRemove)
}
}
}
Row(
modifier = Modifier.padding(horizontal = 40.dp, vertical = 12.dp),
verticalAlignment = Alignment.CenterVertically
) {
Spacer(modifier = Modifier.size(12.dp))
Icon(
painter = painterResource(R.drawable.symbol_lock_24),
contentDescription = null
)
Text(
style = MaterialTheme.typography.bodySmall,
textAlign = TextAlign.Center,
text = stringResource(id = R.string.LinkDeviceFragment__messages_and_chat_info_are_protected)
)
}
}
}
@Composable
fun DeviceRow(device: Device, setDeviceToRemove: (Device) -> Unit) {
val titleString = device.name.ifEmpty { stringResource(R.string.DeviceListItem_unnamed_device) }
val linkedDate = DateUtils.getDayPrecisionTimeSpanString(LocalContext.current, Locale.getDefault(), device.createdMillis)
val lastActive = DateUtils.getDayPrecisionTimeSpanString(LocalContext.current, Locale.getDefault(), device.lastSeenMillis)
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { setDeviceToRemove(device) },
verticalAlignment = Alignment.CenterVertically
) {
Image(
painter = painterResource(id = R.drawable.symbol_devices_24),
contentDescription = null,
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onSurface),
contentScale = ContentScale.Inside,
modifier = Modifier
.padding(start = 24.dp)
.size(40.dp)
.background(
color = MaterialTheme.colorScheme.surfaceVariant,
shape = CircleShape
)
)
Spacer(modifier = Modifier.size(20.dp))
Column {
Text(text = titleString, style = MaterialTheme.typography.bodyLarge)
Spacer(modifier = Modifier.size(4.dp))
Text(stringResource(R.string.DeviceListItem_linked_s, linkedDate), style = MaterialTheme.typography.bodyMedium)
Text(stringResource(R.string.DeviceListItem_last_active_s, lastActive), style = MaterialTheme.typography.bodyMedium)
}
}
Spacer(modifier = Modifier.size(16.dp))
}
@SignalPreview
@Composable
private fun DeviceScreenPreview() {
val previewDevices = listOf(
Device(1, "Sam's Macbook Pro", 1715793982000, 1716053182000),
Device(1, "Sam's iPad", 1715793182000, 1716053122000)
)
val previewState = LinkDeviceSettingsState(devices = previewDevices)
Previews.Preview {
DeviceDescriptionScreen(previewState)
}
}

View File

@@ -0,0 +1,122 @@
package org.thoughtcrime.securesms.linkdevice
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.wrapContentSize
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.signal.core.ui.BottomSheets
import org.signal.core.ui.Previews
import org.signal.core.ui.SignalPreview
import org.signal.core.ui.Texts
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.compose.ComposeBottomSheetDialogFragment
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SpanUtil
/**
* Bottom sheet dialog displayed when users click 'Learn more' when linking a device
*/
class LinkDeviceLearnMoreBottomSheetFragment : ComposeBottomSheetDialogFragment() {
override val peekHeightPercentage: Float = 0.8f
companion object {
const val SIGNAL_DOWNLOAD_URL = "https://signal.org/download"
}
@Composable
override fun SheetContent() {
LearnMoreSheet()
}
}
@Composable
fun LearnMoreSheet() {
val context = LocalContext.current
val downloadUrl = stringResource(id = R.string.LinkDeviceFragment__signal_download_url)
val fullString = stringResource(id = R.string.LinkDeviceFragment__on_other_device_visit_signal, downloadUrl)
val spanned = SpanUtil.urlSubsequence(fullString, downloadUrl, LinkDeviceLearnMoreBottomSheetFragment.SIGNAL_DOWNLOAD_URL)
return Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.wrapContentSize(Alignment.Center)
.padding(bottom = 48.dp)
) {
BottomSheets.Handle()
Icon(
painter = painterResource(R.drawable.ic_all_devices),
contentDescription = null,
tint = Color.Unspecified,
modifier = Modifier.size(110.dp)
)
Text(
style = MaterialTheme.typography.titleLarge,
text = stringResource(R.string.LinkDeviceFragment__signal_on_desktop_ipad),
modifier = Modifier.padding(horizontal = 32.dp, vertical = 8.dp)
)
LinkedDeviceInformationRow(
painterResource(R.drawable.symbol_lock_24),
stringResource(R.string.LinkDeviceFragment__all_messaging_is_private)
)
LinkedDeviceInformationRow(
painterResource(R.drawable.ic_replies_outline_20),
stringResource(R.string.LinkDeviceFragment__signal_messages_are_synchronized)
)
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp, start = 40.dp, end = 32.dp)) {
Icon(
painter = painterResource(R.drawable.symbol_save_android_24),
contentDescription = stringResource(R.string.preferences__linked_devices),
modifier = Modifier.size(24.dp).padding(top = 4.dp)
)
Texts.LinkifiedText(
textWithUrlSpans = spanned,
onUrlClick = { CommunicationActions.openBrowserLink(context, it) },
style = MaterialTheme.typography.bodyLarge.copy(color = MaterialTheme.colorScheme.onSurface),
modifier = Modifier.padding(start = 20.dp)
)
}
}
}
@Composable
private fun LinkedDeviceInformationRow(
iconPainter: Painter,
text: String
) {
Row(modifier = Modifier.fillMaxWidth().padding(bottom = 12.dp, start = 40.dp, end = 32.dp)) {
Icon(
painter = iconPainter,
contentDescription = null,
modifier = Modifier.size(24.dp).padding(top = 4.dp)
)
Text(
style = MaterialTheme.typography.bodyLarge,
text = text,
modifier = Modifier.padding(start = 20.dp)
)
}
}
@SignalPreview
@Composable
fun LearnMorePreview() {
Previews.BottomSheetPreview {
LearnMoreSheet()
}
}

View File

@@ -0,0 +1,74 @@
package org.thoughtcrime.securesms.linkdevice
import org.signal.core.util.Base64.decode
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.devicelist.protos.DeviceName
import org.thoughtcrime.securesms.jobs.LinkedDeviceInactiveCheckJob
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.secondary.DeviceNameCipher
import org.whispersystems.signalservice.api.messages.multidevice.DeviceInfo
import org.whispersystems.signalservice.api.push.SignalServiceAddress
import java.io.IOException
/**
* Repository for linked devices and its various actions (linking, unlinking, listing).
*/
object LinkDeviceRepository {
private val TAG = Log.tag(LinkDeviceRepository::class)
fun removeDevice(deviceId: Long): Boolean {
return try {
val accountManager = AppDependencies.signalServiceAccountManager
accountManager.removeDevice(deviceId)
LinkedDeviceInactiveCheckJob.enqueue()
true
} catch (e: IOException) {
Log.w(TAG, e)
false
}
}
fun loadDevices(): List<Device>? {
val accountManager = AppDependencies.signalServiceAccountManager
return try {
val devices: List<Device> = accountManager.getDevices()
.filter { d: DeviceInfo -> d.getId() != SignalServiceAddress.DEFAULT_DEVICE_ID }
.map { deviceInfo: DeviceInfo -> deviceInfo.toDevice() }
.sortedBy { it.createdMillis }
.toList()
devices
} catch (e: IOException) {
Log.w(TAG, e)
null
}
}
private fun DeviceInfo.toDevice(): Device {
val defaultDevice = Device(getId().toLong(), getName(), getCreated(), getLastSeen())
try {
if (getName().isNullOrEmpty() || getName().length < 4) {
Log.w(TAG, "Invalid DeviceInfo name.")
return defaultDevice
}
val deviceName = DeviceName.ADAPTER.decode(decode(getName()))
if (deviceName.ciphertext == null || deviceName.ephemeralPublic == null || deviceName.syntheticIv == null) {
Log.w(TAG, "Got a DeviceName that wasn't properly populated.")
return defaultDevice
}
val plaintext = DeviceNameCipher.decryptDeviceName(deviceName, SignalStore.account().aciIdentityKey)
if (plaintext == null) {
Log.w(TAG, "Failed to decrypt device name.")
return defaultDevice
}
return Device(getId().toLong(), String(plaintext), getCreated(), getLastSeen())
} catch (e: Exception) {
Log.w(TAG, "Failed while reading the protobuf.", e)
}
return defaultDevice
}
}

View File

@@ -0,0 +1,13 @@
package org.thoughtcrime.securesms.linkdevice
import androidx.annotation.StringRes
/**
* Information about linked devices. Used in [LinkDeviceViewModel].
*/
data class LinkDeviceSettingsState(
val devices: List<Device> = emptyList(),
val deviceToRemove: Device? = null,
@StringRes val progressDialogMessage: Int = -1,
val toastDialog: String = ""
)

View File

@@ -0,0 +1,64 @@
package org.thoughtcrime.securesms.linkdevice
import android.content.Context
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.thoughtcrime.securesms.R
/**
* Maintains the state of the [LinkDeviceFragment]
*/
class LinkDeviceViewModel : ViewModel() {
private val _state = mutableStateOf(LinkDeviceSettingsState())
val state: State<LinkDeviceSettingsState> = _state
fun onResume() {
_state.value = _state.value.copy()
}
fun setDeviceToRemove(device: Device?) {
_state.value = _state.value.copy(deviceToRemove = device)
}
fun removeDevice(context: Context, device: Device) {
viewModelScope.launch(Dispatchers.IO) {
_state.value = _state.value.copy(
progressDialogMessage = R.string.DeviceListActivity_unlinking_device
)
val success = LinkDeviceRepository.removeDevice(device.id)
if (success) {
loadDevices(context)
_state.value = _state.value.copy(
toastDialog = context.getString(R.string.LinkDeviceFragment__s_unlinked, device.name),
progressDialogMessage = -1
)
} else {
_state.value = _state.value.copy(
progressDialogMessage = -1
)
}
}
}
fun loadDevices(context: Context) {
viewModelScope.launch(Dispatchers.IO) {
val devices = LinkDeviceRepository.loadDevices()
if (devices == null) {
_state.value = _state.value.copy(
toastDialog = context.getString(R.string.DeviceListActivity_network_failed),
progressDialogMessage = -1
)
} else {
_state.value = _state.value.copy(
devices = devices,
progressDialogMessage = -1
)
}
}
}
}

View File

@@ -210,13 +210,7 @@ object DateUtils : android.text.format.DateUtils() {
return if (isSameDay(System.currentTimeMillis(), timestamp)) {
context.getString(R.string.DeviceListItem_today)
} else {
val format: String = when {
timestamp.isWithin(6.days) -> "EEE "
timestamp.isWithin(365.days) -> "MMM d"
else -> "MMM d, yyy"
}
timestamp.toDateString(format, locale)
timestamp.toDateString("dd/MM/yy", locale)
}
}

View File

@@ -129,6 +129,7 @@ public final class FeatureFlags {
private static final String RESTORE_POST_REGISTRATION = "android.registration.restorePostRegistration";
private static final String LIBSIGNAL_WEB_SOCKET_SHADOW_PCT = "android.libsignalWebSocketShadowingPercentage";
private static final String DELETE_SYNC_SEND_RECEIVE = "android.deleteSyncSendReceive";
private static final String LINKED_DEVICES_V2 = "android.linkedDevices.v2";
/**
* We will only store remote values for flags in this set. If you want a flag to be controllable
@@ -211,7 +212,7 @@ public final class FeatureFlags {
);
@VisibleForTesting
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2, RESTORE_POST_REGISTRATION);
static final Set<String> NOT_REMOTE_CAPABLE = SetUtil.newHashSet(MESSAGE_BACKUPS, REGISTRATION_V2, RESTORE_POST_REGISTRATION, LINKED_DEVICES_V2);
/**
* Values in this map will take precedence over any value. This should only be used for local
@@ -750,6 +751,11 @@ public final class FeatureFlags {
return getBoolean(DELETE_SYNC_SEND_RECEIVE, false);
}
/** Whether or not to use V2 of linked devices. */
public static boolean linkedDevicesV2() {
return getBoolean(LINKED_DEVICES_V2, false);
}
/** Only for rendering debug info. */
public static synchronized @NonNull Map<String, Object> getMemoryValues() {
return new TreeMap<>(REMOTE_VALUES);

View File

@@ -0,0 +1,45 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="112dp"
android:height="112dp"
android:viewportWidth="112"
android:viewportHeight="112">
<path
android:pathData="M15,31C15,26.58 18.58,23 23,23H71C75.42,23 79,26.58 79,31V71C79,75.42 75.42,79 71,79H23C18.58,79 15,75.42 15,71V31Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M14,31C14,26.03 18.03,22 23,22H71C75.97,22 80,26.03 80,31V71C80,75.97 75.97,80 71,80H23C18.03,80 14,75.97 14,71V31ZM23,24C19.13,24 16,27.13 16,31V71C16,74.87 19.13,78 23,78H71C74.87,78 78,74.87 78,71V31C78,27.13 74.87,24 71,24H23Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M78,71V34.31C76.03,34.11 74.03,34 72,34C47.56,34 26.71,49.39 18.61,71H78Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M5,75C5,72.79 6.79,71 9,71H85C87.21,71 89,72.79 89,75V75C89,77.21 87.21,79 85,79H9C6.79,79 5,77.21 5,75V75Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M4,75C4,72.24 6.24,70 9,70H85C87.76,70 90,72.24 90,75C90,77.76 87.76,80 85,80H9C6.24,80 4,77.76 4,75ZM9,72C7.34,72 6,73.34 6,75C6,76.66 7.34,78 9,78H85C86.66,78 88,76.66 88,75C88,73.34 86.66,72 85,72H9Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M71,37C71,33.69 73.69,31 77,31H101C104.31,31 107,33.69 107,37V73C107,76.31 104.31,79 101,79H77C73.69,79 71,76.31 71,73V37Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M107,53.34V73.5C107,76.81 104.31,79.5 101,79.5H77C73.69,79.5 71,76.81 71,73.5V53.34C76.18,49.96 82.36,48 89,48C95.64,48 101.82,49.96 107,53.34Z"
android:fillColor="#C1C7FE"
android:fillType="evenOdd"/>
<path
android:pathData="M70,37C70,33.13 73.13,30 77,30H101C104.87,30 108,33.13 108,37V73C108,76.87 104.87,80 101,80H77C73.13,80 70,76.87 70,73V37ZM77,32C74.24,32 72,34.24 72,37V73C72,75.76 74.24,78 77,78H101C103.76,78 106,75.76 106,73V37C106,34.24 103.76,32 101,32H77Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M56,57C56,53.69 58.69,51 62,51H74C77.31,51 80,53.69 80,57V85.5C80,88.81 77.31,91.5 74,91.5H62C58.69,91.5 56,88.81 56,85.5V57Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M80,70.56V85.5C80,88.81 77.31,91.5 74,91.5H62C58.69,91.5 56,88.81 56,85.5V70.56C59.45,68.31 63.57,67 68,67C72.43,67 76.55,68.31 80,70.56Z"
android:fillColor="#C1C7FE"
android:fillType="evenOdd"/>
<path
android:pathData="M55,57C55,53.13 58.13,50 62,50H74C77.87,50 81,53.13 81,57V85.5C81,89.37 77.87,92.5 74,92.5H62C58.13,92.5 55,89.37 55,85.5V57ZM62,52C59.24,52 57,54.24 57,57V85.5C57,88.26 59.24,90.5 62,90.5H74C76.76,90.5 79,88.26 79,85.5V57C79,54.24 76.76,52 74,52H62Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="112dp"
android:height="112dp"
android:viewportWidth="112"
android:viewportHeight="112">
<path
android:pathData="M20,31C20,26.58 23.58,23 28,23H76C80.42,23 84,26.58 84,31V71C84,75.42 80.42,79 76,79H28C23.58,79 20,75.42 20,71V31Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M19,31C19,26.03 23.03,22 28,22H76C80.97,22 85,26.03 85,31V71C85,75.97 80.97,80 76,80H28C23.03,80 19,75.97 19,71V31ZM28,24C24.13,24 21,27.13 21,31V71C21,74.87 24.13,78 28,78H76C79.87,78 83,74.87 83,71V31C83,27.13 79.87,24 76,24H28Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M83,71V34.31C81.03,34.11 79.03,34 77,34C52.56,34 31.71,49.39 23.61,71H83Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M10,75C10,72.79 11.79,71 14,71H90C92.21,71 94,72.79 94,75V75C94,77.21 92.21,79 90,79H14C11.79,79 10,77.21 10,75V75Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M9,75C9,72.24 11.24,70 14,70H90C92.76,70 95,72.24 95,75C95,77.76 92.76,80 90,80H14C11.24,80 9,77.76 9,75ZM14,72C12.34,72 11,73.34 11,75C11,76.66 12.34,78 14,78H90C91.66,78 93,76.66 93,75C93,73.34 91.66,72 90,72H14Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M66,47C66,43.69 68.69,41 72,41H96C99.31,41 102,43.69 102,47V83C102,86.31 99.31,89 96,89H72C68.69,89 66,86.31 66,83V47Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M102,63.34V83.5C102,86.81 99.31,89.5 96,89.5H72C68.69,89.5 66,86.81 66,83.5V63.34C71.18,59.96 77.36,58 84,58C90.64,58 96.82,59.96 102,63.34Z"
android:fillColor="#C1C7FE"
android:fillType="evenOdd"/>
<path
android:pathData="M65,47C65,43.13 68.13,40 72,40H96C99.87,40 103,43.13 103,47V83C103,86.87 99.87,90 96,90H72C68.13,90 65,86.87 65,83V47ZM72,42C69.24,42 67,44.24 67,47V83C67,85.76 69.24,88 72,88H96C98.76,88 101,85.76 101,83V47C101,44.24 98.76,42 96,42H72Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,66 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="184dp"
android:height="112dp"
android:viewportWidth="184"
android:viewportHeight="112">
<path
android:pathData="M21,25C21,18.37 26.37,13 33,13H105C111.63,13 117,18.37 117,25V85C117,91.63 111.63,97 105,97H33C26.37,97 21,91.63 21,85V25Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M20,25C20,17.82 25.82,12 33,12H105C112.18,12 118,17.82 118,25V85C118,92.18 112.18,98 105,98H33C25.82,98 20,92.18 20,85V25ZM33,14C26.92,14 22,18.92 22,25V85C22,91.08 26.92,96 33,96H105C111.07,96 116,91.08 116,85V25C116,18.92 111.07,14 105,14H33Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M116,85V26.53C112.85,26.18 109.65,26 106.41,26C68.35,26 36.07,50.75 24.86,85H116Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M6,91C6,87.69 8.69,85 12,85H126C129.31,85 132,87.69 132,91V91C132,94.31 129.31,97 126,97H12C8.69,97 6,94.31 6,91V91Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M5,91C5,87.13 8.13,84 12,84H126C129.87,84 133,87.13 133,91C133,94.87 129.87,98 126,98H12C8.13,98 5,94.87 5,91ZM12,86C9.24,86 7,88.24 7,91C7,93.76 9.24,96 12,96H126C128.76,96 131,93.76 131,91C131,88.24 128.76,86 126,86H12Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M86,45C86,41.69 88.69,39 92,39H129C132.31,39 135,41.69 135,45V50C135,53.31 132.31,56 129,56H92C88.69,56 86,53.31 86,50V45Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M85,45C85,41.13 88.13,38 92,38H129C132.87,38 136,41.13 136,45V50C136,53.87 132.87,57 129,57H92C88.13,57 85,53.87 85,50V45ZM92,40C89.24,40 87,42.24 87,45V50C87,52.76 89.24,55 92,55H129C131.76,55 134,52.76 134,50V45C134,42.24 131.76,40 129,40H92Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M106.5,24.5C106.5,20.63 109.63,17.5 113.5,17.5H148.5C152.37,17.5 155.5,20.63 155.5,24.5V24.5C155.5,28.37 152.37,31.5 148.5,31.5H113.5C109.63,31.5 106.5,28.37 106.5,24.5V24.5Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M105.5,24.5C105.5,20.08 109.08,16.5 113.5,16.5H148.5C152.92,16.5 156.5,20.08 156.5,24.5C156.5,28.92 152.92,32.5 148.5,32.5H113.5C109.08,32.5 105.5,28.92 105.5,24.5ZM113.5,18.5C110.19,18.5 107.5,21.19 107.5,24.5C107.5,27.81 110.19,30.5 113.5,30.5H148.5C151.81,30.5 154.5,27.81 154.5,24.5C154.5,21.19 151.81,18.5 148.5,18.5H113.5Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M141,46C141,41.03 145.03,37 150,37H168C172.97,37 177,41.03 177,46V88C177,92.97 172.97,97 168,97H150C145.03,97 141,92.97 141,88V46Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M177,67.36V88C177,92.97 172.97,97 168,97H150C145.03,97 141,92.97 141,88V68.64C146.37,64.9 152.93,62.7 160,62.7C166.22,62.7 172.04,64.4 177,67.36Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M140,46C140,40.48 144.48,36 150,36H168C173.52,36 178,40.48 178,46V88C178,93.52 173.52,98 168,98H150C144.48,98 140,93.52 140,88V46ZM150,38C145.58,38 142,41.58 142,46V88C142,92.42 145.58,96 150,96H168C172.42,96 176,92.42 176,88V46C176,41.58 172.42,38 168,38H150Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M114,24.5C114,23.95 114.45,23.5 115,23.5H148C148.55,23.5 149,23.95 149,24.5V24.5C149,25.05 148.55,25.5 148,25.5H115C114.45,25.5 114,25.05 114,24.5V24.5Z"
android:fillColor="#3B45FD"/>
<path
android:pathData="M93,50C93,49.45 93.45,49 94,49H127C127.55,49 128,49.45 128,50V50C128,50.55 127.55,51 127,51H94C93.45,51 93,50.55 93,50V50Z"
android:fillColor="#3B45FD"/>
<path
android:pathData="M106.5,71C106.5,67.13 109.63,64 113.5,64H148.5C152.37,64 155.5,67.13 155.5,71V71C155.5,74.87 152.37,78 148.5,78H113.5C109.63,78 106.5,74.87 106.5,71V71Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M105.5,71C105.5,66.58 109.08,63 113.5,63H148.5C152.92,63 156.5,66.58 156.5,71C156.5,75.42 152.92,79 148.5,79H113.5C109.08,79 105.5,75.42 105.5,71ZM113.5,65C110.19,65 107.5,67.69 107.5,71C107.5,74.31 110.19,77 113.5,77H148.5C151.81,77 154.5,74.31 154.5,71C154.5,67.69 151.81,65 148.5,65H113.5Z"
android:fillColor="#3B45FD"
android:fillType="evenOdd"/>
<path
android:pathData="M93,45C93,44.45 93.45,44 94,44H127C127.55,44 128,44.45 128,45V45C128,45.55 127.55,46 127,46H94C93.45,46 93,45.55 93,45V45Z"
android:fillColor="#3B45FD"/>
<path
android:pathData="M114,71C114,70.45 114.45,70 115,70H148C148.55,70 149,70.45 149,71V71C149,71.55 148.55,72 148,72H115C114.45,72 114,71.55 114,71V71Z"
android:fillColor="#3B45FD"/>
</vector>

View File

@@ -0,0 +1,45 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="112dp"
android:height="112dp"
android:viewportWidth="112"
android:viewportHeight="112">
<path
android:pathData="M15,31C15,26.58 18.58,23 23,23H71C75.42,23 79,26.58 79,31V71C79,75.42 75.42,79 71,79H23C18.58,79 15,75.42 15,71V31Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M14,31C14,26.03 18.03,22 23,22H71C75.97,22 80,26.03 80,31V71C80,75.97 75.97,80 71,80H23C18.03,80 14,75.97 14,71V31ZM23,24C19.13,24 16,27.13 16,31V71C16,74.87 19.13,78 23,78H71C74.87,78 78,74.87 78,71V31C78,27.13 74.87,24 71,24H23Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M78,71V34.31C76.03,34.11 74.03,34 72,34C47.56,34 26.71,49.39 18.61,71H78Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M5,75C5,72.79 6.79,71 9,71H85C87.21,71 89,72.79 89,75C89,77.21 87.21,79 85,79H9C6.79,79 5,77.21 5,75Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M4,75C4,72.24 6.24,70 9,70H85C87.76,70 90,72.24 90,75C90,77.76 87.76,80 85,80H9C6.24,80 4,77.76 4,75ZM9,72C7.34,72 6,73.34 6,75C6,76.66 7.34,78 9,78H85C86.66,78 88,76.66 88,75C88,73.34 86.66,72 85,72H9Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M71,37C71,33.69 73.69,31 77,31H101C104.31,31 107,33.69 107,37V73C107,76.31 104.31,79 101,79H77C73.69,79 71,76.31 71,73V37Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M107,53.34V73.5C107,76.81 104.31,79.5 101,79.5H77C73.69,79.5 71,76.81 71,73.5V53.34C76.18,49.96 82.36,48 89,48C95.64,48 101.82,49.96 107,53.34Z"
android:fillColor="#C1C7FE"
android:fillType="evenOdd"/>
<path
android:pathData="M70,37C70,33.13 73.13,30 77,30H101C104.87,30 108,33.13 108,37V73C108,76.87 104.87,80 101,80H77C73.13,80 70,76.87 70,73V37ZM77,32C74.24,32 72,34.24 72,37V73C72,75.76 74.24,78 77,78H101C103.76,78 106,75.76 106,73V37C106,34.24 103.76,32 101,32H77Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M56,57C56,53.69 58.69,51 62,51H74C77.31,51 80,53.69 80,57V85.5C80,88.81 77.31,91.5 74,91.5H62C58.69,91.5 56,88.81 56,85.5V57Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M80,70.56V85.5C80,88.81 77.31,91.5 74,91.5H62C58.69,91.5 56,88.81 56,85.5V70.56C59.45,68.31 63.57,67 68,67C72.43,67 76.55,68.31 80,70.56Z"
android:fillColor="#C1C7FE"
android:fillType="evenOdd"/>
<path
android:pathData="M55,57C55,53.13 58.13,50 62,50H74C77.87,50 81,53.13 81,57V85.5C81,89.37 77.87,92.5 74,92.5H62C58.13,92.5 55,89.37 55,85.5V57ZM62,52C59.24,52 57,54.24 57,57V85.5C57,88.26 59.24,90.5 62,90.5H74C76.76,90.5 79,88.26 79,85.5V57C79,54.24 76.76,52 74,52H62Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="112dp"
android:height="112dp"
android:viewportWidth="112"
android:viewportHeight="112">
<path
android:pathData="M20,31C20,26.58 23.58,23 28,23H76C80.42,23 84,26.58 84,31V71C84,75.42 80.42,79 76,79H28C23.58,79 20,75.42 20,71V31Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M19,31C19,26.03 23.03,22 28,22H76C80.97,22 85,26.03 85,31V71C85,75.97 80.97,80 76,80H28C23.03,80 19,75.97 19,71V31ZM28,24C24.13,24 21,27.13 21,31V71C21,74.87 24.13,78 28,78H76C79.87,78 83,74.87 83,71V31C83,27.13 79.87,24 76,24H28Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M83,71V34.31C81.03,34.11 79.03,34 77,34C52.56,34 31.71,49.39 23.61,71H83Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M10,75C10,72.79 11.79,71 14,71H90C92.21,71 94,72.79 94,75C94,77.21 92.21,79 90,79H14C11.79,79 10,77.21 10,75Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M9,75C9,72.24 11.24,70 14,70H90C92.76,70 95,72.24 95,75C95,77.76 92.76,80 90,80H14C11.24,80 9,77.76 9,75ZM14,72C12.34,72 11,73.34 11,75C11,76.66 12.34,78 14,78H90C91.66,78 93,76.66 93,75C93,73.34 91.66,72 90,72H14Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M66,47C66,43.69 68.69,41 72,41H96C99.31,41 102,43.69 102,47V83C102,86.31 99.31,89 96,89H72C68.69,89 66,86.31 66,83V47Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M102,63.34V83.5C102,86.81 99.31,89.5 96,89.5H72C68.69,89.5 66,86.81 66,83.5V63.34C71.18,59.96 77.36,58 84,58C90.64,58 96.82,59.96 102,63.34Z"
android:fillColor="#C1C7FE"
android:fillType="evenOdd"/>
<path
android:pathData="M65,47C65,43.13 68.13,40 72,40H96C99.87,40 103,43.13 103,47V83C103,86.87 99.87,90 96,90H72C68.13,90 65,86.87 65,83V47ZM72,42C69.24,42 67,44.24 67,47V83C67,85.76 69.24,88 72,88H96C98.76,88 101,85.76 101,83V47C101,44.24 98.76,42 96,42H72Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
</vector>

View File

@@ -0,0 +1,66 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="184dp"
android:height="112dp"
android:viewportWidth="184"
android:viewportHeight="112">
<path
android:pathData="M21,25C21,18.37 26.37,13 33,13H105C111.63,13 117,18.37 117,25V85C117,91.63 111.63,97 105,97H33C26.37,97 21,91.63 21,85V25Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M20,25C20,17.82 25.82,12 33,12H105C112.18,12 118,17.82 118,25V85C118,92.18 112.18,98 105,98H33C25.82,98 20,92.18 20,85V25ZM33,14C26.92,14 22,18.92 22,25V85C22,91.08 26.92,96 33,96H105C111.07,96 116,91.08 116,85V25C116,18.92 111.07,14 105,14H33Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M116,85V26.53C112.85,26.18 109.65,26 106.41,26C68.35,26 36.07,50.75 24.86,85H116Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M6,91C6,87.69 8.69,85 12,85H126C129.31,85 132,87.69 132,91C132,94.31 129.31,97 126,97H12C8.69,97 6,94.31 6,91Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M5,91C5,87.13 8.13,84 12,84H126C129.87,84 133,87.13 133,91C133,94.87 129.87,98 126,98H12C8.13,98 5,94.87 5,91ZM12,86C9.24,86 7,88.24 7,91C7,93.76 9.24,96 12,96H126C128.76,96 131,93.76 131,91C131,88.24 128.76,86 126,86H12Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M86,45C86,41.69 88.69,39 92,39H129C132.31,39 135,41.69 135,45V50C135,53.31 132.31,56 129,56H92C88.69,56 86,53.31 86,50V45Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M85,45C85,41.13 88.13,38 92,38H129C132.87,38 136,41.13 136,45V50C136,53.87 132.87,57 129,57H92C88.13,57 85,53.87 85,50V45ZM92,40C89.24,40 87,42.24 87,45V50C87,52.76 89.24,55 92,55H129C131.76,55 134,52.76 134,50V45C134,42.24 131.76,40 129,40H92Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M106.5,24.5C106.5,20.63 109.63,17.5 113.5,17.5H148.5C152.37,17.5 155.5,20.63 155.5,24.5C155.5,28.37 152.37,31.5 148.5,31.5H113.5C109.63,31.5 106.5,28.37 106.5,24.5Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M105.5,24.5C105.5,20.08 109.08,16.5 113.5,16.5H148.5C152.92,16.5 156.5,20.08 156.5,24.5C156.5,28.92 152.92,32.5 148.5,32.5H113.5C109.08,32.5 105.5,28.92 105.5,24.5ZM113.5,18.5C110.19,18.5 107.5,21.19 107.5,24.5C107.5,27.81 110.19,30.5 113.5,30.5H148.5C151.81,30.5 154.5,27.81 154.5,24.5C154.5,21.19 151.81,18.5 148.5,18.5H113.5Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M141,46C141,41.03 145.03,37 150,37H168C172.97,37 177,41.03 177,46V88C177,92.97 172.97,97 168,97H150C145.03,97 141,92.97 141,88V46Z"
android:fillColor="#E3E8FE"/>
<path
android:pathData="M177,67.36V88C177,92.97 172.97,97 168,97H150C145.03,97 141,92.97 141,88V68.64C146.37,64.9 152.93,62.7 160,62.7C166.22,62.7 172.04,64.4 177,67.36Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M140,46C140,40.48 144.48,36 150,36H168C173.52,36 178,40.48 178,46V88C178,93.52 173.52,98 168,98H150C144.48,98 140,93.52 140,88V46ZM150,38C145.58,38 142,41.58 142,46V88C142,92.42 145.58,96 150,96H168C172.42,96 176,92.42 176,88V46C176,41.58 172.42,38 168,38H150Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M114,24.5C114,23.95 114.45,23.5 115,23.5H148C148.55,23.5 149,23.95 149,24.5C149,25.05 148.55,25.5 148,25.5H115C114.45,25.5 114,25.05 114,24.5Z"
android:fillColor="#020BAC"/>
<path
android:pathData="M93,50C93,49.45 93.45,49 94,49H127C127.55,49 128,49.45 128,50C128,50.55 127.55,51 127,51H94C93.45,51 93,50.55 93,50Z"
android:fillColor="#020BAC"/>
<path
android:pathData="M106.5,71C106.5,67.13 109.63,64 113.5,64H148.5C152.37,64 155.5,67.13 155.5,71C155.5,74.87 152.37,78 148.5,78H113.5C109.63,78 106.5,74.87 106.5,71Z"
android:fillColor="#C1C7FE"/>
<path
android:pathData="M105.5,71C105.5,66.58 109.08,63 113.5,63H148.5C152.92,63 156.5,66.58 156.5,71C156.5,75.42 152.92,79 148.5,79H113.5C109.08,79 105.5,75.42 105.5,71ZM113.5,65C110.19,65 107.5,67.69 107.5,71C107.5,74.31 110.19,77 113.5,77H148.5C151.81,77 154.5,74.31 154.5,71C154.5,67.69 151.81,65 148.5,65H113.5Z"
android:fillColor="#020BAC"
android:fillType="evenOdd"/>
<path
android:pathData="M93,45C93,44.45 93.45,44 94,44H127C127.55,44 128,44.45 128,45C128,45.55 127.55,46 127,46H94C93.45,46 93,45.55 93,45Z"
android:fillColor="#020BAC"/>
<path
android:pathData="M114,71C114,70.45 114.45,70 115,70H148C148.55,70 149,70.45 149,71C149,71.55 148.55,72 148,72H115C114.45,72 114,71.55 114,71Z"
android:fillColor="#020BAC"/>
</vector>

View File

@@ -43,6 +43,13 @@
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/action_appSettingsFragment_to_linkDeviceFragment"
app:destination="@id/linkDeviceFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/action_appSettingsFragment_to_paymentsActivity"
app:destination="@id/paymentsActivity"
@@ -218,12 +225,18 @@
<!-- endregion -->
<!-- Linked Devices -->
<!-- Linked Devices V1-->
<activity
android:id="@+id/deviceActivity"
android:name="org.thoughtcrime.securesms.DeviceActivity"
android:label="device_activity" />
<!-- Linked Devices V2 -->
<fragment
android:id="@+id/linkDeviceFragment"
android:name="org.thoughtcrime.securesms.linkdevice.LinkDeviceFragment"
android:label="link_device_fragment" />
<!-- Payments -->
<activity
android:id="@+id/paymentsActivity"

View File

@@ -43,6 +43,13 @@
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/action_appSettingsFragment_to_linkDeviceFragment"
app:destination="@id/linkDeviceFragment"
app:enterAnim="@anim/fragment_open_enter"
app:exitAnim="@anim/fragment_open_exit"
app:popEnterAnim="@anim/fragment_close_enter"
app:popExitAnim="@anim/fragment_close_exit" />
<action
android:id="@+id/action_appSettingsFragment_to_paymentsActivity"
app:destination="@id/paymentsActivity"
@@ -218,12 +225,18 @@
<!-- endregion -->
<!-- Linked Devices -->
<!-- Linked Devices V1-->
<activity
android:id="@+id/deviceActivity"
android:name="org.thoughtcrime.securesms.DeviceActivity"
android:label="device_activity" />
<!-- Linked Devices V2 -->
<fragment
android:id="@+id/linkDeviceFragment"
android:name="org.thoughtcrime.securesms.linkdevice.LinkDeviceFragment"
android:label="link_device_fragment" />
<!-- Payments -->
<activity
android:id="@+id/paymentsActivity"

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -859,6 +859,29 @@
<string name="DecryptionFailedDialog_chat_session_refreshed">Chat session refreshed</string>
<string name="DecryptionFailedDialog_signal_uses_end_to_end_encryption">Signal uses end-to-end encryption and it may need to refresh your chat session sometimes. This doesn\'t affect your chat\'s security, but you may have missed a message from this contact, and you can ask them to resend it.</string>
<!-- LinkDeviceFragment -->
<!-- Description for how Signal will work with a linked device (eg desktop, iPad) -->
<string name="LinkDeviceFragment__use_signal_on_desktop_ipad">Use Signal on desktop or iPad. Your messages will sync to your linked devices.</string>
<!-- Button prompting users to link a new device to their account -->
<string name="LinkDeviceFragment__link_a_new_device">Link a new device</string>
<!-- Text explaining that on linked devices, messages will be encrypted -->
<string name="LinkDeviceFragment__messages_and_chat_info_are_protected">Messages and chat info are protected by end-to-end encryption on all devices</string>
<!-- Bottom sheet title explaining how Signal works on a linked device -->
<string name="LinkDeviceFragment__signal_on_desktop_ipad">Signal on Desktop or iPad</string>
<!-- Bottom sheet description explaining that messages on linked devices are private -->
<string name="LinkDeviceFragment__all_messaging_is_private">All messaging on linked devices is private</string>
<!-- Bottom sheet description explaining that future messages on linked devices will be in sync with your phone but previous messages will not appear -->
<string name="LinkDeviceFragment__signal_messages_are_synchronized">Signal messages are synchronized with Signal on your mobile phone after it is linked. Your previous message history will not appear.</string>
<!-- Bottom sheet description explaining that for non-desktop/iPad devices, they should go to %s to download Signal where %s is Signal's website -->
<string name="LinkDeviceFragment__on_other_device_visit_signal">On your other device, visit %s to install Signal</string>
<string name="LinkDeviceFragment__signal_download_url" translatable="false">signal.org/download</string>
<!-- Header title listing out current linked devices -->
<string name="LinkDeviceFragment__my_linked_devices">My linked devices</string>
<!-- Dialog confirmation to unlink a device -->
<string name="LinkDeviceFragment__unlink">Unlink</string>
<!-- Toast message indicating a device has been unlinked where %s is the name of the device -->
<string name="LinkDeviceFragment__s_unlinked">%s unlinked</string>
<!-- DeviceListActivity -->
<string name="DeviceListActivity_unlink_s">Unlink \'%s\'?</string>
<string name="DeviceListActivity_by_unlinking_this_device_it_will_no_longer_be_able_to_send_or_receive">By unlinking this device, it will no longer be able to send or receive messages.</string>

View File

@@ -6,6 +6,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ExperimentalLayoutApi
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@@ -128,6 +129,33 @@ object Dialogs {
)
}
/**
* Customizable progress spinner that shows [message] below the spinner to let users know
* an action is completing
*/
@Composable
fun IndeterminateProgressDialog(message: String) {
androidx.compose.material3.AlertDialog(
onDismissRequest = {},
confirmButton = {},
dismissButton = {},
text = {
Column(
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth().fillMaxHeight()
) {
Spacer(modifier = Modifier.size(24.dp))
CircularProgressIndicator()
Spacer(modifier = Modifier.size(20.dp))
Text(message)
}
},
modifier = Modifier
.size(200.dp)
)
}
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun PermissionRationaleDialog(
@@ -237,3 +265,9 @@ private fun MessageDialogPreview() {
private fun IndeterminateProgressDialogPreview() {
Dialogs.IndeterminateProgressDialog()
}
@Preview
@Composable
private fun IndeterminateProgressDialogMessagePreview() {
Dialogs.IndeterminateProgressDialog("Completing...")
}