diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt index 8be5d9b7..15dda193 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt @@ -4,7 +4,6 @@ import android.Manifest import android.annotation.SuppressLint import android.app.NotificationChannel import android.app.NotificationManager -import android.content.Context import android.content.Intent import android.content.res.Configuration import android.os.Build @@ -15,43 +14,20 @@ import androidx.activity.SystemBarStyle import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.appcompat.app.AppCompatActivity -import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.derivedStateOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb -import androidx.compose.ui.platform.LocalResources import androidx.core.content.ContextCompat import androidx.lifecycle.compose.LifecycleResumeEffect -import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.conena.nanokt.android.content.pxToDp import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.isGranted -import com.google.accompanist.permissions.rememberPermissionState import dev.meloda.fast.MainViewModel import dev.meloda.fast.MainViewModelImpl import dev.meloda.fast.common.AppConstants -import dev.meloda.fast.common.LongPollController -import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.datastore.AppSettings -import dev.meloda.fast.datastore.UserSettings -import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.service.OnlineService import dev.meloda.fast.service.longpolling.LongPollingService import dev.meloda.fast.ui.R -import dev.meloda.fast.ui.common.LocalSizeConfig -import dev.meloda.fast.ui.model.DeviceSize -import dev.meloda.fast.ui.model.SizeConfig -import dev.meloda.fast.ui.model.ThemeConfig -import dev.meloda.fast.ui.theme.AppTheme -import dev.meloda.fast.ui.theme.LocalThemeConfig -import dev.meloda.fast.ui.theme.LocalUser -import dev.meloda.fast.ui.util.isNeedToEnableDarkMode import org.koin.androidx.compose.koinViewModel -import org.koin.compose.koinInject class MainActivity : AppCompatActivity() { @@ -88,167 +64,25 @@ class MainActivity : AppCompatActivity() { requestNotificationPermissions() setContent { - val resources = LocalResources.current - - val userSettings: UserSettings = koinInject() - val longPollController: LongPollController = koinInject() - - val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle() - val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle() - val viewModel: MainViewModel = koinViewModel() - - val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle() + LaunchedEffect(viewModel) { + Log.d("VM_CREATE", "onCreate: viewModel: $viewModel") + } LifecycleResumeEffect(true) { viewModel.onAppResumed(intent) onPauseOrDispose {} } - val permissionState = - rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) - - val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle() - val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle() - - LaunchedEffect(isNeedToCheckPermission) { - if (isNeedToCheckPermission) { - viewModel.onPermissionCheckStatus(permissionState.status) - - if (permissionState.status.isGranted) { - if (longPollCurrentState == LongPollState.InApp) { - toggleLongPollService(false) - } - - toggleLongPollService( - enable = true, - inBackground = true - ) - } - } - } - - LaunchedEffect(isNeedToRequestPermission) { - if (isNeedToRequestPermission) { - viewModel.onPermissionsRequested() - permissionState.launchPermissionRequest() - } - } - - LifecycleResumeEffect(longPollStateToApply) { - Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply") - if (longPollStateToApply != LongPollState.Background) { - if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched() - && longPollCurrentState != longPollStateToApply - ) { - toggleLongPollService(false) - Log.d("LongPoll", "recreate()") - } - + RootScreen( + toggleLongPollService = { enable, inBackground -> toggleLongPollService( - enable = longPollStateToApply.isLaunched(), - inBackground = longPollStateToApply == LongPollState.Background + enable = enable, + inBackground = inBackground ?: AppSettings.Experimental.longPollInBackground ) - } - - onPauseOrDispose {} - } - - val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle() - LifecycleResumeEffect(sendOnline) { - toggleOnlineService(sendOnline) - - onPauseOrDispose { - toggleOnlineService(false) - } - } - - val deviceWidthDp = remember(true) { - resources.displayMetrics.widthPixels.pxToDp() - } - val deviceHeightDp = remember(true) { - resources.displayMetrics.heightPixels.pxToDp() - } - - val deviceWidthSize by remember(deviceWidthDp) { - derivedStateOf { - when { - deviceWidthDp <= 360 -> DeviceSize.Small - deviceWidthDp <= 600 -> DeviceSize.Compact - deviceWidthDp <= 840 -> DeviceSize.Medium - else -> DeviceSize.Expanded - } - } - } - - val deviceHeightSize by remember(deviceHeightDp) { - derivedStateOf { - when { - deviceHeightDp <= 480 -> DeviceSize.Small - deviceHeightDp <= 700 -> DeviceSize.Compact - deviceHeightDp <= 900 -> DeviceSize.Medium - else -> DeviceSize.Expanded - } - } - } - - val sizeConfig by remember(deviceWidthSize, deviceHeightSize) { - mutableStateOf( - SizeConfig( - widthSize = deviceWidthSize, - heightSize = deviceHeightSize - ) - ) - } - - val darkMode by userSettings.darkMode.collectAsStateWithLifecycle() - val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle() - val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle() - val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle() - val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle() - val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle() - val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle() - - val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode) - - val themeConfig by remember( - darkMode, - dynamicColors, - amoledDark, - enableBlur, - enableMultiline, - setDarkMode, - useSystemFont - ) { - derivedStateOf { - ThemeConfig( - darkMode = setDarkMode, - dynamicColors = dynamicColors, - selectedColorScheme = 0, - amoledDark = amoledDark, - enableBlur = enableBlur, - enableMultiline = enableMultiline, - useSystemFont = useSystemFont, - enableAnimations = enableAnimations - ) - } - } - - CompositionLocalProvider( - LocalThemeConfig provides themeConfig, - LocalSizeConfig provides sizeConfig, - LocalUser provides currentUser - ) { - AppTheme( - useDarkTheme = themeConfig.darkMode, - useDynamicColors = themeConfig.dynamicColors, - selectedColorScheme = themeConfig.selectedColorScheme, - useAmoledBackground = themeConfig.amoledDark, - useSystemFont = themeConfig.useSystemFont - ) { - RootScreen(viewModel = viewModel) - } - } + }, + toggleOnlineService = ::toggleOnlineService + ) } } @@ -279,7 +113,7 @@ class MainActivity : AppCompatActivity() { } val notificationManager: NotificationManager = - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + getSystemService(NOTIFICATION_SERVICE) as NotificationManager notificationManager.createNotificationChannels( listOf( diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt index a55bf359..ec82afc4 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/RootScreen.kt @@ -1,8 +1,10 @@ package dev.meloda.fast.presentation +import android.Manifest import android.content.Intent import android.net.Uri import android.provider.Settings +import android.util.Log import androidx.activity.compose.LocalActivity import androidx.compose.animation.core.tween import androidx.compose.animation.fadeIn @@ -15,186 +17,373 @@ import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalResources import androidx.compose.ui.res.stringResource import androidx.compose.ui.window.DialogProperties +import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.rememberNavController +import com.conena.nanokt.android.content.pxToDp +import com.google.accompanist.permissions.ExperimentalPermissionsApi +import com.google.accompanist.permissions.isGranted +import com.google.accompanist.permissions.rememberPermissionState import dev.meloda.fast.MainViewModel +import dev.meloda.fast.MainViewModelImpl import dev.meloda.fast.auth.authNavGraph import dev.meloda.fast.auth.navigateToAuth import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials +import dev.meloda.fast.common.LongPollController +import dev.meloda.fast.common.model.LongPollState import dev.meloda.fast.conversations.navigation.createChatScreen import dev.meloda.fast.conversations.navigation.navigateToCreateChat +import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.languagepicker.navigation.languagePickerScreen import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen import dev.meloda.fast.messageshistory.navigation.navigateToMessagesHistory +import dev.meloda.fast.model.api.domain.VkUser import dev.meloda.fast.navigation.Main import dev.meloda.fast.navigation.mainScreen import dev.meloda.fast.photoviewer.presentation.PhotoViewDialog import dev.meloda.fast.settings.navigation.navigateToSettings import dev.meloda.fast.settings.navigation.settingsScreen import dev.meloda.fast.ui.R +import dev.meloda.fast.ui.common.LocalSizeConfig +import dev.meloda.fast.ui.model.DeviceSize +import dev.meloda.fast.ui.model.SizeConfig +import dev.meloda.fast.ui.model.ThemeConfig +import dev.meloda.fast.ui.theme.AppTheme import dev.meloda.fast.ui.theme.LocalNavController import dev.meloda.fast.ui.theme.LocalNavRootController +import dev.meloda.fast.ui.theme.LocalThemeConfig +import dev.meloda.fast.ui.theme.LocalUser +import dev.meloda.fast.ui.util.ImmutableList +import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList +import dev.meloda.fast.ui.util.immutableListOf +import dev.meloda.fast.ui.util.isNeedToEnableDarkMode +import org.koin.androidx.compose.koinViewModel +import org.koin.compose.koinInject +@OptIn(ExperimentalPermissionsApi::class) @Composable fun RootScreen( - navController: NavHostController = rememberNavController(), - viewModel: MainViewModel + toggleLongPollService: (enable: Boolean, inBackground: Boolean?) -> Unit, + toggleOnlineService: (enable: Boolean) -> Unit ) { - val activity = LocalActivity.current - val context = LocalContext.current - val startDestination by viewModel.startDestination.collectAsStateWithLifecycle() - val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle() - val isNeedToShowDeniedDialog by viewModel.isNeedToShowNotificationsDeniedDialog.collectAsStateWithLifecycle() - val isNeedToShowRationaleDialog by viewModel.isNeedToShowNotificationsRationaleDialog.collectAsStateWithLifecycle() + val resources = LocalResources.current - LaunchedEffect(isNeedToOpenAuth) { - if (isNeedToOpenAuth) { - viewModel.onNavigatedToAuth() - navController.navigateToAuth(clearBackStack = true) + val userSettings: UserSettings = koinInject() + val longPollController: LongPollController = koinInject() + + val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle() + val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle() + + val viewModel: MainViewModel = koinViewModel() + LaunchedEffect(viewModel) { + Log.d("VM_CREATE", "RootScreen(): viewModel: $viewModel") + } + + val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle() + + val permissionState = + rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) + + val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle() + val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle() + + LaunchedEffect(isNeedToCheckPermission) { + if (isNeedToCheckPermission) { + viewModel.onPermissionCheckStatus(permissionState.status) + + if (permissionState.status.isGranted) { + if (longPollCurrentState == LongPollState.InApp) { + toggleLongPollService(false, null) + } + + toggleLongPollService(true, true) + } } } - if (isNeedToShowDeniedDialog) { - AlertDialog( - onDismissRequest = viewModel::onNotificationsDeniedDialogDismissed, - title = { Text(text = stringResource(id = R.string.warning)) }, - text = { Text(text = stringResource(id = R.string.background_long_poll_denied_text)) }, - confirmButton = { - TextButton(onClick = viewModel::onNotificationsDeniedDialogConfirmClicked) { - Text(text = stringResource(id = R.string.action_request)) - } - }, - dismissButton = { - TextButton(onClick = viewModel::onNotificationsDeniedDialogCancelClicked) { - Text(text = stringResource(id = R.string.action_disable)) - } - }, - properties = DialogProperties( - dismissOnBackPress = false, - dismissOnClickOutside = false - ) - ) + LaunchedEffect(isNeedToRequestPermission) { + if (isNeedToRequestPermission) { + viewModel.onPermissionsRequested() + permissionState.launchPermissionRequest() + } } - if (isNeedToShowRationaleDialog) { - AlertDialog( - onDismissRequest = viewModel::onNotificationsRationaleDialogDismissed, - title = { Text(text = stringResource(id = R.string.warning)) }, - text = { Text(text = stringResource(id = R.string.background_long_poll_rationale_text)) }, - confirmButton = { - TextButton( - onClick = { - context.startActivity( - Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", context.packageName, null) - ) - ) - } - ) { - Text(text = stringResource(id = R.string.title_settings)) - } - }, - dismissButton = { - TextButton(onClick = viewModel::onNotificationsRationaleDialogCancelClicked) { - Text(text = stringResource(id = R.string.action_disable)) - } - }, - properties = DialogProperties( - dismissOnBackPress = false, - dismissOnClickOutside = false - ) - ) - } - - if (startDestination != null) { - CompositionLocalProvider( - LocalNavRootController provides navController, - LocalNavController provides navController - ) { - var photoViewerInfo by rememberSaveable { - mutableStateOf, Int?>?>(null) + LifecycleResumeEffect(longPollStateToApply) { + Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply") + if (longPollStateToApply != LongPollState.Background) { + if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched() + && longPollCurrentState != longPollStateToApply + ) { + toggleLongPollService(false, null) + Log.d("LongPoll", "recreate()") } - Box(modifier = Modifier.fillMaxSize()) { - NavHost( - navController = navController, - startDestination = requireNotNull(startDestination), - enterTransition = { fadeIn(animationSpec = tween(200)) }, - exitTransition = { fadeOut(animationSpec = tween(200)) } - ) { - authNavGraph( - onNavigateToMain = { - viewModel.onUserAuthenticated() - navController.navigateToMain() - }, - onNavigateToSettings = navController::navigateToSettings, - navController = navController - ) + toggleLongPollService( + longPollStateToApply.isLaunched(), + longPollStateToApply == LongPollState.Background + ) + } - mainScreen( - onError = viewModel::onError, - onSettingsButtonClicked = navController::navigateToSettings, - onNavigateToMessagesHistory = navController::navigateToMessagesHistory, - onPhotoClicked = { url -> photoViewerInfo = listOf(url) to null }, - onMessageClicked = navController::navigateToMessagesHistory, - onNavigateToCreateChat = navController::navigateToCreateChat - ) + onPauseOrDispose {} + } - messagesHistoryScreen( - onError = viewModel::onError, - onBack = navController::navigateUp, - onNavigateToChatMaterials = navController::navigateToChatMaterials, - onNavigateToPhotoViewer = { photos, index -> - photoViewerInfo = photos to index - } - ) - chatMaterialsScreen( - onBack = navController::navigateUp, - onPhotoClicked = { url -> photoViewerInfo = listOf(url) to null } - ) - createChatScreen( - onChatCreated = { conversationId -> - navController.popBackStack() - navController.navigateToMessagesHistory(conversationId) - }, - navController = navController - ) + val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle() + LifecycleResumeEffect(sendOnline) { + toggleOnlineService(sendOnline) - settingsScreen( - onBack = navController::navigateUp, - onLogOutButtonClicked = { navController.navigateToAuth(true) }, - onLanguageItemClicked = navController::navigateToLanguagePicker, - onRestartRequired = { - activity?.let { - val intent = Intent(activity, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) - activity.startActivity(intent) - activity.finish() - } - } - ) - languagePickerScreen(onBack = navController::navigateUp) + onPauseOrDispose { + toggleOnlineService(false) + } + } + + val deviceWidthDp = remember(true) { + resources.displayMetrics.widthPixels.pxToDp() + } + val deviceHeightDp = remember(true) { + resources.displayMetrics.heightPixels.pxToDp() + } + + val deviceWidthSize by remember(deviceWidthDp) { + derivedStateOf { + when { + deviceWidthDp <= 360 -> DeviceSize.Small + deviceWidthDp <= 600 -> DeviceSize.Compact + deviceWidthDp <= 840 -> DeviceSize.Medium + else -> DeviceSize.Expanded + } + } + } + + val deviceHeightSize by remember(deviceHeightDp) { + derivedStateOf { + when { + deviceHeightDp <= 480 -> DeviceSize.Small + deviceHeightDp <= 700 -> DeviceSize.Compact + deviceHeightDp <= 900 -> DeviceSize.Medium + else -> DeviceSize.Expanded + } + } + } + + val sizeConfig by remember(deviceWidthSize, deviceHeightSize) { + mutableStateOf( + SizeConfig( + widthSize = deviceWidthSize, + heightSize = deviceHeightSize + ) + ) + } + + val darkMode by userSettings.darkMode.collectAsStateWithLifecycle() + val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle() + val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle() + val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle() + val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle() + val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle() + val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle() + + val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode) + + val themeConfig by remember( + darkMode, + dynamicColors, + amoledDark, + enableBlur, + enableMultiline, + setDarkMode, + useSystemFont + ) { + derivedStateOf { + ThemeConfig( + darkMode = setDarkMode, + dynamicColors = dynamicColors, + selectedColorScheme = 0, + amoledDark = amoledDark, + enableBlur = enableBlur, + enableMultiline = enableMultiline, + useSystemFont = useSystemFont, + enableAnimations = enableAnimations + ) + } + } + + CompositionLocalProvider( + LocalThemeConfig provides themeConfig, + LocalSizeConfig provides sizeConfig, + LocalUser provides currentUser + ) { + AppTheme( + useDarkTheme = themeConfig.darkMode, + useDynamicColors = themeConfig.dynamicColors, + selectedColorScheme = themeConfig.selectedColorScheme, + useAmoledBackground = themeConfig.amoledDark, + useSystemFont = themeConfig.useSystemFont + ) { + val navController: NavHostController = rememberNavController() + val activity = LocalActivity.current + val context = LocalContext.current + val startDestination by viewModel.startDestination.collectAsStateWithLifecycle() + val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle() + val isNeedToShowDeniedDialog by viewModel.isNeedToShowNotificationsDeniedDialog.collectAsStateWithLifecycle() + val isNeedToShowRationaleDialog by viewModel.isNeedToShowNotificationsRationaleDialog.collectAsStateWithLifecycle() + + LaunchedEffect(isNeedToOpenAuth) { + if (isNeedToOpenAuth) { + viewModel.onNavigatedToAuth() + navController.navigateToAuth(clearBackStack = true) } + } - PhotoViewDialog( - photoViewerInfo = photoViewerInfo, - onDismiss = { photoViewerInfo = null } + if (isNeedToShowDeniedDialog) { + AlertDialog( + onDismissRequest = viewModel::onNotificationsDeniedDialogDismissed, + title = { Text(text = stringResource(id = R.string.warning)) }, + text = { Text(text = stringResource(id = R.string.background_long_poll_denied_text)) }, + confirmButton = { + TextButton(onClick = viewModel::onNotificationsDeniedDialogConfirmClicked) { + Text(text = stringResource(id = R.string.action_request)) + } + }, + dismissButton = { + TextButton(onClick = viewModel::onNotificationsDeniedDialogCancelClicked) { + Text(text = stringResource(id = R.string.action_disable)) + } + }, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ) ) } + + if (isNeedToShowRationaleDialog) { + AlertDialog( + onDismissRequest = viewModel::onNotificationsRationaleDialogDismissed, + title = { Text(text = stringResource(id = R.string.warning)) }, + text = { Text(text = stringResource(id = R.string.background_long_poll_rationale_text)) }, + confirmButton = { + TextButton( + onClick = { + context.startActivity( + Intent( + Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + Uri.fromParts("package", context.packageName, null) + ) + ) + } + ) { + Text(text = stringResource(id = R.string.title_settings)) + } + }, + dismissButton = { + TextButton(onClick = viewModel::onNotificationsRationaleDialogCancelClicked) { + Text(text = stringResource(id = R.string.action_disable)) + } + }, + properties = DialogProperties( + dismissOnBackPress = false, + dismissOnClickOutside = false + ) + ) + } + + if (startDestination != null) { + CompositionLocalProvider( + LocalNavRootController provides navController, + LocalNavController provides navController + ) { + var photoViewerInfo by rememberSaveable { + mutableStateOf, Int?>?>(null) + } + + Box(modifier = Modifier.fillMaxSize()) { + NavHost( + navController = navController, + startDestination = requireNotNull(startDestination), + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) } + ) { + authNavGraph( + onNavigateToMain = { + viewModel.onUserAuthenticated() + navController.navigateToMain() + }, + onNavigateToSettings = navController::navigateToSettings, + navController = navController + ) + + mainScreen( + onError = viewModel::onError, + onSettingsButtonClicked = navController::navigateToSettings, + onNavigateToMessagesHistory = navController::navigateToMessagesHistory, + onPhotoClicked = { url -> + photoViewerInfo = immutableListOf(url) to null + }, + onMessageClicked = navController::navigateToMessagesHistory, + onNavigateToCreateChat = navController::navigateToCreateChat + ) + + messagesHistoryScreen( + onError = viewModel::onError, + onBack = navController::navigateUp, + onNavigateToChatMaterials = navController::navigateToChatMaterials, + onNavigateToPhotoViewer = { photos, index -> + photoViewerInfo = photos.toImmutableList() to index + } + ) + chatMaterialsScreen( + onBack = navController::navigateUp, + onPhotoClicked = { url -> + photoViewerInfo = immutableListOf(url) to null + } + ) + createChatScreen( + onChatCreated = { conversationId -> + navController.popBackStack() + navController.navigateToMessagesHistory(conversationId) + }, + navController = navController + ) + + settingsScreen( + onBack = navController::navigateUp, + onLogOutButtonClicked = { navController.navigateToAuth(true) }, + onLanguageItemClicked = navController::navigateToLanguagePicker, + onRestartRequired = { + activity?.let { + val intent = Intent(activity, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) + activity.startActivity(intent) + activity.finish() + } + } + ) + languagePickerScreen(onBack = navController::navigateUp) + } + + PhotoViewDialog( + photoViewerInfo = photoViewerInfo, + onDismiss = { photoViewerInfo = null } + ) + } + } + } } } } diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt index 9d234732..140c338c 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/messages/MessagesRepositoryImpl.kt @@ -92,12 +92,14 @@ class MessagesRepositoryImpl( group = groupsMap.messageGroup(message), actionUser = usersMap.messageActionUser(message), actionGroup = groupsMap.messageActionGroup(message), - replyMessage = message.replyMessage?.copy( - user = usersMap.messageUser(message), - group = groupsMap.messageGroup(message), - actionUser = usersMap.messageActionUser(message), - actionGroup = groupsMap.messageActionGroup(message), - ) + replyMessage = message.replyMessage.let { replyMessage -> + replyMessage?.copy( + user = usersMap.messageUser(replyMessage), + group = groupsMap.messageGroup(replyMessage), + actionUser = usersMap.messageActionUser(replyMessage), + actionGroup = groupsMap.messageActionGroup(replyMessage), + ) + } ).also { VkMemoryCache[message.id] = it } } } @@ -167,12 +169,14 @@ class MessagesRepositoryImpl( group = groupsMap.messageGroup(message), actionUser = usersMap.messageActionUser(message), actionGroup = groupsMap.messageActionGroup(message), - replyMessage = message.replyMessage?.asDomain()?.copy( - user = usersMap.messageUser(message), - group = groupsMap.messageGroup(message), - actionUser = usersMap.messageActionUser(message), - actionGroup = groupsMap.messageActionGroup(message), - ) + replyMessage = message.replyMessage?.asDomain().let { replyMessage -> + replyMessage?.copy( + user = usersMap.messageUser(replyMessage), + group = groupsMap.messageGroup(replyMessage), + actionUser = usersMap.messageActionUser(replyMessage), + actionGroup = groupsMap.messageActionGroup(replyMessage), + ) + } ) } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt index bc36e2d1..d204f0bb 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/navigation/FriendsNavigation.kt @@ -5,6 +5,7 @@ import androidx.navigation.NavGraphBuilder import androidx.navigation.compose.composable import dev.meloda.fast.friends.FriendsViewModel import dev.meloda.fast.friends.FriendsViewModelImpl +import dev.meloda.fast.friends.OnlineFriendsViewModelImpl import dev.meloda.fast.friends.presentation.FriendsRoute import dev.meloda.fast.model.BaseError import kotlinx.serialization.Serializable @@ -20,14 +21,14 @@ fun NavGraphBuilder.friendsScreen( onMessageClicked: (userId: Long) -> Unit, onScrolledToTop: () -> Unit ) { - val friendsViewModel: FriendsViewModel = with(activity) { - getViewModel() - } + val friendsViewModel: FriendsViewModel = activity.getViewModel() + val onlineFriendsViewModel = + activity.getViewModel() composable { FriendsRoute( - activity = activity, friendsViewModel = friendsViewModel, + onlineFriendsViewModel = onlineFriendsViewModel, onError = onError, onPhotoClicked = onPhotoClicked, onMessageClicked = onMessageClicked, diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt index 0e4ae36e..90489634 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/presentation/RootFriendsScreen.kt @@ -1,6 +1,5 @@ package dev.meloda.fast.friends.presentation -import androidx.appcompat.app.AppCompatActivity import androidx.compose.animation.animateColorAsState import androidx.compose.animation.core.FastOutLinearInEasing import androidx.compose.animation.core.animateFloatAsState @@ -54,17 +53,16 @@ import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList import kotlinx.coroutines.launch -import org.koin.androidx.viewmodel.ext.android.getViewModel @OptIn(ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class) @Composable fun FriendsRoute( - activity: AppCompatActivity, friendsViewModel: FriendsViewModel, + onlineFriendsViewModel: OnlineFriendsViewModelImpl, onError: (BaseError) -> Unit, onPhotoClicked: (url: String) -> Unit, onMessageClicked: (userid: Long) -> Unit, - onScrolledToTop: () -> Unit + onScrolledToTop: () -> Unit, ) { val scope = rememberCoroutineScope() val currentTheme = LocalThemeConfig.current @@ -232,9 +230,7 @@ fun FriendsRoute( modifier = Modifier.fillMaxSize(), ) { index -> FriendsScreen( - viewModel = if (index == 0) friendsViewModel else with(activity) { - getViewModel() - }, + viewModel = if (index == 0) friendsViewModel else onlineFriendsViewModel, orderType = orderType, padding = padding, tabIndex = index, diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt index 80f3ae42..ab710e20 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/UiItem.kt @@ -1,5 +1,6 @@ package dev.meloda.fast.messageshistory.model +import androidx.compose.runtime.Stable import androidx.compose.ui.text.AnnotatedString import dev.meloda.fast.common.model.UiImage import dev.meloda.fast.model.api.domain.VkAttachment @@ -9,6 +10,7 @@ sealed class UiItem( open val cmId: Long ) { + @Stable data class Message( override val id: Long, override val cmId: Long, @@ -35,6 +37,7 @@ sealed class UiItem( val replySummary: String? ) : UiItem(id, cmId) + @Stable data class ActionMessage( override val id: Long, override val cmId: Long, diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt index 007f76be..89f969bc 100644 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/presentation/PhotoViewScreen.kt @@ -1,6 +1,5 @@ package dev.meloda.fast.photoviewer.presentation -import android.content.Intent import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.core.Animatable @@ -59,9 +58,9 @@ import dev.meloda.fast.photoviewer.model.PhotoViewScreenState import dev.meloda.fast.ui.R import dev.meloda.fast.ui.components.FullScreenDialog import dev.meloda.fast.ui.components.Loader +import dev.meloda.fast.ui.util.ImmutableList import dev.meloda.fast.ui.util.getImage import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import java.net.URLEncoder @@ -69,7 +68,7 @@ import kotlin.math.abs @Composable fun PhotoViewDialog( - photoViewerInfo: Pair, Int?>?, + photoViewerInfo: Pair, Int?>?, modifier: Modifier = Modifier, onDismiss: () -> Unit ) { @@ -85,7 +84,7 @@ fun PhotoViewDialog( arguments = PhotoViewArguments( imageUrls = photoViewerInfo.first.map { URLEncoder.encode(it, "utf-8") - }, + }.toList(), selectedIndex = photoViewerInfo.second ), applicationContext = applicationContext