forked from melod1n/fast-messenger
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| bb40d1b36c | |||
| 0eb3146428 | |||
| e645448852 | |||
| 314ff806c0 | |||
| 3beb382334 | |||
| 5b5ba747d8 | |||
| b8937a1590 | |||
| b2879d8756 | |||
| a4feb8978f | |||
| 79f539a27b | |||
| 4cc6ec6b5d | |||
| 36a119ffa9 | |||
| 1a78a51017 | |||
| cbe3313b87 | |||
| 30e132d418 | |||
| 7e5843759d | |||
| 7c14df1824 | |||
| 82695ccf6f |
@@ -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
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -77,6 +77,7 @@ dependencies {
|
||||
implementation(projects.feature.friends)
|
||||
implementation(projects.feature.profile)
|
||||
implementation(projects.feature.photoviewer)
|
||||
implementation(projects.feature.createchat)
|
||||
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import dev.meloda.fast.common.provider.Provider
|
||||
import dev.meloda.fast.common.provider.ResourceProvider
|
||||
import dev.meloda.fast.common.provider.ResourceProviderImpl
|
||||
import dev.meloda.fast.conversations.di.conversationsModule
|
||||
import dev.meloda.fast.conversations.di.createChatModule
|
||||
import dev.meloda.fast.domain.di.domainModule
|
||||
import dev.meloda.fast.friends.di.friendsModule
|
||||
import dev.meloda.fast.languagepicker.di.languagePickerModule
|
||||
@@ -26,8 +27,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
|
||||
@@ -46,7 +47,8 @@ val applicationModule = module {
|
||||
longPollModule,
|
||||
friendsModule,
|
||||
profileModule,
|
||||
chatMaterialsModule
|
||||
chatMaterialsModule,
|
||||
createChatModule
|
||||
)
|
||||
|
||||
// TODO: 14/05/2024, Danil Nikolaev: research on memory leaks and potentials errors
|
||||
@@ -61,7 +63,7 @@ val applicationModule = module {
|
||||
qualifier = qualifier("main")
|
||||
}
|
||||
|
||||
single {
|
||||
single<ImageLoader> {
|
||||
ImageLoader.Builder(get())
|
||||
.crossfade(true)
|
||||
.build()
|
||||
|
||||
@@ -24,6 +24,8 @@ fun NavGraphBuilder.mainScreen(
|
||||
onSettingsButtonClicked: () -> Unit,
|
||||
onConversationClicked: (conversationId: Int) -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userId: Int) -> Unit,
|
||||
onCreateChatClicked: () -> Unit,
|
||||
viewModel: MainViewModel
|
||||
) {
|
||||
val navigationItems = ImmutableList.of(
|
||||
@@ -54,6 +56,8 @@ fun NavGraphBuilder.mainScreen(
|
||||
onSettingsButtonClicked = onSettingsButtonClicked,
|
||||
onConversationItemClicked = onConversationClicked,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked,
|
||||
onCreateChatClicked = onCreateChatClicked,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import androidx.navigation.compose.navigation
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import coil.compose.SubcomposeAsyncImage
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeChild
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.MainViewModel
|
||||
@@ -56,6 +56,8 @@ fun MainScreen(
|
||||
onSettingsButtonClicked: () -> Unit = {},
|
||||
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
||||
onPhotoClicked: (url: String) -> Unit = {},
|
||||
onMessageClicked: (userId: Int) -> Unit = {},
|
||||
onCreateChatClicked: () -> Unit = {},
|
||||
viewModel: MainViewModel
|
||||
) {
|
||||
val currentTheme = LocalThemeConfig.current
|
||||
@@ -74,7 +76,7 @@ fun MainScreen(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeChild(
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
@@ -156,17 +158,23 @@ 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,
|
||||
onPhotoClicked = onPhotoClicked
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onMessageClicked = onMessageClicked
|
||||
)
|
||||
conversationsScreen(
|
||||
onError = onError,
|
||||
onConversationItemClicked = onConversationItemClicked,
|
||||
onPhotoClicked = onPhotoClicked,
|
||||
onCreateChatClicked = onCreateChatClicked,
|
||||
navController = navController,
|
||||
onPhotoClicked = onPhotoClicked
|
||||
)
|
||||
profileScreen(
|
||||
onError = onError,
|
||||
|
||||
@@ -25,6 +25,8 @@ import dev.meloda.fast.auth.authNavGraph
|
||||
import dev.meloda.fast.auth.navigateToAuth
|
||||
import dev.meloda.fast.chatmaterials.navigation.chatMaterialsScreen
|
||||
import dev.meloda.fast.chatmaterials.navigation.navigateToChatMaterials
|
||||
import dev.meloda.fast.conversations.navigation.createChatScreen
|
||||
import dev.meloda.fast.conversations.navigation.navigateToCreateChat
|
||||
import dev.meloda.fast.languagepicker.navigation.languagePickerScreen
|
||||
import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker
|
||||
import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen
|
||||
@@ -124,6 +126,8 @@ fun RootScreen(
|
||||
onSettingsButtonClicked = navController::navigateToSettings,
|
||||
onConversationClicked = navController::navigateToMessagesHistory,
|
||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
|
||||
onMessageClicked = navController::navigateToMessagesHistory,
|
||||
onCreateChatClicked = navController::navigateToCreateChat,
|
||||
viewModel = viewModel
|
||||
)
|
||||
|
||||
@@ -136,6 +140,13 @@ fun RootScreen(
|
||||
onBack = navController::navigateUp,
|
||||
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
|
||||
)
|
||||
createChatScreen(
|
||||
onChatCreated = { conversationId ->
|
||||
navController.popBackStack()
|
||||
navController.navigateToMessagesHistory(conversationId)
|
||||
},
|
||||
navController = navController
|
||||
)
|
||||
|
||||
settingsScreen(
|
||||
onBack = navController::navigateUp,
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -16,11 +16,11 @@ import dev.meloda.fast.common.LongPollController
|
||||
import dev.meloda.fast.common.VkConstants
|
||||
import dev.meloda.fast.common.extensions.listenValue
|
||||
import dev.meloda.fast.common.model.LongPollState
|
||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
||||
import dev.meloda.fast.domain.LongPollUseCase
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.domain.LongPollUpdatesParser
|
||||
import dev.meloda.fast.domain.LongPollUseCase
|
||||
import dev.meloda.fast.model.api.data.LongPollUpdates
|
||||
import dev.meloda.fast.model.api.data.VkLongPollData
|
||||
import dev.meloda.fast.ui.R
|
||||
@@ -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,
|
||||
@@ -249,6 +249,7 @@ class LongPollingService : Service() {
|
||||
override fun onDestroy() {
|
||||
Log.d(STATE_TAG, "onDestroy")
|
||||
longPollController.updateCurrentState(LongPollState.Stopped)
|
||||
updatesParser.clearListeners()
|
||||
try {
|
||||
AppSettings.edit { putBoolean(KEY_LONG_POLL_WAS_DESTROYED, true) }
|
||||
job.cancel()
|
||||
@@ -258,10 +259,9 @@ class LongPollingService : Service() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
Log.d(STATE_TAG, "onLowMemory")
|
||||
longPollController.updateCurrentState(LongPollState.Stopped)
|
||||
super.onLowMemory()
|
||||
override fun onTrimMemory(level: Int) {
|
||||
Log.d(STATE_TAG, "onTrimMemory. Level: $level")
|
||||
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,7 +75,8 @@ fun <T : Any> ApiResult<T, RestApiErrorDomain>.mapToState() = when (this) {
|
||||
is ApiResult.Failure.ApiFailure -> this.error.toStateApiError()
|
||||
}
|
||||
|
||||
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) = when (this) {
|
||||
fun <T : Any, N> ApiResult<T, RestApiErrorDomain>.mapToState(successMapper: (T) -> N) =
|
||||
when (this) {
|
||||
is ApiResult.Success -> State.Success(successMapper(this.value))
|
||||
|
||||
is ApiResult.Failure.NetworkFailure -> State.Error.ConnectionError
|
||||
|
||||
+5
-1
@@ -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>
|
||||
|
||||
+40
-1
@@ -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))
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
package dev.meloda.fast.data.api.messages
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.network.RestApiErrorDomain
|
||||
import com.slack.eithernet.ApiResult
|
||||
|
||||
interface MessagesRepository {
|
||||
|
||||
@@ -41,6 +41,11 @@ interface MessagesRepository {
|
||||
conversationMessageId: Int
|
||||
): ApiResult<List<VkAttachmentHistoryMessage>, RestApiErrorDomain>
|
||||
|
||||
suspend fun createChat(
|
||||
userIds: List<Int>?,
|
||||
title: String?
|
||||
): ApiResult<Int, RestApiErrorDomain>
|
||||
|
||||
suspend fun storeMessages(messages: List<VkMessage>)
|
||||
|
||||
// suspend fun markAsImportant(
|
||||
|
||||
+19
-1
@@ -1,5 +1,6 @@
|
||||
package dev.meloda.fast.data.api.messages
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.meloda.fast.common.VkConstants
|
||||
import dev.meloda.fast.data.VkGroupsMap
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
@@ -14,6 +15,7 @@ import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.model.api.domain.asEntity
|
||||
import dev.meloda.fast.model.api.requests.MessagesCreateChatRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest
|
||||
@@ -23,7 +25,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.messages.MessagesService
|
||||
import com.slack.eithernet.ApiResult
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@@ -198,6 +199,23 @@ class MessagesRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun createChat(
|
||||
userIds: List<Int>?,
|
||||
title: String?
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = MessagesCreateChatRequest(
|
||||
userIds = userIds,
|
||||
title = title
|
||||
)
|
||||
|
||||
messagesService.createChat(requestModel.map).mapApiResult(
|
||||
successMapper = { apiResponse ->
|
||||
apiResponse.requireResponse().chatId
|
||||
},
|
||||
errorMapper = { error -> error?.toDomain() }
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun storeMessages(messages: List<VkMessage>) {
|
||||
messageDao.insertAll(messages.map(VkMessage::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
|
||||
}
|
||||
|
||||
@@ -20,4 +20,8 @@ class GetLocalUserByIdUseCase(private val repository: UsersRepository) {
|
||||
|
||||
emit(newState)
|
||||
}
|
||||
|
||||
suspend fun proceed(userId: Int): VkUser? {
|
||||
return repository.getLocalUsers(userIds = listOf(userId)).singleOrNull()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,15 @@ import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.model.ApiEvent
|
||||
import dev.meloda.fast.model.InteractionType
|
||||
import dev.meloda.fast.model.LongPollEvent
|
||||
import dev.meloda.fast.model.LongPollParsedEvent
|
||||
import dev.meloda.fast.model.MessageFlags
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
@@ -25,8 +29,9 @@ class LongPollUpdatesParser(
|
||||
) {
|
||||
private val job = SupervisorJob()
|
||||
|
||||
private val exceptionHandler = CoroutineExceptionHandler { _, throwable ->
|
||||
Log.d("LongPollUpdatesParser", "error: $throwable")
|
||||
private val exceptionHandler =
|
||||
CoroutineExceptionHandler { _, throwable ->
|
||||
Log.e("LongPollUpdatesParser", "error: $throwable")
|
||||
throwable.printStackTrace()
|
||||
}
|
||||
|
||||
@@ -35,29 +40,26 @@ class LongPollUpdatesParser(
|
||||
|
||||
private val coroutineScope = CoroutineScope(coroutineContext)
|
||||
|
||||
private val listenersMap: MutableMap<ApiEvent, MutableCollection<VkEventCallback<*>>> =
|
||||
private val listenersMap: MutableMap<LongPollEvent, MutableList<VkEventCallback<LongPollParsedEvent>>> =
|
||||
mutableMapOf()
|
||||
|
||||
fun parseNextUpdate(event: List<Any>) {
|
||||
val eventId = event.first().asInt()
|
||||
|
||||
val eventType: ApiEvent = try {
|
||||
ApiEvent.parse(eventId)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
|
||||
return
|
||||
}
|
||||
when (val eventType = ApiEvent.parseOrNull(eventId)) {
|
||||
null -> Log.d("LongPollUpdatesParser", "parseNextUpdate: unknownEvent: $event")
|
||||
|
||||
when (eventType) {
|
||||
ApiEvent.MESSAGE_SET_FLAGS -> parseMessageSetFlags(eventType, event)
|
||||
ApiEvent.MESSAGE_CLEAR_FLAGS -> parseMessageClearFlags(eventType, event)
|
||||
ApiEvent.MESSAGE_NEW -> parseMessageNew(eventType, event)
|
||||
ApiEvent.MESSAGE_EDIT -> parseMessageEdit(eventType, event)
|
||||
ApiEvent.MESSAGE_READ_INCOMING -> parseMessageReadIncoming(eventType, event)
|
||||
ApiEvent.MESSAGE_READ_OUTGOING -> parseMessageReadOutgoing(eventType, event)
|
||||
ApiEvent.CHAT_CLEAR_FLAGS -> parseChatClearFlags(eventType, event)
|
||||
ApiEvent.CHAT_SET_FLAGS -> parseChatSetFlags(eventType, event)
|
||||
ApiEvent.MESSAGES_DELETED -> parseMessagesDeleted(eventType, event)
|
||||
ApiEvent.PIN_UNPIN_CONVERSATION -> parseConversationPinStateChanged(eventType, event)
|
||||
ApiEvent.CHAT_MAJOR_CHANGED -> parseChatMajorChanged(eventType, event)
|
||||
ApiEvent.CHAT_MINOR_CHANGED -> parseChatMinorChanged(eventType, event)
|
||||
|
||||
ApiEvent.TYPING,
|
||||
ApiEvent.AUDIO_MESSAGE_RECORDING,
|
||||
@@ -65,14 +67,10 @@ class LongPollUpdatesParser(
|
||||
ApiEvent.VIDEO_UPLOADING,
|
||||
ApiEvent.FILE_UPLOADING -> parseInteraction(eventType, event)
|
||||
|
||||
ApiEvent.UNREAD_COUNT_UPDATE -> onNewEvent(eventType, event)
|
||||
ApiEvent.UNREAD_COUNT_UPDATE -> parseUnreadCounterUpdate(eventType, event)
|
||||
}
|
||||
}
|
||||
|
||||
private fun onNewEvent(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "newEvent: $eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseInteraction(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
|
||||
@@ -85,6 +83,15 @@ class LongPollUpdatesParser(
|
||||
else -> return
|
||||
}
|
||||
|
||||
val longPollEvent: LongPollEvent = when (eventType) {
|
||||
ApiEvent.TYPING -> LongPollEvent.TYPING
|
||||
ApiEvent.AUDIO_MESSAGE_RECORDING -> LongPollEvent.AUDIO_MESSAGE_RECORDING
|
||||
ApiEvent.PHOTO_UPLOADING -> LongPollEvent.PHOTO_UPLOADING
|
||||
ApiEvent.VIDEO_UPLOADING -> LongPollEvent.VIDEO_UPLOADING
|
||||
ApiEvent.FILE_UPLOADING -> LongPollEvent.FILE_UPLOADING
|
||||
else -> return
|
||||
}
|
||||
|
||||
val peerId = event[1].asInt()
|
||||
val userIds = event[2].toList(Any::asInt).filter { it != UserConfig.userId }
|
||||
val totalCount = event[3].asInt()
|
||||
@@ -93,12 +100,11 @@ class LongPollUpdatesParser(
|
||||
// if userIds contains only account's id, then we don't need to show our status
|
||||
if (userIds.isEmpty()) return
|
||||
|
||||
coroutineScope.launch {
|
||||
listenersMap[eventType]?.let { listeners ->
|
||||
listenersMap[longPollEvent]?.let { listeners ->
|
||||
listeners.forEach { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.Interaction>)
|
||||
(vkEventCallback as VkEventCallback<LongPollParsedEvent.Interaction>)
|
||||
.onEvent(
|
||||
LongPollEvent.Interaction(
|
||||
LongPollParsedEvent.Interaction(
|
||||
interactionType = interactionType,
|
||||
peerId = peerId,
|
||||
userIds = userIds,
|
||||
@@ -109,35 +115,212 @@ class LongPollUpdatesParser(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseConversationPinStateChanged(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
private fun parseUnreadCounterUpdate(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType $event")
|
||||
|
||||
val peerId = event[1].asInt()
|
||||
val majorId = event[2].asInt()
|
||||
val unreadCount = event[1].asInt()
|
||||
val unreadUnmutedCount = event[2].asInt()
|
||||
val showOnlyMuted = event[3].asInt() == 1
|
||||
val businessNotifyUnreadCount = event[4].asInt()
|
||||
val archiveUnreadCount = event[7].asInt()
|
||||
val archiveUnreadUnmutedCount = event[8].asInt()
|
||||
val archiveMentionsCount = event[9].asInt()
|
||||
|
||||
coroutineScope.launch {
|
||||
listenersMap[ApiEvent.PIN_UNPIN_CONVERSATION]?.let { listeners ->
|
||||
listenersMap[LongPollEvent.UNREAD_COUNTER_UPDATE]?.let { listeners ->
|
||||
listeners.forEach { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>)
|
||||
(vkEventCallback as VkEventCallback<LongPollParsedEvent.UnreadCounter>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkConversationPinStateChangedEvent(
|
||||
peerId = peerId,
|
||||
majorId = majorId
|
||||
LongPollParsedEvent.UnreadCounter(
|
||||
unread = unreadCount,
|
||||
unreadUnmuted = unreadUnmutedCount,
|
||||
showOnlyMuted = showOnlyMuted,
|
||||
business = businessNotifyUnreadCount,
|
||||
archive = archiveUnreadCount,
|
||||
archiveUnmuted = archiveUnreadUnmutedCount,
|
||||
archiveMentions = archiveMentionsCount
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageSetFlags(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
|
||||
val messageId = event[1].asInt()
|
||||
val flags = event[2].asInt()
|
||||
val peerId = event[3].asInt()
|
||||
|
||||
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
||||
|
||||
val parsedFlags = MessageFlags.parse(flags)
|
||||
parsedFlags.forEach { flag ->
|
||||
when (flag) {
|
||||
MessageFlags.IMPORTANT -> { // marked as important
|
||||
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
marked = true
|
||||
)
|
||||
eventsToSend += eventToSend
|
||||
|
||||
listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsImportant>)
|
||||
?.onEvent(eventToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageFlags.SPAM -> { // marked as spam
|
||||
val eventToSend = LongPollParsedEvent.MessageMarkedAsSpam(
|
||||
peerId = peerId,
|
||||
messageId = messageId
|
||||
)
|
||||
eventsToSend += eventToSend
|
||||
|
||||
listenersMap[LongPollEvent.MARKED_AS_SPAM]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsSpam>)
|
||||
?.onEvent(eventToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageFlags.DELETED -> {
|
||||
val eventToSend =
|
||||
if (parsedFlags.contains(MessageFlags.DELETED_FOR_ALL)) { // deleted for all
|
||||
LongPollParsedEvent.MessageDeleted(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
forAll = true
|
||||
)
|
||||
} else { // deleted only for me
|
||||
LongPollParsedEvent.MessageDeleted(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
forAll = false
|
||||
)
|
||||
}
|
||||
eventsToSend += eventToSend
|
||||
|
||||
listenersMap[LongPollEvent.MESSAGE_DELETED]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageDeleted>)
|
||||
?.onEvent(eventToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageFlags.AUDIO_LISTENED -> { // audio message listened
|
||||
val eventToSend = LongPollParsedEvent.AudioMessageListened(
|
||||
peerId = peerId,
|
||||
messageId = messageId
|
||||
)
|
||||
eventsToSend += eventToSend
|
||||
|
||||
listenersMap[LongPollEvent.AUDIO_MESSAGE_LISTENED]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.AudioMessageListened>)
|
||||
?.onEvent(eventToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
eventsToSend.forEach { eventToSend ->
|
||||
listenersMap[LongPollEvent.MESSAGE_SET_FLAGS]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as? VkEventCallback<LongPollParsedEvent>)?.onEvent(eventToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageClearFlags(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
|
||||
val messageId = event[1].asInt()
|
||||
val flags = event[2].asInt()
|
||||
val peerId = event[3].asInt()
|
||||
|
||||
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
||||
|
||||
val parsedFlags = MessageFlags.parse(flags)
|
||||
|
||||
coroutineScope.launch {
|
||||
parsedFlags.forEach { flag ->
|
||||
when (flag) {
|
||||
MessageFlags.IMPORTANT -> { // not important anymore
|
||||
val eventToSend = LongPollParsedEvent.MessageMarkedAsImportant(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
marked = false
|
||||
)
|
||||
eventsToSend += eventToSend
|
||||
|
||||
listenersMap[LongPollEvent.MARKED_AS_IMPORTANT]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsImportant>)
|
||||
?.onEvent(eventToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageFlags.SPAM -> {
|
||||
if (parsedFlags.contains(MessageFlags.CANCEL_SPAM)) { // not spam anymore
|
||||
withContext(Dispatchers.IO) {
|
||||
val message = loadMessage(messageId)
|
||||
message?.let {
|
||||
val eventToSend =
|
||||
LongPollParsedEvent.MessageMarkedAsNotSpam(message = message)
|
||||
eventsToSend += eventToSend
|
||||
|
||||
listenersMap[LongPollEvent.MARKED_AS_NOT_SPAM]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageMarkedAsNotSpam>)
|
||||
?.onEvent(eventToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MessageFlags.DELETED -> { // restored
|
||||
withContext(Dispatchers.IO) {
|
||||
val message = loadMessage(messageId)
|
||||
message?.let {
|
||||
val eventToSend =
|
||||
LongPollParsedEvent.MessageRestored(message = message)
|
||||
eventsToSend += eventToSend
|
||||
|
||||
listenersMap[LongPollEvent.MESSAGE_RESTORED]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as? VkEventCallback<LongPollParsedEvent.MessageRestored>)
|
||||
?.onEvent(eventToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
eventsToSend.forEach { eventToSend ->
|
||||
listenersMap[LongPollEvent.MESSAGE_CLEAR_FLAGS]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
vkEventCallback.onEvent(eventToSend)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageNew(eventType: ApiEvent, event: List<Any>) {
|
||||
@@ -145,17 +328,11 @@ class LongPollUpdatesParser(
|
||||
val messageId = event[1].asInt()
|
||||
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
val newMessageEvent: LongPollEvent.VkMessageNewEvent? =
|
||||
loadNormalMessage(
|
||||
eventType,
|
||||
messageId
|
||||
)
|
||||
|
||||
newMessageEvent?.let { event ->
|
||||
listenersMap[ApiEvent.MESSAGE_NEW]?.let {
|
||||
loadMessage(messageId)?.let { message ->
|
||||
listenersMap[LongPollEvent.MESSAGE_NEW]?.let {
|
||||
it.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageNewEvent>)
|
||||
.onEvent(event)
|
||||
(vkEventCallback as VkEventCallback<LongPollParsedEvent.NewMessage>)
|
||||
.onEvent(LongPollParsedEvent.NewMessage(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -166,18 +343,12 @@ class LongPollUpdatesParser(
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
val messageId = event[1].asInt()
|
||||
|
||||
coroutineScope.launch {
|
||||
val editedMessageEvent: LongPollEvent.VkMessageEditEvent? =
|
||||
loadNormalMessage(
|
||||
eventType,
|
||||
messageId
|
||||
)
|
||||
|
||||
editedMessageEvent?.let { event ->
|
||||
listenersMap[ApiEvent.MESSAGE_EDIT]?.let {
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
loadMessage(messageId)?.let { message ->
|
||||
listenersMap[LongPollEvent.MESSAGE_EDITED]?.let {
|
||||
it.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageEditEvent>)
|
||||
.onEvent(event)
|
||||
(vkEventCallback as VkEventCallback<LongPollParsedEvent.MessageEdited>)
|
||||
.onEvent(LongPollParsedEvent.MessageEdited(message))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -190,12 +361,11 @@ class LongPollUpdatesParser(
|
||||
val messageId = event[2].asInt()
|
||||
val unreadCount = event[3].asInt()
|
||||
|
||||
coroutineScope.launch {
|
||||
listenersMap[ApiEvent.MESSAGE_READ_INCOMING]?.let { listeners ->
|
||||
listenersMap[LongPollEvent.INCOMING_MESSAGE_READ]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>)
|
||||
(vkEventCallback as VkEventCallback<LongPollParsedEvent.IncomingMessageRead>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkMessageReadIncomingEvent(
|
||||
LongPollParsedEvent.IncomingMessageRead(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
unreadCount = unreadCount
|
||||
@@ -204,7 +374,6 @@ class LongPollUpdatesParser(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseMessageReadOutgoing(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
@@ -212,12 +381,11 @@ class LongPollUpdatesParser(
|
||||
val messageId = event[2].asInt()
|
||||
val unreadCount = event[3].asInt()
|
||||
|
||||
coroutineScope.launch {
|
||||
listenersMap[ApiEvent.MESSAGE_READ_OUTGOING]?.let { listeners ->
|
||||
listenersMap[LongPollEvent.OUTGOING_MESSAGE_READ]?.let { listeners ->
|
||||
listeners.map { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>)
|
||||
(vkEventCallback as VkEventCallback<LongPollParsedEvent.OutgoingMessageRead>)
|
||||
.onEvent(
|
||||
LongPollEvent.VkMessageReadOutgoingEvent(
|
||||
LongPollParsedEvent.OutgoingMessageRead(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
unreadCount = unreadCount
|
||||
@@ -226,16 +394,73 @@ class LongPollUpdatesParser(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChatClearFlags(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseChatSetFlags(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
}
|
||||
|
||||
private fun parseMessagesDeleted(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
|
||||
val peerId = event[1].asInt()
|
||||
val messageId = event[2].asInt()
|
||||
|
||||
listenersMap[LongPollEvent.CHAT_CLEARED]?.let { listeners ->
|
||||
listeners.forEach { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatCleared>)
|
||||
.onEvent(
|
||||
LongPollParsedEvent.ChatCleared(
|
||||
peerId = peerId,
|
||||
toMessageId = messageId
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend inline fun <reified T : LongPollEvent> loadNormalMessage(
|
||||
eventType: ApiEvent,
|
||||
messageId: Int
|
||||
): T? = suspendCoroutine { continuation ->
|
||||
private fun parseChatMajorChanged(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
|
||||
val peerId = event[1].asInt()
|
||||
val majorId = event[2].asInt()
|
||||
|
||||
listenersMap[LongPollEvent.CHAT_MAJOR_CHANGED]?.let { listeners ->
|
||||
listeners.forEach { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatMajorChanged>)
|
||||
.onEvent(
|
||||
LongPollParsedEvent.ChatMajorChanged(
|
||||
peerId = peerId,
|
||||
majorId = majorId,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseChatMinorChanged(eventType: ApiEvent, event: List<Any>) {
|
||||
Log.d("LongPollUpdatesParser", "$eventType: $event")
|
||||
|
||||
val peerId = event[1].asInt()
|
||||
val minorId = event[2].asInt()
|
||||
|
||||
listenersMap[LongPollEvent.CHAT_MINOR_CHANGED]?.let { listeners ->
|
||||
listeners.forEach { vkEventCallback ->
|
||||
(vkEventCallback as VkEventCallback<LongPollParsedEvent.ChatMinorChanged>)
|
||||
.onEvent(
|
||||
LongPollParsedEvent.ChatMinorChanged(
|
||||
peerId = peerId,
|
||||
minorId = minorId,
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadMessage(messageId: Int): VkMessage? = suspendCoroutine { continuation ->
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
messagesUseCase.getById(
|
||||
messageIds = listOf(messageId),
|
||||
@@ -244,10 +469,11 @@ class LongPollUpdatesParser(
|
||||
).listenValue(this) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
Log.e("LongPollUpdatesParser", "loadNormalMessage: error: $error")
|
||||
Log.e("LongPollUpdatesParser", "loadMessage: error: $error")
|
||||
continuation.resume(null)
|
||||
},
|
||||
success = { messages ->
|
||||
val message = messages.singleOrNull() ?: run {
|
||||
success = { response ->
|
||||
val message = response.singleOrNull() ?: run {
|
||||
continuation.resume(null)
|
||||
return@listenValue
|
||||
}
|
||||
@@ -255,107 +481,113 @@ class LongPollUpdatesParser(
|
||||
VkMemoryCache[message.id] = message
|
||||
messagesUseCase.storeMessage(message)
|
||||
|
||||
val resumeValue: LongPollEvent? = when (eventType) {
|
||||
ApiEvent.MESSAGE_NEW -> LongPollEvent.VkMessageNewEvent(message)
|
||||
ApiEvent.MESSAGE_EDIT -> LongPollEvent.VkMessageEditEvent(message)
|
||||
|
||||
else -> {
|
||||
continuation.resume(null)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
resumeValue?.let { value -> continuation.resume(value as T) }
|
||||
continuation.resume(message)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> registerListener(
|
||||
eventType: ApiEvent,
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun <T : LongPollParsedEvent> registerListener(
|
||||
eventType: LongPollEvent,
|
||||
listener: VkEventCallback<T>
|
||||
) {
|
||||
listenersMap.let { map ->
|
||||
map[eventType] = (map[eventType] ?: mutableListOf()).also { it.add(listener) }
|
||||
map[eventType] = (map[eventType] ?: mutableListOf())
|
||||
.also {
|
||||
it.add(listener as VkEventCallback<LongPollParsedEvent>)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T : Any> registerListeners(
|
||||
eventTypes: List<ApiEvent>,
|
||||
private fun <T : LongPollParsedEvent> registerListeners(
|
||||
eventTypes: List<LongPollEvent>,
|
||||
listener: VkEventCallback<T>
|
||||
) {
|
||||
eventTypes.forEach { eventType -> registerListener(eventType, listener) }
|
||||
}
|
||||
|
||||
fun onConversationPinStateChanged(listener: VkEventCallback<LongPollEvent.VkConversationPinStateChangedEvent>) {
|
||||
registerListener(ApiEvent.PIN_UNPIN_CONVERSATION, listener)
|
||||
fun onMessageSetFlags(block: (LongPollParsedEvent) -> Unit) {
|
||||
registerListener(LongPollEvent.MESSAGE_SET_FLAGS, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onConversationPinStateChanged(block: (LongPollEvent.VkConversationPinStateChangedEvent) -> Unit) {
|
||||
onConversationPinStateChanged(assembleEventCallback(block))
|
||||
fun onMessageMarkedAsImportant(block: (LongPollParsedEvent.MessageMarkedAsImportant) -> Unit) {
|
||||
registerListener(LongPollEvent.MARKED_AS_IMPORTANT, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageIncomingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadIncomingEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_READ_INCOMING, listener)
|
||||
fun onMessageMarkedAsSpam(block: (LongPollParsedEvent.MessageMarkedAsSpam) -> Unit) {
|
||||
registerListener(LongPollEvent.MARKED_AS_SPAM, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageIncomingRead(block: (LongPollEvent.VkMessageReadIncomingEvent) -> Unit) {
|
||||
onMessageIncomingRead(assembleEventCallback(block))
|
||||
fun onMessageDeleted(block: (LongPollParsedEvent.MessageDeleted) -> Unit) {
|
||||
registerListener(LongPollEvent.MESSAGE_DELETED, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageOutgoingRead(listener: VkEventCallback<LongPollEvent.VkMessageReadOutgoingEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_READ_OUTGOING, listener)
|
||||
fun onMessageClearFlags(block: (LongPollParsedEvent) -> Unit) {
|
||||
registerListener(LongPollEvent.MESSAGE_CLEAR_FLAGS, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageOutgoingRead(block: (LongPollEvent.VkMessageReadOutgoingEvent) -> Unit) {
|
||||
onMessageOutgoingRead(assembleEventCallback(block))
|
||||
fun onMessageMarkedAsNotSpam(block: (LongPollParsedEvent.MessageMarkedAsNotSpam) -> Unit) {
|
||||
registerListener(LongPollEvent.MARKED_AS_NOT_SPAM, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onNewMessage(listener: VkEventCallback<LongPollEvent.VkMessageNewEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_NEW, listener)
|
||||
fun onMessageRestored(block: (LongPollParsedEvent.MessageRestored) -> Unit) {
|
||||
registerListener(LongPollEvent.MESSAGE_RESTORED, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onNewMessage(block: (LongPollEvent.VkMessageNewEvent) -> Unit) {
|
||||
onNewMessage(assembleEventCallback(block))
|
||||
fun onNewMessage(block: (LongPollParsedEvent.NewMessage) -> Unit) {
|
||||
registerListener(LongPollEvent.MESSAGE_NEW, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageEdited(listener: VkEventCallback<LongPollEvent.VkMessageEditEvent>) {
|
||||
registerListener(ApiEvent.MESSAGE_EDIT, listener)
|
||||
fun onMessageEdited(block: (LongPollParsedEvent.MessageEdited) -> Unit) {
|
||||
registerListener(LongPollEvent.MESSAGE_EDITED, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onMessageEdited(block: (LongPollEvent.VkMessageEditEvent) -> Unit) {
|
||||
onMessageEdited(assembleEventCallback(block))
|
||||
fun onMessageIncomingRead(block: (LongPollParsedEvent.IncomingMessageRead) -> Unit) {
|
||||
registerListener(LongPollEvent.INCOMING_MESSAGE_READ, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onInteractions(listener: VkEventCallback<LongPollEvent.Interaction>) {
|
||||
fun onMessageOutgoingRead(block: (LongPollParsedEvent.OutgoingMessageRead) -> Unit) {
|
||||
registerListener(LongPollEvent.OUTGOING_MESSAGE_READ, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onChatCleared(block: (LongPollParsedEvent.ChatCleared) -> Unit) {
|
||||
registerListener(LongPollEvent.CHAT_CLEARED, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onChatMajorChanged(block: (LongPollParsedEvent.ChatMajorChanged) -> Unit) {
|
||||
registerListener(LongPollEvent.CHAT_MAJOR_CHANGED, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onChatMinorChanged(block: (LongPollParsedEvent.ChatMinorChanged) -> Unit) {
|
||||
registerListener(LongPollEvent.CHAT_MINOR_CHANGED, assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun onInteractions(block: (LongPollParsedEvent.Interaction) -> Unit) {
|
||||
registerListeners(
|
||||
eventTypes = listOf(
|
||||
ApiEvent.TYPING,
|
||||
ApiEvent.AUDIO_MESSAGE_RECORDING,
|
||||
ApiEvent.PHOTO_UPLOADING,
|
||||
ApiEvent.VIDEO_UPLOADING,
|
||||
ApiEvent.FILE_UPLOADING
|
||||
LongPollEvent.TYPING,
|
||||
LongPollEvent.AUDIO_MESSAGE_RECORDING,
|
||||
LongPollEvent.PHOTO_UPLOADING,
|
||||
LongPollEvent.VIDEO_UPLOADING,
|
||||
LongPollEvent.FILE_UPLOADING
|
||||
),
|
||||
listener = listener
|
||||
listener = assembleEventCallback(block)
|
||||
)
|
||||
}
|
||||
|
||||
fun onInteractions(block: (LongPollEvent.Interaction) -> Unit) {
|
||||
onInteractions(assembleEventCallback(block))
|
||||
}
|
||||
|
||||
fun clearListeners() {
|
||||
listenersMap.clear()
|
||||
}
|
||||
}
|
||||
|
||||
internal inline fun <R : Any> assembleEventCallback(
|
||||
internal inline fun <R : LongPollParsedEvent> assembleEventCallback(
|
||||
crossinline block: (R) -> Unit,
|
||||
): VkEventCallback<R> {
|
||||
return VkEventCallback { event -> block.invoke(event) }
|
||||
}
|
||||
|
||||
fun interface VkEventCallback<in T : Any> {
|
||||
fun interface VkEventCallback<in T : LongPollParsedEvent> {
|
||||
fun onEvent(event: T)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,11 @@ interface MessagesUseCase {
|
||||
conversationMessageId: Int
|
||||
): Flow<State<List<VkAttachmentHistoryMessage>>>
|
||||
|
||||
fun createChat(
|
||||
userIds: List<Int>?,
|
||||
title: String?
|
||||
): Flow<State<Int>>
|
||||
|
||||
suspend fun storeMessage(message: VkMessage)
|
||||
suspend fun storeMessages(messages: List<VkMessage>)
|
||||
}
|
||||
|
||||
@@ -100,6 +100,14 @@ class MessagesUseCaseImpl(
|
||||
emit(newState)
|
||||
}
|
||||
|
||||
override fun createChat(userIds: List<Int>?, title: String?): Flow<State<Int>> = flow {
|
||||
emit(State.Loading)
|
||||
|
||||
val newState = repository.createChat(userIds, title).mapToState()
|
||||
|
||||
emit(newState)
|
||||
}
|
||||
|
||||
override suspend fun storeMessage(message: VkMessage) {
|
||||
repository.storeMessages(listOf(message))
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ class OAuthUseCaseImpl(
|
||||
forceSms = forceSms
|
||||
)
|
||||
|
||||
kotlin.runCatching {
|
||||
val error = response.error?.let(VkOAuthError::parse)
|
||||
val errorType = response.errorType?.let(VkOAuthErrorType::parse)
|
||||
|
||||
@@ -120,5 +121,12 @@ class OAuthUseCaseImpl(
|
||||
}
|
||||
|
||||
emit(newState)
|
||||
}.fold(
|
||||
onSuccess = {
|
||||
},
|
||||
onFailure = {
|
||||
emit(State.Error.TestError(it.stackTraceToString()))
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
+5
-3
@@ -1,9 +1,9 @@
|
||||
package dev.meloda.fast.friends.util
|
||||
package dev.meloda.fast.domain.util
|
||||
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
import dev.meloda.fast.friends.model.UiFriend
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
import dev.meloda.fast.ui.model.api.UiFriend
|
||||
|
||||
fun VkUser.asPresentation(
|
||||
useContactNames: Boolean = false
|
||||
@@ -16,5 +16,7 @@ fun VkUser.asPresentation(
|
||||
fullName
|
||||
},
|
||||
onlineStatus = onlineStatus,
|
||||
photo400Orig = photo400Orig?.let(UiImage::Url)
|
||||
photo400Orig = photo400Orig?.let(UiImage::Url),
|
||||
firstName = firstName,
|
||||
lastName = lastName
|
||||
)
|
||||
@@ -7,8 +7,11 @@ enum class ApiEvent(val value: Int) {
|
||||
MESSAGE_EDIT(5),
|
||||
MESSAGE_READ_INCOMING(6),
|
||||
MESSAGE_READ_OUTGOING(7),
|
||||
CHAT_CLEAR_FLAGS(10),
|
||||
CHAT_SET_FLAGS(12),
|
||||
MESSAGES_DELETED(13),
|
||||
PIN_UNPIN_CONVERSATION(20),
|
||||
CHAT_MAJOR_CHANGED(20),
|
||||
CHAT_MINOR_CHANGED(21),
|
||||
TYPING(63),
|
||||
AUDIO_MESSAGE_RECORDING(64),
|
||||
PHOTO_UPLOADING(65),
|
||||
@@ -18,5 +21,6 @@ enum class ApiEvent(val value: Int) {
|
||||
|
||||
companion object {
|
||||
fun parse(value: Int) = entries.first { it.value == value }
|
||||
fun parseOrNull(value: Int) = entries.firstOrNull { it.value == value }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,4 +6,6 @@ import androidx.compose.runtime.Immutable
|
||||
sealed class BaseError {
|
||||
|
||||
data object SessionExpired : BaseError()
|
||||
|
||||
data class SimpleError(val message: String) : BaseError()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package dev.meloda.fast.model
|
||||
|
||||
enum class ConversationFlags(val value: Int) {
|
||||
DISABLE_PUSH(16),
|
||||
DISABLE_SOUND(32),
|
||||
INCOMING_CHAT_REQUEST(256),
|
||||
DECLINED_CHAT_REQUEST(512),
|
||||
MENTION(1024),
|
||||
HIDE_CHAT_FROM_SEARCH(2048),
|
||||
BUSINESS_CHAT(8192),
|
||||
MARKED_MESSAGE(16384), // mention or disappearing message
|
||||
DO_NOT_NOTIFY_MENTIONS_ALL_ONLINE(262144),
|
||||
DO_NOT_NOTIFY_ALL_MENTIONS(524288),
|
||||
MARKED_AS_UNREAD(1048576),
|
||||
ARCHIVED(8388608),
|
||||
CALL_IN_PROGRESS(16777216),
|
||||
}
|
||||
@@ -1,35 +1,27 @@
|
||||
package dev.meloda.fast.model
|
||||
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
|
||||
sealed interface LongPollEvent {
|
||||
|
||||
data class VkMessageNewEvent(val message: VkMessage) : LongPollEvent
|
||||
|
||||
data class VkMessageEditEvent(val message: VkMessage) : LongPollEvent
|
||||
|
||||
data class VkMessageReadIncomingEvent(
|
||||
val peerId: Int,
|
||||
val messageId: Int,
|
||||
val unreadCount: Int,
|
||||
) : LongPollEvent
|
||||
|
||||
data class VkMessageReadOutgoingEvent(
|
||||
val peerId: Int,
|
||||
val messageId: Int,
|
||||
val unreadCount: Int,
|
||||
) : LongPollEvent
|
||||
|
||||
data class VkConversationPinStateChangedEvent(
|
||||
val peerId: Int,
|
||||
val majorId: Int,
|
||||
) : LongPollEvent
|
||||
|
||||
data class Interaction(
|
||||
val interactionType: InteractionType,
|
||||
val peerId: Int,
|
||||
val userIds: List<Int>,
|
||||
val totalCount: Int,
|
||||
val timestamp: Int
|
||||
) : LongPollEvent
|
||||
enum class LongPollEvent {
|
||||
MESSAGE_SET_FLAGS,
|
||||
MESSAGE_CLEAR_FLAGS,
|
||||
MESSAGE_NEW,
|
||||
MESSAGE_EDITED,
|
||||
INCOMING_MESSAGE_READ,
|
||||
OUTGOING_MESSAGE_READ,
|
||||
CHAT_SET_FLAGS,
|
||||
CHAT_CLEAR_FLAGS,
|
||||
CHAT_MAJOR_CHANGED,
|
||||
CHAT_MINOR_CHANGED,
|
||||
TYPING,
|
||||
AUDIO_MESSAGE_RECORDING,
|
||||
PHOTO_UPLOADING,
|
||||
VIDEO_UPLOADING,
|
||||
FILE_UPLOADING,
|
||||
UNREAD_COUNTER_UPDATE,
|
||||
MARKED_AS_IMPORTANT,
|
||||
MARKED_AS_SPAM,
|
||||
MARKED_AS_NOT_SPAM,
|
||||
MESSAGE_DELETED,
|
||||
MESSAGE_RESTORED,
|
||||
AUDIO_MESSAGE_LISTENED,
|
||||
CHAT_CLEARED
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
package dev.meloda.fast.model
|
||||
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
|
||||
sealed interface LongPollParsedEvent {
|
||||
|
||||
data class NewMessage(val message: VkMessage) : LongPollParsedEvent
|
||||
|
||||
data class MessageEdited(val message: VkMessage) : LongPollParsedEvent
|
||||
|
||||
data class IncomingMessageRead(
|
||||
val peerId: Int,
|
||||
val messageId: Int,
|
||||
val unreadCount: Int,
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class OutgoingMessageRead(
|
||||
val peerId: Int,
|
||||
val messageId: Int,
|
||||
val unreadCount: Int,
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class ChatMajorChanged(
|
||||
val peerId: Int,
|
||||
val majorId: Int,
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class ChatMinorChanged(
|
||||
val peerId: Int,
|
||||
val minorId: Int
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class Interaction(
|
||||
val interactionType: InteractionType,
|
||||
val peerId: Int,
|
||||
val userIds: List<Int>,
|
||||
val totalCount: Int,
|
||||
val timestamp: Int
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class UnreadCounter(
|
||||
val unread: Int,
|
||||
val unreadUnmuted: Int,
|
||||
val showOnlyMuted: Boolean,
|
||||
val business: Int,
|
||||
val archive: Int,
|
||||
val archiveUnmuted: Int,
|
||||
val archiveMentions: Int
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class MessageMarkedAsImportant(
|
||||
val peerId: Int,
|
||||
val messageId: Int,
|
||||
val marked: Boolean
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class MessageMarkedAsSpam(
|
||||
val peerId: Int,
|
||||
val messageId: Int
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class MessageMarkedAsNotSpam(
|
||||
val message: VkMessage
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class MessageDeleted(
|
||||
val peerId: Int,
|
||||
val messageId: Int,
|
||||
val forAll: Boolean
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class MessageRestored(
|
||||
val message: VkMessage
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class AudioMessageListened(
|
||||
val peerId: Int,
|
||||
val messageId: Int
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class ChatCleared(
|
||||
val peerId: Int,
|
||||
val toMessageId: Int
|
||||
): LongPollParsedEvent
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package dev.meloda.fast.model
|
||||
|
||||
enum class MessageFlags(val value: Int) {
|
||||
UNREAD(1),
|
||||
OUTGOING(2),
|
||||
IMPORTANT(8),
|
||||
SPAM(64),
|
||||
DELETED(128),
|
||||
AUDIO_LISTENED(4096),
|
||||
FROM_GROUP_CHAT(8192),
|
||||
CANCEL_SPAM(32768),
|
||||
DELETED_FOR_ALL(131072),
|
||||
DO_NOT_SHOW_NOTIFICATION(1048576),
|
||||
MESSAGE_WITH_REPLY(2097152),
|
||||
REACTION(16777216);
|
||||
|
||||
companion object {
|
||||
|
||||
fun parse(mask: Int): List<MessageFlags> {
|
||||
val flags = mutableListOf<MessageFlags>()
|
||||
|
||||
entries.forEach { flag ->
|
||||
if (mask and flag.value > 0) {
|
||||
flags.add(flag)
|
||||
}
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -38,6 +38,41 @@ data class VkConversation(
|
||||
fun isPinned(): Boolean = majorId > 0
|
||||
fun isInUnread() = inRead - (lastMessageId ?: 0) < 0
|
||||
fun isOutUnread() = outRead - (lastMessageId ?: 0) < 0
|
||||
|
||||
companion object {
|
||||
val EMPTY: VkConversation = VkConversation(
|
||||
id = -1,
|
||||
localId = -1,
|
||||
ownerId = null,
|
||||
title = "...",
|
||||
photo50 = null,
|
||||
photo100 = null,
|
||||
photo200 = null,
|
||||
isCallInProgress = false,
|
||||
isPhantom = false,
|
||||
lastConversationMessageId = -1,
|
||||
inReadCmId = -1,
|
||||
outReadCmId = -1,
|
||||
inRead = -1,
|
||||
outRead = -1,
|
||||
lastMessageId = null,
|
||||
unreadCount = -1,
|
||||
membersCount = null,
|
||||
canChangePin = false,
|
||||
canChangeInfo = false,
|
||||
majorId = -1,
|
||||
minorId = -1,
|
||||
pinnedMessageId = null,
|
||||
interactionType = -1,
|
||||
interactionIds = emptyList(),
|
||||
peerType = PeerType.USER,
|
||||
lastMessage = null,
|
||||
pinnedMessage = null,
|
||||
user = null,
|
||||
group = null
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity(
|
||||
|
||||
@@ -38,11 +38,10 @@ data class VkMessage(
|
||||
|
||||
fun isGroup() = fromId < 0
|
||||
|
||||
fun isRead(conversation: VkConversation) =
|
||||
if (isOut) {
|
||||
conversation.outRead - id >= 0
|
||||
} else {
|
||||
conversation.inRead - id >= 0
|
||||
fun isRead(conversation: VkConversation): Boolean = when {
|
||||
id <= 0 -> false
|
||||
isOut -> conversation.outRead - id >= 0
|
||||
else -> conversation.inRead - id >= 0
|
||||
}
|
||||
|
||||
fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty()
|
||||
|
||||
@@ -267,3 +267,14 @@ data class MessagesGetHistoryAttachmentsRequest(
|
||||
fields?.let { this["fields"] = it }
|
||||
}
|
||||
}
|
||||
|
||||
data class MessagesCreateChatRequest(
|
||||
val userIds: List<Int>?,
|
||||
val title: String?
|
||||
) {
|
||||
|
||||
val map = mutableMapOf<String, String>().apply {
|
||||
userIds?.let { this["user_ids"] = it.joinToString(",") }
|
||||
title?.let { this["title"] = it }
|
||||
}
|
||||
}
|
||||
|
||||
+11
-2
@@ -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,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package dev.meloda.fast.model.api.responses
|
||||
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import dev.meloda.fast.model.api.data.VkAttachmentHistoryMessageData
|
||||
import dev.meloda.fast.model.api.data.VkChatMemberData
|
||||
import dev.meloda.fast.model.api.data.VkContactData
|
||||
@@ -7,8 +9,6 @@ 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 MessagesGetHistoryResponse(
|
||||
@@ -44,3 +44,9 @@ data class MessagesGetHistoryAttachmentsResponse(
|
||||
@Json(name = "groups") val groups: List<VkGroupData>?,
|
||||
@Json(name = "contacts") val contacts: List<VkContactData>?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessagesCreateChatResponse(
|
||||
@Json(name = "chat_id") val chatId: Int,
|
||||
@Json(name = "peer_ids") val peerIds: List<Int>
|
||||
)
|
||||
|
||||
@@ -43,11 +43,22 @@ class ResponseConverterFactory(private val converter: JsonConverter) : Converter
|
||||
converter.fromJson(successType, string)
|
||||
}.fold(
|
||||
onSuccess = { successModel ->
|
||||
if (successModel is ApiResponse<*>) {
|
||||
if (successModel.error != null) {
|
||||
throw ApiException(successModel.error)
|
||||
}
|
||||
}
|
||||
|
||||
return successModel
|
||||
},
|
||||
onFailure = { failure ->
|
||||
if (failure is JsonDataException) {
|
||||
throw failure
|
||||
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),
|
||||
@@ -41,6 +42,8 @@ enum class VkErrorCode(val code: Int) {
|
||||
ACCESS_TO_DOC_DENIED(1153),
|
||||
|
||||
SOME_AUTH_ERROR(104),
|
||||
|
||||
CANNOT_SEND_MESSAGE_DUE_TO_PRIVACY_SETTINGS(902),
|
||||
ACCESS_TOKEN_EXPIRED(1117);
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -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()
|
||||
|
||||
+8
-1
@@ -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(
|
||||
|
||||
+1
@@ -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"
|
||||
|
||||
+8
-1
@@ -1,12 +1,13 @@
|
||||
package dev.meloda.fast.network.service.messages
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.meloda.fast.model.api.data.VkLongPollData
|
||||
import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse
|
||||
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
|
||||
@@ -49,6 +50,12 @@ interface MessagesService {
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<MessagesGetHistoryAttachmentsResponse>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(MessagesUrls.CREATE_CHAT)
|
||||
suspend fun createChat(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<MessagesCreateChatResponse>, RestApiError>
|
||||
|
||||
// @FormUrlEncoded
|
||||
// @POST(MessagesUrls.MarkAsImportant)
|
||||
// suspend fun markAsImportant(
|
||||
|
||||
@@ -19,4 +19,5 @@ object MessagesUrls {
|
||||
const val GET_CONVERSATIONS_MEMBERS = "${AppConstants.URL_API}/messages.getConversationMembers"
|
||||
const val REMOVE_CHAT_USER = "${AppConstants.URL_API}/messages.removeChatUser"
|
||||
const val GET_HISTORY_ATTACHMENTS = "${AppConstants.URL_API}/messages.getHistoryAttachments"
|
||||
const val CREATE_CHAT = "${AppConstants.URL_API}/messages.createChat"
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,12 @@
|
||||
plugins {
|
||||
alias(libs.plugins.fast.android.library)
|
||||
alias(libs.plugins.fast.android.library.compose)
|
||||
alias(libs.plugins.kotlin.serialization)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.meloda.fast.presentation"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
</manifest>
|
||||
@@ -11,6 +11,7 @@ android {
|
||||
dependencies {
|
||||
api(projects.core.common)
|
||||
api(projects.core.model)
|
||||
implementation(projects.core.presentation)
|
||||
|
||||
implementation(libs.haze)
|
||||
implementation(libs.haze.materials)
|
||||
|
||||
@@ -5,12 +5,14 @@ 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
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@@ -22,13 +24,16 @@ fun ErrorView(
|
||||
onButtonClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Column(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = text,
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
buttonText?.let {
|
||||
|
||||
@@ -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(
|
||||
modifier: Modifier = Modifier,
|
||||
onClick: () -> Unit = {},
|
||||
onLongClick: (() -> Unit)? = null,
|
||||
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
|
||||
@@ -1,29 +1,51 @@
|
||||
package dev.meloda.fast.ui.components
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
@Composable
|
||||
fun NoItemsView(
|
||||
modifier: Modifier = Modifier,
|
||||
customText: String? = null
|
||||
customText: String? = null,
|
||||
buttonText: String? = null,
|
||||
onButtonClick: (() -> Unit)? = null,
|
||||
) {
|
||||
Box(
|
||||
modifier = modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Text(
|
||||
text = customText ?: stringResource(id = R.string.no_items),
|
||||
style = MaterialTheme.typography.titleLarge
|
||||
text = customText ?: stringResource(R.string.no_items),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
|
||||
buttonText?.let {
|
||||
Spacer(modifier = Modifier.height(12.dp))
|
||||
Button(
|
||||
onClick = { onButtonClick?.invoke() }
|
||||
) {
|
||||
Text(text = buttonText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,6 +53,7 @@ fun NoItemsView(
|
||||
@Composable
|
||||
private fun NoItemsViewPreview() {
|
||||
NoItemsView(
|
||||
customText = "Nothing here..."
|
||||
customText = "Nothing here...",
|
||||
buttonText = "Refresh"
|
||||
)
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
package dev.meloda.fast.ui.model.api
|
||||
|
||||
enum class ActionState {
|
||||
PHANTOM, CALL_IN_PROGRESS, NONE;
|
||||
@@ -0,0 +1,31 @@
|
||||
package dev.meloda.fast.ui.model.api
|
||||
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.common.model.UiText
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
sealed class ConversationOption(
|
||||
val title: UiText,
|
||||
val icon: UiImage
|
||||
) {
|
||||
|
||||
data object MarkAsRead : ConversationOption(
|
||||
title = UiText.Resource(R.string.action_mark_as_read),
|
||||
icon = UiImage.Resource(R.drawable.round_done_all_24)
|
||||
)
|
||||
|
||||
data object Pin : ConversationOption(
|
||||
title = UiText.Resource(R.string.action_pin),
|
||||
icon = UiImage.Resource(R.drawable.pin_outline_24)
|
||||
)
|
||||
|
||||
data object Unpin : ConversationOption(
|
||||
title = UiText.Resource(R.string.action_unpin),
|
||||
icon = UiImage.Resource(R.drawable.pin_off_outline_24)
|
||||
)
|
||||
|
||||
data object Delete : ConversationOption(
|
||||
title = UiText.Resource(R.string.action_delete),
|
||||
icon = UiImage.Resource(R.drawable.round_delete_outline_24)
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
package dev.meloda.fast.ui.model.api
|
||||
|
||||
data class ConversationsShowOptions(
|
||||
val showDeleteDialog: Int?,
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
package dev.meloda.fast.ui.model.api
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
+5
-1
@@ -1,11 +1,15 @@
|
||||
package dev.meloda.fast.friends.model
|
||||
package dev.meloda.fast.ui.model.api
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.model.api.domain.OnlineStatus
|
||||
|
||||
@Immutable
|
||||
data class UiFriend(
|
||||
val userId: Int,
|
||||
val avatar: UiImage?,
|
||||
val firstName: String,
|
||||
val lastName: String,
|
||||
val title: String,
|
||||
val onlineStatus: OnlineStatus,
|
||||
val photo400Orig: UiImage?
|
||||
@@ -1,7 +1,6 @@
|
||||
package dev.meloda.fast.ui.util
|
||||
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.os.PowerManager
|
||||
import android.view.KeyEvent
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
@@ -21,6 +20,7 @@ import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.pluralStringResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import dev.meloda.fast.common.model.DarkMode
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.common.model.UiText
|
||||
@@ -64,8 +64,8 @@ fun UiImage.getResourcePainter(): Painter? {
|
||||
@Composable
|
||||
fun UiImage.getImage(): Any {
|
||||
return when (this) {
|
||||
is UiImage.Color -> ColorDrawable(color)
|
||||
is UiImage.ColorResource -> ColorDrawable(colorResource(id = resId).toArgb())
|
||||
is UiImage.Color -> color.toDrawable()
|
||||
is UiImage.ColorResource -> colorResource(id = resId).toArgb().toDrawable()
|
||||
is UiImage.Resource -> painterResource(id = resId)
|
||||
is UiImage.Simple -> drawable
|
||||
is UiImage.Url -> url
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,6c1.93,0 3.5,1.57 3.5,3.5S13.93,13 12,13s-3.5,-1.57 -3.5,-3.5S10.07,6 12,6zM12,20c-2.03,0 -4.43,-0.82 -6.14,-2.88C7.55,15.8 9.68,15 12,15s4.45,0.8 6.14,2.12C16.43,19.18 14.03,20 12,20z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM12,6c1.93,0 3.5,1.57 3.5,3.5S13.93,13 12,13s-3.5,-1.57 -3.5,-3.5S10.07,6 12,6zM12,20c-2.03,0 -4.43,-0.82 -6.14,-2.88C7.55,15.8 9.68,15 12,15s4.45,0.8 6.14,2.12C16.43,19.18 14.03,20 12,20z" />
|
||||
|
||||
</vector>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M20,2L4,2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2zM6,9h12v2L6,11L6,9zM14,14L6,14v-2h8v2zM18,8L6,8L6,6h12v2z" />
|
||||
|
||||
</vector>
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M16.67,13.13C18.04,14.06 19,15.32 19,17v3h4v-3C23,14.82 19.43,13.53 16.67,13.13z"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M16.67,13.13C18.04,14.06 19,15.32 19,17v3h4v-3C23,14.82 19.43,13.53 16.67,13.13z" />
|
||||
|
||||
<path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M9,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M9,8m-4,0a4,4 0,1 1,8 0a4,4 0,1 1,-8 0" />
|
||||
|
||||
<path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M15,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4c-0.47,0 -0.91,0.1 -1.33,0.24C14.5,5.27 15,6.58 15,8s-0.5,2.73 -1.33,3.76C14.09,11.9 14.53,12 15,12z"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M15,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4c-0.47,0 -0.91,0.1 -1.33,0.24C14.5,5.27 15,6.58 15,8s-0.5,2.73 -1.33,3.76C14.09,11.9 14.53,12 15,12z" />
|
||||
|
||||
<path android:fillColor="@android:color/white" android:fillType="evenOdd" android:pathData="M9,13c-2.67,0 -8,1.34 -8,4v3h16v-3C17,14.34 11.67,13 9,13z"/>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M9,13c-2.67,0 -8,1.34 -8,4v3h16v-3C17,14.34 11.67,13 9,13z" />
|
||||
|
||||
</vector>
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="28dp"
|
||||
android:height="28dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,7c-0.55,0 -1,0.45 -1,1v3L8,11c-0.55,0 -1,0.45 -1,1s0.45,1 1,1h3v3c0,0.55 0.45,1 1,1s1,-0.45 1,-1v-3h3c0.55,0 1,-0.45 1,-1s-0.45,-1 -1,-1h-3L13,8c0,-0.55 -0.45,-1 -1,-1zM12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM12,20c-4.41,0 -8,-3.59 -8,-8s3.59,-8 8,-8 8,3.59 8,8 -3.59,8 -8,8z" />
|
||||
</vector>
|
||||
@@ -1,9 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M18.3,5.71c-0.39,-0.39 -1.02,-0.39 -1.41,0L12,10.59 7.11,5.7c-0.39,-0.39 -1.02,-0.39 -1.41,0 -0.39,0.39 -0.39,1.02 0,1.41L10.59,12 5.7,16.89c-0.39,0.39 -0.39,1.02 0,1.41 0.39,0.39 1.02,0.39 1.41,0L12,13.41l4.89,4.89c0.39,0.39 1.02,0.39 1.41,0 0.39,-0.39 0.39,-1.02 0,-1.41L13.41,12l4.89,-4.89c0.38,-0.38 0.38,-1.02 0,-1.4z" />
|
||||
</vector>
|
||||
@@ -1,7 +1,15 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM7.35,18.5C8.66,17.56 10.26,17 12,17s3.34,0.56 4.65,1.5C15.34,19.44 13.74,20 12,20S8.66,19.44 7.35,18.5zM18.14,17.12L18.14,17.12C16.45,15.8 14.32,15 12,15s-4.45,0.8 -6.14,2.12l0,0C4.7,15.73 4,13.95 4,12c0,-4.42 3.58,-8 8,-8s8,3.58 8,8C20,13.95 19.3,15.73 18.14,17.12z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10s10,-4.48 10,-10S17.52,2 12,2zM7.35,18.5C8.66,17.56 10.26,17 12,17s3.34,0.56 4.65,1.5C15.34,19.44 13.74,20 12,20S8.66,19.44 7.35,18.5zM18.14,17.12L18.14,17.12C16.45,15.8 14.32,15 12,15s-4.45,0.8 -6.14,2.12l0,0C4.7,15.73 4,13.95 4,12c0,-4.42 3.58,-8 8,-8s8,3.58 8,8C20,13.95 19.3,15.73 18.14,17.12z" />
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M12,6c-1.93,0 -3.5,1.57 -3.5,3.5S10.07,13 12,13s3.5,-1.57 3.5,-3.5S13.93,6 12,6zM12,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S11.17,8 12,8s1.5,0.67 1.5,1.5S12.83,11 12,11z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,6c-1.93,0 -3.5,1.57 -3.5,3.5S10.07,13 12,13s3.5,-1.57 3.5,-3.5S13.93,6 12,6zM12,11c-0.83,0 -1.5,-0.67 -1.5,-1.5S11.17,8 12,8s1.5,0.67 1.5,1.5S12.83,11 12,11z" />
|
||||
|
||||
</vector>
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:autoMirrored="true" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:autoMirrored="true"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M4,4h16v12L5.17,16L4,17.17L4,4m0,-2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2L4,2zM6,12h8v2L6,14v-2zM6,9h12v2L6,11L6,9zM6,6h12v2L6,8L6,6z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M4,4h16v12L5.17,16L4,17.17L4,4m0,-2c-1.1,0 -1.99,0.9 -1.99,2L2,22l4,-4h14c1.1,0 2,-0.9 2,-2L22,4c0,-1.1 -0.9,-2 -2,-2L4,2zM6,12h8v2L6,14v-2zM6,9h12v2L6,11L6,9zM6,6h12v2L6,8L6,6z" />
|
||||
|
||||
</vector>
|
||||
|
||||
@@ -1,11 +1,23 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:tint="#000000" android:viewportHeight="24" android:viewportWidth="24" android:width="24dp">
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M16.67,13.13C18.04,14.06 19,15.32 19,17v3h4v-3C23,14.82 19.43,13.53 16.67,13.13z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M16.67,13.13C18.04,14.06 19,15.32 19,17v3h4v-3C23,14.82 19.43,13.53 16.67,13.13z" />
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M15,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4c-0.47,0 -0.91,0.1 -1.33,0.24C14.5,5.27 15,6.58 15,8s-0.5,2.73 -1.33,3.76C14.09,11.9 14.53,12 15,12z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M15,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4c-0.47,0 -0.91,0.1 -1.33,0.24C14.5,5.27 15,6.58 15,8s-0.5,2.73 -1.33,3.76C14.09,11.9 14.53,12 15,12z" />
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M9,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4S5,5.79 5,8C5,10.21 6.79,12 9,12zM9,6c1.1,0 2,0.9 2,2c0,1.1 -0.9,2 -2,2S7,9.1 7,8C7,6.9 7.9,6 9,6z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,12c2.21,0 4,-1.79 4,-4c0,-2.21 -1.79,-4 -4,-4S5,5.79 5,8C5,10.21 6.79,12 9,12zM9,6c1.1,0 2,0.9 2,2c0,1.1 -0.9,2 -2,2S7,9.1 7,8C7,6.9 7.9,6 9,6z" />
|
||||
|
||||
<path android:fillColor="@android:color/white" android:pathData="M9,13c-2.67,0 -8,1.34 -8,4v3h16v-3C17,14.34 11.67,13 9,13zM15,18H3l0,-0.99C3.2,16.29 6.3,15 9,15s5.8,1.29 6,2V18z"/>
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M9,13c-2.67,0 -8,1.34 -8,4v3h16v-3C17,14.34 11.67,13 9,13zM15,18H3l0,-0.99C3.2,16.29 6.3,15 9,15s5.8,1.29 6,2V18z" />
|
||||
|
||||
</vector>
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM11.78,7h-0.06c-0.4,0 -0.72,0.32 -0.72,0.72v4.72c0,0.35 0.18,0.68 0.49,0.86l4.15,2.49c0.34,0.2 0.78,0.1 0.98,-0.24 0.21,-0.34 0.1,-0.79 -0.25,-0.99l-3.87,-2.3L12.5,7.72c0,-0.4 -0.32,-0.72 -0.72,-0.72z" />
|
||||
|
||||
</vector>
|
||||
@@ -0,0 +1,11 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M12,7c0.55,0 1,0.45 1,1v4c0,0.55 -0.45,1 -1,1s-1,-0.45 -1,-1L11,8c0,-0.55 0.45,-1 1,-1zM11.99,2C6.47,2 2,6.48 2,12s4.47,10 9.99,10C17.52,22 22,17.52 22,12S17.52,2 11.99,2zM12,20c-4.42,0 -8,-3.58 -8,-8s3.58,-8 8,-8 8,3.58 8,8 -3.58,8 -8,8zM13,17h-2v-2h2v2z" />
|
||||
|
||||
</vector>
|
||||
@@ -128,11 +128,12 @@
|
||||
<string name="post_type_community">Запись сообщества</string>
|
||||
<string name="post_type_user">Запись пользователя</string>
|
||||
<string name="post_type_unknown">Запись на стене</string>
|
||||
<string name="log_out">Выйти</string>
|
||||
<string name="action_log_out">Выйти</string>
|
||||
<string name="confirm">Подтверждение</string>
|
||||
<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,20 @@
|
||||
<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>
|
||||
<string name="no_online_friends">Никого в сети</string>
|
||||
<string name="try_again">Попробовать ещё раз</string>
|
||||
<string name="session_expired">Срок действия сессии истёк</string>
|
||||
<string name="title_create_chat">Создать чат</string>
|
||||
<string name="action_create">Создать</string>
|
||||
<string name="create_chat_title">Название</string>
|
||||
</resources>
|
||||
|
||||
@@ -119,7 +119,7 @@
|
||||
<string name="post_type_community">Community post</string>
|
||||
<string name="post_type_user">User post</string>
|
||||
<string name="post_type_unknown">Post</string>
|
||||
<string name="log_out">Log out</string>
|
||||
<string name="action_log_out">Log out</string>
|
||||
<string name="confirm">Confirmation</string>
|
||||
<string name="sign_out_confirm">Signing out will delete all data related to this account from this device. Continue?</string>
|
||||
<string name="yes">Yes</string>
|
||||
@@ -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>
|
||||
@@ -262,6 +276,11 @@
|
||||
<string name="warning_confirmation">Confirmation</string>
|
||||
<string name="captcha_exit_warning">Are you sure? Captcha process will be cancelled</string>
|
||||
<string name="validation_exit_warning">Are you sure? Validation process will be cancelled</string>
|
||||
<string name="settings_general_enable_pull_to_refresh_title">Enable pull to refresh</string>
|
||||
<string name="action_authorize">Authorize</string>
|
||||
<string name="no_online_friends">No one is online</string>
|
||||
<string name="try_again">Try again</string>
|
||||
<string name="session_expired">Session expired</string>
|
||||
<string name="title_create_chat">Create chat</string>
|
||||
<string name="action_create">Create</string>
|
||||
<string name="create_chat_title">Title</string>
|
||||
</resources>
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
+6
-14
@@ -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
|
||||
}
|
||||
|
||||
+256
-84
@@ -1,30 +1,34 @@
|
||||
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
|
||||
import dev.meloda.fast.common.extensions.listenValue
|
||||
import dev.meloda.fast.common.extensions.setValue
|
||||
import dev.meloda.fast.conversations.model.ConversationOption
|
||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||
import dev.meloda.fast.conversations.model.ConversationsShowOptions
|
||||
import dev.meloda.fast.conversations.model.UiConversation
|
||||
import dev.meloda.fast.conversations.util.asPresentation
|
||||
import dev.meloda.fast.conversations.util.extractAvatar
|
||||
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
|
||||
import dev.meloda.fast.model.InteractionType
|
||||
import dev.meloda.fast.model.LongPollEvent
|
||||
import dev.meloda.fast.model.LongPollParsedEvent
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.network.VkErrorCode
|
||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||
import dev.meloda.fast.ui.model.api.ConversationsShowOptions
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -40,7 +44,6 @@ interface ConversationsViewModel {
|
||||
|
||||
val screenState: StateFlow<ConversationsScreenState>
|
||||
val baseError: StateFlow<BaseError?>
|
||||
val imagesToPreload: StateFlow<List<String>>
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
|
||||
@@ -70,15 +73,19 @@ 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)
|
||||
|
||||
private val useContactNames: Boolean get() = userSettings.useContactNames.value
|
||||
|
||||
override fun onPaginationConditionsMet() {
|
||||
currentOffset.update { screenState.value.conversations.size }
|
||||
loadConversations()
|
||||
@@ -97,8 +104,10 @@ class ConversationsViewModelImpl(
|
||||
updatesParser.onMessageEdited(::handleEditedMessage)
|
||||
updatesParser.onMessageIncomingRead(::handleReadIncomingMessage)
|
||||
updatesParser.onMessageOutgoingRead(::handleReadOutgoingMessage)
|
||||
updatesParser.onConversationPinStateChanged(::handlePinStateChanged)
|
||||
updatesParser.onInteractions(::handleInteraction)
|
||||
updatesParser.onChatMajorChanged(::handleChatMajorChanged)
|
||||
updatesParser.onChatMinorChanged(::handleChatMinorChanged)
|
||||
updatesParser.onChatCleared(::handleChatClearing)
|
||||
|
||||
loadConversations()
|
||||
}
|
||||
@@ -115,6 +124,7 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
onErrorConsumed()
|
||||
loadConversations(offset = 0)
|
||||
}
|
||||
|
||||
@@ -180,7 +190,10 @@ class ConversationsViewModelImpl(
|
||||
onPinDialogDismissed()
|
||||
}
|
||||
|
||||
override fun onOptionClicked(conversation: UiConversation, option: ConversationOption) {
|
||||
override fun onOptionClicked(
|
||||
conversation: UiConversation,
|
||||
option: ConversationOption
|
||||
) {
|
||||
when (option) {
|
||||
ConversationOption.Delete -> {
|
||||
emitShowOptions { old ->
|
||||
@@ -240,17 +253,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 +261,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,12 +309,48 @@ 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(
|
||||
error = { error ->
|
||||
|
||||
},
|
||||
error = {},
|
||||
success = {
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
val conversationIndex =
|
||||
@@ -312,11 +359,7 @@ class ConversationsViewModelImpl(
|
||||
|
||||
newConversations.removeAt(conversationIndex)
|
||||
conversations.update { newConversations }
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
)
|
||||
}
|
||||
sortConversations()
|
||||
}
|
||||
)
|
||||
screenState.emit(screenState.value.copy(isLoading = state.isLoading()))
|
||||
@@ -327,15 +370,13 @@ class ConversationsViewModelImpl(
|
||||
conversationsUseCase.changePinState(peerId, pin)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
|
||||
},
|
||||
error = {},
|
||||
success = {
|
||||
handlePinStateChanged(
|
||||
LongPollEvent.VkConversationPinStateChangedEvent(
|
||||
handleChatMajorChanged(
|
||||
LongPollParsedEvent.ChatMajorChanged(
|
||||
peerId = peerId,
|
||||
majorId = if (pin) {
|
||||
(pinnedConversationsCount.value + 1) * 16
|
||||
pinnedConversationsCount.value.plus(1) * 16
|
||||
} else {
|
||||
0
|
||||
}
|
||||
@@ -348,15 +389,28 @@ class ConversationsViewModelImpl(
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNewMessage(event: LongPollEvent.VkMessageNewEvent) {
|
||||
private fun handleNewMessage(event: LongPollParsedEvent.NewMessage) {
|
||||
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 = {},
|
||||
success = { response ->
|
||||
val conversation = (response.firstOrNull() ?: return@listenValue)
|
||||
.copy(lastMessage = message)
|
||||
|
||||
newConversations.add(pinnedConversationsCount.value, conversation)
|
||||
conversations.update { newConversations }
|
||||
sortConversations()
|
||||
}
|
||||
)
|
||||
}
|
||||
} else {
|
||||
val conversation = newConversations[conversationIndex]
|
||||
var newConversation = conversation.copy(
|
||||
@@ -397,13 +451,18 @@ class ConversationsViewModelImpl(
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEditedMessage(event: LongPollEvent.VkMessageEditEvent) {
|
||||
private fun handleEditedMessage(event: LongPollParsedEvent.MessageEdited) {
|
||||
val message = event.message
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
|
||||
@@ -421,18 +480,26 @@ class ConversationsViewModelImpl(
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReadIncomingMessage(event: LongPollEvent.VkMessageReadIncomingEvent) {
|
||||
private fun handleReadIncomingMessage(event: LongPollParsedEvent.IncomingMessageRead) {
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
|
||||
val conversationIndex =
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
||||
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(
|
||||
inRead = event.messageId,
|
||||
@@ -443,17 +510,26 @@ class ConversationsViewModelImpl(
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleReadOutgoingMessage(event: LongPollEvent.VkMessageReadOutgoingEvent) {
|
||||
private fun handleReadOutgoingMessage(event: LongPollParsedEvent.OutgoingMessageRead) {
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
|
||||
val conversationIndex =
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
||||
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(
|
||||
outRead = event.messageId,
|
||||
@@ -463,44 +539,125 @@ 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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChatMajorChanged(event: LongPollParsedEvent.ChatMajorChanged) {
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
val conversationIndex =
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
||||
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(majorId = event.majorId)
|
||||
|
||||
conversations.setValue { newConversations }
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
sortConversations()
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleChatMinorChanged(event: LongPollParsedEvent.ChatMinorChanged) {
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
val conversationIndex =
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
||||
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
newConversations[conversationIndex] =
|
||||
newConversations[conversationIndex].copy(minorId = event.minorId)
|
||||
|
||||
conversations.setValue { newConversations }
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
sortConversations()
|
||||
}
|
||||
}
|
||||
|
||||
private fun sortConversations() {
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
val pinnedConversations = newConversations
|
||||
.filter(VkConversation::isPinned)
|
||||
.sortedWith { c1, c2 ->
|
||||
val diff = c2.majorId - c1.majorId
|
||||
|
||||
if (diff == 0) {
|
||||
c2.minorId - c1.minorId
|
||||
} else {
|
||||
diff
|
||||
}
|
||||
}
|
||||
|
||||
newConversations.removeAll(pinnedConversations)
|
||||
newConversations.sortWith { c1, c2 ->
|
||||
(c2.lastMessage?.date ?: 0) - (c1.lastMessage?.date ?: 0)
|
||||
}
|
||||
|
||||
newConversations.addAll(0, pinnedConversations)
|
||||
|
||||
conversations.update { newConversations }
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePinStateChanged(event: LongPollEvent.VkConversationPinStateChangedEvent) {
|
||||
var pinnedCount = pinnedConversationsCount.value
|
||||
private fun handleChatClearing(event: LongPollParsedEvent.ChatCleared) {
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
|
||||
val conversationIndex =
|
||||
newConversations.indexOfFirstOrNull { it.id == event.peerId } ?: return
|
||||
|
||||
val pin = event.majorId > 0
|
||||
|
||||
val conversation = newConversations[conversationIndex].copy(majorId = event.majorId)
|
||||
val conversationIndex = newConversations.indexOfFirstOrNull { it.id == event.peerId }
|
||||
|
||||
if (conversationIndex == null) { // диалога нет в списке
|
||||
// pizdets
|
||||
} else {
|
||||
newConversations.removeAt(conversationIndex)
|
||||
|
||||
if (pin) {
|
||||
newConversations.add(0, conversation)
|
||||
} else {
|
||||
pinnedCount -= 1
|
||||
|
||||
newConversations.add(conversation)
|
||||
|
||||
val pinnedSubList = newConversations.filter(VkConversation::isPinned)
|
||||
val unpinnedSubList = newConversations
|
||||
.filterNot(VkConversation::isPinned)
|
||||
.sortedByDescending { it.lastMessage?.date }
|
||||
|
||||
newConversations.clear()
|
||||
newConversations += pinnedSubList + unpinnedSubList
|
||||
}
|
||||
|
||||
conversations.update { newConversations }
|
||||
conversations.setValue { newConversations }
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(conversations = newConversations.map { it.asPresentation(resources) })
|
||||
old.copy(
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -511,17 +668,18 @@ class ConversationsViewModelImpl(
|
||||
val timerJob: Job
|
||||
)
|
||||
|
||||
private object NewInteractionException : CancellationException()
|
||||
private class NewInteractionException : CancellationException()
|
||||
|
||||
private fun handleInteraction(event: LongPollEvent.Interaction) {
|
||||
private fun handleInteraction(event: LongPollParsedEvent.Interaction) {
|
||||
val interactionType = event.interactionType
|
||||
val peerId = event.peerId
|
||||
val userIds = event.userIds
|
||||
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
val conversationAndIndex =
|
||||
newConversations.findWithIndex { it.id == peerId } ?: return
|
||||
newConversations.findWithIndex { it.id == peerId }
|
||||
|
||||
if (conversationAndIndex != null) {
|
||||
newConversations[conversationAndIndex.first] =
|
||||
conversationAndIndex.second.copy(
|
||||
interactionType = interactionType.value,
|
||||
@@ -532,13 +690,18 @@ class ConversationsViewModelImpl(
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
conversations = newConversations.map { it.asPresentation(resources) }
|
||||
conversations = newConversations.map {
|
||||
it.asPresentation(
|
||||
resources = resources,
|
||||
useContactName = useContactNames
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
interactionsTimers[peerId]?.let { interactionJob ->
|
||||
if (interactionJob.interactionType == interactionType) {
|
||||
interactionJob.timerJob.cancel(NewInteractionException)
|
||||
interactionJob.timerJob.cancel(NewInteractionException())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -560,6 +723,7 @@ class ConversationsViewModelImpl(
|
||||
stopInteraction(peerId, newInteractionJob)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopInteraction(peerId: Int, interactionJob: InteractionJob) {
|
||||
interactionsTimers[peerId] ?: return
|
||||
@@ -577,7 +741,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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -591,9 +760,7 @@ class ConversationsViewModelImpl(
|
||||
startMessageId = startMessageId
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
|
||||
},
|
||||
error = {},
|
||||
success = {
|
||||
val newConversations = conversations.value.toMutableList()
|
||||
val conversationIndex =
|
||||
@@ -606,7 +773,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
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+3
-3
@@ -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)
|
||||
}
|
||||
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.common.model.UiText
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
sealed class ConversationOption(
|
||||
val title: UiText,
|
||||
val icon: UiImage
|
||||
) {
|
||||
|
||||
data object MarkAsRead : ConversationOption(
|
||||
title = UiText.Resource(UiR.string.action_mark_as_read),
|
||||
icon = UiImage.Resource(UiR.drawable.round_done_all_24)
|
||||
)
|
||||
|
||||
data object Pin : ConversationOption(
|
||||
title = UiText.Resource(UiR.string.action_pin),
|
||||
icon = UiImage.Resource(UiR.drawable.pin_outline_24)
|
||||
)
|
||||
|
||||
data object Unpin : ConversationOption(
|
||||
title = UiText.Resource(UiR.string.action_unpin),
|
||||
icon = UiImage.Resource(UiR.drawable.pin_off_outline_24)
|
||||
)
|
||||
|
||||
data object Delete : ConversationOption(
|
||||
title = UiText.Resource(UiR.string.action_delete),
|
||||
icon = UiImage.Resource(UiR.drawable.round_delete_outline_24)
|
||||
)
|
||||
}
|
||||
+2
@@ -1,6 +1,8 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import dev.meloda.fast.ui.model.api.ConversationsShowOptions
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
|
||||
@Immutable
|
||||
data class ConversationsScreenState(
|
||||
|
||||
+2
@@ -17,6 +17,7 @@ fun NavGraphBuilder.conversationsScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
onConversationItemClicked: (id: Int) -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onCreateChatClicked: () -> Unit,
|
||||
navController: NavController,
|
||||
) {
|
||||
composable<Conversations> {
|
||||
@@ -27,6 +28,7 @@ fun NavGraphBuilder.conversationsScreen(
|
||||
onError = onError,
|
||||
onConversationItemClicked = onConversationItemClicked,
|
||||
onConversationPhotoClicked = onPhotoClicked,
|
||||
onCreateChatButtonClicked = onCreateChatClicked,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
|
||||
+3
-3
@@ -48,11 +48,11 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import dev.meloda.fast.conversations.model.ConversationOption
|
||||
import dev.meloda.fast.conversations.model.UiConversation
|
||||
import dev.meloda.fast.ui.basic.ContentAlpha
|
||||
import dev.meloda.fast.ui.basic.LocalContentAlpha
|
||||
import dev.meloda.fast.ui.components.DotsFlashing
|
||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
import dev.meloda.fast.ui.util.getImage
|
||||
import dev.meloda.fast.ui.util.getResourcePainter
|
||||
import dev.meloda.fast.ui.util.getString
|
||||
@@ -256,7 +256,7 @@ fun ConversationItem(
|
||||
Row {
|
||||
if (conversation.interactionText != null) {
|
||||
Text(
|
||||
text = conversation.interactionText,
|
||||
text = conversation.interactionText.orEmpty(),
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
|
||||
+5
-9
@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.navigationBarsPadding
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
@@ -23,10 +22,10 @@ import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.unit.dp
|
||||
import dev.meloda.fast.conversations.model.ConversationOption
|
||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||
import dev.meloda.fast.conversations.model.UiConversation
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
import dev.meloda.fast.ui.theme.LocalBottomPadding
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -83,8 +82,7 @@ fun ConversationsList(
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null)
|
||||
.navigationBarsPadding(),
|
||||
.animateItem(fadeInSpec = null, fadeOutSpec = null),
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (screenState.isPaginating) {
|
||||
@@ -107,11 +105,9 @@ fun ConversationsList(
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
item {
|
||||
Spacer(modifier = Modifier.height(bottomPadding))
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+35
-60
@@ -2,7 +2,6 @@ package dev.meloda.fast.conversations.presentation
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.animateColorAsState
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -48,13 +47,10 @@ import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
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
|
||||
@@ -63,30 +59,26 @@ import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
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.hazeEffect
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.conversations.ConversationsViewModel
|
||||
import dev.meloda.fast.conversations.model.ConversationOption
|
||||
import dev.meloda.fast.conversations.model.ConversationsScreenState
|
||||
import dev.meloda.fast.conversations.model.UiConversation
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.ui.components.ErrorView
|
||||
import dev.meloda.fast.ui.components.FullScreenLoader
|
||||
import dev.meloda.fast.ui.components.MaterialDialog
|
||||
import dev.meloda.fast.ui.components.NoItemsView
|
||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
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.isScrollingUp
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.debounce
|
||||
import kotlinx.coroutines.launch
|
||||
import dev.meloda.fast.ui.R as UiR
|
||||
|
||||
@Composable
|
||||
@@ -94,25 +86,13 @@ fun ConversationsRoute(
|
||||
onError: (BaseError) -> Unit,
|
||||
onConversationItemClicked: (conversationId: Int) -> Unit,
|
||||
onConversationPhotoClicked: (url: String) -> Unit,
|
||||
onCreateChatButtonClicked: () -> 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()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
ConversationsScreen(
|
||||
screenState = screenState,
|
||||
baseError = baseError,
|
||||
@@ -128,6 +108,7 @@ fun ConversationsRoute(
|
||||
onRefreshDropdownItemClicked = viewModel::onRefresh,
|
||||
onRefresh = viewModel::onRefresh,
|
||||
onConversationPhotoClicked = onConversationPhotoClicked,
|
||||
onCreateChatButtonClicked = onCreateChatButtonClicked,
|
||||
setScrollIndex = viewModel::setScrollIndex,
|
||||
setScrollOffset = viewModel::setScrollOffset
|
||||
)
|
||||
@@ -147,7 +128,7 @@ fun ConversationsScreen(
|
||||
screenState: ConversationsScreenState = ConversationsScreenState.EMPTY,
|
||||
baseError: BaseError? = null,
|
||||
canPaginate: Boolean = false,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit,
|
||||
onSessionExpiredLogOutButtonClicked: () -> Unit = {},
|
||||
onConversationItemClicked: (conversationId: Int) -> Unit = {},
|
||||
onConversationItemLongClicked: (conversation: UiConversation) -> Unit = {},
|
||||
onOptionClicked: (UiConversation, ConversationOption) -> Unit = { _, _ -> },
|
||||
@@ -155,6 +136,7 @@ fun ConversationsScreen(
|
||||
onRefreshDropdownItemClicked: () -> Unit = {},
|
||||
onRefresh: () -> Unit = {},
|
||||
onConversationPhotoClicked: (url: String) -> Unit = {},
|
||||
onCreateChatButtonClicked: () -> Unit = {},
|
||||
setScrollIndex: (Int) -> Unit = {},
|
||||
setScrollOffset: (Int) -> Unit = {}
|
||||
) {
|
||||
@@ -278,7 +260,7 @@ fun ConversationsScreen(
|
||||
modifier = Modifier
|
||||
.then(
|
||||
if (currentTheme.enableBlur) {
|
||||
Modifier.hazeChild(
|
||||
Modifier.hazeEffect(
|
||||
state = hazeState,
|
||||
style = HazeMaterials.thick()
|
||||
)
|
||||
@@ -299,37 +281,13 @@ fun ConversationsScreen(
|
||||
}
|
||||
},
|
||||
floatingActionButton = {
|
||||
val scope = rememberCoroutineScope()
|
||||
val rotation = remember { Animatable(0f) }
|
||||
|
||||
Column {
|
||||
AnimatedVisibility(
|
||||
visible = listState.isScrollingUp(),
|
||||
enter = slideIn { IntOffset(0, 600) } + fadeIn(tween(200)),
|
||||
exit = slideOut { IntOffset(0, 600) } + fadeOut(tween(200))
|
||||
) {
|
||||
FloatingActionButton(
|
||||
onClick = {
|
||||
if (AppSettings.Debug.enableHaptic) {
|
||||
view.performHapticFeedback(HapticFeedbackConstantsCompat.REJECT)
|
||||
}
|
||||
scope.launch {
|
||||
for (i in 20 downTo 0 step 4) {
|
||||
rotation.animateTo(
|
||||
targetValue = i.toFloat(),
|
||||
animationSpec = tween(50)
|
||||
)
|
||||
if (i > 0) {
|
||||
rotation.animateTo(
|
||||
targetValue = -i.toFloat(),
|
||||
animationSpec = tween(50)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
modifier = Modifier.rotate(rotation.value)
|
||||
) {
|
||||
FloatingActionButton(onClick = onCreateChatButtonClicked) {
|
||||
Icon(
|
||||
painter = painterResource(id = UiR.drawable.ic_baseline_create_24),
|
||||
contentDescription = "Add chat button"
|
||||
@@ -342,14 +300,26 @@ fun ConversationsScreen(
|
||||
}
|
||||
) { padding ->
|
||||
when {
|
||||
baseError is BaseError.SessionExpired -> {
|
||||
baseError != null -> {
|
||||
when (baseError) {
|
||||
is BaseError.SessionExpired -> {
|
||||
ErrorView(
|
||||
text = "Session expired",
|
||||
buttonText = "Log out",
|
||||
text = stringResource(UiR.string.session_expired),
|
||||
buttonText = stringResource(UiR.string.action_log_out),
|
||||
onButtonClick = onSessionExpiredLogOutButtonClicked
|
||||
)
|
||||
}
|
||||
|
||||
is BaseError.SimpleError -> {
|
||||
ErrorView(
|
||||
text = baseError.message,
|
||||
buttonText = stringResource(UiR.string.try_again),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
screenState.isLoading && screenState.conversations.isEmpty() -> FullScreenLoader()
|
||||
|
||||
else -> {
|
||||
@@ -381,7 +351,7 @@ fun ConversationsScreen(
|
||||
state = listState,
|
||||
maxLines = maxLines,
|
||||
modifier = if (currentTheme.enableBlur) {
|
||||
Modifier.haze(state = hazeState)
|
||||
Modifier.hazeSource(state = hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}.fillMaxSize(),
|
||||
@@ -389,6 +359,13 @@ fun ConversationsScreen(
|
||||
padding = padding,
|
||||
onPhotoClicked = onConversationPhotoClicked
|
||||
)
|
||||
|
||||
if (screenState.conversations.isEmpty()) {
|
||||
NoItemsView(
|
||||
buttonText = stringResource(UiR.string.action_refresh),
|
||||
onButtonClick = onRefresh
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -413,9 +390,7 @@ fun HandleDialogs(
|
||||
)
|
||||
}
|
||||
|
||||
if (showOptions.showPinDialog != null) {
|
||||
val conversation = showOptions.showPinDialog
|
||||
|
||||
showOptions.showPinDialog?.let { conversation ->
|
||||
MaterialDialog(
|
||||
onDismissRequest = viewModel::onPinDialogDismissed,
|
||||
title = stringResource(
|
||||
|
||||
+3
-3
@@ -14,8 +14,6 @@ import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.common.model.UiText
|
||||
import dev.meloda.fast.common.model.parseString
|
||||
import dev.meloda.fast.common.util.TimeUtils
|
||||
import dev.meloda.fast.conversations.model.ActionState
|
||||
import dev.meloda.fast.conversations.model.UiConversation
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
import dev.meloda.fast.model.InteractionType
|
||||
@@ -24,6 +22,8 @@ import dev.meloda.fast.model.api.data.AttachmentType
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.ui.model.api.ActionState
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
/build
|
||||
@@ -0,0 +1,34 @@
|
||||
plugins {
|
||||
alias(libs.plugins.fast.android.feature)
|
||||
alias(libs.plugins.fast.android.library.compose)
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.meloda.fast.createchat"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(projects.core.common)
|
||||
implementation(projects.core.domain)
|
||||
implementation(projects.core.model)
|
||||
implementation(projects.core.ui)
|
||||
|
||||
implementation(libs.bundles.nanokt)
|
||||
|
||||
implementation(libs.koin.android)
|
||||
implementation(libs.koin.androidx.compose)
|
||||
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.bundles.compose)
|
||||
|
||||
implementation(libs.coil.compose)
|
||||
|
||||
implementation(libs.haze)
|
||||
implementation(libs.haze.materials)
|
||||
|
||||
implementation(libs.eithernet)
|
||||
|
||||
implementation(libs.androidx.navigation.compose)
|
||||
|
||||
implementation(libs.kotlin.serialization)
|
||||
}
|
||||
+243
@@ -0,0 +1,243 @@
|
||||
package dev.meloda.fast.conversations
|
||||
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import dev.meloda.fast.common.extensions.listenValue
|
||||
import dev.meloda.fast.common.extensions.setValue
|
||||
import dev.meloda.fast.conversations.model.CreateChatScreenState
|
||||
import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.domain.FriendsUseCase
|
||||
import dev.meloda.fast.domain.GetLocalUserByIdUseCase
|
||||
import dev.meloda.fast.domain.MessagesUseCase
|
||||
import dev.meloda.fast.domain.util.asPresentation
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.network.VkErrorCode
|
||||
import dev.meloda.fast.ui.model.api.UiFriend
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
interface CreateChatViewModel {
|
||||
|
||||
val screenState: StateFlow<CreateChatScreenState>
|
||||
val baseError: StateFlow<BaseError?>
|
||||
val currentOffset: StateFlow<Int>
|
||||
val canPaginate: StateFlow<Boolean>
|
||||
|
||||
val isChatCreated: StateFlow<Int?>
|
||||
|
||||
fun onPaginationConditionsMet()
|
||||
fun onRefresh()
|
||||
fun onErrorConsumed()
|
||||
|
||||
fun toggleFriendSelection(userId: Int)
|
||||
|
||||
fun onTitleTextInputChanged(newTitle: String)
|
||||
|
||||
fun onCreateChatButtonClicked()
|
||||
|
||||
fun onNavigatedBack()
|
||||
}
|
||||
|
||||
class CreateChatViewModelImpl(
|
||||
private val friendsUseCase: FriendsUseCase,
|
||||
private val messagesUseCase: MessagesUseCase,
|
||||
private val imageLoader: ImageLoader,
|
||||
private val applicationContext: Context,
|
||||
private val getLocalUserByIdUseCase: GetLocalUserByIdUseCase,
|
||||
private val userSettings: UserSettings
|
||||
) : CreateChatViewModel, ViewModel() {
|
||||
|
||||
override val screenState = MutableStateFlow(CreateChatScreenState.EMPTY)
|
||||
override val baseError = MutableStateFlow<BaseError?>(null)
|
||||
override val currentOffset = MutableStateFlow(0)
|
||||
override val canPaginate = MutableStateFlow(false)
|
||||
|
||||
override val isChatCreated = MutableStateFlow<Int?>(null)
|
||||
|
||||
private val useContactNames: Boolean = userSettings.useContactNames.value
|
||||
|
||||
init {
|
||||
loadFriends()
|
||||
}
|
||||
|
||||
override fun onPaginationConditionsMet() {
|
||||
currentOffset.update { screenState.value.friends.size }
|
||||
loadFriends()
|
||||
}
|
||||
|
||||
override fun onRefresh() {
|
||||
onErrorConsumed()
|
||||
loadFriends(offset = 0)
|
||||
}
|
||||
|
||||
override fun onErrorConsumed() {
|
||||
baseError.setValue { null }
|
||||
}
|
||||
|
||||
override fun toggleFriendSelection(userId: Int) {
|
||||
val newSelectionList = screenState.value.selectedFriendsIds.toMutableList()
|
||||
|
||||
if (newSelectionList.contains(userId)) {
|
||||
newSelectionList.remove(userId)
|
||||
} else {
|
||||
newSelectionList.add(userId)
|
||||
}
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(selectedFriendsIds = newSelectionList)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTitleTextInputChanged(newTitle: String) {
|
||||
screenState.setValue { old -> old.copy(chatTitle = newTitle) }
|
||||
}
|
||||
|
||||
override fun onCreateChatButtonClicked() {
|
||||
createChat()
|
||||
}
|
||||
|
||||
override fun onNavigatedBack() {
|
||||
viewModelScope.launch(Dispatchers.Main) {
|
||||
isChatCreated.emit(null)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadFriends(
|
||||
offset: Int = currentOffset.value
|
||||
) {
|
||||
friendsUseCase.getFriends(count = LOAD_COUNT, offset = offset)
|
||||
.listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = ::handleError,
|
||||
success = { response ->
|
||||
val itemsCountSufficient = response.size == LOAD_COUNT
|
||||
canPaginate.setValue { itemsCountSufficient }
|
||||
|
||||
val paginationExhausted = !itemsCountSufficient &&
|
||||
screenState.value.friends.isNotEmpty()
|
||||
|
||||
val imagesToPreload =
|
||||
response.mapNotNull { it.photo100.takeIf { !it.isNullOrEmpty() } }
|
||||
|
||||
imagesToPreload.forEach { url ->
|
||||
imageLoader.enqueue(
|
||||
ImageRequest.Builder(applicationContext)
|
||||
.data(url)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
|
||||
friendsUseCase.storeUsers(response)
|
||||
|
||||
val loadedFriends = response.map {
|
||||
it.asPresentation(useContactNames)
|
||||
}
|
||||
|
||||
val newState = screenState.value.copy(
|
||||
isPaginationExhausted = paginationExhausted
|
||||
)
|
||||
if (offset == 0) {
|
||||
screenState.setValue {
|
||||
newState.copy(friends = loadedFriends)
|
||||
}
|
||||
} else {
|
||||
screenState.setValue {
|
||||
newState.copy(
|
||||
friends = newState.friends.plus(loadedFriends)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
screenState.setValue { old ->
|
||||
old.copy(
|
||||
isLoading = offset == 0 && state.isLoading(),
|
||||
isPaginating = offset > 0 && state.isLoading()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createChat() {
|
||||
viewModelScope.launch {
|
||||
val title = screenState.value.chatTitle.takeUnless(String::isBlank)
|
||||
|
||||
val accountAsFriend =
|
||||
getLocalUserByIdUseCase.proceed(UserConfig.userId)?.asPresentation(useContactNames)
|
||||
|
||||
val accountList = accountAsFriend?.let(::listOf) ?: emptyList()
|
||||
|
||||
val selectedFriends = screenState.value.selectedFriendsIds
|
||||
.takeIf { it.isNotEmpty() }
|
||||
?.mapNotNull { userId -> screenState.value.friends.find { it.userId == userId } }
|
||||
|
||||
messagesUseCase.createChat(
|
||||
userIds = selectedFriends?.map { it.userId },
|
||||
title = title
|
||||
?: (accountList + selectedFriends.orEmpty()).joinToString(transform = UiFriend::firstName)
|
||||
).listenValue(viewModelScope) { state ->
|
||||
state.processState(
|
||||
error = ::handleError,
|
||||
success = { response ->
|
||||
withContext(Dispatchers.Main) {
|
||||
isChatCreated.emit(2_000_000_000 + response)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val LOAD_COUNT = 30
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package dev.meloda.fast.conversations.di
|
||||
|
||||
import dev.meloda.fast.conversations.CreateChatViewModelImpl
|
||||
import org.koin.core.module.dsl.viewModelOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val createChatModule = module {
|
||||
viewModelOf(::CreateChatViewModelImpl)
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package dev.meloda.fast.conversations.model
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import dev.meloda.fast.ui.model.api.UiFriend
|
||||
|
||||
@Immutable
|
||||
data class CreateChatScreenState(
|
||||
val isLoading: Boolean,
|
||||
val isPaginating: Boolean,
|
||||
val isPaginationExhausted: Boolean,
|
||||
val friends: List<UiFriend>,
|
||||
val selectedFriendsIds: List<Int>,
|
||||
val chatTitle: String
|
||||
) {
|
||||
companion object {
|
||||
val EMPTY: CreateChatScreenState = CreateChatScreenState(
|
||||
isLoading = true,
|
||||
isPaginating = false,
|
||||
isPaginationExhausted = false,
|
||||
friends = emptyList(),
|
||||
selectedFriendsIds = emptyList(),
|
||||
chatTitle = ""
|
||||
)
|
||||
}
|
||||
}
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
package dev.meloda.fast.conversations.navigation
|
||||
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import dev.meloda.fast.conversations.CreateChatViewModel
|
||||
import dev.meloda.fast.conversations.CreateChatViewModelImpl
|
||||
import dev.meloda.fast.conversations.presentation.CreateChatRoute
|
||||
import dev.meloda.fast.ui.extensions.sharedViewModel
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
object CreateChat
|
||||
|
||||
fun NavGraphBuilder.createChatScreen(
|
||||
onChatCreated: (Int) -> Unit,
|
||||
navController: NavController,
|
||||
) {
|
||||
composable<CreateChat> {
|
||||
val viewModel: CreateChatViewModel =
|
||||
it.sharedViewModel<CreateChatViewModelImpl>(navController = navController)
|
||||
|
||||
CreateChatRoute(
|
||||
onError = {
|
||||
|
||||
},
|
||||
onBack = navController::popBackStack,
|
||||
onChatCreated = onChatCreated,
|
||||
viewModel = viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToCreateChat() {
|
||||
this.navigate(CreateChat)
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package dev.meloda.fast.conversations.presentation
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.graphics.ColorFilter
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.model.api.UiFriend
|
||||
|
||||
|
||||
@Composable
|
||||
fun CreateChatItem(
|
||||
modifier: Modifier = Modifier,
|
||||
friend: UiFriend,
|
||||
maxLines: Int,
|
||||
isSelected: Boolean,
|
||||
onItemClicked: (Int) -> Unit
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onItemClicked(friend.userId) }
|
||||
.padding(vertical = 8.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
val friendAvatar = friend.avatar?.extractUrl()
|
||||
|
||||
Box(modifier = Modifier.size(56.dp)) {
|
||||
if (friendAvatar == null) {
|
||||
Image(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape),
|
||||
painter = painterResource(id = R.drawable.ic_account_circle_cut),
|
||||
contentDescription = "Avatar",
|
||||
colorFilter = ColorFilter.tint(MaterialTheme.colorScheme.onBackground)
|
||||
)
|
||||
} else {
|
||||
AsyncImage(
|
||||
model = friendAvatar,
|
||||
contentDescription = null,
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape),
|
||||
placeholder = painterResource(id = R.drawable.ic_account_circle_cut)
|
||||
)
|
||||
}
|
||||
|
||||
if (friend.onlineStatus.isOnline()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.size(18.dp)
|
||||
.background(MaterialTheme.colorScheme.background)
|
||||
.padding(2.dp)
|
||||
.align(Alignment.BottomEnd)
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.matchParentSize()
|
||||
.background(MaterialTheme.colorScheme.primary)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Text(
|
||||
text = friend.title,
|
||||
minLines = 1,
|
||||
maxLines = maxLines,
|
||||
style = MaterialTheme.typography.headlineSmall.copy(fontSize = 20.sp),
|
||||
modifier = Modifier.weight(1f)
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
|
||||
Checkbox(
|
||||
checked = isSelected,
|
||||
onCheckedChange = { onItemClicked(friend.userId) },
|
||||
)
|
||||
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user