diff --git a/app/build.gradle b/app/build.gradle index df003aa855..94edee60ad 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -241,6 +241,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 98f9aa4b5f..f8765ff7bd 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/activity_settings_title" /> + <activity + android:name="org.thoughtcrime.securesms.debugmenu.DebugActivity" + android:screenOrientation="portrait" + android:theme="@style/Theme.Session.DayNight.NoActionBar" /> <activity android:name="org.thoughtcrime.securesms.home.PathActivity" android:screenOrientation="portrait" /> 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/conversation/v2/ConversationActivityV2.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityV2.kt index d3192abc05..69844e33b6 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 @@ -2085,7 +2085,10 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe override fun showMessageDetail(messages: Set<MessageRecord>) { 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 b6f9e148b5..065ae8e11d 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<Attachment>, 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<Attachment>, onClick: (Int) -> Unit) { ExperimentalGlideComposeApi::class ) @Composable -private fun CellCarousel( +private fun CarouselPager( pagerState: PagerState, attachments: List<Attachment>, 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.message_details_header__file_id, "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.message_details_header__file_id, "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.message_details_header__file_id, "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.message_details_header__file_id, "Screen Shot 2023-07-06 at 11.35.50 am.png"), TitledText(R.string.message_details_header__file_type, "image/png"), @@ -337,7 +375,7 @@ fun PreviewMessageDetails( fun FileDetails(fileDetails: List<TitledText>) { 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<UIState> + 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<String>, + 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 e56b55aaab..c2f32e553c 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 @@ -45,7 +45,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 17d97dec7b..80c8da8a3f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/ClearAllDataDialog.kt @@ -125,7 +125,7 @@ class ClearAllDataDialog : DialogFragment() { } return } - ApplicationContext.getInstance(context).clearAllData().let { success -> + ApplicationContext.getInstance(context).clearAllDataAndRestart().let { success -> withContext(Main) { if (success) { dismiss() @@ -162,7 +162,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 e8b804f036..1a77201bc9 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/preferences/SettingsActivity.kt @@ -68,6 +68,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 @@ -91,7 +92,6 @@ import org.thoughtcrime.securesms.util.BitmapUtil import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities import org.thoughtcrime.securesms.util.NetworkUtils import org.thoughtcrime.securesms.util.push -import org.thoughtcrime.securesms.util.show import java.io.File import javax.inject.Inject @@ -162,6 +162,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() { @@ -174,7 +177,8 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { btnGroupNameDisplay.text = getDisplayName() publicKeyTextView.text = hexEncodedPublicKey val gitCommitFirstSixChars = BuildConfig.GIT_HASH.take(6) - versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars)") + val environment: String = if(BuildConfig.BUILD_TYPE == "release") "" else " - ${prefs.getEnvironment().label}" + versionTextView.text = String.format(getString(R.string.version_s), "${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE} - $gitCommitFirstSixChars) $environment") } binding.composeView.setThemedContent { @@ -182,6 +186,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) @@ -473,10 +482,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), ) { @@ -498,27 +509,33 @@ class SettingsActivity : PassphraseRequiredActionBarActivity() { Cell { Column { + // add the debug menu in non release builds + if (BuildConfig.BUILD_TYPE != "release") { + LargeItemButton(R.string.activity_settings_debug_button_title, R.drawable.ic_settings) { push<DebugActivity>() } + Divider() + } + Crossfade(if (hasPaths) R.drawable.ic_status else R.drawable.ic_path_yellow, label = "path") { - LargeItemButtonWithDrawable(R.string.activity_path_title, it) { show<PathActivity>() } + LargeItemButtonWithDrawable(R.string.activity_path_title, it) { push<PathActivity>() } } Divider() - LargeItemButton(R.string.activity_settings_privacy_button_title, R.drawable.ic_privacy_icon) { show<PrivacySettingsActivity>() } + LargeItemButton(R.string.activity_settings_privacy_button_title, R.drawable.ic_privacy_icon) { push<PrivacySettingsActivity>() } Divider() - LargeItemButton(R.string.activity_settings_notifications_button_title, R.drawable.ic_speaker, Modifier.contentDescription(R.string.AccessibilityId_notifications)) { show<NotificationSettingsActivity>() } + LargeItemButton(R.string.activity_settings_notifications_button_title, R.drawable.ic_speaker, Modifier.contentDescription(R.string.AccessibilityId_notifications)) { push<NotificationSettingsActivity>() } Divider() - LargeItemButton(R.string.activity_settings_conversations_button_title, R.drawable.ic_conversations, Modifier.contentDescription(R.string.AccessibilityId_conversations)) { show<ChatSettingsActivity>() } + LargeItemButton(R.string.activity_settings_conversations_button_title, R.drawable.ic_conversations, Modifier.contentDescription(R.string.AccessibilityId_conversations)) { push<ChatSettingsActivity>() } Divider() - LargeItemButton(R.string.activity_settings_message_requests_button_title, R.drawable.ic_message_requests, Modifier.contentDescription(R.string.AccessibilityId_message_requests)) { show<MessageRequestsActivity>() } + LargeItemButton(R.string.activity_settings_message_requests_button_title, R.drawable.ic_message_requests, Modifier.contentDescription(R.string.AccessibilityId_message_requests)) { push<MessageRequestsActivity>() } Divider() - LargeItemButton(R.string.activity_settings_message_appearance_button_title, R.drawable.ic_appearance, Modifier.contentDescription(R.string.AccessibilityId_appearance)) { show<AppearanceSettingsActivity>() } + LargeItemButton(R.string.activity_settings_message_appearance_button_title, R.drawable.ic_appearance, Modifier.contentDescription(R.string.AccessibilityId_appearance)) { push<AppearanceSettingsActivity>() } Divider() LargeItemButton(R.string.activity_settings_invite_button_title, R.drawable.ic_invite_friend, Modifier.contentDescription(R.string.AccessibilityId_invite_friend)) { sendInvitationToUseSession() } Divider() if (!prefs.getHidePassword()) { - LargeItemButton(R.string.sessionRecoveryPassword, R.drawable.ic_shield_outline, Modifier.contentDescription(R.string.AccessibilityId_recovery_password_menu_item)) { show<RecoveryPasswordActivity>() } + LargeItemButton(R.string.sessionRecoveryPassword, R.drawable.ic_shield_outline, Modifier.contentDescription(R.string.AccessibilityId_recovery_password_menu_item)) { push<RecoveryPasswordActivity>() } Divider() } - LargeItemButton(R.string.activity_settings_help_button, R.drawable.ic_help, Modifier.contentDescription(R.string.AccessibilityId_help)) { show<HelpSettingsActivity>() } + LargeItemButton(R.string.activity_settings_help_button, R.drawable.ic_help, Modifier.contentDescription(R.string.AccessibilityId_help)) { push<HelpSettingsActivity>() } Divider() LargeItemButton(R.string.activity_settings_clear_all_data_button_title, R.drawable.ic_delete, Modifier.contentDescription(R.string.AccessibilityId_clear_data), dangerButtonColors()) { ClearAllDataDialog().show(supportFragmentManager, "Clear All Data Dialog") } } 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 5f59943432..0b47708b5d 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_recovery_password) .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 5894e1072a..552e24cc58 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<DialogButtonModel>? = 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 daeb6b853d..df64f093ec 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 <T> OptionsCard(card: OptionsCardData<T>, callbacks: Callbacks<T>) { Spacer(modifier = Modifier.height(LocalDimensions.current.xsSpacing)) - CellNoMargin { + Cell { LazyColumn( modifier = Modifier.heightIn(max = 5000.dp) ) { @@ -257,32 +258,19 @@ fun PrewviewItemButton() { @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<String>, + 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 <reified T: Activity> Activity.show() = Intent(this, T::class.java).also(::startActivity).let { overridePendingTransition(R.anim.slide_from_bottom, R.anim.fade_scale_out) } -inline fun <reified T: Activity> 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 <reified T: Activity> 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 <reified T: Activity> 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 @@ +<?xml version="1.0" encoding="utf-8"?> + +<set + xmlns:android="http://schemas.android.com/apk/res/android" + android:interpolator="@android:anim/decelerate_interpolator"> + + <scale + android:duration="250" + android:fromXScale="0.85" + android:fromYScale="0.85" + android:toXScale="1.0" + android:toYScale="1.0" + android:pivotX="50%" + android:pivotY="50%" /> + + <alpha + android:duration="250" + android:fromAlpha="0.6" + android:toAlpha="1" /> + +</set> \ 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 @@ +<?xml version="1.0" encoding="utf-8"?> +<set + xmlns:android="http://schemas.android.com/apk/res/android" + android:interpolator="@android:anim/decelerate_interpolator"> + + <translate + android:duration="250" + android:fromYDelta="0%" + android:toYDelta="100%" /> + +</set> \ 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 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="32dp" + android:height="32dp" + android:viewportWidth="50" + android:viewportHeight="50"> + <path + android:pathData="M25,15.916C19.991,15.916 15.916,19.991 15.916,25C15.916,30.009 19.991,34.084 25,34.084C30.009,34.084 34.084,30.009 34.084,25C34.084,19.991 30.009,15.916 25,15.916ZM25,31.813C21.243,31.813 18.187,28.757 18.187,25C18.187,21.243 21.243,18.187 25,18.187C28.757,18.187 31.813,21.243 31.813,25C31.813,28.757 28.757,31.813 25,31.813Z" + android:fillColor="#000000"/> + <path + android:pathData="M43.106,20.673L40.185,20.038C39.931,19.261 39.617,18.502 39.245,17.772L40.862,15.257C41.151,14.807 41.088,14.217 40.71,13.84L36.16,9.29C35.783,8.913 35.193,8.849 34.743,9.138L32.228,10.755C31.498,10.383 30.739,10.069 29.962,9.815L29.327,6.894C29.213,6.372 28.751,6 28.217,6H21.783C21.249,6 20.787,6.372 20.673,6.894L20.038,9.815C19.261,10.069 18.502,10.383 17.772,10.755L15.257,9.138C14.807,8.849 14.217,8.913 13.84,9.29L9.29,13.84C8.913,14.217 8.849,14.807 9.138,15.257L10.755,17.772C10.383,18.502 10.069,19.261 9.815,20.038L6.894,20.673C6.372,20.787 6,21.249 6,21.783V28.217C6,28.751 6.372,29.213 6.894,29.327L9.815,29.962C10.069,30.739 10.383,31.498 10.755,32.228L9.138,34.743C8.849,35.193 8.913,35.783 9.29,36.16L13.84,40.71C14.217,41.088 14.807,41.151 15.257,40.862L17.772,39.245C18.502,39.617 19.261,39.931 20.038,40.185L20.673,43.106C20.787,43.628 21.249,44 21.783,44H28.217C28.751,44 29.213,43.628 29.327,43.106L29.962,40.185C30.739,39.931 31.498,39.617 32.228,39.245L34.743,40.862C35.193,41.151 35.783,41.088 36.16,40.71L40.71,36.16C41.088,35.783 41.151,35.193 40.862,34.743L39.245,32.228C39.617,31.498 39.931,30.739 40.185,29.962L43.106,29.327C43.628,29.213 44,28.751 44,28.217V21.783C44,21.249 43.628,20.787 43.106,20.673ZM41.729,27.302L39.051,27.884C38.64,27.974 38.312,28.283 38.199,28.689C37.904,29.744 37.481,30.765 36.94,31.723C36.734,32.089 36.746,32.541 36.974,32.895L38.457,35.201L35.202,38.457L32.895,36.974C32.541,36.746 32.089,36.734 31.723,36.94C30.765,37.481 29.744,37.904 28.689,38.199C28.283,38.312 27.974,38.64 27.884,39.051L27.302,41.729H22.698L22.116,39.051C22.026,38.64 21.717,38.312 21.311,38.199C20.256,37.904 19.235,37.481 18.277,36.94C17.911,36.734 17.459,36.747 17.105,36.974L14.799,38.457L11.543,35.201L13.026,32.895C13.254,32.541 13.267,32.089 13.06,31.723C12.519,30.765 12.096,29.744 11.802,28.689C11.689,28.283 11.361,27.974 10.949,27.884L8.271,27.302V22.698L10.949,22.116C11.36,22.026 11.689,21.717 11.802,21.311C12.096,20.256 12.519,19.235 13.059,18.277C13.267,17.911 13.254,17.459 13.026,17.105L11.543,14.799L14.798,11.543L17.105,13.026C17.459,13.254 17.911,13.267 18.277,13.059C19.235,12.519 20.256,12.096 21.311,11.802C21.717,11.689 22.026,11.36 22.116,10.949L22.698,8.271H27.302L27.884,10.949C27.974,11.36 28.283,11.689 28.689,11.802C29.744,12.096 30.765,12.519 31.723,13.059C32.089,13.267 32.541,13.253 32.895,13.026L35.201,11.543L38.457,14.799L36.974,17.105C36.746,17.459 36.733,17.911 36.94,18.277C37.481,19.235 37.904,20.256 38.199,21.311C38.312,21.717 38.639,22.026 39.051,22.116L41.729,22.698V27.302Z" + android:fillColor="#000000"/> +</vector> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 537038921d..d31a433436 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -787,6 +787,7 @@ <string name="activity_settings_display_name_edit_text_hint">Enter a display name</string> <string name="activity_settings_display_name_missing_error">Please pick a display name</string> <string name="activity_settings_display_name_too_long_error">Please pick a shorter display name</string> + <string name="activity_settings_debug_button_title">Debug Menu</string> <string name="activity_settings_privacy_button_title">Privacy</string> <string name="activity_settings_notifications_button_title">Notifications</string> <string name="activity_settings_message_requests_button_title">Message Requests</string> 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/TextSecurePreferences.kt b/libsession/src/main/java/org/session/libsession/utilities/TextSecurePreferences.kt index 5bf109843d..7a60b41bc5 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" @@ -1554,6 +1558,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