34 Commits

Author SHA1 Message Date
melod1n 797e966b65 fixes and improvements 2025-03-21 02:35:57 +03:00
melod1n 2c8536a9da russian translations 2025-03-21 01:41:44 +03:00
dependabot[bot] 12ba4faade Bump com.google.guava:guava from 33.4.0-jre to 33.4.5-jre (#132) 2025-03-20 22:17:45 +00:00
dependabot[bot] 89e5d10bdf Bump haze from 1.5.0 to 1.5.1 (#133) 2025-03-20 22:17:39 +00:00
melod1n 79e5823ff8 fix issues with package names 2025-03-16 18:56:14 +03:00
melod1n 6995fc385c revert agp version to 8.8.2 2025-03-16 17:57:39 +03:00
dependabot[bot] bed1348d19 Bump androidx.compose:compose-bom from 2025.02.00 to 2025.03.00 (#129) 2025-03-12 18:17:44 +00:00
dependabot[bot] 7fa2da8a2c Bump androidx.navigation:navigation-compose from 2.8.8 to 2.8.9 (#130) 2025-03-12 18:17:37 +00:00
dependabot[bot] 842ffe9f5b Bump agp from 8.8.2 to 8.9.0 (#127) 2025-03-08 01:17:59 +00:00
dependabot[bot] 72520863bd Bump haze from 1.4.0 to 1.5.0 (#128) 2025-03-08 01:17:52 +00:00
dependabot[bot] b69273b043 Bump androidx.navigation:navigation-compose from 2.8.7 to 2.8.8 (#122) 2025-03-07 01:49:13 +00:00
dependabot[bot] 81f9742120 Bump agp from 8.8.1 to 8.8.2 (#123) 2025-03-07 01:49:10 +00:00
dependabot[bot] a8cd647d04 Bump haze from 1.3.1 to 1.4.0 (#124) 2025-03-07 01:49:06 +00:00
dependabot[bot] 7e17738024 Bump ksp from 2.1.10-1.0.30 to 2.1.10-1.0.31 (#125) 2025-03-07 01:49:04 +00:00
dependabot[bot] 9983fa3752 Bump com.jraska.module.graph.assertion from 2.7.3 to 2.8.0 (#126) 2025-03-07 01:48:53 +00:00
melod1n 5fd2fb0abb Rename the app's namespace and applicationId to dev.meloda.fastvk, and update the package name in ACTION_MANAGE_UNKNOWN_APP_SOURCES intent. Remove unnecessary onLowMemory method in the OnlineService. 2025-02-22 01:35:09 +03:00
dependabot[bot] 59280a0358 Bump com.google.accompanist:accompanist-permissions (#121) 2025-02-21 22:23:36 +00:00
dependabot[bot] e9f84cbdf4 Bump agp from 8.8.0 to 8.8.1 (#117) 2025-02-19 19:17:53 +00:00
dependabot[bot] b80fc6c936 Bump ksp from 2.1.0-1.0.29 to 2.1.10-1.0.30 (#116) 2025-02-19 19:17:19 +00:00
dependabot[bot] 92a156f2f0 Bump haze from 1.2.2 to 1.3.1 (#118) 2025-02-19 19:17:13 +00:00
dependabot[bot] a20b4e42ad Bump androidx.navigation:navigation-compose from 2.8.5 to 2.8.7 (#119) 2025-02-19 19:17:06 +00:00
dependabot[bot] 124033fb9c Bump androidx.compose:compose-bom from 2024.12.01 to 2025.02.00 (#115) 2025-02-15 15:51:03 +00:00
dependabot[bot] 11c394cc9f Bump kotlin from 2.1.0 to 2.1.10 (#113) 2025-02-15 15:50:41 +00:00
melod1n 60d173e1f3 little improvement 2025-02-15 18:50:19 +03:00
dependabot[bot] e1a2cce08d Bump koin from 4.0.1 to 4.0.2 (#112) 2025-02-15 15:39:53 +00:00
dependabot[bot] 9756a36650 Bump haze from 1.2.0 to 1.2.2 (#111) 2025-02-15 15:39:41 +00:00
dependabot[bot] f363e2c547 Bump com.jraska.module.graph.assertion from 2.7.1 to 2.7.3 (#109) 2025-02-15 15:39:28 +00:00
dependabot[bot] 1cddcd7e99 Bump agp from 8.7.3 to 8.8.0 (#106) 2025-01-11 04:05:22 +00:00
melod1n 095aa20dcc update gradle wrapper 2025-01-11 06:45:01 +03:00
dependabot[bot] ce5a43aea9 Bump org.jetbrains.kotlinx:kotlinx-serialization-json from 1.7.3 to 1.8.0 (#104) 2025-01-11 03:41:28 +00:00
dependabot[bot] 8824775f68 Bump haze from 1.1.1 to 1.2.0 (#105) 2025-01-11 03:41:07 +00:00
melod1n 7e5843759d release/0.1.6 (#103)
* Bump com.google.guava:guava from 33.3.1-jre to 33.4.0-jre (#97)

* Bump coroutines from 1.9.0 to 1.10.1 (#100)

* some improvements + loading conversation on new message if it is not already in the list

* Bump koin from 4.0.0 to 4.0.1 (#101)

* minor update

---------

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-11 06:38:08 +03:00
melod1n 7c14df1824 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
2024-12-17 21:07:22 +03:00
melod1n 82695ccf6f Release 0.1.4 (#95)
update gh actions
2024-12-14 12:34:53 +03:00
67 changed files with 1123 additions and 560 deletions
@@ -1,10 +1,10 @@
name: Android CI
name: Android CI Build
on:
push:
branches: [ "master" ]
branches: [ "dev", "release/*", "hotfix/*" ]
pull_request:
branches: [ "master" ]
branches: [ "dev", "release/*", "hotfix/*" ]
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -13,7 +13,7 @@ env:
jobs:
build_apk_aab:
runs-on: ubuntu-latest
runs-on: ubuntu-24.04
name: Build artifacts
steps:
- name: Checkout
@@ -29,7 +29,7 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build and sign debug APKs
- name: Build and sign debug APK
run: ./gradlew assembleDebug
- name: Upload debug APK
@@ -38,7 +38,7 @@ jobs:
name: app-debug.apk
path: app/build/outputs/apk/debug/app-debug.apk
- name: Build and sign release APKs
- name: Build and sign release APK
run: ./gradlew assembleRelease
- name: Upload release APK
+64
View File
@@ -0,0 +1,64 @@
name: Android CI Release
on:
pull_request:
branches: [ "master" ]
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
RELEASE_SIGN_KEY_ALIAS: ${{ secrets.RELEASE_SIGN_KEY_ALIAS }}
RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}
jobs:
create_release:
runs-on: ubuntu-24.04
name: Build artifacts
steps:
- name: Checkout
uses: actions/checkout@v4
- name: set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'temurin'
cache: gradle
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build and sign debug APK
run: ./gradlew assembleDebug
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug.apk
path: app/build/outputs/apk/debug/app-debug.apk
- name: Build and sign release APK
run: ./gradlew assembleRelease
- name: Upload release APK
uses: actions/upload-artifact@v4
with:
name: app-release.apk
path: app/build/outputs/apk/release/app-release.apk
- name: Build and sign debug Bundle
run: ./gradlew bundleDebug
- name: Upload debug Bundle
uses: actions/upload-artifact@v4
with:
name: app-debug.aab
path: app/build/outputs/bundle/debug/app-debug.aab
- name: Build and sign release Bundle
run: ./gradlew bundleRelease
- name: Upload release Bundle
uses: actions/upload-artifact@v4
with:
name: app-release.aab
path: app/build/outputs/bundle/release/app-release.aab
+2 -2
View File
@@ -7,10 +7,10 @@ plugins {
}
android {
namespace = "dev.meloda.fast"
namespace = "dev.meloda.fastvk"
defaultConfig {
applicationId = "dev.meloda.fast"
applicationId = "dev.meloda.fastvk"
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=".presentation.MainActivity"
android:name="dev.meloda.fast.presentation.MainActivity"
android:exported="true"
android:windowSoftInputMode="adjustResize">
@@ -38,13 +38,13 @@
</activity>
<service
android:name=".service.longpolling.LongPollingService"
android:name="dev.meloda.fast.service.longpolling.LongPollingService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
<service
android:name=".service.OnlineService"
android:name="dev.meloda.fast.service.OnlineService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="dataSync" />
@@ -86,6 +86,8 @@ class MainViewModelImpl(
BaseError.SessionExpired -> {
isNeedToReplaceWithAuth.update { true }
}
is BaseError.SimpleError -> Unit // TODO: 21-Mar-25, Danil Nikolaev: show error in ui
}
}
@@ -209,7 +211,7 @@ class MainViewModelImpl(
}
longPollController.setStateToApply(
if (AppSettings.Debug.longPollInBackground) {
if (AppSettings.Experimental.longPollInBackground) {
LongPollState.Background
} else {
LongPollState.InApp
@@ -233,7 +235,7 @@ class MainViewModelImpl(
}
private fun disableBackgroundLongPoll() {
AppSettings.Debug.longPollInBackground = false
AppSettings.Experimental.longPollInBackground = false
longPollController.setStateToApply(LongPollState.InApp)
}
}
@@ -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 {
single<ImageLoader> {
ImageLoader.Builder(get())
.crossfade(true)
.build()
@@ -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
}
}
@@ -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<Int>(
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<MainGraph>(startDestination = navigationItems[selectedItemIndex].route) {
navigation<MainGraph>(
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,
@@ -95,11 +95,6 @@ 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")
@@ -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 {
@@ -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<KotlinJvmProjectExtension>()
}
private inline fun <reified T : KotlinTopLevelExtension> Project.configureKotlin() = configure<T> {
private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() = configure<T> {
// Treat all Kotlin warnings as errors (disabled by default)
// Override by setting warningsAsErrors=true in your ~/.gradle/gradle.properties
val warningsAsErrors: String? by project
@@ -1,7 +1,5 @@
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.fast")
data = Uri.parse("package:dev.meloda.fastvk")
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES
}
})
@@ -20,18 +20,20 @@ sealed class State<out T> {
data object ConnectionError : Error()
data object Unknown : Error()
data object UnknownError : 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.Unknown
val UNKNOWN_ERROR = Error.UnknownError
}
}
@@ -73,11 +75,12 @@ 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) {
is ApiResult.Success -> State.Success(successMapper(this.value))
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()
}
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()
}
@@ -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<List<VkConversation>, RestApiErrorDomain>
suspend fun getConversationsById(
peerIds: List<Int>
): ApiResult<List<VkConversation>, RestApiErrorDomain>
suspend fun storeConversations(conversations: List<VkConversation>)
suspend fun delete(peerId: Int): ApiResult<Int, RestApiErrorDomain>
suspend fun pin(peerId: Int): ApiResult<Int, RestApiErrorDomain>
@@ -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<Int>
): ApiResult<List<VkConversation>, 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<VkConversation>) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
}
@@ -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,
@@ -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
}
@@ -7,7 +7,6 @@ import kotlinx.coroutines.flow.StateFlow
interface UserSettings {
val useContactNames: StateFlow<Boolean>
val enablePullToRefresh: StateFlow<Boolean>
val enableMultiline: StateFlow<Boolean>
val darkMode: StateFlow<DarkMode>
@@ -28,7 +27,6 @@ interface UserSettings {
val showDebugCategory: StateFlow<Boolean>
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
}
@@ -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<Int>): Flow<State<List<VkConversation>>> = flow {
emit(State.Loading)
val newState = conversationsRepository
.getConversationsById(peerIds = peerIds)
.mapToState()
emit(newState)
}
}
@@ -33,92 +33,100 @@ class OAuthUseCaseImpl(
forceSms = forceSms
)
val error = response.error?.let(VkOAuthError::parse)
val errorType = response.errorType?.let(VkOAuthErrorType::parse)
kotlin.runCatching {
val error = response.error?.let(VkOAuthError::parse)
val errorType = response.errorType?.let(VkOAuthErrorType::parse)
val newState = when (error) {
null -> {
State.Success(
AuthInfo(
userId = response.userId,
accessToken = response.accessToken,
validationHash = response.validationHash
)
)
}
VkOAuthError.FLOOD_CONTROL -> {
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
}
VkOAuthError.NEED_VALIDATION -> {
if (response.banInfo != null) {
val info = requireNotNull(response.banInfo)
State.Error.OAuthError(
OAuthErrorDomain.UserBannedError(
memberName = info.memberName,
message = info.message,
accessToken = info.accessToken,
restoreUrl = info.restoreUrl
)
)
} else {
State.Error.OAuthError(
OAuthErrorDomain.ValidationRequiredError(
description = response.errorDescription.orEmpty(),
validationType = response.validationType.orEmpty()
.let(ValidationType::parse),
validationSid = response.validationSid.orEmpty(),
phoneMask = response.phoneMask.orEmpty(),
redirectUri = response.redirectUri.orEmpty(),
validationResend = response.validationResend,
restoreIfCannotGetCode = response.restoreIfCannotGetCode
val newState = when (error) {
null -> {
State.Success(
AuthInfo(
userId = response.userId,
accessToken = response.accessToken,
validationHash = response.validationHash
)
)
}
}
VkOAuthError.NEED_CAPTCHA -> {
State.Error.OAuthError(
OAuthErrorDomain.CaptchaRequiredError(
captchaSid = response.captchaSid.orEmpty(),
captchaImageUrl = response.captchaImage.orEmpty()
VkOAuthError.FLOOD_CONTROL -> {
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
}
VkOAuthError.NEED_VALIDATION -> {
if (response.banInfo != null) {
val info = requireNotNull(response.banInfo)
State.Error.OAuthError(
OAuthErrorDomain.UserBannedError(
memberName = info.memberName,
message = info.message,
accessToken = info.accessToken,
restoreUrl = info.restoreUrl
)
)
} else {
State.Error.OAuthError(
OAuthErrorDomain.ValidationRequiredError(
description = response.errorDescription.orEmpty(),
validationType = response.validationType.orEmpty()
.let(ValidationType::parse),
validationSid = response.validationSid.orEmpty(),
phoneMask = response.phoneMask.orEmpty(),
redirectUri = response.redirectUri.orEmpty(),
validationResend = response.validationResend,
restoreIfCannotGetCode = response.restoreIfCannotGetCode
)
)
}
}
VkOAuthError.NEED_CAPTCHA -> {
State.Error.OAuthError(
OAuthErrorDomain.CaptchaRequiredError(
captchaSid = response.captchaSid.orEmpty(),
captchaImageUrl = response.captchaImage.orEmpty()
)
)
)
}
}
VkOAuthError.INVALID_CLIENT -> {
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
}
VkOAuthError.INVALID_CLIENT -> {
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
}
VkOAuthError.INVALID_REQUEST -> {
when (errorType) {
null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError)
VkOAuthError.INVALID_REQUEST -> {
when (errorType) {
null -> State.Error.OAuthError(OAuthErrorDomain.UnknownError)
VkOAuthErrorType.WRONG_OTP -> {
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode)
VkOAuthErrorType.WRONG_OTP -> {
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCode)
}
VkOAuthErrorType.WRONG_OTP_FORMAT -> {
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCodeFormat)
}
VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> {
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
}
VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
}
}
}
VkOAuthErrorType.WRONG_OTP_FORMAT -> {
State.Error.OAuthError(OAuthErrorDomain.WrongValidationCodeFormat)
}
VkOAuthErrorType.PASSWORD_BRUTEFORCE_ATTEMPT -> {
State.Error.OAuthError(OAuthErrorDomain.TooManyTriesError)
}
VkOAuthErrorType.USERNAME_OR_PASSWORD_IS_INCORRECT -> {
State.Error.OAuthError(OAuthErrorDomain.InvalidCredentialsError)
}
VkOAuthError.UNKNOWN -> {
State.Error.OAuthError(OAuthErrorDomain.UnknownError)
}
}
VkOAuthError.UNKNOWN -> {
State.Error.OAuthError(OAuthErrorDomain.UnknownError)
emit(newState)
}.fold(
onSuccess = {
},
onFailure = {
emit(State.Error.TestError(it.stackTraceToString()))
}
}
emit(newState)
)
}
}
@@ -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)
}
@@ -6,4 +6,6 @@ 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 dev.meloda.fast.model.api.domain.VkVideoDomain
import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkVideoDomain
@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.map { it.asVideoImage() },
images = image.orEmpty().map { it.asVideoImage() },
firstFrames = first_frame,
accessKey = access_key,
title = title
@@ -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<VkContactData>?
)
@JsonClass(generateAdapter = true)
data class ConversationsGetByIdResponse(
@Json(name = "count") val count: Int,
@Json(name = "items") val items: List<VkConversationData>,
@Json(name = "profiles") val profiles: List<VkUserData>?,
@Json(name = "groups") val groups: List<VkGroupData>?,
@Json(name = "contacts") val contacts: List<VkContactData>?
)
@JsonClass(generateAdapter = true)
data class ConversationsResponseItem(
@Json(name = "conversation") val conversation: VkConversationData,
@@ -46,8 +46,13 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
return successModel
},
onFailure = { failure ->
if(failure is JsonDataException) {
throw failure
if (failure is JsonDataException) {
throw ApiException(
RestApiError(
errorCode = -1,
errorMsg = failure.message.orEmpty()
)
)
}
val isUnit = successType == Unit::class.java
@@ -5,6 +5,8 @@ enum class ValidationType(val value: String) {
SMS("2fa_sms");
companion object {
fun parse(value: String): ValidationType = entries.first { it.value == value }
fun parse(value: String): ValidationType =
entries.firstOrNull { it.value == value }
?: throw IllegalArgumentException("Unknown validation type $value")
}
}
@@ -1,6 +1,7 @@
package dev.meloda.fast.network
enum class VkErrorCode(val code: Int) {
WTF(-1),
UNKNOWN_ERROR(1),
APP_DISABLED(2),
UNKNOWN_METHOD(3),
@@ -6,7 +6,6 @@ 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
@@ -57,12 +56,8 @@ val networkModule = module {
.followSslRedirects(true)
.addInterceptor(
HttpLoggingInterceptor().apply {
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
}
level =
HttpLoggingInterceptor.Level.entries[AppSettings.Debug.networkLogLevel.ordinal]
}
)
.build()
@@ -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<String, String>
): ApiResult<ApiResponse<ConversationsGetResponse>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.GET_BY_ID)
suspend fun getConversationsById(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<ConversationsGetByIdResponse>, RestApiError>
@FormUrlEncoded
@POST(ConversationsUrls.DELETE)
suspend fun delete(
@@ -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"
@@ -5,6 +5,7 @@ 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
@@ -22,7 +23,9 @@ fun ErrorView(
onButtonClick: (() -> Unit)? = null,
) {
Column(
modifier = modifier.fillMaxSize(),
modifier = modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
@@ -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
+15 -1
View File
@@ -133,6 +133,7 @@
<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>
@@ -177,16 +178,19 @@
<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>
<string name="settings_features_title">Фичи</string>
<string name="settings_features_fast_text_title">Fast текст</string>
<string name="settings_features_long_poll_in_background_title">[WIP] LongPoll в фоне</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>
@@ -198,4 +202,14 @@
<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>
+16 -2
View File
@@ -204,6 +204,8 @@
<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>
@@ -235,16 +237,28 @@
<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>
<string name="settings_features_title">Features</string>
<string name="settings_features_fast_text_title">Fast text</string>
<string name="settings_features_long_poll_in_background_title">[WIP] LongPoll in background</string>
<string name="settings_features_long_poll_in_background_summary">Your messages will be updates even when app is not on the screen</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>
@@ -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
@@ -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
@@ -345,6 +345,13 @@ class LoginViewModelImpl(
true
}
is State.Error.TestError -> {
val message = stateError.message
val error = LoginError.SimpleError(message = message)
loginError.update { error }
true
}
else -> false
}
}
@@ -368,7 +375,7 @@ class LoginViewModelImpl(
private fun startLongPoll() {
longPollController.setStateToApply(
if (AppSettings.Debug.longPollInBackground) {
if (AppSettings.Experimental.longPollInBackground) {
LongPollState.Background
} else {
LongPollState.InApp
@@ -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
@@ -9,4 +9,5 @@ sealed class LoginError {
data object TooManyTries : LoginError()
data object WrongValidationCode : LoginError()
data object WrongValidationCodeFormat : LoginError()
data class SimpleError(val message: String): LoginError()
}
@@ -50,8 +50,6 @@ 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
@@ -441,5 +439,14 @@ 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)
)
}
}
}
@@ -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
@@ -62,19 +62,17 @@ 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.haze
import dev.chrisbanes.haze.hazeChild
import dev.chrisbanes.haze.hazeEffect
import dev.chrisbanes.haze.hazeSource
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
import dev.chrisbanes.haze.materials.HazeMaterials
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<ChatMaterialsViewModelImpl>()
) {
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 = {},
@@ -145,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()
@@ -187,7 +179,7 @@ fun ChatMaterialsScreen(
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.hazeChild(
Modifier.hazeEffect(
state = hazeState,
style = hazeStyle
)
@@ -319,7 +311,7 @@ fun ChatMaterialsScreen(
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.haze(state = hazeState)
Modifier.hazeSource(state = hazeState)
} else {
Modifier
}
@@ -354,7 +346,7 @@ fun ChatMaterialsScreen(
modifier = Modifier
.then(
if (currentTheme.enableBlur) {
Modifier.haze(state = hazeState)
Modifier.hazeSource(state = hazeState)
} else {
Modifier
}
@@ -1,8 +1,11 @@
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
@@ -18,6 +21,7 @@ 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
@@ -26,7 +30,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,15 +40,16 @@ 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 {
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>
fun onPaginationConditionsMet()
@@ -63,6 +70,10 @@ interface ConversationsViewModel {
fun setScrollIndex(index: Int)
fun setScrollOffset(offset: Int)
fun setScrollToTopFlow(scrollToTopFlow: Flow<Int>)
fun onScrolledToTop()
}
class ConversationsViewModelImpl(
@@ -70,14 +81,22 @@ class ConversationsViewModelImpl(
private val conversationsUseCase: ConversationsUseCase,
private val messagesUseCase: MessagesUseCase,
private val resources: Resources,
private val userSettings: UserSettings
private val userSettings: UserSettings,
private val imageLoader: ImageLoader,
private val applicationContext: Context,
private val loadConversationsByIdUseCase: LoadConversationsByIdUseCase
) : 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 }
@@ -115,6 +134,7 @@ class ConversationsViewModelImpl(
}
override fun onRefresh() {
baseError.setValue { null }
loadConversations(offset = 0)
}
@@ -217,6 +237,20 @@ class ConversationsViewModelImpl(
screenState.setValue { old -> old.copy(scrollOffset = offset) }
}
override fun setScrollToTopFlow(scrollToTopFlow: Flow<Int>) {
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(
@@ -240,17 +274,7 @@ class ConversationsViewModelImpl(
conversationsUseCase.getConversations(count = LOAD_COUNT, offset = offset)
.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
}
}
},
error = ::handleError,
success = { response ->
val itemsCountSufficient = response.size == LOAD_COUNT
canPaginate.setValue { itemsCountSufficient }
@@ -258,9 +282,17 @@ class ConversationsViewModelImpl(
val paginationExhausted = !itemsCountSufficient &&
screenState.value.conversations.isNotEmpty()
imagesToPreload.setValue {
val imagesToPreload =
response.mapNotNull { it.extractAvatar().extractUrl() }
imagesToPreload.forEach { url ->
imageLoader.enqueue(
ImageRequest.Builder(applicationContext)
.data(url)
.build()
)
}
conversationsUseCase.storeConversations(response)
val loadedConversations = response.map {
@@ -298,6 +330,40 @@ 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(
@@ -314,7 +380,12 @@ class ConversationsViewModelImpl(
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map { it.asPresentation(resources) }
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
)
}
}
@@ -350,13 +421,40 @@ 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) { // диалога нет в списке
// pizdets
// TODO: 04/07/2024, Danil Nikolaev: load conversation and store info
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()
)
}
)
}
}
)
}
} else {
val conversation = newConversations[conversationIndex]
var newConversation = conversation.copy(
@@ -397,7 +495,12 @@ class ConversationsViewModelImpl(
screenState.setValue { old ->
old.copy(
conversations = newConversations.map { it.asPresentation(resources) }
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
)
}
}
@@ -421,7 +524,12 @@ class ConversationsViewModelImpl(
screenState.setValue { old ->
old.copy(
conversations = newConversations.map { it.asPresentation(resources) }
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
)
}
}
@@ -431,20 +539,29 @@ class ConversationsViewModelImpl(
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
newConversations.indexOfFirstOrNull { it.id == event.peerId }
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(
inRead = event.messageId,
unreadCount = event.unreadCount
)
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(
inRead = event.messageId,
unreadCount = event.unreadCount
)
conversations.update { newConversations }
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map { it.asPresentation(resources) }
)
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
)
}
}
}
@@ -452,19 +569,28 @@ class ConversationsViewModelImpl(
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
newConversations.indexOfFirstOrNull { it.id == event.peerId }
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(
outRead = event.messageId,
unreadCount = event.unreadCount
)
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationIndex] =
newConversations[conversationIndex].copy(
outRead = event.messageId,
unreadCount = event.unreadCount
)
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map { it.asPresentation(resources) }
)
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
)
}
}
}
@@ -473,34 +599,43 @@ class ConversationsViewModelImpl(
val newConversations = conversations.value.toMutableList()
val conversationIndex =
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
newConversations.indexOfFirstOrNull { it.id == event.peerId }
val pin = event.majorId > 0
val conversation = newConversations[conversationIndex].copy(majorId = event.majorId)
newConversations.removeAt(conversationIndex)
if (pin) {
newConversations.add(0, conversation)
if (conversationIndex == null) { // диалога нет в списке
// pizdets
} else {
pinnedCount -= 1
val pin = event.majorId > 0
newConversations.add(conversation)
val conversation = newConversations[conversationIndex].copy(majorId = event.majorId)
val pinnedSubList = newConversations.filter(VkConversation::isPinned)
val unpinnedSubList = newConversations
.filterNot(VkConversation::isPinned)
.sortedByDescending { it.lastMessage?.date }
newConversations.removeAt(conversationIndex)
newConversations.clear()
newConversations += pinnedSubList + unpinnedSubList
}
if (pin) {
newConversations.add(0, conversation)
} else {
pinnedCount -= 1
conversations.update { newConversations }
newConversations.add(conversation)
screenState.setValue { old ->
old.copy(conversations = newConversations.map { it.asPresentation(resources) })
val pinnedSubList = newConversations.filter(VkConversation::isPinned)
val unpinnedSubList = newConversations
.filterNot(VkConversation::isPinned)
.sortedByDescending { it.lastMessage?.date }
newConversations.clear()
newConversations += pinnedSubList + unpinnedSubList
}
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
})
}
}
}
@@ -520,44 +655,53 @@ class ConversationsViewModelImpl(
val newConversations = conversations.value.toMutableList()
val conversationAndIndex =
newConversations.findWithIndex { it.id == peerId } ?: return
newConversations.findWithIndex { it.id == peerId }
newConversations[conversationAndIndex.first] =
conversationAndIndex.second.copy(
interactionType = interactionType.value,
interactionIds = userIds
)
if (conversationAndIndex == null) { // диалога нет в списке
// pizdets
} else {
newConversations[conversationAndIndex.first] =
conversationAndIndex.second.copy(
interactionType = interactionType.value,
interactionIds = userIds
)
conversations.update { newConversations }
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map { it.asPresentation(resources) }
)
}
interactionsTimers[peerId]?.let { interactionJob ->
if (interactionJob.interactionType == interactionType) {
interactionJob.timerJob.cancel(NewInteractionException)
screenState.setValue { old ->
old.copy(
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
)
}
}
var timeoutAction: (() -> Unit)? = null
interactionsTimers[peerId]?.let { interactionJob ->
if (interactionJob.interactionType == interactionType) {
interactionJob.timerJob.cancel(NewInteractionException)
}
}
val timerJob = createTimerFlow(
time = 5,
onTimeoutAction = { timeoutAction?.invoke() }
).launchIn(viewModelScope)
var timeoutAction: (() -> Unit)? = null
val newInteractionJob = InteractionJob(
interactionType = interactionType,
timerJob = timerJob
)
val timerJob = createTimerFlow(
time = 5,
onTimeoutAction = { timeoutAction?.invoke() }
).launchIn(viewModelScope)
interactionsTimers[peerId] = newInteractionJob
val newInteractionJob = InteractionJob(
interactionType = interactionType,
timerJob = timerJob
)
timeoutAction = {
stopInteraction(peerId, newInteractionJob)
interactionsTimers[peerId] = newInteractionJob
timeoutAction = {
stopInteraction(peerId, newInteractionJob)
}
}
}
@@ -577,7 +721,12 @@ class ConversationsViewModelImpl(
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map { it.asPresentation(resources) }
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
)
}
@@ -606,7 +755,12 @@ class ConversationsViewModelImpl(
conversations.update { newConversations }
screenState.setValue { old ->
old.copy(
conversations = newConversations.map { it.asPresentation(resources) }
conversations = newConversations.map {
it.asPresentation(
resources = resources,
useContactName = useContactNames()
)
}
)
}
}
@@ -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)
}
@@ -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<Int>,
navController: NavController,
) {
composable<Conversations> {
val viewModel: ConversationsViewModel =
it.sharedViewModel<ConversationsViewModelImpl>(navController = navController)
viewModel.setScrollToTopFlow(scrollToTopFlow)
ConversationsRoute(
onError = onError,
@@ -54,7 +54,6 @@ 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
@@ -65,8 +64,6 @@ 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
@@ -96,22 +93,10 @@ 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 imagesToPreload by viewModel.imagesToPreload.collectAsStateWithLifecycle()
LaunchedEffect(imagesToPreload) {
imagesToPreload.forEach { url ->
context.imageLoader.enqueue(
ImageRequest.Builder(context)
.data(url)
.build()
)
}
}
val isNeedToScrollToTop by viewModel.scrollToTop.collectAsStateWithLifecycle()
ConversationsScreen(
screenState = screenState,
@@ -129,7 +114,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 +143,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 +159,14 @@ fun ConversationsScreen(
initialFirstVisibleItemScrollOffset = screenState.scrollOffset
)
LaunchedEffect(isNeedToScrollToTop) {
if (isNeedToScrollToTop) {
listState.scrollToItem(0)
onScrolledToTop()
}
}
LaunchedEffect(listState) {
snapshotFlow { listState.firstVisibleItemIndex }
.debounce(500L)
@@ -310,7 +307,7 @@ fun ConversationsScreen(
) {
FloatingActionButton(
onClick = {
if (AppSettings.Debug.enableHaptic) {
if (AppSettings.General.enableHaptic) {
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
}
scope.launch {
@@ -342,12 +339,24 @@ fun ConversationsScreen(
}
) { padding ->
when {
baseError is BaseError.SessionExpired -> {
ErrorView(
text = "Session expired",
buttonText = "Log out",
onButtonClick = onSessionExpiredLogOutButtonClicked
)
baseError != null -> {
when (baseError) {
is BaseError.SessionExpired -> {
ErrorView(
text = "Session expired",
buttonText = "Log out",
onButtonClick = onSessionExpiredLogOutButtonClicked
)
}
is BaseError.SimpleError -> {
ErrorView(
text = baseError.message,
buttonText = "Try again",
onButtonClick = onRefresh
)
}
}
}
screenState.isLoading && screenState.conversations.isEmpty() -> FullScreenLoader()
@@ -33,7 +33,7 @@ import dev.meloda.fast.ui.R as UiR
fun VkConversation.asPresentation(
resources: Resources,
useContactName: Boolean = false
useContactName: Boolean
): UiConversation = UiConversation(
id = id,
lastMessageId = lastMessageId,
@@ -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))
}
}
}
@@ -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)
}
@@ -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 {
@@ -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
@@ -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
}
@@ -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
)
}
}
@@ -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 {
@@ -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<UiItem>,
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(
@@ -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
@@ -123,7 +123,7 @@ fun TopBar(
}
},
actions = {
// IconButton(
// IconButton.kt(
// onClick = { dropdownMenuShown = true }
// ) {
// Icon(
@@ -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 {
@@ -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.Resource(UiR.string.settings_general_show_emoji_button_title),
text = UiText.Resource(UiR.string.settings_general_show_emoji_button_summary),
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)
)
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.Resource(UiR.string.settings_appearance_use_system_font_title)
)
val appearanceLanguage = SettingsItem.TitleText(
key = SettingsKeys.KEY_APPEARANCE_LANGUAGE,
title = UiText.Resource(UiR.string.settings_application_language),
@@ -374,6 +377,35 @@ class SettingsViewModelImpl(
text = UiText.Resource(UiR.string.settings_activity_send_online_summary)
)
val experimentalTitle = SettingsItem.Title(
key = "experimental",
title = UiText.Resource(UiR.string.settings_experimental_title)
)
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.Resource(UiR.string.settings_features_show_time_in_action_messages_title)
)
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
)
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)
)
val debugTitle = SettingsItem.Title(
key = "debug",
title = UiText.Resource(UiR.string.settings_debug_title)
@@ -389,34 +421,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 +444,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 +456,8 @@ class SettingsViewModelImpl(
val generalList = listOf(
generalTitle,
generalUseContactNames,
generalEnablePullToRefresh
generalShowEmojiButton,
generalEnableHaptic
)
val appearanceList = listOf(
appearanceTitle,
@@ -466,6 +465,7 @@ class SettingsViewModelImpl(
appearanceDarkTheme,
appearanceUseAmoledDarkTheme,
appearanceUseDynamicColors,
appearanceUseSystemFont,
appearanceLanguage
)
val featuresList = listOf(
@@ -476,18 +476,19 @@ class SettingsViewModelImpl(
activityTitle,
visibilitySendOnlineStatus,
)
val experimentalList = listOf(
experimentalTitle,
experimentalLongPollBackground,
experimentalShowTimeInActionMessages,
experimentalUseBlur,
enableAnimations
)
val debugList = mutableListOf<SettingsItem<*>>()
listOf(
debugTitle,
debugPerformCrash,
debugShowCrashAlert,
debugLongPollBackground,
debugUseBlur,
debugShowEmojiButton,
debugShowTimeInActionMessages,
debugEnableHaptic,
debugNetworkLogLevel,
debugUseSystemFont
).forEach(debugList::add)
debugList += debugHideDebugList
@@ -499,6 +500,7 @@ class SettingsViewModelImpl(
appearanceList,
featuresList,
visibilityList,
experimentalList,
debugList,
).forEach(settingsList::addAll)
@@ -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
@@ -113,7 +113,7 @@ fun SettingsScreen(
LaunchedEffect(hapticType) {
if (hapticType != null) {
if (AppSettings.Debug.enableHaptic) {
if (AppSettings.General.enableHaptic) {
view.performHapticFeedback(hapticType.getHaptic())
}
onHapticPerformed()
+14 -14
View File
@@ -2,25 +2,25 @@
minSdk = "23"
targetSdk = "35"
compileSdk = "35"
versionCode = "6"
versionName = "0.1.3"
versionCode = "9"
versionName = "0.1.6"
agp = "8.7.3"
agp = "8.9.0"
converterMoshi = "2.11.0"
eithernet = "2.0.0"
haze = "1.1.1"
kotlin = "2.1.0"
ksp = "2.1.0-1.0.29"
haze = "1.5.1"
kotlin = "2.1.10"
ksp = "2.1.10-1.0.31"
compose-bom = "2024.12.01"
koin = "4.0.0"
compose-bom = "2025.03.00"
koin = "4.0.2"
accompanist = "0.37.0"
accompanist = "0.37.2"
coil = "2.7.0"
coroutines = "1.9.0"
coroutines = "1.10.1"
junit = "4.13.2"
chucker = "4.1.0"
guava = "33.3.1-jre"
guava = "33.4.5-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.5"
serialization = "1.7.3"
androidx-navigation = "2.8.9"
serialization = "1.8.0"
rebugger = "1.0.0-rc03"
moduleGraph = "2.7.1"
moduleGraph = "2.8.0"
[libraries]
accompanist-permissions = { module = "com.google.accompanist:accompanist-permissions", version.ref = "accompanist" }
Binary file not shown.
+3 -2
View File
@@ -1,6 +1,7 @@
#Mon Oct 28 18:41:43 MSK 2024
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.9-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored
+31 -13
View File
@@ -15,6 +15,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@@ -55,7 +57,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@@ -80,13 +82,12 @@ do
esac
done
APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
APP_NAME="Gradle"
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# 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"'
# 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
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@@ -133,22 +134,29 @@ location of your Java installation."
fi
else
JAVACMD=java
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
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.
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
@@ -193,11 +201,15 @@ if "$cygwin" || "$msys" ; then
done
fi
# 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.
# 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.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
@@ -205,6 +217,12 @@ 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
+21 -16
View File
@@ -13,8 +13,10 @@
@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
@@ -25,7 +27,8 @@
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@@ -40,13 +43,13 @@ if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
if %ERRORLEVEL% equ 0 goto execute
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.
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
goto fail
@@ -56,11 +59,11 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
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.
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
goto fail
@@ -75,13 +78,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
if %ERRORLEVEL% equ 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!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
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%
:mainEnd
if "%OS%"=="Windows_NT" endlocal