android: add logout flow with full local session cleanup
Some checks are pending
CI / test (push) Has started running

This commit is contained in:
Codex
2026-03-09 15:09:10 +03:00
parent 33514265e3
commit d7dfda1d31
15 changed files with 230 additions and 5 deletions

View File

@@ -20,4 +20,8 @@ class ActiveChatTracker @Inject constructor() {
_activeChatId.value = null
}
}
fun clear() {
_activeChatId.value = null
}
}

View File

@@ -0,0 +1,24 @@
package ru.daemonlord.messenger.data.auth.repository
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
import ru.daemonlord.messenger.data.chat.local.db.MessengerDatabase
import ru.daemonlord.messenger.di.IoDispatcher
import ru.daemonlord.messenger.domain.auth.repository.SessionCleanupRepository
import ru.daemonlord.messenger.domain.notifications.repository.NotificationSettingsRepository
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DefaultSessionCleanupRepository @Inject constructor(
private val database: MessengerDatabase,
private val notificationSettingsRepository: NotificationSettingsRepository,
@IoDispatcher private val ioDispatcher: CoroutineDispatcher,
) : SessionCleanupRepository {
override suspend fun clearLocalSessionData() = withContext(ioDispatcher) {
database.clearAllTables()
notificationSettingsRepository.clearChatOverrides()
}
}

View File

@@ -66,17 +66,25 @@ class DataStoreNotificationSettingsRepository @Inject constructor(
}
}
override suspend fun clearChatOverrides() {
dataStore.edit { preferences ->
val keysToRemove = preferences.asMap().keys
.filter { key -> key.name.startsWith(CHAT_OVERRIDE_PREFIX) }
keysToRemove.forEach { key -> preferences.remove(key) }
}
}
private fun Preferences.chatOverride(chatId: Long): ChatNotificationOverride {
return this[chatOverrideKey(chatId)]
?.let { runCatching { ChatNotificationOverride.valueOf(it) }.getOrNull() }
?: ChatNotificationOverride.DEFAULT
}
private fun chatOverrideKey(chatId: Long) = stringPreferencesKey("notification_chat_override_$chatId")
private fun chatOverrideKey(chatId: Long) = stringPreferencesKey("$CHAT_OVERRIDE_PREFIX$chatId")
private companion object {
const val CHAT_OVERRIDE_PREFIX = "notification_chat_override_"
val GLOBAL_ENABLED_KEY = booleanPreferencesKey("notification_global_enabled")
val PREVIEW_ENABLED_KEY = booleanPreferencesKey("notification_preview_enabled")
}
}

View File

