diff --git a/app/build.gradle b/app/build.gradle index 5b45b8c9..417b57dc 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { id 'org.jetbrains.kotlin.plugin.serialization' id 'com.mikepenz.aboutlibraries.plugin' id 'com.google.dagger.hilt.android' + id 'kotlin-kapt' } @@ -89,6 +90,7 @@ dependencies { // Hilt - Dependency Injection implementation "com.google.dagger:hilt-android:$hilt_version" + implementation "androidx.hilt:hilt-navigation-compose:1.1.0-alpha01" kapt "com.google.dagger:hilt-compiler:$hilt_version" // Coil - Image Loading diff --git a/app/src/main/java/chat/revolt/MainActivity.kt b/app/src/main/java/chat/revolt/MainActivity.kt index 1e98f3c6..994337a8 100644 --- a/app/src/main/java/chat/revolt/MainActivity.kt +++ b/app/src/main/java/chat/revolt/MainActivity.kt @@ -20,7 +20,9 @@ import chat.revolt.ui.theme.RevoltTheme import com.google.accompanist.navigation.animation.AnimatedNavHost import com.google.accompanist.navigation.animation.rememberAnimatedNavController import com.google.accompanist.navigation.animation.composable +import dagger.hilt.android.AndroidEntryPoint +@AndroidEntryPoint class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) diff --git a/app/src/main/java/chat/revolt/api/RevoltAPI.kt b/app/src/main/java/chat/revolt/api/RevoltAPI.kt index 4ef2dcfe..a163f2e1 100644 --- a/app/src/main/java/chat/revolt/api/RevoltAPI.kt +++ b/app/src/main/java/chat/revolt/api/RevoltAPI.kt @@ -1,9 +1,5 @@ package chat.revolt.api -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.preferencesDataStore import chat.revolt.api.routes.user.fetchSelf import chat.revolt.api.schemas.CompleteUser import io.ktor.client.* @@ -21,8 +17,6 @@ const val REVOLT_FILES = "https://autumn.revolt.chat" private const val BACKEND_IS_STABLE = false -val Context.revoltKVStorage: DataStore by preferencesDataStore(name = "revolt_kv") - val RevoltJson = Json { ignoreUnknownKeys = true } val RevoltHttp = HttpClient(OkHttp) { @@ -51,6 +45,7 @@ val RevoltHttp = HttpClient(OkHttp) { } } + object RevoltAPI { const val TOKEN_HEADER_NAME = "x-session-token" diff --git a/app/src/main/java/chat/revolt/persistence/KVStorage.kt b/app/src/main/java/chat/revolt/persistence/KVStorage.kt new file mode 100644 index 00000000..a72b361e --- /dev/null +++ b/app/src/main/java/chat/revolt/persistence/KVStorage.kt @@ -0,0 +1,38 @@ +package chat.revolt.persistence + +import android.content.Context +import androidx.datastore.core.DataStore +import androidx.datastore.preferences.core.Preferences +import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.stringPreferencesKey +import androidx.datastore.preferences.preferencesDataStore +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.flow.firstOrNull +import javax.inject.Inject +import javax.inject.Singleton + +val Context.revoltKVStorage: DataStore by preferencesDataStore(name = "revolt_kv") + +@Singleton +class KVStorage @Inject constructor( + @ApplicationContext private val mContext: Context +) { + private val dataStore = mContext.revoltKVStorage + + suspend fun set(key: String, value: String) { + dataStore.edit { preferences -> + preferences[stringPreferencesKey(key)] = value + } + } + + suspend fun get(key: String): String? { + return dataStore.data.firstOrNull()?.get(stringPreferencesKey(key)) + } + + suspend fun remove(key: String) { + dataStore.edit { preferences -> + preferences.remove(stringPreferencesKey(key)) + } + } +} + diff --git a/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt b/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt index 20f39a30..8dbed927 100644 --- a/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt +++ b/app/src/main/java/chat/revolt/screens/chat/HomeScreen.kt @@ -13,13 +13,31 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel +import androidx.lifecycle.ViewModel import androidx.navigation.NavController import chat.revolt.api.REVOLT_FILES import chat.revolt.api.RevoltAPI import chat.revolt.components.generic.RemoteImage +import chat.revolt.persistence.KVStorage +import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.runBlocking +import javax.inject.Inject + +@HiltViewModel +class HomeScreenViewModel @Inject constructor( + private val kvStorage: KVStorage +) : ViewModel() { + fun logout() { + runBlocking { + kvStorage.remove("sessionToken") + RevoltAPI.logout() + } + } +} @Composable -fun HomeScreen(navController: NavController) { +fun HomeScreen(navController: NavController, viewModel: HomeScreenViewModel = hiltViewModel()) { val user = RevoltAPI.userCache[RevoltAPI.selfId] Column() { @@ -61,7 +79,7 @@ fun HomeScreen(navController: NavController) { } Button( onClick = { - RevoltAPI.logout() + viewModel.logout() navController.navigate("login/greeting") { popUpTo("chat/home") { inclusive = true diff --git a/app/src/main/java/chat/revolt/screens/login/GreeterScreen.kt b/app/src/main/java/chat/revolt/screens/login/GreeterScreen.kt index de7fa58f..ee83b26e 100644 --- a/app/src/main/java/chat/revolt/screens/login/GreeterScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/GreeterScreen.kt @@ -19,16 +19,22 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import chat.revolt.api.RevoltAPI import chat.revolt.components.generic.RemoteImage import chat.revolt.components.generic.drawableResource +import chat.revolt.persistence.KVStorage +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import javax.inject.Inject -class GreeterViewModel() : ViewModel() { +@HiltViewModel +class GreeterViewModel @Inject constructor( + private val kvStorage: KVStorage +) : ViewModel() { private var _skipLogin by mutableStateOf(false) val skipLogin: Boolean get() = _skipLogin @@ -47,17 +53,24 @@ class GreeterViewModel() : ViewModel() { init { viewModelScope.launch { + val token = kvStorage.get("sessionToken") + if (token != null) { + RevoltAPI.setSessionHeader(token) + } + RevoltAPI.initialize() + if (RevoltAPI.isLoggedIn()) { _skipLogin = true } + setFinishedLoading(true) } } } @Composable -fun GreeterScreen(navController: NavController, viewModel: GreeterViewModel = viewModel()) { +fun GreeterScreen(navController: NavController, viewModel: GreeterViewModel = hiltViewModel()) { if (viewModel.skipLogin) { navController.navigate("chat/home") { popUpTo("login/greeting") { diff --git a/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt b/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt index 7898dfcc..b9919ec0 100644 --- a/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt @@ -15,9 +15,9 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import chat.revolt.R import chat.revolt.api.REVOLT_SUPPORT @@ -27,9 +27,15 @@ import chat.revolt.api.routes.user.fetchSelfWithNewToken import chat.revolt.components.generic.AnyLink import chat.revolt.components.generic.FormTextField import chat.revolt.components.generic.Weblink +import chat.revolt.persistence.KVStorage +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import javax.inject.Inject -class LoginViewModel() : ViewModel() { +@HiltViewModel +class LoginViewModel @Inject constructor( + private val kvStorage: KVStorage, +) : ViewModel() { private var _email by mutableStateOf("") val email: String get() = _email @@ -68,7 +74,10 @@ class LoginViewModel() : ViewModel() { "Login", "No MFA required. Login is complete! We have a session token: ${response.firstUserHints!!.token}" ) + fetchSelfWithNewToken(response.firstUserHints.token) + kvStorage.set("sessionToken", response.firstUserHints.token) + _navigateTo = "home" } } @@ -91,7 +100,7 @@ class LoginViewModel() : ViewModel() { @Composable fun LoginScreen( navController: NavController, - viewModel: LoginViewModel = viewModel() + viewModel: LoginViewModel = hiltViewModel() ) { if (viewModel.navigateTo == "mfa") { navController.navigate( diff --git a/app/src/main/java/chat/revolt/screens/login/MfaScreen.kt b/app/src/main/java/chat/revolt/screens/login/MfaScreen.kt index fbd710e0..86631669 100644 --- a/app/src/main/java/chat/revolt/screens/login/MfaScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/MfaScreen.kt @@ -18,9 +18,9 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp +import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavController import chat.revolt.R import chat.revolt.api.routes.account.MfaResponseRecoveryCode @@ -30,9 +30,15 @@ import chat.revolt.api.routes.account.authenticateWithMfaTotpCode import chat.revolt.api.routes.user.fetchSelfWithNewToken import chat.revolt.components.generic.CollapsibleCard import chat.revolt.components.generic.FormTextField +import chat.revolt.persistence.KVStorage +import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.launch +import javax.inject.Inject -class MfaScreenViewModel : ViewModel() { +@HiltViewModel +class MfaScreenViewModel @Inject constructor( + private val kvStorage: KVStorage, +) : ViewModel() { private var _totpCode by mutableStateOf("") val totpCode: String get() = _totpCode @@ -72,8 +78,10 @@ class MfaScreenViewModel : ViewModel() { "MFA", "Successfully authorized TOTP. Token: ${response.firstUserHints!!.token}" ) - val self = fetchSelfWithNewToken(response.firstUserHints.token) - Log.d("MFA", "Self: ${self.username}") + + fetchSelfWithNewToken(response.firstUserHints.token) + kvStorage.set("sessionToken", response.firstUserHints.token) + _navigateToHome = true } } @@ -91,8 +99,10 @@ class MfaScreenViewModel : ViewModel() { "MFA", "Successfully authorized recovery code. Token: ${response.firstUserHints!!.token}" ) - val self = fetchSelfWithNewToken(response.firstUserHints.token) - Log.d("MFA", "Self: ${self.username}") + + fetchSelfWithNewToken(response.firstUserHints.token) + kvStorage.set("sessionToken", response.firstUserHints.token) + _navigateToHome = true } } @@ -104,7 +114,7 @@ fun MfaScreen( navController: NavController, allowedAuthTypesCommaSep: String, mfaTicket: String, - viewModel: MfaScreenViewModel = viewModel() + viewModel: MfaScreenViewModel = hiltViewModel() ) { val allowedAuthTypes = allowedAuthTypesCommaSep.split(",")