forked from melod1n/fast-messenger
Release 0.2.0 (#150)
Release Notes * Bumped haze, agp, and guava dependencies * Implemented ordering functionality for friends list * Added scroll to top feature in friends and conversations screens * Improved messages handling * Fixed coloring issues * Cache improvements * Implemented logout functionality * Implemented new authorization flow (no auto-token re-request) * Added support for sticker pack preview attachments * Bump LongPoll to version 19 * Markdown support for messages bubbles * Adjust app name font size based on screen width --------- Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -24,6 +24,7 @@ import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.domain.GetCurrentAccountUseCase
|
||||
import dev.meloda.fast.domain.LoadUserByIdUseCase
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
import dev.meloda.fast.navigation.Main
|
||||
import dev.meloda.fast.settings.navigation.Settings
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -36,14 +37,13 @@ interface MainViewModel {
|
||||
|
||||
val startDestination: StateFlow<Any?>
|
||||
val isNeedToReplaceWithAuth: StateFlow<Boolean>
|
||||
val currentUser: StateFlow<VkUser?>
|
||||
|
||||
val isNeedToShowNotificationsDeniedDialog: StateFlow<Boolean>
|
||||
val isNeedToShowNotificationsRationaleDialog: StateFlow<Boolean>
|
||||
val isNeedToCheckNotificationsPermission: StateFlow<Boolean>
|
||||
val isNeedToRequestNotifications: StateFlow<Boolean>
|
||||
|
||||
val profileImageUrl: StateFlow<String?>
|
||||
|
||||
fun onError(error: BaseError)
|
||||
|
||||
fun onNavigatedToAuth()
|
||||
@@ -59,6 +59,8 @@ interface MainViewModel {
|
||||
fun onNotificationsDeniedDialogDismissed()
|
||||
fun onNotificationsRationaleDialogDismissed()
|
||||
fun onNotificationsRationaleDialogCancelClicked()
|
||||
|
||||
fun onUserAuthenticated()
|
||||
}
|
||||
|
||||
class MainViewModelImpl(
|
||||
@@ -70,24 +72,24 @@ class MainViewModelImpl(
|
||||
|
||||
override val startDestination = MutableStateFlow<Any?>(null)
|
||||
override val isNeedToReplaceWithAuth = MutableStateFlow(false)
|
||||
override val currentUser = MutableStateFlow<VkUser?>(null)
|
||||
|
||||
override val isNeedToShowNotificationsDeniedDialog = MutableStateFlow(false)
|
||||
override val isNeedToShowNotificationsRationaleDialog = MutableStateFlow(false)
|
||||
override val isNeedToCheckNotificationsPermission = MutableStateFlow(false)
|
||||
override val isNeedToRequestNotifications = MutableStateFlow(false)
|
||||
|
||||
override val profileImageUrl = MutableStateFlow<String?>(null)
|
||||
|
||||
private var openNotificationsSettings = false
|
||||
private var openAppSettings = false
|
||||
|
||||
override fun onError(error: BaseError) {
|
||||
when (error) {
|
||||
BaseError.SessionExpired -> {
|
||||
BaseError.SessionExpired,
|
||||
BaseError.AccountBlocked -> {
|
||||
isNeedToReplaceWithAuth.update { true }
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui
|
||||
else -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,17 +172,20 @@ class MainViewModelImpl(
|
||||
disableBackgroundLongPoll()
|
||||
}
|
||||
|
||||
override fun onUserAuthenticated() {
|
||||
loadProfile()
|
||||
}
|
||||
|
||||
private fun loadProfile() {
|
||||
loadUserByIdUseCase(userId = null)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
profileImageUrl.emit(null)
|
||||
currentUser.emit(null)
|
||||
},
|
||||
success = { response ->
|
||||
val user = response ?: return@listenValue
|
||||
|
||||
profileImageUrl.emit(user.photo100)
|
||||
currentUser.emit(user)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -51,7 +51,6 @@ val applicationModule = module {
|
||||
createChatModule
|
||||
)
|
||||
|
||||
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
|
||||
// TODO: 14/05/2024, Danil Nikolaev: extract all operations with preferences to standalone class
|
||||
singleOf(PreferenceManager::getDefaultSharedPreferences)
|
||||
single<Resources> { androidContext().resources }
|
||||
|
||||
@@ -2,8 +2,7 @@ package dev.meloda.fast.navigation
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import dev.meloda.fast.MainViewModel
|
||||
import dev.meloda.fast.conversations.navigation.Conversations
|
||||
import dev.meloda.fast.conversations.navigation.ConversationsGraph
|
||||
import dev.meloda.fast.friends.navigation.Friends
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.model.BottomNavigationItem
|
||||
@@ -22,11 +21,10 @@ object Main
|
||||
fun NavGraphBuilder.mainScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
onSettingsButtonClicked: () -> Unit,
|
||||
onConversationClicked: (conversationId: Int) -> Unit,
|
||||
onNavigateToMessagesHistory: (conversationId: Long) -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userId: Int) -> Unit,
|
||||
onCreateChatClicked: () -> Unit,
|
||||
viewModel: MainViewModel
|
||||
onMessageClicked: (userid: Long) -> Unit,
|
||||
onNavigateToCreateChat: () -> Unit
|
||||
) {
|
||||
val navigationItems = ImmutableList.of(
|
||||
BottomNavigationItem(
|
||||
@@ -39,7 +37,7 @@ fun NavGraphBuilder.mainScreen(
|
||||
titleResId = UiR.string.title_conversations,
|
||||
selectedIconResId = UiR.drawable.baseline_chat_24,
|
||||
unselectedIconResId = UiR.drawable.outline_chat_24,
|
||||
route = Conversations
|
||||
route = ConversationsGraph
|
||||
),
|
||||
BottomNavigationItem(
|
||||
titleResId = UiR.string.title_profile,
|
||||
@@ -54,11 +52,10 @@ fun NavGraphBuilder.mainScreen(
|
||||
navigationItems = navigationItems,
|
||||
onError = onError,
|
||||
onSettingsButtonClicked = onSettingsButtonClicked,
|
||||
onConversationItemClicked = onConversationClicked,
|
||||
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked,
|
||||
onCreateChatClicked = onCreateChatClicked,
|
||||
viewModel = viewModel
|
||||
onNavigateToCreateChat = onNavigateToCreateChat
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@ 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.model.DeviceSize
|
||||
@@ -46,6 +47,7 @@ import dev.meloda.fast.ui.model.ThemeConfig
|
||||
import dev.meloda.fast.ui.theme.AppTheme
|
||||
import dev.meloda.fast.ui.theme.LocalSizeConfig
|
||||
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.KoinContext
|
||||
@@ -98,6 +100,8 @@ class MainActivity : AppCompatActivity() {
|
||||
|
||||
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
|
||||
|
||||
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
|
||||
|
||||
LifecycleResumeEffect(true) {
|
||||
viewModel.onAppResumed(intent)
|
||||
onPauseOrDispose {}
|
||||
@@ -133,7 +137,8 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(longPollStateToApply) {
|
||||
LifecycleResumeEffect(longPollStateToApply) {
|
||||
Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply")
|
||||
if (longPollStateToApply != LongPollState.Background) {
|
||||
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
|
||||
&& longPollCurrentState != longPollStateToApply
|
||||
@@ -147,6 +152,8 @@ class MainActivity : AppCompatActivity() {
|
||||
inBackground = longPollStateToApply == LongPollState.Background
|
||||
)
|
||||
}
|
||||
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
|
||||
@@ -202,6 +209,7 @@ class MainActivity : AppCompatActivity() {
|
||||
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)
|
||||
|
||||
@@ -214,7 +222,7 @@ class MainActivity : AppCompatActivity() {
|
||||
setDarkMode,
|
||||
useSystemFont
|
||||
) {
|
||||
mutableStateOf(
|
||||
derivedStateOf {
|
||||
ThemeConfig(
|
||||
darkMode = setDarkMode,
|
||||
dynamicColors = dynamicColors,
|
||||
@@ -222,14 +230,16 @@ class MainActivity : AppCompatActivity() {
|
||||
amoledDark = amoledDark,
|
||||
enableBlur = enableBlur,
|
||||
enableMultiline = enableMultiline,
|
||||
useSystemFont = useSystemFont
|
||||
useSystemFont = useSystemFont,
|
||||
enableAnimations = enableAnimations
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalThemeConfig provides themeConfig,
|
||||
LocalSizeConfig provides sizeConfig
|
||||
LocalSizeConfig provides sizeConfig,
|
||||
LocalUser provides currentUser
|
||||
) {
|
||||
AppTheme(
|
||||
useDarkTheme = themeConfig.darkMode,
|
||||
@@ -292,12 +302,19 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
private val longPollingServiceIntent by lazy {
|
||||
Intent(this, LongPollingService::class.java)
|
||||
}
|
||||
private val onlineServiceIntent by lazy {
|
||||
Intent(this, OnlineService::class.java)
|
||||
}
|
||||
|
||||
private fun toggleLongPollService(
|
||||
enable: Boolean,
|
||||
inBackground: Boolean = AppSettings.Experimental.longPollInBackground
|
||||
) {
|
||||
if (enable) {
|
||||
val longPollIntent = Intent(this, LongPollingService::class.java)
|
||||
val longPollIntent = longPollingServiceIntent
|
||||
|
||||
if (inBackground) {
|
||||
ContextCompat.startForegroundService(this, longPollIntent)
|
||||
@@ -305,15 +322,15 @@ class MainActivity : AppCompatActivity() {
|
||||
startService(longPollIntent)
|
||||
}
|
||||
} else {
|
||||
stopService(Intent(this, LongPollingService::class.java))
|
||||
stopService(longPollingServiceIntent)
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleOnlineService(enable: Boolean) {
|
||||
if (enable) {
|
||||
startService(Intent(this, OnlineService::class.java))
|
||||
startService(onlineServiceIntent)
|
||||
} else {
|
||||
stopService(Intent(this, OnlineService::class.java))
|
||||
stopService(onlineServiceIntent)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Icon
|
||||
@@ -16,6 +15,7 @@ import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
@@ -25,9 +25,9 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
@@ -36,8 +36,9 @@ import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.MainViewModel
|
||||
import dev.meloda.fast.conversations.navigation.conversationsScreen
|
||||
import dev.meloda.fast.conversations.navigation.Conversations
|
||||
import dev.meloda.fast.conversations.navigation.conversationsGraph
|
||||
import dev.meloda.fast.friends.navigation.Friends
|
||||
import dev.meloda.fast.friends.navigation.friendsScreen
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.model.BottomNavigationItem
|
||||
@@ -45,7 +46,10 @@ import dev.meloda.fast.navigation.MainGraph
|
||||
import dev.meloda.fast.profile.navigation.profileScreen
|
||||
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||
import dev.meloda.fast.ui.theme.LocalHazeState
|
||||
import dev.meloda.fast.ui.theme.LocalNavController
|
||||
import dev.meloda.fast.ui.theme.LocalReselectedTab
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.theme.LocalUser
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
|
||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||
@@ -54,38 +58,47 @@ fun MainScreen(
|
||||
navigationItems: ImmutableList<BottomNavigationItem>,
|
||||
onError: (BaseError) -> Unit = {},
|
||||
onSettingsButtonClicked: () -> Unit = {},
|
||||
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
||||
onNavigateToMessagesHistory: (conversationId: Long) -> Unit = {},
|
||||
onPhotoClicked: (url: String) -> Unit = {},
|
||||
onMessageClicked: (userId: Int) -> Unit = {},
|
||||
onCreateChatClicked: () -> Unit = {},
|
||||
viewModel: MainViewModel
|
||||
onMessageClicked: (userid: Long) -> Unit = {},
|
||||
onNavigateToCreateChat: () -> Unit = {}
|
||||
) {
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
val theme = LocalThemeConfig.current
|
||||
val hazeState = remember { HazeState() }
|
||||
val navController = rememberNavController()
|
||||
|
||||
val profileImageUrl by viewModel.profileImageUrl.collectAsStateWithLifecycle()
|
||||
|
||||
var selectedItemIndex by rememberSaveable {
|
||||
mutableIntStateOf(1)
|
||||
}
|
||||
|
||||
val user = LocalUser.current
|
||||
val profileImageUrl by remember(user) {
|
||||
derivedStateOf { user?.photo100 }
|
||||
}
|
||||
|
||||
var tabReselected by remember {
|
||||
mutableStateOf(
|
||||
navigationItems.associate {
|
||||
it.route to false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
bottomBar = {
|
||||
NavigationBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
if (theme.enableBlur) {
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
} else Modifier
|
||||
)
|
||||
.fillMaxWidth(),
|
||||
containerColor = NavigationBarDefaults.containerColor.copy(
|
||||
alpha = if (currentTheme.enableBlur) 0f else 1f
|
||||
)
|
||||
),
|
||||
containerColor = if (theme.enableBlur) Color.Transparent
|
||||
else NavigationBarDefaults.containerColor
|
||||
) {
|
||||
navigationItems.forEachIndexed { index, item ->
|
||||
NavigationBarItem(
|
||||
@@ -100,6 +113,10 @@ fun MainScreen(
|
||||
inclusive = true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tabReselected = tabReselected.toMutableMap().also {
|
||||
it[navigationItems[index].route] = true
|
||||
}
|
||||
}
|
||||
},
|
||||
icon = {
|
||||
@@ -123,9 +140,7 @@ fun MainScreen(
|
||||
.size(24.dp)
|
||||
.clip(CircleShape)
|
||||
.alpha(if (isLoading) 0f else 1f),
|
||||
onSuccess = {
|
||||
isLoading = false
|
||||
}
|
||||
onSuccess = { isLoading = false }
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
@@ -146,11 +161,12 @@ fun MainScreen(
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(bottom = if (currentTheme.enableBlur) 0.dp else padding.calculateBottomPadding())
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
LocalHazeState provides hazeState,
|
||||
LocalBottomPadding provides if (currentTheme.enableBlur) padding.calculateBottomPadding() else 0.dp
|
||||
LocalBottomPadding provides padding.calculateBottomPadding(),
|
||||
LocalReselectedTab provides tabReselected,
|
||||
LocalNavController provides navController
|
||||
) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
@@ -165,22 +181,28 @@ fun MainScreen(
|
||||
) {
|
||||
friendsScreen(
|
||||
onError = onError,
|
||||
navController = navController,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked
|
||||
onMessageClicked = onMessageClicked,
|
||||
onScrolledToTop = {
|
||||
tabReselected = tabReselected.toMutableMap().also {
|
||||
it[Friends] = false
|
||||
}
|
||||
},
|
||||
)
|
||||
conversationsScreen(
|
||||
conversationsGraph(
|
||||
onError = onError,
|
||||
onConversationItemClicked = onConversationItemClicked,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onCreateChatClicked = onCreateChatClicked,
|
||||
navController = navController,
|
||||
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
||||
onNavigateToCreateChat = onNavigateToCreateChat,
|
||||
onScrolledToTop = {
|
||||
tabReselected = tabReselected.toMutableMap().also {
|
||||
it[Conversations] = false
|
||||
}
|
||||
}
|
||||
)
|
||||
profileScreen(
|
||||
onError = onError,
|
||||
onSettingsButtonClicked = onSettingsButtonClicked,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
navController = navController
|
||||
onPhotoClicked = onPhotoClicked
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
@@ -38,6 +39,8 @@ import dev.meloda.fast.photoviewer.navigation.photoViewScreen
|
||||
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.theme.LocalNavController
|
||||
import dev.meloda.fast.ui.theme.LocalNavRootController
|
||||
|
||||
@Composable
|
||||
fun RootScreen(
|
||||
@@ -111,51 +114,59 @@ fun RootScreen(
|
||||
}
|
||||
|
||||
if (startDestination != null) {
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = requireNotNull(startDestination),
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||
CompositionLocalProvider(
|
||||
LocalNavRootController provides navController,
|
||||
LocalNavController provides navController
|
||||
) {
|
||||
authNavGraph(
|
||||
onNavigateToMain = navController::navigateToMain,
|
||||
navController = navController
|
||||
)
|
||||
mainScreen(
|
||||
onError = viewModel::onError,
|
||||
onSettingsButtonClicked = navController::navigateToSettings,
|
||||
onConversationClicked = navController::navigateToMessagesHistory,
|
||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
|
||||
onMessageClicked = navController::navigateToMessagesHistory,
|
||||
onCreateChatClicked = navController::navigateToCreateChat,
|
||||
viewModel = viewModel
|
||||
)
|
||||
NavHost(
|
||||
navController = navController,
|
||||
startDestination = requireNotNull(startDestination),
|
||||
enterTransition = { fadeIn(animationSpec = tween(200)) },
|
||||
exitTransition = { fadeOut(animationSpec = tween(200)) }
|
||||
) {
|
||||
authNavGraph(
|
||||
onNavigateToMain = {
|
||||
viewModel.onUserAuthenticated()
|
||||
navController.navigateToMain()
|
||||
},
|
||||
navController = navController
|
||||
)
|
||||
|
||||
messagesHistoryScreen(
|
||||
onError = viewModel::onError,
|
||||
onBack = navController::navigateUp,
|
||||
onChatMaterialsDropdownItemClicked = navController::navigateToChatMaterials
|
||||
)
|
||||
chatMaterialsScreen(
|
||||
onBack = navController::navigateUp,
|
||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
|
||||
)
|
||||
createChatScreen(
|
||||
onChatCreated = { conversationId ->
|
||||
navController.popBackStack()
|
||||
navController.navigateToMessagesHistory(conversationId)
|
||||
},
|
||||
navController = navController
|
||||
)
|
||||
mainScreen(
|
||||
onError = viewModel::onError,
|
||||
onSettingsButtonClicked = navController::navigateToSettings,
|
||||
onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
|
||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
|
||||
onMessageClicked = navController::navigateToMessagesHistory,
|
||||
onNavigateToCreateChat = navController::navigateToCreateChat
|
||||
)
|
||||
|
||||
settingsScreen(
|
||||
onBack = navController::navigateUp,
|
||||
onLogOutButtonClicked = { navController.navigateToAuth(true) },
|
||||
onLanguageItemClicked = navController::navigateToLanguagePicker
|
||||
)
|
||||
languagePickerScreen(onBack = navController::navigateUp)
|
||||
messagesHistoryScreen(
|
||||
onError = viewModel::onError,
|
||||
onBack = navController::navigateUp,
|
||||
onNavigateToChatMaterials = navController::navigateToChatMaterials
|
||||
)
|
||||
chatMaterialsScreen(
|
||||
onBack = navController::navigateUp,
|
||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
|
||||
)
|
||||
createChatScreen(
|
||||
onChatCreated = { conversationId ->
|
||||
navController.popBackStack()
|
||||
navController.navigateToMessagesHistory(conversationId)
|
||||
},
|
||||
navController = navController
|
||||
)
|
||||
|
||||
photoViewScreen(onBack = navController::navigateUp)
|
||||
settingsScreen(
|
||||
onBack = navController::navigateUp,
|
||||
onLogOutButtonClicked = { navController.navigateToAuth(true) },
|
||||
onLanguageItemClicked = navController::navigateToLanguagePicker
|
||||
)
|
||||
languagePickerScreen(onBack = navController::navigateUp)
|
||||
|
||||
photoViewScreen(onBack = navController::navigateUp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,11 +30,13 @@ import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
class LongPollingService : Service() {
|
||||
|
||||
@@ -42,17 +44,11 @@ class LongPollingService : Service() {
|
||||
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e(TAG, "error: $throwable")
|
||||
|
||||
if (throwable !is NoAccessTokenException) {
|
||||
throwable.printStackTrace()
|
||||
private val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
handleError(throwable)
|
||||
}
|
||||
|
||||
longPollController.updateCurrentState(LongPollState.Exception)
|
||||
longPollController.setStateToApply(LongPollState.Exception)
|
||||
}
|
||||
|
||||
private val coroutineContext: CoroutineContext
|
||||
get() = Dispatchers.IO + job + exceptionHandler
|
||||
|
||||
@@ -63,6 +59,8 @@ class LongPollingService : Service() {
|
||||
|
||||
private var currentJob: Job? = null
|
||||
|
||||
private val inBackground get() = AppSettings.Experimental.longPollInBackground
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
Log.d(STATE_TAG, "onCreate()")
|
||||
@@ -76,21 +74,12 @@ class LongPollingService : Service() {
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
if (startId > 1) return START_STICKY
|
||||
|
||||
val inBackground = AppSettings.Experimental.longPollInBackground
|
||||
|
||||
Log.d(
|
||||
STATE_TAG,
|
||||
"onStartCommand: asForeground: $inBackground; flags: $flags; startId: $startId;\ninstance: $this"
|
||||
)
|
||||
|
||||
if (currentJob != null) {
|
||||
currentJob?.cancel()
|
||||
currentJob = null
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
currentJob = startPolling().also { it.join() }
|
||||
}
|
||||
startJob()
|
||||
|
||||
val openCategorySettingsIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Intent(Settings.ACTION_CHANNEL_NOTIFICATION_SETTINGS)
|
||||
@@ -108,11 +97,6 @@ class LongPollingService : Service() {
|
||||
PendingIntent.FLAG_IMMUTABLE
|
||||
)
|
||||
|
||||
longPollController.updateCurrentState(
|
||||
if (inBackground) LongPollState.Background
|
||||
else LongPollState.InApp
|
||||
)
|
||||
|
||||
if (inBackground) {
|
||||
val notification =
|
||||
NotificationsUtils.createNotification(
|
||||
@@ -134,17 +118,33 @@ class LongPollingService : Service() {
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun startJob() {
|
||||
if (currentJob != null) {
|
||||
currentJob?.cancel()
|
||||
currentJob = null
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
currentJob = startPolling().also { it.join() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun startPolling(): Job {
|
||||
if (job.isCompleted || job.isCancelled) {
|
||||
Log.d(STATE_TAG, "job is completed or cancelled")
|
||||
Log.d(STATE_TAG, "Job is completed or cancelled")
|
||||
throw Exception("Job is over")
|
||||
}
|
||||
|
||||
Log.d(STATE_TAG, "job started")
|
||||
Log.d(STATE_TAG, "Starting job...")
|
||||
|
||||
return coroutineScope.launch(coroutineContext) {
|
||||
longPollController.updateCurrentState(
|
||||
if (inBackground) LongPollState.Background
|
||||
else LongPollState.InApp
|
||||
)
|
||||
|
||||
return coroutineScope.launch {
|
||||
if (UserConfig.accessToken.isEmpty()) {
|
||||
throw NoAccessTokenException
|
||||
throw NoAccessTokenException()
|
||||
}
|
||||
|
||||
var serverInfo = getServerInfo()
|
||||
@@ -246,10 +246,24 @@ class LongPollingService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleError(throwable: Throwable) {
|
||||
Log.e(TAG, "error: $throwable")
|
||||
|
||||
if (throwable !is NoAccessTokenException) {
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
coroutineScope.launch {
|
||||
delay(5.seconds)
|
||||
startJob()
|
||||
}
|
||||
|
||||
longPollController.updateCurrentState(LongPollState.Exception)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.d(STATE_TAG, "onDestroy")
|
||||
longPollController.updateCurrentState(LongPollState.Stopped)
|
||||
updatesParser.clearListeners()
|
||||
try {
|
||||
AppSettings.edit { putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true) }
|
||||
job.cancel()
|
||||
@@ -276,4 +290,4 @@ class LongPollingService : Service() {
|
||||
}
|
||||
|
||||
private data class LongPollException(override val message: String) : Throwable()
|
||||
private data object NoAccessTokenException : Throwable()
|
||||
private class NoAccessTokenException : Throwable()
|
||||
|
||||
Reference in New Issue
Block a user