1 Commits

Author SHA1 Message Date
melod1n 82fb78e9ea Release 0.2.0 (#150)
Release Notes

* Bumped haze, agp, and guava dependencies
* Implemented ordering functionality for friends list
* Added scroll to top feature in friends and conversations screens
* Improved messages handling
* Fixed coloring issues
* Cache improvements
* Implemented logout functionality
* Implemented new authorization flow (no auto-token re-request)
* Added support for sticker pack preview attachments
* Bump LongPoll to version 19
* Markdown support for messages bubbles
* Adjust app name font size based on screen width

---------

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-04 21:47:05 +03:00
394 changed files with 7030 additions and 11980 deletions
+20 -34
View File
@@ -1,10 +1,10 @@
name: Android CI Build
on:
workflow_dispatch:
permissions:
contents: read
push:
branches: [ "dev", "release/*", "hotfix/*" ]
pull_request:
branches: [ "dev", "release/*", "hotfix/*" ]
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -12,15 +12,15 @@ env:
RELEASE_SIGN_KEY_PASSWORD: ${{ secrets.RELEASE_SIGN_KEY_PASSWORD }}
jobs:
build_apks:
build_apk_aab:
runs-on: ubuntu-24.04
name: Build artifacts
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: set up JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
@@ -29,34 +29,20 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build and sign release APK
run: ./gradlew assembleRelease
- name: Find generated release APK name
id: find_apk_release
run: |
APK_PATH=$(find app/build/outputs/apk/release -name "*.apk" | head -n 1)
echo "APK_PATH=$APK_PATH" >> $GITHUB_ENV
echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV
- name: Upload APK with original name
uses: actions/upload-artifact@v6
with:
name: ${{ env.APK_NAME }}
path: ${{ env.APK_PATH }}
- name: Build and sign debug APK
run: ./gradlew assembleDebug
- name: Find generated debug APK name
id: find_apk_debug
run: |
APK_PATH=$(find app/build/outputs/apk/debug -name "*.apk" | head -n 1)
echo "APK_PATH=$APK_PATH" >> $GITHUB_ENV
echo "APK_NAME=$(basename $APK_PATH)" >> $GITHUB_ENV
- name: Upload APK with original name
uses: actions/upload-artifact@v6
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: ${{ env.APK_NAME }}
path: ${{ env.APK_PATH }}
name: app-debug.apk
path: app/build/outputs/apk/debug/app-debug.apk
- name: Build and sign release APK
run: ./gradlew assembleRelease
- name: Upload release APK
uses: actions/upload-artifact@v4
with:
name: app-release.apk
path: app/build/outputs/apk/release/app-release.apk
+24 -9
View File
@@ -1,11 +1,8 @@
name: Android CI Release
permissions:
contents: read
on:
workflow_dispatch:
push:
branches: [ "release/*"]
pull_request:
branches: [ "master" ]
env:
KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
@@ -18,10 +15,10 @@ jobs:
name: Build artifacts
steps:
- name: Checkout
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: set up JDK 21
uses: actions/setup-java@v5
uses: actions/setup-java@v4
with:
java-version: '21'
distribution: 'temurin'
@@ -30,20 +27,38 @@ jobs:
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Build and sign debug APK
run: ./gradlew assembleDebug
- name: Upload debug APK
uses: actions/upload-artifact@v4
with:
name: app-debug.apk
path: app/build/outputs/apk/debug/app-debug.apk
- name: Build and sign release APK
run: ./gradlew assembleRelease
- name: Upload release APK
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: app-release.apk
path: app/build/outputs/apk/release/app-release.apk
- name: Build and sign debug Bundle
run: ./gradlew bundleDebug
- name: Upload debug Bundle
uses: actions/upload-artifact@v4
with:
name: app-debug.aab
path: app/build/outputs/bundle/debug/app-debug.aab
- name: Build and sign release Bundle
run: ./gradlew bundleRelease
- name: Upload release Bundle
uses: actions/upload-artifact@v6
uses: actions/upload-artifact@v4
with:
name: app-release.aab
path: app/build/outputs/bundle/release/app-release.aab
+12 -21
View File
@@ -7,17 +7,15 @@ Unofficial messenger for russian social network VKontakte
- [x] 2FA support
- [x] Resend otp
- [x] Captcha support
- [x] Support for new authorization with service and refresh tokens
- [ ] Handle token expiration
- [x] Ability to export/import tokens
- [ ] Support for new authorization with service and refresh tokens
- [x] Conversations list
- [x] Pagination
- [x] Manual refresh
- [x] Pin & unpin conversations
- [x] Delete conversations
- [x] Archive
- [x] View archived conversations
- [x] Archive & unarchive conversations
- [ ] Archive
- [ ] View archived conversations
- [ ] Archive & unarchive conversations
- [x] Friends list
- [x] Sort alphabetically, by priority or random
- [x] Separate tab with only friends who are online
@@ -32,24 +30,17 @@ Unofficial messenger for russian social network VKontakte
- [x] Read status
- [x] Edit status
- [x] Sending status
- [x] Message's attachments
- [x] Photo
- [x] Video
- [x] Audio
- [x] File
- [x] Link
- [x] Sticker
- [x] Reply
- [ ] Forwarded messages
- [ ] Wall post
- [ ] Comment in wall post
- [ ] Poll
- [ ] Message's attachments
- [ ] Photo
- [ ] Video
- [ ] Audio
- [ ] File
- [ ] Link
- [ ] TODO
- [x] Send messages
- [x] Pinned message
- [x] Pin & unpin messages
- [x] Reply to message
- [x] Swipe to reply to message
- [ ] Reply to message
- [x] Delete message
- [x] Select multiple messages
- [x] Delete
@@ -64,7 +55,7 @@ Unofficial messenger for russian social network VKontakte
- [x] View attachments
- [x] Open photo
- [x] Internal viewer
- [x] External viewer
- [ ] External viewer
- [ ] Open video in external player
- [ ] TODO
- [ ] Caching
+3 -16
View File
@@ -1,4 +1,3 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import java.util.Properties
plugins {
@@ -13,8 +12,8 @@ android {
defaultConfig {
applicationId = "dev.meloda.fastvk"
versionCode = 10
versionName = "0.2.2"
versionCode = libs.versions.versionCode.get().toInt()
versionName = libs.versions.versionName.get()
}
signingConfigs {
@@ -59,18 +58,6 @@ 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
// }
// }
packaging {
resources {
excludes += "/META-INF/{AL2.0,LGPL2.1}"
@@ -82,7 +69,7 @@ dependencies {
implementation(projects.feature.auth)
implementation(projects.feature.chatmaterials)
implementation(projects.feature.convos)
implementation(projects.feature.conversations)
implementation(projects.feature.languagepicker)
implementation(projects.feature.messageshistory)
implementation(projects.feature.photoviewer)
Binary file not shown.
@@ -4,8 +4,6 @@ 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
@@ -22,8 +20,6 @@ class AppGlobal : Application(), ImageLoaderFactory {
AppSettings.init(preferences)
initKoin()
ComposeStabilityAnalyzer.setEnabled(BuildConfig.DEBUG)
}
private fun initKoin() {
@@ -5,7 +5,6 @@ import android.content.res.Resources
import android.os.PowerManager
import androidx.preference.PreferenceManager
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import dev.meloda.fast.MainViewModelImpl
import dev.meloda.fast.auth.captcha.di.captchaModule
import dev.meloda.fast.auth.login.di.loginModule
@@ -16,8 +15,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.convos.di.convosModule
import dev.meloda.fast.convos.di.createChatModule
import dev.meloda.fast.conversations.di.conversationsModule
import dev.meloda.fast.conversations.di.createChatModule
import dev.meloda.fast.domain.di.domainModule
import dev.meloda.fast.friends.di.friendsModule
import dev.meloda.fast.languagepicker.di.languagePickerModule
@@ -34,14 +33,13 @@ import org.koin.core.qualifier.qualifier
import org.koin.dsl.bind
import org.koin.dsl.module
@OptIn(ExperimentalCoilApi::class)
val applicationModule = module {
includes(domainModule)
includes(
loginModule,
validationModule,
captchaModule,
convosModule,
conversationsModule,
settingsModule,
messagesHistoryModule,
photoViewModule,
@@ -68,7 +66,6 @@ val applicationModule = module {
ImageLoader.Builder(get())
.crossfade(true)
.build()
.also { it.diskCache?.directory?.toFile()?.listFiles() }
}
singleOf(::LongPollControllerImpl) bind LongPollController::class
@@ -2,7 +2,7 @@ package dev.meloda.fast.navigation
import androidx.navigation.NavGraphBuilder
import androidx.navigation.compose.composable
import dev.meloda.fast.convos.navigation.ConvoGraph
import dev.meloda.fast.conversations.navigation.ConversationsGraph
import dev.meloda.fast.friends.navigation.Friends
import dev.meloda.fast.model.BaseError
import dev.meloda.fast.model.BottomNavigationItem
@@ -10,7 +10,7 @@ import dev.meloda.fast.presentation.MainScreen
import dev.meloda.fast.profile.navigation.Profile
import dev.meloda.fast.ui.util.ImmutableList
import kotlinx.serialization.Serializable
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.R as UiR
@Serializable
object MainGraph
@@ -21,28 +21,28 @@ object Main
fun NavGraphBuilder.mainScreen(
onError: (BaseError) -> Unit,
onSettingsButtonClicked: () -> Unit,
onNavigateToMessagesHistory: (convoId: Long) -> Unit,
onNavigateToMessagesHistory: (conversationId: Long) -> Unit,
onPhotoClicked: (url: String) -> Unit,
onMessageClicked: (userid: Long) -> Unit,
onNavigateToCreateChat: () -> Unit
) {
val navigationItems = ImmutableList.of(
BottomNavigationItem(
titleResId = R.string.title_friends,
selectedIconResId = R.drawable.ic_group_fill_round_24,
unselectedIconResId = R.drawable.ic_group_round_24,
titleResId = UiR.string.title_friends,
selectedIconResId = UiR.drawable.baseline_people_alt_24,
unselectedIconResId = UiR.drawable.outline_people_alt_24,
route = Friends,
),
BottomNavigationItem(
titleResId = R.string.title_convos,
selectedIconResId = R.drawable.ic_mail_fill_round_24,
unselectedIconResId = R.drawable.ic_mail_round_24,
route = ConvoGraph
titleResId = UiR.string.title_conversations,
selectedIconResId = UiR.drawable.baseline_chat_24,
unselectedIconResId = UiR.drawable.outline_chat_24,
route = ConversationsGraph
),
BottomNavigationItem(
titleResId = R.string.title_profile,
selectedIconResId = R.drawable.ic_account_circle_fill_round_24,
unselectedIconResId = R.drawable.ic_account_circle_round_24,
titleResId = UiR.string.title_profile,
selectedIconResId = UiR.drawable.baseline_account_circle_24,
unselectedIconResId = UiR.drawable.outline_account_circle_24,
route = Profile
)
)
@@ -4,6 +4,7 @@ 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
@@ -14,20 +15,44 @@ 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.LocalContext
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.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.LocalSizeConfig
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.KoinContext
import org.koin.compose.koinInject
import dev.meloda.fast.ui.R as UiR
class MainActivity : AppCompatActivity() {
@@ -64,33 +89,177 @@ class MainActivity : AppCompatActivity() {
requestNotificationPermissions()
setContent {
KoinContext {
val context = LocalContext.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", "onCreate: viewModel: $viewModel")
}
val currentUser: VkUser? by viewModel.currentUser.collectAsStateWithLifecycle()
LifecycleResumeEffect(true) {
viewModel.onAppResumed(intent)
onPauseOrDispose {}
}
RootScreen(
toggleLongPollService = { enable, inBackground ->
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)
}
toggleLongPollService(
enable = enable,
inBackground = inBackground ?: AppSettings.Experimental.longPollInBackground
enable = true,
inBackground = true
)
},
toggleOnlineService = ::toggleOnlineService
}
}
}
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
)
}
onPauseOrDispose {}
}
val sendOnline by userSettings.sendOnlineStatus.collectAsStateWithLifecycle()
LifecycleResumeEffect(sendOnline) {
toggleOnlineService(sendOnline)
onPauseOrDispose {
toggleOnlineService(false)
}
}
val deviceWidthDp = remember(true) {
context.resources.displayMetrics.widthPixels.pxToDp()
}
val deviceHeightDp = remember(true) {
context.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() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val noCategoryName = getString(R.string.notification_channel_no_category_name)
val noCategoryName = getString(UiR.string.notification_channel_no_category_name)
val noCategoryDescriptionText =
getString(R.string.notification_channel_no_category_description)
getString(UiR.string.notification_channel_no_category_description)
val noCategoryChannel =
NotificationChannel(
AppConstants.NOTIFICATION_CHANNEL_UNCATEGORIZED,
@@ -100,9 +269,9 @@ class MainActivity : AppCompatActivity() {
description = noCategoryDescriptionText
}
val longPollName = getString(R.string.notification_channel_long_polling_service_name)
val longPollName = getString(UiR.string.notification_channel_long_polling_service_name)
val longPollDescriptionText =
getString(R.string.notification_channel_long_polling_service_description)
getString(UiR.string.notification_channel_long_polling_service_description)
val longPollChannel =
NotificationChannel(
AppConstants.NOTIFICATION_CHANNEL_LONG_POLLING,
@@ -113,7 +282,7 @@ class MainActivity : AppCompatActivity() {
}
val notificationManager: NotificationManager =
getSystemService(NOTIFICATION_SERVICE) as NotificationManager
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannels(
listOf(
@@ -1,8 +1,5 @@
package dev.meloda.fast.presentation
import androidx.activity.compose.BackHandler
import androidx.activity.compose.LocalActivity
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
@@ -18,6 +15,7 @@ import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
@@ -38,8 +36,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.convos.navigation.ConvoGraph
import dev.meloda.fast.convos.navigation.convosGraph
import dev.meloda.fast.conversations.navigation.Conversations
import dev.meloda.fast.conversations.navigation.conversationsGraph
import dev.meloda.fast.friends.navigation.Friends
import dev.meloda.fast.friends.navigation.friendsScreen
import dev.meloda.fast.model.BaseError
@@ -60,35 +58,30 @@ fun MainScreen(
navigationItems: ImmutableList<BottomNavigationItem>,
onError: (BaseError) -> Unit = {},
onSettingsButtonClicked: () -> Unit = {},
onNavigateToMessagesHistory: (convoId: Long) -> Unit = {},
onNavigateToMessagesHistory: (conversationId: Long) -> Unit = {},
onPhotoClicked: (url: String) -> Unit = {},
onMessageClicked: (userid: Long) -> Unit = {},
onNavigateToCreateChat: () -> Unit = {}
) {
val activity = LocalActivity.current as? AppCompatActivity ?: return
val theme = LocalThemeConfig.current
val hazeState = remember { HazeState(true) }
val hazeState = remember { HazeState() }
val navController = rememberNavController()
var selectedItemIndex by rememberSaveable {
mutableIntStateOf(1)
}
BackHandler(enabled = selectedItemIndex != 1) {
val currentRoute = navigationItems[selectedItemIndex].route
selectedItemIndex = 1
navController.navigate(navigationItems[selectedItemIndex].route) {
popUpTo(route = currentRoute) {
inclusive = true
val user = LocalUser.current
val profileImageUrl by remember(user) {
derivedStateOf { user?.photo100 }
}
}
}
val profileImageUrl = LocalUser.current?.photo100
var tabReselected by remember {
mutableStateOf(navigationItems.associate { it.route to false })
mutableStateOf(
navigationItems.associate {
it.route to false
}
)
}
Scaffold(
@@ -100,7 +93,7 @@ fun MainScreen(
if (theme.enableBlur) {
Modifier.hazeEffect(
state = hazeState,
style = HazeMaterials.regular(NavigationBarDefaults.containerColor)
style = HazeMaterials.thick()
)
} else Modifier
),
@@ -187,7 +180,6 @@ fun MainScreen(
exitTransition = { fadeOut(animationSpec = tween(200)) }
) {
friendsScreen(
activity = activity,
onError = onError,
onPhotoClicked = onPhotoClicked,
onMessageClicked = onMessageClicked,
@@ -197,19 +189,17 @@ fun MainScreen(
}
},
)
convosGraph(
activity = activity,
conversationsGraph(
onError = onError,
onNavigateToMessagesHistory = onNavigateToMessagesHistory,
onNavigateToCreateChat = onNavigateToCreateChat,
onScrolledToTop = {
tabReselected = tabReselected.toMutableMap().also {
it[ConvoGraph] = false
it[Conversations] = false
}
}
)
profileScreen(
activity = activity,
onError = onError,
onSettingsButtonClicked = onSettingsButtonClicked,
onPhotoClicked = onPhotoClicked
@@ -1,240 +1,52 @@
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
import androidx.compose.animation.fadeOut
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
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.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.conversations.navigation.createChatScreen
import dev.meloda.fast.conversations.navigation.navigateToCreateChat
import dev.meloda.fast.languagepicker.navigation.languagePickerScreen
import dev.meloda.fast.languagepicker.navigation.navigateToLanguagePicker
import dev.meloda.fast.messageshistory.navigation.messagesHistoryScreen
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.photoviewer.navigation.navigateToPhotoView
import dev.meloda.fast.photoviewer.navigation.photoViewScreen
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(
toggleLongPollService: (enable: Boolean, inBackground: Boolean?) -> Unit,
toggleOnlineService: (enable: Boolean) -> Unit
navController: NavHostController = rememberNavController(),
viewModel: MainViewModel
) {
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()
val isNeedToOpenAuth by viewModel.isNeedToReplaceWithAuth.collectAsStateWithLifecycle()
@@ -306,11 +118,6 @@ fun RootScreen(
LocalNavRootController provides navController,
LocalNavController provides navController
) {
var photoViewerInfo by rememberSaveable {
mutableStateOf<Pair<List<String>, Int?>?>(null)
}
Box(modifier = Modifier.fillMaxSize()) {
NavHost(
navController = navController,
startDestination = requireNotNull(startDestination),
@@ -322,7 +129,6 @@ fun RootScreen(
viewModel.onUserAuthenticated()
navController.navigateToMain()
},
onNavigateToSettings = navController::navigateToSettings,
navController = navController
)
@@ -330,9 +136,7 @@ fun RootScreen(
onError = viewModel::onError,
onSettingsButtonClicked = navController::navigateToSettings,
onNavigateToMessagesHistory = navController::navigateToMessagesHistory,
onPhotoClicked = { url ->
photoViewerInfo = listOf(url) to null
},
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) },
onMessageClicked = navController::navigateToMessagesHistory,
onNavigateToCreateChat = navController::navigateToCreateChat
)
@@ -340,21 +144,16 @@ fun RootScreen(
messagesHistoryScreen(
onError = viewModel::onError,
onBack = navController::navigateUp,
onNavigateToChatMaterials = navController::navigateToChatMaterials,
onNavigateToPhotoViewer = { photos, index ->
photoViewerInfo = photos to index
}
onNavigateToChatMaterials = navController::navigateToChatMaterials
)
chatMaterialsScreen(
onBack = navController::navigateUp,
onPhotoClicked = { url ->
photoViewerInfo = listOf(url) to null
}
onPhotoClicked = { url -> navController.navigateToPhotoView(listOf(url)) }
)
createChatScreen(
onChatCreated = { convoId ->
onChatCreated = { conversationId ->
navController.popBackStack()
navController.navigateToMessagesHistory(convoId)
navController.navigateToMessagesHistory(conversationId)
},
navController = navController
)
@@ -362,27 +161,11 @@ fun RootScreen(
settingsScreen(
onBack = navController::navigateUp,
onLogOutButtonClicked = { navController.navigateToAuth(true) },
onLanguageItemClicked = navController::navigateToLanguagePicker,
onRestartRequired = {
activity?.let {
val intent = Intent(activity, MainActivity::class.java)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
activity.startActivity(intent)
activity.finish()
}
}
onLanguageItemClicked = navController::navigateToLanguagePicker
)
languagePickerScreen(onBack = navController::navigateUp)
}
PhotoViewDialog(
photoViewerInfo = photoViewerInfo?.let { info ->
info.first.toImmutableList() to info.second
},
onDismiss = { photoViewerInfo = null }
)
}
}
photoViewScreen(onBack = navController::navigateUp)
}
}
}
@@ -6,7 +6,7 @@ import android.content.Context
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import dev.meloda.fast.common.AppConstants
import dev.meloda.fast.ui.R
import dev.meloda.fast.ui.R as UiR
object NotificationsUtils {
@@ -28,7 +28,7 @@ object NotificationsUtils {
actions: List<NotificationCompat.Action> = emptyList(),
): NotificationCompat.Builder {
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(R.drawable.ic_fast_logo)
.setSmallIcon(UiR.drawable.ic_fast_logo)
.setContentTitle(title)
.setPriority(priority.value)
.setContentIntent(contentIntent)
@@ -1,7 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<network-security-config xmlns:tools="http://schemas.android.com/tools">
<!-- Allow cleartext network traffic -->
<base-config
cleartextTrafficPermitted="false"
cleartextTrafficPermitted="true"
tools:ignore="InsecureBaseConfiguration">
<trust-anchors>
<!-- Trust pre-installed CAs -->
@@ -10,7 +10,6 @@ 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,5 +1,6 @@
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
@@ -9,15 +10,12 @@ 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
compileSdk = 36
minSdk = 23
}
defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
}
}
}
@@ -1,4 +1,4 @@
import com.android.build.api.dsl.LibraryExtension
import com.android.build.gradle.LibraryExtension
import dev.meloda.fast.configureAndroidCompose
import org.gradle.api.Plugin
import org.gradle.api.Project
@@ -10,10 +10,8 @@ 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
configureAndroidCompose(extension)
}
}
@@ -1,7 +1,8 @@
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 dev.meloda.fast.libs
import org.gradle.api.Plugin
import org.gradle.api.Project
import org.gradle.kotlin.dsl.configure
@@ -13,13 +14,14 @@ 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")
}
extensions.configure<LibraryExtension> {
configureKotlinAndroid(this)
androidResources.enable = false
defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
defaultConfig.testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
extensions.configure<LibraryAndroidComponentsExtension> {
@@ -1,5 +1,6 @@
import com.android.build.api.dsl.TestExtension
import com.android.build.gradle.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
@@ -9,11 +10,12 @@ class AndroidTestConventionPlugin : Plugin<Project> {
with(target) {
with(pluginManager) {
apply("com.android.test")
apply("org.jetbrains.kotlin.android")
}
extensions.configure<TestExtension> {
configureKotlinAndroid(this)
defaultConfig.targetSdk = 36
defaultConfig.targetSdk = libs.findVersion("targetSdk").get().toString().toInt()
}
}
}
@@ -5,10 +5,12 @@ 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,9 +1,6 @@
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
@@ -16,25 +13,24 @@ 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 = libs.findVersion("compileSdk").get().toString().toInt()
defaultConfig {
minSdk = libs.findVersion("minSdk").get().toString().toInt()
}
commonExtension.apply {
compileSdk = 36
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
@@ -51,7 +47,7 @@ private inline fun <reified T : KotlinBaseExtension> Project.configureKotlin() =
when (this) {
is KotlinAndroidProjectExtension -> compilerOptions
is KotlinJvmProjectExtension -> compilerOptions
else -> throw IllegalArgumentException("Unsupported project extension $this ${T::class}")
else -> TODO("Unsupported project extension $this ${T::class}")
}.apply {
jvmTarget = JvmTarget.JVM_21
allWarningsAsErrors = warningsAsErrors.toBoolean()
@@ -59,8 +55,7 @@ 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",
"-Xannotation-default-target=param-property",
"-opt-in=kotlinx.coroutines.FlowPreview"
)
}
}
+1 -2
View File
@@ -1,6 +1,7 @@
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
@@ -8,6 +9,4 @@ 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
}
@@ -5,8 +5,8 @@ object AppConstants {
const val INSTALL_APP_MIME_TYPE = "application/vnd.android.package-archive"
const val API_VERSION = "5.238"
const val URL_OAUTH = "https://oauth.vk.ru"
const val URL_API = "https://api.vk.ru/method"
const val URL_OAUTH = "https://oauth.vk.com"
const val URL_API = "https://api.vk.com/method"
const val NOTIFICATION_CHANNEL_UNCATEGORIZED = "uncategorized"
const val NOTIFICATION_CHANNEL_LONG_POLLING = "long_polling"
@@ -1,7 +1,5 @@
package dev.meloda.fast.common.extensions
import android.os.Build
import android.os.Bundle
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
@@ -13,7 +11,6 @@ import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.update
import kotlin.reflect.KClass
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
@@ -26,20 +23,6 @@ fun <T> MutableList<T>.addIf(element: T, condition: () -> Boolean) {
if (condition.invoke()) add(element)
}
fun <T> MutableList<T>.removeIfCompat(condition: (T) -> Boolean): Boolean {
var removed = false
val each = iterator()
while (each.hasNext()) {
if (condition(each.next())) {
each.remove()
removed = true
}
}
return removed
}
fun <T> Flow<T>.listenValue(
coroutineScope: CoroutineScope,
action: suspend (T) -> Unit
@@ -106,7 +89,7 @@ fun Any.asInt(): Int {
}
fun Any.asLong(): Long {
return when (this) {
return when(this) {
is Number -> this.toLong()
else -> throw IllegalArgumentException("Object is not numeric")
@@ -120,33 +103,3 @@ fun <T> Any.toList(mapper: (old: Any) -> T): List<T> {
else -> emptyList()
}
}
fun <T> Bundle.getParcelableCompat(key: String, clazz: Class<T>): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelable(key, clazz)
} else {
@Suppress("DEPRECATION")
getParcelable(key)
}
}
fun <T : Any> Bundle.getParcelableCompat(key: String, clazz: KClass<T>): T? {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
getParcelable(key, clazz.java)
} else {
@Suppress("DEPRECATION")
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,22 +1,19 @@
package dev.meloda.fast.common.util
import android.content.res.Resources
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
import com.conena.nanokt.jvm.util.month
import com.conena.nanokt.jvm.util.second
import com.conena.nanokt.jvm.util.year
import dev.meloda.fast.common.R
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 {
@@ -30,11 +27,7 @@ object TimeUtils {
}.timeInMillis
}
fun getLocalizedDate(
date: Long,
yesterday: () -> String,
today: () -> String
): String {
fun getLocalizedDate(resources: Resources, date: Long): String {
val now = Calendar.getInstance()
val then = Calendar.getInstance().also { it.timeInMillis = date }
@@ -43,41 +36,48 @@ object TimeUtils {
now.month != then.month -> "dd MMMM"
now.dayOfMonth != then.dayOfMonth -> {
if (now.dayOfMonth - then.dayOfMonth == 1) {
return yesterday()
return resources.getString(R.string.yesterday)
} else {
"dd MMMM"
}
}
else -> return today()
else -> return resources.getString(R.string.today)
}
return SimpleDateFormat(pattern, Locale.getDefault()).format(date)
}
fun getLocalizedTime(
date: Long,
yearShort: () -> String,
monthShort: () -> String,
weekShort: () -> String,
dayShort: () -> String,
minuteShort: () -> String,
secondShort: () -> String,
now: () -> String
): String {
val now = Clock.System.now()
val then = Instant.fromEpochMilliseconds(date)
val diff = now - then
fun getLocalizedTime(resources: Resources, date: Long): String {
val now = Calendar.getInstance()
val then = Calendar.getInstance().also { it.timeInMillis = date }
return when {
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()
now.year != then.year -> {
"${now.year - then.year}${resources.getString(R.string.year_short).lowercase()}"
}
now.month != then.month -> {
"${now.month - then.month}${resources.getString(R.string.month_short).lowercase()}"
}
now.dayOfMonth != then.dayOfMonth -> {
val change = now.dayOfMonth - then.dayOfMonth
if (change % 7 == 0) {
"${change / 7}${resources.getString(R.string.week_short).lowercase()}"
} else {
"$change${resources.getString(R.string.day_short).lowercase()}"
}
}
now.hour == then.hour && now.minute == then.minute -> {
resources.getString(R.string.time_now).lowercase()
}
else -> {
SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
}
}
}
}
@@ -1,17 +0,0 @@
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) }
}
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="yesterday">Вчера</string>
<string name="today">Сегодня</string>
<string name="year_short">Г</string>
<string name="month_short">М</string>
<string name="week_short">Н</string>
<string name="day_short">Д</string>
<string name="time_now">Сейчас</string>
</resources>
@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="yesterday">Yesterday</string>
<string name="today">Today</string>
<string name="year_short">Y</string>
<string name="month_short">M</string>
<string name="week_short">W</string>
<string name="day_short">D</string>
<string name="time_now">Now</string>
</resources>
+1
View File
@@ -14,6 +14,7 @@ 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.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
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 convoGroup(convo: VkConvo): VkGroupDomain? =
if (!convo.peerType.isGroup()) null
else map[abs(convo.id)]
fun conversationGroup(conversation: VkConversation): VkGroupDomain? =
if (!conversation.peerType.isGroup()) null
else map[abs(conversation.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.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
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 convos: HashMap<Long, VkConvo> = hashMapOf()
private val conversations: HashMap<Long, VkConversation> = 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 appendConvos(convos: List<VkConvo>) {
convos.forEach { convo ->
VkMemoryCache.convos[convo.id] = convo
fun appendConversations(conversations: List<VkConversation>) {
conversations.forEach { conversation ->
VkMemoryCache.conversations[conversation.id] = conversation
}
}
@@ -50,8 +50,8 @@ object VkMemoryCache {
messages[messageId] = message
}
operator fun set(convoId: Long, convo: VkConvo) {
convos[convoId] = convo
operator fun set(conversationId: Long, conversation: VkConversation) {
conversations[conversationId] = conversation
}
operator fun set(contactId: Long, contact: VkContactDomain) {
@@ -94,16 +94,16 @@ object VkMemoryCache {
return ids.mapNotNull { id -> messages[id] }
}
fun getConvo(id: Long): VkConvo? {
return getConvos(id).firstOrNull()
fun getConversation(id: Long): VkConversation? {
return getConversations(id).firstOrNull()
}
fun getConvos(vararg ids: Long): List<VkConvo> {
return getConvos(ids.toList())
fun getConversations(vararg ids: Long): List<VkConversation> {
return getConversations(ids.toList())
}
fun getConvos(ids: List<Long>): List<VkConvo> {
return ids.mapNotNull { id -> convos[id] }
fun getConversations(ids: List<Long>): List<VkConversation> {
return ids.mapNotNull { id -> conversations[id] }
}
fun getContact(id: Long): VkContactDomain? {
@@ -1,7 +1,8 @@
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.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage
import dev.meloda.fast.model.api.domain.VkUser
@@ -15,9 +16,9 @@ class VkUsersMap(
fun users(): List<VkUser> = map.values.toList()
fun convoUser(convo: VkConvo): VkUser? =
if (!convo.peerType.isUser()) null
else map[convo.id]
fun conversationUser(conversation: VkConversation): VkUser? =
if (!conversation.peerType.isUser()) null
else map[conversation.id]
fun messageActionUser(message: VkMessage): VkUser? =
if (message.actionMemberId == null || message.actionMemberId!! <= 0) null
@@ -35,7 +36,7 @@ class VkUsersMap(
if (message.fromId > 0) map[message.fromId]
else null
fun user(userId: Long): VkUser? = map[userId]
fun user(userid: Long): VkUser? = map[userId]
companion object {
@@ -1,25 +1,25 @@
package dev.meloda.fast.data.api.convos
package dev.meloda.fast.data.api.conversations
import com.slack.eithernet.ApiResult
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.network.RestApiErrorDomain
interface ConvosRepository {
interface ConversationsRepository {
suspend fun storeConvos(convos: List<VkConvo>)
suspend fun storeConversations(conversations: List<VkConversation>)
suspend fun getConvos(
suspend fun getConversations(
count: Int?,
offset: Int?,
filter: ConvosFilter
): ApiResult<List<VkConvo>, RestApiErrorDomain>
filter: ConversationsFilter
): ApiResult<List<VkConversation>, RestApiErrorDomain>
suspend fun getConvosById(
suspend fun getConversationsById(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): ApiResult<List<VkConvo>, RestApiErrorDomain>
): ApiResult<List<VkConversation>, RestApiErrorDomain>
suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain>
suspend fun pin(peerId: Long): ApiResult<Int, RestApiErrorDomain>
@@ -1,51 +1,51 @@
package dev.meloda.fast.data.api.convos
package dev.meloda.fast.data.api.conversations
import com.slack.eithernet.ApiResult
import dev.meloda.fast.common.VkConstants
import dev.meloda.fast.data.VkGroupsMap
import dev.meloda.fast.data.VkMemoryCache
import dev.meloda.fast.data.VkUsersMap
import dev.meloda.fast.database.dao.ConvoDao
import dev.meloda.fast.database.dao.ConversationDao
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.ConvosFilter
import dev.meloda.fast.model.ConversationsFilter
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.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
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.ConvosGetRequest
import dev.meloda.fast.model.api.requests.ConversationsGetRequest
import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiDefault
import dev.meloda.fast.network.mapApiResult
import dev.meloda.fast.network.service.convos.ConvosService
import dev.meloda.fast.network.service.conversations.ConversationsService
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class ConvosRepositoryImpl(
private val convosService: ConvosService,
class ConversationsRepositoryImpl(
private val conversationsService: ConversationsService,
private val messageDao: MessageDao,
private val userDao: UserDao,
private val groupDao: GroupDao,
private val convoDao: ConvoDao
) : ConvosRepository {
private val conversationDao: ConversationDao
) : ConversationsRepository {
override suspend fun storeConvos(convos: List<VkConvo>) {
convoDao.insertAll(convos.map(VkConvo::asEntity))
override suspend fun storeConversations(conversations: List<VkConversation>) {
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
}
override suspend fun getConvos(
override suspend fun getConversations(
count: Int?,
offset: Int?,
filter: ConvosFilter
): ApiResult<List<VkConvo>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConvosGetRequest(
filter: ConversationsFilter
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = ConversationsGetRequest(
count = count,
offset = offset,
fields = VkConstants.ALL_FIELDS,
@@ -54,7 +54,7 @@ class ConvosRepositoryImpl(
startMessageId = null
)
convosService.getConvos(requestModel.map).mapApiResult(
conversationsService.getConversations(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
@@ -69,39 +69,33 @@ class ConvosRepositoryImpl(
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
val convos = response.items.map { item ->
val conversations = response.items.map { item ->
val lastMessage = item.lastMessage?.asDomain()?.let { message ->
message.copy(
user = usersMap.messageUser(message),
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),
)
actionGroup = groupsMap.messageActionGroup(message)
).also { VkMemoryCache[message.id] = it }
}
item.convo.asDomain(lastMessage).let { convo ->
convo.copy(
user = usersMap.convoUser(convo),
group = groupsMap.convoGroup(convo)
).also { VkMemoryCache[convo.id] = it }
item.conversation.asDomain(lastMessage).let { conversation ->
conversation.copy(
user = usersMap.conversationUser(conversation),
group = groupsMap.conversationGroup(conversation)
).also { VkMemoryCache[conversation.id] = it }
}
}
val messages = convos.mapNotNull(VkConvo::lastMessage)
val messages = conversations.mapNotNull(VkConversation::lastMessage)
launch(Dispatchers.IO) {
convoDao.insertAll(convos.map(VkConvo::asEntity))
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
convos
conversations
},
errorMapper = { error ->
error?.toDomain()
@@ -109,11 +103,11 @@ class ConvosRepositoryImpl(
)
}
override suspend fun getConvosById(
override suspend fun getConversationsById(
peerIds: List<Long>,
extended: Boolean?,
fields: String?
): ApiResult<List<VkConvo>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
): ApiResult<List<VkConversation>, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestParams = mutableMapOf(
"peer_ids" to peerIds.joinToString(separator = ",")
).apply {
@@ -121,7 +115,7 @@ class ConvosRepositoryImpl(
fields?.let { this["fields"] = it }
}
convosService.getConvosById(requestParams).mapApiResult(
conversationsService.getConversationsById(requestParams).mapApiResult(
successMapper = { apiResponse ->
val response = apiResponse.requireResponse()
@@ -132,17 +126,17 @@ class ConvosRepositoryImpl(
val usersMap = VkUsersMap.forUsers(profilesList)
val groupsMap = VkGroupsMap.forGroups(groupsList)
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 }
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 }
}
}
launch(Dispatchers.IO) {
convoDao.insertAll(convos.map(VkConvo::asEntity))
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
}
@@ -151,7 +145,7 @@ class ConvosRepositoryImpl(
VkMemoryCache.appendGroups(groupsList)
VkMemoryCache.appendContacts(contactsList)
convos
conversations
},
errorMapper = { error ->
error?.toDomain()
@@ -161,7 +155,7 @@ class ConvosRepositoryImpl(
override suspend fun delete(peerId: Long): ApiResult<Long, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
convosService.delete(mapOf("peer_id" to peerId.toString())).mapApiResult(
conversationsService.delete(mapOf("peer_id" to peerId.toString())).mapApiResult(
successMapper = { response -> response.requireResponse().lastDeletedId },
errorMapper = { error -> error?.toDomain() }
)
@@ -170,19 +164,19 @@ class ConvosRepositoryImpl(
override suspend fun pin(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
convosService.pin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
conversationsService.pin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
}
override suspend fun unpin(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
convosService.unpin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
conversationsService.unpin(mapOf("peer_id" to peerId.toString())).mapApiDefault()
}
override suspend fun reorderPinned(
peerIds: List<Long>
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
convosService
conversationsService
.reorderPinned(mapOf("peer_ids" to peerIds.joinToString(",")))
.mapApiDefault()
}
@@ -190,12 +184,12 @@ class ConvosRepositoryImpl(
override suspend fun archive(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
convosService.archive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
conversationsService.archive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
}
override suspend fun unarchive(
peerId: Long
): ApiResult<Int, RestApiErrorDomain> = withContext(Dispatchers.IO) {
convosService.unarchive(mapOf("peer_id" to peerId.toString())).mapApiDefault()
conversationsService.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.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage
data class MessagesHistoryInfo(
val messages: List<VkMessage>,
val convos: List<VkConvo>
val conversations: List<VkConversation>
)
@@ -5,8 +5,7 @@ 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.MessagesGetConvoMembersResponse
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import dev.meloda.fast.network.RestApiErrorDomain
@@ -15,7 +14,7 @@ interface MessagesRepository {
suspend fun storeMessages(messages: List<VkMessage>)
suspend fun getHistory(
convoId: Long,
conversationId: Long,
offset: Int?,
count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain>
@@ -33,9 +32,8 @@ interface MessagesRepository {
peerId: Long,
randomId: Long,
message: String?,
forward: String?,
attachments: List<VkAttachment>?,
formatData: VkMessage.FormatData?
replyTo: Long?,
attachments: List<VkAttachment>?
): ApiResult<MessagesSendResponse, RestApiErrorDomain>
suspend fun markAsRead(
@@ -99,21 +97,16 @@ interface MessagesRepository {
fields: String? = null
): ApiResult<VkChatData, RestApiErrorDomain>
suspend fun getConvoMembers(
suspend fun getConversationMembers(
peerId: Long,
offset: Int? = null,
count: Int? = null,
extended: Boolean? = null,
fields: String? = null
): ApiResult<MessagesGetConvoMembersResponse, RestApiErrorDomain>
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain>
suspend fun removeChatUser(
chatId: Long,
memberId: Long
): ApiResult<Int, RestApiErrorDomain>
suspend fun getMessageReadPeers(
peerId: Long,
cmId: Long
): ApiResult<MessagesGetReadPeersResponse, RestApiErrorDomain>
}
@@ -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.ConvoDao
import dev.meloda.fast.database.dao.ConversationDao
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.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
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.MessagesGetConvoMembersRequest
import dev.meloda.fast.model.api.requests.MessagesGetConversationMembersRequest
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,8 +36,7 @@ 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.MessagesGetConvoMembersResponse
import dev.meloda.fast.model.api.responses.MessagesGetReadPeersResponse
import dev.meloda.fast.model.api.responses.MessagesGetConversationMembersResponse
import dev.meloda.fast.model.api.responses.MessagesSendResponse
import dev.meloda.fast.network.RestApiErrorDomain
import dev.meloda.fast.network.mapApiDefault
@@ -52,18 +51,18 @@ class MessagesRepositoryImpl(
private val messageDao: MessageDao,
private val userDao: UserDao,
private val groupDao: GroupDao,
private val convoDao: ConvoDao
private val conversationDao: ConversationDao
) : MessagesRepository {
override suspend fun getHistory(
convoId: Long,
conversationId: Long,
offset: Int?,
count: Int?
): ApiResult<MessagesHistoryInfo, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesGetHistoryRequest(
count = count,
offset = offset,
peerId = convoId,
peerId = conversationId,
extended = true,
startMessageId = null,
rev = null,
@@ -91,32 +90,24 @@ class MessagesRepositoryImpl(
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),
)
}
actionGroup = groupsMap.messageActionGroup(message)
).also { VkMemoryCache[message.id] = it }
}
}
val convos = response.convos.orEmpty().map { item ->
val conversations = response.conversations.orEmpty().map { item ->
val message = messages.firstOrNull { it.id == item.lastMessageId }
item.asDomain(message)
.let { convo ->
convo.copy(
user = usersMap.convoUser(convo),
group = groupsMap.convoGroup(convo)
).also { VkMemoryCache[convo.id] = it }
.let { conversation ->
conversation.copy(
user = usersMap.conversationUser(conversation),
group = groupsMap.conversationGroup(conversation)
).also { VkMemoryCache[conversation.id] = it }
}
}
launch(Dispatchers.IO) {
convoDao.insertAll(convos.map(VkConvo::asEntity))
conversationDao.insertAll(conversations.map(VkConversation::asEntity))
messageDao.insertAll(messages.map(VkMessage::asEntity))
userDao.insertAll(profilesList.map(VkUser::asEntity))
groupDao.insertAll(groupsList.map(VkGroupDomain::asEntity))
@@ -124,7 +115,7 @@ class MessagesRepositoryImpl(
MessagesHistoryInfo(
messages = messages,
convos = convos
conversations = conversations
)
},
errorMapper = { error ->
@@ -168,15 +159,7 @@ class MessagesRepositoryImpl(
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),
)
}
actionGroup = groupsMap.messageActionGroup(message)
)
}
@@ -200,17 +183,15 @@ class MessagesRepositoryImpl(
peerId: Long,
randomId: Long,
message: String?,
forward: String?,
attachments: List<VkAttachment>?,
formatData: VkMessage.FormatData?
replyTo: Long?,
attachments: List<VkAttachment>?
): ApiResult<MessagesSendResponse, RestApiErrorDomain> = withContext(Dispatchers.IO) {
val requestModel = MessagesSendRequest(
peerId = peerId,
randomId = randomId,
message = message,
forward = forward,
attachments = attachments,
formatData = formatData
replyTo = replyTo,
attachments = attachments
)
messagesService.send(requestModel.map).mapApiDefault()
@@ -243,7 +224,7 @@ class MessagesRepositoryImpl(
offset = offset,
preserveOrder = true,
attachmentTypes = attachmentTypes,
cmId = cmId,
conversationMessageId = cmId,
fields = VkConstants.ALL_FIELDS
)
@@ -297,7 +278,7 @@ class MessagesRepositoryImpl(
val requestModel = MessagesPinMessageRequest(
peerId = peerId,
messageId = messageId,
cmId = cmId
conversationMessageId = cmId
)
messagesService.pin(requestModel.map).mapApiResult(
@@ -325,12 +306,7 @@ class MessagesRepositoryImpl(
messagesIds = messageIds.orEmpty(),
important = important
)
messagesService.markAsImportant(requestModel.map).mapApiResult(
successMapper = { apiResponse ->
apiResponse.requireResponse().marked.map { it.cmId }
},
errorMapper = { error -> error?.toDomain() }
)
messagesService.markAsImportant(requestModel.map).mapApiDefault()
}
override suspend fun delete(
@@ -343,7 +319,7 @@ class MessagesRepositoryImpl(
val requestModel = MessagesDeleteRequest(
peerId = peerId,
messagesIds = messageIds,
cmIds = cmIds,
conversationsMessagesIds = cmIds,
isSpam = spam,
deleteForAll = deleteForAll
)
@@ -394,15 +370,15 @@ class MessagesRepositoryImpl(
messagesService.getChat(requestModel.map).mapApiDefault()
}
override suspend fun getConvoMembers(
override suspend fun getConversationMembers(
peerId: Long,
offset: Int?,
count: Int?,
extended: Boolean?,
fields: String?
): ApiResult<MessagesGetConvoMembersResponse, RestApiErrorDomain> =
): ApiResult<MessagesGetConversationMembersResponse, RestApiErrorDomain> =
withContext(Dispatchers.IO) {
val requestModel = MessagesGetConvoMembersRequest(
val requestModel = MessagesGetConversationMembersRequest(
peerId = peerId,
offset = offset,
count = count,
@@ -410,7 +386,7 @@ class MessagesRepositoryImpl(
fields = fields
)
messagesService.getConvoMembers(requestModel.map).mapApiDefault()
messagesService.getConversationMembers(requestModel.map).mapApiDefault()
}
override suspend fun removeChatUser(
@@ -424,18 +400,4 @@ 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.convos.ConvosRepository
import dev.meloda.fast.data.api.convos.ConvosRepositoryImpl
import dev.meloda.fast.data.api.conversations.ConversationsRepository
import dev.meloda.fast.data.api.conversations.ConversationsRepositoryImpl
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(::ConvosRepositoryImpl) bind ConvosRepository::class
singleOf(::ConversationsRepositoryImpl) bind ConversationsRepository::class
singleOf(::FilesRepository)
@@ -1,58 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 3,
"identityHash": "ca007bca2ab4a9b901662792042770ad",
"entities": [
{
"tableName": "accounts",
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`userId` INTEGER NOT NULL, `accessToken` TEXT NOT NULL, `fastToken` TEXT, `trustedHash` TEXT, `exchangeToken` TEXT, PRIMARY KEY(`userId`))",
"fields": [
{
"fieldPath": "userId",
"columnName": "userId",
"affinity": "INTEGER",
"notNull": true
},
{
"fieldPath": "accessToken",
"columnName": "accessToken",
"affinity": "TEXT",
"notNull": true
},
{
"fieldPath": "fastToken",
"columnName": "fastToken",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "trustedHash",
"columnName": "trustedHash",
"affinity": "TEXT",
"notNull": false
},
{
"fieldPath": "exchangeToken",
"columnName": "exchangeToken",
"affinity": "TEXT",
"notNull": false
}
],
"primaryKey": {
"autoGenerate": false,
"columnNames": [
"userId"
]
},
"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, 'ca007bca2ab4a9b901662792042770ad')"
]
}
}
@@ -1,413 +0,0 @@
{
"formatVersion": 1,
"database": {
"version": 10,
"identityHash": "6c315b7f800694f635318d86032746ec",
"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": "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, `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, '6c315b7f800694f635318d86032746ec')"
]
}
}
@@ -1,413 +0,0 @@
{
"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.ConvoDao
import dev.meloda.fast.database.dao.ConversationDao
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.VkConvoEntity
import dev.meloda.fast.model.database.VkConversationEntity
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,
VkConvoEntity::class
VkConversationEntity::class
],
version = 11
version = 10
)
@TypeConverters(Converters::class)
abstract class CacheDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
abstract fun groupDao(): GroupDao
abstract fun messageDao(): MessageDao
abstract fun convoDao(): ConvoDao
abstract fun conversationDao(): ConversationDao
}
@@ -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.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
}
@@ -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.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 (:convoId)")
abstract suspend fun getAll(convoId: Long): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE peerId IS (:conversationId)")
abstract suspend fun getAll(conversationId: Long): List<VkMessageEntity>
@Query("SELECT * FROM messages WHERE id IN (:ids)")
abstract suspend fun getAllByIds(ids: List<Int>): List<VkMessageEntity>
@@ -17,13 +17,13 @@ val databaseModule = module {
single {
Room.databaseBuilder(get(), CacheDatabase::class.java, "cache")
.fallbackToDestructiveMigration(true)
.fallbackToDestructiveMigration()
.build()
}
single { cacheDB().userDao() }
single { cacheDB().groupDao() }
single { cacheDB().messageDao() }
single { cacheDB().convoDao() }
single { cacheDB().conversationDao() }
}
private fun Scope.cacheDB(): CacheDatabase = get()
@@ -96,20 +96,6 @@ object AppSettings {
)
set(value) = put(SettingsKeys.KEY_SHOW_EMOJI_BUTTON, value)
var showAttachmentButton: Boolean
get() = get(
SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON,
SettingsKeys.DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON
)
set(value) = put(SettingsKeys.KEY_SHOW_ATTACHMENT_BUTTON, value)
var showManualRefreshOptions: Boolean
get() = get(
SettingsKeys.KEY_SHOW_MANUAL_REFRESH_OPTIONS,
SettingsKeys.DEFAULT_VALUE_SHOW_MANUAL_REFRESH_OPTIONS
)
set(value) = put(SettingsKeys.KEY_SHOW_MANUAL_REFRESH_OPTIONS, value)
var enableHaptic: Boolean
get() = get(
SettingsKeys.KEY_ENABLE_HAPTIC,
@@ -11,10 +11,6 @@ object SettingsKeys {
const val DEFAULT_VALUE_USE_CONTACT_NAMES = false
const val KEY_SHOW_EMOJI_BUTTON = "show_emoji_button"
const val DEFAULT_VALUE_KEY_SHOW_EMOJI_BUTTON = false
const val KEY_SHOW_ATTACHMENT_BUTTON = "show_attachment_button"
const val DEFAULT_VALUE_SHOW_ATTACHMENT_BUTTON = false
const val KEY_SHOW_MANUAL_REFRESH_OPTIONS = "show_manual_refresh_options"
const val DEFAULT_VALUE_SHOW_MANUAL_REFRESH_OPTIONS = false
const val KEY_APPEARANCE = "appearance"
const val KEY_APPEARANCE_MULTILINE = "appearance_multiline"
@@ -50,9 +46,6 @@ object SettingsKeys {
const val DEFAULT_ENABLE_HAPTIC = true
const val KEY_DEBUG_NETWORK_LOG_LEVEL = "debug_network_log_level"
const val DEFAULT_NETWORK_LOG_LEVEL = 3
const val KEY_DEBUG_IMPORT_AUTH_DATA = "debug_import_auth_data"
const val KEY_DEBUG_EXPORT_AUTH_DATA = "debug_export_auth_data"
const val KEY_USE_SYSTEM_FONT = "use_system_font"
const val DEFAULT_USE_SYSTEM_FONT = false
const val KEY_MORE_ANIMATIONS = "more_animations"
@@ -14,10 +14,15 @@ interface UserSettings {
val enableDynamicColors: StateFlow<Boolean>
val appLanguage: StateFlow<String>
val fastText: StateFlow<String>
val sendOnlineStatus: StateFlow<Boolean>
val showAlertAfterCrash: StateFlow<Boolean>
val longPollInBackground: StateFlow<Boolean>
val useBlur: StateFlow<Boolean>
val showEmojiButton: StateFlow<Boolean>
val showTimeInActionMessages: StateFlow<Boolean>
val useSystemFont: StateFlow<Boolean>
val enableAnimations: StateFlow<Boolean>
val showDebugCategory: StateFlow<Boolean>
@@ -30,10 +35,15 @@ interface UserSettings {
fun onEnableDynamicColorsChanged(enable: Boolean)
fun onAppLanguageChanged(language: String)
fun onFastTextChanged(text: String)
fun onSendOnlineStatusChanged(send: Boolean)
fun onShowAlertAfterCrashChanged(show: Boolean)
fun onLongPollInBackgroundChanged(inBackground: Boolean)
fun onUseBlurChanged(use: Boolean)
fun onShowEmojiButtonChanged(show: Boolean)
fun onShowTimeInActionMessagesChanged(show: Boolean)
fun onUseSystemFontChanged(use: Boolean)
fun onShowDebugCategoryChanged(show: Boolean)
}
@@ -48,11 +58,16 @@ class UserSettingsImpl : UserSettings {
override val enableDynamicColors = MutableStateFlow(AppSettings.Appearance.enableDynamicColors)
override val appLanguage = MutableStateFlow(AppSettings.Appearance.appLanguage)
override val fastText = MutableStateFlow(AppSettings.Features.fastText)
override val sendOnlineStatus = MutableStateFlow(AppSettings.Activity.sendOnlineStatus)
override val longPollInBackground =
MutableStateFlow(AppSettings.Experimental.longPollInBackground)
override val showAlertAfterCrash = MutableStateFlow(AppSettings.Debug.showAlertAfterCrash)
override val longPollInBackground = MutableStateFlow(AppSettings.Experimental.longPollInBackground)
override val useBlur = MutableStateFlow(AppSettings.Experimental.useBlur)
override val showEmojiButton = MutableStateFlow(AppSettings.General.showEmojiButton)
override val showTimeInActionMessages =
MutableStateFlow(AppSettings.Experimental.showTimeInActionMessages)
override val useSystemFont = MutableStateFlow(AppSettings.Appearance.useSystemFont)
override val enableAnimations = MutableStateFlow(AppSettings.Experimental.moreAnimations)
override val showDebugCategory = MutableStateFlow(AppSettings.Debug.showDebugCategory)
@@ -81,10 +96,18 @@ class UserSettingsImpl : UserSettings {
appLanguage.value = language
}
override fun onFastTextChanged(text: String) {
fastText.value = text
}
override fun onSendOnlineStatusChanged(send: Boolean) {
sendOnlineStatus.value = send
}
override fun onShowAlertAfterCrashChanged(show: Boolean) {
showAlertAfterCrash.value = show
}
override fun onLongPollInBackgroundChanged(inBackground: Boolean) {
longPollInBackground.value = inBackground
}
@@ -93,6 +116,14 @@ class UserSettingsImpl : UserSettings {
useBlur.value = use
}
override fun onShowEmojiButtonChanged(show: Boolean) {
showEmojiButton.value = show
}
override fun onShowTimeInActionMessagesChanged(show: Boolean) {
showTimeInActionMessages.value = show
}
override fun onUseSystemFontChanged(use: Boolean) {
useSystemFont.value = use
}
+1 -6
View File
@@ -8,16 +8,11 @@ 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)
}
@@ -1,25 +1,25 @@
package dev.meloda.fast.domain
import dev.meloda.fast.data.State
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.flow.Flow
interface ConvoUseCase : BaseUseCase {
interface ConversationsUseCase : BaseUseCase {
suspend fun storeConvos(convos: List<VkConvo>)
suspend fun storeConversations(conversations: List<VkConversation>)
fun getConvos(
fun getConversations(
count: Int? = null,
offset: Int? = null,
filter: ConvosFilter
): Flow<State<List<VkConvo>>>
filter: ConversationsFilter
): Flow<State<List<VkConversation>>>
fun getById(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): Flow<State<List<VkConvo>>>
): Flow<State<List<VkConversation>>>
fun delete(peerId: Long): Flow<State<Long>>
@@ -1,30 +1,30 @@
package dev.meloda.fast.domain
import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.convos.ConvosRepository
import dev.meloda.fast.data.api.conversations.ConversationsRepository
import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.ConversationsFilter
import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.withContext
class ConvoUseCaseImpl(
private val repository: ConvosRepository,
) : ConvoUseCase {
class ConversationsUseCaseImpl(
private val repository: ConversationsRepository,
) : ConversationsUseCase {
override suspend fun storeConvos(
convos: List<VkConvo>
override suspend fun storeConversations(
conversations: List<VkConversation>
) = withContext(Dispatchers.IO) {
repository.storeConvos(convos)
repository.storeConversations(conversations)
}
override fun getConvos(
override fun getConversations(
count: Int?,
offset: Int?,
filter: ConvosFilter
): Flow<State<List<VkConvo>>> = flowNewState {
repository.getConvos(
filter: ConversationsFilter
): Flow<State<List<VkConversation>>> = flowNewState {
repository.getConversations(
count = count,
offset = offset,
filter = filter
@@ -35,8 +35,8 @@ class ConvoUseCaseImpl(
peerIds: List<Long>,
extended: Boolean?,
fields: String?
): Flow<State<List<VkConvo>>> = flowNewState {
repository.getConvosById(
): Flow<State<List<VkConversation>>> = flowNewState {
repository.getConversationsById(
peerIds = peerIds,
extended = extended,
fields = fields
@@ -6,7 +6,10 @@ 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)
}
@@ -1,21 +0,0 @@
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 })
}
}
@@ -1,22 +1,22 @@
package dev.meloda.fast.domain
import dev.meloda.fast.data.State
import dev.meloda.fast.data.api.convos.ConvosRepository
import dev.meloda.fast.data.api.conversations.ConversationsRepository
import dev.meloda.fast.data.mapToState
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
import kotlinx.coroutines.flow.Flow
class LoadConvosByIdUseCase(
private val convosRepository: ConvosRepository
class LoadConversationsByIdUseCase(
private val conversationsRepository: ConversationsRepository
) : BaseUseCase {
operator fun invoke(
peerIds: List<Long>,
extended: Boolean? = null,
fields: String? = null
): Flow<State<List<VkConvo>>> = flowNewState {
convosRepository
.getConvosById(
): Flow<State<List<VkConversation>>> = flowNewState {
conversationsRepository
.getConversationsById(
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.ConvoFlags
import dev.meloda.fast.model.ConversationFlags
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.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
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 convoUseCase: ConvoUseCase,
private val conversationsUseCase: ConversationsUseCase,
private val messagesUseCase: MessagesUseCase
) {
private val job = SupervisorJob()
@@ -271,9 +271,9 @@ class LongPollUpdatesParser(
val message =
async { loadMessage(peerId = peerId, cmId = cmId) }.await()
val convo =
val conversation =
async {
loadConvo(
loadConversation(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
@@ -287,7 +287,7 @@ class LongPollUpdatesParser(
.onEvent(
LongPollParsedEvent.NewMessage(
message = message,
inArchive = convo?.isArchived == true
inArchive = conversation?.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 = ConvoFlags.parse(flags)
val parsedFlags = ConversationFlags.parse(flags)
coroutineScope.launch(Dispatchers.IO) {
parsedFlags.forEach { flag ->
when (flag) {
ConvoFlags.ARCHIVED -> {
val convo = loadConvo(
ConversationFlags.ARCHIVED -> {
val conversation = loadConversation(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
@@ -382,11 +382,11 @@ class LongPollUpdatesParser(
val message = loadMessage(
peerId = peerId,
cmId = convo.lastCmId
cmId = conversation.lastCmId
)
val eventToSend = LongPollParsedEvent.ChatArchived(
convo = convo.copy(lastMessage = message),
conversation = conversation.copy(lastMessage = message),
archived = false
)
eventsToSend += eventToSend
@@ -423,13 +423,13 @@ class LongPollUpdatesParser(
val eventsToSend = mutableListOf<LongPollParsedEvent>()
val parsedFlags = ConvoFlags.parse(flags)
val parsedFlags = ConversationFlags.parse(flags)
coroutineScope.launch(Dispatchers.IO) {
parsedFlags.forEach { flag ->
when (flag) {
ConvoFlags.ARCHIVED -> {
val convo = loadConvo(
ConversationFlags.ARCHIVED -> {
val conversation = loadConversation(
peerId = peerId,
extended = true,
fields = VkConstants.ALL_FIELDS
@@ -437,11 +437,11 @@ class LongPollUpdatesParser(
val message = loadMessage(
peerId = peerId,
cmId = convo.lastCmId
cmId = conversation.lastCmId
)
val eventToSend = LongPollParsedEvent.ChatArchived(
convo = convo.copy(lastMessage = message),
conversation = conversation.copy(lastMessage = message),
archived = true
)
eventsToSend += eventToSend
@@ -673,29 +673,29 @@ class LongPollUpdatesParser(
}
}
private suspend fun loadConvo(
private suspend fun loadConversation(
peerId: Long,
extended: Boolean = false,
fields: String? = null
): VkConvo? = suspendCoroutine { continuation ->
): VkConversation? = suspendCoroutine { continuation ->
coroutineScope.launch(Dispatchers.IO) {
convoUseCase.getById(
conversationsUseCase.getById(
peerIds = listOf(peerId),
extended = extended,
fields = fields
).listenValue(coroutineScope) { state ->
state.processState(
error = { error ->
Log.e("LongPollUpdatesParser", "loadConvo: error: $error")
Log.e("LongPollUpdatesParser", "loadConversation: error: $error")
continuation.resume(null)
},
success = { response ->
val convo = response.singleOrNull() ?: run {
val conversation = response.singleOrNull() ?: run {
continuation.resume(null)
return@listenValue
}
continuation.resume(convo)
continuation.resume(conversation)
}
)
}
@@ -14,7 +14,7 @@ interface MessagesUseCase : BaseUseCase {
suspend fun storeMessages(messages: List<VkMessage>)
fun getMessagesHistory(
convoId: Long,
conversationId: Long,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryInfo>>
@@ -32,9 +32,8 @@ interface MessagesUseCase : BaseUseCase {
peerId: Long,
randomId: Long,
message: String?,
forward: String?,
attachments: List<VkAttachment>?,
formatData: VkMessage.FormatData?
replyTo: Long?,
attachments: List<VkAttachment>?
): Flow<State<MessagesSendResponse>>
fun markAsRead(
@@ -23,12 +23,12 @@ class MessagesUseCaseImpl(
}
override fun getMessagesHistory(
convoId: Long,
conversationId: Long,
count: Int?,
offset: Int?
): Flow<State<MessagesHistoryInfo>> = flowNewState {
repository.getHistory(
convoId = convoId,
conversationId = conversationId,
offset = offset,
count = count
).mapToState()
@@ -56,17 +56,15 @@ class MessagesUseCaseImpl(
peerId: Long,
randomId: Long,
message: String?,
forward: String?,
attachments: List<VkAttachment>?,
formatData: VkMessage.FormatData?
replyTo: Long?,
attachments: List<VkAttachment>?
): Flow<State<MessagesSendResponse>> = flowNewState {
repository.send(
peerId = peerId,
randomId = randomId,
message = message,
forward = forward,
attachments = attachments,
formatData = formatData
replyTo = replyTo,
attachments = attachments
).mapToState()
}
@@ -6,8 +6,7 @@ import dev.meloda.fast.domain.AccountUseCaseImpl
import dev.meloda.fast.domain.GetCurrentAccountUseCase
import dev.meloda.fast.domain.GetLocalUserByIdUseCase
import dev.meloda.fast.domain.GetLocalUsersByIdsUseCase
import dev.meloda.fast.domain.GetMessageReadPeersUseCase
import dev.meloda.fast.domain.LoadConvosByIdUseCase
import dev.meloda.fast.domain.LoadConversationsByIdUseCase
import dev.meloda.fast.domain.LoadUserByIdUseCase
import dev.meloda.fast.domain.LoadUsersByIdsUseCase
import dev.meloda.fast.domain.StoreUsersUseCase
@@ -27,7 +26,5 @@ val domainModule = module {
singleOf(::AccountUseCaseImpl) bind AccountUseCase::class
singleOf(::GetCurrentAccountUseCase)
singleOf(::LoadConvosByIdUseCase)
singleOf(::GetMessageReadPeersUseCase)
singleOf(::LoadConversationsByIdUseCase)
}
@@ -1,49 +0,0 @@
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
)
@@ -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.vk.UiFriend
import dev.meloda.fast.ui.model.api.UiFriend
fun VkUser.asPresentation(
useContactNames: Boolean = false
@@ -1,65 +0,0 @@
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)
)
}
@@ -1,177 +0,0 @@
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
+2 -2
View File
@@ -4,7 +4,7 @@ plugins {
}
android {
namespace = "dev.meloda.fast.model"
namespace = "dev.meloda.fast.datastore"
}
dependencies {
@@ -12,7 +12,7 @@ dependencies {
ksp(libs.moshi.kotlin.codegen)
implementation(platform(libs.compose.bom))
implementation(libs.compose.ui)
implementation(libs.bundles.compose)
implementation(libs.room.ktx)
implementation(libs.room.runtime)
@@ -1,6 +1,6 @@
package dev.meloda.fast.model
enum class ConvoFlags(val value: Int) {
enum class ConversationFlags(val value: Int) {
DISABLE_PUSH(16),
DISABLE_SOUND(32),
INCOMING_CHAT_REQUEST(256),
@@ -17,10 +17,10 @@ enum class ConvoFlags(val value: Int) {
companion object {
fun parse(mask: Int): List<ConvoFlags> {
val flags = mutableListOf<ConvoFlags>()
fun parse(mask: Int): List<ConversationFlags> {
val flags = mutableListOf<ConversationFlags>()
ConvoFlags.entries.forEach { flag ->
ConversationFlags.entries.forEach { flag ->
if (mask and flag.value > 0) {
flags.add(flag)
}
@@ -1,5 +1,5 @@
package dev.meloda.fast.model
enum class ConvosFilter {
enum class ConversationsFilter {
ALL, UNREAD, ARCHIVE, BUSINESS_NOTIFY
}
@@ -1,6 +1,6 @@
package dev.meloda.fast.model
import dev.meloda.fast.model.api.domain.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage
sealed interface LongPollParsedEvent {
@@ -92,7 +92,7 @@ sealed interface LongPollParsedEvent {
) : LongPollParsedEvent
data class ChatArchived(
val convo: VkConvo,
val conversation: VkConversation,
val archived: Boolean
) : LongPollParsedEvent
}
@@ -1,8 +0,0 @@
package dev.meloda.fast.model
data class PhotoSize(
val height: Int,
val width: Int,
val type: String,
val url: String
)
@@ -1,7 +1,5 @@
package dev.meloda.fast.model.api
import dev.meloda.fast.model.api.domain.VkMessage
enum class PeerType(val value: String) {
USER("user"),
GROUP("group"),
@@ -15,14 +13,5 @@ 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")
}
}
}
}
@@ -6,8 +6,8 @@ enum class AttachmentType(var value: String) {
UNKNOWN("unknown"),
PHOTO("photo"),
VIDEO("video"),
FILE("doc"),
AUDIO("audio"),
FILE("doc"),
LINK("link"),
AUDIO_MESSAGE("audio_message"),
MINI_APP("mini_app"),
@@ -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 cmId: Long,
@Json(name = "cmid") val conversationMessageId: 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,
cmId = cmId,
conversationMessageId = conversationMessageId,
date = date,
fromId = fromId,
position = position,
@@ -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.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
import dev.meloda.fast.model.api.domain.VkMessage
@JsonClass(generateAdapter = true)
data class VkConvoData(
data class VkConversationData(
@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 inReadCmId: Long,
@Json(name = "out_read_cmid") val outReadCmId: Long,
@Json(name = "in_read_cmid") val inReadConversationMessageId: Long,
@Json(name = "out_read_cmid") val outReadConversationMessageId: Long,
@Json(name = "sort_id") val sortId: SortId,
@Json(name = "last_conversation_message_id") val lastCmId: Long,
@Json(name = "last_conversation_message_id") val lastConversationMessageId: 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 VkConvoData(
fun asDomain(
lastMessage: VkMessage? = null,
): VkConvo = VkConvo(
): VkConversation = VkConversation(
id = peer.id,
localId = peer.localId,
title = chatSettings?.title,
@@ -120,7 +120,7 @@ data class VkConvoData(
photo200 = chatSettings?.photo?.photo200,
isCallInProgress = callInProgress != null,
isPhantom = chatSettings?.isDisappearing == true,
lastCmId = lastCmId,
lastCmId = lastConversationMessageId,
inRead = inRead,
outRead = outRead,
lastMessageId = lastMessageId,
@@ -132,8 +132,8 @@ data class VkConvoData(
canChangePin = chatSettings?.acl?.canChangePin == true,
canChangeInfo = chatSettings?.acl?.canChangeInfo == true,
pinnedMessageId = chatSettings?.pinnedMessage?.id,
inReadCmId = inReadCmId,
outReadCmId = outReadCmId,
inReadCmId = inReadConversationMessageId,
outReadCmId = outReadConversationMessageId,
interactionType = -1,
interactionIds = emptyList(),
peerType = PeerType.parse(peer.type),
@@ -27,9 +27,7 @@ data class VkFileData(
) {
@JsonClass(generateAdapter = true)
data class Photo(
val sizes: List<Size>
) {
data class Photo(val sizes: List<Size>) {
@JsonClass(generateAdapter = true)
data class Size(
@@ -1,13 +1,12 @@
package dev.meloda.fast.model.api.data
import dev.meloda.fast.model.api.domain.VkGiftDomain
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.api.domain.VkGiftDomain
@JsonClass(generateAdapter = true)
data class VkGiftData(
@Json(name = "id") val id: Long,
@Json(name = "thumb_512") val thumb512: String?,
@Json(name = "thumb_256") val thumb256: String?,
@Json(name = "thumb_96") val thumb96: String?,
@Json(name = "thumb_48") val thumb48: String
@@ -15,7 +14,6 @@ data class VkGiftData(
fun toDomain() = VkGiftDomain(
id = id,
thumb512 = thumb512,
thumb256 = thumb256,
thumb96 = thumb96,
thumb48 = thumb48
@@ -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 cmId: Long?,
@Json(name = "conversation_message_id") val conversationMessageId: Long?,
@Json(name = "message") val message: String?
)
@@ -102,10 +102,10 @@ fun VkMessageData.asDomain(): VkMessage = VkMessage(
action = VkMessage.Action.parse(action?.type),
actionMemberId = action?.memberId,
actionText = action?.text,
actionCmId = action?.cmId,
actionConversationMessageId = action?.conversationMessageId,
actionMessage = action?.message,
geoType = geo?.type,
isImportant = important == true,
isImportant = important ?: false,
updateTime = updateTime,
forwards = fwdMessages.orEmpty().map(VkMessageData::asDomain),
attachments = attachments.map(VkAttachmentItemData::toDomain),
@@ -2,7 +2,6 @@ package dev.meloda.fast.model.api.data
import com.squareup.moshi.Json
import com.squareup.moshi.JsonClass
import dev.meloda.fast.model.PhotoSize
import dev.meloda.fast.model.api.domain.VkPhotoDomain
@JsonClass(generateAdapter = true)
@@ -36,14 +35,7 @@ data class VkPhotoData(
ownerId = ownerId,
hasTags = hasTags == true,
accessKey = accessKey,
sizes = sizes.map { size ->
PhotoSize(
height = size.height,
width = size.width,
type = size.type,
url = size.url
)
},
sizes = sizes,
text = text,
userId = userId
)
@@ -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 cmId: Long,
@Json(name = "conversation_message_id") val conversationMessageId: 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 = cmId,
cmId = conversationMessageId,
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,
actionCmId = action?.cmId,
actionConversationMessageId = action?.conversationMessageId,
actionMessage = action?.message,
geoType = geo?.type,
isImportant = important,
@@ -37,7 +37,7 @@ data class VkUserData(
@JsonClass(generateAdapter = true)
data class LastSeen(
@Json(name = "platform") val platform: Int?,
@Json(name = "platform") val platform: Int,
@Json(name = "time") val time: Int
)
@@ -23,7 +23,7 @@ data class VkVideoData(
@Json(name = "is_favorite") val isFavorite: Boolean?,
@Json(name = "image") val image: List<Image>?,
@Json(name = "first_frame") val firstFrame: List<FirstFrame>?,
@Json(name = "files") val files: File?,
@Json(name = "files") val files: File?
) : VkAttachmentData {
@JsonClass(generateAdapter = true)
@@ -73,7 +73,6 @@ data class VkVideoData(
accessKey = accessKey,
title = title,
views = views,
duration = duration,
isShortVideo = type == "short_video"
duration = duration
)
}
@@ -54,9 +54,9 @@ data class VkVideoMessageData(
@JsonClass(generateAdapter = true)
data class Image(
val height: Int,
val url: String,
val width: Int,
val height: Int?,
val url: String?,
val width: Int?,
val with_padding: Int?,
)
@@ -73,7 +73,6 @@ data class VkVideoMessageData(
)
fun toDomain(): VkVideoMessageDomain = VkVideoMessageDomain(
id = id,
image = image.orEmpty().filter { it.width / it.height == 1 }.maxByOrNull { it.width }?.url
id = id
)
}
@@ -9,10 +9,10 @@ data class VkWallReplyData(
val from_id: Long,
val date: Int,
val text: String,
val post_id: Long?,
val owner_id: Long?,
val parents_stack: List<Int>?,
val likes: Likes?,
val post_id: Long,
val owner_id: Long,
val parents_stack: List<Int>,
val likes: Likes,
val reply_to_user: Int?,
val reply_to_comment: Int?
) {
@@ -3,10 +3,6 @@ package dev.meloda.fast.model.api.domain
enum class FormatDataType {
BOLD, ITALIC, UNDERLINE, URL;
override fun toString(): String {
return super.toString().lowercase()
}
companion object {
fun parse(value: String): FormatDataType? =
entries.firstOrNull { it.name.lowercase() == value }
@@ -1,9 +1,7 @@
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
}
@@ -2,7 +2,7 @@ package dev.meloda.fast.model.api.domain
data class VkAttachmentHistoryMessage(
val messageId: Long,
val cmId: Long,
val conversationMessageId: Long,
val date: Int,
val fromId: Long,
val position: Int,
@@ -1,9 +1,9 @@
package dev.meloda.fast.model.api.domain
import dev.meloda.fast.model.api.PeerType
import dev.meloda.fast.model.database.VkConvoEntity
import dev.meloda.fast.model.database.VkConversationEntity
data class VkConvo(
data class VkConversation(
val id: Long,
val localId: Long,
val ownerId: Long?,
@@ -54,7 +54,7 @@ data class VkConvo(
}
companion object {
val EMPTY: VkConvo = VkConvo(
val EMPTY: VkConversation = VkConversation(
id = -1,
localId = -1,
ownerId = null,
@@ -90,7 +90,7 @@ data class VkConvo(
}
}
fun VkConvo.asEntity(): VkConvoEntity = VkConvoEntity(
fun VkConversation.asEntity(): VkConversationEntity = VkConversationEntity(
id = id,
localId = localId,
ownerId = ownerId,
@@ -99,7 +99,7 @@ fun VkConvo.asEntity(): VkConvoEntity = VkConvoEntity(
photo100 = photo100,
photo200 = photo200,
isPhantom = isPhantom,
lastCmId = lastCmId,
lastConversationMessageId = lastCmId,
inReadCmId = inReadCmId,
outReadCmId = outReadCmId,
inRead = inRead,
@@ -4,14 +4,10 @@ import dev.meloda.fast.model.api.data.AttachmentType
data class VkGiftDomain(
val id: Long,
val thumb512: String?,
val thumb256: String?,
val thumb96: String?,
val thumb48: String
) : VkAttachment {
override val type: AttachmentType = AttachmentType.GIFT
fun getMaxSizeThumb(): String = thumb512 ?: thumb256 ?: thumb96 ?: thumb48
fun getDefaultThumbSizeOrLess(): String = thumb256 ?: thumb96 ?: thumb48
}
@@ -16,7 +16,7 @@ data class VkMessage(
val action: Action?,
val actionMemberId: Long?,
val actionText: String?,
val actionCmId: Long?,
val actionConversationMessageId: Long?,
val actionMessage: String?,
val updateTime: Int?,
@@ -35,7 +35,7 @@ data class VkMessage(
val user: VkUser?,
val group: VkGroupDomain?,
val actionUser: VkUser?,
val actionGroup: VkGroupDomain?,
val actionGroup: VkGroupDomain?
) {
fun isPeerChat() = peerId > 2_000_000_000
@@ -44,9 +44,9 @@ data class VkMessage(
fun isGroup() = fromId < 0
fun isRead(convo: VkConvo): Boolean = when {
fun isRead(conversation: VkConversation): Boolean = when {
id <= 0 -> false
else -> convo.isRead(this)
else -> conversation.isRead(this)
}
fun hasAttachments(): Boolean = attachments.orEmpty().isNotEmpty()
@@ -98,7 +98,7 @@ data class VkMessage(
fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
id = id,
cmId = cmId,
conversationMessageId = cmId,
text = text,
isOut = isOut,
peerId = peerId,
@@ -108,7 +108,7 @@ fun VkMessage.asEntity(): VkMessageEntity = VkMessageEntity(
action = action?.value,
actionMemberId = actionMemberId,
actionText = actionText,
actionCmId = actionCmId,
actionConversationMessageId = actionConversationMessageId,
actionMessage = actionMessage,
updateTime = updateTime,
important = isImportant,
@@ -1,7 +1,7 @@
package dev.meloda.fast.model.api.domain
import dev.meloda.fast.model.PhotoSize
import dev.meloda.fast.model.api.data.AttachmentType
import dev.meloda.fast.model.api.data.VkPhotoData
import java.util.Stack
@@ -13,7 +13,7 @@ data class VkPhotoDomain(
val ownerId: Long,
val hasTags: Boolean,
val accessKey: String?,
val sizes: List<PhotoSize>,
val sizes: List<VkPhotoData.Size>,
val text: String?,
val userId: Long?
) : VkAttachment {
@@ -35,15 +35,11 @@ data class VkPhotoDomain(
sizesChars.push(SIZE_TYPE_2560_2048)
}
fun getMaxSize(): PhotoSize? {
fun getMaxSize(): VkPhotoData.Size? {
return getSizeOrSmaller(sizesChars.peek())
}
fun getDefault(): PhotoSize? {
return getSizeOrSmaller(SIZE_TYPE_1080_1024)
}
fun getSizeOrNull(type: Char): PhotoSize? {
fun getSizeOrNull(type: Char): VkPhotoData.Size? {
for (size in sizes) {
if (size.type == type.toString()) return size
}
@@ -51,7 +47,7 @@ data class VkPhotoDomain(
return null
}
fun getSizeOrSmaller(type: Char): PhotoSize? {
fun getSizeOrSmaller(type: Char): VkPhotoData.Size? {
val photoStack = sizesChars.clone() as Stack<*>
val sizeIndex = photoStack.search(type)
@@ -21,12 +21,4 @@ data class VkStickerDomain(
return null
}
fun getUrl(width: Int = 256, withBackground: Boolean = false): String? = when {
withBackground && backgroundImages != null -> {
backgroundImages.firstOrNull { it.width >= width }?.url
}
images != null -> images.firstOrNull { it.width >= width }?.url
else -> "https://vk.ru/sticker/1-${id}-${width}b"
}
}
@@ -13,8 +13,7 @@ data class VkVideoDomain(
val accessKey: String?,
val title: String,
val views: Int,
val duration: Int,
val isShortVideo: Boolean
val duration: Int
) : VkAttachment {
override val type: AttachmentType = AttachmentType.VIDEO
@@ -23,10 +22,6 @@ data class VkVideoDomain(
return images.find { it.width == width }
}
fun getDefault(): VideoImage? {
return imageForWidthAtLeast(720)
}
fun imageForWidthAtLeast(width: Int): VideoImage? {
var certainImages = images.sortedByDescending { it.width }
var containsVertical = false
@@ -41,11 +36,9 @@ data class VkVideoDomain(
certainImages = certainImages.filter { it.shapeKind == ShapeKind.Vertical }
}
val filteredCertainImages = certainImages.filter { it.width >= width }
certainImages = certainImages.filter { it.width >= width }
return filteredCertainImages
.ifEmpty { certainImages }
.firstOrNull()
return certainImages.firstOrNull()
}
@JsonClass(generateAdapter = true)
@@ -3,8 +3,7 @@ package dev.meloda.fast.model.api.domain
import dev.meloda.fast.model.api.data.AttachmentType
data class VkVideoMessageDomain(
val id: Long,
val image: String?
val id: Long
) : VkAttachment {
override val type: AttachmentType = AttachmentType.VIDEO_MESSAGE
@@ -1,12 +1,12 @@
package dev.meloda.fast.model.api.requests
import dev.meloda.fast.model.ConvosFilter
import dev.meloda.fast.model.ConversationsFilter
data class ConvosGetRequest(
data class ConversationsGetRequest(
val count: Int? = null,
val offset: Int? = null,
val fields: String = "",
val filter: ConvosFilter = ConvosFilter.ALL,
val filter: ConversationsFilter = ConversationsFilter.ALL,
val extended: Boolean? = true,
val startMessageId: Long? = null
) {
@@ -2,7 +2,6 @@ package dev.meloda.fast.model.api.requests
import dev.meloda.fast.model.api.asInt
import dev.meloda.fast.model.api.domain.VkAttachment
import dev.meloda.fast.model.api.domain.VkMessage
data class MessagesGetHistoryRequest(
val count: Int? = null,
@@ -34,13 +33,12 @@ data class MessagesSendRequest(
val message: String?,
val lat: Int? = null,
val lon: Int? = null,
val forward: String? = null,
val replyTo: Long? = null,
val stickerId: Long? = null,
val disableMentions: Boolean? = null,
val doNotParseLinks: Boolean? = null,
val silent: Boolean? = null,
val attachments: List<VkAttachment>? = null,
val formatData: VkMessage.FormatData? = null
val attachments: List<VkAttachment>? = null
) {
val map: Map<String, String>
@@ -51,18 +49,11 @@ data class MessagesSendRequest(
message?.let { this["message"] = it }
lat?.let { this["lat"] = it.toString() }
lon?.let { this["lon"] = it.toString() }
forward?.let { this["forward"] = it }
replyTo?.let { this["reply_to"] = it.toString() }
stickerId?.let { this["sticker_id"] = it.toString() }
disableMentions?.let { this["disable_mentions"] = it.asInt().toString() }
doNotParseLinks?.let { this["dont_parse_links"] = it.asInt().toString() }
silent?.let { this["silent"] = it.toString() }
formatData?.let {
this["format_data"] = "{\"version\":\"${formatData.version}\",\"items\":[" +
formatData.items.joinToString(separator = ", ") { item ->
"{\"type\":\"${item.type}\",\"offset\":${item.offset},\"length\":${item.length}}"
} +
"]}"
}
// TODO: 05/05/2024, Danil Nikolaev: add attachments
// attachments?.let {
@@ -115,7 +106,7 @@ data class MessagesGetLongPollServerRequest(
data class MessagesPinMessageRequest(
val peerId: Long,
val messageId: Long? = null,
val cmId: Long? = null
val conversationMessageId: Long? = null
) {
val map: Map<String, String>
@@ -123,7 +114,7 @@ data class MessagesPinMessageRequest(
"peer_id" to peerId.toString()
).apply {
messageId?.let { this["message_id"] = it.toString() }
cmId?.let { this["conversation_message_id"] = it.toString() }
conversationMessageId?.let { this["conversation_message_id"] = it.toString() }
}
}
@@ -136,7 +127,7 @@ data class MessagesUnpinMessageRequest(val peerId: Long) {
data class MessagesDeleteRequest(
val peerId: Long,
val messagesIds: List<Long>? = null,
val cmIds: List<Long>? = null,
val conversationsMessagesIds: List<Long>? = null,
val isSpam: Boolean? = null,
val deleteForAll: Boolean? = null
) {
@@ -149,7 +140,7 @@ data class MessagesDeleteRequest(
deleteForAll?.let { this["delete_for_all"] = it.asInt().toString() }
messagesIds?.let { this["message_ids"] = it.joinToString() }
cmIds?.let {
conversationsMessagesIds?.let {
this["conversation_message_ids"] = it.joinToString()
}
}
@@ -228,7 +219,7 @@ data class MessagesGetChatRequest(
}
data class MessagesGetConvoMembersRequest(
data class MessagesGetConversationMembersRequest(
val peerId: Long,
val offset: Int? = null,
val count: Int? = null,
@@ -267,14 +258,14 @@ data class MessagesGetHistoryAttachmentsRequest(
val offset: Int?,
val preserveOrder: Boolean?,
val attachmentTypes: List<String>,
val cmId: Long,
val conversationMessageId: Long,
val fields: String?
) {
val map = mutableMapOf(
"peer_id" to peerId.toString(),
"attachment_types" to attachmentTypes.joinToString(","),
"cmid" to cmId.toString()
"cmid" to conversationMessageId.toString()
).apply {
extended?.let { this["extended"] = it.toString() }
count?.let { this["count"] = it.toString() }
@@ -35,7 +35,7 @@ data class AuthDirectRequest(
}
data class AuthWithAppRequest(
val redirectUrl: String = "https://oauth.vk.ru/blank.html",
val redirectUrl: String = "https://oauth.vk.com/blank.html",
val display: String = "page",
val responseType: String = "token",
val accessToken: String,
@@ -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.VkConvoData
import dev.meloda.fast.model.api.data.VkConversationData
import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.data.VkUserData
@JsonClass(generateAdapter = true)
data class ConvosGetResponse(
data class ConversationsGetResponse(
@Json(name = "count") val count: Int,
@Json(name = "items") val items: List<ConvosResponseItem>,
@Json(name = "items") val items: List<ConversationsResponseItem>,
@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 ConvosGetResponse(
)
@JsonClass(generateAdapter = true)
data class ConvosGetByIdResponse(
data class ConversationsGetByIdResponse(
@Json(name = "count") val count: Int,
@Json(name = "items") val items: List<VkConvoData>,
@Json(name = "items") val items: List<VkConversationData>,
@Json(name = "profiles") val profiles: List<VkUserData>?,
@Json(name = "groups") val groups: List<VkGroupData>?,
@Json(name = "contacts") val contacts: List<VkContactData>?
)
@JsonClass(generateAdapter = true)
data class ConvosResponseItem(
@Json(name = "conversation") val convo: VkConvoData,
data class ConversationsResponseItem(
@Json(name = "conversation") val conversation: VkConversationData,
@Json(name = "last_message") val lastMessage: VkMessageData?
)
@JsonClass(generateAdapter = true)
data class ConvosDeleteResponse(
data class ConversationsDeleteResponse(
@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.VkConvoData
import dev.meloda.fast.model.api.data.VkConversationData
import dev.meloda.fast.model.api.data.VkGroupData
import dev.meloda.fast.model.api.data.VkMessageData
import dev.meloda.fast.model.api.data.VkUserData
@@ -14,7 +14,7 @@ import dev.meloda.fast.model.api.data.VkUserData
data class MessagesGetHistoryResponse(
val count: Int,
val items: List<VkMessageData>,
val convos: List<VkConvoData>?,
val conversations: List<VkConversationData>?,
val profiles: List<VkUserData>?,
val groups: List<VkGroupData>?,
val contacts: List<VkContactData>?
@@ -30,7 +30,7 @@ data class MessagesGetByIdResponse(
)
@JsonClass(generateAdapter = true)
data class MessagesGetConvoMembersResponse(
data class MessagesGetConversationMembersResponse(
val count: Int,
val items: List<VkChatMemberData>?,
val profiles: List<VkUserData>?,
@@ -58,22 +58,3 @@ data class MessagesSendResponse(
@Json(name = "message_id") val messageId: Long,
@Json(name = "cmid") val cmId: Long
)
@JsonClass(generateAdapter = true)
data class MessagesMarkAsImportantResponse(
@Json(name = "marked") val marked: List<Mark>
) {
@JsonClass(generateAdapter = true)
data class Mark(
@Json(name = "cmid") val cmId: Long,
@Json(name = "message_id") val messageId: Long,
@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>?,
)
@@ -3,8 +3,8 @@ package dev.meloda.fast.model.database
import androidx.room.Embedded
import androidx.room.Relation
data class ConvoWithMessage(
@Embedded val convo: VkConvoEntity,
data class ConversationWithMessage(
@Embedded val conversation: VkConversationEntity,
@Relation(
parentColumn = "lastMessageId",
entityColumn = "id"
@@ -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.VkConvo
import dev.meloda.fast.model.api.domain.VkConversation
@Entity(tableName = "convos")
data class VkConvoEntity(
@Entity(tableName = "conversations")
data class VkConversationEntity(
@PrimaryKey val id: Long,
val localId: Long,
val ownerId: Long?,
@@ -15,7 +15,7 @@ data class VkConvoEntity(
val photo100: String?,
val photo200: String?,
val isPhantom: Boolean,
val lastCmId: Long,
val lastConversationMessageId: Long,
val inReadCmId: Long,
val outReadCmId: Long,
val inRead: Long,
@@ -32,7 +32,7 @@ data class VkConvoEntity(
val isArchived: Boolean
)
fun VkConvoEntity.asExternalModel(): VkConvo = VkConvo(
fun VkConversationEntity.asExternalModel(): VkConversation = VkConversation(
id = id,
localId = localId,
ownerId = ownerId,
@@ -42,7 +42,7 @@ fun VkConvoEntity.asExternalModel(): VkConvo = VkConvo(
photo200 = photo200,
isCallInProgress = false,
isPhantom = isPhantom,
lastCmId = lastCmId,
lastCmId = lastConversationMessageId,
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 cmId: Long,
val conversationMessageId: 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 actionCmId: Long?,
val actionConversationMessageId: 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 = cmId,
cmId = conversationMessageId,
text = text,
isOut = isOut,
peerId = peerId,
@@ -42,7 +42,7 @@ fun VkMessageEntity.asExternalModel(): VkMessage = VkMessage(
action = VkMessage.Action.parse(action),
actionMemberId = actionMemberId,
actionText = actionText,
actionCmId = actionCmId,
actionConversationMessageId = actionConversationMessageId,
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.convos.ConvosService
import dev.meloda.fast.network.service.conversations.ConversationsService
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(ConvosService::class.java) }
single { service(ConversationsService::class.java) }
single { service(FilesService::class.java) }
single { service(LongPollService::class.java) }
single { service(MessagesService::class.java) }
@@ -1,61 +1,61 @@
package dev.meloda.fast.network.service.convos
package dev.meloda.fast.network.service.conversations
import com.slack.eithernet.ApiResult
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.model.api.responses.ConversationsDeleteResponse
import dev.meloda.fast.model.api.responses.ConversationsGetByIdResponse
import dev.meloda.fast.model.api.responses.ConversationsGetResponse
import dev.meloda.fast.network.ApiResponse
import dev.meloda.fast.network.RestApiError
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.POST
interface ConvosService {
interface ConversationsService {
@FormUrlEncoded
@POST(ConvosUrls.GET)
suspend fun getConvos(
@POST(ConversationsUrls.GET)
suspend fun getConversations(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<ConvosGetResponse>, RestApiError>
): ApiResult<ApiResponse<ConversationsGetResponse>, RestApiError>
@FormUrlEncoded
@POST(ConvosUrls.GET_BY_ID)
suspend fun getConvosById(
@POST(ConversationsUrls.GET_BY_ID)
suspend fun getConversationsById(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<ConvosGetByIdResponse>, RestApiError>
): ApiResult<ApiResponse<ConversationsGetByIdResponse>, RestApiError>
@FormUrlEncoded
@POST(ConvosUrls.DELETE)
@POST(ConversationsUrls.DELETE)
suspend fun delete(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<ConvosDeleteResponse>, RestApiError>
): ApiResult<ApiResponse<ConversationsDeleteResponse>, RestApiError>
@FormUrlEncoded
@POST(ConvosUrls.PIN)
@POST(ConversationsUrls.PIN)
suspend fun pin(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConvosUrls.UNPIN)
@POST(ConversationsUrls.UNPIN)
suspend fun unpin(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConvosUrls.REORDER_PINNED)
@POST(ConversationsUrls.REORDER_PINNED)
suspend fun reorderPinned(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConvosUrls.ARCHIVE)
@POST(ConversationsUrls.ARCHIVE)
suspend fun archive(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>
@FormUrlEncoded
@POST(ConvosUrls.UNARCHIVE)
@POST(ConversationsUrls.UNARCHIVE)
suspend fun unarchive(
@FieldMap params: Map<String, String>
): ApiResult<ApiResponse<Int>, RestApiError>

Some files were not shown because too many files have changed in this diff Show More