From 7c14df1824438c5775b18b601b942c78e53c59e3 Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Tue, 17 Dec 2024 21:07:22 +0300 Subject: [PATCH] release 0.1.5 (#98) * settings reorganization; implement long press on emoji button for fast text; some deprecations fixed; some typos fixed; etc * ability to use more animations (experimental); fix online friends loading; conversation avatar in messages history screen; test second tap on conversations item in bottom bar to scroll to top; etc * version up --- .../kotlin/dev/meloda/fast/MainViewModel.kt | 4 +- .../meloda/fast/presentation/MainActivity.kt | 25 +++- .../meloda/fast/presentation/MainScreen.kt | 21 ++- .../service/longpolling/LongPollingService.kt | 8 +- .../kotlin/dev/meloda/fast/KotlinAndroid.kt | 4 +- .../conversations/ConversationsRepository.kt | 6 +- .../ConversationsRepositoryImpl.kt | 41 +++++- .../dev/meloda/fast/datastore/AppSettings.kt | 95 +++++++------- .../dev/meloda/fast/datastore/SettingsKeys.kt | 37 +++--- .../dev/meloda/fast/datastore/UserSettings.kt | 17 +-- .../domain/LoadConversationsByIdUseCase.kt | 23 ++++ .../dev/meloda/fast/domain/di/DomainModule.kt | 3 + .../api/responses/ConversationsResponse.kt | 13 +- .../conversations/ConversationsService.kt | 9 +- .../conversations/ConversationsUrls.kt | 1 + .../meloda/fast/ui/components/IconButton.kt | 90 +++++++++++++ core/ui/src/main/res/values-ru/strings.xml | 2 +- core/ui/src/main/res/values/strings.xml | 4 +- .../kotlin/dev/meloda/fast/auth/AuthGraph.kt | 1 + .../meloda/fast/auth/captcha/di/CaptchaDI.kt | 2 +- .../meloda/fast/auth/login/LoginViewModel.kt | 2 +- .../meloda/fast/auth/login/di/LoginModule.kt | 2 +- .../auth/validation/di/ValidationModule.kt | 2 +- .../presentation/ChatMaterialsScreen.kt | 8 -- .../conversations/ConversationsViewModel.kt | 23 ++++ .../conversations/di/ConversationsModule.kt | 6 +- .../navigation/ConversationsNavigation.kt | 3 + .../presentation/ConversationsScreen.kt | 19 ++- .../meloda/fast/friends/FriendsViewModel.kt | 61 +++++++-- .../meloda/fast/friends/di/FriendsModule.kt | 5 +- .../languagepicker/di/LanguagePickerModule.kt | 2 +- .../MessagesHistoryViewModel.kt | 31 ++--- .../di/MessagesHistoryModule.kt | 6 +- .../model/MessagesHistoryScreenState.kt | 2 + .../presentation/MessagesHistoryScreen.kt | 111 +++++++++------- .../presentation/MessagesList.kt | 10 +- .../meloda/fast/photoviewer/di/PhotoViewDI.kt | 2 +- .../presentation/PhotoViewScreen.kt | 2 +- .../meloda/fast/profile/di/ProfileModule.kt | 2 +- .../meloda/fast/settings/SettingsViewModel.kt | 123 +++++++++--------- .../meloda/fast/settings/di/SettingsModule.kt | 2 +- .../settings/presentation/SettingsScreen.kt | 2 +- gradle/libs.versions.toml | 4 +- 43 files changed, 563 insertions(+), 273 deletions(-) create mode 100644 core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadConversationsByIdUseCase.kt create mode 100644 core/ui/src/main/kotlin/dev/meloda/fast/ui/components/IconButton.kt diff --git a/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt b/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt index f302b0ee..8670d011 100644 --- a/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt +++ b/app/src/main/kotlin/dev/meloda/fast/MainViewModel.kt @@ -209,7 +209,7 @@ class MainViewModelImpl( } longPollController.setStateToApply( - if (AppSettings.Debug.longPollInBackground) { + if (AppSettings.Experimental.longPollInBackground) { LongPollState.Background } else { LongPollState.InApp @@ -233,7 +233,7 @@ class MainViewModelImpl( } private fun disableBackgroundLongPoll() { - AppSettings.Debug.longPollInBackground = false + AppSettings.Experimental.longPollInBackground = false longPollController.setStateToApply(LongPollState.InApp) } } 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 78bbe1ec..aa576c6e 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainActivity.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.remember import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.toArgb import androidx.compose.ui.platform.LocalContext -import androidx.core.app.NotificationManagerCompat import androidx.core.content.ContextCompat import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -85,6 +84,7 @@ class MainActivity : AppCompatActivity() { ) createNotificationChannels() + requestNotificationPermissions() setContent { KoinContext { @@ -250,12 +250,11 @@ class MainActivity : AppCompatActivity() { val noCategoryName = getString(UiR.string.notification_channel_no_category_name) val noCategoryDescriptionText = getString(UiR.string.notification_channel_no_category_description) - val noCategoryImportance = NotificationManagerCompat.IMPORTANCE_HIGH val noCategoryChannel = NotificationChannel( AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED, noCategoryName, - noCategoryImportance + NotificationManager.IMPORTANCE_HIGH ).apply { description = noCategoryDescriptionText } @@ -263,12 +262,11 @@ class MainActivity : AppCompatActivity() { val longPollName = getString(UiR.string.notification_channel_long_polling_service_name) val longPollDescriptionText = getString(UiR.string.notification_channel_long_polling_service_description) - val longPollImportance = NotificationManagerCompat.IMPORTANCE_NONE val longPollChannel = NotificationChannel( AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING, longPollName, - longPollImportance + NotificationManager.IMPORTANCE_NONE ).apply { description = longPollDescriptionText } @@ -285,9 +283,18 @@ class MainActivity : AppCompatActivity() { } } + private fun requestNotificationPermissions() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + requestPermissions( + arrayOf(Manifest.permission.POST_NOTIFICATIONS), + REQUEST_NOTIFICATION_PERMISSION_CODE + ) + } + } + private fun toggleLongPollService( enable: Boolean, - inBackground: Boolean = AppSettings.Debug.longPollInBackground + inBackground: Boolean = AppSettings.Experimental.longPollInBackground ) { if (enable) { val longPollIntent = Intent(this, LongPollingService::class.java) @@ -313,7 +320,7 @@ class MainActivity : AppCompatActivity() { private fun stopServices() { toggleOnlineService(enable = false) - val asForeground = AppSettings.Debug.longPollInBackground + val asForeground = AppSettings.Experimental.longPollInBackground if (!asForeground) { toggleLongPollService(enable = false) @@ -324,4 +331,8 @@ class MainActivity : AppCompatActivity() { super.onDestroy() stopServices() } + + companion object { + private const val REQUEST_NOTIFICATION_PERMISSION_CODE = 1 + } } diff --git a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt index b2f3fa59..f44085b2 100644 --- a/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt +++ b/app/src/main/kotlin/dev/meloda/fast/presentation/MainScreen.kt @@ -47,6 +47,8 @@ import dev.meloda.fast.ui.theme.LocalBottomPadding import dev.meloda.fast.ui.theme.LocalHazeState import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList +import kotlinx.coroutines.channels.BufferOverflow +import kotlinx.coroutines.flow.MutableSharedFlow @OptIn(ExperimentalHazeMaterialsApi::class) @Composable @@ -68,6 +70,14 @@ fun MainScreen( mutableIntStateOf(1) } + val sharedFlow = remember { + MutableSharedFlow( + replay = 0, + extraBufferCapacity = 1, + onBufferOverflow = BufferOverflow.DROP_OLDEST + ) + } + Scaffold( bottomBar = { NavigationBar( @@ -98,6 +108,8 @@ fun MainScreen( inclusive = true } } + } else { + sharedFlow.tryEmit(index) } }, icon = { @@ -156,7 +168,11 @@ fun MainScreen( enterTransition = { fadeIn(animationSpec = tween(200)) }, exitTransition = { fadeOut(animationSpec = tween(200)) } ) { - navigation(startDestination = navigationItems[selectedItemIndex].route) { + navigation( + startDestination = navigationItems[selectedItemIndex].route, + enterTransition = { fadeIn(animationSpec = tween(200)) }, + exitTransition = { fadeOut(animationSpec = tween(200)) } + ) { friendsScreen( onError = onError, navController = navController, @@ -165,8 +181,9 @@ fun MainScreen( conversationsScreen( onError = onError, onConversationItemClicked = onConversationItemClicked, + onPhotoClicked = onPhotoClicked, + scrollToTopFlow = sharedFlow, navController = navController, - onPhotoClicked = onPhotoClicked ) profileScreen( onError = onError, diff --git a/app/src/main/kotlin/dev/meloda/fast/service/longpolling/LongPollingService.kt b/app/src/main/kotlin/dev/meloda/fast/service/longpolling/LongPollingService.kt index 4c4a7889..30056e51 100644 --- a/app/src/main/kotlin/dev/meloda/fast/service/longpolling/LongPollingService.kt +++ b/app/src/main/kotlin/dev/meloda/fast/service/longpolling/LongPollingService.kt @@ -76,7 +76,7 @@ class LongPollingService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (startId > 1) return START_STICKY - val inBackground = AppSettings.Debug.longPollInBackground + val inBackground = AppSettings.Experimental.longPollInBackground Log.d( STATE_TAG, @@ -258,10 +258,10 @@ class LongPollingService : Service() { super.onDestroy() } - override fun onLowMemory() { - Log.d(STATE_TAG, "onLowMemory") + override fun onTrimMemory(level: Int) { + Log.d(STATE_TAG, "onTrimMemory") longPollController.updateCurrentState(LongPollState.Stopped) - super.onLowMemory() + super.onTrimMemory(level) } companion object { diff --git a/build-logic/convention/src/main/kotlin/dev/meloda/fast/KotlinAndroid.kt b/build-logic/convention/src/main/kotlin/dev/meloda/fast/KotlinAndroid.kt index 85193c88..c0ce7a51 100644 --- a/build-logic/convention/src/main/kotlin/dev/meloda/fast/KotlinAndroid.kt +++ b/build-logic/convention/src/main/kotlin/dev/meloda/fast/KotlinAndroid.kt @@ -9,8 +9,8 @@ import org.gradle.kotlin.dsl.configure import org.gradle.kotlin.dsl.provideDelegate import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension +import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension -import org.jetbrains.kotlin.gradle.dsl.KotlinTopLevelExtension internal fun Project.configureKotlinAndroid( commonExtension: CommonExtension<*, *, *, *, *, *>, @@ -40,7 +40,7 @@ internal fun Project.configureKotlinJvm() { configureKotlin() } -private inline fun Project.configureKotlin() = configure { +private inline fun Project.configureKotlin() = configure { // Treat all Kotlin warnings as errors (disabled by default) // Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties val warningsAsErrors: String? by project diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepository.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepository.kt index bb95625d..7a9c3dd1 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepository.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepository.kt @@ -1,8 +1,8 @@ package dev.meloda.fast.data.api.conversations +import com.slack.eithernet.ApiResult import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.network.RestApiErrorDomain -import com.slack.eithernet.ApiResult interface ConversationsRepository { @@ -11,6 +11,10 @@ interface ConversationsRepository { offset: Int? ): ApiResult, RestApiErrorDomain> + suspend fun getConversationsById( + peerIds: List + ): ApiResult, RestApiErrorDomain> + suspend fun storeConversations(conversations: List) suspend fun delete(peerId: Int): ApiResult suspend fun pin(peerId: Int): ApiResult diff --git a/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepositoryImpl.kt b/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepositoryImpl.kt index bec92d4c..85d0be97 100644 --- a/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepositoryImpl.kt +++ b/core/data/src/main/kotlin/dev/meloda/fast/data/api/conversations/ConversationsRepositoryImpl.kt @@ -1,5 +1,6 @@ package dev.meloda.fast.data.api.conversations +import com.slack.eithernet.ApiResult import dev.meloda.fast.common.VkConstants import dev.meloda.fast.data.VkGroupsMap import dev.meloda.fast.data.VkMemoryCache @@ -19,7 +20,6 @@ import dev.meloda.fast.network.RestApiErrorDomain import dev.meloda.fast.network.mapApiDefault import dev.meloda.fast.network.mapApiResult import dev.meloda.fast.network.service.conversations.ConversationsService -import com.slack.eithernet.ApiResult import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext @@ -79,6 +79,45 @@ class ConversationsRepositoryImpl( ) } + override suspend fun getConversationsById( + peerIds: List + ): ApiResult, RestApiErrorDomain> = withContext(Dispatchers.IO) { + val requestParams = mapOf( + "peer_ids" to peerIds.joinToString(separator = ","), + "extended" to "1", + "fields" to VkConstants.ALL_FIELDS + ) + + conversationsService.getConversationsById(requestParams).mapApiResult( + successMapper = { apiResponse -> + val response = apiResponse.requireResponse() + + val profilesList = response.profiles.orEmpty().map(VkUserData::mapToDomain) + val groupsList = response.groups.orEmpty().map(VkGroupData::mapToDomain) + val contactsList = response.contacts.orEmpty().map(VkContactData::mapToDomain) + + val usersMap = VkUsersMap.forUsers(profilesList) + val groupsMap = VkGroupsMap.forGroups(groupsList) + + VkMemoryCache.appendUsers(profilesList) + VkMemoryCache.appendGroups(groupsList) + VkMemoryCache.appendContacts(contactsList) + + response.items.map { item -> + item.asDomain().let { conversation -> + conversation.copy( + user = usersMap.conversationUser(conversation), + group = groupsMap.conversationGroup(conversation) + ).also { VkMemoryCache[conversation.id] = it } + } + } + }, + errorMapper = { error -> + error?.toDomain() + } + ) + } + override suspend fun storeConversations(conversations: List) { conversationDao.insertAll(conversations.map(VkConversation::asEntity)) } diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt index e1f1f649..386f2365 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/AppSettings.kt @@ -89,12 +89,20 @@ object AppSettings { ) set(value) = put(SettingsKeys.KEY_USE_CONTACT_NAMES, value) - var enablePullToRefresh: Boolean + var showEmojiButton: Boolean get() = get( - SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH, - SettingsKeys.DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH + SettingsKeys.KEY_SHOW_EMOJI_BUTTON, + SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON ) - set(value) = put(SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH, value) + set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value) + + var enableHaptic: Boolean + get() = get( + SettingsKeys.KEY_ENABLE_HAPTIC, + SettingsKeys.DEFAULT_ENABLE_HAPTIC + ) + set(value) = put(SettingsKeys.KEY_ENABLE_HAPTIC, value) + } object Appearance { @@ -126,6 +134,13 @@ object AppSettings { ) set(value) = put(SettingsKeys.KEY_USE_DYNAMIC_COLORS, value) + var useSystemFont: Boolean + get() = get( + SettingsKeys.KEY_USE_SYSTEM_FONT, + SettingsKeys.DEFAULT_USE_SYSTEM_FONT + ) + set(value) = put(SettingsKeys.KEY_USE_SYSTEM_FONT, value) + var appLanguage: String get() = get( SettingsKeys.KEY_APPEARANCE_LANGUAGE, @@ -152,6 +167,36 @@ object AppSettings { set(value) = put(SettingsKeys.KEY_ACTIVITY_SEND_ONLINE_STATUS, value) } + object Experimental { + var longPollInBackground: Boolean + get() = get( + SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND, + SettingsKeys.DEFAULT_LONG_POLL_IN_BACKGROUND + ) + set(value) = put(SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND, value) + + var showTimeInActionMessages: Boolean + get() = get( + SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES, + SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES + ) + set(value) = put(SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES, value) + + var useBlur: Boolean + get() = get( + SettingsKeys.KEY_USE_BLUR, + SettingsKeys.DEFAULT_USE_BLUR + ) + set(value) = put(SettingsKeys.KEY_USE_BLUR, value) + + var moreAnimations: Boolean + get() = get( + SettingsKeys.KEY_MORE_ANIMATIONS, + SettingsKeys.DEFAULT_MORE_ANIMATIONS + ) + set(value) = put(SettingsKeys.KEY_MORE_ANIMATIONS, value) + } + object Debug { var showAlertAfterCrash: Boolean get() = get( @@ -160,41 +205,6 @@ object AppSettings { ) set(value) = put(SettingsKeys.KEY_DEBUG_SHOW_CRASH_ALERT, value) - var longPollInBackground: Boolean - get() = get( - SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND - ) - set(value) = put(SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, value) - - var useBlur: Boolean - get() = get( - SettingsKeys.KEY_APPEARANCE_USE_BLUR, - SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR - ) - set(value) = put(SettingsKeys.KEY_APPEARANCE_USE_BLUR, value) - - var showEmojiButton: Boolean - get() = get( - SettingsKeys.KEY_SHOW_EMOJI_BUTTON, - SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON - ) - set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value) - - var showTimeInActionMessages: Boolean - get() = get( - SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, - SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES - ) - set(value) = put(SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, value) - - var enableHaptic: Boolean - get() = get( - SettingsKeys.KEY_DEBUG_ENABLE_HAPTIC, - SettingsKeys.DEFAULT_DEBUG_ENABLE_HAPTIC - ) - set(value) = put(SettingsKeys.KEY_DEBUG_ENABLE_HAPTIC, value) - var networkLogLevel: LogLevel get() = get( SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, @@ -202,13 +212,6 @@ object AppSettings { ).let(LogLevel::parse) set(level) = put(SettingsKeys.KEY_DEBUG_NETWORK_LOG_LEVEL, level.value) - var useSystemFont: Boolean - get() = get( - SettingsKeys.KEY_DEBUG_USE_SYSTEM_FONT, - SettingsKeys.DEFAULT_DEBUG_USE_SYSTEM_FONT - ) - set(value) = put(SettingsKeys.KEY_DEBUG_USE_SYSTEM_FONT, value) - var showDebugCategory: Boolean get() = get( SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt index 5681017b..4d869d65 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/SettingsKeys.kt @@ -7,11 +7,9 @@ object SettingsKeys { const val KEY_ACCOUNT_LOGOUT = "account_logout" const val KEY_GENERAL = "general" - const val KEY_USE_CONTACT_NAMES = "general_use_contact_names" + const val KEY_USE_CONTACT_NAMES = "use_contact_names" const val DEFAULT_VALUE_USE_CONTACT_NAMES = false - const val KEY_ENABLE_PULL_TO_REFRESH = "general_pull_to_refresh" - const val DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH = false - const val KEY_SHOW_EMOJI_BUTTON = "general_show_emoji_button" + const val KEY_SHOW_EMOJI_BUTTON = "show_emoji_button" const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false const val KEY_APPEARANCE = "appearance" @@ -23,20 +21,20 @@ object SettingsKeys { const val DEFAULT_VALUE_APPEARANCE_AMOLED_THEME = false const val KEY_USE_DYNAMIC_COLORS = "appearance_use_dynamic_colors" const val DEFAULT_VALUE_USE_DYNAMIC_COLORS = false - const val KEY_APPEARANCE_COLOR_SCHEME = "appearance_color_scheme" - const val DEFAULT_VALUE_APPEARANCE_COLOR_SCHEME = 0 + const val KEY_COLOR_SCHEME = "appearance_color_scheme" + const val DEFAULT_COLOR_SCHEME = 0 const val KEY_APPEARANCE_LANGUAGE = "appearance_language" const val DEFAULT_APPEARANCE_LANGUAGE = "" - const val KEY_APPEARANCE_USE_BLUR = "appearance_use_blur" - const val DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR = false - const val KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES = - "appearance_show_time_in_action_messages" - const val DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES = false + const val KEY_USE_BLUR = "use_blur" + const val DEFAULT_USE_BLUR = false + const val KEY_SHOW_TIME_IN_ACTION_MESSAGES = + "show_time_in_action_messages" + const val DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES = false const val KEY_FEATURES_FAST_TEXT = "features_fast_text" const val DEFAULT_VALUE_FEATURES_FAST_TEXT = "¯\\_(ツ)_/¯" - const val KEY_FEATURES_LONG_POLL_IN_BACKGROUND = "features_lp_background" - const val DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND = false + const val KEY_LONG_POLL_IN_BACKGROUND = "lp_background" + const val DEFAULT_LONG_POLL_IN_BACKGROUND = false const val KEY_ACTIVITY_SEND_ONLINE_STATUS = "activity_send_online_status" const val DEFAULT_VALUE_KEY_ACTIVITY_SEND_ONLINE_STATUS = false @@ -44,15 +42,16 @@ object SettingsKeys { const val KEY_DEBUG_PERFORM_CRASH = "debug_perform_crash" const val KEY_DEBUG_SHOW_CRASH_ALERT = "debug_show_crash_alert" const val KEY_DEBUG_HIDE_DEBUG_LIST = "debug_hide_debug_list" - const val KEY_ENABLE_ANIMATIONS_IN_MESSAGES = "debug_enable_animations_in_messages" - const val KEY_DEBUG_ENABLE_HAPTIC = "debug_enable_haptic" - const val DEFAULT_DEBUG_ENABLE_HAPTIC = true + const val KEY_ENABLE_HAPTIC = "enable_haptic" + const val DEFAULT_ENABLE_HAPTIC = true const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level" const val DEFAULT_NETWORK_LOG_LEVEL = 0 - const val KEY_DEBUG_USE_SYSTEM_FONT = "debug_use_system_font" - const val DEFAULT_DEBUG_USE_SYSTEM_FONT = false + const val KEY_USE_SYSTEM_FONT = "use_system_font" + const val DEFAULT_USE_SYSTEM_FONT = false + const val KEY_MORE_ANIMATIONS = "more_animations" + const val DEFAULT_MORE_ANIMATIONS = false + const val KEY_SHOW_DEBUG_CATEGORY = "show_debug_category" - const val ID_DMITRY = 37610580 } diff --git a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/UserSettings.kt b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/UserSettings.kt index 22e57a24..a8585cef 100644 --- a/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/UserSettings.kt +++ b/core/datastore/src/main/kotlin/dev/meloda/fast/datastore/UserSettings.kt @@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.StateFlow interface UserSettings { val useContactNames: StateFlow - val enablePullToRefresh: StateFlow val enableMultiline: StateFlow val darkMode: StateFlow @@ -28,7 +27,6 @@ interface UserSettings { val showDebugCategory: StateFlow fun onUseContactNamesChanged(use: Boolean) - fun onEnablePullToRefreshChanged(enable: Boolean) fun onEnableMultilineChanged(enable: Boolean) fun onDarkModeChanged(mode: DarkMode) @@ -52,7 +50,6 @@ interface UserSettings { class UserSettingsImpl : UserSettings { override val useContactNames = MutableStateFlow(AppSettings.General.useContactNames) - override val enablePullToRefresh = MutableStateFlow(AppSettings.General.enablePullToRefresh) override val enableMultiline = MutableStateFlow(AppSettings.Appearance.enableMultiline) override val darkMode = MutableStateFlow(AppSettings.Appearance.darkMode) @@ -65,22 +62,18 @@ class UserSettingsImpl : UserSettings { override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus) override val showAlertAfterCrash = MutableStateFlow(AppSettings.Debug.showAlertAfterCrash) - override val longPollInBackground = MutableStateFlow(AppSettings.Debug.longPollInBackground) - override val useBlur = MutableStateFlow(AppSettings.Debug.useBlur) - override val showEmojiButton = MutableStateFlow(AppSettings.Debug.showEmojiButton) + override val longPollInBackground = MutableStateFlow(AppSettings.Experimental.longPollInBackground) + override val useBlur = MutableStateFlow(AppSettings.Experimental.useBlur) + override val showEmojiButton = MutableStateFlow(AppSettings.General.showEmojiButton) override val showTimeInActionMessages = - MutableStateFlow(AppSettings.Debug.showTimeInActionMessages) - override val useSystemFont = MutableStateFlow(AppSettings.Debug.useSystemFont) + MutableStateFlow(AppSettings.Experimental.showTimeInActionMessages) + override val useSystemFont = MutableStateFlow(AppSettings.Appearance.useSystemFont) override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory) override fun onUseContactNamesChanged(use: Boolean) { useContactNames.value = use } - override fun onEnablePullToRefreshChanged(enable: Boolean) { - enablePullToRefresh.value = enable - } - override fun onEnableMultilineChanged(enable: Boolean) { enableMultiline.value = enable } diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadConversationsByIdUseCase.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadConversationsByIdUseCase.kt new file mode 100644 index 00000000..309430ab --- /dev/null +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/LoadConversationsByIdUseCase.kt @@ -0,0 +1,23 @@ +package dev.meloda.fast.domain + +import dev.meloda.fast.data.State +import dev.meloda.fast.data.api.conversations.ConversationsRepository +import dev.meloda.fast.data.mapToState +import dev.meloda.fast.model.api.domain.VkConversation +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class LoadConversationsByIdUseCase( + private val conversationsRepository: ConversationsRepository +) { + + operator fun invoke(peerIds: List): Flow>> = flow { + emit(State.Loading) + + val newState = conversationsRepository + .getConversationsById(peerIds = peerIds) + .mapToState() + + emit(newState) + } +} diff --git a/core/domain/src/main/kotlin/dev/meloda/fast/domain/di/DomainModule.kt b/core/domain/src/main/kotlin/dev/meloda/fast/domain/di/DomainModule.kt index 9fccd341..7a8579a7 100644 --- a/core/domain/src/main/kotlin/dev/meloda/fast/domain/di/DomainModule.kt +++ b/core/domain/src/main/kotlin/dev/meloda/fast/domain/di/DomainModule.kt @@ -6,6 +6,7 @@ import dev.meloda.fast.domain.AccountUseCaseImpl import dev.meloda.fast.domain.GetCurrentAccountUseCase import dev.meloda.fast.domain.GetLocalUserByIdUseCase import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase +import dev.meloda.fast.domain.LoadConversationsByIdUseCase import dev.meloda.fast.domain.LoadUserByIdUseCase import dev.meloda.fast.domain.LoadUsersByIdsUseCase import dev.meloda.fast.domain.StoreUsersUseCase @@ -24,4 +25,6 @@ val domainModule = module { singleOf(::AccountUseCaseImpl) bind AccountUseCase::class singleOf(::GetCurrentAccountUseCase) + + singleOf(::LoadConversationsByIdUseCase) } diff --git a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/ConversationsResponse.kt b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/ConversationsResponse.kt index 1fb02ea4..927101a5 100644 --- a/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/ConversationsResponse.kt +++ b/core/model/src/main/kotlin/dev/meloda/fast/model/api/responses/ConversationsResponse.kt @@ -1,12 +1,12 @@ package dev.meloda.fast.model.api.responses +import com.squareup.moshi.Json +import com.squareup.moshi.JsonClass import dev.meloda.fast.model.api.data.VkContactData import dev.meloda.fast.model.api.data.VkConversationData import dev.meloda.fast.model.api.data.VkGroupData import dev.meloda.fast.model.api.data.VkMessageData import dev.meloda.fast.model.api.data.VkUserData -import com.squareup.moshi.Json -import com.squareup.moshi.JsonClass @JsonClass(generateAdapter = true) data class ConversationsGetResponse( @@ -18,6 +18,15 @@ data class ConversationsGetResponse( @Json(name = "contacts") val contacts: List? ) +@JsonClass(generateAdapter = true) +data class ConversationsGetByIdResponse( + @Json(name = "count") val count: Int, + @Json(name = "items") val items: List, + @Json(name = "profiles") val profiles: List?, + @Json(name = "groups") val groups: List?, + @Json(name = "contacts") val contacts: List? +) + @JsonClass(generateAdapter = true) data class ConversationsResponseItem( @Json(name = "conversation") val conversation: VkConversationData, diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsService.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsService.kt index b03277ba..e891472b 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsService.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsService.kt @@ -1,10 +1,11 @@ package dev.meloda.fast.network.service.conversations +import com.slack.eithernet.ApiResult import dev.meloda.fast.model.api.responses.ConversationsDeleteResponse +import dev.meloda.fast.model.api.responses.ConversationsGetByIdResponse import dev.meloda.fast.model.api.responses.ConversationsGetResponse import dev.meloda.fast.network.ApiResponse import dev.meloda.fast.network.RestApiError -import com.slack.eithernet.ApiResult import retrofit2.http.FieldMap import retrofit2.http.FormUrlEncoded import retrofit2.http.POST @@ -17,6 +18,12 @@ interface ConversationsService { @FieldMap params: Map ): ApiResult, RestApiError> + @FormUrlEncoded + @POST(ConversationsUrls.GET_BY_ID) + suspend fun getConversationsById( + @FieldMap params: Map + ): ApiResult, RestApiError> + @FormUrlEncoded @POST(ConversationsUrls.DELETE) suspend fun delete( diff --git a/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsUrls.kt b/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsUrls.kt index 56b9fc0e..ba130cd3 100644 --- a/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsUrls.kt +++ b/core/network/src/main/kotlin/dev/meloda/fast/network/service/conversations/ConversationsUrls.kt @@ -5,6 +5,7 @@ import dev.meloda.fast.common.AppConstants object ConversationsUrls { const val GET = "${AppConstants.URL_API}/messages.getConversations" + const val GET_BY_ID = "${AppConstants.URL_API}/messages.getConversationsById" const val DELETE = "${AppConstants.URL_API}/messages.deleteConversation" const val PIN = "${AppConstants.URL_API}/messages.pinConversation" const val UNPIN = "${AppConstants.URL_API}/messages.unpinConversation" diff --git a/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/IconButton.kt b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/IconButton.kt new file mode 100644 index 00000000..e6516beb --- /dev/null +++ b/core/ui/src/main/kotlin/dev/meloda/fast/ui/components/IconButton.kt @@ -0,0 +1,90 @@ +package dev.meloda.fast.ui.components + +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.Indication +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.IconButtonColors +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LocalContentColor +import androidx.compose.material3.LocalUseFallbackRippleImplementation +import androidx.compose.material3.minimumInteractiveComponentSize +import androidx.compose.material3.ripple +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Stable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun IconButton( + onClick: () -> Unit = {}, + onLongClick: (() -> Unit)? = null, + modifier: Modifier = Modifier, + enabled: Boolean = true, + colors: IconButtonColors = IconButtonDefaults.iconButtonColors(), + interactionSource: MutableInteractionSource? = null, + content: @Composable () -> Unit +) { + Box( + modifier = + modifier + .minimumInteractiveComponentSize() + .size(IconButtonTokens.StateLayerSize) + .clip(IconButtonTokens.StateLayerShape) + .background(color = colors.containerColor(enabled)) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + enabled = enabled, + interactionSource = interactionSource, + indication = rippleOrFallbackImplementation( + bounded = false, + radius = IconButtonTokens.StateLayerSize / 2 + ) + ), + contentAlignment = Alignment.Center + ) { + val contentColor = colors.contentColor(enabled) + CompositionLocalProvider(LocalContentColor provides contentColor, content = content) + } +} + +@Suppress("DEPRECATION_ERROR") +@OptIn(ExperimentalMaterial3Api::class) +@Composable +internal fun rippleOrFallbackImplementation( + bounded: Boolean = true, + radius: Dp = Dp.Unspecified, + color: Color = Color.Unspecified +): Indication { + return if (LocalUseFallbackRippleImplementation.current) { + rememberRipple(bounded, radius, color) + } else { + ripple(bounded, radius, color) + } +} + +internal object IconButtonTokens { + val StateLayerShape = CircleShape + val StateLayerSize = 40.0.dp +} + +@Stable +internal fun IconButtonColors.containerColor(enabled: Boolean): Color = + if (enabled) containerColor else disabledContainerColor + +@Stable +internal fun IconButtonColors.contentColor(enabled: Boolean): Color = + if (enabled) contentColor else disabledContentColor diff --git a/core/ui/src/main/res/values-ru/strings.xml b/core/ui/src/main/res/values-ru/strings.xml index 9368b131..b099304a 100644 --- a/core/ui/src/main/res/values-ru/strings.xml +++ b/core/ui/src/main/res/values-ru/strings.xml @@ -182,7 +182,7 @@ Заголовок чата и текст сообщения смогут занимать несколько строчек Фичи Fast текст - [WIP] LongPoll в фоне + LongPoll в фоне Ваши сообщения будут обновляться, даже если приложение находится в фоне Активность Быть «в сети» diff --git a/core/ui/src/main/res/values/strings.xml b/core/ui/src/main/res/values/strings.xml index 49fa7ae6..a1cbf61e 100644 --- a/core/ui/src/main/res/values/strings.xml +++ b/core/ui/src/main/res/values/strings.xml @@ -240,8 +240,8 @@ The title of the conversation and the text of the message can take up multiple lines Features Fast text - [WIP] LongPoll in background - Your messages will be updates even when app is not on the screen + LongPoll in background + Your messages will be updating even when app is not on the screen Activity Send online status Online status will be sent every five minutes diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt index 308c3bf1..d1aa7d82 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/AuthGraph.kt @@ -87,6 +87,7 @@ fun NavGraphBuilder.authNavGraph( } } +// TODO: 17.12.2024, Danil Nikolaev: check clearing backstack from main screen fun NavController.navigateToAuth(clearBackStack: Boolean = false) { val navController = this diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/di/CaptchaDI.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/di/CaptchaDI.kt index c63fce8e..3ea7203f 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/di/CaptchaDI.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/captcha/di/CaptchaDI.kt @@ -3,8 +3,8 @@ package dev.meloda.fast.auth.captcha.di import dev.meloda.fast.auth.captcha.CaptchaViewModel import dev.meloda.fast.auth.captcha.CaptchaViewModelImpl import dev.meloda.fast.auth.captcha.validation.CaptchaValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.bind import org.koin.dsl.module diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt index f8227713..9390645f 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/LoginViewModel.kt @@ -368,7 +368,7 @@ class LoginViewModelImpl( private fun startLongPoll() { longPollController.setStateToApply( - if (AppSettings.Debug.longPollInBackground) { + if (AppSettings.Experimental.longPollInBackground) { LongPollState.Background } else { LongPollState.InApp diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/di/LoginModule.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/di/LoginModule.kt index 5a149d8a..de8e3c95 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/di/LoginModule.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/login/di/LoginModule.kt @@ -4,8 +4,8 @@ import dev.meloda.fast.auth.login.LoginViewModelImpl import dev.meloda.fast.domain.OAuthUseCase import dev.meloda.fast.domain.OAuthUseCaseImpl import dev.meloda.fast.auth.login.validation.LoginValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.bind import org.koin.dsl.module diff --git a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/di/ValidationModule.kt b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/di/ValidationModule.kt index 54280c3b..a04297c2 100644 --- a/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/di/ValidationModule.kt +++ b/feature/auth/src/main/kotlin/dev/meloda/fast/auth/validation/di/ValidationModule.kt @@ -5,8 +5,8 @@ import dev.meloda.fast.domain.AuthUseCaseImpl import dev.meloda.fast.auth.validation.ValidationViewModel import dev.meloda.fast.auth.validation.ValidationViewModelImpl import dev.meloda.fast.auth.validation.validation.ValidationValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.bind import org.koin.dsl.module diff --git a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt index 1b7ed4bb..bc73daef 100644 --- a/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt +++ b/feature/chatmaterials/src/main/kotlin/dev/meloda/fast/chatmaterials/presentation/ChatMaterialsScreen.kt @@ -70,11 +70,9 @@ import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel import dev.meloda.fast.chatmaterials.ChatMaterialsViewModelImpl import dev.meloda.fast.chatmaterials.model.ChatMaterialsScreenState import dev.meloda.fast.chatmaterials.model.UiChatMaterial -import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.ui.R import dev.meloda.fast.ui.theme.LocalThemeConfig import org.koin.androidx.compose.koinViewModel -import org.koin.compose.koinInject @Composable fun ChatMaterialsRoute( @@ -82,15 +80,10 @@ fun ChatMaterialsRoute( onPhotoClicked: (url: String) -> Unit, viewModel: ChatMaterialsViewModel = koinViewModel() ) { - val userSettings: UserSettings = koinInject() - - val enablePullToRefresh by userSettings.enablePullToRefresh.collectAsStateWithLifecycle() - val screenState by viewModel.screenState.collectAsStateWithLifecycle() ChatMaterialsScreen( screenState = screenState, - enablePullToRefresh = enablePullToRefresh, onBack = onBack, onTypeChanged = viewModel::onTypeChanged, onRefreshDropdownItemClicked = viewModel::onRefresh, @@ -107,7 +100,6 @@ fun ChatMaterialsRoute( @Composable fun ChatMaterialsScreen( screenState: ChatMaterialsScreenState = ChatMaterialsScreenState.EMPTY, - enablePullToRefresh: Boolean = false, onBack: () -> Unit = {}, onTypeChanged: (String) -> Unit = {}, onRefreshDropdownItemClicked: () -> Unit = {}, diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt index f87c309a..cb575634 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/ConversationsViewModel.kt @@ -26,7 +26,9 @@ import dev.meloda.fast.model.LongPollEvent import dev.meloda.fast.model.api.domain.VkConversation import dev.meloda.fast.network.VkErrorCode import dev.meloda.fast.ui.util.ImmutableList +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow @@ -34,6 +36,7 @@ import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlin.coroutines.cancellation.CancellationException interface ConversationsViewModel { @@ -43,6 +46,7 @@ interface ConversationsViewModel { val imagesToPreload: StateFlow> val currentOffset: StateFlow val canPaginate: StateFlow + val scrollToTop: StateFlow fun onPaginationConditionsMet() @@ -63,6 +67,10 @@ interface ConversationsViewModel { fun setScrollIndex(index: Int) fun setScrollOffset(offset: Int) + + + fun setScrollToTopFlow(scrollToTopFlow: Flow) + fun onScrolledToTop() } class ConversationsViewModelImpl( @@ -78,6 +86,7 @@ class ConversationsViewModelImpl( override val imagesToPreload = MutableStateFlow>(emptyList()) override val currentOffset = MutableStateFlow(0) override val canPaginate = MutableStateFlow(false) + override val scrollToTop = MutableStateFlow(false) override fun onPaginationConditionsMet() { currentOffset.update { screenState.value.conversations.size } @@ -217,6 +226,20 @@ class ConversationsViewModelImpl( screenState.setValue { old -> old.copy(scrollOffset = offset) } } + override fun setScrollToTopFlow(scrollToTopFlow: Flow) { + scrollToTopFlow.listenValue(viewModelScope) { index -> + if (index == 1) { + scrollToTop.emit(true) + } + } + } + + override fun onScrolledToTop() { + viewModelScope.launch(Dispatchers.Main) { + scrollToTop.emit(false) + } + } + private fun hideOptions(conversationId: Int) { screenState.setValue { old -> old.copy( diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt index b7ac163e..15b4edf4 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/di/ConversationsModule.kt @@ -1,14 +1,14 @@ package dev.meloda.fast.conversations.di import dev.meloda.fast.conversations.ConversationsViewModelImpl -import dev.meloda.fast.domain.ConversationsUseCaseImpl import dev.meloda.fast.domain.ConversationsUseCase -import org.koin.androidx.viewmodel.dsl.viewModelOf +import dev.meloda.fast.domain.ConversationsUseCaseImpl import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.bind import org.koin.dsl.module val conversationsModule = module { - singleOf(::ConversationsUseCaseImpl) bind dev.meloda.fast.domain.ConversationsUseCase::class + singleOf(::ConversationsUseCaseImpl) bind ConversationsUseCase::class viewModelOf(::ConversationsViewModelImpl) } diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt index 66805ed8..ccc91b6c 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/navigation/ConversationsNavigation.kt @@ -8,6 +8,7 @@ import dev.meloda.fast.conversations.ConversationsViewModelImpl import dev.meloda.fast.conversations.presentation.ConversationsRoute import dev.meloda.fast.model.BaseError import dev.meloda.fast.ui.extensions.sharedViewModel +import kotlinx.coroutines.flow.Flow import kotlinx.serialization.Serializable @Serializable @@ -17,11 +18,13 @@ fun NavGraphBuilder.conversationsScreen( onError: (BaseError) -> Unit, onConversationItemClicked: (id: Int) -> Unit, onPhotoClicked: (url: String) -> Unit, + scrollToTopFlow: Flow, navController: NavController, ) { composable { val viewModel: ConversationsViewModel = it.sharedViewModel(navController = navController) + viewModel.setScrollToTopFlow(scrollToTopFlow) ConversationsRoute( onError = onError, diff --git a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt index 841f19c4..dbf270da 100644 --- a/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt +++ b/feature/conversations/src/main/kotlin/dev/meloda/fast/conversations/presentation/ConversationsScreen.kt @@ -101,6 +101,7 @@ fun ConversationsRoute( val screenState by viewModel.screenState.collectAsStateWithLifecycle() val baseError by viewModel.baseError.collectAsStateWithLifecycle() val canPaginate by viewModel.canPaginate.collectAsStateWithLifecycle() + val isNeedToScrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle() val imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle() LaunchedEffect(imagesToPreload) { @@ -129,7 +130,9 @@ fun ConversationsRoute( onRefresh = viewModel::onRefresh, onConversationPhotoClicked = onConversationPhotoClicked, setScrollIndex = viewModel::setScrollIndex, - setScrollOffset = viewModel::setScrollOffset + setScrollOffset = viewModel::setScrollOffset, + isNeedToScrollToTop = isNeedToScrollToTop, + onScrolledToTop = viewModel::onScrolledToTop ) HandleDialogs( @@ -156,7 +159,9 @@ fun ConversationsScreen( onRefresh: () -> Unit = {}, onConversationPhotoClicked: (url: String) -> Unit = {}, setScrollIndex: (Int) -> Unit = {}, - setScrollOffset: (Int) -> Unit = {} + setScrollOffset: (Int) -> Unit = {}, + isNeedToScrollToTop: Boolean = false, + onScrolledToTop: () -> Unit = {} ) { val view = LocalView.current val currentTheme = LocalThemeConfig.current @@ -170,6 +175,14 @@ fun ConversationsScreen( initialFirstVisibleItemScrollOffset = screenState.scrollOffset ) + LaunchedEffect(isNeedToScrollToTop) { + if (isNeedToScrollToTop) { + listState.scrollToItem(0) + onScrolledToTop() + + } + } + LaunchedEffect(listState) { snapshotFlow { listState.firstVisibleItemIndex } .debounce(500L) @@ -310,7 +323,7 @@ fun ConversationsScreen( ) { FloatingActionButton( onClick = { - if (AppSettings.Debug.enableHaptic) { + if (AppSettings.General.enableHaptic) { view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) } scope.launch { diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt index fc9db494..c01de124 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/FriendsViewModel.kt @@ -8,6 +8,7 @@ import dev.meloda.fast.data.State import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.domain.FriendsUseCase +import dev.meloda.fast.domain.LoadUsersByIdsUseCase import dev.meloda.fast.friends.model.FriendsScreenState import dev.meloda.fast.friends.util.asPresentation import dev.meloda.fast.model.BaseError @@ -42,7 +43,8 @@ interface FriendsViewModel { class FriendsViewModelImpl( private val friendsUseCase: FriendsUseCase, - private val userSettings: UserSettings + private val userSettings: UserSettings, + private val loadUsersByIdsUseCase: LoadUsersByIdsUseCase ) : ViewModel(), FriendsViewModel { override val screenState = MutableStateFlow(FriendsScreenState.EMPTY) @@ -94,6 +96,49 @@ class FriendsViewModelImpl( } private fun loadFriends(offset: Int = currentOffset.value) { + friendsUseCase.getOnlineFriends(null, null) + .listenValue(viewModelScope) { state -> + state.processState( + error = { error -> + if (error is State.Error.ApiError) { + when (error.errorCode) { + VkErrorCode.USER_AUTHORIZATION_FAILED -> { + baseError.setValue { BaseError.SessionExpired } + } + + else -> Unit + } + } + }, + success = { userIds -> + loadUsersByIdsUseCase(userIds = userIds) + .listenValue(viewModelScope) { state -> + state.processState( + error = { error -> + if (error is State.Error.ApiError) { + when (error.errorCode) { + VkErrorCode.USER_AUTHORIZATION_FAILED -> { + baseError.setValue { BaseError.SessionExpired } + } + + else -> Unit + } + } + }, + success = { onlineFriends -> + screenState.setValue { old -> + old.copy( + onlineFriends = onlineFriends.map { + it.asPresentation(userSettings.useContactNames.value) + } + ) + } + } + ) + } + } + ) + } friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset) .listenValue(viewModelScope) { state -> state.processState( @@ -125,10 +170,6 @@ class FriendsViewModelImpl( it.asPresentation(userSettings.useContactNames.value) } - val loadedOnlineFriends = loadedFriends.filter { - it.onlineStatus.isOnline() - } - val newState = screenState.value.copy( isPaginationExhausted = paginationExhausted ) @@ -136,18 +177,12 @@ class FriendsViewModelImpl( if (offset == 0) { friends.emit(response) screenState.setValue { - newState.copy( - friends = loadedFriends, - onlineFriends = loadedOnlineFriends - ) + newState.copy(friends = loadedFriends) } } else { friends.emit(friends.value.plus(response)) screenState.setValue { - newState.copy( - friends = newState.friends.plus(loadedFriends), - onlineFriends = newState.onlineFriends.plus(loadedOnlineFriends) - ) + newState.copy(friends = newState.friends.plus(loadedFriends)) } } } diff --git a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/di/FriendsModule.kt b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/di/FriendsModule.kt index d3e56173..c19b8cd4 100644 --- a/feature/friends/src/main/kotlin/dev/meloda/fast/friends/di/FriendsModule.kt +++ b/feature/friends/src/main/kotlin/dev/meloda/fast/friends/di/FriendsModule.kt @@ -3,13 +3,12 @@ package dev.meloda.fast.friends.di import dev.meloda.fast.domain.FriendsUseCase import dev.meloda.fast.friends.FriendsViewModelImpl import dev.meloda.fast.domain.FriendsUseCaseImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.bind import org.koin.dsl.module val friendsModule = module { - singleOf(::FriendsUseCaseImpl) bind dev.meloda.fast.domain.FriendsUseCase::class - + singleOf(::FriendsUseCaseImpl) bind FriendsUseCase::class viewModelOf(::FriendsViewModelImpl) } diff --git a/feature/languagepicker/src/main/kotlin/dev/meloda/fast/languagepicker/di/LanguagePickerModule.kt b/feature/languagepicker/src/main/kotlin/dev/meloda/fast/languagepicker/di/LanguagePickerModule.kt index 669cf33a..1aad5644 100644 --- a/feature/languagepicker/src/main/kotlin/dev/meloda/fast/languagepicker/di/LanguagePickerModule.kt +++ b/feature/languagepicker/src/main/kotlin/dev/meloda/fast/languagepicker/di/LanguagePickerModule.kt @@ -1,7 +1,7 @@ package dev.meloda.fast.languagepicker.di import dev.meloda.fast.languagepicker.LanguagePickerViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val languagePickerModule = module { diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt index f2004a86..c0f542d5 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/MessagesHistoryViewModel.kt @@ -1,15 +1,15 @@ package dev.meloda.fast.messageshistory -import android.content.SharedPreferences import android.util.Log +import androidx.compose.ui.text.TextRange import androidx.compose.ui.text.input.TextFieldValue -import androidx.core.content.edit import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.conena.nanokt.collections.indexOfFirstOrNull import com.conena.nanokt.collections.indexOfOrNull import com.conena.nanokt.text.isEmptyOrBlank +import com.conena.nanokt.text.isNotEmptyOrBlank import dev.meloda.fast.common.extensions.listenValue import dev.meloda.fast.common.extensions.setValue import dev.meloda.fast.common.provider.ResourceProvider @@ -17,9 +17,9 @@ import dev.meloda.fast.data.UserConfig import dev.meloda.fast.data.VkMemoryCache import dev.meloda.fast.data.processState import dev.meloda.fast.datastore.AppSettings -import dev.meloda.fast.datastore.SettingsKeys import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.domain.ConversationsUseCase +import dev.meloda.fast.domain.LoadConversationsByIdUseCase import dev.meloda.fast.domain.LongPollUpdatesParser import dev.meloda.fast.domain.MessagesUseCase import dev.meloda.fast.messageshistory.model.ActionMode @@ -54,19 +54,18 @@ interface MessagesHistoryViewModel { fun onRefresh() fun onAttachmentButtonClicked() fun onMessageInputChanged(newText: TextFieldValue) - fun onEmojiButtonClicked() + fun onEmojiButtonLongClicked() fun onActionButtonClicked() fun onPaginationConditionsMet() - fun onToggleAnimationsDropdownItemClicked(enableAnimations: Boolean) } class MessagesHistoryViewModelImpl( private val messagesUseCase: MessagesUseCase, private val conversationsUseCase: ConversationsUseCase, - private val preferences: SharedPreferences, private val resourceProvider: ResourceProvider, private val userSettings: UserSettings, + private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase, updatesParser: LongPollUpdatesParser, savedStateHandle: SavedStateHandle ) : MessagesHistoryViewModel, ViewModel() { @@ -123,8 +122,15 @@ class MessagesHistoryViewModelImpl( screenState.setValue { old -> old.copy(message = newText) } } - override fun onEmojiButtonClicked() { - + override fun onEmojiButtonLongClicked() { + AppSettings.Features.fastText.takeIf { it.isNotEmptyOrBlank() }?.let { text -> + screenState.setValue { old -> + val newText = "${old.message.text}$text" + old.copy( + message = TextFieldValue(text = newText, selection = TextRange(newText.length)) + ) + } + } } override fun onActionButtonClicked() { @@ -150,15 +156,6 @@ class MessagesHistoryViewModelImpl( loadMessagesHistory() } - override fun onToggleAnimationsDropdownItemClicked(enableAnimations: Boolean) { - preferences.edit { - putBoolean( - SettingsKeys.KEY_ENABLE_ANIMATIONS_IN_MESSAGES, - enableAnimations - ) - } - } - private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) { val message = event.message diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/di/MessagesHistoryModule.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/di/MessagesHistoryModule.kt index e04ba4d4..deba32e1 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/di/MessagesHistoryModule.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/di/MessagesHistoryModule.kt @@ -1,17 +1,17 @@ package dev.meloda.fast.messageshistory.di import dev.meloda.fast.domain.MessagesUseCase +import dev.meloda.fast.domain.MessagesUseCaseImpl import dev.meloda.fast.messageshistory.MessagesHistoryViewModel import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl -import dev.meloda.fast.domain.MessagesUseCaseImpl import dev.meloda.fast.messageshistory.validation.MessagesHistoryValidator -import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.core.module.dsl.singleOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.bind import org.koin.dsl.module val messagesHistoryModule = module { - singleOf(::MessagesUseCaseImpl) bind dev.meloda.fast.domain.MessagesUseCase::class + singleOf(::MessagesUseCaseImpl) bind MessagesUseCase::class singleOf(::MessagesHistoryValidator) viewModelOf(::MessagesHistoryViewModelImpl) bind MessagesHistoryViewModel::class } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt index 349f4301..a8312f0a 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/model/MessagesHistoryScreenState.kt @@ -18,6 +18,7 @@ data class MessagesHistoryScreenState( val isPaginating: Boolean, val isPaginationExhausted: Boolean, val actionMode: ActionMode, + val chatImageUrl: String? ) { companion object { @@ -33,6 +34,7 @@ data class MessagesHistoryScreenState( isPaginating = false, isPaginationExhausted = false, actionMode = ActionMode.Record, + chatImageUrl = null ) } } diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt index c5b8764b..9f2d2fe1 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesHistoryScreen.kt @@ -1,10 +1,12 @@ package dev.meloda.fast.messageshistory.presentation import android.content.SharedPreferences +import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.animateFloatAsState import androidx.compose.animation.core.tween +import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box @@ -23,9 +25,11 @@ import androidx.compose.foundation.layout.imeNestedScroll import androidx.compose.foundation.layout.imePadding import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars import androidx.compose.foundation.layout.width import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.automirrored.rounded.ArrowBack @@ -37,7 +41,6 @@ import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.LinearProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold @@ -60,6 +63,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.rotate import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalView @@ -72,12 +76,12 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.core.view.HapticFeedbackConstantsCompat import androidx.lifecycle.compose.collectAsStateWithLifecycle +import coil.compose.AsyncImage import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeChild import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.meloda.fast.datastore.AppSettings -import dev.meloda.fast.datastore.SettingsKeys import dev.meloda.fast.datastore.UserSettings import dev.meloda.fast.messageshistory.MessagesHistoryViewModel import dev.meloda.fast.messageshistory.MessagesHistoryViewModelImpl @@ -86,8 +90,10 @@ import dev.meloda.fast.messageshistory.model.MessagesHistoryScreenState import dev.meloda.fast.messageshistory.util.firstMessage import dev.meloda.fast.messageshistory.util.indexOfMessageByCmId import dev.meloda.fast.model.BaseError +import dev.meloda.fast.ui.components.IconButton import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList +import dev.meloda.fast.ui.util.getImage import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject @@ -115,11 +121,11 @@ fun MessagesHistoryRoute( onBack = onBack, onChatMaterialsDropdownItemClicked = onChatMaterialsDropdownItemClicked, onRefreshDropdownItemClicked = viewModel::onRefresh, - onToggleAnimationsDropdownItemClicked = viewModel::onToggleAnimationsDropdownItemClicked, onPaginationConditionsMet = viewModel::onPaginationConditionsMet, onMessageInputChanged = viewModel::onMessageInputChanged, onAttachmentButtonClicked = viewModel::onAttachmentButtonClicked, - onActionButtonClicked = viewModel::onActionButtonClicked + onActionButtonClicked = viewModel::onActionButtonClicked, + onEmojiButtonLongClicked = viewModel::onEmojiButtonLongClicked ) } @@ -141,7 +147,8 @@ fun MessagesHistoryScreen( onPaginationConditionsMet: () -> Unit = {}, onMessageInputChanged: (TextFieldValue) -> Unit = {}, onAttachmentButtonClicked: () -> Unit = {}, - onActionButtonClicked: () -> Unit = {} + onActionButtonClicked: () -> Unit = {}, + onEmojiButtonLongClicked: () -> Unit = {} ) { val view = LocalView.current @@ -172,15 +179,6 @@ fun MessagesHistoryScreen( val hazeState = remember { HazeState() } - var animationsEnabled by remember { - mutableStateOf( - preferences.getBoolean( - SettingsKeys.KEY_ENABLE_ANIMATIONS_IN_MESSAGES, - false - ) - ) - } - val toolbarColorAlpha by animateFloatAsState( targetValue = if (!listState.canScrollForward) 1f else 0f, label = "toolbarColorAlpha", @@ -210,14 +208,42 @@ fun MessagesHistoryScreen( ) .fillMaxWidth(), title = { - Text( - text = - if (screenState.isLoading) stringResource(id = UiR.string.title_loading) - else screenState.title, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.headlineSmall - ) + Row( + modifier = Modifier + .weight(1f), + verticalAlignment = Alignment.CenterVertically + ) { + val avatar = screenState.avatar.getImage() + if (avatar is Painter) { + Image( + painter = avatar, + contentDescription = null, + modifier = Modifier + .size(36.dp) + .clip(CircleShape) + ) + } else { + AsyncImage( + model = screenState.avatar.getImage(), + contentDescription = "Profile Image", + modifier = Modifier + .size(36.dp) + .clip(CircleShape), + placeholder = painterResource(id = UiR.drawable.ic_account_circle_cut), + ) + } + + Spacer(modifier = Modifier.width(12.dp)) + + Text( + text = + if (screenState.isLoading) stringResource(id = UiR.string.title_loading) + else screenState.title, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.headlineSmall + ) + } }, navigationIcon = { IconButton(onClick = onBack) { @@ -280,31 +306,19 @@ fun MessagesHistoryScreen( ) } ) - - if (preferences.getBoolean( - SettingsKeys.KEY_SHOW_DEBUG_CATEGORY, - false - ) - ) { - HorizontalDivider() - - DropdownMenuItem( - text = { - Text(text = if (animationsEnabled) "Disable animations" else "Enable animations") - }, - onClick = { - dropDownMenuExpanded = false - animationsEnabled = !animationsEnabled - onToggleAnimationsDropdownItemClicked(animationsEnabled) - } - ) - } } } ) - if (screenState.isLoading && screenState.messages.isNotEmpty()) { + + val showHorizontalProgressBar by remember(screenState) { + derivedStateOf { screenState.isLoading && screenState.messages.isNotEmpty() } + } + if (showHorizontalProgressBar) { LinearProgressIndicator(modifier = Modifier.fillMaxWidth()) } + AnimatedVisibility(!showHorizontalProgressBar) { + HorizontalDivider() + } } } ) { padding -> @@ -320,7 +334,6 @@ fun MessagesHistoryScreen( listState = listState, immutableMessages = ImmutableList.copyOf(screenState.messages), isPaginating = screenState.isPaginating, - enableAnimations = animationsEnabled, messageBarHeight = messageBarHeight, onRequestScrollToCmId = { cmId -> coroutineScope.launch { @@ -371,7 +384,7 @@ fun MessagesHistoryScreen( Column(verticalArrangement = Arrangement.Bottom) { IconButton( onClick = { - if (AppSettings.Debug.enableHaptic) { + if (AppSettings.General.enableHaptic) { view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) } scope.launch { @@ -389,6 +402,12 @@ fun MessagesHistoryScreen( } } }, + onLongClick = { + if (AppSettings.General.enableHaptic) { + view.performHapticFeedback(HapticFeedbackConstantsCompat.LONG_PRESS) + } + onEmojiButtonLongClicked() + }, modifier = Modifier.rotate(rotation.value) ) { Icon( @@ -427,7 +446,7 @@ fun MessagesHistoryScreen( Column(verticalArrangement = Arrangement.Bottom) { IconButton( onClick = { - if (AppSettings.Debug.enableHaptic) { + if (AppSettings.General.enableHaptic) { view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) } scope.launch { @@ -463,7 +482,7 @@ fun MessagesHistoryScreen( IconButton( onClick = { if (screenState.actionMode == ActionMode.Record) { - if (AppSettings.Debug.enableHaptic) { + if (AppSettings.General.enableHaptic) { view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT) } scope.launch { diff --git a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt index e2dd3426..b329eeee 100644 --- a/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt +++ b/feature/messageshistory/src/main/kotlin/dev/meloda/fast/messageshistory/presentation/MessagesList.kt @@ -13,12 +13,14 @@ import androidx.compose.foundation.lazy.LazyListState import androidx.compose.foundation.lazy.items import androidx.compose.material3.CircularProgressIndicator import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.haze +import dev.meloda.fast.datastore.AppSettings import dev.meloda.fast.messageshistory.model.UiItem import dev.meloda.fast.ui.theme.LocalThemeConfig import dev.meloda.fast.ui.util.ImmutableList @@ -30,11 +32,15 @@ fun MessagesList( listState: LazyListState, immutableMessages: ImmutableList, isPaginating: Boolean, - enableAnimations: Boolean, messageBarHeight: Dp, onRequestScrollToCmId: (cmId: Int) -> Unit = {} ) { - val messages = immutableMessages.toList() + val enableAnimations = remember { + AppSettings.Experimental.moreAnimations + } + val messages = remember(immutableMessages) { + immutableMessages.toList() + } val currentTheme = LocalThemeConfig.current LazyColumn( diff --git a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/di/PhotoViewDI.kt b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/di/PhotoViewDI.kt index 9da0cec5..4bbaca21 100644 --- a/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/di/PhotoViewDI.kt +++ b/feature/photoviewer/src/main/kotlin/dev/meloda/fast/photoviewer/di/PhotoViewDI.kt @@ -2,7 +2,7 @@ package dev.meloda.fast.photoviewer.di import dev.meloda.fast.photoviewer.PhotoViewViewModel import dev.meloda.fast.photoviewer.PhotoViewViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.bind import org.koin.dsl.module 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 11a12915..a756183e 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 @@ -123,7 +123,7 @@ fun TopBar( } }, actions = { -// IconButton( +// IconButton.kt( // onClick = { dropdownMenuShown = true } // ) { // Icon( diff --git a/feature/profile/src/main/kotlin/dev/meloda/fast/profile/di/ProfileModule.kt b/feature/profile/src/main/kotlin/dev/meloda/fast/profile/di/ProfileModule.kt index f4383c12..f4903930 100644 --- a/feature/profile/src/main/kotlin/dev/meloda/fast/profile/di/ProfileModule.kt +++ b/feature/profile/src/main/kotlin/dev/meloda/fast/profile/di/ProfileModule.kt @@ -1,7 +1,7 @@ package dev.meloda.fast.profile.di import dev.meloda.fast.profile.ProfileViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module val profileModule = module { diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt index c2dde53d..00f3028b 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/SettingsViewModel.kt @@ -154,13 +154,6 @@ class SettingsViewModelImpl( userSettings.onUseContactNamesChanged(isUsing) } - SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH -> { - val enable = - newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH - userSettings.onEnablePullToRefreshChanged(enable) - } - - SettingsKeys.KEY_APPEARANCE_MULTILINE -> { val isUsing = newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_MULTILINE userSettings.onEnableMultilineChanged(isUsing) @@ -206,9 +199,9 @@ class SettingsViewModelImpl( userSettings.onShowAlertAfterCrashChanged(show) } - SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND -> { + SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND -> { val inBackground = newValue as? Boolean - ?: SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND + ?: SettingsKeys.DEFAULT_LONG_POLL_IN_BACKGROUND userSettings.onLongPollInBackgroundChanged(inBackground) longPollController.setStateToApply( @@ -221,9 +214,9 @@ class SettingsViewModelImpl( ) } - SettingsKeys.KEY_APPEARANCE_USE_BLUR -> { + SettingsKeys.KEY_USE_BLUR -> { val isUsing = - newValue as? Boolean ?: SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR + newValue as? Boolean ?: SettingsKeys.DEFAULT_USE_BLUR userSettings.onUseBlurChanged(isUsing) } @@ -232,14 +225,14 @@ class SettingsViewModelImpl( userSettings.onShowEmojiButtonChanged(show) } - SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES -> { + SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES -> { val show = newValue as? Boolean - ?: SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES + ?: SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES userSettings.onShowTimeInActionMessagesChanged(show) } - SettingsKeys.KEY_DEBUG_USE_SYSTEM_FONT -> { - val use = newValue as? Boolean ?: SettingsKeys.DEFAULT_DEBUG_USE_SYSTEM_FONT + SettingsKeys.KEY_USE_SYSTEM_FONT -> { + val use = newValue as? Boolean ?: SettingsKeys.DEFAULT_USE_SYSTEM_FONT userSettings.onUseSystemFontChanged(use) } @@ -283,10 +276,16 @@ class SettingsViewModelImpl( text = UiText.Resource(UiR.string.settings_general_contact_names_summary), defaultValue = SettingsKeys.DEFAULT_VALUE_USE_CONTACT_NAMES ) - val generalEnablePullToRefresh = SettingsItem.Switch( - key = SettingsKeys.KEY_ENABLE_PULL_TO_REFRESH, - defaultValue = SettingsKeys.DEFAULT_VALUE_ENABLE_PULL_TO_REFRESH, - title = UiText.Resource(UiR.string.settings_general_enable_pull_to_refresh_title) + val generalShowEmojiButton = SettingsItem.Switch( + key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON, + title = UiText.Simple("Show emoji button"), + text = UiText.Simple("Show emoji button in chat panel"), + defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON + ) + val generalEnableHaptic = SettingsItem.Switch( + key = SettingsKeys.KEY_ENABLE_HAPTIC, + defaultValue = SettingsKeys.DEFAULT_ENABLE_HAPTIC, + title = UiText.Simple("Enable haptic") ) val appearanceTitle = SettingsItem.Title( @@ -340,7 +339,11 @@ class SettingsViewModelImpl( text = UiText.Resource(UiR.string.settings_dynamic_colors_description), defaultValue = SettingsKeys.DEFAULT_VALUE_USE_DYNAMIC_COLORS ) - + val appearanceUseSystemFont = SettingsItem.Switch( + key = SettingsKeys.KEY_USE_SYSTEM_FONT, + defaultValue = SettingsKeys.DEFAULT_USE_SYSTEM_FONT, + title = UiText.Simple("Use system font") + ) val appearanceLanguage = SettingsItem.TitleText( key = SettingsKeys.KEY_APPEARANCE_LANGUAGE, title = UiText.Resource(UiR.string.settings_application_language), @@ -374,6 +377,34 @@ class SettingsViewModelImpl( text = UiText.Resource(UiR.string.settings_activity_send_online_summary) ) + val experimentalTitle = SettingsItem.Title( + key = "experimental", + title = UiText.Simple("Experimental - VERY unstable") + ) + val experimentalLongPollBackground = SettingsItem.Switch( + key = SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND, + defaultValue = SettingsKeys.DEFAULT_LONG_POLL_IN_BACKGROUND, + title = UiText.Resource(UiR.string.settings_features_long_poll_in_background_title), + text = UiText.Resource(UiR.string.settings_features_long_poll_in_background_summary) + ) + val experimentalShowTimeInActionMessages = SettingsItem.Switch( + key = SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES, + defaultValue = SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES, + title = UiText.Simple("Show time in action messages") + ) + val experimentalUseBlur = SettingsItem.Switch( + key = SettingsKeys.KEY_USE_BLUR, + defaultValue = SettingsKeys.DEFAULT_USE_BLUR, + title = UiText.Simple("Use blur"), + text = UiText.Simple("Adds blur wherever possible\nWorks on android 12 and newer"), + ) + val enableAnimations = SettingsItem.Switch( + key = SettingsKeys.KEY_MORE_ANIMATIONS, + defaultValue = SettingsKeys.DEFAULT_MORE_ANIMATIONS, + title = UiText.Simple("More animations"), + text = UiText.Simple("Use animations wherever possible") + ) + val debugTitle = SettingsItem.Title( key = "debug", title = UiText.Resource(UiR.string.settings_debug_title) @@ -389,34 +420,6 @@ class SettingsViewModelImpl( title = UiText.Simple("Show alert after crash"), text = UiText.Simple("Shows alert dialog with stacktrace after app crashed\n(it will be not shown if you perform crash manually)") ) - val debugLongPollBackground = SettingsItem.Switch( - key = SettingsKeys.KEY_FEATURES_LONG_POLL_IN_BACKGROUND, - defaultValue = SettingsKeys.DEFAULT_VALUE_FEATURES_LONG_POLL_IN_BACKGROUND, - title = UiText.Resource(UiR.string.settings_features_long_poll_in_background_title), - text = UiText.Resource(UiR.string.settings_features_long_poll_in_background_summary) - ) - val debugUseBlur = SettingsItem.Switch( - key = SettingsKeys.KEY_APPEARANCE_USE_BLUR, - defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_APPEARANCE_USE_BLUR, - title = UiText.Simple("[WIP] Use blur"), - text = UiText.Simple("Adds blur wherever possible\nWorks on android 12 and newer"), - ) - val debugShowEmojiButton = SettingsItem.Switch( - key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON, - title = UiText.Simple("Show emoji button"), - text = UiText.Simple("Show emoji button in chat panel"), - defaultValue = SettingsKeys.DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON - ) - val debugShowTimeInActionMessages = SettingsItem.Switch( - key = SettingsKeys.KEY_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, - defaultValue = SettingsKeys.DEFAULT_VALUE_APPEARANCE_SHOW_TIME_IN_ACTION_MESSAGES, - title = UiText.Simple("Show time in action messages") - ) - val debugEnableHaptic = SettingsItem.Switch( - key = SettingsKeys.KEY_DEBUG_ENABLE_HAPTIC, - defaultValue = SettingsKeys.DEFAULT_DEBUG_ENABLE_HAPTIC, - title = UiText.Simple("Enable haptic") - ) val logLevelValues = listOf( LogLevel.NONE to UiText.Simple("None"), @@ -440,12 +443,6 @@ class SettingsViewModelImpl( } } - val debugUseSystemFont = SettingsItem.Switch( - key = SettingsKeys.KEY_DEBUG_USE_SYSTEM_FONT, - defaultValue = SettingsKeys.DEFAULT_DEBUG_USE_SYSTEM_FONT, - title = UiText.Simple("Use system font") - ) - val debugHideDebugList = SettingsItem.TitleText( key = SettingsKeys.KEY_DEBUG_HIDE_DEBUG_LIST, title = UiText.Simple("Hide debug list") @@ -458,7 +455,8 @@ class SettingsViewModelImpl( val generalList = listOf( generalTitle, generalUseContactNames, - generalEnablePullToRefresh + generalShowEmojiButton, + generalEnableHaptic ) val appearanceList = listOf( appearanceTitle, @@ -466,6 +464,7 @@ class SettingsViewModelImpl( appearanceDarkTheme, appearanceUseAmoledDarkTheme, appearanceUseDynamicColors, + appearanceUseSystemFont, appearanceLanguage ) val featuresList = listOf( @@ -476,18 +475,19 @@ class SettingsViewModelImpl( activityTitle, visibilitySendOnlineStatus, ) + val experimentalList = listOf( + experimentalTitle, + experimentalLongPollBackground, + experimentalShowTimeInActionMessages, + experimentalUseBlur, + enableAnimations + ) val debugList = mutableListOf>() listOf( debugTitle, debugPerformCrash, debugShowCrashAlert, - debugLongPollBackground, - debugUseBlur, - debugShowEmojiButton, - debugShowTimeInActionMessages, - debugEnableHaptic, debugNetworkLogLevel, - debugUseSystemFont ).forEach(debugList::add) debugList += debugHideDebugList @@ -499,6 +499,7 @@ class SettingsViewModelImpl( appearanceList, featuresList, visibilityList, + experimentalList, debugList, ).forEach(settingsList::addAll) diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/di/SettingsModule.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/di/SettingsModule.kt index 1cfb19d0..2e104d11 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/di/SettingsModule.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/di/SettingsModule.kt @@ -2,7 +2,7 @@ package dev.meloda.fast.settings.di import dev.meloda.fast.settings.SettingsViewModel import dev.meloda.fast.settings.SettingsViewModelImpl -import org.koin.androidx.viewmodel.dsl.viewModelOf +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.bind import org.koin.dsl.module diff --git a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt index 80486577..c177ad53 100644 --- a/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt +++ b/feature/settings/src/main/kotlin/dev/meloda/fast/settings/presentation/SettingsScreen.kt @@ -113,7 +113,7 @@ fun SettingsScreen( LaunchedEffect(hapticType) { if (hapticType != null) { - if (AppSettings.Debug.enableHaptic) { + if (AppSettings.General.enableHaptic) { view.performHapticFeedback(hapticType.getHaptic()) } onHapticPerformed() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 04d53238..379994a3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,8 +2,8 @@ minSdk = "23" targetSdk = "35" compileSdk = "35" -versionCode = "7" -versionName = "0.1.4" +versionCode = "8" +versionName = "0.1.5" agp = "8.7.3" converterMoshi = "2.11.0"