@@ -5,11 +5,13 @@ import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import ru.daemonlord.messenger.data.auth.repository.NetworkAuthRepository
import ru.daemonlord.messenger.data.auth.repository.DefaultSessionCleanupRepository
import ru.daemonlord.messenger.data.chat.repository.NetworkChatRepository
import ru.daemonlord.messenger.data.media.repository.NetworkMediaRepository
import ru.daemonlord.messenger.data.message.repository.NetworkMessageRepository
import ru.daemonlord.messenger.data.notifications.repository.DataStoreNotificationSettingsRepository
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
import ru.daemonlord.messenger.domain.auth.repository.SessionCleanupRepository
import ru.daemonlord.messenger.domain.chat.repository.ChatRepository
import ru.daemonlord.messenger.domain.media.repository.MediaRepository
import ru.daemonlord.messenger.domain.message.repository.MessageRepository
@@ -26,6 +28,12 @@ abstract class RepositoryModule {
repository: NetworkAuthRepository,
): AuthRepository
@Binds
@Singleton
abstract fun bindSessionCleanupRepository(
repository: DefaultSessionCleanupRepository,
): SessionCleanupRepository
@Binds
@Singleton
abstract fun bindChatRepository(

View File

@@ -0,0 +1,6 @@
package ru.daemonlord.messenger.domain.auth.repository
interface SessionCleanupRepository {
suspend fun clearLocalSessionData()
}

View File

@@ -0,0 +1,22 @@
package ru.daemonlord.messenger.domain.auth.usecase
import ru.daemonlord.messenger.core.notifications.ActiveChatTracker
import ru.daemonlord.messenger.domain.auth.repository.AuthRepository
import ru.daemonlord.messenger.domain.auth.repository.SessionCleanupRepository
import ru.daemonlord.messenger.domain.realtime.RealtimeManager
import javax.inject.Inject
class LogoutUseCase @Inject constructor(
private val authRepository: AuthRepository,
private val sessionCleanupRepository: SessionCleanupRepository,
private val realtimeManager: RealtimeManager,
private val activeChatTracker: ActiveChatTracker,
) {
suspend operator fun invoke() {
realtimeManager.disconnect()
activeChatTracker.clear()
authRepository.logout()
sessionCleanupRepository.clearLocalSessionData()
}
}

View File

@@ -14,5 +14,5 @@ interface NotificationSettingsRepository {
suspend fun getChatOverride(chatId: Long): ChatNotificationOverride
suspend fun setChatOverride(chatId: Long, mode: ChatNotificationOverride)
suspend fun clearChatOverride(chatId: Long)
suspend fun clearChatOverrides()
}

View File

@@ -9,6 +9,7 @@ import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import ru.daemonlord.messenger.domain.auth.usecase.LoginUseCase
import ru.daemonlord.messenger.domain.auth.usecase.LogoutUseCase
import ru.daemonlord.messenger.domain.auth.usecase.RestoreSessionUseCase
import ru.daemonlord.messenger.domain.common.AppError
import ru.daemonlord.messenger.domain.common.AppResult
@@ -18,6 +19,7 @@ import javax.inject.Inject
class AuthViewModel @Inject constructor(
private val restoreSessionUseCase: RestoreSessionUseCase,
private val loginUseCase: LoginUseCase,
private val logoutUseCase: LogoutUseCase,
) : ViewModel() {
private val _uiState = MutableStateFlow(AuthUiState())
@@ -68,6 +70,22 @@ class AuthViewModel @Inject constructor(
}
}
fun logout() {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true, errorMessage = null) }
runCatching { logoutUseCase() }
_uiState.update {
it.copy(
email = "",
password = "",
isLoading = false,
isAuthenticated = false,
errorMessage = null,
)
}
}
}
private fun restoreSession() {
viewModelScope.launch {
_uiState.update { it.copy(isCheckingSession = true) }

View File

@@ -29,6 +29,7 @@ import androidx.compose.material3.Surface
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -50,6 +51,7 @@ import java.time.format.DateTimeFormatter
@Composable
fun ChatListRoute(
onOpenChat: (Long) -> Unit,
onLogout: () -> Unit,
inviteToken: String?,
onInviteTokenConsumed: () -> Unit,
viewModel: ChatListViewModel = hiltViewModel(),
@@ -72,6 +74,7 @@ fun ChatListRoute(
onFilterSelected = viewModel::onFilterSelected,
onSearchChanged = viewModel::onSearchChanged,
onRefresh = viewModel::onPullToRefresh,
onLogout = onLogout,
onOpenChat = onOpenChat,
)
}
@@ -84,6 +87,7 @@ fun ChatListScreen(
onFilterSelected: (ChatListFilter) -> Unit,
onSearchChanged: (String) -> Unit,
onRefresh: () -> Unit,
onLogout: () -> Unit,
onOpenChat: (Long) -> Unit,
) {
Column(
@@ -115,6 +119,16 @@ fun ChatListScreen(
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalArrangement = Arrangement.End,
) {
TextButton(onClick = onLogout) {
Text("Logout")
}
}
Row(
modifier = Modifier
.fillMaxWidth()

View File

@@ -125,6 +125,7 @@ fun MessengerNavHost(
ChatListRoute(
inviteToken = inviteToken,
onInviteTokenConsumed = onInviteTokenConsumed,
onLogout = viewModel::logout,
onOpenChat = { chatId ->
navController.navigate("${Routes.Chat}/$chatId")
}