From 0245edc8f1e50bcadd867ff3f10530dac63c9682 Mon Sep 17 00:00:00 2001 From: Infi Date: Mon, 23 Oct 2023 00:15:06 +0200 Subject: [PATCH] feat: session settings Signed-off-by: Infi --- .idea/.gitignore | 1 + .../chat/revolt/activities/MainActivity.kt | 2 + .../main/java/chat/revolt/api/RevoltAPI.kt | 9 +- .../chat/revolt/api/routes/auth/Sessions.kt | 30 +++ .../main/java/chat/revolt/api/schemas/Auth.kt | 15 ++ .../settings/sessions/SessionItem.kt | 87 +++++++ .../java/chat/revolt/screens/SplashScreen.kt | 17 +- .../chat/revolt/screens/login/LoginScreen.kt | 5 +- .../chat/revolt/screens/login/MfaScreen.kt | 8 +- .../screens/settings/SessionSettngsScreen.kt | 239 ++++++++++++++++++ .../revolt/screens/settings/SettingsScreen.kt | 29 ++- .../res/drawable/ic_tablet_cellphone_24dp.xml | 9 + app/src/main/res/values/strings.xml | 12 + 13 files changed, 455 insertions(+), 8 deletions(-) create mode 100644 app/src/main/java/chat/revolt/api/routes/auth/Sessions.kt create mode 100644 app/src/main/java/chat/revolt/api/schemas/Auth.kt create mode 100644 app/src/main/java/chat/revolt/components/settings/sessions/SessionItem.kt create mode 100644 app/src/main/java/chat/revolt/screens/settings/SessionSettngsScreen.kt create mode 100644 app/src/main/res/drawable/ic_tablet_cellphone_24dp.xml diff --git a/.idea/.gitignore b/.idea/.gitignore index 50bebb8e..e84d23c7 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -3,3 +3,4 @@ /workspace.xml /kotlinc.xml /appInsightsSettings.xml +/other.xml diff --git a/app/src/main/java/chat/revolt/activities/MainActivity.kt b/app/src/main/java/chat/revolt/activities/MainActivity.kt index e03c8d45..134cb536 100644 --- a/app/src/main/java/chat/revolt/activities/MainActivity.kt +++ b/app/src/main/java/chat/revolt/activities/MainActivity.kt @@ -43,6 +43,7 @@ import chat.revolt.screens.settings.AppearanceSettingsScreen import chat.revolt.screens.settings.ChangelogsSettingsScreen import chat.revolt.screens.settings.ClosedBetaUpdaterScreen import chat.revolt.screens.settings.DebugSettingsScreen +import chat.revolt.screens.settings.SessionSettingsScreen import chat.revolt.screens.settings.SettingsScreen import chat.revolt.ui.theme.RevoltTheme import dagger.hilt.android.AndroidEntryPoint @@ -142,6 +143,7 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) { composable("chat") { ChatRouterScreen(navController, windowSizeClass) } composable("settings") { SettingsScreen(navController) } + composable("settings/sessions") { SessionSettingsScreen(navController) } composable("settings/appearance") { AppearanceSettingsScreen(navController) } composable("settings/debug") { DebugSettingsScreen(navController) } composable("settings/updater") { ClosedBetaUpdaterScreen(navController) } diff --git a/app/src/main/java/chat/revolt/api/RevoltAPI.kt b/app/src/main/java/chat/revolt/api/RevoltAPI.kt index ae8d57b9..c66a9d50 100644 --- a/app/src/main/java/chat/revolt/api/RevoltAPI.kt +++ b/app/src/main/java/chat/revolt/api/RevoltAPI.kt @@ -9,7 +9,6 @@ import chat.revolt.api.internals.Members import chat.revolt.api.realtime.DisconnectionState import chat.revolt.api.realtime.RealtimeSocket import chat.revolt.api.routes.user.fetchSelf -import chat.revolt.api.schemas.Channel as ChannelSchema import chat.revolt.api.schemas.Emoji import chat.revolt.api.schemas.Message import chat.revolt.api.schemas.Server @@ -39,6 +38,7 @@ import kotlinx.coroutines.withContext import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json +import chat.revolt.api.schemas.Channel as ChannelSchema const val REVOLT_BASE = "https://api.revolt.chat" const val REVOLT_SUPPORT = "https://support.revolt.chat" @@ -118,6 +118,8 @@ object RevoltAPI { var sessionToken: String = "" private set + var sessionId: String = "" + private set @OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class) val realtimeContext = newSingleThreadContext("RealtimeContext") @@ -129,6 +131,10 @@ object RevoltAPI { sessionToken = token } + fun setSessionId(id: String) { + sessionId = id + } + suspend fun loginAs(token: String) { setSessionHeader(token) fetchSelf() @@ -189,6 +195,7 @@ object RevoltAPI { fun logout() { selfId = null sessionToken = "" + sessionId = "" userCache.clear() serverCache.clear() diff --git a/app/src/main/java/chat/revolt/api/routes/auth/Sessions.kt b/app/src/main/java/chat/revolt/api/routes/auth/Sessions.kt new file mode 100644 index 00000000..2d61ecc4 --- /dev/null +++ b/app/src/main/java/chat/revolt/api/routes/auth/Sessions.kt @@ -0,0 +1,30 @@ +package chat.revolt.api.routes.auth + +import chat.revolt.api.RevoltHttp +import chat.revolt.api.RevoltJson +import chat.revolt.api.schemas.Session +import io.ktor.client.request.delete +import io.ktor.client.request.get +import io.ktor.client.request.parameter +import io.ktor.client.statement.bodyAsText +import kotlinx.serialization.builtins.ListSerializer + +suspend fun fetchAllSessions(): List { + val response = RevoltHttp.get("/auth/session/all") + .bodyAsText() + + return RevoltJson.decodeFromString( + ListSerializer(Session.serializer()), + response + ) +} + +suspend fun logoutSessionById(id: String) { + RevoltHttp.delete("/auth/session/$id") +} + +suspend fun logoutAllSessions(includingSelf: Boolean = false) { + RevoltHttp.delete("/auth/session/all") { + parameter("revoke_self", includingSelf) + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/api/schemas/Auth.kt b/app/src/main/java/chat/revolt/api/schemas/Auth.kt new file mode 100644 index 00000000..25fcde1c --- /dev/null +++ b/app/src/main/java/chat/revolt/api/schemas/Auth.kt @@ -0,0 +1,15 @@ +package chat.revolt.api.schemas + +import chat.revolt.api.RevoltAPI +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class Session( + @SerialName("_id") val id: String, + val name: String, +) { + fun isCurrent(): Boolean { + return id == RevoltAPI.sessionId + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/components/settings/sessions/SessionItem.kt b/app/src/main/java/chat/revolt/components/settings/sessions/SessionItem.kt new file mode 100644 index 00000000..d2eefee2 --- /dev/null +++ b/app/src/main/java/chat/revolt/components/settings/sessions/SessionItem.kt @@ -0,0 +1,87 @@ +package chat.revolt.components.settings.sessions + +import android.text.format.DateUtils +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.ElevatedButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableLongStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +import chat.revolt.R +import chat.revolt.api.internals.ULID +import chat.revolt.api.schemas.Session + +@Composable +fun SessionItem( + session: Session, + modifier: Modifier = Modifier, + currentSession: Boolean = false, + onLogout: (Session) -> Unit +) { + val context = LocalContext.current + val decodedUlid by remember(session) { mutableLongStateOf(ULID.asTimestamp(session.id)) } + val formattedTimestamp = remember(decodedUlid) { + DateUtils.getRelativeTimeSpanString( + decodedUlid, + System.currentTimeMillis(), + DateUtils.MINUTE_IN_MILLIS + ) + } + + Row( + modifier = modifier + .fillMaxWidth() + .clip(shape = MaterialTheme.shapes.medium) + .background( + color = if (currentSession) { + MaterialTheme.colorScheme.surfaceColorAtElevation(4.dp) + } else { + MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp) + } + ) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 16.dp) + ) { + Text( + text = session.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.labelLarge + ) + + Text( + text = stringResource(R.string.settings_sessions_first_seen, formattedTimestamp), + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.labelSmall + ) + } + + if (!currentSession) { + ElevatedButton(onClick = { onLogout(session) }) { + Text(stringResource(R.string.logout)) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/chat/revolt/screens/SplashScreen.kt b/app/src/main/java/chat/revolt/screens/SplashScreen.kt index d962817c..9e113a1d 100644 --- a/app/src/main/java/chat/revolt/screens/SplashScreen.kt +++ b/app/src/main/java/chat/revolt/screens/SplashScreen.kt @@ -11,9 +11,17 @@ import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.Image import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.material3.MaterialTheme -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.layout.ContentScale @@ -37,8 +45,8 @@ import chat.revolt.persistence.KVStorage import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import io.ktor.client.request.get -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel @SuppressLint("StaticFieldLeak") @@ -99,6 +107,7 @@ class SplashScreenViewModel @Inject constructor( } val token = kvStorage.get("sessionToken") ?: return@launch setNavigateTo("login") + val id = kvStorage.get("sessionId") ?: "" val canReachRevolt = canReachRevolt() val valid = RevoltAPI.checkSessionToken(token) @@ -110,6 +119,7 @@ class SplashScreenViewModel @Inject constructor( Toast.LENGTH_SHORT ).show() kvStorage.remove("sessionToken") + kvStorage.remove("sessionId") setNavigateTo("login") } else { val onboard = needsOnboarding() @@ -119,6 +129,7 @@ class SplashScreenViewModel @Inject constructor( } RevoltAPI.loginAs(token) + RevoltAPI.setSessionId(id) loadSettings() setNavigateTo("home") } 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 fc00a88c..0148313e 100644 --- a/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/LoginScreen.kt @@ -49,8 +49,8 @@ import chat.revolt.components.generic.FormTextField import chat.revolt.components.generic.Weblink import chat.revolt.persistence.KVStorage import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class LoginViewModel @Inject constructor( @@ -102,8 +102,10 @@ class LoginViewModel @Inject constructor( try { val token = response.firstUserHints!!.token + val id = response.firstUserHints.id kvStorage.set("sessionToken", token) + kvStorage.set("sessionId", id) val onboard = needsOnboarding(token) if (onboard) { @@ -112,6 +114,7 @@ class LoginViewModel @Inject constructor( } RevoltAPI.loginAs(token) + RevoltAPI.setSessionId(response.firstUserHints.token) loadSettings(token) _navigateTo = "home" 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 8060b8f6..03e98623 100644 --- a/app/src/main/java/chat/revolt/screens/login/MfaScreen.kt +++ b/app/src/main/java/chat/revolt/screens/login/MfaScreen.kt @@ -48,8 +48,8 @@ import chat.revolt.components.generic.CollapsibleCard import chat.revolt.components.generic.FormTextField import chat.revolt.persistence.KVStorage import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.launch +import javax.inject.Inject @HiltViewModel class MfaScreenViewModel @Inject constructor( @@ -98,10 +98,13 @@ class MfaScreenViewModel @Inject constructor( try { val token = response.firstUserHints!!.token + val id = response.firstUserHints.id RevoltAPI.loginAs(token) + RevoltAPI.setSessionId(id) loadSettings(token) kvStorage.set("sessionToken", token) + kvStorage.set("sessionId", id) _navigateToHome = true } catch (e: Error) { @@ -126,10 +129,13 @@ class MfaScreenViewModel @Inject constructor( try { val token = response.firstUserHints!!.token + val id = response.firstUserHints.id RevoltAPI.loginAs(token) + RevoltAPI.setSessionId(id) loadSettings(token) kvStorage.set("sessionToken", token) + kvStorage.set("sessionId", id) _navigateToHome = true } catch (e: Error) { diff --git a/app/src/main/java/chat/revolt/screens/settings/SessionSettngsScreen.kt b/app/src/main/java/chat/revolt/screens/settings/SessionSettngsScreen.kt new file mode 100644 index 00000000..857e7f5b --- /dev/null +++ b/app/src/main/java/chat/revolt/screens/settings/SessionSettngsScreen.kt @@ -0,0 +1,239 @@ +package chat.revolt.screens.settings + +import android.util.Log +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.layout.safeDrawingPadding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.FilledTonalButton +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.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +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.RevoltAPI +import chat.revolt.api.routes.auth.fetchAllSessions +import chat.revolt.api.routes.auth.logoutAllSessions +import chat.revolt.api.routes.auth.logoutSessionById +import chat.revolt.api.schemas.Session +import chat.revolt.components.generic.PageHeader +import chat.revolt.components.generic.UIMarkdown +import chat.revolt.components.settings.sessions.SessionItem +import kotlinx.coroutines.launch + +class SessionSettingsScreenViewModel : ViewModel() { + val sessions = mutableStateListOf() + var currentSession by mutableStateOf(null) + var showLogoutOtherConfirmation by mutableStateOf(false) + + fun fetchSessions() { + viewModelScope.launch { + sessions.addAll(fetchAllSessions()) + currentSession = sessions.firstOrNull { it.isCurrent() } + Log.d( + "SessionSettingsScreen", + "Current session: $currentSession. Current session ID: ${RevoltAPI.sessionId}" + ) + } + } + + fun logoutSession(id: String) { + viewModelScope.launch { + logoutSessionById(id) + sessions.removeIf { it.id == id } + } + } + + fun logoutOtherSessions() { + viewModelScope.launch { + logoutAllSessions(includingSelf = false) + sessions.clear() + sessions.addAll(fetchAllSessions()) + } + } +} + +@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) +@Composable +fun SessionSettingsScreen( + navController: NavController, + viewModel: SessionSettingsScreenViewModel = viewModel() +) { + LaunchedEffect(Unit) { + viewModel.fetchSessions() + } + + if (viewModel.showLogoutOtherConfirmation) { + AlertDialog( + onDismissRequest = { + viewModel.showLogoutOtherConfirmation = false + }, + title = { + Text( + text = stringResource(R.string.settings_sessions_log_out_other_confirm) + ) + }, + confirmButton = { + Button( + onClick = { + viewModel.showLogoutOtherConfirmation = false + viewModel.logoutOtherSessions() + } + ) { + Text(stringResource(R.string.settings_sessions_log_out_other_confirm_yes)) + } + }, + dismissButton = { + TextButton( + onClick = { + viewModel.showLogoutOtherConfirmation = false + } + ) { + Text(stringResource(R.string.settings_sessions_log_out_other_confirm_no)) + } + } + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .safeDrawingPadding() + ) { + PageHeader( + text = stringResource(id = R.string.settings_sessions), + showBackButton = true, + onBackButtonClicked = { + navController.popBackStack() + } + ) + + LazyColumn { + stickyHeader(key = "thisDevice") { + Text( + text = stringResource(id = R.string.settings_sessions_this_device), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(10.dp) + ) + } + + viewModel.currentSession?.let { + item(key = it.id) { + Spacer(Modifier.height(8.dp)) + SessionItem( + session = it, + currentSession = true, + onLogout = {}, + modifier = Modifier.padding(horizontal = 8.dp) + ) + } + } ?: run { + item(key = "noCurrentSession") { + UIMarkdown( + text = stringResource(id = R.string.settings_sessions_this_device_unavailable), + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(10.dp) + ) + } + } + + stickyHeader(key = "otherSessions") { + Text( + text = stringResource(id = R.string.settings_sessions_other_sessions), + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .fillMaxWidth() + .background(MaterialTheme.colorScheme.background) + .padding(10.dp) + ) + } + + item(key = "logoutOtherSessions") { + Row( + modifier = Modifier + .padding(8.dp) + .fillMaxWidth() + .clip(shape = MaterialTheme.shapes.medium) + .background( + color = MaterialTheme.colorScheme.surfaceColorAtElevation(6.dp) + ) + .padding(16.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.SpaceBetween + ) { + Column( + modifier = Modifier + .weight(1f) + .padding(end = 16.dp) + ) { + Text( + text = stringResource(R.string.settings_sessions_log_out_other), + style = MaterialTheme.typography.labelLarge + ) + + Text( + text = stringResource(R.string.settings_sessions_log_out_other_description), + style = MaterialTheme.typography.bodySmall.copy( + fontWeight = FontWeight.Normal + ) + ) + } + + FilledTonalButton(onClick = { viewModel.showLogoutOtherConfirmation = true }) { + Text(stringResource(R.string.logout)) + } + } + Spacer(Modifier.height(8.dp)) + } + + items(viewModel.sessions.size) { + val item = viewModel.sessions[it] + + if (item.isCurrent()) { + return@items + } + + SessionItem( + session = item, + onLogout = { session -> + viewModel.logoutSession(session.id) + }, + modifier = Modifier.padding(horizontal = 8.dp) + ) + Spacer(Modifier.height(16.dp)) + } + } + } +} \ 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 ef519b92..8fcba93a 100644 --- a/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt +++ b/app/src/main/java/chat/revolt/screens/settings/SettingsScreen.kt @@ -33,8 +33,8 @@ import chat.revolt.components.generic.SheetClickable import chat.revolt.components.screens.settings.SelfUserOverview import chat.revolt.persistence.KVStorage import dagger.hilt.android.lifecycle.HiltViewModel -import javax.inject.Inject import kotlinx.coroutines.runBlocking +import javax.inject.Inject @HiltViewModel class SettingsScreenViewModel @Inject constructor( @@ -79,10 +79,35 @@ fun SettingsScreen( .fillMaxSize() .padding(10.dp) ) { + Text( + text = stringResource(id = R.string.settings_category_account), + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.padding(bottom = 10.dp, start = 10.dp, top = 20.dp) + ) + + SheetClickable( + icon = { modifier -> + Icon( + painter = painterResource(R.drawable.ic_tablet_cellphone_24dp), + contentDescription = stringResource(id = R.string.settings_sessions), + modifier = modifier + ) + }, + label = { textStyle -> + Text( + text = stringResource(id = R.string.settings_sessions), + style = textStyle + ) + }, + modifier = Modifier.testTag("settings_view_sessions") + ) { + navController.navigate("settings/sessions") + } + Text( text = stringResource(id = R.string.settings_category_general), style = MaterialTheme.typography.bodySmall, - modifier = Modifier.padding(bottom = 10.dp, start = 10.dp) + modifier = Modifier.padding(bottom = 10.dp, start = 10.dp, top = 20.dp) ) SheetClickable( diff --git a/app/src/main/res/drawable/ic_tablet_cellphone_24dp.xml b/app/src/main/res/drawable/ic_tablet_cellphone_24dp.xml new file mode 100644 index 00000000..28b0d54b --- /dev/null +++ b/app/src/main/res/drawable/ic_tablet_cellphone_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index abfed8ca..62088b36 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -369,10 +369,22 @@ You have received an important notice regarding your account from our moderation team. Please read it carefully. View + Account General Miscellaneous Revolt v%1$s + Sessions + This Device + The session of this device is unavailable. Please **log in again** to view your current session. + Other Active Sessions + First seen %1$s + Log out other sessions + This device will stay logged in. + Log out all other sessions? + Log out + Keep logged in + Appearance Theme System