3 Commits

Author SHA1 Message Date
melod1n 0682d6c42c version up 2024-12-17 20:52:22 +03:00
melod1n 6a69f28256 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
2024-12-17 20:51:02 +03:00
melod1n 85cda2065e settings reorganization;
implement long press on emoji button for fast text;
some deprecations fixed;
some typos fixed;
etc
2024-12-17 12:53:02 +03:00
31 changed files with 292 additions and 501 deletions
+2 -2
View File
@@ -7,10 +7,10 @@ plugins {
}
android {
namespace = "dev.meloda.fastvk"
namespace = "dev.meloda.fast"
defaultConfig {
applicationId = "dev.meloda.fastvk"
applicationId = "dev.meloda.fast"
versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get()
+3 -3
View File
@@ -22,7 +22,7 @@
tools:targetApi="tiramisu">
<activity
android:name="dev.meloda.fast.presentation.MainActivity"
android:name=".presentation.MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
@@ -38,13 +38,13 @@
</activity>
<service
android:name="dev.meloda.fast.service.longpolling.LongPollingService"
android:name=".service.longpolling.LongPollingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name="dev.meloda.fast.service.OnlineService"
android:name=".service.OnlineService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
@@ -86,8 +86,6 @@ class MainViewModelImpl(
BaseError.SessionExpired -> {
isNeedToReplaceWithAuth.update { true }
}
is BaseError.SimpleError -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui
}
}
@@ -26,8 +26,8 @@ import dev.meloda.fast.provider.ApiLanguageProvider
import dev.meloda.fast.service.longpolling.di.longPollModule
import dev.meloda.fast.settings.di.settingsModule
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.module.dsl.singleOf
import org.koin.core.module.dsl.viewModelOf
import org.koin.core.qualifier.qualifier
import org.koin.dsl.bind
import org.koin.dsl.module
@@ -61,7 +61,7 @@ val applicationModule = module {
qualifier = qualifier("main")
}
single<ImageLoader> {
single {
ImageLoader.Builder(get())
.crossfade(true)
.build()
@@ -95,6 +95,11 @@ class OnlineService : Service() {
}.also { coroutine -> coroutine.invokeOnCompletion { onlineJob = null } }
}
override fun onLowMemory() {
Log.d(STATE_TAG, "onLowMemory")
super.onLowMemory()
}
override fun onDestroy() {
Log.d(STATE_TAG, "onDestroy")
@@ -1,5 +1,7 @@
package dev.meloda.fast.common.extensions
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -86,7 +86,7 @@ object AndroidUtils {
action = if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
Settings.ACTION_SECURITY_SETTINGS
} else {
data = Uri.parse("package:dev.meloda.fastvk")
data = Uri.parse("package:dev.meloda.fast")
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
}
})
@@ -20,20 +20,18 @@ sealed class State<out T> {
data object ConnectionError : Error()
data object UnknownError : Error()
data object Unknown : Error()
data object InternalError : Error()
data class OAuthError(val error: OAuthErrorDomain) : Error()
data class TestError(val message: String) : Error()
}
fun isLoading(): Boolean = this is Loading
companion object {
val UNKNOWN_ERROR = Error.UnknownError
val UNKNOWN_ERROR = Error.Unknown
}
}
@@ -75,12 +73,11 @@ fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
}
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) =
when (this) {
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) = when (this) {
is ApiResult.Success -> State.Success(successMapper(this.value))
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
is ApiResult.Failure.UnknownFailure -> State.UNKNOWN_ERROR
is ApiResult.Failure.HttpFailure -> this.error.toStateApiError()
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
}
}
@@ -33,7 +33,6 @@ class OAuthUseCaseImpl(
forceSms = forceSms
)
kotlin.runCatching {
val error = response.error?.let(VkOAuthError::parse)
val errorType = response.errorType?.let(VkOAuthErrorType::parse)
@@ -121,12 +120,5 @@ class OAuthUseCaseImpl(
}
emit(newState)
}.fold(
onSuccess = {
},
onFailure = {
emit(State.Error.TestError(it.stackTraceToString()))
}
)
}
}
@@ -6,6 +6,4 @@ import androidx.compose.runtime.Immutable
sealed class BaseError {
data object SessionExpired : BaseError()
data class SimpleError(val message: String) : BaseError()
}
@@ -1,7 +1,7 @@
package dev.meloda.fast.model.api.data
import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkVideoDomain
import com.squareup.moshi.JsonClass
@JsonClass(generateAdapter = true)
data class VkVideoData(
@@ -12,7 +12,7 @@ data class VkVideoData(
val duration: Int,
val date: Int,
val comments: Int?,
val description: String?,
val description: String,
val player: String?,
val added: Int?,
val type: String,
@@ -20,9 +20,9 @@ data class VkVideoData(
val access_key: String?,
val owner_id: Int,
val is_favorite: Boolean?,
val image: List<Image>?,
val image: List<Image>,
val first_frame: List<FirstFrame>?,
val files: File?
val files: File?,
) : VkAttachmentData {
@JsonClass(generateAdapter = true)
@@ -67,7 +67,7 @@ data class VkVideoData(
fun toDomain() = VkVideoDomain(
id = id,
ownerId = owner_id,
images = image.orEmpty().map { it.asVideoImage() },
images = image.map { it.asVideoImage() },
firstFrames = first_frame,
accessKey = access_key,
title = title
@@ -46,13 +46,8 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
return successModel
},
onFailure = { failure ->
if (failure is JsonDataException) {
throw ApiException(
RestApiError(
errorCode = -1,
errorMsg = failure.message.orEmpty()
)
)
if(failure is JsonDataException) {
throw failure
}
val isUnit = successType == Unit::class.java
@@ -5,8 +5,6 @@ enum class ValidationType(val value: String) {
SMS("2fa_sms");
companion object {
fun parse(value: String): ValidationType =
entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown validation type $value")
fun parse(value: String): ValidationType = entries.first { it.value == value }
}
}
@@ -1,7 +1,6 @@
package dev.meloda.fast.network
enum class VkErrorCode(val code: Int) {
WTF(-1),
UNKNOWN_ERROR(1),
APP_DISABLED(2),
UNKNOWN_METHOD(3),
@@ -6,6 +6,7 @@ import com.slack.eithernet.integration.retrofit.ApiResultCallAdapterFactory
import com.slack.eithernet.integration.retrofit.ApiResultConverterFactory
import com.squareup.moshi.Moshi
import dev.meloda.fast.common.AppConstants
import dev.meloda.fast.common.model.LogLevel
import dev.meloda.fast.datastore.AppSettings
import dev.meloda.fast.network.JsonConverter
import dev.meloda.fast.network.MoshiConverter
@@ -56,8 +57,12 @@ val networkModule = module {
.followSslRedirects(true)
.addInterceptor(
HttpLoggingInterceptor().apply {
level =
HttpLoggingInterceptor.Level.entries[AppSettings.Debug.networkLogLevel.ordinal]
level = when (AppSettings.Debug.networkLogLevel) {
LogLevel.NONE -> HttpLoggingInterceptor.Level.NONE
LogLevel.BASIC -> HttpLoggingInterceptor.Level.BASIC
LogLevel.HEADERS -> HttpLoggingInterceptor.Level.HEADERS
LogLevel.BODY -> HttpLoggingInterceptor.Level.BODY
}
}
)
.build()
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
@@ -23,9 +22,7 @@ fun ErrorView(
onButtonClick: (() -> Unit)? = null,
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
@@ -133,7 +133,6 @@
<string name="message_attachment_story_your_story">Ваша история</string>
<string name="settings_dynamic_colors">Динамические цвета</string>
<string name="settings_dynamic_colors_description">Цвета для приложения будут извлечены из ваших обоев на главном экране</string>
<string name="settings_appearance_use_system_font_title">Использовать системный шрифт</string>
<string name="settings_application_language">Язык приложения</string>
<string name="settings_application_language_value">Текущий: %1$s</string>
<string name="language_system">Системный</string>
@@ -178,7 +177,6 @@
<string name="settings_general_title">Основное</string>
<string name="settings_general_contact_names_title">Использовать имена контактов</string>
<string name="settings_general_contact_names_summary">Приложение будет использовать доступные имена контактов для пользователей</string>
<string name="settings_general_enable_haptic_title">Включить тактильную отдачу</string>
<string name="settings_appearance_title">Внешний вид</string>
<string name="settings_appearance_multiline_title">Многострочные заголовки и сообщения</string>
<string name="settings_appearance_multiline_summary">Заголовок чата и текст сообщения смогут занимать несколько строчек</string>
@@ -186,11 +184,9 @@
<string name="settings_features_fast_text_title">Fast текст</string>
<string name="settings_features_long_poll_in_background_title">LongPoll в фоне</string>
<string name="settings_features_long_poll_in_background_summary">Ваши сообщения будут обновляться, даже если приложение находится в фоне</string>
<string name="settings_experimental_more_animations_summary">Использовать анимации везде, где возможно</string>
<string name="settings_activity_title">Активность</string>
<string name="settings_activity_send_online_title">Быть «в сети»</string>
<string name="settings_activity_send_online_summary">Статус «в сети» будет отправляться каждые 5 минут</string>
<string name="settings_experimental_title">Экспериментальные - ОЧЕНЬ нестабильные</string>
<string name="settings_debug_title">Отладка</string>
<string name="action_disable">Отключить</string>
<string name="background_long_poll_rationale_text">Приложение не сможет обновлять сообщения в фоне без доступа к уведомлениям</string>
@@ -202,14 +198,4 @@
<string name="notification_channel_no_category_description">Уведомления без категории</string>
<string name="notification_channel_long_polling_service_name">Сервис обновления сообщений</string>
<string name="notification_channel_long_polling_service_description">Уведомления сервиса обновлений сообщений</string>
<string name="settings_general_show_emoji_button_title">Показывать кнопку эмоджи</string>
<string name="settings_general_show_emoji_button_summary">Показывать кнопку эмоджи на панели чата</string>
<string name="settings_features_show_time_in_action_messages_title">Показывать время в сервисных сообщениях</string>
<string name="settings_experimental_use_blur_title">Использовать размытие</string>
<string name="settings_experimental_use_blur_summary">Добавлять размытие везде, где возможно.\\nРаботает только с 12 версии Android</string>
<string name="settings_experimental_more_animations_title">Больше анимаций</string>
<string name="warning_confirmation">Подтверждение</string>
<string name="captcha_exit_warning">Вы уверены? Процесс ввода капчи будет отменён</string>
<string name="validation_exit_warning">Вы уверены? Процесс ввода кода-подтверждения будет отменён</string>
<string name="action_authorize">Авторизоваться</string>
</resources>
-14
View File
@@ -204,8 +204,6 @@
<string name="settings_dynamic_colors">Dynamic colors</string>
<string name="settings_dynamic_colors_description">The colors for the app will be extracted from your home screen wallpaper</string>
<string name="settings_appearance_use_system_font_title">Use system font</string>
<string name="settings_application_language">Application Language</string>
<string name="settings_application_language_value">Current: %1$s</string>
@@ -237,9 +235,6 @@
<string name="settings_general_title">General</string>
<string name="settings_general_contact_names_title">Use contact names</string>
<string name="settings_general_contact_names_summary">App will use available contact names for users</string>
<string name="settings_general_show_emoji_button_title">Show emoji button</string>
<string name="settings_general_show_emoji_button_summary">Show emoji button in chat panel</string>
<string name="settings_general_enable_haptic_title">Enable haptic</string>
<string name="settings_appearance_title">Appearance</string>
<string name="settings_appearance_multiline_title">Multiline titles and messages</string>
<string name="settings_appearance_multiline_summary">The title of the conversation and the text of the message can take up multiple lines</string>
@@ -247,18 +242,9 @@
<string name="settings_features_fast_text_title">Fast text</string>
<string name="settings_features_long_poll_in_background_title">LongPoll in background</string>
<string name="settings_features_long_poll_in_background_summary">Your messages will be updating even when app is not on the screen</string>
<string name="settings_features_show_time_in_action_messages_title">Show time in action messages</string>
<string name="settings_experimental_use_blur_title">Use blur</string>
<string name="settings_experimental_use_blur_summary">Adds blur wherever possible.\nWorks on android 12 and newer</string>
<string name="settings_experimental_more_animations_title">More animations</string>
<string name="settings_experimental_more_animations_summary">Use animations wherever possible</string>
<string name="settings_activity_title">Activity</string>
<string name="settings_activity_send_online_title">Send online status</string>
<string name="settings_activity_send_online_summary">Online status will be sent every five minutes</string>
<string name="settings_experimental_title">Experimental - VERY unstable</string>
<string name="settings_debug_title">Debug</string>
<string name="background_long_poll_rationale_text">The app won\'t be able to update messages in the background without access to notifications</string>
<string name="action_disable">Disable</string>
@@ -345,13 +345,6 @@ class LoginViewModelImpl(
true
}
is State.Error.TestError -> {
val message = stateError.message
val error = LoginError.SimpleError(message = message)
loginError.update { error }
true
}
else -> false
}
}
@@ -9,5 +9,4 @@ sealed class LoginError {
data object TooManyTries : LoginError()
data object WrongValidationCode : LoginError()
data object WrongValidationCodeFormat : LoginError()
data class SimpleError(val message: String): LoginError()
}
@@ -50,6 +50,8 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.meloda.fast.auth.login.LoginViewModel
import dev.meloda.fast.auth.login.LoginViewModelImpl
import dev.meloda.fast.auth.login.model.CaptchaArguments
import dev.meloda.fast.auth.login.model.LoginError
import dev.meloda.fast.auth.login.model.LoginScreenState
@@ -439,14 +441,5 @@ fun HandleError(
confirmText = stringResource(id = UiR.string.ok)
)
}
is LoginError.SimpleError -> {
MaterialDialog(
onDismissRequest = onDismiss,
title = "Error",
text = error.message,
confirmText = stringResource(id = UiR.string.ok)
)
}
}
}
@@ -62,8 +62,8 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import dev.chrisbanes.haze.HazeState
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
import dev.meloda.fast.chatmaterials.ChatMaterialsViewModel
@@ -137,7 +137,7 @@ fun ChatMaterialsScreen(
)
}
val titles = listOf("Photos", "Videos", "Audios")//, "Files", "Links")
val titles = listOf("Photos", "Videos", "Audios", "Files", "Links")
val listState = rememberLazyListState()
val gridState = rememberLazyGridState()
@@ -179,7 +179,7 @@ fun ChatMaterialsScreen(
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeEffect(
Modifier.hazeChild(
state = hazeState,
style = hazeStyle
)
@@ -311,7 +311,7 @@ fun ChatMaterialsScreen(
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState)
Modifier.haze(state = hazeState)
} else {
Modifier
}
@@ -346,7 +346,7 @@ fun ChatMaterialsScreen(
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeSource(state = hazeState)
Modifier.haze(state = hazeState)
} else {
Modifier
}
@@ -1,11 +1,8 @@
package dev.meloda.fast.conversations
import android.content.Context
import android.content.res.Resources
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import coil.ImageLoader
import coil.request.ImageRequest
import com.conena.nanokt.collections.indexOfFirstOrNull
import dev.meloda.fast.common.extensions.createTimerFlow
import dev.meloda.fast.common.extensions.findWithIndex
@@ -21,7 +18,6 @@ import dev.meloda.fast.data.State
import dev.meloda.fast.data.processState
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.model.BaseError
@@ -47,6 +43,7 @@ interface ConversationsViewModel {
val screenState: StateFlow<ConversationsScreenState>
val baseError: StateFlow<BaseError?>
val imagesToPreload: StateFlow<List<String>>
val currentOffset: StateFlow<Int>
val canPaginate: StateFlow<Boolean>
val scrollToTop: StateFlow<Boolean>
@@ -81,23 +78,16 @@ class ConversationsViewModelImpl(
private val conversationsUseCase: ConversationsUseCase,
private val messagesUseCase: MessagesUseCase,
private val resources: Resources,
private val userSettings: UserSettings,
private val imageLoader: ImageLoader,
private val applicationContext: Context,
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase
private val userSettings: UserSettings
) : ConversationsViewModel, ViewModel() {
override val screenState = MutableStateFlow(ConversationsScreenState.EMPTY)
override val baseError = MutableStateFlow<BaseError?>(null)
override val imagesToPreload = MutableStateFlow<List<String>>(emptyList())
override val currentOffset = MutableStateFlow(0)
override val canPaginate = MutableStateFlow(false)
override val scrollToTop = MutableStateFlow(false)
// TODO: 22-Dec-24, Danil Nikolaev: rewrite
private val useContactNames = {
userSettings.useContactNames.value
}
override fun onPaginationConditionsMet() {
currentOffset.update { screenState.value.conversations.size }
loadConversations()
@@ -134,7 +124,6 @@ class ConversationsViewModelImpl(
}
override fun onRefresh() {
baseError.setValue { null }
loadConversations(offset = 0)
}
@@ -274,7 +263,17 @@ class ConversationsViewModelImpl(
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset)
.listenValue(viewModelScope) { state ->
state.processState(
error = ::handleError,
error = { error ->
if (error is State.Error.ApiError) {
when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
baseError.setValue { BaseError.SessionExpired }
}
else -> Unit
}
}
},
success = { response ->
val itemsCountSufficient = response.size == LOAD_COUNT
canPaginate.setValue { itemsCountSufficient }
@@ -282,17 +281,9 @@ class ConversationsViewModelImpl(
val paginationExhausted = !itemsCountSufficient &&
screenState.value.conversations.isNotEmpty()
val imagesToPreload =
imagesToPreload.setValue {
response.mapNotNull { it.extractAvatar().extractUrl() }
imagesToPreload.forEach { url ->
imageLoader.enqueue(
ImageRequest.Builder(applicationContext)
.data(url)
.build()
)
}
conversationsUseCase.storeConversations(response)
val loadedConversations = response.map {
@@ -330,40 +321,6 @@ class ConversationsViewModelImpl(
}
}
private fun handleError(error: State.Error) {
when (error) {
is State.Error.ApiError -> {
when (error.errorCode) {
VkErrorCode.USER_AUTHORIZATION_FAILED -> {
baseError.setValue { BaseError.SessionExpired }
}
else -> {
baseError.setValue {
BaseError.SimpleError(message = error.errorMessage)
}
}
}
}
State.Error.ConnectionError -> {
baseError.setValue {
BaseError.SimpleError(message = "Connection error")
}
}
State.Error.InternalError -> {
baseError.setValue {
BaseError.SimpleError(message = "Internal error")
}
}
State.Error.UnknownError -> {
baseError.setValue {
BaseError.SimpleError(message = "Unknown error")
}
}
else -> Unit
}
}
private fun deleteConversation(peerId: Int) {
conversationsUseCase.delete(peerId).listenValue(viewModelScope) { state ->
state.processState(
@@ -380,12 +337,7 @@ class ConversationsViewModelImpl(
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
conversations = newConversations.map { it.asPresentation(resources) }
)
}
}
@@ -421,40 +373,13 @@ class ConversationsViewModelImpl(
private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) {
val message = event.message
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == message.peerId }
if (conversationIndex == null) {
loadConversationsByIdUseCase(peerIds = listOf(message.peerId))
.listenValue(viewModelScope) { state ->
state.processState(
error = { error ->
},
success = { response ->
val conversation = (response.firstOrNull() ?: return@listenValue)
.copy(lastMessage = message)
// TODO: 22-Dec-24, Danil Nikolaev: handle interactions and pinned state
newConversations.add(pinnedConversationsCount.value, conversation)
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
)
}
}
)
}
if (conversationIndex == null) { // диалога нет в списке
// pizdets
// TODO: 04/07/2024, Danil Nikolaev: load conversation and store info
} else {
val conversation = newConversations[conversationIndex]
var newConversation = conversation.copy(
@@ -495,12 +420,7 @@ class ConversationsViewModelImpl(
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
conversations = newConversations.map { it.asPresentation(resources) }
)
}
}
@@ -524,12 +444,7 @@ class ConversationsViewModelImpl(
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
conversations = newConversations.map { it.asPresentation(resources) }
)
}
}
@@ -539,11 +454,8 @@ class ConversationsViewModelImpl(
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId }
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(
inRead = event.messageId,
@@ -554,26 +466,17 @@ class ConversationsViewModelImpl(
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
conversations = newConversations.map { it.asPresentation(resources) }
)
}
)
}
}
}
private fun handleReadOutgoingMessage(event: LongPollEvent.VkMessageReadOutgoingEvent) {
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId }
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(
outRead = event.messageId,
@@ -583,15 +486,9 @@ class ConversationsViewModelImpl(
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
conversations = newConversations.map { it.asPresentation(resources) }
)
}
)
}
}
}
private fun handlePinStateChanged(event: LongPollEvent.VkConversationPinStateChangedEvent) {
@@ -599,11 +496,8 @@ class ConversationsViewModelImpl(
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId }
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
val pin = event.majorId > 0
val conversation = newConversations[conversationIndex].copy(majorId = event.majorId)
@@ -629,13 +523,7 @@ class ConversationsViewModelImpl(
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
})
}
old.copy(conversations = newConversations.map { it.asPresentation(resources) })
}
}
@@ -655,11 +543,8 @@ class ConversationsViewModelImpl(
val newConversations = conversations.value.toMutableList()
val conversationAndIndex =
newConversations.findWithIndex { it.id == peerId }
newConversations.findWithIndex { it.id == peerId } ?: return
if (conversationAndIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationAndIndex.first] =
conversationAndIndex.second.copy(
interactionType = interactionType.value,
@@ -670,12 +555,7 @@ class ConversationsViewModelImpl(
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
conversations = newConversations.map { it.asPresentation(resources) }
)
}
@@ -703,7 +583,6 @@ class ConversationsViewModelImpl(
stopInteraction(peerId, newInteractionJob)
}
}
}
private fun stopInteraction(peerId: Int, interactionJob: InteractionJob) {
interactionsTimers[peerId] ?: return
@@ -721,12 +600,7 @@ class ConversationsViewModelImpl(
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
conversations = newConversations.map { it.asPresentation(resources) }
)
}
@@ -755,12 +629,7 @@ class ConversationsViewModelImpl(
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
conversations = newConversations.map { it.asPresentation(resources) }
)
}
}
@@ -54,6 +54,7 @@ import androidx.compose.runtime.snapshotFlow
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@@ -64,6 +65,8 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.core.view.HapticFeedbackConstantsCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.imageLoader
import coil.request.ImageRequest
import dev.chrisbanes.haze.haze
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
@@ -93,11 +96,24 @@ fun ConversationsRoute(
onConversationPhotoClicked: (url: String) -> Unit,
viewModel: ConversationsViewModel
) {
val context = LocalContext.current
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) {
imagesToPreload.forEach { url ->
context.imageLoader.enqueue(
ImageRequest.Builder(context)
.data(url)
.build()
)
}
}
ConversationsScreen(
screenState = screenState,
baseError = baseError,
@@ -339,9 +355,7 @@ fun ConversationsScreen(
}
) { padding ->
when {
baseError != null -> {
when (baseError) {
is BaseError.SessionExpired -> {
baseError is BaseError.SessionExpired -> {
ErrorView(
text = "Session expired",
buttonText = "Log out",
@@ -349,16 +363,6 @@ fun ConversationsScreen(
)
}
is BaseError.SimpleError -> {
ErrorView(
text = baseError.message,
buttonText = "Try again",
onButtonClick = onRefresh
)
}
}
}
screenState.isLoading && screenState.conversations.isEmpty() -> FullScreenLoader()
else -> {
@@ -33,7 +33,7 @@ import dev.meloda.fast.ui.R as UiR
fun VkConversation.asPresentation(
resources: Resources,
useContactName: Boolean
useContactName: Boolean = false
): UiConversation = UiConversation(
id = id,
lastMessageId = lastMessageId,
@@ -278,14 +278,14 @@ class SettingsViewModelImpl(
)
val generalShowEmojiButton = SettingsItem.Switch(
key = SettingsKeys.KEY_SHOW_EMOJI_BUTTON,
title = UiText.Resource(UiR.string.settings_general_show_emoji_button_title),
text = UiText.Resource(UiR.string.settings_general_show_emoji_button_summary),
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.Resource(UiR.string.settings_general_enable_haptic_title)
title = UiText.Simple("Enable haptic")
)
val appearanceTitle = SettingsItem.Title(
@@ -342,7 +342,7 @@ class SettingsViewModelImpl(
val appearanceUseSystemFont = SettingsItem.Switch(
key = SettingsKeys.KEY_USE_SYSTEM_FONT,
defaultValue = SettingsKeys.DEFAULT_USE_SYSTEM_FONT,
title = UiText.Resource(UiR.string.settings_appearance_use_system_font_title)
title = UiText.Simple("Use system font")
)
val appearanceLanguage = SettingsItem.TitleText(
key = SettingsKeys.KEY_APPEARANCE_LANGUAGE,
@@ -379,7 +379,7 @@ class SettingsViewModelImpl(
val experimentalTitle = SettingsItem.Title(
key = "experimental",
title = UiText.Resource(UiR.string.settings_experimental_title)
title = UiText.Simple("Experimental - VERY unstable")
)
val experimentalLongPollBackground = SettingsItem.Switch(
key = SettingsKeys.KEY_LONG_POLL_IN_BACKGROUND,
@@ -390,20 +390,19 @@ class SettingsViewModelImpl(
val experimentalShowTimeInActionMessages = SettingsItem.Switch(
key = SettingsKeys.KEY_SHOW_TIME_IN_ACTION_MESSAGES,
defaultValue = SettingsKeys.DEFAULT_SHOW_TIME_IN_ACTION_MESSAGES,
title = UiText.Resource(UiR.string.settings_features_show_time_in_action_messages_title)
title = UiText.Simple("Show time in action messages")
)
val experimentalUseBlur = SettingsItem.Switch(
key = SettingsKeys.KEY_USE_BLUR,
defaultValue = SettingsKeys.DEFAULT_USE_BLUR,
title = UiText.Resource(UiR.string.settings_experimental_use_blur_title),
text = UiText.Resource(UiR.string.settings_experimental_use_blur_summary),
isVisible = Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
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.Resource(UiR.string.settings_experimental_more_animations_title),
text = UiText.Resource(UiR.string.settings_experimental_more_animations_summary)
title = UiText.Simple("More animations"),
text = UiText.Simple("Use animations wherever possible")
)
val debugTitle = SettingsItem.Title(
+14 -14
View File
@@ -2,25 +2,25 @@
minSdk = "23"
targetSdk = "35"
compileSdk = "35"
versionCode = "9"
versionName = "0.1.6"
versionCode = "8"
versionName = "0.1.5"
agp = "8.9.0"
agp = "8.7.3"
converterMoshi = "2.11.0"
eithernet = "2.0.0"
haze = "1.5.1"
kotlin = "2.1.10"
ksp = "2.1.10-1.0.31"
haze = "1.1.1"
kotlin = "2.1.0"
ksp = "2.1.0-1.0.29"
compose-bom = "2025.03.00"
koin = "4.0.2"
compose-bom = "2024.12.01"
koin = "4.0.0"
accompanist = "0.37.2"
accompanist = "0.37.0"
coil = "2.7.0"
coroutines = "1.10.1"
coroutines = "1.9.0"
junit = "4.13.2"
chucker = "4.1.0"
guava = "33.4.5-jre"
guava = "33.3.1-jre"
lifecycle = "2.8.7"
core-ktx = "1.15.0"
material = "1.12.0"
@@ -33,10 +33,10 @@ nanokt = "1.2.0"
junitVersion = "1.2.1"
espressoCore = "3.6.1"
appcompat = "1.7.0"
androidx-navigation = "2.8.9"
serialization = "1.8.0"
androidx-navigation = "2.8.5"
serialization = "1.7.3"
rebugger = "1.0.0-rc03"
moduleGraph = "2.8.0"
moduleGraph = "2.7.1"
[libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
Binary file not shown.
+2 -3
View File
@@ -1,7 +1,6 @@
#Mon Oct 28 18:41:43 MSK 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+13 -31
View File
@@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -82,12 +80,13 @@ do
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s
' "$PWD" ) || exit
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -134,29 +133,22 @@ location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
@@ -201,15 +193,11 @@ if "$cygwin" || "$msys" ; then
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
# Collect all arguments for the java command;
# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
# shell script including quotes and variable substitutions, so put them in
# double quotes to make sure that they get re-expanded; and
# * put everything else in single quotes, so that it's not re-expanded.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -217,12 +205,6 @@ set -- \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
Vendored
+16 -21
View File
@@ -13,10 +13,8 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@@ -27,8 +25,7 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -43,13 +40,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
if "%ERRORLEVEL%" == "0" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
@@ -59,11 +56,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
@@ -78,15 +75,13 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal