feat: session settings
Signed-off-by: Infi <infi@infi.sh>
This commit is contained in:
parent
97c31d0e3e
commit
0245edc8f1
|
|
@ -3,3 +3,4 @@
|
||||||
/workspace.xml
|
/workspace.xml
|
||||||
/kotlinc.xml
|
/kotlinc.xml
|
||||||
/appInsightsSettings.xml
|
/appInsightsSettings.xml
|
||||||
|
/other.xml
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ import chat.revolt.screens.settings.AppearanceSettingsScreen
|
||||||
import chat.revolt.screens.settings.ChangelogsSettingsScreen
|
import chat.revolt.screens.settings.ChangelogsSettingsScreen
|
||||||
import chat.revolt.screens.settings.ClosedBetaUpdaterScreen
|
import chat.revolt.screens.settings.ClosedBetaUpdaterScreen
|
||||||
import chat.revolt.screens.settings.DebugSettingsScreen
|
import chat.revolt.screens.settings.DebugSettingsScreen
|
||||||
|
import chat.revolt.screens.settings.SessionSettingsScreen
|
||||||
import chat.revolt.screens.settings.SettingsScreen
|
import chat.revolt.screens.settings.SettingsScreen
|
||||||
import chat.revolt.ui.theme.RevoltTheme
|
import chat.revolt.ui.theme.RevoltTheme
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
|
@ -142,6 +143,7 @@ fun AppEntrypoint(windowSizeClass: WindowSizeClass) {
|
||||||
composable("chat") { ChatRouterScreen(navController, windowSizeClass) }
|
composable("chat") { ChatRouterScreen(navController, windowSizeClass) }
|
||||||
|
|
||||||
composable("settings") { SettingsScreen(navController) }
|
composable("settings") { SettingsScreen(navController) }
|
||||||
|
composable("settings/sessions") { SessionSettingsScreen(navController) }
|
||||||
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
composable("settings/appearance") { AppearanceSettingsScreen(navController) }
|
||||||
composable("settings/debug") { DebugSettingsScreen(navController) }
|
composable("settings/debug") { DebugSettingsScreen(navController) }
|
||||||
composable("settings/updater") { ClosedBetaUpdaterScreen(navController) }
|
composable("settings/updater") { ClosedBetaUpdaterScreen(navController) }
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,6 @@ import chat.revolt.api.internals.Members
|
||||||
import chat.revolt.api.realtime.DisconnectionState
|
import chat.revolt.api.realtime.DisconnectionState
|
||||||
import chat.revolt.api.realtime.RealtimeSocket
|
import chat.revolt.api.realtime.RealtimeSocket
|
||||||
import chat.revolt.api.routes.user.fetchSelf
|
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.Emoji
|
||||||
import chat.revolt.api.schemas.Message
|
import chat.revolt.api.schemas.Message
|
||||||
import chat.revolt.api.schemas.Server
|
import chat.revolt.api.schemas.Server
|
||||||
|
|
@ -39,6 +38,7 @@ import kotlinx.coroutines.withContext
|
||||||
import kotlinx.serialization.ExperimentalSerializationApi
|
import kotlinx.serialization.ExperimentalSerializationApi
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
|
import chat.revolt.api.schemas.Channel as ChannelSchema
|
||||||
|
|
||||||
const val REVOLT_BASE = "https://api.revolt.chat"
|
const val REVOLT_BASE = "https://api.revolt.chat"
|
||||||
const val REVOLT_SUPPORT = "https://support.revolt.chat"
|
const val REVOLT_SUPPORT = "https://support.revolt.chat"
|
||||||
|
|
@ -118,6 +118,8 @@ object RevoltAPI {
|
||||||
|
|
||||||
var sessionToken: String = ""
|
var sessionToken: String = ""
|
||||||
private set
|
private set
|
||||||
|
var sessionId: String = ""
|
||||||
|
private set
|
||||||
|
|
||||||
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
@OptIn(DelicateCoroutinesApi::class, ExperimentalCoroutinesApi::class)
|
||||||
val realtimeContext = newSingleThreadContext("RealtimeContext")
|
val realtimeContext = newSingleThreadContext("RealtimeContext")
|
||||||
|
|
@ -129,6 +131,10 @@ object RevoltAPI {
|
||||||
sessionToken = token
|
sessionToken = token
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setSessionId(id: String) {
|
||||||
|
sessionId = id
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun loginAs(token: String) {
|
suspend fun loginAs(token: String) {
|
||||||
setSessionHeader(token)
|
setSessionHeader(token)
|
||||||
fetchSelf()
|
fetchSelf()
|
||||||
|
|
@ -189,6 +195,7 @@ object RevoltAPI {
|
||||||
fun logout() {
|
fun logout() {
|
||||||
selfId = null
|
selfId = null
|
||||||
sessionToken = ""
|
sessionToken = ""
|
||||||
|
sessionId = ""
|
||||||
|
|
||||||
userCache.clear()
|
userCache.clear()
|
||||||
serverCache.clear()
|
serverCache.clear()
|
||||||
|
|
|
||||||
|
|
@ -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<Session> {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -11,9 +11,17 @@ import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.foundation.Image
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.background
|
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.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.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.layout.ContentScale
|
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.lifecycle.HiltViewModel
|
||||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import io.ktor.client.request.get
|
import io.ktor.client.request.get
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
@SuppressLint("StaticFieldLeak")
|
@SuppressLint("StaticFieldLeak")
|
||||||
|
|
@ -99,6 +107,7 @@ class SplashScreenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
val token = kvStorage.get("sessionToken") ?: return@launch setNavigateTo("login")
|
val token = kvStorage.get("sessionToken") ?: return@launch setNavigateTo("login")
|
||||||
|
val id = kvStorage.get("sessionId") ?: ""
|
||||||
|
|
||||||
val canReachRevolt = canReachRevolt()
|
val canReachRevolt = canReachRevolt()
|
||||||
val valid = RevoltAPI.checkSessionToken(token)
|
val valid = RevoltAPI.checkSessionToken(token)
|
||||||
|
|
@ -110,6 +119,7 @@ class SplashScreenViewModel @Inject constructor(
|
||||||
Toast.LENGTH_SHORT
|
Toast.LENGTH_SHORT
|
||||||
).show()
|
).show()
|
||||||
kvStorage.remove("sessionToken")
|
kvStorage.remove("sessionToken")
|
||||||
|
kvStorage.remove("sessionId")
|
||||||
setNavigateTo("login")
|
setNavigateTo("login")
|
||||||
} else {
|
} else {
|
||||||
val onboard = needsOnboarding()
|
val onboard = needsOnboarding()
|
||||||
|
|
@ -119,6 +129,7 @@ class SplashScreenViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
RevoltAPI.loginAs(token)
|
RevoltAPI.loginAs(token)
|
||||||
|
RevoltAPI.setSessionId(id)
|
||||||
loadSettings()
|
loadSettings()
|
||||||
setNavigateTo("home")
|
setNavigateTo("home")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,8 @@ import chat.revolt.components.generic.FormTextField
|
||||||
import chat.revolt.components.generic.Weblink
|
import chat.revolt.components.generic.Weblink
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class LoginViewModel @Inject constructor(
|
class LoginViewModel @Inject constructor(
|
||||||
|
|
@ -102,8 +102,10 @@ class LoginViewModel @Inject constructor(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val token = response.firstUserHints!!.token
|
val token = response.firstUserHints!!.token
|
||||||
|
val id = response.firstUserHints.id
|
||||||
|
|
||||||
kvStorage.set("sessionToken", token)
|
kvStorage.set("sessionToken", token)
|
||||||
|
kvStorage.set("sessionId", id)
|
||||||
|
|
||||||
val onboard = needsOnboarding(token)
|
val onboard = needsOnboarding(token)
|
||||||
if (onboard) {
|
if (onboard) {
|
||||||
|
|
@ -112,6 +114,7 @@ class LoginViewModel @Inject constructor(
|
||||||
}
|
}
|
||||||
|
|
||||||
RevoltAPI.loginAs(token)
|
RevoltAPI.loginAs(token)
|
||||||
|
RevoltAPI.setSessionId(response.firstUserHints.token)
|
||||||
loadSettings(token)
|
loadSettings(token)
|
||||||
|
|
||||||
_navigateTo = "home"
|
_navigateTo = "home"
|
||||||
|
|
|
||||||
|
|
@ -48,8 +48,8 @@ import chat.revolt.components.generic.CollapsibleCard
|
||||||
import chat.revolt.components.generic.FormTextField
|
import chat.revolt.components.generic.FormTextField
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MfaScreenViewModel @Inject constructor(
|
class MfaScreenViewModel @Inject constructor(
|
||||||
|
|
@ -98,10 +98,13 @@ class MfaScreenViewModel @Inject constructor(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val token = response.firstUserHints!!.token
|
val token = response.firstUserHints!!.token
|
||||||
|
val id = response.firstUserHints.id
|
||||||
|
|
||||||
RevoltAPI.loginAs(token)
|
RevoltAPI.loginAs(token)
|
||||||
|
RevoltAPI.setSessionId(id)
|
||||||
loadSettings(token)
|
loadSettings(token)
|
||||||
kvStorage.set("sessionToken", token)
|
kvStorage.set("sessionToken", token)
|
||||||
|
kvStorage.set("sessionId", id)
|
||||||
|
|
||||||
_navigateToHome = true
|
_navigateToHome = true
|
||||||
} catch (e: Error) {
|
} catch (e: Error) {
|
||||||
|
|
@ -126,10 +129,13 @@ class MfaScreenViewModel @Inject constructor(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val token = response.firstUserHints!!.token
|
val token = response.firstUserHints!!.token
|
||||||
|
val id = response.firstUserHints.id
|
||||||
|
|
||||||
RevoltAPI.loginAs(token)
|
RevoltAPI.loginAs(token)
|
||||||
|
RevoltAPI.setSessionId(id)
|
||||||
loadSettings(token)
|
loadSettings(token)
|
||||||
kvStorage.set("sessionToken", token)
|
kvStorage.set("sessionToken", token)
|
||||||
|
kvStorage.set("sessionId", id)
|
||||||
|
|
||||||
_navigateToHome = true
|
_navigateToHome = true
|
||||||
} catch (e: Error) {
|
} catch (e: Error) {
|
||||||
|
|
|
||||||
|
|
@ -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<Session>()
|
||||||
|
var currentSession by mutableStateOf<Session?>(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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -33,8 +33,8 @@ import chat.revolt.components.generic.SheetClickable
|
||||||
import chat.revolt.components.screens.settings.SelfUserOverview
|
import chat.revolt.components.screens.settings.SelfUserOverview
|
||||||
import chat.revolt.persistence.KVStorage
|
import chat.revolt.persistence.KVStorage
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
import javax.inject.Inject
|
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class SettingsScreenViewModel @Inject constructor(
|
class SettingsScreenViewModel @Inject constructor(
|
||||||
|
|
@ -79,10 +79,35 @@ fun SettingsScreen(
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(10.dp)
|
.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(
|
||||||
text = stringResource(id = R.string.settings_category_general),
|
text = stringResource(id = R.string.settings_category_general),
|
||||||
style = MaterialTheme.typography.bodySmall,
|
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(
|
SheetClickable(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:pathData="M3,4H20A2,2 0 0,1 22,6V8H18V6H5V18H14V20H3A2,2 0 0,1 1,18V6A2,2 0 0,1 3,4M17,10H23A1,1 0 0,1 24,11V21A1,1 0 0,1 23,22H17A1,1 0 0,1 16,21V11A1,1 0 0,1 17,10M18,12V19H22V12H18Z" />
|
||||||
|
</vector>
|
||||||
|
|
@ -369,10 +369,22 @@
|
||||||
<string name="notice_platform_mod_dm_description">You have received an important notice regarding your account from our moderation team. Please read it carefully.</string>
|
<string name="notice_platform_mod_dm_description">You have received an important notice regarding your account from our moderation team. Please read it carefully.</string>
|
||||||
<string name="notice_platform_mod_dm_acknowledge">View</string>
|
<string name="notice_platform_mod_dm_acknowledge">View</string>
|
||||||
|
|
||||||
|
<string name="settings_category_account">Account</string>
|
||||||
<string name="settings_category_general">General</string>
|
<string name="settings_category_general">General</string>
|
||||||
<string name="settings_category_miscellaneous">Miscellaneous</string>
|
<string name="settings_category_miscellaneous">Miscellaneous</string>
|
||||||
<string name="settings_category_last" translatable="false">Revolt v%1$s</string>
|
<string name="settings_category_last" translatable="false">Revolt v%1$s</string>
|
||||||
|
|
||||||
|
<string name="settings_sessions">Sessions</string>
|
||||||
|
<string name="settings_sessions_this_device">This Device</string>
|
||||||
|
<string name="settings_sessions_this_device_unavailable">The session of this device is unavailable. Please **log in again** to view your current session.</string>
|
||||||
|
<string name="settings_sessions_other_sessions">Other Active Sessions</string>
|
||||||
|
<string name="settings_sessions_first_seen">First seen %1$s</string>
|
||||||
|
<string name="settings_sessions_log_out_other">Log out other sessions</string>
|
||||||
|
<string name="settings_sessions_log_out_other_description">This device will stay logged in.</string>
|
||||||
|
<string name="settings_sessions_log_out_other_confirm">Log out all other sessions?</string>
|
||||||
|
<string name="settings_sessions_log_out_other_confirm_yes">Log out</string>
|
||||||
|
<string name="settings_sessions_log_out_other_confirm_no">Keep logged in</string>
|
||||||
|
|
||||||
<string name="settings_appearance">Appearance</string>
|
<string name="settings_appearance">Appearance</string>
|
||||||
<string name="settings_appearance_theme">Theme</string>
|
<string name="settings_appearance_theme">Theme</string>
|
||||||
<string name="settings_appearance_theme_none">System</string>
|
<string name="settings_appearance_theme_none">System</string>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue