diff --git a/app/build.gradle b/app/build.gradle index a6360534..4286475e 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -156,6 +156,9 @@ dependencies { // AboutLibraries - automated OSS library attribution implementation "com.mikepenz:aboutlibraries-compose:$aboutlibraries_version" + // Lottie - animated vector graphics + implementation "com.airbnb.android:lottie-compose:6.0.0" + // Sentry - crash reporting implementation 'io.sentry:sentry-android:6.15.0' implementation 'io.sentry:sentry-compose-android:6.15.0' diff --git a/app/src/main/java/chat/revolt/MainActivity.kt b/app/src/main/java/chat/revolt/MainActivity.kt index 43369d82..d6eec214 100644 --- a/app/src/main/java/chat/revolt/MainActivity.kt +++ b/app/src/main/java/chat/revolt/MainActivity.kt @@ -26,6 +26,7 @@ import chat.revolt.screens.login.GreeterScreen import chat.revolt.screens.login.LoginScreen import chat.revolt.screens.login.MfaScreen import chat.revolt.screens.settings.AppearanceSettingsScreen +import chat.revolt.screens.settings.DebugSettingsScreen import chat.revolt.screens.settings.SettingsScreen import chat.revolt.ui.theme.RevoltTheme import com.google.accompanist.navigation.animation.AnimatedNavHost @@ -111,6 +112,7 @@ fun AppEntrypoint() { composable("settings") { SettingsScreen(navController) } composable("settings/appearance") { AppearanceSettingsScreen(navController) } + composable("settings/debug") { DebugSettingsScreen(navController) } dialog("settings/feedback") { FeedbackDialog(navController) } composable("about") { AboutScreen(navController) } diff --git a/app/src/main/java/chat/revolt/persistence/KVStorage.kt b/app/src/main/java/chat/revolt/persistence/KVStorage.kt index a72b361e..81277345 100644 --- a/app/src/main/java/chat/revolt/persistence/KVStorage.kt +++ b/app/src/main/java/chat/revolt/persistence/KVStorage.kt @@ -29,6 +29,16 @@ class KVStorage @Inject constructor( return dataStore.data.firstOrNull()?.get(stringPreferencesKey(key)) } + suspend fun set(key: String, value: Boolean) { + dataStore.edit { preferences -> + preferences[stringPreferencesKey(key)] = value.toString() + } + } + + suspend fun getBoolean(key: String): Boolean? { + return dataStore.data.firstOrNull()?.get(stringPreferencesKey(key))?.toBoolean() + } + suspend fun remove(key: String) { dataStore.edit { preferences -> preferences.remove(stringPreferencesKey(key)) diff --git a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt index 746adc9c..df9e0d7e 100644 --- a/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/ChatRouterScreen.kt @@ -6,6 +6,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -16,13 +17,17 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add +import androidx.compose.material3.AlertDialog import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment @@ -64,6 +69,11 @@ import chat.revolt.screens.chat.sheets.StatusSheet import chat.revolt.screens.chat.views.HomeScreen import chat.revolt.screens.chat.views.NoCurrentChannelScreen import chat.revolt.screens.chat.views.channel.ChannelScreen +import com.airbnb.lottie.RenderMode +import com.airbnb.lottie.compose.LottieAnimation +import com.airbnb.lottie.compose.LottieCompositionSpec +import com.airbnb.lottie.compose.animateLottieCompositionAsState +import com.airbnb.lottie.compose.rememberLottieComposition import com.google.accompanist.navigation.material.ExperimentalMaterialNavigationApi import com.google.accompanist.navigation.material.ModalBottomSheetLayout import com.google.accompanist.navigation.material.bottomSheet @@ -85,10 +95,19 @@ class ChatRouterViewModel @Inject constructor( val currentChannel: String? get() = _currentChannel.value + private var _sidebarSparkDisplayed = mutableStateOf(true) + val sidebarSparkDisplayed: Boolean + get() = _sidebarSparkDisplayed.value + init { viewModelScope.launch { _currentServer.value = kvStorage.get("currentServer") ?: "home" _currentChannel.value = kvStorage.get("currentChannel") + _sidebarSparkDisplayed.value = if (kvStorage.getBoolean("sidebarSpark") == null) { + false + } else { + kvStorage.getBoolean("sidebarSpark")!! + } } } @@ -108,6 +127,10 @@ class ChatRouterViewModel @Inject constructor( } } + suspend fun setSettingsHintDisplayed() { + kvStorage.set("sidebarSpark", true) + } + fun navigateToServer(serverId: String, navController: NavController) { if (serverId == "home") { navController.navigate("home") { @@ -155,6 +178,12 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil val bottomSheetNavigator = rememberBottomSheetNavigator() val navController = rememberNavController(bottomSheetNavigator) + val showSidebarSpark = remember { mutableStateOf(false) } + val sidebarSparkComposition by rememberLottieComposition(LottieCompositionSpec.RawRes(R.raw.open_settings_tutorial)) + val sidebarSparkProgress by animateLottieCompositionAsState( + composition = sidebarSparkComposition, + ) + LaunchedEffect(drawerState) { snapshotFlow { drawerState.currentValue } .distinctUntilChanged() @@ -175,11 +204,52 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil } } + LaunchedEffect(viewModel.sidebarSparkDisplayed) { + snapshotFlow { viewModel.sidebarSparkDisplayed } + .distinctUntilChanged() + .collect { displayed -> + showSidebarSpark.value = !displayed + } + } + ModalBottomSheetLayout( sheetShape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), sheetBackgroundColor = MaterialTheme.colorScheme.surface, bottomSheetNavigator = bottomSheetNavigator, ) { + if (showSidebarSpark.value) { + AlertDialog( + onDismissRequest = {}, + title = { + Text(stringResource(id = R.string.spark_sidebar_settings_tutorial)) + }, + text = { + Column { + LottieAnimation( + composition = sidebarSparkComposition, + progress = { sidebarSparkProgress }, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1f), + renderMode = RenderMode.HARDWARE + ) + Text(stringResource(id = R.string.spark_sidebar_settings_tutorial_description_1)) + Text(stringResource(id = R.string.spark_sidebar_settings_tutorial_description_2)) + } + }, + confirmButton = { + TextButton(onClick = { + scope.launch { + viewModel.setSettingsHintDisplayed() + } + showSidebarSpark.value = false + }) { + Text(stringResource(id = R.string.spark_sidebar_settings_tutorial_acknowledge)) + } + } + ) + } + Column { AnimatedVisibility(visible = RealtimeSocket.disconnectionState != DisconnectionState.Connected) { DisconnectedNotice( @@ -255,7 +325,10 @@ fun ChatRouterScreen(topNav: NavController, viewModel: ChatRouterViewModel = hil } } - Crossfade(targetState = viewModel.currentServer) { + Crossfade( + targetState = viewModel.currentServer, + label = "Channel List" + ) { ChannelList( serverId = it, drawerState = drawerState, diff --git a/app/src/main/java/chat/revolt/screens/settings/DebugSettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/DebugSettingsScreen.kt new file mode 100644 index 00000000..4757ab17 --- /dev/null +++ b/app/src/main/java/chat/revolt/screens/settings/DebugSettingsScreen.kt @@ -0,0 +1,82 @@ +package chat.revolt.screens.settings + +import androidx.compose.foundation.horizontalScroll +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import androidx.navigation.NavController +import chat.revolt.components.generic.PageHeader +import chat.revolt.persistence.KVStorage +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.launch +import javax.inject.Inject + +@HiltViewModel +class DebugSettingsScreenViewModel @Inject constructor( + private val kvStorage: KVStorage, +) : ViewModel() { + fun forgetSidebarSparkShown() { + viewModelScope.launch { + kvStorage.remove("sidebarSpark") + } + } + + fun forgetAllSparks() { + this.forgetSidebarSparkShown() + } +} + +@Composable +fun DebugSettingsScreen( + navController: NavController, + viewModel: DebugSettingsScreenViewModel = hiltViewModel() +) { + Column( + modifier = Modifier + .fillMaxSize() + ) { + PageHeader( + text = "Debug", + showBackButton = true, + onBackButtonClicked = { + navController.popBackStack() + }) + + + Column( + modifier = Modifier + .fillMaxSize() + .verticalScroll(rememberScrollState()) + .padding(20.dp) + ) { + Text( + text = "Sparks", + style = MaterialTheme.typography.headlineSmall, + modifier = Modifier.padding(bottom = 10.dp) + ) + Row( + modifier = Modifier.horizontalScroll(rememberScrollState()) + ) { + TextButton(onClick = { viewModel.forgetSidebarSparkShown() }) { + Text("Forget sidebar spark") + } + ElevatedButton(onClick = { viewModel.forgetAllSparks() }) { + Text("Forget all sparks") + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt index abb16071..441cca7c 100644 --- a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt @@ -8,6 +8,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Build import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.Divider import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -18,6 +19,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.navigation.NavController +import chat.revolt.BuildConfig import chat.revolt.R import chat.revolt.components.generic.PageHeader import chat.revolt.components.generic.SheetClickable @@ -78,6 +80,24 @@ fun SettingsScreen( navController.navigate("about") } + if (BuildConfig.DEBUG) { + SheetClickable( + icon = { modifier -> + Icon( + imageVector = Icons.Default.Settings, + contentDescription = "Debug", + modifier = modifier + ) + }, + label = { textStyle -> + Text(text = "Debug", style = textStyle) + }, + modifier = Modifier.testTag("settings_view_debug") + ) { + navController.navigate("settings/debug") + } + } + Divider() SheetClickable( @@ -109,7 +129,8 @@ fun SettingsScreen( }, modifier = Modifier.testTag("settings_view_logout") ) { - Toast.makeText(navController.context, "Not implemented yet", Toast.LENGTH_SHORT).show() + Toast.makeText(navController.context, "Not implemented yet", Toast.LENGTH_SHORT) + .show() } } } diff --git a/app/src/main/res/raw/open_settings_tutorial.lottie b/app/src/main/res/raw/open_settings_tutorial.lottie new file mode 100644 index 00000000..082eb081 Binary files /dev/null and b/app/src/main/res/raw/open_settings_tutorial.lottie differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 80b06b0c..f0deccbb 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -244,6 +244,11 @@ Share video Share image + The settings are in the sidebar + You can open the sidebar by swiping from the left edge of the screen. + Then long tap your profile picture to open the settings. + Got it + Appearance Theme System