Compare commits
22 Commits
5310596cf6
...
0.2.3
| Author | SHA1 | Date | |
|---|---|---|---|
| 155a3666ad | |||
| ce375c902c | |||
| 96b4fc8539 | |||
| e3e9157dd5 | |||
| 5aa28066d7 | |||
| 1638d70ef2 | |||
| 8c13d9e7e1 | |||
| 5dd829b6f6 | |||
| 2a238fa1bf | |||
| 3f54961ac6 | |||
| 045f2e8268 | |||
| 3eb33b2612 | |||
| f2d565fd3e | |||
| 7ab333280c | |||
| 45ee0acea5 | |||
| 7b6571f208 | |||
| 389d3f9e52 | |||
| 69a50f8fcd | |||
| 8839015249 | |||
| 478639e427 | |||
| dcbfd43896 | |||
| 7b2c102470 |
@@ -40,7 +40,7 @@ jobs:
|
||||
echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload APK with original name
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ env.APK_NAME }}
|
||||
path: ${{ env.APK_PATH }}
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV
|
||||
|
||||
- name: Upload APK with original name
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: ${{ env.APK_NAME }}
|
||||
path: ${{ env.APK_PATH }}
|
||||
|
||||
@@ -34,7 +34,7 @@ jobs:
|
||||
run: ./gradlew assembleRelease
|
||||
|
||||
- name: Upload release APK
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: app-release.apk
|
||||
path: app/build/outputs/apk/release/app-release.apk
|
||||
@@ -43,7 +43,7 @@ jobs:
|
||||
run: ./gradlew bundleRelease
|
||||
|
||||
- name: Upload release Bundle
|
||||
uses: actions/upload-artifact@v5
|
||||
uses: actions/upload-artifact@v6
|
||||
with:
|
||||
name: app-release.aab
|
||||
path: app/build/outputs/bundle/release/app-release.aab
|
||||
|
||||
+14
-14
@@ -13,8 +13,8 @@ android {
|
||||
defaultConfig {
|
||||
applicationId = "dev.meloda.fastvk"
|
||||
|
||||
versionCode = 10
|
||||
versionName = "0.2.2"
|
||||
versionCode = 11
|
||||
versionName = "0.2.3"
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -59,17 +59,17 @@ android {
|
||||
}
|
||||
}
|
||||
|
||||
applicationVariants.all {
|
||||
outputs.all {
|
||||
val date = System.currentTimeMillis() / 1000
|
||||
val buildType = buildType.name
|
||||
val appVersion = versionName
|
||||
val appVersionCode = versionCode
|
||||
|
||||
val newApkName = "app-$buildType-v$appVersion($appVersionCode)-$date.apk"
|
||||
(this as? BaseVariantOutputImpl)?.outputFileName = newApkName
|
||||
}
|
||||
}
|
||||
// applicationVariants.all {
|
||||
// outputs.all {
|
||||
// val date = System.currentTimeMillis() / 1000
|
||||
// val buildType = buildType.name
|
||||
// val appVersion = versionName
|
||||
// val appVersionCode = versionCode
|
||||
//
|
||||
// val newApkName = "app-$buildType-v$appVersion($appVersionCode)-$date.apk"
|
||||
// (this as? BaseVariantOutputImpl)?.outputFileName = newApkName
|
||||
// }
|
||||
// }
|
||||
|
||||
packaging {
|
||||
resources {
|
||||
@@ -82,7 +82,7 @@ dependencies {
|
||||
implementation(projects.feature.auth)
|
||||
|
||||
implementation(projects.feature.chatmaterials)
|
||||
implementation(projects.feature.conversations)
|
||||
implementation(projects.feature.convos)
|
||||
implementation(projects.feature.languagepicker)
|
||||
implementation(projects.feature.messageshistory)
|
||||
implementation(projects.feature.photoviewer)
|
||||
|
||||
@@ -4,6 +4,8 @@ import android.app.Application
|
||||
import androidx.preference.PreferenceManager
|
||||
import coil.ImageLoader
|
||||
import coil.ImageLoaderFactory
|
||||
import com.skydoves.compose.stability.runtime.ComposeStabilityAnalyzer
|
||||
import dev.meloda.fast.auth.BuildConfig
|
||||
import dev.meloda.fast.common.di.applicationModule
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import org.koin.android.ext.android.get
|
||||
@@ -20,6 +22,8 @@ class AppGlobal : Application(), ImageLoaderFactory {
|
||||
AppSettings.init(preferences)
|
||||
|
||||
initKoin()
|
||||
|
||||
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
|
||||
}
|
||||
|
||||
private fun initKoin() {
|
||||
|
||||
@@ -16,8 +16,8 @@ import dev.meloda.fast.common.LongPollControllerImpl
|
||||
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.convos.di.convosModule
|
||||
import dev.meloda.fast.convos.di.createChatModule
|
||||
import dev.meloda.fast.domain.di.domainModule
|
||||
import dev.meloda.fast.friends.di.friendsModule
|
||||
import dev.meloda.fast.languagepicker.di.languagePickerModule
|
||||
@@ -41,7 +41,7 @@ val applicationModule = module {
|
||||
loginModule,
|
||||
validationModule,
|
||||
captchaModule,
|
||||
conversationsModule,
|
||||
convosModule,
|
||||
settingsModule,
|
||||
messagesHistoryModule,
|
||||
photoViewModule,
|
||||
|
||||
@@ -2,7 +2,7 @@ package dev.meloda.fast.navigation
|
||||
|
||||
import androidx.navigation.NavGraphBuilder
|
||||
import androidx.navigation.compose.composable
|
||||
import dev.meloda.fast.conversations.navigation.ConversationsGraph
|
||||
import dev.meloda.fast.convos.navigation.ConvoGraph
|
||||
import dev.meloda.fast.friends.navigation.Friends
|
||||
import dev.meloda.fast.model.BaseError
|
||||
import dev.meloda.fast.model.BottomNavigationItem
|
||||
@@ -21,7 +21,7 @@ object Main
|
||||
fun NavGraphBuilder.mainScreen(
|
||||
onError: (BaseError) -> Unit,
|
||||
onSettingsButtonClicked: () -> Unit,
|
||||
onNavigateToMessagesHistory: (conversationId: Long) -> Unit,
|
||||
onNavigateToMessagesHistory: (convoId: Long) -> Unit,
|
||||
onPhotoClicked: (url: String) -> Unit,
|
||||
onMessageClicked: (userid: Long) -> Unit,
|
||||
onNavigateToCreateChat: () -> Unit
|
||||
@@ -29,20 +29,20 @@ fun NavGraphBuilder.mainScreen(
|
||||
val navigationItems = ImmutableList.of(
|
||||
BottomNavigationItem(
|
||||
titleResId = R.string.title_friends,
|
||||
selectedIconResId = R.drawable.baseline_people_alt_24,
|
||||
unselectedIconResId = R.drawable.outline_people_alt_24,
|
||||
selectedIconResId = R.drawable.ic_group_fill_round_24,
|
||||
unselectedIconResId = R.drawable.ic_group_round_24,
|
||||
route = Friends,
|
||||
),
|
||||
BottomNavigationItem(
|
||||
titleResId = R.string.title_conversations,
|
||||
selectedIconResId = R.drawable.baseline_chat_24,
|
||||
unselectedIconResId = R.drawable.outline_chat_24,
|
||||
route = ConversationsGraph
|
||||
titleResId = R.string.title_convos,
|
||||
selectedIconResId = R.drawable.ic_mail_fill_round_24,
|
||||
unselectedIconResId = R.drawable.ic_mail_round_24,
|
||||
route = ConvoGraph
|
||||
),
|
||||
BottomNavigationItem(
|
||||
titleResId = R.string.title_profile,
|
||||
selectedIconResId = R.drawable.baseline_account_circle_24,
|
||||
unselectedIconResId = R.drawable.outline_account_circle_24,
|
||||
selectedIconResId = R.drawable.ic_account_circle_fill_round_24,
|
||||
unselectedIconResId = R.drawable.ic_account_circle_round_24,
|
||||
route = Profile
|
||||
)
|
||||
)
|
||||
|
||||
@@ -4,7 +4,6 @@ import android.Manifest
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
@@ -15,43 +14,20 @@ import androidx.activity.SystemBarStyle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.conena.nanokt.android.content.pxToDp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import dev.meloda.fast.MainViewModel
|
||||
import dev.meloda.fast.MainViewModelImpl
|
||||
import dev.meloda.fast.common.AppConstants
|
||||
import dev.meloda.fast.common.LongPollController
|
||||
import dev.meloda.fast.common.model.LongPollState
|
||||
import dev.meloda.fast.datastore.AppSettings
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
import dev.meloda.fast.service.OnlineService
|
||||
import dev.meloda.fast.service.longpolling.LongPollingService
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.common.LocalSizeConfig
|
||||
import dev.meloda.fast.ui.model.DeviceSize
|
||||
import dev.meloda.fast.ui.model.SizeConfig
|
||||
import dev.meloda.fast.ui.model.ThemeConfig
|
||||
import dev.meloda.fast.ui.theme.AppTheme
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.theme.LocalUser
|
||||
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
class MainActivity : AppCompatActivity() {
|
||||
|
||||
@@ -88,168 +64,26 @@ class MainActivity : AppCompatActivity() {
|
||||
requestNotificationPermissions()
|
||||
|
||||
setContent {
|
||||
val resources = LocalResources.current
|
||||
|
||||
val userSettings: UserSettings = koinInject()
|
||||
val longPollController: LongPollController = koinInject()
|
||||
|
||||
val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle()
|
||||
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
|
||||
|
||||
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
|
||||
|
||||
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
|
||||
LaunchedEffect(viewModel) {
|
||||
Log.d("VM_CREATE", "onCreate: viewModel: $viewModel")
|
||||
}
|
||||
|
||||
LifecycleResumeEffect(true) {
|
||||
viewModel.onAppResumed(intent)
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
val permissionState =
|
||||
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
|
||||
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(isNeedToCheckPermission) {
|
||||
if (isNeedToCheckPermission) {
|
||||
viewModel.onPermissionCheckStatus(permissionState.status)
|
||||
|
||||
if (permissionState.status.isGranted) {
|
||||
if (longPollCurrentState == LongPollState.InApp) {
|
||||
toggleLongPollService(false)
|
||||
}
|
||||
|
||||
RootScreen(
|
||||
toggleLongPollService = { enable, inBackground ->
|
||||
toggleLongPollService(
|
||||
enable = true,
|
||||
inBackground = true
|
||||
enable = enable,
|
||||
inBackground = inBackground ?: AppSettings.Experimental.longPollInBackground
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isNeedToRequestPermission) {
|
||||
if (isNeedToRequestPermission) {
|
||||
viewModel.onPermissionsRequested()
|
||||
permissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
LifecycleResumeEffect(longPollStateToApply) {
|
||||
Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply")
|
||||
if (longPollStateToApply != LongPollState.Background) {
|
||||
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
|
||||
&& longPollCurrentState != longPollStateToApply
|
||||
) {
|
||||
toggleLongPollService(false)
|
||||
Log.d("LongPoll", "recreate()")
|
||||
}
|
||||
|
||||
toggleLongPollService(
|
||||
enable = longPollStateToApply.isLaunched(),
|
||||
inBackground = longPollStateToApply == LongPollState.Background
|
||||
},
|
||||
toggleOnlineService = ::toggleOnlineService
|
||||
)
|
||||
}
|
||||
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
|
||||
LifecycleResumeEffect(sendOnline) {
|
||||
toggleOnlineService(sendOnline)
|
||||
|
||||
onPauseOrDispose {
|
||||
toggleOnlineService(false)
|
||||
}
|
||||
}
|
||||
|
||||
val deviceWidthDp = remember(true) {
|
||||
resources.displayMetrics.widthPixels.pxToDp()
|
||||
}
|
||||
val deviceHeightDp = remember(true) {
|
||||
resources.displayMetrics.heightPixels.pxToDp()
|
||||
}
|
||||
|
||||
val deviceWidthSize by remember(deviceWidthDp) {
|
||||
derivedStateOf {
|
||||
when {
|
||||
deviceWidthDp <= 360 -> DeviceSize.Small
|
||||
deviceWidthDp <= 600 -> DeviceSize.Compact
|
||||
deviceWidthDp <= 840 -> DeviceSize.Medium
|
||||
else -> DeviceSize.Expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val deviceHeightSize by remember(deviceHeightDp) {
|
||||
derivedStateOf {
|
||||
when {
|
||||
deviceHeightDp <= 480 -> DeviceSize.Small
|
||||
deviceHeightDp <= 700 -> DeviceSize.Compact
|
||||
deviceHeightDp <= 900 -> DeviceSize.Medium
|
||||
else -> DeviceSize.Expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sizeConfig by remember(deviceWidthSize, deviceHeightSize) {
|
||||
mutableStateOf(
|
||||
SizeConfig(
|
||||
widthSize = deviceWidthSize,
|
||||
heightSize = deviceHeightSize
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val darkMode by userSettings.darkMode.collectAsStateWithLifecycle()
|
||||
val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle()
|
||||
val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle()
|
||||
val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle()
|
||||
val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle()
|
||||
val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle()
|
||||
val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle()
|
||||
|
||||
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode)
|
||||
|
||||
val themeConfig by remember(
|
||||
darkMode,
|
||||
dynamicColors,
|
||||
amoledDark,
|
||||
enableBlur,
|
||||
enableMultiline,
|
||||
setDarkMode,
|
||||
useSystemFont
|
||||
) {
|
||||
derivedStateOf {
|
||||
ThemeConfig(
|
||||
darkMode = setDarkMode,
|
||||
dynamicColors = dynamicColors,
|
||||
selectedColorScheme = 0,
|
||||
amoledDark = amoledDark,
|
||||
enableBlur = enableBlur,
|
||||
enableMultiline = enableMultiline,
|
||||
useSystemFont = useSystemFont,
|
||||
enableAnimations = enableAnimations
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalThemeConfig provides themeConfig,
|
||||
LocalSizeConfig provides sizeConfig,
|
||||
LocalUser provides currentUser
|
||||
) {
|
||||
AppTheme(
|
||||
useDarkTheme = themeConfig.darkMode,
|
||||
useDynamicColors = themeConfig.dynamicColors,
|
||||
selectedColorScheme = themeConfig.selectedColorScheme,
|
||||
useAmoledBackground = themeConfig.amoledDark,
|
||||
useSystemFont = themeConfig.useSystemFont
|
||||
) {
|
||||
RootScreen(viewModel = viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun createNotificationChannels() {
|
||||
@@ -279,7 +113,7 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
|
||||
val notificationManager: NotificationManager =
|
||||
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
getSystemService(NOTIFICATION_SERVICE) as NotificationManager
|
||||
|
||||
notificationManager.createNotificationChannels(
|
||||
listOf(
|
||||
|
||||
@@ -38,8 +38,8 @@ import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeEffect
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import dev.chrisbanes.haze.materials.HazeMaterials
|
||||
import dev.meloda.fast.conversations.navigation.ConversationsGraph
|
||||
import dev.meloda.fast.conversations.navigation.conversationsGraph
|
||||
import dev.meloda.fast.convos.navigation.ConvoGraph
|
||||
import dev.meloda.fast.convos.navigation.convosGraph
|
||||
import dev.meloda.fast.friends.navigation.Friends
|
||||
import dev.meloda.fast.friends.navigation.friendsScreen
|
||||
import dev.meloda.fast.model.BaseError
|
||||
@@ -60,7 +60,7 @@ fun MainScreen(
|
||||
navigationItems: ImmutableList<BottomNavigationItem>,
|
||||
onError: (BaseError) -> Unit = {},
|
||||
onSettingsButtonClicked: () -> Unit = {},
|
||||
onNavigateToMessagesHistory: (conversationId: Long) -> Unit = {},
|
||||
onNavigateToMessagesHistory: (convoId: Long) -> Unit = {},
|
||||
onPhotoClicked: (url: String) -> Unit = {},
|
||||
onMessageClicked: (userid: Long) -> Unit = {},
|
||||
onNavigateToCreateChat: () -> Unit = {}
|
||||
@@ -197,14 +197,14 @@ fun MainScreen(
|
||||
}
|
||||
},
|
||||
)
|
||||
conversationsGraph(
|
||||
convosGraph(
|
||||
activity = activity,
|
||||
onError = onError,
|
||||
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
|
||||
onNavigateToCreateChat = onNavigateToCreateChat,
|
||||
onScrolledToTop = {
|
||||
tabReselected = tabReselected.toMutableMap().also {
|
||||
it[ConversationsGraph] = false
|
||||
it[ConvoGraph] = false
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package dev.meloda.fast.presentation
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.activity.compose.LocalActivity
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.fadeIn
|
||||
@@ -15,44 +17,223 @@ import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.platform.LocalResources
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import androidx.lifecycle.compose.LifecycleResumeEffect
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.navigation.NavController
|
||||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.NavHost
|
||||
import androidx.navigation.compose.rememberNavController
|
||||
import com.conena.nanokt.android.content.pxToDp
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.isGranted
|
||||
import com.google.accompanist.permissions.rememberPermissionState
|
||||
import dev.meloda.fast.MainViewModel
|
||||
import dev.meloda.fast.MainViewModelImpl
|
||||
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.common.LongPollController
|
||||
import dev.meloda.fast.common.model.LongPollState
|
||||
import dev.meloda.fast.convos.navigation.createChatScreen
|
||||
import dev.meloda.fast.convos.navigation.navigateToCreateChat
|
||||
import dev.meloda.fast.datastore.UserSettings
|
||||
import dev.meloda.fast.languagepicker.navigation.languagePickerScreen
|
||||
import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker
|
||||
import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen
|
||||
import dev.meloda.fast.messageshistory.navigation.navigateToMessagesHistory
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
import dev.meloda.fast.navigation.Main
|
||||
import dev.meloda.fast.navigation.mainScreen
|
||||
import dev.meloda.fast.photoviewer.presentation.PhotoViewDialog
|
||||
import dev.meloda.fast.settings.navigation.navigateToSettings
|
||||
import dev.meloda.fast.settings.navigation.settingsScreen
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.common.LocalSizeConfig
|
||||
import dev.meloda.fast.ui.model.DeviceSize
|
||||
import dev.meloda.fast.ui.model.SizeConfig
|
||||
import dev.meloda.fast.ui.model.ThemeConfig
|
||||
import dev.meloda.fast.ui.theme.AppTheme
|
||||
import dev.meloda.fast.ui.theme.LocalNavController
|
||||
import dev.meloda.fast.ui.theme.LocalNavRootController
|
||||
import dev.meloda.fast.ui.theme.LocalThemeConfig
|
||||
import dev.meloda.fast.ui.theme.LocalUser
|
||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
import dev.meloda.fast.ui.util.isNeedToEnableDarkMode
|
||||
import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
@Composable
|
||||
fun RootScreen(
|
||||
navController: NavHostController = rememberNavController(),
|
||||
viewModel: MainViewModel
|
||||
toggleLongPollService: (enable: Boolean, inBackground: Boolean?) -> Unit,
|
||||
toggleOnlineService: (enable: Boolean) -> Unit
|
||||
) {
|
||||
val resources = LocalResources.current
|
||||
|
||||
val userSettings: UserSettings = koinInject()
|
||||
val longPollController: LongPollController = koinInject()
|
||||
|
||||
val longPollCurrentState by longPollController.currentState.collectAsStateWithLifecycle()
|
||||
val longPollStateToApply by longPollController.stateToApply.collectAsStateWithLifecycle()
|
||||
|
||||
val viewModel: MainViewModel = koinViewModel<MainViewModelImpl>()
|
||||
LaunchedEffect(viewModel) {
|
||||
Log.d("VM_CREATE", "RootScreen(): viewModel: $viewModel")
|
||||
}
|
||||
|
||||
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
|
||||
|
||||
val permissionState =
|
||||
rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS)
|
||||
|
||||
val isNeedToCheckPermission by viewModel.isNeedToCheckNotificationsPermission.collectAsStateWithLifecycle()
|
||||
val isNeedToRequestPermission by viewModel.isNeedToRequestNotifications.collectAsStateWithLifecycle()
|
||||
|
||||
LaunchedEffect(isNeedToCheckPermission) {
|
||||
if (isNeedToCheckPermission) {
|
||||
viewModel.onPermissionCheckStatus(permissionState.status)
|
||||
|
||||
if (permissionState.status.isGranted) {
|
||||
if (longPollCurrentState == LongPollState.InApp) {
|
||||
toggleLongPollService(false, null)
|
||||
}
|
||||
|
||||
toggleLongPollService(true, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(isNeedToRequestPermission) {
|
||||
if (isNeedToRequestPermission) {
|
||||
viewModel.onPermissionsRequested()
|
||||
permissionState.launchPermissionRequest()
|
||||
}
|
||||
}
|
||||
|
||||
LifecycleResumeEffect(longPollStateToApply) {
|
||||
Log.d("LongPollMainActivity", "longPollStateToApply: $longPollStateToApply")
|
||||
if (longPollStateToApply != LongPollState.Background) {
|
||||
if (longPollStateToApply.isLaunched() && longPollCurrentState.isLaunched()
|
||||
&& longPollCurrentState != longPollStateToApply
|
||||
) {
|
||||
toggleLongPollService(false, null)
|
||||
Log.d("LongPoll", "recreate()")
|
||||
}
|
||||
|
||||
toggleLongPollService(
|
||||
longPollStateToApply.isLaunched(),
|
||||
longPollStateToApply == LongPollState.Background
|
||||
)
|
||||
}
|
||||
|
||||
onPauseOrDispose {}
|
||||
}
|
||||
|
||||
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
|
||||
LifecycleResumeEffect(sendOnline) {
|
||||
toggleOnlineService(sendOnline)
|
||||
|
||||
onPauseOrDispose {
|
||||
toggleOnlineService(false)
|
||||
}
|
||||
}
|
||||
|
||||
val deviceWidthDp = remember(true) {
|
||||
resources.displayMetrics.widthPixels.pxToDp()
|
||||
}
|
||||
val deviceHeightDp = remember(true) {
|
||||
resources.displayMetrics.heightPixels.pxToDp()
|
||||
}
|
||||
|
||||
val deviceWidthSize by remember(deviceWidthDp) {
|
||||
derivedStateOf {
|
||||
when {
|
||||
deviceWidthDp <= 360 -> DeviceSize.Small
|
||||
deviceWidthDp <= 600 -> DeviceSize.Compact
|
||||
deviceWidthDp <= 840 -> DeviceSize.Medium
|
||||
else -> DeviceSize.Expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val deviceHeightSize by remember(deviceHeightDp) {
|
||||
derivedStateOf {
|
||||
when {
|
||||
deviceHeightDp <= 480 -> DeviceSize.Small
|
||||
deviceHeightDp <= 700 -> DeviceSize.Compact
|
||||
deviceHeightDp <= 900 -> DeviceSize.Medium
|
||||
else -> DeviceSize.Expanded
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val sizeConfig by remember(deviceWidthSize, deviceHeightSize) {
|
||||
mutableStateOf(
|
||||
SizeConfig(
|
||||
widthSize = deviceWidthSize,
|
||||
heightSize = deviceHeightSize
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
val darkMode by userSettings.darkMode.collectAsStateWithLifecycle()
|
||||
val dynamicColors by userSettings.enableDynamicColors.collectAsStateWithLifecycle()
|
||||
val amoledDark by userSettings.enableAmoledDark.collectAsStateWithLifecycle()
|
||||
val enableBlur by userSettings.useBlur.collectAsStateWithLifecycle()
|
||||
val enableMultiline by userSettings.enableMultiline.collectAsStateWithLifecycle()
|
||||
val useSystemFont by userSettings.useSystemFont.collectAsStateWithLifecycle()
|
||||
val enableAnimations by userSettings.enableAnimations.collectAsStateWithLifecycle()
|
||||
|
||||
val setDarkMode = isNeedToEnableDarkMode(darkMode = darkMode)
|
||||
|
||||
val themeConfig by remember(
|
||||
darkMode,
|
||||
dynamicColors,
|
||||
amoledDark,
|
||||
enableBlur,
|
||||
enableMultiline,
|
||||
setDarkMode,
|
||||
useSystemFont
|
||||
) {
|
||||
derivedStateOf {
|
||||
ThemeConfig(
|
||||
darkMode = setDarkMode,
|
||||
dynamicColors = dynamicColors,
|
||||
selectedColorScheme = 0,
|
||||
amoledDark = amoledDark,
|
||||
enableBlur = enableBlur,
|
||||
enableMultiline = enableMultiline,
|
||||
useSystemFont = useSystemFont,
|
||||
enableAnimations = enableAnimations
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
CompositionLocalProvider(
|
||||
LocalThemeConfig provides themeConfig,
|
||||
LocalSizeConfig provides sizeConfig,
|
||||
LocalUser provides currentUser
|
||||
) {
|
||||
AppTheme(
|
||||
useDarkTheme = themeConfig.darkMode,
|
||||
useDynamicColors = themeConfig.dynamicColors,
|
||||
selectedColorScheme = themeConfig.selectedColorScheme,
|
||||
useAmoledBackground = themeConfig.amoledDark,
|
||||
useSystemFont = themeConfig.useSystemFont
|
||||
) {
|
||||
val navController: NavHostController = rememberNavController()
|
||||
val activity = LocalActivity.current
|
||||
val context = LocalContext.current
|
||||
val startDestination by viewModel.startDestination.collectAsStateWithLifecycle()
|
||||
@@ -149,7 +330,9 @@ fun RootScreen(
|
||||
onError = viewModel::onError,
|
||||
onSettingsButtonClicked = navController::navigateToSettings,
|
||||
onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
|
||||
onPhotoClicked = { url -> photoViewerInfo = listOf(url) to null },
|
||||
onPhotoClicked = { url ->
|
||||
photoViewerInfo = listOf(url) to null
|
||||
},
|
||||
onMessageClicked = navController::navigateToMessagesHistory,
|
||||
onNavigateToCreateChat = navController::navigateToCreateChat
|
||||
)
|
||||
@@ -164,12 +347,14 @@ fun RootScreen(
|
||||
)
|
||||
chatMaterialsScreen(
|
||||
onBack = navController::navigateUp,
|
||||
onPhotoClicked = { url -> photoViewerInfo = listOf(url) to null }
|
||||
onPhotoClicked = { url ->
|
||||
photoViewerInfo = listOf(url) to null
|
||||
}
|
||||
)
|
||||
createChatScreen(
|
||||
onChatCreated = { conversationId ->
|
||||
onChatCreated = { convoId ->
|
||||
navController.popBackStack()
|
||||
navController.navigateToMessagesHistory(conversationId)
|
||||
navController.navigateToMessagesHistory(convoId)
|
||||
},
|
||||
navController = navController
|
||||
)
|
||||
@@ -191,12 +376,16 @@ fun RootScreen(
|
||||
}
|
||||
|
||||
PhotoViewDialog(
|
||||
photoViewerInfo = photoViewerInfo,
|
||||
photoViewerInfo = photoViewerInfo?.let { info ->
|
||||
info.first.toImmutableList() to info.second
|
||||
},
|
||||
onDismiss = { photoViewerInfo = null }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun NavController.navigateToMain() {
|
||||
|
||||
@@ -10,6 +10,7 @@ class AndroidApplicationComposeConventionPlugin : Plugin<Project> {
|
||||
with(target) {
|
||||
apply(plugin = "com.android.application")
|
||||
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
||||
apply(plugin = "com.github.skydoves.compose.stability.analyzer")
|
||||
|
||||
val extension = extensions.getByType<ApplicationExtension>()
|
||||
configureAndroidCompose(extension)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import dev.meloda.fast.configureKotlinAndroid
|
||||
import dev.meloda.fast.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
@@ -10,12 +9,15 @@ class AndroidApplicationConventionPlugin : Plugin<Project> {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.android.application")
|
||||
apply("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
extensions.configure<ApplicationExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
defaultConfig.targetSdk = 36
|
||||
defaultConfig {
|
||||
targetSdk = 36
|
||||
compileSdk = 36
|
||||
minSdk = 23
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
import dev.meloda.fast.configureAndroidCompose
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
@@ -10,6 +10,7 @@ class AndroidLibraryComposeConventionPlugin : Plugin<Project> {
|
||||
with(target) {
|
||||
apply(plugin = "com.android.library")
|
||||
apply(plugin = "org.jetbrains.kotlin.plugin.compose")
|
||||
apply(plugin = "com.github.skydoves.compose.stability.analyzer")
|
||||
|
||||
val extension = extensions.getByType<LibraryExtension>()
|
||||
extension.androidResources.enable = false
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
import com.android.build.api.variant.LibraryAndroidComponentsExtension
|
||||
import com.android.build.gradle.LibraryExtension
|
||||
import dev.meloda.fast.configureKotlinAndroid
|
||||
import dev.meloda.fast.disableUnnecessaryAndroidTests
|
||||
import org.gradle.api.Plugin
|
||||
@@ -13,7 +13,6 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.android.library")
|
||||
apply("org.jetbrains.kotlin.android")
|
||||
apply("org.jetbrains.kotlin.plugin.parcelize")
|
||||
apply("org.jetbrains.kotlin.plugin.serialization")
|
||||
}
|
||||
@@ -21,7 +20,6 @@ class AndroidLibraryConventionPlugin : Plugin<Project> {
|
||||
extensions.configure<LibraryExtension> {
|
||||
configureKotlinAndroid(this)
|
||||
androidResources.enable = false
|
||||
defaultConfig.targetSdk = 36
|
||||
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
extensions.configure<LibraryAndroidComponentsExtension> {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import com.android.build.gradle.TestExtension
|
||||
import com.android.build.api.dsl.TestExtension
|
||||
import dev.meloda.fast.configureKotlinAndroid
|
||||
import dev.meloda.fast.libs
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.configure
|
||||
@@ -10,7 +9,6 @@ class AndroidTestConventionPlugin : Plugin<Project> {
|
||||
with(target) {
|
||||
with(pluginManager) {
|
||||
apply("com.android.test")
|
||||
apply("org.jetbrains.kotlin.android")
|
||||
}
|
||||
|
||||
extensions.configure<TestExtension> {
|
||||
|
||||
@@ -5,12 +5,10 @@ import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.dependencies
|
||||
|
||||
internal fun Project.configureAndroidCompose(
|
||||
commonExtension: CommonExtension<*, *, *, *, *, *>,
|
||||
commonExtension: CommonExtension,
|
||||
) {
|
||||
commonExtension.apply {
|
||||
buildFeatures {
|
||||
compose = true
|
||||
}
|
||||
buildFeatures.compose = true
|
||||
|
||||
dependencies {
|
||||
val bom = libs.findLibrary("compose-bom").get()
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package dev.meloda.fast
|
||||
|
||||
import com.android.build.api.dsl.ApplicationExtension
|
||||
import com.android.build.api.dsl.CommonExtension
|
||||
import com.android.build.api.dsl.CompileOptions
|
||||
import com.android.build.api.dsl.LibraryExtension
|
||||
import org.gradle.api.JavaVersion
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.plugins.JavaPluginExtension
|
||||
@@ -13,24 +16,25 @@ import org.jetbrains.kotlin.gradle.dsl.KotlinBaseExtension
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinJvmProjectExtension
|
||||
|
||||
internal fun Project.configureKotlinAndroid(
|
||||
commonExtension: CommonExtension<*, *, *, *, *, *>,
|
||||
commonExtension: CommonExtension,
|
||||
) {
|
||||
when (commonExtension) {
|
||||
is ApplicationExtension -> commonExtension.compileOptions(buildCompileOptions())
|
||||
is LibraryExtension -> commonExtension.compileOptions(buildCompileOptions())
|
||||
}
|
||||
|
||||
commonExtension.apply {
|
||||
compileSdk = 36
|
||||
|
||||
defaultConfig {
|
||||
minSdk = 23
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
}
|
||||
|
||||
configureKotlin<KotlinAndroidProjectExtension>()
|
||||
}
|
||||
|
||||
private fun buildCompileOptions(): CompileOptions.() -> Unit = {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
targetCompatibility = JavaVersion.VERSION_21
|
||||
}
|
||||
|
||||
internal fun Project.configureKotlinJvm() {
|
||||
extensions.configure<JavaPluginExtension> {
|
||||
sourceCompatibility = JavaVersion.VERSION_21
|
||||
@@ -47,7 +51,7 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() =
|
||||
when (this) {
|
||||
is KotlinAndroidProjectExtension -> compilerOptions
|
||||
is KotlinJvmProjectExtension -> compilerOptions
|
||||
else -> TODO("Unsupported project extension $this ${T::class}")
|
||||
else -> throw IllegalArgumentException("Unsupported project extension $this ${T::class}")
|
||||
}.apply {
|
||||
jvmTarget = JvmTarget.JVM_21
|
||||
allWarningsAsErrors = warningsAsErrors.toBoolean()
|
||||
@@ -55,7 +59,8 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() =
|
||||
"-opt-in=kotlin.RequiresOptIn",
|
||||
// Enable experimental coroutines APIs, including Flow
|
||||
"-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview"
|
||||
"-opt-in=kotlinx.coroutines.FlowPreview",
|
||||
"-Xannotation-default-target=param-property",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+2
-1
@@ -1,7 +1,6 @@
|
||||
plugins {
|
||||
alias(libs.plugins.android.application) apply false
|
||||
alias(libs.plugins.android.library) apply false
|
||||
alias(libs.plugins.kotlin.android) apply false
|
||||
alias(libs.plugins.kotlin.parcelize) apply false
|
||||
alias(libs.plugins.kotlin.serialization) apply false
|
||||
alias(libs.plugins.kotlin.jvm) apply false
|
||||
@@ -9,4 +8,6 @@ plugins {
|
||||
alias(libs.plugins.room) apply false
|
||||
alias(libs.plugins.ksp) apply false
|
||||
alias(libs.plugins.module.graph) apply true
|
||||
alias(libs.plugins.versions) apply true
|
||||
alias(libs.plugins.stability.analyzer) apply false
|
||||
}
|
||||
|
||||
@@ -138,3 +138,15 @@ fun <T : Any> Bundle.getParcelableCompat(key: String, clazz: KClass<T>): T? {
|
||||
getParcelable(key)
|
||||
}
|
||||
}
|
||||
|
||||
infix fun ClosedRange<Int>.collidesWith(other: ClosedRange<Int>): Boolean {
|
||||
return this.start < other.endInclusive && other.start < this.endInclusive
|
||||
}
|
||||
|
||||
operator fun ClosedRange<Int>.minus(other: ClosedRange<Int>): ClosedRange<Int> {
|
||||
return (this.start - other.start)..(this.endInclusive - other.endInclusive)
|
||||
}
|
||||
|
||||
operator fun ClosedRange<Int>.minus(other: Int): ClosedRange<Int> {
|
||||
return (this.start - other)..(this.endInclusive - other)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package dev.meloda.fast.common.util
|
||||
|
||||
import com.conena.nanokt.jvm.util.dayOfMonth
|
||||
import com.conena.nanokt.jvm.util.hour
|
||||
import com.conena.nanokt.jvm.util.hourOfDay
|
||||
import com.conena.nanokt.jvm.util.millisecond
|
||||
import com.conena.nanokt.jvm.util.minute
|
||||
@@ -12,6 +11,12 @@ import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.Instant
|
||||
|
||||
object TimeUtils {
|
||||
|
||||
@@ -56,37 +61,23 @@ object TimeUtils {
|
||||
monthShort: () -> String,
|
||||
weekShort: () -> String,
|
||||
dayShort: () -> String,
|
||||
minuteShort: () -> String,
|
||||
secondShort: () -> String,
|
||||
now: () -> String
|
||||
): String {
|
||||
val now = Calendar.getInstance()
|
||||
val then = Calendar.getInstance().also { it.timeInMillis = date }
|
||||
val now = Clock.System.now()
|
||||
val then = Instant.fromEpochMilliseconds(date)
|
||||
val diff = now - then
|
||||
|
||||
return when {
|
||||
now.year != then.year -> {
|
||||
"${now.year - then.year}${yearShort().lowercase()}"
|
||||
}
|
||||
|
||||
now.month != then.month -> {
|
||||
"${now.month - then.month}${monthShort().lowercase()}"
|
||||
}
|
||||
|
||||
now.dayOfMonth != then.dayOfMonth -> {
|
||||
val change = now.dayOfMonth - then.dayOfMonth
|
||||
|
||||
if (change % 7 == 0) {
|
||||
"${change / 7}${weekShort().lowercase()}"
|
||||
} else {
|
||||
"$change${dayShort().lowercase()}"
|
||||
}
|
||||
}
|
||||
|
||||
now.hour == then.hour && now.minute == then.minute -> {
|
||||
now().lowercase()
|
||||
}
|
||||
|
||||
else -> {
|
||||
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
|
||||
}
|
||||
diff > 365.days -> "${diff.inWholeDays / 365}${yearShort().lowercase()}"
|
||||
diff > 30.days -> "${diff.inWholeDays / 30}${monthShort().lowercase()}"
|
||||
diff > 7.days -> "${diff.inWholeDays / 7}${weekShort().lowercase()}"
|
||||
diff > 1.days -> "${diff.inWholeDays}${dayShort().lowercase()}"
|
||||
diff > 1.hours -> "${diff.inWholeHours}h"
|
||||
diff > 1.minutes -> "${diff.inWholeMinutes}${minuteShort().lowercase()}"
|
||||
diff > 1.seconds -> "${diff.inWholeSeconds}${secondShort().lowercase()}"
|
||||
else -> now().lowercase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
package dev.meloda.fast.common.util
|
||||
|
||||
import java.net.URLEncoder
|
||||
import java.security.MessageDigest
|
||||
|
||||
fun String.urlEncode(encoding: String = "utf-8"): String {
|
||||
return URLEncoder.encode(this, encoding)
|
||||
}
|
||||
|
||||
fun String.sha256() = this.hashString("SHA-256")
|
||||
|
||||
fun String.hashString(algorithm: String): String {
|
||||
return MessageDigest
|
||||
.getInstance(algorithm)
|
||||
.digest(this.toByteArray())
|
||||
.fold("") { str, it -> str + "%02x".format(it) }
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ dependencies {
|
||||
api(projects.core.network)
|
||||
api(projects.core.database)
|
||||
|
||||
// TODO: 11/08/2024, Danil Nikolaev: remove?
|
||||
implementation(libs.retrofit)
|
||||
implementation(libs.eithernet)
|
||||
implementation(libs.koin.android)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package dev.meloda.fast.data
|
||||
|
||||
import dev.meloda.fast.model.api.data.VkMessageData
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkGroupDomain
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import kotlin.math.abs
|
||||
@@ -16,9 +16,9 @@ class VkGroupsMap(
|
||||
|
||||
fun groups(): List<VkGroupDomain> = map.values.toList()
|
||||
|
||||
fun conversationGroup(conversation: VkConversation): VkGroupDomain? =
|
||||
if (!conversation.peerType.isGroup()) null
|
||||
else map[abs(conversation.id)]
|
||||
fun convoGroup(convo: VkConvo): VkGroupDomain? =
|
||||
if (!convo.peerType.isGroup()) null
|
||||
else map[abs(convo.id)]
|
||||
|
||||
fun messageActionGroup(message: VkMessage): VkGroupDomain? =
|
||||
if (message.actionMemberId == null || message.actionMemberId!! >= 0) null
|
||||
|
||||
@@ -2,7 +2,7 @@ package dev.meloda.fast.data
|
||||
|
||||
import dev.meloda.fast.data.UserConfig.userId
|
||||
import dev.meloda.fast.model.api.domain.VkContactDomain
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkGroupDomain
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
@@ -13,7 +13,7 @@ object VkMemoryCache {
|
||||
private val users: HashMap<Long, VkUser> = hashMapOf()
|
||||
private val groups: HashMap<Long, VkGroupDomain> = hashMapOf()
|
||||
private val messages: HashMap<Long, VkMessage> = hashMapOf()
|
||||
private val conversations: HashMap<Long, VkConversation> = hashMapOf()
|
||||
private val convos: HashMap<Long, VkConvo> = hashMapOf()
|
||||
private val contacts: HashMap<Long, VkContactDomain> = hashMapOf()
|
||||
|
||||
fun appendUsers(users: List<VkUser>) {
|
||||
@@ -28,9 +28,9 @@ object VkMemoryCache {
|
||||
messages.forEach { message -> VkMemoryCache.messages[message.id] = message }
|
||||
}
|
||||
|
||||
fun appendConversations(conversations: List<VkConversation>) {
|
||||
conversations.forEach { conversation ->
|
||||
VkMemoryCache.conversations[conversation.id] = conversation
|
||||
fun appendConvos(convos: List<VkConvo>) {
|
||||
convos.forEach { convo ->
|
||||
VkMemoryCache.convos[convo.id] = convo
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,8 @@ object VkMemoryCache {
|
||||
messages[messageId] = message
|
||||
}
|
||||
|
||||
operator fun set(conversationId: Long, conversation: VkConversation) {
|
||||
conversations[conversationId] = conversation
|
||||
operator fun set(convoId: Long, convo: VkConvo) {
|
||||
convos[convoId] = convo
|
||||
}
|
||||
|
||||
operator fun set(contactId: Long, contact: VkContactDomain) {
|
||||
@@ -94,16 +94,16 @@ object VkMemoryCache {
|
||||
return ids.mapNotNull { id -> messages[id] }
|
||||
}
|
||||
|
||||
fun getConversation(id: Long): VkConversation? {
|
||||
return getConversations(id).firstOrNull()
|
||||
fun getConvo(id: Long): VkConvo? {
|
||||
return getConvos(id).firstOrNull()
|
||||
}
|
||||
|
||||
fun getConversations(vararg ids: Long): List<VkConversation> {
|
||||
return getConversations(ids.toList())
|
||||
fun getConvos(vararg ids: Long): List<VkConvo> {
|
||||
return getConvos(ids.toList())
|
||||
}
|
||||
|
||||
fun getConversations(ids: List<Long>): List<VkConversation> {
|
||||
return ids.mapNotNull { id -> conversations[id] }
|
||||
fun getConvos(ids: List<Long>): List<VkConvo> {
|
||||
return ids.mapNotNull { id -> convos[id] }
|
||||
}
|
||||
|
||||
fun getContact(id: Long): VkContactDomain? {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package dev.meloda.fast.data
|
||||
|
||||
import dev.meloda.fast.data.UserConfig.userId
|
||||
import dev.meloda.fast.model.api.data.VkMessageData
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
|
||||
@@ -16,9 +15,9 @@ class VkUsersMap(
|
||||
|
||||
fun users(): List<VkUser> = map.values.toList()
|
||||
|
||||
fun conversationUser(conversation: VkConversation): VkUser? =
|
||||
if (!conversation.peerType.isUser()) null
|
||||
else map[conversation.id]
|
||||
fun convoUser(convo: VkConvo): VkUser? =
|
||||
if (!convo.peerType.isUser()) null
|
||||
else map[convo.id]
|
||||
|
||||
fun messageActionUser(message: VkMessage): VkUser? =
|
||||
if (message.actionMemberId == null || message.actionMemberId!! <= 0) null
|
||||
|
||||
+10
-10
@@ -1,25 +1,25 @@
|
||||
package dev.meloda.fast.data.api.conversations
|
||||
package dev.meloda.fast.data.api.convos
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.meloda.fast.model.ConversationsFilter
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.ConvosFilter
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.network.RestApiErrorDomain
|
||||
|
||||
interface ConversationsRepository {
|
||||
interface ConvosRepository {
|
||||
|
||||
suspend fun storeConversations(conversations: List<VkConversation>)
|
||||
suspend fun storeConvos(convos: List<VkConvo>)
|
||||
|
||||
suspend fun getConversations(
|
||||
suspend fun getConvos(
|
||||
count: Int?,
|
||||
offset: Int?,
|
||||
filter: ConversationsFilter
|
||||
): ApiResult<List<VkConversation>, RestApiErrorDomain>
|
||||
filter: ConvosFilter
|
||||
): ApiResult<List<VkConvo>, RestApiErrorDomain>
|
||||
|
||||
suspend fun getConversationsById(
|
||||
suspend fun getConvosById(
|
||||
peerIds: List<Long>,
|
||||
extended: Boolean? = null,
|
||||
fields: String? = null
|
||||
): ApiResult<List<VkConversation>, RestApiErrorDomain>
|
||||
): ApiResult<List<VkConvo>, RestApiErrorDomain>
|
||||
|
||||
suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain>
|
||||
suspend fun pin(peerId: Long): ApiResult<Int, RestApiErrorDomain>
|
||||
+43
-43
@@ -1,51 +1,51 @@
|
||||
package dev.meloda.fast.data.api.conversations
|
||||
package dev.meloda.fast.data.api.convos
|
||||
|
||||
import com.slack.eithernet.ApiResult
|
||||
import dev.meloda.fast.common.VkConstants
|
||||
import dev.meloda.fast.data.VkGroupsMap
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
import dev.meloda.fast.data.VkUsersMap
|
||||
import dev.meloda.fast.database.dao.ConversationDao
|
||||
import dev.meloda.fast.database.dao.ConvoDao
|
||||
import dev.meloda.fast.database.dao.GroupDao
|
||||
import dev.meloda.fast.database.dao.MessageDao
|
||||
import dev.meloda.fast.database.dao.UserDao
|
||||
import dev.meloda.fast.model.ConversationsFilter
|
||||
import dev.meloda.fast.model.ConvosFilter
|
||||
import dev.meloda.fast.model.api.data.VkContactData
|
||||
import dev.meloda.fast.model.api.data.VkGroupData
|
||||
import dev.meloda.fast.model.api.data.VkUserData
|
||||
import dev.meloda.fast.model.api.data.asDomain
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkGroupDomain
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
import dev.meloda.fast.model.api.domain.asEntity
|
||||
import dev.meloda.fast.model.api.requests.ConversationsGetRequest
|
||||
import dev.meloda.fast.model.api.requests.ConvosGetRequest
|
||||
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 dev.meloda.fast.network.service.convos.ConvosService
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ConversationsRepositoryImpl(
|
||||
private val conversationsService: ConversationsService,
|
||||
class ConvosRepositoryImpl(
|
||||
private val convosService: ConvosService,
|
||||
private val messageDao: MessageDao,
|
||||
private val userDao: UserDao,
|
||||
private val groupDao: GroupDao,
|
||||
private val conversationDao: ConversationDao
|
||||
) : ConversationsRepository {
|
||||
private val convoDao: ConvoDao
|
||||
) : ConvosRepository {
|
||||
|
||||
override suspend fun storeConversations(conversations: List<VkConversation>) {
|
||||
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
|
||||
override suspend fun storeConvos(convos: List<VkConvo>) {
|
||||
convoDao.insertAll(convos.map(VkConvo::asEntity))
|
||||
}
|
||||
|
||||
override suspend fun getConversations(
|
||||
override suspend fun getConvos(
|
||||
count: Int?,
|
||||
offset: Int?,
|
||||
filter: ConversationsFilter
|
||||
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = ConversationsGetRequest(
|
||||
filter: ConvosFilter
|
||||
): ApiResult<List<VkConvo>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = ConvosGetRequest(
|
||||
count = count,
|
||||
offset = offset,
|
||||
fields = VkConstants.ALL_FIELDS,
|
||||
@@ -54,7 +54,7 @@ class ConversationsRepositoryImpl(
|
||||
startMessageId = null
|
||||
)
|
||||
|
||||
conversationsService.getConversations(requestModel.map).mapApiResult(
|
||||
convosService.getConvos(requestModel.map).mapApiResult(
|
||||
successMapper = { apiResponse ->
|
||||
val response = apiResponse.requireResponse()
|
||||
|
||||
@@ -69,7 +69,7 @@ class ConversationsRepositoryImpl(
|
||||
VkMemoryCache.appendGroups(groupsList)
|
||||
VkMemoryCache.appendContacts(contactsList)
|
||||
|
||||
val conversations = response.items.map { item ->
|
||||
val convos = response.items.map { item ->
|
||||
val lastMessage = item.lastMessage?.asDomain()?.let { message ->
|
||||
message.copy(
|
||||
user = usersMap.messageUser(message),
|
||||
@@ -84,24 +84,24 @@ class ConversationsRepositoryImpl(
|
||||
)
|
||||
).also { VkMemoryCache[message.id] = it }
|
||||
}
|
||||
item.conversation.asDomain(lastMessage).let { conversation ->
|
||||
conversation.copy(
|
||||
user = usersMap.conversationUser(conversation),
|
||||
group = groupsMap.conversationGroup(conversation)
|
||||
).also { VkMemoryCache[conversation.id] = it }
|
||||
item.convo.asDomain(lastMessage).let { convo ->
|
||||
convo.copy(
|
||||
user = usersMap.convoUser(convo),
|
||||
group = groupsMap.convoGroup(convo)
|
||||
).also { VkMemoryCache[convo.id] = it }
|
||||
}
|
||||
}
|
||||
|
||||
val messages = conversations.mapNotNull(VkConversation::lastMessage)
|
||||
val messages = convos.mapNotNull(VkConvo::lastMessage)
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
|
||||
convoDao.insertAll(convos.map(VkConvo::asEntity))
|
||||
messageDao.insertAll(messages.map(VkMessage::asEntity))
|
||||
userDao.insertAll(profilesList.map(VkUser::asEntity))
|
||||
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
|
||||
}
|
||||
|
||||
conversations
|
||||
convos
|
||||
},
|
||||
errorMapper = { error ->
|
||||
error?.toDomain()
|
||||
@@ -109,11 +109,11 @@ class ConversationsRepositoryImpl(
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getConversationsById(
|
||||
override suspend fun getConvosById(
|
||||
peerIds: List<Long>,
|
||||
extended: Boolean?,
|
||||
fields: String?
|
||||
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
): ApiResult<List<VkConvo>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestParams = mutableMapOf(
|
||||
"peer_ids" to peerIds.joinToString(separator = ",")
|
||||
).apply {
|
||||
@@ -121,7 +121,7 @@ class ConversationsRepositoryImpl(
|
||||
fields?.let { this["fields"] = it }
|
||||
}
|
||||
|
||||
conversationsService.getConversationsById(requestParams).mapApiResult(
|
||||
convosService.getConvosById(requestParams).mapApiResult(
|
||||
successMapper = { apiResponse ->
|
||||
val response = apiResponse.requireResponse()
|
||||
|
||||
@@ -132,17 +132,17 @@ class ConversationsRepositoryImpl(
|
||||
val usersMap = VkUsersMap.forUsers(profilesList)
|
||||
val groupsMap = VkGroupsMap.forGroups(groupsList)
|
||||
|
||||
val conversations = response.items.map { item ->
|
||||
item.asDomain().let { conversation ->
|
||||
conversation.copy(
|
||||
user = usersMap.conversationUser(conversation),
|
||||
group = groupsMap.conversationGroup(conversation)
|
||||
).also { VkMemoryCache[conversation.id] = it }
|
||||
val convos = response.items.map { item ->
|
||||
item.asDomain().let { convo ->
|
||||
convo.copy(
|
||||
user = usersMap.convoUser(convo),
|
||||
group = groupsMap.convoGroup(convo)
|
||||
).also { VkMemoryCache[convo.id] = it }
|
||||
}
|
||||
}
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
|
||||
convoDao.insertAll(convos.map(VkConvo::asEntity))
|
||||
userDao.insertAll(profilesList.map(VkUser::asEntity))
|
||||
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
|
||||
}
|
||||
@@ -151,7 +151,7 @@ class ConversationsRepositoryImpl(
|
||||
VkMemoryCache.appendGroups(groupsList)
|
||||
VkMemoryCache.appendContacts(contactsList)
|
||||
|
||||
conversations
|
||||
convos
|
||||
},
|
||||
errorMapper = { error ->
|
||||
error?.toDomain()
|
||||
@@ -161,7 +161,7 @@ class ConversationsRepositoryImpl(
|
||||
|
||||
override suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain> =
|
||||
withContext(Dispatchers.IO) {
|
||||
conversationsService.delete(mapOf("peer_id" to peerId.toString())).mapApiResult(
|
||||
convosService.delete(mapOf("peer_id" to peerId.toString())).mapApiResult(
|
||||
successMapper = { response -> response.requireResponse().lastDeletedId },
|
||||
errorMapper = { error -> error?.toDomain() }
|
||||
)
|
||||
@@ -170,19 +170,19 @@ class ConversationsRepositoryImpl(
|
||||
override suspend fun pin(
|
||||
peerId: Long
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
conversationsService.pin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
|
||||
convosService.pin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun unpin(
|
||||
peerId: Long
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
conversationsService.unpin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
|
||||
convosService.unpin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun reorderPinned(
|
||||
peerIds: List<Long>
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
conversationsService
|
||||
convosService
|
||||
.reorderPinned(mapOf("peer_ids" to peerIds.joinToString(",")))
|
||||
.mapApiDefault()
|
||||
}
|
||||
@@ -190,12 +190,12 @@ class ConversationsRepositoryImpl(
|
||||
override suspend fun archive(
|
||||
peerId: Long
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
conversationsService.archive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
|
||||
convosService.archive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun unarchive(
|
||||
peerId: Long
|
||||
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
conversationsService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
|
||||
convosService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
package dev.meloda.fast.data.api.messages
|
||||
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
|
||||
data class MessagesHistoryInfo(
|
||||
val messages: List<VkMessage>,
|
||||
val conversations: List<VkConversation>
|
||||
val convos: List<VkConvo>
|
||||
)
|
||||
|
||||
@@ -5,7 +5,8 @@ import dev.meloda.fast.model.api.data.VkChatData
|
||||
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.responses.MessagesGetConversationMembersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetConvoMembersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
||||
import dev.meloda.fast.network.RestApiErrorDomain
|
||||
|
||||
@@ -14,7 +15,7 @@ interface MessagesRepository {
|
||||
suspend fun storeMessages(messages: List<VkMessage>)
|
||||
|
||||
suspend fun getHistory(
|
||||
conversationId: Long,
|
||||
convoId: Long,
|
||||
offset: Int?,
|
||||
count: Int?
|
||||
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain>
|
||||
@@ -98,16 +99,21 @@ interface MessagesRepository {
|
||||
fields: String? = null
|
||||
): ApiResult<VkChatData, RestApiErrorDomain>
|
||||
|
||||
suspend fun getConversationMembers(
|
||||
suspend fun getConvoMembers(
|
||||
peerId: Long,
|
||||
offset: Int? = null,
|
||||
count: Int? = null,
|
||||
extended: Boolean? = null,
|
||||
fields: String? = null
|
||||
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain>
|
||||
): ApiResult<MessagesGetConvoMembersResponse, RestApiErrorDomain>
|
||||
|
||||
suspend fun removeChatUser(
|
||||
chatId: Long,
|
||||
memberId: Long
|
||||
): ApiResult<Int, RestApiErrorDomain>
|
||||
|
||||
suspend fun getMessageReadPeers(
|
||||
peerId: Long,
|
||||
cmId: Long
|
||||
): ApiResult<MessagesGetReadPeersResponse, RestApiErrorDomain>
|
||||
}
|
||||
|
||||
+51
-32
@@ -5,7 +5,7 @@ import dev.meloda.fast.common.VkConstants
|
||||
import dev.meloda.fast.data.VkGroupsMap
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
import dev.meloda.fast.data.VkUsersMap
|
||||
import dev.meloda.fast.database.dao.ConversationDao
|
||||
import dev.meloda.fast.database.dao.ConvoDao
|
||||
import dev.meloda.fast.database.dao.GroupDao
|
||||
import dev.meloda.fast.database.dao.MessageDao
|
||||
import dev.meloda.fast.database.dao.UserDao
|
||||
@@ -17,7 +17,7 @@ import dev.meloda.fast.model.api.data.VkUserData
|
||||
import dev.meloda.fast.model.api.data.asDomain
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkGroupDomain
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
@@ -27,7 +27,7 @@ import dev.meloda.fast.model.api.requests.MessagesDeleteRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesEditRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesGetByIdRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesGetChatRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesGetConversationMembersRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesGetConvoMembersRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesGetHistoryAttachmentsRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesGetHistoryRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesMarkAsImportantRequest
|
||||
@@ -36,7 +36,8 @@ import dev.meloda.fast.model.api.requests.MessagesPinMessageRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesRemoveChatUserRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesSendRequest
|
||||
import dev.meloda.fast.model.api.requests.MessagesUnpinMessageRequest
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetConvoMembersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
||||
import dev.meloda.fast.network.RestApiErrorDomain
|
||||
import dev.meloda.fast.network.mapApiDefault
|
||||
@@ -51,18 +52,18 @@ class MessagesRepositoryImpl(
|
||||
private val messageDao: MessageDao,
|
||||
private val userDao: UserDao,
|
||||
private val groupDao: GroupDao,
|
||||
private val conversationDao: ConversationDao
|
||||
private val convoDao: ConvoDao
|
||||
) : MessagesRepository {
|
||||
|
||||
override suspend fun getHistory(
|
||||
conversationId: Long,
|
||||
convoId: Long,
|
||||
offset: Int?,
|
||||
count: Int?
|
||||
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
val requestModel = MessagesGetHistoryRequest(
|
||||
count = count,
|
||||
offset = offset,
|
||||
peerId = conversationId,
|
||||
peerId = convoId,
|
||||
extended = true,
|
||||
startMessageId = null,
|
||||
rev = null,
|
||||
@@ -91,29 +92,31 @@ class MessagesRepositoryImpl(
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message),
|
||||
replyMessage = message.replyMessage?.copy(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message),
|
||||
replyMessage = message.replyMessage.let { replyMessage ->
|
||||
replyMessage?.copy(
|
||||
user = usersMap.messageUser(replyMessage),
|
||||
group = groupsMap.messageGroup(replyMessage),
|
||||
actionUser = usersMap.messageActionUser(replyMessage),
|
||||
actionGroup = groupsMap.messageActionGroup(replyMessage),
|
||||
)
|
||||
}
|
||||
).also { VkMemoryCache[message.id] = it }
|
||||
}
|
||||
}
|
||||
|
||||
val conversations = response.conversations.orEmpty().map { item ->
|
||||
val convos = response.convos.orEmpty().map { item ->
|
||||
val message = messages.firstOrNull { it.id == item.lastMessageId }
|
||||
item.asDomain(message)
|
||||
.let { conversation ->
|
||||
conversation.copy(
|
||||
user = usersMap.conversationUser(conversation),
|
||||
group = groupsMap.conversationGroup(conversation)
|
||||
).also { VkMemoryCache[conversation.id] = it }
|
||||
.let { convo ->
|
||||
convo.copy(
|
||||
user = usersMap.convoUser(convo),
|
||||
group = groupsMap.convoGroup(convo)
|
||||
).also { VkMemoryCache[convo.id] = it }
|
||||
}
|
||||
}
|
||||
|
||||
launch(Dispatchers.IO) {
|
||||
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
|
||||
convoDao.insertAll(convos.map(VkConvo::asEntity))
|
||||
messageDao.insertAll(messages.map(VkMessage::asEntity))
|
||||
userDao.insertAll(profilesList.map(VkUser::asEntity))
|
||||
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
|
||||
@@ -121,7 +124,7 @@ class MessagesRepositoryImpl(
|
||||
|
||||
MessagesHistoryInfo(
|
||||
messages = messages,
|
||||
conversations = conversations
|
||||
convos = convos
|
||||
)
|
||||
},
|
||||
errorMapper = { error ->
|
||||
@@ -166,12 +169,14 @@ class MessagesRepositoryImpl(
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message),
|
||||
replyMessage = message.replyMessage?.asDomain()?.copy(
|
||||
user = usersMap.messageUser(message),
|
||||
group = groupsMap.messageGroup(message),
|
||||
actionUser = usersMap.messageActionUser(message),
|
||||
actionGroup = groupsMap.messageActionGroup(message),
|
||||
replyMessage = message.replyMessage?.asDomain().let { replyMessage ->
|
||||
replyMessage?.copy(
|
||||
user = usersMap.messageUser(replyMessage),
|
||||
group = groupsMap.messageGroup(replyMessage),
|
||||
actionUser = usersMap.messageActionUser(replyMessage),
|
||||
actionGroup = groupsMap.messageActionGroup(replyMessage),
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -238,7 +243,7 @@ class MessagesRepositoryImpl(
|
||||
offset = offset,
|
||||
preserveOrder = true,
|
||||
attachmentTypes = attachmentTypes,
|
||||
conversationMessageId = cmId,
|
||||
cmId = cmId,
|
||||
fields = VkConstants.ALL_FIELDS
|
||||
)
|
||||
|
||||
@@ -292,7 +297,7 @@ class MessagesRepositoryImpl(
|
||||
val requestModel = MessagesPinMessageRequest(
|
||||
peerId = peerId,
|
||||
messageId = messageId,
|
||||
conversationMessageId = cmId
|
||||
cmId = cmId
|
||||
)
|
||||
|
||||
messagesService.pin(requestModel.map).mapApiResult(
|
||||
@@ -338,7 +343,7 @@ class MessagesRepositoryImpl(
|
||||
val requestModel = MessagesDeleteRequest(
|
||||
peerId = peerId,
|
||||
messagesIds = messageIds,
|
||||
conversationsMessagesIds = cmIds,
|
||||
cmIds = cmIds,
|
||||
isSpam = spam,
|
||||
deleteForAll = deleteForAll
|
||||
)
|
||||
@@ -389,15 +394,15 @@ class MessagesRepositoryImpl(
|
||||
messagesService.getChat(requestModel.map).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun getConversationMembers(
|
||||
override suspend fun getConvoMembers(
|
||||
peerId: Long,
|
||||
offset: Int?,
|
||||
count: Int?,
|
||||
extended: Boolean?,
|
||||
fields: String?
|
||||
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> =
|
||||
): ApiResult<MessagesGetConvoMembersResponse, RestApiErrorDomain> =
|
||||
withContext(Dispatchers.IO) {
|
||||
val requestModel = MessagesGetConversationMembersRequest(
|
||||
val requestModel = MessagesGetConvoMembersRequest(
|
||||
peerId = peerId,
|
||||
offset = offset,
|
||||
count = count,
|
||||
@@ -405,7 +410,7 @@ class MessagesRepositoryImpl(
|
||||
fields = fields
|
||||
)
|
||||
|
||||
messagesService.getConversationMembers(requestModel.map).mapApiDefault()
|
||||
messagesService.getConvoMembers(requestModel.map).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun removeChatUser(
|
||||
@@ -419,4 +424,18 @@ class MessagesRepositoryImpl(
|
||||
|
||||
messagesService.removeChatUser(requestModel.map).mapApiDefault()
|
||||
}
|
||||
|
||||
override suspend fun getMessageReadPeers(
|
||||
peerId: Long,
|
||||
cmId: Long
|
||||
): ApiResult<MessagesGetReadPeersResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
|
||||
messagesService.getMessageReadPeers(
|
||||
mapOf(
|
||||
"peer_id" to peerId.toString(),
|
||||
"cmid" to cmId.toString(),
|
||||
"extended" to "1",
|
||||
"fields" to VkConstants.USER_FIELDS
|
||||
)
|
||||
).mapApiDefault()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,8 @@ import dev.meloda.fast.data.api.account.AccountRepositoryImpl
|
||||
import dev.meloda.fast.data.api.audios.AudiosRepository
|
||||
import dev.meloda.fast.data.api.auth.AuthRepository
|
||||
import dev.meloda.fast.data.api.auth.AuthRepositoryImpl
|
||||
import dev.meloda.fast.data.api.conversations.ConversationsRepository
|
||||
import dev.meloda.fast.data.api.conversations.ConversationsRepositoryImpl
|
||||
import dev.meloda.fast.data.api.convos.ConvosRepository
|
||||
import dev.meloda.fast.data.api.convos.ConvosRepositoryImpl
|
||||
import dev.meloda.fast.data.api.files.FilesRepository
|
||||
import dev.meloda.fast.data.api.friends.FriendsRepository
|
||||
import dev.meloda.fast.data.api.friends.FriendsRepositoryImpl
|
||||
@@ -45,7 +45,7 @@ val dataModule = module {
|
||||
|
||||
singleOf(::AuthRepositoryImpl) bind AuthRepository::class
|
||||
|
||||
singleOf(::ConversationsRepositoryImpl) bind ConversationsRepository::class
|
||||
singleOf(::ConvosRepositoryImpl) bind ConvosRepository::class
|
||||
|
||||
singleOf(::FilesRepository)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 10,
|
||||
"identityHash": "fa307a5eb2e1f7d601bd1374174635cd",
|
||||
"identityHash": "6c315b7f800694f635318d86032746ec",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "users",
|
||||
@@ -41,50 +41,42 @@
|
||||
{
|
||||
"fieldPath": "onlineAppId",
|
||||
"columnName": "onlineAppId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSeen",
|
||||
"columnName": "lastSeen",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSeenStatus",
|
||||
"columnName": "lastSeenStatus",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "birthday",
|
||||
"columnName": "birthday",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo50",
|
||||
"columnName": "photo50",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo100",
|
||||
"columnName": "photo100",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo200",
|
||||
"columnName": "photo200",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo400Orig",
|
||||
"columnName": "photo400Orig",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -92,9 +84,7 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "groups",
|
||||
@@ -121,26 +111,22 @@
|
||||
{
|
||||
"fieldPath": "photo50",
|
||||
"columnName": "photo50",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo100",
|
||||
"columnName": "photo100",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo200",
|
||||
"columnName": "photo200",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "membersCount",
|
||||
"columnName": "membersCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
@@ -148,13 +134,11 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "messages",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `conversationMessageId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionConversationMessageId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `cmId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionCmId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
@@ -163,16 +147,15 @@
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "conversationMessageId",
|
||||
"columnName": "conversationMessageId",
|
||||
"fieldPath": "cmId",
|
||||
"columnName": "cmId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isOut",
|
||||
@@ -207,38 +190,32 @@
|
||||
{
|
||||
"fieldPath": "action",
|
||||
"columnName": "action",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "actionMemberId",
|
||||
"columnName": "actionMemberId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "actionText",
|
||||
"columnName": "actionText",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "actionConversationMessageId",
|
||||
"columnName": "actionConversationMessageId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"fieldPath": "actionCmId",
|
||||
"columnName": "actionCmId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "actionMessage",
|
||||
"columnName": "actionMessage",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "updateTime",
|
||||
"columnName": "updateTime",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "important",
|
||||
@@ -249,32 +226,27 @@
|
||||
{
|
||||
"fieldPath": "forwardIds",
|
||||
"columnName": "forwardIds",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachments",
|
||||
"columnName": "attachments",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "replyMessageId",
|
||||
"columnName": "replyMessageId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "geoType",
|
||||
"columnName": "geoType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinnedAt",
|
||||
"columnName": "pinnedAt",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPinned",
|
||||
@@ -288,13 +260,11 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "conversations",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastConversationMessageId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastCmId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
@@ -311,32 +281,27 @@
|
||||
{
|
||||
"fieldPath": "ownerId",
|
||||
"columnName": "ownerId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo50",
|
||||
"columnName": "photo50",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo100",
|
||||
"columnName": "photo100",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo200",
|
||||
"columnName": "photo200",
|
||||
"affinity": "TEXT",
|
||||
"notNull": false
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPhantom",
|
||||
@@ -345,8 +310,8 @@
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastConversationMessageId",
|
||||
"columnName": "lastConversationMessageId",
|
||||
"fieldPath": "lastCmId",
|
||||
"columnName": "lastCmId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
@@ -377,8 +342,7 @@
|
||||
{
|
||||
"fieldPath": "lastMessageId",
|
||||
"columnName": "lastMessageId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "unreadCount",
|
||||
@@ -389,8 +353,7 @@
|
||||
{
|
||||
"fieldPath": "membersCount",
|
||||
"columnName": "membersCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "canChangePin",
|
||||
@@ -419,8 +382,7 @@
|
||||
{
|
||||
"fieldPath": "pinnedMessageId",
|
||||
"columnName": "pinnedMessageId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": false
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "peerType",
|
||||
@@ -440,15 +402,12 @@
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
},
|
||||
"indices": [],
|
||||
"foreignKeys": []
|
||||
}
|
||||
}
|
||||
],
|
||||
"views": [],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'fa307a5eb2e1f7d601bd1374174635cd')"
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '6c315b7f800694f635318d86032746ec')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,413 @@
|
||||
{
|
||||
"formatVersion": 1,
|
||||
"database": {
|
||||
"version": 11,
|
||||
"identityHash": "a746865995959331f8a1b512c049dacb",
|
||||
"entities": [
|
||||
{
|
||||
"tableName": "users",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `firstName` TEXT NOT NULL, `lastName` TEXT NOT NULL, `isOnline` INTEGER NOT NULL, `isOnlineMobile` INTEGER NOT NULL, `onlineAppId` INTEGER, `lastSeen` INTEGER, `lastSeenStatus` TEXT, `birthday` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `photo400Orig` TEXT, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "firstName",
|
||||
"columnName": "firstName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastName",
|
||||
"columnName": "lastName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isOnline",
|
||||
"columnName": "isOnline",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isOnlineMobile",
|
||||
"columnName": "isOnlineMobile",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "onlineAppId",
|
||||
"columnName": "onlineAppId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSeen",
|
||||
"columnName": "lastSeen",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastSeenStatus",
|
||||
"columnName": "lastSeenStatus",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "birthday",
|
||||
"columnName": "birthday",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo50",
|
||||
"columnName": "photo50",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo100",
|
||||
"columnName": "photo100",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo200",
|
||||
"columnName": "photo200",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo400Orig",
|
||||
"columnName": "photo400Orig",
|
||||
"affinity": "TEXT"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "groups",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `screenName` TEXT NOT NULL, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `membersCount` INTEGER, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "name",
|
||||
"columnName": "name",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "screenName",
|
||||
"columnName": "screenName",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo50",
|
||||
"columnName": "photo50",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo100",
|
||||
"columnName": "photo100",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo200",
|
||||
"columnName": "photo200",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "membersCount",
|
||||
"columnName": "membersCount",
|
||||
"affinity": "INTEGER"
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "messages",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `cmId` INTEGER NOT NULL, `text` TEXT, `isOut` INTEGER NOT NULL, `peerId` INTEGER NOT NULL, `fromId` INTEGER NOT NULL, `date` INTEGER NOT NULL, `randomId` INTEGER NOT NULL, `action` TEXT, `actionMemberId` INTEGER, `actionText` TEXT, `actionCmId` INTEGER, `actionMessage` TEXT, `updateTime` INTEGER, `important` INTEGER NOT NULL, `forwardIds` TEXT, `attachments` TEXT, `replyMessageId` INTEGER, `geoType` TEXT, `pinnedAt` INTEGER, `isPinned` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "cmId",
|
||||
"columnName": "cmId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "text",
|
||||
"columnName": "text",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isOut",
|
||||
"columnName": "isOut",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "peerId",
|
||||
"columnName": "peerId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "fromId",
|
||||
"columnName": "fromId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "date",
|
||||
"columnName": "date",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "randomId",
|
||||
"columnName": "randomId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "action",
|
||||
"columnName": "action",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "actionMemberId",
|
||||
"columnName": "actionMemberId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "actionText",
|
||||
"columnName": "actionText",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "actionCmId",
|
||||
"columnName": "actionCmId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "actionMessage",
|
||||
"columnName": "actionMessage",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "updateTime",
|
||||
"columnName": "updateTime",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "important",
|
||||
"columnName": "important",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "forwardIds",
|
||||
"columnName": "forwardIds",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "attachments",
|
||||
"columnName": "attachments",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "replyMessageId",
|
||||
"columnName": "replyMessageId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "geoType",
|
||||
"columnName": "geoType",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinnedAt",
|
||||
"columnName": "pinnedAt",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPinned",
|
||||
"columnName": "isPinned",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"tableName": "convos",
|
||||
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `localId` INTEGER NOT NULL, `ownerId` INTEGER, `title` TEXT, `photo50` TEXT, `photo100` TEXT, `photo200` TEXT, `isPhantom` INTEGER NOT NULL, `lastCmId` INTEGER NOT NULL, `inReadCmId` INTEGER NOT NULL, `outReadCmId` INTEGER NOT NULL, `inRead` INTEGER NOT NULL, `outRead` INTEGER NOT NULL, `lastMessageId` INTEGER, `unreadCount` INTEGER NOT NULL, `membersCount` INTEGER, `canChangePin` INTEGER NOT NULL, `canChangeInfo` INTEGER NOT NULL, `majorId` INTEGER NOT NULL, `minorId` INTEGER NOT NULL, `pinnedMessageId` INTEGER, `peerType` TEXT NOT NULL, `isArchived` INTEGER NOT NULL, PRIMARY KEY(`id`))",
|
||||
"fields": [
|
||||
{
|
||||
"fieldPath": "id",
|
||||
"columnName": "id",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "localId",
|
||||
"columnName": "localId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "ownerId",
|
||||
"columnName": "ownerId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "title",
|
||||
"columnName": "title",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo50",
|
||||
"columnName": "photo50",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo100",
|
||||
"columnName": "photo100",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "photo200",
|
||||
"columnName": "photo200",
|
||||
"affinity": "TEXT"
|
||||
},
|
||||
{
|
||||
"fieldPath": "isPhantom",
|
||||
"columnName": "isPhantom",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastCmId",
|
||||
"columnName": "lastCmId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "inReadCmId",
|
||||
"columnName": "inReadCmId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outReadCmId",
|
||||
"columnName": "outReadCmId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "inRead",
|
||||
"columnName": "inRead",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "outRead",
|
||||
"columnName": "outRead",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "lastMessageId",
|
||||
"columnName": "lastMessageId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "unreadCount",
|
||||
"columnName": "unreadCount",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "membersCount",
|
||||
"columnName": "membersCount",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "canChangePin",
|
||||
"columnName": "canChangePin",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "canChangeInfo",
|
||||
"columnName": "canChangeInfo",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "majorId",
|
||||
"columnName": "majorId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "minorId",
|
||||
"columnName": "minorId",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "pinnedMessageId",
|
||||
"columnName": "pinnedMessageId",
|
||||
"affinity": "INTEGER"
|
||||
},
|
||||
{
|
||||
"fieldPath": "peerType",
|
||||
"columnName": "peerType",
|
||||
"affinity": "TEXT",
|
||||
"notNull": true
|
||||
},
|
||||
{
|
||||
"fieldPath": "isArchived",
|
||||
"columnName": "isArchived",
|
||||
"affinity": "INTEGER",
|
||||
"notNull": true
|
||||
}
|
||||
],
|
||||
"primaryKey": {
|
||||
"autoGenerate": false,
|
||||
"columnNames": [
|
||||
"id"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"setupQueries": [
|
||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'a746865995959331f8a1b512c049dacb')"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,12 @@ package dev.meloda.fast.database
|
||||
import androidx.room.Database
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.room.TypeConverters
|
||||
import dev.meloda.fast.database.dao.ConversationDao
|
||||
import dev.meloda.fast.database.dao.ConvoDao
|
||||
import dev.meloda.fast.database.dao.GroupDao
|
||||
import dev.meloda.fast.database.dao.MessageDao
|
||||
import dev.meloda.fast.database.dao.UserDao
|
||||
import dev.meloda.fast.database.typeconverters.Converters
|
||||
import dev.meloda.fast.model.database.VkConversationEntity
|
||||
import dev.meloda.fast.model.database.VkConvoEntity
|
||||
import dev.meloda.fast.model.database.VkGroupEntity
|
||||
import dev.meloda.fast.model.database.VkMessageEntity
|
||||
import dev.meloda.fast.model.database.VkUserEntity
|
||||
@@ -18,15 +18,15 @@ import dev.meloda.fast.model.database.VkUserEntity
|
||||
VkUserEntity::class,
|
||||
VkGroupEntity::class,
|
||||
VkMessageEntity::class,
|
||||
VkConversationEntity::class
|
||||
VkConvoEntity::class
|
||||
],
|
||||
|
||||
version = 10
|
||||
version = 11
|
||||
)
|
||||
@TypeConverters(Converters::class)
|
||||
abstract class CacheDatabase : RoomDatabase() {
|
||||
abstract fun userDao(): UserDao
|
||||
abstract fun groupDao(): GroupDao
|
||||
abstract fun messageDao(): MessageDao
|
||||
abstract fun conversationDao(): ConversationDao
|
||||
abstract fun convoDao(): ConvoDao
|
||||
}
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
package dev.meloda.fast.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import dev.meloda.fast.model.database.ConversationWithMessage
|
||||
import dev.meloda.fast.model.database.VkConversationEntity
|
||||
|
||||
@Dao
|
||||
abstract class ConversationDao : EntityDao<VkConversationEntity> {
|
||||
|
||||
@Query("SELECT * FROM conversations")
|
||||
abstract suspend fun getAll(): List<VkConversationEntity>
|
||||
|
||||
@Query("SELECT * FROM conversations WHERE id IN (:ids)")
|
||||
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConversationEntity>
|
||||
|
||||
@Query("SELECT * FROM conversations WHERE id IS (:id)")
|
||||
abstract suspend fun getById(id: Long): VkConversationEntity?
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM conversations WHERE id IS (:id)")
|
||||
abstract suspend fun getByIdWithMessage(id: Long): ConversationWithMessage?
|
||||
|
||||
@Query("DELETE FROM conversations WHERE rowid IN (:ids)")
|
||||
abstract suspend fun deleteByIds(ids: List<Int>): Int
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package dev.meloda.fast.database.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Query
|
||||
import androidx.room.Transaction
|
||||
import dev.meloda.fast.model.database.ConvoWithMessage
|
||||
import dev.meloda.fast.model.database.VkConvoEntity
|
||||
|
||||
@Dao
|
||||
abstract class ConvoDao : EntityDao<VkConvoEntity> {
|
||||
|
||||
@Query("SELECT * FROM convos")
|
||||
abstract suspend fun getAll(): List<VkConvoEntity>
|
||||
|
||||
@Query("SELECT * FROM convos WHERE id IN (:ids)")
|
||||
abstract suspend fun getAllByIds(ids: List<Int>): List<VkConvoEntity>
|
||||
|
||||
@Query("SELECT * FROM convos WHERE id IS (:id)")
|
||||
abstract suspend fun getById(id: Long): VkConvoEntity?
|
||||
|
||||
@Transaction
|
||||
@Query("SELECT * FROM convos WHERE id IS (:id)")
|
||||
abstract suspend fun getByIdWithMessage(id: Long): ConvoWithMessage?
|
||||
|
||||
@Query("DELETE FROM convos WHERE rowid IN (:ids)")
|
||||
abstract suspend fun deleteByIds(ids: List<Int>): Int
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -10,8 +10,8 @@ abstract class MessageDao : EntityDao<VkMessageEntity> {
|
||||
@Query("SELECT * FROM messages")
|
||||
abstract suspend fun getAll(): List<VkMessageEntity>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE peerId IS (:conversationId)")
|
||||
abstract suspend fun getAll(conversationId: Long): List<VkMessageEntity>
|
||||
@Query("SELECT * FROM messages WHERE peerId IS (:convoId)")
|
||||
abstract suspend fun getAll(convoId: Long): List<VkMessageEntity>
|
||||
|
||||
@Query("SELECT * FROM messages WHERE id IN (:ids)")
|
||||
abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity>
|
||||
|
||||
@@ -23,7 +23,7 @@ val databaseModule = module {
|
||||
single { cacheDB().userDao() }
|
||||
single { cacheDB().groupDao() }
|
||||
single { cacheDB().messageDao() }
|
||||
single { cacheDB().conversationDao() }
|
||||
single { cacheDB().convoDao() }
|
||||
}
|
||||
|
||||
private fun Scope.cacheDB(): CacheDatabase = get()
|
||||
|
||||
@@ -8,11 +8,16 @@ android {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(projects.core.common)
|
||||
api(projects.core.data)
|
||||
api(projects.core.model)
|
||||
|
||||
// TODO: 11/08/2024, Danil Nikolaev: remove?
|
||||
implementation(libs.kotlinx.coroutines.core)
|
||||
implementation(libs.koin.core)
|
||||
implementation(libs.eithernet)
|
||||
|
||||
implementation(libs.bundles.nanokt)
|
||||
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.compose.ui)
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,25 +1,25 @@
|
||||
package dev.meloda.fast.domain
|
||||
|
||||
import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.model.ConversationsFilter
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.ConvosFilter
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface ConversationsUseCase : BaseUseCase {
|
||||
interface ConvoUseCase : BaseUseCase {
|
||||
|
||||
suspend fun storeConversations(conversations: List<VkConversation>)
|
||||
suspend fun storeConvos(convos: List<VkConvo>)
|
||||
|
||||
fun getConversations(
|
||||
fun getConvos(
|
||||
count: Int? = null,
|
||||
offset: Int? = null,
|
||||
filter: ConversationsFilter
|
||||
): Flow<State<List<VkConversation>>>
|
||||
filter: ConvosFilter
|
||||
): Flow<State<List<VkConvo>>>
|
||||
|
||||
fun getById(
|
||||
peerIds: List<Long>,
|
||||
extended: Boolean? = null,
|
||||
fields: String? = null
|
||||
): Flow<State<List<VkConversation>>>
|
||||
): Flow<State<List<VkConvo>>>
|
||||
|
||||
fun delete(peerId: Long): Flow<State<Long>>
|
||||
|
||||
+15
-15
@@ -1,30 +1,30 @@
|
||||
package dev.meloda.fast.domain
|
||||
|
||||
import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.data.api.conversations.ConversationsRepository
|
||||
import dev.meloda.fast.data.api.convos.ConvosRepository
|
||||
import dev.meloda.fast.data.mapToState
|
||||
import dev.meloda.fast.model.ConversationsFilter
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.ConvosFilter
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class ConversationsUseCaseImpl(
|
||||
private val repository: ConversationsRepository,
|
||||
) : ConversationsUseCase {
|
||||
class ConvoUseCaseImpl(
|
||||
private val repository: ConvosRepository,
|
||||
) : ConvoUseCase {
|
||||
|
||||
override suspend fun storeConversations(
|
||||
conversations: List<VkConversation>
|
||||
override suspend fun storeConvos(
|
||||
convos: List<VkConvo>
|
||||
) = withContext(Dispatchers.IO) {
|
||||
repository.storeConversations(conversations)
|
||||
repository.storeConvos(convos)
|
||||
}
|
||||
|
||||
override fun getConversations(
|
||||
override fun getConvos(
|
||||
count: Int?,
|
||||
offset: Int?,
|
||||
filter: ConversationsFilter
|
||||
): Flow<State<List<VkConversation>>> = flowNewState {
|
||||
repository.getConversations(
|
||||
filter: ConvosFilter
|
||||
): Flow<State<List<VkConvo>>> = flowNewState {
|
||||
repository.getConvos(
|
||||
count = count,
|
||||
offset = offset,
|
||||
filter = filter
|
||||
@@ -35,8 +35,8 @@ class ConversationsUseCaseImpl(
|
||||
peerIds: List<Long>,
|
||||
extended: Boolean?,
|
||||
fields: String?
|
||||
): Flow<State<List<VkConversation>>> = flowNewState {
|
||||
repository.getConversationsById(
|
||||
): Flow<State<List<VkConvo>>> = flowNewState {
|
||||
repository.getConvosById(
|
||||
peerIds = peerIds,
|
||||
extended = extended,
|
||||
fields = fields
|
||||
@@ -6,10 +6,7 @@ import dev.meloda.fast.model.database.AccountEntity
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class GetCurrentAccountUseCase(
|
||||
private val accountsRepository: AccountsRepository
|
||||
) {
|
||||
|
||||
class GetCurrentAccountUseCase(private val accountsRepository: AccountsRepository) {
|
||||
suspend operator fun invoke(): AccountEntity? = withContext(Dispatchers.IO) {
|
||||
accountsRepository.getAccountById(UserConfig.currentUserId)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
package dev.meloda.fast.domain
|
||||
|
||||
import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.data.api.messages.MessagesRepository
|
||||
import dev.meloda.fast.data.mapToState
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class GetMessageReadPeersUseCase(
|
||||
private val repository: MessagesRepository
|
||||
) : BaseUseCase {
|
||||
|
||||
operator fun invoke(
|
||||
peerId: Long,
|
||||
cmId: Long
|
||||
): Flow<State<Int>> = flowNewState {
|
||||
repository.getMessageReadPeers(
|
||||
peerId = peerId,
|
||||
cmId = cmId
|
||||
).mapToState(successMapper = { it.totalCount })
|
||||
}
|
||||
}
|
||||
+7
-7
@@ -1,22 +1,22 @@
|
||||
package dev.meloda.fast.domain
|
||||
|
||||
import dev.meloda.fast.data.State
|
||||
import dev.meloda.fast.data.api.conversations.ConversationsRepository
|
||||
import dev.meloda.fast.data.api.convos.ConvosRepository
|
||||
import dev.meloda.fast.data.mapToState
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
class LoadConversationsByIdUseCase(
|
||||
private val conversationsRepository: ConversationsRepository
|
||||
class LoadConvosByIdUseCase(
|
||||
private val convosRepository: ConvosRepository
|
||||
) : BaseUseCase {
|
||||
|
||||
operator fun invoke(
|
||||
peerIds: List<Long>,
|
||||
extended: Boolean? = null,
|
||||
fields: String? = null
|
||||
): Flow<State<List<VkConversation>>> = flowNewState {
|
||||
conversationsRepository
|
||||
.getConversationsById(
|
||||
): Flow<State<List<VkConvo>>> = flowNewState {
|
||||
convosRepository
|
||||
.getConvosById(
|
||||
peerIds = peerIds,
|
||||
extended = extended,
|
||||
fields = fields,
|
||||
@@ -9,12 +9,12 @@ import dev.meloda.fast.common.extensions.toList
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.data.processState
|
||||
import dev.meloda.fast.model.ApiEvent
|
||||
import dev.meloda.fast.model.ConversationFlags
|
||||
import dev.meloda.fast.model.ConvoFlags
|
||||
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.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
@@ -28,7 +28,7 @@ import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class LongPollUpdatesParser(
|
||||
private val conversationsUseCase: ConversationsUseCase,
|
||||
private val convoUseCase: ConvoUseCase,
|
||||
private val messagesUseCase: MessagesUseCase
|
||||
) {
|
||||
private val job = SupervisorJob()
|
||||
@@ -271,9 +271,9 @@ class LongPollUpdatesParser(
|
||||
val message =
|
||||
async { loadMessage(peerId = peerId, cmId = cmId) }.await()
|
||||
|
||||
val conversation =
|
||||
val convo =
|
||||
async {
|
||||
loadConversation(
|
||||
loadConvo(
|
||||
peerId = peerId,
|
||||
extended = true,
|
||||
fields = VkConstants.ALL_FIELDS
|
||||
@@ -287,7 +287,7 @@ class LongPollUpdatesParser(
|
||||
.onEvent(
|
||||
LongPollParsedEvent.NewMessage(
|
||||
message = message,
|
||||
inArchive = conversation?.isArchived == true
|
||||
inArchive = convo?.isArchived == true
|
||||
// TODO: 03-Apr-25, Danil Nikolaev:
|
||||
// load user settings about restoring chats with
|
||||
// enabled notifications from archive
|
||||
@@ -368,13 +368,13 @@ class LongPollUpdatesParser(
|
||||
|
||||
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
||||
|
||||
val parsedFlags = ConversationFlags.parse(flags)
|
||||
val parsedFlags = ConvoFlags.parse(flags)
|
||||
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
parsedFlags.forEach { flag ->
|
||||
when (flag) {
|
||||
ConversationFlags.ARCHIVED -> {
|
||||
val conversation = loadConversation(
|
||||
ConvoFlags.ARCHIVED -> {
|
||||
val convo = loadConvo(
|
||||
peerId = peerId,
|
||||
extended = true,
|
||||
fields = VkConstants.ALL_FIELDS
|
||||
@@ -382,11 +382,11 @@ class LongPollUpdatesParser(
|
||||
|
||||
val message = loadMessage(
|
||||
peerId = peerId,
|
||||
cmId = conversation.lastCmId
|
||||
cmId = convo.lastCmId
|
||||
)
|
||||
|
||||
val eventToSend = LongPollParsedEvent.ChatArchived(
|
||||
conversation = conversation.copy(lastMessage = message),
|
||||
convo = convo.copy(lastMessage = message),
|
||||
archived = false
|
||||
)
|
||||
eventsToSend += eventToSend
|
||||
@@ -423,13 +423,13 @@ class LongPollUpdatesParser(
|
||||
|
||||
val eventsToSend = mutableListOf<LongPollParsedEvent>()
|
||||
|
||||
val parsedFlags = ConversationFlags.parse(flags)
|
||||
val parsedFlags = ConvoFlags.parse(flags)
|
||||
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
parsedFlags.forEach { flag ->
|
||||
when (flag) {
|
||||
ConversationFlags.ARCHIVED -> {
|
||||
val conversation = loadConversation(
|
||||
ConvoFlags.ARCHIVED -> {
|
||||
val convo = loadConvo(
|
||||
peerId = peerId,
|
||||
extended = true,
|
||||
fields = VkConstants.ALL_FIELDS
|
||||
@@ -437,11 +437,11 @@ class LongPollUpdatesParser(
|
||||
|
||||
val message = loadMessage(
|
||||
peerId = peerId,
|
||||
cmId = conversation.lastCmId
|
||||
cmId = convo.lastCmId
|
||||
)
|
||||
|
||||
val eventToSend = LongPollParsedEvent.ChatArchived(
|
||||
conversation = conversation.copy(lastMessage = message),
|
||||
convo = convo.copy(lastMessage = message),
|
||||
archived = true
|
||||
)
|
||||
eventsToSend += eventToSend
|
||||
@@ -673,29 +673,29 @@ class LongPollUpdatesParser(
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun loadConversation(
|
||||
private suspend fun loadConvo(
|
||||
peerId: Long,
|
||||
extended: Boolean = false,
|
||||
fields: String? = null
|
||||
): VkConversation? = suspendCoroutine { continuation ->
|
||||
): VkConvo? = suspendCoroutine { continuation ->
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
conversationsUseCase.getById(
|
||||
convoUseCase.getById(
|
||||
peerIds = listOf(peerId),
|
||||
extended = extended,
|
||||
fields = fields
|
||||
).listenValue(coroutineScope) { state ->
|
||||
state.processState(
|
||||
error = { error ->
|
||||
Log.e("LongPollUpdatesParser", "loadConversation: error: $error")
|
||||
Log.e("LongPollUpdatesParser", "loadConvo: error: $error")
|
||||
continuation.resume(null)
|
||||
},
|
||||
success = { response ->
|
||||
val conversation = response.singleOrNull() ?: run {
|
||||
val convo = response.singleOrNull() ?: run {
|
||||
continuation.resume(null)
|
||||
return@listenValue
|
||||
}
|
||||
|
||||
continuation.resume(conversation)
|
||||
continuation.resume(convo)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ interface MessagesUseCase : BaseUseCase {
|
||||
suspend fun storeMessages(messages: List<VkMessage>)
|
||||
|
||||
fun getMessagesHistory(
|
||||
conversationId: Long,
|
||||
convoId: Long,
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): Flow<State<MessagesHistoryInfo>>
|
||||
|
||||
@@ -23,12 +23,12 @@ class MessagesUseCaseImpl(
|
||||
}
|
||||
|
||||
override fun getMessagesHistory(
|
||||
conversationId: Long,
|
||||
convoId: Long,
|
||||
count: Int?,
|
||||
offset: Int?
|
||||
): Flow<State<MessagesHistoryInfo>> = flowNewState {
|
||||
repository.getHistory(
|
||||
conversationId = conversationId,
|
||||
convoId = convoId,
|
||||
offset = offset,
|
||||
count = count
|
||||
).mapToState()
|
||||
|
||||
@@ -6,7 +6,8 @@ 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.GetMessageReadPeersUseCase
|
||||
import dev.meloda.fast.domain.LoadConvosByIdUseCase
|
||||
import dev.meloda.fast.domain.LoadUserByIdUseCase
|
||||
import dev.meloda.fast.domain.LoadUsersByIdsUseCase
|
||||
import dev.meloda.fast.domain.StoreUsersUseCase
|
||||
@@ -26,5 +27,7 @@ val domainModule = module {
|
||||
singleOf(::AccountUseCaseImpl) bind AccountUseCase::class
|
||||
singleOf(::GetCurrentAccountUseCase)
|
||||
|
||||
singleOf(::LoadConversationsByIdUseCase)
|
||||
singleOf(::LoadConvosByIdUseCase)
|
||||
|
||||
singleOf(::GetMessageReadPeersUseCase)
|
||||
}
|
||||
|
||||
+72
-185
@@ -1,7 +1,6 @@
|
||||
package dev.meloda.fast.conversations.util
|
||||
package dev.meloda.fast.domain.util
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
@@ -13,64 +12,22 @@ import dev.meloda.fast.common.extensions.orDots
|
||||
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.data.UserConfig
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
import dev.meloda.fast.model.InteractionType
|
||||
import dev.meloda.fast.model.api.PeerType
|
||||
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.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.model.api.domain.VkVideoDomain
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.model.api.ActionState
|
||||
import dev.meloda.fast.ui.model.api.ConversationOption
|
||||
import dev.meloda.fast.ui.model.api.UiConversation
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import dev.meloda.fast.ui.util.emptyImmutableList
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import kotlin.math.ln
|
||||
import kotlin.math.pow
|
||||
|
||||
fun VkConversation.asPresentation(
|
||||
resources: Resources,
|
||||
useContactName: Boolean,
|
||||
isExpanded: Boolean = false,
|
||||
options: ImmutableList<ConversationOption> = emptyImmutableList()
|
||||
): UiConversation = UiConversation(
|
||||
id = id,
|
||||
lastMessageId = lastMessageId,
|
||||
avatar = extractAvatar(),
|
||||
title = extractTitle(this, useContactName, resources),
|
||||
unreadCount = extractUnreadCount(lastMessage, this),
|
||||
date = TimeUtils.getLocalizedTime(
|
||||
date = (lastMessage?.date ?: -1) * 1000L,
|
||||
yearShort = { resources.getString(R.string.year_short) },
|
||||
monthShort = { resources.getString(R.string.month_short) },
|
||||
weekShort = { resources.getString(R.string.week_short) },
|
||||
dayShort = { resources.getString(R.string.day_short) },
|
||||
now = { resources.getString(R.string.time_now) },
|
||||
),
|
||||
message = extractMessage(resources, lastMessage, id, peerType),
|
||||
attachmentImage = if (lastMessage?.text == null) null
|
||||
else getAttachmentConversationIcon(lastMessage),
|
||||
isPinned = majorId > 0,
|
||||
actionImageId = ActionState.parse(isPhantom, isCallInProgress).getResourceId(),
|
||||
isBirthday = extractBirthday(this),
|
||||
isUnread = !isRead(),
|
||||
isAccount = isAccount(id),
|
||||
isOnline = !isAccount(id) && user?.onlineStatus?.isOnline() == true,
|
||||
lastMessage = lastMessage,
|
||||
peerType = peerType,
|
||||
interactionText = extractInteractionText(resources, this),
|
||||
isExpanded = isExpanded,
|
||||
isArchived = isArchived,
|
||||
options = options
|
||||
)
|
||||
|
||||
fun VkConversation.extractAvatar() = when (peerType) {
|
||||
fun VkConvo.extractAvatar(): UiImage = when (peerType) {
|
||||
PeerType.USER -> {
|
||||
if (isAccount(id)) null
|
||||
else user?.photo200
|
||||
@@ -83,20 +40,19 @@ fun VkConversation.extractAvatar() = when (peerType) {
|
||||
PeerType.CHAT -> {
|
||||
photo200
|
||||
}
|
||||
}?.let(UiImage::Url) ?: UiImage.Resource(R.drawable.ic_account_circle_cut)
|
||||
}?.let(UiImage::Url) ?: UiImage.Resource(R.drawable.ic_account_circle_fill_round_24)
|
||||
|
||||
private fun extractTitle(
|
||||
conversation: VkConversation,
|
||||
fun VkConvo.extractTitle(
|
||||
useContactName: Boolean,
|
||||
resources: Resources
|
||||
) = when (conversation.peerType) {
|
||||
) = when (peerType) {
|
||||
PeerType.USER -> {
|
||||
if (isAccount(conversation.id)) {
|
||||
if (isAccount(id)) {
|
||||
UiText.Resource(R.string.favorites)
|
||||
} else {
|
||||
val userName = conversation.user?.let { user ->
|
||||
val userName = user?.let { user ->
|
||||
if (useContactName) {
|
||||
VkMemoryCache.getContact(user.id)?.name ?: user.fullName
|
||||
VkMemoryCache.getContact(user.id)?.name
|
||||
} else {
|
||||
user.fullName
|
||||
}
|
||||
@@ -106,22 +62,22 @@ private fun extractTitle(
|
||||
}
|
||||
}
|
||||
|
||||
PeerType.GROUP -> UiText.Simple(conversation.group?.name.orDots())
|
||||
PeerType.CHAT -> UiText.Simple(conversation.title.orDots())
|
||||
PeerType.GROUP -> UiText.Simple(group?.name.orDots())
|
||||
PeerType.CHAT -> UiText.Simple(title.orDots())
|
||||
}.parseString(resources).orDots()
|
||||
|
||||
private fun extractUnreadCount(
|
||||
fun extractUnreadCount(
|
||||
lastMessage: VkMessage?,
|
||||
conversation: VkConversation
|
||||
convo: VkConvo
|
||||
): String? = when {
|
||||
lastMessage?.isOut == false && conversation.isInRead() -> null
|
||||
conversation.unreadCount == 0 -> null
|
||||
conversation.unreadCount < 1000 -> conversation.unreadCount.toString()
|
||||
lastMessage?.isOut == false && convo.isInRead() -> null
|
||||
convo.unreadCount == 0 -> null
|
||||
convo.unreadCount < 1000 -> convo.unreadCount.toString()
|
||||
else -> {
|
||||
val exp = (ln(conversation.unreadCount.toDouble()) / ln(1000.0)).toInt()
|
||||
val exp = (ln(convo.unreadCount.toDouble()) / ln(1000.0)).toInt()
|
||||
val suffix = "KMBT"[exp - 1]
|
||||
|
||||
val result = conversation.unreadCount / 1000.0.pow(exp.toDouble())
|
||||
val result = convo.unreadCount / 1000.0.pow(exp.toDouble())
|
||||
|
||||
if (result.toLong().toDouble() == result) {
|
||||
String.format(Locale.getDefault(), "%.0f%s", result, suffix)
|
||||
@@ -131,11 +87,12 @@ private fun extractUnreadCount(
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractMessage(
|
||||
fun extractMessage(
|
||||
resources: Resources,
|
||||
lastMessage: VkMessage?,
|
||||
peerId: Long,
|
||||
peerType: PeerType
|
||||
peerType: PeerType,
|
||||
showPeer: Boolean = true
|
||||
): AnnotatedString {
|
||||
val youPrefix = UiText.Resource(R.string.you_message_prefix)
|
||||
.parseString(resources)
|
||||
@@ -160,6 +117,8 @@ private fun extractMessage(
|
||||
val messageText = lastMessage?.text.orEmpty()
|
||||
|
||||
val prefixText: AnnotatedString? = when {
|
||||
!showPeer -> null
|
||||
|
||||
actionMessage != null -> null
|
||||
|
||||
lastMessage == null -> null
|
||||
@@ -226,16 +185,17 @@ private fun extractMessage(
|
||||
.let { text ->
|
||||
extractTextWithVisualizedMentions(
|
||||
isOut = lastMessage?.isOut == true,
|
||||
originalText = text
|
||||
originalText = text,
|
||||
formatData = null
|
||||
)
|
||||
}
|
||||
.let { text -> prefix + text }
|
||||
.let { text -> prefix + text.orEmpty() }
|
||||
}
|
||||
|
||||
return finalText
|
||||
}
|
||||
|
||||
private fun extractActionText(
|
||||
fun extractActionText(
|
||||
lastMessage: VkMessage?,
|
||||
resources: Resources,
|
||||
youPrefix: String
|
||||
@@ -510,16 +470,25 @@ private fun extractActionText(
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractAttachmentIcon(
|
||||
fun extractAttachmentIcon(
|
||||
lastMessage: VkMessage?
|
||||
): UiImage? = when {
|
||||
lastMessage == null -> null
|
||||
lastMessage.text == null -> null
|
||||
lastMessage.geoType != null -> {
|
||||
val geoType = lastMessage.geoType
|
||||
if (geoType == "point") {
|
||||
UiImage.Resource(R.drawable.ic_pin_drop_fill_round_24)
|
||||
} else {
|
||||
UiImage.Resource(R.drawable.ic_map_fill_round_24)
|
||||
}
|
||||
}
|
||||
|
||||
!lastMessage.forwards.isNullOrEmpty() -> {
|
||||
if (lastMessage.forwards.orEmpty().size == 1) {
|
||||
UiImage.Resource(R.drawable.ic_attachment_forwarded_message)
|
||||
UiImage.Resource(R.drawable.ic_reply_round_24)
|
||||
} else {
|
||||
UiImage.Resource(R.drawable.ic_attachment_forwarded_messages)
|
||||
UiImage.Resource(R.drawable.ic_reply_all_round_24)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -527,19 +496,15 @@ private fun extractAttachmentIcon(
|
||||
lastMessage.attachments?.let { attachments ->
|
||||
if (attachments.isEmpty()) return null
|
||||
if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) {
|
||||
lastMessage.geoType?.let {
|
||||
return UiImage.Resource(R.drawable.ic_map_marker)
|
||||
}
|
||||
|
||||
getAttachmentIconByType(attachments.first().type)
|
||||
} else {
|
||||
UiImage.Resource(R.drawable.ic_baseline_attach_file_24)
|
||||
UiImage.Resource(R.drawable.ic_attach_file_round_24)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractAttachmentText(
|
||||
fun extractAttachmentText(
|
||||
resources: Resources,
|
||||
lastMessage: VkMessage?
|
||||
): AnnotatedString? = when {
|
||||
@@ -605,22 +570,22 @@ private fun extractAttachmentText(
|
||||
|
||||
private fun getAttachmentIconByType(attachmentType: AttachmentType): UiImage? {
|
||||
return when (attachmentType) {
|
||||
AttachmentType.PHOTO -> R.drawable.ic_attachment_photo
|
||||
AttachmentType.VIDEO -> R.drawable.ic_attachment_video
|
||||
AttachmentType.AUDIO -> R.drawable.ic_attachment_audio
|
||||
AttachmentType.FILE -> R.drawable.ic_attachment_file
|
||||
AttachmentType.LINK -> R.drawable.ic_attachment_link
|
||||
AttachmentType.AUDIO_MESSAGE -> R.drawable.ic_attachment_voice
|
||||
AttachmentType.MINI_APP -> R.drawable.ic_attachment_mini_app
|
||||
AttachmentType.STICKER -> R.drawable.ic_attachment_sticker
|
||||
AttachmentType.GIFT -> R.drawable.ic_attachment_gift
|
||||
AttachmentType.WALL -> R.drawable.ic_attachment_wall
|
||||
AttachmentType.GRAFFITI -> R.drawable.ic_attachment_graffiti
|
||||
AttachmentType.POLL -> R.drawable.ic_attachment_poll
|
||||
AttachmentType.WALL_REPLY -> R.drawable.ic_attachment_wall_reply
|
||||
AttachmentType.CALL -> R.drawable.ic_attachment_call
|
||||
AttachmentType.GROUP_CALL_IN_PROGRESS -> R.drawable.ic_attachment_group_call
|
||||
AttachmentType.STORY -> R.drawable.ic_attachment_story
|
||||
AttachmentType.PHOTO -> R.drawable.ic_image_fill_round_24
|
||||
AttachmentType.VIDEO -> R.drawable.ic_video_fill_round_24
|
||||
AttachmentType.AUDIO -> R.drawable.ic_music_note_round_24
|
||||
AttachmentType.FILE -> R.drawable.ic_draft_fill_round_24
|
||||
AttachmentType.LINK -> R.drawable.ic_language_round_24
|
||||
AttachmentType.AUDIO_MESSAGE -> R.drawable.ic_mic_fill_round_24
|
||||
AttachmentType.MINI_APP -> R.drawable.ic_widgets_fill_round_24
|
||||
AttachmentType.STICKER -> R.drawable.ic_sticker_fill_round_24
|
||||
AttachmentType.GIFT -> R.drawable.ic_attachment_gift_old
|
||||
AttachmentType.WALL -> R.drawable.ic_brick_fill_round_24
|
||||
AttachmentType.GRAFFITI -> R.drawable.ic_fragrance_fill_round_24
|
||||
AttachmentType.POLL -> R.drawable.ic_insert_chart_fill_round_24
|
||||
AttachmentType.WALL_REPLY -> R.drawable.ic_comment_fill_round_24
|
||||
AttachmentType.CALL -> R.drawable.ic_call_round_24
|
||||
AttachmentType.GROUP_CALL_IN_PROGRESS -> R.drawable.ic_perm_phone_msg_fill_round_24
|
||||
AttachmentType.STORY -> R.drawable.ic_history_toggle_off_round_24
|
||||
AttachmentType.UNKNOWN -> null
|
||||
AttachmentType.CURATOR -> null
|
||||
AttachmentType.EVENT -> null
|
||||
@@ -631,7 +596,7 @@ private fun getAttachmentIconByType(attachmentType: AttachmentType): UiImage? {
|
||||
AttachmentType.NARRATIVE -> null
|
||||
AttachmentType.ARTICLE -> null
|
||||
AttachmentType.VIDEO_MESSAGE -> null
|
||||
AttachmentType.GROUP_CHAT_STICKER -> R.drawable.ic_attachment_sticker
|
||||
AttachmentType.GROUP_CHAT_STICKER -> R.drawable.ic_sticker_fill_round_24
|
||||
AttachmentType.STICKER_PACK_PREVIEW -> null
|
||||
}?.let(UiImage::Resource)
|
||||
}
|
||||
@@ -649,7 +614,7 @@ private fun isAttachmentsHaveOneType(attachments: List<VkAttachment>): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
private fun extractForwardsText(
|
||||
fun extractForwardsText(
|
||||
resources: Resources,
|
||||
lastMessage: VkMessage?
|
||||
): AnnotatedString? = when {
|
||||
@@ -670,69 +635,7 @@ private fun extractForwardsText(
|
||||
else -> null
|
||||
}
|
||||
|
||||
fun extractTextWithVisualizedMentions(
|
||||
isOut: Boolean,
|
||||
originalText: String
|
||||
): AnnotatedString = buildAnnotatedString {
|
||||
val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex()
|
||||
|
||||
val mentions = mutableListOf<MentionIndex>()
|
||||
|
||||
var currentIndex = 0
|
||||
val replacements = mutableListOf<Pair<IntRange, String>>()
|
||||
|
||||
val result = regex.replace(originalText) { matchResult ->
|
||||
val idPrefix = matchResult.groups[1]?.value.orEmpty()
|
||||
val startIndex = matchResult.range.first
|
||||
val endIndex = matchResult.range.last
|
||||
|
||||
val id = matchResult.groups[2]?.value ?: ""
|
||||
|
||||
val replaced = matchResult.groups[3]?.value.orEmpty()
|
||||
|
||||
val indexRange =
|
||||
(startIndex + currentIndex)..startIndex + currentIndex + replaced.length
|
||||
|
||||
replacements.add(indexRange to replaced)
|
||||
|
||||
mentions += MentionIndex(
|
||||
id = id.toLongOrNull() ?: -1,
|
||||
idPrefix = idPrefix,
|
||||
indexRange = indexRange
|
||||
)
|
||||
|
||||
currentIndex += replaced.length - (endIndex - startIndex + 1)
|
||||
|
||||
replaced
|
||||
}
|
||||
|
||||
append(result)
|
||||
|
||||
mentions.forEach { mention ->
|
||||
val startIndex = mention.indexRange.first
|
||||
val endIndex = mention.indexRange.last
|
||||
|
||||
addStyle(
|
||||
style = SpanStyle(color = Color.Red),
|
||||
start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
addStringAnnotation(
|
||||
tag = mention.idPrefix,
|
||||
annotation = mention.id.toString(),
|
||||
start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
data class MentionIndex(
|
||||
val id: Long,
|
||||
val idPrefix: String,
|
||||
val indexRange: IntRange
|
||||
)
|
||||
|
||||
private fun getAttachmentUiText(
|
||||
fun getAttachmentUiText(
|
||||
attachment: VkAttachment,
|
||||
size: Int = 1,
|
||||
): UiText {
|
||||
@@ -787,22 +690,8 @@ private fun getAttachmentUiText(
|
||||
}.let(UiText::Resource)
|
||||
}
|
||||
|
||||
private fun getAttachmentConversationIcon(message: VkMessage?): UiImage? {
|
||||
return message?.attachments?.let { attachments ->
|
||||
if (attachments.isEmpty()) return null
|
||||
if (attachments.size == 1 || isAttachmentsHaveOneType(attachments)) {
|
||||
message.geoType?.let {
|
||||
return UiImage.Resource(R.drawable.ic_map_marker)
|
||||
}
|
||||
getAttachmentIconByType(attachments.first().type)
|
||||
} else {
|
||||
UiImage.Resource(R.drawable.ic_baseline_attach_file_24)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun extractBirthday(conversation: VkConversation): Boolean {
|
||||
val birthday = conversation.user?.birthday ?: return false
|
||||
fun extractBirthday(convo: VkConvo): Boolean {
|
||||
val birthday = convo.user?.birthday ?: return false
|
||||
val splitBirthday = birthday.split(".").mapNotNull(String::toIntOrNull)
|
||||
|
||||
if (splitBirthday.isEmpty()) return false
|
||||
@@ -822,25 +711,23 @@ private fun extractBirthday(conversation: VkConversation): Boolean {
|
||||
} else false
|
||||
}
|
||||
|
||||
private fun extractReadCondition(
|
||||
conversation: VkConversation,
|
||||
fun extractReadCondition(
|
||||
convo: VkConvo,
|
||||
lastMessage: VkMessage?
|
||||
): Boolean = !conversation.isRead(lastMessage)
|
||||
): Boolean = !convo.isRead(lastMessage)
|
||||
|
||||
private fun isAccount(peerId: Long) = peerId == UserConfig.userId
|
||||
|
||||
private fun extractInteractionText(
|
||||
fun extractInteractionText(
|
||||
resources: Resources,
|
||||
conversation: VkConversation
|
||||
convo: VkConvo
|
||||
): String? {
|
||||
val interactionType = InteractionType.parse(conversation.interactionType)
|
||||
val interactiveUsers = extractInteractionUsers(conversation)
|
||||
val interactionType = InteractionType.parse(convo.interactionType)
|
||||
val interactiveUsers = extractInteractionUsers(convo)
|
||||
|
||||
val typingText =
|
||||
if (interactionType == null) {
|
||||
null
|
||||
} else {
|
||||
if (!conversation.peerType.isChat() && interactiveUsers.size == 1) {
|
||||
if (!convo.peerType.isChat() && interactiveUsers.size == 1) {
|
||||
when (interactionType) {
|
||||
InteractionType.File -> R.string.chat_interaction_uploading_file
|
||||
InteractionType.Photo -> R.string.chat_interaction_uploading_photo
|
||||
@@ -865,8 +752,8 @@ private fun extractInteractionText(
|
||||
return typingText
|
||||
}
|
||||
|
||||
private fun extractInteractionUsers(conversation: VkConversation): List<String> {
|
||||
return conversation.interactionIds.mapNotNull { id ->
|
||||
fun extractInteractionUsers(convo: VkConvo): List<String> {
|
||||
return convo.interactionIds.mapNotNull { id ->
|
||||
when {
|
||||
id > 0 -> VkMemoryCache.getUser(id)?.fullName
|
||||
id < 0 -> VkMemoryCache.getGroup(id)?.name
|
||||
@@ -0,0 +1,49 @@
|
||||
package dev.meloda.fast.domain.util
|
||||
|
||||
import android.content.res.Resources
|
||||
import dev.meloda.fast.common.util.TimeUtils
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.model.vk.ActionState
|
||||
import dev.meloda.fast.ui.model.vk.ConvoOption
|
||||
import dev.meloda.fast.ui.model.vk.UiConvo
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import dev.meloda.fast.ui.util.emptyImmutableList
|
||||
|
||||
fun VkConvo.asPresentation(
|
||||
resources: Resources,
|
||||
useContactName: Boolean,
|
||||
isExpanded: Boolean = false,
|
||||
options: ImmutableList<ConvoOption> = emptyImmutableList()
|
||||
): UiConvo = UiConvo(
|
||||
id = id,
|
||||
lastMessageId = lastMessageId,
|
||||
avatar = extractAvatar(),
|
||||
title = extractTitle(useContactName, resources),
|
||||
unreadCount = extractUnreadCount(lastMessage, this),
|
||||
date = TimeUtils.getLocalizedTime(
|
||||
date = (lastMessage?.date ?: -1) * 1000L,
|
||||
yearShort = { resources.getString(R.string.year_short) },
|
||||
monthShort = { resources.getString(R.string.month_short) },
|
||||
weekShort = { resources.getString(R.string.week_short) },
|
||||
dayShort = { resources.getString(R.string.day_short) },
|
||||
minuteShort = { resources.getString(R.string.minute_short) },
|
||||
secondShort = { resources.getString(R.string.second_short) },
|
||||
now = { resources.getString(R.string.time_now) },
|
||||
),
|
||||
message = extractMessage(resources, lastMessage, id, peerType),
|
||||
attachmentImage = if (lastMessage?.text == null) null
|
||||
else extractAttachmentIcon(lastMessage),
|
||||
isPinned = majorId > 0,
|
||||
actionImageId = ActionState.parse(isPhantom, isCallInProgress).getResourceId(),
|
||||
isBirthday = extractBirthday(this),
|
||||
isUnread = !isRead(),
|
||||
isAccount = isAccount(id),
|
||||
isOnline = !isAccount(id) && user?.onlineStatus?.isOnline() == true,
|
||||
lastMessage = lastMessage,
|
||||
peerType = peerType,
|
||||
interactionText = extractInteractionText(resources, this),
|
||||
isExpanded = isExpanded,
|
||||
isArchived = isArchived,
|
||||
options = options
|
||||
)
|
||||
+1
-1
@@ -3,7 +3,7 @@ package dev.meloda.fast.domain.util
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
import dev.meloda.fast.model.api.domain.VkUser
|
||||
import dev.meloda.fast.ui.model.api.UiFriend
|
||||
import dev.meloda.fast.ui.model.vk.UiFriend
|
||||
|
||||
fun VkUser.asPresentation(
|
||||
useContactNames: Boolean = false
|
||||
+10
-269
@@ -1,34 +1,21 @@
|
||||
package dev.meloda.fast.messageshistory.util
|
||||
package dev.meloda.fast.domain.util
|
||||
|
||||
import android.content.res.Resources
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.AnnotatedString.Annotation
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.StringAnnotation
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import dev.meloda.fast.common.extensions.orDots
|
||||
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.provider.ResourceProvider
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.data.VkMemoryCache
|
||||
import dev.meloda.fast.messageshistory.model.SendingStatus
|
||||
import dev.meloda.fast.messageshistory.model.UiItem
|
||||
import dev.meloda.fast.model.api.PeerType
|
||||
import dev.meloda.fast.model.api.domain.FormatDataType
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.PeerType.Companion.getPeerType
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.ui.R
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
|
||||
private fun isAccount(fromId: Long) = fromId == UserConfig.userId
|
||||
|
||||
fun VkMessage.extractAvatar() = when {
|
||||
isUser() -> {
|
||||
if (isAccount(id)) null
|
||||
@@ -40,7 +27,7 @@ fun VkMessage.extractAvatar() = when {
|
||||
}
|
||||
|
||||
else -> null
|
||||
}?.let(UiImage::Url) ?: UiImage.Resource(R.drawable.ic_account_circle_cut)
|
||||
}?.let(UiImage::Url) ?: UiImage.Resource(R.drawable.ic_account_circle_fill_round_24)
|
||||
|
||||
fun VkMessage.extractDate(): String =
|
||||
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date * 1000L)
|
||||
@@ -58,111 +45,15 @@ fun VkMessage.extractTitle(): String = when {
|
||||
|
||||
fun VkMessage.extractReplyTitle(): String? = replyMessage?.extractTitle()
|
||||
|
||||
// TODO: 24-Jun-25, Danil Nikolaev: improve
|
||||
fun VkMessage.extractReplySummary(): String? = when (val message = replyMessage) {
|
||||
null -> null
|
||||
else -> {
|
||||
when {
|
||||
message.text != null -> message.text
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun VkConversation.extractAvatar(): UiImage = when (peerType) {
|
||||
PeerType.USER -> {
|
||||
if (isAccount(id)) null
|
||||
else user?.photo200
|
||||
}
|
||||
|
||||
PeerType.GROUP -> {
|
||||
group?.photo200
|
||||
}
|
||||
|
||||
PeerType.CHAT -> {
|
||||
photo200
|
||||
}
|
||||
}?.let(UiImage::Url) ?: UiImage.Resource(R.drawable.ic_account_circle_cut)
|
||||
|
||||
fun VkConversation.extractTitle(
|
||||
useContactName: Boolean,
|
||||
resources: Resources
|
||||
) = when (peerType) {
|
||||
PeerType.USER -> {
|
||||
if (isAccount(id)) {
|
||||
UiText.Resource(R.string.favorites)
|
||||
} else {
|
||||
val userName = user?.let { user ->
|
||||
if (useContactName) {
|
||||
VkMemoryCache.getContact(user.id)?.name
|
||||
} else {
|
||||
user.fullName
|
||||
}
|
||||
}
|
||||
|
||||
UiText.Simple(userName.orDots())
|
||||
}
|
||||
}
|
||||
|
||||
PeerType.GROUP -> UiText.Simple(group?.name.orDots())
|
||||
PeerType.CHAT -> UiText.Simple(title.orDots())
|
||||
}.parseString(resources).orDots()
|
||||
|
||||
fun VkMessage.asPresentation(
|
||||
conversation: VkConversation,
|
||||
resourceProvider: ResourceProvider,
|
||||
showName: Boolean,
|
||||
prevMessage: VkMessage?,
|
||||
nextMessage: VkMessage?,
|
||||
showTimeInActionMessages: Boolean,
|
||||
isSelected: Boolean
|
||||
): UiItem = when {
|
||||
action != null -> UiItem.ActionMessage(
|
||||
id = id,
|
||||
cmId = cmId,
|
||||
text = extractActionText(
|
||||
resources = resourceProvider.resources,
|
||||
youPrefix = resourceProvider.getString(R.string.you_message_prefix),
|
||||
showTime = showTimeInActionMessages
|
||||
) ?: buildAnnotatedString { },
|
||||
actionCmId = actionConversationMessageId
|
||||
fun VkMessage.extractReplySummary(resources: Resources): AnnotatedString? =
|
||||
extractMessage(
|
||||
resources = resources,
|
||||
lastMessage = this,
|
||||
peerId = peerId,
|
||||
peerType = getPeerType(),
|
||||
showPeer = false
|
||||
)
|
||||
|
||||
else -> UiItem.Message(
|
||||
id = id,
|
||||
cmId = cmId,
|
||||
text = extractTextWithVisualizedMentions(
|
||||
isOut = isOut,
|
||||
originalText = text,
|
||||
formatData = formatData
|
||||
),
|
||||
isOut = isOut,
|
||||
fromId = fromId,
|
||||
date = extractDate(),
|
||||
randomId = randomId,
|
||||
isInChat = isPeerChat(),
|
||||
name = extractTitle(),
|
||||
showDate = true,
|
||||
showAvatar = extractShowAvatar(nextMessage),
|
||||
showName = showName && extractShowName(prevMessage),
|
||||
avatar = extractAvatar(),
|
||||
isEdited = updateTime != null,
|
||||
isRead = isRead(conversation),
|
||||
sendingStatus = when {
|
||||
isFailed() -> SendingStatus.FAILED
|
||||
id <= 0 -> SendingStatus.SENDING
|
||||
else -> SendingStatus.SENT
|
||||
},
|
||||
isSelected = isSelected,
|
||||
isPinned = isPinned,
|
||||
isImportant = isImportant,
|
||||
attachments = attachments?.ifEmpty { null },
|
||||
replyCmId = replyMessage?.cmId,
|
||||
replyTitle = extractReplyTitle(),
|
||||
replySummary = extractReplySummary()
|
||||
)
|
||||
}
|
||||
|
||||
fun VkMessage.extractShowAvatar(nextMessage: VkMessage?): Boolean {
|
||||
if (isOut) return false
|
||||
return nextMessage == null || nextMessage.fromId != fromId
|
||||
@@ -568,153 +459,3 @@ fun VkMessage.extractActionText(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: 04-Apr-25, Danil Nikolaev: get rid of method duplication
|
||||
fun extractTextWithVisualizedMentions(
|
||||
isOut: Boolean,
|
||||
originalText: String?,
|
||||
formatData: VkMessage.FormatData?
|
||||
): AnnotatedString? {
|
||||
if (originalText == null) return null
|
||||
|
||||
val annotations =
|
||||
mutableListOf<AnnotatedString.Range<out Annotation>>()
|
||||
|
||||
val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex()
|
||||
|
||||
val mentions = mutableListOf<MentionIndex>()
|
||||
|
||||
var currentIndex = 0
|
||||
val replacements = mutableListOf<Pair<IntRange, String>>()
|
||||
|
||||
val newText = regex.replace(originalText) { matchResult ->
|
||||
val idPrefix = matchResult.groups[1]?.value.orEmpty()
|
||||
val startIndex = matchResult.range.first
|
||||
val endIndex = matchResult.range.last
|
||||
|
||||
val id = matchResult.groups[2]?.value ?: ""
|
||||
|
||||
val replaced = matchResult.groups[3]?.value.orEmpty()
|
||||
|
||||
val indexRange =
|
||||
(startIndex + currentIndex)..startIndex + currentIndex + replaced.length
|
||||
|
||||
replacements.add(indexRange to replaced)
|
||||
|
||||
mentions += MentionIndex(
|
||||
id = id.toLongOrNull() ?: -1,
|
||||
idPrefix = idPrefix,
|
||||
indexRange = indexRange
|
||||
)
|
||||
|
||||
currentIndex += replaced.length - (endIndex - startIndex + 1)
|
||||
|
||||
replaced
|
||||
}
|
||||
|
||||
mentions.forEach { mention ->
|
||||
val startIndex = mention.indexRange.first
|
||||
val endIndex = mention.indexRange.last
|
||||
|
||||
annotations += if (isOut) {
|
||||
AnnotatedString.Range(
|
||||
item = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||
start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
} else {
|
||||
AnnotatedString.Range(
|
||||
item = SpanStyle(color = Color.Red),
|
||||
start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
}
|
||||
|
||||
annotations += AnnotatedString.Range(
|
||||
item = StringAnnotation(mention.id.toString()),
|
||||
tag = mention.idPrefix,
|
||||
start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
}
|
||||
|
||||
if (formatData == null) return AnnotatedString(text = newText, annotations = annotations)
|
||||
|
||||
var current = 0
|
||||
|
||||
val newOffsets = formatData.items.map { (offset, length) ->
|
||||
val r = replacements.filter { (range, _) ->
|
||||
(range - current) collidesWith (offset..<offset + length) || offset > range.first
|
||||
}
|
||||
|
||||
current = r.sumOf { (range, _) -> range.last - range.first - 1 }
|
||||
|
||||
offset + current
|
||||
}
|
||||
|
||||
formatData.items.forEachIndexed { index, item ->
|
||||
val offset = newOffsets[index]
|
||||
|
||||
val spanStyle = when (item.type) {
|
||||
FormatDataType.BOLD -> {
|
||||
SpanStyle(fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
|
||||
FormatDataType.ITALIC -> {
|
||||
SpanStyle(fontStyle = FontStyle.Italic)
|
||||
}
|
||||
|
||||
FormatDataType.UNDERLINE -> {
|
||||
SpanStyle(textDecoration = TextDecoration.Underline)
|
||||
}
|
||||
|
||||
FormatDataType.URL -> {
|
||||
annotations += AnnotatedString.Range(
|
||||
item = StringAnnotation(item.url.orEmpty()),
|
||||
start = offset,
|
||||
end = offset + item.length,
|
||||
tag = newText.substring(offset, offset + item.length)
|
||||
)
|
||||
|
||||
if (isOut) {
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
|
||||
} else {
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
annotations += AnnotatedString.Range(
|
||||
item = spanStyle,
|
||||
start = offset,
|
||||
end = offset + item.length
|
||||
)
|
||||
}
|
||||
|
||||
return AnnotatedString(text = newText, annotations = annotations)
|
||||
}
|
||||
|
||||
data class MentionIndex(
|
||||
val id: Long,
|
||||
val idPrefix: String,
|
||||
val indexRange: IntRange
|
||||
)
|
||||
|
||||
infix fun ClosedRange<Int>.collidesWith(other: ClosedRange<Int>): Boolean {
|
||||
return this.start < other.endInclusive && other.start < this.endInclusive
|
||||
}
|
||||
|
||||
operator fun ClosedRange<Int>.minus(other: ClosedRange<Int>): ClosedRange<Int> {
|
||||
return (this.start - other.start)..(this.endInclusive - other.endInclusive)
|
||||
}
|
||||
|
||||
operator fun ClosedRange<Int>.minus(other: Int): ClosedRange<Int> {
|
||||
return (this.start - other)..(this.endInclusive - other)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package dev.meloda.fast.domain.util
|
||||
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
import dev.meloda.fast.common.provider.ResourceProvider
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.model.vk.MessageUiItem
|
||||
import dev.meloda.fast.ui.model.vk.SendingStatus
|
||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
|
||||
fun VkMessage.asPresentation(
|
||||
convo: VkConvo,
|
||||
resourceProvider: ResourceProvider,
|
||||
showName: Boolean,
|
||||
prevMessage: VkMessage?,
|
||||
nextMessage: VkMessage?,
|
||||
showTimeInActionMessages: Boolean,
|
||||
isSelected: Boolean
|
||||
): MessageUiItem = when {
|
||||
action != null -> MessageUiItem.ActionMessage(
|
||||
id = id,
|
||||
cmId = cmId,
|
||||
text = extractActionText(
|
||||
resources = resourceProvider.resources,
|
||||
youPrefix = resourceProvider.getString(R.string.you_message_prefix),
|
||||
showTime = showTimeInActionMessages
|
||||
) ?: buildAnnotatedString { },
|
||||
actionCmId = actionCmId
|
||||
)
|
||||
|
||||
else -> MessageUiItem.Message(
|
||||
id = id,
|
||||
cmId = cmId,
|
||||
text = extractTextWithVisualizedMentions(
|
||||
isOut = isOut,
|
||||
originalText = text,
|
||||
formatData = formatData
|
||||
),
|
||||
isOut = isOut,
|
||||
fromId = fromId,
|
||||
date = extractDate(),
|
||||
randomId = randomId,
|
||||
isInChat = isPeerChat(),
|
||||
name = extractTitle(),
|
||||
showDate = true,
|
||||
showAvatar = extractShowAvatar(nextMessage),
|
||||
showName = showName && extractShowName(prevMessage),
|
||||
avatar = extractAvatar(),
|
||||
isEdited = updateTime != null,
|
||||
isRead = isRead(convo),
|
||||
sendingStatus = when {
|
||||
isFailed() -> SendingStatus.FAILED
|
||||
id <= 0 -> SendingStatus.SENDING
|
||||
else -> SendingStatus.SENT
|
||||
},
|
||||
isSelected = isSelected,
|
||||
isPinned = isPinned,
|
||||
isImportant = isImportant,
|
||||
attachments = attachments?.ifEmpty { null }?.toImmutableList(),
|
||||
replyCmId = replyMessage?.cmId,
|
||||
replyTitle = extractReplyTitle(),
|
||||
replySummary = replyMessage?.extractReplySummary(resourceProvider.resources)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,177 @@
|
||||
package dev.meloda.fast.domain.util
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.StringAnnotation
|
||||
import androidx.compose.ui.text.font.FontStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import com.conena.nanokt.collections.indexOfFirstOrNull
|
||||
import dev.meloda.fast.common.extensions.collidesWith
|
||||
import dev.meloda.fast.common.extensions.minus
|
||||
import dev.meloda.fast.data.UserConfig
|
||||
import dev.meloda.fast.model.api.domain.FormatDataType
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.ui.model.vk.MentionIndex
|
||||
import dev.meloda.fast.ui.model.vk.MessageUiItem
|
||||
|
||||
fun emptyAnnotatedString(): AnnotatedString = AnnotatedString(text = "")
|
||||
|
||||
fun AnnotatedString?.orEmpty(): AnnotatedString = this ?: emptyAnnotatedString()
|
||||
|
||||
fun String.annotated(): AnnotatedString = AnnotatedString(text = this)
|
||||
|
||||
fun isAccount(id: Long) = id == UserConfig.userId
|
||||
|
||||
fun extractTextWithVisualizedMentions(
|
||||
isOut: Boolean,
|
||||
originalText: String?,
|
||||
formatData: VkMessage.FormatData?
|
||||
): AnnotatedString? {
|
||||
if (originalText == null) return null
|
||||
|
||||
val annotations =
|
||||
mutableListOf<AnnotatedString.Range<out androidx.compose.ui.text.AnnotatedString.Annotation>>()
|
||||
|
||||
val regex = """\[(id|club)(\d+)\|([^]]+)]""".toRegex()
|
||||
|
||||
val mentions = mutableListOf<MentionIndex>()
|
||||
|
||||
var currentIndex = 0
|
||||
val replacements = mutableListOf<Pair<IntRange, String>>()
|
||||
|
||||
val newText = regex.replace(originalText) { matchResult ->
|
||||
val idPrefix = matchResult.groups[1]?.value.orEmpty()
|
||||
val startIndex = matchResult.range.first
|
||||
val endIndex = matchResult.range.last
|
||||
|
||||
val id = matchResult.groups[2]?.value ?: ""
|
||||
|
||||
val replaced = matchResult.groups[3]?.value.orEmpty()
|
||||
|
||||
val indexRange =
|
||||
(startIndex + currentIndex)..startIndex + currentIndex + replaced.length
|
||||
|
||||
replacements.add(indexRange to replaced)
|
||||
|
||||
mentions += MentionIndex(
|
||||
id = id.toLongOrNull() ?: -1,
|
||||
idPrefix = idPrefix,
|
||||
indexRange = indexRange
|
||||
)
|
||||
|
||||
currentIndex += replaced.length - (endIndex - startIndex + 1)
|
||||
|
||||
replaced
|
||||
}
|
||||
|
||||
mentions.forEach { mention ->
|
||||
val startIndex = mention.indexRange.first
|
||||
val endIndex = mention.indexRange.last
|
||||
|
||||
annotations += if (isOut) {
|
||||
AnnotatedString.Range(
|
||||
item = SpanStyle(textDecoration = TextDecoration.Underline),
|
||||
start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
} else {
|
||||
AnnotatedString.Range(
|
||||
item = SpanStyle(color = Color.Red),
|
||||
start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
}
|
||||
|
||||
annotations += AnnotatedString.Range(
|
||||
item = StringAnnotation(mention.id.toString()),
|
||||
tag = mention.idPrefix,
|
||||
start = startIndex,
|
||||
end = endIndex
|
||||
)
|
||||
}
|
||||
|
||||
if (formatData == null) {
|
||||
return AnnotatedString(text = newText, annotations = annotations)
|
||||
}
|
||||
|
||||
var current = 0
|
||||
|
||||
val newOffsets = formatData.items.map { (offset, length) ->
|
||||
val r = replacements.filter { (range, _) ->
|
||||
(range - current) collidesWith (offset..<offset + length) || offset > range.first
|
||||
}
|
||||
|
||||
current = r.sumOf { (range, _) -> range.last - range.first - 1 }
|
||||
|
||||
offset + current
|
||||
}
|
||||
|
||||
formatData.items.forEachIndexed { index, item ->
|
||||
val offset = newOffsets[index]
|
||||
|
||||
val spanStyle = when (item.type) {
|
||||
FormatDataType.BOLD -> {
|
||||
SpanStyle(fontWeight = FontWeight.SemiBold)
|
||||
}
|
||||
|
||||
FormatDataType.ITALIC -> {
|
||||
SpanStyle(fontStyle = FontStyle.Italic)
|
||||
}
|
||||
|
||||
FormatDataType.UNDERLINE -> {
|
||||
SpanStyle(textDecoration = TextDecoration.Underline)
|
||||
}
|
||||
|
||||
FormatDataType.URL -> {
|
||||
annotations += AnnotatedString.Range(
|
||||
item = StringAnnotation(item.url.orEmpty()),
|
||||
start = offset,
|
||||
end = offset + item.length,
|
||||
tag = newText.substring(offset, offset + item.length)
|
||||
)
|
||||
|
||||
if (isOut) {
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
textDecoration = TextDecoration.Underline
|
||||
)
|
||||
|
||||
} else {
|
||||
SpanStyle(
|
||||
fontWeight = FontWeight.SemiBold,
|
||||
color = Color.Red
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
annotations += AnnotatedString.Range(
|
||||
item = spanStyle,
|
||||
start = offset,
|
||||
end = offset + item.length
|
||||
)
|
||||
}
|
||||
|
||||
return AnnotatedString(text = newText, annotations = annotations)
|
||||
}
|
||||
|
||||
|
||||
fun List<MessageUiItem>.firstMessage(): MessageUiItem.Message =
|
||||
filterIsInstance<MessageUiItem.Message>().first()
|
||||
|
||||
fun List<MessageUiItem>.firstMessageOrNull(): MessageUiItem.Message? =
|
||||
filterIsInstance<MessageUiItem.Message>().firstOrNull()
|
||||
|
||||
fun List<MessageUiItem>.indexOfMessageById(messageId: Long): Int =
|
||||
indexOfFirst { it.id == messageId }
|
||||
|
||||
fun List<MessageUiItem>.findMessageById(messageId: Long): MessageUiItem.Message? =
|
||||
firstOrNull { it.id == messageId } as MessageUiItem.Message?
|
||||
|
||||
fun List<MessageUiItem>.indexOfMessageByCmId(cmId: Long): Int? =
|
||||
indexOfFirstOrNull { it.cmId == cmId }
|
||||
|
||||
fun List<MessageUiItem>.findMessageByCmId(cmId: Long): MessageUiItem.Message =
|
||||
first { it.cmId == cmId } as MessageUiItem.Message
|
||||
@@ -4,7 +4,7 @@ plugins {
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "dev.meloda.fast.datastore"
|
||||
namespace = "dev.meloda.fast.model"
|
||||
}
|
||||
|
||||
dependencies {
|
||||
@@ -12,7 +12,7 @@ dependencies {
|
||||
ksp(libs.moshi.kotlin.codegen)
|
||||
|
||||
implementation(platform(libs.compose.bom))
|
||||
implementation(libs.bundles.compose)
|
||||
implementation(libs.compose.ui)
|
||||
|
||||
implementation(libs.room.ktx)
|
||||
implementation(libs.room.runtime)
|
||||
|
||||
+4
-4
@@ -1,6 +1,6 @@
|
||||
package dev.meloda.fast.model
|
||||
|
||||
enum class ConversationFlags(val value: Int) {
|
||||
enum class ConvoFlags(val value: Int) {
|
||||
DISABLE_PUSH(16),
|
||||
DISABLE_SOUND(32),
|
||||
INCOMING_CHAT_REQUEST(256),
|
||||
@@ -17,10 +17,10 @@ enum class ConversationFlags(val value: Int) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun parse(mask: Int): List<ConversationFlags> {
|
||||
val flags = mutableListOf<ConversationFlags>()
|
||||
fun parse(mask: Int): List<ConvoFlags> {
|
||||
val flags = mutableListOf<ConvoFlags>()
|
||||
|
||||
ConversationFlags.entries.forEach { flag ->
|
||||
ConvoFlags.entries.forEach { flag ->
|
||||
if (mask and flag.value > 0) {
|
||||
flags.add(flag)
|
||||
}
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
package dev.meloda.fast.model
|
||||
|
||||
enum class ConversationsFilter {
|
||||
enum class ConvosFilter {
|
||||
ALL, UNREAD, ARCHIVE, BUSINESS_NOTIFY
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package dev.meloda.fast.model
|
||||
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
|
||||
sealed interface LongPollParsedEvent {
|
||||
@@ -92,7 +92,7 @@ sealed interface LongPollParsedEvent {
|
||||
) : LongPollParsedEvent
|
||||
|
||||
data class ChatArchived(
|
||||
val conversation: VkConversation,
|
||||
val convo: VkConvo,
|
||||
val archived: Boolean
|
||||
) : LongPollParsedEvent
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package dev.meloda.fast.model.api
|
||||
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
|
||||
enum class PeerType(val value: String) {
|
||||
USER("user"),
|
||||
GROUP("group"),
|
||||
@@ -13,5 +15,14 @@ enum class PeerType(val value: String) {
|
||||
fun parse(type: String): PeerType {
|
||||
return entries.first { it.value == type }
|
||||
}
|
||||
|
||||
fun VkMessage.getPeerType(): PeerType {
|
||||
return when {
|
||||
peerId > 2_000_000_000 -> CHAT
|
||||
peerId > 0 -> USER
|
||||
peerId < 0 -> GROUP
|
||||
else -> throw IllegalArgumentException("Unknown peer type for peerId: 0")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+2
-2
@@ -8,7 +8,7 @@ import dev.meloda.fast.model.api.domain.VkAttachmentHistoryMessage
|
||||
data class VkAttachmentHistoryMessageData(
|
||||
@Json(name = "message_id") val messageId: Long,
|
||||
@Json(name = "date") val date: Int,
|
||||
@Json(name = "cmid") val conversationMessageId: Long,
|
||||
@Json(name = "cmid") val cmId: Long,
|
||||
@Json(name = "from_id") val fromId: Long,
|
||||
@Json(name = "position") val position: Int,
|
||||
@Json(name = "attachment") val attachment: VkAttachmentItemData
|
||||
@@ -16,7 +16,7 @@ data class VkAttachmentHistoryMessageData(
|
||||
|
||||
fun toDomain(): VkAttachmentHistoryMessage = VkAttachmentHistoryMessage(
|
||||
messageId = messageId,
|
||||
conversationMessageId = conversationMessageId,
|
||||
cmId = cmId,
|
||||
date = date,
|
||||
fromId = fromId,
|
||||
position = position,
|
||||
|
||||
+9
-9
@@ -3,19 +3,19 @@ package dev.meloda.fast.model.api.data
|
||||
import com.squareup.moshi.Json
|
||||
import com.squareup.moshi.JsonClass
|
||||
import dev.meloda.fast.model.api.PeerType
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
import dev.meloda.fast.model.api.domain.VkMessage
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class VkConversationData(
|
||||
data class VkConvoData(
|
||||
@Json(name = "peer") val peer: Peer,
|
||||
@Json(name = "last_message_id") val lastMessageId: Long?,
|
||||
@Json(name = "in_read") val inRead: Long,
|
||||
@Json(name = "out_read") val outRead: Long,
|
||||
@Json(name = "in_read_cmid") val inReadConversationMessageId: Long,
|
||||
@Json(name = "out_read_cmid") val outReadConversationMessageId: Long,
|
||||
@Json(name = "in_read_cmid") val inReadCmId: Long,
|
||||
@Json(name = "out_read_cmid") val outReadCmId: Long,
|
||||
@Json(name = "sort_id") val sortId: SortId,
|
||||
@Json(name = "last_conversation_message_id") val lastConversationMessageId: Long,
|
||||
@Json(name = "last_conversation_message_id") val lastCmId: Long,
|
||||
@Json(name = "is_marked_unread") val isMarkedUnread: Boolean,
|
||||
@Json(name = "important") val important: Boolean,
|
||||
@Json(name = "push_settings") val pushSettings: PushSettings?,
|
||||
@@ -111,7 +111,7 @@ data class VkConversationData(
|
||||
|
||||
fun asDomain(
|
||||
lastMessage: VkMessage? = null,
|
||||
): VkConversation = VkConversation(
|
||||
): VkConvo = VkConvo(
|
||||
id = peer.id,
|
||||
localId = peer.localId,
|
||||
title = chatSettings?.title,
|
||||
@@ -120,7 +120,7 @@ data class VkConversationData(
|
||||
photo200 = chatSettings?.photo?.photo200,
|
||||
isCallInProgress = callInProgress != null,
|
||||
isPhantom = chatSettings?.isDisappearing == true,
|
||||
lastCmId = lastConversationMessageId,
|
||||
lastCmId = lastCmId,
|
||||
inRead = inRead,
|
||||
outRead = outRead,
|
||||
lastMessageId = lastMessageId,
|
||||
@@ -132,8 +132,8 @@ data class VkConversationData(
|
||||
canChangePin = chatSettings?.acl?.canChangePin == true,
|
||||
canChangeInfo = chatSettings?.acl?.canChangeInfo == true,
|
||||
pinnedMessageId = chatSettings?.pinnedMessage?.id,
|
||||
inReadCmId = inReadConversationMessageId,
|
||||
outReadCmId = outReadConversationMessageId,
|
||||
inReadCmId = inReadCmId,
|
||||
outReadCmId = outReadCmId,
|
||||
interactionType = -1,
|
||||
interactionIds = emptyList(),
|
||||
peerType = PeerType.parse(peer.type),
|
||||
@@ -56,7 +56,7 @@ data class VkMessageData(
|
||||
@Json(name = "type") val type: String,
|
||||
@Json(name = "member_id") val memberId: Long?,
|
||||
@Json(name = "text") val text: String?,
|
||||
@Json(name = "conversation_message_id") val conversationMessageId: Long?,
|
||||
@Json(name = "conversation_message_id") val cmId: Long?,
|
||||
@Json(name = "message") val message: String?
|
||||
)
|
||||
|
||||
@@ -102,7 +102,7 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage(
|
||||
action = VkMessage.Action.parse(action?.type),
|
||||
actionMemberId = action?.memberId,
|
||||
actionText = action?.text,
|
||||
actionConversationMessageId = action?.conversationMessageId,
|
||||
actionCmId = action?.cmId,
|
||||
actionMessage = action?.message,
|
||||
geoType = geo?.type,
|
||||
isImportant = important == true,
|
||||
|
||||
@@ -12,7 +12,7 @@ data class VkPinnedMessageData(
|
||||
@Json(name = "from_id") val fromId: Long,
|
||||
@Json(name = "out") val out: Boolean?,
|
||||
@Json(name = "text") val text: String,
|
||||
@Json(name = "conversation_message_id") val conversationMessageId: Long,
|
||||
@Json(name = "conversation_message_id") val cmId: Long,
|
||||
@Json(name = "fwd_messages") val forwards: List<VkMessageData>?,
|
||||
@Json(name = "important") val important: Boolean = false,
|
||||
@Json(name = "random_id") val randomId: Long = 0,
|
||||
@@ -28,7 +28,7 @@ data class VkPinnedMessageData(
|
||||
|
||||
fun mapToDomain(): VkMessage = VkMessage(
|
||||
id = id ?: -1,
|
||||
cmId = conversationMessageId,
|
||||
cmId = cmId,
|
||||
text = text.ifBlank { null },
|
||||
isOut = out == true,
|
||||
peerId = peerId ?: -1,
|
||||
@@ -38,7 +38,7 @@ data class VkPinnedMessageData(
|
||||
action = VkMessage.Action.parse(action?.type),
|
||||
actionMemberId = action?.memberId,
|
||||
actionText = action?.text,
|
||||
actionConversationMessageId = action?.conversationMessageId,
|
||||
actionCmId = action?.cmId,
|
||||
actionMessage = action?.message,
|
||||
geoType = geo?.type,
|
||||
isImportant = important,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package dev.meloda.fast.model.api.domain
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import dev.meloda.fast.model.api.data.AttachmentType
|
||||
|
||||
@Immutable
|
||||
interface VkAttachment {
|
||||
val type: AttachmentType
|
||||
}
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ package dev.meloda.fast.model.api.domain
|
||||
|
||||
data class VkAttachmentHistoryMessage(
|
||||
val messageId: Long,
|
||||
val conversationMessageId: Long,
|
||||
val cmId: Long,
|
||||
val date: Int,
|
||||
val fromId: Long,
|
||||
val position: Int,
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
package dev.meloda.fast.model.api.domain
|
||||
|
||||
import dev.meloda.fast.model.api.PeerType
|
||||
import dev.meloda.fast.model.database.VkConversationEntity
|
||||
import dev.meloda.fast.model.database.VkConvoEntity
|
||||
|
||||
data class VkConversation(
|
||||
data class VkConvo(
|
||||
val id: Long,
|
||||
val localId: Long,
|
||||
val ownerId: Long?,
|
||||
@@ -54,7 +54,7 @@ data class VkConversation(
|
||||
}
|
||||
|
||||
companion object {
|
||||
val EMPTY: VkConversation = VkConversation(
|
||||
val EMPTY: VkConvo = VkConvo(
|
||||
id = -1,
|
||||
localId = -1,
|
||||
ownerId = null,
|
||||
@@ -90,7 +90,7 @@ data class VkConversation(
|
||||
}
|
||||
}
|
||||
|
||||
fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity(
|
||||
fun VkConvo.asEntity(): VkConvoEntity = VkConvoEntity(
|
||||
id = id,
|
||||
localId = localId,
|
||||
ownerId = ownerId,
|
||||
@@ -99,7 +99,7 @@ fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity(
|
||||
photo100 = photo100,
|
||||
photo200 = photo200,
|
||||
isPhantom = isPhantom,
|
||||
lastConversationMessageId = lastCmId,
|
||||
lastCmId = lastCmId,
|
||||
inReadCmId = inReadCmId,
|
||||
outReadCmId = outReadCmId,
|
||||
inRead = inRead,
|
||||
@@ -16,7 +16,7 @@ data class VkMessage(
|
||||
val action: Action?,
|
||||
val actionMemberId: Long?,
|
||||
val actionText: String?,
|
||||
val actionConversationMessageId: Long?,
|
||||
val actionCmId: Long?,
|
||||
val actionMessage: String?,
|
||||
|
||||
val updateTime: Int?,
|
||||
@@ -44,9 +44,9 @@ data class VkMessage(
|
||||
|
||||
fun isGroup() = fromId < 0
|
||||
|
||||
fun isRead(conversation: VkConversation): Boolean = when {
|
||||
fun isRead(convo: VkConvo): Boolean = when {
|
||||
id <= 0 -> false
|
||||
else -> conversation.isRead(this)
|
||||
else -> convo.isRead(this)
|
||||
}
|
||||
|
||||
fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty()
|
||||
@@ -98,7 +98,7 @@ data class VkMessage(
|
||||
|
||||
fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
|
||||
id = id,
|
||||
conversationMessageId = cmId,
|
||||
cmId = cmId,
|
||||
text = text,
|
||||
isOut = isOut,
|
||||
peerId = peerId,
|
||||
@@ -108,7 +108,7 @@ fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
|
||||
action = action?.value,
|
||||
actionMemberId = actionMemberId,
|
||||
actionText = actionText,
|
||||
actionConversationMessageId = actionConversationMessageId,
|
||||
actionCmId = actionCmId,
|
||||
actionMessage = actionMessage,
|
||||
updateTime = updateTime,
|
||||
important = isImportant,
|
||||
|
||||
+3
-3
@@ -1,12 +1,12 @@
|
||||
package dev.meloda.fast.model.api.requests
|
||||
|
||||
import dev.meloda.fast.model.ConversationsFilter
|
||||
import dev.meloda.fast.model.ConvosFilter
|
||||
|
||||
data class ConversationsGetRequest(
|
||||
data class ConvosGetRequest(
|
||||
val count: Int? = null,
|
||||
val offset: Int? = null,
|
||||
val fields: String = "",
|
||||
val filter: ConversationsFilter = ConversationsFilter.ALL,
|
||||
val filter: ConvosFilter = ConvosFilter.ALL,
|
||||
val extended: Boolean? = true,
|
||||
val startMessageId: Long? = null
|
||||
) {
|
||||
@@ -115,7 +115,7 @@ data class MessagesGetLongPollServerRequest(
|
||||
data class MessagesPinMessageRequest(
|
||||
val peerId: Long,
|
||||
val messageId: Long? = null,
|
||||
val conversationMessageId: Long? = null
|
||||
val cmId: Long? = null
|
||||
) {
|
||||
|
||||
val map: Map<String, String>
|
||||
@@ -123,7 +123,7 @@ data class MessagesPinMessageRequest(
|
||||
"peer_id" to peerId.toString()
|
||||
).apply {
|
||||
messageId?.let { this["message_id"] = it.toString() }
|
||||
conversationMessageId?.let { this["conversation_message_id"] = it.toString() }
|
||||
cmId?.let { this["conversation_message_id"] = it.toString() }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -136,7 +136,7 @@ data class MessagesUnpinMessageRequest(val peerId: Long) {
|
||||
data class MessagesDeleteRequest(
|
||||
val peerId: Long,
|
||||
val messagesIds: List<Long>? = null,
|
||||
val conversationsMessagesIds: List<Long>? = null,
|
||||
val cmIds: List<Long>? = null,
|
||||
val isSpam: Boolean? = null,
|
||||
val deleteForAll: Boolean? = null
|
||||
) {
|
||||
@@ -149,7 +149,7 @@ data class MessagesDeleteRequest(
|
||||
deleteForAll?.let { this["delete_for_all"] = it.asInt().toString() }
|
||||
messagesIds?.let { this["message_ids"] = it.joinToString() }
|
||||
|
||||
conversationsMessagesIds?.let {
|
||||
cmIds?.let {
|
||||
this["conversation_message_ids"] = it.joinToString()
|
||||
}
|
||||
}
|
||||
@@ -228,7 +228,7 @@ data class MessagesGetChatRequest(
|
||||
}
|
||||
|
||||
|
||||
data class MessagesGetConversationMembersRequest(
|
||||
data class MessagesGetConvoMembersRequest(
|
||||
val peerId: Long,
|
||||
val offset: Int? = null,
|
||||
val count: Int? = null,
|
||||
@@ -267,14 +267,14 @@ data class MessagesGetHistoryAttachmentsRequest(
|
||||
val offset: Int?,
|
||||
val preserveOrder: Boolean?,
|
||||
val attachmentTypes: List<String>,
|
||||
val conversationMessageId: Long,
|
||||
val cmId: Long,
|
||||
val fields: String?
|
||||
) {
|
||||
|
||||
val map = mutableMapOf(
|
||||
"peer_id" to peerId.toString(),
|
||||
"attachment_types" to attachmentTypes.joinToString(","),
|
||||
"cmid" to conversationMessageId.toString()
|
||||
"cmid" to cmId.toString()
|
||||
).apply {
|
||||
extended?.let { this["extended"] = it.toString() }
|
||||
count?.let { this["count"] = it.toString() }
|
||||
|
||||
+8
-8
@@ -3,15 +3,15 @@ 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.VkConvoData
|
||||
import dev.meloda.fast.model.api.data.VkGroupData
|
||||
import dev.meloda.fast.model.api.data.VkMessageData
|
||||
import dev.meloda.fast.model.api.data.VkUserData
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ConversationsGetResponse(
|
||||
data class ConvosGetResponse(
|
||||
@Json(name = "count") val count: Int,
|
||||
@Json(name = "items") val items: List<ConversationsResponseItem>,
|
||||
@Json(name = "items") val items: List<ConvosResponseItem>,
|
||||
@Json(name = "unread_count") val unreadCount: Int?,
|
||||
@Json(name = "profiles") val profiles: List<VkUserData>?,
|
||||
@Json(name = "groups") val groups: List<VkGroupData>?,
|
||||
@@ -19,21 +19,21 @@ data class ConversationsGetResponse(
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ConversationsGetByIdResponse(
|
||||
data class ConvosGetByIdResponse(
|
||||
@Json(name = "count") val count: Int,
|
||||
@Json(name = "items") val items: List<VkConversationData>,
|
||||
@Json(name = "items") val items: List<VkConvoData>,
|
||||
@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,
|
||||
data class ConvosResponseItem(
|
||||
@Json(name = "conversation") val convo: VkConvoData,
|
||||
@Json(name = "last_message") val lastMessage: VkMessageData?
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class ConversationsDeleteResponse(
|
||||
data class ConvosDeleteResponse(
|
||||
@Json(name = "last_deleted_id") val lastDeletedId: Long
|
||||
)
|
||||
@@ -5,7 +5,7 @@ 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
|
||||
import dev.meloda.fast.model.api.data.VkConversationData
|
||||
import dev.meloda.fast.model.api.data.VkConvoData
|
||||
import dev.meloda.fast.model.api.data.VkGroupData
|
||||
import dev.meloda.fast.model.api.data.VkMessageData
|
||||
import dev.meloda.fast.model.api.data.VkUserData
|
||||
@@ -14,7 +14,7 @@ import dev.meloda.fast.model.api.data.VkUserData
|
||||
data class MessagesGetHistoryResponse(
|
||||
val count: Int,
|
||||
val items: List<VkMessageData>,
|
||||
val conversations: List<VkConversationData>?,
|
||||
val convos: List<VkConvoData>?,
|
||||
val profiles: List<VkUserData>?,
|
||||
val groups: List<VkGroupData>?,
|
||||
val contacts: List<VkContactData>?
|
||||
@@ -30,7 +30,7 @@ data class MessagesGetByIdResponse(
|
||||
)
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessagesGetConversationMembersResponse(
|
||||
data class MessagesGetConvoMembersResponse(
|
||||
val count: Int,
|
||||
val items: List<VkChatMemberData>?,
|
||||
val profiles: List<VkUserData>?,
|
||||
@@ -70,3 +70,10 @@ data class MessagesMarkAsImportantResponse(
|
||||
@Json(name = "peer_id") val peerId: Long
|
||||
)
|
||||
}
|
||||
|
||||
@JsonClass(generateAdapter = true)
|
||||
data class MessagesGetReadPeersResponse(
|
||||
@Json(name = "items") val items: List<Long>,
|
||||
@Json(name = "total_count") val totalCount: Int,
|
||||
@Json(name = "profiles") val profiles: List<VkUserData>?,
|
||||
)
|
||||
|
||||
+2
-2
@@ -3,8 +3,8 @@ package dev.meloda.fast.model.database
|
||||
import androidx.room.Embedded
|
||||
import androidx.room.Relation
|
||||
|
||||
data class ConversationWithMessage(
|
||||
@Embedded val conversation: VkConversationEntity,
|
||||
data class ConvoWithMessage(
|
||||
@Embedded val convo: VkConvoEntity,
|
||||
@Relation(
|
||||
parentColumn = "lastMessageId",
|
||||
entityColumn = "id"
|
||||
+6
-6
@@ -3,10 +3,10 @@ package dev.meloda.fast.model.database
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import dev.meloda.fast.model.api.PeerType
|
||||
import dev.meloda.fast.model.api.domain.VkConversation
|
||||
import dev.meloda.fast.model.api.domain.VkConvo
|
||||
|
||||
@Entity(tableName = "conversations")
|
||||
data class VkConversationEntity(
|
||||
@Entity(tableName = "convos")
|
||||
data class VkConvoEntity(
|
||||
@PrimaryKey val id: Long,
|
||||
val localId: Long,
|
||||
val ownerId: Long?,
|
||||
@@ -15,7 +15,7 @@ data class VkConversationEntity(
|
||||
val photo100: String?,
|
||||
val photo200: String?,
|
||||
val isPhantom: Boolean,
|
||||
val lastConversationMessageId: Long,
|
||||
val lastCmId: Long,
|
||||
val inReadCmId: Long,
|
||||
val outReadCmId: Long,
|
||||
val inRead: Long,
|
||||
@@ -32,7 +32,7 @@ data class VkConversationEntity(
|
||||
val isArchived: Boolean
|
||||
)
|
||||
|
||||
fun VkConversationEntity.asExternalModel(): VkConversation = VkConversation(
|
||||
fun VkConvoEntity.asExternalModel(): VkConvo = VkConvo(
|
||||
id = id,
|
||||
localId = localId,
|
||||
ownerId = ownerId,
|
||||
@@ -42,7 +42,7 @@ fun VkConversationEntity.asExternalModel(): VkConversation = VkConversation(
|
||||
photo200 = photo200,
|
||||
isCallInProgress = false,
|
||||
isPhantom = isPhantom,
|
||||
lastCmId = lastConversationMessageId,
|
||||
lastCmId = lastCmId,
|
||||
inReadCmId = inReadCmId,
|
||||
outReadCmId = outReadCmId,
|
||||
inRead = inRead,
|
||||
@@ -8,7 +8,7 @@ import dev.meloda.fast.model.api.domain.VkUnknownAttachment
|
||||
@Entity(tableName = "messages")
|
||||
data class VkMessageEntity(
|
||||
@PrimaryKey val id: Long,
|
||||
val conversationMessageId: Long,
|
||||
val cmId: Long,
|
||||
val text: String?,
|
||||
val isOut: Boolean,
|
||||
val peerId: Long,
|
||||
@@ -18,7 +18,7 @@ data class VkMessageEntity(
|
||||
val action: String?,
|
||||
val actionMemberId: Long?,
|
||||
val actionText: String?,
|
||||
val actionConversationMessageId: Long?,
|
||||
val actionCmId: Long?,
|
||||
val actionMessage: String?,
|
||||
val updateTime: Int?,
|
||||
val important: Boolean,
|
||||
@@ -32,7 +32,7 @@ data class VkMessageEntity(
|
||||
|
||||
fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
|
||||
id = id,
|
||||
cmId = conversationMessageId,
|
||||
cmId = cmId,
|
||||
text = text,
|
||||
isOut = isOut,
|
||||
peerId = peerId,
|
||||
@@ -42,7 +42,7 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
|
||||
action = VkMessage.Action.parse(action),
|
||||
actionMemberId = actionMemberId,
|
||||
actionText = actionText,
|
||||
actionConversationMessageId = actionConversationMessageId,
|
||||
actionCmId = actionCmId,
|
||||
actionMessage = actionMessage,
|
||||
updateTime = updateTime,
|
||||
isImportant = important,
|
||||
|
||||
@@ -16,7 +16,7 @@ import dev.meloda.fast.network.interceptor.VersionInterceptor
|
||||
import dev.meloda.fast.network.service.account.AccountService
|
||||
import dev.meloda.fast.network.service.audios.AudiosService
|
||||
import dev.meloda.fast.network.service.auth.AuthService
|
||||
import dev.meloda.fast.network.service.conversations.ConversationsService
|
||||
import dev.meloda.fast.network.service.convos.ConvosService
|
||||
import dev.meloda.fast.network.service.files.FilesService
|
||||
import dev.meloda.fast.network.service.friends.FriendsService
|
||||
import dev.meloda.fast.network.service.longpoll.LongPollService
|
||||
@@ -80,7 +80,7 @@ val networkModule = module {
|
||||
|
||||
single { service(AccountService::class.java) }
|
||||
single { service(AudiosService::class.java) }
|
||||
single { service(ConversationsService::class.java) }
|
||||
single { service(ConvosService::class.java) }
|
||||
single { service(FilesService::class.java) }
|
||||
single { service(LongPollService::class.java) }
|
||||
single { service(MessagesService::class.java) }
|
||||
|
||||
+18
-18
@@ -1,61 +1,61 @@
|
||||
package dev.meloda.fast.network.service.conversations
|
||||
package dev.meloda.fast.network.service.convos
|
||||
|
||||
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.model.api.responses.ConvosDeleteResponse
|
||||
import dev.meloda.fast.model.api.responses.ConvosGetByIdResponse
|
||||
import dev.meloda.fast.model.api.responses.ConvosGetResponse
|
||||
import dev.meloda.fast.network.ApiResponse
|
||||
import dev.meloda.fast.network.RestApiError
|
||||
import retrofit2.http.FieldMap
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface ConversationsService {
|
||||
interface ConvosService {
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(ConversationsUrls.GET)
|
||||
suspend fun getConversations(
|
||||
@POST(ConvosUrls.GET)
|
||||
suspend fun getConvos(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<ConversationsGetResponse>, RestApiError>
|
||||
): ApiResult<ApiResponse<ConvosGetResponse>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(ConversationsUrls.GET_BY_ID)
|
||||
suspend fun getConversationsById(
|
||||
@POST(ConvosUrls.GET_BY_ID)
|
||||
suspend fun getConvosById(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<ConversationsGetByIdResponse>, RestApiError>
|
||||
): ApiResult<ApiResponse<ConvosGetByIdResponse>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(ConversationsUrls.DELETE)
|
||||
@POST(ConvosUrls.DELETE)
|
||||
suspend fun delete(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<ConversationsDeleteResponse>, RestApiError>
|
||||
): ApiResult<ApiResponse<ConvosDeleteResponse>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(ConversationsUrls.PIN)
|
||||
@POST(ConvosUrls.PIN)
|
||||
suspend fun pin(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<Int>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(ConversationsUrls.UNPIN)
|
||||
@POST(ConvosUrls.UNPIN)
|
||||
suspend fun unpin(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<Int>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(ConversationsUrls.REORDER_PINNED)
|
||||
@POST(ConvosUrls.REORDER_PINNED)
|
||||
suspend fun reorderPinned(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<Int>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(ConversationsUrls.ARCHIVE)
|
||||
@POST(ConvosUrls.ARCHIVE)
|
||||
suspend fun archive(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<Int>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(ConversationsUrls.UNARCHIVE)
|
||||
@POST(ConvosUrls.UNARCHIVE)
|
||||
suspend fun unarchive(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<Int>, RestApiError>
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
package dev.meloda.fast.network.service.conversations
|
||||
package dev.meloda.fast.network.service.convos
|
||||
|
||||
import dev.meloda.fast.common.AppConstants
|
||||
|
||||
object ConversationsUrls {
|
||||
object ConvosUrls {
|
||||
|
||||
private const val URL = AppConstants.URL_API
|
||||
|
||||
+11
-4
@@ -6,9 +6,10 @@ import dev.meloda.fast.model.api.data.VkLongPollData
|
||||
import dev.meloda.fast.model.api.data.VkMessageData
|
||||
import dev.meloda.fast.model.api.responses.MessagesCreateChatResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetByIdResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetConvoMembersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetHistoryAttachmentsResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetHistoryResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesMarkAsImportantResponse
|
||||
import dev.meloda.fast.model.api.responses.MessagesSendResponse
|
||||
import dev.meloda.fast.network.ApiResponse
|
||||
@@ -98,14 +99,20 @@ interface MessagesService {
|
||||
): ApiResult<ApiResponse<VkChatData>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(MessagesUrls.GET_CONVERSATIONS_MEMBERS)
|
||||
suspend fun getConversationMembers(
|
||||
@POST(MessagesUrls.GET_CONVOS_MEMBERS)
|
||||
suspend fun getConvoMembers(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<MessagesGetConversationMembersResponse>, RestApiError>
|
||||
): ApiResult<ApiResponse<MessagesGetConvoMembersResponse>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(MessagesUrls.REMOVE_CHAT_USER)
|
||||
suspend fun removeChatUser(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<Int>, RestApiError>
|
||||
|
||||
@FormUrlEncoded
|
||||
@POST(MessagesUrls.GET_MESSAGE_READ_PEERS)
|
||||
suspend fun getMessageReadPeers(
|
||||
@FieldMap params: Map<String, String>
|
||||
): ApiResult<ApiResponse<MessagesGetReadPeersResponse>, RestApiError>
|
||||
}
|
||||
|
||||
+2
-1
@@ -18,8 +18,9 @@ object MessagesUrls {
|
||||
const val GET_BY_ID = "$URL/messages.getById"
|
||||
const val MARK_AS_READ = "$URL/messages.markAsRead"
|
||||
const val GET_CHAT = "$URL/messages.getChat"
|
||||
const val GET_CONVERSATIONS_MEMBERS = "$URL/messages.getConversationMembers"
|
||||
const val GET_CONVOS_MEMBERS = "$URL/messages.getConversationMembers"
|
||||
const val REMOVE_CHAT_USER = "$URL/messages.removeChatUser"
|
||||
const val GET_HISTORY_ATTACHMENTS = "$URL/messages.getHistoryAttachments"
|
||||
const val CREATE_CHAT = "$URL/messages.createChat"
|
||||
const val GET_MESSAGE_READ_PEERS = "$URL/messages.getMessageReadPeers"
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
package dev.meloda.fast.ui.common
|
||||
|
||||
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_NIGHT_YES
|
||||
import androidx.compose.ui.tooling.preview.AndroidUiModes.UI_MODE_TYPE_NORMAL
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.Wallpapers.BLUE_DOMINATED_EXAMPLE
|
||||
import androidx.compose.ui.tooling.preview.Wallpapers.GREEN_DOMINATED_EXAMPLE
|
||||
import androidx.compose.ui.tooling.preview.Wallpapers.RED_DOMINATED_EXAMPLE
|
||||
import androidx.compose.ui.tooling.preview.Wallpapers.YELLOW_DOMINATED_EXAMPLE
|
||||
|
||||
@Preview(name = "70%", fontScale = 0.70f)
|
||||
@Preview(name = "85%", fontScale = 0.85f)
|
||||
@Preview(name = "100%", fontScale = 1.0f)
|
||||
@Preview(name = "115%", fontScale = 1.15f)
|
||||
@Preview(name = "130%", fontScale = 1.3f)
|
||||
@Preview(name = "150%", fontScale = 1.5f)
|
||||
@Preview(name = "180%", fontScale = 1.8f)
|
||||
@Preview(name = "200%", fontScale = 2f)
|
||||
|
||||
@Preview(name = "Light")
|
||||
@Preview(name = "Red", wallpaper = RED_DOMINATED_EXAMPLE)
|
||||
@Preview(name = "Blue", wallpaper = BLUE_DOMINATED_EXAMPLE)
|
||||
@Preview(name = "Green", wallpaper = GREEN_DOMINATED_EXAMPLE)
|
||||
@Preview(name = "Yellow", wallpaper = YELLOW_DOMINATED_EXAMPLE)
|
||||
|
||||
@Preview(name = "Dark", uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL)
|
||||
@Preview(
|
||||
name = "Dark Red",
|
||||
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
|
||||
wallpaper = RED_DOMINATED_EXAMPLE
|
||||
)
|
||||
@Preview(
|
||||
name = "Dark Blue",
|
||||
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
|
||||
wallpaper = BLUE_DOMINATED_EXAMPLE
|
||||
)
|
||||
@Preview(
|
||||
name = "Dark Green",
|
||||
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
|
||||
wallpaper = GREEN_DOMINATED_EXAMPLE
|
||||
)
|
||||
@Preview(
|
||||
name = "Dark Yellow",
|
||||
uiMode = UI_MODE_NIGHT_YES or UI_MODE_TYPE_NORMAL,
|
||||
wallpaper = YELLOW_DOMINATED_EXAMPLE
|
||||
)
|
||||
|
||||
annotation class FastPreview
|
||||
@@ -25,7 +25,7 @@ import dev.meloda.fast.ui.R
|
||||
@Composable
|
||||
fun ErrorView(
|
||||
modifier: Modifier = Modifier,
|
||||
iconResId: Int? = R.drawable.round_error_24,
|
||||
iconResId: Int? = R.drawable.ic_error_fill_round_24,
|
||||
text: String,
|
||||
buttonText: String? = null,
|
||||
onButtonClick: (() -> Unit)? = null,
|
||||
|
||||
@@ -2,6 +2,8 @@ package dev.meloda.fast.ui.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.Row
|
||||
@@ -9,19 +11,27 @@ import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.AlertDialogDefaults
|
||||
import androidx.compose.material3.BasicAlertDialog
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.RadioButton
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.contentColorFor
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -30,9 +40,22 @@ import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
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.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.onPlaced
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewDynamicColors
|
||||
import androidx.compose.ui.tooling.preview.PreviewFontScale
|
||||
import androidx.compose.ui.tooling.preview.PreviewLightDark
|
||||
import androidx.compose.ui.tooling.preview.PreviewScreenSizes
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.DialogProperties
|
||||
import dev.meloda.fast.ui.R
|
||||
import dev.meloda.fast.ui.common.FastPreview
|
||||
import dev.meloda.fast.ui.theme.AppTheme
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
|
||||
@@ -41,23 +64,31 @@ import dev.meloda.fast.ui.util.ImmutableList.Companion.toImmutableList
|
||||
fun MaterialDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
confirmText: String? = null,
|
||||
confirmAction: (() -> Unit)? = null,
|
||||
cancelText: String? = null,
|
||||
cancelAction: (() -> Unit)? = null,
|
||||
neutralText: String? = null,
|
||||
neutralAction: (() -> Unit)? = null,
|
||||
icon: ImageVector? = null,
|
||||
iconTint: Color = MaterialTheme.colorScheme.primary,
|
||||
title: String? = null,
|
||||
text: String? = null,
|
||||
selectionType: SelectionType = SelectionType.None,
|
||||
items: ImmutableList<String> = ImmutableList.empty(),
|
||||
preSelectedItems: ImmutableList<Int> = ImmutableList.empty(),
|
||||
onItemClick: ((index: Int) -> Unit)? = null,
|
||||
confirmText: String? = null,
|
||||
confirmAction: (() -> Unit)? = null,
|
||||
confirmContainerColor: Color = MaterialTheme.colorScheme.primary,
|
||||
confirmContentColor: Color = MaterialTheme.colorScheme.contentColorFor(confirmContainerColor),
|
||||
cancelText: String? = null,
|
||||
cancelAction: (() -> Unit)? = null,
|
||||
cancelContainerColor: Color = Color.Transparent,
|
||||
cancelContentColor: Color = MaterialTheme.colorScheme.contentColorFor(cancelContainerColor),
|
||||
neutralText: String? = null,
|
||||
neutralAction: (() -> Unit)? = null,
|
||||
neutralContainerColor: Color = Color.Transparent,
|
||||
neutralContentColor: Color = MaterialTheme.colorScheme.contentColorFor(neutralContainerColor),
|
||||
properties: DialogProperties = DialogProperties(),
|
||||
actionInvokeDismiss: ActionInvokeDismiss = ActionInvokeDismiss.IfNoAction,
|
||||
customContent: (@Composable ColumnScope.() -> Unit)? = null
|
||||
) {
|
||||
var alertItems by remember {
|
||||
var alertItems by remember(items, preSelectedItems) {
|
||||
mutableStateOf(
|
||||
items.mapIndexed { index, title ->
|
||||
DialogItem(
|
||||
@@ -77,6 +108,13 @@ fun MaterialDialog(
|
||||
val scrollState = rememberScrollState()
|
||||
val canScrollBackward by remember { derivedStateOf { scrollState.value > 0 } }
|
||||
val canScrollForward by remember { derivedStateOf { scrollState.value < scrollState.maxValue } }
|
||||
val shouldAddVerticalPadding = remember(
|
||||
icon, title, text, items,
|
||||
confirmText, cancelText, neutralText
|
||||
) {
|
||||
icon != null || title != null || text != null || items.isNotEmpty() ||
|
||||
confirmText != null || cancelText != null || neutralText != null
|
||||
}
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
@@ -84,19 +122,33 @@ fun MaterialDialog(
|
||||
shape = AlertDialogDefaults.shape,
|
||||
tonalElevation = AlertDialogDefaults.TonalElevation
|
||||
) {
|
||||
Column(modifier = Modifier.padding(bottom = 10.dp)) {
|
||||
if (title != null) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = title,
|
||||
style = MaterialTheme.typography.headlineSmall
|
||||
)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
if (shouldAddVerticalPadding) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
tint = iconTint,
|
||||
modifier = Modifier.size(30.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
}
|
||||
|
||||
if (title != null) {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.fillMaxWidth(),
|
||||
text = title,
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
}
|
||||
|
||||
AnimatedVisibility(isPlaced && canScrollBackward) {
|
||||
@@ -110,25 +162,22 @@ fun MaterialDialog(
|
||||
.verticalScroll(scrollState)
|
||||
.onPlaced { isPlaced = true }
|
||||
) {
|
||||
if (text != null && title == null) {
|
||||
Spacer(modifier = Modifier.height(20.dp))
|
||||
}
|
||||
|
||||
if (text != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(24.dp))
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Row(modifier = Modifier.padding(horizontal = 24.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
text = text,
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
}
|
||||
}
|
||||
|
||||
if (text != null || title != null) {
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
}
|
||||
|
||||
if (alertItems.isNotEmpty()) {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
@@ -158,7 +207,7 @@ fun MaterialDialog(
|
||||
alertItems = newItems.toImmutableList()
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.height(10.dp))
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
} else {
|
||||
customContent?.invoke(this)
|
||||
}
|
||||
@@ -168,67 +217,77 @@ fun MaterialDialog(
|
||||
HorizontalDivider()
|
||||
}
|
||||
|
||||
Row {
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
if (neutralText != null) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
neutralAction?.invoke() ?: kotlin.run {
|
||||
if (actionInvokeDismiss == ActionInvokeDismiss.IfNoAction) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
|
||||
if (actionInvokeDismiss == ActionInvokeDismiss.Always) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
if (confirmText != null || cancelText != null || neutralText != null) {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(horizontal = 24.dp)
|
||||
.padding(top = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||
) {
|
||||
Text(text = neutralText)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
||||
if (cancelText != null) {
|
||||
TextButton(
|
||||
onClick = {
|
||||
cancelAction?.invoke() ?: kotlin.run {
|
||||
if (actionInvokeDismiss == ActionInvokeDismiss.IfNoAction) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
|
||||
if (actionInvokeDismiss == ActionInvokeDismiss.Always) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Text(text = cancelText)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
|
||||
if (confirmText != null) {
|
||||
TextButton(
|
||||
Button(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
confirmAction?.invoke() ?: kotlin.run {
|
||||
if (actionInvokeDismiss == ActionInvokeDismiss.IfNoAction) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
val hadAction = confirmAction != null
|
||||
confirmAction?.invoke()
|
||||
|
||||
if (actionInvokeDismiss == ActionInvokeDismiss.Always) {
|
||||
if (actionInvokeDismiss == ActionInvokeDismiss.Always || (actionInvokeDismiss == ActionInvokeDismiss.IfNoAction && !hadAction)) {
|
||||
onDismissRequest()
|
||||
}
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = confirmContainerColor,
|
||||
contentColor = confirmContentColor
|
||||
)
|
||||
) {
|
||||
Text(text = confirmText)
|
||||
}
|
||||
}
|
||||
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
if (cancelText != null) {
|
||||
OutlinedButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
val hadAction = cancelAction != null
|
||||
cancelAction?.invoke()
|
||||
|
||||
if (actionInvokeDismiss == ActionInvokeDismiss.Always || (actionInvokeDismiss == ActionInvokeDismiss.IfNoAction && !hadAction)) {
|
||||
onDismissRequest()
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.outlinedButtonColors(
|
||||
containerColor = cancelContainerColor,
|
||||
contentColor = cancelContentColor
|
||||
)
|
||||
) {
|
||||
Text(text = cancelText)
|
||||
}
|
||||
}
|
||||
|
||||
if (neutralText != null) {
|
||||
TextButton(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
onClick = {
|
||||
val hadAction = neutralAction != null
|
||||
neutralAction?.invoke()
|
||||
|
||||
if (actionInvokeDismiss == ActionInvokeDismiss.Always || (actionInvokeDismiss == ActionInvokeDismiss.IfNoAction && !hadAction)) {
|
||||
onDismissRequest()
|
||||
}
|
||||
},
|
||||
colors = ButtonDefaults.textButtonColors(
|
||||
containerColor = neutralContainerColor,
|
||||
contentColor = neutralContentColor
|
||||
)
|
||||
) {
|
||||
Text(text = neutralText)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldAddVerticalPadding) {
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -253,41 +312,40 @@ fun AlertItems(
|
||||
} else {
|
||||
onItemClick?.invoke(index)
|
||||
}
|
||||
},
|
||||
}
|
||||
.padding(horizontal = 10.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
when (selectionType) {
|
||||
SelectionType.Multi -> {
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
Checkbox(
|
||||
checked = item.isSelected,
|
||||
onCheckedChange = {
|
||||
onItemCheckedChanged?.invoke(index)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
|
||||
SelectionType.Single -> {
|
||||
Spacer(modifier = Modifier.width(10.dp))
|
||||
RadioButton(
|
||||
selected = item.isSelected,
|
||||
onClick = {
|
||||
onItemClick?.invoke(index)
|
||||
}
|
||||
)
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
|
||||
SelectionType.None -> {
|
||||
Spacer(modifier = Modifier.width(26.dp))
|
||||
Spacer(modifier = Modifier.width(16.dp))
|
||||
}
|
||||
}
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = item.title,
|
||||
style = MaterialTheme.typography.bodyLarge
|
||||
)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -308,3 +366,93 @@ sealed class SelectionType {
|
||||
data object Multi : SelectionType()
|
||||
data object None : SelectionType()
|
||||
}
|
||||
|
||||
@FastPreview
|
||||
@Composable
|
||||
private fun MaterialDialogPreview() {
|
||||
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||
MaterialDialog(
|
||||
onDismissRequest = {},
|
||||
title = "Material Dialog",
|
||||
text = "This is a preview of a Material dialog.",
|
||||
confirmText = "Confirm",
|
||||
cancelText = "Cancel",
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_info_round_24)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@FastPreview
|
||||
@Composable
|
||||
private fun MaterialDialogWithListPreview() {
|
||||
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||
MaterialDialog(
|
||||
onDismissRequest = {},
|
||||
title = "Material Dialog",
|
||||
text = "This is a preview of a Material dialog.",
|
||||
confirmText = "Confirm",
|
||||
cancelText = "Cancel",
|
||||
items = listOf("Item 1", "Item 2", "Item 3").toImmutableList(),
|
||||
selectionType = SelectionType.Single,
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_info_round_24)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@FastPreview
|
||||
@Composable
|
||||
private fun MaterialDialogWithCustomContent() {
|
||||
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||
MaterialDialog(
|
||||
onDismissRequest = {},
|
||||
title = "Material Dialog",
|
||||
confirmText = "Confirm",
|
||||
cancelText = "Cancel",
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_info_round_24)
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.weight(1f),
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
label = { Text(text = "Text") },
|
||||
placeholder = { Text(text = "Text") },
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@FastPreview
|
||||
@Composable
|
||||
private fun MaterialDialogWithOnlyCustomContent() {
|
||||
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||
MaterialDialog(onDismissRequest = {}) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 20.dp)
|
||||
) {
|
||||
TextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(10.dp))
|
||||
.weight(1f),
|
||||
value = "",
|
||||
onValueChange = {},
|
||||
label = { Text(text = "Text") },
|
||||
placeholder = { Text(text = "Text") },
|
||||
shape = RoundedCornerShape(10.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package dev.meloda.fast.ui.components
|
||||
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
@@ -8,15 +9,17 @@ 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.Surface
|
||||
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
|
||||
import dev.meloda.fast.ui.common.FastPreview
|
||||
import dev.meloda.fast.ui.theme.AppTheme
|
||||
|
||||
@Composable
|
||||
fun NoItemsView(
|
||||
@@ -49,11 +52,15 @@ fun NoItemsView(
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@FastPreview
|
||||
@Composable
|
||||
private fun NoItemsViewPreview() {
|
||||
AppTheme(useDarkTheme = isSystemInDarkTheme(), useDynamicColors = true) {
|
||||
Surface {
|
||||
NoItemsView(
|
||||
customText = "Nothing here...",
|
||||
buttonText = "Refresh"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,14 @@ package dev.meloda.fast.ui.extensions
|
||||
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.ProvidableCompositionLocal
|
||||
import androidx.compose.ui.Modifier
|
||||
|
||||
@Composable
|
||||
fun <T> ProvidableCompositionLocal<T?>.getOrThrow(): T {
|
||||
return requireNotNull(current)
|
||||
}
|
||||
|
||||
inline fun Modifier.ifTrue(
|
||||
condition: Boolean,
|
||||
block: Modifier.() -> Modifier
|
||||
): Modifier = if (condition) block() else this
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.koin.androidx.compose.koinViewModel
|
||||
import org.koin.core.parameter.ParametersDefinition
|
||||
import org.koin.core.qualifier.Qualifier
|
||||
|
||||
@Suppress("ParamsComparedByRef")
|
||||
@Composable
|
||||
inline fun <reified T : ViewModel> NavBackStackEntry.sharedViewModel(
|
||||
navController: NavController,
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
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)
|
||||
)
|
||||
|
||||
data object Archive : ConversationOption(
|
||||
title = UiText.Resource(R.string.conversation_context_action_archive),
|
||||
icon = UiImage.Resource(R.drawable.outline_archive_24)
|
||||
)
|
||||
|
||||
data object Unarchive : ConversationOption(
|
||||
title = UiText.Resource(R.string.conversation_context_action_unarchive),
|
||||
icon = UiImage.Resource(R.drawable.outline_unarchive_24)
|
||||
)
|
||||
}
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package dev.meloda.fast.ui.model.api
|
||||
package dev.meloda.fast.ui.model.vk
|
||||
|
||||
enum class ActionState {
|
||||
PHANTOM, CALL_IN_PROGRESS, NONE;
|
||||
@@ -0,0 +1,41 @@
|
||||
package dev.meloda.fast.ui.model.vk
|
||||
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.common.model.UiText
|
||||
import dev.meloda.fast.ui.R
|
||||
|
||||
sealed class ConvoOption(
|
||||
val title: UiText,
|
||||
val icon: UiImage
|
||||
) {
|
||||
|
||||
data object MarkAsRead : ConvoOption(
|
||||
title = UiText.Resource(R.string.action_mark_as_read),
|
||||
icon = UiImage.Resource(R.drawable.ic_done_all_round_24)
|
||||
)
|
||||
|
||||
data object Pin : ConvoOption(
|
||||
title = UiText.Resource(R.string.action_pin),
|
||||
icon = UiImage.Resource(R.drawable.ic_keep_round_24)
|
||||
)
|
||||
|
||||
data object Unpin : ConvoOption(
|
||||
title = UiText.Resource(R.string.action_unpin),
|
||||
icon = UiImage.Resource(R.drawable.ic_keep_off_round_24)
|
||||
)
|
||||
|
||||
data object Delete : ConvoOption(
|
||||
title = UiText.Resource(R.string.action_delete),
|
||||
icon = UiImage.Resource(R.drawable.ic_delete_round_24)
|
||||
)
|
||||
|
||||
data object Archive : ConvoOption(
|
||||
title = UiText.Resource(R.string.convo_context_action_archive),
|
||||
icon = UiImage.Resource(R.drawable.ic_archive_round_24)
|
||||
)
|
||||
|
||||
data object Unarchive : ConvoOption(
|
||||
title = UiText.Resource(R.string.convo_context_action_unarchive),
|
||||
icon = UiImage.Resource(R.drawable.ic_unarchive_round_24)
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package dev.meloda.fast.ui.model.vk
|
||||
|
||||
data class MentionIndex(
|
||||
val id: Long,
|
||||
val idPrefix: String,
|
||||
val indexRange: IntRange
|
||||
)
|
||||
+11
-6
@@ -1,14 +1,18 @@
|
||||
package dev.meloda.fast.messageshistory.model
|
||||
package dev.meloda.fast.ui.model.vk
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
import dev.meloda.fast.model.api.domain.VkAttachment
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
|
||||
sealed class UiItem(
|
||||
@Stable
|
||||
sealed class MessageUiItem(
|
||||
open val id: Long,
|
||||
open val cmId: Long
|
||||
) {
|
||||
|
||||
@Stable
|
||||
data class Message(
|
||||
override val id: Long,
|
||||
override val cmId: Long,
|
||||
@@ -29,16 +33,17 @@ sealed class UiItem(
|
||||
val isSelected: Boolean,
|
||||
val isPinned: Boolean,
|
||||
val isImportant: Boolean,
|
||||
val attachments: List<VkAttachment>?,
|
||||
val attachments: ImmutableList<VkAttachment>?,
|
||||
val replyCmId: Long?,
|
||||
val replyTitle: String?,
|
||||
val replySummary: String?
|
||||
) : UiItem(id, cmId)
|
||||
val replySummary: AnnotatedString?
|
||||
) : MessageUiItem(id, cmId)
|
||||
|
||||
@Stable
|
||||
data class ActionMessage(
|
||||
override val id: Long,
|
||||
override val cmId: Long,
|
||||
val text: AnnotatedString,
|
||||
val actionCmId: Long?
|
||||
) : UiItem(id, cmId)
|
||||
) : MessageUiItem(id, cmId)
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
package dev.meloda.fast.ui.model.vk
|
||||
|
||||
enum class SendingStatus {
|
||||
SENDING, SENT, FAILED
|
||||
}
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
package dev.meloda.fast.ui.model.api
|
||||
package dev.meloda.fast.ui.model.vk
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
@@ -8,7 +8,7 @@ import dev.meloda.fast.model.api.domain.VkMessage
|
||||
import dev.meloda.fast.ui.util.ImmutableList
|
||||
|
||||
@Immutable
|
||||
data class UiConversation(
|
||||
data class UiConvo(
|
||||
val id: Long,
|
||||
val lastMessageId: Long?,
|
||||
val avatar: UiImage?,
|
||||
@@ -28,5 +28,5 @@ data class UiConversation(
|
||||
val interactionText: String?,
|
||||
val isExpanded: Boolean,
|
||||
val isArchived: Boolean,
|
||||
val options: ImmutableList<ConversationOption>,
|
||||
val options: ImmutableList<ConvoOption>,
|
||||
)
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
package dev.meloda.fast.ui.model.api
|
||||
package dev.meloda.fast.ui.model.vk
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import dev.meloda.fast.common.model.UiImage
|
||||
@@ -11,6 +11,10 @@ import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.graphics.lerp
|
||||
import androidx.compose.ui.graphics.luminance
|
||||
import androidx.compose.ui.graphics.painter.Painter
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.ui.input.key.onKeyEvent
|
||||
@@ -121,3 +125,14 @@ fun isNeedToEnableDarkMode(darkMode: DarkMode): Boolean {
|
||||
|
||||
return appForceDarkMode || (appBatterySaver && isSystemBatterySaver) || (!appBatterySaver && isSystemUsingDarkTheme && darkMode == DarkMode.FOLLOW_SYSTEM)
|
||||
}
|
||||
|
||||
fun Color.lighten(amount: Float) = lerp(this, Color.White, amount.coerceIn(0f, 1f))
|
||||
fun Color.darken(amount: Float) = lerp(this, Color.Black, amount.coerceIn(0f, 1f))
|
||||
|
||||
fun Color.isDark(
|
||||
background: Color = Color.White,
|
||||
threshold: Float = 0.5f
|
||||
): Boolean {
|
||||
val opaque = if (alpha < 1f) this.compositeOver(background) else this
|
||||
return opaque.luminance() < threshold
|
||||
}
|
||||
|
||||
@@ -9,10 +9,6 @@ class ImmutableList<T>(val values: List<T>) : Collection<T> {
|
||||
|
||||
operator fun get(index: Int): T = values[index]
|
||||
|
||||
inline fun forEach(action: (T) -> Unit) {
|
||||
for (element in values) action(element)
|
||||
}
|
||||
|
||||
inline fun <R> map(transform: (T) -> R): ImmutableList<R> {
|
||||
return values.map(transform).toImmutableList()
|
||||
}
|
||||
@@ -49,13 +45,15 @@ class ImmutableList<T>(val values: List<T>) : Collection<T> {
|
||||
if (elements.isNotEmpty()) copyOf(elements.asList()) else empty()
|
||||
|
||||
fun <T> of(element: T) = ImmutableList(listOf(element))
|
||||
|
||||
fun <T> ImmutableList<T>?.orEmpty(): ImmutableList<T> = this ?: emptyImmutableList()
|
||||
}
|
||||
|
||||
override fun iterator(): Iterator<T> = values.listIterator()
|
||||
|
||||
val lastIndex: Int get() = this.size - 1
|
||||
}
|
||||
|
||||
fun <T> emptyImmutableList(): ImmutableList<T> = ImmutableList(emptyList())
|
||||
|
||||
fun <T> immutableListOf(vararg elements: T) = ImmutableList(listOf(elements = elements))
|
||||
|
||||
fun <T> ImmutableList<T>?.orEmpty(): ImmutableList<T> = this ?: emptyImmutableList()
|
||||
|
||||
@@ -1,11 +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="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,12 +0,0 @@
|
||||
<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" />
|
||||
|
||||
</vector>
|
||||
@@ -1,27 +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="#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="#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="#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="#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,10 +0,0 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="48"
|
||||
android:viewportHeight="48">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M24,0C10.7452,0 0,10.7452 0,24C0,37.2548 10.7452,48 24,48C37.2548,48 48,37.2548 48,24C48,10.7452 37.2548,0 24,0ZM32.25,13.8152C32.25,9.4191 28.7383,6 24.5,6C20.2617,6 16.75,9.4191 16.75,13.8152C16.75,18.0891 20.2617,21.6304 24.5,21.6304C28.7383,21.6304 32.25,18.0891 32.25,13.8152ZM9,34.5743C12.3906,39.5809 18.082,43 24.5,43C30.918,43 36.6094,39.5809 40,34.5743C39.8789,29.3234 29.5859,26.5149 24.5,26.5149C19.293,26.5149 9.1211,29.3234 9,34.5743Z" />
|
||||
</vector>
|
||||
@@ -0,0 +1,9 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M234,684Q285,645 348,622.5Q411,600 480,600Q549,600 612,622.5Q675,645 726,684Q761,643 780.5,591Q800,539 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,539 179.5,591Q199,643 234,684ZM480,520Q421,520 380.5,479.5Q340,439 340,380Q340,321 380.5,280.5Q421,240 480,240Q539,240 579.5,280.5Q620,321 620,380Q620,439 579.5,479.5Q539,520 480,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880Z"/>
|
||||
</vector>
|
||||
@@ -0,0 +1,10 @@
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="960"
|
||||
android:viewportHeight="960"
|
||||
android:tint="?attr/colorControlNormal">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M234,684Q285,645 348,622.5Q411,600 480,600Q549,600 612,622.5Q675,645 726,684Q761,643 780.5,591Q800,539 800,480Q800,347 706.5,253.5Q613,160 480,160Q347,160 253.5,253.5Q160,347 160,480Q160,539 179.5,591Q199,643 234,684ZM480,520Q421,520 380.5,479.5Q340,439 340,380Q340,321 380.5,280.5Q421,240 480,240Q539,240 579.5,280.5Q620,321 620,380Q620,439 579.5,479.5Q539,520 480,520ZM480,880Q397,880 324,848.5Q251,817 197,763Q143,709 111.5,636Q80,563 80,480Q80,397 111.5,324Q143,251 197,197Q251,143 324,111.5Q397,80 480,80Q563,80 636,111.5Q709,143 763,197Q817,251 848.5,324Q880,397 880,480Q880,563 848.5,636Q817,709 763,763Q709,817 636,848.5Q563,880 480,880ZM480,800Q533,800 580,784.5Q627,769 666,740Q627,711 580,695.5Q533,680 480,680Q427,680 380,695.5Q333,711 294,740Q333,769 380,784.5Q427,800 480,800ZM480,440Q506,440 523,423Q540,406 540,380Q540,354 523,337Q506,320 480,320Q454,320 437,337Q420,354 420,380Q420,406 437,423Q454,440 480,440ZM480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380Q480,380 480,380ZM480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Q480,740 480,740Z"/>
|
||||
</vector>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user