diff --git a/app/build.gradle b/app/build.gradle index 19357e704a..a1a6d43ced 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -243,6 +243,7 @@ dependencies { implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion" + implementation "androidx.lifecycle:lifecycle-viewmodel-compose:$lifecycleVersion" implementation "androidx.lifecycle:lifecycle-extensions:2.2.0" implementation "androidx.paging:paging-runtime-ktx:$pagingVersion" implementation 'androidx.activity:activity-ktx:1.5.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index aa164fe7b5..eac4ef3300 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -136,6 +136,10 @@ android:name="org.thoughtcrime.securesms.preferences.SettingsActivity" android:screenOrientation="portrait" android:label="@string/sessionSettings" /> + diff --git a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java index b23a6741cb..dca939f558 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ApplicationContext.java @@ -27,6 +27,9 @@ import android.os.Handler; import android.os.HandlerThread; import androidx.annotation.NonNull; +import androidx.core.content.pm.ShortcutInfoCompat; +import androidx.core.content.pm.ShortcutManagerCompat; +import androidx.core.graphics.drawable.IconCompat; import androidx.lifecycle.DefaultLifecycleObserver; import androidx.lifecycle.LifecycleOwner; import androidx.lifecycle.ProcessLifecycleOwner; @@ -42,6 +45,7 @@ import org.session.libsession.snode.SnodeModule; import org.session.libsession.utilities.Address; import org.session.libsession.utilities.ConfigFactoryUpdateListener; import org.session.libsession.utilities.Device; +import org.session.libsession.utilities.Environment; import org.session.libsession.utilities.ProfilePictureUtilities; import org.session.libsession.utilities.SSKEnvironment; import org.session.libsession.utilities.TextSecurePreferences; @@ -62,6 +66,7 @@ import org.thoughtcrime.securesms.database.LokiAPIDatabase; import org.thoughtcrime.securesms.database.Storage; import org.thoughtcrime.securesms.database.helpers.SQLCipherOpenHelper; import org.thoughtcrime.securesms.database.model.EmojiSearchData; +import org.thoughtcrime.securesms.debugmenu.DebugActivity; import org.thoughtcrime.securesms.dependencies.AppComponent; import org.thoughtcrime.securesms.dependencies.ConfigFactory; import org.thoughtcrime.securesms.dependencies.DatabaseComponent; @@ -107,6 +112,7 @@ import dagger.hilt.EntryPoints; import dagger.hilt.android.HiltAndroidApp; import kotlin.Unit; import network.loki.messenger.BuildConfig; +import network.loki.messenger.R; import network.loki.messenger.libsession_util.ConfigBase; import network.loki.messenger.libsession_util.UserProfile; @@ -232,7 +238,8 @@ public class ApplicationContext extends Application implements DefaultLifecycleO messageNotifier = new OptimizedMessageNotifier(new DefaultMessageNotifier()); broadcaster = new Broadcaster(this); LokiAPIDatabase apiDB = getDatabaseComponent().lokiAPIDatabase(); - SnodeModule.Companion.configure(apiDB, broadcaster); + boolean useTestNet = textSecurePreferences.getEnvironment() == Environment.TEST_NET; + SnodeModule.Companion.configure(apiDB, broadcaster, useTestNet); initializeExpiringMessageManager(); initializeTypingStatusRepository(); initializeTypingStatusSender(); @@ -248,6 +255,22 @@ public class ApplicationContext extends Application implements DefaultLifecycleO NetworkConstraint networkConstraint = new NetworkConstraint.Factory(this).create(); HTTP.INSTANCE.setConnectedToNetwork(networkConstraint::isMet); + + // add our shortcut debug menu if we are not in a release build + if (BuildConfig.BUILD_TYPE != "release") { + // add the config settings shortcut + Intent intent = new Intent(this, DebugActivity.class); + intent.setAction(Intent.ACTION_VIEW); + + ShortcutInfoCompat shortcut = new ShortcutInfoCompat.Builder(this, "shortcut_debug_menu") + .setShortLabel("Debug Menu") + .setLongLabel("Debug Menu") + .setIcon(IconCompat.createWithResource(this, R.drawable.ic_settings)) + .setIntent(intent) + .build(); + + ShortcutManagerCompat.pushDynamicShortcut(this, shortcut); + } } @Override @@ -486,7 +509,7 @@ public class ApplicationContext extends Application implements DefaultLifecycleO // Method to clear the local data - returns true on success otherwise false /** - * Clear all local profile data and message history then restart the app after a brief delay. + * Clear all local profile data and message history. * @return true on success, false otherwise. */ @SuppressLint("ApplySharedPref") @@ -498,6 +521,16 @@ public class ApplicationContext extends Application implements DefaultLifecycleO return false; } configFactory.keyPairChanged(); + return true; + } + + /** + * Clear all local profile data and message history then restart the app after a brief delay. + * @return true on success, false otherwise. + */ + @SuppressLint("ApplySharedPref") + public boolean clearAllDataAndRestart() { + clearAllData(); Util.runOnMain(() -> new Handler().postDelayed(ApplicationContext.this::restartApplication, 200)); return true; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt index b39fa5b8d7..a3fd6ac1dd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/contacts/SelectContactsActivity.kt @@ -35,7 +35,7 @@ class SelectContactsActivity : PassphraseRequiredActionBarActivity(), LoaderMana super.onCreate(savedInstanceState, isReady) binding = ActivitySelectContactsBinding.inflate(layoutInflater) setContentView(binding.root) - supportActionBar!!.title = resources.getString(R.string.contactSelect) + supportActionBar!!.title = resources.getString(R.string.membersInvite) usersToExclude = intent.getStringArrayExtra(usersToExcludeKey)?.toSet() ?: setOf() val emptyStateText = intent.getStringExtra(emptyStateTextKey) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index 989be7880b..f6e37da6a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt @@ -2192,7 +2192,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun showMessageDetail(messages: Set) { Intent(this, MessageDetailActivity::class.java) .apply { putExtra(MESSAGE_TIMESTAMP, messages.first().timestamp) } - .let { handleMessageDetail.launch(it) } + .let { + handleMessageDetail.launch(it) + overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) + } endActionMode() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt index 8e2b88f25b..a830543ec1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/MessageDetailActivity.kt @@ -2,6 +2,7 @@ package org.thoughtcrime.securesms.conversation.v2 import android.annotation.SuppressLint import android.content.Intent +import android.net.Uri import android.os.Bundle import android.view.LayoutInflater import android.view.MotionEvent.ACTION_UP @@ -15,6 +16,7 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ExperimentalLayoutApi import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.IntrinsicSize +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth @@ -28,6 +30,7 @@ import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -35,6 +38,7 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.res.painterResource @@ -58,23 +62,21 @@ import org.thoughtcrime.securesms.ui.Avatar import org.thoughtcrime.securesms.ui.CarouselNextButton import org.thoughtcrime.securesms.ui.CarouselPrevButton import org.thoughtcrime.securesms.ui.Cell -import org.thoughtcrime.securesms.ui.CellNoMargin -import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin import org.thoughtcrime.securesms.ui.Divider import org.thoughtcrime.securesms.ui.GetString import org.thoughtcrime.securesms.ui.HorizontalPagerIndicator import org.thoughtcrime.securesms.ui.LargeItemButton +import org.thoughtcrime.securesms.ui.TitledText +import org.thoughtcrime.securesms.ui.setComposeContent +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.PreviewTheme import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider -import org.thoughtcrime.securesms.ui.TitledText import org.thoughtcrime.securesms.ui.theme.ThemeColors -import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.blackAlpha40 -import org.thoughtcrime.securesms.ui.theme.dangerButtonColors -import org.thoughtcrime.securesms.ui.setComposeContent -import org.thoughtcrime.securesms.ui.theme.LocalType import org.thoughtcrime.securesms.ui.theme.bold +import org.thoughtcrime.securesms.ui.theme.dangerButtonColors import org.thoughtcrime.securesms.ui.theme.monospace import javax.inject.Inject @@ -191,8 +193,11 @@ fun CellMetadata( ) { state.apply { if (listOfNotNull(sent, received, error, senderInfo).isEmpty()) return - CellWithPaddingAndMargin { - Column(verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing)) { + Cell(modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)) { + Column( + modifier = Modifier.padding(LocalDimensions.current.spacing), + verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) + ) { TitledText(sent) TitledText(received) TitledErrorText(error) @@ -215,7 +220,7 @@ fun CellButtons( onResend: (() -> Unit)? = null, onDelete: () -> Unit = {}, ) { - Cell { + Cell(modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)) { Column { onReply?.let { LargeItemButton( @@ -254,8 +259,11 @@ fun Carousel(attachments: List, onClick: (Int) -> Unit) { Row { CarouselPrevButton(pagerState) Box(modifier = Modifier.weight(1f)) { - CellCarousel(pagerState, attachments, onClick) - HorizontalPagerIndicator(pagerState) + CarouselPager(pagerState, attachments, onClick) + HorizontalPagerIndicator( + pagerState = pagerState, + modifier = Modifier.padding(bottom = LocalDimensions.current.xxsSpacing) + ) ExpandButton( modifier = Modifier .align(Alignment.BottomEnd) @@ -273,12 +281,15 @@ fun Carousel(attachments: List, onClick: (Int) -> Unit) { ExperimentalGlideComposeApi::class ) @Composable -private fun CellCarousel( +private fun CarouselPager( pagerState: PagerState, attachments: List, onClick: (Int) -> Unit ) { - CellNoMargin { + Cell( + modifier = Modifier + .clip(MaterialTheme.shapes.small) + ) { HorizontalPager(state = pagerState) { i -> GlideImage( contentScale = ContentScale.Crop, @@ -317,6 +328,33 @@ fun PreviewMessageDetails( PreviewTheme(colors) { MessageDetails( state = MessageDetailsState( + imageAttachments = listOf( + Attachment( + fileDetails = listOf( + TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png") + ), + fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png", + uri = Uri.parse(""), + hasImage = true + ), + Attachment( + fileDetails = listOf( + TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png") + ), + fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png", + uri = Uri.parse(""), + hasImage = true + ), + Attachment( + fileDetails = listOf( + TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png") + ), + fileName = "Screen Shot 2023-07-06 at 11.35.50 am.png", + uri = Uri.parse(""), + hasImage = true + ) + + ), nonImageAttachmentFileDetails = listOf( TitledText(R.string.attachmentsFileId, "Screen Shot 2023-07-06 at 11.35.50 am.png"), TitledText(R.string.attachmentsFileType, "image/png"), @@ -337,7 +375,7 @@ fun PreviewMessageDetails( fun FileDetails(fileDetails: List) { if (fileDetails.isEmpty()) return - Cell { + Cell(modifier = Modifier.padding(horizontal = LocalDimensions.current.spacing)) { FlowRow( modifier = Modifier.padding(horizontal = LocalDimensions.current.xsSpacing, vertical = LocalDimensions.current.spacing), verticalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing) diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugActivity.kt new file mode 100644 index 0000000000..828b3c3a1e --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugActivity.kt @@ -0,0 +1,21 @@ +package org.thoughtcrime.securesms.debugmenu + +import android.os.Bundle +import androidx.appcompat.app.AppCompatActivity +import dagger.hilt.android.AndroidEntryPoint +import org.thoughtcrime.securesms.ui.setComposeContent + + +@AndroidEntryPoint +class DebugActivity : AppCompatActivity() { + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setComposeContent { + DebugMenuScreen( + onClose = { finish() } + ) + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt new file mode 100644 index 0000000000..5eb378ef30 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenu.kt @@ -0,0 +1,172 @@ +package org.thoughtcrime.securesms.debugmenu + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Scaffold +import androidx.compose.material3.SnackbarHost +import androidx.compose.material3.SnackbarHostState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import network.loki.messenger.BuildConfig +import network.loki.messenger.R +import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Commands.ChangeEnvironment +import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Commands.HideEnvironmentWarningDialog +import org.thoughtcrime.securesms.debugmenu.DebugMenuViewModel.Commands.ShowEnvironmentWarningDialog +import org.thoughtcrime.securesms.ui.AlertDialog +import org.thoughtcrime.securesms.ui.Cell +import org.thoughtcrime.securesms.ui.DialogButtonModel +import org.thoughtcrime.securesms.ui.GetString +import org.thoughtcrime.securesms.ui.LoadingDialog +import org.thoughtcrime.securesms.ui.components.BackAppBar +import org.thoughtcrime.securesms.ui.components.DropDown +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.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.bold + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DebugMenu( + uiState: DebugMenuViewModel.UIState, + sendCommand: (DebugMenuViewModel.Commands) -> Unit, + modifier: Modifier = Modifier, + onClose: () -> Unit +){ + val snackbarHostState = remember { SnackbarHostState() } + + Scaffold( + modifier = modifier.fillMaxSize(), + snackbarHost = { + SnackbarHost(hostState = snackbarHostState) + } + ) { contentPadding -> + // display a snackbar when required + LaunchedEffect(uiState.snackMessage) { + if(!uiState.snackMessage.isNullOrEmpty()){ + snackbarHostState.showSnackbar(uiState.snackMessage) + } + } + + // Alert dialogs + if (uiState.showEnvironmentWarningDialog) { + AlertDialog( + onDismissRequest = { sendCommand(HideEnvironmentWarningDialog) }, + title = "Are you sure you want to switch environments?", + text = "Changing this setting will result in all conversations and Snode data being cleared...", + showCloseButton = false, // don't display the 'x' button + buttons = listOf( + DialogButtonModel( + text = GetString(R.string.cancel), + contentDescription = GetString(R.string.cancel), + onClick = { sendCommand(HideEnvironmentWarningDialog) } + ), + DialogButtonModel( + text = GetString(R.string.ok), + contentDescription = GetString(R.string.ok), + onClick = { sendCommand(ChangeEnvironment) } + ) + ) + ) + } + + if (uiState.showEnvironmentLoadingDialog) { + LoadingDialog(title = "Changing Environment...") + } + + Column( + modifier = Modifier + .padding(contentPadding) + .fillMaxSize() + .background(color = LocalColors.current.background) + ) { + // App bar + BackAppBar(title = "Debug Menu", onBack = onClose) + + Column( + modifier = Modifier + .padding(horizontal = LocalDimensions.current.spacing) + .verticalScroll(rememberScrollState()) + ) { + // Info pane + DebugCell("App Info") { + Text( + text = "Version: ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - ${ + BuildConfig.GIT_HASH.take( + 6 + ) + })", + style = LocalType.current.base + ) + } + + // Environment + DebugCell("Environment") { + DropDown( + modifier = Modifier.fillMaxWidth(0.6f), + selectedText = uiState.currentEnvironment, + values = uiState.environments, + onValueSelected = { + sendCommand(ShowEnvironmentWarningDialog(it)) + } + ) + } + } + } + } +} + +@Composable +fun ColumnScope.DebugCell( + title: String, + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Spacer(modifier = Modifier.height(LocalDimensions.current.smallSpacing)) + + Cell { + Column( + modifier = modifier.padding(LocalDimensions.current.spacing) + ) { + Text( + text = title, + style = LocalType.current.large.bold() + ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) + + content() + } + } +} + +@Preview +@Composable +fun PreviewDebugMenu(){ + PreviewTheme { + DebugMenu( + uiState = DebugMenuViewModel.UIState( + currentEnvironment = "Development", + environments = listOf("Development", "Production"), + snackMessage = null, + showEnvironmentWarningDialog = false, + showEnvironmentLoadingDialog = false + ), + sendCommand = {}, + onClose = {} + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuScreen.kt new file mode 100644 index 0000000000..6c0f22805a --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuScreen.kt @@ -0,0 +1,23 @@ +package org.thoughtcrime.securesms.debugmenu + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.ui.Modifier +import androidx.lifecycle.viewmodel.compose.viewModel + +@Composable +fun DebugMenuScreen( + modifier: Modifier = Modifier, + debugMenuViewModel: DebugMenuViewModel = viewModel(), + onClose: () -> Unit +) { + val uiState by debugMenuViewModel.uiState.collectAsState() + + DebugMenu( + modifier = modifier, + uiState = uiState, + sendCommand = debugMenuViewModel::onCommand, + onClose = onClose + ) +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt new file mode 100644 index 0000000000..750b3e20c7 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/debugmenu/DebugMenuViewModel.kt @@ -0,0 +1,115 @@ +package org.thoughtcrime.securesms.debugmenu + +import android.app.Application +import android.widget.Toast +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.delay +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import network.loki.messenger.R +import org.session.libsession.messaging.open_groups.OpenGroupApi +import org.session.libsession.snode.SnodeAPI +import org.session.libsession.utilities.TextSecurePreferences +import org.session.libsignal.utilities.Log +import org.thoughtcrime.securesms.ApplicationContext +import org.session.libsession.utilities.Environment +import org.thoughtcrime.securesms.dependencies.DatabaseComponent +import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities +import javax.inject.Inject + +@HiltViewModel +class DebugMenuViewModel @Inject constructor( + private val application: Application, + private val textSecurePreferences: TextSecurePreferences +) : ViewModel() { + private val TAG = "DebugMenu" + + private val _uiState = MutableStateFlow( + UIState( + currentEnvironment = textSecurePreferences.getEnvironment().label, + environments = Environment.entries.map { it.label }, + snackMessage = null, + showEnvironmentWarningDialog = false, + showEnvironmentLoadingDialog = false + ) + ) + val uiState: StateFlow + get() = _uiState + + private var temporaryEnv: Environment? = null + + fun onCommand(command: Commands) { + when (command) { + is Commands.ChangeEnvironment -> changeEnvironment() + + is Commands.HideEnvironmentWarningDialog -> _uiState.value = + _uiState.value.copy(showEnvironmentWarningDialog = false) + + is Commands.ShowEnvironmentWarningDialog -> + showEnvironmentWarningDialog(command.environment) + } + } + + private fun showEnvironmentWarningDialog(environment: String) { + if(environment == _uiState.value.currentEnvironment) return + val env = Environment.entries.firstOrNull { it.label == environment } ?: return + + temporaryEnv = env + + _uiState.value = _uiState.value.copy(showEnvironmentWarningDialog = true) + } + + private fun changeEnvironment() { + val env = temporaryEnv ?: return + + // show a loading state + _uiState.value = _uiState.value.copy( + showEnvironmentWarningDialog = false, + showEnvironmentLoadingDialog = true + ) + + // clear remote and local data, then restart the app + viewModelScope.launch { + try { + ConfigurationMessageUtilities.forceSyncConfigurationNowIfNeeded(application).get() + } catch (e: Exception) { + // we can ignore fails here as we might be switching environments before the user gets a public key + } + ApplicationContext.getInstance(application).clearAllData().let { success -> + if(success){ + // save the environment + textSecurePreferences.setEnvironment(env) + delay(500) + ApplicationContext.getInstance(application).restartApplication() + } else { + _uiState.value = _uiState.value.copy( + showEnvironmentWarningDialog = false, + showEnvironmentLoadingDialog = false + ) + Log.e(TAG, "Failed to force sync when deleting data") + _uiState.value = _uiState.value.copy(snackMessage = "Sorry, something went wrong...") + return@launch + } + } + } + } + + data class UIState( + val currentEnvironment: String, + val environments: List, + val snackMessage: String?, + val showEnvironmentWarningDialog: Boolean, + val showEnvironmentLoadingDialog: Boolean + ) + + sealed class Commands { + object ChangeEnvironment : Commands() + data class ShowEnvironmentWarningDialog(val environment: String) : Commands() + object HideEnvironmentWarningDialog : Commands() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt index 7eee4eeaa8..1e093bd7e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotifications.kt @@ -47,7 +47,7 @@ internal fun MessageNotificationsScreen( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { - CircularProgressIndicator(LocalColors.current.primary) + CircularProgressIndicator(color = LocalColors.current.primary) } return diff --git a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt index a39f270bf2..f1f5bee89f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/onboarding/messagenotifications/MessageNotificationsViewModel.kt @@ -72,7 +72,7 @@ internal class MessageNotificationsViewModel( _uiStates.update { it.copy(clearData = true) } viewModelScope.launch(Dispatchers.IO) { - ApplicationContext.getInstance(application).clearAllData() + ApplicationContext.getInstance(application).clearAllDataAndRestart() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt index d67bc97ef2..ca6cf32ce2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt @@ -127,7 +127,7 @@ class ClearAllDataDialog : DialogFragment() { } return } - ApplicationContext.getInstance(context).clearAllData().let { success -> + ApplicationContext.getInstance(context).clearAllDataAndRestart().let { success -> withContext(Main) { if (success) { dismiss() @@ -164,7 +164,7 @@ class ClearAllDataDialog : DialogFragment() { } else if (deletionResultMap.values.all { it }) { // ..otherwise if the network data deletion was successful proceed to delete the local data as well. - ApplicationContext.getInstance(context).clearAllData() + ApplicationContext.getInstance(context).clearAllDataAndRestart() withContext(Main) { dismiss() } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt index d9c74a5b4d..bfba7377a7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -60,6 +60,7 @@ import org.session.libsession.messaging.MessagingModuleConfiguration import org.session.libsession.snode.OnionRequestAPI import org.session.libsession.snode.SnodeAPI import org.session.libsession.utilities.Address +import org.session.libsession.utilities.NonTranslatableStringConstants.DEBUG_MENU import org.session.libsession.utilities.ProfileKeyUtil import org.session.libsession.utilities.ProfilePictureUtilities import org.session.libsession.utilities.SSKEnvironment.ProfileManagerProtocol @@ -72,6 +73,7 @@ import org.session.libsignal.utilities.Util.SECURE_RANDOM import org.thoughtcrime.securesms.PassphraseRequiredActionBarActivity import org.thoughtcrime.securesms.avatar.AvatarSelection import org.thoughtcrime.securesms.components.ProfilePictureView +import org.thoughtcrime.securesms.debugmenu.DebugActivity import org.thoughtcrime.securesms.dependencies.ConfigFactory import org.thoughtcrime.securesms.home.PathActivity import org.thoughtcrime.securesms.messagerequests.MessageRequestsActivity @@ -164,6 +166,9 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { super.onCreate(savedInstanceState, isReady) binding = ActivitySettingsBinding.inflate(layoutInflater) setContentView(binding.root) + + // set the toolbar icon to a close icon + supportActionBar?.setHomeAsUpIndicator(R.drawable.ic_baseline_close_24) } override fun onStart() { @@ -176,8 +181,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { btnGroupNameDisplay.text = getDisplayName() publicKeyTextView.text = hexEncodedPublicKey val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6) - - val versionDetails = " ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars)" + val environment: String = if(BuildConfig.BUILD_TYPE == "release") "" else " - ${prefs.getEnvironment().label}" + val versionDetails = " ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars) $environment" val versionString = Phrase.from(applicationContext, R.string.updateVersion).put(VERSION_KEY, versionDetails).format() versionTextView.text = versionString } @@ -187,6 +192,11 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { } } + override fun finish() { + super.finish() + overridePendingTransition(R.anim.fade_scale_in, R.anim.slide_to_bottom) + } + private fun getDisplayName(): String = TextSecurePreferences.getProfileName(this) ?: truncateIdForDisplay(hexEncodedPublicKey) @@ -484,10 +494,12 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { @Composable fun Buttons() { - Column { + Column( + modifier = Modifier + .padding(horizontal = LocalDimensions.current.spacing) + ) { Row( modifier = Modifier - .padding(horizontal = LocalDimensions.current.spacing) .padding(top = LocalDimensions.current.xxsSpacing), horizontalArrangement = Arrangement.spacedBy(LocalDimensions.current.smallSpacing), ) { @@ -509,58 +521,50 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { Cell { Column { + // add the debug menu in non release builds + if (BuildConfig.BUILD_TYPE != "release") { + LargeItemButton(DEBUG_MENU, R.drawable.ic_settings) { push() } + Divider() + } + Crossfade(if (hasPaths) R.drawable.ic_status else R.drawable.ic_path_yellow, label = "path") { - LargeItemButtonWithDrawable(R.string.onionRoutingPath, it) { show() } + LargeItemButtonWithDrawable(R.string.onionRoutingPath, it) { push() } } Divider() - LargeItemButton( - R.string.sessionPrivacy, - R.drawable.ic_privacy_icon - ) { show() } + + LargeItemButton(R.string.sessionPrivacy, R.drawable.ic_privacy_icon) { push() } Divider() - LargeItemButton( - R.string.sessionNotifications, - R.drawable.ic_speaker, - Modifier.contentDescription(R.string.AccessibilityId_notifications) - ) { show() } + + LargeItemButton(R.string.sessionNotifications, R.drawable.ic_speaker, Modifier.contentDescription(R.string.AccessibilityId_notifications)) { push() } Divider() - LargeItemButton( - R.string.sessionConversations, - R.drawable.ic_conversations, - Modifier.contentDescription(R.string.AccessibilityId_sessionConversations) - ) { show() } + + LargeItemButton(R.string.sessionConversations, R.drawable.ic_conversations, Modifier.contentDescription(R.string.AccessibilityId_sessionConversations)) { push() } Divider() - LargeItemButton( - R.string.sessionMessageRequests, - R.drawable.ic_message_requests, - Modifier.contentDescription(R.string.AccessibilityId_sessionMessageRequests) - ) { show() } + + LargeItemButton(R.string.sessionMessageRequests, R.drawable.ic_message_requests, Modifier.contentDescription(R.string.AccessibilityId_sessionMessageRequests)) { push() } Divider() - LargeItemButton( - R.string.sessionAppearance, - R.drawable.ic_appearance, - Modifier.contentDescription(R.string.AccessibilityId_sessionAppearance) - ) { show() } + + LargeItemButton(R.string.sessionAppearance, R.drawable.ic_appearance, Modifier.contentDescription(R.string.AccessibilityId_sessionAppearance)) { push() } Divider() + LargeItemButton( R.string.sessionInviteAFriend, R.drawable.ic_invite_friend, Modifier.contentDescription(R.string.AccessibilityId_sessionInviteAFriend) ) { sendInvitationToUseSession() } Divider() + + // Only show the recovery password option if the user has not chosen to permanently hide it if (!prefs.getHidePassword()) { LargeItemButton( R.string.sessionRecoveryPassword, R.drawable.ic_shield_outline, Modifier.contentDescription(R.string.AccessibilityId_sessionRecoveryPasswordMenuItem) - ) { show() } + ) { push() } Divider() } - LargeItemButton( - R.string.sessionHelp, - R.drawable.ic_help, - Modifier.contentDescription(R.string.AccessibilityId_help) - ) { show() } + + LargeItemButton(R.string.sessionHelp, R.drawable.ic_help, Modifier.contentDescription(R.string.AccessibilityId_help)) { push() } Divider() LargeItemButton(R.string.sessionClearData, diff --git a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt index 74e0f9f145..4bc2724d6d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/recoverypassword/RecoveryPassword.kt @@ -3,6 +3,7 @@ package org.thoughtcrime.securesms.recoverypassword import androidx.compose.animation.AnimatedVisibility import androidx.compose.foundation.layout.Arrangement 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.height @@ -25,19 +26,19 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.unit.dp import network.loki.messenger.R -import org.thoughtcrime.securesms.ui.CellWithPaddingAndMargin -import org.thoughtcrime.securesms.ui.theme.LocalDimensions -import org.thoughtcrime.securesms.ui.theme.PreviewTheme -import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.Cell import org.thoughtcrime.securesms.ui.SessionShieldIcon -import org.thoughtcrime.securesms.ui.theme.ThemeColors -import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.components.QrImage import org.thoughtcrime.securesms.ui.components.SlimOutlineButton import org.thoughtcrime.securesms.ui.components.SlimOutlineCopyButton import org.thoughtcrime.securesms.ui.components.border import org.thoughtcrime.securesms.ui.contentDescription +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.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.SessionColorsParameterProvider +import org.thoughtcrime.securesms.ui.theme.ThemeColors import org.thoughtcrime.securesms.ui.theme.monospace @Composable @@ -53,6 +54,7 @@ internal fun RecoveryPasswordScreen( .contentDescription(R.string.AccessibilityId_sessionRecoveryPassword) .verticalScroll(rememberScrollState()) .padding(bottom = LocalDimensions.current.smallSpacing) + .padding(horizontal = LocalDimensions.current.spacing) ) { RecoveryPasswordCell(mnemonic, seed, copyMnemonic) HideRecoveryPasswordCell(onHide) @@ -69,8 +71,10 @@ private fun RecoveryPasswordCell( mutableStateOf(false) } - CellWithPaddingAndMargin { - Column { + Cell { + Column( + modifier = Modifier.padding(LocalDimensions.current.smallSpacing) + ) { Row { Text( stringResource(R.string.sessionRecoveryPassword), @@ -148,8 +152,10 @@ private fun RecoveryPassword(mnemonic: String) { @Composable private fun HideRecoveryPasswordCell(onHide: () -> Unit = {}) { - CellWithPaddingAndMargin { - Row { + Cell { + Row( + modifier = Modifier.padding(LocalDimensions.current.smallSpacing) + ) { Column( Modifier.weight(1f) ) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt index f271b348db..1dd1df46cb 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/AlertDialog.kt @@ -3,13 +3,16 @@ package org.thoughtcrime.securesms.ui import androidx.compose.foundation.background import androidx.compose.foundation.border import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width import androidx.compose.material3.BasicAlertDialog import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -29,6 +32,7 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import network.loki.messenger.R +import org.thoughtcrime.securesms.ui.components.CircularProgressIndicator import org.thoughtcrime.securesms.ui.theme.LocalColors import org.thoughtcrime.securesms.ui.theme.LocalDimensions import org.thoughtcrime.securesms.ui.theme.LocalType @@ -48,6 +52,7 @@ class DialogButtonModel( @Composable fun AlertDialog( onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, title: String? = null, text: String? = null, buttons: List? = null, @@ -55,18 +60,10 @@ fun AlertDialog( content: @Composable () -> Unit = {} ) { BasicAlertDialog( + modifier = modifier, onDismissRequest = onDismissRequest, content = { - Box( - modifier = Modifier.background( - color = LocalColors.current.backgroundSecondary, - shape = MaterialTheme.shapes.small) - .border( - width = 1.dp, - color = LocalColors.current.borders, - shape = MaterialTheme.shapes.small) - - ) { + DialogBg { // only show the 'x' button is required if (showCloseButton) { IconButton( @@ -133,7 +130,7 @@ fun AlertDialog( @Composable fun DialogButton( text: String, - modifier: Modifier, + modifier: Modifier = Modifier, color: Color = Color.Unspecified, onClick: () -> Unit ) { @@ -154,6 +151,62 @@ fun DialogButton( } } +@Composable +fun DialogBg( + content: @Composable BoxScope.() -> Unit +){ + Box( + modifier = Modifier + .background( + color = LocalColors.current.backgroundSecondary, + shape = MaterialTheme.shapes.small + ) + .border( + width = 1.dp, + color = LocalColors.current.borders, + shape = MaterialTheme.shapes.small + ) + + ) { + content() + } +} + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun LoadingDialog( + modifier: Modifier = Modifier, + title: String? = null, +){ + BasicAlertDialog( + modifier = modifier, + onDismissRequest = {}, + content = { + DialogBg { + Column( + modifier = Modifier + .fillMaxWidth() + .padding(LocalDimensions.current.spacing) + ) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.CenterHorizontally) + ) + + Spacer(modifier = Modifier.height(LocalDimensions.current.spacing)) + + title?.let { + Text( + it, + modifier = Modifier.align(Alignment.CenterHorizontally), + style = LocalType.current.large + ) + } + } + } + } + ) +} + @Preview @Composable fun PreviewSimpleDialog() { @@ -200,3 +253,13 @@ fun PreviewXCloseDialog() { ) } } + +@Preview +@Composable +fun PreviewLoadingDialog() { + PreviewTheme { + LoadingDialog( + title = stringResource(R.string.warning) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Carousel.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Carousel.kt index ea632d46e7..d94cfc929d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Carousel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Carousel.kt @@ -10,6 +10,7 @@ import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -42,14 +43,17 @@ import kotlin.math.sign @OptIn(ExperimentalFoundationApi::class) @Composable -fun BoxScope.HorizontalPagerIndicator(pagerState: PagerState) { - if (pagerState.pageCount >= 2) Box( - modifier = Modifier - .background(color = blackAlpha40, shape = pillShape) - .align(Alignment.BottomCenter) - .padding(LocalDimensions.current.xxsSpacing) - ) { - Box(modifier = Modifier.padding(LocalDimensions.current.xxsSpacing)) { +fun BoxScope.HorizontalPagerIndicator( + pagerState: PagerState, + modifier: Modifier = Modifier +) { + if (pagerState.pageCount >= 2){ + Box( + modifier = modifier + .background(color = blackAlpha40, shape = pillShape) + .align(Alignment.BottomCenter) + .padding(LocalDimensions.current.xxsSpacing) + ) { ClickableHorizontalPagerIndicator( pagerState = pagerState, pageCount = pagerState.pageCount diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt index effcdca057..da1dd8e280 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/Components.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth @@ -105,7 +106,7 @@ fun OptionsCard(card: OptionsCardData, callbacks: Callbacks) { Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) - CellNoMargin { + Cell { LazyColumn( modifier = Modifier.heightIn(max = 5000.dp) ) { @@ -173,6 +174,38 @@ fun LargeItemButton( ) } +@Composable +fun LargeItemButton( + text: String, + @DrawableRes icon: Int, + modifier: Modifier = Modifier, + colors: ButtonColors = transparentButtonColors(), + onClick: () -> Unit +) { + ItemButton( + text, icon, modifier.heightIn(min = LocalDimensions.current.minLargeItemButtonHeight), + LocalType.current.h8, colors, onClick + ) +} + +@Composable +fun ItemButton(text: String, icon: Int, modifier: Modifier, textStyle: TextStyle, colors: ButtonColors, onClick: () -> Unit) { + ItemButton( + text = text, + modifier = modifier, + icon = { + Icon( + painter = painterResource(id = icon), + contentDescription = null, + modifier = Modifier.align(Alignment.Center) + ) + }, + textStyle = textStyle, + colors = colors, + onClick = onClick + ) +} + /** * Courtesy [ItemButton] implementation that takes a [DrawableRes] for the [icon] */ @@ -257,32 +290,19 @@ fun PreviewItemButton() { @Composable fun Cell( - padding: Dp = 0.dp, - margin: Dp = LocalDimensions.current.spacing, - content: @Composable () -> Unit -) { - CellWithPaddingAndMargin(padding, margin) { content() } -} -@Composable -fun CellNoMargin(content: @Composable () -> Unit) { - CellWithPaddingAndMargin(padding = 0.dp, margin = 0.dp) { content() } -} - -@Composable -fun CellWithPaddingAndMargin( - padding: Dp = LocalDimensions.current.spacing, - margin: Dp = LocalDimensions.current.spacing, + modifier: Modifier = Modifier, content: @Composable () -> Unit ) { Box( - modifier = Modifier - .padding(horizontal = margin) - .background(color = LocalColors.current.backgroundSecondary, - shape = MaterialTheme.shapes.small) + modifier = modifier + .background( + color = LocalColors.current.backgroundSecondary, + shape = MaterialTheme.shapes.small + ) .wrapContentHeight() .fillMaxWidth(), ) { - Box(Modifier.padding(padding)) { content() } + content() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/CircularProgressIndicator.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/CircularProgressIndicator.kt index bbad82d29e..f555c82426 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/components/CircularProgressIndicator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/CircularProgressIndicator.kt @@ -8,18 +8,24 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.unit.dp @Composable -fun CircularProgressIndicator(color: Color = LocalContentColor.current) { +fun CircularProgressIndicator( + modifier: Modifier = Modifier, + color: Color = LocalContentColor.current +) { androidx.compose.material3.CircularProgressIndicator( - modifier = Modifier.size(40.dp), + modifier = modifier.size(40.dp), color = color, strokeWidth = 2.dp ) } @Composable -fun SmallCircularProgressIndicator(color: Color = LocalContentColor.current) { +fun SmallCircularProgressIndicator( + modifier: Modifier = Modifier, + color: Color = LocalContentColor.current +) { androidx.compose.material3.CircularProgressIndicator( - modifier = Modifier.size(20.dp), + modifier = modifier.size(20.dp), color = color, strokeWidth = 2.dp ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/components/DropDown.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/components/DropDown.kt new file mode 100644 index 0000000000..de7f437356 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/components/DropDown.kt @@ -0,0 +1,109 @@ +package org.thoughtcrime.securesms.ui.components + +import androidx.compose.foundation.border +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExposedDropdownMenuBox +import androidx.compose.material3.ExposedDropdownMenuDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.thoughtcrime.securesms.ui.theme.LocalColors +import org.thoughtcrime.securesms.ui.theme.LocalType +import org.thoughtcrime.securesms.ui.theme.PreviewTheme +import org.thoughtcrime.securesms.ui.theme.bold + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun DropDown( + modifier: Modifier = Modifier, + selectedText: String, + values: List, + onValueSelected: (String) -> Unit +) { + var expanded by remember { mutableStateOf(false) } + + ExposedDropdownMenuBox( + modifier = modifier, + expanded = expanded, + onExpandedChange = { + expanded = !expanded + } + ) { + TextField( + value = selectedText, + onValueChange = {}, + readOnly = true, + trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, + modifier = Modifier + .menuAnchor() + .border( + 1.dp, + color = LocalColors.current.borders, + shape = MaterialTheme.shapes.medium + ), + shape = MaterialTheme.shapes.medium, + colors = ExposedDropdownMenuDefaults.textFieldColors( + focusedContainerColor = LocalColors.current.backgroundSecondary, + unfocusedContainerColor = LocalColors.current.backgroundSecondary, + disabledIndicatorColor = Color.Transparent, + errorIndicatorColor = Color.Transparent, + focusedIndicatorColor = Color.Transparent, + unfocusedIndicatorColor = Color.Transparent, + disabledTrailingIconColor = LocalColors.current.primary, + errorTrailingIconColor = LocalColors.current.primary, + focusedTrailingIconColor = LocalColors.current.primary, + unfocusedTrailingIconColor = LocalColors.current.primary, + disabledTextColor = LocalColors.current.text, + errorTextColor = LocalColors.current.text, + focusedTextColor = LocalColors.current.text, + unfocusedTextColor = LocalColors.current.text + ), + textStyle = LocalType.current.base.bold() + ) + + ExposedDropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false } + ) { + values.forEach { item -> + DropdownMenuItem( + text = { + Text( + text = item, + style = LocalType.current.base + ) + }, + colors = MenuDefaults.itemColors( + textColor = LocalColors.current.text + ), + onClick = { + expanded = false + onValueSelected(item) + } + ) + } + } + } +} + +@Preview +@Composable +fun PreviewDropDown() { + PreviewTheme { + DropDown( + selectedText = "Hello", + values = listOf("First Item", "Second Item", "Third Item"), + onValueSelected = {}) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/SessionTypography.kt b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/SessionTypography.kt index 602affa6af..50c3957cbd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ui/theme/SessionTypography.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ui/theme/SessionTypography.kt @@ -8,11 +8,11 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.sp -fun TextStyle.bold() = TextStyle.Default.copy( +fun TextStyle.bold() = copy( fontWeight = FontWeight.Bold ) -fun TextStyle.monospace() = TextStyle.Default.copy( +fun TextStyle.monospace() = copy( fontFamily = FontFamily.Monospace ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt index c3b7eaca96..f50ac33e28 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/ActivityUtilities.kt @@ -53,7 +53,7 @@ fun AppCompatActivity.push(intent: Intent, isForResult: Boolean = false) { } else { startActivity(intent) } - overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out) + overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) } fun AppCompatActivity.show(intent: Intent, isForResult: Boolean = false) { @@ -108,5 +108,5 @@ data class ThemeState ( ) inline fun Activity.show() = Intent(this, T::class.java).also(::startActivity).let { overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) } -inline fun Activity.push(modify: Intent.() -> Unit = {}) = Intent(this, T::class.java).also(modify).also(::startActivity).let { overridePendingTransition(R.anim.slide_from_right, R.anim.fade_scale_out) } +inline fun Activity.push(modify: Intent.() -> Unit = {}) = Intent(this, T::class.java).also(modify).also(::startActivity).let { overridePendingTransition(R.anim.slide_from_right, R.anim.slide_to_left) } inline fun Context.start(modify: Intent.() -> Unit = {}) = Intent(this, T::class.java).also(modify).apply { addFlags(FLAG_ACTIVITY_SINGLE_TOP) }.let(::startActivity) diff --git a/app/src/main/res/anim/fade_scale_in.xml b/app/src/main/res/anim/fade_scale_in.xml new file mode 100644 index 0000000000..3e004af2c2 --- /dev/null +++ b/app/src/main/res/anim/fade_scale_in.xml @@ -0,0 +1,21 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/anim/slide_to_bottom.xml b/app/src/main/res/anim/slide_to_bottom.xml new file mode 100644 index 0000000000..0f62dfa487 --- /dev/null +++ b/app/src/main/res/anim/slide_to_bottom.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_settings.xml b/app/src/main/res/drawable/ic_settings.xml new file mode 100644 index 0000000000..9fd7185331 --- /dev/null +++ b/app/src/main/res/drawable/ic_settings.xml @@ -0,0 +1,12 @@ + + + + diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt index c3335c5a99..414394dff7 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeAPI.kt @@ -2,7 +2,6 @@ package org.session.libsession.snode -import android.os.Build import com.goterl.lazysodium.exceptions.SodiumException import com.goterl.lazysodium.interfaces.GenericHash import com.goterl.lazysodium.interfaces.PwHash @@ -76,9 +75,7 @@ object SnodeAPI { // Use port 4433 to enforce pinned certificates private val seedNodePort = 4443 - private const val useTestnet = false - - private val seedNodePool = if (useTestnet) setOf( + private val seedNodePool = if (SnodeModule.shared.useTestNet) setOf( "http://public.loki.foundation:38157" ) else setOf( "https://seed1.getsession.org:$seedNodePort", diff --git a/libsession/src/main/java/org/session/libsession/snode/SnodeModule.kt b/libsession/src/main/java/org/session/libsession/snode/SnodeModule.kt index 2deb30998e..a048cc124f 100644 --- a/libsession/src/main/java/org/session/libsession/snode/SnodeModule.kt +++ b/libsession/src/main/java/org/session/libsession/snode/SnodeModule.kt @@ -3,16 +3,18 @@ package org.session.libsession.snode import org.session.libsignal.database.LokiAPIDatabaseProtocol import org.session.libsignal.utilities.Broadcaster -class SnodeModule(val storage: LokiAPIDatabaseProtocol, val broadcaster: Broadcaster) { +class SnodeModule( + val storage: LokiAPIDatabaseProtocol, val broadcaster: Broadcaster, val useTestNet: Boolean +) { companion object { lateinit var shared: SnodeModule val isInitialized: Boolean get() = Companion::shared.isInitialized - fun configure(storage: LokiAPIDatabaseProtocol, broadcaster: Broadcaster) { + fun configure(storage: LokiAPIDatabaseProtocol, broadcaster: Broadcaster, useTestNet: Boolean) { if (isInitialized) { return } - shared = SnodeModule(storage, broadcaster) + shared = SnodeModule(storage, broadcaster, useTestNet) } } } \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/Environment.kt b/libsession/src/main/java/org/session/libsession/utilities/Environment.kt new file mode 100644 index 0000000000..a5a878cae4 --- /dev/null +++ b/libsession/src/main/java/org/session/libsession/utilities/Environment.kt @@ -0,0 +1,5 @@ +package org.session.libsession.utilities + +enum class Environment(val label: String) { + MAIN_NET("Mainnet"), TEST_NET("Testnet") +} \ No newline at end of file diff --git a/libsession/src/main/java/org/session/libsession/utilities/NonTranslatableStringConstants.kt b/libsession/src/main/java/org/session/libsession/utilities/NonTranslatableStringConstants.kt index 5c948c8389..da23762e0c 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/NonTranslatableStringConstants.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/NonTranslatableStringConstants.kt @@ -5,6 +5,7 @@ object NonTranslatableStringConstants { const val APP_NAME = "Session" const val ARBISCAN = "Arbiscan" const val ARBITRUM = "Arbitrum" + const val DEBUG_MENU = "Debug Menu" const val ETHEREUM = "Ethereum" const val NETWORK_NAME = "Session Network" const val TOKEN_NAME_LONG = "Session Token" diff --git a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index b346da7651..4c974e7d60 100644 --- a/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt +++ b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt @@ -18,6 +18,7 @@ import org.session.libsession.utilities.TextSecurePreferences.Companion.AUTOPLAY import org.session.libsession.utilities.TextSecurePreferences.Companion.CALL_NOTIFICATIONS_ENABLED import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_DARK import org.session.libsession.utilities.TextSecurePreferences.Companion.CLASSIC_LIGHT +import org.session.libsession.utilities.TextSecurePreferences.Companion.ENVIRONMENT import org.session.libsession.utilities.TextSecurePreferences.Companion.FOLLOW_SYSTEM_SETTINGS import org.session.libsession.utilities.TextSecurePreferences.Companion.HIDE_PASSWORD import org.session.libsession.utilities.TextSecurePreferences.Companion.LAST_VACUUM_TIME @@ -190,6 +191,8 @@ interface TextSecurePreferences { fun setHidePassword(value: Boolean) fun getLastVersionCheck(): Long fun setLastVersionCheck() + fun getEnvironment(): Environment + fun setEnvironment(value: Environment) companion object { val TAG = TextSecurePreferences::class.simpleName @@ -277,6 +280,7 @@ interface TextSecurePreferences { const val FINGERPRINT_KEY_GENERATED = "fingerprint_key_generated" const val SELECTED_ACCENT_COLOR = "selected_accent_color" const val LAST_VERSION_CHECK = "pref_last_version_check" + const val ENVIRONMENT = "debug_environment" const val HAS_RECEIVED_LEGACY_CONFIG = "has_received_legacy_config" const val HAS_FORCED_NEW_CONFIG = "has_forced_new_config" @@ -1572,6 +1576,17 @@ class AppTextSecurePreferences @Inject constructor( setLongPreference(LAST_VERSION_CHECK, System.currentTimeMillis()) } + override fun getEnvironment(): Environment { + val environment = getStringPreference(ENVIRONMENT, null) + return if (environment != null) { + Environment.valueOf(environment) + } else Environment.MAIN_NET + } + + override fun setEnvironment(value: Environment) { + setStringPreference(ENVIRONMENT, value.name) + } + override fun setShownCallNotification(): Boolean { val previousValue = getBooleanPreference(SHOWN_CALL_NOTIFICATION, false) if (previousValue) return false